diff --git a/server/src/repositories/notification.repository.spec.ts b/server/src/repositories/notification.repository.spec.ts index 6253697087..6d17501d03 100644 --- a/server/src/repositories/notification.repository.spec.ts +++ b/server/src/repositories/notification.repository.spec.ts @@ -1,11 +1,12 @@ +import { LoggingRepository } from 'src/repositories/logging.repository'; import { EmailRenderRequest, EmailTemplate, NotificationRepository } from 'src/repositories/notification.repository'; -import { newFakeLoggingRepository } from 'test/repositories/logger.repository.mock'; +import { automock } from 'test/utils'; describe(NotificationRepository.name, () => { let sut: NotificationRepository; beforeEach(() => { - sut = new NotificationRepository(newFakeLoggingRepository()); + sut = new NotificationRepository(automock(LoggingRepository, { args: [, { getEnv: () => ({}) }], strict: false })); }); describe('renderEmail', () => { diff --git a/server/src/repositories/storage.repository.spec.ts b/server/src/repositories/storage.repository.spec.ts index 93b21a7f9b..85ff4a746f 100644 --- a/server/src/repositories/storage.repository.spec.ts +++ b/server/src/repositories/storage.repository.spec.ts @@ -1,7 +1,8 @@ import mockfs from 'mock-fs'; import { CrawlOptionsDto } from 'src/dtos/library.dto'; +import { LoggingRepository } from 'src/repositories/logging.repository'; import { StorageRepository } from 'src/repositories/storage.repository'; -import { newFakeLoggingRepository } from 'test/repositories/logger.repository.mock'; +import { automock } from 'test/utils'; interface Test { test: string; @@ -182,7 +183,7 @@ describe(StorageRepository.name, () => { let sut: StorageRepository; beforeEach(() => { - sut = new StorageRepository(newFakeLoggingRepository()); + sut = new StorageRepository(automock(LoggingRepository, { args: [, { getEnv: () => ({}) }], strict: false })); }); afterEach(() => { diff --git a/server/src/services/activity.service.spec.ts b/server/src/services/activity.service.spec.ts index 368c8f2575..1e5bb3b505 100644 --- a/server/src/services/activity.service.spec.ts +++ b/server/src/services/activity.service.spec.ts @@ -146,6 +146,7 @@ describe(ActivityService.name, () => { const activity = factory.activity(); mocks.access.activity.checkOwnerAccess.mockResolvedValue(new Set([activity.id])); + mocks.activity.delete.mockResolvedValue(); await sut.delete(factory.auth(), activity.id); @@ -156,6 +157,7 @@ describe(ActivityService.name, () => { const activity = factory.activity(); mocks.access.activity.checkAlbumOwnerAccess.mockResolvedValue(new Set([activity.id])); + mocks.activity.delete.mockResolvedValue(); await sut.delete(factory.auth(), activity.id); diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index 832ed59dd5..62d1326cab 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -347,6 +347,7 @@ describe(AlbumService.name, () => { it('should remove a shared user from an owned album', async () => { mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithUser.id])); mocks.album.getById.mockResolvedValue(albumStub.sharedWithUser); + mocks.albumUser.delete.mockResolvedValue(); await expect( sut.removeUser(authStub.admin, albumStub.sharedWithUser.id, userStub.user1.id), @@ -376,6 +377,7 @@ describe(AlbumService.name, () => { it('should allow a shared user to remove themselves', async () => { mocks.album.getById.mockResolvedValue(albumStub.sharedWithUser); + mocks.albumUser.delete.mockResolvedValue(); await sut.removeUser(authStub.user1, albumStub.sharedWithUser.id, authStub.user1.user.id); @@ -388,6 +390,7 @@ describe(AlbumService.name, () => { it('should allow a shared user to remove themselves using "me"', async () => { mocks.album.getById.mockResolvedValue(albumStub.sharedWithUser); + mocks.albumUser.delete.mockResolvedValue(); await sut.removeUser(authStub.user1, albumStub.sharedWithUser.id, 'me'); @@ -422,6 +425,8 @@ describe(AlbumService.name, () => { describe('updateUser', () => { it('should update user role', async () => { mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id])); + mocks.albumUser.update.mockResolvedValue(null as any); + await sut.updateUser(authStub.user1, albumStub.sharedWithAdmin.id, userStub.admin.id, { role: AlbumUserRole.EDITOR, }); diff --git a/server/src/services/api-key.service.spec.ts b/server/src/services/api-key.service.spec.ts index 53266b493e..0a89c04b0d 100644 --- a/server/src/services/api-key.service.spec.ts +++ b/server/src/services/api-key.service.spec.ts @@ -67,6 +67,8 @@ describe(ApiKeyService.name, () => { const id = newUuid(); const auth = factory.auth(); + mocks.apiKey.getById.mockResolvedValue(void 0); + await expect(sut.update(auth, id, { name: 'New Name' })).rejects.toBeInstanceOf(BadRequestException); expect(mocks.apiKey.update).not.toHaveBeenCalledWith(id); @@ -91,6 +93,8 @@ describe(ApiKeyService.name, () => { const auth = factory.auth(); const id = newUuid(); + mocks.apiKey.getById.mockResolvedValue(void 0); + await expect(sut.delete(auth, id)).rejects.toBeInstanceOf(BadRequestException); expect(mocks.apiKey.delete).not.toHaveBeenCalledWith(id); @@ -101,6 +105,7 @@ describe(ApiKeyService.name, () => { const apiKey = factory.apiKey({ userId: auth.user.id }); mocks.apiKey.getById.mockResolvedValue(apiKey); + mocks.apiKey.delete.mockResolvedValue(); await sut.delete(auth, apiKey.id); @@ -113,6 +118,8 @@ describe(ApiKeyService.name, () => { const auth = factory.auth(); const id = newUuid(); + mocks.apiKey.getById.mockResolvedValue(void 0); + await expect(sut.getById(auth, id)).rejects.toBeInstanceOf(BadRequestException); expect(mocks.apiKey.getById).toHaveBeenCalledWith(auth.user.id, id); diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index 9286008581..ca3490a1c0 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -127,8 +127,11 @@ describe(AssetService.name, () => { describe('getRandom', () => { it('should get own random assets', async () => { + mocks.partner.getAll.mockResolvedValue([]); mocks.asset.getRandom.mockResolvedValue([assetStub.image]); + await sut.getRandom(authStub.admin, 1); + expect(mocks.asset.getRandom).toHaveBeenCalledWith([authStub.admin.user.id], 1); }); @@ -531,6 +534,7 @@ describe(AssetService.name, () => { }); it('should update stack primary asset if deleted asset was primary asset in a stack', async () => { + mocks.stack.update.mockResolvedValue(factory.stack() as unknown as any); mocks.asset.getById.mockResolvedValue(assetStub.primaryImage as AssetEntity); await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true }); @@ -542,6 +546,7 @@ describe(AssetService.name, () => { }); it('should delete the entire stack if deleted asset was the primary asset and the stack would only contain one asset afterwards', async () => { + mocks.stack.delete.mockResolvedValue(); mocks.asset.getById.mockResolvedValue({ ...assetStub.primaryImage, stack: { ...assetStub.primaryImage.stack, assets: assetStub.primaryImage.stack!.assets.slice(0, 2) }, diff --git a/server/src/services/audit.service.spec.ts b/server/src/services/audit.service.spec.ts index f5e588cdd0..6ef139f506 100644 --- a/server/src/services/audit.service.spec.ts +++ b/server/src/services/audit.service.spec.ts @@ -18,7 +18,10 @@ describe(AuditService.name, () => { describe('handleCleanup', () => { it('should delete old audit entries', async () => { + mocks.audit.removeBefore.mockResolvedValue(); + await expect(sut.handleCleanup()).resolves.toBe(JobStatus.SUCCESS); + expect(mocks.audit.removeBefore).toHaveBeenCalledWith(expect.any(Date)); }); }); diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index 2a3d905275..b1bd3332bf 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -65,7 +65,10 @@ describe('AuthService', () => { describe('onBootstrap', () => { it('should init the repo', () => { + mocks.oauth.init.mockResolvedValue(); + sut.onBootstrap(); + expect(mocks.oauth.init).toHaveBeenCalled(); }); }); @@ -73,24 +76,30 @@ describe('AuthService', () => { describe('login', () => { it('should throw an error if password login is disabled', async () => { mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.disabled); + await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(UnauthorizedException); }); it('should check the user exists', async () => { mocks.user.getByEmail.mockResolvedValue(void 0); + await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(UnauthorizedException); + expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1); }); it('should check the user has a password', async () => { mocks.user.getByEmail.mockResolvedValue({} as UserEntity); + await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(UnauthorizedException); + expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1); }); it('should successfully log the user in', async () => { mocks.user.getByEmail.mockResolvedValue(userStub.user1); mocks.session.create.mockResolvedValue(sessionStub.valid); + await expect(sut.login(fixtures.login, loginDetails)).resolves.toEqual({ accessToken: 'cmFuZG9tLWJ5dGVz', userId: 'user-id', @@ -100,6 +109,7 @@ describe('AuthService', () => { isAdmin: false, shouldChangePassword: false, }); + expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1); }); }); @@ -159,8 +169,10 @@ describe('AuthService', () => { describe('logout', () => { it('should return the end session endpoint', async () => { + const auth = factory.auth(); + mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled); - const auth = { user: { id: '123' } } as AuthDto; + await expect(sut.logout(auth, AuthType.OAUTH)).resolves.toEqual({ successful: true, redirectUri: 'http://end-session-endpoint', @@ -168,7 +180,7 @@ describe('AuthService', () => { }); it('should return the default redirect', async () => { - const auth = { user: { id: '123' } } as AuthDto; + const auth = factory.auth(); await expect(sut.logout(auth, AuthType.PASSWORD)).resolves.toEqual({ successful: true, @@ -178,6 +190,7 @@ describe('AuthService', () => { it('should delete the access token', async () => { const auth = { user: { id: '123' }, session: { id: 'token123' } } as AuthDto; + mocks.session.delete.mockResolvedValue(); await expect(sut.logout(auth, AuthType.PASSWORD)).resolves.toEqual({ successful: true, @@ -203,7 +216,9 @@ describe('AuthService', () => { it('should only allow one admin', async () => { mocks.user.getAdmin.mockResolvedValue({} as UserEntity); + await expect(sut.adminSignUp(dto)).rejects.toBeInstanceOf(BadRequestException); + expect(mocks.user.getAdmin).toHaveBeenCalled(); }); @@ -215,6 +230,7 @@ describe('AuthService', () => { createdAt: new Date('2021-01-01'), metadata: [] as UserMetadataEntity[], } as UserEntity); + await expect(sut.adminSignUp(dto)).resolves.toMatchObject({ avatarColor: expect.any(String), id: 'admin', @@ -222,6 +238,7 @@ describe('AuthService', () => { email: 'test@immich.com', name: 'immich admin', }); + expect(mocks.user.getAdmin).toHaveBeenCalled(); expect(mocks.user.create).toHaveBeenCalled(); }); @@ -241,6 +258,7 @@ describe('AuthService', () => { it('should validate using authorization header', async () => { mocks.user.get.mockResolvedValue(userStub.user1); mocks.session.getByToken.mockResolvedValue(sessionStub.valid as any); + await expect( sut.authenticate({ headers: { authorization: 'Bearer auth_token' }, @@ -256,6 +274,8 @@ describe('AuthService', () => { describe('validate - shared key', () => { it('should not accept a non-existent key', async () => { + mocks.sharedLink.getByKey.mockResolvedValue(void 0); + await expect( sut.authenticate({ headers: { 'x-immich-share-key': 'key' }, @@ -267,6 +287,7 @@ describe('AuthService', () => { it('should not accept an expired key', async () => { mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.expired); + await expect( sut.authenticate({ headers: { 'x-immich-share-key': 'key' }, @@ -278,6 +299,7 @@ describe('AuthService', () => { it('should not accept a key on a non-shared route', async () => { mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.valid); + await expect( sut.authenticate({ headers: { 'x-immich-share-key': 'key' }, @@ -290,6 +312,7 @@ describe('AuthService', () => { it('should not accept a key without a user', async () => { mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.expired); mocks.user.get.mockResolvedValue(void 0); + await expect( sut.authenticate({ headers: { 'x-immich-share-key': 'key' }, @@ -302,6 +325,7 @@ describe('AuthService', () => { it('should accept a base64url key', async () => { mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.valid); mocks.user.get.mockResolvedValue(userStub.admin); + await expect( sut.authenticate({ headers: { 'x-immich-share-key': sharedLinkStub.valid.key.toString('base64url') }, @@ -318,6 +342,7 @@ describe('AuthService', () => { it('should accept a hex key', async () => { mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.valid); mocks.user.get.mockResolvedValue(userStub.admin); + await expect( sut.authenticate({ headers: { 'x-immich-share-key': sharedLinkStub.valid.key.toString('hex') }, @@ -335,6 +360,7 @@ describe('AuthService', () => { describe('validate - user token', () => { it('should throw if no token is found', async () => { mocks.session.getByToken.mockResolvedValue(void 0); + await expect( sut.authenticate({ headers: { 'x-immich-user-token': 'auth_token' }, @@ -346,6 +372,7 @@ describe('AuthService', () => { it('should return an auth dto', async () => { mocks.session.getByToken.mockResolvedValue(sessionStub.valid as any); + await expect( sut.authenticate({ headers: { cookie: 'immich_access_token=auth_token' }, @@ -360,6 +387,7 @@ describe('AuthService', () => { it('should throw if admin route and not an admin', async () => { mocks.session.getByToken.mockResolvedValue(sessionStub.valid as any); + await expect( sut.authenticate({ headers: { cookie: 'immich_access_token=auth_token' }, @@ -372,6 +400,7 @@ describe('AuthService', () => { it('should update when access time exceeds an hour', async () => { mocks.session.getByToken.mockResolvedValue(sessionStub.inactive as any); mocks.session.update.mockResolvedValue(sessionStub.valid); + await expect( sut.authenticate({ headers: { cookie: 'immich_access_token=auth_token' }, @@ -386,6 +415,7 @@ describe('AuthService', () => { describe('validate - api key', () => { it('should throw an error if no api key is found', async () => { mocks.apiKey.getKey.mockResolvedValue(void 0); + await expect( sut.authenticate({ headers: { 'x-api-key': 'auth_token' }, @@ -401,6 +431,7 @@ describe('AuthService', () => { const authApiKey = factory.authApiKey({ permissions: [] }); mocks.apiKey.getKey.mockResolvedValue({ ...authApiKey, user: authUser }); + await expect( sut.authenticate({ headers: { 'x-api-key': 'auth_token' }, @@ -442,6 +473,7 @@ describe('AuthService', () => { describe('authorize', () => { it('should fail if oauth is disabled', async () => { mocks.systemMetadata.get.mockResolvedValue({ oauth: { enabled: false } }); + await expect(sut.authorize({ redirectUri: 'https://demo.immich.app' })).rejects.toBeInstanceOf( BadRequestException, ); @@ -449,6 +481,7 @@ describe('AuthService', () => { it('should authorize the user', async () => { mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithMobileOverride); + await sut.authorize({ redirectUri: 'https://demo.immich.app' }); }); }); @@ -461,9 +494,11 @@ describe('AuthService', () => { it('should not allow auto registering', async () => { mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthEnabled); mocks.user.getByEmail.mockResolvedValue(void 0); + await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).rejects.toBeInstanceOf( BadRequestException, ); + expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1); }); @@ -540,6 +575,7 @@ describe('AuthService', () => { mocks.session.create.mockResolvedValue(sessionStub.valid); await sut.callback({ url }, loginDetails); + expect(mocks.oauth.getProfile).toHaveBeenCalledWith(expect.objectContaining({}), url, 'http://mobile-redirect'); }); } @@ -549,6 +585,7 @@ describe('AuthService', () => { mocks.user.getByEmail.mockResolvedValue(void 0); mocks.user.getAdmin.mockResolvedValue(userStub.user1); mocks.user.create.mockResolvedValue(userStub.user1); + mocks.session.create.mockResolvedValue(factory.session()); await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual( oauthResponse, @@ -563,6 +600,7 @@ describe('AuthService', () => { mocks.user.getAdmin.mockResolvedValue(userStub.user1); mocks.user.create.mockResolvedValue(userStub.user1); mocks.oauth.getProfile.mockResolvedValue({ sub, email, immich_quota: 'abc' }); + mocks.session.create.mockResolvedValue(factory.session()); await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual( oauthResponse, @@ -577,6 +615,7 @@ describe('AuthService', () => { mocks.user.getAdmin.mockResolvedValue(userStub.user1); mocks.user.create.mockResolvedValue(userStub.user1); mocks.oauth.getProfile.mockResolvedValue({ sub, email, immich_quota: -5 }); + mocks.session.create.mockResolvedValue(factory.session()); await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual( oauthResponse, @@ -591,6 +630,7 @@ describe('AuthService', () => { mocks.user.getAdmin.mockResolvedValue(userStub.user1); mocks.user.create.mockResolvedValue(userStub.user1); mocks.oauth.getProfile.mockResolvedValue({ sub, email, immich_quota: 0 }); + mocks.session.create.mockResolvedValue(factory.session()); await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual( oauthResponse, @@ -611,6 +651,7 @@ describe('AuthService', () => { mocks.user.getAdmin.mockResolvedValue(userStub.user1); mocks.user.create.mockResolvedValue(userStub.user1); mocks.oauth.getProfile.mockResolvedValue({ sub, email, immich_quota: 5 }); + mocks.session.create.mockResolvedValue(factory.session()); await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual( oauthResponse, diff --git a/server/src/services/backup.service.spec.ts b/server/src/services/backup.service.spec.ts index 567859ac01..704087ab05 100644 --- a/server/src/services/backup.service.spec.ts +++ b/server/src/services/backup.service.spec.ts @@ -22,6 +22,7 @@ describe(BackupService.name, () => { describe('onBootstrapEvent', () => { it('should init cron job and handle config changes', async () => { mocks.database.tryLock.mockResolvedValue(true); + mocks.cron.create.mockResolvedValue(); await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig }); @@ -47,10 +48,14 @@ describe(BackupService.name, () => { describe('onConfigUpdateEvent', () => { beforeEach(async () => { mocks.database.tryLock.mockResolvedValue(true); + mocks.cron.create.mockResolvedValue(); + await sut.onConfigInit({ newConfig: defaults }); }); it('should update cron job if backup is enabled', () => { + mocks.cron.update.mockResolvedValue(); + sut.onConfigUpdate({ oldConfig: defaults, newConfig: { diff --git a/server/src/services/cli.service.spec.ts b/server/src/services/cli.service.spec.ts index c585142cbf..ce591a7e62 100644 --- a/server/src/services/cli.service.spec.ts +++ b/server/src/services/cli.service.spec.ts @@ -31,6 +31,8 @@ describe(CliService.name, () => { it('should default to a random password', async () => { mocks.user.getAdmin.mockResolvedValue(userStub.admin); + mocks.user.update.mockResolvedValue(userStub.admin); + const ask = vitest.fn().mockImplementation(() => {}); const response = await sut.resetAdminPassword(ask); @@ -45,6 +47,8 @@ describe(CliService.name, () => { it('should use the supplied password', async () => { mocks.user.getAdmin.mockResolvedValue(userStub.admin); + mocks.user.update.mockResolvedValue(userStub.admin); + const ask = vitest.fn().mockResolvedValue('new-password'); const response = await sut.resetAdminPassword(ask); diff --git a/server/src/services/download.service.spec.ts b/server/src/services/download.service.spec.ts index 5139fbd58f..7646637093 100644 --- a/server/src/services/download.service.spec.ts +++ b/server/src/services/download.service.spec.ts @@ -173,6 +173,7 @@ describe(DownloadService.name, () => { it('should return a list of archives (assetIds)', async () => { const assetIds = ['asset-1', 'asset-2']; + mocks.user.getMetadata.mockResolvedValue([]); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(assetIds)); mocks.downloadRepository.downloadAssetIds.mockReturnValue( makeStream([ @@ -187,6 +188,7 @@ describe(DownloadService.name, () => { }); it('should return a list of archives (albumId)', async () => { + mocks.user.getMetadata.mockResolvedValue([]); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-1'])); mocks.downloadRepository.downloadAlbumId.mockReturnValue( makeStream([ @@ -202,6 +204,7 @@ describe(DownloadService.name, () => { }); it('should return a list of archives (userId)', async () => { + mocks.user.getMetadata.mockResolvedValue([]); mocks.downloadRepository.downloadUserId.mockReturnValue( makeStream([ { id: 'asset-1', livePhotoVideoId: null, size: 100_000 }, @@ -217,6 +220,7 @@ describe(DownloadService.name, () => { }); it('should split archives by size', async () => { + mocks.user.getMetadata.mockResolvedValue([]); mocks.downloadRepository.downloadUserId.mockReturnValue( makeStream([ { id: 'asset-1', livePhotoVideoId: null, size: 5000 }, @@ -244,13 +248,13 @@ describe(DownloadService.name, () => { const assetIds = ['asset-1', 'asset-2']; mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(assetIds)); + mocks.user.getMetadata.mockResolvedValue([]); mocks.downloadRepository.downloadAssetIds.mockReturnValue( makeStream([ { id: 'asset-1', livePhotoVideoId: 'asset-3', size: 5000 }, { id: 'asset-2', livePhotoVideoId: 'asset-4', size: 100_000 }, ]), ); - mocks.downloadRepository.downloadMotionAssetIds.mockReturnValue( makeStream([ { id: 'asset-3', livePhotoVideoId: null, size: 23_456, originalPath: '/path/to/file.mp4' }, @@ -271,11 +275,10 @@ describe(DownloadService.name, () => { const assetIds = ['asset-1']; mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(assetIds)); - + mocks.user.getMetadata.mockResolvedValue([]); mocks.downloadRepository.downloadAssetIds.mockReturnValue( makeStream([{ id: 'asset-1', livePhotoVideoId: 'asset-3', size: 5000 }]), ); - mocks.downloadRepository.downloadMotionAssetIds.mockReturnValue( makeStream([ { id: 'asset-2', livePhotoVideoId: null, size: 23_456, originalPath: 'upload/encoded-video/uuid-MP.mp4' }, diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index 5d6dfb21f5..aef02b7244 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -36,6 +36,9 @@ describe(LibraryService.name, () => { describe('onConfigInit', () => { it('should init cron job and handle config changes', async () => { + mocks.cron.create.mockResolvedValue(); + mocks.cron.update.mockResolvedValue(); + await sut.onConfigInit({ newConfig: defaults }); expect(mocks.cron.create).toHaveBeenCalled(); @@ -65,6 +68,7 @@ describe(LibraryService.name, () => { mocks.library.get.mockImplementation((id) => Promise.resolve([library1, library2].find((library) => library.id === id)), ); + mocks.cron.create.mockResolvedValue(); await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig }); @@ -74,6 +78,8 @@ describe(LibraryService.name, () => { }); it('should not initialize watcher when watching is disabled', async () => { + mocks.cron.create.mockResolvedValue(); + await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchDisabled as SystemConfig }); expect(mocks.storage.watch).not.toHaveBeenCalled(); @@ -99,6 +105,8 @@ describe(LibraryService.name, () => { describe('onConfigUpdateEvent', () => { beforeEach(async () => { mocks.database.tryLock.mockResolvedValue(true); + mocks.cron.create.mockResolvedValue(); + await sut.onConfigInit({ newConfig: defaults }); }); @@ -111,6 +119,9 @@ describe(LibraryService.name, () => { it('should update cron job and enable watching', async () => { mocks.library.getAll.mockResolvedValue([]); + mocks.cron.create.mockResolvedValue(); + mocks.cron.update.mockResolvedValue(); + await sut.onConfigUpdate({ newConfig: systemConfigStub.libraryScanAndWatch as SystemConfig, oldConfig: defaults, @@ -125,6 +136,9 @@ describe(LibraryService.name, () => { it('should update cron job and disable watching', async () => { mocks.library.getAll.mockResolvedValue([]); + mocks.cron.create.mockResolvedValue(); + mocks.cron.update.mockResolvedValue(); + await sut.onConfigUpdate({ newConfig: systemConfigStub.libraryScanAndWatch as SystemConfig, oldConfig: defaults, @@ -620,6 +634,7 @@ describe(LibraryService.name, () => { const mockClose = vitest.fn(); mocks.storage.watch.mockImplementation(makeMockWatcher({ close: mockClose })); + mocks.cron.create.mockResolvedValue(); await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig }); await sut.delete(library.id); @@ -765,6 +780,7 @@ describe(LibraryService.name, () => { mocks.library.create.mockResolvedValue(library); mocks.library.get.mockResolvedValue(library); mocks.library.getAll.mockResolvedValue([]); + mocks.cron.create.mockResolvedValue(); await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig }); await sut.create({ ownerId: authStub.admin.user.id, importPaths: library.importPaths }); @@ -832,6 +848,7 @@ describe(LibraryService.name, () => { describe('update', () => { beforeEach(async () => { mocks.library.getAll.mockResolvedValue([]); + mocks.cron.create.mockResolvedValue(); await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig }); }); @@ -878,6 +895,8 @@ describe(LibraryService.name, () => { describe('watching disabled', () => { beforeEach(async () => { + mocks.cron.create.mockResolvedValue(); + await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchDisabled as SystemConfig }); }); @@ -895,6 +914,8 @@ describe(LibraryService.name, () => { describe('watching enabled', () => { beforeEach(async () => { mocks.library.getAll.mockResolvedValue([]); + mocks.cron.create.mockResolvedValue(); + await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig }); }); @@ -1067,6 +1088,7 @@ describe(LibraryService.name, () => { const mockClose = vitest.fn(); mocks.storage.watch.mockImplementation(makeMockWatcher({ close: mockClose })); + mocks.cron.create.mockResolvedValue(); await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig }); await sut.onShutdown(); diff --git a/server/src/services/memory.service.spec.ts b/server/src/services/memory.service.spec.ts index 6af300c164..22a4199572 100644 --- a/server/src/services/memory.service.spec.ts +++ b/server/src/services/memory.service.spec.ts @@ -33,6 +33,8 @@ describe(MemoryService.name, () => { }); it('should map ', async () => { + mocks.memory.search.mockResolvedValue([]); + await expect(sut.search(factory.auth(), {})).resolves.toEqual([]); }); }); @@ -46,6 +48,7 @@ describe(MemoryService.name, () => { const [memoryId] = newUuids(); mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memoryId])); + mocks.memory.get.mockResolvedValue(void 0); await expect(sut.get(factory.auth(), memoryId)).rejects.toBeInstanceOf(BadRequestException); }); @@ -159,6 +162,7 @@ describe(MemoryService.name, () => { const memoryId = newUuid(); mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memoryId])); + mocks.memory.delete.mockResolvedValue(); await expect(sut.remove(factory.auth(), memoryId)).resolves.toBeUndefined(); @@ -183,6 +187,7 @@ describe(MemoryService.name, () => { mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id])); mocks.memory.get.mockResolvedValue(memory); + mocks.memory.getAssetIds.mockResolvedValue(new Set()); await expect(sut.addAssets(factory.auth(), memory.id, { ids: [assetId] })).resolves.toEqual([ { error: 'no_permission', id: assetId, success: false }, @@ -213,6 +218,9 @@ describe(MemoryService.name, () => { mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id])); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); mocks.memory.get.mockResolvedValue(memory); + mocks.memory.update.mockResolvedValue(memory); + mocks.memory.getAssetIds.mockResolvedValue(new Set()); + mocks.memory.addAssetIds.mockResolvedValue(); await expect(sut.addAssets(factory.auth(), memory.id, { ids: [assetId] })).resolves.toEqual([ { id: assetId, success: true }, @@ -233,6 +241,7 @@ describe(MemoryService.name, () => { it('should skip assets not in the memory', async () => { mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); + mocks.memory.getAssetIds.mockResolvedValue(new Set()); await expect(sut.removeAssets(factory.auth(), 'memory1', { ids: ['not-found'] })).resolves.toEqual([ { error: 'not_found', id: 'not-found', success: false }, @@ -242,15 +251,20 @@ describe(MemoryService.name, () => { }); it('should remove assets', async () => { - mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1'])); - mocks.memory.getAssetIds.mockResolvedValue(new Set(['asset1'])); + const memory = factory.memory(); + const asset = factory.asset(); - await expect(sut.removeAssets(factory.auth(), 'memory1', { ids: ['asset1'] })).resolves.toEqual([ - { id: 'asset1', success: true }, + mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id])); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.memory.getAssetIds.mockResolvedValue(new Set([asset.id])); + mocks.memory.removeAssetIds.mockResolvedValue(); + mocks.memory.update.mockResolvedValue(memory); + + await expect(sut.removeAssets(factory.auth(), memory.id, { ids: [asset.id] })).resolves.toEqual([ + { id: asset.id, success: true }, ]); - expect(mocks.memory.removeAssetIds).toHaveBeenCalledWith('memory1', ['asset1']); + expect(mocks.memory.removeAssetIds).toHaveBeenCalledWith(memory.id, [asset.id]); }); }); }); diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index e505a88605..229b63f20e 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -52,6 +52,10 @@ describe(MetadataService.name, () => { describe('onBootstrapEvent', () => { it('should pause and resume queue during init', async () => { + mocks.job.pause.mockResolvedValue(); + mocks.map.init.mockResolvedValue(); + mocks.job.resume.mockResolvedValue(); + await sut.onBootstrap(); expect(mocks.job.pause).toHaveBeenCalledTimes(1); diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index 038ab1180c..0deb3805e5 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -260,6 +260,7 @@ describe(NotificationService.name, () => { mocks.user.get.mockResolvedValue(userStub.admin); mocks.notification.verifySmtp.mockResolvedValue(true); mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.notification.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' }); await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).resolves.not.toThrow(); expect(mocks.notification.renderEmail).toHaveBeenCalledWith({ @@ -279,6 +280,7 @@ describe(NotificationService.name, () => { mocks.notification.verifySmtp.mockResolvedValue(true); mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.systemMetadata.get.mockResolvedValue({ server: { externalDomain: 'https://demo.immich.app' } }); + mocks.notification.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' }); await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).resolves.not.toThrow(); expect(mocks.notification.renderEmail).toHaveBeenCalledWith({ @@ -297,6 +299,7 @@ describe(NotificationService.name, () => { mocks.user.get.mockResolvedValue(userStub.admin); mocks.notification.verifySmtp.mockResolvedValue(true); mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); + mocks.notification.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' }); await expect( sut.sendTestEmail('', { ...configs.smtpTransport.notifications.smtp, replyTo: 'demo@immich.app' }), diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index 8a8cb254fa..073cf71247 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -324,6 +324,10 @@ describe(PersonService.name, () => { mocks.person.getFacesByIds.mockResolvedValue([faceStub.face1]); mocks.person.reassignFace.mockResolvedValue(1); mocks.person.getRandomFace.mockResolvedValue(faceStub.primaryFace1); + mocks.person.refreshFaces.mockResolvedValue(); + mocks.person.reassignFace.mockResolvedValue(5); + mocks.person.update.mockResolvedValue(personStub.noName); + await expect( sut.reassignFaces(authStub.admin, personStub.noName.id, { data: [{ personId: personStub.withName.id, assetId: assetStub.image.id }], @@ -515,6 +519,7 @@ describe(PersonService.name, () => { hasNextPage: false, }); mocks.person.getAllWithoutFaces.mockResolvedValue([personStub.randomPerson]); + mocks.person.deleteFaces.mockResolvedValue(); await sut.handleQueueDetectFaces({ force: true }); @@ -633,6 +638,7 @@ describe(PersonService.name, () => { mocks.person.getAll.mockReturnValue(makeStream()); mocks.person.getAllFaces.mockReturnValue(makeStream([faceStub.face1])); mocks.person.getAllWithoutFaces.mockResolvedValue([]); + mocks.person.unassignFaces.mockResolvedValue(); await sut.handleQueueRecognizeFaces({ force: true, nightly: true }); @@ -679,6 +685,7 @@ describe(PersonService.name, () => { mocks.person.getAll.mockReturnValue(makeStream([faceStub.face1.person, personStub.randomPerson])); mocks.person.getAllFaces.mockReturnValue(makeStream([faceStub.face1])); mocks.person.getAllWithoutFaces.mockResolvedValue([personStub.randomPerson]); + mocks.person.unassignFaces.mockResolvedValue(); await sut.handleQueueRecognizeFaces({ force: true }); @@ -757,6 +764,7 @@ describe(PersonService.name, () => { mocks.machineLearning.detectFaces.mockResolvedValue(detectFaceMock); mocks.search.searchFaces.mockResolvedValue([{ ...faceStub.face1, distance: 0.7 }]); mocks.asset.getByIds.mockResolvedValue([assetStub.image]); + mocks.person.refreshFaces.mockResolvedValue(); await sut.handleDetectFaces({ id: assetStub.image.id }); @@ -784,6 +792,7 @@ describe(PersonService.name, () => { it('should add new face and delete an existing face not among the new detected faces', async () => { mocks.machineLearning.detectFaces.mockResolvedValue(detectFaceMock); mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.image, faces: [faceStub.primaryFace1] }]); + mocks.person.refreshFaces.mockResolvedValue(); await sut.handleDetectFaces({ id: assetStub.image.id }); @@ -799,6 +808,7 @@ describe(PersonService.name, () => { it('should add embedding to matching metadata face', async () => { mocks.machineLearning.detectFaces.mockResolvedValue(detectFaceMock); mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.image, faces: [faceStub.fromExif1] }]); + mocks.person.refreshFaces.mockResolvedValue(); await sut.handleDetectFaces({ id: assetStub.image.id }); @@ -1006,6 +1016,7 @@ describe(PersonService.name, () => { mocks.person.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.middle.assetId }); mocks.person.getFaceByIdWithAssets.mockResolvedValue(faceStub.middle); mocks.asset.getById.mockResolvedValue(assetStub.primaryImage); + mocks.media.generateThumbnail.mockResolvedValue(); await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id }); @@ -1038,6 +1049,7 @@ describe(PersonService.name, () => { mocks.person.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.start.assetId }); mocks.person.getFaceByIdWithAssets.mockResolvedValue(faceStub.start); mocks.asset.getById.mockResolvedValue(assetStub.image); + mocks.media.generateThumbnail.mockResolvedValue(); await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id }); @@ -1063,7 +1075,9 @@ describe(PersonService.name, () => { it('should generate a thumbnail without overflowing', async () => { mocks.person.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.end.assetId }); mocks.person.getFaceByIdWithAssets.mockResolvedValue(faceStub.end); + mocks.person.update.mockResolvedValue(personStub.primaryPerson); mocks.asset.getById.mockResolvedValue(assetStub.primaryImage); + mocks.media.generateThumbnail.mockResolvedValue(); await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id }); diff --git a/server/src/services/search.service.spec.ts b/server/src/services/search.service.spec.ts index 34f4c7b39f..79f3a77ebe 100644 --- a/server/src/services/search.service.spec.ts +++ b/server/src/services/search.service.spec.ts @@ -57,6 +57,8 @@ describe(SearchService.name, () => { 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']); @@ -65,6 +67,8 @@ describe(SearchService.name, () => { 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]); @@ -73,6 +77,8 @@ describe(SearchService.name, () => { 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']); @@ -81,6 +87,8 @@ describe(SearchService.name, () => { 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]); @@ -89,6 +97,8 @@ describe(SearchService.name, () => { 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']); @@ -97,6 +107,8 @@ describe(SearchService.name, () => { 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]); @@ -105,6 +117,8 @@ describe(SearchService.name, () => { 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']); @@ -113,6 +127,8 @@ describe(SearchService.name, () => { 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]); @@ -121,6 +137,8 @@ describe(SearchService.name, () => { 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']); @@ -129,6 +147,8 @@ describe(SearchService.name, () => { 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]); diff --git a/server/src/services/session.service.spec.ts b/server/src/services/session.service.spec.ts index 96a1dacf64..3d1b09a39d 100644 --- a/server/src/services/session.service.spec.ts +++ b/server/src/services/session.service.spec.ts @@ -36,6 +36,7 @@ describe('SessionService', () => { updateId: 'uuid-v7', }, ]); + mocks.session.delete.mockResolvedValue(); await expect(sut.handleCleanup()).resolves.toEqual(JobStatus.SUCCESS); expect(mocks.session.delete).toHaveBeenCalledWith('123'); @@ -71,6 +72,7 @@ describe('SessionService', () => { describe('logoutDevices', () => { it('should logout all devices', async () => { mocks.session.getByUserId.mockResolvedValue([sessionStub.inactive, sessionStub.valid] as any[]); + mocks.session.delete.mockResolvedValue(); await sut.deleteAll(authStub.user1); @@ -83,6 +85,7 @@ describe('SessionService', () => { describe('logoutDevice', () => { it('should logout the device', async () => { mocks.access.authDevice.checkOwnerAccess.mockResolvedValue(new Set(['token-1'])); + mocks.session.delete.mockResolvedValue(); await sut.delete(authStub.user1, 'token-1'); diff --git a/server/src/services/shared-link.service.spec.ts b/server/src/services/shared-link.service.spec.ts index 4d6cdee6cb..557fdd5780 100644 --- a/server/src/services/shared-link.service.spec.ts +++ b/server/src/services/shared-link.service.spec.ts @@ -71,7 +71,10 @@ describe(SharedLinkService.name, () => { describe('get', () => { it('should throw an error for an invalid shared link', async () => { + mocks.sharedLink.get.mockResolvedValue(void 0); + await expect(sut.get(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException); + expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id'); expect(mocks.sharedLink.update).not.toHaveBeenCalled(); }); @@ -194,7 +197,10 @@ describe(SharedLinkService.name, () => { describe('update', () => { it('should throw an error for an invalid shared link', async () => { + mocks.sharedLink.get.mockResolvedValue(void 0); + await expect(sut.update(authStub.user1, 'missing-id', {})).rejects.toBeInstanceOf(BadRequestException); + expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id'); expect(mocks.sharedLink.update).not.toHaveBeenCalled(); }); @@ -214,14 +220,20 @@ describe(SharedLinkService.name, () => { describe('remove', () => { it('should throw an error for an invalid shared link', async () => { + mocks.sharedLink.get.mockResolvedValue(void 0); + await expect(sut.remove(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException); + expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id'); expect(mocks.sharedLink.update).not.toHaveBeenCalled(); }); it('should remove a key', async () => { mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid); + mocks.sharedLink.remove.mockResolvedValue(); + await sut.remove(authStub.user1, sharedLinkStub.valid.id); + expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id); expect(mocks.sharedLink.remove).toHaveBeenCalledWith(sharedLinkStub.valid); }); @@ -238,6 +250,7 @@ describe(SharedLinkService.name, () => { it('should add assets to a shared link', async () => { mocks.sharedLink.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual)); mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual); + mocks.sharedLink.update.mockResolvedValue(sharedLinkStub.individual); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-3'])); await expect( @@ -268,6 +281,7 @@ describe(SharedLinkService.name, () => { it('should remove assets from a shared link', async () => { mocks.sharedLink.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual)); mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual); + mocks.sharedLink.update.mockResolvedValue(sharedLinkStub.individual); await expect( sut.removeAssets(authStub.admin, 'link-1', { assetIds: [assetStub.image.id, 'asset-2'] }), diff --git a/server/src/services/stack.service.spec.ts b/server/src/services/stack.service.spec.ts index 5fbc0e185d..f6da8bcac7 100644 --- a/server/src/services/stack.service.spec.ts +++ b/server/src/services/stack.service.spec.ts @@ -155,6 +155,7 @@ describe(StackService.name, () => { it('should delete stack', async () => { mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id'])); + mocks.stack.delete.mockResolvedValue(); await sut.delete(authStub.admin, 'stack-id'); @@ -176,6 +177,7 @@ describe(StackService.name, () => { it('should delete all stacks', async () => { mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id'])); + mocks.stack.deleteAll.mockResolvedValue(); await sut.deleteAll(authStub.admin, { ids: ['stack-id'] }); diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts index 1133d25db4..6bfabe2e8c 100644 --- a/server/src/services/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -93,7 +93,9 @@ describe(StorageTemplateService.name, () => { describe('handleMigrationSingle', () => { it('should skip when storage template is disabled', async () => { mocks.systemMetadata.get.mockResolvedValue({ storageTemplate: { enabled: false } }); + await expect(sut.handleMigrationSingle({ id: testAsset.id })).resolves.toBe(JobStatus.SKIPPED); + expect(mocks.asset.getByIds).not.toHaveBeenCalled(); expect(mocks.storage.checkFileExists).not.toHaveBeenCalled(); expect(mocks.storage.rename).not.toHaveBeenCalled(); diff --git a/server/src/services/tag.service.spec.ts b/server/src/services/tag.service.spec.ts index 22d7747d0d..70507ab433 100644 --- a/server/src/services/tag.service.spec.ts +++ b/server/src/services/tag.service.spec.ts @@ -87,9 +87,12 @@ describe(TagService.name, () => { it('should create a new tag with optional color', async () => { mocks.tag.create.mockResolvedValue(tagStub.colorCreate); + mocks.tag.getByValue.mockResolvedValue(void 0); + await expect(sut.create(authStub.admin, { name: 'tag-1', color: '#000000' })).resolves.toEqual( tagResponseStub.color1, ); + expect(mocks.tag.create).toHaveBeenCalledWith({ userId: authStub.admin.user.id, value: 'tag-1', @@ -168,6 +171,8 @@ describe(TagService.name, () => { it('should remove a tag', async () => { mocks.tag.get.mockResolvedValue(tagStub.tag); + mocks.tag.delete.mockResolvedValue(); + await sut.remove(authStub.admin, 'tag-1'); expect(mocks.tag.delete).toHaveBeenCalledWith('tag-1'); }); @@ -223,6 +228,7 @@ describe(TagService.name, () => { it('should accept accept ids that are new and reject the rest', async () => { mocks.tag.get.mockResolvedValue(tagStub.tag); mocks.tag.getAssetIds.mockResolvedValue(new Set(['asset-1'])); + mocks.tag.addAssetIds.mockResolvedValue(); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-2'])); await expect( @@ -242,6 +248,8 @@ describe(TagService.name, () => { describe('removeAssets', () => { it('should throw an error for an invalid id', async () => { mocks.tag.getAssetIds.mockResolvedValue(new Set()); + mocks.tag.removeAssetIds.mockResolvedValue(); + await expect(sut.removeAssets(authStub.admin, 'tag-1', { ids: ['asset-1'] })).resolves.toEqual([ { id: 'asset-1', success: false, error: 'not_found' }, ]); @@ -250,6 +258,7 @@ describe(TagService.name, () => { it('should accept accept ids that are tagged and reject the rest', async () => { mocks.tag.get.mockResolvedValue(tagStub.tag); mocks.tag.getAssetIds.mockResolvedValue(new Set(['asset-1'])); + mocks.tag.removeAssetIds.mockResolvedValue(); await expect( sut.removeAssets(authStub.admin, 'tag-1', { @@ -267,7 +276,10 @@ describe(TagService.name, () => { describe('handleTagCleanup', () => { it('should delete empty tags', async () => { + mocks.tag.deleteEmptyTags.mockResolvedValue(); + await expect(sut.handleTagCleanup()).resolves.toBe(JobStatus.SUCCESS); + expect(mocks.tag.deleteEmptyTags).toHaveBeenCalled(); }); }); diff --git a/server/src/services/timeline.service.spec.ts b/server/src/services/timeline.service.spec.ts index 749633998b..1c2c422433 100644 --- a/server/src/services/timeline.service.spec.ts +++ b/server/src/services/timeline.service.spec.ts @@ -70,6 +70,7 @@ describe(TimelineService.name, () => { it('should include partner shared assets', async () => { mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]); + mocks.partner.getAll.mockResolvedValue([]); await expect( sut.getTimeBucket(authStub.admin, { diff --git a/server/src/services/trash.service.spec.ts b/server/src/services/trash.service.spec.ts index e7eccd374c..b3bee90815 100644 --- a/server/src/services/trash.service.spec.ts +++ b/server/src/services/trash.service.spec.ts @@ -39,6 +39,7 @@ describe(TrashService.name, () => { it('should restore a batch of assets', async () => { mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1', 'asset2'])); + mocks.trash.restoreAll.mockResolvedValue(0); await sut.restoreAssets(authStub.user1, { ids: ['asset1', 'asset2'] }); diff --git a/server/src/services/version.service.spec.ts b/server/src/services/version.service.spec.ts index 500478e9e7..a83d9f85b6 100644 --- a/server/src/services/version.service.spec.ts +++ b/server/src/services/version.service.spec.ts @@ -4,6 +4,7 @@ import { serverVersion } from 'src/constants'; import { ImmichEnvironment, JobName, JobStatus, SystemMetadataKey } from 'src/enum'; import { VersionService } from 'src/services/version.service'; import { mockEnvData } from 'test/repositories/config.repository.mock'; +import { factory } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; const mockRelease = (version: string) => ({ @@ -30,7 +31,12 @@ describe(VersionService.name, () => { describe('onBootstrap', () => { it('should record a new version', async () => { + mocks.versionHistory.getAll.mockResolvedValue([]); + mocks.versionHistory.getLatest.mockResolvedValue(void 0); + mocks.versionHistory.create.mockResolvedValue(factory.versionHistory()); + await expect(sut.onBootstrap()).resolves.toBeUndefined(); + expect(mocks.versionHistory.create).toHaveBeenCalledWith({ version: expect.any(String) }); }); diff --git a/server/test/factory.ts b/server/test/factory.ts index 66c2fbb50c..520119fc3e 100644 --- a/server/test/factory.ts +++ b/server/test/factory.ts @@ -34,9 +34,9 @@ import { TrashRepository } from 'src/repositories/trash.repository'; import { UserRepository } from 'src/repositories/user.repository'; import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; import { ViewRepository } from 'src/repositories/view-repository'; -import { newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock'; import { newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock'; import { newUuid } from 'test/small.factory'; +import { automock } from 'test/utils'; class CustomWritable extends Writable { private data = ''; @@ -213,7 +213,7 @@ export class TestContext { view: ViewRepository; private constructor(public db: Kysely) { - const logger = newLoggingRepositoryMock() as unknown as LoggingRepository; + const logger = automock(LoggingRepository, { args: [, { getEnv: () => ({}) }], strict: false }); const config = new ConfigRepository(); this.access = new AccessRepository(this.db); diff --git a/server/test/medium/specs/metadata.service.spec.ts b/server/test/medium/specs/metadata.service.spec.ts index aefb518c18..28f2c9f64f 100644 --- a/server/test/medium/specs/metadata.service.spec.ts +++ b/server/test/medium/specs/metadata.service.spec.ts @@ -3,12 +3,14 @@ import { writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { AssetEntity } from 'src/entities/asset.entity'; +import { LoggingRepository } from 'src/repositories/logging.repository'; import { MetadataRepository } from 'src/repositories/metadata.repository'; import { MetadataService } from 'src/services/metadata.service'; -import { newFakeLoggingRepository } from 'test/repositories/logger.repository.mock'; -import { newRandomImage, newTestService, ServiceMocks } from 'test/utils'; +import { automock, newRandomImage, newTestService, ServiceMocks } from 'test/utils'; -const metadataRepository = new MetadataRepository(newFakeLoggingRepository()); +const metadataRepository = new MetadataRepository( + automock(LoggingRepository, { args: [, { getEnv: () => ({}) }], strict: false }), +); const createTestFile = async (exifData: Record) => { const data = newRandomImage(); diff --git a/server/test/repositories/activity.repository.mock.ts b/server/test/repositories/activity.repository.mock.ts deleted file mode 100644 index 81208b7232..0000000000 --- a/server/test/repositories/activity.repository.mock.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ActivityRepository } from 'src/repositories/activity.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newActivityRepositoryMock = (): Mocked> => { - return { - search: vitest.fn(), - create: vitest.fn(), - delete: vitest.fn(), - getStatistics: vitest.fn(), - }; -}; diff --git a/server/test/repositories/album-user.repository.mock.ts b/server/test/repositories/album-user.repository.mock.ts deleted file mode 100644 index e3225661a4..0000000000 --- a/server/test/repositories/album-user.repository.mock.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { AlbumUserRepository } from 'src/repositories/album-user.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked } from 'vitest'; - -export const newAlbumUserRepositoryMock = (): Mocked> => { - return { - create: vitest.fn(), - delete: vitest.fn(), - update: vitest.fn(), - }; -}; diff --git a/server/test/repositories/album.repository.mock.ts b/server/test/repositories/album.repository.mock.ts deleted file mode 100644 index 7a1ae68a52..0000000000 --- a/server/test/repositories/album.repository.mock.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { AlbumRepository } from 'src/repositories/album.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newAlbumRepositoryMock = (): Mocked> => { - return { - getById: vitest.fn(), - getByAssetId: vitest.fn(), - getMetadataForIds: vitest.fn(), - getOwned: vitest.fn(), - getShared: vitest.fn(), - getNotShared: vitest.fn(), - restoreAll: vitest.fn(), - softDeleteAll: vitest.fn(), - deleteAll: vitest.fn(), - addAssetIds: vitest.fn(), - removeAsset: vitest.fn(), - removeAssetIds: vitest.fn(), - getAssetIds: vitest.fn(), - create: vitest.fn(), - update: vitest.fn(), - delete: vitest.fn(), - updateThumbnails: vitest.fn(), - }; -}; diff --git a/server/test/repositories/api-key.repository.mock.ts b/server/test/repositories/api-key.repository.mock.ts deleted file mode 100644 index e8ae0bf8e2..0000000000 --- a/server/test/repositories/api-key.repository.mock.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ApiKeyRepository } from 'src/repositories/api-key.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newKeyRepositoryMock = (): Mocked> => { - return { - create: vitest.fn(), - update: vitest.fn(), - delete: vitest.fn(), - getKey: vitest.fn(), - getById: vitest.fn(), - getByUserId: vitest.fn(), - }; -}; diff --git a/server/test/repositories/audit.repository.mock.ts b/server/test/repositories/audit.repository.mock.ts deleted file mode 100644 index a76079f45d..0000000000 --- a/server/test/repositories/audit.repository.mock.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { AuditRepository } from 'src/repositories/audit.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newAuditRepositoryMock = (): Mocked> => { - return { - getAfter: vitest.fn(), - removeBefore: vitest.fn(), - }; -}; diff --git a/server/test/repositories/cron.repository.mock.ts b/server/test/repositories/cron.repository.mock.ts deleted file mode 100644 index 5b74bd3cf5..0000000000 --- a/server/test/repositories/cron.repository.mock.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { CronRepository } from 'src/repositories/cron.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newCronRepositoryMock = (): Mocked> => { - return { - create: vitest.fn(), - update: vitest.fn(), - }; -}; diff --git a/server/test/repositories/download.repository.mock.ts b/server/test/repositories/download.repository.mock.ts deleted file mode 100644 index 50824c4f3d..0000000000 --- a/server/test/repositories/download.repository.mock.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { DownloadRepository } from 'src/repositories/download.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newDownloadRepositoryMock = (): Mocked> => { - return { - downloadAssetIds: vitest.fn(), - downloadMotionAssetIds: vitest.fn(), - downloadAlbumId: vitest.fn(), - downloadUserId: vitest.fn(), - }; -}; diff --git a/server/test/repositories/event.repository.mock.ts b/server/test/repositories/event.repository.mock.ts deleted file mode 100644 index a253e93671..0000000000 --- a/server/test/repositories/event.repository.mock.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { EventRepository } from 'src/repositories/event.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newEventRepositoryMock = (): Mocked> => { - return { - setup: vitest.fn(), - emit: vitest.fn() as any, - clientSend: vitest.fn() as any, - clientBroadcast: vitest.fn() as any, - serverSend: vitest.fn(), - afterInit: vitest.fn(), - handleConnection: vitest.fn(), - handleDisconnect: vitest.fn(), - setAuthFn: vitest.fn(), - }; -}; diff --git a/server/test/repositories/library.repository.mock.ts b/server/test/repositories/library.repository.mock.ts deleted file mode 100644 index 23013e1f30..0000000000 --- a/server/test/repositories/library.repository.mock.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { LibraryRepository } from 'src/repositories/library.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newLibraryRepositoryMock = (): Mocked> => { - return { - get: vitest.fn(), - create: vitest.fn(), - delete: vitest.fn(), - softDelete: vitest.fn(), - update: vitest.fn(), - getStatistics: vitest.fn(), - getAllDeleted: vitest.fn(), - getAll: vitest.fn(), - streamAssetIds: vitest.fn(), - }; -}; diff --git a/server/test/repositories/logger.repository.mock.ts b/server/test/repositories/logger.repository.mock.ts deleted file mode 100644 index 7257d375f1..0000000000 --- a/server/test/repositories/logger.repository.mock.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { LoggingRepository } from 'src/repositories/logging.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newLoggingRepositoryMock = (): Mocked> => { - return { - setLogLevel: vitest.fn(), - setContext: vitest.fn(), - setAppName: vitest.fn(), - isLevelEnabled: vitest.fn(), - verbose: vitest.fn(), - verboseFn: vitest.fn(), - debug: vitest.fn(), - debugFn: vitest.fn(), - log: vitest.fn(), - warn: vitest.fn(), - error: vitest.fn(), - fatal: vitest.fn(), - }; -}; - -export const newFakeLoggingRepository = () => - newLoggingRepositoryMock() as RepositoryInterface as LoggingRepository; diff --git a/server/test/repositories/machine-learning.repository.mock.ts b/server/test/repositories/machine-learning.repository.mock.ts deleted file mode 100644 index 229e8f92ec..0000000000 --- a/server/test/repositories/machine-learning.repository.mock.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { MachineLearningRepository } from 'src/repositories/machine-learning.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newMachineLearningRepositoryMock = (): Mocked> => { - return { - encodeImage: vitest.fn(), - encodeText: vitest.fn(), - detectFaces: vitest.fn(), - }; -}; diff --git a/server/test/repositories/map.repository.mock.ts b/server/test/repositories/map.repository.mock.ts deleted file mode 100644 index 9e7df32252..0000000000 --- a/server/test/repositories/map.repository.mock.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { MapRepository } from 'src/repositories/map.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked } from 'vitest'; - -export const newMapRepositoryMock = (): Mocked> => { - return { - init: vitest.fn(), - reverseGeocode: vitest.fn(), - getMapMarkers: vitest.fn(), - }; -}; diff --git a/server/test/repositories/memory.repository.mock.ts b/server/test/repositories/memory.repository.mock.ts deleted file mode 100644 index c3a6d774f0..0000000000 --- a/server/test/repositories/memory.repository.mock.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { MemoryRepository } from 'src/repositories/memory.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newMemoryRepositoryMock = (): Mocked> => { - return { - search: vitest.fn().mockResolvedValue([]), - get: vitest.fn(), - create: vitest.fn(), - update: vitest.fn(), - delete: vitest.fn(), - getAssetIds: vitest.fn().mockResolvedValue(new Set()), - addAssetIds: vitest.fn(), - removeAssetIds: vitest.fn(), - cleanup: vitest.fn(), - }; -}; diff --git a/server/test/repositories/move.repository.mock.ts b/server/test/repositories/move.repository.mock.ts deleted file mode 100644 index 88dfa29d4f..0000000000 --- a/server/test/repositories/move.repository.mock.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { MoveRepository } from 'src/repositories/move.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newMoveRepositoryMock = (): Mocked> => { - return { - create: vitest.fn(), - getByEntity: vitest.fn(), - update: vitest.fn(), - delete: vitest.fn(), - cleanMoveHistory: vitest.fn(), - cleanMoveHistorySingle: vitest.fn(), - }; -}; diff --git a/server/test/repositories/notification.repository.mock.ts b/server/test/repositories/notification.repository.mock.ts deleted file mode 100644 index 3aa7f63cf2..0000000000 --- a/server/test/repositories/notification.repository.mock.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { NotificationRepository } from 'src/repositories/notification.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked } from 'vitest'; - -export const newNotificationRepositoryMock = (): Mocked> => { - return { - renderEmail: vitest.fn(), - sendEmail: vitest.fn().mockResolvedValue({ messageId: 'message-1' }), - verifySmtp: vitest.fn(), - }; -}; diff --git a/server/test/repositories/oauth.repository.mock.ts b/server/test/repositories/oauth.repository.mock.ts deleted file mode 100644 index 64777fa671..0000000000 --- a/server/test/repositories/oauth.repository.mock.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { OAuthRepository } from 'src/repositories/oauth.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked } from 'vitest'; - -export const newOAuthRepositoryMock = (): Mocked> => { - return { - init: vitest.fn(), - authorize: vitest.fn(), - getLogoutEndpoint: vitest.fn(), - getProfile: vitest.fn(), - }; -}; diff --git a/server/test/repositories/partner.repository.mock.ts b/server/test/repositories/partner.repository.mock.ts deleted file mode 100644 index 6f9d4a36be..0000000000 --- a/server/test/repositories/partner.repository.mock.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { PartnerRepository } from 'src/repositories/partner.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newPartnerRepositoryMock = (): Mocked> => { - return { - create: vitest.fn(), - remove: vitest.fn(), - getAll: vitest.fn().mockResolvedValue([]), - get: vitest.fn(), - update: vitest.fn(), - }; -}; diff --git a/server/test/repositories/person.repository.mock.ts b/server/test/repositories/person.repository.mock.ts deleted file mode 100644 index c8a4253edc..0000000000 --- a/server/test/repositories/person.repository.mock.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { PersonRepository } from 'src/repositories/person.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newPersonRepositoryMock = (): Mocked> => { - return { - getById: vitest.fn(), - getAll: vitest.fn(), - getAllForUser: vitest.fn(), - getAllWithoutFaces: vitest.fn(), - - getByName: vitest.fn(), - getDistinctNames: vitest.fn(), - - create: vitest.fn(), - createAll: vitest.fn(), - update: vitest.fn(), - updateAll: vitest.fn(), - delete: vitest.fn(), - deleteFaces: vitest.fn(), - - getStatistics: vitest.fn(), - getAllFaces: vitest.fn(), - getFacesByIds: vitest.fn(), - getRandomFace: vitest.fn(), - - reassignFaces: vitest.fn(), - unassignFaces: vitest.fn(), - refreshFaces: vitest.fn(), - getFaces: vitest.fn(), - reassignFace: vitest.fn(), - getFaceById: vitest.fn(), - getFaceByIdWithAssets: vitest.fn(), - getNumberOfPeople: vitest.fn(), - getLatestFaceDate: vitest.fn(), - - createAssetFace: vitest.fn(), - deleteAssetFace: vitest.fn(), - softDeleteAssetFaces: vitest.fn(), - }; -}; diff --git a/server/test/repositories/process.repository.mock.ts b/server/test/repositories/process.repository.mock.ts deleted file mode 100644 index f78975310b..0000000000 --- a/server/test/repositories/process.repository.mock.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ProcessRepository } from 'src/repositories/process.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newProcessRepositoryMock = (): Mocked> => { - return { - spawn: vitest.fn(), - }; -}; diff --git a/server/test/repositories/search.repository.mock.ts b/server/test/repositories/search.repository.mock.ts deleted file mode 100644 index 520bf23b3e..0000000000 --- a/server/test/repositories/search.repository.mock.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { SearchRepository } from 'src/repositories/search.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newSearchRepositoryMock = (): Mocked> => { - return { - searchMetadata: vitest.fn(), - searchSmart: vitest.fn(), - searchDuplicates: vitest.fn(), - searchFaces: vitest.fn(), - searchRandom: vitest.fn(), - upsert: vitest.fn(), - searchPlaces: vitest.fn(), - getAssetsByCity: vitest.fn(), - deleteAllSearchEmbeddings: vitest.fn(), - getDimensionSize: vitest.fn(), - setDimensionSize: vitest.fn(), - getCameraMakes: vitest.fn(), - getCameraModels: vitest.fn(), - getCities: vitest.fn(), - getCountries: vitest.fn(), - getStates: vitest.fn(), - }; -}; diff --git a/server/test/repositories/server-info.repository.mock.ts b/server/test/repositories/server-info.repository.mock.ts deleted file mode 100644 index 49f955b283..0000000000 --- a/server/test/repositories/server-info.repository.mock.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ServerInfoRepository } from 'src/repositories/server-info.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newServerInfoRepositoryMock = (): Mocked> => { - return { - getGitHubRelease: vitest.fn(), - getBuildVersions: vitest.fn(), - }; -}; diff --git a/server/test/repositories/session.repository.mock.ts b/server/test/repositories/session.repository.mock.ts deleted file mode 100644 index b519b07e36..0000000000 --- a/server/test/repositories/session.repository.mock.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { SessionRepository } from 'src/repositories/session.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newSessionRepositoryMock = (): Mocked> => { - return { - search: vitest.fn(), - create: vitest.fn() as any, - update: vitest.fn() as any, - delete: vitest.fn(), - getByToken: vitest.fn(), - getByUserId: vitest.fn(), - }; -}; diff --git a/server/test/repositories/shared-link.repository.mock.ts b/server/test/repositories/shared-link.repository.mock.ts deleted file mode 100644 index 66044b9eed..0000000000 --- a/server/test/repositories/shared-link.repository.mock.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { SharedLinkRepository } from 'src/repositories/shared-link.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newSharedLinkRepositoryMock = (): Mocked> => { - return { - getAll: vitest.fn(), - get: vitest.fn(), - getByKey: vitest.fn(), - create: vitest.fn(), - remove: vitest.fn(), - update: vitest.fn(), - }; -}; diff --git a/server/test/repositories/stack.repository.mock.ts b/server/test/repositories/stack.repository.mock.ts deleted file mode 100644 index 74fef6f4b1..0000000000 --- a/server/test/repositories/stack.repository.mock.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { StackRepository } from 'src/repositories/stack.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newStackRepositoryMock = (): Mocked> => { - return { - search: vitest.fn(), - create: vitest.fn(), - update: vitest.fn(), - delete: vitest.fn(), - getById: vitest.fn(), - deleteAll: vitest.fn(), - }; -}; diff --git a/server/test/repositories/sync.repository.mock.ts b/server/test/repositories/sync.repository.mock.ts deleted file mode 100644 index c7fb154db7..0000000000 --- a/server/test/repositories/sync.repository.mock.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { SyncRepository } from 'src/repositories/sync.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newSyncRepositoryMock = (): Mocked> => { - return { - getCheckpoints: vitest.fn(), - upsertCheckpoints: vitest.fn(), - deleteCheckpoints: vitest.fn(), - getUserUpserts: vitest.fn(), - getUserDeletes: vitest.fn(), - getPartnerUpserts: vitest.fn(), - getPartnerDeletes: vitest.fn(), - getPartnerAssetsUpserts: vitest.fn(), - getPartnerAssetDeletes: vitest.fn(), - getAssetDeletes: vitest.fn(), - getAssetUpserts: vitest.fn(), - getAssetExifsUpserts: vitest.fn(), - getPartnerAssetExifsUpserts: vitest.fn(), - }; -}; diff --git a/server/test/repositories/tag.repository.mock.ts b/server/test/repositories/tag.repository.mock.ts deleted file mode 100644 index b6313ca798..0000000000 --- a/server/test/repositories/tag.repository.mock.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { TagRepository } from 'src/repositories/tag.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newTagRepositoryMock = (): Mocked> => { - return { - getAll: vitest.fn(), - getByValue: vitest.fn(), - upsertValue: vitest.fn(), - replaceAssetTags: vitest.fn(), - - get: vitest.fn(), - create: vitest.fn(), - update: vitest.fn(), - delete: vitest.fn(), - - getAssetIds: vitest.fn(), - addAssetIds: vitest.fn(), - removeAssetIds: vitest.fn(), - upsertAssetIds: vitest.fn(), - deleteEmptyTags: vitest.fn(), - }; -}; diff --git a/server/test/repositories/trash.repository.mock.ts b/server/test/repositories/trash.repository.mock.ts deleted file mode 100644 index b42867213a..0000000000 --- a/server/test/repositories/trash.repository.mock.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { TrashRepository } from 'src/repositories/trash.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newTrashRepositoryMock = (): Mocked> => { - return { - empty: vitest.fn(), - restore: vitest.fn(), - restoreAll: vitest.fn(), - getDeletedIds: vitest.fn(), - }; -}; diff --git a/server/test/repositories/user.repository.mock.ts b/server/test/repositories/user.repository.mock.ts deleted file mode 100644 index 2dc6b9eec2..0000000000 --- a/server/test/repositories/user.repository.mock.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { UserRepository } from 'src/repositories/user.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newUserRepositoryMock = (): Mocked> => { - return { - get: vitest.fn(), - getMetadata: vitest.fn().mockResolvedValue([]), - getAdmin: vitest.fn(), - getByEmail: vitest.fn(), - getByStorageLabel: vitest.fn(), - getByOAuthId: vitest.fn(), - getUserStats: vitest.fn(), - getList: vitest.fn(), - create: vitest.fn(), - update: vitest.fn(), - delete: vitest.fn(), - restore: vitest.fn(), - getDeletedUsers: vitest.fn(), - hasAdmin: vitest.fn(), - updateUsage: vitest.fn(), - syncUsage: vitest.fn(), - upsertMetadata: vitest.fn(), - deleteMetadata: vitest.fn(), - }; -}; diff --git a/server/test/repositories/version-history.repository.mock.ts b/server/test/repositories/version-history.repository.mock.ts deleted file mode 100644 index 98a9166487..0000000000 --- a/server/test/repositories/version-history.repository.mock.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newVersionHistoryRepositoryMock = (): Mocked> => { - return { - getAll: vitest.fn().mockResolvedValue([]), - getLatest: vitest.fn(), - create: vitest.fn(), - }; -}; diff --git a/server/test/repositories/view.repository.mock.ts b/server/test/repositories/view.repository.mock.ts deleted file mode 100644 index 057d7ee28a..0000000000 --- a/server/test/repositories/view.repository.mock.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ViewRepository } from 'src/repositories/view-repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newViewRepositoryMock = (): Mocked> => { - return { - getAssetsByOriginalPath: vitest.fn(), - getUniqueOriginalPaths: vitest.fn(), - }; -}; diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts index d7b710eb7d..31effb129a 100644 --- a/server/test/small.factory.ts +++ b/server/test/small.factory.ts @@ -42,6 +42,23 @@ const authUserFactory = (authUser: Partial = {}) => ({ ...authUser, }); +const sessionFactory = () => ({ + id: newUuid(), + createdAt: newDate(), + updatedAt: newDate(), + updateId: newUpdateId(), + deviceOS: 'android', + deviceType: 'mobile', + token: 'abc123', + userId: newUuid(), +}); + +const stackFactory = () => ({ + id: newUuid(), + ownerId: newUuid(), + primaryAssetId: newUuid(), +}); + const userFactory = (user: Partial = {}) => ({ id: newUuid(), name: 'Test User', @@ -145,6 +162,12 @@ const memoryFactory = (memory: Partial = {}) => ({ ...memory, }); +const versionHistoryFactory = () => ({ + id: newUuid(), + createdAt: newDate(), + version: '1.123.45', +}); + export const factory = { activity: activityFactory, apiKey: apiKeyFactory, @@ -154,5 +177,8 @@ export const factory = { authUser: authUserFactory, library: libraryFactory, memory: memoryFactory, + session: sessionFactory, + stack: stackFactory, user: userFactory, + versionHistory: versionHistoryFactory, }; diff --git a/server/test/utils.ts b/server/test/utils.ts index a4a06f91d3..988d4cd97e 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -1,3 +1,4 @@ +import { ClassConstructor } from 'class-transformer'; import { Kysely, sql } from 'kysely'; import { PostgresJSDialect } from 'kysely-postgres-js'; import { ChildProcessWithoutNullStreams } from 'node:child_process'; @@ -50,48 +51,59 @@ import { ViewRepository } from 'src/repositories/view-repository'; import { BaseService } from 'src/services/base.service'; import { RepositoryInterface } from 'src/types'; import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; -import { newActivityRepositoryMock } from 'test/repositories/activity.repository.mock'; -import { newAlbumUserRepositoryMock } from 'test/repositories/album-user.repository.mock'; -import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; -import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock'; import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; -import { newAuditRepositoryMock } from 'test/repositories/audit.repository.mock'; import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; -import { newCronRepositoryMock } from 'test/repositories/cron.repository.mock'; import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; -import { newDownloadRepositoryMock } from 'test/repositories/download.repository.mock'; -import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; -import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock'; -import { newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock'; -import { newMapRepositoryMock } from 'test/repositories/map.repository.mock'; import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock'; -import { newMemoryRepositoryMock } from 'test/repositories/memory.repository.mock'; import { newMetadataRepositoryMock } from 'test/repositories/metadata.repository.mock'; -import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock'; -import { newNotificationRepositoryMock } from 'test/repositories/notification.repository.mock'; -import { newOAuthRepositoryMock } from 'test/repositories/oauth.repository.mock'; -import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock'; -import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock'; -import { newProcessRepositoryMock } from 'test/repositories/process.repository.mock'; -import { newSearchRepositoryMock } from 'test/repositories/search.repository.mock'; -import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock'; -import { newSessionRepositoryMock } from 'test/repositories/session.repository.mock'; -import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock'; -import { newStackRepositoryMock } from 'test/repositories/stack.repository.mock'; import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; -import { newSyncRepositoryMock } from 'test/repositories/sync.repository.mock'; import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; -import { newTagRepositoryMock } from 'test/repositories/tag.repository.mock'; import { ITelemetryRepositoryMock, newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock'; -import { newTrashRepositoryMock } from 'test/repositories/trash.repository.mock'; -import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; -import { newVersionHistoryRepositoryMock } from 'test/repositories/version-history.repository.mock'; -import { newViewRepositoryMock } from 'test/repositories/view.repository.mock'; import { Readable } from 'typeorm/platform/PlatformTools'; -import { Mocked, vitest } from 'vitest'; +import { assert, Mocked, vitest } from 'vitest'; + +const mockFn = (label: string, { strict }: { strict: boolean }) => { + const message = `Called a mock function without a mock implementation (${label})`; + return vitest.fn().mockImplementation(() => { + if (strict) { + assert.fail(message); + } else { + // console.warn(message); + } + }); +}; + +export const automock = ( + Dependency: ClassConstructor, + options?: { + args?: ConstructorParameters>; + strict?: boolean; + }, +): Mocked => { + const mock: Record = {}; + const strict = options?.strict ?? true; + const args = options?.args ?? []; + + const instance = new Dependency(...args); + for (const property of Object.getOwnPropertyNames(Dependency.prototype)) { + if (property === 'constructor') { + continue; + } + + const label = `${Dependency.name}.${property}`; + // console.log(`Automocking ${label}`); + + const target = instance[property as keyof T]; + if (typeof target === 'function') { + mock[property] = mockFn(label, { strict }); + continue; + } + } + + return mock as Mocked; +}; export type ServiceOverrides = { access: AccessRepository; @@ -153,48 +165,52 @@ export const newTestService = ( Service: Constructor, overrides: Partial = {}, ) => { + const loggerMock = { setContext: () => {} }; + const configMock = { getEnv: () => ({}) }; + const mocks: ServiceMocks = { access: newAccessRepositoryMock(), - logger: newLoggingRepositoryMock(), - cron: newCronRepositoryMock(), + logger: automock(LoggingRepository, { args: [, configMock], strict: false }), + cron: automock(CronRepository, { args: [, loggerMock] }), crypto: newCryptoRepositoryMock(), - activity: newActivityRepositoryMock(), - audit: newAuditRepositoryMock(), - album: newAlbumRepositoryMock(), - albumUser: newAlbumUserRepositoryMock(), + activity: automock(ActivityRepository), + audit: automock(AuditRepository), + album: automock(AlbumRepository, { strict: false }), + albumUser: automock(AlbumUserRepository), asset: newAssetRepositoryMock(), config: newConfigRepositoryMock(), database: newDatabaseRepositoryMock(), - downloadRepository: newDownloadRepositoryMock(), - event: newEventRepositoryMock(), + downloadRepository: automock(DownloadRepository, { strict: false }), + event: automock(EventRepository, { args: [, , loggerMock], strict: false }), job: newJobRepositoryMock(), - apiKey: newKeyRepositoryMock(), - library: newLibraryRepositoryMock(), - machineLearning: newMachineLearningRepositoryMock(), - map: newMapRepositoryMock(), + apiKey: automock(ApiKeyRepository), + library: automock(LibraryRepository, { strict: false }), + machineLearning: automock(MachineLearningRepository, { args: [loggerMock], strict: false }), + map: automock(MapRepository, { args: [undefined, undefined, { setContext: () => {} }] }), media: newMediaRepositoryMock(), - memory: newMemoryRepositoryMock(), + memory: automock(MemoryRepository), metadata: newMetadataRepositoryMock(), - move: newMoveRepositoryMock(), - notification: newNotificationRepositoryMock(), - oauth: newOAuthRepositoryMock(), - partner: newPartnerRepositoryMock(), - person: newPersonRepositoryMock(), - process: newProcessRepositoryMock(), - search: newSearchRepositoryMock(), - serverInfo: newServerInfoRepositoryMock(), - session: newSessionRepositoryMock(), - sharedLink: newSharedLinkRepositoryMock(), - stack: newStackRepositoryMock(), + move: automock(MoveRepository, { strict: false }), + notification: automock(NotificationRepository, { args: [loggerMock] }), + oauth: automock(OAuthRepository, { args: [loggerMock] }), + partner: automock(PartnerRepository, { strict: false }), + person: automock(PersonRepository, { strict: false }), + process: automock(ProcessRepository, { args: [loggerMock] }), + search: automock(SearchRepository, { args: [loggerMock], strict: false }), + serverInfo: automock(ServerInfoRepository, { args: [, loggerMock], strict: false }), + session: automock(SessionRepository), + sharedLink: automock(SharedLinkRepository), + stack: automock(StackRepository), storage: newStorageRepositoryMock(), - sync: newSyncRepositoryMock(), + sync: automock(SyncRepository), systemMetadata: newSystemMetadataRepositoryMock(), - tag: newTagRepositoryMock(), + // systemMetadata: automock(SystemMetadataRepository, { strict: false }), + tag: automock(TagRepository, { args: [, loggerMock], strict: false }), telemetry: newTelemetryRepositoryMock(), - trash: newTrashRepositoryMock(), - user: newUserRepositoryMock(), - versionHistory: newVersionHistoryRepositoryMock(), - view: newViewRepositoryMock(), + trash: automock(TrashRepository), + user: automock(UserRepository, { strict: false }), + versionHistory: automock(VersionHistoryRepository), + view: automock(ViewRepository), }; const sut = new Service(