mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
Make user business logic reusable (#1114)
- Refactor user business logic from `user.service` into `user.domain` Make user business logic reusable by using `user.domain` from other services than `user.service` - Add `jest-when` lib to make testing easier and use it in `userService` Using when helps from coupling tests to order of mock implementations execution - Move all user business logic from user-repository to user.service - Fix user.service tests not awaiting promises leaking state between tests - Presentation logic for `getUserProfileImage` moved from UserService to UserController - Fix `user.e2e` test logic. Pending fixing the configuration of the test itself
This commit is contained in:
parent
bf921a41f9
commit
20c5578470
20 changed files with 577 additions and 972 deletions
|
|
@ -1,26 +1,30 @@
|
|||
import { UserEntity } from '@app/database/entities/user.entity';
|
||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { BadRequestException, ForbiddenException, NotFoundException } from '@nestjs/common';
|
||||
import { newUserRepositoryMock } from '../../../test/test-utils';
|
||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||
import { IUserRepository } from './user-repository';
|
||||
import { when } from 'jest-when';
|
||||
import { UserService } from './user.service';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
|
||||
describe('UserService', () => {
|
||||
let sui: UserService;
|
||||
let sut: UserService;
|
||||
let userRepositoryMock: jest.Mocked<IUserRepository>;
|
||||
|
||||
const adminAuthUser: AuthUserDto = Object.freeze({
|
||||
const adminUserAuth: AuthUserDto = Object.freeze({
|
||||
id: 'admin_id',
|
||||
email: 'admin@test.com',
|
||||
isAdmin: true,
|
||||
});
|
||||
|
||||
const immichAuthUser: AuthUserDto = Object.freeze({
|
||||
const immichUserAuth: AuthUserDto = Object.freeze({
|
||||
id: 'immich_id',
|
||||
email: 'immich@test.com',
|
||||
isAdmin: false,
|
||||
});
|
||||
|
||||
const adminUser: UserEntity = {
|
||||
id: 'admin_id',
|
||||
const adminUser: UserEntity = Object.freeze({
|
||||
id: adminUserAuth.id,
|
||||
email: 'admin@test.com',
|
||||
password: 'admin_password',
|
||||
salt: 'admin_salt',
|
||||
|
|
@ -32,10 +36,10 @@ describe('UserService', () => {
|
|||
profileImagePath: '',
|
||||
createdAt: '2021-01-01',
|
||||
tags: [],
|
||||
};
|
||||
});
|
||||
|
||||
const immichUser: UserEntity = {
|
||||
id: 'immich_id',
|
||||
const immichUser: UserEntity = Object.freeze({
|
||||
id: immichUserAuth.id,
|
||||
email: 'immich@test.com',
|
||||
password: 'immich_password',
|
||||
salt: 'immich_salt',
|
||||
|
|
@ -47,10 +51,10 @@ describe('UserService', () => {
|
|||
profileImagePath: '',
|
||||
createdAt: '2021-01-01',
|
||||
tags: [],
|
||||
};
|
||||
});
|
||||
|
||||
const updatedImmichUser: UserEntity = {
|
||||
id: 'immich_id',
|
||||
const updatedImmichUser: UserEntity = Object.freeze({
|
||||
id: immichUserAuth.id,
|
||||
email: 'immich@test.com',
|
||||
password: 'immich_password',
|
||||
salt: 'immich_salt',
|
||||
|
|
@ -62,87 +66,92 @@ describe('UserService', () => {
|
|||
profileImagePath: '',
|
||||
createdAt: '2021-01-01',
|
||||
tags: [],
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
userRepositoryMock = newUserRepositoryMock();
|
||||
|
||||
sui = new UserService(userRepositoryMock);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(sui).toBeDefined();
|
||||
beforeEach(() => {
|
||||
userRepositoryMock = newUserRepositoryMock();
|
||||
when(userRepositoryMock.get).calledWith(adminUser.id).mockResolvedValue(adminUser);
|
||||
when(userRepositoryMock.get).calledWith(adminUser.id, undefined).mockResolvedValue(adminUser);
|
||||
when(userRepositoryMock.get).calledWith(immichUser.id, undefined).mockResolvedValue(immichUser);
|
||||
|
||||
sut = new UserService(userRepositoryMock);
|
||||
});
|
||||
|
||||
describe('Update user', () => {
|
||||
it('should update user', async () => {
|
||||
const requestor = immichAuthUser;
|
||||
const userToUpdate = immichUser;
|
||||
|
||||
userRepositoryMock.get.mockImplementationOnce(() => Promise.resolve(immichUser));
|
||||
userRepositoryMock.get.mockImplementationOnce(() => Promise.resolve(userToUpdate));
|
||||
userRepositoryMock.update.mockImplementationOnce(() => Promise.resolve(updatedImmichUser));
|
||||
|
||||
const result = await sui.updateUser(requestor, {
|
||||
id: userToUpdate.id,
|
||||
const update: UpdateUserDto = {
|
||||
id: immichUser.id,
|
||||
shouldChangePassword: true,
|
||||
});
|
||||
expect(result.shouldChangePassword).toEqual(true);
|
||||
};
|
||||
|
||||
when(userRepositoryMock.update).calledWith(update.id, update).mockResolvedValueOnce(updatedImmichUser);
|
||||
|
||||
const updatedUser = await sut.updateUser(immichUserAuth, update);
|
||||
expect(updatedUser.shouldChangePassword).toEqual(true);
|
||||
});
|
||||
|
||||
it('user can only update its information', () => {
|
||||
const requestor = immichAuthUser;
|
||||
it('user can only update its information', async () => {
|
||||
when(userRepositoryMock.get)
|
||||
.calledWith('not_immich_auth_user_id', undefined)
|
||||
.mockResolvedValueOnce({
|
||||
...immichUser,
|
||||
id: 'not_immich_auth_user_id',
|
||||
});
|
||||
|
||||
userRepositoryMock.get.mockImplementationOnce(() => Promise.resolve(immichUser));
|
||||
|
||||
const result = sui.updateUser(requestor, {
|
||||
const result = sut.updateUser(immichUserAuth, {
|
||||
id: 'not_immich_auth_user_id',
|
||||
password: 'I take over your account now',
|
||||
});
|
||||
expect(result).rejects.toBeInstanceOf(BadRequestException);
|
||||
await expect(result).rejects.toBeInstanceOf(ForbiddenException);
|
||||
});
|
||||
|
||||
it('admin can update any user information', async () => {
|
||||
const requestor = adminAuthUser;
|
||||
const userToUpdate = immichUser;
|
||||
|
||||
userRepositoryMock.get.mockImplementationOnce(() => Promise.resolve(adminUser));
|
||||
userRepositoryMock.get.mockImplementationOnce(() => Promise.resolve(userToUpdate));
|
||||
userRepositoryMock.update.mockImplementationOnce(() => Promise.resolve(updatedImmichUser));
|
||||
|
||||
const result = await sui.updateUser(requestor, {
|
||||
id: userToUpdate.id,
|
||||
const update: UpdateUserDto = {
|
||||
id: immichUser.id,
|
||||
shouldChangePassword: true,
|
||||
});
|
||||
};
|
||||
|
||||
when(userRepositoryMock.update).calledWith(immichUser.id, update).mockResolvedValueOnce(updatedImmichUser);
|
||||
|
||||
const result = await sut.updateUser(adminUserAuth, update);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.id).toEqual(updatedImmichUser.id);
|
||||
expect(result.shouldChangePassword).toEqual(updatedImmichUser.shouldChangePassword);
|
||||
});
|
||||
|
||||
it('update user information should throw error if user not found', () => {
|
||||
const requestor = adminAuthUser;
|
||||
const userToUpdate = immichUser;
|
||||
it('update user information should throw error if user not found', async () => {
|
||||
when(userRepositoryMock.get).calledWith(immichUser.id, undefined).mockResolvedValueOnce(null);
|
||||
|
||||
userRepositoryMock.get.mockImplementationOnce(() => Promise.resolve(adminUser));
|
||||
userRepositoryMock.get.mockImplementationOnce(() => Promise.resolve(null));
|
||||
|
||||
const result = sui.updateUser(requestor, {
|
||||
id: userToUpdate.id,
|
||||
const result = sut.updateUser(adminUser, {
|
||||
id: immichUser.id,
|
||||
shouldChangePassword: true,
|
||||
});
|
||||
expect(result).rejects.toBeInstanceOf(NotFoundException);
|
||||
|
||||
await expect(result).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot delete admin user', () => {
|
||||
const requestor = adminAuthUser;
|
||||
describe('Delete user', () => {
|
||||
it('cannot delete admin user', async () => {
|
||||
const result = sut.deleteUser(adminUserAuth, adminUserAuth.id);
|
||||
|
||||
userRepositoryMock.get.mockImplementationOnce(() => Promise.resolve(adminUser));
|
||||
userRepositoryMock.get.mockImplementationOnce(() => Promise.resolve(adminUser));
|
||||
await expect(result).rejects.toBeInstanceOf(ForbiddenException);
|
||||
});
|
||||
});
|
||||
|
||||
const result = sui.deleteUser(requestor, adminAuthUser.id);
|
||||
describe('Create user', () => {
|
||||
it('should not create a user if there is no local admin account', async () => {
|
||||
when(userRepositoryMock.getAdmin).calledWith().mockResolvedValueOnce(null);
|
||||
|
||||
expect(result).rejects.toBeInstanceOf(BadRequestException);
|
||||
await expect(
|
||||
sut.createUser({
|
||||
email: 'john_smith@email.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Smith',
|
||||
password: 'password',
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue