Files
homarr/scripts/release-v2-qa/report-path-integrity.spec.ts
T

121 lines
5.3 KiB
TypeScript

// @vitest-environment node
import { spawnSync } from "node:child_process";
import { mkdtemp, mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises";
import { createServer } from "node:net";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
assertSafeReportPath,
readSafeReportFile,
validateResolvedArtifactPath,
writeSafeReportFile,
} from "./report-path-integrity.mts";
const temporaryPaths: string[] = [];
const temporaryDirectory = async (suffix: string): Promise<string> => {
const directory = await mkdtemp(path.join(tmpdir(), `homarr-release-v2-qa-${suffix}-`));
temporaryPaths.push(directory);
return directory;
};
afterEach(async () => {
await Promise.all(temporaryPaths.splice(0).map((target) => rm(target, { force: true, recursive: true })));
});
describe("release-v2 QA report path integrity", () => {
it.each(["reports/preflight-01/report.md", "ledger.json", "master-report.md"])(
"rejects a symlinked aggregate input or output path: %s",
async (relativePath) => {
const qaRoot = await temporaryDirectory("report-path");
const target = path.join(qaRoot, "outside-target");
const linkedPath = path.join(qaRoot, relativePath);
await mkdir(path.dirname(linkedPath), { recursive: true });
await writeFile(target, "outside");
await symlink(target, linkedPath);
await expect(assertSafeReportPath(qaRoot, linkedPath, "QA report path")).rejects.toThrow("symlink");
await expect(readSafeReportFile(qaRoot, linkedPath, "QA report path")).rejects.toThrow("symlink");
await expect(writeSafeReportFile(qaRoot, linkedPath, "replacement", "QA report path")).rejects.toThrow("symlink");
await expect(readFile(target, "utf8")).resolves.toBe("outside");
},
);
it("rejects an artifact symlink that ultimately escapes its packet directory", async () => {
const root = await temporaryDirectory("artifact-escape");
const packetDirectory = path.join(root, ".screenshots", "release-v2", "preflight-01");
const outsideArtifact = path.join(root, "outside.png");
const linkedArtifact = path.join(packetDirectory, "escape.png");
await mkdir(packetDirectory, { recursive: true });
await writeFile(outsideArtifact, "outside");
await symlink(outsideArtifact, linkedArtifact);
await expect(
validateResolvedArtifactPath(packetDirectory, linkedArtifact, "preflight-01: artifact"),
).resolves.toContain("resolves outside");
});
it("accepts a resolved regular artifact inside the packet directory", async () => {
const root = await temporaryDirectory("artifact-valid");
const packetDirectory = path.join(root, ".screenshots", "release-v2", "preflight-01");
const artifact = path.join(packetDirectory, "valid.png");
await mkdir(packetDirectory, { recursive: true });
await writeFile(artifact, "valid");
await expect(validateResolvedArtifactPath(packetDirectory, artifact, "preflight-01: artifact")).resolves.toBeNull();
});
it("rejects an artifact directory and an in-directory artifact symlink", async () => {
const root = await temporaryDirectory("artifact-not-file");
const packetDirectory = path.join(root, ".screenshots", "release-v2", "preflight-01");
const artifactDirectory = path.join(packetDirectory, "looks-like-evidence.png");
const realArtifact = path.join(packetDirectory, "real.png");
const linkedArtifact = path.join(packetDirectory, "linked.png");
await mkdir(artifactDirectory, { recursive: true });
await writeFile(realArtifact, "valid");
await symlink(realArtifact, linkedArtifact);
await expect(
validateResolvedArtifactPath(packetDirectory, artifactDirectory, "preflight-01: artifact"),
).resolves.toContain("regular file");
await expect(
validateResolvedArtifactPath(packetDirectory, linkedArtifact, "preflight-01: artifact"),
).resolves.toContain("symlink");
});
it.runIf(process.platform !== "win32")("rejects a FIFO artifact", async () => {
const root = await temporaryDirectory("artifact-fifo");
const packetDirectory = path.join(root, ".screenshots", "release-v2", "preflight-01");
const fifoArtifact = path.join(packetDirectory, "capture.pipe");
await mkdir(packetDirectory, { recursive: true });
const result = spawnSync("mkfifo", [fifoArtifact]);
expect(result.status).toBe(0);
await expect(
validateResolvedArtifactPath(packetDirectory, fifoArtifact, "preflight-01: artifact"),
).resolves.toContain("regular file");
});
it.runIf(process.platform !== "win32")("rejects a socket artifact", async () => {
const root = await temporaryDirectory("artifact-socket");
const packetDirectory = path.join(root, ".screenshots", "release-v2", "preflight-01");
const socketArtifact = path.join(packetDirectory, "capture.sock");
await mkdir(packetDirectory, { recursive: true });
const server = createServer();
await new Promise<void>((resolve, reject) => {
server.once("error", reject);
server.listen(socketArtifact, resolve);
});
try {
await expect(
validateResolvedArtifactPath(packetDirectory, socketArtifact, "preflight-01: artifact"),
).resolves.toContain("regular file");
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
});