@ -4,25 +4,32 @@ import { plainToInstance } from 'class-transformer';
import {
albumStub ,
assetEntityStub ,
asyncTick ,
authStub ,
newAlbumRepositoryMock ,
newAssetRepositoryMock ,
newJobRepositoryMock ,
newMachineLearningRepositoryMock ,
newSearchRepositoryMock ,
searchStub ,
} from '../../test' ;
import { IAlbumRepository } from '../album/album.repository' ;
import { IAssetRepository } from '../asset/asset.repository' ;
import { JobName } from '../job' ;
import { IJobRepository } from '../job/job.repository' ;
import { IMachineLearningRepository } from '../smart-info' ;
import { SearchDto } from './dto' ;
import { ISearchRepository } from './search.repository' ;
import { SearchService } from './search.service' ;
jest . useFakeTimers ( ) ;
describe ( SearchService . name , ( ) = > {
let sut : SearchService ;
let albumMock : jest.Mocked < IAlbumRepository > ;
let assetMock : jest.Mocked < IAssetRepository > ;
let jobMock : jest.Mocked < IJobRepository > ;
let machineMock : jest.Mocked < IMachineLearningRepository > ;
let searchMock : jest.Mocked < ISearchRepository > ;
let configMock : jest.Mocked < ConfigService > ;
@ -30,10 +37,15 @@ describe(SearchService.name, () => {
albumMock = newAlbumRepositoryMock ( ) ;
assetMock = newAssetRepositoryMock ( ) ;
jobMock = newJobRepositoryMock ( ) ;
machineMock = newMachineLearningRepositoryMock ( ) ;
searchMock = newSearchRepositoryMock ( ) ;
configMock = { get : jest . fn ( ) } as unknown as jest . Mocked < ConfigService > ;
sut = new SearchService ( albumMock , assetMock , jobMock , searchMock , configMock ) ;
sut = new SearchService ( albumMock , assetMock , jobMock , machineMock , searchMock , configMock ) ;
} ) ;
afterEach ( ( ) = > {
sut . teardown ( ) ;
} ) ;
it ( 'should work' , ( ) = > {
@ -69,7 +81,7 @@ describe(SearchService.name, () => {
it ( 'should be disabled via an env variable' , ( ) = > {
configMock . get . mockReturnValue ( 'false' ) ;
sut = new SearchService ( albumMock , assetMock , job Mock, searchMock , configMock ) ;
const sut = new SearchService ( albumMock , assetMock , job Mock, machine Mock, searchMock , configMock ) ;
expect ( sut . isEnabled ( ) ) . toBe ( false ) ;
} ) ;
@ -82,7 +94,7 @@ describe(SearchService.name, () => {
it ( 'should return the config when search is disabled' , ( ) = > {
configMock . get . mockReturnValue ( 'false' ) ;
sut = new SearchService ( albumMock , assetMock , job Mock, searchMock , configMock ) ;
const sut = new SearchService ( albumMock , assetMock , job Mock, machine Mock, searchMock , configMock ) ;
expect ( sut . getConfig ( ) ) . toEqual ( { enabled : false } ) ;
} ) ;
@ -91,13 +103,15 @@ describe(SearchService.name, () => {
describe ( ` bootstrap ` , ( ) = > {
it ( 'should skip when search is disabled' , async ( ) = > {
configMock . get . mockReturnValue ( 'false' ) ;
sut = new SearchService ( albumMock , assetMock , job Mock, searchMock , configMock ) ;
const sut = new SearchService ( albumMock , assetMock , job Mock, machine Mock, searchMock , configMock ) ;
await sut . bootstrap ( ) ;
expect ( searchMock . setup ) . not . toHaveBeenCalled ( ) ;
expect ( searchMock . checkMigrationStatus ) . not . toHaveBeenCalled ( ) ;
expect ( jobMock . queue ) . not . toHaveBeenCalled ( ) ;
sut . teardown ( ) ;
} ) ;
it ( 'should skip schema migration if not needed' , async ( ) = > {
@ -123,21 +137,18 @@ describe(SearchService.name, () => {
describe ( 'search' , ( ) = > {
it ( 'should throw an error is search is disabled' , async ( ) = > {
configMock . get . mockReturnValue ( 'false' ) ;
sut = new SearchService ( albumMock , assetMock , job Mock, searchMock , configMock ) ;
const sut = new SearchService ( albumMock , assetMock , job Mock, machine Mock, searchMock , configMock ) ;
await expect ( sut . search ( authStub . admin , { } ) ) . rejects . toBeInstanceOf ( BadRequestException ) ;
expect ( searchMock . search ) . not . toHaveBeenCalled ( ) ;
expect ( searchMock . searchAlbums ) . not . toHaveBeenCalled ( ) ;
expect ( searchMock . searchAssets ) . not . toHaveBeenCalled ( ) ;
} ) ;
it ( 'should search assets and albums' , async ( ) = > {
searchMock . search . mockResolvedValue ( {
total : 0 ,
count : 0 ,
page : 1 ,
items : [ ] ,
facets : [ ] ,
} ) ;
searchMock . searchAssets . mockResolvedValue ( searchStub . emptyResults ) ;
searchMock . searchAlbums . mockResolvedValue ( searchStub . emptyResults ) ;
searchMock . vectorSearch . mockResolvedValue ( searchStub . emptyResults ) ;
await expect ( sut . search ( authStub . admin , { } ) ) . resolves . toEqual ( {
albums : {
@ -156,162 +167,158 @@ describe(SearchService.name, () => {
} ,
} ) ;
expect ( searchMock . search . mock . calls ) . toEqual ( [
[ 'assets' , '*' , { userId : authStub.admin.id } ] ,
[ 'albums' , '*' , { userId : authStub.admin.id } ] ,
] ) ;
// expect(searchMock.searchAssets).toHaveBeenCalledWith('*', { userId: authStub.admin.id });
expect ( searchMock . searchAlbums ) . toHaveBeenCalledWith ( '*' , { userId : authStub.admin.id } ) ;
} ) ;
} ) ;
describe ( 'handleIndexAssets' , ( ) = > {
it ( 'should skip if search is disabled' , async ( ) = > {
configMock . get . mockReturnValue ( 'false' ) ;
sut = new SearchService ( albumMock , assetMock , jobMock , searchMock , configMock ) ;
await sut . handleIndexAssets ( ) ;
expect ( searchMock . import ) . not . toHaveBeenCalled ( ) ;
} ) ;
it ( 'should index all the assets' , async ( ) = > {
assetMock . getAll . mockResolvedValue ( [ ] ) ;
assetMock . getAll . mockResolvedValue ( [ assetEntityStub . image ] ) ;
await sut . handleIndexAssets ( ) ;
expect ( searchMock . import ) . toHaveBeenCalledWith ( 'assets' , [ ] , true ) ;
expect ( searchMock . importAssets ) . toHaveBeenCalledWith ( [ assetEntityStub . image ] , true ) ;
} ) ;
it ( 'should log an error' , async ( ) = > {
assetMock . getAll . mockResolvedValue ( [ ] ) ;
searchMock . import . mockRejectedValue ( new Error ( 'import failed' ) ) ;
assetMock . getAll . mockResolvedValue ( [ assetEntityStub . image ] ) ;
searchMock . importAssets . mockRejectedValue ( new Error ( 'import failed' ) ) ;
await sut . handleIndexAssets ( ) ;
expect ( searchMock . importAssets ) . toHaveBeenCalled ( ) ;
} ) ;
} ) ;
describe ( 'handleIndexAsset' , ( ) = > {
it ( 'should skip if search is disabled' , async ( ) = > {
configMock . get . mockReturnValue ( 'false' ) ;
sut = new SearchService ( albumMock , assetMock , job Mock, searchMock , configMock ) ;
const sut = new SearchService ( albumMock , assetMock , job Mock, machine Mock, searchMock , configMock ) ;
await sut . handleIndexAsset ( { asset : assetEntityStub.image } ) ;
await sut . handleIndexAsset s ( ) ;
expect ( searchMock . index ) . not . toHaveBeenCalled ( ) ;
expect ( searchMock . importAssets ) . not . toHaveBeenCalled ( ) ;
expect ( searchMock . importAlbums ) . not . toHaveBeenCalled ( ) ;
} ) ;
} ) ;
it ( 'should index the asset' , async ( ) = > {
await sut . handleIndexAsset ( { asset : assetEntityStub.image } ) ;
expect ( searchMock . index ) . toHaveBeenCalledWith ( 'assets' , assetEntityStub . image ) ;
describe ( 'handleIndexAsset' , ( ) = > {
it ( 'should skip if search is disabled' , ( ) = > {
configMock . get . mockReturnValue ( 'false' ) ;
const sut = new SearchService ( albumMock , assetMock , jobMock , machineMock , searchMock , configMock ) ;
sut . handleIndexAsset ( { ids : [ assetEntityStub . image . id ] } ) ;
} ) ;
it ( 'should log an error' , async ( ) = > {
searchMock . index . mockRejectedValue ( new Error ( 'index failed' ) ) ;
await sut . handleIndexAsset ( { asset : assetEntityStub.image } ) ;
expect ( searchMock . index ) . toHaveBeenCalled ( ) ;
it ( 'should index the asset' , ( ) = > {
sut . handleIndexAsset ( { ids : [ assetEntityStub . image . id ] } ) ;
} ) ;
} ) ;
describe ( 'handleIndexAlbums' , ( ) = > {
it ( 'should skip if search is disabled' , async ( ) = > {
it ( 'should skip if search is disabled' , ( ) = > {
configMock . get . mockReturnValue ( 'false' ) ;
sut = new SearchService ( albumMock , assetMock , jobMock , searchMock , configMock ) ;
await sut . handleIndexAlbums ( ) ;
expect ( searchMock . import ) . not . toHaveBeenCalled ( ) ;
const sut = new SearchService ( albumMock , assetMock , jobMock , machineMock , searchMock , configMock ) ;
sut . handleIndexAlbums ( ) ;
} ) ;
it ( 'should index all the albums' , async ( ) = > {
albumMock . getAll . mockResolvedValue ( [ ] ) ;
albumMock . getAll . mockResolvedValue ( [ albumStub . empty ] ) ;
await sut . handleIndexAlbums ( ) ;
expect ( searchMock . import ) . toHaveBeenCalledWith ( 'albums' , [ ] , true ) ;
expect ( searchMock . importAlbums ) . toHaveBeenCalledWith ( [ albumStub . empty ] , true ) ;
} ) ;
it ( 'should log an error' , async ( ) = > {
albumMock . getAll . mockResolvedValue ( [ ] ) ;
searchMock . import . mockRejectedValue ( new Error ( 'import failed' ) ) ;
albumMock . getAll . mockResolvedValue ( [ albumStub . empty ] ) ;
searchMock . importAlbums . mockRejectedValue ( new Error ( 'import failed' ) ) ;
await sut . handleIndexAlbums ( ) ;
expect ( searchMock . importAlbums ) . toHaveBeenCalled ( ) ;
} ) ;
} ) ;
describe ( 'handleIndexAlbum' , ( ) = > {
it ( 'should skip if search is disabled' , async ( ) = > {
it ( 'should skip if search is disabled' , ( ) = > {
configMock . get . mockReturnValue ( 'false' ) ;
sut = new SearchService ( albumMock , assetMock , jobMock , searchMock , configMock ) ;
await sut . handleIndexAlbum ( { album : albumStub.empty } ) ;
expect ( searchMock . index ) . not . toHaveBeenCalled ( ) ;
const sut = new SearchService ( albumMock , assetMock , jobMock , machineMock , searchMock , configMock ) ;
sut . handleIndexAlbum ( { ids : [ albumStub . empty . id ] } ) ;
} ) ;
it ( 'should index the album' , async ( ) = > {
await sut . handleIndexAlbum ( { album : albumStub.empty } ) ;
it ( 'should index the album' , ( ) = > {
sut . handleIndexAlbum ( { ids : [ albumStub . empty . id ] } ) ;
} ) ;
} ) ;
expect ( searchMock . index ) . toHaveBeenCalledWith ( 'albums' , albumStub . empty ) ;
describe ( 'handleRemoveAlbum' , ( ) = > {
it ( 'should skip if search is disabled' , ( ) = > {
configMock . get . mockReturnValue ( 'false' ) ;
const sut = new SearchService ( albumMock , assetMock , jobMock , machineMock , searchMock , configMock ) ;
sut . handleRemoveAlbum ( { ids : [ 'album1' ] } ) ;
} ) ;
it ( 'should log an error' , async ( ) = > {
searchMock . index . mockRejectedValue ( new Error ( 'index failed' ) ) ;
it ( 'should remove the album' , ( ) = > {
sut . handleRemoveAlbum ( { ids : [ 'album1' ] } ) ;
} ) ;
} ) ;
await sut . handleIndexAlbum ( { album : albumStub.empty } ) ;
describe ( 'handleRemoveAsset' , ( ) = > {
it ( 'should skip if search is disabled' , ( ) = > {
configMock . get . mockReturnValue ( 'false' ) ;
const sut = new SearchService ( albumMock , assetMock , jobMock , machineMock , searchMock , configMock ) ;
sut . handleRemoveAsset ( { ids : [ 'asset1' ] } ) ;
} ) ;
expect ( searchMock . index ) . toHaveBeenCalled ( ) ;
it ( 'should remove the asset' , ( ) = > {
sut . handleRemoveAsset ( { ids : [ 'asset1' ] } ) ;
} ) ;
} ) ;
describe ( 'handleRemoveAlbum' , ( ) = > {
it ( 'should skip if search is disabled' , async ( ) = > {
configMock . get . mockReturnValue ( 'false' ) ;
sut = new SearchService ( albumMock , assetMock , jobMock , searchMock , configMock ) ;
describe ( 'flush' , ( ) = > {
it ( 'should flush queued album updates' , async ( ) = > {
albumMock . getByIds . mockResolvedValue ( [ albumStub . empty ] ) ;
await sut . handleRemoveAlbum ( { id : 'album1' } ) ;
sut . handleIndexAlbum ( { ids : [ 'album1' ] } ) ;
expect ( searchMock . delete ) . not . toHaveBeenCalled ( ) ;
} ) ;
jest . runOnlyPendingTimers ( ) ;
it ( 'should remove the album' , async ( ) = > {
await sut . handleRemoveAlbum ( { id : 'album1' } ) ;
await asyncTick ( 4 ) ;
expect ( searchMock . delete ) . toHaveBeenCalledWith ( 'albums' , 'album1' ) ;
expect ( albumMock . getByIds ) . toHaveBeenCalledWith ( [ 'album1' ] ) ;
expect ( searchMock . importAlbums ) . toHaveBeenCalledWith ( [ albumStub . empty ] , false ) ;
} ) ;
it ( 'should log an error' , async ( ) = > {
searchMock . delete . mockRejectedValue ( new Error ( 'remove failed' ) ) ;
it ( 'should flush queued album deletes' , async ( ) = > {
sut . handleRemoveAlbum ( { ids : [ 'album1' ] } ) ;
jest . runOnlyPendingTimers ( ) ;
await sut . handleRemoveAlbum ( { id : 'album1' } ) ;
await asyncTick( 4 ) ;
expect ( searchMock . delete ) . toHaveBeenCalled ( ) ;
expect ( searchMock . deleteAlbums ) . toHaveBeenCalledWith ( [ 'album1' ] ) ;
} ) ;
} ) ;
describe ( 'handleRemoveAsset' , ( ) = > {
it ( 'should skip if search is disabled' , async ( ) = > {
configMock . get . mockReturnValue ( 'false' ) ;
sut = new SearchService ( albumMock , assetMock , jobMock , searchMock , configMock ) ;
it ( 'should flush queued asset updates' , async ( ) = > {
assetMock . getByIds . mockResolvedValue ( [ assetEntityStub . image ] ) ;
await sut . handleRemoveAsset ( { id : 'asset1`' } ) ;
sut . handleIndexAsset ( { ids : [ 'asset1' ] } ) ;
expect ( searchMock . delete ) . not . toHaveBeenCalled ( ) ;
} ) ;
jest . runOnlyPendingTimers ( ) ;
it ( 'should remove the asset' , async ( ) = > {
await sut . handleRemoveAsset ( { id : 'asset1' } ) ;
await asyncTick ( 4 ) ;
expect ( searchMock . delete ) . toHaveBeenCalledWith ( 'assets' , 'asset1' ) ;
expect ( assetMock . getByIds ) . toHaveBeenCalledWith ( [ 'asset1' ] ) ;
expect ( searchMock . importAssets ) . toHaveBeenCalledWith ( [ assetEntityStub . image ] , false ) ;
} ) ;
it ( 'should log an error' , async ( ) = > {
searchMock . delete . mockRejectedValue ( new Error ( 'remove failed' ) ) ;
it ( 'should flush queued asset deletes' , async ( ) = > {
sut . handleRemoveAsset ( { ids : [ 'asset1' ] } ) ;
jest . runOnlyPendingTimers ( ) ;
await sut . handleRemoveAsset ( { id : 'asset1' } ) ;
await asyncTick( 4 ) ;
expect ( searchMock . delete ) . toHaveBeenCalled ( ) ;
expect ( searchMock . deleteAssets ) . toHaveBeenCalledWith ( [ 'asset1' ] ) ;
} ) ;
} ) ;
} ) ;