refactor: controller tests (#18035)

* feat: controller unit tests

* refactor: controller tests
This commit is contained in:
Jason Rasmussen
2025-05-03 09:39:44 -04:00
committed by GitHub
parent 62fc5b3c7d
commit ea9f11bf39
23 changed files with 1035 additions and 805 deletions
-1
View File
@@ -47,7 +47,6 @@ export const errorDto = {
error: 'Bad Request',
statusCode: 400,
message: message ?? expect.anything(),
correlationId: expect.any(String),
}),
noPermission: {
error: 'Bad Request',
@@ -1,60 +0,0 @@
import { AuthController } from 'src/controllers/auth.controller';
import { AuthService } from 'src/services/auth.service';
import request from 'supertest';
import { errorDto } from 'test/medium/responses';
import { createControllerTestApp, TestControllerApp } from 'test/medium/utils';
describe(AuthController.name, () => {
let app: TestControllerApp;
beforeAll(async () => {
app = await createControllerTestApp();
});
describe('POST /auth/admin-sign-up', () => {
const name = 'admin';
const email = 'admin@immich.cloud';
const password = 'password';
const invalid = [
{
should: 'require an email address',
data: { name, password },
},
{
should: 'require a password',
data: { name, email },
},
{
should: 'require a name',
data: { email, password },
},
{
should: 'require a valid email',
data: { name, email: 'immich', password },
},
];
for (const { should, data } of invalid) {
it(`should ${should}`, async () => {
const { status, body } = await request(app.getHttpServer()).post('/auth/admin-sign-up').send(data);
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest());
});
}
it('should transform email to lower case', async () => {
const { status } = await request(app.getHttpServer())
.post('/auth/admin-sign-up')
.send({ name: 'admin', password: 'password', email: 'aDmIn@IMMICH.cloud' });
expect(status).toEqual(201);
expect(app.getMockedService(AuthService).adminSignUp).toHaveBeenCalledWith(
expect.objectContaining({ email: 'admin@immich.cloud' }),
);
});
});
afterAll(async () => {
await app.close();
});
});
@@ -1,86 +0,0 @@
import { NotificationController } from 'src/controllers/notification.controller';
import { AuthService } from 'src/services/auth.service';
import { NotificationService } from 'src/services/notification.service';
import request from 'supertest';
import { errorDto } from 'test/medium/responses';
import { createControllerTestApp, TestControllerApp } from 'test/medium/utils';
import { factory } from 'test/small.factory';
describe(NotificationController.name, () => {
let realApp: TestControllerApp;
let mockApp: TestControllerApp;
beforeEach(async () => {
realApp = await createControllerTestApp({ authType: 'real' });
mockApp = await createControllerTestApp({ authType: 'mock' });
});
describe('GET /notifications', () => {
it('should require authentication', async () => {
const { status, body } = await request(realApp.getHttpServer()).get('/notifications');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should call the service with an auth dto', async () => {
const auth = factory.auth({ user: factory.user() });
mockApp.getMockedService(AuthService).authenticate.mockResolvedValue(auth);
const service = mockApp.getMockedService(NotificationService);
const { status } = await request(mockApp.getHttpServer())
.get('/notifications')
.set('Authorization', `Bearer token`);
expect(status).toBe(200);
expect(service.search).toHaveBeenCalledWith(auth, {});
});
it(`should reject an invalid notification level`, async () => {
const auth = factory.auth({ user: factory.user() });
mockApp.getMockedService(AuthService).authenticate.mockResolvedValue(auth);
const service = mockApp.getMockedService(NotificationService);
const { status, body } = await request(mockApp.getHttpServer())
.get(`/notifications`)
.query({ level: 'invalid' })
.set('Authorization', `Bearer token`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest([expect.stringContaining('level must be one of the following values')]));
expect(service.search).not.toHaveBeenCalled();
});
});
describe('PUT /notifications', () => {
it('should require authentication', async () => {
const { status, body } = await request(realApp.getHttpServer())
.put(`/notifications`)
.send({ ids: [], readAt: new Date().toISOString() });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
});
describe('GET /notifications/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(realApp.getHttpServer()).get(`/notifications/${factory.uuid()}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
});
describe('PUT /notifications/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(realApp.getHttpServer())
.put(`/notifications/${factory.uuid()}`)
.send({ readAt: factory.date() });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
});
afterAll(async () => {
await realApp.close();
await mockApp.close();
});
});
@@ -1,100 +0,0 @@
import { UserController } from 'src/controllers/user.controller';
import { AuthService } from 'src/services/auth.service';
import { UserService } from 'src/services/user.service';
import request from 'supertest';
import { errorDto } from 'test/medium/responses';
import { createControllerTestApp, TestControllerApp } from 'test/medium/utils';
import { factory } from 'test/small.factory';
describe(UserController.name, () => {
let realApp: TestControllerApp;
let mockApp: TestControllerApp;
beforeAll(async () => {
realApp = await createControllerTestApp({ authType: 'real' });
mockApp = await createControllerTestApp({ authType: 'mock' });
});
describe('GET /users', () => {
it('should require authentication', async () => {
const { status, body } = await request(realApp.getHttpServer()).get('/users');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should call the service with an auth dto', async () => {
const user = factory.user();
const authService = mockApp.getMockedService(AuthService);
const auth = factory.auth({ user });
authService.authenticate.mockResolvedValue(auth);
const userService = mockApp.getMockedService(UserService);
const { status } = await request(mockApp.getHttpServer()).get('/users').set('Authorization', `Bearer token`);
expect(status).toBe(200);
expect(userService.search).toHaveBeenCalledWith(auth);
});
});
describe('GET /users/me', () => {
it('should require authentication', async () => {
const { status, body } = await request(realApp.getHttpServer()).get(`/users/me`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
});
describe('PUT /users/me', () => {
it('should require authentication', async () => {
const { status, body } = await request(realApp.getHttpServer()).put(`/users/me`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
for (const key of ['email', 'name']) {
it(`should not allow null ${key}`, async () => {
const dto = { [key]: null };
const { status, body } = await request(mockApp.getHttpServer())
.put(`/users/me`)
.set('Authorization', `Bearer token`)
.send(dto);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
}
});
describe('GET /users/:id', () => {
it('should require authentication', async () => {
const { status } = await request(realApp.getHttpServer()).get(`/users/${factory.uuid()}`);
expect(status).toEqual(401);
});
});
describe('GET /server/license', () => {
it('should require authentication', async () => {
const { status, body } = await request(realApp.getHttpServer()).get('/users/me/license');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
});
describe('PUT /users/me/license', () => {
it('should require authentication', async () => {
const { status } = await request(realApp.getHttpServer()).put(`/users/me/license`);
expect(status).toEqual(401);
});
});
describe('DELETE /users/me/license', () => {
it('should require authentication', async () => {
const { status } = await request(realApp.getHttpServer()).put(`/users/me/license`);
expect(status).toEqual(401);
});
});
afterAll(async () => {
await realApp.close();
await mockApp.close();
});
});
-100
View File
@@ -1,100 +0,0 @@
import { Provider } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule';
import { Test } from '@nestjs/testing';
import { ClassConstructor } from 'class-transformer';
import { ClsService } from 'nestjs-cls';
import { middleware } from 'src/app.module';
import { controllers } from 'src/controllers';
import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { services } from 'src/services';
import { ApiService } from 'src/services/api.service';
import { AuthService } from 'src/services/auth.service';
import { BaseService } from 'src/services/base.service';
import { automock } from 'test/utils';
import { Mocked } from 'vitest';
export const createControllerTestApp = async (options?: { authType?: 'mock' | 'real' }) => {
const { authType = 'mock' } = options || {};
const configMock = { getEnv: () => ({ noColor: true }) };
const clsMock = { getId: vitest.fn().mockReturnValue('cls-id') };
const loggerMock = automock(LoggingRepository, { args: [clsMock, configMock], strict: false });
loggerMock.setContext.mockReturnValue(void 0);
loggerMock.error.mockImplementation((...args: any[]) => {
console.log('Logger.error was called with', ...args);
});
const mockBaseService = (service: ClassConstructor<BaseService>) => {
return automock(service, { args: [loggerMock], strict: false });
};
const clsServiceMock = clsMock;
const FAKE_MOCK = vitest.fn();
const providers: Provider[] = [
...middleware,
...services.map((Service) => {
if ((authType === 'real' && Service === AuthService) || Service === ApiService) {
return Service;
}
return { provide: Service, useValue: mockBaseService(Service as ClassConstructor<BaseService>) };
}),
GlobalExceptionFilter,
{ provide: LoggingRepository, useValue: loggerMock },
{ provide: ClsService, useValue: clsServiceMock },
];
const moduleRef = await Test.createTestingModule({
imports: [],
controllers: [...controllers],
providers,
})
.useMocker((token) => {
if (token === LoggingRepository) {
return;
}
if (token === SchedulerRegistry) {
return FAKE_MOCK;
}
if (typeof token === 'function' && token.name.endsWith('Repository')) {
return FAKE_MOCK;
}
if (typeof token === 'string' && token === 'KyselyModuleConnectionToken') {
return FAKE_MOCK;
}
})
.compile();
const app = moduleRef.createNestApplication();
await app.init();
const getMockedRepository = <T>(token: ClassConstructor<T>) => {
return app.get(token) as Mocked<T>;
};
return {
getHttpServer: () => app.getHttpServer(),
getMockedService: <T>(token: ClassConstructor<T>) => {
if (authType === 'real' && token === AuthService) {
throw new Error('Auth type is real, cannot get mocked service');
}
return app.get(token) as Mocked<T>;
},
getMockedRepository,
close: () => app.close(),
};
};
export type TestControllerApp = {
getHttpServer: () => any;
getMockedService: <T>(token: ClassConstructor<T>) => Mocked<T>;
getMockedRepository: <T>(token: ClassConstructor<T>) => Mocked<T>;
close: () => Promise<void>;
};