This repository has no description
14 kB
442 lines
1import { UpdateUrlCardAssociationsUseCase } from '../../application/useCases/commands/UpdateUrlCardAssociationsUseCase';
2import { AddUrlToLibraryUseCase } from '../../application/useCases/commands/AddUrlToLibraryUseCase';
3import { InMemoryCardRepository } from '../utils/InMemoryCardRepository';
4import { InMemoryCollectionRepository } from '../utils/InMemoryCollectionRepository';
5import { FakeCardPublisher } from '../utils/FakeCardPublisher';
6import { FakeCollectionPublisher } from '../utils/FakeCollectionPublisher';
7import { FakeMetadataService } from '../utils/FakeMetadataService';
8import { CardLibraryService } from '../../domain/services/CardLibraryService';
9import { CardCollectionService } from '../../domain/services/CardCollectionService';
10import { CuratorId } from '../../domain/value-objects/CuratorId';
11import { CollectionBuilder } from '../utils/builders/CollectionBuilder';
12import { CardTypeEnum } from '../../domain/value-objects/CardType';
13import { FakeEventPublisher } from '../utils/FakeEventPublisher';
14
15describe('UpdateUrlCardAssociationsUseCase', () => {
16 let useCase: UpdateUrlCardAssociationsUseCase;
17 let addUrlToLibraryUseCase: AddUrlToLibraryUseCase;
18 let cardRepository: InMemoryCardRepository;
19 let collectionRepository: InMemoryCollectionRepository;
20 let cardPublisher: FakeCardPublisher;
21 let collectionPublisher: FakeCollectionPublisher;
22 let metadataService: FakeMetadataService;
23 let cardLibraryService: CardLibraryService;
24 let cardCollectionService: CardCollectionService;
25 let eventPublisher: FakeEventPublisher;
26 let curatorId: CuratorId;
27
28 beforeEach(() => {
29 cardRepository = new InMemoryCardRepository();
30 collectionRepository = new InMemoryCollectionRepository();
31 cardPublisher = new FakeCardPublisher();
32 collectionPublisher = new FakeCollectionPublisher();
33 metadataService = new FakeMetadataService();
34 eventPublisher = new FakeEventPublisher();
35
36 cardLibraryService = new CardLibraryService(
37 cardRepository,
38 cardPublisher,
39 collectionRepository,
40 cardCollectionService,
41 );
42 cardCollectionService = new CardCollectionService(
43 collectionRepository,
44 collectionPublisher,
45 );
46
47 useCase = new UpdateUrlCardAssociationsUseCase(
48 cardRepository,
49 cardLibraryService,
50 cardCollectionService,
51 eventPublisher,
52 );
53
54 addUrlToLibraryUseCase = new AddUrlToLibraryUseCase(
55 cardRepository,
56 metadataService,
57 cardLibraryService,
58 cardCollectionService,
59 eventPublisher,
60 );
61
62 curatorId = CuratorId.create('did:plc:testcurator').unwrap();
63 });
64
65 afterEach(() => {
66 cardRepository.clear();
67 collectionRepository.clear();
68 cardPublisher.clear();
69 collectionPublisher.clear();
70 metadataService.clear();
71 eventPublisher.clear();
72 });
73
74 describe('Note management', () => {
75 it('should create a note for an existing URL card', async () => {
76 const url = 'https://example.com/article';
77
78 // First, add the URL to the library
79 const addResult = await addUrlToLibraryUseCase.execute({
80 url,
81 curatorId: curatorId.value,
82 });
83 expect(addResult.isOk()).toBe(true);
84 const urlCardId = addResult.unwrap().urlCardId;
85
86 // Now create a note for it
87 const updateRequest = {
88 cardId: urlCardId,
89 curatorId: curatorId.value,
90 note: 'This is my note',
91 };
92
93 const result = await useCase.execute(updateRequest);
94
95 expect(result.isOk()).toBe(true);
96 const response = result.unwrap();
97 expect(response.noteCardId).toBeDefined();
98
99 // Verify note card was created
100 const savedCards = cardRepository.getAllCards();
101 const noteCard = savedCards.find(
102 (card) => card.content.type === CardTypeEnum.NOTE,
103 );
104 expect(noteCard).toBeDefined();
105 expect(noteCard?.content.noteContent?.text).toBe('This is my note');
106 });
107
108 it('should update an existing note for a URL card', async () => {
109 const url = 'https://example.com/article';
110
111 // First, add the URL with a note
112 const addResult = await addUrlToLibraryUseCase.execute({
113 url,
114 note: 'Original note',
115 curatorId: curatorId.value,
116 });
117 expect(addResult.isOk()).toBe(true);
118 const addResponse = addResult.unwrap();
119
120 // Now update the note
121 const updateRequest = {
122 cardId: addResponse.urlCardId,
123 curatorId: curatorId.value,
124 note: 'Updated note',
125 };
126
127 const result = await useCase.execute(updateRequest);
128
129 expect(result.isOk()).toBe(true);
130 const response = result.unwrap();
131 expect(response.noteCardId).toBe(addResponse.noteCardId);
132
133 // Verify note was updated
134 const savedCards = cardRepository.getAllCards();
135 const noteCard = savedCards.find(
136 (card) => card.content.type === CardTypeEnum.NOTE,
137 );
138 expect(noteCard).toBeDefined();
139 expect(noteCard?.content.noteContent?.text).toBe('Updated note');
140 });
141
142 it('should fail if URL card does not exist', async () => {
143 const request = {
144 cardId: 'nonexistent-card-id',
145 curatorId: curatorId.value,
146 note: 'This should fail',
147 };
148
149 const result = await useCase.execute(request);
150
151 expect(result.isErr()).toBe(true);
152 if (result.isErr()) {
153 expect(result.error.message).toContain(
154 'URL card not found. Please add the URL to your library first.',
155 );
156 }
157 });
158 });
159
160 describe('Collection management', () => {
161 it('should add URL card to collections', async () => {
162 const url = 'https://example.com/article';
163
164 // Create collections
165 const collection1 = new CollectionBuilder()
166 .withAuthorId(curatorId.value)
167 .withName('Collection 1')
168 .build();
169 const collection2 = new CollectionBuilder()
170 .withAuthorId(curatorId.value)
171 .withName('Collection 2')
172 .build();
173
174 if (collection1 instanceof Error || collection2 instanceof Error) {
175 throw new Error('Failed to create collections');
176 }
177
178 await collectionRepository.save(collection1);
179 await collectionRepository.save(collection2);
180
181 // Add URL to library
182 const addResult = await addUrlToLibraryUseCase.execute({
183 url,
184 curatorId: curatorId.value,
185 });
186 expect(addResult.isOk()).toBe(true);
187 const urlCardId = addResult.unwrap().urlCardId;
188
189 // Add to collections
190 const updateRequest = {
191 cardId: urlCardId,
192 curatorId: curatorId.value,
193 addToCollections: [
194 collection1.collectionId.getStringValue(),
195 collection2.collectionId.getStringValue(),
196 ],
197 };
198
199 const result = await useCase.execute(updateRequest);
200
201 expect(result.isOk()).toBe(true);
202 const response = result.unwrap();
203 expect(response.addedToCollections).toHaveLength(2);
204 expect(response.addedToCollections).toContain(
205 collection1.collectionId.getStringValue(),
206 );
207 expect(response.addedToCollections).toContain(
208 collection2.collectionId.getStringValue(),
209 );
210 });
211
212 it('should remove URL card from collections', async () => {
213 const url = 'https://example.com/article';
214
215 // Create collection
216 const collection = new CollectionBuilder()
217 .withAuthorId(curatorId.value)
218 .withName('Test Collection')
219 .build();
220
221 if (collection instanceof Error) {
222 throw new Error('Failed to create collection');
223 }
224
225 await collectionRepository.save(collection);
226
227 // Add URL to library and collection
228 const addResult = await addUrlToLibraryUseCase.execute({
229 url,
230 collectionIds: [collection.collectionId.getStringValue()],
231 curatorId: curatorId.value,
232 });
233 expect(addResult.isOk()).toBe(true);
234 const urlCardId = addResult.unwrap().urlCardId;
235
236 // Remove from collection
237 const updateRequest = {
238 cardId: urlCardId,
239 curatorId: curatorId.value,
240 removeFromCollections: [collection.collectionId.getStringValue()],
241 };
242
243 const result = await useCase.execute(updateRequest);
244
245 expect(result.isOk()).toBe(true);
246 const response = result.unwrap();
247 expect(response.removedFromCollections).toHaveLength(1);
248 expect(response.removedFromCollections).toContain(
249 collection.collectionId.getStringValue(),
250 );
251 });
252
253 it('should add and remove from different collections in same request', async () => {
254 const url = 'https://example.com/article';
255
256 // Create collections
257 const collection1 = new CollectionBuilder()
258 .withAuthorId(curatorId.value)
259 .withName('Collection 1')
260 .build();
261 const collection2 = new CollectionBuilder()
262 .withAuthorId(curatorId.value)
263 .withName('Collection 2')
264 .build();
265 const collection3 = new CollectionBuilder()
266 .withAuthorId(curatorId.value)
267 .withName('Collection 3')
268 .build();
269
270 if (
271 collection1 instanceof Error ||
272 collection2 instanceof Error ||
273 collection3 instanceof Error
274 ) {
275 throw new Error('Failed to create collections');
276 }
277
278 await collectionRepository.save(collection1);
279 await collectionRepository.save(collection2);
280 await collectionRepository.save(collection3);
281
282 // Add URL to library with collection1
283 const addResult = await addUrlToLibraryUseCase.execute({
284 url,
285 collectionIds: [collection1.collectionId.getStringValue()],
286 curatorId: curatorId.value,
287 });
288 expect(addResult.isOk()).toBe(true);
289 const urlCardId = addResult.unwrap().urlCardId;
290
291 // Add to collection2 and collection3, remove from collection1
292 const updateRequest = {
293 cardId: urlCardId,
294 curatorId: curatorId.value,
295 addToCollections: [
296 collection2.collectionId.getStringValue(),
297 collection3.collectionId.getStringValue(),
298 ],
299 removeFromCollections: [collection1.collectionId.getStringValue()],
300 };
301
302 const result = await useCase.execute(updateRequest);
303
304 expect(result.isOk()).toBe(true);
305 const response = result.unwrap();
306 expect(response.addedToCollections).toHaveLength(2);
307 expect(response.removedFromCollections).toHaveLength(1);
308 expect(response.addedToCollections).toContain(
309 collection2.collectionId.getStringValue(),
310 );
311 expect(response.addedToCollections).toContain(
312 collection3.collectionId.getStringValue(),
313 );
314 expect(response.removedFromCollections).toContain(
315 collection1.collectionId.getStringValue(),
316 );
317 });
318 });
319
320 describe('Combined operations', () => {
321 it('should update note and manage collections in same request', async () => {
322 const url = 'https://example.com/article';
323
324 // Create collection
325 const collection = new CollectionBuilder()
326 .withAuthorId(curatorId.value)
327 .withName('Test Collection')
328 .build();
329
330 if (collection instanceof Error) {
331 throw new Error('Failed to create collection');
332 }
333
334 await collectionRepository.save(collection);
335
336 // Add URL to library
337 const addResult = await addUrlToLibraryUseCase.execute({
338 url,
339 curatorId: curatorId.value,
340 });
341 expect(addResult.isOk()).toBe(true);
342 const urlCardId = addResult.unwrap().urlCardId;
343
344 // Update note and add to collection
345 const updateRequest = {
346 cardId: urlCardId,
347 curatorId: curatorId.value,
348 note: 'My note about this article',
349 addToCollections: [collection.collectionId.getStringValue()],
350 };
351
352 const result = await useCase.execute(updateRequest);
353
354 expect(result.isOk()).toBe(true);
355 const response = result.unwrap();
356 expect(response.noteCardId).toBeDefined();
357 expect(response.addedToCollections).toHaveLength(1);
358 expect(response.addedToCollections).toContain(
359 collection.collectionId.getStringValue(),
360 );
361
362 // Verify note was created
363 const savedCards = cardRepository.getAllCards();
364 const noteCard = savedCards.find(
365 (card) => card.content.type === CardTypeEnum.NOTE,
366 );
367 expect(noteCard).toBeDefined();
368 expect(noteCard?.content.noteContent?.text).toBe(
369 'My note about this article',
370 );
371 });
372 });
373
374 describe('Validation', () => {
375 it('should fail when card does not exist', async () => {
376 const request = {
377 cardId: 'nonexistent-card-id',
378 curatorId: curatorId.value,
379 note: 'This should fail',
380 };
381
382 const result = await useCase.execute(request);
383
384 expect(result.isErr()).toBe(true);
385 if (result.isErr()) {
386 expect(result.error.message).toContain(
387 'URL card not found. Please add the URL to your library first.',
388 );
389 }
390 });
391
392 it('should fail with invalid curator ID', async () => {
393 const url = 'https://example.com/article';
394
395 // Add URL to library first
396 const addResult = await addUrlToLibraryUseCase.execute({
397 url,
398 curatorId: curatorId.value,
399 });
400 expect(addResult.isOk()).toBe(true);
401 const urlCardId = addResult.unwrap().urlCardId;
402
403 const request = {
404 cardId: urlCardId,
405 curatorId: 'invalid-curator-id',
406 note: 'This should fail',
407 };
408
409 const result = await useCase.execute(request);
410
411 expect(result.isErr()).toBe(true);
412 if (result.isErr()) {
413 expect(result.error.message).toContain('Invalid curator ID');
414 }
415 });
416
417 it('should fail with invalid collection ID', async () => {
418 const url = 'https://example.com/article';
419
420 // Add URL to library
421 const addResult = await addUrlToLibraryUseCase.execute({
422 url,
423 curatorId: curatorId.value,
424 });
425 expect(addResult.isOk()).toBe(true);
426 const urlCardId = addResult.unwrap().urlCardId;
427
428 const request = {
429 cardId: urlCardId,
430 curatorId: curatorId.value,
431 addToCollections: ['invalid-collection-id'],
432 };
433
434 const result = await useCase.execute(request);
435
436 expect(result.isErr()).toBe(true);
437 if (result.isErr()) {
438 expect(result.error.message).toContain('Collection not found');
439 }
440 });
441 });
442});