Skip to content

Commit

Permalink
chore(web,server): run code coverage reports (immich-app#1313)
Browse files Browse the repository at this point in the history
* chore(web,server): run code coverage reports

* chore(tests): fail test check if coverage drops

* chore: disable e2e until they are fixed

* chore(web): coverage threshold
  • Loading branch information
jrasm91 authored Jan 12, 2023
1 parent 6db541c commit 755a133
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 84 deletions.
16 changes: 8 additions & 8 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ on:
branches: [main]

jobs:
e2e-tests:
name: Run end-to-end test suites
# e2e-tests:
# name: Run end-to-end test suites

runs-on: ubuntu-latest
# runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3
# steps:
# - name: Checkout code
# uses: actions/checkout@v3

- name: Run Immich Server E2E Test
run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test
# - name: Run Immich Server E2E Test
# run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test

server-unit-tests:
name: Run server unit test suites and checks
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
.idea

docker/upload
coverage
262 changes: 199 additions & 63 deletions server/libs/domain/src/user/user.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,63 +6,76 @@ import { when } from 'jest-when';
import { UserService } from './user.service';
import { UpdateUserDto } from './dto/update-user.dto';

describe('UserService', () => {
let sut: UserService;
let userRepositoryMock: jest.Mocked<IUserRepository>;
const adminUserAuth: AuthUserDto = Object.freeze({
id: 'admin_id',
email: '[email protected]',
isAdmin: true,
});

const adminUserAuth: AuthUserDto = Object.freeze({
id: 'admin_id',
email: 'admin@test.com',
isAdmin: true,
});
const immichUserAuth: AuthUserDto = Object.freeze({
id: 'immich_id',
email: 'immich@test.com',
isAdmin: false,
});

const immichUserAuth: AuthUserDto = Object.freeze({
id: 'immich_id',
email: '[email protected]',
isAdmin: false,
});
const adminUser: UserEntity = Object.freeze({
id: adminUserAuth.id,
email: '[email protected]',
password: 'admin_password',
firstName: 'admin_first_name',
lastName: 'admin_last_name',
isAdmin: true,
oauthId: '',
shouldChangePassword: false,
profileImagePath: '',
createdAt: '2021-01-01',
tags: [],
});

const adminUser: UserEntity = Object.freeze({
id: adminUserAuth.id,
email: 'admin@test.com',
password: 'admin_password',
firstName: 'admin_first_name',
lastName: 'admin_last_name',
isAdmin: true,
oauthId: '',
shouldChangePassword: false,
profileImagePath: '',
createdAt: '2021-01-01',
tags: [],
});
const immichUser: UserEntity = Object.freeze({
id: immichUserAuth.id,
email: 'immich@test.com',
password: 'immich_password',
firstName: 'immich_first_name',
lastName: 'immich_last_name',
isAdmin: false,
oauthId: '',
shouldChangePassword: false,
profileImagePath: '',
createdAt: '2021-01-01',
tags: [],
});

const immichUser: UserEntity = Object.freeze({
id: immichUserAuth.id,
email: '[email protected]',
password: 'immich_password',
firstName: 'immich_first_name',
lastName: 'immich_last_name',
isAdmin: false,
oauthId: '',
shouldChangePassword: false,
profileImagePath: '',
createdAt: '2021-01-01',
tags: [],
});
const updatedImmichUser: UserEntity = Object.freeze({
id: immichUserAuth.id,
email: '[email protected]',
password: 'immich_password',
firstName: 'updated_immich_first_name',
lastName: 'updated_immich_last_name',
isAdmin: false,
oauthId: '',
shouldChangePassword: true,
profileImagePath: '',
createdAt: '2021-01-01',
tags: [],
});

const updatedImmichUser: UserEntity = Object.freeze({
id: immichUserAuth.id,
email: '[email protected]',
password: 'immich_password',
firstName: 'updated_immich_first_name',
lastName: 'updated_immich_last_name',
isAdmin: false,
oauthId: '',
shouldChangePassword: true,
profileImagePath: '',
createdAt: '2021-01-01',
tags: [],
});
const adminUserResponse = Object.freeze({
id: adminUserAuth.id,
email: '[email protected]',
deletedAt: undefined,
firstName: 'admin_first_name',
lastName: 'admin_last_name',
isAdmin: true,
oauthId: '',
shouldChangePassword: false,
profileImagePath: '',
createdAt: '2021-01-01',
});

describe('UserService', () => {
let sut: UserService;
let userRepositoryMock: jest.Mocked<IUserRepository>;

beforeEach(() => {
userRepositoryMock = {
Expand All @@ -78,12 +91,86 @@ describe('UserService', () => {
};
when(userRepositoryMock.get).calledWith(adminUser.id).mockResolvedValue(adminUser);
when(userRepositoryMock.get).calledWith(adminUser.id, undefined).mockResolvedValue(adminUser);
when(userRepositoryMock.get).calledWith(immichUser.id).mockResolvedValue(immichUser);
when(userRepositoryMock.get).calledWith(immichUser.id, undefined).mockResolvedValue(immichUser);

sut = new UserService(userRepositoryMock);
});

describe('Update user', () => {
describe('getAllUsers', () => {
it('should get all users', async () => {
userRepositoryMock.getList.mockResolvedValue([adminUser]);

const response = await sut.getAllUsers(adminUserAuth, false);

expect(userRepositoryMock.getList).toHaveBeenCalledWith({ excludeId: adminUser.id });
expect(response).toEqual([
{
id: adminUserAuth.id,
email: '[email protected]',
deletedAt: undefined,
firstName: 'admin_first_name',
lastName: 'admin_last_name',
isAdmin: true,
oauthId: '',
shouldChangePassword: false,
profileImagePath: '',
createdAt: '2021-01-01',
},
]);
});
});

describe('getUserById', () => {
it('should get a user by id', async () => {
userRepositoryMock.get.mockResolvedValue(adminUser);

const response = await sut.getUserById(adminUser.id);

expect(userRepositoryMock.get).toHaveBeenCalledWith(adminUser.id, false);
expect(response).toEqual(adminUserResponse);
});

it('should throw an error if a user is not found', async () => {
userRepositoryMock.get.mockResolvedValue(null);

await expect(sut.getUserById(adminUser.id)).rejects.toBeInstanceOf(NotFoundException);

expect(userRepositoryMock.get).toHaveBeenCalledWith(adminUser.id, false);
});
});

describe('getUserInfo', () => {
it("should get the auth user's info", async () => {
userRepositoryMock.get.mockResolvedValue(adminUser);

const response = await sut.getUserInfo(adminUser);

expect(userRepositoryMock.get).toHaveBeenCalledWith(adminUser.id, undefined);
expect(response).toEqual(adminUserResponse);
});

it('should throw an error if a user is not found', async () => {
userRepositoryMock.get.mockResolvedValue(null);

await expect(sut.getUserInfo(adminUser)).rejects.toBeInstanceOf(BadRequestException);

expect(userRepositoryMock.get).toHaveBeenCalledWith(adminUser.id, undefined);
});
});

describe('getUserCount', () => {
it('should get the user count', async () => {
userRepositoryMock.getList.mockResolvedValue([adminUser]);

const response = await sut.getUserCount({});

expect(userRepositoryMock.getList).toHaveBeenCalled();
expect(response).toEqual({ userCount: 1 });
});
});

describe('update', () => {
it('should update user', async () => {
const update: UpdateUserDto = {
id: immichUser.id,
Expand Down Expand Up @@ -161,17 +248,7 @@ describe('UserService', () => {

await expect(result).rejects.toBeInstanceOf(NotFoundException);
});
});

describe('Delete user', () => {
it('cannot delete admin user', async () => {
const result = sut.deleteUser(adminUserAuth, adminUserAuth.id);

await expect(result).rejects.toBeInstanceOf(ForbiddenException);
});
});

describe('Create user', () => {
it('should let the admin update himself', async () => {
const dto = { id: adminUser.id, shouldChangePassword: true, isAdmin: true };

Expand All @@ -190,7 +267,37 @@ describe('UserService', () => {

await expect(sut.updateUser(adminUser, dto)).rejects.toBeInstanceOf(BadRequestException);
});
});

describe('restoreUser', () => {
it('should require an admin', async () => {
when(userRepositoryMock.get).calledWith(adminUser.id, true).mockResolvedValue(adminUser);
await expect(sut.restoreUser(immichUserAuth, adminUser.id)).rejects.toBeInstanceOf(ForbiddenException);
expect(userRepositoryMock.get).toHaveBeenCalledWith(adminUser.id, true);
});

it('should require the auth user be an admin', async () => {
await expect(sut.deleteUser(immichUserAuth, adminUserAuth.id)).rejects.toBeInstanceOf(ForbiddenException);

expect(userRepositoryMock.delete).not.toHaveBeenCalled();
});
});

describe('deleteUser', () => {
it('cannot delete admin user', async () => {
const result = sut.deleteUser(adminUserAuth, adminUserAuth.id);

await expect(result).rejects.toBeInstanceOf(ForbiddenException);
});

it('should require the auth user be an admin', async () => {
await expect(sut.deleteUser(immichUserAuth, adminUserAuth.id)).rejects.toBeInstanceOf(ForbiddenException);

expect(userRepositoryMock.delete).not.toHaveBeenCalled();
});
});

describe('update', () => {
it('should not create a user if there is no local admin account', async () => {
when(userRepositoryMock.getAdmin).calledWith().mockResolvedValueOnce(null);

Expand All @@ -204,4 +311,33 @@ describe('UserService', () => {
).rejects.toBeInstanceOf(BadRequestException);
});
});

describe('createProfileImage', () => {
it('should throw an error if the user does not exist', async () => {
const file = { path: '/profile/path' } as Express.Multer.File;
userRepositoryMock.update.mockResolvedValue({ ...adminUser, profileImagePath: file.path });

await sut.createProfileImage(adminUserAuth, file);

expect(userRepositoryMock.update).toHaveBeenCalledWith(adminUserAuth.id, { profileImagePath: file.path });
});
});

describe('getUserProfileImage', () => {
it('should throw an error if the user does not exist', async () => {
userRepositoryMock.get.mockResolvedValue(null);

await expect(sut.getUserProfileImage(adminUserAuth.id)).rejects.toBeInstanceOf(NotFoundException);

expect(userRepositoryMock.get).toHaveBeenCalledWith(adminUserAuth.id, undefined);
});

it('should throw an error if the user does not have a picture', async () => {
userRepositoryMock.get.mockResolvedValue(adminUser);

await expect(sut.getUserProfileImage(adminUserAuth.id)).rejects.toBeInstanceOf(NotFoundException);

expect(userRepositoryMock.get).toHaveBeenCalledWith(adminUserAuth.id, undefined);
});
});
});
14 changes: 13 additions & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"lint:fix": "npm run lint -- --fix",
"check": "tsc --noEmit",
"check:code": "npm run format && npm run lint && npm run check",
"check:all": "npm run check:code && npm run test",
"check:all": "npm run check:code && npm run test:cov",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
Expand Down Expand Up @@ -139,6 +139,18 @@
"**/*.(t|j)s"
],
"coverageDirectory": "./coverage",
"coverageThreshold": {
"global": {
"lines": 20,
"statements": 20
},
"./libs/domain/": {
"branches": 60,
"functions": 80,
"lines": 80,
"statements": 80
}
},
"testEnvironment": "node",
"roots": [
"<rootDir>/apps/",
Expand Down
Loading

0 comments on commit 755a133

Please sign in to comment.