feat: continue work on server
This commit is contained in:
1 parent
71086b2a09
commit
4675efab80
1 file changed
+158
+158
@@ -7873,3 +7873,161 @@ test "server publish to a channel with no subscribers returns 0 without crashing
|
|||||||
try server.start();
|
try server.start();
|
||||||
defer server.stop();
|
defer server.stop();
|
||||||
|
|
||||||
|
const client_mod = @import("client.zig");
|
||||||
|
var client = try client_mod.Client.connect(allocator, server.boundAddress());
|
||||||
|
defer client.close();
|
||||||
|
|
||||||
|
try std.testing.expectEqual(@as(u64, 0), try client.publish("nobody-home", "hello"));
|
||||||
|
// A no-receiver publish is not a mutation of anything - `ping` still
|
||||||
|
// works on the same connection right after, proving `publish` never
|
||||||
|
// left the connection or the server in some half-dispatched state.
|
||||||
|
try client.ping();
|
||||||
|
}
|
||||||
|
|
||||||
|
test "server rejects an empty subscribe channel list as bad_request, connection stays diagnosable" {
|
||||||
|
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 std.testing.expectError(client_mod.ClientError.ServerBadRequest, client.subscribe(&.{}));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "closing a subscriber's connection unregisters it - a later publish no longer reaches it" {
|
||||||
|
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 sub = try client_mod.Client.connect(allocator, server.boundAddress());
|
||||||
|
_ = try sub.subscribe(&.{"ch"});
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), server.pubsub.channelSubscriberCount("ch"));
|
||||||
|
sub.close();
|
||||||
|
|
||||||
|
// The watcher thread's disconnect detection runs asynchronously - poll
|
||||||
|
// with a bounded timeout instead of a fixed sleep, the same style
|
||||||
|
// `drainConnections` already uses elsewhere in this file.
|
||||||
|
const startedAt = std.time.nanoTimestamp();
|
||||||
|
while (server.pubsub.channelSubscriberCount("ch") != 0) {
|
||||||
|
if (std.time.nanoTimestamp() - startedAt > 2 * std.time.ns_per_s) break;
|
||||||
|
std.time.sleep(5 * std.time.ns_per_ms);
|
||||||
|
}
|
||||||
|
try std.testing.expectEqual(@as(usize, 0), server.pubsub.channelSubscriberCount("ch"));
|
||||||
|
|
||||||
|
var pub_ = try client_mod.Client.connect(allocator, server.boundAddress());
|
||||||
|
defer pub_.close();
|
||||||
|
try std.testing.expectEqual(@as(u64, 0), try pub_.publish("ch", "too-late"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "server with auth enabled requires authentication before subscribe or publish, readonly role suffices" {
|
||||||
|
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, "viewer", "hunter2", .readonly, 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 anon = try client_mod.Client.connect(allocator, server.boundAddress());
|
||||||
|
defer anon.close();
|
||||||
|
try std.testing.expectError(client_mod.ClientError.ServerUnauthorized, anon.subscribe(&.{"ch"}));
|
||||||
|
try std.testing.expectError(client_mod.ClientError.ServerUnauthorized, anon.publish("ch", "x"));
|
||||||
|
|
||||||
|
var viewer = try client_mod.Client.connect(allocator, server.boundAddress());
|
||||||
|
defer viewer.close();
|
||||||
|
try viewer.authenticate("viewer", "hunter2");
|
||||||
|
_ = try viewer.subscribe(&.{"ch"});
|
||||||
|
|
||||||
|
var writer_ = try client_mod.Client.connect(allocator, server.boundAddress());
|
||||||
|
defer writer_.close();
|
||||||
|
try writer_.authenticate("viewer", "hunter2");
|
||||||
|
try std.testing.expectEqual(@as(u64, 1), try writer_.publish("ch", "hi"));
|
||||||
|
|
||||||
|
const msg = (try viewer.nextMessage(allocator)).?;
|
||||||
|
defer msg.deinit(allocator);
|
||||||
|
try std.testing.expectEqualStrings("hi", msg.payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "server unsubscribe over real TCP drops one channel while the other keeps flowing" {
|
||||||
|
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 sub = try client_mod.Client.connect(allocator, server.boundAddress());
|
||||||
|
defer sub.close();
|
||||||
|
const subId = try sub.subscribe(&.{ "orders", "alerts" });
|
||||||
|
|
||||||
|
// The subscribe connection itself never answers another request (see
|
||||||
|
// `handleSubscribe`'s doc comment) - unsubscribe always goes over its
|
||||||
|
// own, separate connection.
|
||||||
|
var ctrl = try client_mod.Client.connect(allocator, server.boundAddress());
|
||||||
|
defer ctrl.close();
|
||||||
|
const removed = try ctrl.unsubscribe(subId, &.{"orders"});
|
||||||
|
try std.testing.expectEqual(@as(u32, 1), removed);
|
||||||
|
// `ctrl` is an ordinary connection - it must still answer further
|
||||||
|
// requests normally after an unsubscribe, proving `handleUnsubscribe`
|
||||||
|
// returns like any other opcode rather than hijacking the connection.
|
||||||
|
try ctrl.ping();
|
||||||
|
|
||||||
|
var pub_ = try client_mod.Client.connect(allocator, server.boundAddress());
|
||||||
|
defer pub_.close();
|
||||||
|
try std.testing.expectEqual(@as(u64, 0), try pub_.publish("orders", "too-late"));
|
||||||
|
try std.testing.expectEqual(@as(u64, 1), try pub_.publish("alerts", "fire"));
|
||||||
|
|
||||||
|
const msg = (try sub.nextMessage(allocator)).?;
|
||||||
|
defer msg.deinit(allocator);
|
||||||
|
try std.testing.expectEqualStrings("alerts", msg.channel);
|
||||||
|
try std.testing.expectEqualStrings("fire", msg.payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "server unsubscribe from every remaining channel ends the subscription like a disconnect" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var tmp = std.testing.tmpDir(.{});
|
||||||
|
defer tmp.cleanup();
|
||||||
|
const path = try tmp.dir.realpathAlloc(allocator, ".");
|
||||||
|
defer allocator.free(path);
|
||||||
|
|
||||||
Reference in new issue
Block a user