Files
zerobyte/app/test/backend-integration/runner/verification.ts
Nico 19a0781667 test: backend integration (#889)
* test: backend integration

* docs: mounted shares acls

* feat: smb expose real ACLs when available

* fix: re-init repo on setup

* chore: add missing @hono/standard-validator package

* chore: add happy-dom dev dep
2026-05-17 15:18:56 +02:00

151 lines
4.2 KiB
TypeScript

import crypto from "node:crypto";
import type { Stats } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import type { ExpectedEntry } from "./config";
export type SnapshotNode = {
name: string;
type: string;
path: string;
uid?: number;
gid?: number;
mode?: number;
size?: number;
};
function normalizeRelativePath(input: string): string {
const normalized = path.posix.normalize(input);
if (normalized === "") {
return ".";
}
return normalized;
}
function normalizeEntryType(type: string): string {
if (type === "dir" || type === "directory") {
return "directory";
}
return type;
}
async function readSha256(filePath: string): Promise<string> {
const digest = crypto.createHash("sha256");
digest.update(await fs.readFile(filePath));
return digest.digest("hex");
}
function assertMetadata(
entryLabel: string,
expected: ExpectedEntry,
actual: { uid?: number; gid?: number; mode?: number },
) {
if (expected.uid !== undefined && actual.uid !== expected.uid) {
throw new Error(`${entryLabel} uid mismatch: expected ${expected.uid}, got ${String(actual.uid)}`);
}
if (expected.gid !== undefined && actual.gid !== expected.gid) {
throw new Error(`${entryLabel} gid mismatch: expected ${expected.gid}, got ${String(actual.gid)}`);
}
if (expected.mode === undefined) {
return;
}
if (actual.mode === undefined) {
throw new Error(`${entryLabel} mode is missing`);
}
const permissionBits = actual.mode & 0o7777;
if (permissionBits !== expected.mode) {
throw new Error(
`${entryLabel} mode mismatch: expected ${expected.mode.toString(8)}, got ${permissionBits.toString(8)}`,
);
}
}
function getFilesystemEntryType(stats: Stats): ExpectedEntry["type"] {
if (stats.isDirectory()) {
return "directory";
}
if (stats.isSymbolicLink()) {
return "symlink";
}
return "file";
}
export async function verifyFilesystemEntries(basePath: string, expectedEntries: ExpectedEntry[], label: string) {
for (const expected of expectedEntries) {
const targetPath = path.resolve(basePath, expected.path);
const stats = await fs.lstat(targetPath);
const actualType = getFilesystemEntryType(stats);
if (actualType !== expected.type) {
throw new Error(`${label}: ${expected.path} type mismatch: expected ${expected.type}, got ${actualType}`);
}
assertMetadata(`${label}: ${expected.path}`, expected, stats);
if (expected.type === "file" && expected.text !== undefined) {
const actualText = await fs.readFile(targetPath, "utf8");
if (actualText !== expected.text) {
throw new Error(`${label}: ${expected.path} text content mismatch`);
}
}
if (expected.type === "file" && expected.sha256 !== undefined) {
const actualSha = await readSha256(targetPath);
if (actualSha !== expected.sha256.toLowerCase()) {
throw new Error(`${label}: ${expected.path} sha256 mismatch`);
}
}
if (expected.type !== "symlink" || expected.linkTarget === undefined) {
continue;
}
const actualTarget = await fs.readlink(targetPath);
if (actualTarget !== expected.linkTarget) {
throw new Error(
`${label}: ${expected.path} symlink target mismatch: expected ${expected.linkTarget}, got ${actualTarget}`,
);
}
}
}
export async function verifySnapshotEntries(
snapshotRootPath: string,
nodes: SnapshotNode[],
expectedEntries: ExpectedEntry[],
) {
const nodesByRelativePath = new Map<string, SnapshotNode>();
for (const node of nodes) {
const relativePath = path.posix.relative(snapshotRootPath, node.path);
if (!relativePath || relativePath === "." || relativePath === ".." || relativePath.startsWith("../")) {
continue;
}
nodesByRelativePath.set(normalizeRelativePath(relativePath), node);
}
for (const expected of expectedEntries) {
const normalizedExpectedPath = normalizeRelativePath(expected.path);
const node = nodesByRelativePath.get(normalizedExpectedPath);
if (!node) {
throw new Error(`snapshot: missing ${expected.path}`);
}
const actualType = normalizeEntryType(node.type);
if (actualType !== expected.type) {
throw new Error(`snapshot: ${expected.path} type mismatch: expected ${expected.type}, got ${actualType}`);
}
assertMetadata(`snapshot: ${expected.path}`, expected, node);
}
}