diff --git a/src/modules/atproto/tests/application/ProcessCardFirehoseEventUseCase.test.ts b/src/modules/atproto/tests/application/ProcessCardFirehoseEventUseCase.test.ts index 4e036615..5a49e89b 100644 --- a/src/modules/atproto/tests/application/ProcessCardFirehoseEventUseCase.test.ts +++ b/src/modules/atproto/tests/application/ProcessCardFirehoseEventUseCase.test.ts @@ -5,6 +5,7 @@ import { UpdateUrlCardAssociationsUseCase } from '../../../cards/application/use import { RemoveCardFromLibraryUseCase } from '../../../cards/application/useCases/commands/RemoveCardFromLibraryUseCase'; import { InMemoryCardRepository } from '../../../cards/tests/utils/InMemoryCardRepository'; import { InMemoryCollectionRepository } from '../../../cards/tests/utils/InMemoryCollectionRepository'; +import { InMemoryConnectionRepository } from '../../../cards/tests/utils/InMemoryConnectionRepository'; import { FakeCardPublisher } from '../../../cards/tests/utils/FakeCardPublisher'; import { FakeCollectionPublisher } from '../../../cards/tests/utils/FakeCollectionPublisher'; import { FakeMetadataService } from '../../../cards/tests/utils/FakeMetadataService'; @@ -26,6 +27,7 @@ describe('ProcessCardFirehoseEventUseCase', () => { let removeCardFromLibraryUseCase: RemoveCardFromLibraryUseCase; let cardRepository: InMemoryCardRepository; let collectionRepository: InMemoryCollectionRepository; + let connectionRepository: InMemoryConnectionRepository; let cardPublisher: FakeCardPublisher; let collectionPublisher: FakeCollectionPublisher; let metadataService: FakeMetadataService; @@ -39,6 +41,7 @@ describe('ProcessCardFirehoseEventUseCase', () => { configService = new EnvironmentConfigService(); cardRepository = InMemoryCardRepository.getInstance(); collectionRepository = InMemoryCollectionRepository.getInstance(); + connectionRepository = InMemoryConnectionRepository.getInstance(); cardPublisher = new FakeCardPublisher(); collectionPublisher = new FakeCollectionPublisher(); metadataService = new FakeMetadataService(); @@ -59,6 +62,7 @@ describe('ProcessCardFirehoseEventUseCase', () => { atUriResolutionService = new InMemoryAtUriResolutionService( collectionRepository, cardRepository, + connectionRepository, ); addUrlToLibraryUseCase = new AddUrlToLibraryUseCase( diff --git a/src/modules/atproto/tests/application/ProcessCollectionFirehoseEventUseCase.test.ts b/src/modules/atproto/tests/application/ProcessCollectionFirehoseEventUseCase.test.ts index d42794a4..0909a731 100644 --- a/src/modules/atproto/tests/application/ProcessCollectionFirehoseEventUseCase.test.ts +++ b/src/modules/atproto/tests/application/ProcessCollectionFirehoseEventUseCase.test.ts @@ -4,6 +4,7 @@ import { CreateCollectionUseCase } from '../../../cards/application/useCases/com import { UpdateCollectionUseCase } from '../../../cards/application/useCases/commands/UpdateCollectionUseCase'; import { DeleteCollectionUseCase } from '../../../cards/application/useCases/commands/DeleteCollectionUseCase'; import { InMemoryCollectionRepository } from '../../../cards/tests/utils/InMemoryCollectionRepository'; +import { InMemoryConnectionRepository } from '../../../cards/tests/utils/InMemoryConnectionRepository'; import { FakeCollectionPublisher } from '../../../cards/tests/utils/FakeCollectionPublisher'; import { CollectionBuilder } from '../../../cards/tests/utils/builders/CollectionBuilder'; import { CuratorId } from '../../../cards/domain/value-objects/CuratorId'; @@ -20,6 +21,7 @@ describe('ProcessCollectionFirehoseEventUseCase', () => { let deleteCollectionUseCase: DeleteCollectionUseCase; let collectionRepository: InMemoryCollectionRepository; let cardRepository: InMemoryCardRepository; + let connectionRepository: InMemoryConnectionRepository; let collectionPublisher: FakeCollectionPublisher; let curatorId: CuratorId; let configService: EnvironmentConfigService; @@ -28,11 +30,13 @@ describe('ProcessCollectionFirehoseEventUseCase', () => { configService = new EnvironmentConfigService(); collectionRepository = InMemoryCollectionRepository.getInstance(); cardRepository = InMemoryCardRepository.getInstance(); + connectionRepository = InMemoryConnectionRepository.getInstance(); collectionPublisher = new FakeCollectionPublisher(); atUriResolutionService = new InMemoryAtUriResolutionService( collectionRepository, cardRepository, + connectionRepository, ); createCollectionUseCase = new CreateCollectionUseCase( diff --git a/src/modules/atproto/tests/application/ProcessCollectionLinkFirehoseEventUseCase.test.ts b/src/modules/atproto/tests/application/ProcessCollectionLinkFirehoseEventUseCase.test.ts index 7034c973..531cdd90 100644 --- a/src/modules/atproto/tests/application/ProcessCollectionLinkFirehoseEventUseCase.test.ts +++ b/src/modules/atproto/tests/application/ProcessCollectionLinkFirehoseEventUseCase.test.ts @@ -3,6 +3,7 @@ import { InMemoryAtUriResolutionService } from '../../../cards/tests/utils/InMem import { UpdateUrlCardAssociationsUseCase } from '../../../cards/application/useCases/commands/UpdateUrlCardAssociationsUseCase'; import { InMemoryCardRepository } from '../../../cards/tests/utils/InMemoryCardRepository'; import { InMemoryCollectionRepository } from '../../../cards/tests/utils/InMemoryCollectionRepository'; +import { InMemoryConnectionRepository } from '../../../cards/tests/utils/InMemoryConnectionRepository'; import { FakeCardPublisher } from '../../../cards/tests/utils/FakeCardPublisher'; import { FakeCollectionPublisher } from '../../../cards/tests/utils/FakeCollectionPublisher'; import { FakeEventPublisher } from '../../../cards/tests/utils/FakeEventPublisher'; @@ -23,6 +24,7 @@ describe('ProcessCollectionLinkFirehoseEventUseCase', () => { let updateUrlCardAssociationsUseCase: UpdateUrlCardAssociationsUseCase; let cardRepository: InMemoryCardRepository; let collectionRepository: InMemoryCollectionRepository; + let connectionRepository: InMemoryConnectionRepository; let cardPublisher: FakeCardPublisher; let collectionPublisher: FakeCollectionPublisher; let eventPublisher: FakeEventPublisher; @@ -35,6 +37,7 @@ describe('ProcessCollectionLinkFirehoseEventUseCase', () => { configService = new EnvironmentConfigService(); cardRepository = InMemoryCardRepository.getInstance(); collectionRepository = InMemoryCollectionRepository.getInstance(); + connectionRepository = InMemoryConnectionRepository.getInstance(); cardPublisher = new FakeCardPublisher(); collectionPublisher = new FakeCollectionPublisher(); eventPublisher = new FakeEventPublisher(); @@ -54,6 +57,7 @@ describe('ProcessCollectionLinkFirehoseEventUseCase', () => { atUriResolutionService = new InMemoryAtUriResolutionService( collectionRepository, cardRepository, + connectionRepository, ); updateUrlCardAssociationsUseCase = new UpdateUrlCardAssociationsUseCase( diff --git a/src/modules/atproto/tests/application/ProcessFirehoseEventUseCase.test.ts b/src/modules/atproto/tests/application/ProcessFirehoseEventUseCase.test.ts index c3456874..4104d37a 100644 --- a/src/modules/atproto/tests/application/ProcessFirehoseEventUseCase.test.ts +++ b/src/modules/atproto/tests/application/ProcessFirehoseEventUseCase.test.ts @@ -24,6 +24,7 @@ import { DeleteConnectionUseCase } from '../../../cards/application/useCases/com import { InMemoryCardRepository } from '../../../cards/tests/utils/InMemoryCardRepository'; import { InMemoryCardQueryRepository } from '../../../cards/tests/utils/InMemoryCardQueryRepository'; import { InMemoryCollectionRepository } from '../../../cards/tests/utils/InMemoryCollectionRepository'; +import { InMemoryConnectionRepository } from '../../../cards/tests/utils/InMemoryConnectionRepository'; import { FakeCardPublisher } from '../../../cards/tests/utils/FakeCardPublisher'; import { FakeCollectionPublisher } from '../../../cards/tests/utils/FakeCollectionPublisher'; import { FakeConnectionPublisher } from '../../../cards/tests/utils/FakeConnectionPublisher'; @@ -60,6 +61,7 @@ describe('ProcessFirehoseEventUseCase', () => { let cardRepository: InMemoryCardRepository; let cardQueryRepository: InMemoryCardQueryRepository; let collectionRepository: InMemoryCollectionRepository; + let connectionRepository: InMemoryConnectionRepository; let cardPublisher: FakeCardPublisher; let collectionPublisher: FakeCollectionPublisher; let metadataService: FakeMetadataService; @@ -90,6 +92,7 @@ describe('ProcessFirehoseEventUseCase', () => { // Set up all the real dependencies cardRepository = InMemoryCardRepository.getInstance(); collectionRepository = InMemoryCollectionRepository.getInstance(); + connectionRepository = InMemoryConnectionRepository.getInstance(); cardQueryRepository = new InMemoryCardQueryRepository( cardRepository, collectionRepository, @@ -114,6 +117,7 @@ describe('ProcessFirehoseEventUseCase', () => { atUriResolutionService = new InMemoryAtUriResolutionService( collectionRepository, cardRepository, + connectionRepository, ); // Create use cases for card processing diff --git a/src/modules/cards/tests/utils/InMemoryAtUriResolutionService.ts b/src/modules/cards/tests/utils/InMemoryAtUriResolutionService.ts index f4a4a6af..965f68f5 100644 --- a/src/modules/cards/tests/utils/InMemoryAtUriResolutionService.ts +++ b/src/modules/cards/tests/utils/InMemoryAtUriResolutionService.ts @@ -10,11 +10,13 @@ import { ConnectionId } from '../../domain/value-objects/ConnectionId'; import { FollowTargetType } from '../../../user/domain/value-objects/FollowTargetType'; import { InMemoryCollectionRepository } from './InMemoryCollectionRepository'; import { InMemoryCardRepository } from './InMemoryCardRepository'; +import { InMemoryConnectionRepository } from './InMemoryConnectionRepository'; export class InMemoryAtUriResolutionService implements IAtUriResolutionService { constructor( private collectionRepository: InMemoryCollectionRepository, private cardRepository: InMemoryCardRepository, + private connectionRepository: InMemoryConnectionRepository, ) {} async resolveAtUri( @@ -56,6 +58,17 @@ export class InMemoryAtUriResolutionService implements IAtUriResolutionService { } } + // Check connections + const allConnections = this.connectionRepository.getAllConnections(); + for (const connection of allConnections) { + if (connection.publishedRecordId?.uri === atUri) { + return ok({ + type: AtUriResourceType.CONNECTION, + id: connection.connectionId, + }); + } + } + return ok(null); } catch (error) { return err(error as Error); @@ -128,8 +141,16 @@ export class InMemoryAtUriResolutionService implements IAtUriResolutionService { async resolveConnectionId( atUri: string, ): Promise> { - // For testing purposes, return null (connection not found) - // TODO: When InMemoryConnectionRepository is created, search through connections - return ok(null); + const result = await this.resolveAtUri(atUri); + + if (result.isErr()) { + return err(result.error); + } + + if (!result.value || result.value.type !== AtUriResourceType.CONNECTION) { + return ok(null); + } + + return ok(result.value.id as ConnectionId); } } diff --git a/src/modules/cards/tests/utils/InMemoryConnectionRepository.ts b/src/modules/cards/tests/utils/InMemoryConnectionRepository.ts new file mode 100644 index 00000000..a59269cc --- /dev/null +++ b/src/modules/cards/tests/utils/InMemoryConnectionRepository.ts @@ -0,0 +1,160 @@ +import { Result, ok, err } from '../../../../shared/core/Result'; +import { IConnectionRepository } from '../../domain/IConnectionRepository'; +import { Connection } from '../../domain/Connection'; +import { ConnectionId } from '../../domain/value-objects/ConnectionId'; +import { UrlOrCardId } from '../../domain/value-objects/UrlOrCardId'; +import { CuratorId } from '../../domain/value-objects/CuratorId'; + +export class InMemoryConnectionRepository implements IConnectionRepository { + private static instance: InMemoryConnectionRepository; + private connections: Map = new Map(); + + private constructor() {} + + public static getInstance(): InMemoryConnectionRepository { + if (!InMemoryConnectionRepository.instance) { + InMemoryConnectionRepository.instance = + new InMemoryConnectionRepository(); + } + return InMemoryConnectionRepository.instance; + } + + private clone(connection: Connection): Connection { + const connectionResult = Connection.create( + { + curatorId: connection.curatorId, + source: connection.source, + target: connection.target, + type: connection.type, + note: connection.note, + publishedRecordId: connection.publishedRecordId, + createdAt: connection.createdAt, + updatedAt: connection.updatedAt, + }, + connection.id, + ); + + if (connectionResult.isErr()) { + throw new Error( + `Failed to clone connection: ${connectionResult.error.message}`, + ); + } + + return connectionResult.value; + } + + async findById(id: ConnectionId): Promise> { + try { + const connection = this.connections.get(id.getStringValue()); + return ok(connection ? this.clone(connection) : null); + } catch (error) { + return err(error as Error); + } + } + + async findByIds(ids: ConnectionId[]): Promise> { + try { + const connections: Connection[] = []; + for (const id of ids) { + const connection = this.connections.get(id.getStringValue()); + if (connection) { + connections.push(this.clone(connection)); + } + } + return ok(connections); + } catch (error) { + return err(error as Error); + } + } + + async findByCuratorId(curatorId: CuratorId): Promise> { + try { + const connections = Array.from(this.connections.values()).filter( + (connection) => connection.curatorId.value === curatorId.value, + ); + return ok(connections.map((connection) => this.clone(connection))); + } catch (error) { + return err(error as Error); + } + } + + async findBySource(source: UrlOrCardId): Promise> { + try { + const connections = Array.from(this.connections.values()).filter( + (connection) => + connection.source.type === source.type && + connection.source.stringValue === source.stringValue, + ); + return ok(connections.map((connection) => this.clone(connection))); + } catch (error) { + return err(error as Error); + } + } + + async findByTarget(target: UrlOrCardId): Promise> { + try { + const connections = Array.from(this.connections.values()).filter( + (connection) => + connection.target.type === target.type && + connection.target.stringValue === target.stringValue, + ); + return ok(connections.map((connection) => this.clone(connection))); + } catch (error) { + return err(error as Error); + } + } + + async findBetween( + source: UrlOrCardId, + target: UrlOrCardId, + ): Promise> { + try { + const connections = Array.from(this.connections.values()).filter( + (connection) => + connection.source.type === source.type && + connection.source.stringValue === source.stringValue && + connection.target.type === target.type && + connection.target.stringValue === target.stringValue, + ); + return ok(connections.map((connection) => this.clone(connection))); + } catch (error) { + return err(error as Error); + } + } + + async save(connection: Connection): Promise> { + try { + this.connections.set( + connection.connectionId.getStringValue(), + this.clone(connection), + ); + return ok(undefined); + } catch (error) { + return err(error as Error); + } + } + + async delete(connectionId: ConnectionId): Promise> { + try { + this.connections.delete(connectionId.getStringValue()); + return ok(undefined); + } catch (error) { + return err(error as Error); + } + } + + // Helper methods for testing + public clear(): void { + this.connections.clear(); + } + + public getStoredConnection(id: ConnectionId): Connection | undefined { + return this.connections.get(id.getStringValue()); + } + + public getAllConnections(): Connection[] { + return Array.from(this.connections.values()).map((connection) => + this.clone(connection), + ); + } +} diff --git a/src/shared/infrastructure/http/factories/RepositoryFactory.ts b/src/shared/infrastructure/http/factories/RepositoryFactory.ts index 340ebf8f..aeb60c51 100644 --- a/src/shared/infrastructure/http/factories/RepositoryFactory.ts +++ b/src/shared/infrastructure/http/factories/RepositoryFactory.ts @@ -13,6 +13,7 @@ import { InMemoryCardRepository } from '../../../../modules/cards/tests/utils/In import { InMemoryCardQueryRepository } from '../../../../modules/cards/tests/utils/InMemoryCardQueryRepository'; import { InMemoryCollectionRepository } from '../../../../modules/cards/tests/utils/InMemoryCollectionRepository'; import { InMemoryCollectionQueryRepository } from '../../../../modules/cards/tests/utils/InMemoryCollectionQueryRepository'; +import { InMemoryConnectionRepository } from '../../../../modules/cards/tests/utils/InMemoryConnectionRepository'; import { InMemoryUserRepository } from '../../../../modules/user/tests/infrastructure/InMemoryUserRepository'; import { InMemoryTokenRepository } from '../../../../modules/user/tests/infrastructure/InMemoryTokenRepository'; import { InMemoryAppPasswordSessionRepository } from '../../../../modules/atproto/tests/infrastructure/InMemoryAppPasswordSessionRepository'; @@ -76,6 +77,7 @@ export class RepositoryFactory { const tokenRepository = InMemoryTokenRepository.getInstance(); const cardRepository = InMemoryCardRepository.getInstance(); const collectionRepository = InMemoryCollectionRepository.getInstance(); + const connectionRepository = InMemoryConnectionRepository.getInstance(); const cardQueryRepository = new InMemoryCardQueryRepository( cardRepository, collectionRepository, @@ -91,6 +93,7 @@ export class RepositoryFactory { const atUriResolutionService = new InMemoryAtUriResolutionService( collectionRepository, cardRepository, + connectionRepository, ); const notificationRepository = InMemoryNotificationRepository.getInstance(); @@ -107,8 +110,7 @@ export class RepositoryFactory { cardQueryRepository, collectionRepository, collectionQueryRepository, - // TODO: Create InMemoryConnectionRepository - connectionRepository: null as any, + connectionRepository, appPasswordSessionRepository, feedRepository, followsRepository,