Merge branch 'testHealth' into 'main'

Use health package

See merge request v3/service/auth!43
This commit is contained in:
Sylvain Briat 2023-07-17 15:02:38 +00:00
commit 0825a5f0d9
23 changed files with 1202 additions and 2375 deletions

3038
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "@mobicoop/auth",
"version": "0.1.1",
"version": "0.2.0",
"description": "Mobicoop V3 Auth Service",
"author": "sbriat",
"private": true,
@ -37,6 +37,7 @@
"@grpc/grpc-js": "^1.8.0",
"@grpc/proto-loader": "^0.7.4",
"@mobicoop/ddd-library": "^0.3.0",
"@mobicoop/health-module": "^1.1.0",
"@mobicoop/message-broker-module": "^1.2.0",
"@nestjs/axios": "^1.0.1",
"@nestjs/common": "^9.0.0",

View File

@ -6,8 +6,16 @@ import {
MessageBrokerModule,
MessageBrokerModuleOptions,
} from '@mobicoop/message-broker-module';
import { HealthModule } from '@modules/health/health.module';
import { HealthModule, HealthModuleOptions } from '@mobicoop/health-module';
import { AuthorizationModule } from '@modules/authorization/authorization.module';
import {
AUTHENTICATION_REPOSITORY,
USERNAME_REPOSITORY,
} from '@modules/authentication/authentication.di-tokens';
import { MESSAGE_PUBLISHER } from './modules/message-publisher/message-publisher.di-tokens';
import { MessagePublisherPort } from '@mobicoop/ddd-library';
import { ICheckRepository } from '@mobicoop/health-module/dist/core/domain/types/health.types';
import { MessagePublisherModule } from './modules/message-publisher/message-publisher.module';
@Module({
imports: [
@ -34,11 +42,29 @@ import { AuthorizationModule } from '@modules/authorization/authorization.module
},
}),
}),
HealthModule.forRootAsync({
imports: [AuthenticationModule, MessagePublisherModule],
inject: [
AUTHENTICATION_REPOSITORY,
USERNAME_REPOSITORY,
MESSAGE_PUBLISHER,
],
useFactory: async (
authenticationRepository: ICheckRepository,
usernameRepository: ICheckRepository,
messagePublisher: MessagePublisherPort,
): Promise<HealthModuleOptions> => ({
serviceName: 'auth',
criticalLoggingKey: 'logging.auth.health.crit',
checkRepositories: [authenticationRepository, usernameRepository],
messagePublisher,
}),
}),
AuthenticationModule,
AuthorizationModule,
HealthModule,
MessagePublisherModule,
],
controllers: [],
providers: [],
exports: [AuthenticationModule, MessagePublisherModule],
})
export class AppModule {}

View File

@ -21,10 +21,7 @@ async function bootstrap() {
__dirname,
'modules/authorization/interface/grpc-controllers/authorization.proto',
),
join(
__dirname,
'modules/health/interface/grpc-controllers/health.proto',
),
join(__dirname, 'health.proto'),
],
url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT,
loader: { keepCase: true, enums: String },

View File

@ -1,2 +1,3 @@
export const AUTH_MESSAGE_PUBLISHER = Symbol('AUTH_MESSAGE_PUBLISHER');
export const AUTHENTICATION_REPOSITORY = Symbol('AUTHENTICATION_REPOSITORY');
export const USERNAME_REPOSITORY = Symbol('USERNAME_REPOSITORY');

View File

