Files
anonymous_github/test/review-intent-client.test.js
T

362 lines
12 KiB
JavaScript

const { expect } = require("chai");
const https = require("https");
const fs = require("fs");
const os = require("os");
const path = require("path");
const process = require("process");
const { fork, execFileSync } = require("child_process");
const {
setTimeout,
clearTimeout,
setInterval,
clearInterval,
} = require("timers");
const clientId = "a".repeat(32),
intentId = "c".repeat(32);
const at = (offset) =>
new Date(Math.floor((Date.now() + offset) / 1000) * 1000)
.toISOString()
.replace(".000Z", "Z");
const input = () => ({
contract: "4open.artifacts/1",
clientId,
intentId,
token: "d".repeat(64),
requestId: "e".repeat(32),
});
const intent = () => ({
contract: "4open.artifacts/1",
clientId,
intentId,
submissionRef: "f".repeat(32),
callbackId: "1".repeat(32),
entitlementId: "2".repeat(32),
expiresAt: at(120000),
policy: {
version: 1,
access: "restricted-review",
retainUntil: at(86400000),
},
});
describe("review intent HTTPS client", function () {
this.timeout(20000);
let server,
child,
folder,
origin,
handler,
calls,
serial = 0;
const pending = new Map();
const config = () => ({
origin,
clientId,
keyId: "rotation-1",
token: "b".repeat(64),
notBefore: at(-60000),
notAfter: at(3600000),
});
function call(options = {}) {
const id = ++serial;
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
pending.delete(id);
reject(new Error("child test timed out"));
}, 18000);
pending.set(id, (value) => {
clearTimeout(timer);
resolve(value);
});
child.send({ id, config: config(), input: input(), ...options });
});
}
before(async function () {
folder = fs.mkdtempSync(path.join(os.tmpdir(), "review-intent-tls-"));
execFileSync(
"openssl",
[
"req",
"-x509",
"-newkey",
"rsa:2048",
"-nodes",
"-days",
"1",
"-subj",
"/CN=localhost",
"-addext",
"subjectAltName=IP:127.0.0.1,DNS:localhost",
"-keyout",
path.join(folder, "key"),
"-out",
path.join(folder, "cert"),
],
{ stdio: "ignore" },
);
server = https.createServer(
{
key: fs.readFileSync(path.join(folder, "key")),
cert: fs.readFileSync(path.join(folder, "cert")),
},
(req, res) => {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => {
calls.push({
method: req.method,
url: req.url,
headers: req.headers,
body: JSON.parse(Buffer.concat(chunks).toString()),
});
handler(req, res);
});
},
);
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
origin = "https://127.0.0.1:" + server.address().port;
child = fork(path.join(__dirname, "fixtures/review-intent-client.js"), {
env: { ...process.env, NODE_EXTRA_CA_CERTS: path.join(folder, "cert") },
stdio: ["ignore", "ignore", "pipe", "ipc"],
});
child.on("message", (message) => {
const done = pending.get(message.id);
pending.delete(message.id);
if (done) done(message);
});
});
after(async function () {
child?.kill();
server?.closeAllConnections();
if (server) await new Promise((resolve) => server.close(resolve));
if (folder) fs.rmSync(folder, { recursive: true, force: true });
});
beforeEach(() => {
calls = [];
handler = (_req, res) => {
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(intent()));
};
});
it("uses authenticated TLS, the saved policy, and the caller's retry ID", async () => {
const result = await call({ mode: "mutate" });
expect(result.result.policy.access).to.equal("restricted-review");
expect(result.frozen).to.equal(true);
expect(result.serialized).to.equal("{}");
expect(calls).to.have.length(1);
expect(calls[0].body).to.deep.equal(input());
expect(calls[0].method).to.equal("POST");
expect(calls[0].url).to.equal("/service/v1/artifact-intents/consume");
expect(calls[0].headers.authorization).to.equal("Bearer " + "b".repeat(64));
expect(calls[0].headers["x-4open-artifact-client-id"]).to.equal(clientId);
expect(calls[0].headers["x-4open-artifact-service-key-id"]).to.equal(
"rotation-1",
);
for (const key of ["cookie", "origin", "referer"])
expect(calls[0].headers[key]).to.equal(undefined);
await call();
expect(calls[1].body.requestId).to.equal(calls[0].body.requestId);
});
it("rejects invalid config and caller data before making a request", async () => {
for (const patch of [
{ origin: origin + "/" },
{ origin: "http://127.0.0.1" },
{ origin: origin + "/path" },
{ token: "bad" },
{ notAfter: at(-1000) },
{ notBefore: at(30000) },
{ clientId: "bad" },
{ extra: "ignored?" },
])
expect((await call({ config: { ...config(), ...patch } })).kind).to.equal(
"invalid",
);
for (const patch of [
{ clientId: "0".repeat(32) },
{ token: "bad" },
{ requestId: "bad" },
{ policy: {} },
{ contract: "other" },
])
expect((await call({ input: { ...input(), ...patch } })).kind).to.equal(
"invalid",
);
expect((await call({ mode: "preabort" })).kind).to.equal("unavailable");
expect(calls).to.have.length(0);
});
it("classifies failures without following redirects, retrying, or exposing response bodies", async () => {
for (const [status, kind] of [
[301, "protocol"],
[400, "rejected"],
[401, "rejected"],
[403, "rejected"],
[404, "not-found"],
[409, "conflict"],
[410, "expired"],
[422, "rejected"],
[429, "unavailable"],
[503, "unavailable"],
]) {
handler = (_req, res) => {
res.writeHead(status, {
Location: origin + "/leak",
"Content-Length": "10000000",
});
res.write("private response and token");
};
const result = await call();
expect(result.kind).to.equal(kind);
expect(result.message).to.equal("Review intent consumption " + kind);
}
expect(calls).to.have.length(10);
});
it("rejects mismatched identity, policy, expiry, unknown fields and malformed JSON", async () => {
const good = intent();
const values = [
{ ...good, clientId: "0".repeat(32) },
{ ...good, intentId: "0".repeat(32) },
{ ...good, callbackId: "bad" },
{ ...good, entitlementId: "bad" },
{ ...good, expiresAt: at(-1000) },
{ ...good, expiresAt: at(700000) },
{ ...good, policy: { ...good.policy, access: "public" } },
{ ...good, policy: { ...good.policy, version: 1.5 } },
{ ...good, policy: { ...good.policy, retainUntil: at(-1000) } },
{ ...good, owner: "browser" },
];
const raw = [
...values.map((v) => JSON.stringify(v)),
"null",
"[]",
"{",
JSON.stringify(good).replace(
'"clientId":',
'"clientId":"' + clientId + '","cl\\u0069entId":',
),
JSON.stringify(good).replace('"version":1', '"version":2,"version":1'),
Buffer.from([0xff]),
" ".repeat(65537),
];
for (const body of raw) {
handler = (_req, res) => {
res.setHeader("Content-Type", "application/json");
res.end(body);
};
expect((await call()).kind).to.equal("protocol");
}
});
it("rejects cookies, encoding, ambiguous content types and oversized declared bodies", async () => {
for (const headers of [
{ "Set-Cookie": "identity=secret" },
{ "Content-Encoding": "gzip" },
{ "Content-Type": "text/html" },
{ "Content-Type": ["application/json", "application/json"] },
{ "Content-Length": "65537" },
]) {
handler = (_req, res) => {
res.writeHead(200, { "Content-Type": "application/json", ...headers });
res.write(JSON.stringify(intent()));
};
expect((await call()).kind).to.equal("protocol");
}
});
it("bounds concurrent attempts and releases capacity after completion", async () => {
handler = (_req, res) =>
setTimeout(() => {
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(intent()));
}, 150);
const result = await call({ mode: "parallel" });
expect(result.results).to.deep.equal(["ok", "ok", "ok", "ok", "busy"]);
expect(result.followup.intentId).to.equal(intentId);
expect(calls).to.have.length(5);
});
it("cancels an unfinished response without retrying", async () => {
handler = (_req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.write("{");
};
expect((await call({ mode: "abort" })).kind).to.equal("unavailable");
expect(calls).to.have.length(1);
});
it("times out an inactive response within its request deadline", async () => {
handler = (_req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.write("{");
};
const start = Date.now();
expect((await call()).kind).to.equal("unavailable");
expect(Date.now() - start).to.be.within(4500, 12000);
expect(calls).to.have.length(1);
});
it("rejects an untrusted TLS certificate before sending credentials", async () => {
const env = {
...process.env,
NODE_EXTRA_CA_CERTS: "",
NODE_TLS_REJECT_UNAUTHORIZED: "1",
};
delete env.NODE_OPTIONS;
const untrusted = fork(
path.join(__dirname, "fixtures/review-intent-client.js"),
{ env, stdio: ["ignore", "ignore", "pipe", "ipc"] },
);
try {
const response = new Promise((resolve) =>
untrusted.once("message", resolve),
);
untrusted.send({ id: 1, config: config(), input: input() });
expect((await response).kind).to.equal("unavailable");
expect(calls).to.have.length(0);
} finally {
untrusted.kill();
}
});
it("rejects a response received after the credential expires", async () => {
handler = (_req, res) =>
setTimeout(() => {
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(intent()));
}, 2100);
expect(
(await call({ config: { ...config(), notAfter: at(2000) } })).kind,
).to.equal("unavailable");
expect(calls).to.have.length(1);
});
it("enforces the total deadline even while the peer streams data", async () => {
handler = (_req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.write(" ");
const timer = setInterval(() => res.write(" "), 200);
res.once("close", () => clearInterval(timer));
};
const start = Date.now();
expect((await call()).kind).to.equal("unavailable");
expect(Date.now() - start).to.be.within(9500, 15000);
expect(calls).to.have.length(1);
});
it("measures repeated fresh TLS connections when explicitly requested", async function () {
if (!process.env.TEST_REVIEW_INTENT_PERF) this.skip();
const times = [];
for (let n = 0; n < 100; n++) {
const result = await call();
expect(result.result.intentId).to.equal(intentId);
times.push(result.elapsedMs);
}
times.sort((a, b) => a - b);
const receipt = {
calls: calls.length,
transport: "fresh loopback HTTPS connections",
synthetic: true,
meanMs: times.reduce((a, b) => a + b, 0) / times.length,
medianMs: times[50],
p95Ms: times[94],
};
expect(calls).to.have.length(100);
if (process.env.TEST_REVIEW_INTENT_PERF_REPORT)
fs.writeFileSync(
process.env.TEST_REVIEW_INTENT_PERF_REPORT,
JSON.stringify(receipt, null, 2) + "\n",
);
});
});