diff --git a/src/protocol.zig b/src/protocol.zig index 16aeb96..c35bcda 100644 --- a/src/protocol.zig +++ b/src/protocol.zig @@ -2983,3 +2983,161 @@ test "table_import rejects a format byte outside the DumpFormat enum" { try std.testing.expectError(error.Malformed, decodeTableImport(buf.items)); } +test "Plan round trips full_scan with a null indexName" { + const allocator = std.testing.allocator; + const payload = try encodePlan(allocator, .{ .kind = .full_scan }); + defer allocator.free(payload); + var decoded = try decodePlan(allocator, payload); + defer decoded.deinit(allocator); + try std.testing.expectEqual(querymod.PlanKind.full_scan, decoded.kind); + try std.testing.expect(decoded.indexName == null); + try std.testing.expectEqual(@as(usize, 0), decoded.matchedFields); + try std.testing.expectEqual(@as(usize, 0), decoded.totalFields); +} + +test "Plan round trips index_range with an indexName and field counts" { + const allocator = std.testing.allocator; + const payload = try encodePlan(allocator, .{ .kind = .index_range, .indexName = "score", .matchedFields = 1, .totalFields = 1 }); + defer allocator.free(payload); + var decoded = try decodePlan(allocator, payload); + defer decoded.deinit(allocator); + try std.testing.expectEqual(querymod.PlanKind.index_range, decoded.kind); + try std.testing.expectEqualStrings("score", decoded.indexName.?); + try std.testing.expectEqual(@as(usize, 1), decoded.matchedFields); + try std.testing.expectEqual(@as(usize, 1), decoded.totalFields); +} + +test "Plan round trips composite_eq with a strict prefix match" { + const allocator = std.testing.allocator; + const payload = try encodePlan(allocator, .{ .kind = .composite_eq, .indexName = "city_age", .matchedFields = 1, .totalFields = 2 }); + defer allocator.free(payload); + var decoded = try decodePlan(allocator, payload); + defer decoded.deinit(allocator); + try std.testing.expectEqual(querymod.PlanKind.composite_eq, decoded.kind); + try std.testing.expectEqualStrings("city_age", decoded.indexName.?); + try std.testing.expectEqual(@as(usize, 1), decoded.matchedFields); + try std.testing.expectEqual(@as(usize, 2), decoded.totalFields); +} + +test "decodePlan rejects an unknown PlanKind byte instead of corrupting a plan" { + const allocator = std.testing.allocator; + var buf = std.ArrayList(u8).init(allocator); + defer buf.deinit(); + const w = encode.ByteWriter.init(&buf); + try w.byte(200); // not a valid PlanKind tag + try putLenPrefixed(w, ""); + try w.putU32(0); + try w.putU32(0); + + try std.testing.expectError(error.Malformed, decodePlan(allocator, buf.items)); +} + +test "decodeRows rejects a corrupted row payload without leaking prior rows" { + const allocator = std.testing.allocator; + var buf = std.ArrayList(u8).init(allocator); + defer buf.deinit(); + const w = encode.ByteWriter.init(&buf); + var goodRow = rowmod.Row.init(allocator); + defer goodRow.deinit(); + try goodRow.set("a", .{ .int = 1 }); + const encoded = try goodRow.encode(allocator); + defer allocator.free(encoded); + + try w.putU32(2); + try putLenPrefixed(w, encoded); + try putLenPrefixed(w, &.{ 9, 9, 9 }); + + try std.testing.expectError(error.Malformed, decodeRows(allocator, buf.items)); +} + +test "Cursor.takeLenPrefixed rejects a length that overruns the buffer" { + const allocator = std.testing.allocator; + var buf = std.ArrayList(u8).init(allocator); + defer buf.deinit(); + const w = encode.ByteWriter.init(&buf); + try w.putU32(100); + try w.slice("short"); + var c = Cursor{ .data = buf.items }; + try std.testing.expectError(error.Malformed, c.takeLenPrefixed()); +} + +test "UserAdminAction.fromByte rejects an unknown action" { + try std.testing.expectError(error.Malformed, UserAdminAction.fromByte(200)); + try std.testing.expectEqual(UserAdminAction.list, try UserAdminAction.fromByte(4)); +} + +test "user_admin create payload round trips role and rounds" { + const allocator = std.testing.allocator; + const payload = try encodeUserAdmin(allocator, .create, "alice", "hunter2", .admin, 100_000); + defer allocator.free(payload); + const args = try decodeUserAdmin(payload); + try std.testing.expectEqual(UserAdminAction.create, args.action); + try std.testing.expectEqualStrings("alice", args.username); + try std.testing.expectEqualStrings("hunter2", args.password); + try std.testing.expectEqual(@intFromEnum(authmod.Role.admin), args.roleByte); + try std.testing.expectEqual(@as(u32, 100_000), args.rounds); +} + +test "user_admin delete payload carries only the username" { + const allocator = std.testing.allocator; + const payload = try encodeUserAdmin(allocator, .delete, "bob", "", .readonly, 0); + defer allocator.free(payload); + const args = try decodeUserAdmin(payload); + try std.testing.expectEqual(UserAdminAction.delete, args.action); + try std.testing.expectEqualStrings("bob", args.username); + try std.testing.expectEqualStrings("", args.password); +} + +test "user_admin list payload round trips with an empty username" { + const allocator = std.testing.allocator; + const payload = try encodeUserAdmin(allocator, .list, "", "", .readonly, 0); + defer allocator.free(payload); + const args = try decodeUserAdmin(payload); + try std.testing.expectEqual(UserAdminAction.list, args.action); + try std.testing.expectEqualStrings("", args.username); +} + +test "user list round trips every username with its role" { + const allocator = std.testing.allocator; + const users = [_]authmod.UserInfo{ + .{ .username = @constCast("alice"), .role = .readonly }, + .{ .username = @constCast("root"), .role = .admin }, + }; + const payload = try encodeUserList(allocator, &users); + defer allocator.free(payload); + + const decoded = try decodeUserList(allocator, payload); + defer authmod.freeUserList(allocator, decoded); + + try std.testing.expectEqual(@as(usize, 2), decoded.len); + try std.testing.expectEqualStrings("alice", decoded[0].username); + try std.testing.expectEqual(authmod.Role.readonly, decoded[0].role); + try std.testing.expectEqualStrings("root", decoded[1].username); + try std.testing.expectEqual(authmod.Role.admin, decoded[1].role); +} + +test "empty user list round trips" { + const allocator = std.testing.allocator; + const payload = try encodeUserList(allocator, &[_]authmod.UserInfo{}); + defer allocator.free(payload); + const decoded = try decodeUserList(allocator, payload); + defer authmod.freeUserList(allocator, decoded); + try std.testing.expectEqual(@as(usize, 0), decoded.len); +} + +test "decodeUserList rejects an unknown role byte" { + const allocator = std.testing.allocator; + var buf = std.ArrayList(u8).init(allocator); + defer buf.deinit(); + const w = encode.ByteWriter.init(&buf); + try w.putU32(1); + try putLenPrefixed(w, "ghost"); + try w.byte(200); + try std.testing.expectError(error.Malformed, decodeUserList(allocator, buf.items)); +} + +test "TtlAction.fromByte rejects an unknown action" { + try std.testing.expectError(error.Malformed, TtlAction.fromByte(200)); + try std.testing.expectEqual(TtlAction.sweep, try TtlAction.fromByte(3)); +} +