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