A social knowledge tool for researchers built on ATProto
1import { AddCardToLibraryUseCase } from '../../application/useCases/commands/AddCardToLibraryUseCase';
2import { InMemoryCardRepository } from '../utils/InMemoryCardRepository';
3import { InMemoryCollectionRepository } from '../utils/InMemoryCollectionRepository';
4import { FakeCardPublisher } from '../utils/FakeCardPublisher';
5import { FakeCollectionPublisher } from '../utils/FakeCollectionPublisher';
6import { CardLibraryService } from '../../domain/services/CardLibraryService';
7import { CardCollectionService } from '../../domain/services/CardCollectionService';
8import { CuratorId } from '../../domain/value-objects/CuratorId';
9import { CollectionBuilder } from '../utils/builders/CollectionBuilder';
10import { CardBuilder } from '../utils/builders/CardBuilder';
11import { CardTypeEnum } from '../../domain/value-objects/CardType';
12import { CARD_ERROR_MESSAGES } from '../../domain/Card';
13
14describe('AddCardToLibraryUseCase', () => {
15 let useCase: AddCardToLibraryUseCase;
16 let cardRepository: InMemoryCardRepository;
17 let collectionRepository: InMemoryCollectionRepository;
18 let cardPublisher: FakeCardPublisher;
19 let collectionPublisher: FakeCollectionPublisher;
20 let cardLibraryService: CardLibraryService;
21 let cardCollectionService: CardCollectionService;
22 let curatorId: CuratorId;
23 let curatorId2: CuratorId;
24
25 beforeEach(() => {
26 cardRepository = new InMemoryCardRepository();
27 collectionRepository = new InMemoryCollectionRepository();
28 cardPublisher = new FakeCardPublisher();
29 collectionPublisher = new FakeCollectionPublisher();
30
31 cardLibraryService = new CardLibraryService(
32 cardRepository,
33 cardPublisher,
34 collectionRepository,
35 cardCollectionService,
36 );
37 cardCollectionService = new CardCollectionService(
38 collectionRepository,
39 collectionPublisher,
40 );
41
42 useCase = new AddCardToLibraryUseCase(
43 cardRepository,
44 cardLibraryService,
45 cardCollectionService,
46 );
47
48 curatorId = CuratorId.create('did:plc:testcurator').unwrap();
49 curatorId2 = CuratorId.create('did:plc:testcurator2').unwrap();
50 });
51
52 afterEach(() => {
53 cardRepository.clear();
54 collectionRepository.clear();
55 cardPublisher.clear();
56 collectionPublisher.clear();
57 });
58
59 describe('Basic card addition to library', () => {
60 it('should not allow adding an existing url card to library', async () => {
61 // Create and save a card first
62 const card = new CardBuilder()
63 .withCuratorId(curatorId.value)
64 .withType(CardTypeEnum.URL)
65 .build();
66
67 if (card instanceof Error) {
68 throw new Error(`Failed to create card: ${card.message}`);
69 }
70
71 const addToLibResult = card.addToLibrary(curatorId);
72 if (addToLibResult.isErr()) {
73 throw new Error(
74 `Failed to add card to library: ${addToLibResult.error.message}`,
75 );
76 }
77
78 await cardRepository.save(card);
79
80 const request = {
81 cardId: card.cardId.getStringValue(),
82 curatorId: curatorId2.value,
83 };
84
85 const result = await useCase.execute(request);
86
87 if (result.isOk()) {
88 throw new Error('Expected use case to fail, but it succeeded');
89 }
90 expect(result.isErr()).toBe(true);
91 expect(result.error.message).toContain(
92 CARD_ERROR_MESSAGES.URL_CARD_SINGLE_LIBRARY_ONLY,
93 );
94 });
95
96 it('should fail when card does not exist', async () => {
97 const request = {
98 cardId: 'non-existent-card-id',
99 curatorId: curatorId.value,
100 };
101
102 const result = await useCase.execute(request);
103
104 expect(result.isErr()).toBe(true);
105 if (result.isErr()) {
106 expect(result.error.message).toContain('Card not found');
107 }
108 });
109 });
110
111 describe('Collection handling', () => {
112 it('should add card to specified collections', async () => {
113 // Create and save a card first
114 const card = new CardBuilder()
115 .withCuratorId(curatorId.value)
116 .withType(CardTypeEnum.URL)
117 .build();
118
119 if (card instanceof Error) {
120 throw new Error(`Failed to create card: ${card.message}`);
121 }
122
123 await cardRepository.save(card);
124
125 // Create a test collection
126 const collection = new CollectionBuilder()
127 .withAuthorId(curatorId.value)
128 .withName('Test Collection')
129 .build();
130
131 if (collection instanceof Error) {
132 throw new Error(`Failed to create collection: ${collection.message}`);
133 }
134
135 await collectionRepository.save(collection);
136
137 const request = {
138 cardId: card.cardId.getStringValue(),
139 collectionIds: [collection.collectionId.getStringValue()],
140 curatorId: curatorId.value,
141 };
142
143 const result = await useCase.execute(request);
144
145 expect(result.isOk()).toBe(true);
146
147 // Verify card was published to library
148 const publishedCards = cardPublisher.getPublishedCards();
149 expect(publishedCards).toHaveLength(1);
150
151 // Verify collection link was published
152 const publishedLinks = collectionPublisher.getPublishedLinksForCollection(
153 collection.collectionId.getStringValue(),
154 );
155 expect(publishedLinks).toHaveLength(1);
156 expect(publishedLinks[0]?.cardId).toBe(card.cardId.getStringValue());
157 });
158
159 it('should add card to multiple collections', async () => {
160 // Create and save a card first
161 const card = new CardBuilder()
162 .withCuratorId(curatorId.value)
163 .withType(CardTypeEnum.NOTE)
164 .build();
165
166 if (card instanceof Error) {
167 throw new Error(`Failed to create card: ${card.message}`);
168 }
169
170 await cardRepository.save(card);
171
172 // Create test collections
173 const collection1 = new CollectionBuilder()
174 .withAuthorId(curatorId.value)
175 .withName('Test Collection 1')
176 .build();
177
178 const collection2 = new CollectionBuilder()
179 .withAuthorId(curatorId.value)
180 .withName('Test Collection 2')
181 .build();
182
183 if (collection1 instanceof Error || collection2 instanceof Error) {
184 throw new Error('Failed to create collections');
185 }
186
187 await collectionRepository.save(collection1);
188 await collectionRepository.save(collection2);
189
190 const request = {
191 cardId: card.cardId.getStringValue(),
192 collectionIds: [
193 collection1.collectionId.getStringValue(),
194 collection2.collectionId.getStringValue(),
195 ],
196 curatorId: curatorId.value,
197 };
198
199 const result = await useCase.execute(request);
200
201 expect(result.isOk()).toBe(true);
202
203 // Verify card was published to library
204 const publishedCards = cardPublisher.getPublishedCards();
205 expect(publishedCards).toHaveLength(1);
206
207 // Verify collection links were published for both collections
208 const publishedLinks1 =
209 collectionPublisher.getPublishedLinksForCollection(
210 collection1.collectionId.getStringValue(),
211 );
212 const publishedLinks2 =
213 collectionPublisher.getPublishedLinksForCollection(
214 collection2.collectionId.getStringValue(),
215 );
216
217 expect(publishedLinks1).toHaveLength(1);
218 expect(publishedLinks2).toHaveLength(1);
219 expect(publishedLinks1[0]?.cardId).toBe(card.cardId.getStringValue());
220 expect(publishedLinks2[0]?.cardId).toBe(card.cardId.getStringValue());
221 });
222
223 it('should work without collections when none are specified', async () => {
224 // Create and save a card first
225 const card = new CardBuilder()
226 .withCuratorId(curatorId.value)
227 .withType(CardTypeEnum.URL)
228 .build();
229
230 if (card instanceof Error) {
231 throw new Error(`Failed to create card: ${card.message}`);
232 }
233
234 await cardRepository.save(card);
235
236 const request = {
237 cardId: card.cardId.getStringValue(),
238 curatorId: curatorId.value,
239 // No collectionIds specified
240 };
241
242 const result = await useCase.execute(request);
243
244 expect(result.isOk()).toBe(true);
245
246 // Verify card was published to library
247 const publishedCards = cardPublisher.getPublishedCards();
248 expect(publishedCards).toHaveLength(1);
249
250 // Verify no collection links were published
251 const allPublishedLinks = collectionPublisher.getAllPublishedLinks();
252 expect(allPublishedLinks).toHaveLength(0);
253 });
254
255 it('should fail when collection does not exist', async () => {
256 // Create and save a card first
257 const card = new CardBuilder()
258 .withCuratorId(curatorId.value)
259 .withType(CardTypeEnum.URL)
260 .build();
261
262 if (card instanceof Error) {
263 throw new Error(`Failed to create card: ${card.message}`);
264 }
265
266 await cardRepository.save(card);
267
268 const request = {
269 cardId: card.cardId.getStringValue(),
270 collectionIds: ['non-existent-collection-id'],
271 curatorId: curatorId.value,
272 };
273
274 const result = await useCase.execute(request);
275
276 expect(result.isErr()).toBe(true);
277 if (result.isErr()) {
278 expect(result.error.message).toContain('Collection not found');
279 }
280 });
281 });
282
283 describe('Validation', () => {
284 it('should fail with invalid card ID', async () => {
285 const request = {
286 cardId: 'invalid-card-id',
287 curatorId: curatorId.value,
288 };
289
290 const result = await useCase.execute(request);
291
292 expect(result.isErr()).toBe(true);
293 if (result.isErr()) {
294 expect(result.error.message).toContain('invalid-card-id');
295 }
296 });
297
298 it('should fail with invalid curator ID', async () => {
299 // Create and save a card first
300 const card = new CardBuilder()
301 .withCuratorId(curatorId.value)
302 .withType(CardTypeEnum.URL)
303 .build();
304
305 if (card instanceof Error) {
306 throw new Error(`Failed to create card: ${card.message}`);
307 }
308
309 await cardRepository.save(card);
310
311 const request = {
312 cardId: card.cardId.getStringValue(),
313 curatorId: 'invalid-curator-id',
314 };
315
316 const result = await useCase.execute(request);
317
318 expect(result.isErr()).toBe(true);
319 if (result.isErr()) {
320 expect(result.error.message).toContain('Invalid curator ID');
321 }
322 });
323
324 it('should fail with invalid collection ID', async () => {
325 // Create and save a card first
326 const card = new CardBuilder()
327 .withCuratorId(curatorId.value)
328 .withType(CardTypeEnum.URL)
329 .build();
330
331 if (card instanceof Error) {
332 throw new Error(`Failed to create card: ${card.message}`);
333 }
334
335 await cardRepository.save(card);
336
337 const request = {
338 cardId: card.cardId.getStringValue(),
339 collectionIds: ['invalid-collection-id'],
340 curatorId: curatorId.value,
341 };
342
343 const result = await useCase.execute(request);
344
345 expect(result.isErr()).toBe(true);
346 if (result.isErr()) {
347 expect(result.error.message).toContain('invalid-collection-id');
348 }
349 });
350 });
351});