User profile controls that hide non-sales features from users outside sales

This commit is contained in:
2026-09-24 17:23:50 -03:00
parent f74071e39e
commit 451c27e9d7
22 changed files with 391 additions and 36 deletions

View File

@@ -5,6 +5,7 @@ const response = data => new Response(JSON.stringify(data), { status: 200 });
function setup(fetch) {
const document = { createElement: () => ({ attributes: {}, addEventListener() {}, setAttribute(key, value) { this.attributes[key] = value; } }) };
const app = loadApplication("content", { fetch, document, Intl, Date });
app.currentUser.allowsCrmActivities = () => true;
app.debugContentView.renderDebugPanel = () => {};
app.opportunitiesQueryState.periodRequestVersion = 1;
return app;

View File

@@ -32,6 +32,7 @@ function setup() {
const copied = [];
const app = loadApplication("content", { document, window, setTimeout, clearTimeout,
navigator: { clipboard: { writeText: async value => copied.push(value) } } });
app.currentUser.allowsCrmActivities = () => true;
// Keep the copy test independent of the toast's own DOM/timer implementation.
modal.querySelector = () => ({ classList: { toggle() {} } });
window.setTimeout = () => 0;

105
tests/currentUser.test.cjs Normal file
View File

@@ -0,0 +1,105 @@
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";
for (const family of ["chrome", "firefox"]) {
test(`${family}: current user GET uses SPA cookies and discards identity token before messaging/debug`, 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" });
} });
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.doesNotMatch(JSON.stringify(result), /synthetic-secret/);
assert.equal(result.token, undefined);
});
}
test("identity failures and malformed roles remain explicit and never expose response tokens", 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");
assert.doesNotMatch(JSON.stringify(result), /synthetic-secret/);
}
});
function content(identity) {
const app = loadApplication("content", { document: { createElement: () => ({ setAttribute() {} }) } });
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: { roles } });
await first;
assert.deepEqual(calls, allowed ? [type, "tokenrelay", "crm", "opportunities"] : [type, "tokenrelay", "opportunities"]);
const products = { items: [{ RevnId: "123" }] };
app.productsContentState.opptyProductsByNumber.set("OP", products);
app.opportunityWorkloadActions.loadProducts(products);
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 failure disables only CRM/Actions and does not retry on Reference changes", 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);
}
});

View File

@@ -36,6 +36,7 @@ function setup(items = [synthetic()]) {
const document = { createElement: tag => new Element(tag), createElementNS: (_, tag) => new Element(tag),
getElementById: id => id === app.contracts.OPPORTUNITIES_TABLE_ID ? table : null };
const app = loadApplication("content", { document, Intl, Date });
app.currentUser.allowsCrmActivities = () => true;
const summaries = [];
app.aiSummaryContent.requestGenerateAiSummary = number => summaries.push(number);
app.opportunitiesTable.setOpportunitiesTableState({ status: "ready", items });

View File

@@ -53,6 +53,7 @@ test("workload actions reject missing credentials, invalid ids, HTTP/network/JSO
function contentFixture(send) {
const app = loadApplication("content", { document: { createElement: () => ({ attributes: {}, setAttribute(k, v) { this.attributes[k] = v; } }) } });
app.currentUser.allowsCrmActivities = () => true;
app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ workloadActions: { value: "synthetic-wwb" } });
app.authSessionState.principal = "test@example.invalid";
app.platformContentMessages.sendRuntimeMessage = send;
@@ -165,6 +166,7 @@ test("product progress triggers Actions for cached products and ignores stale pr
window: { requestAnimationFrame() {} },
chrome: { runtime: { onMessage: { addListener(fn) { listener = fn; } } } }
});
app.currentUser.allowsCrmActivities = () => true;
app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ workloadActions: { value: "synthetic" } });
app.platformContentMessages.sendRuntimeMessage = async message => {
calls.push(message);