propagation; improve tests

This commit is contained in:
Gsk54 2023-01-27 10:40:14 +01:00
parent 984bb4f562
commit 0485afdec3
14 changed files with 349 additions and 53 deletions

View File

@ -107,6 +107,12 @@ The app exposes the following [gRPC](https://grpc.io/) services :
} }
``` ```
- **Propagate** : propagate all configuration items using the message broker
```json
{}
```
## Messages ## Messages
As mentionned earlier, RabbitMQ messages are sent after these events : As mentionned earlier, RabbitMQ messages are sent after these events :
@ -117,6 +123,8 @@ As mentionned earlier, RabbitMQ messages are sent after these events :
- **Delete** (message : the uuid of the deleted configuration item) - **Delete** (message : the uuid of the deleted configuration item)
- **Propagate** (message : all the configuration items)
Various messages are also sent for logging purpose. Various messages are also sent for logging purpose.
## Tests ## Tests

View File

@ -16,6 +16,7 @@ import { UpdateConfigurationRequest } from '../../domain/dtos/update-configurati
import { Configuration } from '../../domain/entities/configuration'; import { Configuration } from '../../domain/entities/configuration';
import { FindAllConfigurationsQuery } from '../../queries/find-all-configurations.query'; import { FindAllConfigurationsQuery } from '../../queries/find-all-configurations.query';
import { FindConfigurationByUuidQuery } from '../../queries/find-configuration-by-uuid.query'; import { FindConfigurationByUuidQuery } from '../../queries/find-configuration-by-uuid.query';
import { PropagateConfigurationsQuery } from '../../queries/propagate-configurations.query';
import { ConfigurationPresenter } from './configuration.presenter'; import { ConfigurationPresenter } from './configuration.presenter';
@UsePipes( @UsePipes(
@ -141,4 +142,13 @@ export class ConfigurationController {
throw new RpcException({}); throw new RpcException({});
} }
} }
@GrpcMethod('ConfigurationService', 'Propagate')
async propagate(): Promise<void> {
try {
await this._queryBus.execute(new PropagateConfigurationsQuery());
} catch (e) {
throw new RpcException({});
}
}
} }

View File

@ -8,6 +8,7 @@ service ConfigurationService {
rpc Create(Configuration) returns (Configuration); rpc Create(Configuration) returns (Configuration);
rpc Update(Configuration) returns (Configuration); rpc Update(Configuration) returns (Configuration);
rpc Delete(ConfigurationByUuid) returns (Empty); rpc Delete(ConfigurationByUuid) returns (Empty);
rpc Propagate(Empty) returns (Empty);
} }
message ConfigurationByUuid { message ConfigurationByUuid {

View File

@ -11,6 +11,7 @@ import { CreateConfigurationUseCase } from './domain/usecases/create-configurati
import { DeleteConfigurationUseCase } from './domain/usecases/delete-configuration.usecase'; import { DeleteConfigurationUseCase } from './domain/usecases/delete-configuration.usecase';
import { FindAllConfigurationsUseCase } from './domain/usecases/find-all-configurations.usecase'; import { FindAllConfigurationsUseCase } from './domain/usecases/find-all-configurations.usecase';
import { FindConfigurationByUuidUseCase } from './domain/usecases/find-configuration-by-uuid.usecase'; import { FindConfigurationByUuidUseCase } from './domain/usecases/find-configuration-by-uuid.usecase';
import { PropagateConfigurationsUseCase } from './domain/usecases/propagate-configurations.usecase';
import { UpdateConfigurationUseCase } from './domain/usecases/update-configuration.usecase'; import { UpdateConfigurationUseCase } from './domain/usecases/update-configuration.usecase';
import { ConfigurationProfile } from './mappers/configuration.profile'; import { ConfigurationProfile } from './mappers/configuration.profile';
@ -49,6 +50,7 @@ import { ConfigurationProfile } from './mappers/configuration.profile';
CreateConfigurationUseCase, CreateConfigurationUseCase,
UpdateConfigurationUseCase, UpdateConfigurationUseCase,
DeleteConfigurationUseCase, DeleteConfigurationUseCase,
PropagateConfigurationsUseCase,
], ],
}) })
export class ConfigurationModule {} export class ConfigurationModule {}

View File

