Something went wrong. Try again.
A social knowledge tool for research trails, built on ATProto semble.so
research-tool social-network tools-for-thought atproto
Something went wrong. Try again.
TypeScript
at development
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490import { UpdateCollectionUseCase } from '../../application/useCases/commands/UpdateCollectionUseCase';import { InMemoryCollectionRepository } from '../utils/InMemoryCollectionRepository';import { FakeCollectionPublisher } from '../utils/FakeCollectionPublisher';import { CuratorId } from '../../domain/value-objects/CuratorId';import { CollectionBuilder } from '../utils/builders/CollectionBuilder';import { Err } from 'src/shared/core/Result';
describe('UpdateCollectionUseCase', () => { let useCase: UpdateCollectionUseCase; let collectionRepository: InMemoryCollectionRepository; let collectionPublisher: FakeCollectionPublisher; let curatorId: CuratorId; let otherCuratorId: CuratorId;
beforeEach(() => { collectionRepository = new InMemoryCollectionRepository(); collectionPublisher = new FakeCollectionPublisher(); useCase = new UpdateCollectionUseCase( collectionRepository, collectionPublisher, );
curatorId = CuratorId.create('did:plc:testcurator').unwrap(); otherCuratorId = CuratorId.create('did:plc:othercurator').unwrap(); });
afterEach(() => { collectionRepository.clear(); collectionPublisher.clear(); });
const createCollection = async ( authorId: CuratorId, name: string, description?: string, ) => { const collection = new CollectionBuilder() .withAuthorId(authorId.value) .withName(name) .withDescription(description || '') .withPublished(true) .build();
if (collection instanceof Error) { throw new Error(`Failed to create collection: ${collection.message}`); }
await collectionRepository.save(collection); return collection; };
describe('Basic collection update', () => { it('should successfully update collection name and description', async () => { const collection = await createCollection( curatorId, 'Original Name', 'Original description', );
const request = { collectionId: collection.collectionId.getStringValue(), name: 'Updated Name', description: 'Updated description', curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true); const response = result.unwrap(); expect(response.collectionId).toBe( collection.collectionId.getStringValue(), );
// Verify collection was updated const updatedCollectionResult = await collectionRepository.findById( collection.collectionId, ); const updatedCollection = updatedCollectionResult.unwrap()!; expect(updatedCollection.name.value).toBe('Updated Name'); expect(updatedCollection.description?.value).toBe('Updated description'); expect(updatedCollection.updatedAt.getTime()).toBeGreaterThanOrEqual( collection.createdAt.getTime(), ); });
it('should update collection name only', async () => { const collection = await createCollection( curatorId, 'Original Name', 'Original description', );
const request = { collectionId: collection.collectionId.getStringValue(), name: 'New Name Only', description: 'Original description', // Keep original description curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// Verify only name was updated const updatedCollectionResult = await collectionRepository.findById( collection.collectionId, ); const updatedCollection = updatedCollectionResult.unwrap()!; expect(updatedCollection.name.value).toBe('New Name Only'); expect(updatedCollection.description?.value).toBe('Original description'); });
it('should clear description when not provided', async () => { const collection = await createCollection( curatorId, 'Original Name', 'Original description', );
const request = { collectionId: collection.collectionId.getStringValue(), name: 'Updated Name', curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// Verify description was cleared const updatedCollectionResult = await collectionRepository.findById( collection.collectionId, ); const updatedCollection = updatedCollectionResult.unwrap()!; expect(updatedCollection.name.value).toBe('Updated Name'); expect(updatedCollection.description).toBeUndefined(); });
it('should preserve other collection properties', async () => { const collection = await createCollection( curatorId, 'Original Name', 'Original description', ); const originalCreatedAt = collection.createdAt; const originalAccessType = collection.accessType;
const request = { collectionId: collection.collectionId.getStringValue(), name: 'Updated Name', description: 'Updated description', curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// Verify other properties are preserved const updatedCollectionResult = await collectionRepository.findById( collection.collectionId, ); const updatedCollection = updatedCollectionResult.unwrap()!; expect(updatedCollection.collectionId.getStringValue()).toBe( collection.collectionId.getStringValue(), ); expect(updatedCollection.authorId.equals(curatorId)).toBe(true); expect(updatedCollection.createdAt).toEqual(originalCreatedAt); expect(updatedCollection.accessType).toBe(originalAccessType); }); });
describe('Authorization', () => { it("should fail when trying to update another user's collection", async () => { const collection = await createCollection(curatorId, 'Original Name');
const request = { collectionId: collection.collectionId.getStringValue(), name: 'Unauthorized Update', curatorId: otherCuratorId.value, };
const result = await useCase.execute(request);
expect(result.isErr()).toBe(true); if (result.isErr()) { expect(result.error.message).toContain( 'Only the collection author can update the collection', ); }
// Verify original collection was not modified const originalCollectionResult = await collectionRepository.findById( collection.collectionId, ); const originalCollection = originalCollectionResult.unwrap()!; expect(originalCollection.name.value).toBe('Original Name'); });
it('should allow author to update their own collection', async () => { const collection = await createCollection(curatorId, 'Original Name');
const request = { collectionId: collection.collectionId.getStringValue(), name: 'Author Update', curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// Verify update was successful const updatedCollectionResult = await collectionRepository.findById( collection.collectionId, ); const updatedCollection = updatedCollectionResult.unwrap()!; expect(updatedCollection.name.value).toBe('Author Update'); }); });
describe('Validation', () => { it('should fail with invalid collection ID', async () => { const request = { collectionId: 'invalid-collection-id', name: 'Updated Name', curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isErr()).toBe(true); if (result.isErr()) { expect(result.error.message).toContain('Collection not found'); } });
it('should fail with invalid curator ID', async () => { const collection = await createCollection(curatorId, 'Original Name');
const request = { collectionId: collection.collectionId.getStringValue(), name: 'Updated Name', curatorId: 'invalid-curator-id', };
const result = await useCase.execute(request);
expect(result.isErr()).toBe(true); if (result.isErr()) { expect(result.error.message).toContain('Invalid curator ID'); } });
it('should fail when collection does not exist', async () => { const request = { collectionId: 'non-existent-collection-id', name: 'Updated Name', curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isErr()).toBe(true); if (result.isErr()) { expect(result.error.message).toContain('Collection not found'); } });
it('should fail with empty collection name', async () => { const collection = await createCollection(curatorId, 'Original Name');
const request = { collectionId: collection.collectionId.getStringValue(), name: '', curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isErr()).toBe(true); if (result.isErr()) { expect(result.error.message).toContain( 'Collection name cannot be empty', ); } });
it('should fail with collection name that is too long', async () => { const collection = await createCollection(curatorId, 'Original Name');
const request = { collectionId: collection.collectionId.getStringValue(), name: 'a'.repeat(101), // Exceeds MAX_LENGTH curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isErr()).toBe(true); if (result.isErr()) { expect(result.error.message).toContain('Collection name cannot exceed'); } });
it('should fail with description that is too long', async () => { const collection = await createCollection(curatorId, 'Original Name');
const request = { collectionId: collection.collectionId.getStringValue(), name: 'Valid Name', description: 'a'.repeat(501), // Exceeds MAX_LENGTH curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isErr()).toBe(true); if (result.isErr()) { expect(result.error.message).toContain( 'Collection description cannot exceed', ); } });
it('should trim whitespace from name and description', async () => { const collection = await createCollection(curatorId, 'Original Name');
const request = { collectionId: collection.collectionId.getStringValue(), name: ' Updated Name ', description: ' Updated description ', curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// Verify whitespace was trimmed const updatedCollectionResult = await collectionRepository.findById( collection.collectionId, ); const updatedCollection = updatedCollectionResult.unwrap()!; expect(updatedCollection.name.value).toBe('Updated Name'); expect(updatedCollection.description?.value).toBe('Updated description'); }); });
describe('Publishing integration', () => { it('should republish collection if it was already published', async () => { const collection = await createCollection(curatorId, 'Original Name'); const initialPublishCount = collectionPublisher.getPublishedCollections().length;
const request = { collectionId: collection.collectionId.getStringValue(), name: 'Updated Name', curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// Verify republishing occurred const finalPublishCount = collectionPublisher.getPublishedCollections().length; expect(finalPublishCount).toBeGreaterThanOrEqual(initialPublishCount);
// Verify published record ID was updated const updatedCollectionResult = await collectionRepository.findById( collection.collectionId, ); const updatedCollection = updatedCollectionResult.unwrap()!; expect(updatedCollection.isPublished).toBe(true); expect(updatedCollection.publishedRecordId).toBeDefined(); });
it('should not republish unpublished collection', async () => { const collection = new CollectionBuilder() .withAuthorId(curatorId.value) .withName('Unpublished Collection') .withPublished(false) .build();
if (collection instanceof Error) { throw new Error(`Failed to create collection: ${collection.message}`); }
await collectionRepository.save(collection); const initialPublishCount = collectionPublisher.getPublishedCollections().length;
const request = { collectionId: collection.collectionId.getStringValue(), name: 'Updated Unpublished', curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// Verify no additional publishing occurred const finalPublishCount = collectionPublisher.getPublishedCollections().length; expect(finalPublishCount).toBe(initialPublishCount); });
it('should handle republishing failure gracefully', async () => { const collection = await createCollection(curatorId, 'Original Name');
// Configure publisher to fail collectionPublisher.setShouldFail(true);
const request = { collectionId: collection.collectionId.getStringValue(), name: 'Updated Name', curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isErr()).toBe(true); if (result.isErr()) { expect(result.error.message).toContain( 'Failed to republish collection', ); } }); });
describe('Edge cases', () => { it('should handle updating to the same name and description', async () => { const collection = await createCollection( curatorId, 'Same Name', 'Same description', );
const request = { collectionId: collection.collectionId.getStringValue(), name: 'Same Name', description: 'Same description', curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// Verify collection was still updated (updatedAt should change) const updatedCollectionResult = await collectionRepository.findById( collection.collectionId, ); const updatedCollection = updatedCollectionResult.unwrap()!; expect(updatedCollection.name.value).toBe('Same Name'); expect(updatedCollection.description?.value).toBe('Same description'); expect(updatedCollection.updatedAt.getTime()).toBeGreaterThanOrEqual( collection.createdAt.getTime(), ); });
it('should handle maximum length name and description', async () => { const collection = await createCollection(curatorId, 'Original Name');
const request = { collectionId: collection.collectionId.getStringValue(), name: 'a'.repeat(100), // Exactly MAX_LENGTH description: 'b'.repeat(500), // Exactly MAX_LENGTH curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// Verify values were saved correctly const updatedCollectionResult = await collectionRepository.findById( collection.collectionId, ); const updatedCollection = updatedCollectionResult.unwrap()!; expect(updatedCollection.name.value.length).toBe(100); expect(updatedCollection.description?.value.length).toBe(500); }); });});