@ -3,6 +3,7 @@ import { CreateAuthenticationGrpcController } from './interface/grpc-controllers
import { CreateAuthenticationService } from './core/application/commands/create-authentication/create-authentication.service';
import { AuthenticationMapper } from './authentication.mapper';
import {
AUTH_MESSAGE_PUBLISHER,
AUTHENTICATION_REPOSITORY,
USERNAME_REPOSITORY,
} from './authentication.di-tokens';
@ -11,7 +12,6 @@ import { PrismaService } from './infrastructure/prisma.service';
import { CqrsModule } from '@nestjs/cqrs';
import { DeleteAuthenticationGrpcController } from './interface/grpc-controllers/delete-authentication.grpc.controller';
import { DeleteAuthenticationService } from './core/application/commands/delete-authentication/delete-authentication.service';
import { MESSAGE_PUBLISHER } from '@src/app.di-tokens';
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
import { UsernameRepository } from './infrastructure/username.repository';
import { UsernameMapper } from './username.mapper';
@ -64,10 +64,10 @@ const repositories: Provider[] = [
},
];
const messageBrokers: Provider[] = [
const messagePublishers: Provider[] = [
{
provide: MESSAGE_PUBLISHER,
useClass: MessageBrokerPublisher,
provide: AUTH_MESSAGE_PUBLISHER,
useExisting: MessageBrokerPublisher,
},
];
@ -82,7 +82,7 @@ const orms: Provider[] = [PrismaService];
...queryHandlers,
...mappers,
...repositories,
...messageBrokers,
...messagePublishers,
...orms,
],
exports: [

View File

@ -9,7 +9,7 @@ import { AuthenticationEntity } from '../core/domain/authentication.entity';
import { AuthenticationRepositoryPort } from '../core/application/ports/authentication.repository.port';
import { PrismaService } from './prisma.service';
import { AuthenticationMapper } from '../authentication.mapper';
import { MESSAGE_PUBLISHER } from '@src/app.di-tokens';
import { AUTH_MESSAGE_PUBLISHER } from '../authentication.di-tokens';
type AuthenticationBaseModel = {
uuid: string;
@ -49,7 +49,7 @@ export class AuthenticationRepository
prisma: PrismaService,
mapper: AuthenticationMapper,
eventEmitter: EventEmitter2,
@Inject(MESSAGE_PUBLISHER)
@Inject(AUTH_MESSAGE_PUBLISHER)
protected readonly messagePublisher: MessagePublisherPort,
) {
super(

View File

@ -6,11 +6,11 @@ import {
PrismaRepositoryBase,
} from '@mobicoop/ddd-library';
import { PrismaService } from './prisma.service';
import { MESSAGE_PUBLISHER } from '@src/app.di-tokens';
import { UsernameEntity } from '../core/domain/username.entity';
import { UsernameRepositoryPort } from '../core/application/ports/username.repository.port';
import { UsernameMapper } from '../username.mapper';
import { Type } from '../core/domain/username.types';
import { AUTH_MESSAGE_PUBLISHER } from '../authentication.di-tokens';
type UsernameBaseModel = {
username: string;
@ -41,7 +41,7 @@ export class UsernameRepository
prisma: PrismaService,
mapper: UsernameMapper,
eventEmitter: EventEmitter2,
@Inject(MESSAGE_PUBLISHER)
@Inject(AUTH_MESSAGE_PUBLISHER)
protected readonly messagePublisher: MessagePublisherPort,
) {
super(

View File

@ -5,7 +5,6 @@ import { PrismaService } from '@modules/authentication/infrastructure/prisma.ser
import { AuthenticationRepository } from '@modules/authentication/infrastructure/authentication.repository';
import { AuthenticationMapper } from '@modules/authentication/authentication.mapper';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { MESSAGE_PUBLISHER } from '@src/app.di-tokens';
import {
DatabaseErrorException,
NotFoundException,
@ -14,6 +13,7 @@ import {
import { AuthenticationEntity } from '@modules/authentication/core/domain/authentication.entity';
import { Type } from '@modules/authentication/core/domain/username.types';
import { CreateAuthenticationProps } from '@modules/authentication/core/domain/authentication.types';
import { AUTH_MESSAGE_PUBLISHER } from '@modules/authentication/authentication.di-tokens';
const uuid = '165192d4-398a-4469-a16b-98c02cc6f531';
@ -42,17 +42,6 @@ describe('AuthenticationRepository', () => {
let prismaService: PrismaService;
let authenticationRepository: AuthenticationRepository;
// const createAuthentications = async (nbToCreate = 10) => {
// for (let i = 0; i < nbToCreate; i++) {
// await prismaService.auth.create({
// data: {
// uuid: v4(),
// password: bcrypt.hashSync(`password-${i}`, 10),
// },
// });
// }
// };
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [EventEmitterModule.forRoot()],
@ -61,7 +50,7 @@ describe('AuthenticationRepository', () => {
PrismaService,
AuthenticationMapper,
{
provide: MESSAGE_PUBLISHER,
provide: AUTH_MESSAGE_PUBLISHER,
useValue: mockMessagePublisher,
},
],
@ -84,30 +73,6 @@ describe('AuthenticationRepository', () => {
await prismaService.auth.deleteMany();
});
// describe('findAll', () => {
// it('should return an empty data array', async () => {
// const res = await authenticationRepository.findAll();
// expect(res).toEqual({
// data: [],
// total: 0,
// });
// });
// it('should return a data array with 8 auths', async () => {
// await createAuthentications(8);
// const auths = await authenticationRepository.findAll();
// expect(auths.data.length).toBe(8);
// expect(auths.total).toBe(8);
// });
// it('should return a data array limited to 10 authentications', async () => {
// await createAuthentications(20);
// const auths = await authenticationRepository.findAll();
// expect(auths.data.length).toBe(10);
// expect(auths.total).toBe(20);
// });
// });
describe('findOneById', () => {
it('should return an authentication', async () => {
const authToFind = await prismaService.auth.create({

View File

@ -1,7 +1,6 @@
import { TestingModule, Test } from '@nestjs/testing';
import { PrismaService } from '@modules/authentication/infrastructure/prisma.service';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { MESSAGE_PUBLISHER } from '@src/app.di-tokens';
import {
DatabaseErrorException,
NotFoundException,
@ -15,6 +14,7 @@ import { UsernameRepository } from '@modules/authentication/infrastructure/usern
import { UsernameMapper } from '@modules/authentication/username.mapper';
import * as bcrypt from 'bcrypt';
import { UsernameEntity } from '@modules/authentication/core/domain/username.entity';
import { AUTH_MESSAGE_PUBLISHER } from '@modules/authentication/authentication.di-tokens';
const authUuid = 'a4524d22-7be3-46cd-8444-3145470476dc';
@ -46,7 +46,7 @@ describe('UsernameRepository', () => {
PrismaService,
UsernameMapper,
{
provide: MESSAGE_PUBLISHER,
provide: AUTH_MESSAGE_PUBLISHER,
useValue: mockMessagePublisher,
},
],

View File

@ -1,3 +0,0 @@
export interface CheckRepositoryPort {
healthCheck(): Promise<boolean>;
}

View File

@ -1,54 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import {
HealthCheckError,
HealthCheckResult,
HealthIndicator,
HealthIndicatorResult,
} from '@nestjs/terminus';
import { CheckRepositoryPort } from '../ports/check-repository.port';
import {
AUTHENTICATION_REPOSITORY,
USERNAME_REPOSITORY,
} from '@modules/health/health.di-tokens';
import { MESSAGE_PUBLISHER } from '@src/app.di-tokens';
import { LOGGING_AUTHENTICATION_HEALTH_CRIT } from '@modules/health/health.constants';
import { MessagePublisherPort } from '@mobicoop/ddd-library';
import { AuthenticationRepositoryPort } from '@modules/authentication/core/application/ports/authentication.repository.port';
import { UsernameRepositoryPort } from '@modules/authentication/core/application/ports/username.repository.port';
@Injectable()
export class RepositoriesHealthIndicatorUseCase extends HealthIndicator {
private _checkRepositories: CheckRepositoryPort[];
constructor(
@Inject(AUTHENTICATION_REPOSITORY)
private readonly authenticationRepository: AuthenticationRepositoryPort,
@Inject(USERNAME_REPOSITORY)
private readonly usernameRepository: UsernameRepositoryPort,
@Inject(MESSAGE_PUBLISHER)
private readonly messagePublisher: MessagePublisherPort,
) {
super();
this._checkRepositories = [authenticationRepository, usernameRepository];
}
isHealthy = async (key: string): Promise<HealthIndicatorResult> => {
try {
await Promise.all(
this._checkRepositories.map(
async (checkRepository: CheckRepositoryPort) => {
await checkRepository.healthCheck();
},
),
);
return this.getStatus(key, true);
} catch (error) {
const healthCheckResult: HealthCheckResult = error;
this.messagePublisher.publish(
LOGGING_AUTHENTICATION_HEALTH_CRIT,
JSON.stringify(healthCheckResult.error),
);
throw new HealthCheckError('Repository', {
repository: error.message,
});
}
};
}

View File

@ -1 +0,0 @@
export const LOGGING_AUTHENTICATION_HEALTH_CRIT = 'logging.auth.health.crit';

View File

@ -1,2 +0,0 @@
export const AUTHENTICATION_REPOSITORY = Symbol('AUTHENTICATION_REPOSITORY');
export const USERNAME_REPOSITORY = Symbol('USERNAME_REPOSITORY');

View File

@ -1,45 +0,0 @@
import { Module, Provider } from '@nestjs/common';
import { HealthHttpController } from './interface/http-controllers/health.http.controller';
import { TerminusModule } from '@nestjs/terminus';
import { RepositoriesHealthIndicatorUseCase } from './core/application/usecases/repositories.health-indicator.usecase';
import { HealthGrpcController } from './interface/grpc-controllers/health.grpc.controller';
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
import { AuthenticationRepository } from '@modules/authentication/infrastructure/authentication.repository';
import {
AUTHENTICATION_REPOSITORY,
USERNAME_REPOSITORY,
} from './health.di-tokens';
import { UsernameRepository } from '@modules/authentication/infrastructure/username.repository';
import { MESSAGE_PUBLISHER } from '@src/app.di-tokens';
import { AuthenticationModule } from '@modules/authentication/authentication.module';
const grpcControllers = [HealthGrpcController];
const httpControllers = [HealthHttpController];
const useCases: Provider[] = [RepositoriesHealthIndicatorUseCase];
const repositories: Provider[] = [
{
provide: AUTHENTICATION_REPOSITORY,
useClass: AuthenticationRepository,
},
{
provide: USERNAME_REPOSITORY,
useClass: UsernameRepository,
},
];
const messageBrokers: Provider[] = [
{
provide: MESSAGE_PUBLISHER,
useClass: MessageBrokerPublisher,
},
];
@Module({
imports: [TerminusModule, AuthenticationModule],
controllers: [...grpcControllers, ...httpControllers],
providers: [...useCases, ...repositories, ...messageBrokers],
})
export class HealthModule {}

View File

@ -1,42 +0,0 @@
import { Controller } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
import { RepositoriesHealthIndicatorUseCase } from '../../core/application/usecases/repositories.health-indicator.usecase';
export enum ServingStatus {
UNKNOWN = 0,
SERVING = 1,
NOT_SERVING = 2,
}
interface HealthCheckRequest {
service: string;
}
interface HealthCheckResponse {
status: ServingStatus;
}
@Controller()
export class HealthGrpcController {
constructor(
private readonly repositoriesHealthIndicatorUseCase: RepositoriesHealthIndicatorUseCase,
) {}
@GrpcMethod('Health', 'Check')
async check(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
data?: HealthCheckRequest,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
metadata?: any,
): Promise<HealthCheckResponse> {
const healthCheck = await this.repositoriesHealthIndicatorUseCase.isHealthy(
'repositories',
);
return {
status:
healthCheck['repositories'].status == 'up'
? ServingStatus.SERVING
: ServingStatus.NOT_SERVING,
};
}
}

View File

@ -1,28 +0,0 @@
import { RepositoriesHealthIndicatorUseCase } from '@modules/health/core/application/usecases/repositories.health-indicator.usecase';
import { Controller, Get } from '@nestjs/common';
import {
HealthCheckService,
HealthCheck,
HealthCheckResult,
} from '@nestjs/terminus';
@Controller('health')
export class HealthHttpController {
constructor(
private readonly repositoriesHealthIndicatorUseCase: RepositoriesHealthIndicatorUseCase,
private readonly healthCheckService: HealthCheckService,
) {}
@Get()
@HealthCheck()
async check(): Promise<HealthCheckResult> {
try {
return await this.healthCheckService.check([
async () =>
this.repositoriesHealthIndicatorUseCase.isHealthy('repositories'),
]);
} catch (error) {
throw error;
}
}
}

View File

@ -1,72 +0,0 @@
import { RepositoriesHealthIndicatorUseCase } from '@modules/health/core/application/usecases/repositories.health-indicator.usecase';
import {
HealthGrpcController,
ServingStatus,
} from '@modules/health/interface/grpc-controllers/health.grpc.controller';
import { Test, TestingModule } from '@nestjs/testing';
const mockRepositoriesHealthIndicatorUseCase = {
isHealthy: jest
.fn()
.mockImplementationOnce(() => ({
repositories: {
status: 'up',
},
}))
.mockImplementationOnce(() => ({
repositories: {
status: 'down',
},
})),
};
describe('Health Grpc Controller', () => {
let healthGrpcController: HealthGrpcController;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: RepositoriesHealthIndicatorUseCase,
useValue: mockRepositoriesHealthIndicatorUseCase,
},
HealthGrpcController,
],
}).compile();
healthGrpcController =
module.get<HealthGrpcController>(HealthGrpcController);
});
afterEach(async () => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(healthGrpcController).toBeDefined();
});
it('should return a Serving status ', async () => {
jest.spyOn(mockRepositoriesHealthIndicatorUseCase, 'isHealthy');
const servingStatus: { status: ServingStatus } =
await healthGrpcController.check();
expect(servingStatus).toEqual({
status: ServingStatus.SERVING,
});
expect(
mockRepositoriesHealthIndicatorUseCase.isHealthy,
).toHaveBeenCalledTimes(1);
});
it('should return a Not Serving status ', async () => {
jest.spyOn(mockRepositoriesHealthIndicatorUseCase, 'isHealthy');
const servingStatus: { status: ServingStatus } =
await healthGrpcController.check();
expect(servingStatus).toEqual({
status: ServingStatus.NOT_SERVING,
});
expect(
mockRepositoriesHealthIndicatorUseCase.isHealthy,
).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,90 +0,0 @@
import { RepositoriesHealthIndicatorUseCase } from '@modules/health/core/application/usecases/repositories.health-indicator.usecase';
import { HealthHttpController } from '@modules/health/interface/http-controllers/health.http.controller';
import { HealthCheckResult, HealthCheckService } from '@nestjs/terminus';
import { Test, TestingModule } from '@nestjs/testing';
const mockHealthCheckService = {
check: jest
.fn()
.mockImplementationOnce(() => ({
status: 'ok',
info: {
repositories: {
status: 'up',
},
},
error: {},
details: {
repositories: {
status: 'up',
},
},
}))
.mockImplementationOnce(() => ({
status: 'error',
info: {},
error: {
repository:
"\nInvalid `prisma.$queryRaw()` invocation:\n\n\nCan't reach database server at `v3-db`:`5432`\n\nPlease make sure your database server is running at `v3-db`:`5432`.",
},
details: {
repository:
"\nInvalid `prisma.$queryRaw()` invocation:\n\n\nCan't reach database server at `v3-db`:`5432`\n\nPlease make sure your database server is running at `v3-db`:`5432`.",
},
})),
};
const mockRepositoriesHealthIndicatorUseCase = {
isHealthy: jest.fn(),
};
describe('Health Http Controller', () => {
let healthHttpController: HealthHttpController;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: HealthCheckService,
useValue: mockHealthCheckService,
},
{
provide: RepositoriesHealthIndicatorUseCase,
useValue: mockRepositoriesHealthIndicatorUseCase,
},
HealthHttpController,
],
}).compile();
healthHttpController =
module.get<HealthHttpController>(HealthHttpController);
});
afterEach(async () => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(healthHttpController).toBeDefined();
});
it('should return an HealthCheckResult with Ok status ', async () => {
jest.spyOn(mockHealthCheckService, 'check');
jest.spyOn(mockRepositoriesHealthIndicatorUseCase, 'isHealthy');
const healthCheckResult: HealthCheckResult =
await healthHttpController.check();
expect(healthCheckResult.status).toBe('ok');
expect(mockHealthCheckService.check).toHaveBeenCalledTimes(1);
});
it('should return an HealthCheckResult with Error status ', async () => {
jest.spyOn(mockHealthCheckService, 'check');
jest.spyOn(mockRepositoriesHealthIndicatorUseCase, 'isHealthy');
const healthCheckResult: HealthCheckResult =
await healthHttpController.check();
expect(healthCheckResult.status).toBe('error');
expect(mockHealthCheckService.check).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,84 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { HealthCheckError, HealthIndicatorResult } from '@nestjs/terminus';
import { RepositoriesHealthIndicatorUseCase } from '../../core/application/usecases/repositories.health-indicator.usecase';
import { MESSAGE_PUBLISHER } from '@src/app.di-tokens';
import { DatabaseErrorException } from '@mobicoop/ddd-library';
import {
AUTHENTICATION_REPOSITORY,
USERNAME_REPOSITORY,
} from '@modules/health/health.di-tokens';
const mockAuthenticationRepository = {
healthCheck: jest
.fn()
.mockImplementationOnce(() => {
return Promise.resolve(true);
})
.mockImplementation(() => {
throw new DatabaseErrorException('An error occured in the database');
}),
};
const mockUsernameRepository = {
healthCheck: jest
.fn()
.mockImplementationOnce(() => {
return Promise.resolve(true);
})
.mockImplementation(() => {
throw new DatabaseErrorException('An error occured in the database');
}),
};
const mockMessagePublisher = {
publish: jest.fn().mockImplementation(),
};
describe('RepositoriesHealthIndicatorUseCase', () => {
let repositoriesHealthIndicatorUseCase: RepositoriesHealthIndicatorUseCase;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
RepositoriesHealthIndicatorUseCase,
{
provide: AUTHENTICATION_REPOSITORY,
useValue: mockAuthenticationRepository,
},
{
provide: USERNAME_REPOSITORY,
useValue: mockUsernameRepository,
},
{
provide: MESSAGE_PUBLISHER,
useValue: mockMessagePublisher,
},
],
}).compile();
repositoriesHealthIndicatorUseCase =
module.get<RepositoriesHealthIndicatorUseCase>(
RepositoriesHealthIndicatorUseCase,
);
});
it('should be defined', () => {
expect(repositoriesHealthIndicatorUseCase).toBeDefined();
});
describe('execute', () => {
it('should check health successfully', async () => {
const healthIndicatorResult: HealthIndicatorResult =
await repositoriesHealthIndicatorUseCase.isHealthy('repositories');
expect(healthIndicatorResult['repositories'].status).toBe('up');
});
it('should throw an error if database is unavailable', async () => {
jest.spyOn(mockMessagePublisher, 'publish');
await expect(
repositoriesHealthIndicatorUseCase.isHealthy('repositories'),
).rejects.toBeInstanceOf(HealthCheckError);
expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1);
});
});
});

View File

@ -0,0 +1,16 @@
import { Module, Provider } from '@nestjs/common';
import { MESSAGE_PUBLISHER } from './message-publisher.di-tokens';
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
const messagePublishers: Provider[] = [
{
provide: MESSAGE_PUBLISHER,
useClass: MessageBrokerPublisher,
},
];
@Module({
providers: [...messagePublishers],
exports: [MESSAGE_PUBLISHER],
})
export class MessagePublisherModule {}