A social knowledge tool for researchers built on ATProto
1import { CreateCollectionUseCase } from '../../application/useCases/commands/CreateCollectionUseCase';
2import { InMemoryCollectionRepository } from '../utils/InMemoryCollectionRepository';
3import { FakeCollectionPublisher } from '../utils/FakeCollectionPublisher';
4import { CuratorId } from '../../domain/value-objects/CuratorId';
5import { CollectionAccessType } from '../../domain/Collection';
6import { FakeEventPublisher } from '../utils/FakeEventPublisher';
7import { CollectionCreatedEvent } from '../../domain/events/CollectionCreatedEvent';
8import { EventNames } from 'src/shared/infrastructure/events/EventConfig';
9
10describe('CreateCollectionUseCase', () => {
11 let useCase: CreateCollectionUseCase;
12 let collectionRepository: InMemoryCollectionRepository;
13 let collectionPublisher: FakeCollectionPublisher;
14 let eventPublisher: FakeEventPublisher;
15 let curatorId: CuratorId;
16
17 beforeEach(() => {
18 collectionRepository = new InMemoryCollectionRepository();
19 collectionPublisher = new FakeCollectionPublisher();
20 eventPublisher = new FakeEventPublisher();
21 useCase = new CreateCollectionUseCase(
22 collectionRepository,
23 collectionPublisher,
24 );
25 curatorId = CuratorId.create('did:plc:testcurator').unwrap();
26 });
27
28 afterEach(() => {
29 collectionRepository.clear();
30 collectionPublisher.clear();
31 eventPublisher.clear();
32 });
33
34 describe('Basic collection creation', () => {
35 it('should successfully create a collection', async () => {
36 const request = {
37 name: 'My Test Collection',
38 description: 'A collection for testing purposes',
39 curatorId: curatorId.value,
40 };
41
42 const result = await useCase.execute(request);
43
44 expect(result.isOk()).toBe(true);
45 const response = result.unwrap();
46 expect(response.collectionId).toBeDefined();
47
48 // Verify collection was saved
49 const savedCollections = collectionRepository.getAllCollections();
50 expect(savedCollections).toHaveLength(1);
51
52 const savedCollection = savedCollections[0]!;
53 expect(savedCollection.name.value).toBe('My Test Collection');
54 expect(savedCollection.description?.value).toBe(
55 'A collection for testing purposes',
56 );
57 expect(savedCollection.authorId.equals(curatorId)).toBe(true);
58 expect(savedCollection.accessType).toBe(CollectionAccessType.CLOSED);
59 expect(savedCollection.isPublished).toBe(true);
60 expect(savedCollection.publishedRecordId).toBeDefined();
61 });
62
63 it('should create collection without description', async () => {
64 const request = {
65 name: 'Collection Without Description',
66 curatorId: curatorId.value,
67 };
68
69 const result = await useCase.execute(request);
70
71 expect(result.isOk()).toBe(true);
72 const response = result.unwrap();
73 expect(response.collectionId).toBeDefined();
74
75 // Verify collection was saved
76 const savedCollections = collectionRepository.getAllCollections();
77 expect(savedCollections).toHaveLength(1);
78
79 const savedCollection = savedCollections[0]!;
80 expect(savedCollection.name.value).toBe('Collection Without Description');
81 expect(savedCollection.description).toBeUndefined();
82 });
83
84 it('should publish collection after creation', async () => {
85 const request = {
86 name: 'Published Collection',
87 curatorId: curatorId.value,
88 };
89
90 const result = await useCase.execute(request);
91
92 expect(result.isOk()).toBe(true);
93
94 // Verify collection was published
95 const publishedCollections =
96 collectionPublisher.getPublishedCollections();
97 expect(publishedCollections).toHaveLength(1);
98
99 const publishedCollection = publishedCollections[0]!;
100 expect(publishedCollection.name.value).toBe('Published Collection');
101 });
102 });
103
104 describe('Validation', () => {
105 it('should fail with invalid curator ID', async () => {
106 const request = {
107 name: 'Test Collection',
108 curatorId: 'invalid-curator-id',
109 };
110
111 const result = await useCase.execute(request);
112
113 expect(result.isErr()).toBe(true);
114 if (result.isErr()) {
115 expect(result.error.message).toContain('Invalid curator ID');
116 }
117 });
118
119 it('should fail with empty collection name', async () => {
120 const request = {
121 name: '',
122 curatorId: curatorId.value,
123 };
124
125 const result = await useCase.execute(request);
126
127 expect(result.isErr()).toBe(true);
128 if (result.isErr()) {
129 expect(result.error.message).toContain(
130 'Collection name cannot be empty',
131 );
132 }
133 });
134
135 it('should fail with collection name that is too long', async () => {
136 const request = {
137 name: 'a'.repeat(101), // Exceeds MAX_LENGTH
138 curatorId: curatorId.value,
139 };
140
141 const result = await useCase.execute(request);
142
143 expect(result.isErr()).toBe(true);
144 if (result.isErr()) {
145 expect(result.error.message).toContain('Collection name cannot exceed');
146 }
147 });
148
149 it('should fail with description that is too long', async () => {
150 const request = {
151 name: 'Valid Collection Name',
152 description: 'a'.repeat(501), // Exceeds MAX_LENGTH
153 curatorId: curatorId.value,
154 };
155
156 const result = await useCase.execute(request);
157
158 expect(result.isErr()).toBe(true);
159 if (result.isErr()) {
160 expect(result.error.message).toContain(
161 'Collection description cannot exceed',
162 );
163 }
164 });
165
166 it('should trim whitespace from collection name', async () => {
167 const request = {
168 name: ' Collection With Whitespace ',
169 curatorId: curatorId.value,
170 };
171
172 const result = await useCase.execute(request);
173
174 expect(result.isOk()).toBe(true);
175
176 // Verify name was trimmed
177 const savedCollections = collectionRepository.getAllCollections();
178 const savedCollection = savedCollections[0]!;
179 expect(savedCollection.name.value).toBe('Collection With Whitespace');
180 });
181
182 it('should trim whitespace from description', async () => {
183 const request = {
184 name: 'Test Collection',
185 description: ' Description with whitespace ',
186 curatorId: curatorId.value,
187 };
188
189 const result = await useCase.execute(request);
190
191 expect(result.isOk()).toBe(true);
192
193 // Verify description was trimmed
194 const savedCollections = collectionRepository.getAllCollections();
195 const savedCollection = savedCollections[0]!;
196 expect(savedCollection.description?.value).toBe(
197 'Description with whitespace',
198 );
199 });
200 });
201
202 describe('Publishing integration', () => {
203 it('should handle publishing failure gracefully', async () => {
204 // Configure publisher to fail
205 collectionPublisher.setShouldFail(true);
206
207 const request = {
208 name: 'Collection That Fails to Publish',
209 curatorId: curatorId.value,
210 };
211
212 const result = await useCase.execute(request);
213
214 expect(result.isErr()).toBe(true);
215 if (result.isErr()) {
216 expect(result.error.message).toContain('Failed to publish collection');
217 }
218
219 // Verify collection was not saved if publishing failed
220 const savedCollections = collectionRepository.getAllCollections();
221 expect(savedCollections).toHaveLength(1); // Collection is saved before publishing
222 });
223
224 it('should save collection with published record ID after successful publish', async () => {
225 const request = {
226 name: 'Successfully Published Collection',
227 curatorId: curatorId.value,
228 };
229
230 const result = await useCase.execute(request);
231
232 expect(result.isOk()).toBe(true);
233
234 // Verify collection has published record ID
235 const savedCollections = collectionRepository.getAllCollections();
236 const savedCollection = savedCollections[0]!;
237 expect(savedCollection.isPublished).toBe(true);
238 expect(savedCollection.publishedRecordId).toBeDefined();
239 expect(savedCollection.publishedRecordId?.uri).toBeDefined();
240 expect(savedCollection.publishedRecordId?.cid).toBeDefined();
241 });
242 });
243
244 describe('Edge cases', () => {
245 it('should handle maximum length collection name', async () => {
246 const request = {
247 name: 'a'.repeat(100), // Exactly MAX_LENGTH
248 curatorId: curatorId.value,
249 };
250
251 const result = await useCase.execute(request);
252
253 expect(result.isOk()).toBe(true);
254
255 // Verify name was saved correctly
256 const savedCollections = collectionRepository.getAllCollections();
257 const savedCollection = savedCollections[0]!;
258 expect(savedCollection.name.value.length).toBe(100);
259 });
260
261 it('should handle maximum length description', async () => {
262 const request = {
263 name: 'Test Collection',
264 description: 'a'.repeat(500), // Exactly MAX_LENGTH
265 curatorId: curatorId.value,
266 };
267
268 const result = await useCase.execute(request);
269
270 expect(result.isOk()).toBe(true);
271
272 // Verify description was saved correctly
273 const savedCollections = collectionRepository.getAllCollections();
274 const savedCollection = savedCollections[0]!;
275 expect(savedCollection.description?.value.length).toBe(500);
276 });
277
278 it('should create multiple collections for same curator', async () => {
279 const firstRequest = {
280 name: 'First Collection',
281 curatorId: curatorId.value,
282 };
283
284 const secondRequest = {
285 name: 'Second Collection',
286 curatorId: curatorId.value,
287 };
288
289 const firstResult = await useCase.execute(firstRequest);
290 const secondResult = await useCase.execute(secondRequest);
291
292 expect(firstResult.isOk()).toBe(true);
293 expect(secondResult.isOk()).toBe(true);
294
295 // Verify both collections were saved
296 const savedCollections = collectionRepository.getAllCollections();
297 expect(savedCollections).toHaveLength(2);
298
299 const collectionNames = savedCollections.map((c) => c.name.value);
300 expect(collectionNames).toContain('First Collection');
301 expect(collectionNames).toContain('Second Collection');
302 });
303 });
304});