A social knowledge tool for researchers built on ATProto
at development 339 lines 10 kB view raw
1import { GetLibrariesForUrlUseCase } from '../../application/useCases/queries/GetLibrariesForUrlUseCase'; 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 { CardTypeEnum } from '../../domain/value-objects/CardType'; 8import { URL } from '../../domain/value-objects/URL'; 9import { CardSortField, SortOrder } from '../../domain/ICardQueryRepository'; 10 11describe('GetLibrariesForUrlUseCase', () => { 12 let useCase: GetLibrariesForUrlUseCase; 13 let cardRepository: InMemoryCardRepository; 14 let cardQueryRepository: InMemoryCardQueryRepository; 15 let collectionRepository: InMemoryCollectionRepository; 16 let curator1: CuratorId; 17 let curator2: CuratorId; 18 let curator3: CuratorId; 19 20 beforeEach(() => { 21 cardRepository = new InMemoryCardRepository(); 22 collectionRepository = new InMemoryCollectionRepository(); 23 cardQueryRepository = new InMemoryCardQueryRepository( 24 cardRepository, 25 collectionRepository, 26 ); 27 28 useCase = new GetLibrariesForUrlUseCase(cardQueryRepository); 29 30 curator1 = CuratorId.create('did:plc:curator1').unwrap(); 31 curator2 = CuratorId.create('did:plc:curator2').unwrap(); 32 curator3 = CuratorId.create('did:plc:curator3').unwrap(); 33 }); 34 35 afterEach(() => { 36 cardRepository.clear(); 37 collectionRepository.clear(); 38 cardQueryRepository.clear(); 39 }); 40 41 describe('Multiple users with same URL', () => { 42 it('should return all users who have cards with the specified URL', async () => { 43 const testUrl = 'https://example.com/shared-article'; 44 const url = URL.create(testUrl).unwrap(); 45 46 // Create URL cards for different users with the same URL 47 const card1 = new CardBuilder() 48 .withCuratorId(curator1.value) 49 .withType(CardTypeEnum.URL) 50 .withUrl(url) 51 .build(); 52 53 const card2 = new CardBuilder() 54 .withCuratorId(curator2.value) 55 .withType(CardTypeEnum.URL) 56 .withUrl(url) 57 .build(); 58 59 const card3 = new CardBuilder() 60 .withCuratorId(curator3.value) 61 .withType(CardTypeEnum.URL) 62 .withUrl(url) 63 .build(); 64 65 if ( 66 card1 instanceof Error || 67 card2 instanceof Error || 68 card3 instanceof Error 69 ) { 70 throw new Error('Failed to create cards'); 71 } 72 73 // Add cards to their respective libraries 74 card1.addToLibrary(curator1); 75 card2.addToLibrary(curator2); 76 card3.addToLibrary(curator3); 77 78 // Save cards 79 await cardRepository.save(card1); 80 await cardRepository.save(card2); 81 await cardRepository.save(card3); 82 83 // Execute the use case 84 const query = { 85 url: testUrl, 86 }; 87 88 const result = await useCase.execute(query); 89 90 // Verify the result 91 expect(result.isOk()).toBe(true); 92 const response = result.unwrap(); 93 94 expect(response.libraries).toHaveLength(3); 95 expect(response.pagination.totalCount).toBe(3); 96 97 // Check that all three users are included 98 const userIds = response.libraries.map((lib) => lib.userId); 99 expect(userIds).toContain(curator1.value); 100 expect(userIds).toContain(curator2.value); 101 expect(userIds).toContain(curator3.value); 102 103 // Check that card IDs are correct 104 const cardIds = response.libraries.map((lib) => lib.cardId); 105 expect(cardIds).toContain(card1.cardId.getStringValue()); 106 expect(cardIds).toContain(card2.cardId.getStringValue()); 107 expect(cardIds).toContain(card3.cardId.getStringValue()); 108 }); 109 110 it('should return empty result when no users have cards with the specified URL', async () => { 111 const testUrl = 'https://example.com/nonexistent-article'; 112 113 const query = { 114 url: testUrl, 115 }; 116 117 const result = await useCase.execute(query); 118 119 expect(result.isOk()).toBe(true); 120 const response = result.unwrap(); 121 122 expect(response.libraries).toHaveLength(0); 123 expect(response.pagination.totalCount).toBe(0); 124 }); 125 126 it('should not return users who have different URLs', async () => { 127 const testUrl1 = 'https://example.com/article1'; 128 const testUrl2 = 'https://example.com/article2'; 129 const url1 = URL.create(testUrl1).unwrap(); 130 const url2 = URL.create(testUrl2).unwrap(); 131 132 // Create cards with different URLs 133 const card1 = new CardBuilder() 134 .withCuratorId(curator1.value) 135 .withType(CardTypeEnum.URL) 136 .withUrl(url1) 137 .build(); 138 139 const card2 = new CardBuilder() 140 .withCuratorId(curator2.value) 141 .withType(CardTypeEnum.URL) 142 .withUrl(url2) 143 .build(); 144 145 if (card1 instanceof Error || card2 instanceof Error) { 146 throw new Error('Failed to create cards'); 147 } 148 149 card1.addToLibrary(curator1); 150 card2.addToLibrary(curator2); 151 152 await cardRepository.save(card1); 153 await cardRepository.save(card2); 154 155 // Query for testUrl1 156 const query = { 157 url: testUrl1, 158 }; 159 160 const result = await useCase.execute(query); 161 162 expect(result.isOk()).toBe(true); 163 const response = result.unwrap(); 164 165 expect(response.libraries).toHaveLength(1); 166 expect(response.libraries[0]!.userId).toBe(curator1.value); 167 expect(response.libraries[0]!.cardId).toBe(card1.cardId.getStringValue()); 168 }); 169 }); 170 171 describe('Pagination', () => { 172 it('should paginate results correctly', async () => { 173 const testUrl = 'https://example.com/popular-article'; 174 const url = URL.create(testUrl).unwrap(); 175 176 // Create 5 cards with the same URL from different users 177 const cards = []; 178 const curators = []; 179 for (let i = 1; i <= 5; i++) { 180 const curator = CuratorId.create(`did:plc:curator${i}`).unwrap(); 181 curators.push(curator); 182 183 const card = new CardBuilder() 184 .withCuratorId(curator.value) 185 .withType(CardTypeEnum.URL) 186 .withUrl(url) 187 .build(); 188 189 if (card instanceof Error) { 190 throw new Error(`Failed to create card ${i}`); 191 } 192 193 card.addToLibrary(curator); 194 cards.push(card); 195 await cardRepository.save(card); 196 } 197 198 // Test first page with limit 2 199 const query1 = { 200 url: testUrl, 201 page: 1, 202 limit: 2, 203 }; 204 205 const result1 = await useCase.execute(query1); 206 expect(result1.isOk()).toBe(true); 207 const response1 = result1.unwrap(); 208 209 expect(response1.libraries).toHaveLength(2); 210 expect(response1.pagination.currentPage).toBe(1); 211 expect(response1.pagination.totalCount).toBe(5); 212 expect(response1.pagination.totalPages).toBe(3); 213 expect(response1.pagination.hasMore).toBe(true); 214 215 // Test second page 216 const query2 = { 217 url: testUrl, 218 page: 2, 219 limit: 2, 220 }; 221 222 const result2 = await useCase.execute(query2); 223 expect(result2.isOk()).toBe(true); 224 const response2 = result2.unwrap(); 225 226 expect(response2.libraries).toHaveLength(2); 227 expect(response2.pagination.currentPage).toBe(2); 228 expect(response2.pagination.hasMore).toBe(true); 229 230 // Test last page 231 const query3 = { 232 url: testUrl, 233 page: 3, 234 limit: 2, 235 }; 236 237 const result3 = await useCase.execute(query3); 238 expect(result3.isOk()).toBe(true); 239 const response3 = result3.unwrap(); 240 241 expect(response3.libraries).toHaveLength(1); 242 expect(response3.pagination.currentPage).toBe(3); 243 expect(response3.pagination.hasMore).toBe(false); 244 }); 245 246 it('should respect limit cap of 100', async () => { 247 const query = { 248 url: 'https://example.com/test', 249 limit: 200, // Should be capped at 100 250 }; 251 252 const result = await useCase.execute(query); 253 expect(result.isOk()).toBe(true); 254 const response = result.unwrap(); 255 256 expect(response.pagination.limit).toBe(100); 257 }); 258 }); 259 260 describe('Sorting', () => { 261 it('should use default sorting parameters', async () => { 262 const testUrl = 'https://example.com/test-article'; 263 264 const query = { 265 url: testUrl, 266 }; 267 268 const result = await useCase.execute(query); 269 expect(result.isOk()).toBe(true); 270 const response = result.unwrap(); 271 272 expect(response.sorting.sortBy).toBe(CardSortField.UPDATED_AT); 273 expect(response.sorting.sortOrder).toBe(SortOrder.DESC); 274 }); 275 276 it('should use provided sorting parameters', async () => { 277 const testUrl = 'https://example.com/test-article'; 278 279 const query = { 280 url: testUrl, 281 sortBy: CardSortField.CREATED_AT, 282 sortOrder: SortOrder.ASC, 283 }; 284 285 const result = await useCase.execute(query); 286 expect(result.isOk()).toBe(true); 287 const response = result.unwrap(); 288 289 expect(response.sorting.sortBy).toBe(CardSortField.CREATED_AT); 290 expect(response.sorting.sortOrder).toBe(SortOrder.ASC); 291 }); 292 }); 293 294 describe('Validation', () => { 295 it('should fail with invalid URL', async () => { 296 const query = { 297 url: 'not-a-valid-url', 298 }; 299 300 const result = await useCase.execute(query); 301 302 expect(result.isErr()).toBe(true); 303 if (result.isErr()) { 304 expect(result.error.message).toContain('Invalid URL'); 305 } 306 }); 307 }); 308 309 describe('Error handling', () => { 310 it('should handle repository errors gracefully', async () => { 311 // Create a mock repository that throws an error 312 const errorCardQueryRepository = { 313 getUrlCardsOfUser: jest.fn(), 314 getCardsInCollection: jest.fn(), 315 getUrlCardView: jest.fn(), 316 getLibrariesForCard: jest.fn(), 317 getLibrariesForUrl: jest 318 .fn() 319 .mockRejectedValue(new Error('Database error')), 320 getNoteCardsForUrl: jest.fn(), 321 }; 322 323 const errorUseCase = new GetLibrariesForUrlUseCase( 324 errorCardQueryRepository, 325 ); 326 327 const query = { 328 url: 'https://example.com/test-url', 329 }; 330 331 const result = await errorUseCase.execute(query); 332 333 expect(result.isErr()).toBe(true); 334 if (result.isErr()) { 335 expect(result.error.message).toContain('Database error'); 336 } 337 }); 338 }); 339});