diff --git a/src/protocol.zig b/src/protocol.zig index 3126eaf..ec2fb1d 100644 --- a/src/protocol.zig +++ b/src/protocol.zig @@ -3295,3 +3295,160 @@ test "coll_op payload round trips a zero-arg action" { try std.testing.expectEqual(@as(usize, 0), decoded.args.len); } +test "decodeCollOp rejects an unknown action byte" { + const allocator = std.testing.allocator; + var buf = std.ArrayList(u8).init(allocator); + defer buf.deinit(); + const w = encode.ByteWriter.init(&buf); + try w.byte(250); + try putLenPrefixed(w, "x"); + try w.putU32(0); + try std.testing.expectError(error.Malformed, decodeCollOp(allocator, buf.items)); +} + +test "coll reply round trips nil and ok" { + const allocator = std.testing.allocator; + + const nilPayload = try encodeCollReply(allocator, .nil); + defer allocator.free(nilPayload); + var nilReply = try decodeCollReply(allocator, nilPayload); + defer nilReply.deinit(allocator); + try std.testing.expect(nilReply == .nil); + + const okPayload = try encodeCollReply(allocator, .ok); + defer allocator.free(okPayload); + var okReply = try decodeCollReply(allocator, okPayload); + defer okReply.deinit(allocator); + try std.testing.expect(okReply == .ok); +} + +test "coll reply round trips int and real" { + const allocator = std.testing.allocator; + + const intPayload = try encodeCollReply(allocator, .{ .int = -42 }); + defer allocator.free(intPayload); + var intReply = try decodeCollReply(allocator, intPayload); + defer intReply.deinit(allocator); + try std.testing.expectEqual(@as(i64, -42), intReply.int); + + const realPayload = try encodeCollReply(allocator, .{ .real = 3.5 }); + defer allocator.free(realPayload); + var realReply = try decodeCollReply(allocator, realPayload); + defer realReply.deinit(allocator); + try std.testing.expectEqual(@as(f64, 3.5), realReply.real); +} + +test "coll reply round trips raw bytes independently of the source payload" { + const allocator = std.testing.allocator; + const payload = try encodeCollReply(allocator, .{ .bytes = "hello" }); + defer allocator.free(payload); + var reply = try decodeCollReply(allocator, payload); + defer reply.deinit(allocator); + try std.testing.expectEqualStrings("hello", reply.bytes); +} + +test "coll reply round trips a string list, including empty" { + const allocator = std.testing.allocator; + const items = [_][]const u8{ "a", "bb", "ccc" }; + const payload = try encodeCollReply(allocator, .{ .stringList = &items }); + defer allocator.free(payload); + var reply = try decodeCollReply(allocator, payload); + defer reply.deinit(allocator); + try std.testing.expectEqual(@as(usize, 3), reply.stringList.len); + try std.testing.expectEqualStrings("bb", reply.stringList[1]); + + const emptyPayload = try encodeCollReply(allocator, .{ .stringList = &.{} }); + defer allocator.free(emptyPayload); + var emptyReply = try decodeCollReply(allocator, emptyPayload); + defer emptyReply.deinit(allocator); + try std.testing.expectEqual(@as(usize, 0), emptyReply.stringList.len); +} + +test "coll reply round trips a pair list (hash getall shape)" { + const allocator = std.testing.allocator; + const pairs = [_]CollPair{ + .{ .field = "name", .value = "ada" }, + .{ .field = "age", .value = "36" }, + }; + const payload = try encodeCollReply(allocator, .{ .pairList = &pairs }); + defer allocator.free(payload); + var reply = try decodeCollReply(allocator, payload); + defer reply.deinit(allocator); + try std.testing.expectEqual(@as(usize, 2), reply.pairList.len); + try std.testing.expectEqualStrings("name", reply.pairList[0].field); + try std.testing.expectEqualStrings("ada", reply.pairList[0].value); + try std.testing.expectEqualStrings("age", reply.pairList[1].field); + try std.testing.expectEqualStrings("36", reply.pairList[1].value); +} + +test "coll reply round trips a score pair list (zset range shape), including negative scores" { + const allocator = std.testing.allocator; + const pairs = [_]CollScorePair{ + .{ .member = "bob", .score = -1.5 }, + .{ .member = "ada", .score = 10.25 }, + }; + const payload = try encodeCollReply(allocator, .{ .scorePairList = &pairs }); + defer allocator.free(payload); + var reply = try decodeCollReply(allocator, payload); + defer reply.deinit(allocator); + try std.testing.expectEqual(@as(usize, 2), reply.scorePairList.len); + try std.testing.expectEqualStrings("bob", reply.scorePairList[0].member); + try std.testing.expectEqual(@as(f64, -1.5), reply.scorePairList[0].score); + try std.testing.expectEqualStrings("ada", reply.scorePairList[1].member); + try std.testing.expectEqual(@as(f64, 10.25), reply.scorePairList[1].score); +} + +test "decodeCollReply rejects an unknown tag byte" { + const allocator = std.testing.allocator; + var buf = std.ArrayList(u8).init(allocator); + defer buf.deinit(); + const w = encode.ByteWriter.init(&buf); + try w.byte(250); + try std.testing.expectError(error.Malformed, decodeCollReply(allocator, buf.items)); +} + +test "txn_op entries round trip put and del across two tables" { + const allocator = std.testing.allocator; + const putFields = [_]rowmod.Field{ + .{ .name = "balance", .value = .{ .int = 100 } }, + .{ .name = "city", .value = .{ .text = "warsaw" } }, + }; + const entries = [_]TxnOpEntry{ + .{ .verb = .put, .table = "accounts", .pk = .{ .int = 1 }, .fields = &putFields }, + .{ .verb = .del, .table = "sessions", .pk = .{ .text = "abc" } }, + }; + const payload = try encodeTxnOp(allocator, &entries); + defer allocator.free(payload); + + const decoded = try decodeTxnOp(allocator, payload); + defer allocator.free(decoded); + try std.testing.expectEqual(@as(usize, 2), decoded.len); + + try std.testing.expectEqual(TxnVerb.put, decoded[0].verb); + try std.testing.expectEqualStrings("accounts", decoded[0].table); + const pk0 = try rowmod.decodeValue(allocator, decoded[0].pkBytes); + try std.testing.expectEqual(@as(i64, 1), pk0.int); + var fields0 = try rowmod.Row.decode(allocator, decoded[0].fieldsBytes); + defer fields0.deinit(); + try std.testing.expectEqual(@as(i64, 100), fields0.get("balance").?.int); + try std.testing.expectEqualStrings("warsaw", fields0.get("city").?.text); + + try std.testing.expectEqual(TxnVerb.del, decoded[1].verb); + try std.testing.expectEqualStrings("sessions", decoded[1].table); + var pk1 = try rowmod.decodeValue(allocator, decoded[1].pkBytes); + defer pk1.free(allocator); + try std.testing.expectEqualStrings("abc", pk1.text); + var fields1 = try rowmod.Row.decode(allocator, decoded[1].fieldsBytes); + defer fields1.deinit(); + try std.testing.expectEqual(@as(usize, 0), fields1.count()); +} + +test "empty txn_op entry list round trips" { + const allocator = std.testing.allocator; + const payload = try encodeTxnOp(allocator, &[_]TxnOpEntry{}); + defer allocator.free(payload); + const decoded = try decodeTxnOp(allocator, payload); + defer allocator.free(decoded); + try std.testing.expectEqual(@as(usize, 0), decoded.len); +} +