@ -0,0 +1,42 @@
import { Mapper } from '@automapper/core';
import { InjectMapper } from '@automapper/nestjs';
import { QueryHandler } from '@nestjs/cqrs';
import { ConfigurationMessagerPresenter } from '../../adapters/secondaries/configuration-messager.presenter';
import { ConfigurationMessager } from '../../adapters/secondaries/configuration.messager';
import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository';
import { LoggingMessager } from '../../adapters/secondaries/logging.messager';
import { PropagateConfigurationsQuery } from '../../queries/propagate-configurations.query';
import { Configuration } from '../entities/configuration';
@QueryHandler(PropagateConfigurationsQuery)
export class PropagateConfigurationsUseCase {
constructor(
private readonly _repository: ConfigurationRepository,
private readonly _configurationMessager: ConfigurationMessager,
private readonly _loggingMessager: LoggingMessager,
@InjectMapper() private readonly _mapper: Mapper,
) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async execute(propagateConfigurationsQuery: PropagateConfigurationsQuery) {
try {
const configurations = await this._repository.findAll(1, 999999);
this._configurationMessager.publish(
'propagate',
JSON.stringify(
configurations.data.map((configuration) =>
this._mapper.map(
configuration,
Configuration,
ConfigurationMessagerPresenter,
),
),
),
);
this._loggingMessager.publish('configuration.update.info', 'propagation');
} catch (error) {
this._loggingMessager.publish('configuration.update.crit', 'propagation');
throw error;
}
}
}

View File

@ -0,0 +1 @@
export class PropagateConfigurationsQuery {}

View File

@ -0,0 +1,38 @@
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigurationMessager } from '../../adapters/secondaries/configuration.messager';
const mockAmqpConnection = {
publish: jest.fn().mockImplementation(),
};
describe('ConfigurationMessager', () => {
let configurationMessager: ConfigurationMessager;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [],
providers: [
ConfigurationMessager,
{
provide: AmqpConnection,
useValue: mockAmqpConnection,
},
],
}).compile();
configurationMessager = module.get<ConfigurationMessager>(
ConfigurationMessager,
);
});
it('should be defined', () => {
expect(configurationMessager).toBeDefined();
});
it('should publish a message', async () => {
jest.spyOn(mockAmqpConnection, 'publish');
await configurationMessager.publish('configuration.create.info', 'my-test');
expect(mockAmqpConnection.publish).toHaveBeenCalledTimes(1);
});
});

View File

