From 5a74f25cfacf9e206e4e06ec7dc8d976e8569a78 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sat, 12 Sep 2026 14:01:46 +0200 Subject: [PATCH] feat: fill in server --- src/server.zig | 161 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/src/server.zig b/src/server.zig index aa2c5d2..cffd170 100644 --- a/src/server.zig +++ b/src/server.zig @@ -5803,3 +5803,164 @@ test "user_admin requires admin role once auth is enabled" { try server.start(); defer server.stop(); + const client_mod = @import("client.zig"); + + var viewer = try client_mod.Client.connect(allocator, server.boundAddress()); + defer viewer.close(); + try viewer.authenticate("viewer", "pass"); + try std.testing.expectError(client_mod.ClientError.ServerForbidden, viewer.adminCreateUser("nobody", "x", .readonly, 16)); + + var admin = try client_mod.Client.connect(allocator, server.boundAddress()); + defer admin.close(); + try admin.authenticate("root", "toor"); + try admin.adminCreateUser("bob", "b-pass", .readwrite, 16); + + const users = try admin.adminListUsers(allocator); + defer authmod.freeUserList(allocator, users); + try std.testing.expectEqual(@as(usize, 3), users.len); +} + +test "user_admin set_password, set_role, and delete round trip over the wire, enforced by real auth" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const path = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(path); + + var db = try testDb(allocator, path); + defer db.close(); + const store = authmod.Store.init(&db); + try store.createUser(allocator, "root", "toor", .admin, 16); + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + server.enableAuth(store); + try server.start(); + defer server.stop(); + + const client_mod = @import("client.zig"); + var admin = try client_mod.Client.connect(allocator, server.boundAddress()); + defer admin.close(); + try admin.authenticate("root", "toor"); + + try admin.adminCreateUser("alice", "old-pass", .readonly, 16); + + // The freshly created account really authenticates against the live, + // enforced auth store - not just written to a table nobody checks. + var probe = try client_mod.Client.connect(allocator, server.boundAddress()); + defer probe.close(); + try probe.authenticate("alice", "old-pass"); + + try admin.adminSetPassword("alice", "new-pass", 16); + var probe2 = try client_mod.Client.connect(allocator, server.boundAddress()); + defer probe2.close(); + try std.testing.expectError(client_mod.ClientError.ServerUnauthorized, probe2.authenticate("alice", "old-pass")); + try probe2.authenticate("alice", "new-pass"); + + try admin.adminSetRole("alice", .admin); + const users = try admin.adminListUsers(allocator); + defer authmod.freeUserList(allocator, users); + var sawAlice = false; + for (users) |u| { + if (std.mem.eql(u8, u.username, "alice")) { + try std.testing.expectEqual(authmod.Role.admin, u.role); + sawAlice = true; + } + } + try std.testing.expect(sawAlice); + + try admin.adminDeleteUser("alice"); + var probe3 = try client_mod.Client.connect(allocator, server.boundAddress()); + defer probe3.close(); + try std.testing.expectError(client_mod.ClientError.ServerUnauthorized, probe3.authenticate("alice", "new-pass")); +} + +test "user_admin reports conflict on duplicate create and not-found on missing user" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const path = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(path); + + var db = try testDb(allocator, path); + defer db.close(); + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + try server.start(); + defer server.stop(); + + const client_mod = @import("client.zig"); + var client = try client_mod.Client.connect(allocator, server.boundAddress()); + defer client.close(); + + try client.adminCreateUser("alice", "hunter2", .readonly, 16); + try std.testing.expectError(client_mod.ClientError.ServerConflict, client.adminCreateUser("alice", "other", .admin, 16)); + try std.testing.expectError(client_mod.ClientError.ServerUserNotFound, client.adminSetPassword("ghost", "x", 16)); + try std.testing.expectError(client_mod.ClientError.ServerUserNotFound, client.adminSetRole("ghost", .admin)); + try std.testing.expectError(client_mod.ClientError.ServerUserNotFound, client.adminDeleteUser("ghost")); +} + +test "server rejects a malformed user_admin frame with bad_request without crashing" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const path = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(path); + + var db = try testDb(allocator, path); + defer db.close(); + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + try server.start(); + defer server.stop(); + + const stream = try std.net.tcpConnectToAddress(server.boundAddress()); + defer stream.close(); + + try protocol.writeFrame(stream.writer(), @intFromEnum(protocol.Op.user_admin), "\x00"); + var frame = try protocol.readFrame(allocator, stream.reader()); + defer frame.deinit(); + try std.testing.expectEqual(@intFromEnum(protocol.Status.bad_request), frame.tag); +} + +test "server metrics register into an external Prometheus registry" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const path = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(path); + + var db = try testDb(allocator, path); + defer db.close(); + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + server.enableRateLimit(1, 1000); + try server.start(); + defer server.stop(); + + const client_mod = @import("client.zig"); + var client = try client_mod.Client.connect(allocator, server.boundAddress()); + defer client.close(); + try client.ping(); + client.ping() catch {}; + + var reg = metrics.Registry.init(allocator); + defer reg.deinit(); + try server.registerMetrics(®); + + var buf = std.ArrayList(u8).init(allocator); + defer buf.deinit(); + try reg.exportText(allocator, buf.writer()); + try std.testing.expect(std.mem.indexOf(u8, buf.items, "cobalt_server_requests_total 2\n") != null); + try std.testing.expect(std.mem.indexOf(u8, buf.items, "cobalt_server_rate_limited_total 1\n") != null); + try std.testing.expect(std.mem.indexOf(u8, buf.items, "cobalt_server_conn_rate_limited_total 0\n") != null); + try std.testing.expect(std.mem.indexOf(u8, buf.items, "cobalt_server_unauthorized_total 0\n") != null); + try std.testing.expect(std.mem.indexOf(u8, buf.items, "cobalt_server_forbidden_total 0\n") != null); + try std.testing.expect(std.mem.indexOf(u8, buf.items, "cobalt_server_session_expired_total 0\n") != null); + try std.testing.expect(std.mem.indexOf(u8, buf.items, "cobalt_server_sessions_swept_total 0\n") != null); + try std.testing.expect(std.mem.indexOf(u8, buf.items, "cobalt_server_ttl_swept_total 0\n") != null); +} +