mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
refactor: medium tests (#17634)
This commit is contained in:
parent
76db8cf604
commit
8da5f21fcf
@ -6,7 +6,6 @@ import { DB } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AssetEntity, searchAssetBuilder } from 'src/entities/asset.entity';
|
||||
import { AssetStatus, AssetType } from 'src/enum';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { anyUuid, asUuid } from 'src/utils/database';
|
||||
import { Paginated } from 'src/utils/pagination';
|
||||
import { isValidInteger } from 'src/validation';
|
||||
@ -203,12 +202,7 @@ export interface GetCameraMakesOptions {
|
||||
|
||||
@Injectable()
|
||||
export class SearchRepository {
|
||||
constructor(
|
||||
private logger: LoggingRepository,
|
||||
@InjectKysely() private db: Kysely<DB>,
|
||||
) {
|
||||
this.logger.setContext(SearchRepository.name);
|
||||
}
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({
|
||||
params: [
|
||||
|
@ -1,372 +0,0 @@
|
||||
import { Insertable, Kysely } from 'kysely';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { Writable } from 'node:stream';
|
||||
import { AssetFaces, Assets, DB, Person as DbPerson, FaceSearch, Partners, Sessions } from 'src/db';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AssetType, SourceType } from 'src/enum';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||
import { AlbumRepository } from 'src/repositories/album.repository';
|
||||
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||
import { AuditRepository } from 'src/repositories/audit.repository';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { LibraryRepository } from 'src/repositories/library.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { MachineLearningRepository } from 'src/repositories/machine-learning.repository';
|
||||
import { MediaRepository } from 'src/repositories/media.repository';
|
||||
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||
import { MoveRepository } from 'src/repositories/move.repository';
|
||||
import { NotificationRepository } from 'src/repositories/notification.repository';
|
||||
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { PersonRepository } from 'src/repositories/person.repository';
|
||||
import { ProcessRepository } from 'src/repositories/process.repository';
|
||||
import { SearchRepository } from 'src/repositories/search.repository';
|
||||
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
|
||||
import { SessionRepository } from 'src/repositories/session.repository';
|
||||
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
|
||||
import { StackRepository } from 'src/repositories/stack.repository';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||
import { SyncRepository } from 'src/repositories/sync.repository';
|
||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||
import { TelemetryRepository } from 'src/repositories/telemetry.repository';
|
||||
import { TrashRepository } from 'src/repositories/trash.repository';
|
||||
import { UserRepository } from 'src/repositories/user.repository';
|
||||
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
|
||||
import { ViewRepository } from 'src/repositories/view-repository';
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
import { newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock';
|
||||
import { newDate, newEmbedding, newUuid } from 'test/small.factory';
|
||||
import { automock } from 'test/utils';
|
||||
|
||||
class CustomWritable extends Writable {
|
||||
private data = '';
|
||||
|
||||
_write(chunk: any, encoding: string, callback: () => void) {
|
||||
this.data += chunk.toString();
|
||||
callback();
|
||||
}
|
||||
|
||||
getResponse() {
|
||||
const result = this.data;
|
||||
return result
|
||||
.split('\n')
|
||||
.filter((x) => x.length > 0)
|
||||
.map((x) => JSON.parse(x));
|
||||
}
|
||||
}
|
||||
|
||||
type Asset = Partial<Insertable<Assets>>;
|
||||
type User = Partial<Insertable<UserTable>>;
|
||||
type Session = Omit<Insertable<Sessions>, 'token'> & { token?: string };
|
||||
type Partner = Insertable<Partners>;
|
||||
type AssetFace = Partial<Insertable<AssetFaces>>;
|
||||
type Person = Partial<Insertable<DbPerson>>;
|
||||
type Face = Partial<Insertable<FaceSearch>>;
|
||||
|
||||
export class TestFactory {
|
||||
private assets: Asset[] = [];
|
||||
private sessions: Session[] = [];
|
||||
private users: User[] = [];
|
||||
private partners: Partner[] = [];
|
||||
private assetFaces: AssetFace[] = [];
|
||||
private persons: Person[] = [];
|
||||
private faces: Face[] = [];
|
||||
|
||||
private constructor(private context: TestContext) {}
|
||||
|
||||
static create(context: TestContext) {
|
||||
return new TestFactory(context);
|
||||
}
|
||||
|
||||
static stream() {
|
||||
return new CustomWritable();
|
||||
}
|
||||
|
||||
static asset(asset: Asset) {
|
||||
const assetId = asset.id || newUuid();
|
||||
const defaults: Insertable<Assets> = {
|
||||
deviceAssetId: '',
|
||||
deviceId: '',
|
||||
originalFileName: '',
|
||||
checksum: randomBytes(32),
|
||||
type: AssetType.IMAGE,
|
||||
originalPath: '/path/to/something.jpg',
|
||||
ownerId: '@immich.cloud',
|
||||
isVisible: true,
|
||||
fileCreatedAt: new Date('2000-01-01T00:00:00Z'),
|
||||
fileModifiedAt: new Date('2000-01-01T00:00:00Z'),
|
||||
localDateTime: new Date('2000-01-01T00:00:00Z'),
|
||||
};
|
||||
|
||||
return {
|
||||
...defaults,
|
||||
...asset,
|
||||
id: assetId,
|
||||
};
|
||||
}
|
||||
|
||||
static auth(auth: { user: User; session?: Session }) {
|
||||
return auth as AuthDto;
|
||||
}
|
||||
|
||||
static user(user: User = {}) {
|
||||
const userId = user.id || newUuid();
|
||||
const defaults: Insertable<UserTable> = {
|
||||
email: `${userId}@immich.cloud`,
|
||||
name: `User ${userId}`,
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
return {
|
||||
...defaults,
|
||||
...user,
|
||||
id: userId,
|
||||
};
|
||||
}
|
||||
|
||||
static session(session: Session) {
|
||||
const id = session.id || newUuid();
|
||||
const defaults = {
|
||||
token: randomBytes(36).toString('base64url'),
|
||||
};
|
||||
|
||||
return {
|
||||
...defaults,
|
||||
...session,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
static partner(partner: Partner) {
|
||||
const defaults = {
|
||||
inTimeline: true,
|
||||
};
|
||||
|
||||
return {
|
||||
...defaults,
|
||||
...partner,
|
||||
};
|
||||
}
|
||||
|
||||
static assetFace(assetFace: AssetFace) {
|
||||
const defaults = {
|
||||
assetId: assetFace.assetId || newUuid(),
|
||||
boundingBoxX1: assetFace.boundingBoxX1 || 0,
|
||||
boundingBoxX2: assetFace.boundingBoxX2 || 1,
|
||||
boundingBoxY1: assetFace.boundingBoxY1 || 0,
|
||||
boundingBoxY2: assetFace.boundingBoxY2 || 1,
|
||||
deletedAt: assetFace.deletedAt || null,
|
||||
id: assetFace.id || newUuid(),
|
||||
imageHeight: assetFace.imageHeight || 10,
|
||||
imageWidth: assetFace.imageWidth || 10,
|
||||
personId: assetFace.personId || null,
|
||||
sourceType: assetFace.sourceType || SourceType.MACHINE_LEARNING,
|
||||
};
|
||||
|
||||
return { ...defaults, ...assetFace };
|
||||
}
|
||||
|
||||
static person(person: Person) {
|
||||
const defaults = {
|
||||
birthDate: person.birthDate || null,
|
||||
color: person.color || null,
|
||||
createdAt: person.createdAt || newDate(),
|
||||
faceAssetId: person.faceAssetId || null,
|
||||
id: person.id || newUuid(),
|
||||
isFavorite: person.isFavorite || false,
|
||||
isHidden: person.isHidden || false,
|
||||
name: person.name || 'Test Name',
|
||||
ownerId: person.ownerId || newUuid(),
|
||||
thumbnailPath: person.thumbnailPath || '/path/to/thumbnail.jpg',
|
||||
updatedAt: person.updatedAt || newDate(),
|
||||
updateId: person.updateId || newUuid(),
|
||||
};
|
||||
return { ...defaults, ...person };
|
||||
}
|
||||
|
||||
static face(face: Face) {
|
||||
const defaults = {
|
||||
faceId: face.faceId || newUuid(),
|
||||
embedding: face.embedding || newEmbedding(),
|
||||
};
|
||||
return {
|
||||
...defaults,
|
||||
...face,
|
||||
};
|
||||
}
|
||||
|
||||
withAsset(asset: Asset) {
|
||||
this.assets.push(asset);
|
||||
return this;
|
||||
}
|
||||
|
||||
withSession(session: Session) {
|
||||
this.sessions.push(session);
|
||||
return this;
|
||||
}
|
||||
|
||||
withUser(user: User = {}) {
|
||||
this.users.push(user);
|
||||
return this;
|
||||
}
|
||||
|
||||
withPartner(partner: Partner) {
|
||||
this.partners.push(partner);
|
||||
return this;
|
||||
}
|
||||
|
||||
withAssetFace(assetFace: AssetFace) {
|
||||
this.assetFaces.push(assetFace);
|
||||
return this;
|
||||
}
|
||||
|
||||
withPerson(person: Person) {
|
||||
this.persons.push(person);
|
||||
return this;
|
||||
}
|
||||
|
||||
withFaces(face: Face) {
|
||||
this.faces.push(face);
|
||||
return this;
|
||||
}
|
||||
|
||||
async create() {
|
||||
for (const user of this.users) {
|
||||
await this.context.createUser(user);
|
||||
}
|
||||
|
||||
for (const partner of this.partners) {
|
||||
await this.context.createPartner(partner);
|
||||
}
|
||||
|
||||
for (const session of this.sessions) {
|
||||
await this.context.createSession(session);
|
||||
}
|
||||
|
||||
for (const asset of this.assets) {
|
||||
await this.context.createAsset(asset);
|
||||
}
|
||||
|
||||
for (const person of this.persons) {
|
||||
await this.context.createPerson(person);
|
||||
}
|
||||
|
||||
await this.context.refreshFaces(
|
||||
this.assetFaces,
|
||||
[],
|
||||
this.faces.map((f) => TestFactory.face(f)),
|
||||
);
|
||||
|
||||
return this.context;
|
||||
}
|
||||
}
|
||||
|
||||
export class TestContext {
|
||||
access: AccessRepository;
|
||||
logger: LoggingRepository;
|
||||
activity: ActivityRepository;
|
||||
album: AlbumRepository;
|
||||
apiKey: ApiKeyRepository;
|
||||
asset: AssetRepository;
|
||||
audit: AuditRepository;
|
||||
config: ConfigRepository;
|
||||
library: LibraryRepository;
|
||||
machineLearning: MachineLearningRepository;
|
||||
media: MediaRepository;
|
||||
metadata: MetadataRepository;
|
||||
move: MoveRepository;
|
||||
notification: NotificationRepository;
|
||||
oauth: OAuthRepository;
|
||||
partner: PartnerRepository;
|
||||
person: PersonRepository;
|
||||
process: ProcessRepository;
|
||||
search: SearchRepository;
|
||||
serverInfo: ServerInfoRepository;
|
||||
session: SessionRepository;
|
||||
sharedLink: SharedLinkRepository;
|
||||
stack: StackRepository;
|
||||
storage: StorageRepository;
|
||||
systemMetadata: SystemMetadataRepository;
|
||||
sync: SyncRepository;
|
||||
telemetry: TelemetryRepository;
|
||||
trash: TrashRepository;
|
||||
user: UserRepository;
|
||||
versionHistory: VersionHistoryRepository;
|
||||
view: ViewRepository;
|
||||
|
||||
private constructor(public db: Kysely<DB>) {
|
||||
// eslint-disable-next-line no-sparse-arrays
|
||||
const logger = automock(LoggingRepository, { args: [, { getEnv: () => ({}) }], strict: false });
|
||||
const config = new ConfigRepository();
|
||||
|
||||
this.access = new AccessRepository(this.db);
|
||||
this.logger = logger;
|
||||
this.activity = new ActivityRepository(this.db);
|
||||
this.album = new AlbumRepository(this.db);
|
||||
this.apiKey = new ApiKeyRepository(this.db);
|
||||
this.asset = new AssetRepository(this.db);
|
||||
this.audit = new AuditRepository(this.db);
|
||||
this.config = config;
|
||||
this.library = new LibraryRepository(this.db);
|
||||
this.machineLearning = new MachineLearningRepository(logger);
|
||||
this.media = new MediaRepository(logger);
|
||||
this.metadata = new MetadataRepository(logger);
|
||||
this.move = new MoveRepository(this.db);
|
||||
this.notification = new NotificationRepository(logger);
|
||||
this.oauth = new OAuthRepository(logger);
|
||||
this.partner = new PartnerRepository(this.db);
|
||||
this.person = new PersonRepository(this.db);
|
||||
this.process = new ProcessRepository();
|
||||
this.search = new SearchRepository(logger, this.db);
|
||||
this.serverInfo = new ServerInfoRepository(config, logger);
|
||||
this.session = new SessionRepository(this.db);
|
||||
this.sharedLink = new SharedLinkRepository(this.db);
|
||||
this.stack = new StackRepository(this.db);
|
||||
this.storage = new StorageRepository(logger);
|
||||
this.sync = new SyncRepository(this.db);
|
||||
this.systemMetadata = new SystemMetadataRepository(this.db);
|
||||
this.telemetry = newTelemetryRepositoryMock() as unknown as TelemetryRepository;
|
||||
this.trash = new TrashRepository(this.db);
|
||||
this.user = new UserRepository(this.db);
|
||||
this.versionHistory = new VersionHistoryRepository(this.db);
|
||||
this.view = new ViewRepository(this.db);
|
||||
}
|
||||
|
||||
static from(db: Kysely<DB>) {
|
||||
return new TestContext(db).getFactory();
|
||||
}
|
||||
|
||||
getFactory() {
|
||||
return TestFactory.create(this);
|
||||
}
|
||||
|
||||
createUser(user: User = {}) {
|
||||
return this.user.create(TestFactory.user(user));
|
||||
}
|
||||
|
||||
createPartner(partner: Partner) {
|
||||
return this.partner.create(TestFactory.partner(partner));
|
||||
}
|
||||
|
||||
createAsset(asset: Asset) {
|
||||
return this.asset.create(TestFactory.asset(asset));
|
||||
}
|
||||
|
||||
createSession(session: Session) {
|
||||
return this.session.create(TestFactory.session(session));
|
||||
}
|
||||
|
||||
createPerson(person: Person) {
|
||||
return this.person.create(TestFactory.person(person));
|
||||
}
|
||||
|
||||
refreshFaces(facesToAdd: AssetFace[], faceIdsToRemove: string[], embeddingsToAdd?: Insertable<FaceSearch>[]) {
|
||||
return this.person.refreshFaces(
|
||||
facesToAdd.map((f) => TestFactory.assetFace(f)),
|
||||
faceIdsToRemove,
|
||||
embeddingsToAdd,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
import { ClassConstructor } from 'class-transformer';
|
||||
import { Insertable, Kysely } from 'kysely';
|
||||
import { DateTime } from 'luxon';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { AssetJobStatus, Assets, DB } from 'src/db';
|
||||
import { AssetType } from 'src/enum';
|
||||
import { createHash, randomBytes } from 'node:crypto';
|
||||
import { Writable } from 'node:stream';
|
||||
import { AssetFace } from 'src/database';
|
||||
import { AssetJobStatus, Assets, DB, FaceSearch, Person, Sessions } from 'src/db';
|
||||
import { AssetType, SourceType } from 'src/enum';
|
||||
import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||
import { AlbumRepository } from 'src/repositories/album.repository';
|
||||
import { AssetJobRepository } from 'src/repositories/asset-job.repository';
|
||||
@ -15,17 +17,22 @@ import { JobRepository } from 'src/repositories/job.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { PersonRepository } from 'src/repositories/person.repository';
|
||||
import { SearchRepository } from 'src/repositories/search.repository';
|
||||
import { SessionRepository } from 'src/repositories/session.repository';
|
||||
import { SyncRepository } from 'src/repositories/sync.repository';
|
||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||
import { UserRepository } from 'src/repositories/user.repository';
|
||||
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { newDate, newUuid } from 'test/small.factory';
|
||||
import { newDate, newEmbedding, newUuid } from 'test/small.factory';
|
||||
import { automock, ServiceOverrides } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
const sha256 = (value: string) => createHash('sha256').update(value).digest('base64');
|
||||
|
||||
// type Repositories = Omit<ServiceOverrides, 'access' | 'telemetry'>;
|
||||
type Repositories = {
|
||||
activity: ActivityRepository;
|
||||
@ -40,7 +47,10 @@ type Repositories = {
|
||||
logger: LoggingRepository;
|
||||
memory: MemoryRepository;
|
||||
partner: PartnerRepository;
|
||||
person: PersonRepository;
|
||||
search: SearchRepository;
|
||||
session: SessionRepository;
|
||||
sync: SyncRepository;
|
||||
systemMetadata: SystemMetadataRepository;
|
||||
versionHistory: VersionHistoryRepository;
|
||||
};
|
||||
@ -145,10 +155,22 @@ export const getRepository = <K extends keyof Repositories>(key: K, db: Kysely<D
|
||||
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);
|
||||
}
|
||||
@ -216,10 +238,18 @@ const getRepositoryMock = <K extends keyof Repositories>(key: K) => {
|
||||
return automock(PartnerRepository);
|
||||
}
|
||||
|
||||
case 'person': {
|
||||
return automock(PersonRepository);
|
||||
}
|
||||
|
||||
case 'session': {
|
||||
return automock(SessionRepository);
|
||||
}
|
||||
|
||||
case 'sync': {
|
||||
return automock(SyncRepository);
|
||||
}
|
||||
|
||||
case 'systemMetadata': {
|
||||
return automock(SystemMetadataRepository);
|
||||
}
|
||||
@ -266,7 +296,7 @@ export const asDeps = (repositories: ServiceOverrides) => {
|
||||
repositories.notification,
|
||||
repositories.oauth,
|
||||
repositories.partner || getRepositoryMock('partner'),
|
||||
repositories.person,
|
||||
repositories.person || getRepositoryMock('person'),
|
||||
repositories.process,
|
||||
repositories.search,
|
||||
repositories.serverInfo,
|
||||
@ -274,7 +304,7 @@ export const asDeps = (repositories: ServiceOverrides) => {
|
||||
repositories.sharedLink,
|
||||
repositories.stack,
|
||||
repositories.storage,
|
||||
repositories.sync,
|
||||
repositories.sync || getRepositoryMock('sync'),
|
||||
repositories.systemMetadata || getRepositoryMock('systemMetadata'),
|
||||
repositories.tag,
|
||||
repositories.telemetry,
|
||||
@ -297,6 +327,7 @@ const assetInsert = (asset: Partial<Insertable<Assets>> = {}) => {
|
||||
originalPath: '/path/to/something.jpg',
|
||||
ownerId: '@immich.cloud',
|
||||
isVisible: true,
|
||||
isFavorite: false,
|
||||
fileCreatedAt: now,
|
||||
fileModifiedAt: now,
|
||||
localDateTime: now,
|
||||
@ -309,6 +340,38 @@ const assetInsert = (asset: Partial<Insertable<Assets>> = {}) => {
|
||||
};
|
||||
};
|
||||
|
||||
const faceInsert = (face: Partial<Insertable<FaceSearch>> & { faceId: string }) => {
|
||||
const defaults = {
|
||||
faceId: face.faceId,
|
||||
embedding: face.embedding || newEmbedding(),
|
||||
};
|
||||
return {
|
||||
...defaults,
|
||||
...face,
|
||||
};
|
||||
};
|
||||
|
||||
const assetFaceInsert = (assetFace: Partial<AssetFace> & { assetId: string }) => {
|
||||
const defaults = {
|
||||
assetId: assetFace.assetId ?? newUuid(),
|
||||
boundingBoxX1: assetFace.boundingBoxX1 ?? 0,
|
||||
boundingBoxX2: assetFace.boundingBoxX2 ?? 1,
|
||||
boundingBoxY1: assetFace.boundingBoxY1 ?? 0,
|
||||
boundingBoxY2: assetFace.boundingBoxY2 ?? 1,
|
||||
deletedAt: assetFace.deletedAt ?? null,
|
||||
id: assetFace.id ?? newUuid(),
|
||||
imageHeight: assetFace.imageHeight ?? 10,
|
||||
imageWidth: assetFace.imageWidth ?? 10,
|
||||
personId: assetFace.personId ?? null,
|
||||
sourceType: assetFace.sourceType ?? SourceType.MACHINE_LEARNING,
|
||||
};
|
||||
|
||||
return {
|
||||
...defaults,
|
||||
...assetFace,
|
||||
};
|
||||
};
|
||||
|
||||
const assetJobStatusInsert = (
|
||||
job: Partial<Insertable<AssetJobStatus>> & { assetId: string },
|
||||
): Insertable<AssetJobStatus> => {
|
||||
@ -327,6 +390,41 @@ const assetJobStatusInsert = (
|
||||
};
|
||||
};
|
||||
|
||||
const personInsert = (person: Partial<Insertable<Person>> & { ownerId: string }) => {
|
||||
const defaults = {
|
||||
birthDate: person.birthDate || null,
|
||||
color: person.color || null,
|
||||
createdAt: person.createdAt || newDate(),
|
||||
faceAssetId: person.faceAssetId || null,
|
||||
id: person.id || newUuid(),
|
||||
isFavorite: person.isFavorite || false,
|
||||
isHidden: person.isHidden || false,
|
||||
name: person.name || 'Test Name',
|
||||
ownerId: person.ownerId || newUuid(),
|
||||
thumbnailPath: person.thumbnailPath || '/path/to/thumbnail.jpg',
|
||||
updatedAt: person.updatedAt || newDate(),
|
||||
updateId: person.updateId || newUuid(),
|
||||
};
|
||||
return {
|
||||
...defaults,
|
||||
...person,
|
||||
};
|
||||
};
|
||||
|
||||
const sessionInsert = ({ id = newUuid(), userId, ...session }: Partial<Insertable<Sessions>> & { userId: string }) => {
|
||||
const defaults: Insertable<Sessions> = {
|
||||
id,
|
||||
userId,
|
||||
token: sha256(id),
|
||||
};
|
||||
|
||||
return {
|
||||
...defaults,
|
||||
...session,
|
||||
id,
|
||||
};
|
||||
};
|
||||
|
||||
const userInsert = (user: Partial<Insertable<UserTable>> = {}) => {
|
||||
const id = user.id || newUuid();
|
||||
|
||||
@ -339,8 +437,34 @@ const userInsert = (user: Partial<Insertable<UserTable>> = {}) => {
|
||||
return { ...defaults, ...user, id };
|
||||
};
|
||||
|
||||
class CustomWritable extends Writable {
|
||||
private data = '';
|
||||
|
||||
_write(chunk: any, encoding: string, callback: () => void) {
|
||||
this.data += chunk.toString();
|
||||
callback();
|
||||
}
|
||||
|
||||
getResponse() {
|
||||
const result = this.data;
|
||||
return result
|
||||
.split('\n')
|
||||
.filter((x) => x.length > 0)
|
||||
.map((x) => JSON.parse(x));
|
||||
}
|
||||
}
|
||||
|
||||
const syncStream = () => {
|
||||
return new CustomWritable();
|
||||
};
|
||||
|
||||
export const mediumFactory = {
|
||||
assetInsert,
|
||||
assetFaceInsert,
|
||||
assetJobStatusInsert,
|
||||
faceInsert,
|
||||
personInsert,
|
||||
sessionInsert,
|
||||
syncStream,
|
||||
userInsert,
|
||||
};
|
||||
|
@ -1,201 +0,0 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { JobStatus, SourceType } from 'src/enum';
|
||||
import { PersonService } from 'src/services/person.service';
|
||||
import { TestContext, TestFactory } from 'test/factory';
|
||||
import { newEmbedding } from 'test/small.factory';
|
||||
import { getKyselyDB, newTestService } from 'test/utils';
|
||||
|
||||
const setup = async (db: Kysely<any>) => {
|
||||
const context = await TestContext.from(db).create();
|
||||
const { sut, mocks } = newTestService(PersonService, context);
|
||||
|
||||
return { sut, mocks, context };
|
||||
};
|
||||
|
||||
describe.concurrent(PersonService.name, () => {
|
||||
let sut: PersonService;
|
||||
let context: TestContext;
|
||||
|
||||
beforeAll(async () => {
|
||||
({ sut, context } = await setup(await getKyselyDB()));
|
||||
});
|
||||
|
||||
describe('handleRecognizeFaces', () => {
|
||||
it('should skip if face source type is not MACHINE_LEARNING', async () => {
|
||||
const user = TestFactory.user();
|
||||
const asset = TestFactory.asset({ ownerId: user.id });
|
||||
const assetFace = TestFactory.assetFace({ assetId: asset.id, sourceType: SourceType.MANUAL });
|
||||
const face = TestFactory.face({ faceId: assetFace.id });
|
||||
await context.getFactory().withUser(user).withAsset(asset).withAssetFace(assetFace).withFaces(face).create();
|
||||
|
||||
const result = await sut.handleRecognizeFaces({ id: assetFace.id, deferred: false });
|
||||
|
||||
expect(result).toBe(JobStatus.SKIPPED);
|
||||
const newPersonId = await context.db
|
||||
.selectFrom('asset_faces')
|
||||
.select('asset_faces.personId')
|
||||
.where('asset_faces.id', '=', assetFace.id)
|
||||
.executeTakeFirst();
|
||||
expect(newPersonId?.personId).toBeNull();
|
||||
});
|
||||
|
||||
it('should fail if face does not have an embedding', async () => {
|
||||
const user = TestFactory.user();
|
||||
const asset = TestFactory.asset({ ownerId: user.id });
|
||||
const assetFace = TestFactory.assetFace({ assetId: asset.id, sourceType: SourceType.MACHINE_LEARNING });
|
||||
await context.getFactory().withUser(user).withAsset(asset).withAssetFace(assetFace).create();
|
||||
|
||||
const result = await sut.handleRecognizeFaces({ id: assetFace.id, deferred: false });
|
||||
|
||||
expect(result).toBe(JobStatus.FAILED);
|
||||
const newPersonId = await context.db
|
||||
.selectFrom('asset_faces')
|
||||
.select('asset_faces.personId')
|
||||
.where('asset_faces.id', '=', assetFace.id)
|
||||
.executeTakeFirst();
|
||||
expect(newPersonId?.personId).toBeNull();
|
||||
});
|
||||
|
||||
it('should skip if face already has a person assigned', async () => {
|
||||
const user = TestFactory.user();
|
||||
const asset = TestFactory.asset({ ownerId: user.id });
|
||||
const person = TestFactory.person({ ownerId: user.id });
|
||||
const assetFace = TestFactory.assetFace({
|
||||
assetId: asset.id,
|
||||
sourceType: SourceType.MACHINE_LEARNING,
|
||||
personId: person.id,
|
||||
});
|
||||
const face = TestFactory.face({ faceId: assetFace.id });
|
||||
await context
|
||||
.getFactory()
|
||||
.withUser(user)
|
||||
.withAsset(asset)
|
||||
.withPerson(person)
|
||||
.withAssetFace(assetFace)
|
||||
.withFaces(face)
|
||||
.create();
|
||||
|
||||
const result = await sut.handleRecognizeFaces({ id: assetFace.id, deferred: false });
|
||||
|
||||
expect(result).toBe(JobStatus.SKIPPED);
|
||||
const newPersonId = await context.db
|
||||
.selectFrom('asset_faces')
|
||||
.select('asset_faces.personId')
|
||||
.where('asset_faces.id', '=', assetFace.id)
|
||||
.executeTakeFirst();
|
||||
expect(newPersonId?.personId).toEqual(person.id);
|
||||
});
|
||||
|
||||
it('should create a new person if no matches are found', async () => {
|
||||
const user = TestFactory.user();
|
||||
const embedding = newEmbedding();
|
||||
|
||||
let factory = context.getFactory().withUser(user);
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const existingAsset = TestFactory.asset({ ownerId: user.id });
|
||||
const existingAssetFace = TestFactory.assetFace({
|
||||
assetId: existingAsset.id,
|
||||
sourceType: SourceType.MACHINE_LEARNING,
|
||||
});
|
||||
const existingFace = TestFactory.face({ faceId: existingAssetFace.id, embedding });
|
||||
factory = factory.withAsset(existingAsset).withAssetFace(existingAssetFace).withFaces(existingFace);
|
||||
}
|
||||
|
||||
const newAsset = TestFactory.asset({ ownerId: user.id });
|
||||
const newAssetFace = TestFactory.assetFace({ assetId: newAsset.id, sourceType: SourceType.MACHINE_LEARNING });
|
||||
const newFace = TestFactory.face({ faceId: newAssetFace.id, embedding });
|
||||
|
||||
await factory.withAsset(newAsset).withAssetFace(newAssetFace).withFaces(newFace).create();
|
||||
|
||||
const result = await sut.handleRecognizeFaces({ id: newAssetFace.id, deferred: false });
|
||||
|
||||
expect(result).toBe(JobStatus.SUCCESS);
|
||||
|
||||
const newPersonId = await context.db
|
||||
.selectFrom('asset_faces')
|
||||
.select('asset_faces.personId')
|
||||
.where('asset_faces.id', '=', newAssetFace.id)
|
||||
.executeTakeFirstOrThrow();
|
||||
expect(newPersonId.personId).toBeDefined();
|
||||
});
|
||||
|
||||
it('should assign face to an existing person if matches are found', async () => {
|
||||
const user = TestFactory.user();
|
||||
const existingPerson = TestFactory.person({ ownerId: user.id });
|
||||
const embedding = newEmbedding();
|
||||
|
||||
let factory = context.getFactory().withUser(user).withPerson(existingPerson);
|
||||
|
||||
const assetFaces: string[] = [];
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const existingAsset = TestFactory.asset({ ownerId: user.id });
|
||||
const existingAssetFace = TestFactory.assetFace({
|
||||
assetId: existingAsset.id,
|
||||
sourceType: SourceType.MACHINE_LEARNING,
|
||||
});
|
||||
assetFaces.push(existingAssetFace.id);
|
||||
const existingFace = TestFactory.face({ faceId: existingAssetFace.id, embedding });
|
||||
factory = factory.withAsset(existingAsset).withAssetFace(existingAssetFace).withFaces(existingFace);
|
||||
}
|
||||
|
||||
const newAsset = TestFactory.asset({ ownerId: user.id });
|
||||
const newAssetFace = TestFactory.assetFace({ assetId: newAsset.id, sourceType: SourceType.MACHINE_LEARNING });
|
||||
const newFace = TestFactory.face({ faceId: newAssetFace.id, embedding });
|
||||
await factory.withAsset(newAsset).withAssetFace(newAssetFace).withFaces(newFace).create();
|
||||
await context.person.reassignFaces({ newPersonId: existingPerson.id, faceIds: assetFaces });
|
||||
|
||||
const result = await sut.handleRecognizeFaces({ id: newAssetFace.id, deferred: false });
|
||||
|
||||
expect(result).toBe(JobStatus.SUCCESS);
|
||||
|
||||
const after = await context.db
|
||||
.selectFrom('asset_faces')
|
||||
.select('asset_faces.personId')
|
||||
.where('asset_faces.id', '=', newAssetFace.id)
|
||||
.executeTakeFirstOrThrow();
|
||||
expect(after.personId).toEqual(existingPerson.id);
|
||||
});
|
||||
|
||||
it('should not assign face to an existing person if asset is older than person', async () => {
|
||||
const user = TestFactory.user();
|
||||
const assetCreatedAt = new Date('2020-02-23T05:06:29.716Z');
|
||||
const birthDate = new Date(assetCreatedAt.getTime() + 3600 * 1000 * 365);
|
||||
const existingPerson = TestFactory.person({ ownerId: user.id, birthDate });
|
||||
const embedding = newEmbedding();
|
||||
|
||||
let factory = context.getFactory().withUser(user).withPerson(existingPerson);
|
||||
|
||||
const assetFaces: string[] = [];
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const existingAsset = TestFactory.asset({ ownerId: user.id });
|
||||
const existingAssetFace = TestFactory.assetFace({
|
||||
assetId: existingAsset.id,
|
||||
sourceType: SourceType.MACHINE_LEARNING,
|
||||
});
|
||||
assetFaces.push(existingAssetFace.id);
|
||||
const existingFace = TestFactory.face({ faceId: existingAssetFace.id, embedding });
|
||||
factory = factory.withAsset(existingAsset).withAssetFace(existingAssetFace).withFaces(existingFace);
|
||||
}
|
||||
|
||||
const newAsset = TestFactory.asset({ ownerId: user.id, fileCreatedAt: assetCreatedAt });
|
||||
const newAssetFace = TestFactory.assetFace({ assetId: newAsset.id, sourceType: SourceType.MACHINE_LEARNING });
|
||||
const newFace = TestFactory.face({ faceId: newAssetFace.id, embedding });
|
||||
await factory.withAsset(newAsset).withAssetFace(newAssetFace).withFaces(newFace).create();
|
||||
await context.person.reassignFaces({ newPersonId: existingPerson.id, faceIds: assetFaces });
|
||||
|
||||
const result = await sut.handleRecognizeFaces({ id: newAssetFace.id, deferred: false });
|
||||
|
||||
expect(result).toBe(JobStatus.SKIPPED);
|
||||
|
||||
const after = await context.db
|
||||
.selectFrom('asset_faces')
|
||||
.select('asset_faces.personId')
|
||||
.where('asset_faces.id', '=', newAssetFace.id)
|
||||
.executeTakeFirstOrThrow();
|
||||
expect(after.personId).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,22 +1,37 @@
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { SYNC_TYPES_ORDER, SyncService } from 'src/services/sync.service';
|
||||
import { TestContext, TestFactory } from 'test/factory';
|
||||
import { getKyselyDB, newTestService } from 'test/utils';
|
||||
import { mediumFactory, newMediumService } from 'test/medium.factory';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
const setup = async () => {
|
||||
const user = TestFactory.user();
|
||||
const session = TestFactory.session({ userId: user.id });
|
||||
const auth = TestFactory.auth({ session, user });
|
||||
|
||||
const db = await getKyselyDB();
|
||||
|
||||
const context = await TestContext.from(db).withUser(user).withSession(session).create();
|
||||
const { sut, mocks, repos, getRepository } = newMediumService(SyncService, {
|
||||
database: db,
|
||||
repos: {
|
||||
sync: 'real',
|
||||
session: 'real',
|
||||
},
|
||||
});
|
||||
|
||||
const { sut } = newTestService(SyncService, context);
|
||||
const user = mediumFactory.userInsert();
|
||||
const session = mediumFactory.sessionInsert({ userId: user.id });
|
||||
const auth = factory.auth({
|
||||
session,
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
},
|
||||
});
|
||||
|
||||
await getRepository('user').create(user);
|
||||
await getRepository('session').create(session);
|
||||
|
||||
const testSync = async (auth: AuthDto, types: SyncRequestType[]) => {
|
||||
const stream = TestFactory.stream();
|
||||
const stream = mediumFactory.syncStream();
|
||||
// Wait for 1ms to ensure all updates are available
|
||||
await new Promise((resolve) => setTimeout(resolve, 1));
|
||||
await sut.stream(auth, stream, { types });
|
||||
@ -25,9 +40,11 @@ const setup = async () => {
|
||||
};
|
||||
|
||||
return {
|
||||
auth,
|
||||
context,
|
||||
sut,
|
||||
auth,
|
||||
mocks,
|
||||
repos,
|
||||
getRepository,
|
||||
testSync,
|
||||
};
|
||||
};
|
||||
@ -43,9 +60,10 @@ describe(SyncService.name, () => {
|
||||
|
||||
describe.concurrent(SyncEntityType.UserV1, () => {
|
||||
it('should detect and sync the first user', async () => {
|
||||
const { context, auth, sut, testSync } = await setup();
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const user = await context.user.get(auth.user.id, { withDeleted: false });
|
||||
const userRepo = getRepository('user');
|
||||
const user = await userRepo.get(auth.user.id, { withDeleted: false });
|
||||
if (!user) {
|
||||
expect.fail('First user should exist');
|
||||
}
|
||||
@ -73,10 +91,11 @@ describe(SyncService.name, () => {
|
||||
});
|
||||
|
||||
it('should detect and sync a soft deleted user', async () => {
|
||||
const { auth, context, sut, testSync } = await setup();
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const deletedAt = new Date().toISOString();
|
||||
const deleted = await context.createUser({ deletedAt });
|
||||
const deletedUser = mediumFactory.userInsert({ deletedAt });
|
||||
const deleted = await getRepository('user').create(deletedUser);
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
@ -114,10 +133,12 @@ describe(SyncService.name, () => {
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted user', async () => {
|
||||
const { auth, context, sut, testSync } = await setup();
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const user = await context.createUser();
|
||||
await context.user.delete({ id: user.id }, true);
|
||||
const userRepo = getRepository('user');
|
||||
const user = mediumFactory.userInsert();
|
||||
await userRepo.create(user);
|
||||
await userRepo.delete({ id: user.id }, true);
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
@ -152,7 +173,7 @@ describe(SyncService.name, () => {
|
||||
});
|
||||
|
||||
it('should sync a user and then an update to that same user', async () => {
|
||||
const { auth, context, sut, testSync } = await setup();
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
@ -175,8 +196,8 @@ describe(SyncService.name, () => {
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const updated = await context.user.update(auth.user.id, { name: 'new name' });
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const updated = await userRepo.update(auth.user.id, { name: 'new name' });
|
||||
const updatedSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
expect(updatedSyncResponse).toHaveLength(1);
|
||||
@ -199,12 +220,16 @@ describe(SyncService.name, () => {
|
||||
|
||||
describe.concurrent(SyncEntityType.PartnerV1, () => {
|
||||
it('should detect and sync the first partner', async () => {
|
||||
const { auth, context, sut, testSync } = await setup();
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const user1 = auth.user;
|
||||
const user2 = await context.createUser();
|
||||
const userRepo = getRepository('user');
|
||||
const partnerRepo = getRepository('partner');
|
||||
|
||||
const partner = await context.createPartner({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
@ -232,13 +257,16 @@ describe(SyncService.name, () => {
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted partner', async () => {
|
||||
const { auth, context, sut, testSync } = await setup();
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user1 = auth.user;
|
||||
const user2 = await context.createUser();
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const partner = await context.createPartner({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
await context.partner.remove(partner);
|
||||
const partnerRepo = getRepository('partner');
|
||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
await partnerRepo.remove(partner);
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
@ -265,13 +293,15 @@ describe(SyncService.name, () => {
|
||||
});
|
||||
|
||||
it('should detect and sync a partner share both to and from another user', async () => {
|
||||
const { auth, context, sut, testSync } = await setup();
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user1 = auth.user;
|
||||
const user2 = await context.createUser();
|
||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||
|
||||
const partner1 = await context.createPartner({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
const partner2 = await context.createPartner({ 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]);
|
||||
|
||||
@ -307,12 +337,14 @@ describe(SyncService.name, () => {
|
||||
});
|
||||
|
||||
it('should sync a partner and then an update to that same partner', async () => {
|
||||
const { auth, context, sut, testSync } = await setup();
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user1 = auth.user;
|
||||
const user2 = await context.createUser();
|
||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||
|
||||
const partner = await context.createPartner({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
const partnerRepo = getRepository('partner');
|
||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
@ -334,7 +366,7 @@ describe(SyncService.name, () => {
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const updated = await context.partner.update(
|
||||
const updated = await partnerRepo.update(
|
||||
{ sharedById: partner.sharedById, sharedWithId: partner.sharedWithId },
|
||||
{ inTimeline: true },
|
||||
);
|
||||
@ -358,26 +390,31 @@ describe(SyncService.name, () => {
|
||||
});
|
||||
|
||||
it('should not sync a partner or partner delete for an unrelated user', async () => {
|
||||
const { auth, context, testSync } = await setup();
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const user2 = await context.createUser();
|
||||
const user3 = await context.createUser();
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||
const user3 = await userRepo.create(mediumFactory.userInsert());
|
||||
|
||||
await context.createPartner({ sharedById: user2.id, sharedWithId: user3.id });
|
||||
const partnerRepo = getRepository('partner');
|
||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user3.id });
|
||||
|
||||
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
||||
|
||||
await context.partner.remove({ sharedById: user2.id, sharedWithId: user3.id });
|
||||
await partnerRepo.remove(partner);
|
||||
|
||||
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not sync a partner delete after a user is deleted', async () => {
|
||||
const { auth, context, testSync } = await setup();
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const user2 = await context.createUser();
|
||||
await context.createPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
await context.user.delete({ id: user2.id }, true);
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
await userRepo.delete({ id: user2.id }, true);
|
||||
|
||||
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
||||
});
|
||||
@ -385,21 +422,23 @@ describe(SyncService.name, () => {
|
||||
|
||||
describe.concurrent(SyncEntityType.AssetV1, () => {
|
||||
it('should detect and sync the first asset', async () => {
|
||||
const { auth, context, sut, testSync } = await setup();
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||
const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||
const date = new Date().toISOString();
|
||||
|
||||
const asset = TestFactory.asset({
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({
|
||||
ownerId: auth.user.id,
|
||||
checksum: Buffer.from(checksum, 'base64'),
|
||||
thumbhash: Buffer.from(thumbhash, 'base64'),
|
||||
fileCreatedAt: date,
|
||||
fileModifiedAt: date,
|
||||
localDateTime: date,
|
||||
deletedAt: null,
|
||||
});
|
||||
await context.createAsset(asset);
|
||||
await assetRepo.create(asset);
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||
|
||||
@ -413,12 +452,12 @@ describe(SyncService.name, () => {
|
||||
ownerId: asset.ownerId,
|
||||
thumbhash,
|
||||
checksum,
|
||||
deletedAt: null,
|
||||
fileCreatedAt: date,
|
||||
fileModifiedAt: date,
|
||||
isFavorite: false,
|
||||
isVisible: true,
|
||||
localDateTime: '2000-01-01T00:00:00.000Z',
|
||||
deletedAt: asset.deletedAt,
|
||||
fileCreatedAt: asset.fileCreatedAt,
|
||||
fileModifiedAt: asset.fileModifiedAt,
|
||||
isFavorite: asset.isFavorite,
|
||||
isVisible: asset.isVisible,
|
||||
localDateTime: asset.localDateTime,
|
||||
type: asset.type,
|
||||
},
|
||||
type: 'AssetV1',
|
||||
@ -435,11 +474,12 @@ describe(SyncService.name, () => {
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted asset', async () => {
|
||||
const { auth, context, sut, testSync } = await setup();
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const asset = TestFactory.asset({ ownerId: auth.user.id });
|
||||
await context.createAsset(asset);
|
||||
await context.asset.remove(asset);
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.remove(asset);
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||
|
||||
@ -465,19 +505,26 @@ describe(SyncService.name, () => {
|
||||
});
|
||||
|
||||
it('should not sync an asset or asset delete for an unrelated user', async () => {
|
||||
const { auth, context, testSync } = await setup();
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const user2 = await context.createUser();
|
||||
const session = TestFactory.session({ userId: user2.id });
|
||||
const auth2 = TestFactory.auth({ session, user: user2 });
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const asset = TestFactory.asset({ ownerId: user2.id });
|
||||
await context.createAsset(asset);
|
||||
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 });
|
||||
|
||||
expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1);
|
||||
expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0);
|
||||
|
||||
await context.asset.remove(asset);
|
||||
await assetRepo.remove(asset);
|
||||
expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1);
|
||||
expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0);
|
||||
});
|
||||
@ -485,24 +532,30 @@ describe(SyncService.name, () => {
|
||||
|
||||
describe.concurrent(SyncRequestType.PartnerAssetsV1, () => {
|
||||
it('should detect and sync the first partner asset', async () => {
|
||||
const { auth, context, sut, testSync } = await setup();
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||
const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||
const date = new Date().toISOString();
|
||||
|
||||
const user2 = await context.createUser();
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const asset = TestFactory.asset({
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({
|
||||
ownerId: user2.id,
|
||||
checksum: Buffer.from(checksum, 'base64'),
|
||||
thumbhash: Buffer.from(thumbhash, 'base64'),
|
||||
fileCreatedAt: date,
|
||||
fileModifiedAt: date,
|
||||
localDateTime: date,
|
||||
deletedAt: null,
|
||||
});
|
||||
await context.createAsset(asset);
|
||||
await context.partner.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
await assetRepo.create(asset);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
|
||||
@ -521,7 +574,7 @@ describe(SyncService.name, () => {
|
||||
fileModifiedAt: date,
|
||||
isFavorite: false,
|
||||
isVisible: true,
|
||||
localDateTime: '2000-01-01T00:00:00.000Z',
|
||||
localDateTime: date,
|
||||
type: asset.type,
|
||||
},
|
||||
type: SyncEntityType.PartnerAssetV1,
|
||||
@ -538,13 +591,19 @@ describe(SyncService.name, () => {
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted partner asset', async () => {
|
||||
const { auth, context, sut, testSync } = await setup();
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const user2 = await context.createUser();
|
||||
const asset = TestFactory.asset({ ownerId: user2.id });
|
||||
await context.createAsset(asset);
|
||||
await context.partner.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
await context.asset.remove(asset);
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
await assetRepo.create(asset);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
await assetRepo.remove(asset);
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
|
||||
@ -570,62 +629,89 @@ describe(SyncService.name, () => {
|
||||
});
|
||||
|
||||
it('should not sync a deleted partner asset due to a user delete', async () => {
|
||||
const { auth, context, testSync } = await setup();
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const user2 = await context.createUser();
|
||||
await context.partner.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
await context.createAsset({ ownerId: user2.id });
|
||||
await context.user.delete({ id: user2.id }, true);
|
||||
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 }));
|
||||
|
||||
await userRepo.delete({ id: user2.id }, true);
|
||||
|
||||
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 () => {
|
||||
const { auth, context, testSync } = await setup();
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const user2 = await context.createUser();
|
||||
await context.createAsset({ ownerId: user2.id });
|
||||
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 context.partner.create(partner);
|
||||
await partnerRepo.create(partner);
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(1);
|
||||
|
||||
await context.partner.remove(partner);
|
||||
await partnerRepo.remove(partner);
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not sync an asset or asset delete for own user', async () => {
|
||||
const { auth, context, testSync } = await setup();
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const user2 = await context.createUser();
|
||||
const asset = await context.createAsset({ ownerId: auth.user.id });
|
||||
const partner = { sharedById: user2.id, sharedWithId: auth.user.id };
|
||||
await context.partner.create(partner);
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||
await assetRepo.create(asset);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
|
||||
await context.asset.remove(asset);
|
||||
await assetRepo.remove(asset);
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not sync an asset or asset delete for unrelated user', async () => {
|
||||
const { auth, context, testSync } = await setup();
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const user2 = await context.createUser();
|
||||
const session = TestFactory.session({ userId: user2.id });
|
||||
const auth2 = TestFactory.auth({ session, user: user2 });
|
||||
const asset = await context.createAsset({ ownerId: user2.id });
|
||||
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 auth2 = factory.auth({ session, user: user2 });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||
await assetRepo.create(asset);
|
||||
|
||||
await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
|
||||
await context.asset.remove(asset);
|
||||
await assetRepo.remove(asset);
|
||||
|
||||
await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
@ -634,13 +720,12 @@ describe(SyncService.name, () => {
|
||||
|
||||
describe.concurrent(SyncRequestType.AssetExifsV1, () => {
|
||||
it('should detect and sync the first asset exif', async () => {
|
||||
const { auth, context, sut, testSync } = await setup();
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const asset = TestFactory.asset({ ownerId: auth.user.id });
|
||||
const exif = { assetId: asset.id, make: 'Canon' };
|
||||
|
||||
await context.createAsset(asset);
|
||||
await context.asset.upsertExif(exif);
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]);
|
||||
|
||||
@ -690,19 +775,25 @@ describe(SyncService.name, () => {
|
||||
});
|
||||
|
||||
it('should only sync asset exif for own user', async () => {
|
||||
const { auth, context, testSync } = await setup();
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const user2 = await context.createUser();
|
||||
const session = TestFactory.session({ userId: user2.id });
|
||||
const auth2 = TestFactory.auth({ session, user: user2 });
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
await context.partner.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
const asset = TestFactory.asset({ ownerId: user2.id });
|
||||
const exif = { assetId: asset.id, make: 'Canon' };
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
await context.createAsset(asset);
|
||||
await context.asset.upsertExif(exif);
|
||||
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 });
|
||||
await expect(testSync(auth2, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
@ -710,14 +801,19 @@ describe(SyncService.name, () => {
|
||||
|
||||
describe.concurrent(SyncRequestType.PartnerAssetExifsV1, () => {
|
||||
it('should detect and sync the first partner asset exif', async () => {
|
||||
const { auth, context, sut, testSync } = await setup();
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const user2 = await context.createUser();
|
||||
await context.partner.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
const asset = TestFactory.asset({ ownerId: user2.id });
|
||||
await context.createAsset(asset);
|
||||
const exif = { assetId: asset.id, make: 'Canon' };
|
||||
await context.asset.upsertExif(exif);
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
|
||||
@ -767,32 +863,46 @@ describe(SyncService.name, () => {
|
||||
});
|
||||
|
||||
it('should not sync partner asset exif for own user', async () => {
|
||||
const { auth, context, testSync } = await setup();
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const user2 = await context.createUser();
|
||||
await context.partner.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
const asset = TestFactory.asset({ ownerId: auth.user.id });
|
||||
const exif = { assetId: asset.id, make: 'Canon' };
|
||||
await context.createAsset(asset);
|
||||
await context.asset.upsertExif(exif);
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not sync partner asset exif for unrelated user', async () => {
|
||||
const { auth, context, testSync } = await setup();
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const user2 = await context.createUser();
|
||||
const user3 = await context.createUser();
|
||||
const session = TestFactory.session({ userId: user3.id });
|
||||
const authUser3 = TestFactory.auth({ session, user: user3 });
|
||||
await context.partner.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
const asset = TestFactory.asset({ ownerId: user3.id });
|
||||
const exif = { assetId: asset.id, make: 'Canon' };
|
||||
await context.createAsset(asset);
|
||||
await context.asset.upsertExif(exif);
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const user2 = mediumFactory.userInsert();
|
||||
const user3 = mediumFactory.userInsert();
|
||||
await Promise.all([userRepo.create(user2), userRepo.create(user3)]);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user3.id });
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
const sessionRepo = getRepository('session');
|
||||
const session = mediumFactory.sessionInsert({ userId: user3.id });
|
||||
await sessionRepo.create(session);
|
||||
|
||||
const authUser3 = factory.auth({ session, user: user3 });
|
||||
await expect(testSync(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
|
@ -41,7 +41,10 @@ const authFactory = ({
|
||||
}: {
|
||||
apiKey?: Partial<AuthApiKey>;
|
||||
session?: { id: string };
|
||||
user?: Partial<UserAdmin>;
|
||||
user?: Omit<
|
||||
Partial<UserAdmin>,
|
||||
'createdAt' | 'updatedAt' | 'deletedAt' | 'fileCreatedAt' | 'fileModifiedAt' | 'localDateTime' | 'profileChangedAt'
|
||||
>;
|
||||
sharedLink?: Partial<AuthSharedLink>;
|
||||
} = {}) => {
|
||||
const auth: AuthDto = {
|
||||
|
@ -202,7 +202,7 @@ export const newTestService = <T extends BaseService>(
|
||||
partner: automock(PartnerRepository, { strict: false }),
|
||||
person: newPersonRepositoryMock(),
|
||||
process: automock(ProcessRepository),
|
||||
search: automock(SearchRepository, { args: [loggerMock], strict: false }),
|
||||
search: automock(SearchRepository, { strict: false }),
|
||||
// eslint-disable-next-line no-sparse-arrays
|
||||
serverInfo: automock(ServerInfoRepository, { args: [, loggerMock], strict: false }),
|
||||
session: automock(SessionRepository),
|
||||
|
Loading…
x
Reference in New Issue
Block a user