diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index 1c44abfd92..3e2128ac2c 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -53,6 +53,53 @@ import { UserTable } from 'src/schema/tables/user.table'; import { AccessRequest, checkAccess, requireAccess } from 'src/utils/access'; import { getConfig, updateConfig } from 'src/utils/config'; +export const BASE_SERVICE_DEPENDENCIES = [ + LoggingRepository, + AccessRepository, + ActivityRepository, + AlbumRepository, + AlbumUserRepository, + ApiKeyRepository, + AssetRepository, + AssetJobRepository, + AuditRepository, + ConfigRepository, + CronRepository, + CryptoRepository, + DatabaseRepository, + DownloadRepository, + DuplicateRepository, + EmailRepository, + EventRepository, + JobRepository, + LibraryRepository, + MachineLearningRepository, + MapRepository, + MediaRepository, + MemoryRepository, + MetadataRepository, + MoveRepository, + NotificationRepository, + OAuthRepository, + PartnerRepository, + PersonRepository, + ProcessRepository, + SearchRepository, + ServerInfoRepository, + SessionRepository, + SharedLinkRepository, + StackRepository, + StorageRepository, + SyncRepository, + SystemMetadataRepository, + TagRepository, + TelemetryRepository, + TrashRepository, + UserRepository, + VersionHistoryRepository, + ViewRepository, +]; + @Injectable() export class BaseService { protected storageCore: StorageCore; diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index cbecff267d..69708d0fce 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -1,12 +1,12 @@ -import { ClassConstructor } from 'class-transformer'; +/* eslint-disable @typescript-eslint/no-unsafe-function-type */ import { Insertable, Kysely } from 'kysely'; import { DateTime } from 'luxon'; import { createHash, randomBytes } from 'node:crypto'; import { Writable } from 'node:stream'; import { AssetFace } from 'src/database'; -import { Albums, AssetJobStatus, Assets, DB, FaceSearch, Person, Sessions } from 'src/db'; +import { Albums, AssetJobStatus, Assets, DB, Exif, FaceSearch, Person, Sessions } from 'src/db'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AssetType, AssetVisibility, SourceType, SyncRequestType } from 'src/enum'; +import { AlbumUserRole, AssetType, AssetVisibility, SourceType, SyncRequestType } from 'src/enum'; import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; import { AlbumUserRepository } from 'src/repositories/album-user.repository'; @@ -31,298 +31,263 @@ import { SystemMetadataRepository } from 'src/repositories/system-metadata.repos import { UserRepository } from 'src/repositories/user.repository'; import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; import { UserTable } from 'src/schema/tables/user.table'; -import { BaseService } from 'src/services/base.service'; +import { BASE_SERVICE_DEPENDENCIES, BaseService } from 'src/services/base.service'; import { SyncService } from 'src/services/sync.service'; -import { RepositoryInterface } from 'src/types'; import { factory, newDate, newEmbedding, newUuid } from 'test/small.factory'; -import { automock, ServiceOverrides, wait } from 'test/utils'; +import { automock, wait } from 'test/utils'; import { Mocked } from 'vitest'; -const sha256 = (value: string) => createHash('sha256').update(value).digest('base64'); +interface ClassConstructor extends Function { + new (...args: any[]): T; +} -// type Repositories = Omit; -type RepositoriesTypes = { - access: AccessRepository; - activity: ActivityRepository; - album: AlbumRepository; - albumUser: AlbumUserRepository; - asset: AssetRepository; - assetJob: AssetJobRepository; - config: ConfigRepository; - crypto: CryptoRepository; - database: DatabaseRepository; - email: EmailRepository; - job: JobRepository; - user: UserRepository; - logger: LoggingRepository; - memory: MemoryRepository; - notification: NotificationRepository; - partner: PartnerRepository; - person: PersonRepository; - search: SearchRepository; - session: SessionRepository; - storage: StorageRepository; - sync: SyncRepository; - systemMetadata: SystemMetadataRepository; - versionHistory: VersionHistoryRepository; -}; -type RepositoryMocks = { [K in keyof RepositoriesTypes]: Mocked> }; -type RepositoryOptions = Partial<{ [K in keyof RepositoriesTypes]: 'mock' | 'real' }>; - -type ContextRepositoryMocks = { - [K in keyof RepositoriesTypes as R[K] extends 'mock' ? K : never]: Mocked>; +type MediumTestOptions = { + mock: ClassConstructor[]; + real: ClassConstructor[]; + database: Kysely; }; -type ContextRepositories = { - [K in keyof RepositoriesTypes as R[K] extends 'real' ? K : never]: RepositoriesTypes[K]; +export const newMediumService = (Service: ClassConstructor, options: MediumTestOptions) => { + const ctx = new MediumTestContext(Service, options); + return { sut: ctx.sut, ctx }; }; -export type Context = { +export class MediumTestContext { + private repoCache: Record = {}; + private sutDeps: any[]; + sut: S; - mocks: ContextRepositoryMocks; - repos: ContextRepositories; - getRepository(key: T): RepositoriesTypes[T]; -}; + database: Kysely; -export type SyncTestOptions = { - db: Kysely; -}; + constructor( + Service: ClassConstructor, + private options: MediumTestOptions, + ) { + this.sutDeps = this.makeDeps(options); + this.sut = new Service(...this.sutDeps); + this.database = options.database; + } -export const newSyncAuthUser = () => { - const user = mediumFactory.userInsert(); - const session = mediumFactory.sessionInsert({ userId: user.id }); + private makeDeps(options: MediumTestOptions) { + const deps = BASE_SERVICE_DEPENDENCIES; - const auth = factory.auth({ - session, - user: { - id: user.id, - name: user.name, - email: user.email, - }, - }); + for (const dep of options.mock) { + if (!deps.includes(dep)) { + throw new Error(`Mocked repository ${dep.name} is not a valid dependency`); + } + } - return { - auth, - session, - user, - create: async (db: Kysely) => { - await new UserRepository(db).create(user); - await new SessionRepository(db).create(session); - }, - }; -}; + for (const dep of options.real) { + if (!deps.includes(dep)) { + throw new Error(`Real repository ${dep.name} is not a valid dependency`); + } + } + return (deps as ClassConstructor[]).map((dep) => { + if (options.real.includes(dep)) { + return this.get(dep); + } -export const newSyncTest = (options: SyncTestOptions) => { - const { sut, mocks, repos, getRepository } = newMediumService(SyncService, { - database: options.db, - repos: { - sync: 'real', - session: 'real', - }, - }); + if (options.mock.includes(dep)) { + return newMockRepository(dep); + } + }); + } - const testSync = async (auth: AuthDto, types: SyncRequestType[]) => { + get(key: ClassConstructor): T { + if (!this.repoCache[key.name]) { + const real = newRealRepository(key, this.options.database); + this.repoCache[key.name] = real; + } + + return this.repoCache[key.name]; + } + + getMock>(key: ClassConstructor): R { + const index = BASE_SERVICE_DEPENDENCIES.indexOf(key as any); + if (index === -1 || !this.options.mock.includes(key)) { + throw new Error(`getMock called with a key that is not a mock: ${key.name}`); + } + + return this.sutDeps[index] as R; + } + + async newUser(dto: Partial> = {}) { + const user = mediumFactory.userInsert(dto); + const result = await this.get(UserRepository).create(user); + return { user, result }; + } + + async newPartner(dto: { sharedById: string; sharedWithId: string; inTimeline?: boolean }) { + const partner = { inTimeline: true, ...dto }; + const result = await this.get(PartnerRepository).create(partner); + return { partner, result }; + } + + async newAsset(dto: Partial> = {}) { + const asset = mediumFactory.assetInsert(dto); + const result = await this.get(AssetRepository).create(asset); + return { asset, result }; + } + + async newExif(dto: Insertable) { + const result = await this.get(AssetRepository).upsertExif(dto); + return { result }; + } + + async newAlbum(dto: Insertable) { + const album = mediumFactory.albumInsert(dto); + const result = await this.get(AlbumRepository).create(album, [], []); + return { album, result }; + } + + async newAlbumAsset(albumAsset: { albumId: string; assetId: string }) { + const result = await this.get(AlbumRepository).addAssetIds(albumAsset.albumId, [albumAsset.assetId]); + return { albumAsset, result }; + } + + async newAlbumUser(dto: { albumId: string; userId: string; role?: AlbumUserRole }) { + const { albumId, userId, role = AlbumUserRole.EDITOR } = dto; + const result = await this.get(AlbumUserRepository).create({ albumsId: albumId, usersId: userId, role }); + return { albumUser: { albumId, userId, role }, result }; + } + + async newJobStatus(dto: Partial> & { assetId: string }) { + const jobStatus = mediumFactory.assetJobStatusInsert({ assetId: dto.assetId }); + const result = await this.get(AssetRepository).upsertJobStatus(jobStatus); + return { jobStatus, result }; + } + + async newPerson(dto: Partial> & { ownerId: string }) { + const person = mediumFactory.personInsert(dto); + const result = await this.get(PersonRepository).create(person); + return { person, result }; + } + + async newSession(dto: Partial> & { userId: string }) { + const session = mediumFactory.sessionInsert(dto); + const result = await this.get(SessionRepository).create(session); + return { session, result }; + } + + async newSyncAuthUser() { + const { user } = await this.newUser(); + const { session } = await this.newSession({ userId: user.id }); + const auth = factory.auth({ + session, + user: { + id: user.id, + name: user.name, + email: user.email, + }, + }); + + return { + auth, + session, + user, + }; + } +} + +export class SyncTestContext extends MediumTestContext { + constructor(database: Kysely) { + super(SyncService, { database, real: [SyncRepository, SessionRepository], mock: [LoggingRepository] }); + } + + async syncStream(auth: AuthDto, types: SyncRequestType[]) { const stream = mediumFactory.syncStream(); // Wait for 2ms to ensure all updates are available and account for setTimeout inaccuracy await wait(2); - await sut.stream(auth, stream, { types }); + await this.sut.stream(auth, stream, { types }); return stream.getResponse(); - }; - - return { - sut, - mocks, - repos, - getRepository, - testSync, - }; -}; - -export const newMediumService = ( - Service: ClassConstructor, - options: { - database: Kysely; - repos: R; - }, -): Context => { - const repos: Partial = {}; - const mocks: Partial = {}; - - const loggerMock = getRepositoryMock('logger') as Mocked; - loggerMock.setContext.mockImplementation(() => {}); - repos.logger = loggerMock; - - for (const [_key, type] of Object.entries(options.repos)) { - if (type === 'real') { - const key = _key as keyof RepositoriesTypes; - repos[key] = getRepository(key, options.database) as any; - continue; - } - - if (type === 'mock') { - const key = _key as keyof RepositoryMocks; - mocks[key] = getRepositoryMock(key) as any; - continue; - } } - const makeRepository = (key: K) => { - return repos[key] || getRepository(key, options.database); - }; + async syncAckAll(auth: AuthDto, response: Array<{ type: string; ack: string }>) { + const acks: Record = {}; + for (const { type, ack } of response) { + acks[type] = ack; + } - const deps = asDeps({ ...mocks, ...repos } as ServiceOverrides); - const sut = new Service(...deps); + await this.sut.setAcks(auth, { acks: Object.values(acks) }); + } +} - return { - sut, - mocks, - repos, - getRepository: makeRepository, - } as Context; -}; - -export const getRepository = (key: K, db: Kysely) => { +const newRealRepository = (key: ClassConstructor, db: Kysely): T => { switch (key) { - case 'access': { - return new AccessRepository(db); + case AccessRepository: + case AlbumRepository: + case AlbumUserRepository: + case ActivityRepository: + case AssetRepository: + case AssetJobRepository: + case MemoryRepository: + case NotificationRepository: + case PartnerRepository: + case PersonRepository: + case SearchRepository: + case SessionRepository: + case SyncRepository: + case SystemMetadataRepository: + case UserRepository: + case VersionHistoryRepository: { + return new key(db); } - case 'activity': { - return new ActivityRepository(db); + case ConfigRepository: + case CryptoRepository: { + return new key(); } - case 'album': { - return new AlbumRepository(db); + case DatabaseRepository: { + return new key(db, LoggingRepository.create(), new ConfigRepository()); } - case 'albumUser': { - return new AlbumUserRepository(db); + case EmailRepository: { + return new key(LoggingRepository.create()); } - case 'asset': { - return new AssetRepository(db); - } - - case 'assetJob': { - return new AssetJobRepository(db); - } - - case 'config': { - return new ConfigRepository(); - } - - case 'crypto': { - return new CryptoRepository(); - } - - case 'database': { - return new DatabaseRepository(db, LoggingRepository.create(), new ConfigRepository()); - } - - case 'email': { - return new EmailRepository(LoggingRepository.create()); - } - - case 'logger': { - return LoggingRepository.create(); - } - - case 'memory': { - return new MemoryRepository(db); - } - - case 'notification': { - return new NotificationRepository(db); - } - - case 'partner': { - return new PartnerRepository(db); - } - - case 'person': { - return new PersonRepository(db); - } - - case 'search': { - return new SearchRepository(db); - } - - case 'session': { - return new SessionRepository(db); - } - - case 'sync': { - return new SyncRepository(db); - } - - case 'systemMetadata': { - return new SystemMetadataRepository(db); - } - - case 'user': { - return new UserRepository(db); - } - - case 'versionHistory': { - return new VersionHistoryRepository(db); + case LoggingRepository as unknown as ClassConstructor: { + return new key() as unknown as T; } default: { - throw new Error(`Invalid repository key: ${key}`); + throw new Error(`Unable to create repository instance for key: ${key?.name || key}`); } } }; -const getRepositoryMock = (key: K) => { +const newMockRepository = (key: ClassConstructor) => { switch (key) { - case 'activity': { - return automock(ActivityRepository) as Mocked>; + case ActivityRepository: + case AlbumRepository: + case AssetRepository: + case AssetJobRepository: + case ConfigRepository: + case CryptoRepository: + case MemoryRepository: + case NotificationRepository: + case PartnerRepository: + case PersonRepository: + case SessionRepository: + case SyncRepository: + case SystemMetadataRepository: + case UserRepository: + case VersionHistoryRepository: { + return automock(key); } - case 'album': { - return automock(AlbumRepository); - } - - case 'asset': { - return automock(AssetRepository); - } - - case 'assetJob': { - return automock(AssetJobRepository); - } - - case 'config': { - return automock(ConfigRepository); - } - - case 'crypto': { - return automock(CryptoRepository); - } - - case 'database': { + case DatabaseRepository: { return automock(DatabaseRepository, { - args: [ - undefined, - { - setContext: () => {}, - }, - { getEnv: () => ({ database: { vectorExtension: '' } }) }, - ], + args: [undefined, { setContext: () => {} }, { getEnv: () => ({ database: { vectorExtension: '' } }) }], }); } - case 'email': { - return automock(EmailRepository, { - args: [ - { - setContext: () => {}, - }, - ], - }); + case EmailRepository: { + return automock(EmailRepository, { args: [{ setContext: () => {} }] }); } - case 'job': { + case JobRepository: { return automock(JobRepository, { args: [ undefined, @@ -335,106 +300,21 @@ const getRepositoryMock = (key: K) => { }); } - case 'logger': { + case LoggingRepository as unknown as ClassConstructor: { const configMock = { getEnv: () => ({ noColor: false }) }; return automock(LoggingRepository, { args: [undefined, configMock], strict: false }); } - case 'memory': { - return automock(MemoryRepository); - } - - case 'notification': { - return automock(NotificationRepository); - } - - case 'partner': { - return automock(PartnerRepository); - } - - case 'person': { - return automock(PersonRepository); - } - - case 'session': { - return automock(SessionRepository); - } - - case 'storage': { + case StorageRepository: { return automock(StorageRepository, { args: [{ setContext: () => {} }] }); } - case 'sync': { - return automock(SyncRepository); - } - - case 'systemMetadata': { - return automock(SystemMetadataRepository); - } - - case 'user': { - return automock(UserRepository); - } - - case 'versionHistory': { - return automock(VersionHistoryRepository); - } - default: { throw new Error(`Invalid repository key: ${key}`); } } }; -export const asDeps = (repositories: ServiceOverrides) => { - return [ - repositories.logger || getRepositoryMock('logger'), // logger - repositories.access, // access - repositories.activity || getRepositoryMock('activity'), - repositories.album || getRepositoryMock('album'), - repositories.albumUser, - repositories.apiKey, - repositories.asset || getRepositoryMock('asset'), - repositories.assetJob || getRepositoryMock('assetJob'), - repositories.audit, - repositories.config || getRepositoryMock('config'), - repositories.cron, - repositories.crypto || getRepositoryMock('crypto'), - repositories.database || getRepositoryMock('database'), - repositories.downloadRepository, - repositories.duplicateRepository, - repositories.email || getRepositoryMock('email'), - repositories.event, - repositories.job || getRepositoryMock('job'), - repositories.library, - repositories.machineLearning, - repositories.map, - repositories.media, - repositories.memory || getRepositoryMock('memory'), - repositories.metadata, - repositories.move, - repositories.notification || getRepositoryMock('notification'), - repositories.oauth, - repositories.partner || getRepositoryMock('partner'), - repositories.person || getRepositoryMock('person'), - repositories.process, - repositories.search, - repositories.serverInfo, - repositories.session || getRepositoryMock('session'), - repositories.sharedLink, - repositories.stack, - repositories.storage || getRepositoryMock('storage'), - repositories.sync || getRepositoryMock('sync'), - repositories.systemMetadata || getRepositoryMock('systemMetadata'), - repositories.tag, - repositories.telemetry, - repositories.trash, - repositories.user, - repositories.versionHistory || getRepositoryMock('versionHistory'), - repositories.view, - ]; -}; - const assetInsert = (asset: Partial> = {}) => { const id = asset.id || newUuid(); const now = newDate(); @@ -544,6 +424,8 @@ const personInsert = (person: Partial> & { ownerId: string }) }; }; +const sha256 = (value: string) => createHash('sha256').update(value).digest('base64'); + const sessionInsert = ({ id = newUuid(), userId, ...session }: Partial> & { userId: string }) => { const defaults: Insertable = { id, diff --git a/server/test/medium/specs/services/asset.service.spec.ts b/server/test/medium/specs/services/asset.service.spec.ts index 7be431f5f2..db9a7de082 100644 --- a/server/test/medium/specs/services/asset.service.spec.ts +++ b/server/test/medium/specs/services/asset.service.spec.ts @@ -1,44 +1,33 @@ import { Kysely } from 'kysely'; import { DB } from 'src/db'; import { AssetRepository } from 'src/repositories/asset.repository'; -import { UserRepository } from 'src/repositories/user.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; import { AssetService } from 'src/services/asset.service'; -import { mediumFactory, newMediumService } from 'test/medium.factory'; +import { newMediumService } from 'test/medium.factory'; import { factory } from 'test/small.factory'; import { getKyselyDB } from 'test/utils'; -describe(AssetService.name, () => { - let defaultDatabase: Kysely; - let assetRepo: AssetRepository; - let userRepo: UserRepository; +let defaultDatabase: Kysely; - const createSut = (db?: Kysely) => { - return newMediumService(AssetService, { - database: db || defaultDatabase, - repos: { - asset: 'real', - }, - }); - }; - - beforeAll(async () => { - defaultDatabase = await getKyselyDB(); - - assetRepo = new AssetRepository(defaultDatabase); - userRepo = new UserRepository(defaultDatabase); +const setup = (db?: Kysely) => { + return newMediumService(AssetService, { + database: db || defaultDatabase, + real: [AssetRepository], + mock: [LoggingRepository], }); +}; +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(AssetService.name, () => { describe('getStatistics', () => { it('should return stats as numbers, not strings', async () => { - const { sut } = createSut(); - - const user = mediumFactory.userInsert(); - const asset = mediumFactory.assetInsert({ ownerId: user.id }); - - await userRepo.create(user); - await assetRepo.create(asset); - await assetRepo.upsertExif({ assetId: asset.id, fileSizeInByte: 12_345 }); - + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, fileSizeInByte: 12_345 }); const auth = factory.auth({ user: { id: user.id } }); await expect(sut.getStatistics(auth, {})).resolves.toEqual({ images: 1, total: 1, videos: 0 }); }); diff --git a/server/test/medium/specs/services/audit.database.spec.ts b/server/test/medium/specs/services/audit.database.spec.ts index 33ae38ec6f..377736408c 100644 --- a/server/test/medium/specs/services/audit.database.spec.ts +++ b/server/test/medium/specs/services/audit.database.spec.ts @@ -1,74 +1,63 @@ -import { Kysely } from 'kysely'; -import { DB } from 'src/db'; -import { AssetRepository } from 'src/repositories/asset.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; import { PartnerRepository } from 'src/repositories/partner.repository'; import { UserRepository } from 'src/repositories/user.repository'; import { partners_delete_audit } from 'src/schema/functions'; -import { mediumFactory } from 'test/medium.factory'; +import { BaseService } from 'src/services/base.service'; +import { MediumTestContext } from 'test/medium.factory'; import { getKyselyDB } from 'test/utils'; describe('audit', () => { - let defaultDatabase: Kysely; - let assetRepo: AssetRepository; - let userRepo: UserRepository; - let partnerRepo: PartnerRepository; + let ctx: MediumTestContext; beforeAll(async () => { - defaultDatabase = await getKyselyDB(); - - assetRepo = new AssetRepository(defaultDatabase); - userRepo = new UserRepository(defaultDatabase); - partnerRepo = new PartnerRepository(defaultDatabase); + ctx = new MediumTestContext(BaseService, { + database: await getKyselyDB(), + real: [], + mock: [LoggingRepository], + }); }); describe(partners_delete_audit.name, () => { it('should not cascade user deletes to partners_audit', async () => { - const user1 = mediumFactory.userInsert(); - const user2 = mediumFactory.userInsert(); - - await Promise.all([userRepo.create(user1), userRepo.create(user2)]); + const partnerRepo = ctx.get(PartnerRepository); + const userRepo = ctx.get(UserRepository); + const { user: user1 } = await ctx.newUser(); + const { user: user2 } = await ctx.newUser(); await partnerRepo.create({ sharedById: user1.id, sharedWithId: user2.id }); await userRepo.delete(user1, true); - await expect( - defaultDatabase.selectFrom('partners_audit').select(['id']).where('sharedById', '=', user1.id).execute(), + ctx.database.selectFrom('partners_audit').select(['id']).where('sharedById', '=', user1.id).execute(), ).resolves.toHaveLength(0); }); }); describe('assets_audit', () => { it('should not cascade user deletes to assets_audit', async () => { - const user = mediumFactory.userInsert(); - const asset = mediumFactory.assetInsert({ ownerId: user.id }); - - await userRepo.create(user); - await assetRepo.create(asset); + const userRepo = ctx.get(UserRepository); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); await userRepo.delete(user, true); - await expect( - defaultDatabase.selectFrom('assets_audit').select(['id']).where('assetId', '=', asset.id).execute(), + ctx.database.selectFrom('assets_audit').select(['id']).where('assetId', '=', asset.id).execute(), ).resolves.toHaveLength(0); }); }); describe('exif', () => { it('should automatically set updatedAt and updateId when the row is updated', async () => { - const user = mediumFactory.userInsert(); - const asset = mediumFactory.assetInsert({ ownerId: user.id }); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, make: 'Canon' }); - await userRepo.create(user); - await assetRepo.create(asset); - await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); - - const before = await defaultDatabase + const before = await ctx.database .selectFrom('exif') .select(['updatedAt', 'updateId']) .where('assetId', '=', asset.id) .executeTakeFirstOrThrow(); - await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon 2' }); + await ctx.newExif({ assetId: asset.id, make: 'Canon 2' }); - const after = await defaultDatabase + const after = await ctx.database .selectFrom('exif') .select(['updatedAt', 'updateId']) .where('assetId', '=', asset.id) diff --git a/server/test/medium/specs/services/memory.service.spec.ts b/server/test/medium/specs/services/memory.service.spec.ts index 8489e6bcc9..ff7249d930 100644 --- a/server/test/medium/specs/services/memory.service.spec.ts +++ b/server/test/medium/specs/services/memory.service.spec.ts @@ -2,71 +2,66 @@ import { Kysely } from 'kysely'; import { DateTime } from 'luxon'; import { DB } from 'src/db'; import { AssetFileType } from 'src/enum'; +import { AssetRepository } from 'src/repositories/asset.repository'; +import { DatabaseRepository } from 'src/repositories/database.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { MemoryRepository } from 'src/repositories/memory.repository'; +import { PartnerRepository } from 'src/repositories/partner.repository'; +import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; import { UserRepository } from 'src/repositories/user.repository'; import { MemoryService } from 'src/services/memory.service'; -import { mediumFactory, newMediumService } from 'test/medium.factory'; +import { newMediumService } from 'test/medium.factory'; import { getKyselyDB } from 'test/utils'; +let defaultDatabase: Kysely; + +const setup = (db?: Kysely) => { + return newMediumService(MemoryService, { + database: db || defaultDatabase, + real: [ + AssetRepository, + DatabaseRepository, + MemoryRepository, + UserRepository, + SystemMetadataRepository, + UserRepository, + PartnerRepository, + ], + mock: [LoggingRepository], + }); +}; + describe(MemoryService.name, () => { - let defaultDatabase: Kysely; - - const createSut = (db?: Kysely) => { - return newMediumService(MemoryService, { - database: db || defaultDatabase, - repos: { - asset: 'real', - database: 'real', - memory: 'real', - user: 'real', - systemMetadata: 'real', - partner: 'real', - }, - }); - }; - beforeEach(async () => { defaultDatabase = await getKyselyDB(); - const userRepo = new UserRepository(defaultDatabase); - const admin = mediumFactory.userInsert({ isAdmin: true }); - await userRepo.create(admin); }); describe('onMemoryCreate', () => { it('should work on an empty database', async () => { - const { sut } = createSut(); + const { sut } = setup(); await expect(sut.onMemoriesCreate()).resolves.not.toThrow(); }); it('should create a memory from an asset', async () => { - const { sut, repos, getRepository } = createSut(); - + const { sut, ctx } = setup(); + const assetRepo = ctx.get(AssetRepository); + const memoryRepo = ctx.get(MemoryRepository); const now = DateTime.fromObject({ year: 2025, month: 2, day: 25 }, { zone: 'utc' }) as DateTime; - const user = mediumFactory.userInsert(); - const asset = mediumFactory.assetInsert({ - ownerId: user.id, - localDateTime: now.minus({ years: 1 }).toISO(), - }); - const jobStatus = mediumFactory.assetJobStatusInsert({ assetId: asset.id }); - - const userRepo = getRepository('user'); - const assetRepo = getRepository('asset'); - - await userRepo.create(user); - await assetRepo.create(asset); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id, localDateTime: now.minus({ years: 1 }).toISO() }); await Promise.all([ - assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }), + ctx.newExif({ assetId: asset.id, make: 'Canon' }), + ctx.newJobStatus({ assetId: asset.id }), assetRepo.upsertFiles([ { assetId: asset.id, type: AssetFileType.PREVIEW, path: '/path/to/preview.jpg' }, { assetId: asset.id, type: AssetFileType.THUMBNAIL, path: '/path/to/thumbnail.jpg' }, ]), - assetRepo.upsertJobStatus(jobStatus), ]); vi.setSystemTime(now.toJSDate()); - await sut.onMemoriesCreate(); - const memories = await repos.memory.search(user.id, {}); + const memories = await memoryRepo.search(user.id, {}); expect(memories.length).toBe(1); expect(memories[0]).toEqual( expect.objectContaining({ @@ -88,16 +83,11 @@ describe(MemoryService.name, () => { }); it('should not generate a memory twice for the same day', async () => { - const { sut, repos, getRepository } = createSut(); - + const { sut, ctx } = setup(); + const assetRepo = ctx.get(AssetRepository); + const memoryRepo = ctx.get(MemoryRepository); const now = DateTime.fromObject({ year: 2025, month: 2, day: 20 }, { zone: 'utc' }) as DateTime; - - const assetRepo = getRepository('asset'); - const memoryRepo = getRepository('memory'); - - const user = mediumFactory.userInsert(); - await repos.user.create(user); - + const { user } = await ctx.newUser(); for (const dto of [ { ownerId: user.id, @@ -112,11 +102,10 @@ describe(MemoryService.name, () => { localDateTime: now.minus({ year: 1 }).plus({ days: 5 }).toISO(), }, ]) { - const asset = mediumFactory.assetInsert(dto); - await assetRepo.create(asset); + const { asset } = await ctx.newAsset(dto); await Promise.all([ - assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }), - assetRepo.upsertJobStatus(mediumFactory.assetJobStatusInsert({ assetId: asset.id })), + ctx.newExif({ assetId: asset.id, make: 'Canon' }), + ctx.newJobStatus({ assetId: asset.id }), assetRepo.upsertFiles([ { assetId: asset.id, type: AssetFileType.PREVIEW, path: '/path/to/preview.jpg' }, { assetId: asset.id, type: AssetFileType.THUMBNAIL, path: '/path/to/thumbnail.jpg' }, @@ -125,13 +114,13 @@ describe(MemoryService.name, () => { } vi.setSystemTime(now.toJSDate()); - await sut.onMemoriesCreate(); const memories = await memoryRepo.search(user.id, {}); expect(memories.length).toBe(1); await sut.onMemoriesCreate(); + const memoriesAfter = await memoryRepo.search(user.id, {}); expect(memoriesAfter.length).toBe(1); }); @@ -139,7 +128,7 @@ describe(MemoryService.name, () => { describe('onMemoriesCleanup', () => { it('should run without error', async () => { - const { sut } = createSut(); + const { sut } = setup(); await expect(sut.onMemoriesCleanup()).resolves.not.toThrow(); }); }); diff --git a/server/test/medium/specs/services/person.service.spec.ts b/server/test/medium/specs/services/person.service.spec.ts index bd5eb5d543..2a1080704c 100644 --- a/server/test/medium/specs/services/person.service.spec.ts +++ b/server/test/medium/specs/services/person.service.spec.ts @@ -1,90 +1,80 @@ import { Kysely } from 'kysely'; import { DB } from 'src/db'; +import { AccessRepository } from 'src/repositories/access.repository'; +import { DatabaseRepository } from 'src/repositories/database.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { PersonRepository } from 'src/repositories/person.repository'; +import { StorageRepository } from 'src/repositories/storage.repository'; import { PersonService } from 'src/services/person.service'; -import { mediumFactory, newMediumService } from 'test/medium.factory'; +import { newMediumService } from 'test/medium.factory'; import { factory } from 'test/small.factory'; import { getKyselyDB } from 'test/utils'; -describe.concurrent(PersonService.name, () => { - let defaultDatabase: Kysely; +let defaultDatabase: Kysely; - const createSut = (db?: Kysely) => { - return newMediumService(PersonService, { - database: db || defaultDatabase, - repos: { - access: 'real', - database: 'real', - person: 'real', - storage: 'mock', - }, - }); - }; - - beforeEach(async () => { - defaultDatabase = await getKyselyDB(); +const setup = (db?: Kysely) => { + return newMediumService(PersonService, { + database: db || defaultDatabase, + real: [AccessRepository, DatabaseRepository, PersonRepository], + mock: [LoggingRepository, StorageRepository], }); +}; +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(PersonService.name, () => { describe('delete', () => { it('should throw an error when there is no access', async () => { - const { sut } = createSut(); + const { sut } = setup(); const auth = factory.auth(); const personId = factory.uuid(); await expect(sut.delete(auth, personId)).rejects.toThrow('Not found or no person.delete access'); }); it('should delete the person', async () => { - const { sut, getRepository, mocks } = createSut(); - - const user = mediumFactory.userInsert(); + const { sut, ctx } = setup(); + const personRepo = ctx.get(PersonRepository); + const storageMock = ctx.getMock(StorageRepository); + const { user } = await ctx.newUser(); + const { person } = await ctx.newPerson({ ownerId: user.id }); const auth = factory.auth({ user }); - const person = mediumFactory.personInsert({ ownerId: auth.user.id }); - mocks.storage.unlink.mockResolvedValue(); - - const userRepo = getRepository('user'); - await userRepo.create(user); - - const personRepo = getRepository('person'); - await personRepo.create(person); + storageMock.unlink.mockResolvedValue(); await expect(personRepo.getById(person.id)).resolves.toEqual(expect.objectContaining({ id: person.id })); await expect(sut.delete(auth, person.id)).resolves.toBeUndefined(); await expect(personRepo.getById(person.id)).resolves.toBeUndefined(); - expect(mocks.storage.unlink).toHaveBeenCalledWith(person.thumbnailPath); + expect(storageMock.unlink).toHaveBeenCalledWith(person.thumbnailPath); }); }); describe('deleteAll', () => { it('should throw an error when there is no access', async () => { - const { sut } = createSut(); + const { sut } = setup(); const auth = factory.auth(); const personId = factory.uuid(); await expect(sut.deleteAll(auth, { ids: [personId] })).rejects.toThrow('Not found or no person.delete access'); }); it('should delete the person', async () => { - const { sut, getRepository, mocks } = createSut(); - - const user = mediumFactory.userInsert(); + const { sut, ctx } = setup(); + const storageMock = ctx.getMock(StorageRepository); + const personRepo = ctx.get(PersonRepository); + const { user } = await ctx.newUser(); + const { person: person1 } = await ctx.newPerson({ ownerId: user.id }); + const { person: person2 } = await ctx.newPerson({ ownerId: user.id }); const auth = factory.auth({ user }); - const person1 = mediumFactory.personInsert({ ownerId: auth.user.id }); - const person2 = mediumFactory.personInsert({ ownerId: auth.user.id }); - mocks.storage.unlink.mockResolvedValue(); - - const userRepo = getRepository('user'); - await userRepo.create(user); - - const personRepo = getRepository('person'); - await personRepo.create(person1); - await personRepo.create(person2); + storageMock.unlink.mockResolvedValue(); await expect(sut.deleteAll(auth, { ids: [person1.id, person2.id] })).resolves.toBeUndefined(); await expect(personRepo.getById(person1.id)).resolves.toBeUndefined(); await expect(personRepo.getById(person2.id)).resolves.toBeUndefined(); - expect(mocks.storage.unlink).toHaveBeenCalledTimes(2); - expect(mocks.storage.unlink).toHaveBeenCalledWith(person1.thumbnailPath); - expect(mocks.storage.unlink).toHaveBeenCalledWith(person2.thumbnailPath); + expect(storageMock.unlink).toHaveBeenCalledTimes(2); + expect(storageMock.unlink).toHaveBeenCalledWith(person1.thumbnailPath); + expect(storageMock.unlink).toHaveBeenCalledWith(person2.thumbnailPath); }); }); }); diff --git a/server/test/medium/specs/services/user.service.spec.ts b/server/test/medium/specs/services/user.service.spec.ts index 0113c70158..a16ea3c877 100644 --- a/server/test/medium/specs/services/user.service.spec.ts +++ b/server/test/medium/specs/services/user.service.spec.ts @@ -2,72 +2,65 @@ import { Kysely } from 'kysely'; import { DateTime } from 'luxon'; import { DB } from 'src/db'; import { ImmichEnvironment, JobName, JobStatus } from 'src/enum'; +import { ConfigRepository } from 'src/repositories/config.repository'; +import { CryptoRepository } from 'src/repositories/crypto.repository'; +import { JobRepository } from 'src/repositories/job.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; +import { UserRepository } from 'src/repositories/user.repository'; import { UserService } from 'src/services/user.service'; import { mediumFactory, newMediumService } from 'test/medium.factory'; import { factory } from 'test/small.factory'; import { getKyselyDB } from 'test/utils'; -describe(UserService.name, () => { - let defaultDatabase: Kysely; +let defaultDatabase: Kysely; - const createSut = (db?: Kysely) => { - process.env.IMMICH_ENV = ImmichEnvironment.TESTING; +const setup = (db?: Kysely) => { + process.env.IMMICH_ENV = ImmichEnvironment.TESTING; - return newMediumService(UserService, { - database: db || defaultDatabase, - repos: { - user: 'real', - crypto: 'real', - config: 'real', - job: 'mock', - systemMetadata: 'real', - }, - }); - }; - - beforeAll(async () => { - defaultDatabase = await getKyselyDB(); - const { repos } = createSut(); - await repos.user.create({ isAdmin: true, email: 'admin@immich.cloud' }); + return newMediumService(UserService, { + database: db || defaultDatabase, + real: [CryptoRepository, ConfigRepository, SystemMetadataRepository, UserRepository], + mock: [LoggingRepository, JobRepository], }); +}; +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); + const { ctx } = setup(); + await ctx.newUser({ isAdmin: true, email: 'admin@immich.cloud' }); +}); + +describe(UserService.name, () => { describe('create', () => { it('should create a user', async () => { - const { sut } = createSut(); + const { sut } = setup(); const user = mediumFactory.userInsert(); - await expect(sut.createUser({ name: user.name, email: user.email })).resolves.toEqual( expect.objectContaining({ name: user.name, email: user.email }), ); }); it('should reject user with duplicate email', async () => { - const { sut } = createSut(); - + const { sut } = setup(); const user = mediumFactory.userInsert(); - await expect(sut.createUser({ email: user.email })).resolves.toMatchObject({ email: user.email }); await expect(sut.createUser({ email: user.email })).rejects.toThrow('User exists'); }); it('should not return password', async () => { - const { sut } = createSut(); + const { sut } = setup(); const dto = mediumFactory.userInsert({ password: 'password' }); - const user = await sut.createUser({ email: dto.email, password: 'password' }); - expect((user as any).password).toBeUndefined(); }); }); describe('search', () => { it('should get users', async () => { - const { sut, repos } = createSut(); - const user1 = mediumFactory.userInsert(); - const user2 = mediumFactory.userInsert(); - - await Promise.all([repos.user.create(user1), repos.user.create(user2)]); - + const { sut, ctx } = setup(); + const { user: user1 } = await ctx.newUser(); + const { user: user2 } = await ctx.newUser(); const auth = factory.auth({ user: user1 }); await expect(sut.search(auth)).resolves.toEqual( @@ -81,10 +74,8 @@ describe(UserService.name, () => { describe('get', () => { it('should get a user', async () => { - const { sut, repos } = createSut(); - const user = mediumFactory.userInsert(); - - await repos.user.create(user); + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); await expect(sut.get(user.id)).resolves.toEqual( expect.objectContaining({ @@ -96,11 +87,8 @@ describe(UserService.name, () => { }); it('should not return password', async () => { - const { sut, repos } = createSut(); - const user = mediumFactory.userInsert(); - - await repos.user.create(user); - + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); const result = await sut.get(user.id); expect((result as any).password).toBeUndefined(); @@ -109,10 +97,9 @@ describe(UserService.name, () => { describe('updateMe', () => { it('should update a user', async () => { - const { sut, repos: repositories } = createSut(); - - const before = await repositories.user.create(mediumFactory.userInsert()); - const auth = factory.auth({ user: { id: before.id } }); + const { sut, ctx } = setup(); + const { user, result: before } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); const after = await sut.updateMe(auth, { name: `${before.name} Updated` }); expect(before.updatedAt).toBeDefined(); @@ -128,17 +115,13 @@ describe(UserService.name, () => { activationKey: 'KuX8KsktrBSiXpQMAH0zLgA5SpijXVr_PDkzLdWUlAogCTMBZ0I3KCHXK0eE9EEd7harxup8_EHMeqAWeHo5VQzol6LGECpFv585U9asXD4Zc-UXt3mhJr2uhazqipBIBwJA2YhmUCDy8hiyiGsukDQNu9Rg9C77UeoKuZBWVjWUBWG0mc1iRqfvF0faVM20w53czAzlhaMxzVGc3Oimbd7xi_CAMSujF_2y8QpA3X2fOVkQkzdcH9lV0COejl7IyH27zQQ9HrlrXv3Lai5Hw67kNkaSjmunVBxC5PS0TpKoc9SfBJMaAGWnaDbjhjYUrm-8nIDQnoeEAidDXVAdPw', }; - const { sut, repos } = createSut(); - const user = mediumFactory.userInsert(); - await repos.user.create(user); + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); const auth = factory.auth({ user: { id: user.id } }); - await expect(sut.getLicense(auth)).rejects.toThrowError(); const after = await sut.setLicense(auth, license); - expect(after.licenseKey).toEqual(license.licenseKey); expect(after.activationKey).toEqual(license.activationKey); - const getResponse = await sut.getLicense(auth); expect(getResponse).toEqual(after); }); @@ -146,7 +129,7 @@ describe(UserService.name, () => { describe.sequential('handleUserDeleteCheck', () => { beforeEach(async () => { - const { sut } = createSut(); + const { sut } = setup(); // These tests specifically have to be sequential otherwise we hit race conditions with config changes applying in incorrect tests const config = await sut.getConfig({ withCache: false }); config.user.deleteDelay = 7; @@ -154,52 +137,43 @@ describe(UserService.name, () => { }); it('should work when there are no deleted users', async () => { - const { sut, mocks } = createSut(); - mocks.job.queueAll.mockResolvedValue(void 0); - + const { sut, ctx } = setup(); + const jobMock = ctx.getMock(JobRepository); + jobMock.queueAll.mockResolvedValue(void 0); await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS); - - expect(mocks.job.queueAll).toHaveBeenCalledExactlyOnceWith([]); + expect(jobMock.queueAll).toHaveBeenCalledExactlyOnceWith([]); }); it('should work when there is a user to delete', async () => { - const { sut, repos, mocks } = createSut(await getKyselyDB()); - mocks.job.queueAll.mockResolvedValue(void 0); - const user = mediumFactory.userInsert({ deletedAt: DateTime.now().minus({ days: 60 }).toJSDate() }); - await repos.user.create(user); - + const { sut, ctx } = setup(await getKyselyDB()); + const jobMock = ctx.getMock(JobRepository); + const { user } = await ctx.newUser({ deletedAt: DateTime.now().minus({ days: 60 }).toJSDate() }); + jobMock.queueAll.mockResolvedValue(void 0); await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS); - - expect(mocks.job.queueAll).toHaveBeenCalledExactlyOnceWith([ + expect(jobMock.queueAll).toHaveBeenCalledExactlyOnceWith([ { name: JobName.USER_DELETION, data: { id: user.id } }, ]); }); it('should skip a recently deleted user', async () => { - const { sut, repos, mocks } = createSut(await getKyselyDB()); - mocks.job.queueAll.mockResolvedValue(void 0); - const user = mediumFactory.userInsert({ deletedAt: DateTime.now().minus({ days: 5 }).toJSDate() }); - await repos.user.create(user); - + const { sut, ctx } = setup(await getKyselyDB()); + const jobMock = ctx.getMock(JobRepository); + await ctx.newUser({ deletedAt: DateTime.now().minus({ days: 5 }).toJSDate() }); + jobMock.queueAll.mockResolvedValue(void 0); await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS); - - expect(mocks.job.queueAll).toHaveBeenCalledExactlyOnceWith([]); + expect(jobMock.queueAll).toHaveBeenCalledExactlyOnceWith([]); }); it('should respect a custom user delete delay', async () => { - const { sut, repos, mocks } = createSut(await getKyselyDB()); - mocks.job.queueAll.mockResolvedValue(void 0); - const user = mediumFactory.userInsert({ deletedAt: DateTime.now().minus({ days: 25 }).toJSDate() }); - await repos.user.create(user); - + const { sut, ctx } = setup(await getKyselyDB()); + const jobMock = ctx.getMock(JobRepository); + await ctx.newUser({ deletedAt: DateTime.now().minus({ days: 25 }).toJSDate() }); + jobMock.queueAll.mockResolvedValue(void 0); const config = await sut.getConfig({ withCache: false }); config.user.deleteDelay = 30; - await sut.updateConfig(config); - await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS); - - expect(mocks.job.queueAll).toHaveBeenCalledExactlyOnceWith([]); + expect(jobMock.queueAll).toHaveBeenCalledExactlyOnceWith([]); }); }); }); diff --git a/server/test/medium/specs/services/version.service.spec.ts b/server/test/medium/specs/services/version.service.spec.ts index f99e439e1f..6e9984cc50 100644 --- a/server/test/medium/specs/services/version.service.spec.ts +++ b/server/test/medium/specs/services/version.service.spec.ts @@ -2,38 +2,40 @@ import { Kysely } from 'kysely'; import { serverVersion } from 'src/constants'; import { DB } from 'src/db'; import { JobName } from 'src/enum'; +import { DatabaseRepository } from 'src/repositories/database.repository'; +import { JobRepository } from 'src/repositories/job.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; import { VersionService } from 'src/services/version.service'; import { newMediumService } from 'test/medium.factory'; import { getKyselyDB } from 'test/utils'; -describe(VersionService.name, () => { - let defaultDatabase: Kysely; +let defaultDatabase: Kysely; - const setup = (db?: Kysely) => { - return newMediumService(VersionService, { - database: db || defaultDatabase, - repos: { - job: 'mock', - database: 'real', - versionHistory: 'real', - }, - }); - }; - - beforeAll(async () => { - defaultDatabase = await getKyselyDB(); +const setup = (db?: Kysely) => { + return newMediumService(VersionService, { + database: db || defaultDatabase, + real: [DatabaseRepository, VersionHistoryRepository], + mock: [LoggingRepository, JobRepository], }); +}; +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(VersionService.name, () => { describe('onBootstrap', () => { it('record the current version on startup', async () => { - const { sut, repos } = setup(); + const { sut, ctx } = setup(); + const versionHistoryRepo = ctx.get(VersionHistoryRepository); - const itemsBefore = await repos.versionHistory.getAll(); + const itemsBefore = await versionHistoryRepo.getAll(); expect(itemsBefore).toHaveLength(0); await sut.onBootstrap(); - const itemsAfter = await repos.versionHistory.getAll(); + const itemsAfter = await versionHistoryRepo.getAll(); expect(itemsAfter).toHaveLength(1); expect(itemsAfter[0]).toEqual({ createdAt: expect.any(Date), @@ -43,22 +45,26 @@ describe(VersionService.name, () => { }); it('should queue memory creation when upgrading from 1.128.0', async () => { - const { sut, repos, mocks } = setup(); - mocks.job.queue.mockResolvedValue(void 0); + const { sut, ctx } = setup(); + const jobMock = ctx.getMock(JobRepository); + const versionHistoryRepo = ctx.get(VersionHistoryRepository); + jobMock.queue.mockResolvedValue(void 0); - await repos.versionHistory.create({ version: 'v1.128.0' }); + await versionHistoryRepo.create({ version: 'v1.128.0' }); await sut.onBootstrap(); - expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.MEMORIES_CREATE }); + expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.MEMORIES_CREATE }); }); it('should not queue memory creation when upgrading from 1.129.0', async () => { - const { sut, repos, mocks } = setup(); + const { sut, ctx } = setup(); + const jobMock = ctx.getMock(JobRepository); + const versionHistoryRepo = ctx.get(VersionHistoryRepository); - await repos.versionHistory.create({ version: 'v1.129.0' }); + await versionHistoryRepo.create({ version: 'v1.129.0' }); await sut.onBootstrap(); - expect(mocks.job.queue).not.toHaveBeenCalled(); + expect(jobMock.queue).not.toHaveBeenCalled(); }); }); }); diff --git a/server/test/medium/specs/sync/sync-album-asset-exif.spec.ts b/server/test/medium/specs/sync/sync-album-asset-exif.spec.ts index 07ed8e5785..811dbb374b 100644 --- a/server/test/medium/specs/sync/sync-album-asset-exif.spec.ts +++ b/server/test/medium/specs/sync/sync-album-asset-exif.spec.ts @@ -1,158 +1,118 @@ import { Kysely } from 'kysely'; import { DB } from 'src/db'; import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum'; -import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory'; +import { SyncTestContext } from 'test/medium.factory'; import { factory } from 'test/small.factory'; import { getKyselyDB, wait } from 'test/utils'; let defaultDatabase: Kysely; const setup = async (db?: Kysely) => { - const database = db || defaultDatabase; - const result = newSyncTest({ db: database }); - const { auth, create } = newSyncAuthUser(); - await create(database); - return { ...result, auth }; + const ctx = new SyncTestContext(db || defaultDatabase); + const { auth, user, session } = await ctx.newSyncAuthUser(); + return { auth, user, session, ctx }; }; beforeAll(async () => { defaultDatabase = await getKyselyDB(); }); -describe.concurrent(SyncRequestType.AlbumAssetExifsV1, () => { +describe(SyncRequestType.AlbumAssetExifsV1, () => { it('should detect and sync the first album asset exif', async () => { - const { auth, sut, getRepository, testSync } = await setup(); + const { auth, ctx } = await setup(); + const { user: user2 } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user2.id }); + await ctx.newExif({ assetId: asset.id, make: 'Canon' }); + const { album } = await ctx.newAlbum({ ownerId: user2.id }); + await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); + await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const albumRepo = getRepository('album'); - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: user2.id }); - await assetRepo.create(asset); - await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); - await albumRepo.create({ ownerId: user2.id }, [asset.id], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumAssetExifsV1]); - - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - assetId: asset.id, - city: null, - country: null, - dateTimeOriginal: null, - description: '', - exifImageHeight: null, - exifImageWidth: null, - exposureTime: null, - fNumber: null, - fileSizeInByte: null, - focalLength: null, - fps: null, - iso: null, - latitude: null, - lensModel: null, - longitude: null, - make: 'Canon', - model: null, - modifyDate: null, - orientation: null, - profileDescription: null, - projectionType: null, - rating: null, - state: null, - timeZone: null, - }, - type: SyncEntityType.AlbumAssetExifV1, + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + assetId: asset.id, + city: null, + country: null, + dateTimeOriginal: null, + description: '', + exifImageHeight: null, + exifImageWidth: null, + exposureTime: null, + fNumber: null, + fileSizeInByte: null, + focalLength: null, + fps: null, + iso: null, + latitude: null, + lensModel: null, + longitude: null, + make: 'Canon', + model: null, + modifyDate: null, + orientation: null, + profileDescription: null, + projectionType: null, + rating: null, + state: null, + timeZone: null, }, - ]), - ); + type: SyncEntityType.AlbumAssetExifV1, + }, + ]); - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.AlbumAssetExifsV1]); - - expect(ackSyncResponse).toEqual([]); + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([]); }); it('should sync album asset exif for own user', async () => { - const { auth, getRepository, testSync } = await setup(); + const { auth, ctx } = await setup(); + const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); + await ctx.newExif({ assetId: asset.id, make: 'Canon' }); + const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); + await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); - const albumRepo = getRepository('album'); - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); - await assetRepo.create(asset); - await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); - await albumRepo.create({ ownerId: auth.user.id }, [asset.id], []); - - await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); - await expect(testSync(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toHaveLength(1); + await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toHaveLength(1); }); it('should not sync album asset exif for unrelated user', async () => { - const { auth, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - const user3 = mediumFactory.userInsert(); - await userRepo.create(user2); - await userRepo.create(user3); - - const albumRepo = getRepository('album'); - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: user3.id }); - await assetRepo.create(asset); - await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); - await albumRepo.create({ ownerId: user2.id }, [asset.id], [{ userId: user3.id, role: AlbumUserRole.EDITOR }]); - - const sessionRepo = getRepository('session'); - const session = mediumFactory.sessionInsert({ userId: user3.id }); - await sessionRepo.create(session); - + const { auth, ctx } = await setup(); + const { user: user2 } = await ctx.newUser(); + const { user: user3 } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user3.id }); + await ctx.newExif({ assetId: asset.id, make: 'Canon' }); + const { album } = await ctx.newAlbum({ ownerId: user2.id }); + await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); + await ctx.newAlbumUser({ albumId: album.id, userId: user3.id, role: AlbumUserRole.EDITOR }); + const { session } = await ctx.newSession({ userId: user3.id }); const authUser3 = factory.auth({ session, user: user3 }); - await expect(testSync(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); - await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toHaveLength(0); }); it('should backfill album assets exif when a user shares an album with you', async () => { - const { auth, sut, testSync, getRepository } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - const user3 = mediumFactory.userInsert(); - await userRepo.create(user2); - await userRepo.create(user3); - - const assetRepo = getRepository('asset'); - // Asset to check that we do backfill our own assets - const asset1Owner = mediumFactory.assetInsert({ ownerId: auth.user.id }); - const asset1User2 = mediumFactory.assetInsert({ ownerId: user2.id }); - const asset2User2 = mediumFactory.assetInsert({ ownerId: user2.id }); - const asset3User2 = mediumFactory.assetInsert({ ownerId: user2.id }); - await assetRepo.create(asset1Owner); - await assetRepo.upsertExif({ assetId: asset1Owner.id, make: 'asset1Owner' }); + const { auth, ctx } = await setup(); + const { user: user2 } = await ctx.newUser(); + const { asset: asset1Owner } = await ctx.newAsset({ ownerId: auth.user.id }); + await ctx.newExif({ assetId: asset1Owner.id, make: 'asset1Owner' }); await wait(2); - await assetRepo.create(asset1User2); - await assetRepo.upsertExif({ assetId: asset1User2.id, make: 'asset1User2' }); + const { asset: asset1User2 } = await ctx.newAsset({ ownerId: user2.id }); + await ctx.newExif({ assetId: asset1User2.id, make: 'asset1User2' }); await wait(2); - await assetRepo.create(asset2User2); - await assetRepo.upsertExif({ assetId: asset2User2.id, make: 'asset2User2' }); + const { asset: asset2User2 } = await ctx.newAsset({ ownerId: user2.id }); + await ctx.newExif({ assetId: asset2User2.id, make: 'asset2User2' }); await wait(2); - await assetRepo.create(asset3User2); - await assetRepo.upsertExif({ assetId: asset3User2.id, make: 'asset3User2' }); + const { asset: asset3User2 } = await ctx.newAsset({ ownerId: user2.id }); + await ctx.newExif({ assetId: asset3User2.id, make: 'asset3User2' }); + const { album: album1 } = await ctx.newAlbum({ ownerId: user2.id }); + await ctx.newAlbumAsset({ albumId: album1.id, assetId: asset2User2.id }); + await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); - const albumRepo = getRepository('album'); - const album1 = mediumFactory.albumInsert({ ownerId: user2.id }); - await albumRepo.create(album1, [asset2User2.id], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]); - - const response = await testSync(auth, [SyncRequestType.AlbumAssetExifsV1]); + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]); expect(response).toHaveLength(1); expect(response).toEqual([ { @@ -165,20 +125,20 @@ describe.concurrent(SyncRequestType.AlbumAssetExifsV1, () => { ]); // ack initial album asset exif sync - const acks = response.map(({ ack }) => ack); - await sut.setAcks(auth, { acks }); + await ctx.syncAckAll(auth, response); - // create a second album with - const album2 = mediumFactory.albumInsert({ ownerId: user2.id }); - await albumRepo.create( - album2, - [asset1User2.id, asset2User2.id, asset3User2.id, asset1Owner.id], - [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }], + // create a second album + const { album: album2 } = await ctx.newAlbum({ ownerId: user2.id }); + await Promise.all( + [asset1User2.id, asset2User2.id, asset3User2.id, asset1Owner.id].map((assetId) => + ctx.newAlbumAsset({ albumId: album2.id, assetId }), + ), ); + await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); // should backfill the album user - const backfillResponse = await testSync(auth, [SyncRequestType.AlbumAssetExifsV1]); - expect(backfillResponse).toEqual([ + const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]); + expect(newResponse).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ @@ -214,9 +174,7 @@ describe.concurrent(SyncRequestType.AlbumAssetExifsV1, () => { }, ]); - await sut.setAcks(auth, { acks: [backfillResponse[3].ack, backfillResponse.at(-1).ack] }); - - const finalResponse = await testSync(auth, [SyncRequestType.AlbumAssetExifsV1]); - expect(finalResponse).toEqual([]); + await ctx.syncAckAll(auth, newResponse); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([]); }); }); diff --git a/server/test/medium/specs/sync/sync-album-asset.spec.ts b/server/test/medium/specs/sync/sync-album-asset.spec.ts index ea16393f11..ae4833de34 100644 --- a/server/test/medium/specs/sync/sync-album-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-album-asset.spec.ts @@ -1,40 +1,32 @@ import { Kysely } from 'kysely'; import { DB } from 'src/db'; import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum'; -import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory'; +import { SyncTestContext } from 'test/medium.factory'; import { factory } from 'test/small.factory'; import { getKyselyDB, wait } from 'test/utils'; let defaultDatabase: Kysely; const setup = async (db?: Kysely) => { - const database = db || defaultDatabase; - const result = newSyncTest({ db: database }); - const { auth, create } = newSyncAuthUser(); - await create(database); - return { ...result, auth }; + const ctx = new SyncTestContext(db || defaultDatabase); + const { auth, user, session } = await ctx.newSyncAuthUser(); + return { auth, user, session, ctx }; }; beforeAll(async () => { defaultDatabase = await getKyselyDB(); }); -describe.concurrent(SyncRequestType.AlbumAssetsV1, () => { +describe(SyncRequestType.AlbumAssetsV1, () => { it('should detect and sync the first album asset', async () => { - const { auth, sut, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - const originalFileName = 'firstAsset'; const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const date = new Date().toISOString(); - const albumRepo = getRepository('album'); - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ + const { auth, ctx } = await setup(); + const { user: user2 } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ originalFileName, ownerId: user2.id, checksum: Buffer.from(checksum, 'base64'), @@ -45,110 +37,79 @@ describe.concurrent(SyncRequestType.AlbumAssetsV1, () => { deletedAt: null, duration: '0:10:00.00000', }); - await assetRepo.create(asset); - await albumRepo.create({ ownerId: user2.id }, [asset.id], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]); + const { album } = await ctx.newAlbum({ ownerId: user2.id }); + await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); + await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); - const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumAssetsV1]); - - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - id: asset.id, - originalFileName, - ownerId: asset.ownerId, - thumbhash, - checksum, - deletedAt: asset.deletedAt, - fileCreatedAt: asset.fileCreatedAt, - fileModifiedAt: asset.fileModifiedAt, - isFavorite: asset.isFavorite, - localDateTime: asset.localDateTime, - type: asset.type, - visibility: asset.visibility, - duration: asset.duration, - }, - type: SyncEntityType.AlbumAssetV1, + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + id: asset.id, + originalFileName, + ownerId: asset.ownerId, + thumbhash, + checksum, + deletedAt: asset.deletedAt, + fileCreatedAt: asset.fileCreatedAt, + fileModifiedAt: asset.fileModifiedAt, + isFavorite: asset.isFavorite, + localDateTime: asset.localDateTime, + type: asset.type, + visibility: asset.visibility, + duration: asset.duration, }, - ]), - ); + type: SyncEntityType.AlbumAssetV1, + }, + ]); - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.AlbumAssetsV1]); - - expect(ackSyncResponse).toEqual([]); + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toEqual([]); }); it('should sync album asset for own user', async () => { - const { auth, getRepository, testSync } = await setup(); + const { auth, ctx } = await setup(); + const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); + const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); + await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); - const albumRepo = getRepository('album'); - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); - await assetRepo.create(asset); - await albumRepo.create({ ownerId: auth.user.id }, [asset.id], []); - - await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); - await expect(testSync(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toHaveLength(1); + await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toHaveLength(1); }); it('should not sync album asset for unrelated user', async () => { - const { auth, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - const user3 = mediumFactory.userInsert(); - await userRepo.create(user2); - await userRepo.create(user3); - - const albumRepo = getRepository('album'); - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: user3.id }); - await assetRepo.create(asset); - await albumRepo.create({ ownerId: user2.id }, [asset.id], [{ userId: user3.id, role: AlbumUserRole.EDITOR }]); - - const sessionRepo = getRepository('session'); - const session = mediumFactory.sessionInsert({ userId: user3.id }); - await sessionRepo.create(session); - + const { auth, ctx } = await setup(); + const { user: user2 } = await ctx.newUser(); + const { user: user3 } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user3.id }); + const { album } = await ctx.newAlbum({ ownerId: user2.id }); + await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); + await ctx.newAlbumUser({ albumId: album.id, userId: user3.id, role: AlbumUserRole.EDITOR }); + const { session } = await ctx.newSession({ userId: user3.id }); const authUser3 = factory.auth({ session, user: user3 }); - await expect(testSync(authUser3, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); - await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toHaveLength(0); }); it('should backfill album assets when a user shares an album with you', async () => { - const { auth, sut, testSync, getRepository } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - const user3 = mediumFactory.userInsert(); - await userRepo.create(user2); - await userRepo.create(user3); - - const assetRepo = getRepository('asset'); - // Asset to check that we do backfill our own assets - const asset1Owner = mediumFactory.assetInsert({ ownerId: auth.user.id }); - const asset1User2 = mediumFactory.assetInsert({ ownerId: user2.id }); - const asset2User2 = mediumFactory.assetInsert({ ownerId: user2.id }); - const asset3User2 = mediumFactory.assetInsert({ ownerId: user2.id }); - await assetRepo.create(asset1Owner); + const { auth, ctx } = await setup(); + const { user: user2 } = await ctx.newUser(); + const { asset: asset1Owner } = await ctx.newAsset({ ownerId: auth.user.id }); await wait(2); - await assetRepo.create(asset1User2); + const { asset: asset1User2 } = await ctx.newAsset({ ownerId: user2.id }); await wait(2); - await assetRepo.create(asset2User2); + const { asset: asset2User2 } = await ctx.newAsset({ ownerId: user2.id }); await wait(2); - await assetRepo.create(asset3User2); + const { asset: asset3User2 } = await ctx.newAsset({ ownerId: user2.id }); + await wait(2); + const { album: album1 } = await ctx.newAlbum({ ownerId: user2.id }); + await ctx.newAlbumAsset({ albumId: album1.id, assetId: asset2User2.id }); + await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); - const albumRepo = getRepository('album'); - const album1 = mediumFactory.albumInsert({ ownerId: user2.id }); - await albumRepo.create(album1, [asset2User2.id], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]); - - const response = await testSync(auth, [SyncRequestType.AlbumAssetsV1]); + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); expect(response).toHaveLength(1); expect(response).toEqual([ { @@ -161,20 +122,20 @@ describe.concurrent(SyncRequestType.AlbumAssetsV1, () => { ]); // ack initial album asset sync - const acks = response.map(({ ack }) => ack); - await sut.setAcks(auth, { acks }); + await ctx.syncAckAll(auth, response); - // create a second album with - const album2 = mediumFactory.albumInsert({ ownerId: user2.id }); - await albumRepo.create( - album2, - [asset1User2.id, asset2User2.id, asset3User2.id, asset1Owner.id], - [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }], + // create a second album + const { album: album2 } = await ctx.newAlbum({ ownerId: user2.id }); + await Promise.all( + [asset1User2.id, asset2User2.id, asset3User2.id, asset1Owner.id].map((assetId) => + ctx.newAlbumAsset({ albumId: album2.id, assetId }), + ), ); + await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); // should backfill the album user - const backfillResponse = await testSync(auth, [SyncRequestType.AlbumAssetsV1]); - expect(backfillResponse).toEqual([ + const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]); + expect(newResponse).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ @@ -210,9 +171,7 @@ describe.concurrent(SyncRequestType.AlbumAssetsV1, () => { }, ]); - await sut.setAcks(auth, { acks: [backfillResponse[3].ack, backfillResponse.at(-1).ack] }); - - const finalResponse = await testSync(auth, [SyncRequestType.AlbumAssetsV1]); - expect(finalResponse).toEqual([]); + await ctx.syncAckAll(auth, newResponse); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toEqual([]); }); }); diff --git a/server/test/medium/specs/sync/sync-album-to-asset.spec.ts b/server/test/medium/specs/sync/sync-album-to-asset.spec.ts index 0941ab05b7..c1af6676f8 100644 --- a/server/test/medium/specs/sync/sync-album-to-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-album-to-asset.spec.ts @@ -1,173 +1,119 @@ import { Kysely } from 'kysely'; import { DB } from 'src/db'; import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum'; -import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory'; +import { AlbumRepository } from 'src/repositories/album.repository'; +import { AssetRepository } from 'src/repositories/asset.repository'; +import { SyncTestContext } from 'test/medium.factory'; import { getKyselyDB, wait } from 'test/utils'; let defaultDatabase: Kysely; const setup = async (db?: Kysely) => { - const database = db || defaultDatabase; - const result = newSyncTest({ db: database }); - const { auth, create } = newSyncAuthUser(); - await create(database); - return { ...result, auth }; + const ctx = new SyncTestContext(db || defaultDatabase); + const { auth, user, session } = await ctx.newSyncAuthUser(); + return { auth, user, session, ctx }; }; beforeAll(async () => { defaultDatabase = await getKyselyDB(); }); -describe.concurrent(SyncRequestType.AlbumToAssetsV1, () => { +describe(SyncRequestType.AlbumToAssetsV1, () => { it('should detect and sync the first album to asset relation', async () => { - const { auth, sut, getRepository, testSync } = await setup(); + const { auth, ctx } = await setup(); + const { user: user2 } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user2.id }); + const { album } = await ctx.newAlbum({ ownerId: user2.id }); + await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); + await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id }); - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const albumRepo = getRepository('album'); - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: user2.id }); - await assetRepo.create(asset); - const album = mediumFactory.albumInsert({ ownerId: user2.id }); - await albumRepo.create(album, [asset.id], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]); - - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - albumId: album.id, - assetId: asset.id, - }, - type: SyncEntityType.AlbumToAssetV1, + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + albumId: album.id, + assetId: asset.id, }, - ]), - ); + type: SyncEntityType.AlbumToAssetV1, + }, + ]); - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]); - - expect(ackSyncResponse).toEqual([]); + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); }); it('should sync album to asset for owned albums', async () => { - const { auth, sut, getRepository, testSync } = await setup(); + const { auth, ctx } = await setup(); + const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); + const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); + await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); - const albumRepo = getRepository('album'); - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); - await assetRepo.create(asset); - const album = mediumFactory.albumInsert({ ownerId: auth.user.id }); - await albumRepo.create(album, [asset.id], []); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]); - - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - albumId: album.id, - assetId: asset.id, - }, - type: SyncEntityType.AlbumToAssetV1, + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + albumId: album.id, + assetId: asset.id, }, - ]), - ); + type: SyncEntityType.AlbumToAssetV1, + }, + ]); - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(ackSyncResponse).toEqual([]); + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); }); it('should detect and sync the album to asset for shared albums', async () => { - const { auth, sut, getRepository, testSync } = await setup(); + const { auth, ctx } = await setup(); + const { user: user2 } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); + const { album } = await ctx.newAlbum({ ownerId: user2.id }); + await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); + await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const albumRepo = getRepository('album'); - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); - await assetRepo.create(asset); - const album = mediumFactory.albumInsert({ ownerId: user2.id }); - await albumRepo.create(album, [asset.id], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]); - - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - albumId: album.id, - assetId: asset.id, - }, - type: SyncEntityType.AlbumToAssetV1, + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + albumId: album.id, + assetId: asset.id, }, - ]), - ); + type: SyncEntityType.AlbumToAssetV1, + }, + ]); - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]); - - expect(ackSyncResponse).toEqual([]); + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); }); it('should not sync album to asset for an album owned by another user', async () => { - const { auth, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const albumRepo = getRepository('album'); - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: user2.id }); - await assetRepo.create(asset); - await albumRepo.create({ ownerId: user2.id }, [asset.id], []); - - await expect(testSync(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toHaveLength(0); + const { auth, ctx } = await setup(); + const { user: user2 } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user2.id }); + const { album } = await ctx.newAlbum({ ownerId: user2.id }); + await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); }); it('should backfill album to assets when a user shares an album with you', async () => { - const { auth, sut, testSync, getRepository } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const assetRepo = getRepository('asset'); - const album1Asset = mediumFactory.assetInsert({ ownerId: user2.id }); - await assetRepo.create(album1Asset); - const album2Asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); - await assetRepo.create(album2Asset); - + const { auth, ctx } = await setup(); + const { user: user2 } = await ctx.newUser(); + const { asset: album1Asset } = await ctx.newAsset({ ownerId: user2.id }); + const { asset: album2Asset } = await ctx.newAsset({ ownerId: auth.user.id }); // Backfill album - const albumRepo = getRepository('album'); - const album2 = mediumFactory.albumInsert({ ownerId: user2.id }); - await albumRepo.create(album2, [album2Asset.id], []); - + const { album: album2 } = await ctx.newAlbum({ ownerId: user2.id }); + await ctx.newAlbumAsset({ albumId: album2.id, assetId: album2Asset.id }); await wait(2); + const { album: album1 } = await ctx.newAlbum({ ownerId: auth.user.id }); + await ctx.newAlbumAsset({ albumId: album1.id, assetId: album1Asset.id }); - const album1 = mediumFactory.albumInsert({ ownerId: auth.user.id }); - await albumRepo.create(album1, [album1Asset.id], []); - - const response = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]); + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); expect(response).toHaveLength(1); expect(response).toEqual([ { @@ -181,16 +127,14 @@ describe.concurrent(SyncRequestType.AlbumToAssetsV1, () => { ]); // ack initial album to asset sync - const acks = response.map(({ ack }) => ack); - await sut.setAcks(auth, { acks }); + await ctx.syncAckAll(auth, response); // add user to backfill album - const albumUserRepo = getRepository('albumUser'); - await albumUserRepo.create({ albumsId: album2.id, usersId: auth.user.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); // should backfill the album to asset relation - const backfillResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(backfillResponse).toEqual([ + const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); + expect(newResponse).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ @@ -206,26 +150,20 @@ describe.concurrent(SyncRequestType.AlbumToAssetsV1, () => { }, ]); - await sut.setAcks(auth, { acks: [backfillResponse[1].ack] }); - - const finalResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(finalResponse).toEqual([]); + await ctx.syncAckAll(auth, newResponse); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); }); it('should detect and sync a deleted album to asset relation', async () => { - const { auth, sut, getRepository, testSync } = await setup(); + const { auth, ctx } = await setup(); + const albumRepo = ctx.get(AlbumRepository); + const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); + const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); + await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); - const albumRepo = getRepository('album'); - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); - await assetRepo.create(asset); - const album = mediumFactory.albumInsert({ ownerId: auth.user.id }); - await albumRepo.create(album, [asset.id], []); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]); - - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual([ + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ { ack: expect.any(String), data: { @@ -236,16 +174,13 @@ describe.concurrent(SyncRequestType.AlbumToAssetsV1, () => { }, ]); - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - + await ctx.syncAckAll(auth, response); await albumRepo.removeAssetIds(album.id, [asset.id]); - await wait(2); - const syncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(syncResponse).toHaveLength(1); - expect(syncResponse).toEqual([ + const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); + expect(newResponse).toHaveLength(1); + expect(newResponse).toEqual([ { ack: expect.any(String), data: { @@ -256,26 +191,20 @@ describe.concurrent(SyncRequestType.AlbumToAssetsV1, () => { }, ]); - await sut.setAcks(auth, { acks: [syncResponse[0].ack] }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(ackSyncResponse).toEqual([]); + await ctx.syncAckAll(auth, newResponse); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); }); it('should detect and sync a deleted album to asset relation when an asset is deleted', async () => { - const { auth, sut, getRepository, testSync } = await setup(); + const { auth, ctx } = await setup(); + const assetRepo = ctx.get(AssetRepository); + const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); + const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); + await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); - const albumRepo = getRepository('album'); - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); - await assetRepo.create(asset); - const album = mediumFactory.albumInsert({ ownerId: auth.user.id }); - await albumRepo.create(album, [asset.id], []); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]); - - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual([ + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ { ack: expect.any(String), data: { @@ -286,16 +215,13 @@ describe.concurrent(SyncRequestType.AlbumToAssetsV1, () => { }, ]); - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - + await ctx.syncAckAll(auth, response); await assetRepo.remove({ id: asset.id }); - await wait(2); - const syncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(syncResponse).toHaveLength(1); - expect(syncResponse).toEqual([ + const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); + expect(newResponse).toHaveLength(1); + expect(newResponse).toEqual([ { ack: expect.any(String), data: { @@ -306,26 +232,20 @@ describe.concurrent(SyncRequestType.AlbumToAssetsV1, () => { }, ]); - await sut.setAcks(auth, { acks: [syncResponse[0].ack] }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(ackSyncResponse).toEqual([]); + await ctx.syncAckAll(auth, newResponse); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); }); it('should not sync a deleted album to asset relation when the album is deleted', async () => { - const { auth, sut, getRepository, testSync } = await setup(); + const { auth, ctx } = await setup(); + const albumRepo = ctx.get(AlbumRepository); + const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); + const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); + await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); - const albumRepo = getRepository('album'); - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); - await assetRepo.create(asset); - const album = mediumFactory.albumInsert({ ownerId: auth.user.id }); - await albumRepo.create(album, [asset.id], []); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]); - - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual([ + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ { ack: expect.any(String), data: { @@ -336,15 +256,9 @@ describe.concurrent(SyncRequestType.AlbumToAssetsV1, () => { }, ]); - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - + await ctx.syncAckAll(auth, response); await albumRepo.delete(album.id); - await wait(2); - - const syncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]); - expect(syncResponse).toHaveLength(0); - expect(syncResponse).toEqual([]); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]); }); }); diff --git a/server/test/medium/specs/sync/sync-album-user.spec.ts b/server/test/medium/specs/sync/sync-album-user.spec.ts index 305bead275..215752ca6e 100644 --- a/server/test/medium/specs/sync/sync-album-user.spec.ts +++ b/server/test/medium/specs/sync/sync-album-user.spec.ts @@ -1,17 +1,16 @@ import { Kysely } from 'kysely'; import { DB } from 'src/db'; import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum'; -import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory'; +import { AlbumUserRepository } from 'src/repositories/album-user.repository'; +import { SyncTestContext } from 'test/medium.factory'; import { getKyselyDB, wait } from 'test/utils'; let defaultDatabase: Kysely; const setup = async (db?: Kysely) => { - const database = db || defaultDatabase; - const result = newSyncTest({ db: database }); - const { auth, create } = newSyncAuthUser(); - await create(database); - return { ...result, auth }; + const ctx = new SyncTestContext(db || defaultDatabase); + const { auth, user, session } = await ctx.newSyncAuthUser(); + return { auth, user, session, ctx }; }; beforeAll(async () => { @@ -20,198 +19,156 @@ beforeAll(async () => { describe(SyncRequestType.AlbumUsersV1, () => { it('should sync an album user with the correct properties', async () => { - const { auth, getRepository, testSync } = await setup(); + const { auth, ctx } = await setup(); + const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); + const { user } = await ctx.newUser(); + const { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: user.id, role: AlbumUserRole.EDITOR }); - const albumRepo = getRepository('album'); - const albumUserRepo = getRepository('albumUser'); - const userRepo = getRepository('user'); - - const album = mediumFactory.albumInsert({ ownerId: auth.user.id }); - await albumRepo.create(album, [], []); - - const user = mediumFactory.userInsert(); - await userRepo.create(user); - - const albumUser = { albumsId: album.id, usersId: user.id, role: AlbumUserRole.EDITOR }; - await albumUserRepo.create(albumUser); - - await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([ + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([ { ack: expect.any(String), data: expect.objectContaining({ - albumId: albumUser.albumsId, + albumId: albumUser.albumId, role: albumUser.role, - userId: albumUser.usersId, + userId: albumUser.userId, }), type: SyncEntityType.AlbumUserV1, }, ]); }); + describe('owner', () => { it('should detect and sync a new shared user', async () => { - const { auth, testSync, getRepository } = await setup(); + const { auth, ctx } = await setup(); + const { user: user1 } = await ctx.newUser(); + const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); + const { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: user1.id, role: AlbumUserRole.EDITOR }); - const albumRepo = getRepository('album'); - const albumUserRepo = getRepository('albumUser'); - const userRepo = getRepository('user'); - - const user1 = mediumFactory.userInsert(); - await userRepo.create(user1); - - const album = mediumFactory.albumInsert({ ownerId: auth.user.id }); - await albumRepo.create(album, [], []); - - const albumUser = { albumsId: album.id, usersId: user1.id, role: AlbumUserRole.EDITOR }; - await albumUserRepo.create(albumUser); - - await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([ + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ - albumId: albumUser.albumsId, + albumId: albumUser.albumId, role: albumUser.role, - userId: albumUser.usersId, + userId: albumUser.userId, }), type: SyncEntityType.AlbumUserV1, }, ]); + + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); }); it('should detect and sync an updated shared user', async () => { - const { auth, testSync, getRepository, sut } = await setup(); + const { auth, ctx } = await setup(); + const albumUserRepo = ctx.get(AlbumUserRepository); + const { user: user1 } = await ctx.newUser(); + const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); + const { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: user1.id, role: AlbumUserRole.EDITOR }); - const albumRepo = getRepository('album'); - const albumUserRepo = getRepository('albumUser'); - const userRepo = getRepository('user'); - - const user1 = mediumFactory.userInsert(); - await userRepo.create(user1); - - const album = mediumFactory.albumInsert({ ownerId: auth.user.id }); - await albumRepo.create(album, [], []); - - const albumUser = { albumsId: album.id, usersId: user1.id, role: AlbumUserRole.EDITOR }; - await albumUserRepo.create(albumUser); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]); - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - - await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); await albumUserRepo.update({ albumsId: album.id, usersId: user1.id }, { role: AlbumUserRole.VIEWER }); - - await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([ + const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); + expect(newResponse).toHaveLength(1); + expect(newResponse).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ - albumId: albumUser.albumsId, + albumId: albumUser.albumId, role: AlbumUserRole.VIEWER, - userId: albumUser.usersId, + userId: albumUser.userId, }), type: SyncEntityType.AlbumUserV1, }, ]); + + await ctx.syncAckAll(auth, newResponse); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); }); it('should detect and sync a deleted shared user', async () => { - const { auth, testSync, getRepository, sut } = await setup(); + const { auth, ctx } = await setup(); + const albumUserRepo = ctx.get(AlbumUserRepository); + const { user: user1 } = await ctx.newUser(); + const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); + const { albumUser } = await ctx.newAlbumUser({ albumId: album.id, userId: user1.id, role: AlbumUserRole.EDITOR }); - const albumRepo = getRepository('album'); - const albumUserRepo = getRepository('albumUser'); - const userRepo = getRepository('user'); - - const user1 = mediumFactory.userInsert(); - await userRepo.create(user1); - - const album = mediumFactory.albumInsert({ ownerId: auth.user.id }); - await albumRepo.create(album, [], []); - - const albumUser = { albumsId: album.id, usersId: user1.id, role: AlbumUserRole.EDITOR }; - await albumUserRepo.create(albumUser); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]); - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - - await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); + expect(response).toHaveLength(1); + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); await albumUserRepo.delete({ albumsId: album.id, usersId: user1.id }); - - await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([ + const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); + expect(newResponse).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ - albumId: albumUser.albumsId, - userId: albumUser.usersId, + albumId: albumUser.albumId, + userId: albumUser.userId, }), type: SyncEntityType.AlbumUserDeleteV1, }, ]); + + await ctx.syncAckAll(auth, newResponse); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); }); }); describe('shared user', () => { it('should detect and sync a new shared user', async () => { - const { auth, testSync, getRepository } = await setup(); + const { auth, ctx } = await setup(); + const { user: user1 } = await ctx.newUser(); + const { album } = await ctx.newAlbum({ ownerId: user1.id }); + const { albumUser } = await ctx.newAlbumUser({ + albumId: album.id, + userId: auth.user.id, + role: AlbumUserRole.EDITOR, + }); - const albumRepo = getRepository('album'); - const albumUserRepo = getRepository('albumUser'); - const userRepo = getRepository('user'); - - const user1 = mediumFactory.userInsert(); - await userRepo.create(user1); - - const album = mediumFactory.albumInsert({ ownerId: user1.id }); - await albumRepo.create(album, [], []); - - const albumUser = { albumsId: album.id, usersId: auth.user.id, role: AlbumUserRole.EDITOR }; - await albumUserRepo.create(albumUser); - - await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([ + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ - albumId: albumUser.albumsId, + albumId: albumUser.albumId, role: albumUser.role, - userId: albumUser.usersId, + userId: albumUser.userId, }), type: SyncEntityType.AlbumUserV1, }, ]); + + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); }); it('should detect and sync an updated shared user', async () => { - const { auth, testSync, getRepository, sut } = await setup(); + const { auth, ctx } = await setup(); + const albumUserRepo = ctx.get(AlbumUserRepository); + const { user: owner } = await ctx.newUser(); + const { user: user } = await ctx.newUser(); + const { album } = await ctx.newAlbum({ ownerId: owner.id }); + await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album.id, userId: user.id, role: AlbumUserRole.EDITOR }); - const albumRepo = getRepository('album'); - const albumUserRepo = getRepository('albumUser'); - const userRepo = getRepository('user'); + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); + expect(response).toHaveLength(2); - const owner = mediumFactory.userInsert(); - const user = mediumFactory.userInsert(); - await Promise.all([userRepo.create(owner), userRepo.create(user)]); - - const album = mediumFactory.albumInsert({ ownerId: owner.id }); - await albumRepo.create( - album, - [], - [ - { userId: auth.user.id, role: AlbumUserRole.EDITOR }, - { userId: user.id, role: AlbumUserRole.EDITOR }, - ], - ); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]); - expect(initialSyncResponse).toHaveLength(2); - const acks = [initialSyncResponse[1].ack]; - await sut.setAcks(auth, { acks }); - - await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); await albumUserRepo.update({ albumsId: album.id, usersId: user.id }, { role: AlbumUserRole.VIEWER }); - - await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([ + const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); + expect(newResponse).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ @@ -222,39 +179,29 @@ describe(SyncRequestType.AlbumUsersV1, () => { type: SyncEntityType.AlbumUserV1, }, ]); + + await ctx.syncAckAll(auth, newResponse); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); }); it('should detect and sync a deleted shared user', async () => { - const { auth, testSync, getRepository, sut } = await setup(); + const { auth, ctx } = await setup(); + const albumUserRepo = ctx.get(AlbumUserRepository); + const { user: owner } = await ctx.newUser(); + const { user: user } = await ctx.newUser(); + const { album } = await ctx.newAlbum({ ownerId: owner.id }); + await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album.id, userId: user.id, role: AlbumUserRole.EDITOR }); - const albumRepo = getRepository('album'); - const albumUserRepo = getRepository('albumUser'); - const userRepo = getRepository('user'); - - const owner = mediumFactory.userInsert(); - const user = mediumFactory.userInsert(); - await Promise.all([userRepo.create(owner), userRepo.create(user)]); - - const album = mediumFactory.albumInsert({ ownerId: owner.id }); - await albumRepo.create( - album, - [], - [ - { userId: auth.user.id, role: AlbumUserRole.EDITOR }, - { userId: user.id, role: AlbumUserRole.EDITOR }, - ], - ); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]); - expect(initialSyncResponse).toHaveLength(2); - const acks = [initialSyncResponse[1].ack]; - await sut.setAcks(auth, { acks }); - - await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); + expect(response).toHaveLength(2); + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); await albumUserRepo.delete({ albumsId: album.id, usersId: user.id }); - await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([ + const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); + expect(newResponse).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ @@ -264,35 +211,27 @@ describe(SyncRequestType.AlbumUsersV1, () => { type: SyncEntityType.AlbumUserDeleteV1, }, ]); + + await ctx.syncAckAll(auth, newResponse); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); }); it('should backfill album users when a user shares an album with you', async () => { - const { auth, sut, testSync, getRepository } = await setup(); - - const albumRepo = getRepository('album'); - const albumUserRepo = getRepository('albumUser'); - const userRepo = getRepository('user'); - - const user1 = mediumFactory.userInsert(); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user1); - await userRepo.create(user2); - - const album1 = mediumFactory.albumInsert({ ownerId: user1.id }); - const album2 = mediumFactory.albumInsert({ ownerId: user1.id }); - await albumRepo.create(album1, [], []); - await albumRepo.create(album2, [], []); - + const { auth, ctx } = await setup(); + const { user: user1 } = await ctx.newUser(); + const { user: user2 } = await ctx.newUser(); + const { album: album1 } = await ctx.newAlbum({ ownerId: user1.id }); + const { album: album2 } = await ctx.newAlbum({ ownerId: user1.id }); // backfill album user - await albumUserRepo.create({ albumsId: album1.id, usersId: user1.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album1.id, userId: user1.id, role: AlbumUserRole.EDITOR }); await wait(2); // initial album user - await albumUserRepo.create({ albumsId: album2.id, usersId: auth.user.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); await wait(2); // post checkpoint album user - await albumUserRepo.create({ albumsId: album1.id, usersId: user2.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album1.id, userId: user2.id, role: AlbumUserRole.EDITOR }); - const response = await testSync(auth, [SyncRequestType.AlbumUsersV1]); + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); expect(response).toHaveLength(1); expect(response).toEqual([ { @@ -307,15 +246,13 @@ describe(SyncRequestType.AlbumUsersV1, () => { ]); // ack initial user - const acks = response.map(({ ack }) => ack); - await sut.setAcks(auth, { acks }); - + await ctx.syncAckAll(auth, response); // get access to the backfill album user - await albumUserRepo.create({ albumsId: album1.id, usersId: auth.user.id, role: AlbumUserRole.EDITOR }); + await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); // should backfill the album user - const backfillResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]); - expect(backfillResponse).toEqual([ + const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]); + expect(newResponse).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ @@ -350,10 +287,8 @@ describe(SyncRequestType.AlbumUsersV1, () => { }, ]); - await sut.setAcks(auth, { acks: [backfillResponse[1].ack, backfillResponse.at(-1).ack] }); - - const finalResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]); - expect(finalResponse).toEqual([]); + await ctx.syncAckAll(auth, newResponse); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]); }); }); }); diff --git a/server/test/medium/specs/sync/sync-album.spec.ts b/server/test/medium/specs/sync/sync-album.spec.ts index 7ee7bf624f..90aa8c4576 100644 --- a/server/test/medium/specs/sync/sync-album.spec.ts +++ b/server/test/medium/specs/sync/sync-album.spec.ts @@ -1,17 +1,17 @@ import { Kysely } from 'kysely'; import { DB } from 'src/db'; import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum'; -import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory'; +import { AlbumUserRepository } from 'src/repositories/album-user.repository'; +import { AlbumRepository } from 'src/repositories/album.repository'; +import { SyncTestContext } from 'test/medium.factory'; import { getKyselyDB } from 'test/utils'; let defaultDatabase: Kysely; const setup = async (db?: Kysely) => { - const database = db || defaultDatabase; - const result = newSyncTest({ db: database }); - const { auth, create } = newSyncAuthUser(); - await create(database); - return { ...result, auth }; + const ctx = new SyncTestContext(db || defaultDatabase); + const { auth, user, session } = await ctx.newSyncAuthUser(); + return { auth, user, session, ctx }; }; beforeAll(async () => { @@ -20,11 +20,12 @@ beforeAll(async () => { describe(SyncRequestType.AlbumsV1, () => { it('should sync an album with the correct properties', async () => { - const { auth, getRepository, testSync } = await setup(); - const albumRepo = getRepository('album'); - const album = mediumFactory.albumInsert({ ownerId: auth.user.id }); - await albumRepo.create(album, [], []); - await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([ + const { auth, ctx } = await setup(); + const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); + + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ @@ -35,14 +36,18 @@ describe(SyncRequestType.AlbumsV1, () => { type: SyncEntityType.AlbumV1, }, ]); + + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); }); it('should detect and sync a new album', async () => { - const { auth, getRepository, testSync } = await setup(); - const albumRepo = getRepository('album'); - const album = mediumFactory.albumInsert({ ownerId: auth.user.id }); - await albumRepo.create(album, [], []); - await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([ + const { auth, ctx } = await setup(); + const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); + + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ @@ -51,14 +56,19 @@ describe(SyncRequestType.AlbumsV1, () => { type: SyncEntityType.AlbumV1, }, ]); + + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); }); it('should detect and sync an album delete', async () => { - const { auth, getRepository, testSync } = await setup(); - const albumRepo = getRepository('album'); - const album = mediumFactory.albumInsert({ ownerId: auth.user.id }); - await albumRepo.create(album, [], []); - await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([ + const { auth, ctx } = await setup(); + const albumRepo = ctx.get(AlbumRepository); + const { album } = await ctx.newAlbum({ ownerId: auth.user.id }); + + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ @@ -69,7 +79,10 @@ describe(SyncRequestType.AlbumsV1, () => { ]); await albumRepo.delete(album.id); - await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([ + + const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); + expect(newResponse).toHaveLength(1); + expect(newResponse).toEqual([ { ack: expect.any(String), data: { @@ -78,67 +91,60 @@ describe(SyncRequestType.AlbumsV1, () => { type: SyncEntityType.AlbumDeleteV1, }, ]); + + await ctx.syncAckAll(auth, newResponse); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); }); describe('shared albums', () => { it('should detect and sync an album create', async () => { - const { auth, getRepository, testSync } = await setup(); - const albumRepo = getRepository('album'); - const userRepo = getRepository('user'); + const { auth, ctx } = await setup(); + const { user: user2 } = await ctx.newUser(); + const { album } = await ctx.newAlbum({ ownerId: user2.id }); + await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const album = mediumFactory.albumInsert({ ownerId: user2.id }); - await albumRepo.create(album, [], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]); - - await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([ + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ id: album.id }), type: SyncEntityType.AlbumV1, }, ]); + + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); }); it('should detect and sync an album share (share before sync)', async () => { - const { auth, getRepository, testSync } = await setup(); - const albumRepo = getRepository('album'); - const albumUserRepo = getRepository('albumUser'); - const userRepo = getRepository('user'); + const { auth, ctx } = await setup(); + const { user: user2 } = await ctx.newUser(); + const { album } = await ctx.newAlbum({ ownerId: user2.id }); + await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const album = mediumFactory.albumInsert({ ownerId: user2.id }); - await albumRepo.create(album, [], []); - await albumUserRepo.create({ usersId: auth.user.id, albumsId: album.id, role: AlbumUserRole.EDITOR }); - - await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([ + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ id: album.id }), type: SyncEntityType.AlbumV1, }, ]); + + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); }); it('should detect and sync an album share (share after sync)', async () => { - const { auth, getRepository, sut, testSync } = await setup(); - const albumRepo = getRepository('album'); - const albumUserRepo = getRepository('albumUser'); - const userRepo = getRepository('user'); + const { auth, ctx } = await setup(); + const { user: user2 } = await ctx.newUser(); + const { album: userAlbum } = await ctx.newAlbum({ ownerId: auth.user.id }); + const { album: user2Album } = await ctx.newAlbum({ ownerId: user2.id }); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const userAlbum = mediumFactory.albumInsert({ ownerId: auth.user.id }); - const user2Album = mediumFactory.albumInsert({ ownerId: user2.id }); - await Promise.all([albumRepo.create(user2Album, [], []), albumRepo.create(userAlbum, [], [])]); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumsV1]); - - expect(initialSyncResponse).toEqual([ + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); + expect(response).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ id: userAlbum.id }), @@ -146,75 +152,76 @@ describe(SyncRequestType.AlbumsV1, () => { }, ]); - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); + await ctx.syncAckAll(auth, response); + await ctx.newAlbumUser({ userId: auth.user.id, albumId: user2Album.id, role: AlbumUserRole.EDITOR }); - await albumUserRepo.create({ usersId: auth.user.id, albumsId: user2Album.id, role: AlbumUserRole.EDITOR }); - - await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([ + const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); + expect(newResponse).toHaveLength(1); + expect(newResponse).toEqual([ { ack: expect.any(String), data: expect.objectContaining({ id: user2Album.id }), type: SyncEntityType.AlbumV1, }, ]); + + await ctx.syncAckAll(auth, newResponse); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); }); it('should detect and sync an album delete`', async () => { - const { auth, getRepository, testSync, sut } = await setup(); - const albumRepo = getRepository('album'); - const userRepo = getRepository('user'); + const { auth, ctx } = await setup(); + const albumRepo = ctx.get(AlbumRepository); + const { user: user2 } = await ctx.newUser(); + const { album } = await ctx.newAlbum({ ownerId: user2.id }); + await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); + expect(response).toHaveLength(1); - const album = mediumFactory.albumInsert({ ownerId: user2.id }); - await albumRepo.create(album, [], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumsV1]); - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - - await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); await albumRepo.delete(album.id); - - await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([ + const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); + expect(newResponse).toHaveLength(1); + expect(newResponse).toEqual([ { ack: expect.any(String), data: { albumId: album.id }, type: SyncEntityType.AlbumDeleteV1, }, ]); + + await ctx.syncAckAll(auth, newResponse); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); }); it('should detect and sync an album unshare as an album delete', async () => { - const { auth, getRepository, testSync, sut } = await setup(); - const albumRepo = getRepository('album'); - const albumUserRepo = getRepository('albumUser'); - const userRepo = getRepository('user'); + const { auth, ctx } = await setup(); + const albumUserRepo = ctx.get(AlbumUserRepository); + const { user: user2 } = await ctx.newUser(); + const { album } = await ctx.newAlbum({ ownerId: user2.id }); + await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR }); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); + const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); + expect(response).toHaveLength(1); - const album = mediumFactory.albumInsert({ ownerId: user2.id }); - await albumRepo.create(album, [], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumsV1]); - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - - await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); await albumUserRepo.delete({ albumsId: album.id, usersId: auth.user.id }); - - await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([ + const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]); + expect(newResponse).toEqual([ { ack: expect.any(String), data: { albumId: album.id }, type: SyncEntityType.AlbumDeleteV1, }, ]); + + await ctx.syncAckAll(auth, newResponse); + await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]); }); }); }); diff --git a/server/test/medium/specs/sync/sync-asset-exif.spec.ts b/server/test/medium/specs/sync/sync-asset-exif.spec.ts index 9a3bcb4314..8ddb05a2b7 100644 --- a/server/test/medium/specs/sync/sync-asset-exif.spec.ts +++ b/server/test/medium/specs/sync/sync-asset-exif.spec.ts @@ -1,100 +1,78 @@ import { Kysely } from 'kysely'; import { DB } from 'src/db'; import { SyncEntityType, SyncRequestType } from 'src/enum'; -import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory'; +import { SyncTestContext } from 'test/medium.factory'; import { factory } from 'test/small.factory'; import { getKyselyDB } from 'test/utils'; let defaultDatabase: Kysely; const setup = async (db?: Kysely) => { - const database = db || defaultDatabase; - const result = newSyncTest({ db: database }); - const { auth, create } = newSyncAuthUser(); - await create(database); - return { ...result, auth }; + const ctx = new SyncTestContext(db || defaultDatabase); + const { auth, user, session } = await ctx.newSyncAuthUser(); + return { auth, user, session, ctx }; }; + beforeAll(async () => { defaultDatabase = await getKyselyDB(); }); -describe.concurrent(SyncRequestType.AssetExifsV1, () => { +describe(SyncRequestType.AssetExifsV1, () => { it('should detect and sync the first asset exif', async () => { - const { auth, sut, getRepository, testSync } = await setup(); + const { auth, ctx } = await setup(); + const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); + await ctx.newExif({ assetId: asset.id, make: 'Canon' }); - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); - await assetRepo.create(asset); - await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]); - - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - assetId: asset.id, - city: null, - country: null, - dateTimeOriginal: null, - description: '', - exifImageHeight: null, - exifImageWidth: null, - exposureTime: null, - fNumber: null, - fileSizeInByte: null, - focalLength: null, - fps: null, - iso: null, - latitude: null, - lensModel: null, - longitude: null, - make: 'Canon', - model: null, - modifyDate: null, - orientation: null, - profileDescription: null, - projectionType: null, - rating: null, - state: null, - timeZone: null, - }, - type: SyncEntityType.AssetExifV1, + const response = await ctx.syncStream(auth, [SyncRequestType.AssetExifsV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + assetId: asset.id, + city: null, + country: null, + dateTimeOriginal: null, + description: '', + exifImageHeight: null, + exifImageWidth: null, + exposureTime: null, + fNumber: null, + fileSizeInByte: null, + focalLength: null, + fps: null, + iso: null, + latitude: null, + lensModel: null, + longitude: null, + make: 'Canon', + model: null, + modifyDate: null, + orientation: null, + profileDescription: null, + projectionType: null, + rating: null, + state: null, + timeZone: null, }, - ]), - ); + type: SyncEntityType.AssetExifV1, + }, + ]); - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]); - - expect(ackSyncResponse).toHaveLength(0); + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toEqual([]); }); it('should only sync asset exif for own user', async () => { - const { auth, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const partnerRepo = getRepository('partner'); - await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: user2.id }); - await assetRepo.create(asset); - await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); - - const sessionRepo = getRepository('session'); - const session = mediumFactory.sessionInsert({ userId: user2.id }); - await sessionRepo.create(session); - + const { auth, ctx } = await setup(); + const { user: user2 } = await ctx.newUser(); + await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); + const { asset } = await ctx.newAsset({ ownerId: user2.id }); + await ctx.newExif({ assetId: asset.id, make: 'Canon' }); + const { session } = await ctx.newSession({ userId: user2.id }); const auth2 = factory.auth({ session, user: user2 }); - await expect(testSync(auth2, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); - await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(0); + + await expect(ctx.syncStream(auth2, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); + await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(0); }); }); diff --git a/server/test/medium/specs/sync/sync-asset.spec.ts b/server/test/medium/specs/sync/sync-asset.spec.ts index 913edac5d9..5ffec3612e 100644 --- a/server/test/medium/specs/sync/sync-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-asset.spec.ts @@ -1,35 +1,32 @@ import { Kysely } from 'kysely'; import { DB } from 'src/db'; import { SyncEntityType, SyncRequestType } from 'src/enum'; -import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory'; +import { AssetRepository } from 'src/repositories/asset.repository'; +import { SyncTestContext } from 'test/medium.factory'; import { factory } from 'test/small.factory'; import { getKyselyDB } from 'test/utils'; let defaultDatabase: Kysely; const setup = async (db?: Kysely) => { - const database = db || defaultDatabase; - const result = newSyncTest({ db: database }); - const { auth, create } = newSyncAuthUser(); - await create(database); - return { ...result, auth }; + const ctx = new SyncTestContext(db || defaultDatabase); + const { auth, user, session } = await ctx.newSyncAuthUser(); + return { auth, user, session, ctx }; }; beforeAll(async () => { defaultDatabase = await getKyselyDB(); }); -describe.concurrent(SyncEntityType.AssetV1, () => { +describe(SyncEntityType.AssetV1, () => { it('should detect and sync the first asset', async () => { - const { auth, sut, getRepository, testSync } = await setup(); - const originalFileName = 'firstAsset'; const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const date = new Date().toISOString(); - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ + const { auth, ctx } = await setup(); + const { asset } = await ctx.newAsset({ originalFileName, ownerId: auth.user.id, checksum: Buffer.from(checksum, 'base64'), @@ -40,96 +37,70 @@ describe.concurrent(SyncEntityType.AssetV1, () => { deletedAt: null, duration: '0:10:00.00000', }); - await assetRepo.create(asset); - const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]); - - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - id: asset.id, - originalFileName, - ownerId: asset.ownerId, - thumbhash, - checksum, - deletedAt: asset.deletedAt, - fileCreatedAt: asset.fileCreatedAt, - fileModifiedAt: asset.fileModifiedAt, - isFavorite: asset.isFavorite, - localDateTime: asset.localDateTime, - type: asset.type, - visibility: asset.visibility, - duration: asset.duration, - }, - type: 'AssetV1', + const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + id: asset.id, + originalFileName, + ownerId: asset.ownerId, + thumbhash, + checksum, + deletedAt: asset.deletedAt, + fileCreatedAt: asset.fileCreatedAt, + fileModifiedAt: asset.fileModifiedAt, + isFavorite: asset.isFavorite, + localDateTime: asset.localDateTime, + type: asset.type, + visibility: asset.visibility, + duration: asset.duration, }, - ]), - ); + type: 'AssetV1', + }, + ]); - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]); - - expect(ackSyncResponse).toHaveLength(0); + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([]); }); it('should detect and sync a deleted asset', async () => { - const { auth, sut, getRepository, testSync } = await setup(); - - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); - await assetRepo.create(asset); + const { auth, ctx } = await setup(); + const assetRepo = ctx.get(AssetRepository); + const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); await assetRepo.remove(asset); - const response = await testSync(auth, [SyncRequestType.AssetsV1]); - + const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]); expect(response).toHaveLength(1); - expect(response).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - assetId: asset.id, - }, - type: 'AssetDeleteV1', + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + assetId: asset.id, }, - ]), - ); + type: 'AssetDeleteV1', + }, + ]); - const acks = response.map(({ ack }) => ack); - await sut.setAcks(auth, { acks }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]); - - expect(ackSyncResponse).toHaveLength(0); + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([]); }); it('should not sync an asset or asset delete for an unrelated user', async () => { - const { auth, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const sessionRepo = getRepository('session'); - const session = mediumFactory.sessionInsert({ userId: user2.id }); - await sessionRepo.create(session); - - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: user2.id }); - await assetRepo.create(asset); - + const { auth, ctx } = await setup(); + const assetRepo = ctx.get(AssetRepository); + const { user: user2 } = await ctx.newUser(); + const { session } = await ctx.newSession({ userId: user2.id }); + const { asset } = await ctx.newAsset({ ownerId: user2.id }); const auth2 = factory.auth({ session, user: user2 }); - expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1); - expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0); + expect(await ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1); + expect(await ctx.syncStream(auth, [SyncRequestType.AssetsV1])).toHaveLength(0); await assetRepo.remove(asset); - expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1); - expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0); + expect(await ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1); + expect(await ctx.syncStream(auth, [SyncRequestType.AssetsV1])).toHaveLength(0); }); }); diff --git a/server/test/medium/specs/sync/sync-partner-asset-exif.spec.ts b/server/test/medium/specs/sync/sync-partner-asset-exif.spec.ts index c47c55ecdc..2fee9b09a9 100644 --- a/server/test/medium/specs/sync/sync-partner-asset-exif.spec.ts +++ b/server/test/medium/specs/sync/sync-partner-asset-exif.spec.ts @@ -1,18 +1,16 @@ import { Kysely } from 'kysely'; import { DB } from 'src/db'; import { SyncEntityType, SyncRequestType } from 'src/enum'; -import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory'; +import { SyncTestContext } from 'test/medium.factory'; import { factory } from 'test/small.factory'; import { getKyselyDB, wait } from 'test/utils'; let defaultDatabase: Kysely; const setup = async (db?: Kysely) => { - const database = db || defaultDatabase; - const result = newSyncTest({ db: database }); - const { auth, create } = newSyncAuthUser(); - await create(database); - return { ...result, auth }; + const ctx = new SyncTestContext(db || defaultDatabase); + const { auth, user, session } = await ctx.newSyncAuthUser(); + return { auth, user, session, ctx }; }; beforeAll(async () => { @@ -21,134 +19,89 @@ beforeAll(async () => { describe(SyncRequestType.PartnerAssetExifsV1, () => { it('should detect and sync the first partner asset exif', async () => { - const { auth, sut, getRepository, testSync } = await setup(); + const { auth, ctx } = await setup(); + const { user: user2 } = await ctx.newUser(); + await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); + const { asset } = await ctx.newAsset({ ownerId: user2.id }); + await ctx.newExif({ assetId: asset.id, make: 'Canon' }); - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const partnerRepo = getRepository('partner'); - await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: user2.id }); - await assetRepo.create(asset); - await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]); - - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - assetId: asset.id, - city: null, - country: null, - dateTimeOriginal: null, - description: '', - exifImageHeight: null, - exifImageWidth: null, - exposureTime: null, - fNumber: null, - fileSizeInByte: null, - focalLength: null, - fps: null, - iso: null, - latitude: null, - lensModel: null, - longitude: null, - make: 'Canon', - model: null, - modifyDate: null, - orientation: null, - profileDescription: null, - projectionType: null, - rating: null, - state: null, - timeZone: null, - }, - type: SyncEntityType.PartnerAssetExifV1, + const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + assetId: asset.id, + city: null, + country: null, + dateTimeOriginal: null, + description: '', + exifImageHeight: null, + exifImageWidth: null, + exposureTime: null, + fNumber: null, + fileSizeInByte: null, + focalLength: null, + fps: null, + iso: null, + latitude: null, + lensModel: null, + longitude: null, + make: 'Canon', + model: null, + modifyDate: null, + orientation: null, + profileDescription: null, + projectionType: null, + rating: null, + state: null, + timeZone: null, }, - ]), - ); + type: SyncEntityType.PartnerAssetExifV1, + }, + ]); - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]); - expect(ackSyncResponse).toHaveLength(0); + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]); }); it('should not sync partner asset exif for own user', async () => { - const { auth, getRepository, testSync } = await setup(); + const { auth, ctx } = await setup(); + const { user: user2 } = await ctx.newUser(); + await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); + const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); + await ctx.newExif({ assetId: asset.id, make: 'Canon' }); - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const partnerRepo = getRepository('partner'); - await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); - await assetRepo.create(asset); - await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); - - await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); - await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0); }); it('should not sync partner asset exif for unrelated user', async () => { - const { auth, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - - const user2 = mediumFactory.userInsert(); - const user3 = mediumFactory.userInsert(); - await Promise.all([userRepo.create(user2), userRepo.create(user3)]); - - const partnerRepo = getRepository('partner'); - await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: user3.id }); - await assetRepo.create(asset); - await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }); - - const sessionRepo = getRepository('session'); - const session = mediumFactory.sessionInsert({ userId: user3.id }); - await sessionRepo.create(session); - + const { auth, ctx } = await setup(); + const { user: user2 } = await ctx.newUser(); + const { user: user3 } = await ctx.newUser(); + await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); + const { asset } = await ctx.newAsset({ ownerId: user3.id }); + await ctx.newExif({ assetId: asset.id, make: 'Canon' }); + const { session } = await ctx.newSession({ userId: user3.id }); const authUser3 = factory.auth({ session, user: user3 }); - await expect(testSync(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); - await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0); + + await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0); }); it('should backfill partner asset exif when a partner shared their library with you', async () => { - const { auth, sut, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - const user3 = mediumFactory.userInsert(); - await userRepo.create(user2); - await userRepo.create(user3); - - const assetRepo = getRepository('asset'); - const assetUser3 = mediumFactory.assetInsert({ ownerId: user3.id }); - const assetUser2 = mediumFactory.assetInsert({ ownerId: user2.id }); - await assetRepo.create(assetUser3); - await assetRepo.upsertExif({ assetId: assetUser3.id, make: 'Canon' }); + const { auth, ctx } = await setup(); + const { user: user2 } = await ctx.newUser(); + const { user: user3 } = await ctx.newUser(); + const { asset: assetUser3 } = await ctx.newAsset({ ownerId: user3.id }); + await ctx.newExif({ assetId: assetUser3.id, make: 'Canon' }); await wait(2); - await assetRepo.create(assetUser2); - await assetRepo.upsertExif({ assetId: assetUser2.id, make: 'Canon' }); - - const partnerRepo = getRepository('partner'); - await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - - const response = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]); + const { asset: assetUser2 } = await ctx.newAsset({ ownerId: user2.id }); + await ctx.newExif({ assetId: assetUser2.id, make: 'Canon' }); + await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); + const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); expect(response).toHaveLength(1); expect(response).toEqual( expect.arrayContaining([ @@ -162,189 +115,133 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => { ]), ); - const acks = response.map(({ ack }) => ack); - await sut.setAcks(auth, { acks }); + await ctx.syncAckAll(auth, response); + await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id }); - await partnerRepo.create({ sharedById: user3.id, sharedWithId: auth.user.id }); - const backfillResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]); + const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); + expect(newResponse).toHaveLength(2); + expect(newResponse).toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + assetId: assetUser3.id, + }), + type: SyncEntityType.PartnerAssetExifBackfillV1, + }, + { + ack: expect.any(String), + data: {}, + type: SyncEntityType.SyncAckV1, + }, + ]); - expect(backfillResponse).toHaveLength(2); - expect(backfillResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: expect.objectContaining({ - assetId: assetUser3.id, - }), - type: SyncEntityType.PartnerAssetExifBackfillV1, - }, - { - ack: expect.any(String), - data: {}, - type: SyncEntityType.SyncAckV1, - }, - ]), - ); - - const backfillAck = backfillResponse[1].ack; - await sut.setAcks(auth, { acks: [backfillAck] }); - - const finalResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]); - - const finalAcks = finalResponse.map(({ ack }) => ack); - expect(finalAcks).toEqual([]); + await ctx.syncAckAll(auth, newResponse); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]); }); it('should handle partners with users ids lower than a uuidv7', async () => { - const { auth, sut, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert({ id: '00d4c0af-7695-4cf2-85e6-415eeaf449cb' }); - const user3 = mediumFactory.userInsert({ id: '00e4c0af-7695-4cf2-85e6-415eeaf449cb' }); - await userRepo.create(user2); - await userRepo.create(user3); - - const assetRepo = getRepository('asset'); - const assetUser3 = mediumFactory.assetInsert({ ownerId: user3.id }); - await assetRepo.create(assetUser3); - await assetRepo.upsertExif({ assetId: assetUser3.id, make: 'assetUser3' }); - + const { auth, ctx } = await setup(); + const { user: user2 } = await ctx.newUser({ id: '00d4c0af-7695-4cf2-85e6-415eeaf449cb' }); + const { user: user3 } = await ctx.newUser({ id: '00e4c0af-7695-4cf2-85e6-415eeaf449cb' }); + const { asset: assetUser3 } = await ctx.newAsset({ ownerId: user3.id }); + await ctx.newExif({ assetId: assetUser3.id, make: 'assetUser3' }); await wait(2); + const { asset: assetUser2 } = await ctx.newAsset({ ownerId: user2.id }); + await ctx.newExif({ assetId: assetUser2.id, make: 'assetUser2' }); + await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); - const assetUser2 = mediumFactory.assetInsert({ ownerId: user2.id }); - await assetRepo.create(assetUser2); - await assetRepo.upsertExif({ assetId: assetUser2.id, make: 'assetUser2' }); - - const partnerRepo = getRepository('partner'); - await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - - const response = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]); - + const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); expect(response).toHaveLength(1); - expect(response).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: expect.objectContaining({ - assetId: assetUser2.id, - }), - type: SyncEntityType.PartnerAssetExifV1, - }, - ]), - ); - - const acks = response.map(({ ack }) => ack); - await sut.setAcks(auth, { acks }); + expect(response).toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + assetId: assetUser2.id, + }), + type: SyncEntityType.PartnerAssetExifV1, + }, + ]); + await ctx.syncAckAll(auth, response); // This checks that our ack upsert is correct - const ackUpsertResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]); - expect(ackUpsertResponse).toEqual([]); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]); + await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id }); - await partnerRepo.create({ sharedById: user3.id, sharedWithId: auth.user.id }); + const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); + expect(newResponse).toHaveLength(2); + expect(newResponse).toEqual([ + { + ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)), + data: expect.objectContaining({ + assetId: assetUser3.id, + }), + type: SyncEntityType.PartnerAssetExifBackfillV1, + }, + { + ack: expect.stringContaining(SyncEntityType.PartnerAssetExifBackfillV1), + data: {}, + type: SyncEntityType.SyncAckV1, + }, + ]); - const syncAckResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]); - expect(syncAckResponse).toHaveLength(2); - expect(syncAckResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)), - data: expect.objectContaining({ - assetId: assetUser3.id, - }), - type: SyncEntityType.PartnerAssetExifBackfillV1, - }, - { - ack: expect.stringContaining(SyncEntityType.PartnerAssetExifBackfillV1), - data: {}, - type: SyncEntityType.SyncAckV1, - }, - ]), - ); - - const syncAckResponseAcks = syncAckResponse.map(({ ack }) => ack); - await sut.setAcks(auth, { acks: [syncAckResponseAcks[1]] }); - - const finalResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]); - expect(finalResponse).toEqual([]); + await ctx.syncAckAll(auth, newResponse); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]); }); it('should only backfill partner assets created prior to the current partner asset checkpoint', async () => { - const { auth, sut, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - const user3 = mediumFactory.userInsert(); - await userRepo.create(user2); - await userRepo.create(user3); - - const assetRepo = getRepository('asset'); - const assetUser3 = mediumFactory.assetInsert({ ownerId: user3.id }); - const assetUser2 = mediumFactory.assetInsert({ ownerId: user2.id }); - const asset2User3 = mediumFactory.assetInsert({ ownerId: user3.id }); - await assetRepo.create(assetUser3); - await assetRepo.upsertExif({ assetId: assetUser3.id, make: 'assetUser3' }); + const { auth, ctx } = await setup(); + const { user: user2 } = await ctx.newUser(); + const { user: user3 } = await ctx.newUser(); + const { asset: assetUser3 } = await ctx.newAsset({ ownerId: user3.id }); + await ctx.newExif({ assetId: assetUser3.id, make: 'assetUser3' }); await wait(2); - await assetRepo.create(assetUser2); - await assetRepo.upsertExif({ assetId: assetUser2.id, make: 'assetUser2' }); + const { asset: assetUser2 } = await ctx.newAsset({ ownerId: user2.id }); + await ctx.newExif({ assetId: assetUser2.id, make: 'assetUser2' }); await wait(2); - await assetRepo.create(asset2User3); - await assetRepo.upsertExif({ assetId: asset2User3.id, make: 'asset2User3' }); - - const partnerRepo = getRepository('partner'); - await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - - const response = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]); + const { asset: asset2User3 } = await ctx.newAsset({ ownerId: user3.id }); + await ctx.newExif({ assetId: asset2User3.id, make: 'asset2User3' }); + await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); + const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); expect(response).toHaveLength(1); - expect(response).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: expect.objectContaining({ - assetId: assetUser2.id, - }), - type: SyncEntityType.PartnerAssetExifV1, - }, - ]), - ); + expect(response).toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + assetId: assetUser2.id, + }), + type: SyncEntityType.PartnerAssetExifV1, + }, + ]); - const acks = response.map(({ ack }) => ack); - await sut.setAcks(auth, { acks }); + await ctx.syncAckAll(auth, response); + await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id }); - await partnerRepo.create({ sharedById: user3.id, sharedWithId: auth.user.id }); - const backfillResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]); + const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]); + expect(newResponse).toHaveLength(3); + expect(newResponse).toEqual([ + { + ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)), + data: expect.objectContaining({ + assetId: assetUser3.id, + }), + type: SyncEntityType.PartnerAssetExifBackfillV1, + }, + { + ack: expect.stringContaining(SyncEntityType.PartnerAssetExifBackfillV1), + data: {}, + type: SyncEntityType.SyncAckV1, + }, + { + ack: expect.any(String), + data: expect.objectContaining({ + assetId: asset2User3.id, + }), + type: SyncEntityType.PartnerAssetExifV1, + }, + ]); - expect(backfillResponse).toHaveLength(3); - expect(backfillResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)), - data: expect.objectContaining({ - assetId: assetUser3.id, - }), - type: SyncEntityType.PartnerAssetExifBackfillV1, - }, - { - ack: expect.stringContaining(SyncEntityType.PartnerAssetExifBackfillV1), - data: {}, - type: SyncEntityType.SyncAckV1, - }, - { - ack: expect.any(String), - data: expect.objectContaining({ - assetId: asset2User3.id, - }), - type: SyncEntityType.PartnerAssetExifV1, - }, - ]), - ); - - const backfillAck = backfillResponse[1].ack; - const partnerAssetAck = backfillResponse[2].ack; - await sut.setAcks(auth, { acks: [backfillAck, partnerAssetAck] }); - const finalResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]); - - const finalAcks = finalResponse.map(({ ack }) => ack); - expect(finalAcks).toEqual([]); + await ctx.syncAckAll(auth, newResponse); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]); }); }); diff --git a/server/test/medium/specs/sync/sync-partner-asset.spec.ts b/server/test/medium/specs/sync/sync-partner-asset.spec.ts index fe3d4edbcc..4faa2f6966 100644 --- a/server/test/medium/specs/sync/sync-partner-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-partner-asset.spec.ts @@ -1,18 +1,19 @@ import { Kysely } from 'kysely'; import { DB } from 'src/db'; import { SyncEntityType, SyncRequestType } from 'src/enum'; -import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory'; +import { AssetRepository } from 'src/repositories/asset.repository'; +import { PartnerRepository } from 'src/repositories/partner.repository'; +import { UserRepository } from 'src/repositories/user.repository'; +import { SyncTestContext } from 'test/medium.factory'; import { factory } from 'test/small.factory'; import { getKyselyDB, wait } from 'test/utils'; let defaultDatabase: Kysely; const setup = async (db?: Kysely) => { - const database = db || defaultDatabase; - const result = newSyncTest({ db: database }); - const { auth, create } = newSyncAuthUser(); - await create(database); - return { ...result, auth }; + const ctx = new SyncTestContext(db || defaultDatabase); + const { auth, user, session } = await ctx.newSyncAuthUser(); + return { auth, user, session, ctx }; }; beforeAll(async () => { @@ -21,19 +22,15 @@ beforeAll(async () => { describe(SyncRequestType.PartnerAssetsV1, () => { it('should detect and sync the first partner asset', async () => { - const { auth, sut, getRepository, testSync } = await setup(); + const { auth, ctx } = await setup(); const originalFileName = 'firstPartnerAsset'; const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const date = new Date().toISOString(); - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ + const { user: user2 } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user2.id, originalFileName, checksum: Buffer.from(checksum, 'base64'), @@ -44,315 +41,217 @@ describe(SyncRequestType.PartnerAssetsV1, () => { deletedAt: null, duration: '0:10:00.00000', }); - await assetRepo.create(asset); - const partnerRepo = getRepository('partner'); - await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); + await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); - const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); - - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - id: asset.id, - ownerId: asset.ownerId, - originalFileName, - thumbhash, - checksum, - deletedAt: null, - fileCreatedAt: date, - fileModifiedAt: date, - isFavorite: false, - localDateTime: date, - type: asset.type, - visibility: asset.visibility, - duration: asset.duration, - }, - type: SyncEntityType.PartnerAssetV1, + const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + id: asset.id, + ownerId: asset.ownerId, + originalFileName, + thumbhash, + checksum, + deletedAt: null, + fileCreatedAt: date, + fileModifiedAt: date, + isFavorite: false, + localDateTime: date, + type: asset.type, + visibility: asset.visibility, + duration: asset.duration, }, - ]), - ); + type: SyncEntityType.PartnerAssetV1, + }, + ]); - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); - - expect(ackSyncResponse).toHaveLength(0); + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]); }); it('should detect and sync a deleted partner asset', async () => { - const { auth, sut, getRepository, testSync } = await setup(); + const { auth, ctx } = await setup(); + const assetRepo = ctx.get(AssetRepository); - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - const asset = mediumFactory.assetInsert({ ownerId: user2.id }); - - const assetRepo = getRepository('asset'); - await assetRepo.create(asset); - - const partnerRepo = getRepository('partner'); - await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); + const { user: user2 } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user2.id }); + await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); await assetRepo.remove(asset); - const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); - + const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); expect(response).toHaveLength(1); - expect(response).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - assetId: asset.id, - }, - type: SyncEntityType.PartnerAssetDeleteV1, + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + assetId: asset.id, }, - ]), - ); + type: SyncEntityType.PartnerAssetDeleteV1, + }, + ]); - const acks = response.map(({ ack }) => ack); - await sut.setAcks(auth, { acks }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); - - expect(ackSyncResponse).toHaveLength(0); + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]); }); it('should not sync a deleted partner asset due to a user delete', async () => { - const { auth, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const partnerRepo = getRepository('partner'); - await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - - const assetRepo = getRepository('asset'); - await assetRepo.create(mediumFactory.assetInsert({ ownerId: user2.id })); + const { auth, ctx } = await setup(); + const userRepo = ctx.get(UserRepository); + const { user: user2 } = await ctx.newUser(); + await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); + await ctx.newAsset({ ownerId: user2.id }); await userRepo.delete({ id: user2.id }, true); - - const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); - expect(response).toHaveLength(0); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]); }); it('should not sync a deleted partner asset due to a partner delete (unshare)', async () => { - const { auth, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const assetRepo = getRepository('asset'); - await assetRepo.create(mediumFactory.assetInsert({ ownerId: user2.id })); - - const partnerRepo = getRepository('partner'); - const partner = { sharedById: user2.id, sharedWithId: auth.user.id }; - await partnerRepo.create(partner); - - await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(1); + const { auth, ctx } = await setup(); + const partnerRepo = ctx.get(PartnerRepository); + const { user: user2 } = await ctx.newUser(); + await ctx.newAsset({ ownerId: user2.id }); + const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(1); await partnerRepo.remove(partner); - - await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]); }); it('should not sync an asset or asset delete for own user', async () => { - const { auth, getRepository, testSync } = await setup(); + const { auth, ctx } = await setup(); + const assetRepo = ctx.get(AssetRepository); - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); + const { user: user2 } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); + await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); - await assetRepo.create(asset); - - const partnerRepo = getRepository('partner'); - await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - - await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); - await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); await assetRepo.remove(asset); - await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); - await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); }); it('should not sync an asset or asset delete for unrelated user', async () => { - const { auth, getRepository, testSync } = await setup(); - - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const sessionRepo = getRepository('session'); - const session = mediumFactory.sessionInsert({ userId: user2.id }); - await sessionRepo.create(session); + const { auth, ctx } = await setup(); + const assetRepo = ctx.get(AssetRepository); + const { user: user2 } = await ctx.newUser(); + const { session } = await ctx.newSession({ userId: user2.id }); + const { asset } = await ctx.newAsset({ ownerId: user2.id }); const auth2 = factory.auth({ session, user: user2 }); - const assetRepo = getRepository('asset'); - const asset = mediumFactory.assetInsert({ ownerId: user2.id }); - await assetRepo.create(asset); - - await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); - await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); await assetRepo.remove(asset); - await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); - await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); + await expect(ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); }); it('should backfill partner assets when a partner shared their library with you', async () => { - const { auth, sut, getRepository, testSync } = await setup(); + const { auth, ctx } = await setup(); - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - const user3 = mediumFactory.userInsert(); - await userRepo.create(user2); - await userRepo.create(user3); - - const assetRepo = getRepository('asset'); - const assetUser3 = mediumFactory.assetInsert({ ownerId: user3.id }); - const assetUser2 = mediumFactory.assetInsert({ ownerId: user2.id }); - await assetRepo.create(assetUser3); + const { user: user2 } = await ctx.newUser(); + const { user: user3 } = await ctx.newUser(); + const { asset: assetUser3 } = await ctx.newAsset({ ownerId: user3.id }); await wait(2); - await assetRepo.create(assetUser2); - - const partnerRepo = getRepository('partner'); - await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - - const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); + const { asset: assetUser2 } = await ctx.newAsset({ ownerId: user2.id }); + await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); + const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); expect(response).toHaveLength(1); - expect(response).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: expect.objectContaining({ - id: assetUser2.id, - }), - type: SyncEntityType.PartnerAssetV1, - }, - ]), - ); + expect(response).toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + id: assetUser2.id, + }), + type: SyncEntityType.PartnerAssetV1, + }, + ]); - const acks = response.map(({ ack }) => ack); - await sut.setAcks(auth, { acks }); + await ctx.syncAckAll(auth, response); + await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id }); - await partnerRepo.create({ sharedById: user3.id, sharedWithId: auth.user.id }); - const backfillResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); + const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); + expect(newResponse).toHaveLength(2); + expect(newResponse).toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + id: assetUser3.id, + }), + type: SyncEntityType.PartnerAssetBackfillV1, + }, + { + ack: expect.stringContaining(SyncEntityType.PartnerAssetBackfillV1), + data: {}, + type: SyncEntityType.SyncAckV1, + }, + ]); - expect(backfillResponse).toHaveLength(2); - expect(backfillResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: expect.objectContaining({ - id: assetUser3.id, - }), - type: SyncEntityType.PartnerAssetBackfillV1, - }, - { - ack: expect.stringContaining(SyncEntityType.PartnerAssetBackfillV1), - data: {}, - type: SyncEntityType.SyncAckV1, - }, - ]), - ); - - const backfillAck = backfillResponse[1].ack; - await sut.setAcks(auth, { acks: [backfillAck] }); - - const finalResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); - - const finalAcks = finalResponse.map(({ ack }) => ack); - expect(finalAcks).toEqual([]); + await ctx.syncAckAll(auth, newResponse); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]); }); it('should only backfill partner assets created prior to the current partner asset checkpoint', async () => { - const { auth, sut, getRepository, testSync } = await setup(); + const { auth, ctx } = await setup(); - const userRepo = getRepository('user'); - const user2 = mediumFactory.userInsert(); - const user3 = mediumFactory.userInsert(); - await userRepo.create(user2); - await userRepo.create(user3); - - const assetRepo = getRepository('asset'); - const assetUser3 = mediumFactory.assetInsert({ ownerId: user3.id }); - const assetUser2 = mediumFactory.assetInsert({ ownerId: user2.id }); - const asset2User3 = mediumFactory.assetInsert({ ownerId: user3.id }); - await assetRepo.create(assetUser3); + const { user: user2 } = await ctx.newUser(); + const { user: user3 } = await ctx.newUser(); + const { asset: assetUser3 } = await ctx.newAsset({ ownerId: user3.id }); await wait(2); - await assetRepo.create(assetUser2); + const { asset: assetUser2 } = await ctx.newAsset({ ownerId: user2.id }); await wait(2); - await assetRepo.create(asset2User3); - - const partnerRepo = getRepository('partner'); - await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); - - const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); + const { asset: asset2User3 } = await ctx.newAsset({ ownerId: user3.id }); + await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); + const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); expect(response).toHaveLength(1); - expect(response).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: expect.objectContaining({ - id: assetUser2.id, - }), - type: SyncEntityType.PartnerAssetV1, - }, - ]), - ); + expect(response).toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + id: assetUser2.id, + }), + type: SyncEntityType.PartnerAssetV1, + }, + ]); + await ctx.syncAckAll(auth, response); - const acks = response.map(({ ack }) => ack); - await sut.setAcks(auth, { acks }); + await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id }); + const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]); + expect(newResponse).toHaveLength(3); + expect(newResponse).toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + id: assetUser3.id, + }), + type: SyncEntityType.PartnerAssetBackfillV1, + }, + { + ack: expect.stringContaining(SyncEntityType.PartnerAssetBackfillV1), + data: {}, + type: SyncEntityType.SyncAckV1, + }, + { + ack: expect.any(String), + data: expect.objectContaining({ + id: asset2User3.id, + }), + type: SyncEntityType.PartnerAssetV1, + }, + ]); - await partnerRepo.create({ sharedById: user3.id, sharedWithId: auth.user.id }); - const backfillResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); - - expect(backfillResponse).toHaveLength(3); - expect(backfillResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: expect.objectContaining({ - id: assetUser3.id, - }), - type: SyncEntityType.PartnerAssetBackfillV1, - }, - { - ack: expect.stringContaining(SyncEntityType.PartnerAssetBackfillV1), - data: {}, - type: SyncEntityType.SyncAckV1, - }, - { - ack: expect.any(String), - data: expect.objectContaining({ - id: asset2User3.id, - }), - type: SyncEntityType.PartnerAssetV1, - }, - ]), - ); - - const backfillAck = backfillResponse[1].ack; - const partnerAssetAck = backfillResponse[2].ack; - await sut.setAcks(auth, { acks: [backfillAck, partnerAssetAck] }); - - const finalResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); - - const finalAcks = finalResponse.map(({ ack }) => ack); - expect(finalAcks).toEqual([]); + await ctx.syncAckAll(auth, newResponse); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]); }); }); diff --git a/server/test/medium/specs/sync/sync-partner.spec.ts b/server/test/medium/specs/sync/sync-partner.spec.ts index f262eec853..11d843243d 100644 --- a/server/test/medium/specs/sync/sync-partner.spec.ts +++ b/server/test/medium/specs/sync/sync-partner.spec.ts @@ -1,75 +1,58 @@ import { Kysely } from 'kysely'; import { DB } from 'src/db'; import { SyncEntityType, SyncRequestType } from 'src/enum'; -import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory'; +import { PartnerRepository } from 'src/repositories/partner.repository'; +import { UserRepository } from 'src/repositories/user.repository'; +import { SyncTestContext } from 'test/medium.factory'; import { getKyselyDB } from 'test/utils'; let defaultDatabase: Kysely; const setup = async (db?: Kysely) => { - const database = db || defaultDatabase; - const result = newSyncTest({ db: database }); - const { auth, create } = newSyncAuthUser(); - await create(database); - return { ...result, auth }; + const ctx = new SyncTestContext(db || defaultDatabase); + const { auth, user, session } = await ctx.newSyncAuthUser(); + return { auth, user, session, ctx }; }; beforeAll(async () => { defaultDatabase = await getKyselyDB(); }); -describe.concurrent(SyncEntityType.PartnerV1, () => { +describe(SyncEntityType.PartnerV1, () => { it('should detect and sync the first partner', async () => { - const { auth, sut, getRepository, testSync } = await setup(); + const { auth, user: user1, ctx } = await setup(); - const user1 = auth.user; - const userRepo = getRepository('user'); - const partnerRepo = getRepository('partner'); + const { user: user2 } = await ctx.newUser(); + const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user1.id }); - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); - - const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id }); - - const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); - - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - inTimeline: partner.inTimeline, - sharedById: partner.sharedById, - sharedWithId: partner.sharedWithId, - }, - type: 'PartnerV1', + const response = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + inTimeline: partner.inTimeline, + sharedById: partner.sharedById, + sharedWithId: partner.sharedWithId, }, - ]), - ); + type: 'PartnerV1', + }, + ]); - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); - - expect(ackSyncResponse).toHaveLength(0); + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]); }); it('should detect and sync a deleted partner', async () => { - const { auth, sut, getRepository, testSync } = await setup(); + const { auth, user: user1, ctx } = await setup(); - const userRepo = getRepository('user'); - const user1 = auth.user; - const user2 = mediumFactory.userInsert(); - await userRepo.create(user2); + const partnerRepo = ctx.get(PartnerRepository); - const partnerRepo = getRepository('partner'); - const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id }); + const { user: user2 } = await ctx.newUser(); + const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user1.id }); await partnerRepo.remove(partner); - const response = await testSync(auth, [SyncRequestType.PartnersV1]); - + const response = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]); expect(response).toHaveLength(1); expect(response).toEqual( expect.arrayContaining([ @@ -84,27 +67,18 @@ describe.concurrent(SyncEntityType.PartnerV1, () => { ]), ); - const acks = response.map(({ ack }) => ack); - await sut.setAcks(auth, { acks }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); - - expect(ackSyncResponse).toHaveLength(0); + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]); }); it('should detect and sync a partner share both to and from another user', async () => { - const { auth, sut, getRepository, testSync } = await setup(); + const { auth, user: user1, ctx } = await setup(); - const userRepo = getRepository('user'); - const user1 = auth.user; - const user2 = await userRepo.create(mediumFactory.userInsert()); - - const partnerRepo = getRepository('partner'); - const partner1 = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id }); - const partner2 = await partnerRepo.create({ sharedById: user1.id, sharedWithId: user2.id }); - - const response = await testSync(auth, [SyncRequestType.PartnersV1]); + const { user: user2 } = await ctx.newUser(); + const { partner: partner1 } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user1.id }); + const { partner: partner2 } = await ctx.newPartner({ sharedById: user1.id, sharedWithId: user2.id }); + const response = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]); expect(response).toHaveLength(2); expect(response).toEqual( expect.arrayContaining([ @@ -129,93 +103,80 @@ describe.concurrent(SyncEntityType.PartnerV1, () => { ]), ); - await sut.setAcks(auth, { acks: [response[1].ack] }); - - const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); - - expect(ackSyncResponse).toHaveLength(0); + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]); }); it('should sync a partner and then an update to that same partner', async () => { - const { auth, sut, getRepository, testSync } = await setup(); + const { auth, user: user1, ctx } = await setup(); - const userRepo = getRepository('user'); - const user1 = auth.user; - const user2 = await userRepo.create(mediumFactory.userInsert()); + const partnerRepo = ctx.get(PartnerRepository); - const partnerRepo = getRepository('partner'); - const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id }); + const { user: user2 } = await ctx.newUser(); + const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user1.id }); - const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); - - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - inTimeline: partner.inTimeline, - sharedById: partner.sharedById, - sharedWithId: partner.sharedWithId, - }, - type: 'PartnerV1', + const response = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + inTimeline: partner.inTimeline, + sharedById: partner.sharedById, + sharedWithId: partner.sharedWithId, }, - ]), - ); + type: 'PartnerV1', + }, + ]); - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); + await ctx.syncAckAll(auth, response); const updated = await partnerRepo.update( { sharedById: partner.sharedById, sharedWithId: partner.sharedWithId }, { inTimeline: true }, ); - const updatedSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); - - expect(updatedSyncResponse).toHaveLength(1); - expect(updatedSyncResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - inTimeline: updated.inTimeline, - sharedById: updated.sharedById, - sharedWithId: updated.sharedWithId, - }, - type: 'PartnerV1', + const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]); + expect(newResponse).toHaveLength(1); + expect(newResponse).toEqual([ + { + ack: expect.any(String), + data: { + inTimeline: updated.inTimeline, + sharedById: updated.sharedById, + sharedWithId: updated.sharedWithId, }, - ]), - ); + type: 'PartnerV1', + }, + ]); + + await ctx.syncAckAll(auth, newResponse); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]); }); it('should not sync a partner or partner delete for an unrelated user', async () => { - const { auth, getRepository, testSync } = await setup(); + const { auth, ctx } = await setup(); - const userRepo = getRepository('user'); - const user2 = await userRepo.create(mediumFactory.userInsert()); - const user3 = await userRepo.create(mediumFactory.userInsert()); + const partnerRepo = ctx.get(PartnerRepository); - const partnerRepo = getRepository('partner'); - const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user3.id }); - - expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0); + const { user: user2 } = await ctx.newUser(); + const { user: user3 } = await ctx.newUser(); + const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user3.id }); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]); await partnerRepo.remove(partner); - - expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]); }); it('should not sync a partner delete after a user is deleted', async () => { - const { auth, getRepository, testSync } = await setup(); + const { auth, ctx } = await setup(); - const userRepo = getRepository('user'); - const user2 = await userRepo.create(mediumFactory.userInsert()); + const userRepo = ctx.get(UserRepository); - const partnerRepo = getRepository('partner'); - await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); + const { user: user2 } = await ctx.newUser(); + await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id }); await userRepo.delete({ id: user2.id }, true); - expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0); + await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]); }); }); diff --git a/server/test/medium/specs/sync/sync-user.spec.ts b/server/test/medium/specs/sync/sync-user.spec.ts index 2cea38267c..10a06bd9f7 100644 --- a/server/test/medium/specs/sync/sync-user.spec.ts +++ b/server/test/medium/specs/sync/sync-user.spec.ts @@ -1,36 +1,35 @@ import { Kysely } from 'kysely'; import { DB } from 'src/db'; import { SyncEntityType, SyncRequestType } from 'src/enum'; -import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory'; +import { UserRepository } from 'src/repositories/user.repository'; +import { SyncTestContext } from 'test/medium.factory'; import { getKyselyDB } from 'test/utils'; let defaultDatabase: Kysely; const setup = async (db?: Kysely) => { - const database = db || defaultDatabase; - const result = newSyncTest({ db: database }); - const { auth, create } = newSyncAuthUser(); - await create(database); - return { ...result, auth }; + const ctx = new SyncTestContext(db || defaultDatabase); + const { auth, user, session } = await ctx.newSyncAuthUser(); + return { auth, user, session, ctx }; }; beforeAll(async () => { defaultDatabase = await getKyselyDB(); }); -describe.concurrent(SyncEntityType.UserV1, () => { +describe(SyncEntityType.UserV1, () => { it('should detect and sync the first user', async () => { - const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB()); + const { auth, ctx } = await setup(await getKyselyDB()); - const userRepo = getRepository('user'); + const userRepo = ctx.get(UserRepository); const user = await userRepo.get(auth.user.id, { withDeleted: false }); if (!user) { expect.fail('First user should exist'); } - const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]); - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual([ + const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ { ack: expect.any(String), data: { @@ -43,21 +42,17 @@ describe.concurrent(SyncEntityType.UserV1, () => { }, ]); - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); - const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]); - - expect(ackSyncResponse).toHaveLength(0); + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.UsersV1])).resolves.toEqual([]); }); it('should detect and sync a soft deleted user', async () => { - const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB()); + const { auth, ctx } = await setup(await getKyselyDB()); const deletedAt = new Date().toISOString(); - const deletedUser = mediumFactory.userInsert({ deletedAt }); - const deleted = await getRepository('user').create(deletedUser); + const { user: deleted } = await ctx.newUser({ deletedAt }); - const response = await testSync(auth, [SyncRequestType.UsersV1]); + const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]); expect(response).toHaveLength(2); expect(response).toEqual( @@ -85,95 +80,81 @@ describe.concurrent(SyncEntityType.UserV1, () => { ]), ); - const acks = [response[1].ack]; - await sut.setAcks(auth, { acks }); - const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]); - - expect(ackSyncResponse).toHaveLength(0); + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.UsersV1])).resolves.toEqual([]); }); it('should detect and sync a deleted user', async () => { - const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB()); + const { auth, ctx } = await setup(await getKyselyDB()); - const userRepo = getRepository('user'); - const user = mediumFactory.userInsert(); - await userRepo.create(user); + const userRepo = ctx.get(UserRepository); + + const { user } = await ctx.newUser(); await userRepo.delete({ id: user.id }, true); - const response = await testSync(auth, [SyncRequestType.UsersV1]); - + const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]); expect(response).toHaveLength(2); - expect(response).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - userId: user.id, - }, - type: 'UserDeleteV1', + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + userId: user.id, }, - { - ack: expect.any(String), - data: { - deletedAt: null, - email: auth.user.email, - id: auth.user.id, - name: auth.user.name, - }, - type: 'UserV1', + type: 'UserDeleteV1', + }, + { + ack: expect.any(String), + data: { + deletedAt: null, + email: auth.user.email, + id: auth.user.id, + name: auth.user.name, }, - ]), - ); + type: 'UserV1', + }, + ]); - const acks = response.map(({ ack }) => ack); - await sut.setAcks(auth, { acks }); - const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]); - - expect(ackSyncResponse).toHaveLength(0); + await ctx.syncAckAll(auth, response); + await expect(ctx.syncStream(auth, [SyncRequestType.UsersV1])).resolves.toEqual([]); }); it('should sync a user and then an update to that same user', async () => { - const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB()); + const { auth, ctx } = await setup(await getKyselyDB()); - const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]); + const userRepo = ctx.get(UserRepository); - expect(initialSyncResponse).toHaveLength(1); - expect(initialSyncResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - deletedAt: null, - email: auth.user.email, - id: auth.user.id, - name: auth.user.name, - }, - type: 'UserV1', + const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]); + expect(response).toHaveLength(1); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + deletedAt: null, + email: auth.user.email, + id: auth.user.id, + name: auth.user.name, }, - ]), - ); + type: 'UserV1', + }, + ]); - const acks = [initialSyncResponse[0].ack]; - await sut.setAcks(auth, { acks }); + await ctx.syncAckAll(auth, response); - const userRepo = getRepository('user'); const updated = await userRepo.update(auth.user.id, { name: 'new name' }); - const updatedSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]); - expect(updatedSyncResponse).toHaveLength(1); - expect(updatedSyncResponse).toEqual( - expect.arrayContaining([ - { - ack: expect.any(String), - data: { - deletedAt: null, - email: auth.user.email, - id: auth.user.id, - name: updated.name, - }, - type: 'UserV1', + const newResponse = await ctx.syncStream(auth, [SyncRequestType.UsersV1]); + expect(newResponse).toHaveLength(1); + expect(newResponse).toEqual([ + { + ack: expect.any(String), + data: { + deletedAt: null, + email: auth.user.email, + id: auth.user.id, + name: updated.name, }, - ]), - ); + type: 'UserV1', + }, + ]); }); });