Skip to content

Commit

Permalink
Merge pull request #483 from Hexastack/fix/settings-emit
Browse files Browse the repository at this point in the history
fix: setting emit + unit tests
  • Loading branch information
marrouchi authored Dec 24, 2024
2 parents 79f64ea + 88f3b21 commit 85f5733
Show file tree
Hide file tree
Showing 2 changed files with 218 additions and 4 deletions.
186 changes: 186 additions & 0 deletions api/src/setting/repositories/setting.repository.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
});
36 changes: 32 additions & 4 deletions api/src/setting/repositories/setting.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
}
Expand All @@ -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.
*
Expand All @@ -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' &&
Expand Down

0 comments on commit 85f5733

Please sign in to comment.