237 lines
8.6 KiB
C#
237 lines
8.6 KiB
C#
using System.Net;
|
|
using System.Text;
|
|
using System.Text.Json.Nodes;
|
|
using w4c_workflows.Models.Credentials;
|
|
using w4c_workflows.Models.Nodes;
|
|
using w4c_workflows.Services.Credentials;
|
|
using w4c_workflows.Services.Nodes;
|
|
using w4c_workflows.Services.Nodes.Executors;
|
|
using w4c_workflows.Services.Security;
|
|
using Xunit;
|
|
|
|
namespace w4c_workflows.Tests;
|
|
|
|
/// <summary>
|
|
/// Unit tests for the HTTP Request node, driven through a stub
|
|
/// <see cref="HttpMessageHandler"/> so no real network is touched.
|
|
/// </summary>
|
|
public class HttpRequestNodeExecutorTests
|
|
{
|
|
private static readonly NodeBlueprintCatalog Catalog = NodeTestData.CoreCatalog();
|
|
|
|
private sealed class StubHandler : HttpMessageHandler
|
|
{
|
|
private readonly Func<HttpRequestMessage, HttpResponseMessage> _responder;
|
|
|
|
public StubHandler(Func<HttpRequestMessage, HttpResponseMessage> responder) => _responder = responder;
|
|
|
|
public HttpRequestMessage? LastRequest { get; private set; }
|
|
public string? LastBody { get; private set; }
|
|
|
|
protected override async Task<HttpResponseMessage> SendAsync(
|
|
HttpRequestMessage request, CancellationToken cancellationToken)
|
|
{
|
|
LastRequest = request;
|
|
if (request.Content != null)
|
|
LastBody = await request.Content.ReadAsStringAsync(cancellationToken);
|
|
return _responder(request);
|
|
}
|
|
}
|
|
|
|
private sealed class StubFactory : IHttpClientFactory
|
|
{
|
|
private readonly HttpMessageHandler _handler;
|
|
public StubFactory(HttpMessageHandler handler) => _handler = handler;
|
|
public HttpClient CreateClient(string name) => new(_handler, disposeHandler: false);
|
|
}
|
|
|
|
private static HttpResponseMessage Json(string body, HttpStatusCode status = HttpStatusCode.OK)
|
|
=> new(status) { Content = new StringContent(body, Encoding.UTF8, "application/json") };
|
|
|
|
private static FlowItem Item(string json) => FlowItem.FromJson(JsonNode.Parse(json)!.AsObject());
|
|
|
|
private static NodeExecutionContext Context(
|
|
JsonObject parameters,
|
|
IReadOnlyList<FlowItem>? input = null,
|
|
IReadOnlyDictionary<string, CredentialData>? credentials = null)
|
|
=> new()
|
|
{
|
|
Blueprint = Catalog.Get("core.httpRequest")!,
|
|
Parameters = parameters,
|
|
Inputs = new IReadOnlyList<FlowItem>[] { input ?? Array.Empty<FlowItem>() },
|
|
Credentials = credentials ?? new Dictionary<string, CredentialData>(),
|
|
};
|
|
|
|
private static HttpRequestNodeExecutor Executor(StubHandler handler)
|
|
=> new(
|
|
new StubFactory(handler),
|
|
new CredentialTypeCatalog(),
|
|
EgressTestData.Guard(),
|
|
EgressTestData.Quota(),
|
|
BinaryTestData.Store());
|
|
|
|
[Fact]
|
|
public async Task Get_json_object_returns_one_item()
|
|
{
|
|
var handler = new StubHandler(_ => Json("""{"a":1}"""));
|
|
|
|
var outcome = await Executor(handler).RunAsync(
|
|
Context(new JsonObject { ["url"] = "https://example.com/x" }), default);
|
|
|
|
Assert.True(outcome.Succeeded, outcome.Failure?.Message);
|
|
Assert.Equal(1, Assert.Single(outcome.Outputs[0]).Json["a"]!.GetValue<int>());
|
|
Assert.Equal(HttpMethod.Get, handler.LastRequest!.Method);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Get_json_array_returns_one_item_per_element()
|
|
{
|
|
var handler = new StubHandler(_ => Json("""[{"n":1},{"n":2}]"""));
|
|
|
|
var outcome = await Executor(handler).RunAsync(
|
|
Context(new JsonObject { ["url"] = "https://example.com/x" }), default);
|
|
|
|
Assert.Equal(2, outcome.Outputs[0].Count);
|
|
Assert.Equal(2, outcome.Outputs[0][1].Json["n"]!.GetValue<int>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Non_success_status_fails_with_the_http_status()
|
|
{
|
|
var handler = new StubHandler(_ => Json("""{"message":"nope"}""", HttpStatusCode.NotFound));
|
|
|
|
var outcome = await Executor(handler).RunAsync(
|
|
Context(new JsonObject { ["url"] = "https://example.com/x" }), default);
|
|
|
|
Assert.False(outcome.Succeeded);
|
|
Assert.Equal("http_error", outcome.Failure!.Code);
|
|
Assert.Equal(404, outcome.Failure.HttpStatus);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Never_error_returns_the_body_instead_of_failing()
|
|
{
|
|
var handler = new StubHandler(_ => Json("""{"message":"nope"}""", HttpStatusCode.BadRequest));
|
|
|
|
var outcome = await Executor(handler).RunAsync(
|
|
Context(new JsonObject
|
|
{
|
|
["url"] = "https://example.com/x",
|
|
["options"] = new JsonObject { ["neverError"] = true },
|
|
}), default);
|
|
|
|
Assert.True(outcome.Succeeded, outcome.Failure?.Message);
|
|
Assert.Equal("nope", Assert.Single(outcome.Outputs[0]).Json["message"]!.GetValue<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Post_sends_headers_and_a_json_body()
|
|
{
|
|
var handler = new StubHandler(_ => Json("""{"ok":true}"""));
|
|
|
|
var outcome = await Executor(handler).RunAsync(
|
|
Context(new JsonObject
|
|
{
|
|
["method"] = "POST",
|
|
["url"] = "https://example.com/x",
|
|
["sendHeaders"] = true,
|
|
["headers"] = new JsonArray(
|
|
new JsonObject { ["name"] = "X-Trace", ["value"] = "abc" }),
|
|
["sendBody"] = true,
|
|
["bodyContentType"] = "json",
|
|
["body"] = new JsonObject { ["name"] = "Ada" },
|
|
}), default);
|
|
|
|
Assert.True(outcome.Succeeded, outcome.Failure?.Message);
|
|
Assert.Equal(HttpMethod.Post, handler.LastRequest!.Method);
|
|
Assert.Equal("abc", handler.LastRequest.Headers.GetValues("X-Trace").Single());
|
|
Assert.Contains("\"name\":\"Ada\"", handler.LastBody);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Invalid_url_fails_before_sending()
|
|
{
|
|
var handler = new StubHandler(_ => Json("{}"));
|
|
|
|
var outcome = await Executor(handler).RunAsync(
|
|
Context(new JsonObject { ["url"] = "not a url" }), default);
|
|
|
|
Assert.False(outcome.Succeeded);
|
|
Assert.Equal("invalid_url", outcome.Failure!.Code);
|
|
Assert.Null(handler.LastRequest);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Text_response_format_wraps_the_body_in_data()
|
|
{
|
|
var handler = new StubHandler(_ => new HttpResponseMessage(HttpStatusCode.OK)
|
|
{
|
|
Content = new StringContent("hello", Encoding.UTF8, "text/plain"),
|
|
});
|
|
|
|
var outcome = await Executor(handler).RunAsync(
|
|
Context(new JsonObject
|
|
{
|
|
["url"] = "https://example.com/x",
|
|
["options"] = new JsonObject { ["responseFormat"] = "text" },
|
|
}), default);
|
|
|
|
Assert.Equal("hello", Assert.Single(outcome.Outputs[0]).Json["data"]!.GetValue<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Put_output_in_field_wraps_the_response()
|
|
{
|
|
var handler = new StubHandler(_ => Json("""{"a":1}"""));
|
|
|
|
var outcome = await Executor(handler).RunAsync(
|
|
Context(new JsonObject
|
|
{
|
|
["url"] = "https://example.com/x",
|
|
["options"] = new JsonObject { ["putOutputInField"] = "response" },
|
|
}), default);
|
|
|
|
var item = Assert.Single(outcome.Outputs[0]);
|
|
Assert.Equal(1, item.Json["response"]!["a"]!.GetValue<int>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Credential_auth_applies_basic_authorization()
|
|
{
|
|
var handler = new StubHandler(_ => Json("{}"));
|
|
var credentials = new Dictionary<string, CredentialData>
|
|
{
|
|
[HttpRequestNodeExecutor.CredentialAlias] = new(
|
|
"httpBasicAuth",
|
|
new JsonObject { ["username"] = "ada", ["password"] = "secret" }),
|
|
};
|
|
|
|
var outcome = await Executor(handler).RunAsync(
|
|
Context(new JsonObject
|
|
{
|
|
["url"] = "https://example.com/x",
|
|
["authentication"] = "credential",
|
|
}, credentials: credentials), default);
|
|
|
|
Assert.True(outcome.Succeeded, outcome.Failure?.Message);
|
|
var expected = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes("ada:secret"));
|
|
Assert.Equal(expected, handler.LastRequest!.Headers.Authorization!.ToString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Missing_required_credential_fails()
|
|
{
|
|
var handler = new StubHandler(_ => Json("{}"));
|
|
|
|
var outcome = await Executor(handler).RunAsync(
|
|
Context(new JsonObject
|
|
{
|
|
["url"] = "https://example.com/x",
|
|
["authentication"] = "credential",
|
|
}), default);
|
|
|
|
Assert.False(outcome.Succeeded);
|
|
Assert.Equal("missing_credential", outcome.Failure!.Code);
|
|
}
|
|
}
|