import { BadRequestException } from '@nestjs/common'; import { mapAsset } from 'src/dtos/asset-response.dto'; import { SearchSuggestionType } from 'src/dtos/search.dto'; import { SearchService } from 'src/services/search.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { personStub } from 'test/fixtures/person.stub'; import { newTestService, ServiceMocks } from 'test/utils'; import { beforeEach, vitest } from 'vitest'; vitest.useFakeTimers(); describe(SearchService.name, () => { let sut: SearchService; let mocks: ServiceMocks; beforeEach(() => { ({ sut, mocks } = newTestService(SearchService)); mocks.partner.getAll.mockResolvedValue([]); }); it('should work', () => { expect(sut).toBeDefined(); }); describe('searchPerson', () => { it('should pass options to search', async () => { const { name } = personStub.withName; mocks.person.getByName.mockResolvedValue([]); await sut.searchPerson(authStub.user1, { name, withHidden: false }); expect(mocks.person.getByName).toHaveBeenCalledWith(authStub.user1.user.id, name, { withHidden: false }); await sut.searchPerson(authStub.user1, { name, withHidden: true }); expect(mocks.person.getByName).toHaveBeenCalledWith(authStub.user1.user.id, name, { withHidden: true }); }); }); describe('searchPlaces', () => { it('should search places', async () => { mocks.search.searchPlaces.mockResolvedValue([ { id: 42, name: 'my place', latitude: 420, longitude: 69, admin1Code: null, admin1Name: null, admin2Code: null, admin2Name: null, alternateNames: null, countryCode: 'US', modificationDate: new Date(), }, ]); await sut.searchPlaces({ name: 'place' }); expect(mocks.search.searchPlaces).toHaveBeenCalledWith('place'); }); }); describe('getExploreData', () => { it('should get assets by city and tag', async () => { mocks.asset.getAssetIdByCity.mockResolvedValue({ fieldName: 'exifInfo.city', items: [{ value: 'test-city', data: assetStub.withLocation.id }], }); mocks.asset.getByIdsWithAllRelationsButStacks.mockResolvedValue([assetStub.withLocation]); const expectedResponse = [ { fieldName: 'exifInfo.city', items: [{ value: 'test-city', data: mapAsset(assetStub.withLocation) }] }, ]; const result = await sut.getExploreData(authStub.user1); expect(result).toEqual(expectedResponse); }); }); describe('getSearchSuggestions', () => { it('should return search suggestions for country', async () => { mocks.search.getCountries.mockResolvedValue(['USA']); mocks.partner.getAll.mockResolvedValue([]); await expect( sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.COUNTRY }), ).resolves.toEqual(['USA']); expect(mocks.search.getCountries).toHaveBeenCalledWith([authStub.user1.user.id]); }); it('should return search suggestions for country (including null)', async () => { mocks.search.getCountries.mockResolvedValue(['USA']); mocks.partner.getAll.mockResolvedValue([]); await expect( sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.COUNTRY }), ).resolves.toEqual(['USA', null]); expect(mocks.search.getCountries).toHaveBeenCalledWith([authStub.user1.user.id]); }); it('should return search suggestions for state', async () => { mocks.search.getStates.mockResolvedValue(['California']); mocks.partner.getAll.mockResolvedValue([]); await expect( sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.STATE }), ).resolves.toEqual(['California']); expect(mocks.search.getStates).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything()); }); it('should return search suggestions for state (including null)', async () => { mocks.search.getStates.mockResolvedValue(['California']); mocks.partner.getAll.mockResolvedValue([]); await expect( sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.STATE }), ).resolves.toEqual(['California', null]); expect(mocks.search.getStates).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything()); }); it('should return search suggestions for city', async () => { mocks.search.getCities.mockResolvedValue(['Denver']); mocks.partner.getAll.mockResolvedValue([]); await expect( sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.CITY }), ).resolves.toEqual(['Denver']); expect(mocks.search.getCities).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything()); }); it('should return search suggestions for city (including null)', async () => { mocks.search.getCities.mockResolvedValue(['Denver']); mocks.partner.getAll.mockResolvedValue([]); await expect( sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.CITY }), ).resolves.toEqual(['Denver', null]); expect(mocks.search.getCities).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything()); }); it('should return search suggestions for camera make', async () => { mocks.search.getCameraMakes.mockResolvedValue(['Nikon']); mocks.partner.getAll.mockResolvedValue([]); await expect( sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.CAMERA_MAKE }), ).resolves.toEqual(['Nikon']); expect(mocks.search.getCameraMakes).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything()); }); it('should return search suggestions for camera make (including null)', async () => { mocks.search.getCameraMakes.mockResolvedValue(['Nikon']); mocks.partner.getAll.mockResolvedValue([]); await expect( sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.CAMERA_MAKE }), ).resolves.toEqual(['Nikon', null]); expect(mocks.search.getCameraMakes).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything()); }); it('should return search suggestions for camera model', async () => { mocks.search.getCameraModels.mockResolvedValue(['Fujifilm X100VI']); mocks.partner.getAll.mockResolvedValue([]); await expect( sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.CAMERA_MODEL }), ).resolves.toEqual(['Fujifilm X100VI']); expect(mocks.search.getCameraModels).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything()); }); it('should return search suggestions for camera model (including null)', async () => { mocks.search.getCameraModels.mockResolvedValue(['Fujifilm X100VI']); mocks.partner.getAll.mockResolvedValue([]); await expect( sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.CAMERA_MODEL }), ).resolves.toEqual(['Fujifilm X100VI', null]); expect(mocks.search.getCameraModels).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything()); }); it('should return search suggestions for camera lens model', async () => { mocks.search.getCameraLensModels.mockResolvedValue(['10-24mm']); mocks.partner.getAll.mockResolvedValue([]); await expect( sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.CAMERA_LENS_MODEL }), ).resolves.toEqual(['10-24mm']); expect(mocks.search.getCameraLensModels).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything()); }); it('should return search suggestions for camera lens model (including null)', async () => { mocks.search.getCameraLensModels.mockResolvedValue(['10-24mm']); mocks.partner.getAll.mockResolvedValue([]); await expect( sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.CAMERA_LENS_MODEL }), ).resolves.toEqual(['10-24mm', null]); expect(mocks.search.getCameraLensModels).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything()); }); }); describe('searchSmart', () => { beforeEach(() => { mocks.search.searchSmart.mockResolvedValue({ hasNextPage: false, items: [] }); mocks.machineLearning.encodeText.mockResolvedValue('[1, 2, 3]'); }); it('should raise a BadRequestException if machine learning is disabled', async () => { mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { enabled: false }, }); await expect(sut.searchSmart(authStub.user1, { query: 'test' })).rejects.toThrowError( new BadRequestException('Smart search is not enabled'), ); }); it('should raise a BadRequestException if smart search is disabled', async () => { mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { clip: { enabled: false } }, }); await expect(sut.searchSmart(authStub.user1, { query: 'test' })).rejects.toThrowError( new BadRequestException('Smart search is not enabled'), ); }); it('should work', async () => { await sut.searchSmart(authStub.user1, { query: 'test' }); expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith( 'test', expect.objectContaining({ modelName: expect.any(String) }), ); expect(mocks.search.searchSmart).toHaveBeenCalledWith( { page: 1, size: 100 }, { query: 'test', embedding: '[1, 2, 3]', userIds: [authStub.user1.user.id] }, ); }); it('should consider page and size parameters', async () => { await sut.searchSmart(authStub.user1, { query: 'test', page: 2, size: 50 }); expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith( 'test', expect.objectContaining({ modelName: expect.any(String) }), ); expect(mocks.search.searchSmart).toHaveBeenCalledWith( { page: 2, size: 50 }, expect.objectContaining({ query: 'test', embedding: '[1, 2, 3]', userIds: [authStub.user1.user.id] }), ); }); it('should use clip model specified in config', async () => { mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { clip: { modelName: 'ViT-B-16-SigLIP__webli' } }, }); await sut.searchSmart(authStub.user1, { query: 'test' }); expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith( 'test', expect.objectContaining({ modelName: 'ViT-B-16-SigLIP__webli' }), ); }); it('should use language specified in request', async () => { await sut.searchSmart(authStub.user1, { query: 'test', language: 'de' }); expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith( 'test', expect.objectContaining({ language: 'de' }), ); }); }); });