refactor: controller tests (#18100)

This commit is contained in:
Jason Rasmussen 2025-05-05 18:57:32 -04:00 committed by GitHub
parent df2cf5d106
commit f34f83e164
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 183 additions and 101 deletions

View file

@ -6,15 +6,15 @@ import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'
describe(ActivityController.name, () => {
let ctx: ControllerContext;
const service = mockBaseService(ActivityService);
beforeAll(async () => {
ctx = await controllerSetup(ActivityController, [
{ provide: ActivityService, useValue: mockBaseService(ActivityService) },
]);
ctx = await controllerSetup(ActivityController, [{ provide: ActivityService, useValue: service }]);
return () => ctx.close();
});
beforeEach(() => {
service.resetAllMocks();
ctx.reset();
});

View file

@ -6,13 +6,15 @@ import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'
describe(AlbumController.name, () => {
let ctx: ControllerContext;
const service = mockBaseService(AlbumService);
beforeAll(async () => {
ctx = await controllerSetup(AlbumController, [{ provide: AlbumService, useValue: mockBaseService(AlbumService) }]);
ctx = await controllerSetup(AlbumController, [{ provide: AlbumService, useValue: service }]);
return () => ctx.close();
});
beforeEach(() => {
service.resetAllMocks();
ctx.reset();
});

View file

@ -6,15 +6,15 @@ import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'
describe(APIKeyController.name, () => {
let ctx: ControllerContext;
const service = mockBaseService(ApiKeyService);
beforeAll(async () => {
ctx = await controllerSetup(APIKeyController, [
{ provide: ApiKeyService, useValue: mockBaseService(ApiKeyService) },
]);
ctx = await controllerSetup(APIKeyController, [{ provide: ApiKeyService, useValue: service }]);
return () => ctx.close();
});
beforeEach(() => {
service.resetAllMocks();
ctx.reset();
});

View file

@ -1,4 +1,5 @@
import { AuthController } from 'src/controllers/auth.controller';
import { LoginResponseDto } from 'src/dtos/auth.dto';
import { AuthService } from 'src/services/auth.service';
import request from 'supertest';
import { errorDto } from 'test/medium/responses';
@ -14,6 +15,7 @@ describe(AuthController.name, () => {
});
beforeEach(() => {
service.resetAllMocks();
ctx.reset();
});
@ -56,5 +58,88 @@ describe(AuthController.name, () => {
expect(status).toEqual(201);
expect(service.adminSignUp).toHaveBeenCalledWith(expect.objectContaining({ email: 'admin@immich.cloud' }));
});
it('should accept an email with a local domain', async () => {
const { status } = await request(ctx.getHttpServer())
.post('/auth/admin-sign-up')
.send({ name: 'admin', password: 'password', email: 'admin@local' });
expect(status).toEqual(201);
});
});
describe('POST /auth/login', () => {
it(`should require an email and password`, async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/auth/login').send({ name: 'admin' });
expect(status).toBe(400);
expect(body).toEqual(
errorDto.badRequest([
'email should not be empty',
'email must be an email',
'password should not be empty',
'password must be a string',
]),
);
});
it(`should not allow null email`, async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/auth/login')
.send({ name: 'admin', email: null, password: 'password' });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['email should not be empty', 'email must be an email']));
});
it(`should not allow null password`, async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/auth/login')
.send({ name: 'admin', email: 'admin@immich.cloud', password: null });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['password should not be empty', 'password must be a string']));
});
it('should reject an invalid email', async () => {
service.login.mockResolvedValue({ accessToken: 'access-token' } as LoginResponseDto);
const { status, body } = await request(ctx.getHttpServer())
.post('/auth/login')
.send({ name: 'admin', email: [], password: 'password' });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['email must be an email']));
});
it('should transform the email to all lowercase', async () => {
service.login.mockResolvedValue({ accessToken: 'access-token' } as LoginResponseDto);
const { status } = await request(ctx.getHttpServer())
.post('/auth/login')
.send({ name: 'admin', email: 'aDmIn@iMmIcH.ApP', password: 'password' });
expect(status).toBe(201);
expect(service.login).toHaveBeenCalledWith(
expect.objectContaining({ email: 'admin@immich.app' }),
expect.anything(),
);
});
it('should accept an email with a local domain', async () => {
service.login.mockResolvedValue({ accessToken: 'access-token' } as LoginResponseDto);
const { status } = await request(ctx.getHttpServer())
.post('/auth/login')
.send({ name: 'admin', email: 'admin@local', password: 'password' });
expect(status).toEqual(201);
expect(service.login).toHaveBeenCalledWith(expect.objectContaining({ email: 'admin@local' }), expect.anything());
});
});
describe('POST /auth/change-password', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer())
.post('/auth/change-password')
.send({ password: 'password', newPassword: 'Password1234' });
expect(ctx.authenticate).toHaveBeenCalled();
});
});
});

