ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
16
fork

Configure Feed

Select the types of activity you want to include in your feed.

test(api): add follow service unit tests

byarielm.fyi 9bf02b0f c930099b

verified
+249
+249
packages/api/src/services/FollowService.test.ts
··· 1 + import { describe, it, expect, vi } from "vitest"; 2 + import type { Agent } from "@atproto/api"; 3 + import { FollowService } from "./FollowService"; 4 + import { createMockAgent } from "../../__tests__/fixtures"; 5 + import type { MockFollowRecord } from "../../__tests__/fixtures"; 6 + 7 + /** Helper to build follow records for a list of DIDs */ 8 + function buildFollowRecords(dids: string[]): MockFollowRecord[] { 9 + return dids.map((did) => ({ 10 + uri: `at://did:plc:self/app.bsky.graph.follow/${did}`, 11 + cid: "mock-cid", 12 + value: { subject: did, createdAt: new Date().toISOString() }, 13 + })); 14 + } 15 + 16 + describe("FollowService", () => { 17 + describe("checkFollowStatus", () => { 18 + it("returns all false for empty DID array", async () => { 19 + const agent = createMockAgent(); 20 + const result = await FollowService.checkFollowStatus( 21 + agent, 22 + "did:plc:self", 23 + [], 24 + ); 25 + expect(result).toEqual({}); 26 + }); 27 + 28 + it("returns all false when no follows exist", async () => { 29 + const agent = createMockAgent({ 30 + listRecords: async () => ({ 31 + data: { records: [], cursor: undefined }, 32 + }), 33 + }); 34 + 35 + const result = await FollowService.checkFollowStatus( 36 + agent, 37 + "did:plc:self", 38 + ["did:plc:user1", "did:plc:user2"], 39 + ); 40 + 41 + expect(result).toEqual({ 42 + "did:plc:user1": false, 43 + "did:plc:user2": false, 44 + }); 45 + }); 46 + 47 + it("detects followed DIDs from records", async () => { 48 + const records = buildFollowRecords(["did:plc:user1", "did:plc:user3"]); 49 + const agent = createMockAgent({ 50 + listRecords: async () => ({ 51 + data: { records, cursor: undefined }, 52 + }), 53 + }); 54 + 55 + const result = await FollowService.checkFollowStatus( 56 + agent, 57 + "did:plc:self", 58 + ["did:plc:user1", "did:plc:user2", "did:plc:user3"], 59 + ); 60 + 61 + expect(result).toEqual({ 62 + "did:plc:user1": true, 63 + "did:plc:user2": false, 64 + "did:plc:user3": true, 65 + }); 66 + }); 67 + 68 + it("handles pagination with cursor", async () => { 69 + let callCount = 0; 70 + const agent = createMockAgent({ 71 + listRecords: async () => { 72 + callCount++; 73 + if (callCount === 1) { 74 + return { 75 + data: { 76 + records: buildFollowRecords(["did:plc:user1"]), 77 + cursor: "page2", 78 + }, 79 + }; 80 + } 81 + return { 82 + data: { 83 + records: buildFollowRecords(["did:plc:user2"]), 84 + cursor: undefined, 85 + }, 86 + }; 87 + }, 88 + }); 89 + 90 + const result = await FollowService.checkFollowStatus( 91 + agent, 92 + "did:plc:self", 93 + ["did:plc:user1", "did:plc:user2"], 94 + ); 95 + 96 + expect(result).toEqual({ 97 + "did:plc:user1": true, 98 + "did:plc:user2": true, 99 + }); 100 + expect(callCount).toBe(2); 101 + }); 102 + 103 + it("stops early when all DIDs are found", async () => { 104 + let callCount = 0; 105 + const agent = createMockAgent({ 106 + listRecords: async () => { 107 + callCount++; 108 + return { 109 + data: { 110 + records: buildFollowRecords(["did:plc:user1"]), 111 + cursor: "more", 112 + }, 113 + }; 114 + }, 115 + }); 116 + 117 + const result = await FollowService.checkFollowStatus( 118 + agent, 119 + "did:plc:self", 120 + ["did:plc:user1"], 121 + ); 122 + 123 + expect(result["did:plc:user1"]).toBe(true); 124 + expect(callCount).toBe(1); // Should stop after finding all DIDs 125 + }); 126 + 127 + it("returns all false on API error (fail-safe)", async () => { 128 + const agent = createMockAgent({ 129 + listRecords: async () => { 130 + throw new Error("Network error"); 131 + }, 132 + }); 133 + 134 + const errorSpy = vi 135 + .spyOn(console, "error") 136 + .mockImplementation(() => {}); 137 + const result = await FollowService.checkFollowStatus( 138 + agent, 139 + "did:plc:self", 140 + ["did:plc:user1", "did:plc:user2"], 141 + ); 142 + 143 + expect(result).toEqual({ 144 + "did:plc:user1": false, 145 + "did:plc:user2": false, 146 + }); 147 + expect(errorSpy).toHaveBeenCalled(); 148 + errorSpy.mockRestore(); 149 + }); 150 + 151 + it("handles records without subject field", async () => { 152 + const agent = createMockAgent({ 153 + listRecords: async () => ({ 154 + data: { 155 + records: [ 156 + { 157 + uri: "at://did:plc:self/app.bsky.graph.follow/1", 158 + cid: "mock-cid", 159 + value: { createdAt: new Date().toISOString() }, 160 + } as MockFollowRecord, 161 + ], 162 + cursor: undefined, 163 + }, 164 + }), 165 + }); 166 + 167 + const result = await FollowService.checkFollowStatus( 168 + agent, 169 + "did:plc:self", 170 + ["did:plc:user1"], 171 + ); 172 + 173 + expect(result["did:plc:user1"]).toBe(false); 174 + }); 175 + 176 + it("passes correct parameters to listRecords", async () => { 177 + const listRecordsFn = vi.fn().mockResolvedValue({ 178 + data: { records: [], cursor: undefined }, 179 + }); 180 + const agent = createMockAgent({ listRecords: listRecordsFn }); 181 + 182 + await FollowService.checkFollowStatus( 183 + agent, 184 + "did:plc:self", 185 + ["did:plc:user1"], 186 + "custom.lexicon", 187 + ); 188 + 189 + expect(listRecordsFn).toHaveBeenCalledWith( 190 + expect.objectContaining({ 191 + repo: "did:plc:self", 192 + collection: "custom.lexicon", 193 + limit: 100, 194 + }), 195 + ); 196 + }); 197 + }); 198 + 199 + describe("getAlreadyFollowing", () => { 200 + it("returns set of followed DIDs", async () => { 201 + const records = buildFollowRecords(["did:plc:user1", "did:plc:user3"]); 202 + const agent = createMockAgent({ 203 + listRecords: async () => ({ 204 + data: { records, cursor: undefined }, 205 + }), 206 + }); 207 + 208 + const result = await FollowService.getAlreadyFollowing( 209 + agent, 210 + "did:plc:self", 211 + ["did:plc:user1", "did:plc:user2", "did:plc:user3"], 212 + ); 213 + 214 + expect(result).toBeInstanceOf(Set); 215 + expect(result.size).toBe(2); 216 + expect(result.has("did:plc:user1")).toBe(true); 217 + expect(result.has("did:plc:user3")).toBe(true); 218 + expect(result.has("did:plc:user2")).toBe(false); 219 + }); 220 + 221 + it("returns empty set when no DIDs are followed", async () => { 222 + const agent = createMockAgent({ 223 + listRecords: async () => ({ 224 + data: { records: [], cursor: undefined }, 225 + }), 226 + }); 227 + 228 + const result = await FollowService.getAlreadyFollowing( 229 + agent, 230 + "did:plc:self", 231 + ["did:plc:user1"], 232 + ); 233 + 234 + expect(result.size).toBe(0); 235 + }); 236 + 237 + it("returns empty set for empty input", async () => { 238 + const agent = createMockAgent(); 239 + 240 + const result = await FollowService.getAlreadyFollowing( 241 + agent, 242 + "did:plc:self", 243 + [], 244 + ); 245 + 246 + expect(result.size).toBe(0); 247 + }); 248 + }); 249 + });