test: isolate QA trusted certificates

This commit is contained in:
ajnart committed 2026-09-01 07:12:47 +02:00
1 parent 28a433386e
commit 4799dd0251
3 files changed
+53 -1

No files matched your search

+14
View File
@@ -123,6 +123,7 @@ interface RuntimeManifest {
runRoot: string;
slotDir: string;
dbPath: string;
trustedCertificatePath: string;
fixtureManifestPath: string;
nextDistDir: string;
url: string;
@@ -140,6 +141,16 @@ interface RuntimeManifest {
docker: { redisContainer: string };
}
export const createSlotTrustedCertificateDirectory = async (runRoot: string, slotDir: string) => {
await assertSafeContainedPath(runRoot, slotDir, "QA slot directory");
const trustedCertificatePath = path.join(slotDir, "trusted-certificates");
await assertSafeContainedPath(slotDir, trustedCertificatePath, "QA trusted certificate directory");
await mkdir(trustedCertificatePath, { mode: 0o700 });
await assertSafeContainedPath(slotDir, trustedCertificatePath, "QA trusted certificate directory");
await chmod(trustedCertificatePath, 0o700);
return trustedCertificatePath;
};
const usage = () => {
console.log(
"Usage: runtime.mts <start|status|stop> --slot <1|2|3> [--profile <main-writable|main-readonly|onboarding-fresh|degraded>] [--run-root <path>] [--candidate <ref>] [--reset]",
@@ -664,6 +675,7 @@ const startWithLease = async (options: Options) => {
const runId = `release-v2-${candidateSha.slice(0, 8)}-s${options.slot}-${Date.now()}`;
const redisContainer = `homarr-release-v2-qa-redis-s${options.slot}-${candidateSha.slice(0, 8)}-${Date.now().toString(36)}`;
const dbPath = path.join(slotDir, "db.sqlite");
const trustedCertificatePath = await createSlotTrustedCertificateDirectory(options.runRoot, slotDir);
const fixtureReadyPath = path.join(slotDir, "fixture-ready.json");
const fixtureManifestPath = path.join(slotDir, "fixture-manifest.json");
const fixtureLogPath = path.join(slotDir, "fixture.log");
@@ -725,6 +737,7 @@ const startWithLease = async (options: Options) => {
AUTH_COOKIE_PREFIX: `homarr_qa_s${options.slot}`,
DB_DRIVER: "better-sqlite3",
DB_URL: dbPath,
LOCAL_CERTIFICATE_PATH: trustedCertificatePath,
DEMO_MODE: String(flags.demoMode),
DEMO_READ_ONLY: String(flags.demoReadOnly),
UNSAFE_ENABLE_MOCK_INTEGRATION: String(flags.unsafeMockIntegration),
@@ -807,6 +820,7 @@ const startWithLease = async (options: Options) => {
runRoot: options.runRoot,
slotDir,
dbPath,
trustedCertificatePath,
fixtureManifestPath,
nextDistDir: candidateBuild.buildDir,
url,
+30
View File
@@ -1,7 +1,10 @@
// @vitest-environment node
import { mkdtemp, mkdir, rm, stat } from "node:fs/promises";
import { createServer } from "node:net";
import type { Server } from "node:net";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, test } from "vitest";
import {
@@ -9,12 +12,14 @@ import {
createAppLaunchCommand,
createCandidateBuildPaths,
createRuntimeExecutionContract,
createSlotTrustedCertificateDirectory,
launchWithPortRetry,
reserveLoopbackPort,
validateRuntimeExecutionContract,
} from "./runtime.mts";
const openServers: Server[] = [];
const temporaryDirectories: string[] = [];
const listen = async (server: Server, port: number) =>
await new Promise<void>((resolve, reject) => {
@@ -36,6 +41,31 @@ afterEach(async () => {
if (server.listening) await close(server);
}),
);
await Promise.all(temporaryDirectories.splice(0).map(async (directory) => await rm(directory, { recursive: true })));
});
describe("release-v2 QA slot storage", () => {
test("creates a restrictive slot-local trusted certificate directory", async () => {
const runRoot = await mkdtemp(path.join(tmpdir(), "homarr-release-v2-qa-runtime-spec-"));
temporaryDirectories.push(runRoot);
const slotDir = path.join(runRoot, "slots", "1");
await mkdir(slotDir, { recursive: true, mode: 0o700 });
const trustedCertificatePath = await createSlotTrustedCertificateDirectory(runRoot, slotDir);
const directoryStats = await stat(trustedCertificatePath);
expect(trustedCertificatePath).toBe(path.join(slotDir, "trusted-certificates"));
expect(directoryStats.isDirectory()).toBe(true);
expect(directoryStats.mode & 0o777).toBe(0o700);
});
test("refuses a trusted certificate directory outside the approved run root", async () => {
const runRoot = await mkdtemp(path.join(tmpdir(), "homarr-release-v2-qa-runtime-spec-"));
const outsideRoot = await mkdtemp(path.join(tmpdir(), "homarr-release-v2-qa-runtime-outside-"));
temporaryDirectories.push(runRoot, outsideRoot);
await expect(createSlotTrustedCertificateDirectory(runRoot, outsideRoot)).rejects.toThrow(/escapes/u);
});
});
describe("release-v2 QA runtime port startup", () => {
+9 -1
View File
@@ -112,6 +112,7 @@ interface RuntimeManifest {
runRoot: string;
slotDir: string;
dbPath: string;
trustedCertificatePath: string;
fixtureManifestPath: string;
nextDistDir: string;
url: string;
@@ -646,6 +647,7 @@ const validateRuntimeManifest = async (
const expectedSlotDir = join(runtime.runRoot, "slots", String(runtime.slot));
const expectedManifestPath = join(expectedSlotDir, "runtime-manifest.json");
const expectedDbPath = join(expectedSlotDir, "db.sqlite");
const expectedTrustedCertificatePath = join(expectedSlotDir, "trusted-certificates");
const expectedFixtureManifestPath = join(expectedSlotDir, "fixture-manifest.json");
const expectedBuild = createCandidateBuildPaths(campaignCandidateSha);
if (resolve(runtime.slotDir) !== resolve(expectedSlotDir))
@@ -653,6 +655,9 @@ const validateRuntimeManifest = async (
if (resolve(manifestPath) !== resolve(expectedManifestPath))
errors.push(`${label}: runtime manifest is outside the selected slot`);
if (resolve(runtime.dbPath) !== resolve(expectedDbPath)) errors.push(`${label}: dbPath is outside the selected slot`);
if (resolve(runtime.trustedCertificatePath ?? "") !== resolve(expectedTrustedCertificatePath)) {
errors.push(`${label}: trusted certificate path is outside the selected slot`);
}
if (resolve(runtime.fixtureManifestPath) !== resolve(expectedFixtureManifestPath)) {
errors.push(`${label}: fixture manifest path is outside the selected slot`);
}
@@ -675,8 +680,11 @@ const validateRuntimeManifest = async (
errors.push(`${label}: runRoot must not be a symlink`);
if ((await realpath(runtime.slotDir)) !== resolve(runtime.slotDir))
errors.push(`${label}: slotDir must not be a symlink`);
if ((await realpath(runtime.trustedCertificatePath)) !== resolve(runtime.trustedCertificatePath)) {
errors.push(`${label}: trusted certificate path must exist inside the selected slot and must not be a symlink`);
}
} catch {
errors.push(`${label}: runRoot or slotDir is missing`);
errors.push(`${label}: runRoot, slotDir, or trusted certificate path is missing`);
}
let appUrl: URL | undefined;