mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
225 lines
7.3 KiB
TypeScript
225 lines
7.3 KiB
TypeScript
|
|
import { AssetFileUploadResponseDto, LoginResponseDto } from '@immich/sdk';
|
||
|
|
import { readFile } from 'node:fs/promises';
|
||
|
|
import { join } from 'node:path';
|
||
|
|
import { Socket } from 'socket.io-client';
|
||
|
|
import { errorDto } from 'src/responses';
|
||
|
|
import { app, testAssetDir, utils } from 'src/utils';
|
||
|
|
import request from 'supertest';
|
||
|
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||
|
|
|
||
|
|
const albums = { total: 0, count: 0, items: [], facets: [] };
|
||
|
|
|
||
|
|
describe('/search', () => {
|
||
|
|
let admin: LoginResponseDto;
|
||
|
|
let assetFalcon: AssetFileUploadResponseDto;
|
||
|
|
let assetDenali: AssetFileUploadResponseDto;
|
||
|
|
let websocket: Socket;
|
||
|
|
|
||
|
|
beforeAll(async () => {
|
||
|
|
await utils.resetDatabase();
|
||
|
|
admin = await utils.adminSetup();
|
||
|
|
websocket = await utils.connectWebsocket(admin.accessToken);
|
||
|
|
|
||
|
|
const files: string[] = [
|
||
|
|
'/albums/nature/prairie_falcon.jpg',
|
||
|
|
'/formats/webp/denali.webp',
|
||
|
|
'/formats/raw/Nikon/D700/philadelphia.nef',
|
||
|
|
'/albums/nature/orychophragmus_violaceus.jpg',
|
||
|
|
'/albums/nature/notocactus_minimus.jpg',
|
||
|
|
'/albums/nature/silver_fir.jpg',
|
||
|
|
'/albums/nature/tanners_ridge.jpg',
|
||
|
|
'/albums/nature/cyclamen_persicum.jpg',
|
||
|
|
'/albums/nature/polemonium_reptans.jpg',
|
||
|
|
'/albums/nature/wood_anemones.jpg',
|
||
|
|
'/formats/heic/IMG_2682.heic',
|
||
|
|
'/formats/jpg/el_torcal_rocks.jpg',
|
||
|
|
'/formats/png/density_plot.png',
|
||
|
|
'/formats/motionphoto/Samsung One UI 6.jpg',
|
||
|
|
'/formats/motionphoto/Samsung One UI 6.heic',
|
||
|
|
'/formats/motionphoto/Samsung One UI 5.jpg',
|
||
|
|
'/formats/raw/Nikon/D80/glarus.nef',
|
||
|
|
'/metadata/gps-position/thompson-springs.jpg',
|
||
|
|
];
|
||
|
|
const assets: AssetFileUploadResponseDto[] = [];
|
||
|
|
for (const filename of files) {
|
||
|
|
const bytes = await readFile(join(testAssetDir, filename));
|
||
|
|
assets.push(
|
||
|
|
await utils.createAsset(admin.accessToken, {
|
||
|
|
deviceAssetId: `test-${filename}`,
|
||
|
|
assetData: { bytes, filename },
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const asset of assets) {
|
||
|
|
await utils.waitForWebsocketEvent({ event: 'upload', assetId: asset.id });
|
||
|
|
}
|
||
|
|
|
||
|
|
[assetFalcon, assetDenali] = assets;
|
||
|
|
});
|
||
|
|
|
||
|
|
afterAll(async () => {
|
||
|
|
await utils.disconnectWebsocket(websocket);
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('POST /search/metadata', () => {
|
||
|
|
it('should require authentication', async () => {
|
||
|
|
const { status, body } = await request(app).post('/search/metadata');
|
||
|
|
expect(status).toBe(401);
|
||
|
|
expect(body).toEqual(errorDto.unauthorized);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should search by camera make', async () => {
|
||
|
|
const { status, body } = await request(app)
|
||
|
|
.post('/search/metadata')
|
||
|
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||
|
|
.send({ make: 'Canon' });
|
||
|
|
expect(status).toBe(200);
|
||
|
|
expect(body).toEqual({
|
||
|
|
albums,
|
||
|
|
assets: {
|
||
|
|
count: 2,
|
||
|
|
items: expect.arrayContaining([
|
||
|
|
expect.objectContaining({ id: assetDenali.id }),
|
||
|
|
expect.objectContaining({ id: assetFalcon.id }),
|
||
|
|
]),
|
||
|
|
facets: [],
|
||
|
|
nextPage: null,
|
||
|
|
total: 2,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should search by camera model', async () => {
|
||
|
|
const { status, body } = await request(app)
|
||
|
|
.post('/search/metadata')
|
||
|
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||
|
|
.send({ model: 'Canon EOS 7D' });
|
||
|
|
expect(status).toBe(200);
|
||
|
|
expect(body).toEqual({
|
||
|
|
albums,
|
||
|
|
assets: {
|
||
|
|
count: 1,
|
||
|
|
items: [expect.objectContaining({ id: assetDenali.id })],
|
||
|
|
facets: [],
|
||
|
|
nextPage: null,
|
||
|
|
total: 1,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('POST /search/smart', () => {
|
||
|
|
it('should require authentication', async () => {
|
||
|
|
const { status, body } = await request(app).post('/search/smart');
|
||
|
|
expect(status).toBe(401);
|
||
|
|
expect(body).toEqual(errorDto.unauthorized);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('GET /search/explore', () => {
|
||
|
|
it('should require authentication', async () => {
|
||
|
|
const { status, body } = await request(app).get('/search/explore');
|
||
|
|
expect(status).toBe(401);
|
||
|
|
expect(body).toEqual(errorDto.unauthorized);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should get explore data', async () => {
|
||
|
|
const { status, body } = await request(app)
|
||
|
|
.get('/search/explore')
|
||
|
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||
|
|
expect(status).toBe(200);
|
||
|
|
expect(body).toEqual([
|
||
|
|
{ fieldName: 'exifInfo.city', items: [] },
|
||
|
|
{ fieldName: 'smartInfo.tags', items: [] },
|
||
|
|
]);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('GET /search/places', () => {
|
||
|
|
it('should require authentication', async () => {
|
||
|
|
const { status, body } = await request(app).get('/search/places');
|
||
|
|
expect(status).toBe(401);
|
||
|
|
expect(body).toEqual(errorDto.unauthorized);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should get places', async () => {
|
||
|
|
const { status, body } = await request(app)
|
||
|
|
.get('/search/places?name=Paris')
|
||
|
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||
|
|
expect(status).toBe(200);
|
||
|
|
expect(Array.isArray(body)).toBe(true);
|
||
|
|
expect(body.length).toBeGreaterThan(10);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('GET /search/suggestions', () => {
|
||
|
|
it('should require authentication', async () => {
|
||
|
|
const { status, body } = await request(app).get('/search/suggestions');
|
||
|
|
expect(status).toBe(401);
|
||
|
|
expect(body).toEqual(errorDto.unauthorized);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should get suggestions for country', async () => {
|
||
|
|
const { status, body } = await request(app)
|
||
|
|
.get('/search/suggestions?type=country')
|
||
|
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||
|
|
expect(body).toEqual(['United States of America']);
|
||
|
|
expect(status).toBe(200);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should get suggestions for state', async () => {
|
||
|
|
const { status, body } = await request(app)
|
||
|
|
.get('/search/suggestions?type=state')
|
||
|
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||
|
|
expect(body).toEqual(['Douglas County, Nebraska', 'Mesa County, Colorado']);
|
||
|
|
expect(status).toBe(200);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should get suggestions for city', async () => {
|
||
|
|
const { status, body } = await request(app)
|
||
|
|
.get('/search/suggestions?type=city')
|
||
|
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||
|
|
expect(body).toEqual(['Palisade', 'Ralston']);
|
||
|
|
expect(status).toBe(200);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should get suggestions for camera make', async () => {
|
||
|
|
const { status, body } = await request(app)
|
||
|
|
.get('/search/suggestions?type=camera-make')
|
||
|
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||
|
|
expect(body).toEqual([
|
||
|
|
'Apple',
|
||
|
|
'Canon',
|
||
|
|
'FUJIFILM',
|
||
|
|
'NIKON CORPORATION',
|
||
|
|
'PENTAX Corporation',
|
||
|
|
'samsung',
|
||
|
|
'SONY',
|
||
|
|
]);
|
||
|
|
expect(status).toBe(200);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should get suggestions for camera model', async () => {
|
||
|
|
const { status, body } = await request(app)
|
||
|
|
.get('/search/suggestions?type=camera-model')
|
||
|
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||
|
|
expect(body).toEqual([
|
||
|
|
'Canon EOS 7D',
|
||
|
|
'Canon EOS R5',
|
||
|
|
'DSLR-A550',
|
||
|
|
'FinePix S3Pro',
|
||
|
|
'iPhone 7',
|
||
|
|
'NIKON D700',
|
||
|
|
'NIKON D750',
|
||
|
|
'NIKON D80',
|
||
|
|
'PENTAX K10D',
|
||
|
|
'SM-F711N',
|
||
|
|
'SM-S906U',
|
||
|
|
'SM-T970',
|
||
|
|
]);
|
||
|
|
expect(status).toBe(200);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|