"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