mirror of https://github.com/immich-app/immich.git
refactor: migrate some e2e to medium (#17640)
parent
f50e5d006c
commit
8cefa0b84b
@ -0,0 +1,121 @@
|
|||||||
|
import { expect } from 'vitest';
|
||||||
|
|
||||||
|
export const errorDto = {
|
||||||
|
unauthorized: {
|
||||||
|
error: 'Unauthorized',
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Authentication required',
|
||||||
|
correlationId: expect.any(String),
|
||||||
|
},
|
||||||
|
forbidden: {
|
||||||
|
error: 'Forbidden',
|
||||||
|
statusCode: 403,
|
||||||
|
message: expect.any(String),
|
||||||
|
correlationId: expect.any(String),
|
||||||
|
},
|
||||||
|
missingPermission: (permission: string) => ({
|
||||||
|
error: 'Forbidden',
|
||||||
|
statusCode: 403,
|
||||||
|
message: `Missing required permission: ${permission}`,
|
||||||
|
correlationId: expect.any(String),
|
||||||
|
}),
|
||||||
|
wrongPassword: {
|
||||||
|
error: 'Bad Request',
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'Wrong password',
|
||||||
|
correlationId: expect.any(String),
|
||||||
|
},
|
||||||
|
invalidToken: {
|
||||||
|
error: 'Unauthorized',
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Invalid user token',
|
||||||
|
correlationId: expect.any(String),
|
||||||
|
},
|
||||||
|
invalidShareKey: {
|
||||||
|
error: 'Unauthorized',
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Invalid share key',
|
||||||
|
correlationId: expect.any(String),
|
||||||
|
},
|
||||||
|
invalidSharePassword: {
|
||||||
|
error: 'Unauthorized',
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Invalid password',
|
||||||
|
correlationId: expect.any(String),
|
||||||
|
},
|
||||||
|
badRequest: (message: any = null) => ({
|
||||||
|
error: 'Bad Request',
|
||||||
|
statusCode: 400,
|
||||||
|
message: message ?? expect.anything(),
|
||||||
|
correlationId: expect.any(String),
|
||||||
|
}),
|
||||||
|
noPermission: {
|
||||||
|
error: 'Bad Request',
|
||||||
|
statusCode: 400,
|
||||||
|
message: expect.stringContaining('Not found or no'),
|
||||||
|
correlationId: expect.any(String),
|
||||||
|
},
|
||||||
|
incorrectLogin: {
|
||||||
|
error: 'Unauthorized',
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Incorrect email or password',
|
||||||
|
correlationId: expect.any(String),
|
||||||
|
},
|
||||||
|
alreadyHasAdmin: {
|
||||||
|
error: 'Bad Request',
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'The server already has an admin',
|
||||||
|
correlationId: expect.any(String),
|
||||||
|
},
|
||||||
|
invalidEmail: {
|
||||||
|
error: 'Bad Request',
|
||||||
|
statusCode: 400,
|
||||||
|
message: ['email must be an email'],
|
||||||
|
correlationId: expect.any(String),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const signupResponseDto = {
|
||||||
|
admin: {
|
||||||
|
avatarColor: expect.any(String),
|
||||||
|
id: expect.any(String),
|
||||||
|
name: 'Immich Admin',
|
||||||
|
email: 'admin@immich.cloud',
|
||||||
|
storageLabel: 'admin',
|
||||||
|
profileImagePath: '',
|
||||||
|
// why? lol
|
||||||
|
shouldChangePassword: true,
|
||||||
|
isAdmin: true,
|
||||||
|
createdAt: expect.any(String),
|
||||||
|
updatedAt: expect.any(String),
|
||||||
|
deletedAt: null,
|
||||||
|
oauthId: '',
|
||||||
|
quotaUsageInBytes: 0,
|
||||||
|
quotaSizeInBytes: null,
|
||||||
|
status: 'active',
|
||||||
|
license: null,
|
||||||
|
profileChangedAt: expect.any(String),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loginResponseDto = {
|
||||||
|
admin: {
|
||||||
|
accessToken: expect.any(String),
|
||||||
|
name: 'Immich Admin',
|
||||||
|
isAdmin: true,
|
||||||
|
profileImagePath: '',
|
||||||
|
shouldChangePassword: true,
|
||||||
|
userEmail: 'admin@immich.cloud',
|
||||||
|
userId: expect.any(String),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const deviceDto = {
|
||||||
|
current: {
|
||||||
|
id: expect.any(String),
|
||||||
|
createdAt: expect.any(String),
|
||||||
|
updatedAt: expect.any(String),
|
||||||
|
current: true,
|
||||||
|
deviceOS: '',
|
||||||
|
deviceType: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
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>;
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue