fix(api): 400 for non-object connection config + null workflow graph; 409 for duplicate connection name

This commit is contained in:
Claude
2026-08-28 14:32:30 +00:00
parent 3ec74b33c9
commit 11aae45000
4 changed files with 61 additions and 6 deletions

View File

@@ -29,10 +29,21 @@ public static class ConnectionsEndpoints
g.MapPost("", (CreateConnectionRequest req, ConnectionRepository repo, IConnectionSecretPolicy policy) =>
{
var id = repo.Create(req.ProviderId, req.Name, req.Config.GetRawText());
var stripped = StripSecrets(req.Config.GetRawText(), policy.SecretFieldsFor(req.ProviderId));
return Results.Created($"/api/connections/{id}",
new ConnectionResponse(id, req.ProviderId, req.Name, stripped));
if (req.Config.ValueKind != JsonValueKind.Object)
return Results.BadRequest(new { error = "config must be a JSON object" });
try
{
var id = repo.Create(req.ProviderId, req.Name, req.Config.GetRawText());
var stripped = StripSecrets(req.Config.GetRawText(), policy.SecretFieldsFor(req.ProviderId));
return Results.Created($"/api/connections/{id}",
new ConnectionResponse(id, req.ProviderId, req.Name, stripped));
}
catch (Microsoft.Data.Sqlite.SqliteException ex) when (ex.SqliteErrorCode == 19)
{
// SQLITE_CONSTRAINT — unique index on (provider_id, name).
return Results.Conflict(new { error = "a connection with that name already exists for this provider" });
}
});
g.MapDelete("/{id}", (string id, ConnectionRepository repo) =>

View File

@@ -21,14 +21,22 @@ public static class WorkflowsEndpoints
g.MapPost("", (CreateWorkflowRequest req, WorkflowRepository repo) =>
{
if (req.Graph.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null)
return Results.BadRequest(new { error = "graph is required" });
var id = repo.Create(req.Name, req.Graph.GetRawText());
var w = repo.Get(id)!;
return Results.Created($"/api/workflows/{id}", ToDto(w));
});
g.MapPut("/{id}", (string id, UpdateWorkflowRequest req, WorkflowRepository repo) =>
repo.Update(id, req.Name, req.Graph.GetRawText())
? Results.NoContent() : Results.NotFound());
{
if (req.Graph.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null)
return Results.BadRequest(new { error = "graph is required" });
return repo.Update(id, req.Name, req.Graph.GetRawText())
? Results.NoContent() : Results.NotFound();
});
g.MapPost("/{id}/activate", (string id, WorkflowRepository repo) =>
repo.SetActive(id, true) ? Results.NoContent() : Results.NotFound());

View File

@@ -46,5 +46,33 @@ public class ConnectionsEndpointsTests : IClassFixture<WebFactory>
Assert.DoesNotContain(list2!, x => x.Id == created.Id);
}
[Fact]
public async Task Post_with_non_object_config_returns_400()
{
var c = await LoggedIn();
var res = await c.PostAsJsonAsync("/api/connections", new
{
providerId = "nats", name = "x", config = "not-an-object"
});
Assert.Equal(HttpStatusCode.BadRequest, res.StatusCode);
}
[Fact]
public async Task Post_with_duplicate_name_per_provider_returns_409()
{
var c = await LoggedIn();
var first = await c.PostAsJsonAsync("/api/connections", new
{
providerId = "nats", name = "dup", config = new { url = "nats://x" }
});
Assert.Equal(HttpStatusCode.Created, first.StatusCode);
var second = await c.PostAsJsonAsync("/api/connections", new
{
providerId = "nats", name = "dup", config = new { url = "nats://y" }
});
Assert.Equal(HttpStatusCode.Conflict, second.StatusCode);
}
private sealed record ConnectionDto(string Id, string ProviderId, string Name, JsonElement Config);
}

View File

@@ -50,6 +50,14 @@ public class WorkflowsEndpointsTests : IClassFixture<WebFactory>
Assert.Equal(HttpStatusCode.NotFound, missing.StatusCode);
}
[Fact]
public async Task Post_with_missing_graph_returns_400()
{
var c = await LoggedIn();
var res = await c.PostAsJsonAsync("/api/workflows", new { name = "nograph" });
Assert.Equal(HttpStatusCode.BadRequest, res.StatusCode);
}
private sealed record WorkflowDto(string Id, string Name, bool Active,
System.Text.Json.JsonElement Graph, DateTimeOffset CreatedAt, DateTimeOffset UpdatedAt);
}