refactor: repository mocks (#16785)

This commit is contained in:
Jason Rasmussen 2025-03-10 16:52:44 -04:00 committed by GitHub
parent 1b35400043
commit 1382b27349
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
59 changed files with 320 additions and 550 deletions

View File

@ -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', () => {

View File

@ -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(() => {

View File

@ -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);

View File

@ -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,
});

View File

@ -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);

View File

@ -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) },

View File

@ -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));
});
});

View File

@ -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,

View File

@ -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: {

View File

@ -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);

View File

@ -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' },

View File

@ -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();

View File

@ -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]);
});
});
});

View File

@ -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);

View File

@ -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' }),

View File

@ -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 });

View File

@ -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]);

View File

@ -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');

View File

@ -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'] }),

View File

@ -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'] });

View File

@ -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();

View File

@ -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();
});
});

View File

@ -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, {

View File

@ -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'] });

View File

@ -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) });
});

View File

@ -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<DB>) {
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);

View File

@ -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<string, any>) => {
const data = newRandomImage();

View File

@ -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<RepositoryInterface<ActivityRepository>> => {
return {
search: vitest.fn(),
create: vitest.fn(),
delete: vitest.fn(),
getStatistics: vitest.fn(),
};
};

View File

@ -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<RepositoryInterface<AlbumUserRepository>> => {
return {
create: vitest.fn(),
delete: vitest.fn(),
update: vitest.fn(),
};
};

View File

@ -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<RepositoryInterface<AlbumRepository>> => {
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(),
};
};

View File

@ -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<RepositoryInterface<ApiKeyRepository>> => {
return {
create: vitest.fn(),
update: vitest.fn(),
delete: vitest.fn(),
getKey: vitest.fn(),
getById: vitest.fn(),
getByUserId: vitest.fn(),
};
};

View File

@ -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<RepositoryInterface<AuditRepository>> => {
return {
getAfter: vitest.fn(),
removeBefore: vitest.fn(),
};
};

View File

@ -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<RepositoryInterface<CronRepository>> => {
return {
create: vitest.fn(),
update: vitest.fn(),
};
};

View File

@ -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<RepositoryInterface<DownloadRepository>> => {
return {
downloadAssetIds: vitest.fn(),
downloadMotionAssetIds: vitest.fn(),
downloadAlbumId: vitest.fn(),
downloadUserId: vitest.fn(),
};
};

View File

@ -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<RepositoryInterface<EventRepository>> => {
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(),
};
};

View File

@ -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<RepositoryInterface<LibraryRepository>> => {
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(),
};
};

View File

@ -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<RepositoryInterface<LoggingRepository>> => {
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<LoggingRepository> as LoggingRepository;

View File

@ -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<RepositoryInterface<MachineLearningRepository>> => {
return {
encodeImage: vitest.fn(),
encodeText: vitest.fn(),
detectFaces: vitest.fn(),
};
};

View File

@ -1,11 +0,0 @@
import { MapRepository } from 'src/repositories/map.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked } from 'vitest';
export const newMapRepositoryMock = (): Mocked<RepositoryInterface<MapRepository>> => {
return {
init: vitest.fn(),
reverseGeocode: vitest.fn(),
getMapMarkers: vitest.fn(),
};
};

View File

@ -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<RepositoryInterface<MemoryRepository>> => {
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(),
};
};

View File

@ -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<RepositoryInterface<MoveRepository>> => {
return {
create: vitest.fn(),
getByEntity: vitest.fn(),
update: vitest.fn(),
delete: vitest.fn(),
cleanMoveHistory: vitest.fn(),
cleanMoveHistorySingle: vitest.fn(),
};
};

View File

@ -1,11 +0,0 @@
import { NotificationRepository } from 'src/repositories/notification.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked } from 'vitest';
export const newNotificationRepositoryMock = (): Mocked<RepositoryInterface<NotificationRepository>> => {
return {
renderEmail: vitest.fn(),
sendEmail: vitest.fn().mockResolvedValue({ messageId: 'message-1' }),
verifySmtp: vitest.fn(),
};
};

View File

@ -1,12 +0,0 @@
import { OAuthRepository } from 'src/repositories/oauth.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked } from 'vitest';
export const newOAuthRepositoryMock = (): Mocked<RepositoryInterface<OAuthRepository>> => {
return {
init: vitest.fn(),
authorize: vitest.fn(),
getLogoutEndpoint: vitest.fn(),
getProfile: vitest.fn(),
};
};

View File

@ -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<RepositoryInterface<PartnerRepository>> => {
return {
create: vitest.fn(),
remove: vitest.fn(),
getAll: vitest.fn().mockResolvedValue([]),
get: vitest.fn(),
update: vitest.fn(),
};
};

View File

@ -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<RepositoryInterface<PersonRepository>> => {
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(),
};
};

View File

@ -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<RepositoryInterface<ProcessRepository>> => {
return {
spawn: vitest.fn(),
};
};

View File

@ -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<RepositoryInterface<SearchRepository>> => {
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(),
};
};

View File

@ -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<RepositoryInterface<ServerInfoRepository>> => {
return {
getGitHubRelease: vitest.fn(),
getBuildVersions: vitest.fn(),
};
};

View File

@ -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<RepositoryInterface<SessionRepository>> => {
return {
search: vitest.fn(),
create: vitest.fn() as any,
update: vitest.fn() as any,
delete: vitest.fn(),
getByToken: vitest.fn(),
getByUserId: vitest.fn(),
};
};

View File

@ -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<RepositoryInterface<SharedLinkRepository>> => {
return {
getAll: vitest.fn(),
get: vitest.fn(),
getByKey: vitest.fn(),
create: vitest.fn(),
remove: vitest.fn(),
update: vitest.fn(),
};
};

View File

@ -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<RepositoryInterface<StackRepository>> => {
return {
search: vitest.fn(),
create: vitest.fn(),
update: vitest.fn(),
delete: vitest.fn(),
getById: vitest.fn(),
deleteAll: vitest.fn(),
};
};

View File

@ -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<RepositoryInterface<SyncRepository>> => {
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(),
};
};

View File

@ -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<RepositoryInterface<TagRepository>> => {
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(),
};
};

View File

@ -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<RepositoryInterface<TrashRepository>> => {
return {
empty: vitest.fn(),
restore: vitest.fn(),
restoreAll: vitest.fn(),
getDeletedIds: vitest.fn(),
};
};

View File

@ -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<RepositoryInterface<UserRepository>> => {
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(),
};
};

View File

@ -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<RepositoryInterface<VersionHistoryRepository>> => {
return {
getAll: vitest.fn().mockResolvedValue([]),
getLatest: vitest.fn(),
create: vitest.fn(),
};
};

View File

@ -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<RepositoryInterface<ViewRepository>> => {
return {
getAssetsByOriginalPath: vitest.fn(),
getUniqueOriginalPaths: vitest.fn(),
};
};

View File

@ -42,6 +42,23 @@ const authUserFactory = (authUser: Partial<AuthUser> = {}) => ({
...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<User> = {}) => ({
id: newUuid(),
name: 'Test User',
@ -145,6 +162,12 @@ const memoryFactory = (memory: Partial<MemoryItem> = {}) => ({
...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,
};

View File

@ -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 = <T>(
Dependency: ClassConstructor<T>,
options?: {
args?: ConstructorParameters<ClassConstructor<T>>;
strict?: boolean;
},
): Mocked<T> => {
const mock: Record<string, unknown> = {};
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<T>;
};
export type ServiceOverrides = {
access: AccessRepository;
@ -153,48 +165,52 @@ export const newTestService = <T extends BaseService>(
Service: Constructor<T, BaseServiceArgs>,
overrides: Partial<ServiceOverrides> = {},
) => {
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(