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});