diff --git a/cli/src/__tests__/shared-common-oauth-security.test.ts b/cli/src/__tests__/shared-common-oauth-security.test.ts new file mode 100644 index 00000000..9438769a --- /dev/null +++ b/cli/src/__tests__/shared-common-oauth-security.test.ts @@ -0,0 +1,911 @@ +import { describe, it, expect } from "bun:test"; +import { execSync } from "child_process"; +import { resolve, join } from "path"; +import { mkdirSync, writeFileSync, readFileSync, rmSync, existsSync } from "fs"; +import { tmpdir } from "os"; + +/** + * Tests for OAuth security functions in shared/common.sh that previously + * had zero test coverage: + * + * - _generate_oauth_html: OAuth callback HTML generation + * - _validate_oauth_server_args: Port validation + CSRF state file reading + * - _generate_oauth_server_script: Node.js OAuth server script generation + * - exchange_oauth_code: OAuth code-to-key exchange (JSON escaping) + * - cleanup_oauth_session: Cleanup of OAuth temp resources + * - execute_agent_non_interactive: Non-interactive agent prompt execution + * + * These are SECURITY-CRITICAL: they handle CSRF state, port validation, + * user prompt escaping, and code generation from untrusted inputs. + * + * Agent: test-engineer + */ + +const REPO_ROOT = resolve(import.meta.dir, "../../.."); +const COMMON_SH = resolve(REPO_ROOT, "shared/common.sh"); + +/** + * Run a bash snippet that sources shared/common.sh first. + * Uses a heredoc approach to avoid single-quote escaping issues. + */ +function runBash( + script: string, + env?: Record +): { exitCode: number; stdout: string; stderr: string } { + const fullScript = `source "${COMMON_SH}"\n${script}`; + try { + const stdout = execSync(`bash -c '${fullScript.replace(/'/g, "'\\''")}'`, { + encoding: "utf-8", + timeout: 10000, + stdio: ["pipe", "pipe", "pipe"], + env: { ...process.env, ...env }, + }); + return { exitCode: 0, stdout: stdout.trim(), stderr: "" }; + } catch (err: any) { + return { + exitCode: err.status ?? 1, + stdout: (err.stdout || "").trim(), + stderr: (err.stderr || "").trim(), + }; + } +} + +/** + * Run bash script using heredoc to handle complex quoting. + */ +function runBashHeredoc( + script: string, + env?: Record +): { exitCode: number; stdout: string; stderr: string } { + const tmpFile = join( + tmpdir(), + `spawn-test-${Date.now()}-${Math.random().toString(36).slice(2)}.sh` + ); + try { + writeFileSync( + tmpFile, + `#!/bin/bash\nset -eo pipefail\nsource "${COMMON_SH}"\n${script}\n` + ); + const stdout = execSync(`bash "${tmpFile}"`, { + encoding: "utf-8", + timeout: 10000, + stdio: ["pipe", "pipe", "pipe"], + env: { ...process.env, ...env }, + }); + return { exitCode: 0, stdout: stdout.trim(), stderr: "" }; + } catch (err: any) { + return { + exitCode: err.status ?? 1, + stdout: (err.stdout || "").trim(), + stderr: (err.stderr || "").trim(), + }; + } finally { + try { + rmSync(tmpFile); + } catch {} + } +} + +function createTempDir(): string { + const dir = join( + tmpdir(), + `spawn-oauth-test-${Date.now()}-${Math.random().toString(36).slice(2)}` + ); + mkdirSync(dir, { recursive: true }); + return dir; +} + +// ── _generate_oauth_html ──────────────────────────────────────────────── + +describe("_generate_oauth_html", () => { + it("should set OAUTH_SUCCESS_HTML variable", () => { + const result = runBash(` + _generate_oauth_html + echo "$OAUTH_SUCCESS_HTML" + `); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Authentication Successful"); + }); + + it("should set OAUTH_ERROR_HTML variable", () => { + const result = runBash(` + _generate_oauth_html + echo "$OAUTH_ERROR_HTML" + `); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Authentication Failed"); + }); + + it("should include CSRF protection message in error HTML", () => { + const result = runBash(` + _generate_oauth_html + echo "$OAUTH_ERROR_HTML" + `); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("CSRF protection"); + }); + + it("should include auto-close script in success HTML", () => { + const result = runBash(` + _generate_oauth_html + echo "$OAUTH_SUCCESS_HTML" + `); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("window.close"); + }); + + it("success HTML should be valid HTML structure", () => { + const result = runBash(` + _generate_oauth_html + echo "$OAUTH_SUCCESS_HTML" + `); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain(""); + expect(result.stdout).toContain(""); + expect(result.stdout).toContain(""); + expect(result.stdout).toContain(""); + }); + + it("error HTML should be valid HTML structure", () => { + const result = runBash(` + _generate_oauth_html + echo "$OAUTH_ERROR_HTML" + `); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain(""); + expect(result.stdout).toContain(""); + expect(result.stdout).toContain(""); + expect(result.stdout).toContain(""); + }); + + it("should include CSS styling", () => { + const result = runBash(` + _generate_oauth_html + echo "$OAUTH_SUCCESS_HTML" + `); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("