import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import http from "node:http";
import os from "node:os";
import path from "node:path";
import {
createDeploymentHttpServer,
listenDeploymentHttpServer,
loadDeploymentContract,
} from "./deployment-http-server.mjs";
const contract = loadDeploymentContract();
const root = fs.mkdtempSync(path.join(os.tmpdir(), "blender-deployment-http-"));
function write(relative, bytes) {
const filePath = path.join(root, relative);
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, bytes);
return Buffer.from(bytes);
}
function sha256(bytes) {
return crypto.createHash("sha256").update(bytes).digest("hex");
}
function deterministicBytes(size, seed) {
const bytes = Buffer.alloc(size);
for (let index = 0; index < bytes.length; index += 1) bytes[index] = (index * 31 + seed) & 0xff;
return bytes;
}
const fixtures = new Map([
["/index.html", write("index.html", "
Web Blender")],
["/assets/app.js", write("assets/app.js", "export const app = true;\n")],
["/assets/app.css", write("assets/app.css", "body { color: black; }\n")],
["/vendor/blender/single/web_engine.js", write("vendor/blender/single/web_engine.js", "export default () => {};\n")],
["/vendor/blender/single/web_engine.wasm", write("vendor/blender/single/web_engine.wasm", deterministicBytes(1024, 7))],
["/vendor/blender/pthread/web_engine.js", write("vendor/blender/pthread/web_engine.js", "export default () => {}; // pthread\n")],
["/engine-manifest.json", write("engine-manifest.json", "{\"schemaVersion\":2}\n")],
["/media/frame.png", write("media/frame.png", Buffer.from([0x89, 0x50, 0x4e, 0x47]))],
["/media/silence.wav", write("media/silence.wav", Buffer.from("RIFF0000WAVE"))],
["/fixtures/basic.blend", write("fixtures/basic.blend", deterministicBytes(4096, 11))],
["/volumes/smoke.nvdb", write("volumes/smoke.nvdb", deterministicBytes(1024 * 1024, 19))],
]);
const server = createDeploymentHttpServer({ root, contract });
let origin;
async function body(response) {
return Buffer.from(await response.arrayBuffer());
}
async function expectMime(url, expected, method = "GET") {
const response = await fetch(`${origin}${url}`, { method });
const actual = response.headers.get("content-type");
assert.equal(actual, expected, `MIME mismatch url=${url} expected=${expected} actual=${actual}`);
assert.equal(response.status, 200, `HTTP status mismatch url=${url} expected=200 actual=${response.status}`);
return response;
}
function expectedMime(url) {
const extension = path.extname(new URL(url, "http://deployment.local").pathname);
const mime = contract.mimeTypes[extension];
assert.ok(mime, `deployment contract has no MIME for url=${url}`);
return mime;
}
async function expectRange(url, header, expectedStart, expectedEnd) {
const source = fixtures.get(url);
const response = await fetch(`${origin}${url}`, { headers: { Range: header } });
assert.equal(response.status, contract.rangeRequests.satisfiedStatus, `range status url=${url} range=${header}`);
const bytes = await body(response);
assert.deepEqual(bytes, source.subarray(expectedStart, expectedEnd + 1), `range bytes url=${url} range=${header}`);
for (const name of contract.rangeRequests.requiredSatisfiedHeaders) {
assert.ok(response.headers.has(name), `missing ${name} url=${url} range=${header}`);
}
assert.equal(response.headers.get("content-range"), `bytes ${expectedStart}-${expectedEnd}/${source.length}`);
assert.equal(Number(response.headers.get("content-length")), expectedEnd - expectedStart + 1);
assert.match(response.headers.get("etag"), /^"sha256-[a-f0-9]{64}"$/);
return response;
}
async function interruptedPrefix(url) {
return new Promise((resolve, reject) => {
const chunks = [];
const request = http.get(`${origin}${url}`, (response) => {
response.once("data", (chunk) => {
chunks.push(chunk);
response.destroy();
resolve({ bytes: Buffer.concat(chunks), etag: response.headers.etag });
});
response.on("error", (error) => {
if (error.code !== "ECONNRESET") reject(error);
});
});
request.on("error", reject);
});
}
try {
origin = await listenDeploymentHttpServer(server);
const mimeCases = [
["/index.html", ".html"],
["/assets/app.js", ".js"],
["/assets/app.css", ".css"],
["/vendor/blender/single/web_engine.wasm", ".wasm"],
["/engine-manifest.json", ".json"],
["/media/frame.png", ".png"],
["/media/silence.wav", ".wav"],
["/fixtures/basic.blend", ".blend"],
["/volumes/smoke.nvdb", ".nvdb"],
];
for (const [url, extension] of mimeCases) {
assert.equal(expectedMime(url), contract.mimeTypes[extension]);
const response = await expectMime(url, contract.mimeTypes[extension]);
assert.deepEqual(await body(response), fixtures.get(url));
}
for (const url of [
"/vendor/blender/single/web_engine.js",
"/vendor/blender/single/web_engine.wasm",
"/vendor/blender/pthread/web_engine.js",
"/fixtures/basic.blend",
]) {
const response = await expectMime(url, expectedMime(url), "HEAD");
assert.equal((await response.arrayBuffer()).byteLength, 0, `HEAD returned a body url=${url}`);
assert.equal(Number(response.headers.get("content-length")), fixtures.get(url).length);
}
await expectRange("/vendor/blender/single/web_engine.wasm", "bytes=100-199", 100, 199);
await expectRange("/fixtures/basic.blend", "bytes=200-399", 200, 399);
await expectRange("/volumes/smoke.nvdb", "bytes=300-599", 300, 599);
await expectRange("/fixtures/basic.blend", "bytes=400-", 400, fixtures.get("/fixtures/basic.blend").length - 1);
await expectRange("/fixtures/basic.blend", "bytes=-128", fixtures.get("/fixtures/basic.blend").length - 128, fixtures.get("/fixtures/basic.blend").length - 1);
for (const range of ["items=0-1", "bytes=", "bytes=20-10", "bytes=0-999999", "bytes=0-1,4-5", "bytes=-0"]) {
const response = await fetch(`${origin}/fixtures/basic.blend`, { headers: { Range: range } });
assert.equal(response.status, contract.rangeRequests.unsatisfiedStatus, `invalid range accepted range=${range}`);
assert.equal((await response.arrayBuffer()).byteLength, 0);
assert.equal(response.headers.get("content-range"), `bytes */${fixtures.get("/fixtures/basic.blend").length}`);
}
const etagFirst = (await fetch(`${origin}/fixtures/basic.blend`)).headers.get("etag");
const etagSecond = (await fetch(`${origin}/fixtures/basic.blend`)).headers.get("etag");
assert.equal(etagFirst, etagSecond);
const matchingIfRange = await fetch(`${origin}/fixtures/basic.blend`, {
headers: { Range: "bytes=0-31", "If-Range": etagFirst },
});
assert.equal(matchingIfRange.status, 206);
const mismatchingIfRange = await fetch(`${origin}/fixtures/basic.blend`, {
headers: { Range: "bytes=0-31", "If-Range": '"sha256-old-revision"' },
});
assert.equal(mismatchingIfRange.status, 200);
assert.deepEqual(await body(mismatchingIfRange), fixtures.get("/fixtures/basic.blend"));
const interrupted = await interruptedPrefix("/volumes/smoke.nvdb");
assert.ok(interrupted.bytes.length > 0 && interrupted.bytes.length < fixtures.get("/volumes/smoke.nvdb").length);
const resumed = await fetch(`${origin}/volumes/smoke.nvdb`, {
headers: {
Range: `bytes=${interrupted.bytes.length}-`,
"If-Range": interrupted.etag,
},
});
assert.equal(resumed.status, 206);
const merged = Buffer.concat([interrupted.bytes, await body(resumed)]);
assert.equal(sha256(merged), sha256(fixtures.get("/volumes/smoke.nvdb")));
const replacement = deterministicBytes(fixtures.get("/fixtures/basic.blend").length, 23);
fs.writeFileSync(path.join(root, "fixtures/basic.blend"), replacement);
const changed = await fetch(`${origin}/fixtures/basic.blend`, {
headers: { Range: "bytes=32-", "If-Range": etagFirst },
});
assert.equal(changed.status, 200);
assert.notEqual(changed.headers.get("etag"), etagFirst);
assert.deepEqual(await body(changed), replacement);
process.stdout.write("deployment-http-ok mime=9 head=4 ranges=5 invalidRanges=6 resumeSha256=verified\n");
}
finally {
if (server.listening) await new Promise((resolve) => server.close(resolve));
fs.rmSync(root, { recursive: true, force: true });
}