From 8f66a8a2c14942da99cae9757f10200326008f5c Mon Sep 17 00:00:00 2001 From: manu Date: Sun, 30 Jun 2024 16:54:59 +0200 Subject: [PATCH] Refactor server code and create new tests The server integrations and use-cases have been updated for improved efficiency and clarity. The `syncToRepository` method was removed from multiple components for redundancy. The handling of asynchronous calls and data manipulation methods was modified as well. New test files were added to verify tree-utils functionality and the `PlaybookRepositoryComponent`. --- client/src/components/TerminalModal/index.tsx | 2 +- .../git-repository/GitRepositoryComponent.ts | 7 - .../LocalRepositoryComponent.ts | 4 - .../PlaybooksRepositoryComponent.ts | 3 +- .../{utils.ts => tree-utils.ts} | 0 .../PlaybookRepositoryComponent.test.ts | 30 ++ .../playbooks-repository/tree-utils.test.ts | 316 ++++++++++++++++++ .../src/use-cases/LocalRepositoryUseCases.ts | 4 +- .../use-cases/PlaybooksRepositoryUseCases.ts | 2 +- 9 files changed, 350 insertions(+), 18 deletions(-) rename server/src/integrations/playbooks-repository/{utils.ts => tree-utils.ts} (100%) create mode 100644 server/src/tests/integrations/playbooks-repository/PlaybookRepositoryComponent.test.ts create mode 100644 server/src/tests/integrations/playbooks-repository/tree-utils.test.ts diff --git a/client/src/components/TerminalModal/index.tsx b/client/src/components/TerminalModal/index.tsx index d6a7c659..b021c4fd 100644 --- a/client/src/components/TerminalModal/index.tsx +++ b/client/src/components/TerminalModal/index.tsx @@ -111,7 +111,7 @@ const TerminalModal = (props: TerminalModalProps) => { try { const res = !props.terminalProps.quickRef ? await executePlaybook( - props.terminalProps.command, + props.terminalProps.command as string, props.terminalProps.target?.map((e) => e.uuid), props.terminalProps.extraVars, ) diff --git a/server/src/integrations/git-repository/GitRepositoryComponent.ts b/server/src/integrations/git-repository/GitRepositoryComponent.ts index 5edce628..fc4e8b61 100644 --- a/server/src/integrations/git-repository/GitRepositoryComponent.ts +++ b/server/src/integrations/git-repository/GitRepositoryComponent.ts @@ -121,13 +121,6 @@ class GitRepositoryComponent extends PlaybooksRepositoryComponent implements Abs await this.clone(); } - async syncToRepository() { - const files = await findFilesInDirectory(this.directory, FILE_PATTERN); - for (const file of files) { - this.childLogger.info(`syncToDatabase --> ${file}`); - } - } - async syncFromRepository() { await this.forcePull(); } diff --git a/server/src/integrations/local-repository/LocalRepositoryComponent.ts b/server/src/integrations/local-repository/LocalRepositoryComponent.ts index a02b423f..328a9f39 100644 --- a/server/src/integrations/local-repository/LocalRepositoryComponent.ts +++ b/server/src/integrations/local-repository/LocalRepositoryComponent.ts @@ -19,10 +19,6 @@ class LocalRepositoryComponent extends PlaybooksRepositoryComponent implements A async syncFromRepository(): Promise { await this.syncToDatabase(); } - - async syncToRepository(): Promise { - await this.syncToDatabase(); - } } export default LocalRepositoryComponent; diff --git a/server/src/integrations/playbooks-repository/PlaybooksRepositoryComponent.ts b/server/src/integrations/playbooks-repository/PlaybooksRepositoryComponent.ts index c432cf80..5cf5b28b 100644 --- a/server/src/integrations/playbooks-repository/PlaybooksRepositoryComponent.ts +++ b/server/src/integrations/playbooks-repository/PlaybooksRepositoryComponent.ts @@ -10,7 +10,7 @@ import logger from '../../logger'; import { Playbooks } from '../../types/typings'; import Shell from '../shell'; import { deleteFilesAndDirectory } from '../shell/utils'; -import { recursivelyFlattenTree } from './utils'; +import { recursivelyFlattenTree } from './tree-utils'; export const DIRECTORY_ROOT = '/playbooks'; export const FILE_PATTERN = /\.yml$/; @@ -160,7 +160,6 @@ export interface AbstractComponent extends PlaybooksRepositoryComponent { save(playbookUuid: string, content: string): Promise; init(): Promise; delete(): Promise; - syncToRepository(): Promise; syncFromRepository(): Promise; } diff --git a/server/src/integrations/playbooks-repository/utils.ts b/server/src/integrations/playbooks-repository/tree-utils.ts similarity index 100% rename from server/src/integrations/playbooks-repository/utils.ts rename to server/src/integrations/playbooks-repository/tree-utils.ts diff --git a/server/src/tests/integrations/playbooks-repository/PlaybookRepositoryComponent.test.ts b/server/src/tests/integrations/playbooks-repository/PlaybookRepositoryComponent.test.ts new file mode 100644 index 00000000..646e8935 --- /dev/null +++ b/server/src/tests/integrations/playbooks-repository/PlaybookRepositoryComponent.test.ts @@ -0,0 +1,30 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import LocalRepositoryComponent from '../../../integrations/local-repository/LocalRepositoryComponent'; +import PlaybooksRepositoryComponent from '../../../integrations/playbooks-repository/PlaybooksRepositoryComponent'; + +describe('PlaybooksRepositoryComponent', () => { + let playbooksRepositoryComponent: PlaybooksRepositoryComponent; + + beforeEach(() => { + const logger = { child: vi.fn() }; + playbooksRepositoryComponent = new LocalRepositoryComponent('uuid', logger, 'name', 'path'); + }); + + describe('fileBelongToRepository method', () => { + test('returns true if the root of the file path matches the root of the repository directory', () => { + const filePath = 'repository/file.ts'; + playbooksRepositoryComponent.directory = 'repository'; + const result = playbooksRepositoryComponent.fileBelongToRepository(filePath); + + expect(result).toBe(true); + }); + + test('returns false if the root of the file path does not match the root of the repository directory', () => { + const filePath = 'another_repository/file.ts'; + playbooksRepositoryComponent.directory = 'repository'; + const result = playbooksRepositoryComponent.fileBelongToRepository(filePath); + + expect(result).toBe(false); + }); + }); +}); diff --git a/server/src/tests/integrations/playbooks-repository/tree-utils.test.ts b/server/src/tests/integrations/playbooks-repository/tree-utils.test.ts new file mode 100644 index 00000000..a7fc03bd --- /dev/null +++ b/server/src/tests/integrations/playbooks-repository/tree-utils.test.ts @@ -0,0 +1,316 @@ +import { describe, expect, test, vi } from 'vitest'; +import { DirectoryTree } from 'ssm-shared-lib'; +import { + recursiveTreeCompletion, + recursivelyFlattenTree, +} from '../../../integrations/playbooks-repository/tree-utils'; + +const mockTree: DirectoryTree.TreeNode = { + path: '/root', + name: 'root', + type: DirectoryTree.CONSTANTS.DIRECTORY, + children: [ + { + path: '/root/folder1', + name: 'folder1', + type: DirectoryTree.CONSTANTS.DIRECTORY, + children: [ + { + path: '/root/folder1/file1', + name: 'file1', + extension: '.yml', + type: DirectoryTree.CONSTANTS.FILE, + }, + ], + }, + { + path: '/root/folder2', + name: 'folder2', + type: DirectoryTree.CONSTANTS.DIRECTORY, + children: [ + { + path: '/root/folder2/file2', + name: 'file2', + extension: '.yml', + type: DirectoryTree.CONSTANTS.FILE, + }, + ], + }, + ], +}; + +const complexTree1: DirectoryTree.TreeNode = { + path: '/root', + name: 'root', + type: DirectoryTree.CONSTANTS.DIRECTORY, + children: [ + { + path: '/root/folder1', + name: 'folder1', + type: DirectoryTree.CONSTANTS.DIRECTORY, + children: [ + { + path: '/root/folder1/subfolder1', + name: 'subfolder1', + type: DirectoryTree.CONSTANTS.DIRECTORY, + children: [ + { + path: '/root/folder1/subfolder1/file1', + name: 'file1', + extension: '.yml', + type: DirectoryTree.CONSTANTS.FILE, + }, + { + path: '/root/folder1/subfolder1/file2', + name: 'file2', + extension: '.txt', + type: DirectoryTree.CONSTANTS.FILE, + }, + ], + }, + ], + }, + { + path: '/root/folder2', + name: 'folder2', + type: DirectoryTree.CONSTANTS.DIRECTORY, + children: [ + { + path: '/root/folder2/file1', + name: 'file1', + extension: '.yml', + type: DirectoryTree.CONSTANTS.FILE, + }, + ], + }, + ], +}; + +const complexTree2: DirectoryTree.TreeNode = { + path: '/home', + name: 'home', + type: DirectoryTree.CONSTANTS.DIRECTORY, + children: [ + { + path: '/home/documents', + name: 'documents', + type: DirectoryTree.CONSTANTS.DIRECTORY, + children: [ + { + path: '/home/documents/file1', + name: 'file1', + extension: '.docx', + type: DirectoryTree.CONSTANTS.FILE, + }, + { + path: '/home/documents/file2', + name: 'image', + extension: '.jpeg', + type: DirectoryTree.CONSTANTS.FILE, + }, + ], + }, + { + path: '/home/pictures', + name: 'pictures', + type: DirectoryTree.CONSTANTS.DIRECTORY, + }, + ], +}; + +describe('recursiveTreeCompletion', () => { + vi.mock('../../../data/database/repository/PlaybookRepo', async (importOriginal) => { + return { + default: { + ...(await importOriginal< + typeof import('../../../data/database/repository/PlaybookRepo') + >()), + findOneByPath: async () => { + return { uuid: 'uuid' }; + }, + }, + }; + }); + + vi.mock('../../../integrations/ansible/utils/ExtraVars', async (importOriginal) => { + return { + default: { + ...(await importOriginal()), + findValueOfExtraVars: async () => { + return undefined; + }, + }, + }; + }); + + test('should recursively process a tree and return new tree with completed nodes', async () => { + const newTree = await recursiveTreeCompletion(mockTree); + expect(newTree).not.toBeNull(); + expect(newTree).not.toBeUndefined(); + expect(newTree.length).toBeGreaterThan(0); + }); + + test('should throws an Error when the depth is greater than 20', async () => { + let error: Error | undefined; + try { + await recursiveTreeCompletion(mockTree, 21); + } catch (err) { + error = err as Error; + } + expect(error).toBeDefined(); + expect(error?.message).toEqual( + 'Depth is too high, to prevent any infinite loop, directories depth is limited to 20', + ); + }); + + test('should handle an empty tree', async () => { + const emptyTree: DirectoryTree.TreeNode = { + path: '/', + name: '', + type: DirectoryTree.CONSTANTS.DIRECTORY, + children: [], + }; + + const result = await recursiveTreeCompletion(emptyTree); + + expect(result).not.toBeNull(); + expect(result[0]?.children).toBeUndefined(); + }); + + test('should not modify original tree', async () => { + const originalTree = { ...mockTree }; + + await recursiveTreeCompletion(mockTree); + + expect(JSON.stringify(originalTree)).toEqual(JSON.stringify(mockTree)); + }); + + test('should keep the original structure of tree', async () => { + const newTree = await recursiveTreeCompletion(mockTree); + + expect(JSON.stringify(newTree)).not.toEqual(JSON.stringify(mockTree)); + }); + + test('should correctly process complexTree1 and return a completed tree', async () => { + const newTree = await recursiveTreeCompletion(complexTree1); + expect(newTree).not.toBeNull(); + expect(newTree).not.toBeUndefined(); + expect(newTree[0].children?.length).toBe(1); + expect(newTree[0].children?.[0]?.children?.length).toBe(2); + expect(newTree[0].children?.[0]?.children?.[0]?.children?.length).toBeUndefined(); + expect(newTree[0].children?.[1]?.children?.length).toBeUndefined(); + }); + + test('should process complexTree2 correctly when it has a node with no children', async () => { + const newTree = await recursiveTreeCompletion(complexTree2); + expect(newTree[0]?.children?.[1]?.children?.length).toBe(2); + }); + + test('should add correct UUID to each node in the tree', async () => { + const newTree = await recursiveTreeCompletion(mockTree); + const assertUUID = (node: DirectoryTree.TreeNode) => { + if (node.type === DirectoryTree.CONSTANTS.FILE) { + expect((node as DirectoryTree.ExtendedTreeNode).uuid).toBe('uuid'); // make sure UUID is added correctly + } + if (Array.isArray(node.children)) { + (node.children as DirectoryTree.TreeNode[]).forEach(assertUUID); + } + }; + newTree.forEach(assertUUID); + }); +}); + +describe('recursivelyFlattenTree', () => { + const fileNode: DirectoryTree.TreeNode = { + path: '/file', + name: 'file', + extension: '.yml', + type: DirectoryTree.CONSTANTS.FILE, + }; + + const directoryNode: DirectoryTree.TreeNode = { + path: '/dir', + name: 'dir', + type: DirectoryTree.CONSTANTS.DIRECTORY, + children: [fileNode], + }; + + test('should correctly flatten a tree with one node', () => { + const result = recursivelyFlattenTree(fileNode); + expect(result).toEqual([fileNode]); + }); + + test('should correctly flatten a tree with a file and a directory', () => { + const result = recursivelyFlattenTree(directoryNode); + expect(result).toEqual([fileNode]); + }); + + test('should throw an error when depth is greater than 20', () => { + const deepTree: DirectoryTree.TreeNode = directoryNode; + let node = deepTree; + for (let i = 0; i < 20; i++) { + node.children = [{ ...directoryNode, path: `/dir${i}` }]; + node = node.children[0] as DirectoryTree.TreeNode; + } + expect(() => recursivelyFlattenTree(deepTree)).toThrowError( + 'Depth is too high, to prevent any infinite loop, directories depth is limited to 20', + ); + }); + + test('should correctly handle a tree with more than one level of depth', () => { + const result = recursivelyFlattenTree({ + path: '/root', + name: 'root', + type: DirectoryTree.CONSTANTS.DIRECTORY, + children: [ + { + path: '/root/dir1', + name: 'dir1', + type: DirectoryTree.CONSTANTS.DIRECTORY, + children: [ + { ...fileNode, path: '/root/dir1/file1', name: 'file1' }, + { ...fileNode, path: '/root/dir1/file2', name: 'file2' }, + ], + }, + { + path: '/root/dir2', + name: 'dir2', + type: DirectoryTree.CONSTANTS.DIRECTORY, + children: [ + { ...fileNode, path: '/root/dir2/file1', name: 'file1' }, + { ...fileNode, path: '/root/dir2/file2', name: 'file2' }, + ], + }, + ], + }); + expect(result).toHaveLength(4); + expect(result).toContainEqual( + expect.objectContaining({ + path: '/root/dir1/file1', + name: 'file1', + type: DirectoryTree.CONSTANTS.FILE, + }), + ); + expect(result).toContainEqual( + expect.objectContaining({ + path: '/root/dir1/file2', + name: 'file2', + type: DirectoryTree.CONSTANTS.FILE, + }), + ); + expect(result).toContainEqual( + expect.objectContaining({ + path: '/root/dir2/file1', + name: 'file1', + type: DirectoryTree.CONSTANTS.FILE, + }), + ); + expect(result).toContainEqual( + expect.objectContaining({ + path: '/root/dir2/file2', + name: 'file2', + type: DirectoryTree.CONSTANTS.FILE, + }), + ); + }); +}); diff --git a/server/src/use-cases/LocalRepositoryUseCases.ts b/server/src/use-cases/LocalRepositoryUseCases.ts index e95f6b95..fe0d2f12 100644 --- a/server/src/use-cases/LocalRepositoryUseCases.ts +++ b/server/src/use-cases/LocalRepositoryUseCases.ts @@ -4,7 +4,6 @@ import { NotFoundError } from '../core/api/ApiError'; import PlaybooksRepositoryRepo from '../data/database/repository/PlaybooksRepositoryRepo'; import { DIRECTORY_ROOT } from '../integrations/playbooks-repository/PlaybooksRepositoryComponent'; import PlaybooksRepositoryEngine from '../integrations/playbooks-repository/PlaybooksRepositoryEngine'; -import { createDirectoryWithFullPath } from '../integrations/shell/utils'; import logger from '../logger'; async function addLocalRepository(name: string) { @@ -16,7 +15,6 @@ async function addLocalRepository(name: string) { enabled: true, directory: DIRECTORY_ROOT, }); - logger.info(localRepository.uuid); await PlaybooksRepositoryRepo.create({ uuid, type: Playbooks.PlaybooksRepositoryType.LOCAL, @@ -25,7 +23,7 @@ async function addLocalRepository(name: string) { enabled: true, }); try { - await createDirectoryWithFullPath(localRepository.getDirectory()); + await localRepository.init(); void localRepository.syncToDatabase(); } catch (error: any) { logger.warn(error); diff --git a/server/src/use-cases/PlaybooksRepositoryUseCases.ts b/server/src/use-cases/PlaybooksRepositoryUseCases.ts index 445faee7..f7d8bb5d 100644 --- a/server/src/use-cases/PlaybooksRepositoryUseCases.ts +++ b/server/src/use-cases/PlaybooksRepositoryUseCases.ts @@ -5,7 +5,7 @@ import PlaybooksRepository from '../data/database/model/PlaybooksRepository'; import PlaybooksRepositoryRepo from '../data/database/repository/PlaybooksRepositoryRepo'; import PlaybooksRepositoryComponent from '../integrations/playbooks-repository/PlaybooksRepositoryComponent'; import PlaybooksRepositoryEngine from '../integrations/playbooks-repository/PlaybooksRepositoryEngine'; -import { recursiveTreeCompletion } from '../integrations/playbooks-repository/utils'; +import { recursiveTreeCompletion } from '../integrations/playbooks-repository/tree-utils'; import shell from '../integrations/shell'; import { createDirectoryWithFullPath, deleteFilesAndDirectory } from '../integrations/shell/utils'; import logger from '../logger';