182 lines
7.3 KiB
JavaScript
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);
|
|
});
|