using System; using System.Collections; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using UnityEngine; namespace Unity.Pipeline.Tests.Runtime { /// /// Unified client for making HTTP requests to Pipeline server in tests. /// Provides consistent JSON payload structure and response parsing. /// public class PipelineClient : IDisposable { private readonly string m_BaseUrl; private readonly string m_AuthToken; private HttpClient m_HttpClient; private bool m_Disposed = false; /// /// Create a Pipeline client for the specified server. /// /// Base URL of the Pipeline server (e.g., "http://localhost:7900") /// Optional authentication token public PipelineClient(string baseUrl, string authToken = null) { m_BaseUrl = baseUrl.TrimEnd('/'); m_AuthToken = authToken; } /// /// Create a client for a running server, deriving the URL (port) and auth token from it. /// The server must be started so its token is available. /// public PipelineClient(BasePipelineServer server) : this($"http://localhost:{server.Port}", server.Token) { } /// /// Create a client for a runtime server owned by a . /// The manager must have started its server. /// public PipelineClient(RuntimePipelineManager manager) : this(manager.Server) { } /// /// Initialize HttpClient for async operations (lazy initialization). /// private HttpClient GetHttpClient() { if (m_HttpClient == null) { m_HttpClient = new HttpClient(); if (!string.IsNullOrEmpty(m_AuthToken)) { m_HttpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", m_AuthToken); } } return m_HttpClient; } // Coroutine forms for [UnityTest]. They drive the async request by polling the Task each // frame (yield return null) rather than yielding a UnityWebRequest AsyncOperation: the // EditMode test runner does not await an AsyncOperation yielded from a nested enumerator, // and frame-pumping also keeps the editor/player update loop (and the server's main-thread // dispatcher) ticking so main-thread commands can complete. /// GET an endpoint (coroutine form for [UnityTest]). public IEnumerator GetAsync(string endpoint, System.Action onComplete) => Await(GetAsync(endpoint), onComplete); /// Get server status via /api/status endpoint (coroutine form). public IEnumerator GetStatusAsync(System.Action onComplete) => GetAsync("/api/status", onComplete); /// Execute a command via /api/exec endpoint (coroutine form). public IEnumerator ExecuteCommandAsync(string command, object parameters, System.Action onComplete) => Await(ExecuteCommandAsync(command, parameters), onComplete); /// Execute a code evaluation command (coroutine form). public IEnumerator ExecuteCodeAsync(string code, int timeout, System.Action onComplete) => Await(ExecuteCodeAsync(code, timeout), onComplete); /// Post JSON to an endpoint (coroutine form). public IEnumerator PostJsonAsync(string endpoint, object payload, System.Action onComplete) => Await(PostJsonAsync(endpoint, payload), onComplete); private static IEnumerator Await(Task task, System.Action onComplete) { while (!task.IsCompleted) yield return null; onComplete?.Invoke(task.Result); } // ============================================================================ // ASYNC METHODS (for Editor tests using async/await) // ============================================================================ /// /// Get server status via /api/status endpoint (async version). /// public Task GetStatusAsync() => GetAsync("/api/status"); /// /// GET an endpoint (async version), returning a parsed . /// public async Task GetAsync(string endpoint) { var url = $"{m_BaseUrl}{endpoint}"; try { var httpClient = GetHttpClient(); var httpResponse = await httpClient.GetAsync(url); var content = await httpResponse.Content.ReadAsStringAsync(); return CreateResponseFromHttp(httpResponse, content); } catch (Exception ex) { return new PipelineResponse { IsSuccess = false, Error = ex.Message, RawResponse = "" }; } } /// /// Execute a command via /api/exec endpoint (async version). /// public async Task ExecuteCommandAsync(string command, object parameters) { var payload = new { command = command, parameters = parameters ?? new { } }; return await PostJsonAsync("/api/exec", payload); } /// /// Execute a code evaluation command (async version). /// public async Task ExecuteCodeAsync(string code, int timeout) { var parameters = new { code = code, timeout = timeout }; return await ExecuteCommandAsync("eval", parameters); } /// /// Post JSON data to an endpoint (async version). /// public async Task PostJsonAsync(string endpoint, object payload) { var url = $"{m_BaseUrl}{endpoint}"; var jsonData = JsonConvert.SerializeObject(payload, Formatting.Indented); var content = new StringContent(jsonData, Encoding.UTF8, "application/json"); try { var httpClient = GetHttpClient(); var httpResponse = await httpClient.PostAsync(url, content); var responseContent = await httpResponse.Content.ReadAsStringAsync(); return CreateResponseFromHttp(httpResponse, responseContent); } catch (Exception ex) { return new PipelineResponse { IsSuccess = false, Error = ex.Message, RawResponse = "" }; } } /// /// Get raw HTTP response (async version). /// public async Task GetHttpAsync(string endpoint) { var url = $"{m_BaseUrl}{endpoint}"; var httpClient = GetHttpClient(); return await httpClient.GetAsync(url); } // ============================================================================ // SHARED HELPER METHODS // ============================================================================ /// /// Create a PipelineResponse from HttpResponseMessage. /// private PipelineResponse CreateResponseFromHttp(HttpResponseMessage httpResponse, string content) { var response = new PipelineResponse { IsSuccess = httpResponse.IsSuccessStatusCode, StatusCode = (int)httpResponse.StatusCode, Error = httpResponse.IsSuccessStatusCode ? null : httpResponse.ReasonPhrase, RawResponse = content ?? "", HttpResponse = httpResponse }; // Try to parse JSON response if (!string.IsNullOrEmpty(response.RawResponse)) { try { response.JsonResponse = JObject.Parse(response.RawResponse); } catch (JsonException ex) { response.ParseError = ex.Message; } } return response; } /// /// Dispose of any resources. /// public void Dispose() { if (!m_Disposed) { m_HttpClient?.Dispose(); m_Disposed = true; } } } /// /// Standardized response from Pipeline server. /// public class PipelineResponse { /// /// Whether the HTTP request succeeded. /// public bool IsSuccess { get; set; } /// /// HTTP status code. /// public int StatusCode { get; set; } /// /// HTTP error message (if any). /// public string Error { get; set; } /// /// Raw response text. /// public string RawResponse { get; set; } /// /// Parsed JSON response (null if parsing failed). /// public JObject JsonResponse { get; set; } /// /// JSON parsing error (if any). /// public string ParseError { get; set; } /// /// Original HttpResponseMessage (for advanced scenarios with async methods). /// public HttpResponseMessage HttpResponse { get; set; } /// /// Whether the response has valid JSON. /// public bool HasValidJson => JsonResponse != null && string.IsNullOrEmpty(ParseError); /// /// Get a typed response object from the JSON. /// public T GetTypedResponse() where T : class { if (!HasValidJson || JsonResponse == null || !JsonResponse.ContainsKey("result")) return null; try { return JsonResponse["result"].ToObject(); } catch (JsonException) { return null; } } /// /// Get a JSON property value. /// public T GetProperty(string propertyName, T defaultValue = default(T)) { if (!HasValidJson) return defaultValue; var token = JsonResponse[propertyName]; if (token == null) return defaultValue; try { return token.ToObject(); } catch { return defaultValue; } } /// /// Check if the command execution was successful (different from HTTP success). /// public bool IsCommandSuccess => GetProperty("success", false); /// /// Get command error message. /// public string CommandError => GetProperty("error"); /// /// Get command error details. /// public string CommandErrorDetails => GetProperty("errorDetails"); public override string ToString() { var status = IsSuccess ? "Success" : "Failed"; var content = HasValidJson ? "JSON" : "Raw"; return $"{status} ({StatusCode}) - {content}: {RawResponse?.Substring(0, Math.Min(100, RawResponse.Length))}..."; } } /// /// Extension methods for common Pipeline operations. /// public static class PipelineClientExtensions { /// /// Execute code evaluation and get strongly-typed response (coroutine version). /// public static IEnumerator ExecuteCodeWithResponseAsync(this PipelineClient client, string code, int timeout, System.Action onComplete) { yield return client.ExecuteCodeAsync(code, timeout, response => { if (response.HasValidJson) { var evalResponse = response.GetTypedResponse(); onComplete?.Invoke(evalResponse); } else { // Create error response var errorResponse = new Unity.Pipeline.Models.EvalResponse { Success = false, Error = response.Error ?? "HTTP Error", ErrorDetails = response.RawResponse ?? "No response data" }; onComplete?.Invoke(errorResponse); } }); } /// /// Execute code evaluation and get strongly-typed response (async version). /// public static async Task ExecuteCodeWithResponseAsync(this PipelineClient client, string code, int timeout = 5000) { var response = await client.ExecuteCodeAsync(code, timeout); if (response.HasValidJson) { var evalResponse = response.GetTypedResponse(); if (evalResponse != null) return evalResponse; } // Create error response if parsing failed return new Unity.Pipeline.Models.EvalResponse { Success = false, Error = response.Error ?? "HTTP Error", ErrorDetails = response.RawResponse ?? "No response data" }; } } }