Fixed the SPA authentication flow

This commit is contained in:
2026-10-21 15:55:29 -03:00
parent 5ef4c22c59
commit ba61f91c53
33 changed files with 1924 additions and 117 deletions

View File

@@ -3,13 +3,18 @@ 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({ user: { id: "synthetic@example.invalid", roles: ["SALES_REP", "SIMPLE_USER"] }, token: "synthetic-secret" });
return response(validIdentity);
} });
const result = await app.dispatch({ type });
assert.equal(result.ok, true);
@@ -26,6 +31,8 @@ for (const family of ["chrome", "firefox"]) {
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);
});
}
@@ -46,8 +53,116 @@ test("identity failures retain diagnostics but hide response tokens by default",
}
});
function content(identity) {
const app = loadApplication("content", { document: { createElement: () => ({ setAttribute() {} }) } });
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 releases its in-flight request", 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, 5, "background must not persist identity after the shared request settles");
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 => {
@@ -76,7 +191,7 @@ for (const [roles, allowed] of [[ ["SALES_REP", "SIMPLE_USER"], false ], [ ["SOL
assert.equal(app.currentUser.allowsCrmActivities(), false);
const same = app.currentUser.ensure();
assert.equal(same, app.currentUser.ensure());
finish({ ok: true, user: { roles } });
finish({ ok: true, user: { ...validIdentity.user, roles } });
await first;
assert.deepEqual(calls, [type, "tokenrelay", "opportunities"]);
app.productsContentState.currentProductsRequestId = "products-cycle";
@@ -98,15 +213,63 @@ for (const [roles, allowed] of [[ ["SALES_REP", "SIMPLE_USER"], false ], [ ["SOL
});
}
test("identity failure disables only CRM/Actions and does not retry on Reference changes", async () => {
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 { app, calls } = content(identity);
await app.opportunitiesController.requestOpportunitiesForPeriod("Current Quarter");
await app.opportunitiesController.requestOpportunitiesForPeriod("Next Quarter");
assert.equal(calls.filter(value => value === type).length, 1);
assert.equal(calls.filter(value => value === "opportunities").length, 2);
assert.equal(calls.includes("crm"), false);
app.opportunityWorkloadActions.loadProducts({ items: [{ RevnId: "123" }] });
assert.equal(calls.includes("opportunitiesExtension.requestWlActions"), false);
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);
});
}