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

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));
});