diff --git a/src/api/config.zig b/src/api/config.zig index 0e36827..4df209b 100644 --- a/src/api/config.zig +++ b/src/api/config.zig @@ -125,6 +125,16 @@ pub fn handlePatch(allocator: std.mem.Allocator, p: paths_mod.Paths, component: } fn writeConfig(allocator: std.mem.Allocator, p: paths_mod.Paths, component: []const u8, name: []const u8, body: []const u8) ApiResponse { + const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{ + .allocate = .alloc_always, + .ignore_unknown_fields = true, + }) catch return .{ + .status = "400 Bad Request", + .content_type = "application/json", + .body = "{\"error\":\"invalid JSON body\"}", + }; + defer parsed.deinit(); + const config_path = p.instanceConfig(allocator, component, name) catch return .{ .status = "500 Internal Server Error", .content_type = "application/json", diff --git a/src/api/settings.zig b/src/api/settings.zig index 2b64079..753bb2b 100644 --- a/src/api/settings.zig +++ b/src/api/settings.zig @@ -2,6 +2,7 @@ const std = @import("std"); const builtin = @import("builtin"); const access = @import("../access.zig"); const service_manager = @import("../service.zig"); +const helpers = @import("helpers.zig"); // ─── Handlers ──────────────────────────────────────────────────────────────── @@ -26,11 +27,15 @@ pub fn handleGetSettings(allocator: std.mem.Allocator, host: []const u8, port: u } /// PUT /api/settings — update hub settings. Echo the body back as acknowledgment. -/// Caller owns the returned memory. -pub fn handlePutSettings(allocator: std.mem.Allocator, body: []const u8) ![]const u8 { +/// Caller owns the returned response body. +pub fn handlePutSettings(allocator: std.mem.Allocator, body: []const u8) !helpers.ApiResponse { // Validate the body is parseable JSON const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{ .allocate = .alloc_always }) catch { - return try allocator.dupe(u8, "{\"error\":\"invalid JSON body\"}"); + return .{ + .status = "400 Bad Request", + .content_type = "application/json", + .body = try allocator.dupe(u8, "{\"error\":\"invalid JSON body\"}"), + }; }; defer parsed.deinit(); @@ -41,7 +46,11 @@ pub fn handlePutSettings(allocator: std.mem.Allocator, body: []const u8) ![]cons try buf.appendSlice(body); try buf.append('}'); - return try buf.toOwnedSlice(); + return .{ + .status = "200 OK", + .content_type = "application/json", + .body = try buf.toOwnedSlice(), + }; } /// POST /api/service/install — install and enable the user service. @@ -255,21 +264,23 @@ test "handlePutSettings returns ok status" { const allocator = std.testing.allocator; const body = "{\"port\":19801,\"host\":\"0.0.0.0\"}"; - const json = try handlePutSettings(allocator, body); - defer allocator.free(json); + const resp = try handlePutSettings(allocator, body); + defer allocator.free(resp.body); - try std.testing.expect(std.mem.indexOf(u8, json, "\"status\":\"ok\"") != null); - try std.testing.expect(std.mem.indexOf(u8, json, "\"settings\":") != null); - try std.testing.expect(std.mem.indexOf(u8, json, "\"port\":19801") != null); + try std.testing.expectEqualStrings("200 OK", resp.status); + try std.testing.expect(std.mem.indexOf(u8, resp.body, "\"status\":\"ok\"") != null); + try std.testing.expect(std.mem.indexOf(u8, resp.body, "\"settings\":") != null); + try std.testing.expect(std.mem.indexOf(u8, resp.body, "\"port\":19801") != null); } test "handlePutSettings rejects invalid JSON" { const allocator = std.testing.allocator; - const json = try handlePutSettings(allocator, "not json"); - defer allocator.free(json); + const resp = try handlePutSettings(allocator, "not json"); + defer allocator.free(resp.body); - try std.testing.expect(std.mem.indexOf(u8, json, "\"error\"") != null); + try std.testing.expectEqualStrings("400 Bad Request", resp.status); + try std.testing.expect(std.mem.indexOf(u8, resp.body, "\"error\"") != null); } test "handleServiceInstall returns platform info" { diff --git a/src/integration_tests.zig b/src/integration_tests.zig index b403970..913b99e 100644 --- a/src/integration_tests.zig +++ b/src/integration_tests.zig @@ -364,6 +364,61 @@ test "integration harness covers settings and config round-trips" { } } +test "integration harness covers settings and config failure paths" { + var server = try IntegrationServer.startWithSeed(std.testing.allocator, struct { + fn call(srv: *IntegrationServer) !void { + try seedManagedInstance(srv, "nullboiler", "demo"); + } + }.call); + defer server.deinit(); + + { + const resp = try server.fetch(.{ + .path = "/api/settings", + .method = .PUT, + .body = "not json", + }); + defer resp.deinit(std.testing.allocator); + try std.testing.expectEqual(std.http.Status.bad_request, resp.status); + try std.testing.expect(std.mem.indexOf(u8, resp.body, "invalid JSON body") != null); + } + + { + const resp = try server.fetch(.{ + .path = "/api/instances/nullboiler/demo/config", + .method = .PUT, + .body = "{\"gateway\":{\"port\":43123},\"provider\":\"openrouter\"}", + }); + defer resp.deinit(std.testing.allocator); + try std.testing.expectEqual(std.http.Status.ok, resp.status); + } + + { + const resp = try server.fetch(.{ .path = "/api/instances/nullboiler/demo/config?path=missing.path" }); + defer resp.deinit(std.testing.allocator); + try std.testing.expectEqual(std.http.Status.not_found, resp.status); + try std.testing.expect(std.mem.indexOf(u8, resp.body, "config path not found") != null); + } + + { + const resp = try server.fetch(.{ + .path = "/api/instances/nullboiler/demo/config", + .method = .PUT, + .body = "not json", + }); + defer resp.deinit(std.testing.allocator); + try std.testing.expectEqual(std.http.Status.bad_request, resp.status); + try std.testing.expect(std.mem.indexOf(u8, resp.body, "invalid JSON body") != null); + } + + { + const resp = try server.fetch(.{ .path = "/api/instances/nullboiler/demo/config?path=gateway.port" }); + defer resp.deinit(std.testing.allocator); + try std.testing.expectEqual(std.http.Status.ok, resp.status); + try std.testing.expect(std.mem.indexOf(u8, resp.body, "\"value\":43123") != null); + } +} + test "integration harness covers instance lifecycle endpoints" { if (builtin.os.tag == .windows) return error.SkipZigTest; diff --git a/src/server.zig b/src/server.zig index e4a3215..1d7bec5 100644 --- a/src/server.zig +++ b/src/server.zig @@ -937,8 +937,12 @@ pub const Server = struct { } } if (std.mem.eql(u8, method, "PUT")) { - if (settings_api.handlePutSettings(allocator, body)) |json| { - return jsonResponse(json); + if (settings_api.handlePutSettings(allocator, body)) |resp| { + return .{ + .status = resp.status, + .content_type = resp.content_type, + .body = resp.body, + }; } else |_| { return .{ .status = "500 Internal Server Error", @@ -2485,6 +2489,16 @@ test "route PUT /api/settings returns ok" { try std.testing.expect(std.mem.indexOf(u8, resp.body, "\"status\":\"ok\"") != null); } +test "route PUT /api/settings rejects invalid JSON" { + var ctx = TestContext.init(std.testing.allocator); + defer ctx.deinit(std.testing.allocator); + + const resp = ctx.route(std.testing.allocator, "PUT", "/api/settings", "not json"); + defer std.testing.allocator.free(resp.body); + try std.testing.expectEqualStrings("400 Bad Request", resp.status); + try std.testing.expect(std.mem.indexOf(u8, resp.body, "\"error\":\"invalid JSON body\"") != null); +} + test "route POST /api/service/install returns platform info" { var ctx = TestContext.init(std.testing.allocator); defer ctx.deinit(std.testing.allocator);