Merge pull request #483 from Hexastack/fix/settings-emit

fix: setting emit + unit tests
This commit is contained in:
Med Marrouchi 2024-12-24 08:51:15 +01:00 committed by GitHub
commit 85f5733710
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 218 additions and 4 deletions

View File

@ -0,0 +1,186 @@
/*
* Copyright © 2024 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import { EventEmitter2 } from '@nestjs/event-emitter';
import { getModelToken, MongooseModule } from '@nestjs/mongoose';
import { Test } from '@nestjs/testing';
import { Model } from 'mongoose';
import { installSettingFixtures } from '@/utils/test/fixtures/setting';
import {
closeInMongodConnection,
rootMongooseTestModule,
} from '@/utils/test/test';
import { Setting, SettingModel } from '../schemas/setting.schema';
import { SettingType } from '../schemas/types';
import { SettingRepository } from './setting.repository';
describe('SettingRepository', () => {
let settingRepository: SettingRepository;
let settingModel: Model<Setting>;
let eventEmitter: EventEmitter2;
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [
rootMongooseTestModule(installSettingFixtures),
MongooseModule.forFeature([SettingModel]),
],
providers: [SettingRepository, EventEmitter2],
}).compile();
settingRepository = module.get<SettingRepository>(SettingRepository);
settingModel = module.get<Model<Setting>>(getModelToken(Setting.name));
eventEmitter = module.get<EventEmitter2>(EventEmitter2);
});
afterEach(() => {
jest.clearAllMocks();
});
afterAll(closeInMongodConnection);
describe('preCreateValidate', () => {
it('should validate setting value during creation', async () => {
const mockSetting = new settingModel({
type: SettingType.text,
value: 'Sample Text',
});
jest.spyOn(settingRepository, 'validateSettingValue');
await settingRepository.preCreateValidate(mockSetting);
expect(settingRepository['validateSettingValue']).toHaveBeenCalledWith(
SettingType.text,
'Sample Text',
);
});
it('should throw an error for invalid value type', async () => {
const mockSetting = new settingModel({
type: SettingType.checkbox,
value: 'Invalid Value',
});
await expect(
settingRepository.preCreateValidate(mockSetting),
).rejects.toThrow('Setting Model : Value must be a boolean!');
});
});
describe('preUpdateValidate', () => {
it('should validate updated setting value', async () => {
const criteria = { _id: '123' };
const updates = {
$set: { value: 'Updated Text' },
};
jest.spyOn(settingRepository, 'findOne').mockResolvedValue({
type: SettingType.text,
} as any);
await settingRepository.preUpdateValidate(criteria, updates);
expect(settingRepository.findOne).toHaveBeenCalledWith(criteria);
expect(settingRepository['validateSettingValue']).toHaveBeenCalledWith(
SettingType.text,
'Updated Text',
);
});
});
describe('postUpdate', () => {
it('should emit an event after updating a setting', async () => {
const mockSetting = new settingModel({
group: 'general',
label: 'theme',
});
jest.spyOn(eventEmitter, 'emit');
await settingRepository.postUpdate({} as any, mockSetting);
expect(eventEmitter.emit).toHaveBeenCalledWith(
'hook:general:theme',
mockSetting,
);
});
});
describe('validateSettingValue', () => {
it('should validate value types correctly', () => {
expect(() =>
settingRepository['validateSettingValue'](
SettingType.text,
'Valid Text',
),
).not.toThrow();
expect(() =>
settingRepository['validateSettingValue'](SettingType.checkbox, true),
).not.toThrow();
expect(() =>
settingRepository['validateSettingValue'](SettingType.number, 123),
).not.toThrow();
expect(() =>
settingRepository['validateSettingValue'](SettingType.text, 123),
).toThrow('Setting Model : Value must be a string!');
});
});
describe('validateSettingValue', () => {
const testCases = [
{
type: SettingType.text,
value: 123,
error: 'Setting Model : Value must be a string!',
},
{
type: SettingType.checkbox,
value: 'true',
error: 'Setting Model : Value must be a boolean!',
},
{
type: SettingType.number,
value: '123',
},
{
type: SettingType.multiple_text,
value: ['valid', 123],
},
{
type: SettingType.attachment,
value: 123,
},
{
type: SettingType.secret,
value: 123,
},
{
type: SettingType.select,
value: 123,
},
{
type: SettingType.multiple_attachment,
value: [123, 'valid'],
},
];
testCases.forEach(({ type, value }) => {
it(`should throw an error when value type does not match SettingType.${type}`, () => {
expect(() =>
settingRepository['validateSettingValue'](type, value),
).toThrow();
});
});
});
});

View File

@ -7,18 +7,21 @@
*/
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import {
EventEmitter2,
IHookSettingsGroupLabelOperationMap,
} from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import {
Document,
FilterQuery,
Model,
Query,
Types,
UpdateQuery,
UpdateWithAggregationPipeline,
} from 'mongoose';
import { I18nService } from '@/i18n/services/i18n.service';
import { BaseRepository } from '@/utils/generics/base-repository';
import { Setting } from '../schemas/setting.schema';
@ -29,7 +32,6 @@ export class SettingRepository extends BaseRepository<Setting> {
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(Setting.name) readonly model: Model<Setting>,
private readonly i18n: I18nService,
) {
super(eventEmitter, model, Setting);
}
@ -55,6 +57,32 @@ export class SettingRepository extends BaseRepository<Setting> {
}
}
/**
* Emits an event after a `Setting` has been updated.
*
* This method is used to synchronize global settings by emitting an event
* based on the `group` and `label` of the `Setting`.
*
* @param _query The Mongoose query object used to find and update the document.
* @param setting The updated `Setting` object.
*/
async postUpdate(
_query: Query<
Document<Setting, any, any>,
Document<Setting, any, any>,
unknown,
Setting,
'findOneAndUpdate'
>,
setting: Setting,
) {
const group = setting.group as keyof IHookSettingsGroupLabelOperationMap;
const label = setting.label as '*';
// Sync global settings var
this.eventEmitter.emit(`hook:${group}:${label}`, setting);
}
/**
* Validates the `Setting` document after it has been retrieved.
*
@ -65,7 +93,7 @@ export class SettingRepository extends BaseRepository<Setting> {
*
* @param setting The `Setting` document to be validated.
*/
private validateSettingValue(type: SettingType, value: any) {
public validateSettingValue(type: SettingType, value: any) {
if (
(type === SettingType.text || type === SettingType.textarea) &&
typeof value !== 'string' &&