Implemented the Create Internal Service Request functionality
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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/);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
13
tests/fixtures/extension.js
vendored
13
tests/fixtures/extension.js
vendored
@@ -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}`);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user