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