feat(esg): {param} path-template substitution with consumed-param removal — implements the spec'd /recipes/{id} form
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -1114,4 +1114,79 @@ public class ExternalSystemClientTests
|
||||
|
||||
Assert.True(result.Success);
|
||||
}
|
||||
|
||||
// ── ExternalSystemGateway: {param} path-template substitution ──
|
||||
|
||||
[Fact]
|
||||
public async Task PathTemplate_SubstitutesAndConsumesParameter()
|
||||
{
|
||||
// A `{id}` placeholder in the method path is replaced with the escaped
|
||||
// parameter value and the consumed name is removed from the query string.
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
|
||||
var method = new ExternalSystemMethod("getRecipe", "GET", "/recipes/{id}") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
StubResolution(system, method);
|
||||
|
||||
var handler = new RequestCapturingHandler(HttpStatusCode.OK, "{}");
|
||||
_httpClientFactory.CreateClient(Arg.Any<string>()).Returns(new HttpClient(handler));
|
||||
|
||||
var client = new ExternalSystemClient(
|
||||
_httpClientFactory, _repository, NullLogger<ExternalSystemClient>.Instance);
|
||||
|
||||
await client.CallAsync("TestAPI", "getRecipe", new Dictionary<string, object?>
|
||||
{
|
||||
["id"] = "R 1",
|
||||
["filter"] = "x",
|
||||
});
|
||||
|
||||
var uri = handler.LastUri!.AbsoluteUri;
|
||||
// The value is substituted into the path (escaped) and consumed — it must
|
||||
// NOT reappear in the query string; the remaining parameter still does.
|
||||
Assert.Equal("https://api.example.com/recipes/R%201?filter=x", uri);
|
||||
Assert.DoesNotContain("id=", uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PathTemplate_Post_ConsumedParamExcludedFromBody()
|
||||
{
|
||||
// A path-template parameter consumed by a POST path must not also be
|
||||
// serialized into the JSON body.
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
|
||||
var method = new ExternalSystemMethod("addLine", "POST", "/orders/{orderId}/lines") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
StubResolution(system, method);
|
||||
|
||||
var handler = new RequestCapturingHandler(HttpStatusCode.OK, "{}");
|
||||
_httpClientFactory.CreateClient(Arg.Any<string>()).Returns(new HttpClient(handler));
|
||||
|
||||
var client = new ExternalSystemClient(
|
||||
_httpClientFactory, _repository, NullLogger<ExternalSystemClient>.Instance);
|
||||
|
||||
await client.CallAsync("TestAPI", "addLine", new Dictionary<string, object?>
|
||||
{
|
||||
["orderId"] = 5,
|
||||
["qty"] = 2,
|
||||
});
|
||||
|
||||
Assert.Equal("https://api.example.com/orders/5/lines", handler.LastUri!.ToString());
|
||||
Assert.Contains("\"qty\":2", handler.LastBody);
|
||||
Assert.DoesNotContain("orderId", handler.LastBody);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PathTemplate_MissingParameter_ThrowsClearError()
|
||||
{
|
||||
// A `{id}` placeholder with no matching parameter is an authoring error —
|
||||
// reject it with a clear ArgumentException naming the placeholder.
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
|
||||
var method = new ExternalSystemMethod("getRecipe", "GET", "/recipes/{id}") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
StubResolution(system, method);
|
||||
|
||||
_httpClientFactory.CreateClient(Arg.Any<string>()).Returns(new HttpClient(new RequestCapturingHandler(HttpStatusCode.OK, "{}")));
|
||||
|
||||
var client = new ExternalSystemClient(
|
||||
_httpClientFactory, _repository, NullLogger<ExternalSystemClient>.Instance);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<ArgumentException>(
|
||||
() => client.CallAsync("TestAPI", "getRecipe"));
|
||||
Assert.Contains("id", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user