This repository has no description
19 kB
603 lines
1import { GetUrlStatusForMyLibraryUseCase } from '../../application/useCases/queries/GetUrlStatusForMyLibraryUseCase';
2import { InMemoryCardRepository } from '../utils/InMemoryCardRepository';
3import { InMemoryCollectionRepository } from '../utils/InMemoryCollectionRepository';
4import { InMemoryCollectionQueryRepository } from '../utils/InMemoryCollectionQueryRepository';
5import { FakeCardPublisher } from '../utils/FakeCardPublisher';
6import { FakeCollectionPublisher } from '../utils/FakeCollectionPublisher';
7import { FakeEventPublisher } from '../utils/FakeEventPublisher';
8import { FakeProfileService } from '../utils/FakeProfileService';
9import { CuratorId } from '../../domain/value-objects/CuratorId';
10import { CardBuilder } from '../utils/builders/CardBuilder';
11import { CollectionBuilder } from '../utils/builders/CollectionBuilder';
12import { CardTypeEnum } from '../../domain/value-objects/CardType';
13import { PublishedRecordId } from '../../domain/value-objects/PublishedRecordId';
14import { URL } from '../../domain/value-objects/URL';
15import { err } from 'src/shared/core/Result';
16import { ICardRepository } from '../../domain/ICardRepository';
17
18describe('GetUrlStatusForMyLibraryUseCase', () => {
19 let useCase: GetUrlStatusForMyLibraryUseCase;
20 let cardRepository: InMemoryCardRepository;
21 let collectionRepository: InMemoryCollectionRepository;
22 let collectionQueryRepository: InMemoryCollectionQueryRepository;
23 let cardPublisher: FakeCardPublisher;
24 let collectionPublisher: FakeCollectionPublisher;
25 let eventPublisher: FakeEventPublisher;
26 let profileService: FakeProfileService;
27 let curatorId: CuratorId;
28 let otherCuratorId: CuratorId;
29
30 beforeEach(() => {
31 cardRepository = new InMemoryCardRepository();
32 collectionRepository = new InMemoryCollectionRepository();
33 collectionQueryRepository = new InMemoryCollectionQueryRepository(
34 collectionRepository,
35 );
36 cardPublisher = new FakeCardPublisher();
37 collectionPublisher = new FakeCollectionPublisher();
38 eventPublisher = new FakeEventPublisher();
39 profileService = new FakeProfileService();
40
41 useCase = new GetUrlStatusForMyLibraryUseCase(
42 cardRepository,
43 collectionQueryRepository,
44 collectionRepository,
45 profileService,
46 eventPublisher,
47 );
48
49 curatorId = CuratorId.create('did:plc:testcurator').unwrap();
50 otherCuratorId = CuratorId.create('did:plc:othercurator').unwrap();
51
52 // Set up profiles
53 profileService.addProfile({
54 id: curatorId.value,
55 name: 'Test Curator',
56 handle: 'testcurator',
57 avatarUrl: 'https://example.com/avatar1.jpg',
58 bio: 'Test curator bio',
59 });
60
61 profileService.addProfile({
62 id: otherCuratorId.value,
63 name: 'Other Curator',
64 handle: 'othercurator',
65 avatarUrl: 'https://example.com/avatar2.jpg',
66 bio: 'Other curator bio',
67 });
68 });
69
70 afterEach(() => {
71 cardRepository.clear();
72 collectionRepository.clear();
73 collectionQueryRepository.clear();
74 cardPublisher.clear();
75 collectionPublisher.clear();
76 eventPublisher.clear();
77 profileService.clear();
78 });
79
80 describe('URL card in collections', () => {
81 it('should return card ID and collections when user has URL card in multiple collections', async () => {
82 const testUrl = 'https://example.com/test-article';
83
84 // Create a URL card
85 const url = URL.create(testUrl).unwrap();
86 const card = new CardBuilder()
87 .withCuratorId(curatorId.value)
88 .withType(CardTypeEnum.URL)
89 .withUrl(url)
90 .build();
91
92 if (card instanceof Error) {
93 throw new Error(`Failed to create card: ${card.message}`);
94 }
95
96 // Add card to library
97 const addToLibResult = card.addToLibrary(curatorId);
98 if (addToLibResult.isErr()) {
99 throw new Error(
100 `Failed to add card to library: ${addToLibResult.error.message}`,
101 );
102 }
103
104 await cardRepository.save(card);
105
106 // Publish the card to simulate it being published
107 cardPublisher.publishCardToLibrary(card, curatorId);
108
109 // Create first collection
110 const collection1 = new CollectionBuilder()
111 .withAuthorId(curatorId.value)
112 .withName('Tech Articles')
113 .withDescription('Collection of technology articles')
114 .build();
115
116 if (collection1 instanceof Error) {
117 throw new Error(`Failed to create collection1: ${collection1.message}`);
118 }
119
120 // Create second collection
121 const collection2 = new CollectionBuilder()
122 .withAuthorId(curatorId.value)
123 .withName('Reading List')
124 .withDescription('My personal reading list')
125 .build();
126
127 if (collection2 instanceof Error) {
128 throw new Error(`Failed to create collection2: ${collection2.message}`);
129 }
130
131 // Add card to both collections
132 const addToCollection1Result = collection1.addCard(
133 card.cardId,
134 curatorId,
135 );
136 if (addToCollection1Result.isErr()) {
137 throw new Error(
138 `Failed to add card to collection1: ${addToCollection1Result.error.message}`,
139 );
140 }
141
142 const addToCollection2Result = collection2.addCard(
143 card.cardId,
144 curatorId,
145 );
146 if (addToCollection2Result.isErr()) {
147 throw new Error(
148 `Failed to add card to collection2: ${addToCollection2Result.error.message}`,
149 );
150 }
151
152 // Mark collections as published
153 const collection1PublishedRecordId = PublishedRecordId.create({
154 uri: 'at://did:plc:testcurator/network.cosmik.collection/collection1',
155 cid: 'bafyreicollection1cid',
156 });
157
158 const collection2PublishedRecordId = PublishedRecordId.create({
159 uri: 'at://did:plc:testcurator/network.cosmik.collection/collection2',
160 cid: 'bafyreicollection2cid',
161 });
162
163 collection1.markAsPublished(collection1PublishedRecordId);
164 collection2.markAsPublished(collection2PublishedRecordId);
165
166 // Mark card links as published in collections
167 const cardLinkPublishedRecordId1 = PublishedRecordId.create({
168 uri: 'at://did:plc:testcurator/network.cosmik.collection/collection1/link1',
169 cid: 'bafyreilink1cid',
170 });
171
172 const cardLinkPublishedRecordId2 = PublishedRecordId.create({
173 uri: 'at://did:plc:testcurator/network.cosmik.collection/collection2/link2',
174 cid: 'bafyreilink2cid',
175 });
176
177 collection1.markCardLinkAsPublished(
178 card.cardId,
179 cardLinkPublishedRecordId1,
180 );
181 collection2.markCardLinkAsPublished(
182 card.cardId,
183 cardLinkPublishedRecordId2,
184 );
185
186 // Save collections
187 await collectionRepository.save(collection1);
188 await collectionRepository.save(collection2);
189
190 // Publish collections and links
191 collectionPublisher.publish(collection1);
192 collectionPublisher.publish(collection2);
193 collectionPublisher.publishCardAddedToCollection(
194 card,
195 collection1,
196 curatorId,
197 );
198 collectionPublisher.publishCardAddedToCollection(
199 card,
200 collection2,
201 curatorId,
202 );
203
204 // Execute the use case
205 const query = {
206 url: testUrl,
207 curatorId: curatorId.value,
208 };
209
210 const result = await useCase.execute(query);
211
212 // Verify the result
213 expect(result.isOk()).toBe(true);
214 const response = result.unwrap();
215
216 expect(response.cardId).toBe(card.cardId.getStringValue());
217 expect(response.collections).toHaveLength(2);
218
219 // Verify collection details
220 const techArticlesCollection = response.collections?.find(
221 (c) => c.name === 'Tech Articles',
222 );
223 const readingListCollection = response.collections?.find(
224 (c) => c.name === 'Reading List',
225 );
226
227 expect(techArticlesCollection).toBeDefined();
228 expect(techArticlesCollection?.id).toBe(
229 collection1.collectionId.getStringValue(),
230 );
231 expect(techArticlesCollection?.uri).toBe(
232 'at://did:plc:testcurator/network.cosmik.collection/collection1',
233 );
234 expect(techArticlesCollection?.name).toBe('Tech Articles');
235 expect(techArticlesCollection?.description).toBe(
236 'Collection of technology articles',
237 );
238
239 expect(readingListCollection).toBeDefined();
240 expect(readingListCollection?.id).toBe(
241 collection2.collectionId.getStringValue(),
242 );
243 expect(readingListCollection?.uri).toBe(
244 'at://did:plc:testcurator/network.cosmik.collection/collection2',
245 );
246 expect(readingListCollection?.name).toBe('Reading List');
247 expect(readingListCollection?.description).toBe(
248 'My personal reading list',
249 );
250 });
251
252 it('should return card ID and empty collections when user has URL card but not in any collections', async () => {
253 const testUrl = 'https://example.com/standalone-article';
254
255 // Create a URL card
256 const url = URL.create(testUrl).unwrap();
257 const card = new CardBuilder()
258 .withCuratorId(curatorId.value)
259 .withType(CardTypeEnum.URL)
260 .withUrl(url)
261 .build();
262
263 if (card instanceof Error) {
264 throw new Error(`Failed to create card: ${card.message}`);
265 }
266
267 // Add card to library
268 const addToLibResult = card.addToLibrary(curatorId);
269 if (addToLibResult.isErr()) {
270 throw new Error(
271 `Failed to add card to library: ${addToLibResult.error.message}`,
272 );
273 }
274
275 await cardRepository.save(card);
276
277 // Publish the card
278 cardPublisher.publishCardToLibrary(card, curatorId);
279
280 // Execute the use case
281 const query = {
282 url: testUrl,
283 curatorId: curatorId.value,
284 };
285
286 const result = await useCase.execute(query);
287
288 // Verify the result
289 expect(result.isOk()).toBe(true);
290 const response = result.unwrap();
291
292 expect(response.cardId).toBe(card.cardId.getStringValue());
293 expect(response.collections).toHaveLength(0);
294 });
295
296 it('should return empty result when user does not have URL card for the URL', async () => {
297 const testUrl = 'https://example.com/nonexistent-article';
298
299 // Execute the use case without creating any cards
300 const query = {
301 url: testUrl,
302 curatorId: curatorId.value,
303 };
304
305 const result = await useCase.execute(query);
306
307 // Verify the result
308 expect(result.isOk()).toBe(true);
309 const response = result.unwrap();
310
311 expect(response.cardId).toBeUndefined();
312 expect(response.collections).toBeUndefined();
313 });
314
315 it('should not return collections from other users even if they have the same URL', async () => {
316 const testUrl = 'https://example.com/shared-article';
317
318 // Create URL card for first user
319 const url = URL.create(testUrl).unwrap();
320 const card1 = new CardBuilder()
321 .withCuratorId(curatorId.value)
322 .withType(CardTypeEnum.URL)
323 .withUrl(url)
324 .build();
325
326 if (card1 instanceof Error) {
327 throw new Error(`Failed to create card1: ${card1.message}`);
328 }
329
330 const addToLibResult1 = card1.addToLibrary(curatorId);
331 if (addToLibResult1.isErr()) {
332 throw new Error(
333 `Failed to add card1 to library: ${addToLibResult1.error.message}`,
334 );
335 }
336
337 await cardRepository.save(card1);
338
339 // Create URL card for second user (different card, same URL)
340 const card2 = new CardBuilder()
341 .withCuratorId(otherCuratorId.value)
342 .withType(CardTypeEnum.URL)
343 .withUrl(url)
344 .build();
345
346 if (card2 instanceof Error) {
347 throw new Error(`Failed to create card2: ${card2.message}`);
348 }
349
350 const addToLibResult2 = card2.addToLibrary(otherCuratorId);
351 if (addToLibResult2.isErr()) {
352 throw new Error(
353 `Failed to add card2 to library: ${addToLibResult2.error.message}`,
354 );
355 }
356
357 await cardRepository.save(card2);
358
359 // Create collection for second user and add their card
360 const otherUserCollection = new CollectionBuilder()
361 .withAuthorId(otherCuratorId.value)
362 .withName('Other User Collection')
363 .build();
364
365 if (otherUserCollection instanceof Error) {
366 throw new Error(
367 `Failed to create other user collection: ${otherUserCollection.message}`,
368 );
369 }
370
371 const addToOtherCollectionResult = otherUserCollection.addCard(
372 card2.cardId,
373 otherCuratorId,
374 );
375 if (addToOtherCollectionResult.isErr()) {
376 throw new Error(
377 `Failed to add card2 to other collection: ${addToOtherCollectionResult.error.message}`,
378 );
379 }
380
381 await collectionRepository.save(otherUserCollection);
382
383 // Execute the use case for first user
384 const query = {
385 url: testUrl,
386 curatorId: curatorId.value,
387 };
388
389 const result = await useCase.execute(query);
390
391 // Verify the result - should only see first user's card, no collections
392 expect(result.isOk()).toBe(true);
393 const response = result.unwrap();
394
395 expect(response.cardId).toBe(card1.cardId.getStringValue());
396 expect(response.collections).toHaveLength(0); // No collections for first user
397 });
398
399 it('should only return collections owned by the requesting user', async () => {
400 const testUrl = 'https://example.com/multi-user-article';
401
402 // Create URL card for the user
403 const url = URL.create(testUrl).unwrap();
404 const card = new CardBuilder()
405 .withCuratorId(curatorId.value)
406 .withType(CardTypeEnum.URL)
407 .withUrl(url)
408 .build();
409
410 if (card instanceof Error) {
411 throw new Error(`Failed to create card: ${card.message}`);
412 }
413
414 const addToLibResult = card.addToLibrary(curatorId);
415 if (addToLibResult.isErr()) {
416 throw new Error(
417 `Failed to add card to library: ${addToLibResult.error.message}`,
418 );
419 }
420
421 await cardRepository.save(card);
422
423 // Create user's own collection
424 const userCollection = new CollectionBuilder()
425 .withAuthorId(curatorId.value)
426 .withName('My Collection')
427 .build();
428
429 if (userCollection instanceof Error) {
430 throw new Error(
431 `Failed to create user collection: ${userCollection.message}`,
432 );
433 }
434
435 const addToUserCollectionResult = userCollection.addCard(
436 card.cardId,
437 curatorId,
438 );
439 if (addToUserCollectionResult.isErr()) {
440 throw new Error(
441 `Failed to add card to user collection: ${addToUserCollectionResult.error.message}`,
442 );
443 }
444
445 await collectionRepository.save(userCollection);
446
447 // Create another user's collection (this should not appear in results)
448 const otherUserCollection = new CollectionBuilder()
449 .withAuthorId(otherCuratorId.value)
450 .withName('Other User Collection')
451 .build();
452
453 if (otherUserCollection instanceof Error) {
454 throw new Error(
455 `Failed to create other user collection: ${otherUserCollection.message}`,
456 );
457 }
458
459 // Note: We don't add the card to the other user's collection since they can't add
460 // another user's card to their collection in this domain model
461
462 await collectionRepository.save(otherUserCollection);
463
464 // Execute the use case
465 const query = {
466 url: testUrl,
467 curatorId: curatorId.value,
468 };
469
470 const result = await useCase.execute(query);
471
472 // Verify the result - should only see user's own collection
473 expect(result.isOk()).toBe(true);
474 const response = result.unwrap();
475
476 expect(response.cardId).toBe(card.cardId.getStringValue());
477 expect(response.collections).toHaveLength(1);
478 expect(response.collections?.[0]?.name).toBe('My Collection');
479 expect(response.collections?.[0]?.id).toBe(
480 userCollection.collectionId.getStringValue(),
481 );
482 });
483 });
484
485 describe('Validation', () => {
486 it('should fail with invalid URL', async () => {
487 const query = {
488 url: 'not-a-valid-url',
489 curatorId: curatorId.value,
490 };
491
492 const result = await useCase.execute(query);
493
494 expect(result.isErr()).toBe(true);
495 if (result.isErr()) {
496 expect(result.error.message).toContain('Invalid URL');
497 }
498 });
499
500 it('should fail with invalid curator ID', async () => {
501 const query = {
502 url: 'https://example.com/valid-url',
503 curatorId: 'invalid-curator-id',
504 };
505
506 const result = await useCase.execute(query);
507
508 expect(result.isErr()).toBe(true);
509 if (result.isErr()) {
510 expect(result.error.message).toContain('Invalid curator ID');
511 }
512 });
513 });
514
515 describe('Error handling', () => {
516 it('should handle repository errors gracefully', async () => {
517 // Create a mock repository that returns an error Result
518 const errorCardRepository: ICardRepository = {
519 findUsersUrlCardByUrl: jest
520 .fn()
521 .mockResolvedValue(err(new Error('Database error'))),
522 save: jest.fn(),
523 findById: jest.fn(),
524 delete: jest.fn(),
525 findUsersNoteCardByUrl: jest.fn(),
526 };
527
528 const errorUseCase = new GetUrlStatusForMyLibraryUseCase(
529 errorCardRepository,
530 collectionQueryRepository,
531 collectionRepository,
532 profileService,
533 eventPublisher,
534 );
535
536 const query = {
537 url: 'https://example.com/test-url',
538 curatorId: curatorId.value,
539 };
540
541 const result = await errorUseCase.execute(query);
542
543 expect(result.isErr()).toBe(true);
544 if (result.isErr()) {
545 expect(result.error.message).toContain('Database error');
546 }
547 });
548
549 it('should handle collection query repository errors gracefully', async () => {
550 const testUrl = 'https://example.com/error-test';
551
552 // Create a URL card
553 const url = URL.create(testUrl).unwrap();
554 const card = new CardBuilder()
555 .withCuratorId(curatorId.value)
556 .withType(CardTypeEnum.URL)
557 .withUrl(url)
558 .build();
559
560 if (card instanceof Error) {
561 throw new Error(`Failed to create card: ${card.message}`);
562 }
563
564 const addToLibResult = card.addToLibrary(curatorId);
565 if (addToLibResult.isErr()) {
566 throw new Error(
567 `Failed to add card to library: ${addToLibResult.error.message}`,
568 );
569 }
570
571 await cardRepository.save(card);
572
573 // Create a mock collection query repository that throws an error
574 const errorCollectionQueryRepository = {
575 findByCreator: jest.fn(),
576 getCollectionsContainingCardForUser: jest
577 .fn()
578 .mockRejectedValue(new Error('Collection query error')),
579 getCollectionsWithUrl: jest.fn(),
580 };
581
582 const errorUseCase = new GetUrlStatusForMyLibraryUseCase(
583 cardRepository,
584 errorCollectionQueryRepository,
585 collectionRepository,
586 profileService,
587 eventPublisher,
588 );
589
590 const query = {
591 url: testUrl,
592 curatorId: curatorId.value,
593 };
594
595 const result = await errorUseCase.execute(query);
596
597 expect(result.isErr()).toBe(true);
598 if (result.isErr()) {
599 expect(result.error.message).toContain('Collection query error');
600 }
601 });
602 });
603});