Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/api/config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
35 changes: 23 additions & 12 deletions src/api/settings.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 ────────────────────────────────────────────────────────────────

Expand All @@ -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();

Expand All @@ -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.
Expand Down Expand Up @@ -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" {
Expand Down
55 changes: 55 additions & 0 deletions src/integration_tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
18 changes: 16 additions & 2 deletions src/server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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);
Expand Down
Loading