Fixed the SPA authentication flow
This commit is contained in:
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user