diff --git a/src/server.zig b/src/server.zig index 43c37bb..16cccfc 100644 --- a/src/server.zig +++ b/src/server.zig @@ -7392,3 +7392,163 @@ test "user_admin survives many real concurrent connections mutating and listing } }.run; + const ReadCtx = struct { + allocator: Allocator, + addr: std.net.Address, + ok: bool = false, + }; + + const readWorker = struct { + fn run(ctx: *ReadCtx) void { + var client = client_mod.Client.connect(ctx.allocator, ctx.addr) catch return; + defer client.close(); + var i: usize = 0; + while (i < readsPerReader) : (i += 1) { + // Any decode failure inside the server (a torn/corrupted + // record) surfaces here as `error.ServerInternal`, not a + // silently wrong answer, since `handleUserAdmin` maps every + // `UserRecord.decode` error straight to an `internal` + // status frame. + const users = client.adminListUsers(ctx.allocator) catch return; + defer authmod.freeUserList(ctx.allocator, users); + if (users.len != 1) return; + if (!std.mem.eql(u8, users[0].username, "shared-admin")) return; + } + ctx.ok = true; + } + }.run; + + var writeContexts: [writerCount]WriteCtx = undefined; + var writeThreads: [writerCount]std.Thread = undefined; + var readContexts: [readerCount]ReadCtx = undefined; + var readThreads: [readerCount]std.Thread = undefined; + + for (0..readerCount) |i| { + readContexts[i] = .{ .allocator = allocator, .addr = server.boundAddress() }; + readThreads[i] = try std.Thread.spawn(.{}, readWorker, .{&readContexts[i]}); + } + for (0..writerCount) |i| { + writeContexts[i] = .{ .allocator = allocator, .addr = server.boundAddress(), .thread = i }; + writeThreads[i] = try std.Thread.spawn(.{}, writeWorker, .{&writeContexts[i]}); + } + for (0..writerCount) |i| writeThreads[i].join(); + for (0..readerCount) |i| readThreads[i].join(); + for (0..writerCount) |i| try std.testing.expect(writeContexts[i].ok); + for (0..readerCount) |i| try std.testing.expect(readContexts[i].ok); + + // A final, deterministic write from the main thread after every racer + // has joined, then verified through `auth.Store.verify` directly + // (bypassing the network entirely) against the SAME `*Db` the server + // just hammered concurrently: this proves the record surviving on disk + // is not merely "decodable" (the readers above already checked that) + // but functionally intact - the exact salt/rounds/hash triple that + // `setPassword` wrote is still paired correctly with the role + // `setRole` wrote after it, not a torn mix of two different racers' + // writes. + try setup.adminSetPassword("shared-admin", "final-known-pw", 4); + try setup.adminSetRole("shared-admin", .admin); + + const store = authmod.Store.init(&db); + const role = try store.verify(allocator, "shared-admin", "final-known-pw"); + try std.testing.expectEqual(authmod.Role.admin, role); + + const finalUsers = try setup.adminListUsers(allocator); + defer authmod.freeUserList(allocator, finalUsers); + try std.testing.expectEqual(@as(usize, 1), finalUsers.len); + try std.testing.expectEqual(authmod.Role.admin, finalUsers[0].role); +} + +// `txn_op` is the network counterpart of the local CLI's `txn