Files
opportunities-extension/tests/spaSessionWait.test.cjs

182 lines
7.3 KiB
JavaScript

const assert = require("node:assert/strict");
const { test } = require("node:test");
const { loadApplication } = require("./support/application.cjs");
const type = "opportunitiesExtension.requestCurrentUser";
const identity = { user: { id: "test@example.invalid", name: "Test User", roles: [] }, token: "synthetic-token" };
const flush = () => new Promise(resolve => setImmediate(resolve));
function harness({ validAfter = 0 } = {}) {
let now = Date.UTC(2026, 9, 21);
let sequence = 0;
let backgroundCalls = 0;
let tabCalls = 0;
let listener;
let page = { status: "loading", documentId: "first-document" };
const timers = new Map();
const tabs = [];
const updates = [];
const removed = [];
function event() {
const listeners = new Set();
return { listeners, addListener: callback => listeners.add(callback), removeListener: callback => listeners.delete(callback),
emit: value => [...listeners].forEach(callback => callback(value)) };
}
const headers = event();
const cookies = event();
class ClockDate extends Date {
constructor(...args) { super(...(args.length ? args : [now])); }
static now() { return now; }
}
const app = loadApplication("background", {
chrome: { runtime: { onMessage: { addListener: callback => { listener = callback; } } },
webRequest: { onHeadersReceived: headers }, cookies: { onChanged: cookies } }, URL,
Date: ClockDate,
setTimeout: function(callback, delay) {
"use strict";
assert.equal(this, undefined);
const id = ++sequence;
timers.set(id, { callback, at: now + delay });
return id;
},
clearTimeout: id => timers.delete(id),
fetch: async () => { backgroundCalls++; return new Response("<html>SSO</html>"); }
});
app.platformBackgroundApi.tabsCreate = async details => { tabs.push(details); return { id: 91 }; };
app.platformBackgroundApi.tabsUpdate = async (id, details) => { updates.push({ id, details }); return { id }; };
app.platformBackgroundApi.tabsRemove = async id => { removed.push(id); };
app.platformBackgroundApi.tabsSendMessage = async (id, message, options) => {
assert.equal(id, 91);
assert.equal(options.frameId, 0);
if (message.type === "opportunitiesExtension.inspectSpaPage") return page;
assert.equal(message.type, "opportunitiesExtension.readSpaIdentity");
assert.equal(message.documentId, page.documentId);
assert.equal(page.status, "ready");
const valid = ++tabCalls > validAfter;
return { status: 200, contentType: valid ? "application/json" : "text/html", body: valid ? JSON.stringify(identity) : "<html>SSO</html>" };
};
app.start();
function dispatch() {
return new Promise(resolve => listener({ type }, { tab: { id: 7, windowId: 3, cookieStoreId: "store-a" } }, resolve));
}
async function advance(ms) {
const until = now + ms;
for (;;) {
const next = [...timers].filter(([, timer]) => timer.at <= until).sort((a, b) => a[1].at - b[1].at)[0];
if (!next) break;
now = next[1].at;
timers.delete(next[0]);
next[1].callback();
await flush();
}
now = until;
await flush();
}
function navigation(overrides = {}) {
headers.emit({ tabId: 91, type: "main_frame", statusCode: 200, url: "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=whitespace", ...overrides });
}
function cookie() {
cookies.emit({ removed: false, cookie: { name: "synthetic-session", value: "never-log-this-cookie", domain: "spa.oracle.com",
storeId: "store-a", secure: true, httpOnly: true } });
}
return { dispatch, advance, navigation, cookie, tabs, updates, removed, timers, cookies, headers,
setPage: (status, documentId = "first-document") => { page = { status, documentId }; },
get backgroundCalls() { return backgroundCalls; }, get tabCalls() { return tabCalls; } };
}
test("HTTP 200, cookies and sixty seconds do not release identity GETs on a login page", async () => {
const h = harness();
const first = h.dispatch();
const second = h.dispatch();
await flush();
h.setPage("login");
h.navigation();
h.cookie();
await h.advance(60000);
assert.equal(h.backgroundCalls, 1);
assert.equal(h.tabCalls, 0);
h.setPage("ready");
await h.advance(1500);
const [result, shared] = await Promise.all([first, second]);
assert.equal(result.ok, true);
assert.deepEqual(result, shared);
assert.equal(h.backgroundCalls, 1);
assert.equal(h.tabCalls, 1, "identity is read in the verified SPA tab, not in the background");
assert.equal(h.tabs.length, 1);
assert.deepEqual(h.removed, [91]);
assert.equal(h.timers.size, 0);
assert.equal(h.cookies.listeners.size, 0);
assert.equal(h.headers.listeners.size, 1, "only the unrelated SR observer remains");
assert.equal(result.debugRequests[0].metadata.sessionWaitReason, "spa-document-verified");
assert.equal(result.debugRequests[1].metadata.executionContext, "spa-tab");
assert.doesNotMatch(JSON.stringify(result), /never-log-this-cookie/);
});
test("callback 401 restarts only the owned SPA home; another HTTP 200 error page still blocks GETs", async () => {
const h = harness();
const pending = h.dispatch();
await flush();
h.setPage("error");
h.navigation({ statusCode: 401, url: "https://spa.oracle.com/cloudgate/v1/oauth2/callback?code=never-replay" });
await h.advance(1000);
assert.equal(h.updates.length, 1);
assert.equal(h.updates[0].details.url, "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=whitespace");
assert.equal(h.updates[0].details.active, false);
h.navigation();
await h.advance(10000);
assert.equal(h.tabCalls, 0);
assert.equal(h.updates.length, 1, "do not repeatedly restart a document already being navigated away from");
h.setPage("ready", "new-document");
await h.advance(1500);
assert.equal((await pending).ok, true);
assert.equal(h.tabCalls, 1);
});
test("timeout fails closed with no fallback API request", async () => {
const h = harness();
const pending = h.dispatch();
await flush();
h.navigation();
h.cookie();
await h.advance(120000);
const result = await pending;
assert.equal(result.ok, false);
assert.match(result.error, /did not finish authentication/);
assert.equal(result.debugRequests.length, 1);
assert.equal(h.backgroundCalls, 1);
assert.equal(h.tabCalls, 0);
assert.deepEqual(h.removed, [91]);
assert.equal(h.timers.size, 0);
});
test("invalid identity requires a new verified document; the old ready document is never probed twice", async () => {
const h = harness({ validAfter: 1 });
h.setPage("ready");
const pending = h.dispatch();
await flush();
await h.advance(500);
assert.equal(h.tabCalls, 1);
assert.equal(h.updates.length, 1);
await h.advance(30000);
assert.equal(h.tabCalls, 1);
assert.equal(h.updates.length, 1);
h.setPage("ready", "reloaded-document");
await h.advance(1500);
assert.equal((await pending).ok, true);
assert.equal(h.tabCalls, 2);
assert.equal(h.backgroundCalls, 1);
});
test("a redirect during stable-document confirmation does not trigger a GET", async () => {
const h = harness();
h.setPage("ready");
const pending = h.dispatch();
await flush();
h.setPage("login", "redirect-document");
await h.advance(2000);
assert.equal(h.tabCalls, 0);
h.setPage("ready", "final-document");
await h.advance(2000);
assert.equal((await pending).ok, true);
assert.equal(h.tabCalls, 1);
});