import assert from 'node:assert/strict'; import { EventEmitter } from 'node:events'; import path from 'node:path'; import { PassThrough } from 'node:stream'; import test from 'node:test'; import { startCloneProject } from '@/modules/projects/services/project-clone.service.js'; import { AppError } from '@/shared/utils.js'; type TestDependencies = Parameters[2]; function buildDependencies(overrides: Partial> = {}): NonNullable { return { validatePath: async () => ({ valid: true, resolvedPath: '/workspace/root' }), ensureDirectory: async () => undefined, resolveUserWorkspaceRoot: async () => '/workspace/root', pathExists: async () => false, removePath: async () => undefined, getGithubTokenById: async () => ({ github_token: 'token-value' }), spawnGitClone: async () => { throw new Error('spawnGitClone should be overridden in this test'); }, registerProject: async () => ({ project: { projectId: 'project-1' } }), logError: () => undefined, ...overrides, }; } function createMockGitProcess() { const emitter = new EventEmitter() as EventEmitter & { stdout: PassThrough; stderr: PassThrough; kill: () => void; }; emitter.stdout = new PassThrough(); emitter.stderr = new PassThrough(); emitter.kill = () => { emitter.emit('close', null); }; return emitter; } test('startCloneProject rejects when workspace path is missing', async () => { await assert.rejects( async () => startCloneProject( { workspacePath: '', githubUrl: 'https://github.com/example/repo', userId: 1, username: 'tester', }, { onProgress: () => undefined, onComplete: () => undefined, }, buildDependencies(), ), (error: unknown) => { assert.ok(error instanceof AppError); assert.equal(error.code, 'WORKSPACE_PATH_REQUIRED'); return true; }, ); }); test('startCloneProject rejects when github URL is missing', async () => { await assert.rejects( async () => startCloneProject( { workspacePath: '/workspace/root', githubUrl: '', userId: 1, username: 'tester', }, { onProgress: () => undefined, onComplete: () => undefined, }, buildDependencies(), ), (error: unknown) => { assert.ok(error instanceof AppError); assert.equal(error.code, 'GITHUB_URL_REQUIRED'); return true; }, ); }); test('startCloneProject rejects github URL values that begin with option prefixes', async () => { await assert.rejects( async () => startCloneProject( { workspacePath: '/workspace/root', githubUrl: '--upload-pack=malicious', userId: 1, username: 'tester', }, { onProgress: () => undefined, onComplete: () => undefined, }, buildDependencies(), ), (error: unknown) => { assert.ok(error instanceof AppError); assert.equal(error.code, 'INVALID_GITHUB_URL'); return true; }, ); }); test('startCloneProject rejects when selected github token does not exist', async () => { await assert.rejects( async () => startCloneProject( { workspacePath: '/workspace/root', githubUrl: 'https://github.com/example/repo', githubTokenId: 12, userId: 1, username: 'tester', }, { onProgress: () => undefined, onComplete: () => undefined, }, buildDependencies({ getGithubTokenById: async () => null, }), ), (error: unknown) => { assert.ok(error instanceof AppError); assert.equal(error.code, 'GITHUB_TOKEN_NOT_FOUND'); return true; }, ); }); test('startCloneProject completes and emits complete payload when git exits successfully', async () => { const gitProcess = createMockGitProcess(); const progressMessages: string[] = []; let completePayload: { project: Record; message: string } | null = null; let capturedUserId = 0; let capturedProjectPath = ''; let capturedCustomName = ''; let capturedUsername = ''; const operation = await startCloneProject( { workspacePath: '/workspace/root', githubUrl: 'https://github.com/example/repo.git', userId: 1, username: 'tester', }, { onProgress: (message) => { progressMessages.push(message); }, onComplete: (payload: { project: Record; message: string }) => { completePayload = payload; }, }, buildDependencies({ spawnGitClone: async () => gitProcess as any, registerProject: async (userId, username, projectPath, customName) => { capturedUserId = userId; capturedUsername = username; capturedProjectPath = projectPath; capturedCustomName = customName; return { project: { projectId: 'project-1', path: projectPath } }; }, }), ); gitProcess.emit('close', 0); await operation.waitForCompletion; assert.ok(progressMessages.some((message) => message.includes("Cloning into 'repo'"))); assert.equal(capturedUserId, 1); assert.equal(capturedUsername, 'tester'); assert.equal(capturedCustomName, 'repo'); assert.equal(path.basename(capturedProjectPath), 'repo'); assert.notEqual(completePayload, null); const resolvedCompletePayload = completePayload as unknown as { project: Record; message: string; }; assert.equal(resolvedCompletePayload.message, 'Repository cloned successfully'); assert.equal((resolvedCompletePayload.project.projectId as string) || '', 'project-1'); }); test('startCloneProject expands tilde workspace paths before validation', async () => { let capturedWorkspacePath = ''; const gitProcess = createMockGitProcess(); const operation = await startCloneProject( { workspacePath: '~/workspace-root', githubUrl: 'https://github.com/example/repo.git', userId: 1, username: 'tester', }, { onProgress: () => undefined, onComplete: () => undefined, }, buildDependencies({ validatePath: async (workspacePath) => { capturedWorkspacePath = workspacePath; return { valid: true, resolvedPath: '/workspace/root/workspace-root' }; }, spawnGitClone: async () => gitProcess as any, }), ); gitProcess.emit('close', 0); await operation.waitForCompletion; assert.equal(capturedWorkspacePath, '/workspace/root/workspace-root'); });