Files
curriculum-project-hub/hub/test/unit/deployment-preflight.test.ts
T

113 lines
3.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
DeploymentPreflightError,
validateDeploymentPreflight,
type DeploymentPreflightInput,
} from "../../src/deployment/preflight.js";
const VALID_ENV = {
NODE_ENV: "production",
DATABASE_URL: "postgresql://hub:secret@127.0.0.1:5432/hub",
FEISHU_APP_ID: "cli_app_id",
FEISHU_APP_SECRET: "feishu-app-secret",
FEISHU_BOT_OPEN_ID: "ou_bot",
CPH_BIN: "/srv/curriculum-project-hub/current/bin/cph",
HOST: "127.0.0.1",
PORT: "8788",
HUB_PROJECT_WORKSPACE_ROOT: "/var/lib/cph-hub/workspaces",
HUB_PUBLIC_BASE_URL: "https://hub.example.com",
HUB_SESSION_SECRET: "a-production-session-secret-with-32-bytes",
} as const;
function input(overrides: Partial<DeploymentPreflightInput> = {}): DeploymentPreflightInput {
return {
baseDir: "/srv/curriculum-project-hub",
hubDir: "/srv/curriculum-project-hub/current/hub",
serviceHome: "/var/lib/cph-hub/home",
stateDir: "/var/lib/cph-hub/state",
cacheDir: "/var/cache/cph-hub",
workspaceRoot: "/var/lib/cph-hub/workspaces",
env: VALID_ENV,
...overrides,
};
}
describe("validateDeploymentPreflight", () => {
it("accepts a complete production configuration with persistent storage outside the deployment tree", () => {
expect(validateDeploymentPreflight(input())).toEqual({
bind: { host: "127.0.0.1", port: 8788 },
databaseUrl: "postgresql://hub:secret@127.0.0.1:5432/hub",
cphBin: "/srv/curriculum-project-hub/current/bin/cph",
});
});
it.each([
["inside the live Hub", "/srv/curriculum-project-hub/current/hub/data/workspaces"],
["inside future releases", "/srv/curriculum-project-hub/releases/workspaces"],
["an ancestor of the deployment root", "/srv"],
])("rejects a workspace %s", (_description, workspaceRoot) => {
expect(() =>
validateDeploymentPreflight(
input({
workspaceRoot,
env: { ...VALID_ENV, HUB_PROJECT_WORKSPACE_ROOT: workspaceRoot },
}),
),
).toThrow("must not overlap deployment root");
});
it("rejects a runtime workspace that differs from the provisioned path", () => {
expect(() =>
validateDeploymentPreflight(
input({ env: { ...VALID_ENV, HUB_PROJECT_WORKSPACE_ROOT: "/var/lib/cph-hub/other" } }),
),
).toThrow("HUB_PROJECT_WORKSPACE_ROOT must equal provisioned workspace root");
});
it("rejects a relative runtime workspace even if it resolves to the provisioned path", () => {
expect(() =>
validateDeploymentPreflight(
input({ env: { ...VALID_ENV, HUB_PROJECT_WORKSPACE_ROOT: "./workspaces" } }),
),
).toThrow("HUB_PROJECT_WORKSPACE_ROOT must be an absolute normalized path");
});
it("reports all invalid required production values in one failed preflight", () => {
expect.assertions(2);
try {
validateDeploymentPreflight(
input({
env: {
...VALID_ENV,
FEISHU_APP_SECRET: "",
HUB_PUBLIC_BASE_URL: "http://hub.example.com",
HUB_SESSION_SECRET: "short",
},
}),
);
} catch (error) {
expect(error).toBeInstanceOf(DeploymentPreflightError);
expect((error as Error).message).toMatchInlineSnapshot(`
"deployment preflight failed:
- FEISHU_APP_SECRET is required
- HUB_PUBLIC_BASE_URL must use https
- HUB_SESSION_SECRET must contain at least 32 characters"
`);
}
});
it("rejects relative operational paths", () => {
expect(() => validateDeploymentPreflight(input({ cacheDir: "./cache" }))).toThrow(
"cache directory must be an absolute normalized path",
);
});
it("rejects legacy process-global provider settings instead of silently ignoring them", () => {
expect(() =>
validateDeploymentPreflight(
input({ env: { ...VALID_ENV, ANTHROPIC_AUTH_TOKEN: "global-secret" } }),
),
).toThrow("ANTHROPIC_AUTH_TOKEN must not be set");
});
});