refactor: medium tests (#19537)

This commit is contained in:
Jason Rasmussen 2025-06-26 15:32:06 -04:00 committed by GitHub
parent b96c95beda
commit 3105094a3d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1579 additions and 2253 deletions

View File

@ -53,6 +53,53 @@ import { UserTable } from 'src/schema/tables/user.table';
import { AccessRequest, checkAccess, requireAccess } from 'src/utils/access'; import { AccessRequest, checkAccess, requireAccess } from 'src/utils/access';
import { getConfig, updateConfig } from 'src/utils/config'; 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() @Injectable()
export class BaseService { export class BaseService {
protected storageCore: StorageCore; protected storageCore: StorageCore;

View File

@ -1,12 +1,12 @@
import { ClassConstructor } from 'class-transformer'; /* eslint-disable @typescript-eslint/no-unsafe-function-type */
import { Insertable, Kysely } from 'kysely'; import { Insertable, Kysely } from 'kysely';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { createHash, randomBytes } from 'node:crypto'; import { createHash, randomBytes } from 'node:crypto';
import { Writable } from 'node:stream'; import { Writable } from 'node:stream';
import { AssetFace } from 'src/database'; 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 { 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 { AccessRepository } from 'src/repositories/access.repository';
import { ActivityRepository } from 'src/repositories/activity.repository'; import { ActivityRepository } from 'src/repositories/activity.repository';
import { AlbumUserRepository } from 'src/repositories/album-user.repository'; import { AlbumUserRepository } from 'src/repositories/album-user.repository';
@ -31,67 +31,147 @@ import { SystemMetadataRepository } from 'src/repositories/system-metadata.repos
import { UserRepository } from 'src/repositories/user.repository'; import { UserRepository } from 'src/repositories/user.repository';
import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
import { UserTable } from 'src/schema/tables/user.table'; 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 { SyncService } from 'src/services/sync.service';
import { RepositoryInterface } from 'src/types';
import { factory, newDate, newEmbedding, newUuid } from 'test/small.factory'; 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'; import { Mocked } from 'vitest';
const sha256 = (value: string) => createHash('sha256').update(value).digest('base64'); interface ClassConstructor<T = any> extends Function {
new (...args: any[]): T;
}
// type Repositories = Omit<ServiceOverrides, 'access' | 'telemetry'>; type MediumTestOptions = {
type RepositoriesTypes = { mock: ClassConstructor<any>[];
access: AccessRepository; real: ClassConstructor<any>[];
activity: ActivityRepository; database: Kysely<DB>;
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<RepositoryInterface<RepositoriesTypes[K]>> };
type RepositoryOptions = Partial<{ [K in keyof RepositoriesTypes]: 'mock' | 'real' }>;
type ContextRepositoryMocks<R extends RepositoryOptions> = {
[K in keyof RepositoriesTypes as R[K] extends 'mock' ? K : never]: Mocked<RepositoryInterface<RepositoriesTypes[K]>>;
}; };
type ContextRepositories<R extends RepositoryOptions> = { export const newMediumService = <S extends BaseService>(Service: ClassConstructor<S>, options: MediumTestOptions) => {
[K in keyof RepositoriesTypes as R[K] extends 'real' ? K : never]: RepositoriesTypes[K]; const ctx = new MediumTestContext(Service, options);
return { sut: ctx.sut, ctx };
}; };
export type Context<R extends RepositoryOptions, S extends BaseService> = { export class MediumTestContext<S extends BaseService = BaseService> {
private repoCache: Record<string, any> = {};
private sutDeps: any[];
sut: S; sut: S;
mocks: ContextRepositoryMocks<R>; database: Kysely<DB>;
repos: ContextRepositories<R>;
getRepository<T extends keyof RepositoriesTypes>(key: T): RepositoriesTypes[T];
};
export type SyncTestOptions = { constructor(
db: Kysely<DB>; Service: ClassConstructor<S>,
}; private options: MediumTestOptions,
) {
this.sutDeps = this.makeDeps(options);
this.sut = new Service(...this.sutDeps);
this.database = options.database;
}
export const newSyncAuthUser = () => { private makeDeps(options: MediumTestOptions) {
const user = mediumFactory.userInsert(); const deps = BASE_SERVICE_DEPENDENCIES;
const session = mediumFactory.sessionInsert({ userId: user.id });
for (const dep of options.mock) {
if (!deps.includes(dep)) {
throw new Error(`Mocked repository ${dep.name} is not a valid dependency`);
}
}
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<any>[]).map((dep) => {
if (options.real.includes(dep)) {
return this.get(dep);
}
if (options.mock.includes(dep)) {
return newMockRepository(dep);
}
});
}
get<T>(key: ClassConstructor<T>): T {
if (!this.repoCache[key.name]) {
const real = newRealRepository(key, this.options.database);
this.repoCache[key.name] = real;
}
return this.repoCache[key.name];
}
getMock<T, R = Mocked<T>>(key: ClassConstructor<T>): 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<Insertable<UserTable>> = {}) {
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<Insertable<Assets>> = {}) {
const asset = mediumFactory.assetInsert(dto);
const result = await this.get(AssetRepository).create(asset);
return { asset, result };
}
async newExif(dto: Insertable<Exif>) {
const result = await this.get(AssetRepository).upsertExif(dto);
return { result };
}
async newAlbum(dto: Insertable<Albums>) {
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<Insertable<AssetJobStatus>> & { assetId: string }) {
const jobStatus = mediumFactory.assetJobStatusInsert({ assetId: dto.assetId });
const result = await this.get(AssetRepository).upsertJobStatus(jobStatus);
return { jobStatus, result };
}
async newPerson(dto: Partial<Insertable<Person>> & { ownerId: string }) {
const person = mediumFactory.personInsert(dto);
const result = await this.get(PersonRepository).create(person);
return { person, result };
}
async newSession(dto: Partial<Insertable<Sessions>> & { 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({ const auth = factory.auth({
session, session,
user: { user: {
@ -105,224 +185,109 @@ export const newSyncAuthUser = () => {
auth, auth,
session, session,
user, user,
create: async (db: Kysely<DB>) => {
await new UserRepository(db).create(user);
await new SessionRepository(db).create(session);
},
};
}; };
}
}
export const newSyncTest = (options: SyncTestOptions) => { export class SyncTestContext extends MediumTestContext<SyncService> {
const { sut, mocks, repos, getRepository } = newMediumService(SyncService, { constructor(database: Kysely<DB>) {
database: options.db, super(SyncService, { database, real: [SyncRepository, SessionRepository], mock: [LoggingRepository] });
repos: { }
sync: 'real',
session: 'real',
},
});
const testSync = async (auth: AuthDto, types: SyncRequestType[]) => { async syncStream(auth: AuthDto, types: SyncRequestType[]) {
const stream = mediumFactory.syncStream(); const stream = mediumFactory.syncStream();
// Wait for 2ms to ensure all updates are available and account for setTimeout inaccuracy // Wait for 2ms to ensure all updates are available and account for setTimeout inaccuracy
await wait(2); await wait(2);
await sut.stream(auth, stream, { types }); await this.sut.stream(auth, stream, { types });
return stream.getResponse(); return stream.getResponse();
};
return {
sut,
mocks,
repos,
getRepository,
testSync,
};
};
export const newMediumService = <R extends RepositoryOptions, S extends BaseService>(
Service: ClassConstructor<S>,
options: {
database: Kysely<DB>;
repos: R;
},
): Context<R, S> => {
const repos: Partial<RepositoriesTypes> = {};
const mocks: Partial<RepositoryMocks> = {};
const loggerMock = getRepositoryMock('logger') as Mocked<LoggingRepository>;
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') { async syncAckAll(auth: AuthDto, response: Array<{ type: string; ack: string }>) {
const key = _key as keyof RepositoryMocks; const acks: Record<string, string> = {};
mocks[key] = getRepositoryMock(key) as any; for (const { type, ack } of response) {
continue; acks[type] = ack;
}
await this.sut.setAcks(auth, { acks: Object.values(acks) });
} }
} }
const makeRepository = <K extends keyof RepositoriesTypes>(key: K) => { const newRealRepository = <T>(key: ClassConstructor<T>, db: Kysely<DB>): T => {
return repos[key] || getRepository(key, options.database);
};
const deps = asDeps({ ...mocks, ...repos } as ServiceOverrides);
const sut = new Service(...deps);
return {
sut,
mocks,
repos,
getRepository: makeRepository,
} as Context<R, S>;
};
export const getRepository = <K extends keyof RepositoriesTypes>(key: K, db: Kysely<DB>) => {
switch (key) { switch (key) {
case 'access': { case AccessRepository:
return new AccessRepository(db); 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': { case ConfigRepository:
return new ActivityRepository(db); case CryptoRepository: {
return new key();
} }
case 'album': { case DatabaseRepository: {
return new AlbumRepository(db); return new key(db, LoggingRepository.create(), new ConfigRepository());
} }
case 'albumUser': { case EmailRepository: {
return new AlbumUserRepository(db); return new key(LoggingRepository.create());
} }
case 'asset': { case LoggingRepository as unknown as ClassConstructor<LoggingRepository>: {
return new AssetRepository(db); return new key() as unknown as T;
}
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);
} }
default: { default: {
throw new Error(`Invalid repository key: ${key}`); throw new Error(`Unable to create repository instance for key: ${key?.name || key}`);
} }
} }
}; };
const getRepositoryMock = <K extends keyof RepositoryMocks>(key: K) => { const newMockRepository = <T>(key: ClassConstructor<T>) => {
switch (key) { switch (key) {
case 'activity': { case ActivityRepository:
return automock(ActivityRepository) as Mocked<RepositoryInterface<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': { case DatabaseRepository: {
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': {
return automock(DatabaseRepository, { return automock(DatabaseRepository, {
args: [ args: [undefined, { setContext: () => {} }, { getEnv: () => ({ database: { vectorExtension: '' } }) }],
undefined,
{
setContext: () => {},
},
{ getEnv: () => ({ database: { vectorExtension: '' } }) },
],
}); });
} }
case 'email': { case EmailRepository: {
return automock(EmailRepository, { return automock(EmailRepository, { args: [{ setContext: () => {} }] });
args: [
{
setContext: () => {},
},
],
});
} }
case 'job': { case JobRepository: {
return automock(JobRepository, { return automock(JobRepository, {
args: [ args: [
undefined, undefined,
@ -335,106 +300,21 @@ const getRepositoryMock = <K extends keyof RepositoryMocks>(key: K) => {
}); });
} }
case 'logger': { case LoggingRepository as unknown as ClassConstructor<T>: {
const configMock = { getEnv: () => ({ noColor: false }) }; const configMock = { getEnv: () => ({ noColor: false }) };
return automock(LoggingRepository, { args: [undefined, configMock], strict: false }); return automock(LoggingRepository, { args: [undefined, configMock], strict: false });
} }
case 'memory': { case StorageRepository: {
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': {
return automock(StorageRepository, { args: [{ setContext: () => {} }] }); 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: { default: {
throw new Error(`Invalid repository key: ${key}`); 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<Insertable<Assets>> = {}) => { const assetInsert = (asset: Partial<Insertable<Assets>> = {}) => {
const id = asset.id || newUuid(); const id = asset.id || newUuid();
const now = newDate(); const now = newDate();
@ -544,6 +424,8 @@ const personInsert = (person: Partial<Insertable<Person>> & { ownerId: string })
}; };
}; };
const sha256 = (value: string) => createHash('sha256').update(value).digest('base64');
const sessionInsert = ({ id = newUuid(), userId, ...session }: Partial<Insertable<Sessions>> & { userId: string }) => { const sessionInsert = ({ id = newUuid(), userId, ...session }: Partial<Insertable<Sessions>> & { userId: string }) => {
const defaults: Insertable<Sessions> = { const defaults: Insertable<Sessions> = {
id, id,

View File

@ -1,44 +1,33 @@
import { Kysely } from 'kysely'; import { Kysely } from 'kysely';
import { DB } from 'src/db'; import { DB } from 'src/db';
import { AssetRepository } from 'src/repositories/asset.repository'; 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 { 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 { factory } from 'test/small.factory';
import { getKyselyDB } from 'test/utils'; import { getKyselyDB } from 'test/utils';
describe(AssetService.name, () => {
let defaultDatabase: Kysely<DB>; let defaultDatabase: Kysely<DB>;
let assetRepo: AssetRepository;
let userRepo: UserRepository;
const createSut = (db?: Kysely<DB>) => { const setup = (db?: Kysely<DB>) => {
return newMediumService(AssetService, { return newMediumService(AssetService, {
database: db || defaultDatabase, database: db || defaultDatabase,
repos: { real: [AssetRepository],
asset: 'real', mock: [LoggingRepository],
},
}); });
}; };
beforeAll(async () => { beforeAll(async () => {
defaultDatabase = await getKyselyDB(); defaultDatabase = await getKyselyDB();
assetRepo = new AssetRepository(defaultDatabase);
userRepo = new UserRepository(defaultDatabase);
}); });
describe(AssetService.name, () => {
describe('getStatistics', () => { describe('getStatistics', () => {
it('should return stats as numbers, not strings', async () => { it('should return stats as numbers, not strings', async () => {
const { sut } = createSut(); const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const user = mediumFactory.userInsert(); const { asset } = await ctx.newAsset({ ownerId: user.id });
const asset = mediumFactory.assetInsert({ ownerId: user.id }); await ctx.newExif({ assetId: asset.id, fileSizeInByte: 12_345 });
await userRepo.create(user);
await assetRepo.create(asset);
await assetRepo.upsertExif({ assetId: asset.id, fileSizeInByte: 12_345 });
const auth = factory.auth({ user: { id: user.id } }); const auth = factory.auth({ user: { id: user.id } });
await expect(sut.getStatistics(auth, {})).resolves.toEqual({ images: 1, total: 1, videos: 0 }); await expect(sut.getStatistics(auth, {})).resolves.toEqual({ images: 1, total: 1, videos: 0 });
}); });

View File

@ -1,74 +1,63 @@
import { Kysely } from 'kysely'; import { LoggingRepository } from 'src/repositories/logging.repository';
import { DB } from 'src/db';
import { AssetRepository } from 'src/repositories/asset.repository';
import { PartnerRepository } from 'src/repositories/partner.repository'; import { PartnerRepository } from 'src/repositories/partner.repository';
import { UserRepository } from 'src/repositories/user.repository'; import { UserRepository } from 'src/repositories/user.repository';
import { partners_delete_audit } from 'src/schema/functions'; 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'; import { getKyselyDB } from 'test/utils';
describe('audit', () => { describe('audit', () => {
let defaultDatabase: Kysely<DB>; let ctx: MediumTestContext;
let assetRepo: AssetRepository;
let userRepo: UserRepository;
let partnerRepo: PartnerRepository;
beforeAll(async () => { beforeAll(async () => {
defaultDatabase = await getKyselyDB(); ctx = new MediumTestContext(BaseService, {
database: await getKyselyDB(),
assetRepo = new AssetRepository(defaultDatabase); real: [],
userRepo = new UserRepository(defaultDatabase); mock: [LoggingRepository],
partnerRepo = new PartnerRepository(defaultDatabase); });
}); });
describe(partners_delete_audit.name, () => { describe(partners_delete_audit.name, () => {
it('should not cascade user deletes to partners_audit', async () => { it('should not cascade user deletes to partners_audit', async () => {
const user1 = mediumFactory.userInsert(); const partnerRepo = ctx.get(PartnerRepository);
const user2 = mediumFactory.userInsert(); const userRepo = ctx.get(UserRepository);
const { user: user1 } = await ctx.newUser();
await Promise.all([userRepo.create(user1), userRepo.create(user2)]); const { user: user2 } = await ctx.newUser();
await partnerRepo.create({ sharedById: user1.id, sharedWithId: user2.id }); await partnerRepo.create({ sharedById: user1.id, sharedWithId: user2.id });
await userRepo.delete(user1, true); await userRepo.delete(user1, true);
await expect( 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); ).resolves.toHaveLength(0);
}); });
}); });
describe('assets_audit', () => { describe('assets_audit', () => {
it('should not cascade user deletes to assets_audit', async () => { it('should not cascade user deletes to assets_audit', async () => {
const user = mediumFactory.userInsert(); const userRepo = ctx.get(UserRepository);
const asset = mediumFactory.assetInsert({ ownerId: user.id }); const { user } = await ctx.newUser();
const { asset } = await ctx.newAsset({ ownerId: user.id });
await userRepo.create(user);
await assetRepo.create(asset);
await userRepo.delete(user, true); await userRepo.delete(user, true);
await expect( 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); ).resolves.toHaveLength(0);
}); });
}); });
describe('exif', () => { describe('exif', () => {
it('should automatically set updatedAt and updateId when the row is updated', async () => { it('should automatically set updatedAt and updateId when the row is updated', async () => {
const user = mediumFactory.userInsert(); const { user } = await ctx.newUser();
const asset = mediumFactory.assetInsert({ ownerId: user.id }); const { asset } = await ctx.newAsset({ ownerId: user.id });
await ctx.newExif({ assetId: asset.id, make: 'Canon' });
await userRepo.create(user); const before = await ctx.database
await assetRepo.create(asset);
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
const before = await defaultDatabase
.selectFrom('exif') .selectFrom('exif')
.select(['updatedAt', 'updateId']) .select(['updatedAt', 'updateId'])
.where('assetId', '=', asset.id) .where('assetId', '=', asset.id)
.executeTakeFirstOrThrow(); .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') .selectFrom('exif')
.select(['updatedAt', 'updateId']) .select(['updatedAt', 'updateId'])
.where('assetId', '=', asset.id) .where('assetId', '=', asset.id)

View File

@ -2,71 +2,66 @@ import { Kysely } from 'kysely';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { DB } from 'src/db'; import { DB } from 'src/db';
import { AssetFileType } from 'src/enum'; 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 { UserRepository } from 'src/repositories/user.repository';
import { MemoryService } from 'src/services/memory.service'; 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'; import { getKyselyDB } from 'test/utils';
describe(MemoryService.name, () => {
let defaultDatabase: Kysely<DB>; let defaultDatabase: Kysely<DB>;
const createSut = (db?: Kysely<DB>) => { const setup = (db?: Kysely<DB>) => {
return newMediumService(MemoryService, { return newMediumService(MemoryService, {
database: db || defaultDatabase, database: db || defaultDatabase,
repos: { real: [
asset: 'real', AssetRepository,
database: 'real', DatabaseRepository,
memory: 'real', MemoryRepository,
user: 'real', UserRepository,
systemMetadata: 'real', SystemMetadataRepository,
partner: 'real', UserRepository,
}, PartnerRepository,
],
mock: [LoggingRepository],
}); });
}; };
describe(MemoryService.name, () => {
beforeEach(async () => { beforeEach(async () => {
defaultDatabase = await getKyselyDB(); defaultDatabase = await getKyselyDB();
const userRepo = new UserRepository(defaultDatabase);
const admin = mediumFactory.userInsert({ isAdmin: true });
await userRepo.create(admin);
}); });
describe('onMemoryCreate', () => { describe('onMemoryCreate', () => {
it('should work on an empty database', async () => { it('should work on an empty database', async () => {
const { sut } = createSut(); const { sut } = setup();
await expect(sut.onMemoriesCreate()).resolves.not.toThrow(); await expect(sut.onMemoriesCreate()).resolves.not.toThrow();
}); });
it('should create a memory from an asset', async () => { 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<true>; const now = DateTime.fromObject({ year: 2025, month: 2, day: 25 }, { zone: 'utc' }) as DateTime<true>;
const user = mediumFactory.userInsert(); const { user } = await ctx.newUser();
const asset = mediumFactory.assetInsert({ const { asset } = await ctx.newAsset({ ownerId: user.id, localDateTime: now.minus({ years: 1 }).toISO() });
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);
await Promise.all([ await Promise.all([
assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }), ctx.newExif({ assetId: asset.id, make: 'Canon' }),
ctx.newJobStatus({ assetId: asset.id }),
assetRepo.upsertFiles([ assetRepo.upsertFiles([
{ assetId: asset.id, type: AssetFileType.PREVIEW, path: '/path/to/preview.jpg' }, { assetId: asset.id, type: AssetFileType.PREVIEW, path: '/path/to/preview.jpg' },
{ assetId: asset.id, type: AssetFileType.THUMBNAIL, path: '/path/to/thumbnail.jpg' }, { assetId: asset.id, type: AssetFileType.THUMBNAIL, path: '/path/to/thumbnail.jpg' },
]), ]),
assetRepo.upsertJobStatus(jobStatus),
]); ]);
vi.setSystemTime(now.toJSDate()); vi.setSystemTime(now.toJSDate());
await sut.onMemoriesCreate(); 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.length).toBe(1);
expect(memories[0]).toEqual( expect(memories[0]).toEqual(
expect.objectContaining({ expect.objectContaining({
@ -88,16 +83,11 @@ describe(MemoryService.name, () => {
}); });
it('should not generate a memory twice for the same day', async () => { 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<true>; const now = DateTime.fromObject({ year: 2025, month: 2, day: 20 }, { zone: 'utc' }) as DateTime<true>;
const { user } = await ctx.newUser();
const assetRepo = getRepository('asset');
const memoryRepo = getRepository('memory');
const user = mediumFactory.userInsert();
await repos.user.create(user);
for (const dto of [ for (const dto of [
{ {
ownerId: user.id, ownerId: user.id,
@ -112,11 +102,10 @@ describe(MemoryService.name, () => {
localDateTime: now.minus({ year: 1 }).plus({ days: 5 }).toISO(), localDateTime: now.minus({ year: 1 }).plus({ days: 5 }).toISO(),
}, },
]) { ]) {
const asset = mediumFactory.assetInsert(dto); const { asset } = await ctx.newAsset(dto);
await assetRepo.create(asset);
await Promise.all([ await Promise.all([
assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' }), ctx.newExif({ assetId: asset.id, make: 'Canon' }),
assetRepo.upsertJobStatus(mediumFactory.assetJobStatusInsert({ assetId: asset.id })), ctx.newJobStatus({ assetId: asset.id }),
assetRepo.upsertFiles([ assetRepo.upsertFiles([
{ assetId: asset.id, type: AssetFileType.PREVIEW, path: '/path/to/preview.jpg' }, { assetId: asset.id, type: AssetFileType.PREVIEW, path: '/path/to/preview.jpg' },
{ assetId: asset.id, type: AssetFileType.THUMBNAIL, path: '/path/to/thumbnail.jpg' }, { assetId: asset.id, type: AssetFileType.THUMBNAIL, path: '/path/to/thumbnail.jpg' },
@ -125,13 +114,13 @@ describe(MemoryService.name, () => {
} }
vi.setSystemTime(now.toJSDate()); vi.setSystemTime(now.toJSDate());
await sut.onMemoriesCreate(); await sut.onMemoriesCreate();
const memories = await memoryRepo.search(user.id, {}); const memories = await memoryRepo.search(user.id, {});
expect(memories.length).toBe(1); expect(memories.length).toBe(1);
await sut.onMemoriesCreate(); await sut.onMemoriesCreate();
const memoriesAfter = await memoryRepo.search(user.id, {}); const memoriesAfter = await memoryRepo.search(user.id, {});
expect(memoriesAfter.length).toBe(1); expect(memoriesAfter.length).toBe(1);
}); });
@ -139,7 +128,7 @@ describe(MemoryService.name, () => {
describe('onMemoriesCleanup', () => { describe('onMemoriesCleanup', () => {
it('should run without error', async () => { it('should run without error', async () => {
const { sut } = createSut(); const { sut } = setup();
await expect(sut.onMemoriesCleanup()).resolves.not.toThrow(); await expect(sut.onMemoriesCleanup()).resolves.not.toThrow();
}); });
}); });

View File

@ -1,90 +1,80 @@
import { Kysely } from 'kysely'; import { Kysely } from 'kysely';
import { DB } from 'src/db'; 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 { 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 { factory } from 'test/small.factory';
import { getKyselyDB } from 'test/utils'; import { getKyselyDB } from 'test/utils';
describe.concurrent(PersonService.name, () => {
let defaultDatabase: Kysely<DB>; let defaultDatabase: Kysely<DB>;
const createSut = (db?: Kysely<DB>) => { const setup = (db?: Kysely<DB>) => {
return newMediumService(PersonService, { return newMediumService(PersonService, {
database: db || defaultDatabase, database: db || defaultDatabase,
repos: { real: [AccessRepository, DatabaseRepository, PersonRepository],
access: 'real', mock: [LoggingRepository, StorageRepository],
database: 'real',
person: 'real',
storage: 'mock',
},
}); });
}; };
beforeEach(async () => { beforeAll(async () => {
defaultDatabase = await getKyselyDB(); defaultDatabase = await getKyselyDB();
}); });
describe(PersonService.name, () => {
describe('delete', () => { describe('delete', () => {
it('should throw an error when there is no access', async () => { it('should throw an error when there is no access', async () => {
const { sut } = createSut(); const { sut } = setup();
const auth = factory.auth(); const auth = factory.auth();
const personId = factory.uuid(); const personId = factory.uuid();
await expect(sut.delete(auth, personId)).rejects.toThrow('Not found or no person.delete access'); await expect(sut.delete(auth, personId)).rejects.toThrow('Not found or no person.delete access');
}); });
it('should delete the person', async () => { it('should delete the person', async () => {
const { sut, getRepository, mocks } = createSut(); const { sut, ctx } = setup();
const personRepo = ctx.get(PersonRepository);
const user = mediumFactory.userInsert(); const storageMock = ctx.getMock(StorageRepository);
const { user } = await ctx.newUser();
const { person } = await ctx.newPerson({ ownerId: user.id });
const auth = factory.auth({ user }); const auth = factory.auth({ user });
const person = mediumFactory.personInsert({ ownerId: auth.user.id }); storageMock.unlink.mockResolvedValue();
mocks.storage.unlink.mockResolvedValue();
const userRepo = getRepository('user');
await userRepo.create(user);
const personRepo = getRepository('person');
await personRepo.create(person);
await expect(personRepo.getById(person.id)).resolves.toEqual(expect.objectContaining({ id: person.id })); await expect(personRepo.getById(person.id)).resolves.toEqual(expect.objectContaining({ id: person.id }));
await expect(sut.delete(auth, person.id)).resolves.toBeUndefined(); await expect(sut.delete(auth, person.id)).resolves.toBeUndefined();
await expect(personRepo.getById(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', () => { describe('deleteAll', () => {
it('should throw an error when there is no access', async () => { it('should throw an error when there is no access', async () => {
const { sut } = createSut(); const { sut } = setup();
const auth = factory.auth(); const auth = factory.auth();
const personId = factory.uuid(); const personId = factory.uuid();
await expect(sut.deleteAll(auth, { ids: [personId] })).rejects.toThrow('Not found or no person.delete access'); await expect(sut.deleteAll(auth, { ids: [personId] })).rejects.toThrow('Not found or no person.delete access');
}); });
it('should delete the person', async () => { it('should delete the person', async () => {
const { sut, getRepository, mocks } = createSut(); const { sut, ctx } = setup();
const storageMock = ctx.getMock(StorageRepository);
const user = mediumFactory.userInsert(); 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 auth = factory.auth({ user });
const person1 = mediumFactory.personInsert({ ownerId: auth.user.id }); storageMock.unlink.mockResolvedValue();
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);
await expect(sut.deleteAll(auth, { ids: [person1.id, person2.id] })).resolves.toBeUndefined(); await expect(sut.deleteAll(auth, { ids: [person1.id, person2.id] })).resolves.toBeUndefined();
await expect(personRepo.getById(person1.id)).resolves.toBeUndefined(); await expect(personRepo.getById(person1.id)).resolves.toBeUndefined();
await expect(personRepo.getById(person2.id)).resolves.toBeUndefined(); await expect(personRepo.getById(person2.id)).resolves.toBeUndefined();
expect(mocks.storage.unlink).toHaveBeenCalledTimes(2); expect(storageMock.unlink).toHaveBeenCalledTimes(2);
expect(mocks.storage.unlink).toHaveBeenCalledWith(person1.thumbnailPath); expect(storageMock.unlink).toHaveBeenCalledWith(person1.thumbnailPath);
expect(mocks.storage.unlink).toHaveBeenCalledWith(person2.thumbnailPath); expect(storageMock.unlink).toHaveBeenCalledWith(person2.thumbnailPath);
}); });
}); });
}); });

View File

@ -2,72 +2,65 @@ import { Kysely } from 'kysely';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { DB } from 'src/db'; import { DB } from 'src/db';
import { ImmichEnvironment, JobName, JobStatus } from 'src/enum'; 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 { UserService } from 'src/services/user.service';
import { mediumFactory, newMediumService } from 'test/medium.factory'; import { mediumFactory, newMediumService } from 'test/medium.factory';
import { factory } from 'test/small.factory'; import { factory } from 'test/small.factory';
import { getKyselyDB } from 'test/utils'; import { getKyselyDB } from 'test/utils';
describe(UserService.name, () => {
let defaultDatabase: Kysely<DB>; let defaultDatabase: Kysely<DB>;
const createSut = (db?: Kysely<DB>) => { const setup = (db?: Kysely<DB>) => {
process.env.IMMICH_ENV = ImmichEnvironment.TESTING; process.env.IMMICH_ENV = ImmichEnvironment.TESTING;
return newMediumService(UserService, { return newMediumService(UserService, {
database: db || defaultDatabase, database: db || defaultDatabase,
repos: { real: [CryptoRepository, ConfigRepository, SystemMetadataRepository, UserRepository],
user: 'real', mock: [LoggingRepository, JobRepository],
crypto: 'real',
config: 'real',
job: 'mock',
systemMetadata: 'real',
},
}); });
}; };
beforeAll(async () => { beforeAll(async () => {
defaultDatabase = await getKyselyDB(); defaultDatabase = await getKyselyDB();
const { repos } = createSut(); const { ctx } = setup();
await repos.user.create({ isAdmin: true, email: 'admin@immich.cloud' }); await ctx.newUser({ isAdmin: true, email: 'admin@immich.cloud' });
}); });
describe(UserService.name, () => {
describe('create', () => { describe('create', () => {
it('should create a user', async () => { it('should create a user', async () => {
const { sut } = createSut(); const { sut } = setup();
const user = mediumFactory.userInsert(); const user = mediumFactory.userInsert();
await expect(sut.createUser({ name: user.name, email: user.email })).resolves.toEqual( await expect(sut.createUser({ name: user.name, email: user.email })).resolves.toEqual(
expect.objectContaining({ name: user.name, email: user.email }), expect.objectContaining({ name: user.name, email: user.email }),
); );
}); });
it('should reject user with duplicate email', async () => { it('should reject user with duplicate email', async () => {
const { sut } = createSut(); const { sut } = setup();
const user = mediumFactory.userInsert(); const user = mediumFactory.userInsert();
await expect(sut.createUser({ email: user.email })).resolves.toMatchObject({ email: user.email }); await expect(sut.createUser({ email: user.email })).resolves.toMatchObject({ email: user.email });
await expect(sut.createUser({ email: user.email })).rejects.toThrow('User exists'); await expect(sut.createUser({ email: user.email })).rejects.toThrow('User exists');
}); });
it('should not return password', async () => { it('should not return password', async () => {
const { sut } = createSut(); const { sut } = setup();
const dto = mediumFactory.userInsert({ password: 'password' }); const dto = mediumFactory.userInsert({ password: 'password' });
const user = await sut.createUser({ email: dto.email, password: 'password' }); const user = await sut.createUser({ email: dto.email, password: 'password' });
expect((user as any).password).toBeUndefined(); expect((user as any).password).toBeUndefined();
}); });
}); });
describe('search', () => { describe('search', () => {
it('should get users', async () => { it('should get users', async () => {
const { sut, repos } = createSut(); const { sut, ctx } = setup();
const user1 = mediumFactory.userInsert(); const { user: user1 } = await ctx.newUser();
const user2 = mediumFactory.userInsert(); const { user: user2 } = await ctx.newUser();
await Promise.all([repos.user.create(user1), repos.user.create(user2)]);
const auth = factory.auth({ user: user1 }); const auth = factory.auth({ user: user1 });
await expect(sut.search(auth)).resolves.toEqual( await expect(sut.search(auth)).resolves.toEqual(
@ -81,10 +74,8 @@ describe(UserService.name, () => {
describe('get', () => { describe('get', () => {
it('should get a user', async () => { it('should get a user', async () => {
const { sut, repos } = createSut(); const { sut, ctx } = setup();
const user = mediumFactory.userInsert(); const { user } = await ctx.newUser();
await repos.user.create(user);
await expect(sut.get(user.id)).resolves.toEqual( await expect(sut.get(user.id)).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
@ -96,11 +87,8 @@ describe(UserService.name, () => {
}); });
it('should not return password', async () => { it('should not return password', async () => {
const { sut, repos } = createSut(); const { sut, ctx } = setup();
const user = mediumFactory.userInsert(); const { user } = await ctx.newUser();
await repos.user.create(user);
const result = await sut.get(user.id); const result = await sut.get(user.id);
expect((result as any).password).toBeUndefined(); expect((result as any).password).toBeUndefined();
@ -109,10 +97,9 @@ describe(UserService.name, () => {
describe('updateMe', () => { describe('updateMe', () => {
it('should update a user', async () => { it('should update a user', async () => {
const { sut, repos: repositories } = createSut(); const { sut, ctx } = setup();
const { user, result: before } = await ctx.newUser();
const before = await repositories.user.create(mediumFactory.userInsert()); const auth = factory.auth({ user: { id: user.id } });
const auth = factory.auth({ user: { id: before.id } });
const after = await sut.updateMe(auth, { name: `${before.name} Updated` }); const after = await sut.updateMe(auth, { name: `${before.name} Updated` });
expect(before.updatedAt).toBeDefined(); expect(before.updatedAt).toBeDefined();
@ -128,17 +115,13 @@ describe(UserService.name, () => {
activationKey: activationKey:
'KuX8KsktrBSiXpQMAH0zLgA5SpijXVr_PDkzLdWUlAogCTMBZ0I3KCHXK0eE9EEd7harxup8_EHMeqAWeHo5VQzol6LGECpFv585U9asXD4Zc-UXt3mhJr2uhazqipBIBwJA2YhmUCDy8hiyiGsukDQNu9Rg9C77UeoKuZBWVjWUBWG0mc1iRqfvF0faVM20w53czAzlhaMxzVGc3Oimbd7xi_CAMSujF_2y8QpA3X2fOVkQkzdcH9lV0COejl7IyH27zQQ9HrlrXv3Lai5Hw67kNkaSjmunVBxC5PS0TpKoc9SfBJMaAGWnaDbjhjYUrm-8nIDQnoeEAidDXVAdPw', 'KuX8KsktrBSiXpQMAH0zLgA5SpijXVr_PDkzLdWUlAogCTMBZ0I3KCHXK0eE9EEd7harxup8_EHMeqAWeHo5VQzol6LGECpFv585U9asXD4Zc-UXt3mhJr2uhazqipBIBwJA2YhmUCDy8hiyiGsukDQNu9Rg9C77UeoKuZBWVjWUBWG0mc1iRqfvF0faVM20w53czAzlhaMxzVGc3Oimbd7xi_CAMSujF_2y8QpA3X2fOVkQkzdcH9lV0COejl7IyH27zQQ9HrlrXv3Lai5Hw67kNkaSjmunVBxC5PS0TpKoc9SfBJMaAGWnaDbjhjYUrm-8nIDQnoeEAidDXVAdPw',
}; };
const { sut, repos } = createSut(); const { sut, ctx } = setup();
const user = mediumFactory.userInsert(); const { user } = await ctx.newUser();
await repos.user.create(user);
const auth = factory.auth({ user: { id: user.id } }); const auth = factory.auth({ user: { id: user.id } });
await expect(sut.getLicense(auth)).rejects.toThrowError(); await expect(sut.getLicense(auth)).rejects.toThrowError();
const after = await sut.setLicense(auth, license); const after = await sut.setLicense(auth, license);
expect(after.licenseKey).toEqual(license.licenseKey); expect(after.licenseKey).toEqual(license.licenseKey);
expect(after.activationKey).toEqual(license.activationKey); expect(after.activationKey).toEqual(license.activationKey);
const getResponse = await sut.getLicense(auth); const getResponse = await sut.getLicense(auth);
expect(getResponse).toEqual(after); expect(getResponse).toEqual(after);
}); });
@ -146,7 +129,7 @@ describe(UserService.name, () => {
describe.sequential('handleUserDeleteCheck', () => { describe.sequential('handleUserDeleteCheck', () => {
beforeEach(async () => { 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 // 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 }); const config = await sut.getConfig({ withCache: false });
config.user.deleteDelay = 7; config.user.deleteDelay = 7;
@ -154,52 +137,43 @@ describe(UserService.name, () => {
}); });
it('should work when there are no deleted users', async () => { it('should work when there are no deleted users', async () => {
const { sut, mocks } = createSut(); const { sut, ctx } = setup();
mocks.job.queueAll.mockResolvedValue(void 0); const jobMock = ctx.getMock(JobRepository);
jobMock.queueAll.mockResolvedValue(void 0);
await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS); await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS);
expect(jobMock.queueAll).toHaveBeenCalledExactlyOnceWith([]);
expect(mocks.job.queueAll).toHaveBeenCalledExactlyOnceWith([]);
}); });
it('should work when there is a user to delete', async () => { it('should work when there is a user to delete', async () => {
const { sut, repos, mocks } = createSut(await getKyselyDB()); const { sut, ctx } = setup(await getKyselyDB());
mocks.job.queueAll.mockResolvedValue(void 0); const jobMock = ctx.getMock(JobRepository);
const user = mediumFactory.userInsert({ deletedAt: DateTime.now().minus({ days: 60 }).toJSDate() }); const { user } = await ctx.newUser({ deletedAt: DateTime.now().minus({ days: 60 }).toJSDate() });
await repos.user.create(user); jobMock.queueAll.mockResolvedValue(void 0);
await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS); await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS);
expect(jobMock.queueAll).toHaveBeenCalledExactlyOnceWith([
expect(mocks.job.queueAll).toHaveBeenCalledExactlyOnceWith([
{ name: JobName.USER_DELETION, data: { id: user.id } }, { name: JobName.USER_DELETION, data: { id: user.id } },
]); ]);
}); });
it('should skip a recently deleted user', async () => { it('should skip a recently deleted user', async () => {
const { sut, repos, mocks } = createSut(await getKyselyDB()); const { sut, ctx } = setup(await getKyselyDB());
mocks.job.queueAll.mockResolvedValue(void 0); const jobMock = ctx.getMock(JobRepository);
const user = mediumFactory.userInsert({ deletedAt: DateTime.now().minus({ days: 5 }).toJSDate() }); await ctx.newUser({ deletedAt: DateTime.now().minus({ days: 5 }).toJSDate() });
await repos.user.create(user); jobMock.queueAll.mockResolvedValue(void 0);
await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS); await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS);
expect(jobMock.queueAll).toHaveBeenCalledExactlyOnceWith([]);
expect(mocks.job.queueAll).toHaveBeenCalledExactlyOnceWith([]);
}); });
it('should respect a custom user delete delay', async () => { it('should respect a custom user delete delay', async () => {
const { sut, repos, mocks } = createSut(await getKyselyDB()); const { sut, ctx } = setup(await getKyselyDB());
mocks.job.queueAll.mockResolvedValue(void 0); const jobMock = ctx.getMock(JobRepository);
const user = mediumFactory.userInsert({ deletedAt: DateTime.now().minus({ days: 25 }).toJSDate() }); await ctx.newUser({ deletedAt: DateTime.now().minus({ days: 25 }).toJSDate() });
await repos.user.create(user); jobMock.queueAll.mockResolvedValue(void 0);
const config = await sut.getConfig({ withCache: false }); const config = await sut.getConfig({ withCache: false });
config.user.deleteDelay = 30; config.user.deleteDelay = 30;
await sut.updateConfig(config); await sut.updateConfig(config);
await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS); await expect(sut.handleUserDeleteCheck()).resolves.toEqual(JobStatus.SUCCESS);
expect(jobMock.queueAll).toHaveBeenCalledExactlyOnceWith([]);
expect(mocks.job.queueAll).toHaveBeenCalledExactlyOnceWith([]);
}); });
}); });
}); });

View File

@ -2,21 +2,21 @@ import { Kysely } from 'kysely';
import { serverVersion } from 'src/constants'; import { serverVersion } from 'src/constants';
import { DB } from 'src/db'; import { DB } from 'src/db';
import { JobName } from 'src/enum'; 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 { VersionService } from 'src/services/version.service';
import { newMediumService } from 'test/medium.factory'; import { newMediumService } from 'test/medium.factory';
import { getKyselyDB } from 'test/utils'; import { getKyselyDB } from 'test/utils';
describe(VersionService.name, () => {
let defaultDatabase: Kysely<DB>; let defaultDatabase: Kysely<DB>;
const setup = (db?: Kysely<DB>) => { const setup = (db?: Kysely<DB>) => {
return newMediumService(VersionService, { return newMediumService(VersionService, {
database: db || defaultDatabase, database: db || defaultDatabase,
repos: { real: [DatabaseRepository, VersionHistoryRepository],
job: 'mock', mock: [LoggingRepository, JobRepository],
database: 'real',
versionHistory: 'real',
},
}); });
}; };
@ -24,16 +24,18 @@ describe(VersionService.name, () => {
defaultDatabase = await getKyselyDB(); defaultDatabase = await getKyselyDB();
}); });
describe(VersionService.name, () => {
describe('onBootstrap', () => { describe('onBootstrap', () => {
it('record the current version on startup', async () => { 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); expect(itemsBefore).toHaveLength(0);
await sut.onBootstrap(); await sut.onBootstrap();
const itemsAfter = await repos.versionHistory.getAll(); const itemsAfter = await versionHistoryRepo.getAll();
expect(itemsAfter).toHaveLength(1); expect(itemsAfter).toHaveLength(1);
expect(itemsAfter[0]).toEqual({ expect(itemsAfter[0]).toEqual({
createdAt: expect.any(Date), createdAt: expect.any(Date),
@ -43,22 +45,26 @@ describe(VersionService.name, () => {
}); });
it('should queue memory creation when upgrading from 1.128.0', async () => { it('should queue memory creation when upgrading from 1.128.0', async () => {
const { sut, repos, mocks } = setup(); const { sut, ctx } = setup();
mocks.job.queue.mockResolvedValue(void 0); 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(); 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 () => { 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(); await sut.onBootstrap();
expect(mocks.job.queue).not.toHaveBeenCalled(); expect(jobMock.queue).not.toHaveBeenCalled();
}); });
}); });
}); });

View File

@ -1,44 +1,35 @@
import { Kysely } from 'kysely'; import { Kysely } from 'kysely';
import { DB } from 'src/db'; import { DB } from 'src/db';
import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum'; 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 { factory } from 'test/small.factory';
import { getKyselyDB, wait } from 'test/utils'; import { getKyselyDB, wait } from 'test/utils';
let defaultDatabase: Kysely<DB>; let defaultDatabase: Kysely<DB>;
const setup = async (db?: Kysely<DB>) => { const setup = async (db?: Kysely<DB>) => {
const database = db || defaultDatabase; const ctx = new SyncTestContext(db || defaultDatabase);
const result = newSyncTest({ db: database }); const { auth, user, session } = await ctx.newSyncAuthUser();
const { auth, create } = newSyncAuthUser(); return { auth, user, session, ctx };
await create(database);
return { ...result, auth };
}; };
beforeAll(async () => { beforeAll(async () => {
defaultDatabase = await getKyselyDB(); defaultDatabase = await getKyselyDB();
}); });
describe.concurrent(SyncRequestType.AlbumAssetExifsV1, () => { describe(SyncRequestType.AlbumAssetExifsV1, () => {
it('should detect and sync the first album asset exif', async () => { 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 response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]);
const user2 = mediumFactory.userInsert(); expect(response).toHaveLength(1);
await userRepo.create(user2); expect(response).toEqual([
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), ack: expect.any(String),
data: { data: {
@ -70,89 +61,58 @@ describe.concurrent(SyncRequestType.AlbumAssetExifsV1, () => {
}, },
type: SyncEntityType.AlbumAssetExifV1, type: SyncEntityType.AlbumAssetExifV1,
}, },
]), ]);
);
const acks = [initialSyncResponse[0].ack]; await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks }); await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([]);
const ackSyncResponse = await testSync(auth, [SyncRequestType.AlbumAssetExifsV1]);
expect(ackSyncResponse).toEqual([]);
}); });
it('should sync album asset exif for own user', async () => { 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'); await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
const assetRepo = getRepository('asset'); await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toHaveLength(1);
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);
}); });
it('should not sync album asset exif for unrelated user', async () => { it('should not sync album asset exif for unrelated user', async () => {
const { auth, getRepository, testSync } = await setup(); const { auth, ctx } = await setup();
const { user: user2 } = await ctx.newUser();
const userRepo = getRepository('user'); const { user: user3 } = await ctx.newUser();
const user2 = mediumFactory.userInsert(); const { asset } = await ctx.newAsset({ ownerId: user3.id });
const user3 = mediumFactory.userInsert(); await ctx.newExif({ assetId: asset.id, make: 'Canon' });
await userRepo.create(user2); const { album } = await ctx.newAlbum({ ownerId: user2.id });
await userRepo.create(user3); await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
await ctx.newAlbumUser({ albumId: album.id, userId: user3.id, role: AlbumUserRole.EDITOR });
const albumRepo = getRepository('album'); const { session } = await ctx.newSession({ userId: user3.id });
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 authUser3 = factory.auth({ session, user: user3 }); const authUser3 = factory.auth({ session, user: user3 });
await expect(testSync(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1); await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0); 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 () => { it('should backfill album assets exif when a user shares an album with you', async () => {
const { auth, sut, testSync, getRepository } = await setup(); const { auth, ctx } = await setup();
const { user: user2 } = await ctx.newUser();
const userRepo = getRepository('user'); const { asset: asset1Owner } = await ctx.newAsset({ ownerId: auth.user.id });
const user2 = mediumFactory.userInsert(); await ctx.newExif({ assetId: asset1Owner.id, make: 'asset1Owner' });
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' });
await wait(2); await wait(2);
await assetRepo.create(asset1User2); const { asset: asset1User2 } = await ctx.newAsset({ ownerId: user2.id });
await assetRepo.upsertExif({ assetId: asset1User2.id, make: 'asset1User2' }); await ctx.newExif({ assetId: asset1User2.id, make: 'asset1User2' });
await wait(2); await wait(2);
await assetRepo.create(asset2User2); const { asset: asset2User2 } = await ctx.newAsset({ ownerId: user2.id });
await assetRepo.upsertExif({ assetId: asset2User2.id, make: 'asset2User2' }); await ctx.newExif({ assetId: asset2User2.id, make: 'asset2User2' });
await wait(2); await wait(2);
await assetRepo.create(asset3User2); const { asset: asset3User2 } = await ctx.newAsset({ ownerId: user2.id });
await assetRepo.upsertExif({ assetId: asset3User2.id, make: 'asset3User2' }); 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 response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]);
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]);
expect(response).toHaveLength(1); expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
@ -165,20 +125,20 @@ describe.concurrent(SyncRequestType.AlbumAssetExifsV1, () => {
]); ]);
// ack initial album asset exif sync // ack initial album asset exif sync
const acks = response.map(({ ack }) => ack); await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks });
// create a second album with // create a second album
const album2 = mediumFactory.albumInsert({ ownerId: user2.id }); const { album: album2 } = await ctx.newAlbum({ ownerId: user2.id });
await albumRepo.create( await Promise.all(
album2, [asset1User2.id, asset2User2.id, asset3User2.id, asset1Owner.id].map((assetId) =>
[asset1User2.id, asset2User2.id, asset3User2.id, asset1Owner.id], ctx.newAlbumAsset({ albumId: album2.id, assetId }),
[{ userId: auth.user.id, role: AlbumUserRole.EDITOR }], ),
); );
await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.EDITOR });
// should backfill the album user // should backfill the album user
const backfillResponse = await testSync(auth, [SyncRequestType.AlbumAssetExifsV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1]);
expect(backfillResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ data: expect.objectContaining({
@ -214,9 +174,7 @@ describe.concurrent(SyncRequestType.AlbumAssetExifsV1, () => {
}, },
]); ]);
await sut.setAcks(auth, { acks: [backfillResponse[3].ack, backfillResponse.at(-1).ack] }); await ctx.syncAckAll(auth, newResponse);
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toEqual([]);
const finalResponse = await testSync(auth, [SyncRequestType.AlbumAssetExifsV1]);
expect(finalResponse).toEqual([]);
}); });
}); });

View File

@ -1,40 +1,32 @@
import { Kysely } from 'kysely'; import { Kysely } from 'kysely';
import { DB } from 'src/db'; import { DB } from 'src/db';
import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum'; 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 { factory } from 'test/small.factory';
import { getKyselyDB, wait } from 'test/utils'; import { getKyselyDB, wait } from 'test/utils';
let defaultDatabase: Kysely<DB>; let defaultDatabase: Kysely<DB>;
const setup = async (db?: Kysely<DB>) => { const setup = async (db?: Kysely<DB>) => {
const database = db || defaultDatabase; const ctx = new SyncTestContext(db || defaultDatabase);
const result = newSyncTest({ db: database }); const { auth, user, session } = await ctx.newSyncAuthUser();
const { auth, create } = newSyncAuthUser(); return { auth, user, session, ctx };
await create(database);
return { ...result, auth };
}; };
beforeAll(async () => { beforeAll(async () => {
defaultDatabase = await getKyselyDB(); defaultDatabase = await getKyselyDB();
}); });
describe.concurrent(SyncRequestType.AlbumAssetsV1, () => { describe(SyncRequestType.AlbumAssetsV1, () => {
it('should detect and sync the first album asset', async () => { 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 originalFileName = 'firstAsset';
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
const date = new Date().toISOString(); const date = new Date().toISOString();
const albumRepo = getRepository('album'); const { auth, ctx } = await setup();
const assetRepo = getRepository('asset'); const { user: user2 } = await ctx.newUser();
const asset = mediumFactory.assetInsert({ const { asset } = await ctx.newAsset({
originalFileName, originalFileName,
ownerId: user2.id, ownerId: user2.id,
checksum: Buffer.from(checksum, 'base64'), checksum: Buffer.from(checksum, 'base64'),
@ -45,14 +37,13 @@ describe.concurrent(SyncRequestType.AlbumAssetsV1, () => {
deletedAt: null, deletedAt: null,
duration: '0:10:00.00000', duration: '0:10:00.00000',
}); });
await assetRepo.create(asset); const { album } = await ctx.newAlbum({ ownerId: user2.id });
await albumRepo.create({ ownerId: user2.id }, [asset.id], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]); 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]); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
expect(response).toHaveLength(1);
expect(initialSyncResponse).toHaveLength(1); expect(response).toEqual([
expect(initialSyncResponse).toEqual(
expect.arrayContaining([
{ {
ack: expect.any(String), ack: expect.any(String),
data: { data: {
@ -72,83 +63,53 @@ describe.concurrent(SyncRequestType.AlbumAssetsV1, () => {
}, },
type: SyncEntityType.AlbumAssetV1, type: SyncEntityType.AlbumAssetV1,
}, },
]), ]);
);
const acks = [initialSyncResponse[0].ack]; await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks }); await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toEqual([]);
const ackSyncResponse = await testSync(auth, [SyncRequestType.AlbumAssetsV1]);
expect(ackSyncResponse).toEqual([]);
}); });
it('should sync album asset for own user', async () => { 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'); await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
const assetRepo = getRepository('asset'); await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toHaveLength(1);
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);
}); });
it('should not sync album asset for unrelated user', async () => { it('should not sync album asset for unrelated user', async () => {
const { auth, getRepository, testSync } = await setup(); const { auth, ctx } = await setup();
const { user: user2 } = await ctx.newUser();
const userRepo = getRepository('user'); const { user: user3 } = await ctx.newUser();
const user2 = mediumFactory.userInsert(); const { asset } = await ctx.newAsset({ ownerId: user3.id });
const user3 = mediumFactory.userInsert(); const { album } = await ctx.newAlbum({ ownerId: user2.id });
await userRepo.create(user2); await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
await userRepo.create(user3); await ctx.newAlbumUser({ albumId: album.id, userId: user3.id, role: AlbumUserRole.EDITOR });
const { session } = await ctx.newSession({ userId: user3.id });
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 authUser3 = factory.auth({ session, user: user3 }); const authUser3 = factory.auth({ session, user: user3 });
await expect(testSync(authUser3, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toHaveLength(0);
}); });
it('should backfill album assets when a user shares an album with you', async () => { it('should backfill album assets when a user shares an album with you', async () => {
const { auth, sut, testSync, getRepository } = await setup(); const { auth, ctx } = await setup();
const { user: user2 } = await ctx.newUser();
const userRepo = getRepository('user'); const { asset: asset1Owner } = await ctx.newAsset({ ownerId: auth.user.id });
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 wait(2); await wait(2);
await assetRepo.create(asset1User2); const { asset: asset1User2 } = await ctx.newAsset({ ownerId: user2.id });
await wait(2); await wait(2);
await assetRepo.create(asset2User2); const { asset: asset2User2 } = await ctx.newAsset({ ownerId: user2.id });
await wait(2); 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 response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
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]);
expect(response).toHaveLength(1); expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
@ -161,20 +122,20 @@ describe.concurrent(SyncRequestType.AlbumAssetsV1, () => {
]); ]);
// ack initial album asset sync // ack initial album asset sync
const acks = response.map(({ ack }) => ack); await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks });
// create a second album with // create a second album
const album2 = mediumFactory.albumInsert({ ownerId: user2.id }); const { album: album2 } = await ctx.newAlbum({ ownerId: user2.id });
await albumRepo.create( await Promise.all(
album2, [asset1User2.id, asset2User2.id, asset3User2.id, asset1Owner.id].map((assetId) =>
[asset1User2.id, asset2User2.id, asset3User2.id, asset1Owner.id], ctx.newAlbumAsset({ albumId: album2.id, assetId }),
[{ userId: auth.user.id, role: AlbumUserRole.EDITOR }], ),
); );
await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.EDITOR });
// should backfill the album user // should backfill the album user
const backfillResponse = await testSync(auth, [SyncRequestType.AlbumAssetsV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
expect(backfillResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ data: expect.objectContaining({
@ -210,9 +171,7 @@ describe.concurrent(SyncRequestType.AlbumAssetsV1, () => {
}, },
]); ]);
await sut.setAcks(auth, { acks: [backfillResponse[3].ack, backfillResponse.at(-1).ack] }); await ctx.syncAckAll(auth, newResponse);
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toEqual([]);
const finalResponse = await testSync(auth, [SyncRequestType.AlbumAssetsV1]);
expect(finalResponse).toEqual([]);
}); });
}); });

View File

@ -1,43 +1,35 @@
import { Kysely } from 'kysely'; import { Kysely } from 'kysely';
import { DB } from 'src/db'; import { DB } from 'src/db';
import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum'; 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'; import { getKyselyDB, wait } from 'test/utils';
let defaultDatabase: Kysely<DB>; let defaultDatabase: Kysely<DB>;
const setup = async (db?: Kysely<DB>) => { const setup = async (db?: Kysely<DB>) => {
const database = db || defaultDatabase; const ctx = new SyncTestContext(db || defaultDatabase);
const result = newSyncTest({ db: database }); const { auth, user, session } = await ctx.newSyncAuthUser();
const { auth, create } = newSyncAuthUser(); return { auth, user, session, ctx };
await create(database);
return { ...result, auth };
}; };
beforeAll(async () => { beforeAll(async () => {
defaultDatabase = await getKyselyDB(); defaultDatabase = await getKyselyDB();
}); });
describe.concurrent(SyncRequestType.AlbumToAssetsV1, () => { describe(SyncRequestType.AlbumToAssetsV1, () => {
it('should detect and sync the first album to asset relation', async () => { 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 response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
const user2 = mediumFactory.userInsert(); expect(response).toHaveLength(1);
await userRepo.create(user2); expect(response).toEqual([
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), ack: expect.any(String),
data: { data: {
@ -46,32 +38,21 @@ describe.concurrent(SyncRequestType.AlbumToAssetsV1, () => {
}, },
type: SyncEntityType.AlbumToAssetV1, type: SyncEntityType.AlbumToAssetV1,
}, },
]), ]);
);
const acks = [initialSyncResponse[0].ack]; await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks }); await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]);
const ackSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
expect(ackSyncResponse).toEqual([]);
}); });
it('should sync album to asset for owned albums', async () => { 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 response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
const assetRepo = getRepository('asset'); expect(response).toHaveLength(1);
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); expect(response).toEqual([
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), ack: expect.any(String),
data: { data: {
@ -80,35 +61,23 @@ describe.concurrent(SyncRequestType.AlbumToAssetsV1, () => {
}, },
type: SyncEntityType.AlbumToAssetV1, type: SyncEntityType.AlbumToAssetV1,
}, },
]), ]);
);
const acks = [initialSyncResponse[0].ack]; await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks }); await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]);
const ackSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
expect(ackSyncResponse).toEqual([]);
}); });
it('should detect and sync the album to asset for shared albums', async () => { 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 response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
const user2 = mediumFactory.userInsert(); expect(response).toHaveLength(1);
await userRepo.create(user2); expect(response).toEqual([
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), ack: expect.any(String),
data: { data: {
@ -117,57 +86,34 @@ describe.concurrent(SyncRequestType.AlbumToAssetsV1, () => {
}, },
type: SyncEntityType.AlbumToAssetV1, type: SyncEntityType.AlbumToAssetV1,
}, },
]), ]);
);
const acks = [initialSyncResponse[0].ack]; await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks }); await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]);
const ackSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
expect(ackSyncResponse).toEqual([]);
}); });
it('should not sync album to asset for an album owned by another user', async () => { it('should not sync album to asset for an album owned by another user', async () => {
const { auth, getRepository, testSync } = await setup(); const { auth, ctx } = await setup();
const { user: user2 } = await ctx.newUser();
const userRepo = getRepository('user'); const { asset } = await ctx.newAsset({ ownerId: user2.id });
const user2 = mediumFactory.userInsert(); const { album } = await ctx.newAlbum({ ownerId: user2.id });
await userRepo.create(user2); await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]);
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);
}); });
it('should backfill album to assets when a user shares an album with you', async () => { it('should backfill album to assets when a user shares an album with you', async () => {
const { auth, sut, testSync, getRepository } = await setup(); const { auth, ctx } = await setup();
const { user: user2 } = await ctx.newUser();
const userRepo = getRepository('user'); const { asset: album1Asset } = await ctx.newAsset({ ownerId: user2.id });
const user2 = mediumFactory.userInsert(); const { asset: album2Asset } = await ctx.newAsset({ ownerId: auth.user.id });
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);
// Backfill album // Backfill album
const albumRepo = getRepository('album'); const { album: album2 } = await ctx.newAlbum({ ownerId: user2.id });
const album2 = mediumFactory.albumInsert({ ownerId: user2.id }); await ctx.newAlbumAsset({ albumId: album2.id, assetId: album2Asset.id });
await albumRepo.create(album2, [album2Asset.id], []);
await wait(2); 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 }); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
await albumRepo.create(album1, [album1Asset.id], []);
const response = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
expect(response).toHaveLength(1); expect(response).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
@ -181,16 +127,14 @@ describe.concurrent(SyncRequestType.AlbumToAssetsV1, () => {
]); ]);
// ack initial album to asset sync // ack initial album to asset sync
const acks = response.map(({ ack }) => ack); await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks });
// add user to backfill album // add user to backfill album
const albumUserRepo = getRepository('albumUser'); await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.EDITOR });
await albumUserRepo.create({ albumsId: album2.id, usersId: auth.user.id, role: AlbumUserRole.EDITOR });
// should backfill the album to asset relation // should backfill the album to asset relation
const backfillResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
expect(backfillResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ data: expect.objectContaining({
@ -206,26 +150,20 @@ describe.concurrent(SyncRequestType.AlbumToAssetsV1, () => {
}, },
]); ]);
await sut.setAcks(auth, { acks: [backfillResponse[1].ack] }); await ctx.syncAckAll(auth, newResponse);
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]);
const finalResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
expect(finalResponse).toEqual([]);
}); });
it('should detect and sync a deleted album to asset relation', async () => { 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 response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
const assetRepo = getRepository('asset'); expect(response).toHaveLength(1);
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); expect(response).toEqual([
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([
{ {
ack: expect.any(String), ack: expect.any(String),
data: { data: {
@ -236,16 +174,13 @@ describe.concurrent(SyncRequestType.AlbumToAssetsV1, () => {
}, },
]); ]);
const acks = [initialSyncResponse[0].ack]; await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks });
await albumRepo.removeAssetIds(album.id, [asset.id]); await albumRepo.removeAssetIds(album.id, [asset.id]);
await wait(2); await wait(2);
const syncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
expect(syncResponse).toHaveLength(1); expect(newResponse).toHaveLength(1);
expect(syncResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: { data: {
@ -256,26 +191,20 @@ describe.concurrent(SyncRequestType.AlbumToAssetsV1, () => {
}, },
]); ]);
await sut.setAcks(auth, { acks: [syncResponse[0].ack] }); await ctx.syncAckAll(auth, newResponse);
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]);
const ackSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
expect(ackSyncResponse).toEqual([]);
}); });
it('should detect and sync a deleted album to asset relation when an asset is deleted', async () => { 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 response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
const assetRepo = getRepository('asset'); expect(response).toHaveLength(1);
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); expect(response).toEqual([
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([
{ {
ack: expect.any(String), ack: expect.any(String),
data: { data: {
@ -286,16 +215,13 @@ describe.concurrent(SyncRequestType.AlbumToAssetsV1, () => {
}, },
]); ]);
const acks = [initialSyncResponse[0].ack]; await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks });
await assetRepo.remove({ id: asset.id }); await assetRepo.remove({ id: asset.id });
await wait(2); await wait(2);
const syncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
expect(syncResponse).toHaveLength(1); expect(newResponse).toHaveLength(1);
expect(syncResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: { data: {
@ -306,26 +232,20 @@ describe.concurrent(SyncRequestType.AlbumToAssetsV1, () => {
}, },
]); ]);
await sut.setAcks(auth, { acks: [syncResponse[0].ack] }); await ctx.syncAckAll(auth, newResponse);
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]);
const ackSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
expect(ackSyncResponse).toEqual([]);
}); });
it('should not sync a deleted album to asset relation when the album is deleted', async () => { 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 response = await ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1]);
const assetRepo = getRepository('asset'); expect(response).toHaveLength(1);
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); expect(response).toEqual([
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([
{ {
ack: expect.any(String), ack: expect.any(String),
data: { data: {
@ -336,15 +256,9 @@ describe.concurrent(SyncRequestType.AlbumToAssetsV1, () => {
}, },
]); ]);
const acks = [initialSyncResponse[0].ack]; await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks });
await albumRepo.delete(album.id); await albumRepo.delete(album.id);
await wait(2); await wait(2);
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toEqual([]);
const syncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
expect(syncResponse).toHaveLength(0);
expect(syncResponse).toEqual([]);
}); });
}); });

