diff --git a/apps/server/tests/unit/services/dev-server-service.test.ts b/apps/server/tests/unit/services/dev-server-service.test.ts index efa36842..b6bae863 100644 --- a/apps/server/tests/unit/services/dev-server-service.test.ts +++ b/apps/server/tests/unit/services/dev-server-service.test.ts @@ -1,37 +1,33 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { EventEmitter } from "events"; -import path from "path"; -import os from "os"; -import fs from "fs/promises"; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { EventEmitter } from 'events'; +import path from 'path'; +import os from 'os'; +import fs from 'fs/promises'; // Mock child_process -vi.mock("child_process", () => ({ +vi.mock('child_process', () => ({ spawn: vi.fn(), execSync: vi.fn(), })); -// Mock fs existsSync -vi.mock("fs", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - existsSync: vi.fn(), - }; -}); +// Mock secure-fs +vi.mock('@/lib/secure-fs.js', () => ({ + access: vi.fn(), +})); // Mock net -vi.mock("net", () => ({ +vi.mock('net', () => ({ default: { createServer: vi.fn(), }, createServer: vi.fn(), })); -import { spawn, execSync } from "child_process"; -import { existsSync } from "fs"; -import net from "net"; +import { spawn, execSync } from 'child_process'; +import * as secureFs from '@/lib/secure-fs.js'; +import net from 'net'; -describe("dev-server-service.ts", () => { +describe('dev-server-service.ts', () => { let testDir: string; beforeEach(async () => { @@ -41,20 +37,20 @@ describe("dev-server-service.ts", () => { testDir = path.join(os.tmpdir(), `dev-server-test-${Date.now()}`); await fs.mkdir(testDir, { recursive: true }); - // Default mock for existsSync - return true - vi.mocked(existsSync).mockReturnValue(true); + // Default mock for secureFs.access - return resolved (file exists) + vi.mocked(secureFs.access).mockResolvedValue(undefined); // Default mock for net.createServer - port available const mockServer = new EventEmitter() as any; mockServer.listen = vi.fn().mockImplementation((port: number, host: string) => { - process.nextTick(() => mockServer.emit("listening")); + process.nextTick(() => mockServer.emit('listening')); }); mockServer.close = vi.fn(); vi.mocked(net.createServer).mockReturnValue(mockServer); // Default mock for execSync - no process on port vi.mocked(execSync).mockImplementation(() => { - throw new Error("No process found"); + throw new Error('No process found'); }); }); @@ -66,11 +62,9 @@ describe("dev-server-service.ts", () => { } }); - describe("getDevServerService", () => { - it("should return a singleton instance", async () => { - const { getDevServerService } = await import( - "@/services/dev-server-service.js" - ); + describe('getDevServerService', () => { + it('should return a singleton instance', async () => { + const { getDevServerService } = await import('@/services/dev-server-service.js'); const instance1 = getDevServerService(); const instance2 = getDevServerService(); @@ -79,148 +73,125 @@ describe("dev-server-service.ts", () => { }); }); - describe("startDevServer", () => { - it("should return error if worktree path does not exist", async () => { - vi.mocked(existsSync).mockReturnValue(false); + describe('startDevServer', () => { + it('should return error if worktree path does not exist', async () => { + vi.mocked(secureFs.access).mockRejectedValueOnce(new Error('File not found')); - const { getDevServerService } = await import( - "@/services/dev-server-service.js" - ); + const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); - const result = await service.startDevServer( - "/project", - "/nonexistent/worktree" - ); + const result = await service.startDevServer('/project', '/nonexistent/worktree'); expect(result.success).toBe(false); - expect(result.error).toContain("does not exist"); + expect(result.error).toContain('does not exist'); }); - it("should return error if no package.json found", async () => { - vi.mocked(existsSync).mockImplementation((p: any) => { - if (p.includes("package.json")) return false; - return true; + it('should return error if no package.json found', async () => { + vi.mocked(secureFs.access).mockImplementation(async (p: any) => { + if (typeof p === 'string' && p.includes('package.json')) { + throw new Error('File not found'); + } + return undefined; }); - const { getDevServerService } = await import( - "@/services/dev-server-service.js" - ); + const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); const result = await service.startDevServer(testDir, testDir); expect(result.success).toBe(false); - expect(result.error).toContain("No package.json found"); + expect(result.error).toContain('No package.json found'); }); - it("should detect npm as package manager with package-lock.json", async () => { - vi.mocked(existsSync).mockImplementation((p: any) => { - if (p.includes("bun.lockb")) return false; - if (p.includes("pnpm-lock.yaml")) return false; - if (p.includes("yarn.lock")) return false; - if (p.includes("package-lock.json")) return true; - if (p.includes("package.json")) return true; - return true; + it('should detect npm as package manager with package-lock.json', async () => { + vi.mocked(secureFs.access).mockImplementation(async (p: any) => { + const pathStr = typeof p === 'string' ? p : ''; + if (pathStr.includes('bun.lockb')) throw new Error('Not found'); + if (pathStr.includes('pnpm-lock.yaml')) throw new Error('Not found'); + if (pathStr.includes('yarn.lock')) throw new Error('Not found'); + if (pathStr.includes('package-lock.json')) return undefined; + if (pathStr.includes('package.json')) return undefined; + return undefined; }); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); - const { getDevServerService } = await import( - "@/services/dev-server-service.js" - ); + const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); await service.startDevServer(testDir, testDir); - expect(spawn).toHaveBeenCalledWith( - "npm", - ["run", "dev"], - expect.any(Object) - ); + expect(spawn).toHaveBeenCalledWith('npm', ['run', 'dev'], expect.any(Object)); }); - it("should detect yarn as package manager with yarn.lock", async () => { - vi.mocked(existsSync).mockImplementation((p: any) => { - if (p.includes("bun.lockb")) return false; - if (p.includes("pnpm-lock.yaml")) return false; - if (p.includes("yarn.lock")) return true; - if (p.includes("package.json")) return true; - return true; + it('should detect yarn as package manager with yarn.lock', async () => { + vi.mocked(secureFs.access).mockImplementation(async (p: any) => { + const pathStr = typeof p === 'string' ? p : ''; + if (pathStr.includes('bun.lockb')) throw new Error('Not found'); + if (pathStr.includes('pnpm-lock.yaml')) throw new Error('Not found'); + if (pathStr.includes('yarn.lock')) return undefined; + if (pathStr.includes('package.json')) return undefined; + return undefined; }); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); - const { getDevServerService } = await import( - "@/services/dev-server-service.js" - ); + const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); await service.startDevServer(testDir, testDir); - expect(spawn).toHaveBeenCalledWith("yarn", ["dev"], expect.any(Object)); + expect(spawn).toHaveBeenCalledWith('yarn', ['dev'], expect.any(Object)); }); - it("should detect pnpm as package manager with pnpm-lock.yaml", async () => { - vi.mocked(existsSync).mockImplementation((p: any) => { - if (p.includes("bun.lockb")) return false; - if (p.includes("pnpm-lock.yaml")) return true; - if (p.includes("package.json")) return true; - return true; + it('should detect pnpm as package manager with pnpm-lock.yaml', async () => { + vi.mocked(secureFs.access).mockImplementation(async (p: any) => { + const pathStr = typeof p === 'string' ? p : ''; + if (pathStr.includes('bun.lockb')) throw new Error('Not found'); + if (pathStr.includes('pnpm-lock.yaml')) return undefined; + if (pathStr.includes('package.json')) return undefined; + return undefined; }); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); - const { getDevServerService } = await import( - "@/services/dev-server-service.js" - ); + const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); await service.startDevServer(testDir, testDir); - expect(spawn).toHaveBeenCalledWith( - "pnpm", - ["run", "dev"], - expect.any(Object) - ); + expect(spawn).toHaveBeenCalledWith('pnpm', ['run', 'dev'], expect.any(Object)); }); - it("should detect bun as package manager with bun.lockb", async () => { - vi.mocked(existsSync).mockImplementation((p: any) => { - if (p.includes("bun.lockb")) return true; - if (p.includes("package.json")) return true; - return true; + it('should detect bun as package manager with bun.lockb', async () => { + vi.mocked(secureFs.access).mockImplementation(async (p: any) => { + const pathStr = typeof p === 'string' ? p : ''; + if (pathStr.includes('bun.lockb')) return undefined; + if (pathStr.includes('package.json')) return undefined; + return undefined; }); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); - const { getDevServerService } = await import( - "@/services/dev-server-service.js" - ); + const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); await service.startDevServer(testDir, testDir); - expect(spawn).toHaveBeenCalledWith( - "bun", - ["run", "dev"], - expect.any(Object) - ); + expect(spawn).toHaveBeenCalledWith('bun', ['run', 'dev'], expect.any(Object)); }); - it("should return existing server info if already running", async () => { - vi.mocked(existsSync).mockReturnValue(true); + it('should return existing server info if already running', async () => { + vi.mocked(secureFs.access).mockResolvedValue(undefined); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); - const { getDevServerService } = await import( - "@/services/dev-server-service.js" - ); + const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); // Start first server @@ -230,18 +201,16 @@ describe("dev-server-service.ts", () => { // Try to start again - should return existing const result2 = await service.startDevServer(testDir, testDir); expect(result2.success).toBe(true); - expect(result2.result?.message).toContain("already running"); + expect(result2.result?.message).toContain('already running'); }); - it("should start dev server successfully", async () => { - vi.mocked(existsSync).mockReturnValue(true); + it('should start dev server successfully', async () => { + vi.mocked(secureFs.access).mockResolvedValue(undefined); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); - const { getDevServerService } = await import( - "@/services/dev-server-service.js" - ); + const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); const result = await service.startDevServer(testDir, testDir); @@ -249,32 +218,28 @@ describe("dev-server-service.ts", () => { expect(result.success).toBe(true); expect(result.result).toBeDefined(); expect(result.result?.port).toBeGreaterThanOrEqual(3001); - expect(result.result?.url).toContain("http://localhost:"); + expect(result.result?.url).toContain('http://localhost:'); }); }); - describe("stopDevServer", () => { - it("should return success if server not found", async () => { - const { getDevServerService } = await import( - "@/services/dev-server-service.js" - ); + describe('stopDevServer', () => { + it('should return success if server not found', async () => { + const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); - const result = await service.stopDevServer("/nonexistent/path"); + const result = await service.stopDevServer('/nonexistent/path'); expect(result.success).toBe(true); - expect(result.result?.message).toContain("already stopped"); + expect(result.result?.message).toContain('already stopped'); }); - it("should stop a running server", async () => { - vi.mocked(existsSync).mockReturnValue(true); + it('should stop a running server', async () => { + vi.mocked(secureFs.access).mockResolvedValue(undefined); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); - const { getDevServerService } = await import( - "@/services/dev-server-service.js" - ); + const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); // Start server @@ -284,15 +249,13 @@ describe("dev-server-service.ts", () => { const result = await service.stopDevServer(testDir); expect(result.success).toBe(true); - expect(mockProcess.kill).toHaveBeenCalledWith("SIGTERM"); + expect(mockProcess.kill).toHaveBeenCalledWith('SIGTERM'); }); }); - describe("listDevServers", () => { - it("should return empty list when no servers running", async () => { - const { getDevServerService } = await import( - "@/services/dev-server-service.js" - ); + describe('listDevServers', () => { + it('should return empty list when no servers running', async () => { + const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); const result = service.listDevServers(); @@ -301,15 +264,13 @@ describe("dev-server-service.ts", () => { expect(result.result.servers).toEqual([]); }); - it("should list running servers", async () => { - vi.mocked(existsSync).mockReturnValue(true); + it('should list running servers', async () => { + vi.mocked(secureFs.access).mockResolvedValue(undefined); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); - const { getDevServerService } = await import( - "@/services/dev-server-service.js" - ); + const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); await service.startDevServer(testDir, testDir); @@ -322,25 +283,21 @@ describe("dev-server-service.ts", () => { }); }); - describe("isRunning", () => { - it("should return false for non-running server", async () => { - const { getDevServerService } = await import( - "@/services/dev-server-service.js" - ); + describe('isRunning', () => { + it('should return false for non-running server', async () => { + const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); - expect(service.isRunning("/some/path")).toBe(false); + expect(service.isRunning('/some/path')).toBe(false); }); - it("should return true for running server", async () => { - vi.mocked(existsSync).mockReturnValue(true); + it('should return true for running server', async () => { + vi.mocked(secureFs.access).mockResolvedValue(undefined); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); - const { getDevServerService } = await import( - "@/services/dev-server-service.js" - ); + const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); await service.startDevServer(testDir, testDir); @@ -349,25 +306,21 @@ describe("dev-server-service.ts", () => { }); }); - describe("getServerInfo", () => { - it("should return undefined for non-running server", async () => { - const { getDevServerService } = await import( - "@/services/dev-server-service.js" - ); + describe('getServerInfo', () => { + it('should return undefined for non-running server', async () => { + const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); - expect(service.getServerInfo("/some/path")).toBeUndefined(); + expect(service.getServerInfo('/some/path')).toBeUndefined(); }); - it("should return info for running server", async () => { - vi.mocked(existsSync).mockReturnValue(true); + it('should return info for running server', async () => { + vi.mocked(secureFs.access).mockResolvedValue(undefined); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); - const { getDevServerService } = await import( - "@/services/dev-server-service.js" - ); + const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); await service.startDevServer(testDir, testDir); @@ -379,16 +332,14 @@ describe("dev-server-service.ts", () => { }); }); - describe("getAllocatedPorts", () => { - it("should return allocated ports", async () => { - vi.mocked(existsSync).mockReturnValue(true); + describe('getAllocatedPorts', () => { + it('should return allocated ports', async () => { + vi.mocked(secureFs.access).mockResolvedValue(undefined); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); - const { getDevServerService } = await import( - "@/services/dev-server-service.js" - ); + const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); await service.startDevServer(testDir, testDir); @@ -399,16 +350,14 @@ describe("dev-server-service.ts", () => { }); }); - describe("stopAll", () => { - it("should stop all running servers", async () => { - vi.mocked(existsSync).mockReturnValue(true); + describe('stopAll', () => { + it('should stop all running servers', async () => { + vi.mocked(secureFs.access).mockResolvedValue(undefined); const mockProcess = createMockProcess(); vi.mocked(spawn).mockReturnValue(mockProcess as any); - const { getDevServerService } = await import( - "@/services/dev-server-service.js" - ); + const { getDevServerService } = await import('@/services/dev-server-service.js'); const service = getDevServerService(); await service.startDevServer(testDir, testDir);