120 lines
5.9 KiB
JavaScript
120 lines
5.9 KiB
JavaScript
const assert = require("node:assert/strict");
|
|
const { test } = require("node:test");
|
|
const vm = require("node:vm");
|
|
const { composeScript } = require("../scripts/compose.cjs");
|
|
const script = new vm.Script(composeScript("spa-page"));
|
|
const inspect = "opportunitiesExtension.inspectSpaPage";
|
|
const read = "opportunitiesExtension.readSpaIdentity";
|
|
const validIdentity = { user: { id: "test@example.invalid", name: "Test User", roles: [] }, token: "synthetic-token" };
|
|
|
|
function harness({ family = "chrome", origin = "https://spa.oracle.com", subframe = false, fetchResponse } = {}) {
|
|
let listener;
|
|
const requests = [];
|
|
const timers = new Map();
|
|
const dom = { pathname: "/oalcrm/", title: "SPA", body: "Sales Planning and Analytics", readyState: "complete", controls: true, loginSelector: "" };
|
|
const window = {};
|
|
window.top = subframe ? {} : window;
|
|
const document = {
|
|
get title() { return dom.title; },
|
|
get body() { return { innerText: dom.body }; },
|
|
get readyState() { return dom.readyState; },
|
|
querySelector: selector => selector.startsWith("a[href]") ? dom.controls : Boolean(dom.loginSelector && selector.includes(dom.loginSelector))
|
|
};
|
|
script.runInNewContext({
|
|
window, document, location: { origin, get pathname() { return dom.pathname; } }, performance: { timeOrigin: 123456 },
|
|
[family]: { runtime: { id: "test-extension", onMessage: { addListener: callback => { listener = callback; } } } },
|
|
AbortController,
|
|
setTimeout: callback => { const id = {}; timers.set(id, callback); return id; },
|
|
clearTimeout: id => timers.delete(id),
|
|
fetch: async (url, options) => {
|
|
requests.push({ url, options });
|
|
return fetchResponse ? fetchResponse(options) : new Response(JSON.stringify(validIdentity), { headers: { "Content-Type": "application/json", "X-Request-Id": "synthetic" } });
|
|
}
|
|
});
|
|
const send = (type, documentId = "123456:/oalcrm/") => new Promise(resolve => listener({ type, documentId }, { id: "test-extension" }, resolve));
|
|
return { dom, send, requests, timers, get listener() { return listener; } };
|
|
}
|
|
|
|
test("SPA inspector is restricted to the top-level SPA origin and trusted extension messages", () => {
|
|
assert.equal(harness({ origin: "https://other.example.invalid" }).listener, undefined);
|
|
assert.equal(harness({ subframe: true }).listener, undefined);
|
|
const h = harness();
|
|
assert.equal(h.listener({ type: inspect }, { id: "other-extension" }, () => assert.fail("Must not reply")), false);
|
|
assert.equal(h.listener({ type: "unknown" }, { id: "test-extension" }, () => assert.fail("Must not reply")), false);
|
|
assert.equal(h.requests.length, 0, "installing the inspector never starts an API request");
|
|
});
|
|
|
|
test("error, SAML/login, blank and incomplete pages never authorize a fetch", async () => {
|
|
const cases = [
|
|
[{ body: "401 Authorization Required" }, "error"],
|
|
[{ title: "403 Forbidden" }, "error"],
|
|
[{ body: "Internal Server Error" }, "error"],
|
|
[{ pathname: "/cloudgate/v1/oauth2/callback" }, "login"],
|
|
[{ title: "Oracle Sign In" }, "login"],
|
|
[{ loginSelector: 'input[name="SAMLResponse"]' }, "login"],
|
|
[{ loginSelector: 'input[name="SAMLRequest"]' }, "login"],
|
|
[{ loginSelector: 'input[type="password"]' }, "login"],
|
|
[{ loginSelector: 'meta[http-equiv="refresh" i]' }, "login"],
|
|
[{ readyState: "interactive" }, "loading"],
|
|
[{ readyState: "loading" }, "loading"],
|
|
[{ title: "", body: "" }, "loading"],
|
|
[{ controls: false }, "loading"]
|
|
];
|
|
for (const [dom, expected] of cases) {
|
|
const h = harness();
|
|
Object.assign(h.dom, dom);
|
|
assert.equal((await h.send(inspect)).status, expected, JSON.stringify(dom));
|
|
assert.match((await h.send(read)).error, /not ready/);
|
|
assert.equal(h.requests.length, 0);
|
|
}
|
|
});
|
|
|
|
for (const family of ["chrome", "browser"]) {
|
|
test(`${family}: a loaded application reads identity once per document in its own authenticated context`, async () => {
|
|
const h = harness({ family });
|
|
const page = await h.send(inspect);
|
|
assert.equal(page.status, "ready");
|
|
const [first, second] = await Promise.all([h.send(read, page.documentId), h.send(read, page.documentId)]);
|
|
assert.deepEqual(first, second);
|
|
assert.deepEqual(JSON.parse(first.body), validIdentity);
|
|
assert.equal(first.headers["x-request-id"], "synthetic");
|
|
assert.equal((await h.send(read)).body, first.body);
|
|
assert.equal(h.requests.length, 1);
|
|
assert.equal(h.requests[0].url, "https://spa.oracle.com/oalcrm/web/api/identity-management/users/current");
|
|
assert.equal(h.requests[0].options.method, "GET");
|
|
assert.equal(h.requests[0].options.credentials, "include");
|
|
assert.equal(h.requests[0].options.cache, "no-store");
|
|
assert.equal(h.timers.size, 0);
|
|
assert.match((await h.send(read, "old-document")).error, /navigated/);
|
|
h.dom.pathname = "/login";
|
|
assert.match((await h.send(read)).error, /not ready/);
|
|
assert.equal(h.requests.length, 1);
|
|
});
|
|
}
|
|
|
|
test("network or HTML failures are not retried in the same SPA document", async () => {
|
|
for (const fetchResponse of [
|
|
() => { throw new Error("Network failure"); },
|
|
() => new Response('<html><form><input name="SAMLResponse"></form></html>', { headers: { "Content-Type": "text/html" } })
|
|
]) {
|
|
const h = harness({ fetchResponse });
|
|
const first = await h.send(read);
|
|
for (let i = 0; i < 20; i++) assert.deepEqual(await h.send(read), first);
|
|
assert.equal(h.requests.length, 1);
|
|
assert.equal(h.timers.size, 0);
|
|
}
|
|
});
|
|
|
|
test("a stalled identity fetch is aborted and never leaves a retry timer", async () => {
|
|
const h = harness({ fetchResponse: options => new Promise((resolve, reject) => {
|
|
options.signal.addEventListener("abort", () => reject(new Error("Timed out")));
|
|
}) });
|
|
const pending = h.send(read);
|
|
assert.equal(h.timers.size, 1);
|
|
[...h.timers.values()][0]();
|
|
assert.equal((await pending).error, "Timed out");
|
|
assert.equal(h.timers.size, 0);
|
|
await h.send(read);
|
|
assert.equal(h.requests.length, 1);
|
|
});
|