View File

@ -1,17 +1,16 @@
import { Kysely } from 'kysely'; import { Kysely } from 'kysely';
import { DB } from 'src/db'; import { DB } from 'src/db';
import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum'; 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'; import { getKyselyDB, wait } from 'test/utils';
let defaultDatabase: Kysely<DB>; let defaultDatabase: Kysely<DB>;
const setup = async (db?: Kysely<DB>) => { const setup = async (db?: Kysely<DB>) => {
const database = db || defaultDatabase; const ctx = new SyncTestContext(db || defaultDatabase);
const result = newSyncTest({ db: database }); const { auth, user, session } = await ctx.newSyncAuthUser();
const { auth, create } = newSyncAuthUser(); return { auth, user, session, ctx };
await create(database);
return { ...result, auth };
}; };
beforeAll(async () => { beforeAll(async () => {
@ -20,198 +19,156 @@ beforeAll(async () => {
describe(SyncRequestType.AlbumUsersV1, () => { describe(SyncRequestType.AlbumUsersV1, () => {
it('should sync an album user with the correct properties', async () => { 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'); await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
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([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ data: expect.objectContaining({
albumId: albumUser.albumsId, albumId: albumUser.albumId,
role: albumUser.role, role: albumUser.role,
userId: albumUser.usersId, userId: albumUser.userId,
}), }),
type: SyncEntityType.AlbumUserV1, type: SyncEntityType.AlbumUserV1,
}, },
]); ]);
}); });
describe('owner', () => { describe('owner', () => {
it('should detect and sync a new shared user', async () => { 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 response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
const albumUserRepo = getRepository('albumUser'); expect(response).toHaveLength(1);
const userRepo = getRepository('user'); expect(response).toEqual([
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([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ data: expect.objectContaining({
albumId: albumUser.albumsId, albumId: albumUser.albumId,
role: albumUser.role, role: albumUser.role,
userId: albumUser.usersId, userId: albumUser.userId,
}), }),
type: SyncEntityType.AlbumUserV1, 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 () => { 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 response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
const albumUserRepo = getRepository('albumUser'); await ctx.syncAckAll(auth, response);
const userRepo = getRepository('user'); await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
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([]);
await albumUserRepo.update({ albumsId: album.id, usersId: user1.id }, { role: AlbumUserRole.VIEWER }); await albumUserRepo.update({ albumsId: album.id, usersId: user1.id }, { role: AlbumUserRole.VIEWER });
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([ expect(newResponse).toHaveLength(1);
expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ data: expect.objectContaining({
albumId: albumUser.albumsId, albumId: albumUser.albumId,
role: AlbumUserRole.VIEWER, role: AlbumUserRole.VIEWER,
userId: albumUser.usersId, userId: albumUser.userId,
}), }),
type: SyncEntityType.AlbumUserV1, 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 () => { 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 response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
const albumUserRepo = getRepository('albumUser'); expect(response).toHaveLength(1);
const userRepo = getRepository('user'); await ctx.syncAckAll(auth, response);
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
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([]);
await albumUserRepo.delete({ albumsId: album.id, usersId: user1.id }); await albumUserRepo.delete({ albumsId: album.id, usersId: user1.id });
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ data: expect.objectContaining({
albumId: albumUser.albumsId, albumId: albumUser.albumId,
userId: albumUser.usersId, userId: albumUser.userId,
}), }),
type: SyncEntityType.AlbumUserDeleteV1, type: SyncEntityType.AlbumUserDeleteV1,
}, },
]); ]);
await ctx.syncAckAll(auth, newResponse);
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
}); });
}); });
describe('shared user', () => { describe('shared user', () => {
it('should detect and sync a new shared user', async () => { 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 response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
const albumUserRepo = getRepository('albumUser'); expect(response).toHaveLength(1);
const userRepo = getRepository('user'); expect(response).toEqual([
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([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ data: expect.objectContaining({
albumId: albumUser.albumsId, albumId: albumUser.albumId,
role: albumUser.role, role: albumUser.role,
userId: albumUser.usersId, userId: albumUser.userId,
}), }),
type: SyncEntityType.AlbumUserV1, 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 () => { 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 response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
const albumUserRepo = getRepository('albumUser'); expect(response).toHaveLength(2);
const userRepo = getRepository('user');
const owner = mediumFactory.userInsert(); await ctx.syncAckAll(auth, response);
const user = mediumFactory.userInsert(); await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
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 albumUserRepo.update({ albumsId: album.id, usersId: user.id }, { role: AlbumUserRole.VIEWER }); await albumUserRepo.update({ albumsId: album.id, usersId: user.id }, { role: AlbumUserRole.VIEWER });
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ data: expect.objectContaining({
@ -222,39 +179,29 @@ describe(SyncRequestType.AlbumUsersV1, () => {
type: SyncEntityType.AlbumUserV1, 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 () => { 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 response = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
const albumUserRepo = getRepository('albumUser'); expect(response).toHaveLength(2);
const userRepo = getRepository('user'); await ctx.syncAckAll(auth, response);
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 expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
await albumUserRepo.delete({ albumsId: album.id, usersId: user.id }); 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), ack: expect.any(String),
data: expect.objectContaining({ data: expect.objectContaining({
@ -264,35 +211,27 @@ describe(SyncRequestType.AlbumUsersV1, () => {
type: SyncEntityType.AlbumUserDeleteV1, 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 () => { it('should backfill album users when a user shares an album with you', async () => {
const { auth, sut, testSync, getRepository } = await setup(); const { auth, ctx } = await setup();
const { user: user1 } = await ctx.newUser();
const albumRepo = getRepository('album'); const { user: user2 } = await ctx.newUser();
const albumUserRepo = getRepository('albumUser'); const { album: album1 } = await ctx.newAlbum({ ownerId: user1.id });
const userRepo = getRepository('user'); const { album: album2 } = await ctx.newAlbum({ ownerId: user1.id });
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, [], []);
// backfill album user // 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); await wait(2);
// initial album user // 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); await wait(2);
// post checkpoint album user // 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).toHaveLength(1);
expect(response).toEqual([ expect(response).toEqual([
{ {
@ -307,15 +246,13 @@ describe(SyncRequestType.AlbumUsersV1, () => {
]); ]);
// ack initial user // ack initial user
const acks = response.map(({ ack }) => ack); await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks });
// get access to the backfill album user // 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 // should backfill the album user
const backfillResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1]);
expect(backfillResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ data: expect.objectContaining({
@ -350,10 +287,8 @@ describe(SyncRequestType.AlbumUsersV1, () => {
}, },
]); ]);
await sut.setAcks(auth, { acks: [backfillResponse[1].ack, backfillResponse.at(-1).ack] }); await ctx.syncAckAll(auth, newResponse);
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
const finalResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
expect(finalResponse).toEqual([]);
}); });
}); });
}); });

View File

@ -1,17 +1,17 @@
import { Kysely } from 'kysely'; import { Kysely } from 'kysely';
import { DB } from 'src/db'; import { DB } from 'src/db';
import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum'; 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'; import { getKyselyDB } from 'test/utils';
let defaultDatabase: Kysely<DB>; let defaultDatabase: Kysely<DB>;
const setup = async (db?: Kysely<DB>) => { const setup = async (db?: Kysely<DB>) => {
const database = db || defaultDatabase; const ctx = new SyncTestContext(db || defaultDatabase);
const result = newSyncTest({ db: database }); const { auth, user, session } = await ctx.newSyncAuthUser();
const { auth, create } = newSyncAuthUser(); return { auth, user, session, ctx };
await create(database);
return { ...result, auth };
}; };
beforeAll(async () => { beforeAll(async () => {
@ -20,11 +20,12 @@ beforeAll(async () => {
describe(SyncRequestType.AlbumsV1, () => { describe(SyncRequestType.AlbumsV1, () => {
it('should sync an album with the correct properties', async () => { it('should sync an album with the correct properties', async () => {
const { auth, getRepository, testSync } = await setup(); const { auth, ctx } = await setup();
const albumRepo = getRepository('album'); const { album } = await ctx.newAlbum({ ownerId: auth.user.id });
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
await albumRepo.create(album, [], []); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([ expect(response).toHaveLength(1);
expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ data: expect.objectContaining({
@ -35,14 +36,18 @@ describe(SyncRequestType.AlbumsV1, () => {
type: SyncEntityType.AlbumV1, 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 () => { it('should detect and sync a new album', async () => {
const { auth, getRepository, testSync } = await setup(); const { auth, ctx } = await setup();
const albumRepo = getRepository('album'); const { album } = await ctx.newAlbum({ ownerId: auth.user.id });
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
await albumRepo.create(album, [], []); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([ expect(response).toHaveLength(1);
expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ data: expect.objectContaining({
@ -51,14 +56,19 @@ describe(SyncRequestType.AlbumsV1, () => {
type: SyncEntityType.AlbumV1, 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 () => { it('should detect and sync an album delete', async () => {
const { auth, getRepository, testSync } = await setup(); const { auth, ctx } = await setup();
const albumRepo = getRepository('album'); const albumRepo = ctx.get(AlbumRepository);
const album = mediumFactory.albumInsert({ ownerId: auth.user.id }); const { album } = await ctx.newAlbum({ ownerId: auth.user.id });
await albumRepo.create(album, [], []);
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), ack: expect.any(String),
data: expect.objectContaining({ data: expect.objectContaining({
@ -69,7 +79,10 @@ describe(SyncRequestType.AlbumsV1, () => {
]); ]);
await albumRepo.delete(album.id); 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), ack: expect.any(String),
data: { data: {
@ -78,67 +91,60 @@ describe(SyncRequestType.AlbumsV1, () => {
type: SyncEntityType.AlbumDeleteV1, type: SyncEntityType.AlbumDeleteV1,
}, },
]); ]);
await ctx.syncAckAll(auth, newResponse);
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]);
}); });
describe('shared albums', () => { describe('shared albums', () => {
it('should detect and sync an album create', async () => { it('should detect and sync an album create', async () => {
const { auth, getRepository, testSync } = await setup(); const { auth, ctx } = await setup();
const albumRepo = getRepository('album'); const { user: user2 } = await ctx.newUser();
const userRepo = getRepository('user'); 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(); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
await userRepo.create(user2); expect(response).toHaveLength(1);
expect(response).toEqual([
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([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ id: album.id }), data: expect.objectContaining({ id: album.id }),
type: SyncEntityType.AlbumV1, 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 () => { it('should detect and sync an album share (share before sync)', async () => {
const { auth, getRepository, testSync } = await setup(); const { auth, ctx } = await setup();
const albumRepo = getRepository('album'); const { user: user2 } = await ctx.newUser();
const albumUserRepo = getRepository('albumUser'); const { album } = await ctx.newAlbum({ ownerId: user2.id });
const userRepo = getRepository('user'); await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.EDITOR });
const user2 = mediumFactory.userInsert(); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
await userRepo.create(user2); expect(response).toHaveLength(1);
expect(response).toEqual([
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([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ id: album.id }), data: expect.objectContaining({ id: album.id }),
type: SyncEntityType.AlbumV1, 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 () => { it('should detect and sync an album share (share after sync)', async () => {
const { auth, getRepository, sut, testSync } = await setup(); const { auth, ctx } = await setup();
const albumRepo = getRepository('album'); const { user: user2 } = await ctx.newUser();
const albumUserRepo = getRepository('albumUser'); const { album: userAlbum } = await ctx.newAlbum({ ownerId: auth.user.id });
const userRepo = getRepository('user'); const { album: user2Album } = await ctx.newAlbum({ ownerId: user2.id });
const user2 = mediumFactory.userInsert(); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
await userRepo.create(user2); expect(response).toEqual([
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([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ id: userAlbum.id }), data: expect.objectContaining({ id: userAlbum.id }),
@ -146,75 +152,76 @@ describe(SyncRequestType.AlbumsV1, () => {
}, },
]); ]);
const acks = [initialSyncResponse[0].ack]; await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks }); 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 }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
expect(newResponse).toHaveLength(1);
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ id: user2Album.id }), data: expect.objectContaining({ id: user2Album.id }),
type: SyncEntityType.AlbumV1, 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 () => { it('should detect and sync an album delete`', async () => {
const { auth, getRepository, testSync, sut } = await setup(); const { auth, ctx } = await setup();
const albumRepo = getRepository('album'); const albumRepo = ctx.get(AlbumRepository);
const userRepo = getRepository('user'); 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(); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
await userRepo.create(user2); expect(response).toHaveLength(1);
const album = mediumFactory.albumInsert({ ownerId: user2.id }); await ctx.syncAckAll(auth, response);
await albumRepo.create(album, [], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]); await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]);
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 albumRepo.delete(album.id); await albumRepo.delete(album.id);
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([ expect(newResponse).toHaveLength(1);
expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: { albumId: album.id }, data: { albumId: album.id },
type: SyncEntityType.AlbumDeleteV1, 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 () => { it('should detect and sync an album unshare as an album delete', async () => {
const { auth, getRepository, testSync, sut } = await setup(); const { auth, ctx } = await setup();
const albumRepo = getRepository('album'); const albumUserRepo = ctx.get(AlbumUserRepository);
const albumUserRepo = getRepository('albumUser'); const { user: user2 } = await ctx.newUser();
const userRepo = getRepository('user'); 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(); const response = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
await userRepo.create(user2); expect(response).toHaveLength(1);
const album = mediumFactory.albumInsert({ ownerId: user2.id }); await ctx.syncAckAll(auth, response);
await albumRepo.create(album, [], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]); await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]);
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 albumUserRepo.delete({ albumsId: album.id, usersId: auth.user.id }); await albumUserRepo.delete({ albumsId: album.id, usersId: auth.user.id });
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumsV1]);
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: { albumId: album.id }, data: { albumId: album.id },
type: SyncEntityType.AlbumDeleteV1, type: SyncEntityType.AlbumDeleteV1,
}, },
]); ]);
await ctx.syncAckAll(auth, newResponse);
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]);
}); });
}); });
}); });

View File

@ -1,37 +1,31 @@
import { Kysely } from 'kysely'; import { Kysely } from 'kysely';
import { DB } from 'src/db'; import { DB } from 'src/db';
import { SyncEntityType, SyncRequestType } from 'src/enum'; 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 { factory } from 'test/small.factory';
import { getKyselyDB } from 'test/utils'; import { getKyselyDB } from 'test/utils';
let defaultDatabase: Kysely<DB>; let defaultDatabase: Kysely<DB>;
const setup = async (db?: Kysely<DB>) => { const setup = async (db?: Kysely<DB>) => {
const database = db || defaultDatabase; const ctx = new SyncTestContext(db || defaultDatabase);
const result = newSyncTest({ db: database }); const { auth, user, session } = await ctx.newSyncAuthUser();
const { auth, create } = newSyncAuthUser(); return { auth, user, session, ctx };
await create(database);
return { ...result, auth };
}; };
beforeAll(async () => { beforeAll(async () => {
defaultDatabase = await getKyselyDB(); defaultDatabase = await getKyselyDB();
}); });
describe.concurrent(SyncRequestType.AssetExifsV1, () => { describe(SyncRequestType.AssetExifsV1, () => {
it('should detect and sync the first asset exif', async () => { 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 response = await ctx.syncStream(auth, [SyncRequestType.AssetExifsV1]);
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); expect(response).toHaveLength(1);
await assetRepo.create(asset); expect(response).toEqual([
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), ack: expect.any(String),
data: { data: {
@ -63,38 +57,22 @@ describe.concurrent(SyncRequestType.AssetExifsV1, () => {
}, },
type: SyncEntityType.AssetExifV1, type: SyncEntityType.AssetExifV1,
}, },
]), ]);
);
const acks = [initialSyncResponse[0].ack]; await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks }); await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toEqual([]);
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]);
expect(ackSyncResponse).toHaveLength(0);
}); });
it('should only sync asset exif for own user', async () => { it('should only sync asset exif for own user', async () => {
const { auth, getRepository, testSync } = await setup(); const { auth, ctx } = await setup();
const { user: user2 } = await ctx.newUser();
const userRepo = getRepository('user'); await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
const user2 = mediumFactory.userInsert(); const { asset } = await ctx.newAsset({ ownerId: user2.id });
await userRepo.create(user2); await ctx.newExif({ assetId: asset.id, make: 'Canon' });
const { session } = await ctx.newSession({ userId: user2.id });
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 auth2 = factory.auth({ session, user: user2 }); 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);
}); });
}); });

View File

@ -1,35 +1,32 @@
import { Kysely } from 'kysely'; import { Kysely } from 'kysely';
import { DB } from 'src/db'; import { DB } from 'src/db';
import { SyncEntityType, SyncRequestType } from 'src/enum'; 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 { factory } from 'test/small.factory';
import { getKyselyDB } from 'test/utils'; import { getKyselyDB } from 'test/utils';
let defaultDatabase: Kysely<DB>; let defaultDatabase: Kysely<DB>;
const setup = async (db?: Kysely<DB>) => { const setup = async (db?: Kysely<DB>) => {
const database = db || defaultDatabase; const ctx = new SyncTestContext(db || defaultDatabase);
const result = newSyncTest({ db: database }); const { auth, user, session } = await ctx.newSyncAuthUser();
const { auth, create } = newSyncAuthUser(); return { auth, user, session, ctx };
await create(database);
return { ...result, auth };
}; };
beforeAll(async () => { beforeAll(async () => {
defaultDatabase = await getKyselyDB(); defaultDatabase = await getKyselyDB();
}); });
describe.concurrent(SyncEntityType.AssetV1, () => { describe(SyncEntityType.AssetV1, () => {
it('should detect and sync the first asset', async () => { it('should detect and sync the first asset', async () => {
const { auth, sut, getRepository, testSync } = await setup();
const originalFileName = 'firstAsset'; const originalFileName = 'firstAsset';
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
const date = new Date().toISOString(); const date = new Date().toISOString();
const assetRepo = getRepository('asset'); const { auth, ctx } = await setup();
const asset = mediumFactory.assetInsert({ const { asset } = await ctx.newAsset({
originalFileName, originalFileName,
ownerId: auth.user.id, ownerId: auth.user.id,
checksum: Buffer.from(checksum, 'base64'), checksum: Buffer.from(checksum, 'base64'),
@ -40,13 +37,10 @@ describe.concurrent(SyncEntityType.AssetV1, () => {
deletedAt: null, deletedAt: null,
duration: '0:10:00.00000', duration: '0:10:00.00000',
}); });
await assetRepo.create(asset);
const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]);
expect(response).toHaveLength(1);
expect(initialSyncResponse).toHaveLength(1); expect(response).toEqual([
expect(initialSyncResponse).toEqual(
expect.arrayContaining([
{ {
ack: expect.any(String), ack: expect.any(String),
data: { data: {
@ -66,30 +60,21 @@ describe.concurrent(SyncEntityType.AssetV1, () => {
}, },
type: 'AssetV1', type: 'AssetV1',
}, },
]), ]);
);
const acks = [initialSyncResponse[0].ack]; await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks }); await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([]);
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
expect(ackSyncResponse).toHaveLength(0);
}); });
it('should detect and sync a deleted asset', async () => { it('should detect and sync a deleted asset', async () => {
const { auth, sut, getRepository, testSync } = await setup(); const { auth, ctx } = await setup();
const assetRepo = ctx.get(AssetRepository);
const assetRepo = getRepository('asset'); const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
await assetRepo.create(asset);
await assetRepo.remove(asset); 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).toHaveLength(1);
expect(response).toEqual( expect(response).toEqual([
expect.arrayContaining([
{ {
ack: expect.any(String), ack: expect.any(String),
data: { data: {
@ -97,39 +82,25 @@ describe.concurrent(SyncEntityType.AssetV1, () => {
}, },
type: 'AssetDeleteV1', type: 'AssetDeleteV1',
}, },
]), ]);
);
const acks = response.map(({ ack }) => ack); await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks }); await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([]);
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
expect(ackSyncResponse).toHaveLength(0);
}); });
it('should not sync an asset or asset delete for an unrelated user', async () => { it('should not sync an asset or asset delete for an unrelated user', async () => {
const { auth, getRepository, testSync } = await setup(); const { auth, ctx } = await setup();
const assetRepo = ctx.get(AssetRepository);
const userRepo = getRepository('user'); const { user: user2 } = await ctx.newUser();
const user2 = mediumFactory.userInsert(); const { session } = await ctx.newSession({ userId: user2.id });
await userRepo.create(user2); const { asset } = await ctx.newAsset({ ownerId: user2.id });
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 auth2 = factory.auth({ session, user: user2 }); const auth2 = factory.auth({ session, user: user2 });
expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1); expect(await ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1);
expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0); expect(await ctx.syncStream(auth, [SyncRequestType.AssetsV1])).toHaveLength(0);
await assetRepo.remove(asset); await assetRepo.remove(asset);
expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1); expect(await ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1);
expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0); expect(await ctx.syncStream(auth, [SyncRequestType.AssetsV1])).toHaveLength(0);
}); });
}); });

View File

@ -1,18 +1,16 @@
import { Kysely } from 'kysely'; import { Kysely } from 'kysely';
import { DB } from 'src/db'; import { DB } from 'src/db';
import { SyncEntityType, SyncRequestType } from 'src/enum'; 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 { factory } from 'test/small.factory';
import { getKyselyDB, wait } from 'test/utils'; import { getKyselyDB, wait } from 'test/utils';
let defaultDatabase: Kysely<DB>; let defaultDatabase: Kysely<DB>;
const setup = async (db?: Kysely<DB>) => { const setup = async (db?: Kysely<DB>) => {
const database = db || defaultDatabase; const ctx = new SyncTestContext(db || defaultDatabase);
const result = newSyncTest({ db: database }); const { auth, user, session } = await ctx.newSyncAuthUser();
const { auth, create } = newSyncAuthUser(); return { auth, user, session, ctx };
await create(database);
return { ...result, auth };
}; };
beforeAll(async () => { beforeAll(async () => {
@ -21,25 +19,15 @@ beforeAll(async () => {
describe(SyncRequestType.PartnerAssetExifsV1, () => { describe(SyncRequestType.PartnerAssetExifsV1, () => {
it('should detect and sync the first partner asset exif', async () => { 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 response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]);
const user2 = mediumFactory.userInsert(); expect(response).toHaveLength(1);
await userRepo.create(user2); expect(response).toEqual([
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), ack: expect.any(String),
data: { data: {
@ -71,84 +59,49 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => {
}, },
type: SyncEntityType.PartnerAssetExifV1, type: SyncEntityType.PartnerAssetExifV1,
}, },
]), ]);
);
const acks = [initialSyncResponse[0].ack]; await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks }); await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]);
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
expect(ackSyncResponse).toHaveLength(0);
}); });
it('should not sync partner asset exif for own user', async () => { 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'); await expect(ctx.syncStream(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
const user2 = mediumFactory.userInsert(); await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0);
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);
}); });
it('should not sync partner asset exif for unrelated user', async () => { it('should not sync partner asset exif for unrelated user', async () => {
const { auth, getRepository, testSync } = await setup(); const { auth, ctx } = await setup();
const { user: user2 } = await ctx.newUser();
const userRepo = getRepository('user'); const { user: user3 } = await ctx.newUser();
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
const user2 = mediumFactory.userInsert(); const { asset } = await ctx.newAsset({ ownerId: user3.id });
const user3 = mediumFactory.userInsert(); await ctx.newExif({ assetId: asset.id, make: 'Canon' });
await Promise.all([userRepo.create(user2), userRepo.create(user3)]); const { session } = await ctx.newSession({ userId: user3.id });
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 authUser3 = factory.auth({ session, user: user3 }); 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 () => { it('should backfill partner asset exif when a partner shared their library with you', async () => {
const { auth, sut, getRepository, testSync } = await setup(); const { auth, ctx } = await setup();
const { user: user2 } = await ctx.newUser();
const userRepo = getRepository('user'); const { user: user3 } = await ctx.newUser();
const user2 = mediumFactory.userInsert(); const { asset: assetUser3 } = await ctx.newAsset({ ownerId: user3.id });
const user3 = mediumFactory.userInsert(); await ctx.newExif({ assetId: assetUser3.id, make: 'Canon' });
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' });
await wait(2); await wait(2);
await assetRepo.create(assetUser2); const { asset: assetUser2 } = await ctx.newAsset({ ownerId: user2.id });
await assetRepo.upsertExif({ assetId: assetUser2.id, make: 'Canon' }); await ctx.newExif({ assetId: assetUser2.id, make: 'Canon' });
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
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).toHaveLength(1);
expect(response).toEqual( expect(response).toEqual(
expect.arrayContaining([ expect.arrayContaining([
@ -162,15 +115,12 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => {
]), ]),
); );
const acks = response.map(({ ack }) => ack); await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks }); 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]);
const backfillResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]); expect(newResponse).toHaveLength(2);
expect(newResponse).toEqual([
expect(backfillResponse).toHaveLength(2);
expect(backfillResponse).toEqual(
expect.arrayContaining([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ data: expect.objectContaining({
@ -183,46 +133,26 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => {
data: {}, data: {},
type: SyncEntityType.SyncAckV1, type: SyncEntityType.SyncAckV1,
}, },
]), ]);
);
const backfillAck = backfillResponse[1].ack; await ctx.syncAckAll(auth, newResponse);
await sut.setAcks(auth, { acks: [backfillAck] }); await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]);
const finalResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
const finalAcks = finalResponse.map(({ ack }) => ack);
expect(finalAcks).toEqual([]);
}); });
it('should handle partners with users ids lower than a uuidv7', async () => { it('should handle partners with users ids lower than a uuidv7', async () => {
const { auth, sut, getRepository, testSync } = await setup(); const { auth, ctx } = await setup();
const { user: user2 } = await ctx.newUser({ id: '00d4c0af-7695-4cf2-85e6-415eeaf449cb' });
const userRepo = getRepository('user'); const { user: user3 } = await ctx.newUser({ id: '00e4c0af-7695-4cf2-85e6-415eeaf449cb' });
const user2 = mediumFactory.userInsert({ id: '00d4c0af-7695-4cf2-85e6-415eeaf449cb' }); const { asset: assetUser3 } = await ctx.newAsset({ ownerId: user3.id });
const user3 = mediumFactory.userInsert({ id: '00e4c0af-7695-4cf2-85e6-415eeaf449cb' }); await ctx.newExif({ assetId: assetUser3.id, make: 'assetUser3' });
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' });
await wait(2); 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 }); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]);
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]);
expect(response).toHaveLength(1); expect(response).toHaveLength(1);
expect(response).toEqual( expect(response).toEqual([
expect.arrayContaining([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ data: expect.objectContaining({
@ -230,22 +160,16 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => {
}), }),
type: SyncEntityType.PartnerAssetExifV1, type: SyncEntityType.PartnerAssetExifV1,
}, },
]), ]);
);
const acks = response.map(({ ack }) => ack);
await sut.setAcks(auth, { acks });
await ctx.syncAckAll(auth, response);
// This checks that our ack upsert is correct // This checks that our ack upsert is correct
const ackUpsertResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]); await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]);
expect(ackUpsertResponse).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);
const syncAckResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]); expect(newResponse).toEqual([
expect(syncAckResponse).toHaveLength(2);
expect(syncAckResponse).toEqual(
expect.arrayContaining([
{ {
ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)), ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)),
data: expect.objectContaining({ data: expect.objectContaining({
@ -258,46 +182,29 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => {
data: {}, data: {},
type: SyncEntityType.SyncAckV1, type: SyncEntityType.SyncAckV1,
}, },
]), ]);
);
const syncAckResponseAcks = syncAckResponse.map(({ ack }) => ack); await ctx.syncAckAll(auth, newResponse);
await sut.setAcks(auth, { acks: [syncAckResponseAcks[1]] }); await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]);
const finalResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
expect(finalResponse).toEqual([]);
}); });
it('should only backfill partner assets created prior to the current partner asset checkpoint', async () => { 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 { user: user2 } = await ctx.newUser();
const userRepo = getRepository('user'); const { user: user3 } = await ctx.newUser();
const user2 = mediumFactory.userInsert(); const { asset: assetUser3 } = await ctx.newAsset({ ownerId: user3.id });
const user3 = mediumFactory.userInsert(); await ctx.newExif({ assetId: assetUser3.id, make: 'assetUser3' });
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' });
await wait(2); await wait(2);
await assetRepo.create(assetUser2); const { asset: assetUser2 } = await ctx.newAsset({ ownerId: user2.id });
await assetRepo.upsertExif({ assetId: assetUser2.id, make: 'assetUser2' }); await ctx.newExif({ assetId: assetUser2.id, make: 'assetUser2' });
await wait(2); await wait(2);
await assetRepo.create(asset2User3); const { asset: asset2User3 } = await ctx.newAsset({ ownerId: user3.id });
await assetRepo.upsertExif({ assetId: asset2User3.id, make: 'asset2User3' }); await ctx.newExif({ assetId: asset2User3.id, make: 'asset2User3' });
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
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).toHaveLength(1);
expect(response).toEqual( expect(response).toEqual([
expect.arrayContaining([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ data: expect.objectContaining({
@ -305,18 +212,14 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => {
}), }),
type: SyncEntityType.PartnerAssetExifV1, type: SyncEntityType.PartnerAssetExifV1,
}, },
]), ]);
);
const acks = response.map(({ ack }) => ack); await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks }); 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]);
const backfillResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]); expect(newResponse).toHaveLength(3);
expect(newResponse).toEqual([
expect(backfillResponse).toHaveLength(3);
expect(backfillResponse).toEqual(
expect.arrayContaining([
{ {
ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)), ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)),
data: expect.objectContaining({ data: expect.objectContaining({
@ -336,15 +239,9 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => {
}), }),
type: SyncEntityType.PartnerAssetExifV1, type: SyncEntityType.PartnerAssetExifV1,
}, },
]), ]);
);
const backfillAck = backfillResponse[1].ack; await ctx.syncAckAll(auth, newResponse);
const partnerAssetAck = backfillResponse[2].ack; await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toEqual([]);
await sut.setAcks(auth, { acks: [backfillAck, partnerAssetAck] });
const finalResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
const finalAcks = finalResponse.map(({ ack }) => ack);
expect(finalAcks).toEqual([]);
}); });
}); });

