145 lines
6.6 KiB
TypeScript
145 lines
6.6 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
|
|
const paths = {
|
|
api: "src/lib/api.ts",
|
|
capabilities: "src/server/services/container-capabilities.service.ts",
|
|
deploymentService: "src/server/services/deployment.service.ts",
|
|
routes: "src/server/routes/containers.ts",
|
|
environmentPage:
|
|
"src/app/projects/[projectId]/environments/[environmentId]/page.tsx",
|
|
environmentToolbar:
|
|
"src/app/projects/[projectId]/environments/[environmentId]/components/toolbar/EnvironmentContainersToolbar.tsx",
|
|
environmentTable:
|
|
"src/app/projects/[projectId]/environments/[environmentId]/components/containers/EnvironmentContainersTable.tsx",
|
|
detailPage:
|
|
"src/app/projects/[projectId]/environments/[environmentId]/containers/[containerId]/page.tsx",
|
|
detailHeader:
|
|
"src/app/projects/[projectId]/environments/[environmentId]/containers/[containerId]/components/header/AppDetailHeader.tsx",
|
|
actionConstants:
|
|
"src/app/projects/[projectId]/environments/[environmentId]/containers/[containerId]/data/app-detail-constants.ts",
|
|
deploymentsPanel:
|
|
"src/app/projects/[projectId]/environments/[environmentId]/containers/[containerId]/components/deployments/DeploymentsTabPanel.tsx",
|
|
configurationModal:
|
|
"src/app/projects/[projectId]/environments/[environmentId]/containers/[containerId]/components/configuration/EditContainerConfigurationModal.tsx",
|
|
} as const;
|
|
|
|
function read(path: (typeof paths)[keyof typeof paths]) {
|
|
return readFileSync(path, "utf8");
|
|
}
|
|
|
|
test("container list and detail expose the same backend-derived capability contract", () => {
|
|
const routes = read(paths.routes);
|
|
const api = read(paths.api);
|
|
|
|
assert.ok(
|
|
(routes.match(/capabilities:\s*buildContainerCapabilities\(/g) ?? [])
|
|
.length >= 2,
|
|
"list and detail responses must both build capabilities",
|
|
);
|
|
assert.ok(
|
|
(routes.match(/const \{ deploymentSource, \.\.\.publicContainer \} = container;/g) ?? [])
|
|
.length >= 2,
|
|
"internal deployment source metadata must not be copied to public responses",
|
|
);
|
|
assert.match(routes, /hasDeploymentHistory:\s*Boolean\(deploymentSummary\?\.latestAttempt\)/);
|
|
assert.match(routes, /redeployAvailable:\s*deploymentSummary\?\.redeployAvailable \?\? false/);
|
|
assert.match(routes, /rollbackAvailable:\s*deploymentSummary\?\.rollbackAvailable \?\? false/);
|
|
assert.match(api, /capabilities\?: ContainerCapabilities/);
|
|
assert.match(api, /mode: "ORCHESTRATED_SINGLE_CONTAINER" \| "COMPOSE_RECREATE" \| null/);
|
|
});
|
|
|
|
test("redeploy capability belongs to the active eligible revision", () => {
|
|
const service = read(paths.deploymentService);
|
|
|
|
assert.match(
|
|
service,
|
|
/redeployAvailable:\s*rollbackEligibleRevisions\.some\(\s*\(revision\) => revision\.id === activeRevision\?\.id,?\s*\)/,
|
|
);
|
|
assert.match(
|
|
service,
|
|
/rollbackAvailable:\s*rollbackEligibleRevisions\.some\([\s\S]*?revision\.id !== activeRevision\?\.id/,
|
|
);
|
|
});
|
|
|
|
test("detail actions combine backend capability with frontend role gating", () => {
|
|
const detailPage = read(paths.detailPage);
|
|
const deploymentsPanel = read(paths.deploymentsPanel);
|
|
const detailHeader = read(paths.detailHeader);
|
|
|
|
assert.match(detailPage, /getRoleCapabilities\(currentUser\?\.role/);
|
|
assert.match(detailPage, /canMutateContainers/);
|
|
assert.match(detailPage, /capabilities\?\.editConfiguration\.available/);
|
|
assert.match(detailPage, /capabilities\?\.redeploy\.available/);
|
|
assert.match(detailPage, /capabilities\?\.rebuild\.available/);
|
|
assert.match(detailPage, /allowRollback=\{canMutateContainers\}/);
|
|
assert.match(deploymentsPanel, /allowRollback && deployment\.canRollback/);
|
|
assert.match(detailHeader, /menuActions\.length > 0/);
|
|
assert.match(detailHeader, /disabled=\{actionBusy \|\| action\.disabled\}/);
|
|
});
|
|
|
|
test("viewer cannot open environment mutation controls", () => {
|
|
const environmentPage = read(paths.environmentPage);
|
|
const toolbar = read(paths.environmentToolbar);
|
|
|
|
assert.match(environmentPage, /canManageContainers && showDeploy/);
|
|
assert.match(environmentPage, /canManageContainers && showDeployDatabase/);
|
|
assert.match(environmentPage, /canManageContainers && showImportFromSync/);
|
|
assert.match(environmentPage, /canManage=\{canManageContainers\}/);
|
|
assert.match(toolbar, /canManage: boolean/);
|
|
assert.match(toolbar, /\{canManage \? <div className="ui-toolbar-actions">/);
|
|
});
|
|
|
|
test("environment inventory distinguishes managed and imported containers", () => {
|
|
const capabilities = read(paths.capabilities);
|
|
const environmentPage = read(paths.environmentPage);
|
|
const environmentTable = read(paths.environmentTable);
|
|
const toolbar = read(paths.environmentToolbar);
|
|
|
|
assert.match(capabilities, /managementLabel: managed \? "Doktainer managed" : "Docker import"/);
|
|
assert.match(environmentPage, /container\.capabilities\?\.managementLabel \?\? "Docker import"/);
|
|
assert.match(environmentTable, /container\.managementLabel/);
|
|
assert.match(environmentTable, /className=\{`ui-badge/);
|
|
assert.match(
|
|
environmentPage,
|
|
/Existing deployment revision history was not modified/,
|
|
);
|
|
assert.match(toolbar, /deployment revision history is unchanged/);
|
|
});
|
|
|
|
test("Compose remains visibly outside safe single-container orchestration", () => {
|
|
const capabilities = read(paths.capabilities);
|
|
const detailPage = read(paths.detailPage);
|
|
|
|
assert.match(capabilities, /mode: composeManaged\s*\? "COMPOSE_RECREATE"\s*: "ORCHESTRATED_SINGLE_CONTAINER"/);
|
|
assert.match(capabilities, /must be edited through their Compose project/);
|
|
assert.match(detailPage, /label: "Rebuild Compose project"/);
|
|
assert.match(
|
|
detailPage,
|
|
/This is not a safe single-container candidate switch and service interruption may occur/,
|
|
);
|
|
});
|
|
|
|
test("configuration preview and header reuse established UI primitives", () => {
|
|
const modal = read(paths.configurationModal);
|
|
const header = read(paths.detailHeader);
|
|
const actions = read(paths.actionConstants);
|
|
const primaryActions = actions.slice(
|
|
actions.indexOf("export const primaryActions"),
|
|
actions.indexOf("export const headerActions"),
|
|
);
|
|
const headerActions = actions.slice(actions.indexOf("export const headerActions"));
|
|
|
|
assert.match(modal, /className="modal-close"/);
|
|
assert.match(modal, /className="input"/);
|
|
assert.match(modal, /className=\{`ui-badge/);
|
|
assert.match(modal, /require(?:s)? a separate runtime-replacement flow/);
|
|
assert.match(header, /const statusColor =/);
|
|
assert.match(header, /normalizedStatus === "ERROR"/);
|
|
assert.doesNotMatch(header, /app\.managementLabel/);
|
|
assert.match(primaryActions, /id: "edit"/);
|
|
assert.match(primaryActions, /label: "Edit configuration"/);
|
|
assert.doesNotMatch(headerActions, /id: "edit"/);
|
|
});
|