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.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumId]));
mocks.activity.search.mockResolvedValue([]); 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 }); expect(mocks.activity.search).toHaveBeenCalledWith({ assetId, albumId, isLiked: undefined });
}); });
@ -35,7 +35,7 @@ describe(ActivityService.name, () => {
mocks.activity.search.mockResolvedValue([]); mocks.activity.search.mockResolvedValue([]);
await expect( 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([]); ).resolves.toEqual([]);
expect(mocks.activity.search).toHaveBeenCalledWith({ assetId, albumId, isLiked: true }); 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.access.activity.checkCreateAccess.mockResolvedValue(new Set([albumId]));
mocks.activity.create.mockResolvedValue(activity); mocks.activity.create.mockResolvedValue(activity);
await sut.create(factory.auth({ id: userId }), { await sut.create(factory.auth({ user: { id: userId } }), {
albumId, albumId,
assetId, assetId,
type: ReactionType.COMMENT, type: ReactionType.COMMENT,
@ -116,7 +116,7 @@ describe(ActivityService.name, () => {
mocks.activity.create.mockResolvedValue(activity); mocks.activity.create.mockResolvedValue(activity);
mocks.activity.search.mockResolvedValue([]); 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 }); 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 () => { 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( await expect(sut.create(auth, { permissions: [Permission.ASSET_UPDATE] })).rejects.toBeInstanceOf(
BadRequestException, BadRequestException,

View File

@ -88,7 +88,7 @@ describe(AssetService.name, () => {
it('should get memories with partners with inTimeline enabled', async () => { it('should get memories with partners with inTimeline enabled', async () => {
const partner = factory.partner(); 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.partner.getAll.mockResolvedValue([partner]);
mocks.asset.getByDayOfYear.mockResolvedValue([]); mocks.asset.getByDayOfYear.mockResolvedValue([]);
@ -139,7 +139,7 @@ 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 = 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.asset.getRandom.mockResolvedValue([assetStub.image]);
mocks.partner.getAll.mockResolvedValue([partner]); mocks.partner.getAll.mockResolvedValue([partner]);
@ -151,7 +151,7 @@ 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 = 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.asset.getRandom.mockResolvedValue([assetStub.image]);
mocks.partner.getAll.mockResolvedValue([partner]); mocks.partner.getAll.mockResolvedValue([partner]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1280,7 +1280,7 @@ describe(PersonService.name, () => {
describe('mapFace', () => { describe('mapFace', () => {
it('should map a face', () => { 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({ expect(mapFaces(faceStub.face1, authDto)).toEqual({
boundingBoxX1: 0, boundingBoxX1: 0,
boundingBoxX2: 1, boundingBoxX2: 1,

View File

@ -7,6 +7,7 @@ import { albumStub } from 'test/fixtures/album.stub';
import { assetStub } from 'test/fixtures/asset.stub'; import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { sharedLinkResponseStub, sharedLinkStub } from 'test/fixtures/shared-link.stub'; import { sharedLinkResponseStub, sharedLinkStub } from 'test/fixtures/shared-link.stub';
import { factory } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils'; import { newTestService, ServiceMocks } from 'test/utils';
describe(SharedLinkService.name, () => { describe(SharedLinkService.name, () => {
@ -46,7 +47,13 @@ describe(SharedLinkService.name, () => {
}); });
it('should not return metadata', async () => { 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); mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.readonlyNoExif);
await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.readonlyNoMetadata); await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.readonlyNoMetadata);
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id); 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 () => { it('should update a shared link', async () => {
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid); mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
mocks.sharedLink.update.mockResolvedValue(sharedLinkStub.valid); mocks.sharedLink.update.mockResolvedValue(sharedLinkStub.valid);
await sut.update(authStub.user1, sharedLinkStub.valid.id, { allowDownload: false }); 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.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
expect(mocks.sharedLink.update).toHaveBeenCalledWith({ expect(mocks.sharedLink.update).toHaveBeenCalledWith({
id: sharedLinkStub.valid.id, id: sharedLinkStub.valid.id,
@ -242,6 +251,7 @@ describe(SharedLinkService.name, () => {
describe('addAssets', () => { describe('addAssets', () => {
it('should not work on album shared links', async () => { it('should not work on album shared links', async () => {
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid); mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
await expect(sut.addAssets(authStub.admin, 'link-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf( await expect(sut.addAssets(authStub.admin, 'link-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf(
BadRequestException, BadRequestException,
); );
@ -273,6 +283,7 @@ describe(SharedLinkService.name, () => {
describe('removeAssets', () => { describe('removeAssets', () => {
it('should not work on album shared links', async () => { it('should not work on album shared links', async () => {
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid); mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
await expect(sut.removeAssets(authStub.admin, 'link-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf( await expect(sut.removeAssets(authStub.admin, 'link-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf(
BadRequestException, BadRequestException,
); );
@ -297,31 +308,39 @@ describe(SharedLinkService.name, () => {
describe('getMetadataTags', () => { describe('getMetadataTags', () => {
it('should return null when auth is not a shared link', async () => { it('should return null when auth is not a shared link', async () => {
await expect(sut.getMetadataTags(authStub.admin)).resolves.toBe(null); await expect(sut.getMetadataTags(authStub.admin)).resolves.toBe(null);
expect(mocks.sharedLink.get).not.toHaveBeenCalled(); expect(mocks.sharedLink.get).not.toHaveBeenCalled();
}); });
it('should return null when shared link has a password', async () => { 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(); expect(mocks.sharedLink.get).not.toHaveBeenCalled();
}); });
it('should return metadata tags', async () => { it('should return metadata tags', async () => {
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.individual); mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.individual);
await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({ await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({
description: '1 shared photos & videos', description: '1 shared photos & videos',
imageUrl: `https://my.immich.app/api/assets/asset-id/thumbnail?key=LCtkaJX4R1O_9D-2lq0STzsPryoL1UdAbyb6Sna1xxmQCSuqU2J1ZUsqt6GR-yGm1s0`, imageUrl: `https://my.immich.app/api/assets/asset-id/thumbnail?key=LCtkaJX4R1O_9D-2lq0STzsPryoL1UdAbyb6Sna1xxmQCSuqU2J1ZUsqt6GR-yGm1s0`,
title: 'Public Share', title: 'Public Share',
}); });
expect(mocks.sharedLink.get).toHaveBeenCalled(); expect(mocks.sharedLink.get).toHaveBeenCalled();
}); });
it('should return metadata tags with a default image path if the asset id is not set', async () => { 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: [] }); mocks.sharedLink.get.mockResolvedValue({ ...sharedLinkStub.individual, album: undefined, assets: [] });
await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({ await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({
description: '0 shared photos & videos', description: '0 shared photos & videos',
imageUrl: `https://my.immich.app/feature-panel.png`, imageUrl: `https://my.immich.app/feature-panel.png`,
title: 'Public Share', title: 'Public Share',
}); });
expect(mocks.sharedLink.get).toHaveBeenCalled(); 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 { albumStub } from 'test/fixtures/album.stub';
import { assetStub } from 'test/fixtures/asset.stub'; import { assetStub } from 'test/fixtures/asset.stub';
import { userStub } from 'test/fixtures/user.stub'; import { userStub } from 'test/fixtures/user.stub';
import { factory } from 'test/small.factory';
import { makeStream, newTestService, ServiceMocks } from 'test/utils'; import { makeStream, newTestService, ServiceMocks } from 'test/utils';
const motionAsset = assetStub.storageAsset({}); const motionAsset = assetStub.storageAsset({});
@ -426,15 +427,16 @@ describe(StorageTemplateService.name, () => {
}); });
it('should use the user storage label', async () => { 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.asset.streamStorageTemplateAssets.mockReturnValue(makeStream([asset]));
mocks.user.getList.mockResolvedValue([userStub.storageLabel]); mocks.user.getList.mockResolvedValue([user]);
mocks.move.create.mockResolvedValue({ mocks.move.create.mockResolvedValue({
id: '123', id: '123',
entityId: asset.id, entityId: asset.id,
pathType: AssetPathType.ORIGINAL, pathType: AssetPathType.ORIGINAL,
oldPath: asset.originalPath, 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(); await sut.handleMigration();
@ -442,11 +444,11 @@ describe(StorageTemplateService.name, () => {
expect(mocks.asset.streamStorageTemplateAssets).toHaveBeenCalled(); expect(mocks.asset.streamStorageTemplateAssets).toHaveBeenCalled();
expect(mocks.storage.rename).toHaveBeenCalledWith( expect(mocks.storage.rename).toHaveBeenCalledWith(
'/original/path.jpg', '/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({ expect(mocks.asset.update).toHaveBeenCalledWith({
id: asset.id, 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', () => { describe('file rename correctness', () => {
it('should not create double extensions when filename has lower extension', async () => { it('should not create double extensions when filename has lower extension', async () => {
const user = factory.userAdmin({ storageLabel: 'label-1' });
const asset = assetStub.storageAsset({ 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', originalFileName: 'IMG_7065.HEIC',
}); });
mocks.asset.streamStorageTemplateAssets.mockReturnValue(makeStream([asset])); mocks.asset.streamStorageTemplateAssets.mockReturnValue(makeStream([asset]));
mocks.user.getList.mockResolvedValue([userStub.storageLabel]); mocks.user.getList.mockResolvedValue([user]);
mocks.move.create.mockResolvedValue({ mocks.move.create.mockResolvedValue({
id: '123', id: '123',
entityId: asset.id, entityId: asset.id,
pathType: AssetPathType.ORIGINAL, pathType: AssetPathType.ORIGINAL,
oldPath: 'upload/library/user-id/2022/2022-06-19/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', newPath: `upload/library/${user.id}/2023/2023-02-23/IMG_7065.heic`,
}); });
await sut.handleMigration(); await sut.handleMigration();
expect(mocks.asset.streamStorageTemplateAssets).toHaveBeenCalled(); expect(mocks.asset.streamStorageTemplateAssets).toHaveBeenCalled();
expect(mocks.storage.rename).toHaveBeenCalledWith( expect(mocks.storage.rename).toHaveBeenCalledWith(
'upload/library/user-id/2022/2022-06-19/IMG_7065.heic', `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.storageLabel}/2022/2022-06-19/IMG_7065.heic`,
); );
}); });
it('should not create double extensions when filename has uppercase extension', async () => { it('should not create double extensions when filename has uppercase extension', async () => {
const user = factory.userAdmin();
const asset = assetStub.storageAsset({ 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', originalFileName: 'IMG_7065.HEIC',
}); });
mocks.asset.streamStorageTemplateAssets.mockReturnValue(makeStream([asset])); mocks.asset.streamStorageTemplateAssets.mockReturnValue(makeStream([asset]));
mocks.user.getList.mockResolvedValue([userStub.storageLabel]); mocks.user.getList.mockResolvedValue([user]);
mocks.move.create.mockResolvedValue({ mocks.move.create.mockResolvedValue({
id: '123', id: '123',
entityId: asset.id, entityId: asset.id,
pathType: AssetPathType.ORIGINAL, pathType: AssetPathType.ORIGINAL,
oldPath: 'upload/library/user-id/2022/2022-06-19/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', newPath: `upload/library/${user.id}/2023/2023-02-23/IMG_7065.heic`,
}); });
await sut.handleMigration(); await sut.handleMigration();
expect(mocks.asset.streamStorageTemplateAssets).toHaveBeenCalled(); expect(mocks.asset.streamStorageTemplateAssets).toHaveBeenCalled();
expect(mocks.storage.rename).toHaveBeenCalledWith( expect(mocks.storage.rename).toHaveBeenCalledWith(
'upload/library/user-id/2022/2022-06-19/IMG_7065.HEIC', `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`,
); );
}); });
it('should normalize the filename to lowercase (JPEG > jpg)', async () => { it('should normalize the filename to lowercase (JPEG > jpg)', async () => {
const user = factory.userAdmin();
const asset = assetStub.storageAsset({ 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', originalFileName: 'IMG_7065.JPEG',
}); });
mocks.asset.streamStorageTemplateAssets.mockReturnValue(makeStream([asset])); mocks.asset.streamStorageTemplateAssets.mockReturnValue(makeStream([asset]));
mocks.user.getList.mockResolvedValue([userStub.storageLabel]); mocks.user.getList.mockResolvedValue([user]);
mocks.move.create.mockResolvedValue({ mocks.move.create.mockResolvedValue({
id: '123', id: '123',
entityId: asset.id, entityId: asset.id,
pathType: AssetPathType.ORIGINAL, pathType: AssetPathType.ORIGINAL,
oldPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.JPEG', oldPath: `upload/library/${user.id}/2022/2022-06-19/IMG_7065.JPEG`,
newPath: 'upload/library/user-id/2023/2023-02-23/IMG_7065.jpg', newPath: `upload/library/${user.id}/2023/2023-02-23/IMG_7065.jpg`,
}); });
await sut.handleMigration(); await sut.handleMigration();
expect(mocks.asset.streamStorageTemplateAssets).toHaveBeenCalled(); expect(mocks.asset.streamStorageTemplateAssets).toHaveBeenCalled();
expect(mocks.storage.rename).toHaveBeenCalledWith( expect(mocks.storage.rename).toHaveBeenCalledWith(
'upload/library/user-id/2022/2022-06-19/IMG_7065.JPEG', `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.jpg`,
); );
}); });
it('should normalize the filename to lowercase (JPG > jpg)', async () => { it('should normalize the filename to lowercase (JPG > jpg)', async () => {
const user = factory.userAdmin();
const asset = assetStub.storageAsset({ const asset = assetStub.storageAsset({
ownerId: user.id,
originalPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.JPG', originalPath: 'upload/library/user-id/2022/2022-06-19/IMG_7065.JPG',
originalFileName: 'IMG_7065.JPG', originalFileName: 'IMG_7065.JPG',
}); });
mocks.asset.streamStorageTemplateAssets.mockReturnValue(makeStream([asset])); mocks.asset.streamStorageTemplateAssets.mockReturnValue(makeStream([asset]));
mocks.user.getList.mockResolvedValue([userStub.storageLabel]); mocks.user.getList.mockResolvedValue([user]);
mocks.move.create.mockResolvedValue({ mocks.move.create.mockResolvedValue({
id: '123', id: '123',
entityId: asset.id, entityId: asset.id,
pathType: AssetPathType.ORIGINAL, pathType: AssetPathType.ORIGINAL,
oldPath: 'upload/library/user-id/2022/2022-06-19/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', newPath: `upload/library/${user.id}/2023/2023-02-23/IMG_7065.jpg`,
}); });
await sut.handleMigration(); await sut.handleMigration();
expect(mocks.asset.streamStorageTemplateAssets).toHaveBeenCalled(); expect(mocks.asset.streamStorageTemplateAssets).toHaveBeenCalled();
expect(mocks.storage.rename).toHaveBeenCalledWith( expect(mocks.storage.rename).toHaveBeenCalledWith(
'upload/library/user-id/2022/2022-06-19/IMG_7065.JPG', `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`,
); );
}); });
}); });

View File

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

View File

@ -29,7 +29,7 @@ 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 = factory.userAdmin();
const auth = factory.auth(user); const auth = factory.auth({ user });
mocks.user.getList.mockResolvedValue([user]); mocks.user.getList.mockResolvedValue([user]);
@ -39,14 +39,12 @@ 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 () => {
mocks.user.getList.mockResolvedValue([userStub.user1]); const user = factory.userAdmin();
const auth = factory.auth({ user });
await expect(sut.search(authStub.user1)).resolves.toEqual([ mocks.user.getList.mockResolvedValue([user]);
expect.objectContaining({
id: authStub.user1.user.id, await expect(sut.search(auth)).resolves.toEqual([expect.objectContaining({ id: user.id, email: user.email })]);
email: authStub.user1.user.email,
}),
]);
expect(mocks.user.getList).toHaveBeenCalledWith({ withDeleted: false }); 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 () => { 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;
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')); mocks.user.update.mockRejectedValue(new InternalServerErrorException('mocked error'));
await expect(sut.createProfileImage(authStub.admin, file)).rejects.toThrowError(InternalServerErrorException); await expect(sut.createProfileImage(authStub.admin, file)).rejects.toThrowError(InternalServerErrorException);
}); });
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 file = { path: '/profile/path' } as Express.Multer.File; 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 }); mocks.user.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path });
await sut.createProfileImage(authStub.admin, file); await sut.createProfileImage(authStub.admin, file);
@ -149,8 +149,10 @@ 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 () => {
mocks.user.get.mockResolvedValue(userStub.profilePath); const user = factory.userAdmin({ profileImagePath: '/path/to/profile.jpg' });
const files = [userStub.profilePath.profileImagePath]; const files = [user.profileImagePath];
mocks.user.get.mockResolvedValue(user);
await sut.deleteProfileImage(authStub.admin); await sut.deleteProfileImage(authStub.admin);
@ -176,9 +178,10 @@ describe(UserService.name, () => {
}); });
it('should return the profile picture', async () => { 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({ new ImmichFileResponse({
path: '/path/to/profile.jpg', path: '/path/to/profile.jpg',
contentType: 'image/jpeg', 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'), key: Buffer.from('shared-link-key'),
} as SharedLinkEntity, } 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, quotaSizeInBytes: null,
quotaUsageInBytes: 0, 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, ApiKey,
Asset, Asset,
AuthApiKey, AuthApiKey,
AuthSharedLink,
AuthUser, AuthUser,
Library, Library,
Memory, Memory,
@ -35,12 +36,20 @@ export const newEmbedding = () => {
const authFactory = ({ const authFactory = ({
apiKey, apiKey,
session, session,
...user sharedLink,
}: Partial<AuthUser> & { apiKey?: Partial<AuthApiKey>; session?: { id: string } } = {}) => { user,
}: {
apiKey?: Partial<AuthApiKey>;
session?: { id: string };
user?: Partial<UserAdmin>;
sharedLink?: Partial<AuthSharedLink>;
} = {}) => {
const auth: AuthDto = { const auth: AuthDto = {
user: authUserFactory(user), user: authUserFactory(userAdminFactory(user ?? {})),
}; };
const userId = auth.user.id;
if (apiKey) { if (apiKey) {
auth.apiKey = authApiKeyFactory(apiKey); auth.apiKey = authApiKeyFactory(apiKey);
} }
@ -49,24 +58,45 @@ const authFactory = ({
auth.session = { id: session.id }; auth.session = { id: session.id };
} }
if (sharedLink) {
auth.sharedLink = authSharedLinkFactory({ ...sharedLink, userId });
}
return auth; 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> = {}) => ({ const authApiKeyFactory = (apiKey: Partial<AuthApiKey> = {}) => ({
id: newUuid(), id: newUuid(),
permissions: [Permission.ALL], permissions: [Permission.ALL],
...apiKey, ...apiKey,
}); });
const authUserFactory = (authUser: Partial<AuthUser> = {}) => ({ const authUserFactory = (authUser: Partial<AuthUser> = {}) => {
id: newUuid(), const {
isAdmin: false, id = newUuid(),
name: 'Test User', isAdmin = false,
email: 'test@immich.cloud', name = 'Test User',
quotaUsageInBytes: 0, email = 'test@immich.cloud',
quotaSizeInBytes: null, quotaUsageInBytes = 0,
...authUser, quotaSizeInBytes = null,
}); } = authUser;
return { id, isAdmin, name, email, quotaUsageInBytes, quotaSizeInBytes };
};
const partnerFactory = (partner: Partial<Partner> = {}) => { const partnerFactory = (partner: Partial<Partner> = {}) => {
const sharedBy = userFactory(partner.sharedBy || {}); const sharedBy = userFactory(partner.sharedBy || {});
@ -112,25 +142,44 @@ const userFactory = (user: Partial<User> = {}) => ({
...user, ...user,
}); });
const userAdminFactory = (user: Partial<UserAdmin> = {}) => ({ const userAdminFactory = (user: Partial<UserAdmin> = {}) => {
id: newUuid(), const {
name: 'Test User', id = newUuid(),
email: 'test@immich.cloud', name = 'Test User',
profileImagePath: '', email = 'test@immich.cloud',
profileChangedAt: newDate(), profileImagePath = '',
storageLabel: null, profileChangedAt = newDate(),
shouldChangePassword: false, storageLabel = null,
isAdmin: false, shouldChangePassword = false,
createdAt: newDate(), isAdmin = false,
updatedAt: newDate(), createdAt = newDate(),
deletedAt: null, updatedAt = newDate(),
oauthId: '', deletedAt = null,
quotaSizeInBytes: null, oauthId = '',
quotaUsageInBytes: 0, quotaSizeInBytes = null,
status: UserStatus.ACTIVE, quotaUsageInBytes = 0,
metadata: [], status = UserStatus.ACTIVE,
...user, metadata = [],
}); } = user;
return {
id,
name,
email,
profileImagePath,
profileChangedAt,
storageLabel,
shouldChangePassword,
isAdmin,
createdAt,
updatedAt,
deletedAt,
oauthId,
quotaSizeInBytes,
quotaUsageInBytes,
status,
metadata,
};
};
const assetFactory = (asset: Partial<Asset> = {}) => ({ const assetFactory = (asset: Partial<Asset> = {}) => ({
id: newUuid(), id: newUuid(),