View File

@ -1,18 +1,19 @@
import { Kysely } from 'kysely'; import { Kysely } from 'kysely';
import { DB } from 'src/db'; import { DB } from 'src/db';
import { SyncEntityType, SyncRequestType } from 'src/enum'; 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 { factory } from 'test/small.factory';
import { getKyselyDB, wait } from 'test/utils'; import { getKyselyDB, wait } from 'test/utils';
let defaultDatabase: Kysely<DB>; let defaultDatabase: Kysely<DB>;
const setup = async (db?: Kysely<DB>) => { const setup = async (db?: Kysely<DB>) => {
const database = db || defaultDatabase; const ctx = new SyncTestContext(db || defaultDatabase);
const result = newSyncTest({ db: database }); const { auth, user, session } = await ctx.newSyncAuthUser();
const { auth, create } = newSyncAuthUser(); return { auth, user, session, ctx };
await create(database);
return { ...result, auth };
}; };
beforeAll(async () => { beforeAll(async () => {
@ -21,19 +22,15 @@ beforeAll(async () => {
describe(SyncRequestType.PartnerAssetsV1, () => { describe(SyncRequestType.PartnerAssetsV1, () => {
it('should detect and sync the first partner asset', async () => { 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 originalFileName = 'firstPartnerAsset';
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
const date = new Date().toISOString(); const date = new Date().toISOString();
const userRepo = getRepository('user'); const { user: user2 } = await ctx.newUser();
const user2 = mediumFactory.userInsert(); const { asset } = await ctx.newAsset({
await userRepo.create(user2);
const assetRepo = getRepository('asset');
const asset = mediumFactory.assetInsert({
ownerId: user2.id, ownerId: user2.id,
originalFileName, originalFileName,
checksum: Buffer.from(checksum, 'base64'), checksum: Buffer.from(checksum, 'base64'),
@ -44,16 +41,12 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
deletedAt: null, deletedAt: null,
duration: '0:10:00.00000', duration: '0:10:00.00000',
}); });
await assetRepo.create(asset);
const partnerRepo = getRepository('partner'); await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]);
expect(response).toHaveLength(1);
expect(initialSyncResponse).toHaveLength(1); expect(response).toEqual([
expect(initialSyncResponse).toEqual(
expect.arrayContaining([
{ {
ack: expect.any(String), ack: expect.any(String),
data: { data: {
@ -73,37 +66,24 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
}, },
type: SyncEntityType.PartnerAssetV1, type: SyncEntityType.PartnerAssetV1,
}, },
]), ]);
);
const acks = [initialSyncResponse[0].ack]; await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks }); await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]);
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
expect(ackSyncResponse).toHaveLength(0);
}); });
it('should detect and sync a deleted partner asset', async () => { 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 { user: user2 } = await ctx.newUser();
const user2 = mediumFactory.userInsert(); const { asset } = await ctx.newAsset({ ownerId: user2.id });
await userRepo.create(user2); await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
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 });
await assetRepo.remove(asset); 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).toHaveLength(1);
expect(response).toEqual( expect(response).toEqual([
expect.arrayContaining([
{ {
ack: expect.any(String), ack: expect.any(String),
data: { data: {
@ -111,130 +91,83 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
}, },
type: SyncEntityType.PartnerAssetDeleteV1, type: SyncEntityType.PartnerAssetDeleteV1,
}, },
]), ]);
);
const acks = response.map(({ ack }) => ack); await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks }); await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]);
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
expect(ackSyncResponse).toHaveLength(0);
}); });
it('should not sync a deleted partner asset due to a user delete', async () => { it('should not sync a deleted partner asset due to a user delete', async () => {
const { auth, getRepository, testSync } = await setup(); const { auth, ctx } = await setup();
const userRepo = ctx.get(UserRepository);
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 { 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); await userRepo.delete({ id: user2.id }, true);
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]);
const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
expect(response).toHaveLength(0);
}); });
it('should not sync a deleted partner asset due to a partner delete (unshare)', async () => { it('should not sync a deleted partner asset due to a partner delete (unshare)', async () => {
const { auth, getRepository, testSync } = await setup(); const { auth, ctx } = await setup();
const partnerRepo = ctx.get(PartnerRepository);
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 { 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 partnerRepo.remove(partner);
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]);
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
}); });
it('should not sync an asset or asset delete for own user', async () => { 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 { user: user2 } = await ctx.newUser();
const user2 = mediumFactory.userInsert(); const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
await userRepo.create(user2); await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
const assetRepo = getRepository('asset'); await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id }); await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
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 assetRepo.remove(asset); await assetRepo.remove(asset);
await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
}); });
it('should not sync an asset or asset delete for unrelated user', async () => { it('should not sync an asset or asset delete for unrelated 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 sessionRepo = getRepository('session');
const session = mediumFactory.sessionInsert({ userId: user2.id });
await sessionRepo.create(session);
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 auth2 = factory.auth({ session, user: user2 });
const assetRepo = getRepository('asset'); await expect(ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
const asset = mediumFactory.assetInsert({ ownerId: user2.id }); await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
await assetRepo.create(asset);
await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
await assetRepo.remove(asset); await assetRepo.remove(asset);
await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1); await expect(ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0); await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
}); });
it('should backfill partner assets when a partner shared their library with you', async () => { 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 { user: user2 } = await ctx.newUser();
const user2 = mediumFactory.userInsert(); const { user: user3 } = await ctx.newUser();
const user3 = mediumFactory.userInsert(); const { asset: assetUser3 } = await ctx.newAsset({ ownerId: user3.id });
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 wait(2); await wait(2);
await assetRepo.create(assetUser2); const { asset: assetUser2 } = await ctx.newAsset({ ownerId: user2.id });
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
const partnerRepo = getRepository('partner');
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]);
expect(response).toHaveLength(1); expect(response).toHaveLength(1);
expect(response).toEqual( expect(response).toEqual([
expect.arrayContaining([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ data: expect.objectContaining({
@ -242,18 +175,14 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
}), }),
type: SyncEntityType.PartnerAssetV1, type: SyncEntityType.PartnerAssetV1,
}, },
]), ]);
);
const acks = response.map(({ ack }) => ack); await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks }); 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.PartnerAssetsV1]);
const backfillResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]); expect(newResponse).toHaveLength(2);
expect(newResponse).toEqual([
expect(backfillResponse).toHaveLength(2);
expect(backfillResponse).toEqual(
expect.arrayContaining([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ data: expect.objectContaining({
@ -266,45 +195,27 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
data: {}, data: {},
type: SyncEntityType.SyncAckV1, type: SyncEntityType.SyncAckV1,
}, },
]), ]);
);
const backfillAck = backfillResponse[1].ack; await ctx.syncAckAll(auth, newResponse);
await sut.setAcks(auth, { acks: [backfillAck] }); await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]);
const finalResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
const finalAcks = finalResponse.map(({ ack }) => ack);
expect(finalAcks).toEqual([]);
}); });
it('should only backfill partner assets created prior to the current partner asset checkpoint', async () => { 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 { user: user2 } = await ctx.newUser();
const user2 = mediumFactory.userInsert(); const { user: user3 } = await ctx.newUser();
const user3 = mediumFactory.userInsert(); const { asset: assetUser3 } = await ctx.newAsset({ ownerId: user3.id });
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 wait(2); await wait(2);
await assetRepo.create(assetUser2); const { asset: assetUser2 } = await ctx.newAsset({ ownerId: user2.id });
await wait(2); await wait(2);
await assetRepo.create(asset2User3); const { asset: asset2User3 } = await ctx.newAsset({ ownerId: user3.id });
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
const partnerRepo = getRepository('partner');
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]);
expect(response).toHaveLength(1); expect(response).toHaveLength(1);
expect(response).toEqual( expect(response).toEqual([
expect.arrayContaining([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ data: expect.objectContaining({
@ -312,18 +223,13 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
}), }),
type: SyncEntityType.PartnerAssetV1, type: SyncEntityType.PartnerAssetV1,
}, },
]), ]);
); await ctx.syncAckAll(auth, response);
const acks = response.map(({ ack }) => ack); await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id });
await sut.setAcks(auth, { acks }); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]);
expect(newResponse).toHaveLength(3);
await partnerRepo.create({ sharedById: user3.id, sharedWithId: auth.user.id }); expect(newResponse).toEqual([
const backfillResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
expect(backfillResponse).toHaveLength(3);
expect(backfillResponse).toEqual(
expect.arrayContaining([
{ {
ack: expect.any(String), ack: expect.any(String),
data: expect.objectContaining({ data: expect.objectContaining({
@ -343,16 +249,9 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
}), }),
type: SyncEntityType.PartnerAssetV1, type: SyncEntityType.PartnerAssetV1,
}, },
]), ]);
);
const backfillAck = backfillResponse[1].ack; await ctx.syncAckAll(auth, newResponse);
const partnerAssetAck = backfillResponse[2].ack; await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([]);
await sut.setAcks(auth, { acks: [backfillAck, partnerAssetAck] });
const finalResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
const finalAcks = finalResponse.map(({ ack }) => ack);
expect(finalAcks).toEqual([]);
}); });
}); });