@ -33,7 +33,11 @@ const mockConfigurationRepository = {
}), }),
}; };
const mockMessager = { const mockConfigurationMessager = {
publish: jest.fn().mockImplementation(),
};
const mockLoggingMessager = {
publish: jest.fn().mockImplementation(), publish: jest.fn().mockImplementation(),
}; };
@ -52,11 +56,11 @@ describe('CreateConfigurationUseCase', () => {
ConfigurationProfile, ConfigurationProfile,
{ {
provide: ConfigurationMessager, provide: ConfigurationMessager,
useValue: mockMessager, useValue: mockConfigurationMessager,
}, },
{ {
provide: LoggingMessager, provide: LoggingMessager,
useValue: mockMessager, useValue: mockLoggingMessager,
}, },
], ],
}).compile(); }).compile();
@ -72,11 +76,13 @@ describe('CreateConfigurationUseCase', () => {
describe('execute', () => { describe('execute', () => {
it('should create and return a new configuration', async () => { it('should create and return a new configuration', async () => {
jest.spyOn(mockConfigurationMessager, 'publish');
const newConfiguration: Configuration = const newConfiguration: Configuration =
await createConfigurationUseCase.execute(newConfigurationCommand); await createConfigurationUseCase.execute(newConfigurationCommand);
expect(newConfiguration.key).toBe(newConfigurationRequest.key); expect(newConfiguration.key).toBe(newConfigurationRequest.key);
expect(newConfiguration.uuid).toBeDefined(); expect(newConfiguration.uuid).toBeDefined();
expect(mockConfigurationMessager.publish).toHaveBeenCalledTimes(1);
}); });
it('should throw an error if configuration already exists', async () => { it('should throw an error if configuration already exists', async () => {

View File

@ -1,15 +1,18 @@
import { classes } from '@automapper/classes'; import { classes } from '@automapper/classes';
import { AutomapperModule } from '@automapper/nestjs'; import { AutomapperModule } from '@automapper/nestjs';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { ICollection } from 'src/modules/database/src/interfaces/collection.interface';
import { ConfigurationMessager } from '../../adapters/secondaries/configuration.messager'; import { ConfigurationMessager } from '../../adapters/secondaries/configuration.messager';
import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository'; import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository';
import { LoggingMessager } from '../../adapters/secondaries/logging.messager'; import { LoggingMessager } from '../../adapters/secondaries/logging.messager';
import { DeleteConfigurationCommand } from '../../commands/delete-configuration.command'; import { DeleteConfigurationCommand } from '../../commands/delete-configuration.command';
import { Domain } from '../../domain/dtos/domain.enum'; import { Domain } from '../../domain/dtos/domain.enum';
import { Configuration } from '../../domain/entities/configuration';
import { DeleteConfigurationUseCase } from '../../domain/usecases/delete-configuration.usecase'; import { DeleteConfigurationUseCase } from '../../domain/usecases/delete-configuration.usecase';
import { ConfigurationProfile } from '../../mappers/configuration.profile'; import { ConfigurationProfile } from '../../mappers/configuration.profile';
const mockConfigurations = [ const mockConfigurations: ICollection<Configuration> = {
data: [
{ {
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
domain: Domain.USER, domain: Domain.USER,
@ -28,17 +31,19 @@ const mockConfigurations = [
key: 'key3', key: 'key3',
value: 'value3', value: 'value3',
}, },
]; ],
total: 3,
};
const mockConfigurationRepository = { const mockConfigurationRepository = {
delete: jest delete: jest
.fn() .fn()
.mockImplementationOnce((uuid: string) => { .mockImplementationOnce((uuid: string) => {
let savedConfiguration = {}; let savedConfiguration = {};
mockConfigurations.forEach((configuration, index) => { mockConfigurations.data.forEach((configuration, index) => {
if (configuration.uuid === uuid) { if (configuration.uuid === uuid) {
savedConfiguration = { ...configuration }; savedConfiguration = { ...configuration };
mockConfigurations.splice(index, 1); mockConfigurations.data.splice(index, 1);
} }
}); });
return savedConfiguration; return savedConfiguration;
@ -48,10 +53,13 @@ const mockConfigurationRepository = {
}), }),
}; };
const mockMessager = { const mockConfigurationMessager = {
publish: jest.fn().mockImplementation(), publish: jest.fn().mockImplementation(),
}; };
const mockLoggingMessager = {
publish: jest.fn().mockImplementation(),
};
describe('DeleteConfigurationUseCase', () => { describe('DeleteConfigurationUseCase', () => {
let deleteConfigurationUseCase: DeleteConfigurationUseCase; let deleteConfigurationUseCase: DeleteConfigurationUseCase;
@ -67,11 +75,11 @@ describe('DeleteConfigurationUseCase', () => {
ConfigurationProfile, ConfigurationProfile,
{ {
provide: ConfigurationMessager, provide: ConfigurationMessager,
useValue: mockMessager, useValue: mockConfigurationMessager,
}, },
{ {
provide: LoggingMessager, provide: LoggingMessager,
useValue: mockMessager, useValue: mockLoggingMessager,
}, },
], ],
}).compile(); }).compile();
@ -87,16 +95,18 @@ describe('DeleteConfigurationUseCase', () => {
describe('execute', () => { describe('execute', () => {
it('should delete a configuration', async () => { it('should delete a configuration', async () => {
const savedUuid = mockConfigurations[0].uuid; jest.spyOn(mockConfigurationMessager, 'publish');
const savedUuid = mockConfigurations.data[0].uuid;
const deleteConfigurationCommand = new DeleteConfigurationCommand( const deleteConfigurationCommand = new DeleteConfigurationCommand(
savedUuid, savedUuid,
); );
await deleteConfigurationUseCase.execute(deleteConfigurationCommand); await deleteConfigurationUseCase.execute(deleteConfigurationCommand);
const deletedConfiguration = mockConfigurations.find( const deletedConfiguration = mockConfigurations.data.find(
(configuration) => configuration.uuid === savedUuid, (configuration) => configuration.uuid === savedUuid,
); );
expect(deletedConfiguration).toBeUndefined(); expect(deletedConfiguration).toBeUndefined();
expect(mockConfigurationMessager.publish).toHaveBeenCalledTimes(1);
}); });
it('should throw an error if configuration does not exist', async () => { it('should throw an error if configuration does not exist', async () => {
await expect( await expect(

View File

@ -1,7 +1,9 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { ICollection } from 'src/modules/database/src/interfaces/collection.interface';
import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository'; import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository';
import { Domain } from '../../domain/dtos/domain.enum'; import { Domain } from '../../domain/dtos/domain.enum';
import { FindAllConfigurationsRequest } from '../../domain/dtos/find-all-configurations.request'; import { FindAllConfigurationsRequest } from '../../domain/dtos/find-all-configurations.request';
import { Configuration } from '../../domain/entities/configuration';
import { FindAllConfigurationsUseCase } from '../../domain/usecases/find-all-configurations.usecase'; import { FindAllConfigurationsUseCase } from '../../domain/usecases/find-all-configurations.usecase';
import { FindAllConfigurationsQuery } from '../../queries/find-all-configurations.query'; import { FindAllConfigurationsQuery } from '../../queries/find-all-configurations.query';
@ -13,7 +15,8 @@ findAllConfigurationsRequest.perPage = 10;
const findAllConfigurationsQuery: FindAllConfigurationsQuery = const findAllConfigurationsQuery: FindAllConfigurationsQuery =
new FindAllConfigurationsQuery(findAllConfigurationsRequest); new FindAllConfigurationsQuery(findAllConfigurationsRequest);
const mockConfigurations = [ const mockConfigurations: ICollection<Configuration> = {
data: [
{ {
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
domain: Domain.USER, domain: Domain.USER,
@ -32,7 +35,9 @@ const mockConfigurations = [
key: 'key3', key: 'key3',
value: 'value3', value: 'value3',
}, },
]; ],
total: 3,
};
const mockConfigurationRepository = { const mockConfigurationRepository = {
findAll: jest findAll: jest

View File

@ -0,0 +1,36 @@
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
import { Test, TestingModule } from '@nestjs/testing';
import { LoggingMessager } from '../../adapters/secondaries/logging.messager';
const mockAmqpConnection = {
publish: jest.fn().mockImplementation(),
};
describe('LoggingMessager', () => {
let loggingMessager: LoggingMessager;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [],
providers: [
LoggingMessager,
{
provide: AmqpConnection,
useValue: mockAmqpConnection,
},
],
}).compile();
loggingMessager = module.get<LoggingMessager>(LoggingMessager);
});
it('should be defined', () => {
expect(LoggingMessager).toBeDefined();
});
it('should publish a message', async () => {
jest.spyOn(mockAmqpConnection, 'publish');
await loggingMessager.publish('configuration.create.info', 'my-test');
expect(mockAmqpConnection.publish).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,109 @@
import { classes } from '@automapper/classes';
import { AutomapperModule } from '@automapper/nestjs';
import { Test, TestingModule } from '@nestjs/testing';
import { ICollection } from 'src/modules/database/src/interfaces/collection.interface';
import { ConfigurationMessager } from '../../adapters/secondaries/configuration.messager';
import { ConfigurationRepository } from '../../adapters/secondaries/configuration.repository';
import { LoggingMessager } from '../../adapters/secondaries/logging.messager';
import { Domain } from '../../domain/dtos/domain.enum';
import { Configuration } from '../../domain/entities/configuration';
import { PropagateConfigurationsUseCase } from '../../domain/usecases/propagate-configurations.usecase';
import { ConfigurationProfile } from '../../mappers/configuration.profile';
import { PropagateConfigurationsQuery } from '../../queries/propagate-configurations.query';
const propagateConfigurationsQuery: PropagateConfigurationsQuery =
new PropagateConfigurationsQuery();
const mockConfigurations: ICollection<Configuration> = {
data: [
{
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
domain: Domain.USER,
key: 'key1',
value: 'value1',
},
{
uuid: 'bb281075-1b98-4456-89d6-c643d3044a92',
domain: Domain.USER,
key: 'key2',
value: 'value2',
},
{
uuid: 'bb281075-1b98-4456-89d6-c643d3044a93',
domain: Domain.USER,
key: 'key3',
value: 'value3',
},
],
total: 3,
};
const mockConfigurationRepository = {
findAll: jest
.fn()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.mockImplementationOnce((query?: PropagateConfigurationsQuery) => {
return Promise.resolve(mockConfigurations);
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.mockImplementationOnce((query?: PropagateConfigurationsQuery) => {
throw new Error();
}),
};
const mockConfigurationMessager = {
publish: jest.fn().mockImplementation(),
};
const mockLoggingMessager = {
publish: jest.fn().mockImplementation(),
};
describe('PropagateConfigurationsUseCase', () => {
let propagateConfigurationsUseCase: PropagateConfigurationsUseCase;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
providers: [
{
provide: ConfigurationRepository,
useValue: mockConfigurationRepository,
},
{
provide: ConfigurationMessager,
useValue: mockConfigurationMessager,
},
{
provide: LoggingMessager,
useValue: mockLoggingMessager,
},
PropagateConfigurationsUseCase,
ConfigurationProfile,
],
}).compile();
propagateConfigurationsUseCase = module.get<PropagateConfigurationsUseCase>(
PropagateConfigurationsUseCase,
);
});
it('should be defined', () => {
expect(propagateConfigurationsUseCase).toBeDefined();
});
describe('execute', () => {
it('should propagate configurations', async () => {
jest.spyOn(mockConfigurationMessager, 'publish');
await propagateConfigurationsUseCase.execute(
propagateConfigurationsQuery,
);
expect(mockConfigurationMessager.publish).toHaveBeenCalledTimes(1);
});
it('should throw an error if repository call fails', async () => {
await expect(
propagateConfigurationsUseCase.execute(propagateConfigurationsQuery),
).rejects.toBeInstanceOf(Error);
});
});
});

View File

@ -38,7 +38,11 @@ const mockConfigurationRepository = {
}), }),
}; };
const mockMessager = { const mockConfigurationMessager = {
publish: jest.fn().mockImplementation(),
};
const mockLoggingMessager = {
publish: jest.fn().mockImplementation(), publish: jest.fn().mockImplementation(),
}; };
@ -57,11 +61,11 @@ describe('UpdateConfigurationUseCase', () => {
ConfigurationProfile, ConfigurationProfile,
{ {
provide: ConfigurationMessager, provide: ConfigurationMessager,
useValue: mockMessager, useValue: mockConfigurationMessager,
}, },
{ {
provide: LoggingMessager, provide: LoggingMessager,
useValue: mockMessager, useValue: mockLoggingMessager,
}, },
], ],
}).compile(); }).compile();
@ -77,10 +81,12 @@ describe('UpdateConfigurationUseCase', () => {
describe('execute', () => { describe('execute', () => {
it('should update a configuration value', async () => { it('should update a configuration value', async () => {
jest.spyOn(mockConfigurationMessager, 'publish');
const updatedConfiguration: Configuration = const updatedConfiguration: Configuration =
await updateConfigurationUseCase.execute(updateConfigurationCommand); await updateConfigurationUseCase.execute(updateConfigurationCommand);
expect(updatedConfiguration.value).toBe(updateConfigurationRequest.value); expect(updatedConfiguration.value).toBe(updateConfigurationRequest.value);
expect(mockConfigurationMessager.publish).toHaveBeenCalledTimes(1);
}); });
it('should throw an error if configuration does not exist', async () => { it('should throw an error if configuration does not exist', async () => {
await expect( await expect(

View File

@ -0,0 +1,22 @@
import { ArgumentMetadata } from '@nestjs/common';
import { FindConfigurationByUuidRequest } from '../../../modules/configuration/domain/dtos/find-configuration-by-uuid.request';
import { RpcValidationPipe } from '../../pipes/rpc.validation-pipe';
describe('RpcValidationPipe', () => {
it('should not validate request', async () => {
const target: RpcValidationPipe = new RpcValidationPipe({
whitelist: true,
forbidUnknownValues: false,
});
const metadata: ArgumentMetadata = {
type: 'body',
metatype: FindConfigurationByUuidRequest,
data: '',
};
await target
.transform(<FindConfigurationByUuidRequest>{}, metadata)
.catch((err) => {
expect(err.message).toEqual('Rpc Exception');
});
});
});