299 lines
16 KiB
JavaScript
299 lines
16 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 flush = () => new Promise(resolve => setImmediate(resolve));
|
|
|
|
for (const family of ["chrome", "firefox"]) {
|
|
test(`${family}: WWB-TS selection, fixed workload header, first-page GET and masked debug`, async () => {
|
|
const calls = [];
|
|
const h = loadBackground({ family, fetch: async (url, options) => {
|
|
calls.push({ url, options });
|
|
if (url.endsWith("consumerTokenService")) return response([
|
|
{ tokenService: "ACCP-TS", uiaasHeader: { value: "summary" } },
|
|
{ tokenService: "OCICONS-TS", uiaasHeader: { value: "consumption" } },
|
|
{ tokenService: "WWB-TS", uiaasHeader: { label: "ignored-label", value: "synthetic-wwb" } }
|
|
]);
|
|
return response({ actions: [{ id: 1, name: "TEST" }], totalCount: 20, hasNext: true });
|
|
} });
|
|
const auth = await h.dispatch({ type: "opportunitiesExtension.requestConsumerTokenService" });
|
|
assert.equal(auth.authorizations.workloadActions.value, "synthetic-wwb");
|
|
const result = await h.dispatch({ type: "opportunitiesExtension.requestWlActions", revnId: "12345", authorization: auth.authorizations.workloadActions });
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.hasNext, true);
|
|
assert.equal(result.actions.length, 1);
|
|
assert.equal(calls.length, 2, "no pagination");
|
|
assert.equal(calls[1].url, "https://spa.oracle.com/oalcrm/web/api/provider-proxy/wwb-provider/service/workbench-proxy/wwb/mgmt/actions?parentType=WORKLOAD&parentId=12345");
|
|
assert.equal(calls[1].options.method, "GET");
|
|
assert.equal(calls[1].options.headers["Wwb-Provider-Authorization"], "synthetic-wwb");
|
|
assert.equal(calls[1].options.headers.Authorization, undefined);
|
|
assert.equal(result.debugRequests[0].name, "requestWlActions");
|
|
assert.equal(result.debugRequests[0].requestHeaders["Wwb-Provider-Authorization"], "(present)");
|
|
});
|
|
}
|
|
|
|
test("workload actions reject missing credentials, invalid ids, HTTP/network/JSON failures", async () => {
|
|
for (const fetch of [
|
|
async () => { throw new Error("Synthetic network failure"); },
|
|
async () => new Response("Denied", { status: 401 }),
|
|
async () => new Response("Denied", { status: 403 }),
|
|
async () => new Response("<html>Login</html>"),
|
|
async () => response({ actions: null, hasNext: false })
|
|
]) {
|
|
const h = loadBackground({ fetch });
|
|
const result = await h.dispatch({ type: "opportunitiesExtension.requestWlActions", revnId: "123", authorization: { value: "synthetic" } });
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.debugRequests.length, 1);
|
|
}
|
|
const h = loadBackground({ fetch: () => assert.fail("Unexpected network") });
|
|
for (const fields of [{ revnId: "123" }, { revnId: "bad&id=2", authorization: { value: "synthetic" } }]) {
|
|
assert.equal((await h.dispatch({ type: "opportunitiesExtension.requestWlActions", ...fields })).ok, false);
|
|
}
|
|
});
|
|
|
|
function contentFixture(send) {
|
|
const document = { createElement: () => ({ attributes: {}, append() {}, addEventListener() {}, setAttribute(k, v) { this.attributes[k] = v; } }) };
|
|
document.createElementNS = (_, tag) => document.createElement(tag);
|
|
const app = loadApplication("content", { document });
|
|
app.currentUser.allowsCrmActivities = () => true;
|
|
app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ workloadActions: { value: "synthetic-wwb" } });
|
|
app.authSessionState.principal = "test@example.invalid";
|
|
app.platformContentMessages.sendRuntimeMessage = send;
|
|
app.debugContentView.renderDebugPanel = () => {};
|
|
app.productsContentJobs.queueProductsTableRender = () => {};
|
|
return app;
|
|
}
|
|
|
|
test("Actions progress counts product workloads, not opportunities, even with filters", async () => {
|
|
const pending = new Map();
|
|
const app = contentFixture(message => new Promise(resolve => pending.set(message.revnId, resolve)));
|
|
const progress = [];
|
|
app.activityProgress.update = (...args) => progress.push(args);
|
|
app.opportunitiesFilterState.tableSearch = "hidden";
|
|
const a = { items: [{ RevnId: "1" }, { RevnId: "2" }] };
|
|
const b = { items: [{ RevnId: "3" }] };
|
|
app.productsContentState.opptyProductsByNumber = new Map([["A", a], ["B", b]]);
|
|
const loading = [app.opportunityWorkloadActions.loadProducts(a), app.opportunityWorkloadActions.loadProducts(b)];
|
|
await new Promise(resolve => setImmediate(resolve));
|
|
assert.equal(pending.size, 3);
|
|
pending.get("1")({ ok: true, actions: [], hasNext: false });
|
|
await new Promise(resolve => setImmediate(resolve));
|
|
assert.deepEqual(progress.at(-1), ["actions", "running", 1, 3]);
|
|
pending.get("2")({ ok: true, actions: [], hasNext: false });
|
|
await new Promise(resolve => setImmediate(resolve));
|
|
assert.deepEqual(progress.at(-1), ["actions", "running", 2, 3]);
|
|
pending.get("3")({ ok: true, actions: [], hasNext: false });
|
|
await Promise.all(loading);
|
|
assert.deepEqual(progress.at(-1), ["actions", "complete", 3, 3]);
|
|
});
|
|
|
|
test("saved response updates the workload badge, tooltip and matching cache without fetching", async () => {
|
|
const app = contentFixture(() => assert.fail("Save response must not trigger a GET"));
|
|
const writes = [];
|
|
app.activityCache.write = async (...args) => writes.push(args);
|
|
const products = { items: [{ RevnId: "123" }], cachedAt: 1000 };
|
|
app.productsContentState.opptyProductsByNumber.set("A", products);
|
|
app.productsContentState.opptyProductsByNumber.set("B", { items: [{ RevnId: "456" }] });
|
|
const action = { id: 77, name: "REVIEW", ownerEmail: "test@example.invalid", revenueId: 123, status: "COMPLETE" };
|
|
assert.equal(await app.opportunityWorkloadActions.applySavedAction("A", "123", action), true);
|
|
assert.equal(app.opportunityWorkloadActions.getSummary("A").state, "matched");
|
|
assert.equal(app.opportunityWorkloadActions.getSummary("A").actions.length, 1);
|
|
assert.equal(app.opportunityWorkloadActions.getSummary("B").actions.length, 0);
|
|
assert.ok(app.opportunityWorkloadActions.createBadge("A").title.includes("ID: 77"));
|
|
assert.deepEqual(writes[0].slice(0, 3), ["actions", "123", 1000]);
|
|
assert.equal(writes[0][3][0].id, 77);
|
|
await app.opportunityWorkloadActions.applySavedAction("A", "123", { action: { ...action, status: "OPEN" } });
|
|
assert.equal(app.opportunityWorkloadActions.getSummary("A").actions.length, 1);
|
|
assert.equal(app.opportunityWorkloadActions.getSummary("A").actions[0].status, "OPEN");
|
|
await app.opportunityWorkloadActions.applySavedAction("A", "123", { actions: [{ ...action, id: 78, ownerEmail: "other@example.invalid" }] });
|
|
assert.equal(app.opportunityWorkloadActions.getSummary("A").actions.length, 2);
|
|
assert.equal(await app.opportunityWorkloadActions.applySavedAction("B", "123", action), false);
|
|
assert.equal(await app.opportunityWorkloadActions.applySavedAction("A", "123", null), false);
|
|
});
|
|
|
|
test("saved Action uses its email rather than display name for green badge and removes plus", async () => {
|
|
const app = contentFixture(() => assert.fail("No follow-up GET"));
|
|
app.activityCache.write = async () => {};
|
|
app.productsContentState.opptyProductsByNumber.set("A", { items: [{ RevnId: "123" }] });
|
|
const saved = { id: 90, owner: "Test User", ownerEmail: " TEST@example.invalid ", name: "REVIEW" };
|
|
await app.opportunityWorkloadActions.applySavedAction("A", "123", saved);
|
|
const badge = app.opportunityWorkloadActions.createBadge("A");
|
|
assert.equal(badge.attributes["data-state"], "matched");
|
|
assert.equal(badge.textContent, "1");
|
|
await app.opportunityWorkloadActions.applySavedAction("A", "123", { id: 90, owner: "Test User" }, { ownerEmail: "test@example.invalid" });
|
|
assert.equal(app.opportunityWorkloadActions.getSummary("A").state, "matched");
|
|
app.authSessionState.principal = "another@example.invalid";
|
|
assert.equal(app.opportunityWorkloadActions.getSummary("A").state, "missing");
|
|
});
|
|
|
|
test("Actions completion includes queued calls, shared pending records, failures and cache writes", async () => {
|
|
const pending = [];
|
|
let finishWrite;
|
|
const app = contentFixture(() => new Promise(resolve => pending.push(resolve)));
|
|
app.activityCache.read = async () => null;
|
|
app.activityCache.write = () => new Promise(resolve => { finishWrite = resolve; });
|
|
const products = { items: Array.from({ length: 5 }, (_, i) => ({ RevnId: String(i + 1) })) };
|
|
let complete = false, sharedComplete = false;
|
|
const batch = app.opportunityWorkloadActions.loadProducts(products, true, { cachedAt: 200 }).then(() => { complete = true; });
|
|
const shared = app.opportunityWorkloadActions.loadProducts(products).then(() => { sharedComplete = true; });
|
|
await flush();
|
|
assert.equal(pending.length, 4);
|
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
pending.slice(attempt * 4, attempt * 4 + 4).forEach(resolve => resolve({ ok: false, error: "Synthetic" }));
|
|
await flush();
|
|
}
|
|
assert.equal(pending.length, 21);
|
|
assert.equal(complete, false);
|
|
assert.equal(sharedComplete, false);
|
|
pending[20]({ ok: true, actions: [], hasNext: false });
|
|
await flush();
|
|
assert.equal(complete, false, "cache write still pending");
|
|
finishWrite();
|
|
await batch;
|
|
await shared;
|
|
assert.equal(complete, true);
|
|
assert.equal(sharedComplete, true);
|
|
});
|
|
|
|
test("Actions reset settles discarded queue entries without waiting for unstarted requests", async () => {
|
|
const pending = [];
|
|
const app = contentFixture(() => new Promise(resolve => pending.push(resolve)));
|
|
const batch = app.opportunityWorkloadActions.loadProducts({ items: Array.from({ length: 8 }, (_, i) => ({ RevnId: String(i) })) });
|
|
await flush();
|
|
app.opportunityWorkloadActions.reset(false);
|
|
pending.forEach(resolve => resolve({ ok: true, actions: [], hasNext: false }));
|
|
await batch;
|
|
assert.equal(pending.length, 4);
|
|
});
|
|
|
|
test("actions aggregate all products, share RevnId queries across Reference changes and restart on reopen", async () => {
|
|
const calls = [];
|
|
const app = contentFixture(async message => {
|
|
calls.push(message);
|
|
return { ok: true, hasNext: false, actions: message.revnId === "1" ? [{ id: 1, owner: "test@example.invalid", name: "<script>safe</script>" }] : [] };
|
|
});
|
|
const products = { items: [{ RevnId: "1" }, { RevnId: 2 }, { RevnId: "1" }] };
|
|
app.productsContentState.opptyProductsByNumber.set("A", products);
|
|
app.opportunityWorkloadActions.loadProducts(products);
|
|
assert.equal(app.opportunityWorkloadActions.createBadge("A").textContent, "...");
|
|
await flush();
|
|
assert.equal(calls.length, 2);
|
|
assert.equal(calls[0].authorization.value, "synthetic-wwb");
|
|
const badge = app.opportunityWorkloadActions.createBadge("A");
|
|
assert.equal(badge.textContent, "1");
|
|
assert.match(badge.title, /<script>safe<\/script>/);
|
|
app.opportunityWorkloadActions.loadProducts(products);
|
|
await flush();
|
|
assert.equal(calls.length, 2);
|
|
app.productsContentState.opptyProductsByNumber.set("B", { items: [{ RevnId: 2 }] });
|
|
assert.equal(app.opportunityWorkloadActions.createBadge("B").textContent, "0");
|
|
app.opportunityWorkloadActions.reset(false);
|
|
app.opportunityWorkloadActions.loadProducts(products);
|
|
await flush();
|
|
assert.equal(calls.length, 2);
|
|
app.opportunityWorkloadActions.reopen();
|
|
await flush();
|
|
assert.equal(calls.length, 4);
|
|
assert.equal(app.debugContentView.getRequestCategory({ name: "requestWlActions" }), "actions");
|
|
});
|
|
|
|
test("errors, missing ids and partial empty pages never appear as confirmed absence", async () => {
|
|
for (const result of [{ ok: false, error: "Synthetic" }, { ok: true, actions: [], hasNext: true }]) {
|
|
const app = contentFixture(async () => result);
|
|
const products = { items: [{ RevnId: "1" }] };
|
|
app.productsContentState.opptyProductsByNumber.set("A", products);
|
|
app.opportunityWorkloadActions.loadProducts(products);
|
|
await flush();
|
|
assert.equal(app.opportunityWorkloadActions.createBadge("A").textContent, "?");
|
|
app.productsContentState.opptyProductsByNumber.set("A", { items: [{}] });
|
|
assert.equal(app.opportunityWorkloadActions.createBadge("A").textContent, "?");
|
|
}
|
|
});
|
|
|
|
test("Act requires an owner matching tokenrelay principal while tooltip retains every action", async () => {
|
|
const actions = [
|
|
{ id: 1, name: "Other action", owner: "other@example.invalid", ownerEmail: "test@example.invalid" },
|
|
{ id: 2, name: "My action", owner: "test@example.invalid", ownerEmail: "different@example.invalid" }
|
|
];
|
|
const app = contentFixture(async () => ({ ok: true, actions, hasNext: false }));
|
|
const products = { items: [{ RevnId: "1" }] };
|
|
app.productsContentState.opptyProductsByNumber.set("A", products);
|
|
app.opportunityWorkloadActions.loadProducts(products);
|
|
await flush();
|
|
const badge = () => app.opportunityWorkloadActions.createBadge("A");
|
|
assert.equal(badge().textContent, "2");
|
|
assert.equal(badge().attributes["data-state"], "matched");
|
|
assert.match(badge().title, /Other action/);
|
|
assert.match(badge().title, /My action/);
|
|
app.authSessionState.principal = "different@example.invalid";
|
|
assert.equal(badge().textContent, "2");
|
|
assert.equal(badge().attributes["data-state"], "missing", "ownerEmail must not determine ownership");
|
|
assert.match(badge().title, /Other action/);
|
|
assert.match(badge().title, /My action/);
|
|
app.authSessionState.principal = "";
|
|
assert.equal(badge().textContent, "?");
|
|
});
|
|
|
|
test("general tokenrelay retains principal without changing access-token return or accepting sales-common identity", async () => {
|
|
let payload = { access_token: "synthetic-token", principal: "test@example.invalid" };
|
|
const app = loadApplication("content", { fetch: async () => response(payload) });
|
|
app.authXsrfContent.getXsrfToken = async () => ({ token: "synthetic-xsrf" });
|
|
app.debugContentView.renderDebugPanel = () => {};
|
|
assert.equal(await app.authTokenRelay.requestTokenRelay(), "synthetic-token");
|
|
assert.equal(app.authSessionState.principal, "test@example.invalid");
|
|
payload = { access_token: "scoped-token", principal: "other@example.invalid", expires_in: 3600 };
|
|
await app.authTokenRelay.requestTokenRelay({ url: `${app.contracts.TOKEN_RELAY_URL}?scopesfq=synthetic`, includeExpiry: true });
|
|
assert.equal(app.authSessionState.principal, "test@example.invalid");
|
|
payload = { access_token: "synthetic-token" };
|
|
await app.authTokenRelay.requestTokenRelay();
|
|
assert.equal(app.authSessionState.principal, "");
|
|
});
|
|
|
|
test("actions queue caps concurrency and discards results/queued jobs from closed modals", async () => {
|
|
const pending = [];
|
|
const app = contentFixture(() => new Promise(resolve => pending.push(resolve)));
|
|
const products = { items: Array.from({ length: 8 }, (_, i) => ({ RevnId: String(i + 1) })) };
|
|
app.productsContentState.opptyProductsByNumber.set("A", products);
|
|
app.opportunityWorkloadActions.loadProducts(products);
|
|
await flush();
|
|
assert.equal(pending.length, 4);
|
|
app.opportunityWorkloadActions.reset(false);
|
|
pending.forEach(resolve => resolve({ ok: true, hasNext: false, actions: [{ id: 1 }] }));
|
|
await flush();
|
|
assert.equal(pending.length, 4);
|
|
assert.equal(app.opportunityWorkloadActions.createBadge("A"), null);
|
|
});
|
|
|
|
test("product progress triggers Actions only for fresh products and ignores stale product jobs", async () => {
|
|
let listener;
|
|
const calls = [];
|
|
const app = loadApplication("content", {
|
|
document: { getElementById: () => null },
|
|
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);
|
|
return { ok: true, actions: [], hasNext: false };
|
|
};
|
|
app.productsContentState.currentProductsRequestId = "current";
|
|
app.productActivities.begin("current", "synthetic");
|
|
app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => [];
|
|
app.productsContentJobs.ensureProductsBackgroundMessageHandler();
|
|
const message = { type: app.messageTypes?.opptyProductsProgress || "opportunitiesExtension.opptyProductsProgress",
|
|
requestId: "current", optyNumber: "A", items: [{ RevnId: "123" }], fromCache: false };
|
|
listener({ ...message, requestId: "old" });
|
|
await flush();
|
|
assert.equal(calls.length, 0);
|
|
listener(message);
|
|
await flush();
|
|
assert.equal(calls.length, 0);
|
|
listener({ ...message, optyNumber: undefined, state: "complete" });
|
|
await flush();
|
|
assert.equal(calls.length, 1);
|
|
assert.equal(calls[0].type, "opportunitiesExtension.requestWlActions");
|
|
assert.equal(calls[0].revnId, "123");
|
|
});
|