299 lines
15 KiB
JavaScript
299 lines
15 KiB
JavaScript
const assert = require("node:assert/strict");
|
|
const { test } = require("node:test");
|
|
const { loadBackground, response } = require("./support/characterization.cjs");
|
|
const { loadApplication } = require("./support/application.cjs");
|
|
const type = "opportunitiesExtension.requestCurrentUser";
|
|
const validIdentity = {
|
|
user: { id: "synthetic@example.invalid", name: "Synthetic User", onWhiteList: false,
|
|
provisioned: true, administrator: false, roles: ["SALES_REP", "SIMPLE_USER"] },
|
|
token: "synthetic-secret"
|
|
};
|
|
|
|
for (const family of ["chrome", "firefox"]) {
|
|
test(`${family}: current user GET preserves identity token only in debug data for explicit reveal`, async () => {
|
|
const calls = [];
|
|
const app = loadBackground({ family, fetch: async (url, options) => {
|
|
calls.push({ url, options });
|
|
return response(validIdentity);
|
|
} });
|
|
const result = await app.dispatch({ type });
|
|
assert.equal(result.ok, true);
|
|
assert.equal(calls.length, 1);
|
|
assert.equal(calls[0].url, "https://spa.oracle.com/oalcrm/web/api/identity-management/users/current");
|
|
assert.equal(calls[0].options.method, "GET");
|
|
assert.equal(calls[0].options.credentials, "include");
|
|
assert.equal(calls[0].options.cache, "no-store");
|
|
assert.equal(result.debugRequests[0].name, "requestCurrentUser");
|
|
assert.equal(result.debugRequests[0].httpStatus, 200);
|
|
assert.equal(JSON.parse(result.debugRequests[0].responseBody).token, "synthetic-secret");
|
|
const content = loadApplication("content");
|
|
assert.doesNotMatch(content.debugContentLog.formatDebugBody(result.debugRequests[0].responseBody, "requestCurrentUser"), /synthetic-secret/);
|
|
content.debugState.debugSecretsVisible = true;
|
|
assert.match(content.debugContentLog.formatDebugBody(result.debugRequests[0].responseBody, "requestCurrentUser"), /synthetic-secret/);
|
|
assert.equal(result.token, undefined);
|
|
assert.equal(app.tabs.length, 0, "a valid existing session must not open a tab");
|
|
assert.equal(app.removedTabs.length, 0);
|
|
});
|
|
}
|
|
|
|
test("identity failures retain diagnostics but hide response tokens by default", async () => {
|
|
for (const fetch of [
|
|
async () => new Response("synthetic-secret<html>fallback</html>"),
|
|
async () => response({ token: "synthetic-secret", user: {} }),
|
|
async () => response({ token: "synthetic-secret", user: { roles: [null] } }),
|
|
async () => response({ token: "synthetic-secret" }, 401),
|
|
async () => { throw new Error("Synthetic network failure"); }
|
|
]) {
|
|
const app = loadBackground({ fetch });
|
|
const result = await app.dispatch({ type });
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.debugRequests[0].status, "failed");
|
|
const content = loadApplication("content");
|
|
assert.doesNotMatch(content.debugContentLog.formatDebugBody(result.debugRequests[0].responseBody, "requestCurrentUser"), /synthetic-secret/);
|
|
}
|
|
});
|
|
|
|
test("retry timer is called without the environment object as its receiver", async () => {
|
|
let attempts = 0;
|
|
let timers = 0;
|
|
const { app } = content(() => ++attempts === 1 ? { ok: false } : { ok: true, user: validIdentity.user }, {
|
|
setTimeout: function(callback) {
|
|
"use strict";
|
|
assert.equal(this, undefined, "Window timers reject an arbitrary receiver in Edge");
|
|
timers++;
|
|
setImmediate(callback);
|
|
}
|
|
});
|
|
await app.currentUser.ensure();
|
|
assert.equal(attempts, 2);
|
|
assert.equal(timers, 1);
|
|
});
|
|
|
|
for (const family of ["chrome", "firefox"]) {
|
|
test(`${family}: current-user SSO recovery shares one verified SPA tab and closes on valid JSON`, async () => {
|
|
let attempts = 0;
|
|
const app = loadBackground({ family, fetch: async (url, options) => {
|
|
assert.equal(url, "https://spa.oracle.com/oalcrm/web/api/identity-management/users/current");
|
|
assert.equal(options.credentials, "include");
|
|
attempts++;
|
|
if (attempts <= 2) return new Response('<html><body onload="document.forms[0].submit()"><form method="POST"></form></body></html>');
|
|
assert.equal(app.tabs.length, 1, "SSO must be started before retry can succeed");
|
|
assert.equal(app.removedTabs.length, 0, "do not close before a valid identity response");
|
|
return response(validIdentity);
|
|
} });
|
|
const [first, second] = await Promise.all([app.dispatch({ type }), app.dispatch({ type })]);
|
|
assert.equal(first.ok, true);
|
|
assert.deepEqual(first, second);
|
|
assert.equal(attempts, 3);
|
|
assert.deepEqual(app.tabs, [{
|
|
url: "https://spa.oracle.com/",
|
|
active: false, windowId: 3, ...(family === "firefox" ? { cookieStoreId: "test-store" } : {})
|
|
}]);
|
|
assert.deepEqual(app.removedTabs, [91]);
|
|
assert.equal(first.debugRequests[0].metadata.sessionBootstrapState, "opened-inactive-tab");
|
|
assert.equal(first.debugRequests[1].metadata.sessionBootstrapState, "waiting-for-valid-session");
|
|
assert.equal(first.debugRequests[2].metadata.sessionBootstrapState, "closed-after-valid-json");
|
|
});
|
|
}
|
|
|
|
test("expired SPA session recovery handles 401, 403 and network errors, then reuses its successful identity", async () => {
|
|
let attempts = 0;
|
|
const app = loadBackground({ fetch: async () => {
|
|
attempts++;
|
|
if (attempts === 1) return response({}, 401);
|
|
if (attempts === 2) return response({}, 403);
|
|
if (attempts === 3) throw new TypeError("Failed to fetch");
|
|
return response(validIdentity);
|
|
} });
|
|
const first = await app.dispatch({ type });
|
|
assert.equal(first.ok, true);
|
|
assert.equal(app.tabs.length, 1);
|
|
assert.deepEqual(first.debugRequests.map(entry => entry.httpStatus), [401, 403, null, 200]);
|
|
assert.deepEqual(app.removedTabs, [91]);
|
|
assert.equal((await app.dispatch({ type })).ok, true);
|
|
assert.equal(attempts, 4, "successful identity must not be fetched again");
|
|
assert.equal((await app.dispatch({ type })).debugRequests.length, 0);
|
|
assert.equal(app.tabs.length, 1);
|
|
});
|
|
|
|
test("failed recovery closes its own tab and allows another cycle; server errors do not open SPA", async () => {
|
|
let ready = false;
|
|
const app = loadBackground({ fetch: async () => ready ? response(validIdentity) : new Response("<html>Sign in</html>") });
|
|
const failure = await app.dispatch({ type });
|
|
assert.equal(failure.ok, false);
|
|
assert.equal(failure.debugRequests.length, 6);
|
|
assert.deepEqual(app.removedTabs, [91]);
|
|
assert.equal(failure.debugRequests.at(-1).metadata.retryLimitReached, "true");
|
|
ready = true;
|
|
assert.equal((await app.dispatch({ type })).ok, true);
|
|
const unavailable = loadBackground({ fetch: async () => response({}, 503) });
|
|
assert.equal((await unavailable.dispatch({ type })).ok, false);
|
|
assert.equal(unavailable.tabs.length, 0);
|
|
});
|
|
|
|
test("SPA tab opening failures stop API probes; a later cycle and cleanup failure preserve valid identity", async () => {
|
|
let listener;
|
|
let attempts = 0;
|
|
let opens = 0;
|
|
const app = loadApplication("background", {
|
|
chrome: { runtime: { onMessage: { addListener: callback => { listener = callback; } } } }, URL,
|
|
fetch: async () => { attempts++; return new Response("<html>SSO</html>"); }
|
|
});
|
|
app.platformBackgroundApi.tabsCreate = async details => {
|
|
assert.equal(details.active, false);
|
|
if (++opens === 1) throw new Error("Temporary tab error");
|
|
return { id: 92 };
|
|
};
|
|
app.platformBackgroundApi.wait = async () => {};
|
|
app.platformBackgroundApi.tabsSendMessage = async (id, message) => message.type === "opportunitiesExtension.inspectSpaPage"
|
|
? { status: "ready", documentId: "test-document" }
|
|
: { status: 200, contentType: "application/json", body: JSON.stringify(validIdentity) };
|
|
app.platformBackgroundApi.tabsRemove = async id => { assert.equal(id, 92); throw new Error("Tab already closed"); };
|
|
app.start();
|
|
const dispatch = () => new Promise(resolve => listener({ type }, { tab: { windowId: 3 } }, resolve));
|
|
const failure = await dispatch();
|
|
assert.equal(failure.ok, false);
|
|
assert.equal(attempts, 1);
|
|
assert.match(failure.debugRequests[0].metadata.sessionBootstrapState, /tab-open-failed/);
|
|
const result = await dispatch();
|
|
assert.equal(result.ok, true);
|
|
assert.equal(opens, 2);
|
|
assert.equal(attempts, 2);
|
|
assert.match(result.debugRequests[1].metadata.sessionBootstrapState, /tab-close-failed/);
|
|
});
|
|
|
|
function content(identity, overrides = {}) {
|
|
const app = loadApplication("content", { document: { createElement: () => ({ setAttribute() {} }) }, ...overrides });
|
|
app.debugContentView.renderDebugPanel = () => {};
|
|
const calls = [];
|
|
app.platformContentMessages.sendRuntimeMessage = async message => {
|
|
calls.push(message.type);
|
|
if (message.type === type) return identity();
|
|
return { ok: true, actions: [], hasNext: false };
|
|
};
|
|
app.authContentSession.requestTokenRelayOnce = async () => { calls.push("tokenrelay"); return "synthetic"; };
|
|
app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ workloadActions: { value: "synthetic" } });
|
|
app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => { calls.push("crm"); return []; };
|
|
app.fusionOpportunities.requestOpportunitiesPage = async () => { calls.push("opportunities"); return []; };
|
|
app.productsContentJobs.renderProductsProgress = () => {};
|
|
app.productsContentJobs.requestOpptyProductsInBackground = () => {};
|
|
app.productsContentJobs.queueProductsTableRender = () => {};
|
|
app.opportunitiesTable.setOpportunitiesTableState = () => {};
|
|
app.preferencesState.selectedOpportunityTypeValues = new Set(["ORA_ALLOPTIES"]);
|
|
return { app, calls };
|
|
}
|
|
|
|
for (const [roles, allowed] of [[ ["SALES_REP", "SIMPLE_USER"], false ], [ ["SOLUTION_ENG_NOTERR"], true ], [ [], true ]]) {
|
|
test(`identity runs first, once; CRM/Actions enabled=${allowed} for ${JSON.stringify(roles)}`, async () => {
|
|
let finish;
|
|
const { app, calls } = content(() => new Promise(resolve => { finish = resolve; }));
|
|
const first = app.opportunitiesController.requestOpportunitiesForPeriod("Current Quarter");
|
|
assert.deepEqual(calls, [type]);
|
|
assert.equal(app.currentUser.allowsCrmActivities(), false);
|
|
const same = app.currentUser.ensure();
|
|
assert.equal(same, app.currentUser.ensure());
|
|
finish({ ok: true, user: { ...validIdentity.user, roles } });
|
|
await first;
|
|
assert.deepEqual(calls, [type, "tokenrelay", "opportunities"]);
|
|
app.productsContentState.currentProductsRequestId = "products-cycle";
|
|
app.productActivities.begin("products-cycle", "synthetic");
|
|
const products = { items: [{ RevnId: "123" }] };
|
|
app.productsContentState.opptyProductsByNumber.set("OP", products);
|
|
app.productActivities.complete("products-cycle");
|
|
await new Promise(resolve => setImmediate(resolve));
|
|
assert.equal(calls.includes("opportunitiesExtension.requestWlActions"), allowed);
|
|
if (!allowed) {
|
|
assert.equal(app.opportunityCrmServiceRequests.createBadge("OP"), null);
|
|
assert.equal(app.opportunityWorkloadActions.createBadge("OP"), null);
|
|
}
|
|
await app.opportunitiesController.requestOpportunitiesForPeriod("Next Quarter");
|
|
app.opportunityCrmServiceRequests.reset();
|
|
app.opportunityWorkloadActions.reopen();
|
|
await app.currentUser.ensure();
|
|
assert.equal(calls.filter(value => value === type).length, 1);
|
|
});
|
|
}
|
|
|
|
test("identity failures retry with shared backoff instead of being cached as a final result", async () => {
|
|
for (const identity of [async () => ({ ok: false }), async () => ({ ok: true, user: {} }), async () => { throw new Error("Offline"); }]) {
|
|
const timers = [];
|
|
const delays = [];
|
|
let attempts = 0;
|
|
const { app, calls } = content(() => ++attempts <= 6 ? identity() : { ok: true, user: validIdentity.user }, {
|
|
setTimeout: (callback, delay) => { timers.push(callback); delays.push(delay); }
|
|
});
|
|
const first = app.opportunitiesController.requestOpportunitiesForPeriod("Current Quarter");
|
|
const second = app.opportunitiesController.requestOpportunitiesForPeriod("Next Quarter");
|
|
assert.equal(app.currentUser.ensure(), app.currentUser.ensure());
|
|
for (let i = 0; i < 6; i++) {
|
|
await new Promise(resolve => setImmediate(resolve));
|
|
assert.ok(calls.every(value => value === type));
|
|
assert.equal(timers.length, 1);
|
|
assert.equal(app.currentUser.getUserId(), "");
|
|
timers.shift()();
|
|
}
|
|
await Promise.all([first, second]);
|
|
assert.deepEqual(delays, [1000, 2000, 4000, 8000, 10000, 10000]);
|
|
assert.equal(attempts, 7);
|
|
assert.deepEqual(calls.slice(-2), ["tokenrelay", "opportunities"]);
|
|
await app.currentUser.ensure();
|
|
assert.equal(attempts, 7);
|
|
assert.equal(app.currentUser.getUserId(), validIdentity.user.id);
|
|
}
|
|
});
|
|
|
|
for (const family of ["chrome", "firefox"]) {
|
|
test(`${family}: HTML, malformed JSON and invalid identity objects retry until valid user/token JSON`, async () => {
|
|
const payloads = [
|
|
() => new Response("<html>Sign in</html>"),
|
|
() => new Response('{"user":'),
|
|
() => response(null),
|
|
() => response([]),
|
|
() => response({}),
|
|
() => response({ ...validIdentity, token: "" }),
|
|
() => response({ ...validIdentity, user: { ...validIdentity.user, id: "" } }),
|
|
() => response({ ...validIdentity, user: { ...validIdentity.user, name: null } }),
|
|
() => response({ ...validIdentity, user: { ...validIdentity.user, roles: [null] } }),
|
|
() => response({ ...validIdentity, user: { ...validIdentity.user, provisioned: "true" } }),
|
|
() => response(validIdentity)
|
|
];
|
|
let attempts = 0;
|
|
const background = loadBackground({ family, fetch: async () => payloads[attempts++]() });
|
|
const { app } = content(() => background.dispatch({ type }), { setTimeout: callback => setImmediate(callback) });
|
|
const logs = [];
|
|
app.debugContentLog.appendBackgroundRequestLog = entries => logs.push(...entries);
|
|
const pending = app.currentUser.ensure();
|
|
assert.equal(pending, app.currentUser.ensure());
|
|
await pending;
|
|
assert.equal(attempts, payloads.length);
|
|
assert.equal(app.currentUser.getUserId(), validIdentity.user.id);
|
|
assert.equal(app.currentUser.allowsCrmActivities(), false, "valid SALES_REP identity must not retry");
|
|
assert.equal(logs.filter(entry => entry.status === "failed").length, payloads.length - 1);
|
|
assert.equal(logs.at(-1).status, "success");
|
|
await app.currentUser.ensure();
|
|
assert.equal(attempts, payloads.length);
|
|
});
|
|
}
|
|
|
|
test("identity cache survives background recreation, shares windows and isolates containers", async () => {
|
|
const factory = new (require("node:vm").Script)(require("node:fs").readFileSync("src/services/spa/current-user.js", "utf8") + "\ncreateSpaCurrentUser").runInNewContext();
|
|
const data = {};
|
|
const sessionStorage = { async get(key) { return { [key]: data[key] }; }, async set(values) { Object.assign(data, values); } };
|
|
let calls = 0;
|
|
const deps = { sessionStorage, fetchJson: async () => { calls++; return validIdentity; } };
|
|
const service = factory(deps);
|
|
const sender = (windowId, cookieStoreId = "", incognito = false) => ({ tab: { windowId, cookieStoreId, incognito } });
|
|
await Promise.all([service.requestCurrentUser(sender(1)), service.requestCurrentUser(sender(2))]);
|
|
assert.equal(calls, 1);
|
|
await service.requestCurrentUser(sender(3));
|
|
assert.equal(calls, 1);
|
|
const restored = await factory(deps).requestCurrentUser(sender(4));
|
|
assert.equal(restored.user.id, validIdentity.user.id);
|
|
assert.equal(restored.debugRequests.length, 0);
|
|
assert.equal(calls, 1);
|
|
await service.requestCurrentUser(sender(1, "container-2"));
|
|
await service.requestCurrentUser(sender(1, "", true));
|
|
assert.equal(calls, 3);
|
|
assert.ok(Object.values(data).every(user => !user.token));
|
|
});
|