A social knowledge tool for researchers built on ATProto
1import { GetNoteCardsForUrlUseCase } from '../../application/useCases/queries/GetNoteCardsForUrlUseCase';
2import { InMemoryCardRepository } from '../utils/InMemoryCardRepository';
3import { InMemoryCardQueryRepository } from '../utils/InMemoryCardQueryRepository';
4import { InMemoryCollectionRepository } from '../utils/InMemoryCollectionRepository';
5import { CuratorId } from '../../domain/value-objects/CuratorId';
6import { CardBuilder } from '../utils/builders/CardBuilder';
7import { URL } from '../../domain/value-objects/URL';
8import { CardSortField, SortOrder } from '../../domain/ICardQueryRepository';
9
10describe('GetNoteCardsForUrlUseCase', () => {
11 let useCase: GetNoteCardsForUrlUseCase;
12 let cardRepository: InMemoryCardRepository;
13 let cardQueryRepository: InMemoryCardQueryRepository;
14 let collectionRepository: InMemoryCollectionRepository;
15 let curator1: CuratorId;
16 let curator2: CuratorId;
17 let curator3: CuratorId;
18
19 beforeEach(() => {
20 cardRepository = new InMemoryCardRepository();
21 collectionRepository = new InMemoryCollectionRepository();
22 cardQueryRepository = new InMemoryCardQueryRepository(
23 cardRepository,
24 collectionRepository,
25 );
26
27 useCase = new GetNoteCardsForUrlUseCase(cardQueryRepository);
28
29 curator1 = CuratorId.create('did:plc:curator1').unwrap();
30 curator2 = CuratorId.create('did:plc:curator2').unwrap();
31 curator3 = CuratorId.create('did:plc:curator3').unwrap();
32 });
33
34 afterEach(() => {
35 cardRepository.clear();
36 collectionRepository.clear();
37 cardQueryRepository.clear();
38 });
39
40 describe('Multiple users with notes for same URL', () => {
41 it('should return all note cards for the specified URL', async () => {
42 const testUrl = 'https://example.com/shared-article';
43 const url = URL.create(testUrl).unwrap();
44
45 // Create note cards for different users with the same URL
46 const noteCard1 = new CardBuilder()
47 .withCuratorId(curator1.value)
48 .withNoteCard('First user note about the article')
49 .withUrl(url)
50 .buildOrThrow();
51
52 const noteCard2 = new CardBuilder()
53 .withCuratorId(curator2.value)
54 .withNoteCard('Second user note about the article')
55 .withUrl(url)
56 .buildOrThrow();
57
58 const noteCard3 = new CardBuilder()
59 .withCuratorId(curator3.value)
60 .withNoteCard('Third user note about the article')
61 .withUrl(url)
62 .buildOrThrow();
63
64 // Save note cards
65 await cardRepository.save(noteCard1);
66 await cardRepository.save(noteCard2);
67 await cardRepository.save(noteCard3);
68
69 // Execute the use case
70 const query = {
71 url: testUrl,
72 };
73
74 const result = await useCase.execute(query);
75
76 // Verify the result
77 expect(result.isOk()).toBe(true);
78 const response = result.unwrap();
79
80 expect(response.notes).toHaveLength(3);
81 expect(response.pagination.totalCount).toBe(3);
82
83 // Check that all three users' notes are included
84 const authorIds = response.notes.map((note) => note.authorId);
85 expect(authorIds).toContain(curator1.value);
86 expect(authorIds).toContain(curator2.value);
87 expect(authorIds).toContain(curator3.value);
88
89 // Check note content
90 const noteTexts = response.notes.map((note) => note.note);
91 expect(noteTexts).toContain('First user note about the article');
92 expect(noteTexts).toContain('Second user note about the article');
93 expect(noteTexts).toContain('Third user note about the article');
94 });
95
96 it('should return empty result when no notes exist for the URL', async () => {
97 const testUrl = 'https://example.com/nonex istent-article';
98
99 const query = {
100 url: testUrl,
101 };
102
103 const result = await useCase.execute(query);
104
105 expect(result.isOk()).toBe(true);
106 const response = result.unwrap();
107
108 expect(response.notes).toHaveLength(0);
109 expect(response.pagination.totalCount).toBe(0);
110 });
111
112 it('should not return notes for different URLs', async () => {
113 const testUrl1 = 'https://example.com/article1';
114 const testUrl2 = 'https://example.com/article2';
115 const url1 = URL.create(testUrl1).unwrap();
116 const url2 = URL.create(testUrl2).unwrap();
117
118 // Create note cards with different URLs
119 const noteCard1 = new CardBuilder()
120 .withCuratorId(curator1.value)
121 .withNoteCard('Note for article 1')
122 .withUrl(url1)
123 .buildOrThrow();
124
125 const noteCard2 = new CardBuilder()
126 .withCuratorId(curator2.value)
127 .withNoteCard('Note for article 2')
128 .withUrl(url2)
129 .buildOrThrow();
130
131 await cardRepository.save(noteCard1);
132 await cardRepository.save(noteCard2);
133
134 // Query for testUrl1
135 const query = {
136 url: testUrl1,
137 };
138
139 const result = await useCase.execute(query);
140
141 expect(result.isOk()).toBe(true);
142 const response = result.unwrap();
143
144 expect(response.notes).toHaveLength(1);
145 expect(response.notes[0]!.note).toBe('Note for article 1');
146 expect(response.notes[0]!.authorId).toBe(curator1.value);
147 });
148 });
149
150 describe('Pagination', () => {
151 it('should paginate results correctly', async () => {
152 const testUrl = 'https://example.com/popular-article';
153 const url = URL.create(testUrl).unwrap();
154
155 // Create 5 note cards with the same URL from different users
156 for (let i = 1; i <= 5; i++) {
157 const curator = CuratorId.create(`did:plc:curator${i}`).unwrap();
158
159 const noteCard = new CardBuilder()
160 .withCuratorId(curator.value)
161 .withNoteCard(`Note ${i} about the article`)
162 .withUrl(url)
163 .buildOrThrow();
164
165 await cardRepository.save(noteCard);
166 }
167
168 // Test first page with limit 2
169 const query1 = {
170 url: testUrl,
171 page: 1,
172 limit: 2,
173 };
174
175 const result1 = await useCase.execute(query1);
176 expect(result1.isOk()).toBe(true);
177 const response1 = result1.unwrap();
178
179 expect(response1.notes).toHaveLength(2);
180 expect(response1.pagination.currentPage).toBe(1);
181 expect(response1.pagination.totalCount).toBe(5);
182 expect(response1.pagination.totalPages).toBe(3);
183 expect(response1.pagination.hasMore).toBe(true);
184
185 // Test second page
186 const query2 = {
187 url: testUrl,
188 page: 2,
189 limit: 2,
190 };
191
192 const result2 = await useCase.execute(query2);
193 expect(result2.isOk()).toBe(true);
194 const response2 = result2.unwrap();
195
196 expect(response2.notes).toHaveLength(2);
197 expect(response2.pagination.currentPage).toBe(2);
198 expect(response2.pagination.hasMore).toBe(true);
199
200 // Test last page
201 const query3 = {
202 url: testUrl,
203 page: 3,
204 limit: 2,
205 };
206
207 const result3 = await useCase.execute(query3);
208 expect(result3.isOk()).toBe(true);
209 const response3 = result3.unwrap();
210
211 expect(response3.notes).toHaveLength(1);
212 expect(response3.pagination.currentPage).toBe(3);
213 expect(response3.pagination.hasMore).toBe(false);
214 });
215
216 it('should respect limit cap of 100', async () => {
217 const query = {
218 url: 'https://example.com/test',
219 limit: 200, // Should be capped at 100
220 };
221
222 const result = await useCase.execute(query);
223 expect(result.isOk()).toBe(true);
224 const response = result.unwrap();
225
226 expect(response.pagination.limit).toBe(100);
227 });
228 });
229
230 describe('Sorting', () => {
231 it('should use default sorting parameters', async () => {
232 const testUrl = 'https://example.com/test-article';
233
234 const query = {
235 url: testUrl,
236 };
237
238 const result = await useCase.execute(query);
239 expect(result.isOk()).toBe(true);
240 const response = result.unwrap();
241
242 expect(response.sorting.sortBy).toBe(CardSortField.UPDATED_AT);
243 expect(response.sorting.sortOrder).toBe(SortOrder.DESC);
244 });
245
246 it('should use provided sorting parameters', async () => {
247 const testUrl = 'https://example.com/test-article';
248
249 const query = {
250 url: testUrl,
251 sortBy: CardSortField.CREATED_AT,
252 sortOrder: SortOrder.ASC,
253 };
254
255 const result = await useCase.execute(query);
256 expect(result.isOk()).toBe(true);
257 const response = result.unwrap();
258
259 expect(response.sorting.sortBy).toBe(CardSortField.CREATED_AT);
260 expect(response.sorting.sortOrder).toBe(SortOrder.ASC);
261 });
262 });
263
264 describe('Validation', () => {
265 it('should fail with invalid URL', async () => {
266 const query = {
267 url: 'not-a-valid-url',
268 };
269
270 const result = await useCase.execute(query);
271
272 expect(result.isErr()).toBe(true);
273 if (result.isErr()) {
274 expect(result.error.message).toContain('Invalid URL');
275 }
276 });
277 });
278});