import { mkdir, mkdtemp, realpath, rm, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import type { PrismaClient } from "@rakazo/db"; import { afterAll, describe, expect, it, vi } from "vitest"; import { applyOnboardingScan, discoverOnboardingScan, onboardingScanFolder, onboardingScanStatus, startOnboardingScan, } from "./onboarding-scan.js"; const owner = { userId: "user-owner", spaceId: "space-owner", isDeploymentOwner: true }; const member = { userId: "user-member", spaceId: "space-member", isDeploymentOwner: false }; const roots: string[] = []; /** Fails the test if the scan writes anything before its checks pass. */ const untouchedPrisma = new Proxy( {}, { get() { throw new Error("the database must not be touched"); }, }, ) as PrismaClient; async function tempDir(): Promise { const dir = await realpath(await mkdtemp(path.join(tmpdir(), "evoo-scan-"))); roots.push(dir); return dir; } async function files(root: string, entries: string[]): Promise { for (const entry of entries) { const file = path.join(root, entry); await mkdir(path.dirname(file), { recursive: true }); await writeFile(file, "x\n"); } } async function project(root: string, relative: string, entries: string[]): Promise { await mkdir(path.join(root, relative, ".git"), { recursive: true }); await files(path.join(root, relative), entries); } async function completed(scanId: string, userId: string) { return await vi.waitFor( () => { const status = onboardingScanStatus(scanId, userId); if (status.status === "scanning") throw new Error("pending"); return status; }, { timeout: 10_000, interval: 20 }, ); } afterAll(async () => { await Promise.all(roots.map((root) => rm(root, { recursive: true, force: true }))); }); describe("onboarding scan", () => { it("lists git projects with their languages and tools and never follows a symlink", async () => { const home = await tempDir(); const outside = await tempDir(); // A home folder tracked by git is not itself a project. await mkdir(path.join(home, ".git")); await project(home, "code/atlas", [ "src/a.ts", "src/b.ts", "src/c.ts", "src/d.tsx", "scripts/build.js", "Dockerfile", ".github/workflows/ci.yml", "infra/main.tf", "src/odd.constructor", "node_modules/dep/index.js", "node_modules/dep/more.js", ]); await project(home, "code/garden", ["main.py"]); await project(home, "deep/a/b/edge", ["tool.py"]); await project(home, "deep/a/b/c/too-deep", ["main.go"]); await project(home, ".hidden/secret", ["main.go"]); await project(home, "notes/node_modules/vendored", ["main.go"]); await files(home, ["notes/todo.md"]); await project(outside, "escaped", ["main.rs"]); await symlink(path.join(outside, "escaped"), path.join(home, "linked")); await symlink(outside, path.join(home, "code/atlas/src/outside")); const findings = await discoverOnboardingScan(home); expect(findings.projects).toEqual([ { id: "project-0", name: "atlas", path: "code/atlas", languages: ["TypeScript", "JavaScript"], tools: ["Docker", "GitHub Actions", "Terraform"], }, { id: "project-1", name: "garden", path: "code/garden", languages: ["Python"], tools: [] }, { id: "project-2", name: "edge", path: "deep/a/b/edge", languages: ["Python"], tools: [] }, ]); expect(findings.facts).toEqual([ { id: "language:Python", label: "Python", category: "work", value: "Codes in Python" }, { id: "language:JavaScript", label: "JavaScript", category: "work", value: "Codes in JavaScript", }, { id: "language:TypeScript", label: "TypeScript", category: "work", value: "Codes in TypeScript", }, { id: "tool:Docker", label: "Docker", category: "tools", value: "Uses Docker" }, { id: "tool:GitHub Actions", label: "GitHub Actions", category: "tools", value: "Uses GitHub Actions", }, { id: "tool:Terraform", label: "Terraform", category: "tools", value: "Uses Terraform" }, ]); }); it("offers a scan folder only to the deployment owner and only when it exists", async () => { const home = await tempDir(); await files(home, ["file.txt"]); await expect(onboardingScanFolder(member, home)).resolves.toBeNull(); await expect(onboardingScanFolder(owner, undefined)).resolves.toBeNull(); await expect(onboardingScanFolder(owner, path.join(home, "missing"))).resolves.toBeNull(); await expect(onboardingScanFolder(owner, path.join(home, "file.txt"))).resolves.toBeNull(); await expect(onboardingScanFolder(owner, home)).resolves.toBe(home); await expect(startOnboardingScan(member, home)).rejects.toMatchObject({ code: "FORBIDDEN" }); }); it("keeps a scan with the account that started it and returns only relative paths", async () => { const home = await tempDir(); await project(home, "code/atlas", ["index.ts"]); const { scanId } = await startOnboardingScan(owner, home); const status = await completed(scanId, owner.userId); expect(status).toEqual({ status: "complete", projects: [{ id: "project-0", name: "atlas", path: "code/atlas" }], facts: [{ id: "language:TypeScript", label: "TypeScript" }], }); expect(JSON.stringify(status)).not.toContain(home); expect(() => onboardingScanStatus(scanId, member.userId)).toThrow( expect.objectContaining({ code: "NOT_FOUND" }), ); expect(() => onboardingScanStatus("missing-scan", owner.userId)).toThrow( expect.objectContaining({ code: "NOT_FOUND" }), ); await expect( applyOnboardingScan(untouchedPrisma, member, home, { scanId, projectIds: ["project-0"], factIds: [], }), ).rejects.toMatchObject({ code: "NOT_FOUND" }); }); it("checks ownership, the folder and every chosen project again before writing", async () => { const home = await tempDir(); const outside = await tempDir(); await project(home, "code/atlas", ["index.ts"]); await project(home, "code/garden", ["main.py"]); await project(outside, "elsewhere", ["main.rs"]); const { scanId } = await startOnboardingScan(owner, home); await completed(scanId, owner.userId); const choice = { scanId, projectIds: ["project-1"], factIds: ["language:Python"] }; await expect( applyOnboardingScan(untouchedPrisma, { ...owner, isDeploymentOwner: false }, home, choice), ).rejects.toMatchObject({ code: "FORBIDDEN" }); await expect( applyOnboardingScan(untouchedPrisma, owner, outside, choice), ).rejects.toMatchObject({ code: "FORBIDDEN" }); // The chosen project became a symlink out of the scan folder after the scan. await rm(path.join(home, "code/garden"), { recursive: true }); await symlink(path.join(outside, "elsewhere"), path.join(home, "code/garden")); await expect(applyOnboardingScan(untouchedPrisma, owner, home, choice)).rejects.toMatchObject({ code: "BAD_REQUEST", message: "That folder does not exist or is not readable.", }); }); });