diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index ec09d71d21..32cbdd6df8 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -3,11 +3,11 @@ import { AssetMediaStatus, AssetResponseDto, AssetTypeEnum, - LoginResponseDto, - SharedLinkType, getAssetInfo, getConfig, getMyUser, + LoginResponseDto, + SharedLinkType, updateConfig, } from '@immich/sdk'; import { exiftool } from 'exiftool-vendored'; @@ -19,7 +19,7 @@ import { Socket } from 'socket.io-client'; import { createUserDto, uuidDto } from 'src/fixtures'; import { makeRandomImage } from 'src/generators'; import { errorDto } from 'src/responses'; -import { app, asBearerAuth, tempDir, testAssetDir, utils } from 'src/utils'; +import { app, asBearerAuth, tempDir, TEN_TIMES, testAssetDir, utils } from 'src/utils'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; @@ -41,8 +41,6 @@ const makeUploadDto = (options?: { omit: string }): Record<string, any> => { return dto; }; -const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; - const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`; const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`; const facesAssetFilepath = `${testAssetDir}/metadata/faces/portrait.jpg`; diff --git a/e2e/src/api/specs/search.e2e-spec.ts b/e2e/src/api/specs/search.e2e-spec.ts index 11bb37be18..50fce29ce0 100644 --- a/e2e/src/api/specs/search.e2e-spec.ts +++ b/e2e/src/api/specs/search.e2e-spec.ts @@ -1,10 +1,10 @@ -import { AssetMediaResponseDto, LoginResponseDto, deleteAssets, updateAsset } from '@immich/sdk'; +import { AssetMediaResponseDto, AssetResponseDto, deleteAssets, LoginResponseDto, updateAsset } from '@immich/sdk'; import { DateTime } from 'luxon'; import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { Socket } from 'socket.io-client'; import { errorDto } from 'src/responses'; -import { app, asBearerAuth, testAssetDir, utils } from 'src/utils'; +import { app, asBearerAuth, TEN_TIMES, testAssetDir, utils } from 'src/utils'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; const today = DateTime.now(); @@ -462,6 +462,55 @@ describe('/search', () => { }); }); + describe('POST /search/random', () => { + beforeAll(async () => { + await Promise.all([ + utils.createAsset(admin.accessToken), + utils.createAsset(admin.accessToken), + utils.createAsset(admin.accessToken), + utils.createAsset(admin.accessToken), + utils.createAsset(admin.accessToken), + utils.createAsset(admin.accessToken), + ]); + + await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration'); + }); + + it('should require authentication', async () => { + const { status, body } = await request(app).post('/search/random').send({ size: 1 }); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it.each(TEN_TIMES)('should return 1 random assets', async () => { + const { status, body } = await request(app) + .post('/search/random') + .send({ size: 1 }) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + + const assets: AssetResponseDto[] = body; + expect(assets.length).toBe(1); + expect(assets[0].ownerId).toBe(admin.userId); + }); + + it.each(TEN_TIMES)('should return 2 random assets', async () => { + const { status, body } = await request(app) + .post('/search/random') + .send({ size: 2 }) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + + const assets: AssetResponseDto[] = body; + expect(assets.length).toBe(2); + expect(assets[0].ownerId).toBe(admin.userId); + expect(assets[1].ownerId).toBe(admin.userId); + }); + }); + describe('GET /search/explore', () => { it('should require authentication', async () => { const { status, body } = await request(app).get('/search/explore'); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 7b80ba49aa..efd9ce76b9 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -76,6 +76,7 @@ export const immichCli = (args: string[]) => export const immichAdmin = (args: string[]) => executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]); export const specialCharStrings = ["'", '"', ',', '{', '}', '*']; +export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; const executeCommand = (command: string, args: string[]) => { let _resolve: (value: CommandResponse) => void; diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql index 1e50943781..2d5da4d381 100644 --- a/server/src/queries/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -60,6 +60,8 @@ union all limit $14 ) +limit + $15 -- SearchRepository.searchSmart select diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index 9abee70de3..fb59157c80 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -69,12 +69,13 @@ export class SearchRepository implements ISearchRepository { }, ], }) - searchRandom(size: number, options: AssetSearchOptions): Promise<AssetEntity[]> { + async searchRandom(size: number, options: AssetSearchOptions): Promise<AssetEntity[]> { const uuid = randomUUID(); const builder = searchAssetBuilder(this.db, options); const lessThan = builder.where('assets.id', '<', uuid).orderBy('assets.id').limit(size); const greaterThan = builder.where('assets.id', '>', uuid).orderBy('assets.id').limit(size); - return sql`${lessThan} union all ${greaterThan}`.execute(this.db) as any as Promise<AssetEntity[]>; + const { rows } = await sql`${lessThan} union all ${greaterThan} limit ${size}`.execute(this.db); + return rows as any as AssetEntity[]; } @GenerateSql({