View File

@ -1,41 +1,33 @@
import { Kysely } from 'kysely'; import { Kysely } from 'kysely';
import { DB } from 'src/db'; import { DB } from 'src/db';
import { SyncEntityType, SyncRequestType } from 'src/enum'; 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'; import { getKyselyDB } from 'test/utils';
let defaultDatabase: Kysely<DB>; let defaultDatabase: Kysely<DB>;
const setup = async (db?: Kysely<DB>) => { const setup = async (db?: Kysely<DB>) => {
const database = db || defaultDatabase; const ctx = new SyncTestContext(db || defaultDatabase);
const result = newSyncTest({ db: database }); const { auth, user, session } = await ctx.newSyncAuthUser();
const { auth, create } = newSyncAuthUser(); return { auth, user, session, ctx };
await create(database);
return { ...result, auth };
}; };
beforeAll(async () => { beforeAll(async () => {
defaultDatabase = await getKyselyDB(); defaultDatabase = await getKyselyDB();
}); });
describe.concurrent(SyncEntityType.PartnerV1, () => { describe(SyncEntityType.PartnerV1, () => {
it('should detect and sync the first partner', async () => { 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 { user: user2 } = await ctx.newUser();
const userRepo = getRepository('user'); const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user1.id });
const partnerRepo = getRepository('partner');
const user2 = mediumFactory.userInsert(); const response = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]);
await userRepo.create(user2); expect(response).toHaveLength(1);
expect(response).toEqual([
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), ack: expect.any(String),
data: { data: {
@ -45,31 +37,22 @@ describe.concurrent(SyncEntityType.PartnerV1, () => {
}, },
type: 'PartnerV1', type: 'PartnerV1',
}, },
]), ]);
);
const acks = [initialSyncResponse[0].ack]; await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks }); await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]);
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
expect(ackSyncResponse).toHaveLength(0);
}); });
it('should detect and sync a deleted partner', async () => { 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 partnerRepo = ctx.get(PartnerRepository);
const user1 = auth.user;
const user2 = mediumFactory.userInsert();
await userRepo.create(user2);
const partnerRepo = getRepository('partner'); const { user: user2 } = await ctx.newUser();
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id }); const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user1.id });
await partnerRepo.remove(partner); 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).toHaveLength(1);
expect(response).toEqual( expect(response).toEqual(
expect.arrayContaining([ expect.arrayContaining([
@ -84,27 +67,18 @@ describe.concurrent(SyncEntityType.PartnerV1, () => {
]), ]),
); );
const acks = response.map(({ ack }) => ack); await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks }); await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]);
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
expect(ackSyncResponse).toHaveLength(0);
}); });
it('should detect and sync a partner share both to and from another user', async () => { 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 { user: user2 } = await ctx.newUser();
const user1 = auth.user; const { partner: partner1 } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user1.id });
const user2 = await userRepo.create(mediumFactory.userInsert()); const { partner: partner2 } = await ctx.newPartner({ sharedById: user1.id, sharedWithId: user2.id });
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 response = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]);
expect(response).toHaveLength(2); expect(response).toHaveLength(2);
expect(response).toEqual( expect(response).toEqual(
expect.arrayContaining([ expect.arrayContaining([
@ -129,28 +103,21 @@ describe.concurrent(SyncEntityType.PartnerV1, () => {
]), ]),
); );
await sut.setAcks(auth, { acks: [response[1].ack] }); await ctx.syncAckAll(auth, response);
await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]);
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
expect(ackSyncResponse).toHaveLength(0);
}); });
it('should sync a partner and then an update to that same partner', async () => { 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 partnerRepo = ctx.get(PartnerRepository);
const user1 = auth.user;
const user2 = await userRepo.create(mediumFactory.userInsert());
const partnerRepo = getRepository('partner'); const { user: user2 } = await ctx.newUser();
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id }); const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user1.id });
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); const response = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]);
expect(response).toHaveLength(1);
expect(initialSyncResponse).toHaveLength(1); expect(response).toEqual([
expect(initialSyncResponse).toEqual(
expect.arrayContaining([
{ {
ack: expect.any(String), ack: expect.any(String),
data: { data: {
@ -160,22 +127,18 @@ describe.concurrent(SyncEntityType.PartnerV1, () => {
}, },
type: 'PartnerV1', type: 'PartnerV1',
}, },
]), ]);
);
const acks = [initialSyncResponse[0].ack]; await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks });
const updated = await partnerRepo.update( const updated = await partnerRepo.update(
{ sharedById: partner.sharedById, sharedWithId: partner.sharedWithId }, { sharedById: partner.sharedById, sharedWithId: partner.sharedWithId },
{ inTimeline: true }, { inTimeline: true },
); );
const updatedSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnersV1]);
expect(newResponse).toHaveLength(1);
expect(updatedSyncResponse).toHaveLength(1); expect(newResponse).toEqual([
expect(updatedSyncResponse).toEqual(
expect.arrayContaining([
{ {
ack: expect.any(String), ack: expect.any(String),
data: { data: {
@ -185,37 +148,35 @@ describe.concurrent(SyncEntityType.PartnerV1, () => {
}, },
type: 'PartnerV1', 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 () => { 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 partnerRepo = ctx.get(PartnerRepository);
const user2 = await userRepo.create(mediumFactory.userInsert());
const user3 = await userRepo.create(mediumFactory.userInsert());
const partnerRepo = getRepository('partner'); const { user: user2 } = await ctx.newUser();
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user3.id }); const { user: user3 } = await ctx.newUser();
const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: user3.id });
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]);
await partnerRepo.remove(partner); await partnerRepo.remove(partner);
await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]);
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
}); });
it('should not sync a partner delete after a user is deleted', async () => { 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 userRepo = ctx.get(UserRepository);
const user2 = await userRepo.create(mediumFactory.userInsert());
const partnerRepo = getRepository('partner'); const { user: user2 } = await ctx.newUser();
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id }); await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
await userRepo.delete({ id: user2.id }, true); await userRepo.delete({ id: user2.id }, true);
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0); await expect(ctx.syncStream(auth, [SyncRequestType.PartnersV1])).resolves.toEqual([]);
}); });
}); });

