test: share media server harness

This commit is contained in:
Peter Steinberger
2026-04-20 19:18:27 +01:00
parent 5934a8eacc
commit f8bb35ead0
3 changed files with 125 additions and 107 deletions

View File

@@ -1,8 +1,11 @@
import { createRequire } from "node:module";
import type { AddressInfo } from "node:net";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { createSuiteTempRootTracker } from "../test-helpers/temp-dir.js";
import { withEnvAsync } from "../test-utils/env.js";
import {
LOOPBACK_FETCH_ENV,
startMediaServerTestHarness,
type MediaServerTestHarness,
} from "./server.test-support.js";
const mocks = vi.hoisted(() => ({
readFileWithinRoot: vi.fn(),
@@ -24,59 +27,29 @@ vi.mock("./server.runtime.js", () => {
};
});
let startMediaServer: typeof import("./server.js").startMediaServer;
let realFetch: typeof import("undici").fetch;
let mediaHarness: MediaServerTestHarness | undefined;
const mediaRootTracker = createSuiteTempRootTracker({
prefix: "openclaw-media-outside-workspace-",
});
const LOOPBACK_FETCH_ENV = {
HTTP_PROXY: undefined,
HTTPS_PROXY: undefined,
ALL_PROXY: undefined,
http_proxy: undefined,
https_proxy: undefined,
all_proxy: undefined,
NO_PROXY: "127.0.0.1,localhost",
no_proxy: "127.0.0.1,localhost",
} as const;
async function expectOutsideWorkspaceServerResponse(url: string) {
const response = await withEnvAsync(LOOPBACK_FETCH_ENV, () => realFetch(url));
const response = await withEnvAsync(LOOPBACK_FETCH_ENV, () => mediaHarness!.fetch(url));
expect(response.status).toBe(400);
expect(await response.text()).toBe("file is outside workspace root");
}
describe("media server outside-workspace mapping", () => {
let server: Awaited<ReturnType<typeof startMediaServer>> | undefined;
let listenBlocked = false;
let port = 0;
beforeAll(async () => {
vi.useRealTimers();
vi.doUnmock("undici");
const require = createRequire(import.meta.url);
({ startMediaServer } = await import("./server.js"));
({ fetch: realFetch } = require("undici") as typeof import("undici"));
await mediaRootTracker.setup();
mediaDir = await mediaRootTracker.make("case");
try {
server = await startMediaServer(0, 1_000);
} catch (error) {
if (
error instanceof Error &&
"code" in error &&
(error.code === "EPERM" || error.code === "EACCES")
) {
listenBlocked = true;
return;
}
throw error;
}
const boundServer = server;
if (!boundServer) {
return;
}
port = (boundServer.address() as AddressInfo).port;
mediaHarness = await startMediaServerTestHarness({
setupMediaRoot: async () => {
await mediaRootTracker.setup();
mediaDir = await mediaRootTracker.make("case");
},
cleanupMediaRoot: async () => {
await mediaRootTracker.cleanup();
mediaDir = "";
},
});
});
beforeEach(() => {
@@ -85,16 +58,12 @@ describe("media server outside-workspace mapping", () => {
});
afterAll(async () => {
const boundServer = server;
if (boundServer) {
await new Promise((resolve) => boundServer.close(resolve));
}
await mediaRootTracker.cleanup();
mediaDir = "";
await mediaHarness?.cleanup();
mediaHarness = undefined;
});
it("returns 400 with a specific outside-workspace message", async () => {
if (listenBlocked) {
if (mediaHarness?.listenBlocked) {
return;
}
mocks.readFileWithinRoot.mockRejectedValueOnce({
@@ -102,6 +71,6 @@ describe("media server outside-workspace mapping", () => {
message: "file is outside workspace root",
});
await expectOutsideWorkspaceServerResponse(`http://127.0.0.1:${port}/media/ok-id`);
await expectOutsideWorkspaceServerResponse(mediaHarness!.url("ok-id"));
});
});

View File

@@ -0,0 +1,76 @@
import { createRequire } from "node:module";
import type { AddressInfo } from "node:net";
import { vi } from "vitest";
type MediaTestServer = Awaited<ReturnType<typeof import("./server.js").startMediaServer>>;
type UndiciFetch = typeof import("undici").fetch;
export const LOOPBACK_FETCH_ENV = {
HTTP_PROXY: undefined,
HTTPS_PROXY: undefined,
ALL_PROXY: undefined,
http_proxy: undefined,
https_proxy: undefined,
all_proxy: undefined,
NO_PROXY: "127.0.0.1,localhost",
no_proxy: "127.0.0.1,localhost",
} as const;
export interface MediaServerTestHarness {
fetch: UndiciFetch;
listenBlocked: boolean;
port: number;
url: (mediaPath: string) => string;
cleanup: () => Promise<void>;
}
function isListenPermissionError(error: unknown): boolean {
return (
error instanceof Error && "code" in error && (error.code === "EPERM" || error.code === "EACCES")
);
}
export async function startMediaServerTestHarness(params: {
setupMediaRoot: () => Promise<void>;
cleanupMediaRoot: () => Promise<void>;
cleanupTtlMs?: number;
}): Promise<MediaServerTestHarness> {
vi.useRealTimers();
vi.doUnmock("undici");
const require = createRequire(import.meta.url);
const { startMediaServer } = await import("./server.js");
const { fetch } = require("undici") as typeof import("undici");
let server: MediaTestServer | undefined;
let listenBlocked = false;
let port = 0;
await params.setupMediaRoot();
try {
server = await startMediaServer(0, params.cleanupTtlMs ?? 1_000);
} catch (error) {
if (!isListenPermissionError(error)) {
throw error;
}
listenBlocked = true;
}
if (server) {
port = (server.address() as AddressInfo).port;
}
return {
fetch,
listenBlocked,
port,
url: (mediaPath: string) => `http://127.0.0.1:${port}/media/${mediaPath}`,
cleanup: async () => {
if (server) {
await new Promise((resolve) => server?.close(resolve));
}
await params.cleanupMediaRoot();
},
};
}

View File

@@ -1,10 +1,13 @@
import fs from "node:fs/promises";
import { createRequire } from "node:module";
import type { AddressInfo } from "node:net";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { createSuiteTempRootTracker } from "../test-helpers/temp-dir.js";
import { withEnvAsync } from "../test-utils/env.js";
import {
LOOPBACK_FETCH_ENV,
startMediaServerTestHarness,
type MediaServerTestHarness,
} from "./server.test-support.js";
let MEDIA_DIR = "";
const cleanOldMedia = vi.fn().mockResolvedValue(undefined);
@@ -18,20 +21,9 @@ vi.mock("./store.js", async () => {
};
});
let startMediaServer: typeof import("./server.js").startMediaServer;
let MEDIA_MAX_BYTES: typeof import("./store.js").MEDIA_MAX_BYTES;
let realFetch: typeof import("undici").fetch;
let mediaHarness: MediaServerTestHarness | undefined;
const mediaRootTracker = createSuiteTempRootTracker({ prefix: "openclaw-media-test-" });
const LOOPBACK_FETCH_ENV = {
HTTP_PROXY: undefined,
HTTPS_PROXY: undefined,
ALL_PROXY: undefined,
http_proxy: undefined,
https_proxy: undefined,
all_proxy: undefined,
NO_PROXY: "127.0.0.1,localhost",
no_proxy: "127.0.0.1,localhost",
} as const;
async function waitForFileRemoval(filePath: string, maxTicks = 1000) {
for (let tick = 0; tick < maxTicks; tick += 1) {
@@ -46,12 +38,8 @@ async function waitForFileRemoval(filePath: string, maxTicks = 1000) {
}
describe("media server", () => {
let server: Awaited<ReturnType<typeof startMediaServer>> | undefined;
let listenBlocked = false;
let port = 0;
function mediaUrl(id: string) {
return `http://127.0.0.1:${port}/media/${id}`;
return mediaHarness?.url(id) ?? "";
}
async function writeMediaFile(id: string, contents: string) {
@@ -70,7 +58,7 @@ describe("media server", () => {
}
function expectFetchedResponse(
response: Awaited<ReturnType<typeof realFetch>>,
response: Awaited<ReturnType<MediaServerTestHarness["fetch"]>>,
expected: { status: number; noSniff?: boolean },
) {
expect(response.status).toBe(expected.status);
@@ -89,7 +77,9 @@ describe("media server", () => {
}) {
const file = await writeMediaFile(params.id, params.contents);
await params.mutateFile?.(file);
const res = await withEnvAsync(LOOPBACK_FETCH_ENV, () => realFetch(mediaUrl(params.id)));
const res = await withEnvAsync(LOOPBACK_FETCH_ENV, () =>
mediaHarness!.fetch(mediaUrl(params.id)),
);
expectFetchedResponse(res, { status: params.expectedStatus });
if (params.expectedBody !== undefined) {
expect(await res.text()).toBe(params.expectedBody);
@@ -105,7 +95,9 @@ describe("media server", () => {
setup?: () => Promise<void>;
}) {
await params.setup?.();
const res = await withEnvAsync(LOOPBACK_FETCH_ENV, () => realFetch(mediaUrl(params.mediaPath)));
const res = await withEnvAsync(LOOPBACK_FETCH_ENV, () =>
mediaHarness!.fetch(mediaUrl(params.mediaPath)),
);
expectFetchedResponse(res, {
status: params.expectedStatus,
...(params.expectedNoSniff ? { noSniff: true } : {}),
@@ -116,41 +108,22 @@ describe("media server", () => {
}
beforeAll(async () => {
vi.useRealTimers();
vi.doUnmock("undici");
const require = createRequire(import.meta.url);
({ startMediaServer } = await import("./server.js"));
({ MEDIA_MAX_BYTES } = await import("./store.js"));
({ fetch: realFetch } = require("undici") as typeof import("undici"));
await mediaRootTracker.setup();
MEDIA_DIR = await mediaRootTracker.make("case");
try {
server = await startMediaServer(0, 1_000);
} catch (error) {
if (
error instanceof Error &&
"code" in error &&
(error.code === "EPERM" || error.code === "EACCES")
) {
listenBlocked = true;
return;
}
throw error;
}
const boundServer = server;
if (!boundServer) {
return;
}
port = (boundServer.address() as AddressInfo).port;
mediaHarness = await startMediaServerTestHarness({
setupMediaRoot: async () => {
await mediaRootTracker.setup();
MEDIA_DIR = await mediaRootTracker.make("case");
},
cleanupMediaRoot: async () => {
await mediaRootTracker.cleanup();
MEDIA_DIR = "";
},
});
});
afterAll(async () => {
const boundServer = server;
if (boundServer) {
await new Promise((r) => boundServer.close(r));
}
await mediaRootTracker.cleanup();
MEDIA_DIR = "";
await mediaHarness?.cleanup();
mediaHarness = undefined;
});
it.each([
@@ -173,7 +146,7 @@ describe("media server", () => {
assertAfterFetch: expectMissingMediaFile,
},
] as const)("$name", async (testCase) => {
if (listenBlocked) {
if (mediaHarness?.listenBlocked) {
return;
}
await expectMediaFileLifecycleCase(testCase);
@@ -235,7 +208,7 @@ describe("media server", () => {
expectedBody: "invalid path",
},
] as const)("%#", async (testCase) => {
if (listenBlocked) {
if (mediaHarness?.listenBlocked) {
return;
}
await expectFetchedMediaCase(testCase);