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