refactor: user factories instead of stubs (#17540)

This commit is contained in:
Jason Rasmussen 2025-04-11 11:53:37 -04:00 committed by GitHub
parent 52d4b2fe57
commit 584e5894bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 297 additions and 251 deletions

View File

@ -23,7 +23,7 @@ describe(ActivityService.name, () => {
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId]));
mocks.activity.search.mockResolvedValue([]);
await expect(sut.getAll(factory.auth({ id: userId }), { assetId, albumId })).resolves.toEqual([]);
await expect(sut.getAll(factory.auth({ user: { id: userId } }), { assetId, albumId })).resolves.toEqual([]);
expect(mocks.activity.search).toHaveBeenCalledWith({ assetId, albumId, isLiked: undefined });
});
@ -35,7 +35,7 @@ describe(ActivityService.name, () => {
mocks.activity.search.mockResolvedValue([]);
await expect(
sut.getAll(factory.auth({ id: userId }), { assetId, albumId, type: ReactionType.LIKE }),
sut.getAll(factory.auth({ user: { id: userId } }), { assetId, albumId, type: ReactionType.LIKE }),
).resolves.toEqual([]);
expect(mocks.activity.search).toHaveBeenCalledWith({ assetId, albumId, isLiked: true });
@ -80,7 +80,7 @@ describe(ActivityService.name, () => {
mocks.access.activity.checkCreateAccess.mockResolvedValue(new Set([albumId]));
mocks.activity.create.mockResolvedValue(activity);
await sut.create(factory.auth({ id: userId }), {
await sut.create(factory.auth({ user: { id: userId } }), {
albumId,
assetId,
type: ReactionType.COMMENT,
@ -116,7 +116,7 @@ describe(ActivityService.name, () => {
mocks.activity.create.mockResolvedValue(activity);
mocks.activity.search.mockResolvedValue([]);
await sut.create(factory.auth({ id: userId }), { albumId, assetId, type: ReactionType.LIKE });
await sut.create(factory.auth({ user: { id: userId } }), { albumId, assetId, type: ReactionType.LIKE });
expect(mocks.activity.create).toHaveBeenCalledWith({ userId: activity.userId, albumId, assetId, isLiked: true });
});

View File

@ -54,7 +54,7 @@ describe(ApiKeyService.name, () => {
});
it('should throw an error if the api key does not have sufficient permissions', async () => {
const auth = factory.auth({ apiKey: factory.authApiKey({ permissions: [Permission.ASSET_READ] }) });
const auth = factory.auth({ apiKey: { permissions: [Permission.ASSET_READ] } });
await expect(sut.create(auth, { permissions: [Permission.ASSET_UPDATE] })).rejects.toBeInstanceOf(
BadRequestException,

View File

@ -88,7 +88,7 @@ describe(AssetService.name, () => {
it('should get memories with partners with inTimeline enabled', async () => {
const partner = factory.partner();
const auth = factory.auth({ id: partner.sharedWithId });
const auth = factory.auth({ user: { id: partner.sharedWithId } });
mocks.partner.getAll.mockResolvedValue([partner]);
mocks.asset.getByDayOfYear.mockResolvedValue([]);
@ -139,7 +139,7 @@ describe(AssetService.name, () => {
it('should not include partner assets if not in timeline', async () => {
const partner = factory.partner({ inTimeline: false });
const auth = factory.auth({ id: partner.sharedWithId });
const auth = factory.auth({ user: { id: partner.sharedWithId } });
mocks.asset.getRandom.mockResolvedValue([assetStub.image]);
mocks.partner.getAll.mockResolvedValue([partner]);
@ -151,7 +151,7 @@ describe(AssetService.name, () => {
it('should include partner assets if in timeline', async () => {
const partner = factory.partner({ inTimeline: true });
const auth = factory.auth({ id: partner.sharedWithId });
const auth = factory.auth({ user: { id: partner.sharedWithId } });
mocks.asset.getRandom.mockResolvedValue([assetStub.image]);
mocks.partner.getAll.mockResolvedValue([partner]);

View File

@ -7,19 +7,18 @@ import { AuthService } from 'src/services/auth.service';
import { UserMetadataItem } from 'src/types';
import { sharedLinkStub } from 'test/fixtures/shared-link.stub';
import { systemConfigStub } from 'test/fixtures/system-config.stub';
import { userStub } from 'test/fixtures/user.stub';
import { factory } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils';
const oauthResponse = {
const oauthResponse = ({ id, email, name }: { id: string; email: string; name: string }) => ({
accessToken: 'cmFuZG9tLWJ5dGVz',
userId: 'user-id',
userEmail: 'immich@test.com',
name: 'immich_name',
userId: id,
userEmail: email,
name,
profileImagePath: '',
isAdmin: false,
shouldChangePassword: false,
};
});
// const token = Buffer.from('my-api-key', 'utf8').toString('base64');
@ -39,15 +38,7 @@ const fixtures = {
},
};
const oauthUserWithDefaultQuota = {
email,
name: ' ',
oauthId: sub,
quotaSizeInBytes: '1073741824',
storageLabel: null,
};
describe('AuthService', () => {
describe(AuthService.name, () => {
let sut: AuthService;
let mocks: ServiceMocks;
@ -118,14 +109,12 @@ describe('AuthService', () => {
describe('changePassword', () => {
it('should change the password', async () => {
const auth = { user: { email: 'test@imimch.com' } } as AuthDto;
const user = factory.userAdmin();
const auth = factory.auth({ user });
const dto = { password: 'old-password', newPassword: 'new-password' };
mocks.user.getByEmail.mockResolvedValue({
email: 'test@immich.com',
password: 'hash-password',
} as UserAdmin & { password: string });
mocks.user.update.mockResolvedValue(userStub.user1);
mocks.user.getByEmail.mockResolvedValue({ ...user, password: 'hash-password' });
mocks.user.update.mockResolvedValue(user);
await sut.changePassword(auth, dto);
@ -331,37 +320,39 @@ describe('AuthService', () => {
});
it('should accept a base64url key', async () => {
mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.valid as any);
mocks.user.get.mockResolvedValue(userStub.admin);
const user = factory.userAdmin();
const sharedLink = { ...sharedLinkStub.valid, user } as any;
mocks.sharedLink.getByKey.mockResolvedValue(sharedLink);
mocks.user.get.mockResolvedValue(user);
await expect(
sut.authenticate({
headers: { 'x-immich-share-key': sharedLinkStub.valid.key.toString('base64url') },
headers: { 'x-immich-share-key': sharedLink.key.toString('base64url') },
queryParams: {},
metadata: { adminRoute: false, sharedLinkRoute: true, uri: 'test' },
}),
).resolves.toEqual({
user: userStub.admin,
sharedLink: sharedLinkStub.valid,
});
expect(mocks.sharedLink.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key);
).resolves.toEqual({ user, sharedLink });
expect(mocks.sharedLink.getByKey).toHaveBeenCalledWith(sharedLink.key);
});
it('should accept a hex key', async () => {
mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.valid as any);
mocks.user.get.mockResolvedValue(userStub.admin);
const user = factory.userAdmin();
const sharedLink = { ...sharedLinkStub.valid, user } as any;
mocks.sharedLink.getByKey.mockResolvedValue(sharedLink);
mocks.user.get.mockResolvedValue(user);
await expect(
sut.authenticate({
headers: { 'x-immich-share-key': sharedLinkStub.valid.key.toString('hex') },
headers: { 'x-immich-share-key': sharedLink.key.toString('hex') },
queryParams: {},
metadata: { adminRoute: false, sharedLinkRoute: true, uri: 'test' },
}),
).resolves.toEqual({
user: userStub.admin,
sharedLink: sharedLinkStub.valid,
});
expect(mocks.sharedLink.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key);
).resolves.toEqual({ user, sharedLink });
expect(mocks.sharedLink.getByKey).toHaveBeenCalledWith(sharedLink.key);
});
});
@ -533,24 +524,28 @@ describe('AuthService', () => {
});
it('should link an existing user', async () => {
const user = factory.userAdmin();
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthEnabled);
mocks.user.getByEmail.mockResolvedValue(userStub.user1);
mocks.user.update.mockResolvedValue(userStub.user1);
mocks.user.getByEmail.mockResolvedValue(user);
mocks.user.update.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(factory.session());
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
oauthResponse,
oauthResponse(user),
);
expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1);
expect(mocks.user.update).toHaveBeenCalledWith(userStub.user1.id, { oauthId: sub });
expect(mocks.user.update).toHaveBeenCalledWith(user.id, { oauthId: sub });
});
it('should not link to a user with a different oauth sub', async () => {
const user = factory.userAdmin({ isAdmin: true, oauthId: 'existing-sub' });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithAutoRegister);
mocks.user.getByEmail.mockResolvedValueOnce({ ...userStub.user1, oauthId: 'existing-sub' });
mocks.user.getAdmin.mockResolvedValue(userStub.user1);
mocks.user.create.mockResolvedValue(userStub.user1);
mocks.user.getByEmail.mockResolvedValueOnce(user);
mocks.user.getAdmin.mockResolvedValue(user);
mocks.user.create.mockResolvedValue(user);
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).rejects.toThrow(
BadRequestException,
@ -561,14 +556,16 @@ describe('AuthService', () => {
});
it('should allow auto registering by default', async () => {
const user = factory.userAdmin({ oauthId: 'oauth-id' });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled);
mocks.user.getByEmail.mockResolvedValue(void 0);
mocks.user.getAdmin.mockResolvedValue(userStub.user1);
mocks.user.create.mockResolvedValue(userStub.user1);
mocks.user.getAdmin.mockResolvedValue(factory.userAdmin({ isAdmin: true }));
mocks.user.create.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(factory.session());
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
oauthResponse,
oauthResponse(user),
);
expect(mocks.user.getByEmail).toHaveBeenCalledTimes(2); // second call is for domain check before create
@ -576,10 +573,12 @@ describe('AuthService', () => {
});
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 });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled);
mocks.user.getByEmail.mockResolvedValue(void 0);
mocks.user.getAdmin.mockResolvedValue(userStub.user1);
mocks.user.create.mockResolvedValue(userStub.user1);
mocks.user.getAdmin.mockResolvedValue(user);
mocks.user.create.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(factory.session());
mocks.oauth.getProfile.mockResolvedValue({ sub, email: undefined });
@ -600,8 +599,10 @@ describe('AuthService', () => {
'app.immich:///oauth-callback?code=abc123',
]) {
it(`should use the mobile redirect override for a url of ${url}`, async () => {
const user = factory.userAdmin();
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithMobileOverride);
mocks.user.getByOAuthId.mockResolvedValue(userStub.user1);
mocks.user.getByOAuthId.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(factory.session());
await sut.callback({ url }, loginDetails);
@ -611,86 +612,97 @@ describe('AuthService', () => {
}
it('should use the default quota', async () => {
const user = factory.userAdmin({ oauthId: 'oauth-id' });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota);
mocks.user.getByEmail.mockResolvedValue(void 0);
mocks.user.getAdmin.mockResolvedValue(userStub.user1);
mocks.user.create.mockResolvedValue(userStub.user1);
mocks.user.getAdmin.mockResolvedValue(factory.userAdmin({ isAdmin: true }));
mocks.user.create.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(factory.session());
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
oauthResponse,
oauthResponse(user),
);
expect(mocks.user.create).toHaveBeenCalledWith({ ...oauthUserWithDefaultQuota, quotaSizeInBytes: 1_073_741_824 });
expect(mocks.user.create).toHaveBeenCalledWith(expect.objectContaining({ quotaSizeInBytes: 1_073_741_824 }));
});
it('should ignore an invalid storage quota', async () => {
const user = factory.userAdmin({ oauthId: 'oauth-id' });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota);
mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, immich_quota: 'abc' });
mocks.user.getAdmin.mockResolvedValue(factory.userAdmin({ isAdmin: true }));
mocks.user.getByEmail.mockResolvedValue(void 0);
mocks.user.getAdmin.mockResolvedValue(userStub.user1);
mocks.user.create.mockResolvedValue(userStub.user1);
mocks.oauth.getProfile.mockResolvedValue({ sub, email, immich_quota: 'abc' });
mocks.user.create.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(factory.session());
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
oauthResponse,
oauthResponse(user),
);
expect(mocks.user.create).toHaveBeenCalledWith({ ...oauthUserWithDefaultQuota, quotaSizeInBytes: 1_073_741_824 });
expect(mocks.user.create).toHaveBeenCalledWith(expect.objectContaining({ quotaSizeInBytes: 1_073_741_824 }));
});
it('should ignore a negative quota', async () => {
const user = factory.userAdmin({ oauthId: 'oauth-id' });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota);
mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, immich_quota: -5 });
mocks.user.getAdmin.mockResolvedValue(user);
mocks.user.getByEmail.mockResolvedValue(void 0);
mocks.user.getAdmin.mockResolvedValue(userStub.user1);
mocks.user.create.mockResolvedValue(userStub.user1);
mocks.oauth.getProfile.mockResolvedValue({ sub, email, immich_quota: -5 });
mocks.user.create.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(factory.session());
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
oauthResponse,
oauthResponse(user),
);
expect(mocks.user.create).toHaveBeenCalledWith({ ...oauthUserWithDefaultQuota, quotaSizeInBytes: 1_073_741_824 });
expect(mocks.user.create).toHaveBeenCalledWith(expect.objectContaining({ quotaSizeInBytes: 1_073_741_824 }));
});
it('should not set quota for 0 quota', async () => {
const user = factory.userAdmin({ oauthId: 'oauth-id' });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota);
mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, immich_quota: 0 });
mocks.user.getAdmin.mockResolvedValue(factory.userAdmin({ isAdmin: true }));
mocks.user.getByEmail.mockResolvedValue(void 0);
mocks.user.getAdmin.mockResolvedValue(userStub.user1);
mocks.user.create.mockResolvedValue(userStub.user1);
mocks.oauth.getProfile.mockResolvedValue({ sub, email, immich_quota: 0 });
mocks.user.create.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(factory.session());
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
oauthResponse,
oauthResponse(user),
);
expect(mocks.user.create).toHaveBeenCalledWith({
email,
email: user.email,
name: ' ',
oauthId: sub,
oauthId: user.oauthId,
quotaSizeInBytes: null,
storageLabel: null,
});
});
it('should use a valid storage quota', async () => {
const user = factory.userAdmin({ oauthId: 'oauth-id' });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithStorageQuota);
mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, immich_quota: 5 });
mocks.user.getByEmail.mockResolvedValue(void 0);
mocks.user.getAdmin.mockResolvedValue(userStub.user1);
mocks.user.create.mockResolvedValue(userStub.user1);
mocks.oauth.getProfile.mockResolvedValue({ sub, email, immich_quota: 5 });
mocks.user.getAdmin.mockResolvedValue(factory.userAdmin({ isAdmin: true }));
mocks.user.getByOAuthId.mockResolvedValue(void 0);
mocks.user.create.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(factory.session());
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
oauthResponse,
oauthResponse(user),
);
expect(mocks.user.create).toHaveBeenCalledWith({
email,
email: user.email,
name: ' ',
oauthId: sub,
oauthId: user.oauthId,
quotaSizeInBytes: 5_368_709_120,
storageLabel: null,
});
@ -699,12 +711,11 @@ describe('AuthService', () => {
describe('link', () => {
it('should link an account', async () => {
const authUser = factory.authUser();
const authApiKey = factory.authApiKey({ permissions: [] });
const auth = { user: authUser, apiKey: authApiKey };
const user = factory.userAdmin();
const auth = factory.auth({ apiKey: { permissions: [] }, user });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled);
mocks.user.update.mockResolvedValue(userStub.user1);
mocks.user.update.mockResolvedValue(user);
await sut.link(auth, { url: 'http://immich/user-settings?code=abc123' });
@ -729,12 +740,11 @@ describe('AuthService', () => {
describe('unlink', () => {
it('should unlink an account', async () => {
const authUser = factory.authUser();
const authApiKey = factory.authApiKey({ permissions: [] });
const auth = { user: authUser, apiKey: authApiKey };
const user = factory.userAdmin();
const auth = factory.auth({ user, apiKey: { permissions: [] } });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled);
mocks.user.update.mockResolvedValue(userStub.user1);
mocks.user.update.mockResolvedValue(user);
await sut.unlink(auth);

View File

@ -1,5 +1,5 @@
import { CliService } from 'src/services/cli.service';
import { userStub } from 'test/fixtures/user.stub';
import { factory } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils';
import { describe, it } from 'vitest';
@ -13,7 +13,7 @@ describe(CliService.name, () => {
describe('listUsers', () => {
it('should list users', async () => {
mocks.user.getList.mockResolvedValue([userStub.admin]);
mocks.user.getList.mockResolvedValue([factory.userAdmin({ isAdmin: true })]);
await expect(sut.listUsers()).resolves.toEqual([expect.objectContaining({ isAdmin: true })]);
expect(mocks.user.getList).toHaveBeenCalledWith({ withDeleted: true });
});
@ -30,8 +30,10 @@ describe(CliService.name, () => {
});
it('should default to a random password', async () => {
mocks.user.getAdmin.mockResolvedValue(userStub.admin);
mocks.user.update.mockResolvedValue(userStub.admin);
const admin = factory.userAdmin({ isAdmin: true });
mocks.user.getAdmin.mockResolvedValue(admin);
mocks.user.update.mockResolvedValue(factory.userAdmin({ isAdmin: true }));
const ask = vitest.fn().mockImplementation(() => {});
@ -41,13 +43,15 @@ describe(CliService.name, () => {
expect(response.provided).toBe(false);
expect(ask).toHaveBeenCalled();
expect(id).toEqual(userStub.admin.id);
expect(id).toEqual(admin.id);
expect(update.password).toBeDefined();
});
it('should use the supplied password', async () => {
mocks.user.getAdmin.mockResolvedValue(userStub.admin);
mocks.user.update.mockResolvedValue(userStub.admin);
const admin = factory.userAdmin({ isAdmin: true });
mocks.user.getAdmin.mockResolvedValue(admin);
mocks.user.update.mockResolvedValue(admin);
const ask = vitest.fn().mockResolvedValue('new-password');
@ -57,7 +61,7 @@ describe(CliService.name, () => {
expect(response.provided).toBe(true);
expect(ask).toHaveBeenCalled();
expect(id).toEqual(userStub.admin.id);
expect(id).toEqual(admin.id);
expect(update.password).toBeDefined();
});
});

View File

@ -35,7 +35,7 @@ describe(MapService.name, () => {
it('should include partner assets', async () => {
const partner = factory.partner();
const auth = factory.auth({ id: partner.sharedWithId });
const auth = factory.auth({ user: { id: partner.sharedWithId } });
const asset = assetStub.withLocation;
const marker = {

View File

@ -24,7 +24,7 @@ describe(MemoryService.name, () => {
mocks.memory.search.mockResolvedValue([memory1, memory2]);
await expect(sut.search(factory.auth({ id: userId }), {})).resolves.toEqual(
await expect(sut.search(factory.auth({ user: { id: userId } }), {})).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({ id: memory1.id, assets: [expect.objectContaining({ id: asset.id })] }),
expect.objectContaining({ id: memory2.id, assets: [] }),
@ -60,7 +60,9 @@ describe(MemoryService.name, () => {
mocks.memory.get.mockResolvedValue(memory);
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
await expect(sut.get(factory.auth({ id: userId }), memory.id)).resolves.toMatchObject({ id: memory.id });
await expect(sut.get(factory.auth({ user: { id: userId } }), memory.id)).resolves.toMatchObject({
id: memory.id,
});
expect(mocks.memory.get).toHaveBeenCalledWith(memory.id);
expect(mocks.access.memory.checkOwnerAccess).toHaveBeenCalledWith(memory.ownerId, new Set([memory.id]));
@ -75,7 +77,7 @@ describe(MemoryService.name, () => {
mocks.memory.create.mockResolvedValue(memory);
await expect(
sut.create(factory.auth({ id: userId }), {
sut.create(factory.auth({ user: { id: userId } }), {
type: memory.type,
data: memory.data,
memoryAt: memory.memoryAt,
@ -105,7 +107,7 @@ describe(MemoryService.name, () => {
mocks.memory.create.mockResolvedValue(memory);
await expect(
sut.create(factory.auth({ id: userId }), {
sut.create(factory.auth({ user: { id: userId } }), {
type: memory.type,
data: memory.data,
assetIds: memory.assets.map((asset) => asset.id),

View File

@ -22,7 +22,7 @@ describe(PartnerService.name, () => {
const user2 = factory.user();
const sharedWithUser2 = factory.partner({ sharedBy: user1, sharedWith: user2 });
const sharedWithUser1 = factory.partner({ sharedBy: user2, sharedWith: user1 });
const auth = factory.auth({ id: user1.id });
const auth = factory.auth({ user: { id: user1.id } });
mocks.partner.getAll.mockResolvedValue([sharedWithUser1, sharedWithUser2]);
@ -35,7 +35,7 @@ describe(PartnerService.name, () => {
const user2 = factory.user();
const sharedWithUser2 = factory.partner({ sharedBy: user1, sharedWith: user2 });
const sharedWithUser1 = factory.partner({ sharedBy: user2, sharedWith: user1 });
const auth = factory.auth({ id: user1.id });
const auth = factory.auth({ user: { id: user1.id } });
mocks.partner.getAll.mockResolvedValue([sharedWithUser1, sharedWithUser2]);
await expect(sut.search(auth, { direction: PartnerDirection.SharedWith })).resolves.toBeDefined();
@ -48,7 +48,7 @@ describe(PartnerService.name, () => {
const user1 = factory.user();
const user2 = factory.user();
const partner = factory.partner({ sharedBy: user1, sharedWith: user2 });
const auth = factory.auth({ id: user1.id });
const auth = factory.auth({ user: { id: user1.id } });
mocks.partner.get.mockResolvedValue(void 0);
mocks.partner.create.mockResolvedValue(partner);
@ -65,7 +65,7 @@ describe(PartnerService.name, () => {
const user1 = factory.user();
const user2 = factory.user();
const partner = factory.partner({ sharedBy: user1, sharedWith: user2 });
const auth = factory.auth({ id: user1.id });
const auth = factory.auth({ user: { id: user1.id } });
mocks.partner.get.mockResolvedValue(partner);
@ -80,7 +80,7 @@ describe(PartnerService.name, () => {
const user1 = factory.user();
const user2 = factory.user();
const partner = factory.partner({ sharedBy: user1, sharedWith: user2 });
const auth = factory.auth({ id: user1.id });
const auth = factory.auth({ user: { id: user1.id } });
mocks.partner.get.mockResolvedValue(partner);
@ -113,7 +113,7 @@ describe(PartnerService.name, () => {
const user1 = factory.user();
const user2 = factory.user();
const partner = factory.partner({ sharedBy: user1, sharedWith: user2 });
const auth = factory.auth({ id: user1.id });
const auth = factory.auth({ user: { id: user1.id } });
mocks.access.partner.checkUpdateAccess.mockResolvedValue(new Set([user2.id]));
mocks.partner.update.mockResolvedValue(partner);

View File

@ -1280,7 +1280,7 @@ describe(PersonService.name, () => {
describe('mapFace', () => {
it('should map a face', () => {
const authDto = factory.auth({ id: faceStub.face1.person.ownerId });
const authDto = factory.auth({ user: { id: faceStub.face1.person.ownerId } });
expect(mapFaces(faceStub.face1, authDto)).toEqual({
boundingBoxX1: 0,
boundingBoxX2: 1,

View File

@ -7,6 +7,7 @@ import { albumStub } from 'test/fixtures/album.stub';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { sharedLinkResponseStub, sharedLinkStub } from 'test/fixtures/shared-link.stub';
import { factory } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils';
describe(SharedLinkService.name, () => {
@ -46,7 +47,13 @@ describe(SharedLinkService.name, () => {
});
it('should not return metadata', async () => {
const authDto = authStub.adminSharedLinkNoExif;
const authDto = factory.auth({
sharedLink: {
showExif: false,
allowDownload: true,
allowUpload: true,
},
});
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.readonlyNoExif);
await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.readonlyNoMetadata);
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
@ -208,7 +215,9 @@ describe(SharedLinkService.name, () => {
it('should update a shared link', async () => {
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
mocks.sharedLink.update.mockResolvedValue(sharedLinkStub.valid);
await sut.update(authStub.user1, sharedLinkStub.valid.id, { allowDownload: false });
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
expect(mocks.sharedLink.update).toHaveBeenCalledWith({
id: sharedLinkStub.valid.id,
@ -242,6 +251,7 @@ describe(SharedLinkService.name, () => {
describe('addAssets', () => {
it('should not work on album shared links', async () => {
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
await expect(sut.addAssets(authStub.admin, 'link-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf(
BadRequestException,
);
@ -273,6 +283,7 @@ describe(SharedLinkService.name, () => {
describe('removeAssets', () => {
it('should not work on album shared links', async () => {
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
await expect(sut.removeAssets(authStub.admin, 'link-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf(
BadRequestException,
);
@ -297,31 +308,39 @@ describe(SharedLinkService.name, () => {
describe('getMetadataTags', () => {
it('should return null when auth is not a shared link', async () => {
await expect(sut.getMetadataTags(authStub.admin)).resolves.toBe(null);
expect(mocks.sharedLink.get).not.toHaveBeenCalled();
});
it('should return null when shared link has a password', async () => {
await expect(sut.getMetadataTags(authStub.passwordSharedLink)).resolves.toBe(null);
const auth = factory.auth({ user: {}, sharedLink: { password: 'password' } });
await expect(sut.getMetadataTags(auth)).resolves.toBe(null);
expect(mocks.sharedLink.get).not.toHaveBeenCalled();
});
it('should return metadata tags', async () => {
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.individual);
await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({
description: '1 shared photos & videos',
imageUrl: `https://my.immich.app/api/assets/asset-id/thumbnail?key=LCtkaJX4R1O_9D-2lq0STzsPryoL1UdAbyb6Sna1xxmQCSuqU2J1ZUsqt6GR-yGm1s0`,
title: 'Public Share',
});
expect(mocks.sharedLink.get).toHaveBeenCalled();
});
it('should return metadata tags with a default image path if the asset id is not set', async () => {
mocks.sharedLink.get.mockResolvedValue({ ...sharedLinkStub.individual, album: undefined, assets: [] });
await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({
description: '0 shared photos & videos',
imageUrl: `https://my.immich.app/feature-panel.png`,
title: 'Public Share',
});
expect(mocks.sharedLink.get).toHaveBeenCalled();
});
});

View File

@ -5,6 +5,7 @@ import { StorageTemplateService } from 'src/services/storage-template.service';
import { albumStub } from 'test/fixtures/album.stub';
import { assetStub } from 'test/fixtures/asset.stub';
import { userStub } from 'test/fixtures/user.stub';
import { factory } from 'test/small.factory';
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
const motionAsset = assetStub.storageAsset({});
@ -426,15 +427,16 @@ describe(StorageTemplateService.name, () => {
});
it('should use the user storage label', async () => {
const asset = assetStub.storageAsset();
const user = factory.userAdmin({ storageLabel: 'label-1' });
const asset = assetStub.storageAsset({ ownerId: user.id });
mocks.asset.streamStorageTemplateAssets.mockReturnValue(makeStream([asset]));
mocks.user.getList.mockResolvedValue([userStub.storageLabel]);
mocks.user.getList.mockResolvedValue([user]);
mocks.move.create.mockResolvedValue({
id: '123',
entityId: asset.id,
pathType: AssetPathType.ORIGINAL,
oldPath: asset.originalPath,
newPath: `upload/library/user-id/2023/2023-02-23/${asset.originalFileName}`,
newPath: `upload/library/${user.storageLabel}/2023/2023-02-23/${asset.originalFileName}`,
});
await sut.handleMigration();
@ -442,11 +444,11 @@ describe(StorageTemplateService.name, () => {
expect(mocks.asset.streamStorageTemplateAssets).toHaveBeenCalled();
expect(mocks.storage.rename).toHaveBeenCalledWith(
'/original/path.jpg',
`upload/library/label-1/2022/2022-06-19/${asset.originalFileName}`,
`upload/library/${user.storageLabel}/2022/2022-06-19/${asset.originalFileName}`,
);
expect(mocks.asset.update).toHaveBeenCalledWith({
id: asset.id,
originalPath: `upload/library/label-1/2022/2022-06-19/${asset.originalFileName}`,
originalPath: `upload/library/${user.storageLabel}/2022/2022-06-19/${asset.originalFileName}`,
});
});
@ -551,98 +553,106 @@ describe(StorageTemplateService.name, () => {
describe('file rename correctness', () => {
it('should not create double extensions when filename has lower extension', async () => {
const user = factory.userAdmin({ storageLabel: 'label-1' });
const asset = assetStub.storageAsset({
originalPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.heic',
ownerId: user.id,
originalPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.heic`,
originalFileName: 'IMG_7065.HEIC',
});
mocks.asset.streamStorageTemplateAssets.mockReturnValue(makeStream([asset]));
mocks.user.getList.mockResolvedValue([userStub.storageLabel]);
mocks.user.getList.mockResolvedValue([user]);
mocks.move.create.mockResolvedValue({
id: '123',
entityId: asset.id,
pathType: AssetPathType.ORIGINAL,
oldPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.heic',
newPath: 'upload/library/user-id/2023/2023-02-23/IMG_7065.heic',
oldPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.heic`,
newPath: `upload/library/${user.id}/2023/2023-02-23/IMG_7065.heic`,
});
await sut.handleMigration();
expect(mocks.asset.streamStorageTemplateAssets).toHaveBeenCalled();
expect(mocks.storage.rename).toHaveBeenCalledWith(
'upload/library/user-id/2022/2022-06-19/IMG_7065.heic',
'upload/library/label-1/2022/2022-06-19/IMG_7065.heic',
`upload/library/${user.id}/2022/2022-06-19/IMG_7065.heic`,
`upload/library/${user.storageLabel}/2022/2022-06-19/IMG_7065.heic`,
);
});
it('should not create double extensions when filename has uppercase extension', async () => {
const user = factory.userAdmin();
const asset = assetStub.storageAsset({
originalPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.HEIC',
ownerId: user.id,
originalPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.HEIC`,
originalFileName: 'IMG_7065.HEIC',
});
mocks.asset.streamStorageTemplateAssets.mockReturnValue(makeStream([asset]));
mocks.user.getList.mockResolvedValue([userStub.storageLabel]);
mocks.user.getList.mockResolvedValue([user]);
mocks.move.create.mockResolvedValue({
id: '123',
entityId: asset.id,
pathType: AssetPathType.ORIGINAL,
oldPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.HEIC',
newPath: 'upload/library/user-id/2023/2023-02-23/IMG_7065.heic',
oldPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.HEIC`,
newPath: `upload/library/${user.id}/2023/2023-02-23/IMG_7065.heic`,
});
await sut.handleMigration();
expect(mocks.asset.streamStorageTemplateAssets).toHaveBeenCalled();
expect(mocks.storage.rename).toHaveBeenCalledWith(
'upload/library/user-id/2022/2022-06-19/IMG_7065.HEIC',
'upload/library/label-1/2022/2022-06-19/IMG_7065.heic',
`upload/library/${user.id}/2022/2022-06-19/IMG_7065.HEIC`,
`upload/library/${user.id}/2022/2022-06-19/IMG_7065.heic`,
);
});
it('should normalize the filename to lowercase (JPEG > jpg)', async () => {
const user = factory.userAdmin();
const asset = assetStub.storageAsset({
originalPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.JPEG',
ownerId: user.id,
originalPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.JPEG`,
originalFileName: 'IMG_7065.JPEG',
});
mocks.asset.streamStorageTemplateAssets.mockReturnValue(makeStream([asset]));
mocks.user.getList.mockResolvedValue([userStub.storageLabel]);
mocks.user.getList.mockResolvedValue([user]);
mocks.move.create.mockResolvedValue({
id: '123',
entityId: asset.id,
pathType: AssetPathType.ORIGINAL,
oldPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.JPEG',
newPath: 'upload/library/user-id/2023/2023-02-23/IMG_7065.jpg',
oldPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.JPEG`,
newPath: `upload/library/${user.id}/2023/2023-02-23/IMG_7065.jpg`,
});
await sut.handleMigration();
expect(mocks.asset.streamStorageTemplateAssets).toHaveBeenCalled();
expect(mocks.storage.rename).toHaveBeenCalledWith(
'upload/library/user-id/2022/2022-06-19/IMG_7065.JPEG',
'upload/library/label-1/2022/2022-06-19/IMG_7065.jpg',
`upload/library/${user.id}/2022/2022-06-19/IMG_7065.JPEG`,
`upload/library/${user.id}/2022/2022-06-19/IMG_7065.jpg`,
);
});
it('should normalize the filename to lowercase (JPG > jpg)', async () => {
const user = factory.userAdmin();
const asset = assetStub.storageAsset({
ownerId: user.id,
originalPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.JPG',
originalFileName: 'IMG_7065.JPG',
});
mocks.asset.streamStorageTemplateAssets.mockReturnValue(makeStream([asset]));
mocks.user.getList.mockResolvedValue([userStub.storageLabel]);
mocks.user.getList.mockResolvedValue([user]);
mocks.move.create.mockResolvedValue({
id: '123',
entityId: asset.id,
pathType: AssetPathType.ORIGINAL,
oldPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.JPG',
newPath: 'upload/library/user-id/2023/2023-02-23/IMG_7065.jpg',
oldPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.JPG`,
newPath: `upload/library/${user.id}/2023/2023-02-23/IMG_7065.jpg`,
});
await sut.handleMigration();
expect(mocks.asset.streamStorageTemplateAssets).toHaveBeenCalled();
expect(mocks.storage.rename).toHaveBeenCalledWith(
'upload/library/user-id/2022/2022-06-19/IMG_7065.JPG',
'upload/library/label-1/2022/2022-06-19/IMG_7065.jpg',
`upload/library/${user.id}/2022/2022-06-19/IMG_7065.JPG`,
`upload/library/${user.id}/2022/2022-06-19/IMG_7065.jpg`,
);
});
});

View File

@ -39,7 +39,7 @@ describe(SyncService.name, () => {
describe('getChangesForDeltaSync', () => {
it('should return a response requiring a full sync when partners are out of sync', async () => {
const partner = factory.partner();
const auth = factory.auth({ id: partner.sharedWithId });
const auth = factory.auth({ user: { id: partner.sharedWithId } });
mocks.partner.getAll.mockResolvedValue([partner]);

View File

@ -3,6 +3,7 @@ import { TimeBucketSize } from 'src/repositories/asset.repository';
import { TimelineService } from 'src/services/timeline.service';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { factory } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils';
describe(TimelineService.name, () => {
@ -114,15 +115,15 @@ describe(TimelineService.name, () => {
mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set(['album-id']));
mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]);
const buckets = await sut.getTimeBucket(
{ ...authStub.admin, sharedLink: { ...authStub.adminSharedLink.sharedLink!, showExif: false } },
{
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isArchived: true,
albumId: 'album-id',
},
);
const auth = factory.auth({ sharedLink: { showExif: false } });
const buckets = await sut.getTimeBucket(auth, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isArchived: true,
albumId: 'album-id',
});
expect(buckets).toEqual([expect.objectContaining({ id: 'asset-id' })]);
expect(buckets[0]).not.toHaveProperty('exif');
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {

View File

@ -29,7 +29,7 @@ describe(UserService.name, () => {
describe('getAll', () => {
it('admin should get all users', async () => {
const user = factory.userAdmin();
const auth = factory.auth(user);
const auth = factory.auth({ user });
mocks.user.getList.mockResolvedValue([user]);
@ -39,14 +39,12 @@ describe(UserService.name, () => {
});
it('non-admin should get all users when publicUsers enabled', async () => {
mocks.user.getList.mockResolvedValue([userStub.user1]);
const user = factory.userAdmin();
const auth = factory.auth({ user });
await expect(sut.search(authStub.user1)).resolves.toEqual([
expect.objectContaining({
id: authStub.user1.user.id,
email: authStub.user1.user.email,
}),
]);
mocks.user.getList.mockResolvedValue([user]);
await expect(sut.search(auth)).resolves.toEqual([expect.objectContaining({ id: user.id, email: user.email })]);
expect(mocks.user.getList).toHaveBeenCalledWith({ withDeleted: false });
});
@ -107,17 +105,19 @@ describe(UserService.name, () => {
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;
mocks.user.get.mockResolvedValue(userStub.profilePath);
const user = factory.userAdmin({ profileImagePath: '/path/to/profile.jpg' });
mocks.user.get.mockResolvedValue(user);
mocks.user.update.mockRejectedValue(new InternalServerErrorException('mocked error'));
await expect(sut.createProfileImage(authStub.admin, file)).rejects.toThrowError(InternalServerErrorException);
});
it('should delete the previous profile image', async () => {
const user = factory.userAdmin({ profileImagePath: '/path/to/profile.jpg' });
const file = { path: '/profile/path' } as Express.Multer.File;
const files = [userStub.profilePath.profileImagePath];
const files = [user.profileImagePath];
mocks.user.get.mockResolvedValue(userStub.profilePath);
mocks.user.get.mockResolvedValue(user);
mocks.user.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path });
await sut.createProfileImage(authStub.admin, file);
@ -149,8 +149,10 @@ describe(UserService.name, () => {
});
it('should delete the profile image if user has one', async () => {
mocks.user.get.mockResolvedValue(userStub.profilePath);
const files = [userStub.profilePath.profileImagePath];
const user = factory.userAdmin({ profileImagePath: '/path/to/profile.jpg' });
const files = [user.profileImagePath];
mocks.user.get.mockResolvedValue(user);
await sut.deleteProfileImage(authStub.admin);
@ -176,9 +178,10 @@ describe(UserService.name, () => {
});
it('should return the profile picture', async () => {
mocks.user.get.mockResolvedValue(userStub.profilePath);
const user = factory.userAdmin({ profileImagePath: '/path/to/profile.jpg' });
mocks.user.get.mockResolvedValue(user);
await expect(sut.getProfileImage(userStub.profilePath.id)).resolves.toEqual(
await expect(sut.getProfileImage(user.id)).resolves.toEqual(
new ImmichFileResponse({
path: '/path/to/profile.jpg',
contentType: 'image/jpeg',
@ -186,7 +189,7 @@ describe(UserService.name, () => {
}),
);
expect(mocks.user.get).toHaveBeenCalledWith(userStub.profilePath.id, {});
expect(mocks.user.get).toHaveBeenCalledWith(user.id, {});
});
});

View File

@ -52,24 +52,4 @@ export const authStub = {
key: Buffer.from('shared-link-key'),
} as SharedLinkEntity,
}),
adminSharedLinkNoExif: Object.freeze<AuthDto>({
user: authUser.admin,
sharedLink: {
id: '123',
showExif: false,
allowDownload: true,
allowUpload: true,
key: Buffer.from('shared-link-key'),
} as SharedLinkEntity,
}),
passwordSharedLink: Object.freeze<AuthDto>({
user: authUser.admin,
sharedLink: {
id: '123',
allowUpload: false,
allowDownload: false,
password: 'password-123',
showExif: true,
} as SharedLinkEntity,
}),
};

View File

@ -57,36 +57,4 @@ export const userStub = {
quotaSizeInBytes: null,
quotaUsageInBytes: 0,
},
storageLabel: <UserAdmin>{
...authStub.user1.user,
status: UserStatus.ACTIVE,
profileChangedAt: new Date('2021-01-01'),
metadata: [],
name: 'immich_name',
storageLabel: 'label-1',
oauthId: '',
shouldChangePassword: false,
profileImagePath: '',
createdAt: new Date('2021-01-01'),
deletedAt: null,
updatedAt: new Date('2021-01-01'),
quotaSizeInBytes: null,
quotaUsageInBytes: 0,
},
profilePath: <UserAdmin>{
...authStub.user1.user,
status: UserStatus.ACTIVE,
profileChangedAt: new Date('2021-01-01'),
metadata: [],
name: 'immich_name',
storageLabel: 'label-1',
oauthId: '',
shouldChangePassword: false,
profileImagePath: '/path/to/profile.jpg',
createdAt: new Date('2021-01-01'),
deletedAt: null,
updatedAt: new Date('2021-01-01'),
quotaSizeInBytes: null,
quotaUsageInBytes: 0,
},
};

View File

@ -4,6 +4,7 @@ import {
ApiKey,
Asset,
AuthApiKey,
AuthSharedLink,
AuthUser,
Library,
Memory,
@ -35,12 +36,20 @@ export const newEmbedding = () => {
const authFactory = ({
apiKey,
session,
...user
}: Partial<AuthUser> & { apiKey?: Partial<AuthApiKey>; session?: { id: string } } = {}) => {
sharedLink,
user,
}: {
apiKey?: Partial<AuthApiKey>;
session?: { id: string };
user?: Partial<UserAdmin>;
sharedLink?: Partial<AuthSharedLink>;
} = {}) => {
const auth: AuthDto = {
user: authUserFactory(user),
user: authUserFactory(userAdminFactory(user ?? {})),
};
const userId = auth.user.id;
if (apiKey) {
auth.apiKey = authApiKeyFactory(apiKey);
}
@ -49,24 +58,45 @@ const authFactory = ({
auth.session = { id: session.id };
}
if (sharedLink) {
auth.sharedLink = authSharedLinkFactory({ ...sharedLink, userId });
}
return auth;
};
const authSharedLinkFactory = (sharedLink: Partial<AuthSharedLink> = {}) => {
const {
id = newUuid(),
expiresAt = null,
userId = newUuid(),
showExif = true,
allowUpload = false,
allowDownload = true,
password = null,
} = sharedLink;
return { id, expiresAt, userId, showExif, allowUpload, allowDownload, password };
};
const authApiKeyFactory = (apiKey: Partial<AuthApiKey> = {}) => ({
id: newUuid(),
permissions: [Permission.ALL],
...apiKey,
});
const authUserFactory = (authUser: Partial<AuthUser> = {}) => ({
id: newUuid(),
isAdmin: false,
name: 'Test User',
email: 'test@immich.cloud',
quotaUsageInBytes: 0,
quotaSizeInBytes: null,
...authUser,
});
const authUserFactory = (authUser: Partial<AuthUser> = {}) => {
const {
id = newUuid(),
isAdmin = false,
name = 'Test User',
email = 'test@immich.cloud',
quotaUsageInBytes = 0,
quotaSizeInBytes = null,
} = authUser;
return { id, isAdmin, name, email, quotaUsageInBytes, quotaSizeInBytes };
};
const partnerFactory = (partner: Partial<Partner> = {}) => {
const sharedBy = userFactory(partner.sharedBy || {});
@ -112,25 +142,44 @@ const userFactory = (user: Partial<User> = {}) => ({
...user,
});
const userAdminFactory = (user: Partial<UserAdmin> = {}) => ({
id: newUuid(),
name: 'Test User',
email: 'test@immich.cloud',
profileImagePath: '',
profileChangedAt: newDate(),
storageLabel: null,
shouldChangePassword: false,
isAdmin: false,
createdAt: newDate(),
updatedAt: newDate(),
deletedAt: null,
oauthId: '',
quotaSizeInBytes: null,
quotaUsageInBytes: 0,
status: UserStatus.ACTIVE,
metadata: [],
...user,
});
const userAdminFactory = (user: Partial<UserAdmin> = {}) => {
const {
id = newUuid(),
name = 'Test User',
email = 'test@immich.cloud',
profileImagePath = '',
profileChangedAt = newDate(),
storageLabel = null,
shouldChangePassword = false,
isAdmin = false,
createdAt = newDate(),
updatedAt = newDate(),
deletedAt = null,
oauthId = '',
quotaSizeInBytes = null,
quotaUsageInBytes = 0,
status = UserStatus.ACTIVE,
metadata = [],
} = user;
return {
id,
name,
email,
profileImagePath,
profileChangedAt,
storageLabel,
shouldChangePassword,
isAdmin,
createdAt,
updatedAt,
deletedAt,
oauthId,
quotaSizeInBytes,
quotaUsageInBytes,
status,
metadata,
};
};
const assetFactory = (asset: Partial<Asset> = {}) => ({
id: newUuid(),