A social knowledge tool for researchers built on ATProto
1import { DeleteCollectionUseCase } from '../../application/useCases/commands/DeleteCollectionUseCase';
2import { InMemoryCollectionRepository } from '../utils/InMemoryCollectionRepository';
3import { FakeCollectionPublisher } from '../utils/FakeCollectionPublisher';
4import { CuratorId } from '../../domain/value-objects/CuratorId';
5import { CollectionBuilder } from '../utils/builders/CollectionBuilder';
6
7describe('DeleteCollectionUseCase', () => {
8 let useCase: DeleteCollectionUseCase;
9 let collectionRepository: InMemoryCollectionRepository;
10 let collectionPublisher: FakeCollectionPublisher;
11 let curatorId: CuratorId;
12 let otherCuratorId: CuratorId;
13
14 beforeEach(() => {
15 collectionRepository = new InMemoryCollectionRepository();
16 collectionPublisher = new FakeCollectionPublisher();
17 useCase = new DeleteCollectionUseCase(
18 collectionRepository,
19 collectionPublisher,
20 );
21
22 curatorId = CuratorId.create('did:plc:testcurator').unwrap();
23 otherCuratorId = CuratorId.create('did:plc:othercurator').unwrap();
24 });
25
26 afterEach(() => {
27 collectionRepository.clear();
28 collectionPublisher.clear();
29 });
30
31 const createCollection = async (
32 authorId: CuratorId,
33 name: string,
34 published: boolean = true,
35 ) => {
36 const collection = new CollectionBuilder()
37 .withAuthorId(authorId.value)
38 .withName(name)
39 .withPublished(published)
40 .build();
41
42 if (collection instanceof Error) {
43 throw new Error(`Failed to create collection: ${collection.message}`);
44 }
45
46 await collectionRepository.save(collection);
47 return collection;
48 };
49
50 describe('Basic collection deletion', () => {
51 it('should successfully delete a published collection', async () => {
52 const collection = await createCollection(
53 curatorId,
54 'Collection to Delete',
55 true,
56 );
57
58 const request = {
59 collectionId: collection.collectionId.getStringValue(),
60 curatorId: curatorId.value,
61 };
62
63 const result = await useCase.execute(request);
64
65 expect(result.isOk()).toBe(true);
66 const response = result.unwrap();
67 expect(response.collectionId).toBe(
68 collection.collectionId.getStringValue(),
69 );
70
71 // Verify collection was deleted from repository
72 const deletedCollectionResult = await collectionRepository.findById(
73 collection.collectionId,
74 );
75 expect(deletedCollectionResult.unwrap()).toBeNull();
76
77 // Verify collection was unpublished
78 const unpublishedCollections =
79 collectionPublisher.getUnpublishedCollections();
80 expect(unpublishedCollections).toHaveLength(1);
81 expect(unpublishedCollections[0]?.uri).toBe(
82 collection.publishedRecordId?.uri,
83 );
84 });
85
86 it('should successfully delete an unpublished collection', async () => {
87 const collection = await createCollection(
88 curatorId,
89 'Unpublished Collection',
90 false,
91 );
92
93 const request = {
94 collectionId: collection.collectionId.getStringValue(),
95 curatorId: curatorId.value,
96 };
97
98 const result = await useCase.execute(request);
99
100 expect(result.isOk()).toBe(true);
101
102 // Verify collection was deleted from repository
103 const deletedCollectionResult = await collectionRepository.findById(
104 collection.collectionId,
105 );
106 expect(deletedCollectionResult.unwrap()).toBeNull();
107
108 // Verify no unpublish operation was performed
109 const unpublishedCollections =
110 collectionPublisher.getUnpublishedCollections();
111 expect(unpublishedCollections).toHaveLength(0);
112 });
113
114 it('should delete collection without affecting other collections', async () => {
115 const collection1 = await createCollection(curatorId, 'Collection 1');
116 const collection2 = await createCollection(curatorId, 'Collection 2');
117
118 const request = {
119 collectionId: collection1.collectionId.getStringValue(),
120 curatorId: curatorId.value,
121 };
122
123 const result = await useCase.execute(request);
124
125 expect(result.isOk()).toBe(true);
126
127 // Verify only the specified collection was deleted
128 const remainingCollections = collectionRepository.getAllCollections();
129 expect(remainingCollections).toHaveLength(1);
130 expect(remainingCollections[0]!.collectionId.getStringValue()).toBe(
131 collection2.collectionId.getStringValue(),
132 );
133 });
134 });
135
136 describe('Authorization', () => {
137 it("should fail when trying to delete another user's collection", async () => {
138 const collection = await createCollection(
139 curatorId,
140 "Someone Else's Collection",
141 );
142
143 const request = {
144 collectionId: collection.collectionId.getStringValue(),
145 curatorId: otherCuratorId.value,
146 };
147
148 const result = await useCase.execute(request);
149
150 expect(result.isErr()).toBe(true);
151 if (result.isErr()) {
152 expect(result.error.message).toContain(
153 'Only the collection author can delete the collection',
154 );
155 }
156
157 // Verify collection was not deleted
158 const existingCollectionResult = await collectionRepository.findById(
159 collection.collectionId,
160 );
161 expect(existingCollectionResult.unwrap()).not.toBeNull();
162 });
163
164 it('should allow author to delete their own collection', async () => {
165 const collection = await createCollection(curatorId, 'My Collection');
166
167 const request = {
168 collectionId: collection.collectionId.getStringValue(),
169 curatorId: curatorId.value,
170 };
171
172 const result = await useCase.execute(request);
173
174 expect(result.isOk()).toBe(true);
175
176 // Verify collection was deleted
177 const deletedCollectionResult = await collectionRepository.findById(
178 collection.collectionId,
179 );
180 expect(deletedCollectionResult.unwrap()).toBeNull();
181 });
182 });
183
184 describe('Validation', () => {
185 it('should fail with invalid collection ID', async () => {
186 const request = {
187 collectionId: 'invalid-collection-id',
188 curatorId: curatorId.value,
189 };
190
191 const result = await useCase.execute(request);
192
193 expect(result.isErr()).toBe(true);
194 if (result.isErr()) {
195 expect(result.error.message).toContain('Collection not found');
196 }
197 });
198
199 it('should fail with invalid curator ID', async () => {
200 const collection = await createCollection(curatorId, 'Test Collection');
201
202 const request = {
203 collectionId: collection.collectionId.getStringValue(),
204 curatorId: 'invalid-curator-id',
205 };
206
207 const result = await useCase.execute(request);
208
209 expect(result.isErr()).toBe(true);
210 if (result.isErr()) {
211 expect(result.error.message).toContain('Invalid curator ID');
212 }
213 });
214
215 it('should fail when collection does not exist', async () => {
216 const request = {
217 collectionId: 'non-existent-collection-id',
218 curatorId: curatorId.value,
219 };
220
221 const result = await useCase.execute(request);
222
223 expect(result.isErr()).toBe(true);
224 if (result.isErr()) {
225 expect(result.error.message).toContain('Collection not found');
226 }
227 });
228 });
229
230 describe('Publishing integration', () => {
231 it('should unpublish collection before deletion', async () => {
232 const collection = await createCollection(
233 curatorId,
234 'Published Collection',
235 true,
236 );
237 const initialUnpublishCount =
238 collectionPublisher.getUnpublishedCollections().length;
239
240 const request = {
241 collectionId: collection.collectionId.getStringValue(),
242 curatorId: curatorId.value,
243 };
244
245 const result = await useCase.execute(request);
246
247 expect(result.isOk()).toBe(true);
248
249 // Verify unpublish operation occurred
250 const finalUnpublishCount =
251 collectionPublisher.getUnpublishedCollections().length;
252 expect(finalUnpublishCount).toBe(initialUnpublishCount + 1);
253
254 // Verify the correct collection was unpublished
255 const unpublishedCollections =
256 collectionPublisher.getUnpublishedCollections();
257 const unpublishedCollection = unpublishedCollections.find(
258 (uc) => uc.uri === collection.publishedRecordId?.uri,
259 );
260 expect(unpublishedCollection).toBeDefined();
261 });
262
263 it('should handle unpublish failure gracefully', async () => {
264 const collection = await createCollection(
265 curatorId,
266 'Collection with Unpublish Failure',
267 true,
268 );
269
270 // Configure publisher to fail unpublish
271 collectionPublisher.setShouldFailUnpublish(true);
272
273 const request = {
274 collectionId: collection.collectionId.getStringValue(),
275 curatorId: curatorId.value,
276 };
277
278 const result = await useCase.execute(request);
279
280 expect(result.isErr()).toBe(true);
281 if (result.isErr()) {
282 expect(result.error.message).toContain(
283 'Failed to unpublish collection',
284 );
285 }
286
287 // Verify collection was not deleted if unpublish failed
288 const existingCollectionResult = await collectionRepository.findById(
289 collection.collectionId,
290 );
291 expect(existingCollectionResult.unwrap()).not.toBeNull();
292 });
293
294 it('should not attempt to unpublish if collection was never published', async () => {
295 const collection = await createCollection(
296 curatorId,
297 'Never Published Collection',
298 false,
299 );
300 const initialUnpublishCount =
301 collectionPublisher.getUnpublishedCollections().length;
302
303 const request = {
304 collectionId: collection.collectionId.getStringValue(),
305 curatorId: curatorId.value,
306 };
307
308 const result = await useCase.execute(request);
309
310 expect(result.isOk()).toBe(true);
311
312 // Verify no unpublish operation occurred
313 const finalUnpublishCount =
314 collectionPublisher.getUnpublishedCollections().length;
315 expect(finalUnpublishCount).toBe(initialUnpublishCount);
316
317 // Verify collection was still deleted
318 const deletedCollectionResult = await collectionRepository.findById(
319 collection.collectionId,
320 );
321 expect(deletedCollectionResult.unwrap()).toBeNull();
322 });
323 });
324
325 describe('Edge cases', () => {
326 it('should handle deletion of collection with no published record ID', async () => {
327 const collection = new CollectionBuilder()
328 .withAuthorId(curatorId.value)
329 .withName('Collection Without Published Record')
330 .withPublished(true)
331 .build();
332
333 if (collection instanceof Error) {
334 throw new Error(`Failed to create collection: ${collection.message}`);
335 }
336
337 // Manually clear the published record ID to simulate edge case
338 (collection as any).props.publishedRecordId = undefined;
339 await collectionRepository.save(collection);
340
341 const request = {
342 collectionId: collection.collectionId.getStringValue(),
343 curatorId: curatorId.value,
344 };
345
346 const result = await useCase.execute(request);
347
348 expect(result.isOk()).toBe(true);
349
350 // Verify collection was deleted despite missing published record ID
351 const deletedCollectionResult = await collectionRepository.findById(
352 collection.collectionId,
353 );
354 expect(deletedCollectionResult.unwrap()).toBeNull();
355 });
356
357 it('should handle multiple deletion attempts on same collection', async () => {
358 const collection = await createCollection(
359 curatorId,
360 'Collection to Delete Twice',
361 );
362
363 const request = {
364 collectionId: collection.collectionId.getStringValue(),
365 curatorId: curatorId.value,
366 };
367
368 // First deletion should succeed
369 const firstResult = await useCase.execute(request);
370 expect(firstResult.isOk()).toBe(true);
371
372 // Second deletion should fail
373 const secondResult = await useCase.execute(request);
374 expect(secondResult.isErr()).toBe(true);
375 if (secondResult.isErr()) {
376 expect(secondResult.error.message).toContain('Collection not found');
377 }
378 });
379 });
380});