141 lines
4.2 KiB
TypeScript
141 lines
4.2 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
import {
|
|
DEVELOPMENT_ENCRYPTION_KEY,
|
|
getEncryptionKeyOrThrow,
|
|
} from "../../src/server/lib/crypto";
|
|
import {
|
|
redactDeploymentErrorDetails,
|
|
sanitizeDeploymentError,
|
|
} from "../../src/server/services/deployment-error.service";
|
|
|
|
test("production rejects a missing or short encryption key", () => {
|
|
assert.throws(
|
|
() => getEncryptionKeyOrThrow({ NODE_ENV: "production" }),
|
|
/must be configured/,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
getEncryptionKeyOrThrow({
|
|
NODE_ENV: "production",
|
|
ENCRYPTION_KEY: "short",
|
|
}),
|
|
/at least 32 characters/,
|
|
);
|
|
});
|
|
|
|
test("production allows long placeholder-compatible encryption keys for deployment ergonomics", () => {
|
|
assert.equal(
|
|
getEncryptionKeyOrThrow({
|
|
NODE_ENV: "production",
|
|
ENCRYPTION_KEY: DEVELOPMENT_ENCRYPTION_KEY,
|
|
}),
|
|
DEVELOPMENT_ENCRYPTION_KEY,
|
|
);
|
|
assert.equal(
|
|
getEncryptionKeyOrThrow({
|
|
NODE_ENV: "production",
|
|
ENCRYPTION_KEY:
|
|
"dev-this-key-is-long-enough-but-still-not-production",
|
|
}),
|
|
"dev-this-key-is-long-enough-but-still-not-production",
|
|
);
|
|
assert.equal(
|
|
getEncryptionKeyOrThrow({
|
|
NODE_ENV: "production",
|
|
ENCRYPTION_KEY: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
}),
|
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
);
|
|
});
|
|
|
|
test("development keeps the legacy fallback while production accepts a strong key", () => {
|
|
assert.equal(
|
|
getEncryptionKeyOrThrow({ NODE_ENV: "development" }),
|
|
DEVELOPMENT_ENCRYPTION_KEY,
|
|
);
|
|
assert.equal(
|
|
getEncryptionKeyOrThrow({
|
|
NODE_ENV: "production",
|
|
ENCRYPTION_KEY:
|
|
"47c2ecffbb4a488eb9a8d1939b54810f1db621bccf1bbf87c87a07de1e6e42d1",
|
|
}),
|
|
"47c2ecffbb4a488eb9a8d1939b54810f1db621bccf1bbf87c87a07de1e6e42d1",
|
|
);
|
|
});
|
|
|
|
test("backend startup validates encryption configuration before connecting to the database", () => {
|
|
const source = readFileSync("src/server/index.ts", "utf8");
|
|
const validationIndex = source.indexOf("validateEncryptionConfiguration();");
|
|
const databaseIndex = source.indexOf("await ensureDatabaseConnection();");
|
|
|
|
assert.ok(validationIndex >= 0);
|
|
assert.ok(databaseIndex > validationIndex);
|
|
});
|
|
|
|
test("deployment error redaction removes credentials, authorization, query secrets, and private keys", () => {
|
|
const message = redactDeploymentErrorDetails(
|
|
new Error(
|
|
[
|
|
"Command failed with password=super-secret",
|
|
"Authorization: Bearer abc.def.ghi",
|
|
"https://deploy-user:[email protected]/repo.git?access_token=query-token",
|
|
"-----BEGIN PRIVATE KEY-----",
|
|
"private-material",
|
|
"-----END PRIVATE KEY-----",
|
|
].join("\n"),
|
|
),
|
|
["super-secret"],
|
|
);
|
|
|
|
assert.doesNotMatch(
|
|
message,
|
|
/super-secret|abc\.def\.ghi|deploy-user|deploy-pass|query-token|private-material/,
|
|
);
|
|
assert.match(message, /REDACTED/);
|
|
});
|
|
|
|
test("deployment errors normalize raw command details", () => {
|
|
assert.equal(
|
|
sanitizeDeploymentError(
|
|
new Error(
|
|
"Command failed: docker run -e API_TOKEN=secret example/app; stderr: access denied",
|
|
),
|
|
{ fallback: "Git redeploy failed" },
|
|
),
|
|
"Git redeploy failed",
|
|
);
|
|
|
|
});
|
|
|
|
test("deployment and rollback boundaries use the centralized sanitizer before persistence and API responses", () => {
|
|
const rollbackService = readFileSync(
|
|
"src/server/services/deployment-rollback.service.ts",
|
|
"utf8",
|
|
);
|
|
const containerRoutes = readFileSync(
|
|
"src/server/routes/containers.ts",
|
|
"utf8",
|
|
);
|
|
const containerHealth = readFileSync(
|
|
"src/server/services/container-health.service.ts",
|
|
"utf8",
|
|
);
|
|
|
|
assert.match(
|
|
rollbackService,
|
|
/const message = sanitizeDeploymentError\(error,[\s\S]*?updateDeployment\(rollback\.id,[\s\S]*?error: message/,
|
|
);
|
|
assert.match(
|
|
containerRoutes,
|
|
/"\/:id\/deployments\/:deploymentId\/rollback"[\s\S]*?error: sanitizeDeploymentError\(error/,
|
|
);
|
|
assert.match(
|
|
containerRoutes,
|
|
/sanitizeDeploymentError\(ssh\.formatDeploymentErrorMessage\(err\)/,
|
|
);
|
|
assert.match(containerHealth, /lastReason = sanitizeDeploymentError\(error/);
|
|
});
|
|
|