use configuration package as middleware

This commit is contained in:
sbriat 2023-10-25 16:03:17 +02:00
parent efb35d6d2b
commit 5094e27ad1
28 changed files with 558 additions and 747 deletions

View File

@ -1,6 +1,6 @@
# Mobicoop V3 - Configuration Service
Configuration items management. Used to configure and store all services using redis as database.
Configuration items management. Used to configure and store configuration items for all Mobicoop V3 services using redis as database.
Each item consists in :
@ -8,10 +8,6 @@ Each item consists in :
- a **key** : the key of the configuration item (a string)
- a **value** : the value of the configuration item (always a string, each service must cast the value if needed)
This service centralizes the configuration items, but theoratically each service should "push" its items toward the configuration service.
Practically, it's the other way round as it's easier to use this configuration service as the single source of truth. This is why configuration items key and domain are immutable : services may use hardcoded domain-key pairs. Therefore, only values can be updated.
## Available domains
- **CARPOOL** : carpool related configuration items (eg. default number of seats proposed as a driver)
@ -27,8 +23,6 @@ You also need NodeJS installed locally : we **strongly** advise to install [Node
The API will run inside a docker container, **but** the install itself is made outside the container, because during development we need tools that need to be available locally (eg. ESLint, Prettier...).
A RabbitMQ instance is also required to send / receive messages when data has been inserted/updated/deleted.
## Installation
- copy `.env.dist` to `.env` :

747
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -26,6 +26,7 @@
"dependencies": {
"@grpc/grpc-js": "^1.9.6",
"@grpc/proto-loader": "^0.7.10",
"@mobicoop/configuration-module": "^4.0.0",
"@mobicoop/ddd-library": "^2.1.1",
"@mobicoop/health-module": "^2.3.1",
"@mobicoop/message-broker-module": "^2.1.1",
@ -37,14 +38,10 @@
"@nestjs/microservices": "^10.2.7",
"@nestjs/platform-express": "^10.2.7",
"@nestjs/terminus": "^10.1.1",
"@songkeys/nestjs-redis": "^10.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"ioredis": "^5.3.2",
"reflect-metadata": "^0.1.13",
"rimraf": "^5.0.5",
"rxjs": "^7.8.1",
"uuid": "^9.0.1"
"rimraf": "^5.0.5"
},
"devDependencies": {
"@nestjs/cli": "^10.1.18",
@ -54,7 +51,6 @@
"@types/jest": "29.5.6",
"@types/node": "^20.8.7",
"@types/supertest": "^2.0.15",
"@types/uuid": "^9.0.6",
"@typescript-eslint/eslint-plugin": "^6.8.0",
"@typescript-eslint/parser": "^6.8.0",
"dotenv-cli": "^7.3.0",

View File

@ -4,6 +4,10 @@ import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { HEALTH_CRITICAL_LOGGING_KEY, SERVICE_NAME } from './app.constants';
import { MessagePublisherPort } from '@mobicoop/ddd-library';
import {
ConfigurationModuleOptions,
ConfigurationModule as ConfigurationModulePackage,
} from '@mobicoop/configuration-module';
import { MESSAGE_PUBLISHER } from '@modules/messager/messager.di-tokens';
import { ConfigurationModule } from '@modules/configuration/configuration.module';
import { EventEmitterModule } from '@nestjs/event-emitter';
@ -11,7 +15,6 @@ import brokerConfig from './config/broker.config';
import carpoolConfig from './config/carpool.config';
import paginationConfig from './config/pagination.config';
import serviceConfig from './config/service.config';
import { RedisModule, RedisModuleOptions } from '@songkeys/nestjs-redis';
import redisConfig from './config/redis.config';
import { Transport } from '@nestjs/microservices';
@ -49,18 +52,16 @@ import { Transport } from '@nestjs/microservices';
],
}),
}),
RedisModule.forRootAsync({
ConfigurationModulePackage.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (
configService: ConfigService,
): Promise<RedisModuleOptions> => {
): Promise<ConfigurationModuleOptions> => {
return {
config: {
host: configService.get<string>('redis.host') as string,
port: configService.get<number>('redis.port') as number,
password: configService.get<string>('redis.password'),
},
host: configService.get<string>('redis.host') as string,
port: configService.get<number>('redis.port') as number,
password: configService.get<string>('redis.password'),
};
},
}),

