hexabot/api/src/chat/repositories/block.repository.spec.ts
2024-11-21 09:38:09 +01:00

301 lines
10 KiB
TypeScript

/*
* 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 { MongooseModule, getModelToken } from '@nestjs/mongoose';
import { Test } from '@nestjs/testing';
import { Model, Types } from 'mongoose';
import {
blockFixtures,
installBlockFixtures,
} from '@/utils/test/fixtures/block';
import {
closeInMongodConnection,
rootMongooseTestModule,
} from '@/utils/test/test';
import { Block, BlockModel } from '../schemas/block.schema';
import { Category, CategoryModel } from '../schemas/category.schema';
import { LabelModel } from '../schemas/label.schema';
import { BlockRepository } from './block.repository';
import { CategoryRepository } from './category.repository';
describe('BlockRepository', () => {
let blockRepository: BlockRepository;
let categoryRepository: CategoryRepository;
let blockModel: Model<Block>;
let category: Category;
let hasPreviousBlocks: Block;
let hasNextBlocks: Block;
let validIds: string[];
let validCategory: string;
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [
rootMongooseTestModule(installBlockFixtures),
MongooseModule.forFeature([BlockModel, CategoryModel, LabelModel]),
],
providers: [BlockRepository, CategoryRepository, EventEmitter2],
}).compile();
blockRepository = module.get<BlockRepository>(BlockRepository);
categoryRepository = module.get<CategoryRepository>(CategoryRepository);
blockModel = module.get<Model<Block>>(getModelToken('Block'));
validIds = ['64abc1234def567890fedcba', '64abc1234def567890fedcbc'];
validCategory = '64def5678abc123490fedcba';
category = await categoryRepository.findOne({ label: 'default' });
hasPreviousBlocks = await blockRepository.findOne({
name: 'hasPreviousBlocks',
});
hasNextBlocks = await blockRepository.findOne({
name: 'hasNextBlocks',
});
});
afterEach(jest.clearAllMocks);
afterAll(closeInMongodConnection);
describe('findOneAndPopulate', () => {
it('should find one block by id, and populate its trigger_labels, assign_labels, nextBlocks, attachedBlock, category,previousBlocks', async () => {
jest.spyOn(blockModel, 'findById');
const result = await blockRepository.findOneAndPopulate(hasNextBlocks.id);
expect(blockModel.findById).toHaveBeenCalledWith(hasNextBlocks.id);
expect(result).toEqualPayload({
...blockFixtures.find(({ name }) => name === hasNextBlocks.name),
category,
nextBlocks: [hasPreviousBlocks],
previousBlocks: [],
});
});
});
describe('findAndPopulate', () => {
it('should find blocks, and foreach block populate its trigger_labels, assign_labels, attachedBlock, category, previousBlocks', async () => {
jest.spyOn(blockModel, 'find');
const category = await categoryRepository.findOne({ label: 'default' });
const result = await blockRepository.findAndPopulate({});
const blocksWithCategory = blockFixtures.map((blockFixture) => ({
...blockFixture,
category,
previousBlocks:
blockFixture.name === 'hasPreviousBlocks' ? [hasNextBlocks] : [],
nextBlocks:
blockFixture.name === 'hasNextBlocks' ? [hasPreviousBlocks] : [],
}));
expect(blockModel.find).toHaveBeenCalledWith({});
expect(result).toEqualPayload(blocksWithCategory);
});
it('should find blocks, and foreach block populate its trigger_labels, assign_labels, nextBlocks, attachedBlock, category', async () => {
jest.spyOn(blockModel, 'find');
const category = await categoryRepository.findOne({ label: 'default' });
const result = await blockRepository.findAndPopulate({});
const blocksWithCategory = blockFixtures.map((blockFixture) => ({
...blockFixture,
category,
previousBlocks:
blockFixture.name === 'hasPreviousBlocks' ? [hasNextBlocks] : [],
nextBlocks:
blockFixture.name === 'hasNextBlocks' ? [hasPreviousBlocks] : [],
}));
expect(blockModel.find).toHaveBeenCalledWith({});
expect(result).toEqualPayload(blocksWithCategory);
});
});
describe('preUpdate', () => {
it('should remove references to a moved block when updating category', async () => {
const mockUpdateMany = jest.spyOn(blockRepository, 'updateMany');
const criteria = { _id: validIds[0] };
const updates = { $set: { category: validCategory } };
const mockFindOne = jest
.spyOn(blockRepository, 'findOne')
.mockResolvedValue({
id: validIds[0],
category: 'oldCategory',
} as Block);
await blockRepository.preUpdate({} as any, criteria, updates);
expect(mockFindOne).toHaveBeenCalledWith(criteria);
expect(mockUpdateMany).toHaveBeenCalledTimes(2);
expect(mockUpdateMany).toHaveBeenNthCalledWith(
1,
{ nextBlocks: validIds[0] },
{ $pull: { nextBlocks: validIds[0] } },
);
expect(mockUpdateMany).toHaveBeenNthCalledWith(
2,
{ attachedBlock: validIds[0] },
{ $set: { attachedBlock: null } },
);
});
it('should do nothing if no block is found for the criteria', async () => {
jest.spyOn(blockRepository, 'findOne').mockResolvedValue(null);
const mockUpdateMany = jest.spyOn(blockRepository, 'updateMany');
await blockRepository.preUpdate({} as any, { _id: 'nonexistent' }, {});
expect(mockUpdateMany).not.toHaveBeenCalled();
});
});
describe('prepareBlocksInCategoryUpdateScope', () => {
it('should update blocks within the scope based on category and ids', async () => {
jest.spyOn(blockRepository, 'findOne').mockResolvedValue({
id: validIds[0],
category: 'oldCategory',
nextBlocks: [validIds[1]],
attachedBlock: validIds[1],
} as Block);
const mockUpdateOne = jest.spyOn(blockRepository, 'updateOne');
await blockRepository.prepareBlocksInCategoryUpdateScope(
validCategory,
validIds,
);
expect(mockUpdateOne).toHaveBeenCalledWith(validIds[0], {
nextBlocks: [validIds[1]],
attachedBlock: validIds[1],
});
});
it('should not update blocks if the category already matches', async () => {
jest.spyOn(blockRepository, 'findOne').mockResolvedValue({
id: validIds[0],
category: validCategory,
nextBlocks: [],
attachedBlock: null,
} as Block);
const mockUpdateOne = jest.spyOn(blockRepository, 'updateOne');
await blockRepository.prepareBlocksInCategoryUpdateScope(
validCategory,
validIds,
);
expect(mockUpdateOne).not.toHaveBeenCalled();
});
});
describe('prepareBlocksOutOfCategoryUpdateScope', () => {
it('should update blocks outside the scope by removing references from attachedBlock', async () => {
const otherBlocks = [
{
id: '64abc1234def567890fedcab',
attachedBlock: validIds[0],
nextBlocks: [validIds[0]],
},
] as Block[];
const mockUpdateOne = jest.spyOn(blockRepository, 'updateOne');
await blockRepository.prepareBlocksOutOfCategoryUpdateScope(
otherBlocks,
validIds,
);
expect(mockUpdateOne).toHaveBeenCalledWith('64abc1234def567890fedcab', {
attachedBlock: null,
});
});
it('should update blocks outside the scope by removing references from nextBlocks', async () => {
const otherBlocks = [
{
id: '64abc1234def567890fedcab',
attachedBlock: null,
nextBlocks: [validIds[0], validIds[1]],
},
] as Block[];
const mockUpdateOne = jest.spyOn(blockRepository, 'updateOne');
await blockRepository.prepareBlocksOutOfCategoryUpdateScope(otherBlocks, [
validIds[0],
]);
expect(mockUpdateOne).toHaveBeenCalledWith('64abc1234def567890fedcab', {
nextBlocks: [new Types.ObjectId(validIds[1])],
});
});
});
describe('preUpdateMany', () => {
it('should update blocks in and out of the scope', async () => {
const mockFind = jest.spyOn(blockRepository, 'find').mockResolvedValue([
{
id: '64abc1234def567890fedcab',
attachedBlock: validIds[0],
nextBlocks: [validIds[0]],
},
] as Block[]);
const prepareBlocksInCategoryUpdateScope = jest.spyOn(
blockRepository,
'prepareBlocksInCategoryUpdateScope',
);
const prepareBlocksOutOfCategoryUpdateScope = jest.spyOn(
blockRepository,
'prepareBlocksOutOfCategoryUpdateScope',
);
await blockRepository.preUpdateMany(
{} as any,
{ _id: { $in: validIds } },
{ $set: { category: validCategory } },
);
expect(mockFind).toHaveBeenCalled();
expect(prepareBlocksInCategoryUpdateScope).toHaveBeenCalledWith(
validCategory,
['64abc1234def567890fedcab'],
);
expect(prepareBlocksOutOfCategoryUpdateScope).toHaveBeenCalledWith(
[
{
id: '64abc1234def567890fedcab',
attachedBlock: validIds[0],
nextBlocks: [validIds[0]],
},
],
['64abc1234def567890fedcab'],
);
});
it('should not perform updates if no category is provided', async () => {
const mockFind = jest.spyOn(blockRepository, 'find');
const prepareBlocksInCategoryUpdateScope = jest.spyOn(
blockRepository,
'prepareBlocksInCategoryUpdateScope',
);
const prepareBlocksOutOfCategoryUpdateScope = jest.spyOn(
blockRepository,
'prepareBlocksOutOfCategoryUpdateScope',
);
await blockRepository.preUpdateMany({} as any, {}, { $set: {} });
expect(mockFind).not.toHaveBeenCalled();
expect(prepareBlocksInCategoryUpdateScope).not.toHaveBeenCalled();
expect(prepareBlocksOutOfCategoryUpdateScope).not.toHaveBeenCalled();
});
});
});