From 4f10674feb28b4fe90f106259fbb2540f587b846 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 7 Sep 2026 22:26:04 +0200 Subject: [PATCH] feat: extend protocol --- src/protocol.zig | 160 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/src/protocol.zig b/src/protocol.zig index 883179b..dee7524 100644 --- a/src/protocol.zig +++ b/src/protocol.zig @@ -2195,3 +2195,163 @@ test "frame round trip through an in-memory buffer" { try std.testing.expectEqualStrings("hello", frame.payload); } +test "frame with empty payload round trips" { + const allocator = std.testing.allocator; + var buf = std.ArrayList(u8).init(allocator); + defer buf.deinit(); + + try writeFrame(buf.writer(), @intFromEnum(Op.ping), ""); + var stream = std.io.fixedBufferStream(buf.items); + var frame = try readFrame(allocator, stream.reader()); + defer frame.deinit(); + + try std.testing.expectEqual(@as(usize, 0), frame.payload.len); +} + +test "readFrame rejects a length prefix beyond the frame guard" { + const allocator = std.testing.allocator; + var header: [5]u8 = undefined; + header[0] = 0; + std.mem.writeInt(u32, header[1..5], maxFrameBytes + 1, encode.endian); + var stream = std.io.fixedBufferStream(&header); + try std.testing.expectError(error.FrameTooLarge, readFrame(allocator, stream.reader())); +} + +test "readFrame rejects a truncated body" { + const allocator = std.testing.allocator; + var header: [5]u8 = undefined; + header[0] = 0; + std.mem.writeInt(u32, header[1..5], 10, encode.endian); + var stream = std.io.fixedBufferStream(&header); + try std.testing.expectError(error.EndOfStream, readFrame(allocator, stream.reader())); +} + +test "Op.fromByte rejects unknown opcodes" { + try std.testing.expectError(error.UnknownOp, Op.fromByte(200)); + try std.testing.expectEqual(Op.batch, try Op.fromByte(9)); + try std.testing.expectEqual(Op.table_select, try Op.fromByte(10)); + try std.testing.expectEqual(Op.auth, try Op.fromByte(11)); + try std.testing.expectEqual(Op.user_admin, try Op.fromByte(12)); + try std.testing.expectEqual(Op.auth_token, try Op.fromByte(13)); + try std.testing.expectEqual(Op.metrics, try Op.fromByte(14)); + try std.testing.expectEqual(Op.ttl_op, try Op.fromByte(15)); + try std.testing.expectEqual(Op.coll_ttl_op, try Op.fromByte(16)); + try std.testing.expectEqual(Op.coll_op, try Op.fromByte(17)); + try std.testing.expectEqual(Op.txn_op, try Op.fromByte(18)); + try std.testing.expectEqual(Op.table_join, try Op.fromByte(19)); + try std.testing.expectEqual(Op.table_aggregate, try Op.fromByte(20)); + try std.testing.expectEqual(Op.table_explain, try Op.fromByte(21)); + try std.testing.expectEqual(Op.table_update, try Op.fromByte(22)); + try std.testing.expectEqual(Op.table_delete, try Op.fromByte(23)); + try std.testing.expectEqual(Op.table_export, try Op.fromByte(24)); + try std.testing.expectEqual(Op.table_import, try Op.fromByte(25)); + try std.testing.expectEqual(Op.table_window, try Op.fromByte(26)); + try std.testing.expectEqual(Op.subscribe, try Op.fromByte(27)); + try std.testing.expectEqual(Op.publish, try Op.fromByte(28)); + try std.testing.expectEqual(Op.unsubscribe, try Op.fromByte(29)); +} + +test "Status.fromByte rejects unknown status codes" { + try std.testing.expectError(error.UnknownStatus, Status.fromByte(200)); + try std.testing.expectEqual(Status.internal, try Status.fromByte(5)); + try std.testing.expectEqual(Status.rate_limited, try Status.fromByte(6)); + try std.testing.expectEqual(Status.unauthorized, try Status.fromByte(7)); + try std.testing.expectEqual(Status.forbidden, try Status.fromByte(8)); +} + +test "auth payload encodes and decodes username and password" { + const allocator = std.testing.allocator; + const payload = try encodeAuth(allocator, "alice", "hunter2"); + defer allocator.free(payload); + const args = try decodeAuth(payload); + try std.testing.expectEqualStrings("alice", args.username); + try std.testing.expectEqualStrings("hunter2", args.password); +} + +test "auth payload handles an empty password" { + const allocator = std.testing.allocator; + const payload = try encodeAuth(allocator, "alice", ""); + defer allocator.free(payload); + const args = try decodeAuth(payload); + try std.testing.expectEqualStrings("alice", args.username); + try std.testing.expectEqualStrings("", args.password); +} + +test "encodeAuthOk with no token round trips to an empty payload" { + const allocator = std.testing.allocator; + const payload = try encodeAuthOk(allocator, null); + defer allocator.free(payload); + try std.testing.expectEqual(@as(usize, 0), payload.len); +} + +test "encodeAuthOk with a token round trips exactly" { + const allocator = std.testing.allocator; + const payload = try encodeAuthOk(allocator, "deadbeef"); + defer allocator.free(payload); + try std.testing.expectEqualStrings("deadbeef", payload); +} + +test "auth_token payload round trips the opaque token bytes" { + const allocator = std.testing.allocator; + const payload = try encodeAuthToken(allocator, "abc123token"); + defer allocator.free(payload); + try std.testing.expectEqualStrings("abc123token", decodeAuthToken(payload)); +} + +test "set payload encodes and decodes key and value" { + const allocator = std.testing.allocator; + const payload = try encodeSet(allocator, "k", "value-bytes"); + defer allocator.free(payload); + const args = try decodeSet(payload); + try std.testing.expectEqualStrings("k", args.key); + try std.testing.expectEqualStrings("value-bytes", args.value); +} + +test "set payload handles an empty value" { + const allocator = std.testing.allocator; + const payload = try encodeSet(allocator, "k", ""); + defer allocator.free(payload); + const args = try decodeSet(payload); + try std.testing.expectEqualStrings("k", args.key); + try std.testing.expectEqualStrings("", args.value); +} + +test "scan args round trip with both bounds" { + const allocator = std.testing.allocator; + const payload = try encodeScan(allocator, .{ .start = "a", .end = "z", .limit = 42, .reverse = true }); + defer allocator.free(payload); + const args = try decodeScan(payload); + try std.testing.expectEqualStrings("a", args.start.?); + try std.testing.expectEqualStrings("z", args.end.?); + try std.testing.expectEqual(@as(u32, 42), args.limit); + try std.testing.expect(args.reverse); +} + +test "scan args round trip with no bounds" { + const allocator = std.testing.allocator; + const payload = try encodeScan(allocator, .{}); + defer allocator.free(payload); + const args = try decodeScan(payload); + try std.testing.expect(args.start == null); + try std.testing.expect(args.end == null); + try std.testing.expect(!args.reverse); +} + +test "prefix args round trip" { + const allocator = std.testing.allocator; + const payload = try encodePrefix(allocator, "user:", 7); + defer allocator.free(payload); + const args = try decodePrefix(payload); + try std.testing.expectEqualStrings("user:", args.prefix); + try std.testing.expectEqual(@as(u32, 7), args.limit); +} + +test "entries round trip and are independently owned" { + const allocator = std.testing.allocator; + const entries = [_]dbmod.Entry{ + .{ .key = "a", .value = "1" }, + .{ .key = "b", .value = "2" }, + }; + const payload = try encodeEntries(allocator, &entries); + defer allocator.free(payload); +