milestone 10: doh and dot listeners, cert store with hot reload and cert reload api

This commit is contained in:
2026-08-02 14:39:18 +02:00
parent 617cc966a2
commit a589df7515
20 changed files with 4398 additions and 21 deletions
+36
View File
@@ -56,6 +56,7 @@ const types = @import("../dns/types.zig");
const upstreams_repo = @import("../storage/repositories/upstreams_repo.zig");
const handlers_blocklists = @import("handlers/blocklists.zig");
const handlers_certs = @import("handlers/certs.zig");
const handlers_health = @import("handlers/health.zig");
const handlers_live = @import("handlers/live.zig");
const handlers_lookup = @import("handlers/lookup.zig");
@@ -670,6 +671,11 @@ const contract = [_]Contract{
.{ .method = .GET, .pattern = "/api/settings", .auth = .session, .target = "/api/settings", .status = 200, .check = jsonShape(SettingsView) },
.{ .method = .PUT, .pattern = "/api/settings", .auth = .session, .target = "/api/settings", .body = "{\"dns\":{\"port\":5353}}", .status = 200, .check = jsonShape(SettingsView) },
// Certificates. The walk's environment wires no cert store, so both
// endpoints report disabled — and the reload still answers 200 (m10
// ruling 8: the outcome is the payload).
.{ .method = .POST, .pattern = "/api/certs/reload", .auth = .session, .target = "/api/certs/reload", .status = 200, .check = jsonShape(handlers_certs.View) },
// The walk's last delete returns the groups table to its seeded shape.
.{ .method = .DELETE, .pattern = "/api/groups/{id}", .auth = .session, .target = "/api/groups/2", .status = 204, .kind = .none },
};
@@ -773,6 +779,36 @@ test "W10 contract: every route answers its documented status and shape" {
try bounded(env.io(), default_budget, contractWalk, .{ env.io(), env });
}
// The wire shape of the certs reload payload, restated so the handler's own
// `View` cannot vouch for itself. Runs in every suite: it needs no socket.
const CertOutcomeShape = struct { enabled: bool, reloaded: bool, @"error": ?[]const u8 };
const CertsReloadShape = struct { doh: CertOutcomeShape, dot: CertOutcomeShape };
test "the certs reload payload with both endpoints disabled parses strictly" {
const gpa = testing.allocator;
var state: server.WebState = .{ .gpa = gpa };
const view = handlers_certs.applyReload(&state, undefined);
var out: std.Io.Writer.Allocating = .init(gpa);
defer out.deinit();
try std.json.Stringify.value(view, .{}, &out.writer);
var arena_state: std.heap.ArenaAllocator = .init(gpa);
defer arena_state.deinit();
const parsed = try std.json.parseFromSliceLeaky(
CertsReloadShape,
arena_state.allocator(),
out.written(),
.{ .ignore_unknown_fields = false },
);
for ([_]CertOutcomeShape{ parsed.doh, parsed.dot }) |per_endpoint| {
try testing.expect(!per_endpoint.enabled);
try testing.expect(!per_endpoint.reloaded);
try testing.expectEqual(@as(?[]const u8, null), per_endpoint.@"error");
}
}
// ---------------------------------------------------------------------------
// auth on/off matrix (rulings 17, 18)
// ---------------------------------------------------------------------------