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 { getConfig, updateConfig } from 'src/utils/config';
export const BASE_SERVICE_DEPENDENCIES = [
LoggingRepository,
AccessRepository,
ActivityRepository,
AlbumRepository,
AlbumUserRepository,
ApiKeyRepository,
AssetRepository,
AssetJobRepository,
AuditRepository,
ConfigRepository,
CronRepository,
CryptoRepository,
DatabaseRepository,
DownloadRepository,
DuplicateRepository,
EmailRepository,
EventRepository,
JobRepository,
LibraryRepository,
MachineLearningRepository,
MapRepository,
MediaRepository,
MemoryRepository,
MetadataRepository,
MoveRepository,
NotificationRepository,
OAuthRepository,
PartnerRepository,
PersonRepository,
ProcessRepository,
SearchRepository,
ServerInfoRepository,
SessionRepository,
SharedLinkRepository,
StackRepository,
StorageRepository,
SyncRepository,
SystemMetadataRepository,
TagRepository,
TelemetryRepository,
TrashRepository,
UserRepository,
VersionHistoryRepository,
ViewRepository,
];
@Injectable()
export class BaseService {
protected storageCore: StorageCore;

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 { DateTime } from 'luxon';
import { createHash, randomBytes } from 'node:crypto';
import { Writable } from 'node:stream';
import { AssetFace } from 'src/database';
import { Albums, AssetJobStatus, Assets, DB, FaceSearch, Person, Sessions } from 'src/db';
import { Albums, AssetJobStatus, Assets, DB, Exif, FaceSearch, Person, Sessions } from 'src/db';
import { AuthDto } from 'src/dtos/auth.dto';
import { AssetType, AssetVisibility, SourceType, SyncRequestType } from 'src/enum';
import { AlbumUserRole, AssetType, AssetVisibility, SourceType, SyncRequestType } from 'src/enum';
import { AccessRepository } from 'src/repositories/access.repository';
import { ActivityRepository } from 'src/repositories/activity.repository';
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
@ -31,67 +31,147 @@ import { SystemMetadataRepository } from 'src/repositories/system-metadata.repos
import { UserRepository } from 'src/repositories/user.repository';
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
import { UserTable } from 'src/schema/tables/user.table';
import { BaseService } from 'src/services/base.service';
import { BASE_SERVICE_DEPENDENCIES, BaseService } from 'src/services/base.service';
import { SyncService } from 'src/services/sync.service';
import { RepositoryInterface } from 'src/types';
import { factory, newDate, newEmbedding, newUuid } from 'test/small.factory';
import { automock, ServiceOverrides, wait } from 'test/utils';
import { automock, wait } from 'test/utils';
import { Mocked } from 'vitest';
const sha256 = (value: string) => createHash('sha256').update(value).digest('base64');
interface ClassConstructor<T = any> extends Function {
new (...args: any[]): T;
}
// type Repositories = Omit<ServiceOverrides, 'access' | 'telemetry'>;
type RepositoriesTypes = {
access: AccessRepository;
activity: ActivityRepository;
album: AlbumRepository;
albumUser: AlbumUserRepository;
asset: AssetRepository;
assetJob: AssetJobRepository;
config: ConfigRepository;
crypto: CryptoRepository;
database: DatabaseRepository;
email: EmailRepository;
job: JobRepository;
user: UserRepository;
logger: LoggingRepository;
memory: MemoryRepository;
notification: NotificationRepository;
partner: PartnerRepository;
person: PersonRepository;
search: SearchRepository;
session: SessionRepository;
storage: StorageRepository;
sync: SyncRepository;
systemMetadata: SystemMetadataRepository;
versionHistory: VersionHistoryRepository;
};
type RepositoryMocks = { [K in keyof RepositoriesTypes]: Mocked<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 MediumTestOptions = {
mock: ClassConstructor<any>[];
real: ClassConstructor<any>[];
database: Kysely<DB>;
};
type ContextRepositories<R extends RepositoryOptions> = {
[K in keyof RepositoriesTypes as R[K] extends 'real' ? K : never]: RepositoriesTypes[K];
export const newMediumService = <S extends BaseService>(Service: ClassConstructor<S>, options: MediumTestOptions) => {
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;
mocks: ContextRepositoryMocks<R>;
repos: ContextRepositories<R>;
getRepository<T extends keyof RepositoriesTypes>(key: T): RepositoriesTypes[T];
};
database: Kysely<DB>;
export type SyncTestOptions = {
db: Kysely<DB>;
};
constructor(
Service: ClassConstructor<S>,
private options: MediumTestOptions,
) {
this.sutDeps = this.makeDeps(options);
this.sut = new Service(...this.sutDeps);
this.database = options.database;
}
export const newSyncAuthUser = () => {
const user = mediumFactory.userInsert();
const session = mediumFactory.sessionInsert({ userId: user.id });
private makeDeps(options: MediumTestOptions) {
const deps = BASE_SERVICE_DEPENDENCIES;
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({
session,
user: {
@ -105,224 +185,109 @@ export const newSyncAuthUser = () => {
auth,
session,
user,
create: async (db: Kysely<DB>) => {
await new UserRepository(db).create(user);
await new SessionRepository(db).create(session);
},
};
};
}
}
export const newSyncTest = (options: SyncTestOptions) => {
const { sut, mocks, repos, getRepository } = newMediumService(SyncService, {
database: options.db,
repos: {
sync: 'real',
session: 'real',
},
});
export class SyncTestContext extends MediumTestContext<SyncService> {
constructor(database: Kysely<DB>) {
super(SyncService, { database, real: [SyncRepository, SessionRepository], mock: [LoggingRepository] });
}
const testSync = async (auth: AuthDto, types: SyncRequestType[]) => {
async syncStream(auth: AuthDto, types: SyncRequestType[]) {
const stream = mediumFactory.syncStream();
// Wait for 2ms to ensure all updates are available and account for setTimeout inaccuracy
await wait(2);
await sut.stream(auth, stream, { types });
await this.sut.stream(auth, stream, { types });
return stream.getResponse();
};
return {
sut,
mocks,
repos,
getRepository,
testSync,
};
};
export const newMediumService = <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') {
const key = _key as keyof RepositoryMocks;
mocks[key] = getRepositoryMock(key) as any;
continue;
}
async syncAckAll(auth: AuthDto, response: Array<{ type: string; ack: string }>) {
const acks: Record<string, string> = {};
for (const { type, ack } of response) {
acks[type] = ack;
}
const makeRepository = <K extends keyof RepositoriesTypes>(key: K) => {
return repos[key] || getRepository(key, options.database);
};
await this.sut.setAcks(auth, { acks: Object.values(acks) });
}
}
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>) => {
const newRealRepository = <T>(key: ClassConstructor<T>, db: Kysely<DB>): T => {
switch (key) {
case 'access': {
return new AccessRepository(db);
case AccessRepository:
case AlbumRepository:
case AlbumUserRepository:
case ActivityRepository:
case AssetRepository:
case AssetJobRepository:
case MemoryRepository:
case NotificationRepository:
case PartnerRepository:
case PersonRepository:
case SearchRepository:
case SessionRepository:
case SyncRepository:
case SystemMetadataRepository:
case UserRepository:
case VersionHistoryRepository: {
return new key(db);
}
case 'activity': {
return new ActivityRepository(db);
case ConfigRepository:
case CryptoRepository: {
return new key();
}
case 'album': {
return new AlbumRepository(db);
case DatabaseRepository: {
return new key(db, LoggingRepository.create(), new ConfigRepository());
}
case 'albumUser': {
return new AlbumUserRepository(db);
case EmailRepository: {
return new key(LoggingRepository.create());
}
case 'asset': {
return new AssetRepository(db);
}
case 'assetJob': {
return new AssetJobRepository(db);
}
case 'config': {
return new ConfigRepository();
}
case 'crypto': {
return new CryptoRepository();
}
case 'database': {
return new DatabaseRepository(db, LoggingRepository.create(), new ConfigRepository());
}
case 'email': {
return new EmailRepository(LoggingRepository.create());
}
case 'logger': {
return LoggingRepository.create();
}
case 'memory': {
return new MemoryRepository(db);
}
case 'notification': {
return new NotificationRepository(db);
}
case 'partner': {
return new PartnerRepository(db);
}
case 'person': {
return new PersonRepository(db);
}
case 'search': {
return new SearchRepository(db);
}
case 'session': {
return new SessionRepository(db);
}
case 'sync': {
return new SyncRepository(db);
}
case 'systemMetadata': {
return new SystemMetadataRepository(db);
}
case 'user': {
return new UserRepository(db);
}
case 'versionHistory': {
return new VersionHistoryRepository(db);
case LoggingRepository as unknown as ClassConstructor<LoggingRepository>: {
return new key() as unknown as T;
}
default: {
throw new Error(`Invalid repository key: ${key}`);
throw new Error(`Unable to create repository instance for key: ${key?.name || key}`);
}
}
};
const getRepositoryMock = <K extends keyof RepositoryMocks>(key: K) => {
const newMockRepository = <T>(key: ClassConstructor<T>) => {
switch (key) {
case 'activity': {
return automock(ActivityRepository) as Mocked<RepositoryInterface<ActivityRepository>>;
case ActivityRepository:
case AlbumRepository:
case AssetRepository:
case AssetJobRepository:
case ConfigRepository:
case CryptoRepository:
case MemoryRepository:
case NotificationRepository:
case PartnerRepository:
case PersonRepository:
case SessionRepository:
case SyncRepository:
case SystemMetadataRepository:
case UserRepository:
case VersionHistoryRepository: {
return automock(key);
}
case 'album': {
return automock(AlbumRepository);
}
case 'asset': {
return automock(AssetRepository);
}
case 'assetJob': {
return automock(AssetJobRepository);
}
case 'config': {
return automock(ConfigRepository);
}
case 'crypto': {
return automock(CryptoRepository);
}
case 'database': {
case DatabaseRepository: {
return automock(DatabaseRepository, {
args: [
undefined,
{
setContext: () => {},
},
{ getEnv: () => ({ database: { vectorExtension: '' } }) },
],
args: [undefined, { setContext: () => {} }, { getEnv: () => ({ database: { vectorExtension: '' } }) }],
});
}
case 'email': {
return automock(EmailRepository, {
args: [
{
setContext: () => {},
},
],
});
case EmailRepository: {
return automock(EmailRepository, { args: [{ setContext: () => {} }] });
}
case 'job': {
case JobRepository: {
return automock(JobRepository, {
args: [
undefined,
@ -335,106 +300,21 @@ const getRepositoryMock = <K extends keyof RepositoryMocks>(key: K) => {
});
}
case 'logger': {
case LoggingRepository as unknown as ClassConstructor<T>: {
const configMock = { getEnv: () => ({ noColor: false }) };
return automock(LoggingRepository, { args: [undefined, configMock], strict: false });
}
case 'memory': {
return automock(MemoryRepository);
}
case 'notification': {
return automock(NotificationRepository);
}
case 'partner': {
return automock(PartnerRepository);
}
case 'person': {
return automock(PersonRepository);
}
case 'session': {
return automock(SessionRepository);
}
case 'storage': {
case StorageRepository: {
return automock(StorageRepository, { args: [{ setContext: () => {} }] });
}
case 'sync': {
return automock(SyncRepository);
}
case 'systemMetadata': {
return automock(SystemMetadataRepository);
}
case 'user': {
return automock(UserRepository);
}
case 'versionHistory': {
return automock(VersionHistoryRepository);
}
default: {
throw new Error(`Invalid repository key: ${key}`);
}
}
};
export const asDeps = (repositories: ServiceOverrides) => {
return [
repositories.logger || getRepositoryMock('logger'), // logger
repositories.access, // access
repositories.activity || getRepositoryMock('activity'),
repositories.album || getRepositoryMock('album'),
repositories.albumUser,
repositories.apiKey,
repositories.asset || getRepositoryMock('asset'),
repositories.assetJob || getRepositoryMock('assetJob'),
repositories.audit,
repositories.config || getRepositoryMock('config'),
repositories.cron,
repositories.crypto || getRepositoryMock('crypto'),
repositories.database || getRepositoryMock('database'),
repositories.downloadRepository,
repositories.duplicateRepository,
repositories.email || getRepositoryMock('email'),
repositories.event,
repositories.job || getRepositoryMock('job'),
repositories.library,
repositories.machineLearning,
repositories.map,
repositories.media,
repositories.memory || getRepositoryMock('memory'),
repositories.metadata,
repositories.move,
repositories.notification || getRepositoryMock('notification'),
repositories.oauth,
repositories.partner || getRepositoryMock('partner'),
repositories.person || getRepositoryMock('person'),
repositories.process,
repositories.search,
repositories.serverInfo,
repositories.session || getRepositoryMock('session'),
repositories.sharedLink,
repositories.stack,
repositories.storage || getRepositoryMock('storage'),
repositories.sync || getRepositoryMock('sync'),
repositories.systemMetadata || getRepositoryMock('systemMetadata'),
repositories.tag,
repositories.telemetry,
repositories.trash,
repositories.user,
repositories.versionHistory || getRepositoryMock('versionHistory'),
repositories.view,
];
};
const assetInsert = (asset: Partial<Insertable<Assets>> = {}) => {
const id = asset.id || newUuid();
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 defaults: Insertable<Sessions> = {
id,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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