import { AssetResponseDto, IAssetRepository, ISmartInfoRepository, LibraryResponseDto, LoginResponseDto, mapAsset, } from '@app/domain'; import { SearchController } from '@app/immich'; import { INestApplication } from '@nestjs/common'; import { errorStub, searchStub } from '@test/fixtures'; import request from 'supertest'; import { api } from '../../client'; import { generateAsset, testApp } from '../utils'; 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 () => { app = await testApp.create(); server = app.getHttpServer(); assetRepository = app.get(IAssetRepository); smartInfoRepository = app.get(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); }); describe('GET /search (exif)', () => { beforeEach(async () => { const assetId = (await assetRepository.create(generateAsset(loginResponse.userId, libraries))).id; await assetRepository.upsertExif({ assetId, ...searchStub.exif }); const assetWithMetadata = await assetRepository.getById(assetId, { exifInfo: true }); if (!assetWithMetadata) { throw new Error('Asset not found'); } asset1 = mapAsset(assetWithMetadata); }); 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: [], }, }); }); }); describe('GET /search (smart info)', () => { beforeEach(async () => { const assetId = (await assetRepository.create(generateAsset(loginResponse.userId, libraries))).id; await assetRepository.upsertExif({ assetId, ...searchStub.exif }); await smartInfoRepository.upsert({ assetId, ...searchStub.smartInfo }, 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); }); 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: [], }, }); }); }); describe('GET /search (file name)', () => { beforeEach(async () => { const assetId = (await assetRepository.create(generateAsset(loginResponse.userId, libraries))).id; await assetRepository.upsertExif({ assetId, ...searchStub.exif }); const assetWithMetadata = await assetRepository.getById(assetId, { exifInfo: true }); if (!assetWithMetadata) { throw new Error('Asset not found'); } asset1 = mapAsset(assetWithMetadata); }); it('should return assets when searching by file name', async () => { if (asset1?.originalFileName.length === 0) { throw new Error('Asset 1 does not have an original file name'); } const { status, body } = await request(server) .get('/search') .set('Authorization', `Bearer ${accessToken}`) .query({ q: asset1.originalFileName }); expect(status).toBe(200); expect(body).toMatchObject({ albums: { total: 0, count: 0, items: [], facets: [], }, assets: { total: 1, count: 1, items: [ { id: asset1.id, originalFileName: asset1.originalFileName, }, ], facets: [], }, }); }); it('should return assets when searching by file name with extension', async () => { if (asset1?.originalFileName.length === 0) { throw new Error('Asset 1 does not have an original file name'); } const { status, body } = await request(server) .get('/search') .set('Authorization', `Bearer ${accessToken}`) .query({ q: asset1.originalFileName + '.jpg' }); expect(status).toBe(200); expect(body).toMatchObject({ albums: { total: 0, count: 0, items: [], facets: [], }, assets: { total: 1, count: 1, items: [ { id: asset1.id, originalFileName: asset1.originalFileName, }, ], facets: [], }, }); }); }); });