Skip to content

Commit 7dbe704

Browse files
sij411claude
andcommitted
Add comprehensive test coverage for KvStore.list() in relay
Added test cases to verify list() behavior across all scenarios: - Empty list on initialization - Multiple follower additions and retrieval - Correct exclusion after deletions - Complete actor data preservation - State filtering (pending vs accepted for LitePub) These tests ensure the race condition fix using individual key-value pairs works correctly and list() properly replaces the array-based approach. Related to #505 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 04aa2fd commit 7dbe704

2 files changed

Lines changed: 274 additions & 0 deletions

File tree

packages/relay/src/litepub.test.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -877,4 +877,166 @@ describe("LitePubRelay", () => {
877877
}
878878
strictEqual(count, 3);
879879
});
880+
881+
test("list() returns empty when no followers exist", async () => {
882+
const kv = new MemoryKvStore();
883+
884+
// Verify list is initially empty
885+
let count = 0;
886+
for await (const _ of kv.list(["follower"])) {
887+
count++;
888+
}
889+
strictEqual(count, 0);
890+
});
891+
892+
test("list() returns all followers after additions", async () => {
893+
const kv = new MemoryKvStore();
894+
895+
// Add multiple followers
896+
const followerIds = [
897+
"https://server1.example.com/users/alice",
898+
"https://server2.example.com/users/bob",
899+
"https://server3.example.com/users/carol",
900+
];
901+
902+
for (const followerId of followerIds) {
903+
const actor = new Person({
904+
id: new URL(followerId),
905+
preferredUsername: followerId.split("/").pop(),
906+
inbox: new URL(`${followerId}/inbox`),
907+
});
908+
await kv.set(
909+
["follower", followerId],
910+
{ actor: await actor.toJsonLd(), state: "accepted" },
911+
);
912+
}
913+
914+
// Verify list returns all followers
915+
const retrievedIds: string[] = [];
916+
for await (const { key, value } of kv.list(["follower"])) {
917+
strictEqual(key.length, 2);
918+
strictEqual(key[0], "follower");
919+
retrievedIds.push(key[1] as string);
920+
ok(value);
921+
strictEqual((value as any).state, "accepted");
922+
}
923+
924+
strictEqual(retrievedIds.length, 3);
925+
for (const id of followerIds) {
926+
ok(retrievedIds.includes(id));
927+
}
928+
});
929+
930+
test("list() excludes followers after deletion", async () => {
931+
const kv = new MemoryKvStore();
932+
933+
// Add three followers
934+
const follower1Id = "https://server1.example.com/users/alice";
935+
const follower2Id = "https://server2.example.com/users/bob";
936+
const follower3Id = "https://server3.example.com/users/carol";
937+
938+
for (const followerId of [follower1Id, follower2Id, follower3Id]) {
939+
const actor = new Person({
940+
id: new URL(followerId),
941+
preferredUsername: followerId.split("/").pop(),
942+
inbox: new URL(`${followerId}/inbox`),
943+
});
944+
await kv.set(
945+
["follower", followerId],
946+
{ actor: await actor.toJsonLd(), state: "accepted" },
947+
);
948+
}
949+
950+
// Delete one follower
951+
await kv.delete(["follower", follower2Id]);
952+
953+
// Verify list only returns remaining followers
954+
const retrievedIds: string[] = [];
955+
for await (const { key } of kv.list(["follower"])) {
956+
retrievedIds.push(key[1] as string);
957+
}
958+
959+
strictEqual(retrievedIds.length, 2);
960+
ok(retrievedIds.includes(follower1Id));
961+
ok(!retrievedIds.includes(follower2Id)); // Deleted follower not in list
962+
ok(retrievedIds.includes(follower3Id));
963+
});
964+
965+
test("list() distinguishes between pending and accepted followers", async () => {
966+
const kv = new MemoryKvStore();
967+
968+
// Add followers with different states
969+
const pendingFollowerId = "https://server1.example.com/users/alice";
970+
const acceptedFollowerId = "https://server2.example.com/users/bob";
971+
972+
const pendingFollower = new Person({
973+
id: new URL(pendingFollowerId),
974+
preferredUsername: "alice",
975+
inbox: new URL("https://server1.example.com/users/alice/inbox"),
976+
});
977+
978+
const acceptedFollower = new Person({
979+
id: new URL(acceptedFollowerId),
980+
preferredUsername: "bob",
981+
inbox: new URL("https://server2.example.com/users/bob/inbox"),
982+
});
983+
984+
await kv.set(
985+
["follower", pendingFollowerId],
986+
{ actor: await pendingFollower.toJsonLd(), state: "pending" },
987+
);
988+
989+
await kv.set(
990+
["follower", acceptedFollowerId],
991+
{ actor: await acceptedFollower.toJsonLd(), state: "accepted" },
992+
);
993+
994+
// Verify list returns both with correct states
995+
const followers: { id: string; state: string }[] = [];
996+
for await (const { key, value } of kv.list(["follower"])) {
997+
const followerData = value as any;
998+
followers.push({
999+
id: key[1] as string,
1000+
state: followerData.state,
1001+
});
1002+
}
1003+
1004+
strictEqual(followers.length, 2);
1005+
1006+
const pendingEntry = followers.find((f) => f.id === pendingFollowerId);
1007+
ok(pendingEntry);
1008+
strictEqual(pendingEntry.state, "pending");
1009+
1010+
const acceptedEntry = followers.find((f) => f.id === acceptedFollowerId);
1011+
ok(acceptedEntry);
1012+
strictEqual(acceptedEntry.state, "accepted");
1013+
});
1014+
1015+
test("list() returns correct actor data", async () => {
1016+
const kv = new MemoryKvStore();
1017+
1018+
const followerId = "https://remote.example.com/users/alice";
1019+
const follower = new Person({
1020+
id: new URL(followerId),
1021+
preferredUsername: "alice",
1022+
name: "Alice Wonderland",
1023+
inbox: new URL("https://remote.example.com/users/alice/inbox"),
1024+
});
1025+
1026+
await kv.set(
1027+
["follower", followerId],
1028+
{ actor: await follower.toJsonLd(), state: "accepted" },
1029+
);
1030+
1031+
// Verify list returns complete actor data
1032+
for await (const { key, value } of kv.list(["follower"])) {
1033+
strictEqual(key[1], followerId);
1034+
ok(value);
1035+
const followerData = value as any;
1036+
strictEqual(followerData.state, "accepted");
1037+
ok(followerData.actor);
1038+
strictEqual(followerData.actor.preferredUsername, "alice");
1039+
strictEqual(followerData.actor.name, "Alice Wonderland");
1040+
}
1041+
});
8801042
});

packages/relay/src/mastodon.test.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,4 +768,116 @@ describe("MastodonRelay", () => {
768768
]);
769769
ok(followerData);
770770
});
771+
772+
test("list() returns empty when no followers exist", async () => {
773+
const kv = new MemoryKvStore();
774+
775+
// Verify list is initially empty
776+
let count = 0;
777+
for await (const _ of kv.list(["follower"])) {
778+
count++;
779+
}
780+
strictEqual(count, 0);
781+
});
782+
783+
test("list() returns all followers after additions", async () => {
784+
const kv = new MemoryKvStore();
785+
786+
// Add multiple followers
787+
const followerIds = [
788+
"https://server1.example.com/users/alice",
789+
"https://server2.example.com/users/bob",
790+
"https://server3.example.com/users/carol",
791+
];
792+
793+
for (const followerId of followerIds) {
794+
const actor = new Person({
795+
id: new URL(followerId),
796+
preferredUsername: followerId.split("/").pop(),
797+
inbox: new URL(`${followerId}/inbox`),
798+
});
799+
await kv.set(
800+
["follower", followerId],
801+
{ actor: await actor.toJsonLd(), state: "accepted" },
802+
);
803+
}
804+
805+
// Verify list returns all followers
806+
const retrievedIds: string[] = [];
807+
for await (const { key, value } of kv.list(["follower"])) {
808+
strictEqual(key.length, 2);
809+
strictEqual(key[0], "follower");
810+
retrievedIds.push(key[1] as string);
811+
ok(value);
812+
strictEqual((value as any).state, "accepted");
813+
}
814+
815+
strictEqual(retrievedIds.length, 3);
816+
for (const id of followerIds) {
817+
ok(retrievedIds.includes(id));
818+
}
819+
});
820+
821+
test("list() excludes followers after deletion", async () => {
822+
const kv = new MemoryKvStore();
823+
824+
// Add three followers
825+
const follower1Id = "https://server1.example.com/users/alice";
826+
const follower2Id = "https://server2.example.com/users/bob";
827+
const follower3Id = "https://server3.example.com/users/carol";
828+
829+
for (const followerId of [follower1Id, follower2Id, follower3Id]) {
830+
const actor = new Person({
831+
id: new URL(followerId),
832+
preferredUsername: followerId.split("/").pop(),
833+
inbox: new URL(`${followerId}/inbox`),
834+
});
835+
await kv.set(
836+
["follower", followerId],
837+
{ actor: await actor.toJsonLd(), state: "accepted" },
838+
);
839+
}
840+
841+
// Delete one follower
842+
await kv.delete(["follower", follower2Id]);
843+
844+
// Verify list only returns remaining followers
845+
const retrievedIds: string[] = [];
846+
for await (const { key } of kv.list(["follower"])) {
847+
retrievedIds.push(key[1] as string);
848+
}
849+
850+
strictEqual(retrievedIds.length, 2);
851+
ok(retrievedIds.includes(follower1Id));
852+
ok(!retrievedIds.includes(follower2Id)); // Deleted follower not in list
853+
ok(retrievedIds.includes(follower3Id));
854+
});
855+
856+
test("list() returns correct actor data", async () => {
857+
const kv = new MemoryKvStore();
858+
859+
const followerId = "https://remote.example.com/users/alice";
860+
const follower = new Person({
861+
id: new URL(followerId),
862+
preferredUsername: "alice",
863+
name: "Alice Wonderland",
864+
inbox: new URL("https://remote.example.com/users/alice/inbox"),
865+
});
866+
867+
await kv.set(
868+
["follower", followerId],
869+
{ actor: await follower.toJsonLd(), state: "accepted" },
870+
);
871+
872+
// Verify list returns complete actor data
873+
for await (const { key, value } of kv.list(["follower"])) {
874+
strictEqual(key[1], followerId);
875+
ok(value);
876+
const followerData = value as any;
877+
strictEqual(followerData.state, "accepted");
878+
ok(followerData.actor);
879+
strictEqual(followerData.actor.preferredUsername, "alice");
880+
strictEqual(followerData.actor.name, "Alice Wonderland");
881+
}
882+
});
771883
});

0 commit comments

Comments
 (0)