added e2e tests for metadata search
fixed fileCreatedAt
This commit is contained in:
parent
8a8da5f5c8
commit
c18affaad1
7 changed files with 304 additions and 49 deletions
|
@ -1,5 +1,5 @@
|
||||||
import { SearchExploreItem } from '@app/domain';
|
import { SearchExploreItem } from '@app/domain';
|
||||||
import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity } from '@app/infra/entities';
|
import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity, SmartInfoEntity } from '@app/infra/entities';
|
||||||
import { FindOptionsRelations } from 'typeorm';
|
import { FindOptionsRelations } from 'typeorm';
|
||||||
import { Paginated, PaginationOptions } from '../domain.util';
|
import { Paginated, PaginationOptions } from '../domain.util';
|
||||||
|
|
||||||
|
@ -173,7 +173,7 @@ export interface IAssetRepository {
|
||||||
getByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity | null>;
|
getByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity | null>;
|
||||||
getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated<AssetEntity>;
|
getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated<AssetEntity>;
|
||||||
getByUserId(pagination: PaginationOptions, userId: string, options?: AssetSearchOptions): Paginated<AssetEntity>;
|
getByUserId(pagination: PaginationOptions, userId: string, options?: AssetSearchOptions): Paginated<AssetEntity>;
|
||||||
getById(id: string): Promise<AssetEntity | null>;
|
getById(id: string, relations?: FindOptionsRelations<AssetEntity>): Promise<AssetEntity | null>;
|
||||||
getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity>;
|
getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity>;
|
||||||
getWith(pagination: PaginationOptions, property: WithProperty, libraryId?: string): Paginated<AssetEntity>;
|
getWith(pagination: PaginationOptions, property: WithProperty, libraryId?: string): Paginated<AssetEntity>;
|
||||||
getRandom(userId: string, count: number): Promise<AssetEntity[]>;
|
getRandom(userId: string, count: number): Promise<AssetEntity[]>;
|
||||||
|
|
|
@ -54,9 +54,15 @@ export class SearchService {
|
||||||
|
|
||||||
async search(authUser: AuthUserDto, dto: SearchDto): Promise<SearchResponseDto> {
|
async search(authUser: AuthUserDto, dto: SearchDto): Promise<SearchResponseDto> {
|
||||||
const { machineLearning } = await this.configCore.getConfig();
|
const { machineLearning } = await this.configCore.getConfig();
|
||||||
const query = dto.q || dto.query || '*';
|
const query = dto.q || dto.query;
|
||||||
|
if (!query) {
|
||||||
|
throw new Error('Missing query');
|
||||||
|
}
|
||||||
const hasClip = machineLearning.enabled && machineLearning.clip.enabled;
|
const hasClip = machineLearning.enabled && machineLearning.clip.enabled;
|
||||||
const strategy = dto.clip && hasClip ? SearchStrategy.CLIP : SearchStrategy.TEXT;
|
if (dto.clip && !hasClip) {
|
||||||
|
throw new Error('CLIP is not enabled');
|
||||||
|
}
|
||||||
|
const strategy = dto.clip ? SearchStrategy.CLIP : SearchStrategy.TEXT;
|
||||||
|
|
||||||
let assets: AssetEntity[] = [];
|
let assets: AssetEntity[] = [];
|
||||||
|
|
||||||
|
@ -74,7 +80,7 @@ export class SearchService {
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
albums: {
|
albums: {
|
||||||
total: 0,
|
total: 0,
|
||||||
|
|
|
@ -2,7 +2,6 @@ import {
|
||||||
AssetBuilderOptions,
|
AssetBuilderOptions,
|
||||||
AssetCreate,
|
AssetCreate,
|
||||||
AssetExploreFieldOptions,
|
AssetExploreFieldOptions,
|
||||||
AssetExploreOptions,
|
|
||||||
AssetSearchOptions,
|
AssetSearchOptions,
|
||||||
AssetStats,
|
AssetStats,
|
||||||
AssetStatsOptions,
|
AssetStatsOptions,
|
||||||
|
@ -361,16 +360,20 @@ export class AssetRepository implements IAssetRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
getById(id: string): Promise<AssetEntity | null> {
|
getById(id: string, relations: FindOptionsRelations<AssetEntity>): Promise<AssetEntity | null> {
|
||||||
return this.repository.findOne({
|
if (!relations) {
|
||||||
where: { id },
|
relations = {
|
||||||
relations: {
|
|
||||||
faces: {
|
faces: {
|
||||||
person: true,
|
person: true,
|
||||||
},
|
},
|
||||||
library: true,
|
library: true,
|
||||||
stack: true,
|
stack: true,
|
||||||
},
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.repository.findOne({
|
||||||
|
where: { id },
|
||||||
|
relations,
|
||||||
// We are specifically asking for this asset. Return it even if it is soft deleted
|
// We are specifically asking for this asset. Return it even if it is soft deleted
|
||||||
withDeleted: true,
|
withDeleted: true,
|
||||||
});
|
});
|
||||||
|
@ -705,8 +708,13 @@ export class AssetRepository implements IAssetRepository {
|
||||||
.having('count(city) >= :minAssetsPerField', { minAssetsPerField })
|
.having('count(city) >= :minAssetsPerField', { minAssetsPerField })
|
||||||
.orderBy('random()')
|
.orderBy('random()')
|
||||||
.limit(maxFields);
|
.limit(maxFields);
|
||||||
|
|
||||||
const items = await this.getBuilder({ userIds: [ownerId], exifInfo: false, assetType: AssetType.IMAGE, isArchived: false })
|
const items = await this.getBuilder({
|
||||||
|
userIds: [ownerId],
|
||||||
|
exifInfo: false,
|
||||||
|
assetType: AssetType.IMAGE,
|
||||||
|
isArchived: false,
|
||||||
|
})
|
||||||
.select('c.city', 'value')
|
.select('c.city', 'value')
|
||||||
.addSelect('asset.id', 'data')
|
.addSelect('asset.id', 'data')
|
||||||
.distinctOn(['c.city'])
|
.distinctOn(['c.city'])
|
||||||
|
@ -731,7 +739,12 @@ export class AssetRepository implements IAssetRepository {
|
||||||
.orderBy('random()')
|
.orderBy('random()')
|
||||||
.limit(maxFields);
|
.limit(maxFields);
|
||||||
|
|
||||||
const items = await this.getBuilder({ userIds: [ownerId], exifInfo: false, assetType: AssetType.IMAGE, isArchived: false })
|
const items = await this.getBuilder({
|
||||||
|
userIds: [ownerId],
|
||||||
|
exifInfo: false,
|
||||||
|
assetType: AssetType.IMAGE,
|
||||||
|
isArchived: false,
|
||||||
|
})
|
||||||
.select('unnest(si.tags)', 'value')
|
.select('unnest(si.tags)', 'value')
|
||||||
.addSelect('asset.id', 'data')
|
.addSelect('asset.id', 'data')
|
||||||
.distinctOn(['unnest(si.tags)'])
|
.distinctOn(['unnest(si.tags)'])
|
||||||
|
@ -808,7 +821,10 @@ export class AssetRepository implements IAssetRepository {
|
||||||
.innerJoin('smart_info', 'si', 'si."assetId" = assets."id"')
|
.innerJoin('smart_info', 'si', 'si."assetId" = assets."id"')
|
||||||
.innerJoin('exif', 'e', 'assets."id" = e."assetId"')
|
.innerJoin('exif', 'e', 'assets."id" = e."assetId"')
|
||||||
.where('a.ownerId = :ownerId', { ownerId })
|
.where('a.ownerId = :ownerId', { ownerId })
|
||||||
.where('(e."exifTextSearchableColumn" || si."smartInfoTextSearchableColumn") @@ PLAINTO_TSQUERY(\'english\', :query)', { query })
|
.where(
|
||||||
|
'(e."exifTextSearchableColumn" || si."smartInfoTextSearchableColumn") @@ PLAINTO_TSQUERY(\'english\', :query)',
|
||||||
|
{ query },
|
||||||
|
)
|
||||||
.limit(numResults)
|
.limit(numResults)
|
||||||
.getRawMany();
|
.getRawMany();
|
||||||
|
|
||||||
|
|
|
@ -10,19 +10,15 @@ import {
|
||||||
usePagination,
|
usePagination,
|
||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { AssetController } from '@app/immich';
|
import { AssetController } from '@app/immich';
|
||||||
import { AssetEntity, AssetType, LibraryType, SharedLinkType } from '@app/infra/entities';
|
import { AssetEntity, AssetType, SharedLinkType } from '@app/infra/entities';
|
||||||
import { AssetRepository } from '@app/infra/repositories';
|
import { AssetRepository } from '@app/infra/repositories';
|
||||||
import { INestApplication } from '@nestjs/common';
|
import { INestApplication } from '@nestjs/common';
|
||||||
import { api } from '@test/api';
|
import { api } from '@test/api';
|
||||||
import { errorStub, userDto, uuidStub } from '@test/fixtures';
|
import { errorStub, userDto, uuidStub } from '@test/fixtures';
|
||||||
import { testApp } from '@test/test-utils';
|
import { generateAsset, testApp, today, yesterday } from '@test/test-utils';
|
||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
|
|
||||||
const today = DateTime.fromObject({ year: 2023, month: 11, day: 3 });
|
|
||||||
const yesterday = today.minus({ days: 1 });
|
|
||||||
|
|
||||||
const makeUploadDto = (options?: { omit: string }): Record<string, any> => {
|
const makeUploadDto = (options?: { omit: string }): Record<string, any> => {
|
||||||
const dto: Record<string, any> = {
|
const dto: Record<string, any> = {
|
||||||
deviceAssetId: 'example-image',
|
deviceAssetId: 'example-image',
|
||||||
|
@ -54,30 +50,14 @@ describe(`${AssetController.name} (e2e)`, () => {
|
||||||
let asset4: AssetResponseDto;
|
let asset4: AssetResponseDto;
|
||||||
let asset5: AssetResponseDto;
|
let asset5: AssetResponseDto;
|
||||||
|
|
||||||
let assetCount = 0;
|
const createAsset = async (
|
||||||
const createAsset = async (loginResponse: LoginResponseDto, createdAt: Date, other: Partial<AssetEntity> = {}) => {
|
loginResponse: LoginResponseDto,
|
||||||
const id = assetCount++;
|
fileCreatedAt: Date,
|
||||||
const asset = await assetRepository.create({
|
other: Partial<AssetEntity> = {},
|
||||||
createdAt: today.toJSDate(),
|
) => {
|
||||||
updatedAt: today.toJSDate(),
|
const asset = await assetRepository.create(
|
||||||
ownerId: loginResponse.userId,
|
generateAsset(loginResponse.userId, libraries, { fileCreatedAt, ...other }),
|
||||||
checksum: randomBytes(20),
|
);
|
||||||
originalPath: `/tests/test_${id}`,
|
|
||||||
deviceAssetId: `test_${id}`,
|
|
||||||
deviceId: 'e2e-test',
|
|
||||||
libraryId: (
|
|
||||||
libraries.find(
|
|
||||||
({ ownerId, type }) => ownerId === loginResponse.userId && type === LibraryType.UPLOAD,
|
|
||||||
) as LibraryResponseDto
|
|
||||||
).id,
|
|
||||||
isVisible: true,
|
|
||||||
fileCreatedAt: createdAt,
|
|
||||||
fileModifiedAt: new Date(),
|
|
||||||
localDateTime: createdAt,
|
|
||||||
type: AssetType.IMAGE,
|
|
||||||
originalFileName: `test_${id}`,
|
|
||||||
...other,
|
|
||||||
});
|
|
||||||
|
|
||||||
return mapAsset(asset);
|
return mapAsset(asset);
|
||||||
};
|
};
|
||||||
|
@ -764,7 +744,11 @@ describe(`${AssetController.name} (e2e)`, () => {
|
||||||
const personRepository = app.get<IPersonRepository>(IPersonRepository);
|
const personRepository = app.get<IPersonRepository>(IPersonRepository);
|
||||||
const person = await personRepository.create({ ownerId: asset1.ownerId, name: 'Test Person' });
|
const person = await personRepository.create({ ownerId: asset1.ownerId, name: 'Test Person' });
|
||||||
|
|
||||||
await personRepository.createFace({ assetId: asset1.id, personId: person.id, embedding: Array.from({length: 512}, Math.random) });
|
await personRepository.createFace({
|
||||||
|
assetId: asset1.id,
|
||||||
|
personId: person.id,
|
||||||
|
embedding: Array.from({ length: 512 }, Math.random),
|
||||||
|
});
|
||||||
|
|
||||||
const { status, body } = await request(server)
|
const { status, body } = await request(server)
|
||||||
.put(`/asset/${asset1.id}`)
|
.put(`/asset/${asset1.id}`)
|
||||||
|
@ -1339,7 +1323,11 @@ describe(`${AssetController.name} (e2e)`, () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const personRepository = app.get<IPersonRepository>(IPersonRepository);
|
const personRepository = app.get<IPersonRepository>(IPersonRepository);
|
||||||
const person = await personRepository.create({ ownerId: asset1.ownerId, name: 'Test Person' });
|
const person = await personRepository.create({ ownerId: asset1.ownerId, name: 'Test Person' });
|
||||||
await personRepository.createFace({ assetId: asset1.id, personId: person.id, embedding: Array.from({length: 512}, Math.random) });
|
await personRepository.createFace({
|
||||||
|
assetId: asset1.id,
|
||||||
|
personId: person.id,
|
||||||
|
embedding: Array.from({ length: 512 }, Math.random),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not return asset with facesRecognizedAt unset', async () => {
|
it('should not return asset with facesRecognizedAt unset', async () => {
|
||||||
|
|
205
server/test/e2e/search.e2e-spec.ts
Normal file
205
server/test/e2e/search.e2e-spec.ts
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
import { AssetResponseDto, IAssetRepository, ISmartInfoRepository, LibraryResponseDto, LoginResponseDto, mapAsset } from '@app/domain';
|
||||||
|
import { SearchController } from '@app/immich';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import { api } from '@test/api';
|
||||||
|
import { errorStub } from '@test/fixtures';
|
||||||
|
import { generateAsset, testApp } from '@test/test-utils';
|
||||||
|
import request from 'supertest';
|
||||||
|
|
||||||
|
describe(`${SearchController.name}`, () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
let server: any;
|
||||||
|
let loginResponse: LoginResponseDto;
|
||||||
|
let accessToken: string;
|
||||||
|
let libraries: LibraryResponseDto[];
|
||||||
|
let assetRepository: IAssetRepository;
|
||||||
|
let smartInfoRepository: ISmartInfoRepository;
|
||||||
|
let asset1: AssetResponseDto;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[server, app] = await testApp.create();
|
||||||
|
assetRepository = app.get<IAssetRepository>(IAssetRepository);
|
||||||
|
smartInfoRepository = app.get<ISmartInfoRepository>(ISmartInfoRepository);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await testApp.teardown();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await testApp.reset();
|
||||||
|
await api.authApi.adminSignUp(server);
|
||||||
|
loginResponse = await api.authApi.adminLogin(server);
|
||||||
|
accessToken = loginResponse.accessToken;
|
||||||
|
libraries = await api.libraryApi.getAll(server, accessToken);
|
||||||
|
|
||||||
|
const assetId = (await assetRepository.create(generateAsset(loginResponse.userId, libraries))).id;
|
||||||
|
await assetRepository.upsertExif({
|
||||||
|
assetId,
|
||||||
|
latitude: 90,
|
||||||
|
longitude: 90,
|
||||||
|
city: 'Immich',
|
||||||
|
state: 'Nebraska',
|
||||||
|
country: 'United States',
|
||||||
|
make: 'Canon',
|
||||||
|
model: 'EOS Rebel T7',
|
||||||
|
lensModel: 'Fancy lens',
|
||||||
|
});
|
||||||
|
await smartInfoRepository.upsert(
|
||||||
|
{ assetId, objects: ['car', 'tree'], tags: ['accident'] },
|
||||||
|
Array.from({ length: 512 }, Math.random));
|
||||||
|
const assetWithMetadata = await assetRepository.getById(assetId, { exifInfo: true, smartInfo: true });
|
||||||
|
if (!assetWithMetadata) {
|
||||||
|
throw new Error('Asset not found');
|
||||||
|
}
|
||||||
|
asset1 = mapAsset(assetWithMetadata);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /search', () => {
|
||||||
|
beforeEach(async () => {});
|
||||||
|
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(server).get('/search');
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorStub.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return assets when searching by exif', async () => {
|
||||||
|
if (!asset1?.exifInfo?.make) {
|
||||||
|
throw new Error('Asset 1 does not have exif info');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.get('/search')
|
||||||
|
.set('Authorization', `Bearer ${accessToken}`)
|
||||||
|
.query({ q: asset1.exifInfo.make });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
albums: {
|
||||||
|
total: 0,
|
||||||
|
count: 0,
|
||||||
|
items: [],
|
||||||
|
facets: [],
|
||||||
|
},
|
||||||
|
assets: {
|
||||||
|
total: 1,
|
||||||
|
count: 1,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: asset1.id,
|
||||||
|
exifInfo: {
|
||||||
|
make: asset1.exifInfo.make,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
facets: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be case-insensitive for metadata search', async () => {
|
||||||
|
if (!asset1?.exifInfo?.make) {
|
||||||
|
throw new Error('Asset 1 does not have exif info');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.get('/search')
|
||||||
|
.set('Authorization', `Bearer ${accessToken}`)
|
||||||
|
.query({ q: asset1.exifInfo.make.toLowerCase() });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
albums: {
|
||||||
|
total: 0,
|
||||||
|
count: 0,
|
||||||
|
items: [],
|
||||||
|
facets: [],
|
||||||
|
},
|
||||||
|
assets: {
|
||||||
|
total: 1,
|
||||||
|
count: 1,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: asset1.id,
|
||||||
|
exifInfo: {
|
||||||
|
make: asset1.exifInfo.make,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
facets: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be whitespace-insensitive for metadata search', async () => {
|
||||||
|
if (!asset1?.exifInfo?.make) {
|
||||||
|
throw new Error('Asset 1 does not have exif info');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.get('/search')
|
||||||
|
.set('Authorization', `Bearer ${accessToken}`)
|
||||||
|
.query({ q: ` ${asset1.exifInfo.make} ` });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
albums: {
|
||||||
|
total: 0,
|
||||||
|
count: 0,
|
||||||
|
items: [],
|
||||||
|
facets: [],
|
||||||
|
},
|
||||||
|
assets: {
|
||||||
|
total: 1,
|
||||||
|
count: 1,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: asset1.id,
|
||||||
|
exifInfo: {
|
||||||
|
make: asset1.exifInfo.make,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
facets: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return assets when searching by object', async () => {
|
||||||
|
if (!asset1?.smartInfo?.objects) {
|
||||||
|
throw new Error('Asset 1 does not have smart info');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.get('/search')
|
||||||
|
.set('Authorization', `Bearer ${accessToken}`)
|
||||||
|
.query({ q: asset1.smartInfo.objects[0] });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
albums: {
|
||||||
|
total: 0,
|
||||||
|
count: 0,
|
||||||
|
items: [],
|
||||||
|
facets: [],
|
||||||
|
},
|
||||||
|
assets: {
|
||||||
|
total: 1,
|
||||||
|
count: 1,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: asset1.id,
|
||||||
|
smartInfo: {
|
||||||
|
objects: asset1.smartInfo.objects,
|
||||||
|
tags: asset1.smartInfo.tags,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
facets: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -35,7 +35,7 @@ export default async () => {
|
||||||
|
|
||||||
if (process.env.DB_HOSTNAME === undefined) {
|
if (process.env.DB_HOSTNAME === undefined) {
|
||||||
// DB hostname not set which likely means we're not running e2e through docker compose. Start a local postgres container.
|
// DB hostname not set which likely means we're not running e2e through docker compose. Start a local postgres container.
|
||||||
const pg = await new PostgreSqlContainer('postgres')
|
const pg = await new PostgreSqlContainer('tensorchord/pgvecto-rs:pg14-v0.1.10')
|
||||||
.withExposedPorts(5432)
|
.withExposedPorts(5432)
|
||||||
.withDatabase('immich')
|
.withDatabase('immich')
|
||||||
.withUsername('postgres')
|
.withUsername('postgres')
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import { IJobRepository, JobItem, JobItemHandler, QueueName } from '@app/domain';
|
import { AssetCreate, IJobRepository, JobItem, JobItemHandler, LibraryResponseDto, QueueName } from '@app/domain';
|
||||||
import { AppModule } from '@app/immich';
|
import { AppModule } from '@app/immich';
|
||||||
import { dataSource } from '@app/infra';
|
import { dataSource } from '@app/infra';
|
||||||
|
import { AssetEntity, AssetType, LibraryType } from '@app/infra/entities';
|
||||||
import { INestApplication } from '@nestjs/common';
|
import { INestApplication } from '@nestjs/common';
|
||||||
import { Test } from '@nestjs/testing';
|
import { Test } from '@nestjs/testing';
|
||||||
|
import { randomBytes } from 'crypto';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { EntityTarget, ObjectLiteral } from 'typeorm';
|
import { EntityTarget, ObjectLiteral } from 'typeorm';
|
||||||
import { AppService } from '../src/microservices/app.service';
|
import { AppService } from '../src/microservices/app.service';
|
||||||
|
@ -11,6 +14,9 @@ import { AppService } from '../src/microservices/app.service';
|
||||||
export const IMMICH_TEST_ASSET_PATH = process.env.IMMICH_TEST_ASSET_PATH;
|
export const IMMICH_TEST_ASSET_PATH = process.env.IMMICH_TEST_ASSET_PATH;
|
||||||
export const IMMICH_TEST_ASSET_TEMP_PATH = path.normalize(`${IMMICH_TEST_ASSET_PATH}/temp/`);
|
export const IMMICH_TEST_ASSET_TEMP_PATH = path.normalize(`${IMMICH_TEST_ASSET_PATH}/temp/`);
|
||||||
|
|
||||||
|
export const today = DateTime.fromObject({ year: 2023, month: 11, day: 3 });
|
||||||
|
export const yesterday = today.minus({ days: 1 });
|
||||||
|
|
||||||
export interface ResetOptions {
|
export interface ResetOptions {
|
||||||
entities?: EntityTarget<ObjectLiteral>[];
|
entities?: EntityTarget<ObjectLiteral>[];
|
||||||
}
|
}
|
||||||
|
@ -115,3 +121,37 @@ export async function restoreTempFolder(): Promise<void> {
|
||||||
// Create temp folder
|
// Create temp folder
|
||||||
await fs.promises.mkdir(IMMICH_TEST_ASSET_TEMP_PATH);
|
await fs.promises.mkdir(IMMICH_TEST_ASSET_TEMP_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function randomDate(start: Date, end: Date): Date {
|
||||||
|
return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let assetCount = 0;
|
||||||
|
export function generateAsset(
|
||||||
|
userId: string,
|
||||||
|
libraries: LibraryResponseDto[],
|
||||||
|
other: Partial<AssetEntity> = {},
|
||||||
|
): AssetCreate {
|
||||||
|
const id = assetCount++;
|
||||||
|
const { fileCreatedAt = randomDate(new Date(1970, 1, 1), new Date(2023, 1, 1)) } = other;
|
||||||
|
|
||||||
|
return {
|
||||||
|
createdAt: today.toJSDate(),
|
||||||
|
updatedAt: today.toJSDate(),
|
||||||
|
ownerId: userId,
|
||||||
|
checksum: randomBytes(20),
|
||||||
|
originalPath: `/tests/test_${id}`,
|
||||||
|
deviceAssetId: `test_${id}`,
|
||||||
|
deviceId: 'e2e-test',
|
||||||
|
libraryId: (
|
||||||
|
libraries.find(({ ownerId, type }) => ownerId === userId && type === LibraryType.UPLOAD) as LibraryResponseDto
|
||||||
|
).id,
|
||||||
|
isVisible: true,
|
||||||
|
fileCreatedAt,
|
||||||
|
fileModifiedAt: new Date(),
|
||||||
|
localDateTime: fileCreatedAt,
|
||||||
|
type: AssetType.IMAGE,
|
||||||
|
originalFileName: `test_${id}`,
|
||||||
|
...other,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue