diff --git a/src/protocol.zig b/src/protocol.zig index 2aaa534..a565fad 100644 --- a/src/protocol.zig +++ b/src/protocol.zig @@ -3775,3 +3775,224 @@ test "subscribe ack round trips a subscriber id and channel count" { try std.testing.expectEqual(@as(u32, 3), ack.channelCount); } +test "unsubscribe args round trip a subscriber id and multi-channel list" { + const allocator = std.testing.allocator; + const payload = try encodeUnsubscribe(allocator, 7, &.{ "orders", "alerts" }); + defer allocator.free(payload); + + const args = try decodeUnsubscribe(allocator, payload); + defer args.deinit(allocator); + try std.testing.expectEqual(@as(u64, 7), args.subscriberId); + try std.testing.expectEqual(@as(usize, 2), args.channels.len); + try std.testing.expectEqualStrings("orders", args.channels[0]); + try std.testing.expectEqualStrings("alerts", args.channels[1]); +} + +test "unsubscribe args round trip an empty channel list" { + const allocator = std.testing.allocator; + const payload = try encodeUnsubscribe(allocator, 1, &.{}); + defer allocator.free(payload); + + const args = try decodeUnsubscribe(allocator, payload); + defer args.deinit(allocator); + try std.testing.expectEqual(@as(usize, 0), args.channels.len); +} + +test "decodeUnsubscribe rejects a truncated channel count" { + const allocator = std.testing.allocator; + // 8 bytes of subscriber id, then only 2 of the 4 count bytes. + try std.testing.expectError(error.Malformed, decodeUnsubscribe(allocator, &.{ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0 })); +} + +test "decodeUnsubscribe rejects a channel count larger than the payload can hold" { + const allocator = std.testing.allocator; + var buf = std.ArrayList(u8).init(allocator); + defer buf.deinit(); + const w = encode.ByteWriter.init(&buf); + try w.putU64(1); + try w.putU32(5); + try std.testing.expectError(error.Malformed, decodeUnsubscribe(allocator, buf.items)); +} + +test "unsubscribe result round trips a removed count, including zero" { + const allocator = std.testing.allocator; + const zero = try encodeUnsubscribeResult(allocator, 0); + defer allocator.free(zero); + try std.testing.expectEqual(@as(u32, 0), try decodeUnsubscribeResult(zero)); + + const some = try encodeUnsubscribeResult(allocator, 2); + defer allocator.free(some); + try std.testing.expectEqual(@as(u32, 2), try decodeUnsubscribeResult(some)); +} + +test "publish args round trip a channel and a binary-safe message" { + const allocator = std.testing.allocator; + const message = [_]u8{ 0, 1, 2, 0, 255 }; + const payload = try encodePublish(allocator, "orders", &message); + defer allocator.free(payload); + + const args = try decodePublish(payload); + try std.testing.expectEqualStrings("orders", args.channel); + try std.testing.expectEqualSlices(u8, &message, args.message); +} + +test "publish result round trips a subscriber count, including zero" { + const allocator = std.testing.allocator; + const payload = try encodePublishResult(allocator, 0); + defer allocator.free(payload); + try std.testing.expectEqual(@as(u64, 0), try decodePublishResult(payload)); + + const payload2 = try encodePublishResult(allocator, 7); + defer allocator.free(payload2); + try std.testing.expectEqual(@as(u64, 7), try decodePublishResult(payload2)); +} + +test "pubsub message round trips channel and payload as independent owned copies" { + const allocator = std.testing.allocator; + const payload = try encodePubSubMessage(allocator, "alerts", "disk full"); + defer allocator.free(payload); + + const decoded = try decodePubSubMessage(allocator, payload); + defer decoded.deinit(allocator); + + try std.testing.expectEqualStrings("alerts", decoded.channel); + try std.testing.expectEqualStrings("disk full", decoded.payload); +} + +test "pubsub message round trips an empty payload" { + const allocator = std.testing.allocator; + const payload = try encodePubSubMessage(allocator, "ch", ""); + defer allocator.free(payload); + + const decoded = try decodePubSubMessage(allocator, payload); + defer decoded.deinit(allocator); + + try std.testing.expectEqualStrings("ch", decoded.channel); + try std.testing.expectEqual(@as(usize, 0), decoded.payload.len); +} + +test "table_window args round trip a lag spec with a nonzero offset and an int default" { + const allocator = std.testing.allocator; + const specs = [_]windowmod.WindowSpec{.{ .field = "amount", .kind = .lag, .offset = 3, .default = .{ .int = -1 } }}; + const order = [_]querymod.OrderBy{.{ .field = "day" }}; + const payload = try encodeTableWindow(allocator, "orders", &specs, &.{}, &order, 0, 0, "", .rows); + defer allocator.free(payload); + const args = try decodeTableWindow(allocator, payload); + defer allocator.free(args.specs); + defer allocator.free(args.partitionBy); + defer allocator.free(args.orderBy); + + try std.testing.expectEqual(@as(usize, 1), args.specs.len); + try std.testing.expectEqual(windowmod.WindowKind.lag, args.specs[0].kind); + try std.testing.expectEqualStrings("amount", args.specs[0].field); + try std.testing.expectEqual(@as(u64, 3), args.specs[0].offset); + try std.testing.expectEqual(@as(i64, -1), args.specs[0].default.int); +} + +test "table_window args round trip a lead spec with its default text and default offset of 1" { + const allocator = std.testing.allocator; + const specs = [_]windowmod.WindowSpec{.{ .field = "status", .kind = .lead, .default = .{ .text = "none-yet" } }}; + const payload = try encodeTableWindow(allocator, "orders", &specs, &.{}, &.{}, 0, 0, "", .rows); + defer allocator.free(payload); + const args = try decodeTableWindow(allocator, payload); + defer allocator.free(args.specs); + defer allocator.free(args.partitionBy); + defer allocator.free(args.orderBy); + + try std.testing.expectEqual(windowmod.WindowKind.lead, args.specs[0].kind); + try std.testing.expectEqual(@as(u64, 1), args.specs[0].offset); + try std.testing.expectEqualStrings("none-yet", args.specs[0].default.text); +} + +test "table_window args round trip lag/lead alongside a plain sum spec in the same request" { + const allocator = std.testing.allocator; + const specs = [_]windowmod.WindowSpec{ + .{ .field = "amount", .kind = .sum }, + .{ .field = "amount", .kind = .lag, .offset = 2 }, + .{ .field = "amount", .kind = .lead }, + }; + const payload = try encodeTableWindow(allocator, "orders", &specs, &.{}, &.{}, 0, 0, "", .rows); + defer allocator.free(payload); + const args = try decodeTableWindow(allocator, payload); + defer allocator.free(args.specs); + defer allocator.free(args.partitionBy); + defer allocator.free(args.orderBy); + + try std.testing.expectEqual(@as(usize, 3), args.specs.len); + try std.testing.expectEqual(windowmod.WindowKind.sum, args.specs[0].kind); + try std.testing.expectEqual(windowmod.WindowKind.lag, args.specs[1].kind); + try std.testing.expectEqual(@as(u64, 2), args.specs[1].offset); + try std.testing.expectEqual(windowmod.WindowKind.lead, args.specs[2].kind); + try std.testing.expectEqual(@as(u64, 1), args.specs[2].offset); +} + +test "table_window args round trip an nth_value spec with its position and a custom default" { + const allocator = std.testing.allocator; + const specs = [_]windowmod.WindowSpec{.{ .field = "amount", .kind = .nth_value, .offset = 2, .default = .{ .int = -1 } }}; + const order = [_]querymod.OrderBy{.{ .field = "day" }}; + const payload = try encodeTableWindow(allocator, "orders", &specs, &.{}, &order, 0, 0, "", .range); + defer allocator.free(payload); + const args = try decodeTableWindow(allocator, payload); + defer allocator.free(args.specs); + defer allocator.free(args.partitionBy); + defer allocator.free(args.orderBy); + + try std.testing.expectEqual(@as(usize, 1), args.specs.len); + try std.testing.expectEqual(windowmod.WindowKind.nth_value, args.specs[0].kind); + try std.testing.expectEqualStrings("amount", args.specs[0].field); + try std.testing.expectEqual(@as(u64, 2), args.specs[0].offset); + try std.testing.expectEqual(@as(i64, -1), args.specs[0].default.int); +} + +test "table_window args round trip nth_value alongside lag/lead/sum in the same request" { + const allocator = std.testing.allocator; + const specs = [_]windowmod.WindowSpec{ + .{ .field = "amount", .kind = .sum }, + .{ .field = "amount", .kind = .nth_value, .offset = 3 }, + .{ .field = "amount", .kind = .lag }, + }; + const payload = try encodeTableWindow(allocator, "orders", &specs, &.{}, &.{}, 0, 0, "", .rows); + defer allocator.free(payload); + const args = try decodeTableWindow(allocator, payload); + defer allocator.free(args.specs); + defer allocator.free(args.partitionBy); + defer allocator.free(args.orderBy); + + try std.testing.expectEqual(@as(usize, 3), args.specs.len); + try std.testing.expectEqual(windowmod.WindowKind.sum, args.specs[0].kind); + try std.testing.expectEqual(windowmod.WindowKind.nth_value, args.specs[1].kind); + try std.testing.expectEqual(@as(u64, 3), args.specs[1].offset); + try std.testing.expectEqual(windowmod.WindowKind.lag, args.specs[2].kind); +} + +test "decodeTableWindow rejects a truncated nth_value default value instead of misreading trailing bytes" { + const allocator = std.testing.allocator; + var buf = std.ArrayList(u8).init(allocator); + defer buf.deinit(); + const w = encode.ByteWriter.init(&buf); + try putLenPrefixed(w, "orders"); + try w.byte(1); // one spec + try w.byte(@intFromEnum(windowmod.WindowKind.nth_value)); + try putLenPrefixed(w, "amount"); + try w.putU64(2); // n + try w.byte(@intFromEnum(rowmod.ValueTag.int)); // default tag says int... + // ...but the 8-byte payload is missing entirely. + + try std.testing.expectError(error.Malformed, decodeTableWindow(allocator, buf.items)); +} + +test "decodeTableWindow rejects a truncated lag/lead default value instead of misreading trailing bytes" { + const allocator = std.testing.allocator; + var buf = std.ArrayList(u8).init(allocator); + defer buf.deinit(); + const w = encode.ByteWriter.init(&buf); + try putLenPrefixed(w, "orders"); + try w.byte(1); // one spec + try w.byte(@intFromEnum(windowmod.WindowKind.lag)); + try putLenPrefixed(w, "amount"); + try w.putU64(1); // offset + try w.byte(@intFromEnum(rowmod.ValueTag.text)); // default tag says text... + // ...but the length-prefixed payload is missing entirely. + + try std.testing.expectError(error.Malformed, decodeTableWindow(allocator, buf.items)); +}