View File

@ -1,36 +1,35 @@
import { Kysely } from 'kysely'; import { Kysely } from 'kysely';
import { DB } from 'src/db'; import { DB } from 'src/db';
import { SyncEntityType, SyncRequestType } from 'src/enum'; 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'; import { getKyselyDB } from 'test/utils';
let defaultDatabase: Kysely<DB>; let defaultDatabase: Kysely<DB>;
const setup = async (db?: Kysely<DB>) => { const setup = async (db?: Kysely<DB>) => {
const database = db || defaultDatabase; const ctx = new SyncTestContext(db || defaultDatabase);
const result = newSyncTest({ db: database }); const { auth, user, session } = await ctx.newSyncAuthUser();
const { auth, create } = newSyncAuthUser(); return { auth, user, session, ctx };
await create(database);
return { ...result, auth };
}; };
beforeAll(async () => { beforeAll(async () => {
defaultDatabase = await getKyselyDB(); defaultDatabase = await getKyselyDB();
}); });
describe.concurrent(SyncEntityType.UserV1, () => { describe(SyncEntityType.UserV1, () => {
it('should detect and sync the first user', async () => { 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 }); const user = await userRepo.get(auth.user.id, { withDeleted: false });
if (!user) { if (!user) {
expect.fail('First user should exist'); expect.fail('First user should exist');
} }
const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]); const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]);
expect(initialSyncResponse).toHaveLength(1); expect(response).toHaveLength(1);
expect(initialSyncResponse).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: { data: {
@ -43,21 +42,17 @@ describe.concurrent(SyncEntityType.UserV1, () => {
}, },
]); ]);
const acks = [initialSyncResponse[0].ack]; await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks }); await expect(ctx.syncStream(auth, [SyncRequestType.UsersV1])).resolves.toEqual([]);
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
expect(ackSyncResponse).toHaveLength(0);
}); });
it('should detect and sync a soft deleted user', async () => { 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 deletedAt = new Date().toISOString();
const deletedUser = mediumFactory.userInsert({ deletedAt }); const { user: deleted } = await ctx.newUser({ deletedAt });
const deleted = await getRepository('user').create(deletedUser);
const response = await testSync(auth, [SyncRequestType.UsersV1]); const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]);
expect(response).toHaveLength(2); expect(response).toHaveLength(2);
expect(response).toEqual( expect(response).toEqual(
@ -85,26 +80,21 @@ describe.concurrent(SyncEntityType.UserV1, () => {
]), ]),
); );
const acks = [response[1].ack]; await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks }); await expect(ctx.syncStream(auth, [SyncRequestType.UsersV1])).resolves.toEqual([]);
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
expect(ackSyncResponse).toHaveLength(0);
}); });
it('should detect and sync a deleted user', async () => { 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 userRepo = ctx.get(UserRepository);
const user = mediumFactory.userInsert();
await userRepo.create(user); const { user } = await ctx.newUser();
await userRepo.delete({ id: user.id }, true); 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).toHaveLength(2);
expect(response).toEqual( expect(response).toEqual([
expect.arrayContaining([
{ {
ack: expect.any(String), ack: expect.any(String),
data: { data: {
@ -122,24 +112,20 @@ describe.concurrent(SyncEntityType.UserV1, () => {
}, },
type: 'UserV1', type: 'UserV1',
}, },
]), ]);
);
const acks = response.map(({ ack }) => ack); await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks }); await expect(ctx.syncStream(auth, [SyncRequestType.UsersV1])).resolves.toEqual([]);
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
expect(ackSyncResponse).toHaveLength(0);
}); });
it('should sync a user and then an update to that same user', async () => { 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); const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]);
expect(initialSyncResponse).toEqual( expect(response).toHaveLength(1);
expect.arrayContaining([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: { data: {
@ -150,19 +136,15 @@ describe.concurrent(SyncEntityType.UserV1, () => {
}, },
type: 'UserV1', type: 'UserV1',
}, },
]), ]);
);
const acks = [initialSyncResponse[0].ack]; await ctx.syncAckAll(auth, response);
await sut.setAcks(auth, { acks });
const userRepo = getRepository('user');
const updated = await userRepo.update(auth.user.id, { name: 'new name' }); const updated = await userRepo.update(auth.user.id, { name: 'new name' });
const updatedSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
expect(updatedSyncResponse).toHaveLength(1); const newResponse = await ctx.syncStream(auth, [SyncRequestType.UsersV1]);
expect(updatedSyncResponse).toEqual( expect(newResponse).toHaveLength(1);
expect.arrayContaining([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: { data: {
@ -173,7 +155,6 @@ describe.concurrent(SyncEntityType.UserV1, () => {
}, },
type: 'UserV1', type: 'UserV1',
}, },
]), ]);
);
}); });
}); });