View file

@ -23,8 +23,8 @@ export class AuthController {
@Post('login')
async login(
@Body() loginCredential: LoginCredentialDto,
@Res({ passthrough: true }) res: Response,
@Body() loginCredential: LoginCredentialDto,
@GetLoginDetails() loginDetails: LoginDetails,
): Promise<LoginResponseDto> {
const body = await this.service.login(loginCredential, loginDetails);

View file

@ -0,0 +1,46 @@
import { DownloadController } from 'src/controllers/download.controller';
import { DownloadService } from 'src/services/download.service';
import request from 'supertest';
import { factory } from 'test/small.factory';
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
import { Readable } from 'typeorm/platform/PlatformTools.js';
describe(DownloadController.name, () => {
let ctx: ControllerContext;
const service = mockBaseService(DownloadService);
beforeAll(async () => {
ctx = await controllerSetup(DownloadController, [{ provide: DownloadService, useValue: service }]);
return () => ctx.close();
});
beforeEach(() => {
service.resetAllMocks();
ctx.reset();
});
describe('POST /download/info', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer())
.post('/download/info')
.send({ assetIds: [factory.uuid()] });
expect(ctx.authenticate).toHaveBeenCalled();
});
});
describe('POST /download/archive', () => {
it('should be an authenticated route', async () => {
const stream = new Readable({
read() {
this.push('test');
this.push(null);
},
});
service.downloadArchive.mockResolvedValue({ stream });
await request(ctx.getHttpServer())
.post('/download/archive')
.send({ assetIds: [factory.uuid()] });
expect(ctx.authenticate).toHaveBeenCalled();
});
});
});

View file

@ -7,15 +7,15 @@ import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'
describe(NotificationController.name, () => {
let ctx: ControllerContext;
const service = mockBaseService(NotificationService);
beforeAll(async () => {
ctx = await controllerSetup(NotificationController, [
{ provide: NotificationService, useValue: mockBaseService(NotificationService) },
]);
ctx = await controllerSetup(NotificationController, [{ provide: NotificationService, useValue: service }]);
return () => ctx.close();
});
beforeEach(() => {
service.resetAllMocks();
ctx.reset();
});

View file

@ -6,15 +6,15 @@ import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'
describe(SearchController.name, () => {
let ctx: ControllerContext;
const service = mockBaseService(SearchService);
beforeAll(async () => {
ctx = await controllerSetup(SearchController, [
{ provide: SearchService, useValue: mockBaseService(SearchService) },
]);
ctx = await controllerSetup(SearchController, [{ provide: SearchService, useValue: service }]);
return () => ctx.close();
});
beforeEach(() => {
service.resetAllMocks();
ctx.reset();
});

View file

@ -6,16 +6,20 @@ import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'
describe(ServerController.name, () => {
let ctx: ControllerContext;
const serverService = mockBaseService(ServerService);
const versionService = mockBaseService(VersionService);
beforeAll(async () => {
ctx = await controllerSetup(ServerController, [
{ provide: ServerService, useValue: mockBaseService(ServerService) },
{ provide: VersionService, useValue: mockBaseService(VersionService) },
{ provide: ServerService, useValue: serverService },
{ provide: VersionService, useValue: versionService },
]);
return () => ctx.close();
});
beforeEach(() => {
serverService.resetAllMocks();
versionService.resetAllMocks();
ctx.reset();
});

View file

@ -8,16 +8,18 @@ import { automock, ControllerContext, controllerSetup, mockBaseService } from 't
describe(UserController.name, () => {
let ctx: ControllerContext;
const service = mockBaseService(UserService);
beforeAll(async () => {
ctx = await controllerSetup(UserController, [
{ provide: LoggingRepository, useValue: automock(LoggingRepository, { strict: false }) },
{ provide: UserService, useValue: mockBaseService(UserService) },
{ provide: UserService, useValue: service },
]);
return () => ctx.close();
});
beforeEach(() => {
service.resetAllMocks();
ctx.reset();
});