refactor: small test factories (#26862)

This commit is contained in:
Daniel Dietzler
2026-03-12 19:48:49 +01:00
committed by GitHub
parent 3fd24e2083
commit 001d7d083f
18 changed files with 414 additions and 389 deletions

View File

@@ -1,8 +1,10 @@
import { BadRequestException } from '@nestjs/common'; import { BadRequestException } from '@nestjs/common';
import { ReactionType } from 'src/dtos/activity.dto'; import { ReactionType } from 'src/dtos/activity.dto';
import { ActivityService } from 'src/services/activity.service'; import { ActivityService } from 'src/services/activity.service';
import { ActivityFactory } from 'test/factories/activity.factory';
import { AuthFactory } from 'test/factories/auth.factory';
import { getForActivity } from 'test/mappers'; import { getForActivity } from 'test/mappers';
import { factory, newUuid, newUuids } from 'test/small.factory'; import { newUuid, newUuids } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils'; import { newTestService, ServiceMocks } from 'test/utils';
describe(ActivityService.name, () => { describe(ActivityService.name, () => {
@@ -24,7 +26,7 @@ describe(ActivityService.name, () => {
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId])); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId]));
mocks.activity.search.mockResolvedValue([]); mocks.activity.search.mockResolvedValue([]);
await expect(sut.getAll(factory.auth({ user: { id: userId } }), { assetId, albumId })).resolves.toEqual([]); await expect(sut.getAll(AuthFactory.create({ id: userId }), { assetId, albumId })).resolves.toEqual([]);
expect(mocks.activity.search).toHaveBeenCalledWith({ assetId, albumId, isLiked: undefined }); expect(mocks.activity.search).toHaveBeenCalledWith({ assetId, albumId, isLiked: undefined });
}); });
@@ -36,7 +38,7 @@ describe(ActivityService.name, () => {
mocks.activity.search.mockResolvedValue([]); mocks.activity.search.mockResolvedValue([]);
await expect( await expect(
sut.getAll(factory.auth({ user: { id: userId } }), { assetId, albumId, type: ReactionType.LIKE }), sut.getAll(AuthFactory.create({ id: userId }), { assetId, albumId, type: ReactionType.LIKE }),
).resolves.toEqual([]); ).resolves.toEqual([]);
expect(mocks.activity.search).toHaveBeenCalledWith({ assetId, albumId, isLiked: true }); expect(mocks.activity.search).toHaveBeenCalledWith({ assetId, albumId, isLiked: true });
@@ -48,7 +50,9 @@ describe(ActivityService.name, () => {
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId])); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId]));
mocks.activity.search.mockResolvedValue([]); mocks.activity.search.mockResolvedValue([]);
await expect(sut.getAll(factory.auth(), { assetId, albumId, type: ReactionType.COMMENT })).resolves.toEqual([]); await expect(sut.getAll(AuthFactory.create(), { assetId, albumId, type: ReactionType.COMMENT })).resolves.toEqual(
[],
);
expect(mocks.activity.search).toHaveBeenCalledWith({ assetId, albumId, isLiked: false }); expect(mocks.activity.search).toHaveBeenCalledWith({ assetId, albumId, isLiked: false });
}); });
@@ -61,7 +65,10 @@ describe(ActivityService.name, () => {
mocks.activity.getStatistics.mockResolvedValue({ comments: 1, likes: 3 }); mocks.activity.getStatistics.mockResolvedValue({ comments: 1, likes: 3 });
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId])); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId]));
await expect(sut.getStatistics(factory.auth(), { assetId, albumId })).resolves.toEqual({ comments: 1, likes: 3 }); await expect(sut.getStatistics(AuthFactory.create(), { assetId, albumId })).resolves.toEqual({
comments: 1,
likes: 3,
});
}); });
}); });
@@ -70,18 +77,18 @@ describe(ActivityService.name, () => {
const [albumId, assetId] = newUuids(); const [albumId, assetId] = newUuids();
await expect( await expect(
sut.create(factory.auth(), { albumId, assetId, type: ReactionType.COMMENT, comment: 'comment' }), sut.create(AuthFactory.create(), { albumId, assetId, type: ReactionType.COMMENT, comment: 'comment' }),
).rejects.toBeInstanceOf(BadRequestException); ).rejects.toBeInstanceOf(BadRequestException);
}); });
it('should create a comment', async () => { it('should create a comment', async () => {
const [albumId, assetId, userId] = newUuids(); const [albumId, assetId, userId] = newUuids();
const activity = factory.activity({ albumId, assetId, userId }); const activity = ActivityFactory.create({ albumId, assetId, userId });
mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set([albumId])); mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set([albumId]));
mocks.activity.create.mockResolvedValue(getForActivity(activity)); mocks.activity.create.mockResolvedValue(getForActivity(activity));
await sut.create(factory.auth({ user: { id: userId } }), { await sut.create(AuthFactory.create({ id: userId }), {
albumId, albumId,
assetId, assetId,
type: ReactionType.COMMENT, type: ReactionType.COMMENT,
@@ -99,38 +106,38 @@ describe(ActivityService.name, () => {
it('should fail because activity is disabled for the album', async () => { it('should fail because activity is disabled for the album', async () => {
const [albumId, assetId] = newUuids(); const [albumId, assetId] = newUuids();
const activity = factory.activity({ albumId, assetId }); const activity = ActivityFactory.create({ albumId, assetId });
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId])); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId]));
mocks.activity.create.mockResolvedValue(getForActivity(activity)); mocks.activity.create.mockResolvedValue(getForActivity(activity));
await expect( await expect(
sut.create(factory.auth(), { albumId, assetId, type: ReactionType.COMMENT, comment: 'comment' }), sut.create(AuthFactory.create(), { albumId, assetId, type: ReactionType.COMMENT, comment: 'comment' }),
).rejects.toBeInstanceOf(BadRequestException); ).rejects.toBeInstanceOf(BadRequestException);
}); });
it('should create a like', async () => { it('should create a like', async () => {
const [albumId, assetId, userId] = newUuids(); const [albumId, assetId, userId] = newUuids();
const activity = factory.activity({ userId, albumId, assetId, isLiked: true }); const activity = ActivityFactory.create({ userId, albumId, assetId, isLiked: true });
mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set([albumId])); mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set([albumId]));
mocks.activity.create.mockResolvedValue(getForActivity(activity)); mocks.activity.create.mockResolvedValue(getForActivity(activity));
mocks.activity.search.mockResolvedValue([]); mocks.activity.search.mockResolvedValue([]);
await sut.create(factory.auth({ user: { id: userId } }), { albumId, assetId, type: ReactionType.LIKE }); await sut.create(AuthFactory.create({ id: userId }), { albumId, assetId, type: ReactionType.LIKE });
expect(mocks.activity.create).toHaveBeenCalledWith({ userId: activity.userId, albumId, assetId, isLiked: true }); expect(mocks.activity.create).toHaveBeenCalledWith({ userId: activity.userId, albumId, assetId, isLiked: true });
}); });
it('should skip if like exists', async () => { it('should skip if like exists', async () => {
const [albumId, assetId] = newUuids(); const [albumId, assetId] = newUuids();
const activity = factory.activity({ albumId, assetId, isLiked: true }); const activity = ActivityFactory.create({ albumId, assetId, isLiked: true });
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId])); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId]));
mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set([albumId])); mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set([albumId]));
mocks.activity.search.mockResolvedValue([getForActivity(activity)]); mocks.activity.search.mockResolvedValue([getForActivity(activity)]);
await sut.create(factory.auth(), { albumId, assetId, type: ReactionType.LIKE }); await sut.create(AuthFactory.create(), { albumId, assetId, type: ReactionType.LIKE });
expect(mocks.activity.create).not.toHaveBeenCalled(); expect(mocks.activity.create).not.toHaveBeenCalled();
}); });
@@ -138,29 +145,29 @@ describe(ActivityService.name, () => {
describe('delete', () => { describe('delete', () => {
it('should require access', async () => { it('should require access', async () => {
await expect(sut.delete(factory.auth(), newUuid())).rejects.toBeInstanceOf(BadRequestException); await expect(sut.delete(AuthFactory.create(), newUuid())).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.activity.delete).not.toHaveBeenCalled(); expect(mocks.activity.delete).not.toHaveBeenCalled();
}); });
it('should let the activity owner delete a comment', async () => { it('should let the activity owner delete a comment', async () => {
const activity = factory.activity(); const activity = ActivityFactory.create();
mocks.access.activity.checkOwnerAccess.mockResolvedValue(new Set([activity.id])); mocks.access.activity.checkOwnerAccess.mockResolvedValue(new Set([activity.id]));
mocks.activity.delete.mockResolvedValue(); mocks.activity.delete.mockResolvedValue();
await sut.delete(factory.auth(), activity.id); await sut.delete(AuthFactory.create(), activity.id);
expect(mocks.activity.delete).toHaveBeenCalledWith(activity.id); expect(mocks.activity.delete).toHaveBeenCalledWith(activity.id);
}); });
it('should let the album owner delete a comment', async () => { it('should let the album owner delete a comment', async () => {
const activity = factory.activity(); const activity = ActivityFactory.create();
mocks.access.activity.checkAlbumOwnerAccess.mockResolvedValue(new Set([activity.id])); mocks.access.activity.checkAlbumOwnerAccess.mockResolvedValue(new Set([activity.id]));
mocks.activity.delete.mockResolvedValue(); mocks.activity.delete.mockResolvedValue();
await sut.delete(factory.auth(), activity.id); await sut.delete(AuthFactory.create(), activity.id);
expect(mocks.activity.delete).toHaveBeenCalledWith(activity.id); expect(mocks.activity.delete).toHaveBeenCalledWith(activity.id);
}); });

View File

@@ -1,7 +1,10 @@
import { BadRequestException, ForbiddenException } from '@nestjs/common'; import { BadRequestException, ForbiddenException } from '@nestjs/common';
import { Permission } from 'src/enum'; import { Permission } from 'src/enum';
import { ApiKeyService } from 'src/services/api-key.service'; import { ApiKeyService } from 'src/services/api-key.service';
import { factory, newUuid } from 'test/small.factory'; import { ApiKeyFactory } from 'test/factories/api-key.factory';
import { AuthFactory } from 'test/factories/auth.factory';
import { SessionFactory } from 'test/factories/session.factory';
import { newUuid } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils'; import { newTestService, ServiceMocks } from 'test/utils';
describe(ApiKeyService.name, () => { describe(ApiKeyService.name, () => {
@@ -14,8 +17,8 @@ describe(ApiKeyService.name, () => {
describe('create', () => { describe('create', () => {
it('should create a new key', async () => { it('should create a new key', async () => {
const auth = factory.auth(); const auth = AuthFactory.create();
const apiKey = factory.apiKey({ userId: auth.user.id, permissions: [Permission.All] }); const apiKey = ApiKeyFactory.create({ userId: auth.user.id, permissions: [Permission.All] });
const key = 'super-secret'; const key = 'super-secret';
mocks.crypto.randomBytesAsText.mockReturnValue(key); mocks.crypto.randomBytesAsText.mockReturnValue(key);
@@ -34,8 +37,8 @@ describe(ApiKeyService.name, () => {
}); });
it('should not require a name', async () => { it('should not require a name', async () => {
const auth = factory.auth(); const auth = AuthFactory.create();
const apiKey = factory.apiKey({ userId: auth.user.id }); const apiKey = ApiKeyFactory.create({ userId: auth.user.id });
const key = 'super-secret'; const key = 'super-secret';
mocks.crypto.randomBytesAsText.mockReturnValue(key); mocks.crypto.randomBytesAsText.mockReturnValue(key);
@@ -54,7 +57,9 @@ describe(ApiKeyService.name, () => {
}); });
it('should throw an error if the api key does not have sufficient permissions', async () => { it('should throw an error if the api key does not have sufficient permissions', async () => {
const auth = factory.auth({ apiKey: { permissions: [Permission.AssetRead] } }); const auth = AuthFactory.from()
.apiKey({ permissions: [Permission.AssetRead] })
.build();
await expect(sut.create(auth, { permissions: [Permission.AssetUpdate] })).rejects.toBeInstanceOf( await expect(sut.create(auth, { permissions: [Permission.AssetUpdate] })).rejects.toBeInstanceOf(
BadRequestException, BadRequestException,
@@ -65,7 +70,7 @@ describe(ApiKeyService.name, () => {
describe('update', () => { describe('update', () => {
it('should throw an error if the key is not found', async () => { it('should throw an error if the key is not found', async () => {
const id = newUuid(); const id = newUuid();
const auth = factory.auth(); const auth = AuthFactory.create();
mocks.apiKey.getById.mockResolvedValue(void 0); mocks.apiKey.getById.mockResolvedValue(void 0);
@@ -77,8 +82,8 @@ describe(ApiKeyService.name, () => {
}); });
it('should update a key', async () => { it('should update a key', async () => {
const auth = factory.auth(); const auth = AuthFactory.create();
const apiKey = factory.apiKey({ userId: auth.user.id }); const apiKey = ApiKeyFactory.create({ userId: auth.user.id });
const newName = 'New name'; const newName = 'New name';
mocks.apiKey.getById.mockResolvedValue(apiKey); mocks.apiKey.getById.mockResolvedValue(apiKey);
@@ -93,8 +98,8 @@ describe(ApiKeyService.name, () => {
}); });
it('should update permissions', async () => { it('should update permissions', async () => {
const auth = factory.auth(); const auth = AuthFactory.create();
const apiKey = factory.apiKey({ userId: auth.user.id }); const apiKey = ApiKeyFactory.create({ userId: auth.user.id });
const newPermissions = [Permission.ActivityCreate, Permission.ActivityRead, Permission.ActivityUpdate]; const newPermissions = [Permission.ActivityCreate, Permission.ActivityRead, Permission.ActivityUpdate];
mocks.apiKey.getById.mockResolvedValue(apiKey); mocks.apiKey.getById.mockResolvedValue(apiKey);
@@ -111,8 +116,8 @@ describe(ApiKeyService.name, () => {
describe('api key auth', () => { describe('api key auth', () => {
it('should prevent adding Permission.all', async () => { it('should prevent adding Permission.all', async () => {
const permissions = [Permission.ApiKeyCreate, Permission.ApiKeyUpdate, Permission.AssetRead]; const permissions = [Permission.ApiKeyCreate, Permission.ApiKeyUpdate, Permission.AssetRead];
const auth = factory.auth({ apiKey: { permissions } }); const auth = AuthFactory.from().apiKey({ permissions }).build();
const apiKey = factory.apiKey({ userId: auth.user.id, permissions }); const apiKey = ApiKeyFactory.create({ userId: auth.user.id, permissions });
mocks.apiKey.getById.mockResolvedValue(apiKey); mocks.apiKey.getById.mockResolvedValue(apiKey);
@@ -125,8 +130,8 @@ describe(ApiKeyService.name, () => {
it('should prevent adding a new permission', async () => { it('should prevent adding a new permission', async () => {
const permissions = [Permission.ApiKeyCreate, Permission.ApiKeyUpdate, Permission.AssetRead]; const permissions = [Permission.ApiKeyCreate, Permission.ApiKeyUpdate, Permission.AssetRead];
const auth = factory.auth({ apiKey: { permissions } }); const auth = AuthFactory.from().apiKey({ permissions }).build();
const apiKey = factory.apiKey({ userId: auth.user.id, permissions }); const apiKey = ApiKeyFactory.create({ userId: auth.user.id, permissions });
mocks.apiKey.getById.mockResolvedValue(apiKey); mocks.apiKey.getById.mockResolvedValue(apiKey);
@@ -138,8 +143,10 @@ describe(ApiKeyService.name, () => {
}); });
it('should allow removing permissions', async () => { it('should allow removing permissions', async () => {
const auth = factory.auth({ apiKey: { permissions: [Permission.ApiKeyUpdate, Permission.AssetRead] } }); const auth = AuthFactory.from()
const apiKey = factory.apiKey({ .apiKey({ permissions: [Permission.ApiKeyUpdate, Permission.AssetRead] })
.build();
const apiKey = ApiKeyFactory.create({
userId: auth.user.id, userId: auth.user.id,
permissions: [Permission.AssetRead, Permission.AssetDelete], permissions: [Permission.AssetRead, Permission.AssetDelete],
}); });
@@ -158,10 +165,10 @@ describe(ApiKeyService.name, () => {
}); });
it('should allow adding new permissions', async () => { it('should allow adding new permissions', async () => {
const auth = factory.auth({ const auth = AuthFactory.from()
apiKey: { permissions: [Permission.ApiKeyUpdate, Permission.AssetRead, Permission.AssetUpdate] }, .apiKey({ permissions: [Permission.ApiKeyUpdate, Permission.AssetRead, Permission.AssetUpdate] })
}); .build();
const apiKey = factory.apiKey({ userId: auth.user.id, permissions: [Permission.AssetRead] }); const apiKey = ApiKeyFactory.create({ userId: auth.user.id, permissions: [Permission.AssetRead] });
mocks.apiKey.getById.mockResolvedValue(apiKey); mocks.apiKey.getById.mockResolvedValue(apiKey);
mocks.apiKey.update.mockResolvedValue(apiKey); mocks.apiKey.update.mockResolvedValue(apiKey);
@@ -183,7 +190,7 @@ describe(ApiKeyService.name, () => {
describe('delete', () => { describe('delete', () => {
it('should throw an error if the key is not found', async () => { it('should throw an error if the key is not found', async () => {
const auth = factory.auth(); const auth = AuthFactory.create();
const id = newUuid(); const id = newUuid();
mocks.apiKey.getById.mockResolvedValue(void 0); mocks.apiKey.getById.mockResolvedValue(void 0);
@@ -194,8 +201,8 @@ describe(ApiKeyService.name, () => {
}); });
it('should delete a key', async () => { it('should delete a key', async () => {
const auth = factory.auth(); const auth = AuthFactory.create();
const apiKey = factory.apiKey({ userId: auth.user.id }); const apiKey = ApiKeyFactory.create({ userId: auth.user.id });
mocks.apiKey.getById.mockResolvedValue(apiKey); mocks.apiKey.getById.mockResolvedValue(apiKey);
mocks.apiKey.delete.mockResolvedValue(); mocks.apiKey.delete.mockResolvedValue();
@@ -208,8 +215,8 @@ describe(ApiKeyService.name, () => {
describe('getMine', () => { describe('getMine', () => {
it('should not work with a session token', async () => { it('should not work with a session token', async () => {
const session = factory.session(); const session = SessionFactory.create();
const auth = factory.auth({ session }); const auth = AuthFactory.from().session(session).build();
mocks.apiKey.getById.mockResolvedValue(void 0); mocks.apiKey.getById.mockResolvedValue(void 0);
@@ -219,8 +226,8 @@ describe(ApiKeyService.name, () => {
}); });
it('should throw an error if the key is not found', async () => { it('should throw an error if the key is not found', async () => {
const apiKey = factory.authApiKey(); const apiKey = ApiKeyFactory.create();
const auth = factory.auth({ apiKey }); const auth = AuthFactory.from().apiKey(apiKey).build();
mocks.apiKey.getById.mockResolvedValue(void 0); mocks.apiKey.getById.mockResolvedValue(void 0);
@@ -230,8 +237,8 @@ describe(ApiKeyService.name, () => {
}); });
it('should get a key by id', async () => { it('should get a key by id', async () => {
const auth = factory.auth(); const auth = AuthFactory.create();
const apiKey = factory.apiKey({ userId: auth.user.id }); const apiKey = ApiKeyFactory.create({ userId: auth.user.id });
mocks.apiKey.getById.mockResolvedValue(apiKey); mocks.apiKey.getById.mockResolvedValue(apiKey);
@@ -243,7 +250,7 @@ describe(ApiKeyService.name, () => {
describe('getById', () => { describe('getById', () => {
it('should throw an error if the key is not found', async () => { it('should throw an error if the key is not found', async () => {
const auth = factory.auth(); const auth = AuthFactory.create();
const id = newUuid(); const id = newUuid();
mocks.apiKey.getById.mockResolvedValue(void 0); mocks.apiKey.getById.mockResolvedValue(void 0);
@@ -254,8 +261,8 @@ describe(ApiKeyService.name, () => {
}); });
it('should get a key by id', async () => { it('should get a key by id', async () => {
const auth = factory.auth(); const auth = AuthFactory.create();
const apiKey = factory.apiKey({ userId: auth.user.id }); const apiKey = ApiKeyFactory.create({ userId: auth.user.id });
mocks.apiKey.getById.mockResolvedValue(apiKey); mocks.apiKey.getById.mockResolvedValue(apiKey);
@@ -267,8 +274,8 @@ describe(ApiKeyService.name, () => {
describe('getAll', () => { describe('getAll', () => {
it('should return all the keys for a user', async () => { it('should return all the keys for a user', async () => {
const auth = factory.auth(); const auth = AuthFactory.create();
const apiKey = factory.apiKey({ userId: auth.user.id }); const apiKey = ApiKeyFactory.create({ userId: auth.user.id });
mocks.apiKey.getByUserId.mockResolvedValue([apiKey]); mocks.apiKey.getByUserId.mockResolvedValue([apiKey]);

View File

@@ -7,6 +7,7 @@ import { AssetStats } from 'src/repositories/asset.repository';
import { AssetService } from 'src/services/asset.service'; import { AssetService } from 'src/services/asset.service';
import { AssetFactory } from 'test/factories/asset.factory'; import { AssetFactory } from 'test/factories/asset.factory';
import { AuthFactory } from 'test/factories/auth.factory'; import { AuthFactory } from 'test/factories/auth.factory';
import { PartnerFactory } from 'test/factories/partner.factory';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { getForAsset, getForAssetDeletion, getForPartner } from 'test/mappers'; import { getForAsset, getForAssetDeletion, getForPartner } from 'test/mappers';
import { factory, newUuid } from 'test/small.factory'; import { factory, newUuid } from 'test/small.factory';
@@ -80,8 +81,8 @@ describe(AssetService.name, () => {
}); });
it('should not include partner assets if not in timeline', async () => { it('should not include partner assets if not in timeline', async () => {
const partner = factory.partner({ inTimeline: false }); const partner = PartnerFactory.create({ inTimeline: false });
const auth = factory.auth({ user: { id: partner.sharedWithId } }); const auth = AuthFactory.create({ id: partner.sharedWithId });
mocks.asset.getRandom.mockResolvedValue([getForAsset(AssetFactory.create())]); mocks.asset.getRandom.mockResolvedValue([getForAsset(AssetFactory.create())]);
mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]); mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]);
@@ -92,8 +93,8 @@ describe(AssetService.name, () => {
}); });
it('should include partner assets if in timeline', async () => { it('should include partner assets if in timeline', async () => {
const partner = factory.partner({ inTimeline: true }); const partner = PartnerFactory.create({ inTimeline: true });
const auth = factory.auth({ user: { id: partner.sharedWithId } }); const auth = AuthFactory.create({ id: partner.sharedWithId });
mocks.asset.getRandom.mockResolvedValue([getForAsset(AssetFactory.create())]); mocks.asset.getRandom.mockResolvedValue([getForAsset(AssetFactory.create())]);
mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]); mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]);

View File

@@ -6,9 +6,13 @@ import { AuthDto, SignUpDto } from 'src/dtos/auth.dto';
import { AuthType, Permission } from 'src/enum'; import { AuthType, Permission } from 'src/enum';
import { AuthService } from 'src/services/auth.service'; import { AuthService } from 'src/services/auth.service';
import { UserMetadataItem } from 'src/types'; import { UserMetadataItem } from 'src/types';
import { ApiKeyFactory } from 'test/factories/api-key.factory';
import { AuthFactory } from 'test/factories/auth.factory';
import { SessionFactory } from 'test/factories/session.factory';
import { UserFactory } from 'test/factories/user.factory';
import { sharedLinkStub } from 'test/fixtures/shared-link.stub'; import { sharedLinkStub } from 'test/fixtures/shared-link.stub';
import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub';
import { factory, newUuid } from 'test/small.factory'; import { newUuid } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils'; import { newTestService, ServiceMocks } from 'test/utils';
const oauthResponse = ({ const oauthResponse = ({
@@ -91,8 +95,8 @@ describe(AuthService.name, () => {
}); });
it('should successfully log the user in', async () => { it('should successfully log the user in', async () => {
const user = { ...(factory.user() as UserAdmin), password: 'immich_password' }; const user = UserFactory.create({ password: 'immich_password' });
const session = factory.session(); const session = SessionFactory.create();
mocks.user.getByEmail.mockResolvedValue(user); mocks.user.getByEmail.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(session); mocks.session.create.mockResolvedValue(session);
@@ -113,8 +117,8 @@ describe(AuthService.name, () => {
describe('changePassword', () => { describe('changePassword', () => {
it('should change the password', async () => { it('should change the password', async () => {
const user = factory.userAdmin(); const user = UserFactory.create();
const auth = factory.auth({ user }); const auth = AuthFactory.create(user);
const dto = { password: 'old-password', newPassword: 'new-password' }; const dto = { password: 'old-password', newPassword: 'new-password' };
mocks.user.getForChangePassword.mockResolvedValue({ id: user.id, password: 'hash-password' }); mocks.user.getForChangePassword.mockResolvedValue({ id: user.id, password: 'hash-password' });
@@ -132,8 +136,8 @@ describe(AuthService.name, () => {
}); });
it('should throw when password does not match existing password', async () => { it('should throw when password does not match existing password', async () => {
const user = factory.user(); const user = UserFactory.create();
const auth = factory.auth({ user }); const auth = AuthFactory.create(user);
const dto = { password: 'old-password', newPassword: 'new-password' }; const dto = { password: 'old-password', newPassword: 'new-password' };
mocks.crypto.compareBcrypt.mockReturnValue(false); mocks.crypto.compareBcrypt.mockReturnValue(false);
@@ -144,8 +148,8 @@ describe(AuthService.name, () => {
}); });
it('should throw when user does not have a password', async () => { it('should throw when user does not have a password', async () => {
const user = factory.user(); const user = UserFactory.create();
const auth = factory.auth({ user }); const auth = AuthFactory.create(user);
const dto = { password: 'old-password', newPassword: 'new-password' }; const dto = { password: 'old-password', newPassword: 'new-password' };
mocks.user.getForChangePassword.mockResolvedValue({ id: user.id, password: '' }); mocks.user.getForChangePassword.mockResolvedValue({ id: user.id, password: '' });
@@ -154,8 +158,8 @@ describe(AuthService.name, () => {
}); });
it('should change the password and logout other sessions', async () => { it('should change the password and logout other sessions', async () => {
const user = factory.userAdmin(); const user = UserFactory.create();
const auth = factory.auth({ user }); const auth = AuthFactory.create(user);
const dto = { password: 'old-password', newPassword: 'new-password', invalidateSessions: true }; const dto = { password: 'old-password', newPassword: 'new-password', invalidateSessions: true };
mocks.user.getForChangePassword.mockResolvedValue({ id: user.id, password: 'hash-password' }); mocks.user.getForChangePassword.mockResolvedValue({ id: user.id, password: 'hash-password' });
@@ -175,7 +179,7 @@ describe(AuthService.name, () => {
describe('logout', () => { describe('logout', () => {
it('should return the end session endpoint', async () => { it('should return the end session endpoint', async () => {
const auth = factory.auth(); const auth = AuthFactory.create();
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled); mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled);
@@ -186,7 +190,7 @@ describe(AuthService.name, () => {
}); });
it('should return the default redirect', async () => { it('should return the default redirect', async () => {
const auth = factory.auth(); const auth = AuthFactory.create();
await expect(sut.logout(auth, AuthType.Password)).resolves.toEqual({ await expect(sut.logout(auth, AuthType.Password)).resolves.toEqual({
successful: true, successful: true,
@@ -262,11 +266,11 @@ describe(AuthService.name, () => {
}); });
it('should validate using authorization header', async () => { it('should validate using authorization header', async () => {
const session = factory.session(); const session = SessionFactory.create();
const sessionWithToken = { const sessionWithToken = {
id: session.id, id: session.id,
updatedAt: session.updatedAt, updatedAt: session.updatedAt,
user: factory.authUser(), user: UserFactory.create(),
pinExpiresAt: null, pinExpiresAt: null,
appVersion: null, appVersion: null,
}; };
@@ -340,7 +344,7 @@ describe(AuthService.name, () => {
}); });
it('should accept a base64url key', async () => { it('should accept a base64url key', async () => {
const user = factory.userAdmin(); const user = UserFactory.create();
const sharedLink = { ...sharedLinkStub.valid, user } as any; const sharedLink = { ...sharedLinkStub.valid, user } as any;
mocks.sharedLink.getByKey.mockResolvedValue(sharedLink); mocks.sharedLink.getByKey.mockResolvedValue(sharedLink);
@@ -361,7 +365,7 @@ describe(AuthService.name, () => {
}); });
it('should accept a hex key', async () => { it('should accept a hex key', async () => {
const user = factory.userAdmin(); const user = UserFactory.create();
const sharedLink = { ...sharedLinkStub.valid, user } as any; const sharedLink = { ...sharedLinkStub.valid, user } as any;
mocks.sharedLink.getByKey.mockResolvedValue(sharedLink); mocks.sharedLink.getByKey.mockResolvedValue(sharedLink);
@@ -396,7 +400,7 @@ describe(AuthService.name, () => {
}); });
it('should accept a valid slug', async () => { it('should accept a valid slug', async () => {
const user = factory.userAdmin(); const user = UserFactory.create();
const sharedLink = { ...sharedLinkStub.valid, slug: 'slug-123', user } as any; const sharedLink = { ...sharedLinkStub.valid, slug: 'slug-123', user } as any;
mocks.sharedLink.getBySlug.mockResolvedValue(sharedLink); mocks.sharedLink.getBySlug.mockResolvedValue(sharedLink);
@@ -428,11 +432,11 @@ describe(AuthService.name, () => {
}); });
it('should return an auth dto', async () => { it('should return an auth dto', async () => {
const session = factory.session(); const session = SessionFactory.create();
const sessionWithToken = { const sessionWithToken = {
id: session.id, id: session.id,
updatedAt: session.updatedAt, updatedAt: session.updatedAt,
user: factory.authUser(), user: UserFactory.create(),
pinExpiresAt: null, pinExpiresAt: null,
appVersion: null, appVersion: null,
}; };
@@ -455,11 +459,11 @@ describe(AuthService.name, () => {
}); });
it('should throw if admin route and not an admin', async () => { it('should throw if admin route and not an admin', async () => {
const session = factory.session(); const session = SessionFactory.create();
const sessionWithToken = { const sessionWithToken = {
id: session.id, id: session.id,
updatedAt: session.updatedAt, updatedAt: session.updatedAt,
user: factory.authUser(), user: UserFactory.create(),
isPendingSyncReset: false, isPendingSyncReset: false,
pinExpiresAt: null, pinExpiresAt: null,
appVersion: null, appVersion: null,
@@ -477,11 +481,11 @@ describe(AuthService.name, () => {
}); });
it('should update when access time exceeds an hour', async () => { it('should update when access time exceeds an hour', async () => {
const session = factory.session({ updatedAt: DateTime.now().minus({ hours: 2 }).toJSDate() }); const session = SessionFactory.create({ updatedAt: DateTime.now().minus({ hours: 2 }).toJSDate() });
const sessionWithToken = { const sessionWithToken = {
id: session.id, id: session.id,
updatedAt: session.updatedAt, updatedAt: session.updatedAt,
user: factory.authUser(), user: UserFactory.create(),
isPendingSyncReset: false, isPendingSyncReset: false,
pinExpiresAt: null, pinExpiresAt: null,
appVersion: null, appVersion: null,
@@ -517,8 +521,8 @@ describe(AuthService.name, () => {
}); });
it('should throw an error if api key has insufficient permissions', async () => { it('should throw an error if api key has insufficient permissions', async () => {
const authUser = factory.authUser(); const authUser = UserFactory.create();
const authApiKey = factory.authApiKey({ permissions: [] }); const authApiKey = ApiKeyFactory.create({ permissions: [] });
mocks.apiKey.getKey.mockResolvedValue({ ...authApiKey, user: authUser }); mocks.apiKey.getKey.mockResolvedValue({ ...authApiKey, user: authUser });
@@ -533,8 +537,8 @@ describe(AuthService.name, () => {
}); });
it('should default to requiring the all permission when omitted', async () => { it('should default to requiring the all permission when omitted', async () => {
const authUser = factory.authUser(); const authUser = UserFactory.create();
const authApiKey = factory.authApiKey({ permissions: [Permission.AssetRead] }); const authApiKey = ApiKeyFactory.create({ permissions: [Permission.AssetRead] });
mocks.apiKey.getKey.mockResolvedValue({ ...authApiKey, user: authUser }); mocks.apiKey.getKey.mockResolvedValue({ ...authApiKey, user: authUser });
@@ -548,10 +552,12 @@ describe(AuthService.name, () => {
}); });
it('should not require any permission when metadata is set to `false`', async () => { it('should not require any permission when metadata is set to `false`', async () => {
const authUser = factory.authUser(); const authUser = UserFactory.create();
const authApiKey = factory.authApiKey({ permissions: [Permission.ActivityRead] }); const authApiKey = ApiKeyFactory.from({ permissions: [Permission.ActivityRead] })
.user(authUser)
.build();
mocks.apiKey.getKey.mockResolvedValue({ ...authApiKey, user: authUser }); mocks.apiKey.getKey.mockResolvedValue(authApiKey);
const result = sut.authenticate({ const result = sut.authenticate({
headers: { 'x-api-key': 'auth_token' }, headers: { 'x-api-key': 'auth_token' },
@@ -562,10 +568,12 @@ describe(AuthService.name, () => {
}); });
it('should return an auth dto', async () => { it('should return an auth dto', async () => {
const authUser = factory.authUser(); const authUser = UserFactory.create();
const authApiKey = factory.authApiKey({ permissions: [Permission.All] }); const authApiKey = ApiKeyFactory.from({ permissions: [Permission.All] })
.user(authUser)
.build();
mocks.apiKey.getKey.mockResolvedValue({ ...authApiKey, user: authUser }); mocks.apiKey.getKey.mockResolvedValue(authApiKey);
await expect( await expect(
sut.authenticate({ sut.authenticate({
@@ -629,12 +637,12 @@ describe(AuthService.name, () => {
}); });
it('should link an existing user', async () => { it('should link an existing user', async () => {
const user = factory.userAdmin(); const user = UserFactory.create();
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthEnabled); mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthEnabled);
mocks.user.getByEmail.mockResolvedValue(user); mocks.user.getByEmail.mockResolvedValue(user);
mocks.user.update.mockResolvedValue(user); mocks.user.update.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(factory.session()); mocks.session.create.mockResolvedValue(SessionFactory.create());
await expect( await expect(
sut.callback( sut.callback(
@@ -649,7 +657,7 @@ describe(AuthService.name, () => {
}); });
it('should not link to a user with a different oauth sub', async () => { it('should not link to a user with a different oauth sub', async () => {
const user = factory.userAdmin({ isAdmin: true, oauthId: 'existing-sub' }); const user = UserFactory.create({ isAdmin: true, oauthId: 'existing-sub' });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithAutoRegister); mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithAutoRegister);
mocks.user.getByEmail.mockResolvedValueOnce(user); mocks.user.getByEmail.mockResolvedValueOnce(user);
@@ -669,13 +677,13 @@ describe(AuthService.name, () => {
}); });
it('should allow auto registering by default', async () => { it('should allow auto registering by default', async () => {
const user = factory.userAdmin({ oauthId: 'oauth-id' }); const user = UserFactory.create({ oauthId: 'oauth-id' });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled); mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled);
mocks.user.getByEmail.mockResolvedValue(void 0); mocks.user.getByEmail.mockResolvedValue(void 0);
mocks.user.getAdmin.mockResolvedValue(factory.userAdmin({ isAdmin: true })); mocks.user.getAdmin.mockResolvedValue(UserFactory.create({ isAdmin: true }));
mocks.user.create.mockResolvedValue(user); mocks.user.create.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(factory.session()); mocks.session.create.mockResolvedValue(SessionFactory.create());
await expect( await expect(
sut.callback( sut.callback(
@@ -690,13 +698,13 @@ describe(AuthService.name, () => {
}); });
it('should throw an error if user should be auto registered but the email claim does not exist', async () => { it('should throw an error if user should be auto registered but the email claim does not exist', async () => {
const user = factory.userAdmin({ isAdmin: true }); const user = UserFactory.create({ isAdmin: true });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled); mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled);
mocks.user.getByEmail.mockResolvedValue(void 0); mocks.user.getByEmail.mockResolvedValue(void 0);
mocks.user.getAdmin.mockResolvedValue(user); mocks.user.getAdmin.mockResolvedValue(user);
mocks.user.create.mockResolvedValue(user); mocks.user.create.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(factory.session()); mocks.session.create.mockResolvedValue(SessionFactory.create());
mocks.oauth.getProfile.mockResolvedValue({ sub, email: undefined }); mocks.oauth.getProfile.mockResolvedValue({ sub, email: undefined });
await expect( await expect(
@@ -717,11 +725,11 @@ describe(AuthService.name, () => {
'app.immich:///oauth-callback?code=abc123', 'app.immich:///oauth-callback?code=abc123',
]) { ]) {
it(`should use the mobile redirect override for a url of ${url}`, async () => { it(`should use the mobile redirect override for a url of ${url}`, async () => {
const user = factory.userAdmin(); const user = UserFactory.create();
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithMobileOverride); mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithMobileOverride);
mocks.user.getByOAuthId.mockResolvedValue(user); mocks.user.getByOAuthId.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(factory.session()); mocks.session.create.mockResolvedValue(SessionFactory.create());
await sut.callback({ url, state: 'xyz789', codeVerifier: 'foo' }, {}, loginDetails); await sut.callback({ url, state: 'xyz789', codeVerifier: 'foo' }, {}, loginDetails);
@@ -735,13 +743,13 @@ describe(AuthService.name, () => {
} }
it('should use the default quota', async () => { it('should use the default quota', async () => {
const user = factory.userAdmin({ oauthId: 'oauth-id' }); const user = UserFactory.create({ oauthId: 'oauth-id' });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota); mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota);
mocks.user.getByEmail.mockResolvedValue(void 0); mocks.user.getByEmail.mockResolvedValue(void 0);
mocks.user.getAdmin.mockResolvedValue(factory.userAdmin({ isAdmin: true })); mocks.user.getAdmin.mockResolvedValue(UserFactory.create({ isAdmin: true }));
mocks.user.create.mockResolvedValue(user); mocks.user.create.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(factory.session()); mocks.session.create.mockResolvedValue(SessionFactory.create());
await expect( await expect(
sut.callback( sut.callback(
@@ -755,14 +763,14 @@ describe(AuthService.name, () => {
}); });
it('should ignore an invalid storage quota', async () => { it('should ignore an invalid storage quota', async () => {
const user = factory.userAdmin({ oauthId: 'oauth-id' }); const user = UserFactory.create({ oauthId: 'oauth-id' });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota); mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota);
mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, immich_quota: 'abc' }); mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, immich_quota: 'abc' });
mocks.user.getAdmin.mockResolvedValue(factory.userAdmin({ isAdmin: true })); mocks.user.getAdmin.mockResolvedValue(UserFactory.create({ isAdmin: true }));
mocks.user.getByEmail.mockResolvedValue(void 0); mocks.user.getByEmail.mockResolvedValue(void 0);
mocks.user.create.mockResolvedValue(user); mocks.user.create.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(factory.session()); mocks.session.create.mockResolvedValue(SessionFactory.create());
await expect( await expect(
sut.callback( sut.callback(
@@ -776,14 +784,14 @@ describe(AuthService.name, () => {
}); });
it('should ignore a negative quota', async () => { it('should ignore a negative quota', async () => {
const user = factory.userAdmin({ oauthId: 'oauth-id' }); const user = UserFactory.create({ oauthId: 'oauth-id' });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota); mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota);
mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, immich_quota: -5 }); mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, immich_quota: -5 });
mocks.user.getAdmin.mockResolvedValue(user); mocks.user.getAdmin.mockResolvedValue(user);
mocks.user.getByEmail.mockResolvedValue(void 0); mocks.user.getByEmail.mockResolvedValue(void 0);
mocks.user.create.mockResolvedValue(user); mocks.user.create.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(factory.session()); mocks.session.create.mockResolvedValue(SessionFactory.create());
await expect( await expect(
sut.callback( sut.callback(
@@ -797,14 +805,14 @@ describe(AuthService.name, () => {
}); });
it('should set quota for 0 quota', async () => { it('should set quota for 0 quota', async () => {
const user = factory.userAdmin({ oauthId: 'oauth-id' }); const user = UserFactory.create({ oauthId: 'oauth-id' });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota); mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota);
mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, immich_quota: 0 }); mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, immich_quota: 0 });
mocks.user.getAdmin.mockResolvedValue(factory.userAdmin({ isAdmin: true })); mocks.user.getAdmin.mockResolvedValue(UserFactory.create({ isAdmin: true }));
mocks.user.getByEmail.mockResolvedValue(void 0); mocks.user.getByEmail.mockResolvedValue(void 0);
mocks.user.create.mockResolvedValue(user); mocks.user.create.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(factory.session()); mocks.session.create.mockResolvedValue(SessionFactory.create());
await expect( await expect(
sut.callback( sut.callback(
@@ -825,15 +833,15 @@ describe(AuthService.name, () => {
}); });
it('should use a valid storage quota', async () => { it('should use a valid storage quota', async () => {
const user = factory.userAdmin({ oauthId: 'oauth-id' }); const user = UserFactory.create({ oauthId: 'oauth-id' });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota); mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota);
mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, immich_quota: 5 }); mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, immich_quota: 5 });
mocks.user.getByEmail.mockResolvedValue(void 0); mocks.user.getByEmail.mockResolvedValue(void 0);
mocks.user.getAdmin.mockResolvedValue(factory.userAdmin({ isAdmin: true })); mocks.user.getAdmin.mockResolvedValue(UserFactory.create({ isAdmin: true }));
mocks.user.getByOAuthId.mockResolvedValue(void 0); mocks.user.getByOAuthId.mockResolvedValue(void 0);
mocks.user.create.mockResolvedValue(user); mocks.user.create.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(factory.session()); mocks.session.create.mockResolvedValue(SessionFactory.create());
await expect( await expect(
sut.callback( sut.callback(
@@ -855,7 +863,7 @@ describe(AuthService.name, () => {
it('should sync the profile picture', async () => { it('should sync the profile picture', async () => {
const fileId = newUuid(); const fileId = newUuid();
const user = factory.userAdmin({ oauthId: 'oauth-id' }); const user = UserFactory.create({ oauthId: 'oauth-id' });
const pictureUrl = 'https://auth.immich.cloud/profiles/1.jpg'; const pictureUrl = 'https://auth.immich.cloud/profiles/1.jpg';
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthEnabled); mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthEnabled);
@@ -871,7 +879,7 @@ describe(AuthService.name, () => {
data: new Uint8Array([1, 2, 3, 4, 5]).buffer, data: new Uint8Array([1, 2, 3, 4, 5]).buffer,
}); });
mocks.user.update.mockResolvedValue(user); mocks.user.update.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(factory.session()); mocks.session.create.mockResolvedValue(SessionFactory.create());
await expect( await expect(
sut.callback( sut.callback(
@@ -889,7 +897,7 @@ describe(AuthService.name, () => {
}); });
it('should not sync the profile picture if the user already has one', async () => { it('should not sync the profile picture if the user already has one', async () => {
const user = factory.userAdmin({ oauthId: 'oauth-id', profileImagePath: 'not-empty' }); const user = UserFactory.create({ oauthId: 'oauth-id', profileImagePath: 'not-empty' });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthEnabled); mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthEnabled);
mocks.oauth.getProfile.mockResolvedValue({ mocks.oauth.getProfile.mockResolvedValue({
@@ -899,7 +907,7 @@ describe(AuthService.name, () => {
}); });
mocks.user.getByOAuthId.mockResolvedValue(user); mocks.user.getByOAuthId.mockResolvedValue(user);
mocks.user.update.mockResolvedValue(user); mocks.user.update.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(factory.session()); mocks.session.create.mockResolvedValue(SessionFactory.create());
await expect( await expect(
sut.callback( sut.callback(
@@ -914,15 +922,15 @@ describe(AuthService.name, () => {
}); });
it('should only allow "admin" and "user" for the role claim', async () => { it('should only allow "admin" and "user" for the role claim', async () => {
const user = factory.userAdmin({ oauthId: 'oauth-id' }); const user = UserFactory.create({ oauthId: 'oauth-id' });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithAutoRegister); mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithAutoRegister);
mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, immich_role: 'foo' }); mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, immich_role: 'foo' });
mocks.user.getByEmail.mockResolvedValue(void 0); mocks.user.getByEmail.mockResolvedValue(void 0);
mocks.user.getAdmin.mockResolvedValue(factory.userAdmin({ isAdmin: true })); mocks.user.getAdmin.mockResolvedValue(UserFactory.create({ isAdmin: true }));
mocks.user.getByOAuthId.mockResolvedValue(void 0); mocks.user.getByOAuthId.mockResolvedValue(void 0);
mocks.user.create.mockResolvedValue(user); mocks.user.create.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(factory.session()); mocks.session.create.mockResolvedValue(SessionFactory.create());
await expect( await expect(
sut.callback( sut.callback(
@@ -943,14 +951,14 @@ describe(AuthService.name, () => {
}); });
it('should create an admin user if the role claim is set to admin', async () => { it('should create an admin user if the role claim is set to admin', async () => {
const user = factory.userAdmin({ oauthId: 'oauth-id' }); const user = UserFactory.create({ oauthId: 'oauth-id' });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithAutoRegister); mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithAutoRegister);
mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, immich_role: 'admin' }); mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, immich_role: 'admin' });
mocks.user.getByEmail.mockResolvedValue(void 0); mocks.user.getByEmail.mockResolvedValue(void 0);
mocks.user.getByOAuthId.mockResolvedValue(void 0); mocks.user.getByOAuthId.mockResolvedValue(void 0);
mocks.user.create.mockResolvedValue(user); mocks.user.create.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(factory.session()); mocks.session.create.mockResolvedValue(SessionFactory.create());
await expect( await expect(
sut.callback( sut.callback(
@@ -971,7 +979,7 @@ describe(AuthService.name, () => {
}); });
it('should accept a custom role claim', async () => { it('should accept a custom role claim', async () => {
const user = factory.userAdmin({ oauthId: 'oauth-id' }); const user = UserFactory.create({ oauthId: 'oauth-id' });
mocks.systemMetadata.get.mockResolvedValue({ mocks.systemMetadata.get.mockResolvedValue({
oauth: { ...systemConfigStub.oauthWithAutoRegister, roleClaim: 'my_role' }, oauth: { ...systemConfigStub.oauthWithAutoRegister, roleClaim: 'my_role' },
@@ -980,7 +988,7 @@ describe(AuthService.name, () => {
mocks.user.getByEmail.mockResolvedValue(void 0); mocks.user.getByEmail.mockResolvedValue(void 0);
mocks.user.getByOAuthId.mockResolvedValue(void 0); mocks.user.getByOAuthId.mockResolvedValue(void 0);
mocks.user.create.mockResolvedValue(user); mocks.user.create.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(factory.session()); mocks.session.create.mockResolvedValue(SessionFactory.create());
await expect( await expect(
sut.callback( sut.callback(
@@ -1003,8 +1011,8 @@ describe(AuthService.name, () => {
describe('link', () => { describe('link', () => {
it('should link an account', async () => { it('should link an account', async () => {
const user = factory.userAdmin(); const user = UserFactory.create();
const auth = factory.auth({ apiKey: { permissions: [] }, user }); const auth = AuthFactory.from(user).apiKey({ permissions: [] }).build();
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled); mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled);
mocks.user.update.mockResolvedValue(user); mocks.user.update.mockResolvedValue(user);
@@ -1019,8 +1027,8 @@ describe(AuthService.name, () => {
}); });
it('should not link an already linked oauth.sub', async () => { it('should not link an already linked oauth.sub', async () => {
const authUser = factory.authUser(); const authUser = UserFactory.create();
const authApiKey = factory.authApiKey({ permissions: [] }); const authApiKey = ApiKeyFactory.create({ permissions: [] });
const auth = { user: authUser, apiKey: authApiKey }; const auth = { user: authUser, apiKey: authApiKey };
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled); mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled);
@@ -1036,8 +1044,8 @@ describe(AuthService.name, () => {
describe('unlink', () => { describe('unlink', () => {
it('should unlink an account', async () => { it('should unlink an account', async () => {
const user = factory.userAdmin(); const user = UserFactory.create();
const auth = factory.auth({ user, apiKey: { permissions: [] } }); const auth = AuthFactory.from(user).apiKey({ permissions: [] }).build();
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled); mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled);
mocks.user.update.mockResolvedValue(user); mocks.user.update.mockResolvedValue(user);
@@ -1050,8 +1058,8 @@ describe(AuthService.name, () => {
describe('setupPinCode', () => { describe('setupPinCode', () => {
it('should setup a PIN code', async () => { it('should setup a PIN code', async () => {
const user = factory.userAdmin(); const user = UserFactory.create();
const auth = factory.auth({ user }); const auth = AuthFactory.create(user);
const dto = { pinCode: '123456' }; const dto = { pinCode: '123456' };
mocks.user.getForPinCode.mockResolvedValue({ pinCode: null, password: '' }); mocks.user.getForPinCode.mockResolvedValue({ pinCode: null, password: '' });
@@ -1065,8 +1073,8 @@ describe(AuthService.name, () => {
}); });
it('should fail if the user already has a PIN code', async () => { it('should fail if the user already has a PIN code', async () => {
const user = factory.userAdmin(); const user = UserFactory.create();
const auth = factory.auth({ user }); const auth = AuthFactory.create(user);
mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' }); mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' });
@@ -1076,8 +1084,8 @@ describe(AuthService.name, () => {
describe('changePinCode', () => { describe('changePinCode', () => {
it('should change the PIN code', async () => { it('should change the PIN code', async () => {
const user = factory.userAdmin(); const user = UserFactory.create();
const auth = factory.auth({ user }); const auth = AuthFactory.create(user);
const dto = { pinCode: '123456', newPinCode: '012345' }; const dto = { pinCode: '123456', newPinCode: '012345' };
mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' }); mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' });
@@ -1091,37 +1099,37 @@ describe(AuthService.name, () => {
}); });
it('should fail if the PIN code does not match', async () => { it('should fail if the PIN code does not match', async () => {
const user = factory.userAdmin(); const user = UserFactory.create();
mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' }); mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' });
mocks.crypto.compareBcrypt.mockImplementation((a, b) => `${a} (hashed)` === b); mocks.crypto.compareBcrypt.mockImplementation((a, b) => `${a} (hashed)` === b);
await expect( await expect(
sut.changePinCode(factory.auth({ user }), { pinCode: '000000', newPinCode: '012345' }), sut.changePinCode(AuthFactory.create(user), { pinCode: '000000', newPinCode: '012345' }),
).rejects.toThrow('Wrong PIN code'); ).rejects.toThrow('Wrong PIN code');
}); });
}); });
describe('resetPinCode', () => { describe('resetPinCode', () => {
it('should reset the PIN code', async () => { it('should reset the PIN code', async () => {
const currentSession = factory.session(); const currentSession = SessionFactory.create();
const user = factory.userAdmin(); const user = UserFactory.create();
mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' }); mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' });
mocks.crypto.compareBcrypt.mockImplementation((a, b) => `${a} (hashed)` === b); mocks.crypto.compareBcrypt.mockImplementation((a, b) => `${a} (hashed)` === b);
mocks.session.lockAll.mockResolvedValue(void 0); mocks.session.lockAll.mockResolvedValue(void 0);
mocks.session.update.mockResolvedValue(currentSession); mocks.session.update.mockResolvedValue(currentSession);
await sut.resetPinCode(factory.auth({ user }), { pinCode: '123456' }); await sut.resetPinCode(AuthFactory.create(user), { pinCode: '123456' });
expect(mocks.user.update).toHaveBeenCalledWith(user.id, { pinCode: null }); expect(mocks.user.update).toHaveBeenCalledWith(user.id, { pinCode: null });
expect(mocks.session.lockAll).toHaveBeenCalledWith(user.id); expect(mocks.session.lockAll).toHaveBeenCalledWith(user.id);
}); });
it('should throw if the PIN code does not match', async () => { it('should throw if the PIN code does not match', async () => {
const user = factory.userAdmin(); const user = UserFactory.create();
mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' }); mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' });
mocks.crypto.compareBcrypt.mockImplementation((a, b) => `${a} (hashed)` === b); mocks.crypto.compareBcrypt.mockImplementation((a, b) => `${a} (hashed)` === b);
await expect(sut.resetPinCode(factory.auth({ user }), { pinCode: '000000' })).rejects.toThrow('Wrong PIN code'); await expect(sut.resetPinCode(AuthFactory.create(user), { pinCode: '000000' })).rejects.toThrow('Wrong PIN code');
}); });
}); });
}); });

View File

@@ -1,7 +1,7 @@
import { jwtVerify } from 'jose'; import { jwtVerify } from 'jose';
import { MaintenanceAction, SystemMetadataKey } from 'src/enum'; import { MaintenanceAction, SystemMetadataKey } from 'src/enum';
import { CliService } from 'src/services/cli.service'; import { CliService } from 'src/services/cli.service';
import { factory } from 'test/small.factory'; import { UserFactory } from 'test/factories/user.factory';
import { newTestService, ServiceMocks } from 'test/utils'; import { newTestService, ServiceMocks } from 'test/utils';
import { describe, it } from 'vitest'; import { describe, it } from 'vitest';
@@ -15,7 +15,7 @@ describe(CliService.name, () => {
describe('listUsers', () => { describe('listUsers', () => {
it('should list users', async () => { it('should list users', async () => {
mocks.user.getList.mockResolvedValue([factory.userAdmin({ isAdmin: true })]); mocks.user.getList.mockResolvedValue([UserFactory.create({ isAdmin: true })]);
await expect(sut.listUsers()).resolves.toEqual([expect.objectContaining({ isAdmin: true })]); await expect(sut.listUsers()).resolves.toEqual([expect.objectContaining({ isAdmin: true })]);
expect(mocks.user.getList).toHaveBeenCalledWith({ withDeleted: true }); expect(mocks.user.getList).toHaveBeenCalledWith({ withDeleted: true });
}); });
@@ -32,10 +32,10 @@ describe(CliService.name, () => {
}); });
it('should default to a random password', async () => { it('should default to a random password', async () => {
const admin = factory.userAdmin({ isAdmin: true }); const admin = UserFactory.create({ isAdmin: true });
mocks.user.getAdmin.mockResolvedValue(admin); mocks.user.getAdmin.mockResolvedValue(admin);
mocks.user.update.mockResolvedValue(factory.userAdmin({ isAdmin: true })); mocks.user.update.mockResolvedValue(UserFactory.create({ isAdmin: true }));
const ask = vitest.fn().mockImplementation(() => {}); const ask = vitest.fn().mockImplementation(() => {});
@@ -50,7 +50,7 @@ describe(CliService.name, () => {
}); });
it('should use the supplied password', async () => { it('should use the supplied password', async () => {
const admin = factory.userAdmin({ isAdmin: true }); const admin = UserFactory.create({ isAdmin: true });
mocks.user.getAdmin.mockResolvedValue(admin); mocks.user.getAdmin.mockResolvedValue(admin);
mocks.user.update.mockResolvedValue(admin); mocks.user.update.mockResolvedValue(admin);

View File

@@ -2,9 +2,9 @@ import { MapService } from 'src/services/map.service';
import { AlbumFactory } from 'test/factories/album.factory'; import { AlbumFactory } from 'test/factories/album.factory';
import { AssetFactory } from 'test/factories/asset.factory'; import { AssetFactory } from 'test/factories/asset.factory';
import { AuthFactory } from 'test/factories/auth.factory'; import { AuthFactory } from 'test/factories/auth.factory';
import { PartnerFactory } from 'test/factories/partner.factory';
import { userStub } from 'test/fixtures/user.stub'; import { userStub } from 'test/fixtures/user.stub';
import { getForAlbum, getForPartner } from 'test/mappers'; import { getForAlbum, getForPartner } from 'test/mappers';
import { factory } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils'; import { newTestService, ServiceMocks } from 'test/utils';
describe(MapService.name, () => { describe(MapService.name, () => {
@@ -40,7 +40,7 @@ describe(MapService.name, () => {
it('should include partner assets', async () => { it('should include partner assets', async () => {
const auth = AuthFactory.create(); const auth = AuthFactory.create();
const partner = factory.partner({ sharedWithId: auth.user.id }); const partner = PartnerFactory.create({ sharedWithId: auth.user.id });
const asset = AssetFactory.from() const asset = AssetFactory.from()
.exif({ latitude: 42, longitude: 69, city: 'city', state: 'state', country: 'country' }) .exif({ latitude: 42, longitude: 69, city: 'city', state: 'state', country: 'country' })

View File

@@ -1,9 +1,10 @@
import { BadRequestException } from '@nestjs/common'; import { BadRequestException } from '@nestjs/common';
import { PartnerDirection } from 'src/repositories/partner.repository'; import { PartnerDirection } from 'src/repositories/partner.repository';
import { PartnerService } from 'src/services/partner.service'; import { PartnerService } from 'src/services/partner.service';
import { AuthFactory } from 'test/factories/auth.factory';
import { PartnerFactory } from 'test/factories/partner.factory';
import { UserFactory } from 'test/factories/user.factory'; import { UserFactory } from 'test/factories/user.factory';
import { getDehydrated, getForPartner } from 'test/mappers'; import { getForPartner } from 'test/mappers';
import { factory } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils'; import { newTestService, ServiceMocks } from 'test/utils';
describe(PartnerService.name, () => { describe(PartnerService.name, () => {
@@ -22,15 +23,9 @@ describe(PartnerService.name, () => {
it("should return a list of partners with whom I've shared my library", async () => { it("should return a list of partners with whom I've shared my library", async () => {
const user1 = UserFactory.create(); const user1 = UserFactory.create();
const user2 = UserFactory.create(); const user2 = UserFactory.create();
const sharedWithUser2 = factory.partner({ const sharedWithUser2 = PartnerFactory.from().sharedBy(user1).sharedWith(user2).build();
sharedBy: getDehydrated(user1), const sharedWithUser1 = PartnerFactory.from().sharedBy(user2).sharedWith(user1).build();
sharedWith: getDehydrated(user2), const auth = AuthFactory.create({ id: user1.id });
});
const sharedWithUser1 = factory.partner({
sharedBy: getDehydrated(user2),
sharedWith: getDehydrated(user1),
});
const auth = factory.auth({ user: { id: user1.id } });
mocks.partner.getAll.mockResolvedValue([getForPartner(sharedWithUser1), getForPartner(sharedWithUser2)]); mocks.partner.getAll.mockResolvedValue([getForPartner(sharedWithUser1), getForPartner(sharedWithUser2)]);
@@ -41,15 +36,9 @@ describe(PartnerService.name, () => {
it('should return a list of partners who have shared their libraries with me', async () => { it('should return a list of partners who have shared their libraries with me', async () => {
const user1 = UserFactory.create(); const user1 = UserFactory.create();
const user2 = UserFactory.create(); const user2 = UserFactory.create();
const sharedWithUser2 = factory.partner({ const sharedWithUser2 = PartnerFactory.from().sharedBy(user1).sharedWith(user2).build();
sharedBy: getDehydrated(user1), const sharedWithUser1 = PartnerFactory.from().sharedBy(user2).sharedWith(user1).build();
sharedWith: getDehydrated(user2), const auth = AuthFactory.create({ id: user1.id });
});
const sharedWithUser1 = factory.partner({
sharedBy: getDehydrated(user2),
sharedWith: getDehydrated(user1),
});
const auth = factory.auth({ user: { id: user1.id } });
mocks.partner.getAll.mockResolvedValue([getForPartner(sharedWithUser1), getForPartner(sharedWithUser2)]); mocks.partner.getAll.mockResolvedValue([getForPartner(sharedWithUser1), getForPartner(sharedWithUser2)]);
await expect(sut.search(auth, { direction: PartnerDirection.SharedWith })).resolves.toBeDefined(); await expect(sut.search(auth, { direction: PartnerDirection.SharedWith })).resolves.toBeDefined();
@@ -61,8 +50,8 @@ describe(PartnerService.name, () => {
it('should create a new partner', async () => { it('should create a new partner', async () => {
const user1 = UserFactory.create(); const user1 = UserFactory.create();
const user2 = UserFactory.create(); const user2 = UserFactory.create();
const partner = factory.partner({ sharedBy: getDehydrated(user1), sharedWith: getDehydrated(user2) }); const partner = PartnerFactory.from().sharedBy(user1).sharedWith(user2).build();
const auth = factory.auth({ user: { id: user1.id } }); const auth = AuthFactory.create({ id: user1.id });
mocks.partner.get.mockResolvedValue(void 0); mocks.partner.get.mockResolvedValue(void 0);
mocks.partner.create.mockResolvedValue(getForPartner(partner)); mocks.partner.create.mockResolvedValue(getForPartner(partner));
@@ -78,8 +67,8 @@ describe(PartnerService.name, () => {
it('should throw an error when the partner already exists', async () => { it('should throw an error when the partner already exists', async () => {
const user1 = UserFactory.create(); const user1 = UserFactory.create();
const user2 = UserFactory.create(); const user2 = UserFactory.create();
const partner = factory.partner({ sharedBy: getDehydrated(user1), sharedWith: getDehydrated(user2) }); const partner = PartnerFactory.from().sharedBy(user1).sharedWith(user2).build();
const auth = factory.auth({ user: { id: user1.id } }); const auth = AuthFactory.create({ id: user1.id });
mocks.partner.get.mockResolvedValue(getForPartner(partner)); mocks.partner.get.mockResolvedValue(getForPartner(partner));
@@ -93,8 +82,8 @@ describe(PartnerService.name, () => {
it('should remove a partner', async () => { it('should remove a partner', async () => {
const user1 = UserFactory.create(); const user1 = UserFactory.create();
const user2 = UserFactory.create(); const user2 = UserFactory.create();
const partner = factory.partner({ sharedBy: getDehydrated(user1), sharedWith: getDehydrated(user2) }); const partner = PartnerFactory.from().sharedBy(user1).sharedWith(user2).build();
const auth = factory.auth({ user: { id: user1.id } }); const auth = AuthFactory.create({ id: user1.id });
mocks.partner.get.mockResolvedValue(getForPartner(partner)); mocks.partner.get.mockResolvedValue(getForPartner(partner));
@@ -104,8 +93,8 @@ describe(PartnerService.name, () => {
}); });
it('should throw an error when the partner does not exist', async () => { it('should throw an error when the partner does not exist', async () => {
const user2 = factory.user(); const user2 = UserFactory.create();
const auth = factory.auth(); const auth = AuthFactory.create();
mocks.partner.get.mockResolvedValue(void 0); mocks.partner.get.mockResolvedValue(void 0);
@@ -117,8 +106,8 @@ describe(PartnerService.name, () => {
describe('update', () => { describe('update', () => {
it('should require access', async () => { it('should require access', async () => {
const user2 = factory.user(); const user2 = UserFactory.create();
const auth = factory.auth(); const auth = AuthFactory.create();
await expect(sut.update(auth, user2.id, { inTimeline: false })).rejects.toBeInstanceOf(BadRequestException); await expect(sut.update(auth, user2.id, { inTimeline: false })).rejects.toBeInstanceOf(BadRequestException);
}); });
@@ -126,8 +115,8 @@ describe(PartnerService.name, () => {
it('should update partner', async () => { it('should update partner', async () => {
const user1 = UserFactory.create(); const user1 = UserFactory.create();
const user2 = UserFactory.create(); const user2 = UserFactory.create();
const partner = factory.partner({ sharedBy: getDehydrated(user1), sharedWith: getDehydrated(user2) }); const partner = PartnerFactory.from().sharedBy(user1).sharedWith(user2).build();
const auth = factory.auth({ user: { id: user1.id } }); const auth = AuthFactory.create({ id: user1.id });
mocks.access.partner.checkUpdateAccess.mockResolvedValue(new Set([user2.id])); mocks.access.partner.checkUpdateAccess.mockResolvedValue(new Set([user2.id]));
mocks.partner.update.mockResolvedValue(getForPartner(partner)); mocks.partner.update.mockResolvedValue(getForPartner(partner));

View File

@@ -1,7 +1,8 @@
import { JobStatus } from 'src/enum'; import { JobStatus } from 'src/enum';
import { SessionService } from 'src/services/session.service'; import { SessionService } from 'src/services/session.service';
import { AuthFactory } from 'test/factories/auth.factory';
import { SessionFactory } from 'test/factories/session.factory';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { factory } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils'; import { newTestService, ServiceMocks } from 'test/utils';
describe('SessionService', () => { describe('SessionService', () => {
@@ -25,9 +26,9 @@ describe('SessionService', () => {
describe('getAll', () => { describe('getAll', () => {
it('should get the devices', async () => { it('should get the devices', async () => {
const currentSession = factory.session(); const currentSession = SessionFactory.create();
const otherSession = factory.session(); const otherSession = SessionFactory.create();
const auth = factory.auth({ session: currentSession }); const auth = AuthFactory.from().session(currentSession).build();
mocks.session.getByUserId.mockResolvedValue([currentSession, otherSession]); mocks.session.getByUserId.mockResolvedValue([currentSession, otherSession]);
@@ -42,8 +43,8 @@ describe('SessionService', () => {
describe('logoutDevices', () => { describe('logoutDevices', () => {
it('should logout all devices', async () => { it('should logout all devices', async () => {
const currentSession = factory.session(); const currentSession = SessionFactory.create();
const auth = factory.auth({ session: currentSession }); const auth = AuthFactory.from().session(currentSession).build();
mocks.session.invalidate.mockResolvedValue(); mocks.session.invalidate.mockResolvedValue();

View File

@@ -1,6 +1,7 @@
import { mapAsset } from 'src/dtos/asset-response.dto'; import { mapAsset } from 'src/dtos/asset-response.dto';
import { SyncService } from 'src/services/sync.service'; import { SyncService } from 'src/services/sync.service';
import { AssetFactory } from 'test/factories/asset.factory'; import { AssetFactory } from 'test/factories/asset.factory';
import { PartnerFactory } from 'test/factories/partner.factory';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { getForAsset, getForPartner } from 'test/mappers'; import { getForAsset, getForPartner } from 'test/mappers';
import { factory } from 'test/small.factory'; import { factory } from 'test/small.factory';
@@ -42,7 +43,7 @@ describe(SyncService.name, () => {
describe('getChangesForDeltaSync', () => { describe('getChangesForDeltaSync', () => {
it('should return a response requiring a full sync when partners are out of sync', async () => { it('should return a response requiring a full sync when partners are out of sync', async () => {
const partner = factory.partner(); const partner = PartnerFactory.create();
const auth = factory.auth({ user: { id: partner.sharedWithId } }); const auth = factory.auth({ user: { id: partner.sharedWithId } });
mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]); mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]);

View File

@@ -2,9 +2,10 @@ import { BadRequestException, ForbiddenException } from '@nestjs/common';
import { mapUserAdmin } from 'src/dtos/user.dto'; import { mapUserAdmin } from 'src/dtos/user.dto';
import { JobName, UserStatus } from 'src/enum'; import { JobName, UserStatus } from 'src/enum';
import { UserAdminService } from 'src/services/user-admin.service'; import { UserAdminService } from 'src/services/user-admin.service';
import { AuthFactory } from 'test/factories/auth.factory';
import { UserFactory } from 'test/factories/user.factory';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { userStub } from 'test/fixtures/user.stub'; import { userStub } from 'test/fixtures/user.stub';
import { factory } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils'; import { newTestService, ServiceMocks } from 'test/utils';
import { describe } from 'vitest'; import { describe } from 'vitest';
@@ -126,8 +127,8 @@ describe(UserAdminService.name, () => {
}); });
it('should not allow deleting own account', async () => { it('should not allow deleting own account', async () => {
const user = factory.userAdmin({ isAdmin: false }); const user = UserFactory.create({ isAdmin: false });
const auth = factory.auth({ user }); const auth = AuthFactory.create(user);
mocks.user.get.mockResolvedValue(user); mocks.user.get.mockResolvedValue(user);
await expect(sut.delete(auth, user.id, {})).rejects.toBeInstanceOf(ForbiddenException); await expect(sut.delete(auth, user.id, {})).rejects.toBeInstanceOf(ForbiddenException);

View File

@@ -3,10 +3,11 @@ import { UserAdmin } from 'src/database';
import { CacheControl, JobName, UserMetadataKey } from 'src/enum'; import { CacheControl, JobName, UserMetadataKey } from 'src/enum';
import { UserService } from 'src/services/user.service'; import { UserService } from 'src/services/user.service';
import { ImmichFileResponse } from 'src/utils/file'; import { ImmichFileResponse } from 'src/utils/file';
import { AuthFactory } from 'test/factories/auth.factory';
import { UserFactory } from 'test/factories/user.factory';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub';
import { userStub } from 'test/fixtures/user.stub'; import { userStub } from 'test/fixtures/user.stub';
import { factory } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils'; import { newTestService, ServiceMocks } from 'test/utils';
const makeDeletedAt = (daysAgo: number) => { const makeDeletedAt = (daysAgo: number) => {
@@ -28,8 +29,8 @@ describe(UserService.name, () => {
describe('getAll', () => { describe('getAll', () => {
it('admin should get all users', async () => { it('admin should get all users', async () => {
const user = factory.userAdmin(); const user = UserFactory.create();
const auth = factory.auth({ user }); const auth = AuthFactory.create(user);
mocks.user.getList.mockResolvedValue([user]); mocks.user.getList.mockResolvedValue([user]);
@@ -39,8 +40,8 @@ describe(UserService.name, () => {
}); });
it('non-admin should get all users when publicUsers enabled', async () => { it('non-admin should get all users when publicUsers enabled', async () => {
const user = factory.userAdmin(); const user = UserFactory.create();
const auth = factory.auth({ user }); const auth = AuthFactory.create(user);
mocks.user.getList.mockResolvedValue([user]); mocks.user.getList.mockResolvedValue([user]);
@@ -105,7 +106,7 @@ describe(UserService.name, () => {
it('should throw an error if the user profile could not be updated with the new image', async () => { it('should throw an error if the user profile could not be updated with the new image', async () => {
const file = { path: '/profile/path' } as Express.Multer.File; const file = { path: '/profile/path' } as Express.Multer.File;
const user = factory.userAdmin({ profileImagePath: '/path/to/profile.jpg' }); const user = UserFactory.create({ profileImagePath: '/path/to/profile.jpg' });
mocks.user.get.mockResolvedValue(user); mocks.user.get.mockResolvedValue(user);
mocks.user.update.mockRejectedValue(new InternalServerErrorException('mocked error')); mocks.user.update.mockRejectedValue(new InternalServerErrorException('mocked error'));
@@ -113,7 +114,7 @@ describe(UserService.name, () => {
}); });
it('should delete the previous profile image', async () => { it('should delete the previous profile image', async () => {
const user = factory.userAdmin({ profileImagePath: '/path/to/profile.jpg' }); const user = UserFactory.create({ profileImagePath: '/path/to/profile.jpg' });
const file = { path: '/profile/path' } as Express.Multer.File; const file = { path: '/profile/path' } as Express.Multer.File;
const files = [user.profileImagePath]; const files = [user.profileImagePath];
@@ -149,7 +150,7 @@ describe(UserService.name, () => {
}); });
it('should delete the profile image if user has one', async () => { it('should delete the profile image if user has one', async () => {
const user = factory.userAdmin({ profileImagePath: '/path/to/profile.jpg' }); const user = UserFactory.create({ profileImagePath: '/path/to/profile.jpg' });
const files = [user.profileImagePath]; const files = [user.profileImagePath];
mocks.user.get.mockResolvedValue(user); mocks.user.get.mockResolvedValue(user);
@@ -178,7 +179,7 @@ describe(UserService.name, () => {
}); });
it('should return the profile picture', async () => { it('should return the profile picture', async () => {
const user = factory.userAdmin({ profileImagePath: '/path/to/profile.jpg' }); const user = UserFactory.create({ profileImagePath: '/path/to/profile.jpg' });
mocks.user.get.mockResolvedValue(user); mocks.user.get.mockResolvedValue(user);
await expect(sut.getProfileImage(user.id)).resolves.toEqual( await expect(sut.getProfileImage(user.id)).resolves.toEqual(
@@ -205,7 +206,7 @@ describe(UserService.name, () => {
}); });
it('should queue user ready for deletion', async () => { it('should queue user ready for deletion', async () => {
const user = factory.user(); const user = UserFactory.create();
mocks.user.getDeletedAfter.mockResolvedValue([{ id: user.id }]); mocks.user.getDeletedAfter.mockResolvedValue([{ id: user.id }]);
await sut.handleUserDeleteCheck(); await sut.handleUserDeleteCheck();

View File

@@ -0,0 +1,42 @@
import { Selectable } from 'kysely';
import { ActivityTable } from 'src/schema/tables/activity.table';
import { build } from 'test/factories/builder.factory';
import { ActivityLike, FactoryBuilder, UserLike } from 'test/factories/types';
import { UserFactory } from 'test/factories/user.factory';
import { newDate, newUuid, newUuidV7 } from 'test/small.factory';
export class ActivityFactory {
#user!: UserFactory;
private constructor(private value: Selectable<ActivityTable>) {}
static create(dto: ActivityLike = {}) {
return ActivityFactory.from(dto).build();
}
static from(dto: ActivityLike = {}) {
const userId = dto.userId ?? newUuid();
return new ActivityFactory({
albumId: newUuid(),
assetId: null,
comment: null,
createdAt: newDate(),
id: newUuid(),
isLiked: false,
userId,
updatedAt: newDate(),
updateId: newUuidV7(),
...dto,
}).user({ id: userId });
}
user(dto: UserLike = {}, builder?: FactoryBuilder<UserFactory>) {
this.#user = build(UserFactory.from(dto), builder);
this.value.userId = this.#user.build().id;
return this;
}
build() {
return { ...this.value, user: this.#user.build() };
}
}

View File

@@ -0,0 +1,42 @@
import { Selectable } from 'kysely';
import { Permission } from 'src/enum';
import { ApiKeyTable } from 'src/schema/tables/api-key.table';
import { build } from 'test/factories/builder.factory';
import { ApiKeyLike, FactoryBuilder, UserLike } from 'test/factories/types';
import { UserFactory } from 'test/factories/user.factory';
import { newDate, newUuid, newUuidV7 } from 'test/small.factory';
export class ApiKeyFactory {
#user!: UserFactory;
private constructor(private value: Selectable<ApiKeyTable>) {}
static create(dto: ApiKeyLike = {}) {
return ApiKeyFactory.from(dto).build();
}
static from(dto: ApiKeyLike = {}) {
const userId = dto.userId ?? newUuid();
return new ApiKeyFactory({
createdAt: newDate(),
id: newUuid(),
key: Buffer.from('api-key-buffer'),
name: 'API Key',
permissions: [Permission.All],
updatedAt: newDate(),
updateId: newUuidV7(),
userId,
...dto,
}).user({ id: userId });
}
user(dto: UserLike = {}, builder?: FactoryBuilder<UserFactory>) {
this.#user = build(UserFactory.from(dto), builder);
this.value.userId = this.#user.build().id;
return this;
}
build() {
return { ...this.value, user: this.#user.build() };
}
}

View File

@@ -1,12 +1,16 @@
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { ApiKeyFactory } from 'test/factories/api-key.factory';
import { build } from 'test/factories/builder.factory'; import { build } from 'test/factories/builder.factory';
import { SharedLinkFactory } from 'test/factories/shared-link.factory'; import { SharedLinkFactory } from 'test/factories/shared-link.factory';
import { FactoryBuilder, SharedLinkLike, UserLike } from 'test/factories/types'; import { ApiKeyLike, FactoryBuilder, SharedLinkLike, UserLike } from 'test/factories/types';
import { UserFactory } from 'test/factories/user.factory'; import { UserFactory } from 'test/factories/user.factory';
import { newUuid } from 'test/small.factory';
export class AuthFactory { export class AuthFactory {
#user: UserFactory; #user: UserFactory;
#sharedLink?: SharedLinkFactory; #sharedLink?: SharedLinkFactory;
#apiKey?: ApiKeyFactory;
#session?: AuthDto['session'];
private constructor(user: UserFactory) { private constructor(user: UserFactory) {
this.#user = user; this.#user = user;
@@ -20,8 +24,8 @@ export class AuthFactory {
return new AuthFactory(UserFactory.from(dto)); return new AuthFactory(UserFactory.from(dto));
} }
apiKey() { apiKey(dto: ApiKeyLike = {}, builder?: FactoryBuilder<ApiKeyFactory>) {
// TODO this.#apiKey = build(ApiKeyFactory.from(dto), builder);
return this; return this;
} }
@@ -30,6 +34,11 @@ export class AuthFactory {
return this; return this;
} }
session(dto: Partial<AuthDto['session']> = {}) {
this.#session = { id: newUuid(), hasElevatedPermission: false, ...dto };
return this;
}
build(): AuthDto { build(): AuthDto {
const { id, isAdmin, name, email, quotaUsageInBytes, quotaSizeInBytes } = this.#user.build(); const { id, isAdmin, name, email, quotaUsageInBytes, quotaSizeInBytes } = this.#user.build();
@@ -43,6 +52,8 @@ export class AuthFactory {
quotaSizeInBytes, quotaSizeInBytes,
}, },
sharedLink: this.#sharedLink?.build(), sharedLink: this.#sharedLink?.build(),
apiKey: this.#apiKey?.build(),
session: this.#session,
}; };
} }
} }

View File

@@ -0,0 +1,50 @@
import { Selectable } from 'kysely';
import { PartnerTable } from 'src/schema/tables/partner.table';
import { build } from 'test/factories/builder.factory';
import { FactoryBuilder, PartnerLike, UserLike } from 'test/factories/types';
import { UserFactory } from 'test/factories/user.factory';
import { newDate, newUuid, newUuidV7 } from 'test/small.factory';
export class PartnerFactory {
#sharedWith!: UserFactory;
#sharedBy!: UserFactory;
private constructor(private value: Selectable<PartnerTable>) {}
static create(dto: PartnerLike = {}) {
return PartnerFactory.from(dto).build();
}
static from(dto: PartnerLike = {}) {
const sharedById = dto.sharedById ?? newUuid();
const sharedWithId = dto.sharedWithId ?? newUuid();
return new PartnerFactory({
createdAt: newDate(),
createId: newUuidV7(),
inTimeline: true,
sharedById,
sharedWithId,
updatedAt: newDate(),
updateId: newUuidV7(),
...dto,
})
.sharedBy({ id: sharedById })
.sharedWith({ id: sharedWithId });
}
sharedWith(dto: UserLike = {}, builder?: FactoryBuilder<UserFactory>) {
this.#sharedWith = build(UserFactory.from(dto), builder);
this.value.sharedWithId = this.#sharedWith.build().id;
return this;
}
sharedBy(dto: UserLike = {}, builder?: FactoryBuilder<UserFactory>) {
this.#sharedBy = build(UserFactory.from(dto), builder);
this.value.sharedById = this.#sharedBy.build().id;
return this;
}
build() {
return { ...this.value, sharedWith: this.#sharedWith.build(), sharedBy: this.#sharedBy.build() };
}
}

View File

@@ -0,0 +1,35 @@
import { Selectable } from 'kysely';
import { SessionTable } from 'src/schema/tables/session.table';
import { SessionLike } from 'test/factories/types';
import { newDate, newUuid, newUuidV7 } from 'test/small.factory';
export class SessionFactory {
private constructor(private value: Selectable<SessionTable>) {}
static create(dto: SessionLike = {}) {
return SessionFactory.from(dto).build();
}
static from(dto: SessionLike = {}) {
return new SessionFactory({
appVersion: null,
createdAt: newDate(),
deviceOS: 'android',
deviceType: 'mobile',
expiresAt: null,
id: newUuid(),
isPendingSyncReset: false,
parentId: null,
pinExpiresAt: null,
token: Buffer.from('abc123'),
updateId: newUuidV7(),
updatedAt: newDate(),
userId: newUuid(),
...dto,
});
}
build() {
return { ...this.value };
}
}

View File

@@ -1,13 +1,17 @@
import { Selectable } from 'kysely'; import { Selectable } from 'kysely';
import { ActivityTable } from 'src/schema/tables/activity.table';
import { AlbumUserTable } from 'src/schema/tables/album-user.table'; import { AlbumUserTable } from 'src/schema/tables/album-user.table';
import { AlbumTable } from 'src/schema/tables/album.table'; import { AlbumTable } from 'src/schema/tables/album.table';
import { ApiKeyTable } from 'src/schema/tables/api-key.table';
import { AssetEditTable } from 'src/schema/tables/asset-edit.table'; import { AssetEditTable } from 'src/schema/tables/asset-edit.table';
import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
import { AssetFileTable } from 'src/schema/tables/asset-file.table'; import { AssetFileTable } from 'src/schema/tables/asset-file.table';
import { AssetTable } from 'src/schema/tables/asset.table'; import { AssetTable } from 'src/schema/tables/asset.table';
import { MemoryTable } from 'src/schema/tables/memory.table'; import { MemoryTable } from 'src/schema/tables/memory.table';
import { PartnerTable } from 'src/schema/tables/partner.table';
import { PersonTable } from 'src/schema/tables/person.table'; import { PersonTable } from 'src/schema/tables/person.table';
import { SessionTable } from 'src/schema/tables/session.table';
import { SharedLinkTable } from 'src/schema/tables/shared-link.table'; import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
import { StackTable } from 'src/schema/tables/stack.table'; import { StackTable } from 'src/schema/tables/stack.table';
import { UserTable } from 'src/schema/tables/user.table'; import { UserTable } from 'src/schema/tables/user.table';
@@ -26,3 +30,7 @@ export type AssetFaceLike = Partial<Selectable<AssetFaceTable>>;
export type PersonLike = Partial<Selectable<PersonTable>>; export type PersonLike = Partial<Selectable<PersonTable>>;
export type StackLike = Partial<Selectable<StackTable>>; export type StackLike = Partial<Selectable<StackTable>>;
export type MemoryLike = Partial<Selectable<MemoryTable>>; export type MemoryLike = Partial<Selectable<MemoryTable>>;
export type PartnerLike = Partial<Selectable<PartnerTable>>;
export type ActivityLike = Partial<Selectable<ActivityTable>>;
export type ApiKeyLike = Partial<Selectable<ApiKeyTable>>;
export type SessionLike = Partial<Selectable<SessionTable>>;

View File

@@ -1,26 +1,7 @@
import { ShallowDehydrateObject } from 'kysely'; import { AuthApiKey, AuthSharedLink, AuthUser, Exif, Library, UserAdmin } from 'src/database';
import {
Activity,
Album,
ApiKey,
AuthApiKey,
AuthSharedLink,
AuthUser,
Exif,
Library,
Partner,
Person,
Session,
Tag,
User,
UserAdmin,
} from 'src/database';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { AssetEditAction, AssetEditActionItem, MirrorAxis } from 'src/dtos/editing.dto';
import { QueueStatisticsDto } from 'src/dtos/queue.dto'; import { QueueStatisticsDto } from 'src/dtos/queue.dto';
import { AssetFileType, AssetOrder, Permission, UserMetadataKey, UserStatus } from 'src/enum'; import { AssetFileType, Permission, UserStatus } from 'src/enum';
import { UserMetadataItem } from 'src/types';
import { UserFactory } from 'test/factories/user.factory';
import { v4, v7 } from 'uuid'; import { v4, v7 } from 'uuid';
export const newUuid = () => v4(); export const newUuid = () => v4();
@@ -109,49 +90,6 @@ const authUserFactory = (authUser: Partial<AuthUser> = {}) => {
return { id, isAdmin, name, email, quotaUsageInBytes, quotaSizeInBytes }; return { id, isAdmin, name, email, quotaUsageInBytes, quotaSizeInBytes };
}; };
const partnerFactory = ({
sharedBy: sharedByProvided,
sharedWith: sharedWithProvided,
...partner
}: Partial<Partner> = {}) => {
const hydrateUser = (user: Partial<ShallowDehydrateObject<User>>) => ({
...user,
profileChangedAt: user.profileChangedAt ? new Date(user.profileChangedAt) : undefined,
});
const sharedBy = UserFactory.create(sharedByProvided ? hydrateUser(sharedByProvided) : {});
const sharedWith = UserFactory.create(sharedWithProvided ? hydrateUser(sharedWithProvided) : {});
return {
sharedById: sharedBy.id,
sharedBy,
sharedWithId: sharedWith.id,
sharedWith,
createId: newUuidV7(),
createdAt: newDate(),
updatedAt: newDate(),
updateId: newUuidV7(),
inTimeline: true,
...partner,
};
};
const sessionFactory = (session: Partial<Session> = {}) => ({
id: newUuid(),
createdAt: newDate(),
updatedAt: newDate(),
updateId: newUuidV7(),
deviceOS: 'android',
deviceType: 'mobile',
token: Buffer.from('abc123'),
parentId: null,
expiresAt: null,
userId: newUuid(),
pinExpiresAt: newDate(),
isPendingSyncReset: false,
appVersion: session.appVersion ?? null,
...session,
});
const queueStatisticsFactory = (dto?: Partial<QueueStatisticsDto>) => ({ const queueStatisticsFactory = (dto?: Partial<QueueStatisticsDto>) => ({
active: 0, active: 0,
completed: 0, completed: 0,
@@ -162,22 +100,6 @@ const queueStatisticsFactory = (dto?: Partial<QueueStatisticsDto>) => ({
...dto, ...dto,
}); });
const userFactory = (user: Partial<User> = {}) => ({
id: newUuid(),
name: 'Test User',
email: 'test@immich.cloud',
avatarColor: null,
profileImagePath: '',
profileChangedAt: newDate(),
metadata: [
{
key: UserMetadataKey.Onboarding,
value: 'true',
},
] as UserMetadataItem[],
...user,
});
const userAdminFactory = (user: Partial<UserAdmin> = {}) => { const userAdminFactory = (user: Partial<UserAdmin> = {}) => {
const { const {
id = newUuid(), id = newUuid(),
@@ -219,34 +141,6 @@ const userAdminFactory = (user: Partial<UserAdmin> = {}) => {
}; };
}; };
const activityFactory = (activity: Omit<Partial<Activity>, 'user'> = {}) => {
const userId = activity.userId || newUuid();
return {
id: newUuid(),
comment: null,
isLiked: false,
userId,
user: UserFactory.create({ id: userId }),
assetId: newUuid(),
albumId: newUuid(),
createdAt: newDate(),
updatedAt: newDate(),
updateId: newUuidV7(),
...activity,
};
};
const apiKeyFactory = (apiKey: Partial<ApiKey> = {}) => ({
id: newUuid(),
userId: newUuid(),
createdAt: newDate(),
updatedAt: newDate(),
updateId: newUuidV7(),
name: 'Api Key',
permissions: [Permission.All],
...apiKey,
});
const libraryFactory = (library: Partial<Library> = {}) => ({ const libraryFactory = (library: Partial<Library> = {}) => ({
id: newUuid(), id: newUuid(),
createdAt: newDate(), createdAt: newDate(),
@@ -328,88 +222,15 @@ const assetOcrFactory = (
...ocr, ...ocr,
}); });
const tagFactory = (tag: Partial<Tag>): Tag => ({
id: newUuid(),
color: null,
createdAt: newDate(),
parentId: null,
updatedAt: newDate(),
value: `tag-${newUuid()}`,
...tag,
});
const assetEditFactory = (edit?: Partial<AssetEditActionItem>): AssetEditActionItem => {
switch (edit?.action) {
case AssetEditAction.Crop: {
return { action: AssetEditAction.Crop, parameters: { height: 42, width: 42, x: 0, y: 10 }, ...edit };
}
case AssetEditAction.Mirror: {
return { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal }, ...edit };
}
case AssetEditAction.Rotate: {
return { action: AssetEditAction.Rotate, parameters: { angle: 90 }, ...edit };
}
default: {
return { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Vertical } };
}
}
};
const personFactory = (person?: Partial<Person>): Person => ({
birthDate: newDate(),
color: null,
createdAt: newDate(),
faceAssetId: null,
id: newUuid(),
isFavorite: false,
isHidden: false,
name: 'person',
ownerId: newUuid(),
thumbnailPath: '/path/to/person/thumbnail.jpg',
updatedAt: newDate(),
updateId: newUuidV7(),
...person,
});
const albumFactory = (album?: Partial<Omit<Album, 'assets'>>) => ({
albumName: 'My Album',
albumThumbnailAssetId: null,
albumUsers: [],
assets: [],
createdAt: newDate(),
deletedAt: null,
description: 'Album description',
id: newUuid(),
isActivityEnabled: false,
order: AssetOrder.Desc,
ownerId: newUuid(),
sharedLinks: [],
updatedAt: newDate(),
updateId: newUuidV7(),
...album,
});
export const factory = { export const factory = {
activity: activityFactory,
apiKey: apiKeyFactory,
assetOcr: assetOcrFactory, assetOcr: assetOcrFactory,
auth: authFactory, auth: authFactory,
authApiKey: authApiKeyFactory,
authUser: authUserFactory,
library: libraryFactory, library: libraryFactory,
partner: partnerFactory,
queueStatistics: queueStatisticsFactory, queueStatistics: queueStatisticsFactory,
session: sessionFactory,
user: userFactory,
userAdmin: userAdminFactory,
versionHistory: versionHistoryFactory, versionHistory: versionHistoryFactory,
jobAssets: { jobAssets: {
sidecarWrite: assetSidecarWriteFactory, sidecarWrite: assetSidecarWriteFactory,
}, },
person: personFactory,
assetEdit: assetEditFactory,
tag: tagFactory,
album: albumFactory,
uuid: newUuid, uuid: newUuid,
buffer: () => Buffer.from('this is a fake buffer'), buffer: () => Buffer.from('this is a fake buffer'),
date: newDate, date: newDate,