2023-01-23 23:13:42 -05:00
|
|
|
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
|
2024-02-02 04:18:00 +01:00
|
|
|
import { IncomingHttpHeaders } from 'node:http';
|
2023-09-04 15:45:59 -04:00
|
|
|
import { Issuer, generators } from 'openid-client';
|
2023-06-16 15:54:17 -04:00
|
|
|
import { Socket } from 'socket.io';
|
2024-03-20 22:15:09 -05:00
|
|
|
import { AuthType } from 'src/constants';
|
2024-03-20 23:53:07 +01:00
|
|
|
import { AuthDto, SignUpDto } from 'src/dtos/auth.dto';
|
2024-03-20 16:02:51 -05:00
|
|
|
import { UserEntity } from 'src/entities/user.entity';
|
2024-03-20 21:42:58 +01:00
|
|
|
import { IKeyRepository } from 'src/interfaces/api-key.repository';
|
|
|
|
|
import { ICryptoRepository } from 'src/interfaces/crypto.repository';
|
|
|
|
|
import { ILibraryRepository } from 'src/interfaces/library.repository';
|
|
|
|
|
import { ISharedLinkRepository } from 'src/interfaces/shared-link.repository';
|
|
|
|
|
import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
|
|
|
|
|
import { IUserTokenRepository } from 'src/interfaces/user-token.repository';
|
|
|
|
|
import { IUserRepository } from 'src/interfaces/user.repository';
|
2024-03-21 00:07:30 +01:00
|
|
|
import { AuthService } from 'src/services/auth.service';
|
2024-03-20 19:32:04 +01:00
|
|
|
import { keyStub } from 'test/fixtures/api-key.stub';
|
|
|
|
|
import { authStub, loginResponseStub } from 'test/fixtures/auth.stub';
|
|
|
|
|
import { sharedLinkStub } from 'test/fixtures/shared-link.stub';
|
|
|
|
|
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
|
|
|
|
import { userTokenStub } from 'test/fixtures/user-token.stub';
|
|
|
|
|
import { userStub } from 'test/fixtures/user.stub';
|
|
|
|
|
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
|
|
|
|
import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock';
|
|
|
|
|
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
|
|
|
|
import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock';
|
|
|
|
|
import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock';
|
|
|
|
|
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
|
|
|
|
|
import { newUserTokenRepositoryMock } from 'test/repositories/user-token.repository.mock';
|
|
|
|
|
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
|
2023-01-31 13:11:49 -05:00
|
|
|
|
|
|
|
|
// const token = Buffer.from('my-api-key', 'utf8').toString('base64');
|
2023-01-23 23:13:42 -05:00
|
|
|
|
|
|
|
|
const email = 'test@immich.com';
|
|
|
|
|
const sub = 'my-auth-user-sub';
|
2023-04-25 22:19:23 -04:00
|
|
|
const loginDetails = {
|
|
|
|
|
isSecure: true,
|
|
|
|
|
clientIp: '127.0.0.1',
|
|
|
|
|
deviceOS: '',
|
|
|
|
|
deviceType: '',
|
|
|
|
|
};
|
2023-01-23 23:13:42 -05:00
|
|
|
|
|
|
|
|
const fixtures = {
|
|
|
|
|
login: {
|
|
|
|
|
email,
|
|
|
|
|
password: 'password',
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2024-03-02 15:18:56 -05:00
|
|
|
const oauthUserWithDefaultQuota = {
|
|
|
|
|
email: email,
|
|
|
|
|
name: ' ',
|
|
|
|
|
oauthId: sub,
|
|
|
|
|
quotaSizeInBytes: 1_073_741_824,
|
|
|
|
|
storageLabel: null,
|
|
|
|
|
};
|
|
|
|
|
|
2023-01-23 23:13:42 -05:00
|
|
|
describe('AuthService', () => {
|
|
|
|
|
let sut: AuthService;
|
2023-10-30 11:48:38 -04:00
|
|
|
let accessMock: jest.Mocked<IAccessRepositoryMock>;
|
2023-01-23 23:13:42 -05:00
|
|
|
let cryptoMock: jest.Mocked<ICryptoRepository>;
|
|
|
|
|
let userMock: jest.Mocked<IUserRepository>;
|
2023-09-20 13:16:33 +02:00
|
|
|
let libraryMock: jest.Mocked<ILibraryRepository>;
|
2023-01-23 23:13:42 -05:00
|
|
|
let configMock: jest.Mocked<ISystemConfigRepository>;
|
2023-01-27 20:50:07 +00:00
|
|
|
let userTokenMock: jest.Mocked<IUserTokenRepository>;
|
2023-01-31 13:11:49 -05:00
|
|
|
let shareMock: jest.Mocked<ISharedLinkRepository>;
|
|
|
|
|
let keyMock: jest.Mocked<IKeyRepository>;
|
2023-01-23 23:13:42 -05:00
|
|
|
|
2024-03-01 19:46:07 -05:00
|
|
|
let callbackMock: jest.Mock;
|
|
|
|
|
let userinfoMock: jest.Mock;
|
2023-01-23 23:13:42 -05:00
|
|
|
|
2024-03-05 23:23:06 +01:00
|
|
|
beforeEach(() => {
|
2023-01-23 23:13:42 -05:00
|
|
|
callbackMock = jest.fn().mockReturnValue({ access_token: 'access-token' });
|
2024-03-01 19:46:07 -05:00
|
|
|
userinfoMock = jest.fn().mockResolvedValue({ sub, email });
|
2023-01-23 23:13:42 -05:00
|
|
|
|
|
|
|
|
jest.spyOn(generators, 'state').mockReturnValue('state');
|
|
|
|
|
jest.spyOn(Issuer, 'discover').mockResolvedValue({
|
2024-02-02 06:27:54 +01:00
|
|
|
id_token_signing_alg_values_supported: ['RS256'],
|
2023-01-23 23:13:42 -05:00
|
|
|
Client: jest.fn().mockResolvedValue({
|
|
|
|
|
issuer: {
|
|
|
|
|
metadata: {
|
|
|
|
|
end_session_endpoint: 'http://end-session-endpoint',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
authorizationUrl: jest.fn().mockReturnValue('http://authorization-url'),
|
|
|
|
|
callbackParams: jest.fn().mockReturnValue({ state: 'state' }),
|
|
|
|
|
callback: callbackMock,
|
2024-03-01 19:46:07 -05:00
|
|
|
userinfo: userinfoMock,
|
2023-01-23 23:13:42 -05:00
|
|
|
}),
|
|
|
|
|
} as any);
|
|
|
|
|
|
2023-10-30 11:48:38 -04:00
|
|
|
accessMock = newAccessRepositoryMock();
|
2023-01-23 23:13:42 -05:00
|
|
|
cryptoMock = newCryptoRepositoryMock();
|
|
|
|
|
userMock = newUserRepositoryMock();
|
2023-09-20 13:16:33 +02:00
|
|
|
libraryMock = newLibraryRepositoryMock();
|
2023-01-23 23:13:42 -05:00
|
|
|
configMock = newSystemConfigRepositoryMock();
|
2023-01-27 20:50:07 +00:00
|
|
|
userTokenMock = newUserTokenRepositoryMock();
|
2023-01-31 13:11:49 -05:00
|
|
|
shareMock = newSharedLinkRepositoryMock();
|
|
|
|
|
keyMock = newKeyRepositoryMock();
|
2023-01-23 23:13:42 -05:00
|
|
|
|
2023-10-30 11:48:38 -04:00
|
|
|
sut = new AuthService(accessMock, cryptoMock, configMock, libraryMock, userMock, userTokenMock, shareMock, keyMock);
|
2023-01-23 23:13:42 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should be defined', () => {
|
|
|
|
|
expect(sut).toBeDefined();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('login', () => {
|
|
|
|
|
it('should throw an error if password login is disabled', async () => {
|
2023-07-15 00:03:56 -04:00
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.disabled);
|
2023-04-25 22:19:23 -04:00
|
|
|
await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(UnauthorizedException);
|
2023-01-23 23:13:42 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should check the user exists', async () => {
|
|
|
|
|
userMock.getByEmail.mockResolvedValue(null);
|
2023-08-01 11:49:50 -04:00
|
|
|
await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(UnauthorizedException);
|
2023-01-23 23:13:42 -05:00
|
|
|
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should check the user has a password', async () => {
|
|
|
|
|
userMock.getByEmail.mockResolvedValue({} as UserEntity);
|
2023-08-01 11:49:50 -04:00
|
|
|
await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(UnauthorizedException);
|
2023-01-23 23:13:42 -05:00
|
|
|
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should successfully log the user in', async () => {
|
2023-07-31 21:28:07 -04:00
|
|
|
userMock.getByEmail.mockResolvedValue(userStub.user1);
|
|
|
|
|
userTokenMock.create.mockResolvedValue(userTokenStub.userToken);
|
2023-04-25 22:19:23 -04:00
|
|
|
await expect(sut.login(fixtures.login, loginDetails)).resolves.toEqual(loginResponseStub.user1password);
|
2023-01-23 23:13:42 -05:00
|
|
|
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should generate the cookie headers (insecure)', async () => {
|
2023-07-31 21:28:07 -04:00
|
|
|
userMock.getByEmail.mockResolvedValue(userStub.user1);
|
|
|
|
|
userTokenMock.create.mockResolvedValue(userTokenStub.userToken);
|
2023-04-25 22:19:23 -04:00
|
|
|
await expect(
|
|
|
|
|
sut.login(fixtures.login, {
|
|
|
|
|
clientIp: '127.0.0.1',
|
|
|
|
|
isSecure: false,
|
|
|
|
|
deviceOS: '',
|
|
|
|
|
deviceType: '',
|
|
|
|
|
}),
|
|
|
|
|
).resolves.toEqual(loginResponseStub.user1insecure);
|
2023-01-23 23:13:42 -05:00
|
|
|
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('changePassword', () => {
|
|
|
|
|
it('should change the password', async () => {
|
2023-12-09 23:34:12 -05:00
|
|
|
const auth = { user: { email: 'test@imimch.com' } } as AuthDto;
|
2023-01-23 23:13:42 -05:00
|
|
|
const dto = { password: 'old-password', newPassword: 'new-password' };
|
|
|
|
|
|
|
|
|
|
userMock.getByEmail.mockResolvedValue({
|
|
|
|
|
email: 'test@immich.com',
|
|
|
|
|
password: 'hash-password',
|
|
|
|
|
} as UserEntity);
|
|
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
await sut.changePassword(auth, dto);
|
2023-01-23 23:13:42 -05:00
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
expect(userMock.getByEmail).toHaveBeenCalledWith(auth.user.email, true);
|
2023-01-27 20:50:07 +00:00
|
|
|
expect(cryptoMock.compareBcrypt).toHaveBeenCalledWith('old-password', 'hash-password');
|
2023-01-23 23:13:42 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should throw when auth user email is not found', async () => {
|
2023-12-09 23:34:12 -05:00
|
|
|
const auth = { user: { email: 'test@imimch.com' } } as AuthDto;
|
2023-01-23 23:13:42 -05:00
|
|
|
const dto = { password: 'old-password', newPassword: 'new-password' };
|
|
|
|
|
|
|
|
|
|
userMock.getByEmail.mockResolvedValue(null);
|
|
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
await expect(sut.changePassword(auth, dto)).rejects.toBeInstanceOf(UnauthorizedException);
|
2023-01-23 23:13:42 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should throw when password does not match existing password', async () => {
|
2023-12-09 23:34:12 -05:00
|
|
|
const auth = { user: { email: 'test@imimch.com' } as UserEntity };
|
2023-01-23 23:13:42 -05:00
|
|
|
const dto = { password: 'old-password', newPassword: 'new-password' };
|
|
|
|
|
|
2023-01-27 20:50:07 +00:00
|
|
|
cryptoMock.compareBcrypt.mockReturnValue(false);
|
2023-01-23 23:13:42 -05:00
|
|
|
|
|
|
|
|
userMock.getByEmail.mockResolvedValue({
|
|
|
|
|
email: 'test@immich.com',
|
|
|
|
|
password: 'hash-password',
|
|
|
|
|
} as UserEntity);
|
|
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
await expect(sut.changePassword(auth, dto)).rejects.toBeInstanceOf(BadRequestException);
|
2023-01-23 23:13:42 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should throw when user does not have a password', async () => {
|
2023-12-09 23:34:12 -05:00
|
|
|
const auth = { user: { email: 'test@imimch.com' } } as AuthDto;
|
2023-01-23 23:13:42 -05:00
|
|
|
const dto = { password: 'old-password', newPassword: 'new-password' };
|
|
|
|
|
|
|
|
|
|
userMock.getByEmail.mockResolvedValue({
|
|
|
|
|
email: 'test@immich.com',
|
|
|
|
|
password: '',
|
|
|
|
|
} as UserEntity);
|
|
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
await expect(sut.changePassword(auth, dto)).rejects.toBeInstanceOf(BadRequestException);
|
2023-01-23 23:13:42 -05:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('logout', () => {
|
|
|
|
|
it('should return the end session endpoint', async () => {
|
2023-07-15 00:03:56 -04:00
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.enabled);
|
2023-12-09 23:34:12 -05:00
|
|
|
const auth = { user: { id: '123' } } as AuthDto;
|
|
|
|
|
await expect(sut.logout(auth, AuthType.OAUTH)).resolves.toEqual({
|
2023-01-23 23:13:42 -05:00
|
|
|
successful: true,
|
|
|
|
|
redirectUri: 'http://end-session-endpoint',
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return the default redirect', async () => {
|
2023-12-09 23:34:12 -05:00
|
|
|
const auth = { user: { id: '123' } } as AuthDto;
|
2023-02-05 23:31:16 -06:00
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
await expect(sut.logout(auth, AuthType.PASSWORD)).resolves.toEqual({
|
2023-01-23 23:13:42 -05:00
|
|
|
successful: true,
|
|
|
|
|
redirectUri: '/auth/login?autoLaunch=0',
|
|
|
|
|
});
|
|
|
|
|
});
|
2023-02-25 09:12:03 -05:00
|
|
|
|
|
|
|
|
it('should delete the access token', async () => {
|
2023-12-09 23:34:12 -05:00
|
|
|
const auth = { user: { id: '123' }, userToken: { id: 'token123' } } as AuthDto;
|
2023-02-25 09:12:03 -05:00
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
await expect(sut.logout(auth, AuthType.PASSWORD)).resolves.toEqual({
|
2023-02-25 09:12:03 -05:00
|
|
|
successful: true,
|
|
|
|
|
redirectUri: '/auth/login?autoLaunch=0',
|
|
|
|
|
});
|
|
|
|
|
|
2023-10-30 11:48:38 -04:00
|
|
|
expect(userTokenMock.delete).toHaveBeenCalledWith('token123');
|
2023-02-25 09:12:03 -05:00
|
|
|
});
|
2023-09-11 17:56:38 +02:00
|
|
|
|
|
|
|
|
it('should return the default redirect if auth type is OAUTH but oauth is not enabled', async () => {
|
2023-12-09 23:34:12 -05:00
|
|
|
const auth = { user: { id: '123' } } as AuthDto;
|
2023-09-11 17:56:38 +02:00
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
await expect(sut.logout(auth, AuthType.OAUTH)).resolves.toEqual({
|
2023-09-11 17:56:38 +02:00
|
|
|
successful: true,
|
|
|
|
|
redirectUri: '/auth/login?autoLaunch=0',
|
|
|
|
|
});
|
|
|
|
|
});
|
2023-01-23 23:13:42 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('adminSignUp', () => {
|
2023-11-11 20:03:32 -05:00
|
|
|
const dto: SignUpDto = { email: 'test@immich.com', password: 'password', name: 'immich admin' };
|
2023-01-23 23:13:42 -05:00
|
|
|
|
|
|
|
|
it('should only allow one admin', async () => {
|
|
|
|
|
userMock.getAdmin.mockResolvedValue({} as UserEntity);
|
|
|
|
|
await expect(sut.adminSignUp(dto)).rejects.toBeInstanceOf(BadRequestException);
|
|
|
|
|
expect(userMock.getAdmin).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should sign up the admin', async () => {
|
|
|
|
|
userMock.getAdmin.mockResolvedValue(null);
|
2023-05-30 15:15:56 +02:00
|
|
|
userMock.create.mockResolvedValue({ ...dto, id: 'admin', createdAt: new Date('2021-01-01') } as UserEntity);
|
2023-01-23 23:13:42 -05:00
|
|
|
await expect(sut.adminSignUp(dto)).resolves.toEqual({
|
2023-11-14 04:10:35 +01:00
|
|
|
avatarColor: expect.any(String),
|
2023-01-23 23:13:42 -05:00
|
|
|
id: 'admin',
|
2023-05-30 15:15:56 +02:00
|
|
|
createdAt: new Date('2021-01-01'),
|
2023-01-23 23:13:42 -05:00
|
|
|
email: 'test@immich.com',
|
2023-11-11 20:03:32 -05:00
|
|
|
name: 'immich admin',
|
2023-01-23 23:13:42 -05:00
|
|
|
});
|
|
|
|
|
expect(userMock.getAdmin).toHaveBeenCalled();
|
|
|
|
|
expect(userMock.create).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2023-01-27 20:50:07 +00:00
|
|
|
describe('validate - socket connections', () => {
|
2023-01-31 13:11:49 -05:00
|
|
|
it('should throw token is not provided', async () => {
|
|
|
|
|
await expect(sut.validate({}, {})).rejects.toBeInstanceOf(UnauthorizedException);
|
|
|
|
|
});
|
|
|
|
|
|
2023-01-23 23:13:42 -05:00
|
|
|
it('should validate using authorization header', async () => {
|
2023-07-31 21:28:07 -04:00
|
|
|
userMock.get.mockResolvedValue(userStub.user1);
|
|
|
|
|
userTokenMock.getByToken.mockResolvedValue(userTokenStub.userToken);
|
2023-01-27 20:50:07 +00:00
|
|
|
const client = { request: { headers: { authorization: 'Bearer auth_token' } } };
|
2023-12-09 23:34:12 -05:00
|
|
|
await expect(sut.validate((client as Socket).request.headers, {})).resolves.toEqual({
|
|
|
|
|
user: userStub.user1,
|
|
|
|
|
userToken: userTokenStub.userToken,
|
|
|
|
|
});
|
2023-01-23 23:13:42 -05:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2023-01-31 13:11:49 -05:00
|
|
|
describe('validate - shared key', () => {
|
|
|
|
|
it('should not accept a non-existent key', async () => {
|
|
|
|
|
shareMock.getByKey.mockResolvedValue(null);
|
|
|
|
|
const headers: IncomingHttpHeaders = { 'x-immich-share-key': 'key' };
|
|
|
|
|
await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException);
|
2023-01-23 23:13:42 -05:00
|
|
|
});
|
|
|
|
|
|
2023-01-31 13:11:49 -05:00
|
|
|
it('should not accept an expired key', async () => {
|
|
|
|
|
shareMock.getByKey.mockResolvedValue(sharedLinkStub.expired);
|
|
|
|
|
const headers: IncomingHttpHeaders = { 'x-immich-share-key': 'key' };
|
|
|
|
|
await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException);
|
2023-01-23 23:13:42 -05:00
|
|
|
});
|
|
|
|
|
|
2023-01-31 13:11:49 -05:00
|
|
|
it('should not accept a key without a user', async () => {
|
|
|
|
|
shareMock.getByKey.mockResolvedValue(sharedLinkStub.expired);
|
|
|
|
|
userMock.get.mockResolvedValue(null);
|
|
|
|
|
const headers: IncomingHttpHeaders = { 'x-immich-share-key': 'key' };
|
|
|
|
|
await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException);
|
2023-01-23 23:13:42 -05:00
|
|
|
});
|
|
|
|
|
|
2023-06-01 16:56:37 -04:00
|
|
|
it('should accept a base64url key', async () => {
|
2023-01-31 13:11:49 -05:00
|
|
|
shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid);
|
2023-07-31 21:28:07 -04:00
|
|
|
userMock.get.mockResolvedValue(userStub.admin);
|
2023-06-01 16:56:37 -04:00
|
|
|
const headers: IncomingHttpHeaders = { 'x-immich-share-key': sharedLinkStub.valid.key.toString('base64url') };
|
2023-12-09 23:34:12 -05:00
|
|
|
await expect(sut.validate(headers, {})).resolves.toEqual({
|
|
|
|
|
user: userStub.admin,
|
|
|
|
|
sharedLink: sharedLinkStub.valid,
|
|
|
|
|
});
|
2023-06-01 16:56:37 -04:00
|
|
|
expect(shareMock.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should accept a hex key', async () => {
|
|
|
|
|
shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid);
|
2023-07-31 21:28:07 -04:00
|
|
|
userMock.get.mockResolvedValue(userStub.admin);
|
2023-06-01 16:56:37 -04:00
|
|
|
const headers: IncomingHttpHeaders = { 'x-immich-share-key': sharedLinkStub.valid.key.toString('hex') };
|
2023-12-09 23:34:12 -05:00
|
|
|
await expect(sut.validate(headers, {})).resolves.toEqual({
|
|
|
|
|
user: userStub.admin,
|
|
|
|
|
sharedLink: sharedLinkStub.valid,
|
|
|
|
|
});
|
2023-06-01 16:56:37 -04:00
|
|
|
expect(shareMock.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key);
|
2023-01-23 23:13:42 -05:00
|
|
|
});
|
2023-01-31 13:11:49 -05:00
|
|
|
});
|
2023-01-23 23:13:42 -05:00
|
|
|
|
2023-01-31 13:11:49 -05:00
|
|
|
describe('validate - user token', () => {
|
|
|
|
|
it('should throw if no token is found', async () => {
|
2023-04-25 22:19:23 -04:00
|
|
|
userTokenMock.getByToken.mockResolvedValue(null);
|
2023-01-31 13:11:49 -05:00
|
|
|
const headers: IncomingHttpHeaders = { 'x-immich-user-token': 'auth_token' };
|
|
|
|
|
await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException);
|
2023-01-23 23:13:42 -05:00
|
|
|
});
|
|
|
|
|
|
2023-01-31 13:11:49 -05:00
|
|
|
it('should return an auth dto', async () => {
|
2023-07-31 21:28:07 -04:00
|
|
|
userTokenMock.getByToken.mockResolvedValue(userTokenStub.userToken);
|
2023-01-31 13:11:49 -05:00
|
|
|
const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' };
|
2023-12-09 23:34:12 -05:00
|
|
|
await expect(sut.validate(headers, {})).resolves.toEqual({
|
|
|
|
|
user: userStub.user1,
|
|
|
|
|
userToken: userTokenStub.userToken,
|
|
|
|
|
});
|
2023-01-23 23:13:42 -05:00
|
|
|
});
|
2023-04-25 22:19:23 -04:00
|
|
|
|
|
|
|
|
it('should update when access time exceeds an hour', async () => {
|
2023-07-31 21:28:07 -04:00
|
|
|
userTokenMock.getByToken.mockResolvedValue(userTokenStub.inactiveToken);
|
|
|
|
|
userTokenMock.save.mockResolvedValue(userTokenStub.userToken);
|
2023-04-25 22:19:23 -04:00
|
|
|
const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' };
|
2023-12-09 23:34:12 -05:00
|
|
|
await expect(sut.validate(headers, {})).resolves.toEqual({
|
|
|
|
|
user: userStub.user1,
|
|
|
|
|
userToken: userTokenStub.userToken,
|
|
|
|
|
});
|
2023-04-25 22:19:23 -04:00
|
|
|
expect(userTokenMock.save.mock.calls[0][0]).toMatchObject({
|
|
|
|
|
id: 'not_active',
|
|
|
|
|
token: 'auth_token',
|
2023-05-21 23:18:10 -04:00
|
|
|
userId: 'user-id',
|
2023-04-25 22:19:23 -04:00
|
|
|
createdAt: new Date('2021-01-01'),
|
|
|
|
|
updatedAt: expect.any(Date),
|
|
|
|
|
deviceOS: 'Android',
|
|
|
|
|
deviceType: 'Mobile',
|
|
|
|
|
});
|
|
|
|
|
});
|
2023-01-31 13:11:49 -05:00
|
|
|
});
|
2023-01-23 23:13:42 -05:00
|
|
|
|
2023-01-31 13:11:49 -05:00
|
|
|
describe('validate - api key', () => {
|
|
|
|
|
it('should throw an error if no api key is found', async () => {
|
|
|
|
|
keyMock.getKey.mockResolvedValue(null);
|
|
|
|
|
const headers: IncomingHttpHeaders = { 'x-api-key': 'auth_token' };
|
|
|
|
|
await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException);
|
|
|
|
|
expect(keyMock.getKey).toHaveBeenCalledWith('auth_token (hashed)');
|
2023-01-23 23:13:42 -05:00
|
|
|
});
|
|
|
|
|
|
2023-01-31 13:11:49 -05:00
|
|
|
it('should return an auth dto', async () => {
|
|
|
|
|
keyMock.getKey.mockResolvedValue(keyStub.admin);
|
|
|
|
|
const headers: IncomingHttpHeaders = { 'x-api-key': 'auth_token' };
|
2023-12-09 23:34:12 -05:00
|
|
|
await expect(sut.validate(headers, {})).resolves.toEqual({ user: userStub.admin, apiKey: keyStub.admin });
|
2023-01-31 13:11:49 -05:00
|
|
|
expect(keyMock.getKey).toHaveBeenCalledWith('auth_token (hashed)');
|
2023-01-23 23:13:42 -05:00
|
|
|
});
|
|
|
|
|
});
|
2023-04-25 22:19:23 -04:00
|
|
|
|
|
|
|
|
describe('getDevices', () => {
|
|
|
|
|
it('should get the devices', async () => {
|
2023-07-31 21:28:07 -04:00
|
|
|
userTokenMock.getAll.mockResolvedValue([userTokenStub.userToken, userTokenStub.inactiveToken]);
|
2023-04-25 22:19:23 -04:00
|
|
|
await expect(sut.getDevices(authStub.user1)).resolves.toEqual([
|
|
|
|
|
{
|
|
|
|
|
createdAt: '2021-01-01T00:00:00.000Z',
|
|
|
|
|
current: true,
|
|
|
|
|
deviceOS: '',
|
|
|
|
|
deviceType: '',
|
|
|
|
|
id: 'token-id',
|
|
|
|
|
updatedAt: expect.any(String),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
createdAt: '2021-01-01T00:00:00.000Z',
|
|
|
|
|
current: false,
|
|
|
|
|
deviceOS: 'Android',
|
|
|
|
|
deviceType: 'Mobile',
|
|
|
|
|
id: 'not_active',
|
|
|
|
|
updatedAt: expect.any(String),
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
expect(userTokenMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
|
2023-04-25 22:19:23 -04:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2023-05-09 15:34:17 -04:00
|
|
|
describe('logoutDevices', () => {
|
|
|
|
|
it('should logout all devices', async () => {
|
2023-07-31 21:28:07 -04:00
|
|
|
userTokenMock.getAll.mockResolvedValue([userTokenStub.inactiveToken, userTokenStub.userToken]);
|
2023-05-09 15:34:17 -04:00
|
|
|
|
|
|
|
|
await sut.logoutDevices(authStub.user1);
|
|
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
expect(userTokenMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
|
2023-10-30 11:48:38 -04:00
|
|
|
expect(userTokenMock.delete).toHaveBeenCalledWith('not_active');
|
|
|
|
|
expect(userTokenMock.delete).not.toHaveBeenCalledWith('token-id');
|
2023-05-09 15:34:17 -04:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2023-04-25 22:19:23 -04:00
|
|
|
describe('logoutDevice', () => {
|
|
|
|
|
it('should logout the device', async () => {
|
chore(server): Check more permissions in bulk (#5315)
Modify Access repository, to evaluate `authDevice`, `library`, `partner`,
`person`, and `timeline` permissions in bulk.
Queries have been validated to match what they currently generate for
single ids.
As an extra performance improvement, we now use a custom QueryBuilder
for the Partners queries, to avoid the eager relationships that add
unneeded `LEFT JOIN` clauses. We only filter based on the ids present in
the `partners` table, so those joins can be avoided.
Queries:
* `library` owner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "libraries" "LibraryEntity"
WHERE
"LibraryEntity"."id" = $1
AND "LibraryEntity"."ownerId" = $2
AND "LibraryEntity"."deletedAt" IS NULL
)
LIMIT 1
-- After
SELECT "LibraryEntity"."id" AS "LibraryEntity_id"
FROM "libraries" "LibraryEntity"
WHERE
"LibraryEntity"."id" IN ($1, $2)
AND "LibraryEntity"."ownerId" = $3
AND "LibraryEntity"."deletedAt" IS NULL
```
* `library` partner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "partners" "PartnerEntity"
LEFT JOIN "users" "PartnerEntity__sharedBy"
ON "PartnerEntity__sharedBy"."id"="PartnerEntity"."sharedById"
AND "PartnerEntity__sharedBy"."deletedAt" IS NULL
LEFT JOIN "users" "PartnerEntity__sharedWith"
ON "PartnerEntity__sharedWith"."id"="PartnerEntity"."sharedWithId"
AND "PartnerEntity__sharedWith"."deletedAt" IS NULL
WHERE
"PartnerEntity"."sharedWithId" = $1
AND "PartnerEntity"."sharedById" = $2
)
LIMIT 1
-- After
SELECT
"partner"."sharedById" AS "partner_sharedById",
"partner"."sharedWithId" AS "partner_sharedWithId"
FROM "partners" "partner"
WHERE
"partner"."sharedById" IN ($1, $2)
AND "partner"."sharedWithId" = $3
```
* `authDevice` owner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "user_token" "UserTokenEntity"
WHERE
"UserTokenEntity"."userId" = $1
AND "UserTokenEntity"."id" = $2
)
LIMIT 1
-- After
SELECT "UserTokenEntity"."id" AS "UserTokenEntity_id"
FROM "user_token" "UserTokenEntity"
WHERE
"UserTokenEntity"."userId" = $1
AND "UserTokenEntity"."id" IN ($2, $3)
```
* `timeline` partner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "partners" "PartnerEntity"
LEFT JOIN "users" "PartnerEntity__sharedBy"
ON "PartnerEntity__sharedBy"."id"="PartnerEntity"."sharedById"
AND "PartnerEntity__sharedBy"."deletedAt" IS NULL
LEFT JOIN "users" "PartnerEntity__sharedWith"
ON "PartnerEntity__sharedWith"."id"="PartnerEntity"."sharedWithId"
AND "PartnerEntity__sharedWith"."deletedAt" IS NULL
WHERE
"PartnerEntity"."sharedWithId" = $1
AND "PartnerEntity"."sharedById" = $2
)
LIMIT 1
-- After
SELECT
"partner"."sharedById" AS "partner_sharedById",
"partner"."sharedWithId" AS "partner_sharedWithId"
FROM "partners" "partner"
WHERE
"partner"."sharedById" IN ($1, $2)
AND "partner"."sharedWithId" = $3
```
* `person` owner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "person" "PersonEntity"
WHERE
"PersonEntity"."id" = $1
AND "PersonEntity"."ownerId" = $2
)
LIMIT 1
-- After
SELECT "PersonEntity"."id" AS "PersonEntity_id"
FROM "person" "PersonEntity"
WHERE
"PersonEntity"."id" IN ($1, $2)
AND "PersonEntity"."ownerId" = $3
```
* `partner` update access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "partners" "PartnerEntity"
LEFT JOIN "users" "PartnerEntity__sharedBy"
ON "PartnerEntity__sharedBy"."id"="PartnerEntity"."sharedById"
AND "PartnerEntity__sharedBy"."deletedAt" IS NULL
LEFT JOIN "users" "PartnerEntity__sharedWith"
ON "PartnerEntity__sharedWith"."id"="PartnerEntity"."sharedWithId"
AND "PartnerEntity__sharedWith"."deletedAt" IS NULL
WHERE
"PartnerEntity"."sharedWithId" = $1
AND "PartnerEntity"."sharedById" = $2
)
LIMIT 1
-- After
SELECT
"partner"."sharedById" AS "partner_sharedById",
"partner"."sharedWithId" AS "partner_sharedWithId"
FROM "partners" "partner"
WHERE
"partner"."sharedById" IN ($1, $2)
AND "partner"."sharedWithId" = $3
```
2023-11-26 07:50:41 -05:00
|
|
|
accessMock.authDevice.checkOwnerAccess.mockResolvedValue(new Set(['token-1']));
|
2023-10-30 11:48:38 -04:00
|
|
|
|
2023-04-25 22:19:23 -04:00
|
|
|
await sut.logoutDevice(authStub.user1, 'token-1');
|
|
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
expect(accessMock.authDevice.checkOwnerAccess).toHaveBeenCalledWith(authStub.user1.user.id, new Set(['token-1']));
|
2023-10-30 11:48:38 -04:00
|
|
|
expect(userTokenMock.delete).toHaveBeenCalledWith('token-1');
|
2023-04-25 22:19:23 -04:00
|
|
|
});
|
|
|
|
|
});
|
2023-07-15 00:03:56 -04:00
|
|
|
|
|
|
|
|
describe('getMobileRedirect', () => {
|
|
|
|
|
it('should pass along the query params', () => {
|
|
|
|
|
expect(sut.getMobileRedirect('http://immich.app?code=123&state=456')).toEqual('app.immich:/?code=123&state=456');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should work if called without query params', () => {
|
|
|
|
|
expect(sut.getMobileRedirect('http://immich.app')).toEqual('app.immich:/?');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('callback', () => {
|
|
|
|
|
it('should throw an error if OAuth is not enabled', async () => {
|
|
|
|
|
await expect(sut.callback({ url: '' }, loginDetails)).rejects.toBeInstanceOf(BadRequestException);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not allow auto registering', async () => {
|
|
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.noAutoRegister);
|
|
|
|
|
userMock.getByEmail.mockResolvedValue(null);
|
|
|
|
|
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).rejects.toBeInstanceOf(
|
|
|
|
|
BadRequestException,
|
|
|
|
|
);
|
|
|
|
|
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should link an existing user', async () => {
|
|
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.noAutoRegister);
|
2023-07-31 21:28:07 -04:00
|
|
|
userMock.getByEmail.mockResolvedValue(userStub.user1);
|
|
|
|
|
userMock.update.mockResolvedValue(userStub.user1);
|
|
|
|
|
userTokenMock.create.mockResolvedValue(userTokenStub.userToken);
|
2023-07-15 00:03:56 -04:00
|
|
|
|
|
|
|
|
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
|
|
|
|
loginResponseStub.user1oauth,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
|
2023-07-31 21:28:07 -04:00
|
|
|
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { oauthId: sub });
|
2023-07-15 00:03:56 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should allow auto registering by default', async () => {
|
|
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.enabled);
|
|
|
|
|
userMock.getByEmail.mockResolvedValue(null);
|
2023-07-31 21:28:07 -04:00
|
|
|
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
|
|
|
|
userMock.create.mockResolvedValue(userStub.user1);
|
|
|
|
|
userTokenMock.create.mockResolvedValue(userTokenStub.userToken);
|
2023-07-15 00:03:56 -04:00
|
|
|
|
|
|
|
|
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
|
|
|
|
loginResponseStub.user1oauth,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(userMock.getByEmail).toHaveBeenCalledTimes(2); // second call is for domain check before create
|
|
|
|
|
expect(userMock.create).toHaveBeenCalledTimes(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should use the mobile redirect override', async () => {
|
|
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.override);
|
2023-07-31 21:28:07 -04:00
|
|
|
userMock.getByOAuthId.mockResolvedValue(userStub.user1);
|
|
|
|
|
userTokenMock.create.mockResolvedValue(userTokenStub.userToken);
|
2023-07-15 00:03:56 -04:00
|
|
|
|
|
|
|
|
await sut.callback({ url: `app.immich:/?code=abc123` }, loginDetails);
|
|
|
|
|
|
|
|
|
|
expect(callbackMock).toHaveBeenCalledWith('http://mobile-redirect', { state: 'state' }, { state: 'state' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should use the mobile redirect override for ios urls with multiple slashes', async () => {
|
|
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.override);
|
2023-07-31 21:28:07 -04:00
|
|
|
userMock.getByOAuthId.mockResolvedValue(userStub.user1);
|
|
|
|
|
userTokenMock.create.mockResolvedValue(userTokenStub.userToken);
|
2023-07-15 00:03:56 -04:00
|
|
|
|
|
|
|
|
await sut.callback({ url: `app.immich:///?code=abc123` }, loginDetails);
|
|
|
|
|
|
|
|
|
|
expect(callbackMock).toHaveBeenCalledWith('http://mobile-redirect', { state: 'state' }, { state: 'state' });
|
|
|
|
|
});
|
2024-03-01 19:46:07 -05:00
|
|
|
|
|
|
|
|
it('should use the default quota', async () => {
|
|
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
|
|
|
|
|
userMock.getByEmail.mockResolvedValue(null);
|
|
|
|
|
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
|
|
|
|
userMock.create.mockResolvedValue(userStub.user1);
|
|
|
|
|
|
|
|
|
|
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
|
|
|
|
loginResponseStub.user1oauth,
|
|
|
|
|
);
|
|
|
|
|
|
2024-03-02 15:18:56 -05:00
|
|
|
expect(userMock.create).toHaveBeenCalledWith(oauthUserWithDefaultQuota);
|
2024-03-01 19:46:07 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should ignore an invalid storage quota', async () => {
|
|
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
|
|
|
|
|
userMock.getByEmail.mockResolvedValue(null);
|
|
|
|
|
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
|
|
|
|
userMock.create.mockResolvedValue(userStub.user1);
|
|
|
|
|
userinfoMock.mockResolvedValue({ sub, email, immich_quota: 'abc' });
|
|
|
|
|
|
|
|
|
|
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
|
|
|
|
loginResponseStub.user1oauth,
|
|
|
|
|
);
|
|
|
|
|
|
2024-03-02 15:18:56 -05:00
|
|
|
expect(userMock.create).toHaveBeenCalledWith(oauthUserWithDefaultQuota);
|
2024-03-01 19:46:07 -05:00
|
|
|
});
|
2024-03-02 15:18:56 -05:00
|
|
|
|
2024-03-01 19:46:07 -05:00
|
|
|
it('should ignore a negative quota', async () => {
|
|
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
|
|
|
|
|
userMock.getByEmail.mockResolvedValue(null);
|
|
|
|
|
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
|
|
|
|
userMock.create.mockResolvedValue(userStub.user1);
|
|
|
|
|
userinfoMock.mockResolvedValue({ sub, email, immich_quota: -5 });
|
|
|
|
|
|
|
|
|
|
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
|
|
|
|
loginResponseStub.user1oauth,
|
|
|
|
|
);
|
|
|
|
|
|
2024-03-02 15:18:56 -05:00
|
|
|
expect(userMock.create).toHaveBeenCalledWith(oauthUserWithDefaultQuota);
|
2024-03-01 19:46:07 -05:00
|
|
|
});
|
|
|
|
|
|
2024-03-02 15:18:56 -05:00
|
|
|
it('should not set quota for 0 quota', async () => {
|
2024-03-01 19:46:07 -05:00
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
|
|
|
|
|
userMock.getByEmail.mockResolvedValue(null);
|
|
|
|
|
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
|
|
|
|
userMock.create.mockResolvedValue(userStub.user1);
|
|
|
|
|
userinfoMock.mockResolvedValue({ sub, email, immich_quota: 0 });
|
|
|
|
|
|
|
|
|
|
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
|
|
|
|
loginResponseStub.user1oauth,
|
|
|
|
|
);
|
|
|
|
|
|
2024-03-02 15:18:56 -05:00
|
|
|
expect(userMock.create).toHaveBeenCalledWith({
|
|
|
|
|
email: email,
|
|
|
|
|
name: ' ',
|
|
|
|
|
oauthId: sub,
|
|
|
|
|
quotaSizeInBytes: null,
|
|
|
|
|
storageLabel: null,
|
|
|
|
|
});
|
2024-03-01 19:46:07 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should use a valid storage quota', async () => {
|
|
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
|
|
|
|
|
userMock.getByEmail.mockResolvedValue(null);
|
|
|
|
|
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
|
|
|
|
userMock.create.mockResolvedValue(userStub.user1);
|
|
|
|
|
userinfoMock.mockResolvedValue({ sub, email, immich_quota: 5 });
|
|
|
|
|
|
|
|
|
|
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
|
|
|
|
loginResponseStub.user1oauth,
|
|
|
|
|
);
|
|
|
|
|
|
2024-03-02 15:18:56 -05:00
|
|
|
expect(userMock.create).toHaveBeenCalledWith({
|
|
|
|
|
email: email,
|
|
|
|
|
name: ' ',
|
|
|
|
|
oauthId: sub,
|
|
|
|
|
quotaSizeInBytes: 5_368_709_120,
|
|
|
|
|
storageLabel: null,
|
|
|
|
|
});
|
2024-03-01 19:46:07 -05:00
|
|
|
});
|
2023-07-15 00:03:56 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('link', () => {
|
|
|
|
|
it('should link an account', async () => {
|
|
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.enabled);
|
2023-07-31 21:28:07 -04:00
|
|
|
userMock.update.mockResolvedValue(userStub.user1);
|
2023-07-15 00:03:56 -04:00
|
|
|
|
|
|
|
|
await sut.link(authStub.user1, { url: 'http://immich/user-settings?code=abc123' });
|
|
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
expect(userMock.update).toHaveBeenCalledWith(authStub.user1.user.id, { oauthId: sub });
|
2023-07-15 00:03:56 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not link an already linked oauth.sub', async () => {
|
|
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.enabled);
|
|
|
|
|
userMock.getByOAuthId.mockResolvedValue({ id: 'other-user' } as UserEntity);
|
|
|
|
|
|
|
|
|
|
await expect(sut.link(authStub.user1, { url: 'http://immich/user-settings?code=abc123' })).rejects.toBeInstanceOf(
|
|
|
|
|
BadRequestException,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(userMock.update).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('unlink', () => {
|
|
|
|
|
it('should unlink an account', async () => {
|
|
|
|
|
configMock.load.mockResolvedValue(systemConfigStub.enabled);
|
2023-07-31 21:28:07 -04:00
|
|
|
userMock.update.mockResolvedValue(userStub.user1);
|
2023-07-15 00:03:56 -04:00
|
|
|
|
|
|
|
|
await sut.unlink(authStub.user1);
|
|
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
expect(userMock.update).toHaveBeenCalledWith(authStub.user1.user.id, { oauthId: '' });
|
2023-07-15 00:03:56 -04:00
|
|
|
});
|
|
|
|
|
});
|
2023-01-23 23:13:42 -05:00
|
|
|
});
|