View File

@ -1,5 +1,13 @@
import { registerAs } from '@nestjs/config';
export interface CarpoolConfig {
departureTimeMargin: number;
role: string;
seatsProposed: number;
seatsRequested: number;
strictFrequency: boolean;
}
export default registerAs('carpool', () => ({
departureTimeMargin: process.env.DEPARTURE_TIME_MARGIN
? parseInt(process.env.DEPARTURE_TIME_MARGIN, 10)

View File

@ -1,5 +1,9 @@
import { registerAs } from '@nestjs/config';
export interface PaginationConfig {
perPage: number;
}
export default registerAs('pagination', () => ({
perPage: process.env.PER_PAGE ? parseInt(process.env.PER_PAGE, 10) : 10,
}));

View File

@ -1,55 +1,20 @@
import { Mapper } from '@mobicoop/ddd-library';
import { Injectable } from '@nestjs/common';
import { ConfigurationEntity } from './core/domain/configuration.entity';
import { ConfigurationResponseDto } from './interface/dtos/configuration.response.dto';
import { ConfigurationDomain } from './core/domain/configuration.types';
import {
ConfigurationReadModel,
ConfigurationWriteModel,
} from './infrastructure/configuration.repository';
import { v4 } from 'uuid';
ConfigurationIdentifier,
ConfigurationValue,
} from '@mobicoop/configuration-module';
@Injectable()
export class ConfigurationMapper
implements
Mapper<
ConfigurationEntity,
ConfigurationReadModel,
ConfigurationWriteModel,
ConfigurationResponseDto
>
{
toPersistence = (entity: ConfigurationEntity): ConfigurationWriteModel => {
const copy = entity.getProps();
const record: ConfigurationWriteModel = {
key: `${copy.identifier.domain}:${copy.identifier.key}`,
value: copy.value,
};
return record;
};
toDomain = (record: ConfigurationReadModel): ConfigurationEntity => {
const entity = new ConfigurationEntity({
id: v4(),
createdAt: new Date(),
updatedAt: new Date(),
props: {
identifier: {
domain: record.key.split(':')[0] as ConfigurationDomain,
key: record.key.split(':')[1],
},
value: record.value,
},
});
return entity;
};
toResponse = (entity: ConfigurationEntity): ConfigurationResponseDto => {
const props = entity.getProps();
const response = new ConfigurationResponseDto(entity);
response.domain = props.identifier.domain;
response.key = props.identifier.key;
response.value = props.value;
export class ConfigurationMapper {
toResponse = (
configurationIdentifier: ConfigurationIdentifier,
configurationValue: ConfigurationValue,
): ConfigurationResponseDto => {
const response = new ConfigurationResponseDto();
response.domain = configurationIdentifier.domain;
response.key = configurationIdentifier.key;
response.value = configurationValue;
return response;
};
}

View File

@ -7,7 +7,7 @@ import { GetConfigurationQueryHandler } from './core/application/queries/get-con
import { ConfigurationMapper } from './configuration.mapper';
import { CONFIGURATION_REPOSITORY } from './configuration.di-tokens';
import { PopulateService } from './core/application/services/populate.service';
import { ConfigurationRepository } from './infrastructure/configuration.repository';
import { ConfigurationRepository } from '@mobicoop/configuration-module';
const grpcControllers = [
GetConfigurationGrpcController,

View File

@ -2,18 +2,23 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { Inject } from '@nestjs/common';
import { SetConfigurationCommand } from './set-configuration.command';
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
import { ConfigurationRepositoryPort } from '../../ports/configuration.repository.port';
import {
ConfigurationDomain,
ConfigurationIdentifier,
SetConfigurationRepositoryPort,
} from '@mobicoop/configuration-module';
@CommandHandler(SetConfigurationCommand)
export class SetConfigurationService implements ICommandHandler {
constructor(
@Inject(CONFIGURATION_REPOSITORY)
private readonly configurationRepository: ConfigurationRepositoryPort,
private readonly configurationRepository: SetConfigurationRepositoryPort,
) {}
async execute(command: SetConfigurationCommand): Promise<void> {
await this.configurationRepository.set(
async execute(
command: SetConfigurationCommand,
): Promise<ConfigurationIdentifier> {
return await this.configurationRepository.set(
{
domain: command.domain as ConfigurationDomain,
key: command.key,

View File

@ -1,13 +0,0 @@
import { ConfigurationEntity } from '../../domain/configuration.entity';
import {
ConfigurationIdentifier,
ConfigurationValue,
} from '../../domain/configuration.types';
export interface ConfigurationRepositoryPort {
get(identifier: ConfigurationIdentifier): Promise<ConfigurationEntity>;
set(
identifier: ConfigurationIdentifier,
value: ConfigurationValue,
): Promise<void>;
}

View File

@ -2,17 +2,19 @@ import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { GetConfigurationQuery } from './get-configuration.query';
import { Inject } from '@nestjs/common';
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
import { ConfigurationRepositoryPort } from '../../ports/configuration.repository.port';
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
import {
ConfigurationDomain,
ConfigurationValue,
GetConfigurationRepositoryPort,
} from '@mobicoop/configuration-module';
@QueryHandler(GetConfigurationQuery)
export class GetConfigurationQueryHandler implements IQueryHandler {
constructor(
@Inject(CONFIGURATION_REPOSITORY)
private readonly configurationRepository: ConfigurationRepositoryPort,
private readonly configurationRepository: GetConfigurationRepositoryPort,
) {}
async execute(query: GetConfigurationQuery): Promise<ConfigurationEntity> {
async execute(query: GetConfigurationQuery): Promise<ConfigurationValue> {
return await this.configurationRepository.get({
domain: query.domain as ConfigurationDomain,
key: query.key,

View File

@ -1,19 +1,23 @@
import {
ConfigurationDomain,
GetConfigurationRepositoryPort,
NotFoundException,
SetConfigurationRepositoryPort,
} from '@mobicoop/configuration-module';
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
import { Inject, Injectable, OnApplicationBootstrap } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ConfigurationRepositoryPort } from '../ports/configuration.repository.port';
import {
CarpoolConfig,
ConfigurationDomain,
PaginationConfig,
} from '../../domain/configuration.types';
import { NotFoundException } from '@mobicoop/ddd-library';
import { CarpoolConfig } from '@src/config/carpool.config';
import { PaginationConfig } from '@src/config/pagination.config';
@Injectable()
export class PopulateService implements OnApplicationBootstrap {
constructor(
@Inject(CONFIGURATION_REPOSITORY)
private readonly configurationRepository: ConfigurationRepositoryPort,
private readonly getConfigurationRepository: GetConfigurationRepositoryPort,
@Inject(CONFIGURATION_REPOSITORY)
private readonly setConfigurationRepository: SetConfigurationRepositoryPort,
private readonly configService: ConfigService,
) {}
@ -42,13 +46,13 @@ export class PopulateService implements OnApplicationBootstrap {
let key: keyof typeof config;
for (key in config) {
try {
await this.configurationRepository.get({
await this.getConfigurationRepository.get({
domain,
key,
});
} catch (error: any) {
if (error instanceof NotFoundException) {
this.configurationRepository.set(
this.setConfigurationRepository.set(
{
domain,
key,

View File

@ -1,21 +0,0 @@
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
import { v4 } from 'uuid';
import {
ConfigurationProps,
CreateConfigurationProps,
} from './configuration.types';
export class ConfigurationEntity extends AggregateRoot<ConfigurationProps> {
protected readonly _id: AggregateID;
static create = (create: CreateConfigurationProps): ConfigurationEntity => {
const id = v4();
const props: ConfigurationProps = { ...create };
const configuration = new ConfigurationEntity({ id, props });
return configuration;
};
validate(): void {
// entity business rules validation to protect it's invariant before saving entity to a database
}
}

View File

@ -1,36 +0,0 @@
// All properties that a Configuration has
export interface ConfigurationProps {
identifier: ConfigurationIdentifier;
value: ConfigurationValue;
}
// Properties that are needed for a Configuration creation
export interface CreateConfigurationProps {
identifier: ConfigurationIdentifier;
value: ConfigurationValue;
}
export enum ConfigurationDomain {
CARPOOL = 'CARPOOL',
PAGINATION = 'PAGINATION',
}
export type ConfigurationIdentifier = {
domain: ConfigurationDomain;
key: ConfigurationKey;
};
export type ConfigurationKey = string;
export type ConfigurationValue = string;
export interface CarpoolConfig {
departureTimeMargin: number;
role: string;
seatsProposed: number;
seatsRequested: number;
strictFrequency: boolean;
}
export interface PaginationConfig {
perPage: number;
}

View File

@ -1,47 +0,0 @@
import { Injectable } from '@nestjs/common';
import { ConfigurationRepositoryPort } from '../core/application/ports/configuration.repository.port';
import { InjectRedis } from '@songkeys/nestjs-redis';
import { Redis } from 'ioredis';
import {
ConfigurationIdentifier,
ConfigurationValue,
} from '../core/domain/configuration.types';
import { ConfigurationEntity } from '../core/domain/configuration.entity';
import { ConfigurationMapper } from '../configuration.mapper';
import { NotFoundException } from '@mobicoop/ddd-library';
export type ConfigurationReadModel = {
key: string;
value: string;
};
export type ConfigurationWriteModel = ConfigurationReadModel;
@Injectable()
export class ConfigurationRepository implements ConfigurationRepositoryPort {
constructor(
@InjectRedis() private readonly redis: Redis,
private readonly mapper: ConfigurationMapper,
) {}
get = async (
identifier: ConfigurationIdentifier,
): Promise<ConfigurationEntity> => {
const key: string = `${identifier.domain}:${identifier.key}`;
const value: ConfigurationValue | null = await this.redis.get(key);
if (!value)
throw new NotFoundException(
`Configuration item not found for key ${key}`,
);
return this.mapper.toDomain({
key,
value,
});
};
set = async (
identifier: ConfigurationIdentifier,
value: ConfigurationValue,
): Promise<void> => {
await this.redis.set(`${identifier.domain}:${identifier.key}`, value);
};
}

View File

@ -1,6 +1,4 @@
import { ResponseBase } from '@mobicoop/ddd-library';
export class ConfigurationResponseDto extends ResponseBase {
export class ConfigurationResponseDto {
domain: string;
key: string;
value: string;

View File

@ -1,4 +1,4 @@
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
import { ConfigurationDomain } from '@mobicoop/configuration-module';
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
export class GetConfigurationRequestDto {

View File

@ -1,4 +1,4 @@
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
import { ConfigurationDomain } from '@mobicoop/configuration-module';
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
export class SetConfigurationRequestDto {

View File

@ -1,15 +1,17 @@
import { Controller, UsePipes } from '@nestjs/common';
import { QueryBus } from '@nestjs/cqrs';
import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { NotFoundException } from '@mobicoop/ddd-library';
import { RpcExceptionCode } from '@mobicoop/ddd-library';
import { RpcValidationPipe } from '@mobicoop/ddd-library';
import { GRPC_SERVICE_NAME } from '@src/app.constants';
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
import { GetConfigurationRequestDto } from './dtos/get-configuration.request.dto';
import { ConfigurationResponseDto } from '../dtos/configuration.response.dto';
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
import { GetConfigurationQuery } from '@modules/configuration/core/application/queries/get-configuration/get-configuration.query';
import {
NotFoundException,
RpcExceptionCode,
RpcValidationPipe,
} from '@mobicoop/ddd-library';
import { ConfigurationValue } from '@mobicoop/configuration-module';
@UsePipes(
new RpcValidationPipe({
@ -29,10 +31,11 @@ export class GetConfigurationGrpcController {
data: GetConfigurationRequestDto,
): Promise<ConfigurationResponseDto> {
try {
const configuration: ConfigurationEntity = await this.queryBus.execute(
new GetConfigurationQuery(data.domain, data.key),
);
return this.mapper.toResponse(configuration);
const configurationValue: ConfigurationValue =
await this.queryBus.execute(
new GetConfigurationQuery(data.domain, data.key),
);
return this.mapper.toResponse(data, configurationValue);
} catch (e) {
if (e instanceof NotFoundException) {
throw new RpcException({

View File

@ -1,11 +1,11 @@
import { Controller, UsePipes } from '@nestjs/common';
import { CommandBus } from '@nestjs/cqrs';
import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { RpcExceptionCode } from '@mobicoop/ddd-library';
import { RpcValidationPipe } from '@mobicoop/ddd-library';
import { RpcExceptionCode, RpcValidationPipe } from '@mobicoop/ddd-library';
import { GRPC_SERVICE_NAME } from '@src/app.constants';
import { SetConfigurationRequestDto } from './dtos/set-configuration.request.dto';
import { SetConfigurationCommand } from '@modules/configuration/core/application/commands/set-configuration/set-configuration.command';
import { ConfigurationIdentifier } from '@mobicoop/configuration-module';
@UsePipes(
new RpcValidationPipe({
@ -20,11 +20,13 @@ export class SetConfigurationGrpcController {
@GrpcMethod(GRPC_SERVICE_NAME, 'Set')
async set(
setConfigurationRequestDto: SetConfigurationRequestDto,
): Promise<void> {
): Promise<ConfigurationIdentifier> {
try {
await this.commandBus.execute(
new SetConfigurationCommand(setConfigurationRequestDto),
);
const configurationIdentifier: ConfigurationIdentifier =
await this.commandBus.execute(
new SetConfigurationCommand(setConfigurationRequestDto),
);
return configurationIdentifier;
} catch (error: any) {
throw new RpcException({
code: RpcExceptionCode.UNKNOWN,

View File

@ -1,31 +1,8 @@
import { ConfigurationDomain } from '@mobicoop/configuration-module';
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
import {
ConfigurationReadModel,
ConfigurationWriteModel,
} from '@modules/configuration/infrastructure/configuration.repository';
import { ConfigurationResponseDto } from '@modules/configuration/interface/dtos/configuration.response.dto';
import { Test } from '@nestjs/testing';
const now = new Date('2023-06-21 06:00:00');
const configurationEntity: ConfigurationEntity = new ConfigurationEntity({
id: 'c160cf8c-f057-4962-841f-3ad68346df44',
props: {
identifier: {
domain: ConfigurationDomain.CARPOOL,
key: 'seatsProposed',
},
value: '3',
},
createdAt: now,
updatedAt: now,
});
const configurationReadModel: ConfigurationReadModel = {
key: 'AD:seatsProposed',
value: '4',
};
describe('Configuration Mapper', () => {
let configurationMapper: ConfigurationMapper;
@ -40,22 +17,16 @@ describe('Configuration Mapper', () => {
expect(configurationMapper).toBeDefined();
});
it('should map domain entity to persistence data', async () => {
const mapped: ConfigurationWriteModel =
configurationMapper.toPersistence(configurationEntity);
it('should map configuration to response', async () => {
const mapped: ConfigurationResponseDto = configurationMapper.toResponse(
{
domain: ConfigurationDomain.CARPOOL,
key: 'seatsProposed',
},
'3',
);
expect(mapped.domain).toBe('CARPOOL');
expect(mapped.key).toBe('seatsProposed');
expect(mapped.value).toBe('3');
});
it('should map persisted data to domain entity', async () => {
const mapped: ConfigurationEntity = configurationMapper.toDomain(
configurationReadModel,
);
expect(mapped.getProps().value).toBe('4');
});
it('should map domain entity to response', async () => {
const mapped: ConfigurationResponseDto =
configurationMapper.toResponse(configurationEntity);
expect(mapped.id).toBe('c160cf8c-f057-4962-841f-3ad68346df44');
});
});

View File

@ -1,23 +0,0 @@
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
import {
CreateConfigurationProps,
ConfigurationDomain,
} from '@modules/configuration/core/domain/configuration.types';
const createConfigurationProps: CreateConfigurationProps = {
identifier: {
domain: ConfigurationDomain.CARPOOL,
key: 'seatsProposed',
},
value: '3',
};
describe('Configuration entity create', () => {
it('should create a new configuration entity', async () => {
const configurationEntity: ConfigurationEntity = ConfigurationEntity.create(
createConfigurationProps,
);
expect(configurationEntity.id.length).toBe(36);
expect(configurationEntity.getProps().value).toBe('3');
});
});

View File

@ -1,26 +1,16 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
import { GetConfigurationQueryHandler } from '@modules/configuration/core/application/queries/get-configuration/get-configuration.query-handler';
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
import { GetConfigurationQuery } from '@modules/configuration/core/application/queries/get-configuration/get-configuration.query';
import {
ConfigurationDomain,
ConfigurationValue,
} from '@mobicoop/configuration-module';
const now = new Date('2023-06-21 06:00:00');
const configuration: ConfigurationEntity = new ConfigurationEntity({
id: 'c160cf8c-f057-4962-841f-3ad68346df44',
props: {
identifier: {
domain: ConfigurationDomain.CARPOOL,
key: 'seatsProposed',
},
value: '3',
},
createdAt: now,
updatedAt: now,
});
const configurationValue: ConfigurationValue = '3';
const mockConfigurationRepository = {
get: jest.fn().mockImplementation(() => configuration),
get: jest.fn().mockImplementation(() => configurationValue),
};
describe('Get Configuration Query Handler', () => {
@ -47,14 +37,14 @@ describe('Get Configuration Query Handler', () => {
});
describe('execution', () => {
it('should return a configuration item', async () => {
it('should return a configuration value', async () => {
const getConfigurationQuery = new GetConfigurationQuery(
ConfigurationDomain.CARPOOL,
'seatsProposed',
);
const configuration: ConfigurationEntity =
const configurationValue: ConfigurationValue =
await getConfigurationQueryHandler.execute(getConfigurationQuery);
expect(configuration.getProps().value).toBe('3');
expect(configurationValue).toBe('3');
});
});
});

View File

@ -1,29 +1,13 @@
import { NotFoundException } from '@mobicoop/ddd-library';
import { NotFoundException } from '@mobicoop/configuration-module';
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
import { PopulateService } from '@modules/configuration/core/application/services/populate.service';
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
const mockConfigurationRepository = {
get: jest
.fn()
.mockImplementationOnce(
() =>
new ConfigurationEntity({
id: '001199d4-7187-4e83-a044-12159cba2e33',
props: {
identifier: {
domain: ConfigurationDomain.CARPOOL,
key: 'someKey',
},
value: 'someValue',
},
createdAt: new Date('2023-10-23'),
updatedAt: new Date('2023-10-23'),
}),
)
.mockImplementationOnce(() => 'someValue')
.mockImplementationOnce(() => {
throw new NotFoundException('Configuration not found');
}),

View File

@ -1,10 +1,9 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SetConfigurationRequestDto } from '@modules/configuration/interface/grpc-controllers/dtos/set-configuration.request.dto';
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
import { SetConfigurationService } from '@modules/configuration/core/application/commands/set-configuration/set-configuration.service';
import { CONFIGURATION_REPOSITORY } from '@modules/configuration/configuration.di-tokens';
import { SetConfigurationCommand } from '@modules/configuration/core/application/commands/set-configuration/set-configuration.command';
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
import { ConfigurationDomain } from '@mobicoop/configuration-module';
const setConfigurationRequest: SetConfigurationRequestDto = {
domain: ConfigurationDomain.CARPOOL,
@ -50,16 +49,10 @@ describe('Set Configuration Service', () => {
);
it('should set an existing configuration item', async () => {
jest.spyOn(mockConfigurationRepository, 'set');
ConfigurationEntity.create = jest.fn().mockReturnValue({
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
});
await setConfigurationService.execute(setConfigurationCommand);
expect(mockConfigurationRepository.set).toHaveBeenCalledTimes(1);
});
it('should throw an error if something bad happens on configuration item update', async () => {
ConfigurationEntity.create = jest.fn().mockReturnValue({
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
});
await expect(
setConfigurationService.execute(setConfigurationCommand),
).rejects.toBeInstanceOf(Error);

View File

@ -1,93 +0,0 @@
import { NotFoundException } from '@mobicoop/ddd-library';
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
import { ConfigurationEntity } from '@modules/configuration/core/domain/configuration.entity';
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
import { ConfigurationRepository } from '@modules/configuration/infrastructure/configuration.repository';
import { Test, TestingModule } from '@nestjs/testing';
import { getRedisToken } from '@songkeys/nestjs-redis';
const mockRedis = {
get: jest
.fn()
.mockImplementationOnce(() => '1')
.mockImplementation(() => null),
set: jest.fn().mockImplementation(),
};
const mockConfigurationMapper = {
toDomain: jest.fn().mockImplementation(
() =>
new ConfigurationEntity({
id: '001199d4-7187-4e83-a044-12159cba2e33',
props: {
identifier: {
domain: ConfigurationDomain.CARPOOL,
key: 'seatsProposed',
},
value: '1',
},
createdAt: new Date('2023-10-23'),
updatedAt: new Date('2023-10-23'),
}),
),
};
describe('Configuration Repository', () => {
let configurationRepository: ConfigurationRepository;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: getRedisToken('default'),
useValue: mockRedis,
},
{
provide: ConfigurationMapper,
useValue: mockConfigurationMapper,
},
ConfigurationRepository,
],
}).compile();
configurationRepository = module.get<ConfigurationRepository>(
ConfigurationRepository,
);
});
it('should be defined', () => {
expect(configurationRepository).toBeDefined();
});
describe('interact', () => {
it('should get a value', async () => {
expect(
(
await configurationRepository.get({
domain: ConfigurationDomain.CARPOOL,
key: 'seatsProposed',
})
).getProps().value,
).toBe('1');
});
it('should throw if configuration is not found', async () => {
await expect(
configurationRepository.get({
domain: ConfigurationDomain.CARPOOL,
key: 'seatsProposed',
}),
).rejects.toBeInstanceOf(NotFoundException);
});
it('should set a value', async () => {
expect(
await configurationRepository.set(
{
domain: ConfigurationDomain.CARPOOL,
key: 'seatsProposed',
},
'3',
),
).toBeUndefined();
});
});
});

View File

@ -1,7 +1,6 @@
import { NotFoundException } from '@mobicoop/ddd-library';
import { RpcExceptionCode } from '@mobicoop/ddd-library';
import { ConfigurationDomain } from '@mobicoop/configuration-module';
import { NotFoundException, RpcExceptionCode } from '@mobicoop/ddd-library';
import { ConfigurationMapper } from '@modules/configuration/configuration.mapper';
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
import { GetConfigurationGrpcController } from '@modules/configuration/interface/grpc-controllers/get-configuration.grpc.controller';
import { QueryBus } from '@nestjs/cqrs';
import { RpcException } from '@nestjs/microservices';

View File

@ -1,5 +1,8 @@
import {
ConfigurationDomain,
ConfigurationIdentifier,
} from '@mobicoop/configuration-module';
import { RpcExceptionCode } from '@mobicoop/ddd-library';
import { ConfigurationDomain } from '@modules/configuration/core/domain/configuration.types';
import { SetConfigurationRequestDto } from '@modules/configuration/interface/grpc-controllers/dtos/set-configuration.request.dto';
import { SetConfigurationGrpcController } from '@modules/configuration/interface/grpc-controllers/set-configuration.grpc.controller';
import { CommandBus } from '@nestjs/cqrs';
@ -15,7 +18,10 @@ const setConfigurationRequest: SetConfigurationRequestDto = {
const mockCommandBus = {
execute: jest
.fn()
.mockImplementationOnce(() => '200d61a8-d878-4378-a609-c19ea71633d2')
.mockImplementationOnce(() => ({
domain: ConfigurationDomain.CARPOOL,
key: 'seatsProposed',
}))
.mockImplementationOnce(() => {
throw new Error();
}),
@ -50,7 +56,9 @@ describe('Set Configuration Grpc Controller', () => {
it('should set a configuration item', async () => {
jest.spyOn(mockCommandBus, 'execute');
await setConfigurationGrpcController.set(setConfigurationRequest);
const configurationIdentifier: ConfigurationIdentifier =
await setConfigurationGrpcController.set(setConfigurationRequest);
expect(configurationIdentifier.key).toBe('seatsProposed');
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
});