/* * 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; 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); categoryRepository = module.get(CategoryRepository); blockModel = module.get>(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(); }); }); });