Implemented the Create Internal Service Request functionality

This commit is contained in:
2026-10-09 18:41:39 -03:00
parent c9de750261
commit 8b4d88b226
34 changed files with 1690 additions and 480 deletions

View File

@@ -93,7 +93,7 @@ test("opportunity query preserves fields, record-set OR, date AND, and paginatio
{ op: "$or", criteria: types.map(value => ({ op: "$eq", attribute: "RecordSet", value })) },
{ op: "$wi", attribute: "EffectiveDate", value1: range.startCloseDate, value2: range.endCloseDate, dynamicDate: false }
] }, keywords: null, keywordsFields: fields,
fields: [...fields, "CustomerAccount.PartyUniqueName", "CustomerAccount.PartyId", "CustomerAccount.PartyNumber", "CustomerAccount.Region_c"],
fields: [...fields, "CustomerAccount.PartyUniqueName", "CustomerAccount.PartyId", "CustomerAccount.PartyNumber", "CustomerAccount.Region_c", "CustomerAccount.PrimaryAddress.CountryCode"],
sort: [], language: "en", skipInValidFields: true, skipHiddenFromUIFields: true,
copiedFrom: "queries/d5dbcd01-9ff3-40be-9b5b-64744cbf7162", limit: 15, offset: 15
});
@@ -183,6 +183,8 @@ test("tokenrelay missing-cookie recovery and HTTP failure do not change the requ
assert.equal(h.refreshes.length, 1);
assert.equal(calls.length, 1);
assert.equal(calls[0].options.headers["x-xsrf-token"], "synthetic-xsrf");
assert.equal(h.requestLog.at(-1).requestHeaders["x-xsrf-token"], "synthetic-xsrf");
assert.equal(JSON.parse(h.requestLog.at(-1).responseBody).access_token, "synthetic-access");
assert.equal(calls[0].options.method, "GET");
for (const status of [400, 401, 403]) {
let attempts = 0;

View File

@@ -28,6 +28,14 @@ test("SR badge shows reported hours, sums linked SRs and preserves missing value
}
});
test("SR tooltip formats customer deadlines without timezone day shifts", async () => {
for (const [value, expected] of [["2026-05-14T00:00:00.000+0000", "14/05/2026"], [null, "N/D"], [undefined, "N/D"], ["", "N/D"]]) {
const app = setup(async () => response({ items: [{ ...sr("A"), CustomerDeadline_c: value }], hasMore: false }));
await app.opportunityCrmServiceRequests.load("synthetic", 1);
assert.ok(app.opportunityCrmServiceRequests.createBadge("A").attributes["aria-label"].includes(`Customer Deadline: ${expected}`));
}
});
test("CRM fetch is called without the environment object as its receiver", async () => {
let calls = 0;
const app = setup(async function (url, options) {
@@ -76,7 +84,7 @@ test("CRM query preserves payload and pagination after opportunities, uses token
{ op: "$in", attribute: "StatusCd", values: ["SVC_PENDING_DELIVERY"] },
{ op: "$eq", attribute: "RecordSet", value: "ORA_SVC_MY_SR_TEAM_SRS" }
] }, keywords: null,
fields: ["SrNumber", "OpportunityName_Id_c.OptyNumber", "ReportedHours_c", "_actions", "Title", "Owner.PersonFirstName",
fields: ["SrNumber", "OpportunityName_Id_c.OptyNumber", "ReportedHours_c", "CustomerDeadline_c", "PrimaryPillarNew_c", "_actions", "Title", "Owner.PersonFirstName",
"Owner.PersonLastName", "Owner.PrimaryEmail.EmailAddress", "DoNotCallFlag", "DoNotEmailFlag"],
sort: [{ attribute: "_score", direction: "descending" }], language: "en",
keywordsFields: ["SrNumber", "ServiceNew_Id_c.RecordName", "CustomerAccount", "OpportunityName_Id_c", "OpportunityName_Id_c.OptyNumber", "Owner"],
@@ -90,7 +98,11 @@ test("CRM query preserves payload and pagination after opportunities, uses token
assert.equal(log.status, "success");
assert.ok(log.responseBody);
assert.ok(log.durationMs >= 0);
assert.doesNotMatch(log.requestHeaders.Authorization, /synthetic-token/);
assert.equal(log.requestHeaders.Authorization, "Bearer synthetic-token");
assert.doesNotMatch(app.debugContentLog.formatHeaderBlock(log.requestHeaders), /synthetic-token/);
app.debugState.debugSecretsVisible = true;
assert.match(app.debugContentLog.formatHeaderBlock(log.requestHeaders), /Bearer synthetic-token/);
app.debugState.debugSecretsVisible = false;
assert.equal(app.debugContentView.getRequestCategory(log), "serviceRequests");
}
const found = app.opportunityCrmServiceRequests.createBadge("TEST-1");

View File

@@ -3,6 +3,33 @@ const { test } = require("node:test");
const { loadBackground, response } = require("./support/characterization.cjs");
const type = "opportunitiesExtension.requestInternalServiceRequest";
test("internal SR form lookups use management proxy, Session and parent SrId", async () => {
const calls = [];
const h = loadBackground({ fetch: async (url, options) => {
calls.push({ url: new URL(url), options });
return response({ items: String(url).includes("/lookups?")
? [{ code: "OCI", name: "Oracle Cloud" }]
: [{ Id: 123, RecordName: "Review", Description_c: "Review the architecture" }] });
} });
const platform = await h.dispatch({ type: "opportunitiesExtension.requestLookupTypePlatform" });
assert.equal(platform.ok, true);
assert.equal(platform.items[0].code, "OCI");
const service = await h.dispatch({ type: "opportunitiesExtension.requestInternalSrServices", parentServiceId: "456", emailAddress: "user+test@example.invalid" });
assert.equal(service.ok, true);
assert.equal(service.items[0].Id, "123");
assert.equal(service.items[0].Description_c, "Review the architecture");
assert.equal(calls[1].url.searchParams.get("parentServiceId"), "456");
assert.equal(calls[1].url.searchParams.get("emailAddress"), "user+test@example.invalid");
assert.equal(calls[1].url.searchParams.get("serviceType"), "INTERNAL");
for (const call of calls) {
assert.match(call.url.pathname, /SeaaS_SR_management.*oalsetSeaaSOKECustomRestAPI/);
assert.equal(call.options.headers.Authorization, "Session");
assert.equal(call.options.credentials, "include");
}
assert.equal((await h.dispatch({ type: "opportunitiesExtension.requestInternalSrServices", parentServiceId: "bad", emailAddress: "user@example.invalid" })).ok, false);
assert.equal(calls.length, 2);
});
test("preparation reuses an existing unexpired cookie pair without navigation or API calls", async () => {
const domain = "comcipapic-oalprod.integration.ocp.oraclecloud.com";
const h = loadBackground({

View File

@@ -3,6 +3,87 @@ const { test } = require("node:test");
const { loadApplication } = require("./support/application.cjs");
const flush = () => new Promise(resolve => setImmediate(resolve));
test("internal SR form validates Save, maps parent data, prevents duplicate posts and preserves failures", async () => {
const h = setup();
h.app.currentUser.getUserId = () => "current@example.invalid";
h.app.opportunitiesTableState.opportunitiesTableState.items = [
{ OptyNumber: "OTHER", Name: "Other opportunity", CustomerAccount: { PrimaryAddress: { CountryCode: "US" } } },
{ OptyNumber: "OP1", Name: "<Opportunity name>", OptyId: 456, CountryCode: "CA",
CustomerAccount: { PartyId: 789, Region_c_localizedValue: "LAD", CountryCode: "AR", PrimaryAddress: { CountryCode: "BR" } } }
];
h.app.debugContentView.renderDebugPanel = () => {};
h.app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => [{ SrId: "123", SrNumber: "SR123",
CustomerDeadline_c: "2026-05-14T00:00:00.000+0000", PrimaryPillarNew_c: "Tech_Cloud_Infrastructure", Title: "<Parent SR title>", OpportunityName_Id_c: { OptyNumber: "OP1" } }];
const calls = [];
h.app.platformContentMessages.sendRuntimeMessage = async message => {
calls.push(message);
return { ok: true, items: message.type.endsWith("requestLookupTypePlatform") ? [{ code: "OCI", name: "Oracle Cloud" }]
: message.type.endsWith("requestInternalSrServices") ? [{ Id: "45", RecordName: "Review", Description_c: "Architecture review" }] : [] };
};
await h.app.opportunityCrmServiceRequests.load("synthetic", 1);
const badge = h.app.opportunityCrmServiceRequests.createBadge("OP1");
h.modal.append(badge);
badge.listeners.pointerenter();
await flush();
const add = h.walk(h.panel()).find(el => el.title === "Create Internal Service Request");
assert.ok(add);
assert.equal(add.parent.children[2].title, "Copy SR number");
add.listeners.click({ preventDefault() {}, stopPropagation() {} });
await flush();
const nodes = h.walk(h.modal);
const context = nodes.find(el => el.className === "opportunities-extension-action-form-context");
assert.deepEqual(context.children.map(el => el.textContent), ["SR Number", "SR123", "Opportunity", "<Opportunity name>", "Title", "<Parent SR title>"]);
assert.equal(nodes.find(el => el.name === "customerDeadline").value, "14/05/2026");
assert.equal(nodes.find(el => el.name === "customerDeadline").readOnly, true);
assert.equal(nodes.find(el => el.name === "customerDeadline").required, false);
for (const name of ["title", "internalDeadline", "platform", "description", "services"]) {
const input = nodes.find(el => el.name === name);
assert.equal(input.required, true, name);
assert.equal(input.attributes["aria-required"], "true", name);
assert.match(input.parent.children[0].textContent, /\(required\)$/);
}
for (const name of ["platform", "services"]) {
const input = nodes.find(el => el.id === `internal-sr-${name}-search`);
assert.equal(input.required, true);
assert.equal(input.attributes["aria-required"], "true");
}
assert.equal(nodes.find(el => el.name === "internalDeadline").type, "date");
assert.equal(nodes.find(el => el.name === "platform").children[1].value, "OCI");
assert.equal(nodes.find(el => el.name === "services").children[1].attributes["data-description"], "Architecture review");
assert.equal(calls.at(-1).parentServiceId, "123");
assert.equal(calls.at(-1).emailAddress, "current@example.invalid");
const form = nodes.find(el => el.tag === "form");
assert.deepEqual(h.walk(form).filter(el => el.tag === "button").map(el => el.textContent), ["Save", "Cancel"]);
const submit = () => form.listeners.submit({ preventDefault() {} });
const before = calls.length;
await submit();
assert.equal(calls.length, before);
assert.equal(nodes.find(el => el.name === "title").attributes["aria-invalid"], "true");
for (const [name, value] of Object.entries({ title: "New internal SR", internalDeadline: "2026-05-01", platform: "OCI", description: "Detailed request", services: "45" })) nodes.find(el => el.name === name).value = value;
const posts = [], toasts = [];
h.app.opportunitiesLinks.showCopyConfirmation = (message, isError = false) => toasts.push({ message, isError });
h.app.platformContentMessages.sendRuntimeMessage = message => new Promise(resolve => posts.push({ message, resolve }));
const saving = submit();
await submit();
assert.equal(posts.length, 1);
assert.equal(posts[0].message.type, "opportunitiesExtension.requestPostInternalServiceRequest");
assert.equal(posts[0].message.emailAddress, "current@example.invalid");
assert.deepEqual(JSON.parse(JSON.stringify(posts[0].message.payload)), {
Title: "New internal SR", ServiceNew_Id_c: "45",
ProblemDescription: "Detailed request", CustomerDeadline_c: "2026-05-14", TechPlatformLookUp_c: "OCI",
ServiceRequest_Id_SRToInternalSR: "123", SvcQuestionsCollection_c: [], InternalDeadline_c: "2026-05-01"
});
posts[0].resolve({ ok: false, httpStatus: 403, error: "Synthetic save failure" });
await saving;
assert.equal(nodes.find(el => el.name === "title").value, "New internal SR");
assert.deepEqual(toasts, [{ message: "Synthetic save failure", isError: true }]);
const retry = submit();
posts[1].resolve({ ok: true, result: { SrNumber: "ISR1" } });
await retry;
assert.deepEqual(toasts.at(-1), { message: "Internal Service Request created successfully.", isError: false });
assert.ok(!h.walk(h.modal).includes(form));
});
test("internal SR uses Session without TokenRelay and reports unavailable iframe host", async () => {
const h = setup();
let renewals = 0;
@@ -146,7 +227,8 @@ test("SR tooltip loads nested details with copy/link and empty text, discarding
assert.equal(calls[0].message.token, undefined);
assert.equal(calls[0].message.srId, "NOT-THE-URL-ID");
assert.ok(h.walk(h.panel()).some(node => node.textContent === "Loading SR details..."));
calls[0].resolve({ ok: true, items: [{ SrId: 99, SrNumber: "SR &child", Title: "<child>", ServiceNew_c: "Queue" }] });
calls[0].resolve({ ok: true, items: [{ SrId: 99, SrNumber: "SR &child", Title: "<child>", ServiceNew_c: "Queue",
LastUpdateDate: "2026-05-14T00:00:00.000+0000", OpenDate: "2026-04-01T23:30:00.000+0000" }] });
calls[1].resolve({ ok: true, items: [] });
await flush();
const nodes = h.walk(h.panel());
@@ -155,6 +237,8 @@ test("SR tooltip loads nested details with copy/link and empty text, discarding
assert.ok(link.href.endsWith("srNumber=SR%20%26child&srId=99"));
assert.equal(link.parent.children[2].title, "Copy SR number");
assert.ok(nodes.some(node => node.textContent.includes("Service Queue: Queue")));
assert.ok(nodes.some(node => node.textContent.includes("LastUpdateDate: 14/05/2026")));
assert.ok(nodes.some(node => node.textContent.includes("OpenDate: 01/04/2026")));
assert.ok(nodes.some(node => node.textContent === "No further details."));
h.app.crmSrPopover?.close();
h.app.opportunityCrmServiceRequests.reset();

View File

@@ -5,7 +5,7 @@ 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 () => {
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 });
@@ -20,12 +20,16 @@ for (const family of ["chrome", "firefox"]) {
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(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);
});
}
test("identity failures and malformed roles remain explicit and never expose response tokens", async () => {
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: {} }),
@@ -37,7 +41,8 @@ test("identity failures and malformed roles remain explicit and never expose res
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/);
const content = loadApplication("content");
assert.doesNotMatch(content.debugContentLog.formatDebugBody(result.debugRequests[0].responseBody, "requestCurrentUser"), /synthetic-secret/);
}
});

View File

@@ -20,6 +20,26 @@ function fixture() {
return { ...dom, app, add, nodes, tab, ids, select };
}
test("catalog and identity secrets are revealed exactly by the eye button and hidden again", () => {
const h = fixture();
const body = JSON.stringify({ result: "catalog-exact-secret", detail: "Preserved" });
h.app.debugContentLog.appendBackgroundRequestLog([{
name: "requestSrTokenCatalog", method: "POST", startedAt: new Date().toISOString(), status: "success",
responseBody: body, requestHeaders: { "vb-proxy-header-x-oracle-ectoken": "header-exact-secret", "x-appbuilder-client-id": "client-exact-secret" }
}, {
name: "requestCurrentUser", method: "GET", startedAt: new Date().toISOString(), status: "success",
responseBody: JSON.stringify({ user: { roles: ["SIMPLE_USER"] }, token: "identity-exact-secret" })
}]);
const text = node => node.text + node.children.map(text).join("");
const toggle = () => h.nodes().find(node => node.getAttribute("data-debug-focus") === "visibility").listeners.click();
assert.doesNotMatch(text(h.panel), /exact-secret/);
toggle();
for (const secret of ["catalog-exact-secret", "header-exact-secret", "client-exact-secret", "identity-exact-secret"]) assert.ok(text(h.panel).includes(secret));
toggle();
assert.doesNotMatch(text(h.panel), /exact-secret/);
assert.equal(h.app.debugState.requestLog[0].responseBody, body, "display masking never overwrites originals");
});
test("request summaries show duration after status, including zero, failure and pending", () => {
const h = fixture();
h.add("duration", "requestOpportunities", 1);
@@ -33,23 +53,23 @@ test("request summaries show duration after status, including zero, failure and
}
});
test("large response previews stay valid, formatted, masked and colored in both runtimes", () => {
test("complete large responses stay formatted, masked and colored in both runtimes", () => {
const h = fixture();
const background = loadApplication("background");
const body = JSON.stringify({ items: Array.from({ length: 2000 }, (_, index) => ({
id: index, name: "Synthetic opportunity", access_token: "synthetic-secret",
uiaasHeader: { value: "synthetic-header-secret" }, owner: { name: "Synthetic owner" }
})) });
for (const [logger, limit] of [[h.app.debugContentLog, 12000], [background.httpBackgroundJson, 100000]]) {
for (const logger of [h.app.debugContentLog, background.httpBackgroundJson]) {
const preview = logger.createDebugBody(body);
assert.equal(preview.truncated, true);
assert.ok(preview.value.length <= limit);
assert.equal(preview.truncated, false);
assert.equal(preview.value, body);
const parsed = JSON.parse(preview.value);
assert.match(parsed.debugNotice, /Partial response/);
assert.ok(parsed.preview.items.length > 0);
assert.ok(parsed.preview.items.length < 2000);
assert.equal(parsed.items.length, 2000);
assert.equal(parsed.items.at(-1).id, 1999);
const formatted = h.app.debugContentLog.formatDebugBody(preview.value);
assert.match(formatted, /\n "debugNotice"/);
assert.match(formatted, /\n "items"/);
assert.match(formatted, /"id": 1999/);
assert.doesNotMatch(formatted, /synthetic-secret|synthetic-header-secret/);
const container = h.document.createElement("div");
h.app.debugContentView.appendDebugBlock(container, "Response Body", formatted);
@@ -58,7 +78,9 @@ test("large response previews stay valid, formatted, masked and colored in both
assert.match(h.app.debugContentLog.formatDebugBody(preview.value), /synthetic-secret/);
h.app.debugState.debugSecretsVisible = false;
assert.equal(logger.createDebugBody('{"ok":true}').value, '{"ok":true}');
assert.match(logger.createDebugBody("<html>" + "x".repeat(limit + 1000)).value, /truncated/);
const html = "<html>" + "x".repeat(150000) + "</html>";
assert.equal(logger.createDebugBody(html).value, html);
assert.equal(logger.createDebugBody("").value, "(empty response body)");
}
});
@@ -69,7 +91,7 @@ test("request categories cover every current family and unknown requests stay vi
requestConsumerTokenService: "authentication", requestCustomerTokenService: "authentication",
requestOpportunities: "opportunities", requestOpptyProducts: "products", requestGenerateAiSummary: "ai",
requestAccountSummary: "consumption", requestAccountPlans: "consumption",
requestInstallBase: "installBase", requestCustomerServiceRequest: "serviceRequests",
requestInstallBase: "installBase", requestCustomerServiceRequest: "serviceRequests", requestSRInfo: "serviceRequests",
requestConsumptionPlanDetail: "consumption",
requestRatecardByPlan: "consumption",
requestConsumptionServiceQuarterly: "consumption", requestConsumptionServiceMonthly: "consumption",

View File

@@ -1,10 +1,12 @@
(() => {
const listeners = [];
const storage = new Map();
const internalSrPreview = new URLSearchParams(location.search).has("internal-sr");
const opportunities = Array.from({ length: 25 }, (_, index) => ({
OptyNumber: `TEST${index + 1}`,
OptyId: 1000 + index,
Name: `Synthetic opportunity ${index + 1} - Database migration`,
CustomerAccount: { PartyUniqueName: "Example public sector customer (Example City, BR)", PartyId: "test-party", PartyNumber: "test-account" },
CustomerAccount: { PartyUniqueName: "Example public sector customer (Example City, BR)", PartyId: internalSrPreview ? 789 : "test-party", PartyNumber: "test-account", Region_c_localizedValue: "LAD", PrimaryAddress: { CountryCode: "BR" } },
Owner: { PersonFirstName: "Example", PersonLastName: "Owner" },
PrimaryRevenue: { RevnAmountCurcyCode: "USD", RevnAmount: "25000", WinProb: 60 },
EffectiveDate: index === 24 ? null : `2026-11-${String(index < 8 ? 15 : 1 + index).padStart(2, "0")}T00:00:00Z`,
@@ -28,7 +30,11 @@
onMessage: { addListener: listener => listeners.push(listener) },
async sendMessage(message) {
const operation = message.type.split(".").at(-1);
if (operation === "requestCurrentUser") return { ok: true, user: { id: "example@example.invalid", name: "Example Owner", roles: ["SALES_REP"] }, debugRequests: [] };
if (operation === "requestPostInternalServiceRequest") return { ok: true, result: { SrNumber: "ISR123" }, debugRequests: [] };
if (operation === "requestCurrentUser") return { ok: true, user: { id: "example@example.invalid", name: "Example Owner", roles: internalSrPreview ? [] : ["SALES_REP"] }, debugRequests: [] };
if (["prepareSrSession", "requestInternalServiceRequest", "requestWlActions"].includes(operation)) return { ok: true, items: [], actions: [], debugRequests: [] };
if (operation === "requestLookupTypePlatform") return { ok: true, items: [{ code: "OCI", name: "Oracle Cloud Infrastructure" }, { code: "MULTI", name: "Multicloud" }], debugRequests: [] };
if (operation === "requestInternalSrServices") return { ok: true, items: [{ Id: "123", RecordName: "Architecture review", Description_c: "Review the customer architecture, workloads and technical requirements." }, { Id: "124", RecordName: "Technical workshop", Description_c: "Prepare and deliver a technical session for the customer team." }], debugRequests: [] };
if (operation === "getXsrfToken") return { ok: true, token: "synthetic-xsrf", cookieName: "XSRF-TOKEN-TEST" };
if (operation === "requestConsumerTokenService") return { ok: true, authorizations, debugRequests: [] };
if (operation === "requestOpptyProducts") {
@@ -65,7 +71,8 @@
let body;
if (url.pathname === "/fscmRestApi/tokenrelay") body = { access_token: "synthetic-fusion-token", expires_in: 3600 };
else if (url.pathname.endsWith("custom-actions/queries")) {
const { offset, limit } = JSON.parse(options.body);
const { offset, limit, entity } = JSON.parse(options.body);
if (entity === "CRMServiceRequest") return new Response(JSON.stringify({ items: [{ SrId: "123456", SrNumber: "SR123456", Title: "Example parent SR", PrimaryPillarNew_c: "Tech_Cloud_Infrastructure", CustomerDeadline_c: "2026-11-30T00:00:00.000+0000", ReportedHours_c: 4, OpportunityName_Id_c: { OptyNumber: "TEST1" } }], hasMore: false }), { status: 200 });
body = { items: opportunities.slice(offset, offset + limit), totalResults: opportunities.length, count: Math.min(limit, opportunities.length - offset), hasMore: offset + limit < opportunities.length, offset, limit };
} else if (url.pathname.endsWith("summarizations/generate")) body = { summaryText: "Synthetic AI summary.\nText is displayed safely: <script>never executed</script>." };
else throw new Error(`Unexpected synthetic fetch: ${url.pathname}`);

View File

@@ -1,8 +1,141 @@
const assert = require("node:assert/strict");
const { test } = require("node:test");
const { loadBackground, response } = require("./support/characterization.cjs");
const { loadApplication } = require("./support/application.cjs");
function assertDebugVisibility(result, secret) {
const app = loadApplication("content");
const entries = result.debugRequests || [];
const render = () => entries.map(entry => app.debugContentLog.formatHeaderBlock(entry.requestHeaders)
+ app.debugContentLog.formatDebugBody(entry.responseBody, entry.name)).join("\n");
assert.ok(!render().includes(secret));
app.debugState.debugSecretsVisible = true;
assert.equal(render().includes(secret), JSON.stringify(entries).includes(secret));
}
const type = "opportunitiesExtension.requestPostServiceRequest";
const emailAddress = "signed.in+sr@example.invalid";
const internalType = "opportunitiesExtension.requestPostInternalServiceRequest";
const internalInput = () => ({ Title: "Internal request title", ServiceNew_Id_c: "404", ProblemDescription: "Form description",
CustomerDeadline_c: "2026-11-29", TechPlatformLookUp_c: "DATABASE", InternalDeadline_c: "2027-01-31",
ServiceRequest_Id_SRToInternalSR: "505" });
const parentInfo = () => ({ SrId: 505, SrNumber: "SR505", AccountPartyId: 100000693906507, OpportunityName_c: "A8YPN8",
OpportunityName_Id_c: 302959906052470, RegionText_c: "LAD", SvcHealth_c: "SVC_HEALTH_YELLOW", Language_c: "PORTUGUESE",
CountryText_c: "BR", PrimaryPillarNew_c: "Tech_Cloud_Platform", SecondaryPillarNew_c: "Tech_Cloud_Infrastructure", ServiceNew_Id_c: 300010753841648 });
for (const family of ["chrome", "firefox"]) {
test(`${family}: internal SR reads parent SRInfo, resolves user and posts form values with Session and observed client ID`, async () => {
const calls = [];
const h = loadBackground({ family, fetch: async (url, options) => {
calls.push({ url, options });
if (options.method === "GET" && url.includes("/serviceRequests?")) return withClientId({ items: [parentInfo()] }, 200, "internal-client");
if (url.includes("/resourceUsers?")) return response({ resourcePartyId: 303 });
assert.ok(!url.includes("Catalogue_c"), "Internal SR must not use catalog signing");
return response({ SrNumber: "ISR1" }, 201);
} });
const input = { ...internalInput(), AccountPartyId: 1, OpportunityName_c: "STALE", OpportunityName_Id_c: 2, RegionText_c: "APAC",
SvcHealth_c: "SVC_HEALTH_GREEN", Language_c: "ENGLISH", CountryText_c: "US", PrimaryPillarNew_c: "STALE", SecondaryPillarNew_c: null, ReportedByPartyId: 999 };
const result = await h.dispatch({ type: internalType, emailAddress, payload: input });
assert.equal(result.ok, true);
assert.equal(calls.length, 3);
const info = new URL(calls[0].url);
assert.match(info.pathname, /SeaaS_SR_management.*\/oalsetCRMRestAPI\/.*resources\/latest\/serviceRequests$/);
assert.equal(info.searchParams.get("q"), "SrId=505");
assert.equal(info.searchParams.get("onlyData"), "true");
assert.equal(info.searchParams.get("orderBy"), "LastUpdateDate:desc");
assert.equal(info.searchParams.get("fields"), "PrimaryPillarNew_c,SecondaryPillarNew_c,SvcHealth_c,SrNumber,AccountPartyId,CountryText_c,Language_c,OpportunityName_Id_c,OpportunityName_c,RegionText_c,ServiceNew_Id_c");
const post = calls[2];
assert.equal(new URL(calls[1].url).searchParams.get("emailAddress"), emailAddress);
assert.match(post.url, /SeaaS_SR_management.*oalsetCRMRestAPICloudAccount.*resources\/latest\/serviceRequests$/);
assert.equal(post.options.method, "POST");
const { SrId, SrNumber, ...parent } = parentInfo();
assert.deepEqual(JSON.parse(post.options.body), { ...parent, ...internalInput(),
ServiceRequest_Id_SRToInternalSR: "505", ServiceNew_Id_c: 404,
InternalDeadline_c: "2027-01-31T00:00:00.000Z", ReportedByPartyId: 303, SvcQuestionsCollection_c: [] });
assert.ok(calls.every(call => call.options.headers.Authorization === "Session" && call.options.credentials === "include"));
const origin = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com";
assert.deepEqual({ ...post.options.headers }, { Accept: "application/json", "Content-Type": "application/json",
Authorization: "Session", Origin: origin, "x-appbuilder-client-id": "internal-client" });
assertDebugVisibility(result, "internal-client");
assert.equal(calls[0].options.headers.Origin, undefined);
assert.equal(result.debugRequests[2].requestHeaders.Origin, origin);
assert.equal(h.headerRules.length, 1);
const { removeRuleIds, addRules } = h.headerRules[0];
assert.deepEqual(removeRuleIds, [7203]);
assert.equal(addRules.length, 1);
const rule = addRules[0];
assert.deepEqual(rule.action, { type: "modifyHeaders", requestHeaders: [{ header: "Origin", operation: "set", value: origin }] });
assert.deepEqual(rule.condition.initiatorDomains, ["synthetic-extension"]);
assert.deepEqual(rule.condition.requestMethods, ["post"]);
assert.deepEqual(rule.condition.resourceTypes, ["xmlhttprequest"]);
const pattern = new RegExp(rule.condition.regexFilter);
assert.ok(pattern.test(post.url));
assert.ok(!pattern.test(calls[0].url));
assert.ok(!pattern.test(post.url + "/other"));
assert.ok(!pattern.test(post.url + "?other=1"));
assert.deepEqual(result.debugRequests.map(entry => entry.name), ["requestSRInfo", "requestResourceUser", "requestPostInternalServiceRequest"]);
});
}
test("internal SR queries the current parent on every Save and reuses captured client ID after worker restart", async () => {
const sessionStorage = {};
const queries = [], posts = [];
const fetch = async (url, options) => {
if (url.includes("/serviceRequests?")) {
const id = Number(new URL(url).searchParams.get("q").slice("SrId=".length));
queries.push(id);
const parent = { ...parentInfo(), SrId: id, SecondaryPillarNew_c: null, ServiceNew_Id_c: id + 1000 };
if (id === 606) delete parent.ServiceNew_Id_c;
return id === 505 ? withClientId({ items: [parent] }, 200, "saved-internal-client") : response({ items: [parent] });
}
if (url.includes("/resourceUsers?")) return response({ resourcePartyId: 303 });
posts.push(options);
return response({ SrNumber: "ISR1" }, 201);
};
const h = loadBackground({ fetch, sessionStorage });
assert.equal((await h.dispatch({ type: internalType, emailAddress, payload: internalInput() })).ok, true);
const restarted = loadBackground({ fetch, sessionStorage });
assert.equal((await restarted.dispatch({ type: internalType, emailAddress,
payload: { ...internalInput(), ServiceRequest_Id_SRToInternalSR: "606" } })).ok, true);
assert.deepEqual(queries, [505, 606]);
assert.equal(posts[1].headers["x-appbuilder-client-id"], "saved-internal-client");
assert.equal(JSON.parse(posts[1].body).ServiceRequest_Id_SRToInternalSR, "606", "links to parent SrId, independently of the parent's service ID");
assert.equal(JSON.parse(posts[1].body).SecondaryPillarNew_c, null);
});
test("internal SR save validates form fields and parent ID before requesting SRInfo", async () => {
let calls = 0;
const h = loadBackground({ fetch: async () => { calls++; return response({}, 403); } });
for (const change of [{ Title: "" }, { ProblemDescription: "" }, { TechPlatformLookUp_c: "" }, { InternalDeadline_c: "2026-02-30" },
{ CustomerDeadline_c: "2026-02-30" }, { ServiceNew_Id_c: "bad" }, { ServiceRequest_Id_SRToInternalSR: "bad" }]) {
assert.equal((await h.dispatch({ type: internalType, emailAddress, payload: { ...internalInput(), ...change } })).ok, false);
}
assert.equal(calls, 0);
assert.equal((await h.dispatch({ type: internalType, emailAddress, payload: internalInput() })).ok, false);
assert.equal(calls, 1);
});
test("internal SR stops after missing or rejected parent data, failed user lookup or POST and never retries creation", async () => {
for (const mode of ["info-401", "info-500", "empty", "multiple", "wrong-parent", "missing-country", "user", "post-403", "post-500", "network"]) {
const calls = [];
const h = loadBackground({ fetch: async (url, options) => {
calls.push({ url, options });
if (url.includes("/serviceRequests?")) {
const parent = parentInfo();
if (mode === "wrong-parent") parent.SrId = 999;
if (mode === "missing-country") delete parent.CountryText_c;
return response({ items: mode === "empty" ? [] : mode === "multiple" ? [parent, parent] : [parent] }, mode === "info-401" ? 401 : mode === "info-500" ? 500 : 200);
}
if (url.includes("/resourceUsers?")) return response({ resourcePartyId: 303 }, mode === "user" ? 403 : 200);
if (mode === "network") throw new Error("Synthetic network failure");
return response({}, mode === "post-403" ? 403 : 500);
} });
const result = await h.dispatch({ type: internalType, emailAddress, payload: internalInput() });
assert.equal(result.ok, false, mode);
const hasPost = ["post-403", "post-500", "network"].includes(mode);
assert.equal(calls.length, hasPost ? 3 : mode === "user" ? 2 : 1, mode);
assert.equal(calls.filter(call => call.options.method === "POST").length, hasPost ? 1 : 0);
assert.equal(result.debugRequests.at(-1).status, ["empty", "multiple", "wrong-parent", "missing-country"].includes(mode) ? "success" : "failed");
}
});
const payload = () => ({ AccountPartyId: 100000000000101, OpportunityName_Id_c: 202,
ServiceNew_Id_c: 404, Title: "Service-OP1-Customer", RegionText_c: "LAD", CountryText_c: "BR",
ProblemDescription: "Test description", Language_c: "ENGLISH", CustomerDeadline_c: "2027-01-31",
@@ -80,7 +213,7 @@ test("token 403 retries once only with a newly returned client ID; SR POST is ne
if (["recovered", "save-failure"].includes(mode)) {
assert.deepEqual({ ...calls.at(-1).options.headers }, { ...tokenCalls.at(-1).options.headers, "vb-proxy-header-x-oracle-ectoken": "retry-token" });
}
assert.ok(!JSON.stringify(result).includes("retry-token"));
assertDebugVisibility(result, "retry-token");
}
});
@@ -137,7 +270,7 @@ for (const family of ["chrome", "firefox"]) {
assert.deepEqual(JSON.parse(calls[3].options.body), { ...payload(), ReportedByPartyId: 303, SvcQuestionsCollection_c: [],
SourceCd: "SVC_OPPORTUNITY_SCREEN", SeverityCd: "ORA_SVC_SEV2", ChannelTypeCd: "ORA_SVC_WEB" });
assert.equal(calls[3].options.body, JSON.stringify(prepared.payload));
assert.ok(!JSON.stringify(result).includes("synthetic-secret-token"));
assertDebugVisibility(result, "synthetic-secret-token");
assert.equal(h.tabs.length, 0);
});
}
@@ -209,6 +342,20 @@ test("Origin rule failure blocks token acquisition and SR creation", async () =>
assert.match(result.error, /Rule permission denied/);
});
test("Origin rule failure prevents the Internal SR POST", async () => {
const calls = [];
const h = loadBackground({ ruleError: true, fetch: async url => {
calls.push(url);
if (url.includes("/serviceRequests?")) return response({ items: [parentInfo()] });
assert.match(url, /\/resourceUsers\?/);
return response({ resourcePartyId: 303 });
} });
const result = await h.dispatch({ type: internalType, emailAddress, payload: internalInput() });
assert.equal(result.ok, false);
assert.match(result.error, /Rule permission denied/);
assert.equal(calls.length, 2);
});
test("resource lookup failures or invalid resourcePartyId never obtain a token or create SR", async () => {
for (const body of [{ resourcePartyId: null }, { resourceProfileId: 999 }, { resourcePartyId: "9007199254740993" }, { resourcePartyId: 0 }, "unauthorized"]) {
let calls = 0;
@@ -238,7 +385,7 @@ test("token rejection never creates SR; creation failures are never automaticall
const result = await h.dispatch({ type, phase: "confirm", catalogId: 505, payload: prepared.payload });
assert.equal(result.ok, false);
assert.equal(calls, failure === "token" || failure === "empty-token" ? 2 : 3);
assert.ok(!JSON.stringify(result).includes("secret-token"));
assertDebugVisibility(result, "secret-token");
assert.equal(h.tabs.length, 0);
}
});

View File

@@ -125,9 +125,9 @@ test("SR Save validates form, maps dynamic values, locks duplicate clicks and pr
h.app.debugContentView.renderDebugPanel = () => {};
await h.app.opportunityCrmServiceRequests.load("test", 1);
h.app.currentUser.getUserId = () => "current@example.invalid";
h.app.opportunitiesTableState.opportunitiesTableState.items = [{ OptyNumber: "B", CustomerAccount: { PartyId: 999, Region_c: "EMEA" } }, {
OptyNumber: "A", OptyId: 202, PartyId: 888, AccountPartyId: 777, Owner: { PartyId: 666 }, Region_c: "EMEA", RegionText_c: "LAD", Name: "Opportunity",
CustomerAccount: { PartyId: 100000000000101, PartyUniqueName: "Customer", Region_c: "APAC", PrimaryAddress: { CountryCode: "BR" } }
h.app.opportunitiesTableState.opportunitiesTableState.items = [{ OptyNumber: "B", CustomerAccount: { PartyId: 999, Region_c: "EMEA", PrimaryAddress: { CountryCode: "US" } } }, {
OptyNumber: "A", OptyId: 202, PartyId: 888, AccountPartyId: 777, Owner: { PartyId: 666 }, Region_c: "EMEA", RegionText_c: "LAD", Name: "Opportunity", CountryCode: "CA",
CustomerAccount: { PartyId: 100000000000101, PartyUniqueName: "Customer", Region_c: "APAC", CountryCode: "AR", PrimaryAddress: { CountryCode: "BR" } }
}];
const posts = [];
const notices = [];
@@ -167,7 +167,7 @@ test("SR Save validates form, maps dynamic values, locks duplicate clicks and pr
assert.equal(payload.PrimaryPillarNew_c, "CODE");
assert.equal(payload.Language_c, "CODE");
assert.equal(payload.TechPlatformLookUp_c, "CODE");
assert.equal(payload.CountryText_c, "BR");
assert.equal(payload.CountryText_c, "BR", "uses the selected opportunity's CustomerAccount.PrimaryAddress.CountryCode, not CustomerAccount, root or another opportunity");
assert.equal(payload.RegionText_c, "APAC", "uses CustomerAccount.Region_c from the selected opportunity, not a root-level region or another opportunity");
assert.equal(payload.ProblemDescription, "Dynamic description");
assert.equal(payload.CustomerDeadline_c, "2027-01-31");
@@ -230,7 +230,9 @@ test("SR Save validates form, maps dynamic values, locks duplicate clicks and pr
posts[3].resolve({ ok: false, error: "Synthetic failure" });
await failed;
assert.equal(notices.at(-1)[1], true);
delete h.app.opportunitiesTableState.opportunitiesTableState.items[1].CustomerAccount.CountryCode;
const retry = submit();
assert.equal(posts[4].message.payload.CountryText_c, "BR", "uses PrimaryAddress.CountryCode even without CustomerAccount.CountryCode");
posts[4].resolve({ ok: true, payload: preparedPayload });
await flush();
confirm().click();