directus-task-management/dist/tests/services/project/project-tree.service.test.js

302 lines
16 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
require("reflect-metadata");
const typeorm_1 = require("typeorm");
const project_entity_1 = require("../../../src/entities/project.entity");
const project_tree_service_1 = require("../../../src/services/project/project-tree.service");
describe('ProjectTreeService', () => {
let dataSource;
let service;
beforeAll(async () => {
// Create in-memory SQLite database for testing
dataSource = new typeorm_1.DataSource({
type: 'sqlite',
database: ':memory:',
synchronize: true,
logging: false,
entities: [project_entity_1.ProjectEntity]
});
await dataSource.initialize();
service = new project_tree_service_1.ProjectTreeService(dataSource);
});
afterAll(async () => {
if (dataSource?.isInitialized) {
await dataSource.destroy();
}
});
beforeEach(async () => {
// Clear all data before each test
await dataSource.getRepository(project_entity_1.ProjectEntity).clear();
});
describe('insertNode', () => {
it('should create a root node with correct path', async () => {
const project = await service.insertNode({
name: 'Root Project',
description: 'Test root project'
});
expect(project.path).toBe('0001');
expect(project.level).toBe(0);
expect(project.parentId).toBeNull();
});
it('should create multiple root nodes with sequential paths', async () => {
const project1 = await service.insertNode({ name: 'Root 1' });
const project2 = await service.insertNode({ name: 'Root 2' });
const project3 = await service.insertNode({ name: 'Root 3' });
expect(project1.path).toBe('0001');
expect(project2.path).toBe('0002');
expect(project3.path).toBe('0003');
});
it('should create child node with correct path', async () => {
const parent = await service.insertNode({ name: 'Parent' });
const child = await service.insertNode({ name: 'Child' }, parent.id);
expect(child.path).toBe('0001.0001');
expect(child.level).toBe(1);
expect(child.parentId).toBe(parent.id);
});
it('should create nested hierarchy correctly', async () => {
const root = await service.insertNode({ name: 'Root' });
const child1 = await service.insertNode({ name: 'Child 1' }, root.id);
const child2 = await service.insertNode({ name: 'Child 2' }, root.id);
const grandchild = await service.insertNode({ name: 'Grandchild' }, child1.id);
expect(root.path).toBe('0001');
expect(child1.path).toBe('0001.0001');
expect(child2.path).toBe('0001.0002');
expect(grandchild.path).toBe('0001.0001.0001');
expect(grandchild.level).toBe(2);
});
it('should throw error when parent does not exist', async () => {
await expect(service.insertNode({ name: 'Orphan' }, 'non-existent-id')).rejects.toThrow('Parent project with ID non-existent-id not found');
});
});
describe('moveNode', () => {
it('should move node to different parent', async () => {
const parent1 = await service.insertNode({ name: 'Parent 1' });
const parent2 = await service.insertNode({ name: 'Parent 2' });
const child = await service.insertNode({ name: 'Child' }, parent1.id);
const result = await service.moveNode(child.id, parent2.id);
expect(result.success).toBe(true);
const movedChild = await dataSource.getRepository(project_entity_1.ProjectEntity).findOne({
where: { id: child.id }
});
expect(movedChild?.path).toBe('0002.0001');
expect(movedChild?.parentId).toBe(parent2.id);
});
it('should move node to root level', async () => {
const parent = await service.insertNode({ name: 'Parent' });
const child = await service.insertNode({ name: 'Child' }, parent.id);
const result = await service.moveNode(child.id, null);
expect(result.success).toBe(true);
const movedChild = await dataSource.getRepository(project_entity_1.ProjectEntity).findOne({
where: { id: child.id }
});
expect(movedChild?.path).toBe('0002');
expect(movedChild?.parentId).toBeNull();
expect(movedChild?.level).toBe(0);
});
it('should update descendants when moving node', async () => {
const root = await service.insertNode({ name: 'Root' });
const branch1 = await service.insertNode({ name: 'Branch 1' }, root.id);
const leaf1 = await service.insertNode({ name: 'Leaf 1' }, branch1.id);
const leaf2 = await service.insertNode({ name: 'Leaf 2' }, branch1.id);
const newRoot = await service.insertNode({ name: 'New Root' });
const result = await service.moveNode(branch1.id, newRoot.id);
expect(result.success).toBe(true);
expect(result.affectedNodes).toBe(3); // branch1 + 2 leaves
const movedBranch = await dataSource.getRepository(project_entity_1.ProjectEntity).findOne({
where: { id: branch1.id }
});
const movedLeaf1 = await dataSource.getRepository(project_entity_1.ProjectEntity).findOne({
where: { id: leaf1.id }
});
const movedLeaf2 = await dataSource.getRepository(project_entity_1.ProjectEntity).findOne({
where: { id: leaf2.id }
});
expect(movedBranch?.path).toBe('0002.0001');
expect(movedLeaf1?.path).toBe('0002.0001.0001');
expect(movedLeaf2?.path).toBe('0002.0001.0002');
});
it('should prevent moving node to itself', async () => {
const node = await service.insertNode({ name: 'Node' });
const result = await service.moveNode(node.id, node.id);
expect(result.success).toBe(false);
expect(result.message).toContain('Cannot move a node to itself');
});
it('should prevent moving node to its descendant', async () => {
const parent = await service.insertNode({ name: 'Parent' });
const child = await service.insertNode({ name: 'Child' }, parent.id);
const grandchild = await service.insertNode({ name: 'Grandchild' }, child.id);
const result = await service.moveNode(parent.id, grandchild.id);
expect(result.success).toBe(false);
expect(result.message).toContain('Cannot move a node to its own descendant');
});
});
describe('deleteNode', () => {
it('should delete leaf node', async () => {
const parent = await service.insertNode({ name: 'Parent' });
const child = await service.insertNode({ name: 'Child' }, parent.id);
const result = await service.deleteNode(child.id);
expect(result.success).toBe(true);
expect(result.deletedCount).toBe(1);
const deletedNode = await dataSource.getRepository(project_entity_1.ProjectEntity).findOne({
where: { id: child.id }
});
expect(deletedNode).toBeNull();
});
it('should cascade delete node with descendants', async () => {
const root = await service.insertNode({ name: 'Root' });
await service.insertNode({ name: 'Child 1' }, root.id);
await service.insertNode({ name: 'Child 2' }, root.id);
const child1 = await service.insertNode({ name: 'Child 1.1' }, root.id);
await service.insertNode({ name: 'Grandchild' }, child1.id);
const result = await service.deleteNode(root.id, true);
expect(result.success).toBe(true);
expect(result.deletedCount).toBe(5);
const remainingNodes = await dataSource.getRepository(project_entity_1.ProjectEntity).find();
expect(remainingNodes).toHaveLength(0);
});
it('should promote children when deleting parent without cascade', async () => {
const grandparent = await service.insertNode({ name: 'Grandparent' });
const parent = await service.insertNode({ name: 'Parent' }, grandparent.id);
const child1 = await service.insertNode({ name: 'Child 1' }, parent.id);
const child2 = await service.insertNode({ name: 'Child 2' }, parent.id);
const result = await service.deleteNode(parent.id, false);
expect(result.success).toBe(true);
expect(result.message).toContain('2 children promoted');
const promotedChild1 = await dataSource.getRepository(project_entity_1.ProjectEntity).findOne({
where: { id: child1.id }
});
const promotedChild2 = await dataSource.getRepository(project_entity_1.ProjectEntity).findOne({
where: { id: child2.id }
});
expect(promotedChild1?.parentId).toBe(grandparent.id);
expect(promotedChild2?.parentId).toBe(grandparent.id);
});
});
describe('getAncestors', () => {
it('should return empty array for root node', async () => {
const root = await service.insertNode({ name: 'Root' });
const ancestors = await service.getAncestors(root.id);
expect(ancestors).toHaveLength(0);
});
it('should return all ancestors in correct order', async () => {
const root = await service.insertNode({ name: 'Root' });
const parent = await service.insertNode({ name: 'Parent' }, root.id);
const child = await service.insertNode({ name: 'Child' }, parent.id);
const grandchild = await service.insertNode({ name: 'Grandchild' }, child.id);
const ancestors = await service.getAncestors(grandchild.id);
expect(ancestors).toHaveLength(3);
expect(ancestors[0].id).toBe(root.id);
expect(ancestors[1].id).toBe(parent.id);
expect(ancestors[2].id).toBe(child.id);
});
});
describe('getDescendants', () => {
it('should return empty array for leaf node', async () => {
const leaf = await service.insertNode({ name: 'Leaf' });
const descendants = await service.getDescendants(leaf.id);
expect(descendants).toHaveLength(0);
});
it('should return all descendants', async () => {
const root = await service.insertNode({ name: 'Root' });
const child1 = await service.insertNode({ name: 'Child 1' }, root.id);
const child2 = await service.insertNode({ name: 'Child 2' }, root.id);
const grandchild1 = await service.insertNode({ name: 'Grandchild 1' }, child1.id);
const grandchild2 = await service.insertNode({ name: 'Grandchild 2' }, child1.id);
const descendants = await service.getDescendants(root.id);
expect(descendants).toHaveLength(4);
const descendantIds = descendants.map(d => d.id);
expect(descendantIds).toContain(child1.id);
expect(descendantIds).toContain(child2.id);
expect(descendantIds).toContain(grandchild1.id);
expect(descendantIds).toContain(grandchild2.id);
});
});
describe('getSubtree', () => {
it('should return subtree including root', async () => {
const root = await service.insertNode({ name: 'Root' });
const child1 = await service.insertNode({ name: 'Child 1' }, root.id);
await service.insertNode({ name: 'Child 2' }, root.id);
await service.insertNode({ name: 'Grandchild' }, child1.id);
const subtree = await service.getSubtree(root.id);
expect(subtree).toHaveLength(4);
expect(subtree[0].id).toBe(root.id);
});
it('should respect maxDepth parameter', async () => {
const root = await service.insertNode({ name: 'Root' });
const child = await service.insertNode({ name: 'Child' }, root.id);
const grandchild = await service.insertNode({ name: 'Grandchild' }, child.id);
const greatGrandchild = await service.insertNode({ name: 'Great Grandchild' }, grandchild.id);
const subtree = await service.getSubtree(root.id, 2);
expect(subtree).toHaveLength(3); // root + child + grandchild
const subtreeIds = subtree.map(n => n.id);
expect(subtreeIds).not.toContain(greatGrandchild.id);
});
});
describe('getSiblings', () => {
it('should return siblings excluding self by default', async () => {
const parent = await service.insertNode({ name: 'Parent' });
const child1 = await service.insertNode({ name: 'Child 1' }, parent.id);
const child2 = await service.insertNode({ name: 'Child 2' }, parent.id);
const child3 = await service.insertNode({ name: 'Child 3' }, parent.id);
const siblings = await service.getSiblings(child2.id);
expect(siblings).toHaveLength(2);
const siblingIds = siblings.map(s => s.id);
expect(siblingIds).toContain(child1.id);
expect(siblingIds).toContain(child3.id);
expect(siblingIds).not.toContain(child2.id);
});
it('should include self when requested', async () => {
const parent = await service.insertNode({ name: 'Parent' });
const child1 = await service.insertNode({ name: 'Child 1' }, parent.id);
const child2 = await service.insertNode({ name: 'Child 2' }, parent.id);
const siblings = await service.getSiblings(child1.id, true);
expect(siblings).toHaveLength(2);
const siblingIds = siblings.map(s => s.id);
expect(siblingIds).toContain(child1.id);
expect(siblingIds).toContain(child2.id);
});
});
describe('rebuildPaths', () => {
it('should rebuild corrupted paths', async () => {
const repo = dataSource.getRepository(project_entity_1.ProjectEntity);
// Create nodes with corrupted paths
const root = repo.create({ name: 'Root', path: 'wrong', level: 0, parentId: null });
await repo.save(root);
const child = repo.create({ name: 'Child', path: 'also-wrong', level: 1, parentId: root.id });
await repo.save(child);
const updatedCount = await service.rebuildPaths();
expect(updatedCount).toBe(2);
const fixedRoot = await repo.findOne({ where: { id: root.id } });
const fixedChild = await repo.findOne({ where: { id: child.id } });
expect(fixedRoot?.path).toBe('0001');
expect(fixedChild?.path).toBe('0001.0001');
});
});
describe('validateTreeIntegrity', () => {
it('should validate correct tree', async () => {
const root = await service.insertNode({ name: 'Root' });
await service.insertNode({ name: 'Child' }, root.id);
const validation = await service.validateTreeIntegrity();
expect(validation.valid).toBe(true);
expect(validation.errors).toHaveLength(0);
});
it('should detect duplicate paths', async () => {
const repo = dataSource.getRepository(project_entity_1.ProjectEntity);
const node1 = repo.create({ name: 'Node 1', path: '0001', level: 0 });
const node2 = repo.create({ name: 'Node 2', path: '0001', level: 0 }); // Duplicate path
await repo.save([node1, node2]);
const validation = await service.validateTreeIntegrity();
expect(validation.valid).toBe(false);
expect(validation.errors).toContain('Duplicate path found: 0001');
});
it('should detect incorrect levels', async () => {
const repo = dataSource.getRepository(project_entity_1.ProjectEntity);
const node = repo.create({ name: 'Node', path: '0001.0002.0003', level: 1 }); // Should be 2
await repo.save(node);
const validation = await service.validateTreeIntegrity();
expect(validation.valid).toBe(false);
expect(validation.errors[0]).toContain('incorrect level');
});
});
});
//# sourceMappingURL=project-tree.service.test.js.map