Implemented the functionality to create Service Requests
This commit is contained in:
@@ -40,7 +40,7 @@ test("distribution contracts: manifests, execution boundaries and deterministic
|
||||
const base = {
|
||||
manifest_version: 3, name: "Opportunities Extension",
|
||||
description: "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.",
|
||||
version: "1.0.2", permissions: ["cookies", "storage"],
|
||||
version: "1.0.2", permissions: ["cookies", "storage", "webRequest", "declarativeNetRequestWithHostAccess"],
|
||||
host_permissions: ["https://eeho.fa.us2.oraclecloud.com/*", "https://spa.oracle.com/*", "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/*"],
|
||||
content_scripts: [{
|
||||
matches: ["https://eeho.fa.us2.oraclecloud.com/hcmUI/faces/FuseWelcome*", "https://eeho.fa.us2.oraclecloud.com/fscmUI/faces/FuseWelcome*"],
|
||||
@@ -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"],
|
||||
fields: [...fields, "CustomerAccount.PartyUniqueName", "CustomerAccount.PartyId", "CustomerAccount.PartyNumber", "CustomerAccount.Region_c"],
|
||||
sort: [], language: "en", skipInValidFields: true, skipHiddenFromUIFields: true,
|
||||
copiedFrom: "queries/d5dbcd01-9ff3-40be-9b5b-64744cbf7162", limit: 15, offset: 15
|
||||
});
|
||||
|
||||
@@ -84,7 +84,7 @@ test("ramp months preserve zero and flag missing values beside the product count
|
||||
assert.equal(badge(), undefined);
|
||||
for (const [values, expected] of [
|
||||
[[], null], [[null], "N/D"], [[null, undefined, "", " "], "N/D"], [[6], "6"],
|
||||
[[0], "0"], [[0, "3", null, " 6 ", 3], "0,3,N/D,6,3"]
|
||||
[[0], "0"], [[0, "3", null, " 6 ", 3], "0 | 3 | N/D | 6 | 3"]
|
||||
]) {
|
||||
h.app.productsContentState.opptyProductsByNumber.set("TEST-1", {
|
||||
fromCache: true, items: values.map(RampMonths_c => ({ RampMonths_c }))
|
||||
@@ -94,7 +94,7 @@ test("ramp months preserve zero and flag missing values beside the product count
|
||||
else {
|
||||
assert.equal(badge().textContent, expected);
|
||||
assert.equal(badge().attributes["data-missing"], String(expected.includes("N/D")));
|
||||
assert.equal(badge().title, `Ramp Months: ${expected.split(",").join(", ")}`);
|
||||
assert.equal(badge().title, `Ramp Months: ${expected.split(" | ").join(", ")}`);
|
||||
assert.equal(badge().attributes["aria-label"], badge().title);
|
||||
assert.equal(h.cell("name").children[0].children[1].children[1], badge());
|
||||
}
|
||||
@@ -243,7 +243,8 @@ test("table styles reserve compact widths, prioritize Name/Customer and keep nes
|
||||
assert.match(css, /td\[data-column="customer"\] \{\s*position: relative;/);
|
||||
assert.match(css, /grid-template-columns: minmax\(0, 1fr\) 12px/);
|
||||
const tableRules = css.replace(/[^{}]*\.opportunities-extension-products-ramp\s*\{[^}]*\}/g, "")
|
||||
.replace(/[^{}]*\.opportunities-extension-title-row[^{}]*\{[^}]*\}/g, "");
|
||||
.replace(/[^{}]*\.opportunities-extension-title-row[^{}]*\{[^}]*\}/g, "")
|
||||
.replace(/[^{}]*\.opportunities-extension-sr-confirm-dialog\s*\{[^}]*\}/g, "");
|
||||
assert.doesNotMatch(tableRules, /text-overflow: ellipsis|overflow: hidden|th:nth-child|td:nth-child/);
|
||||
assert.match(css, /\.opportunities-extension-products-ramp\s*\{[^}]*background: #f0e8f7/);
|
||||
assert.match(css, /\[data-theme="dark"\] \.opportunities-extension-products-ramp\s*\{[^}]*color: #ead5fc/);
|
||||
|
||||
244
tests/srCreate.test.cjs
Normal file
244
tests/srCreate.test.cjs
Normal file
@@ -0,0 +1,244 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const { test } = require("node:test");
|
||||
const { loadBackground, response } = require("./support/characterization.cjs");
|
||||
const type = "opportunitiesExtension.requestPostServiceRequest";
|
||||
const emailAddress = "signed.in+sr@example.invalid";
|
||||
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",
|
||||
TechPlatformLookUp_c: "NO_ALL_PLATFORM", PrimaryPillarNew_c: "Tech_Cloud_Infrastructure" });
|
||||
const withClientId = (body, status, value) => {
|
||||
const result = response(body, status);
|
||||
result.headers.set("X-AppBuilder-Client-Id", value);
|
||||
return result;
|
||||
};
|
||||
|
||||
for (const family of ["chrome", "firefox"]) {
|
||||
test(`${family}: response client ID from resource lookup appears in confirmation and token request`, async () => {
|
||||
const calls = [];
|
||||
const h = loadBackground({ family, fetch: async (url, options) => {
|
||||
calls.push({ url, options });
|
||||
if (url.includes("/resourceUsers?")) return withClientId({ resourcePartyId: 303 }, 200, "response-client");
|
||||
if (url.endsWith("/Catalogue_c/505")) return response({ result: "test-token" });
|
||||
return response({ SrNumber: "SR1" }, 201);
|
||||
} });
|
||||
const prepared = await h.dispatch({ type, phase: "prepare", emailAddress, payload: payload() });
|
||||
assert.equal(prepared.ok, true);
|
||||
assert.equal(prepared.headers.requestSrTokenCatalog["x-appbuilder-client-id"], "response-client");
|
||||
assert.deepEqual(prepared.headers.requestPostServiceRequest, { ...prepared.headers.requestSrTokenCatalog,
|
||||
"vb-proxy-header-x-oracle-ectoken": "(generated after confirmation)" });
|
||||
assert.equal(calls.length, 1);
|
||||
const result = await h.dispatch({ type, phase: "confirm", catalogId: 505, payload: prepared.payload });
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(calls[1].options.headers["x-appbuilder-client-id"], "response-client");
|
||||
assert.deepEqual({ ...calls[2].options.headers }, { ...calls[1].options.headers, "vb-proxy-header-x-oracle-ectoken": "test-token" });
|
||||
assert.equal(calls.filter(call => call.url.endsWith("/mgmt/serviceRequests")).length, 1);
|
||||
});
|
||||
}
|
||||
|
||||
test("response observation includes background responses and excludes other apps and private requests", async () => {
|
||||
const h = loadBackground();
|
||||
assert.equal(h.responseHeaderListeners.length, 1);
|
||||
const capture = h.responseHeaderListeners[0];
|
||||
assert.deepEqual(capture.options, ["responseHeaders"]);
|
||||
const details = { tabId: -1,
|
||||
url: "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live;profile=PROD/services/auth/1.1/proxy/example",
|
||||
responseHeaders: [{ name: "X-AppBuilder-Client-ID", value: "background-response-client" }, { name: "Set-Cookie", value: "not-retained" }] };
|
||||
capture.listener({ ...details, incognito: true });
|
||||
capture.listener({ ...details, url: details.url.replace("SeaaS_initiation", "unrelated") });
|
||||
assert.deepEqual(h.sessionStorage, {});
|
||||
capture.listener(details);
|
||||
assert.equal(Object.values(h.sessionStorage)[0].value, "background-response-client");
|
||||
assert.ok(!JSON.stringify(h.sessionStorage).includes("not-retained"));
|
||||
});
|
||||
|
||||
test("token 403 retries once only with a newly returned client ID; SR POST is never retried", async () => {
|
||||
for (const mode of ["recovered", "still-forbidden", "unchanged", "no-header", "save-failure"]) {
|
||||
const calls = [];
|
||||
let tokens = 0;
|
||||
const h = loadBackground({ fetch: async (url, options) => {
|
||||
calls.push({ url, options });
|
||||
if (url.includes("/resourceUsers?")) return withClientId({ resourcePartyId: 303 }, 200, "old-client");
|
||||
if (url.endsWith("/Catalogue_c/505")) {
|
||||
tokens += 1;
|
||||
if (tokens === 1) return mode === "no-header" ? response({}, 403)
|
||||
: withClientId({}, 403, mode === "unchanged" ? "old-client" : "new-client");
|
||||
if (mode === "still-forbidden") return withClientId({}, 403, "another-client");
|
||||
return response({ result: "retry-token" });
|
||||
}
|
||||
return response({ SrNumber: "SR1" }, mode === "save-failure" ? 403 : 201);
|
||||
} });
|
||||
const prepared = await h.dispatch({ type, phase: "prepare", emailAddress, payload: payload() });
|
||||
const result = await h.dispatch({ type, phase: "confirm", catalogId: 505, payload: prepared.payload });
|
||||
assert.equal(result.ok, mode === "recovered");
|
||||
const expectedTokens = ["unchanged", "no-header"].includes(mode) ? 1 : 2;
|
||||
assert.equal(tokens, expectedTokens);
|
||||
const tokenCalls = calls.filter(call => call.url.endsWith("/Catalogue_c/505"));
|
||||
assert.equal(tokenCalls[0].options.headers["x-appbuilder-client-id"], "old-client");
|
||||
if (expectedTokens === 2) assert.equal(tokenCalls[1].options.headers["x-appbuilder-client-id"], "new-client");
|
||||
assert.equal(calls.filter(call => call.url.endsWith("/mgmt/serviceRequests")).length, ["recovered", "save-failure"].includes(mode) ? 1 : 0);
|
||||
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"));
|
||||
}
|
||||
});
|
||||
|
||||
for (const family of ["chrome", "firefox"]) {
|
||||
test(`${family}: Save resolves current user, discovers catalog, obtains token and creates SR once with protected diagnostics`, async () => {
|
||||
const calls = [];
|
||||
const h = loadBackground({ family, fetch: async (url, options) => {
|
||||
calls.push({ url, options });
|
||||
if (calls.length === 1) return response({ emailAddress, resourceEmail: emailAddress, resourcePartyId: 303, resourceProfileId: 999 });
|
||||
if (calls.length === 2) return response({ items: [{ Id: 505 }] });
|
||||
if (calls.length === 3) return response({ result: "synthetic-secret-token" });
|
||||
return response({ SrId: 606, SrNumber: "SR606" }, 201);
|
||||
} });
|
||||
const prepared = await h.dispatch({ type, phase: "prepare", emailAddress, payload: { ...payload(), ReportedByPartyId: 999 } });
|
||||
assert.equal(prepared.ok, true);
|
||||
assert.equal(calls.length, 1, "preview only resolves the user, never generates a token or creates SR");
|
||||
assert.equal(h.headerRules.length, 0, "preview does not install request modifications");
|
||||
assert.equal(prepared.headers.requestSrTokenCatalog["Content-Type"], "application/vnd.oracle.adf.action+json");
|
||||
assert.equal(prepared.headers.requestPostServiceRequest["vb-proxy-header-x-oracle-ectoken"], "(generated after confirmation)");
|
||||
assert.equal(prepared.payload.ReportedByPartyId, 303);
|
||||
const result = await h.dispatch({ type, phase: "confirm", payload: prepared.payload });
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.result.SrNumber, "SR606");
|
||||
assert.equal(calls.length, 4);
|
||||
assert.equal(calls[0].options.method, "GET");
|
||||
assert.equal(new URL(calls[0].url).searchParams.get("emailAddress"), emailAddress);
|
||||
assert.ok(calls[0].url.includes("/set/seaas/crm/resourceUsers?"));
|
||||
assert.equal(calls[1].options.method, "GET");
|
||||
assert.ok(calls[2].url.endsWith("/Catalogue_c/505"));
|
||||
assert.deepEqual(JSON.parse(calls[2].options.body), { name: "getJwtToken" });
|
||||
assert.equal(calls[2].options.headers["Content-Type"], "application/vnd.oracle.adf.action+json");
|
||||
assert.equal(calls[2].options.headers.Origin, new URL(calls[2].url).origin);
|
||||
assert.deepEqual({ ...calls[3].options.headers }, { ...calls[2].options.headers, "vb-proxy-header-x-oracle-ectoken": "synthetic-secret-token" });
|
||||
assert.equal(h.headerRules.length, 1);
|
||||
const rule = h.headerRules[0].addRules[0];
|
||||
const serviceRule = h.headerRules[0].addRules[1];
|
||||
assert.ok(new RegExp(serviceRule.condition.regexFilter).test(calls[3].url));
|
||||
assert.ok(!new RegExp(serviceRule.condition.regexFilter).test(calls[2].url));
|
||||
assert.ok(!new RegExp(serviceRule.condition.regexFilter).test(calls[3].url + "/other"));
|
||||
assert.deepEqual(serviceRule.condition.initiatorDomains, ["synthetic-extension"]);
|
||||
assert.deepEqual(serviceRule.condition.requestMethods, ["post"]);
|
||||
assert.deepEqual(serviceRule.action, rule.action);
|
||||
assert.deepEqual(rule.condition.initiatorDomains, ["synthetic-extension"]);
|
||||
assert.deepEqual(rule.condition.requestMethods, ["post"]);
|
||||
assert.ok(new RegExp(rule.condition.regexFilter).test(calls[2].url));
|
||||
assert.ok(!new RegExp(rule.condition.regexFilter).test(calls[3].url));
|
||||
assert.ok(!new RegExp(rule.condition.regexFilter).test(calls[2].url + "/other"));
|
||||
assert.deepEqual(rule.action.requestHeaders, [{ header: "Origin", operation: "set", value: new URL(calls[2].url).origin }]);
|
||||
assert.equal(calls[3].options.method, "POST");
|
||||
assert.ok(calls[3].url.endsWith("/set/seaas/mgmt/serviceRequests"));
|
||||
assert.equal(calls[3].options.headers["vb-proxy-header-x-oracle-ectoken"], "synthetic-secret-token");
|
||||
assert.ok(calls.slice(0, 3).every(call => !call.options.headers["vb-proxy-header-x-oracle-ectoken"]));
|
||||
assert.ok(calls.every(call => call.options.credentials === "include" && call.options.headers.Authorization === "Session"));
|
||||
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"));
|
||||
assert.equal(h.tabs.length, 0);
|
||||
});
|
||||
}
|
||||
|
||||
test("missing region, invalid dates or unsafe IDs stop before token acquisition", async () => {
|
||||
const h = loadBackground({ fetch: async () => assert.fail("Unexpected network") });
|
||||
for (const change of [{ RegionText_c: null }, { CustomerDeadline_c: "2027-02-30" },
|
||||
{ AccountPartyId: "9007199254740993" }, { OpportunityName_Id_c: "bad/id" }]) {
|
||||
assert.equal((await h.dispatch({ type, phase: "prepare", emailAddress, payload: { ...payload(), ...change } })).ok, false);
|
||||
}
|
||||
assert.equal((await h.dispatch({ type, phase: "prepare", payload: payload() })).ok, false);
|
||||
assert.equal((await h.dispatch({ type, phase: "confirm", payload: payload() })).ok, false);
|
||||
assert.equal((await h.dispatch({ type, emailAddress, payload: payload() })).ok, false);
|
||||
});
|
||||
|
||||
for (const family of ["chrome", "firefox"]) {
|
||||
test(`${family}: captures only SeaaS client ID, reuses it after worker restart and expires stale values`, async () => {
|
||||
const tokenCalls = [];
|
||||
const sessionStorage = {};
|
||||
const fetch = async (url, options) => {
|
||||
if (url.includes("/resourceUsers?")) return response({ resourcePartyId: 303 });
|
||||
if (url.endsWith("/Catalogue_c/505")) {
|
||||
tokenCalls.push(options);
|
||||
return response({ result: "synthetic-token" });
|
||||
}
|
||||
return response({ SrNumber: "SR1" }, 201);
|
||||
};
|
||||
const h = loadBackground({ family, fetch, sessionStorage });
|
||||
assert.equal(h.headerListeners.length, 1);
|
||||
const capture = h.headerListeners[0];
|
||||
assert.deepEqual(capture.options, ["requestHeaders"]);
|
||||
assert.deepEqual(capture.filter.types, ["xmlhttprequest"]);
|
||||
const request = { tabId: 12, url: "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live;profile=PROD/services/auth/1.1/proxy/example",
|
||||
requestHeaders: [{ name: "X-AppBuilder-Client-Id", value: "synthetic-client" },
|
||||
{ name: "Cookie", value: "do-not-store-cookie" }, { name: "Authorization", value: "do-not-store-auth" }] };
|
||||
capture.listener({ ...request, url: request.url.replace("SeaaS_initiation", "otherApp") });
|
||||
capture.listener({ ...request, url: request.url.replace("comcipapic-oalprod.integration.ocp.oraclecloud.com", "other.example") });
|
||||
capture.listener({ ...request, tabId: -1 });
|
||||
capture.listener({ ...request, incognito: true });
|
||||
capture.listener({ ...request, requestHeaders: [{ name: "x-appbuilder-client-id", value: "invalid\r\nheader" }] });
|
||||
assert.deepEqual(sessionStorage, {});
|
||||
capture.listener(request);
|
||||
assert.equal(Object.keys(sessionStorage).length, 1);
|
||||
assert.ok(!JSON.stringify(sessionStorage).includes("do-not-store"));
|
||||
const prepared = await h.dispatch({ type, phase: "prepare", emailAddress, payload: payload() });
|
||||
assert.equal(prepared.headers.requestSrTokenCatalog["x-appbuilder-client-id"], "synthetic-client");
|
||||
assert.equal(tokenCalls.length, 0);
|
||||
const restarted = loadBackground({ family, fetch, sessionStorage });
|
||||
const result = await restarted.dispatch({ type, phase: "confirm", catalogId: 505, payload: prepared.payload });
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(tokenCalls[0].headers["x-appbuilder-client-id"], "synthetic-client");
|
||||
assert.equal(result.debugRequests[0].metadata.appBuilderClientId, "Observed in SeaaS requests");
|
||||
restarted.advance(60 * 60 * 1000 + 1);
|
||||
const expired = await restarted.dispatch({ type, phase: "confirm", catalogId: 505, payload: prepared.payload });
|
||||
assert.equal(expired.ok, true);
|
||||
assert.equal(tokenCalls[1].headers["x-appbuilder-client-id"], undefined);
|
||||
assert.equal(expired.debugRequests[0].metadata.appBuilderClientId, "Not observed in SeaaS requests");
|
||||
capture.listener({ ...request, requestHeaders: [{ name: "x-appbuilder-client-id", value: "renewed-client" }] });
|
||||
const refreshed = await h.dispatch({ type, phase: "prepare", emailAddress, payload: payload() });
|
||||
assert.equal(refreshed.headers.requestSrTokenCatalog["x-appbuilder-client-id"], "renewed-client");
|
||||
assert.equal(h.tabs.length, 0, "header discovery does not add background page loads");
|
||||
});
|
||||
}
|
||||
|
||||
test("Origin rule failure blocks token acquisition and SR creation", async () => {
|
||||
const h = loadBackground({ ruleError: true, fetch: async () => assert.fail("No request expected") });
|
||||
const result = await h.dispatch({ type, phase: "confirm", catalogId: 505, payload: { ...payload(), ReportedByPartyId: 303 } });
|
||||
assert.equal(result.ok, false);
|
||||
assert.match(result.error, /Rule permission denied/);
|
||||
});
|
||||
|
||||
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;
|
||||
const h = loadBackground({ fetch: async () => {
|
||||
calls += 1;
|
||||
return response(body, body === "unauthorized" ? 401 : 200);
|
||||
} });
|
||||
const result = await h.dispatch({ type, phase: "prepare", emailAddress, payload: payload() });
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(calls, 1);
|
||||
assert.equal(result.debugRequests[0].name, "requestResourceUser");
|
||||
}
|
||||
});
|
||||
|
||||
test("token rejection never creates SR; creation failures are never automatically retried", async () => {
|
||||
for (const failure of ["token", "create", "empty-token", "network"]) {
|
||||
let calls = 0;
|
||||
const h = loadBackground({ fetch: async () => {
|
||||
calls += 1;
|
||||
if (calls === 1) return response({ resourcePartyId: 303 });
|
||||
if (calls === 2) return failure === "token" ? response({}, 401)
|
||||
: response({ result: failure === "empty-token" ? "" : "secret-token" });
|
||||
if (failure === "network") throw new Error("Network error");
|
||||
return response({}, 500);
|
||||
} });
|
||||
const prepared = await h.dispatch({ type, phase: "prepare", emailAddress, payload: payload() });
|
||||
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"));
|
||||
assert.equal(h.tabs.length, 0);
|
||||
}
|
||||
});
|
||||
@@ -57,7 +57,7 @@ function loadContent({ fetch = unexpectedFetch, storage = new Map(), xsrf = null
|
||||
};
|
||||
}
|
||||
|
||||
function loadBackground({ family = "chrome", fetch = unexpectedFetch, storage = {}, cookies = () => [], storageError = false, messageTimeout = 2000 } = {}) {
|
||||
function loadBackground({ family = "chrome", fetch = unexpectedFetch, storage = {}, sessionStorage = {}, cookies = () => [], storageError = false, ruleError = false, messageTimeout = 2000 } = {}) {
|
||||
const time = clock();
|
||||
const listeners = [];
|
||||
const progress = [];
|
||||
@@ -65,7 +65,17 @@ function loadBackground({ family = "chrome", fetch = unexpectedFetch, storage =
|
||||
const tabs = [];
|
||||
const removedTabs = [];
|
||||
const cookieQueries = [];
|
||||
const api = { runtime: { onMessage: { addListener: fn => listeners.push(fn) } } };
|
||||
const headerListeners = [];
|
||||
const responseHeaderListeners = [];
|
||||
const headerRules = [];
|
||||
const api = { runtime: { onMessage: { addListener: fn => listeners.push(fn) },
|
||||
getURL: path => `${family === "firefox" ? "moz" : "chrome"}-extension://synthetic-extension${path}` },
|
||||
webRequest: {
|
||||
onBeforeSendHeaders: { addListener: (listener, filter, options) => headerListeners.push({ listener, filter: plain(filter), options: plain(options) }) },
|
||||
onHeadersReceived: { addListener: (listener, filter, options) => responseHeaderListeners.push({ listener, filter: plain(filter), options: plain(options) }) }
|
||||
},
|
||||
declarativeNetRequest: { updateSessionRules: async rules => { if (ruleError) throw new Error("Rule permission denied"); headerRules.push(plain(rules)); } }
|
||||
};
|
||||
const readCookies = details => { cookieQueries.push(plain(details)); return cookies(details); };
|
||||
const getStored = key => ({ [key]: storage[key] === undefined ? undefined : plain(storage[key]) });
|
||||
const store = value => Object.assign(storage, plain(value));
|
||||
@@ -110,6 +120,10 @@ function loadBackground({ family = "chrome", fetch = unexpectedFetch, storage =
|
||||
getAllCookieStores(callback) { callback([{ id: "test-store" }]); }
|
||||
};
|
||||
}
|
||||
api.storage.session = {
|
||||
get: async key => ({ [key]: sessionStorage[key] === undefined ? undefined : plain(sessionStorage[key]) }),
|
||||
set: async value => Object.assign(sessionStorage, plain(value))
|
||||
};
|
||||
const context = {
|
||||
[family === "firefox" ? "browser" : "chrome"]: api,
|
||||
fetch, URL, Date: time.Date,
|
||||
@@ -132,7 +146,7 @@ function loadBackground({ family = "chrome", fetch = unexpectedFetch, storage =
|
||||
if (pending !== true) { clearTimeout(timer); reject(new Error("Expected asynchronous response contract")); }
|
||||
});
|
||||
}
|
||||
return { dispatch, listener: listeners[0], progress, requestDebug, tabs, removedTabs, cookieQueries, storage, advance: time.advance };
|
||||
return { dispatch, listener: listeners[0], progress, requestDebug, tabs, removedTabs, cookieQueries, storage, sessionStorage, headerRules, headerListeners, responseHeaderListeners, advance: time.advance };
|
||||
}
|
||||
|
||||
module.exports = { root, plain, response, loadContent, loadBackground };
|
||||
|
||||
@@ -118,6 +118,129 @@ test("missing SR plus opens pillar form, handles retry, ignores closed requests
|
||||
assert.equal(h.host.children.length, 0);
|
||||
});
|
||||
|
||||
test("SR Save validates form, maps dynamic values, locks duplicate clicks and preserves failures", async () => {
|
||||
const h = setup();
|
||||
h.app.opportunitiesQueryState.periodRequestVersion = 1;
|
||||
h.app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => [];
|
||||
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" } }
|
||||
}];
|
||||
const posts = [];
|
||||
const notices = [];
|
||||
h.app.opportunitiesLinks.showCopyConfirmation = (...args) => notices.push(args);
|
||||
h.app.platformContentMessages.sendRuntimeMessage = message => {
|
||||
if (message.type.endsWith("requestPostServiceRequest")) return new Promise(resolve => posts.push({ message, resolve }));
|
||||
return Promise.resolve({ ok: true, items: message.type.endsWith("requestSrLookupOpportunityBusiness")
|
||||
? [{ Id: "404", RecordName: "Opportunity Pursuit", Abstract_c: "Description" }] : [{ code: "CODE", name: "Label" }] });
|
||||
};
|
||||
const plus = h.app.opportunityCrmServiceRequests.createBadge("A").children.find(node => node.tag === "button");
|
||||
plus.click();
|
||||
await flush();
|
||||
const save = () => h.nodes().find(node => node.tag === "button" && ["Save", "Preparing...", "Saving..."].includes(node.textContent));
|
||||
const submit = () => h.form().listeners.submit({ preventDefault() {} });
|
||||
assert.equal(save().disabled, false);
|
||||
await submit();
|
||||
assert.equal(posts.length, 0);
|
||||
assert.ok(h.nodes().some(node => node.attributes["aria-invalid"] === "true"));
|
||||
for (const name of ["primaryPillar", "language", "platform"]) h.nodes().find(node => node.name === name).value = "CODE";
|
||||
h.nodes().find(node => node.name === "customerDeadline").value = "2027-01-31";
|
||||
h.nodes().find(node => node.name === "description").value = "Dynamic description";
|
||||
h.nodes().find(node => node.name === "service").checked = true;
|
||||
const saving = submit();
|
||||
await submit();
|
||||
assert.equal(posts.length, 1);
|
||||
assert.equal(save().textContent, "Preparing...");
|
||||
assert.equal(save().disabled, true);
|
||||
h.nodes().find(node => node.textContent === "Cancel").click();
|
||||
assert.ok(h.form(), "cannot dismiss while Save is in flight");
|
||||
const payload = posts[0].message.payload;
|
||||
assert.equal(payload.AccountPartyId, 100000000000101, "uses only the selected item's CustomerAccount.PartyId");
|
||||
assert.equal(payload.OpportunityName_Id_c, 202);
|
||||
assert.equal(payload.ReportedByPartyId, undefined);
|
||||
assert.equal(posts[0].message.emailAddress, "current@example.invalid");
|
||||
assert.equal(payload.Title, "Opportunity Pursuit-A-Customer");
|
||||
assert.equal(payload.ServiceNew_Id_c, "404");
|
||||
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.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");
|
||||
assert.equal(posts[0].message.phase, "prepare");
|
||||
const preparedPayload = { ...payload, ReportedByPartyId: 303, SvcQuestionsCollection_c: [],
|
||||
SourceCd: "SVC_OPPORTUNITY_SCREEN", SeverityCd: "ORA_SVC_SEV2", ChannelTypeCd: "ORA_SVC_WEB" };
|
||||
const preparedHeaders = {
|
||||
requestSrTokenCatalog: { Authorization: "Session", "Content-Type": "application/vnd.oracle.adf.action+json", Origin: "https://example.invalid" },
|
||||
requestPostServiceRequest: { Authorization: "Session", "Content-Type": "application/vnd.oracle.adf.action+json", Origin: "https://example.invalid", "vb-proxy-header-x-oracle-ectoken": "(generated after confirmation)" }
|
||||
};
|
||||
posts[0].resolve({ ok: true, payload: preparedPayload, headers: preparedHeaders });
|
||||
await flush();
|
||||
const confirmation = () => h.nodes().find(node => node.className?.includes("opportunities-extension-sr-confirmation"));
|
||||
const confirm = () => h.nodes().find(node => node.textContent === "Confirm Save");
|
||||
assert.ok(confirmation());
|
||||
const preview = h.nodes().find(node => node.tag === "pre");
|
||||
assert.equal(preview.textContent, JSON.stringify(preparedPayload, null, 2));
|
||||
const previews = h.nodes().filter(node => node.tag === "pre");
|
||||
assert.equal(previews.length, 3);
|
||||
assert.equal(previews[1].textContent, JSON.stringify(preparedHeaders.requestSrTokenCatalog, null, 2));
|
||||
assert.equal(previews[2].textContent, JSON.stringify(preparedHeaders.requestPostServiceRequest, null, 2));
|
||||
assert.ok(h.nodes().some(node => node.textContent === "x-appbuilder-client-id: not yet observed in SeaaS requests."));
|
||||
assert.equal(h.form().parent.inert, true);
|
||||
assert.equal(h.document.activeElement.textContent, "Cancel");
|
||||
await submit();
|
||||
assert.equal(posts.length, 1, "awaiting confirmation must never send a creation request");
|
||||
confirm().focus();
|
||||
confirmation().listeners.keydown({ key: "Tab", preventDefault() {}, stopPropagation() {} });
|
||||
assert.equal(h.document.activeElement, preview);
|
||||
h.nodes().filter(node => node.tag === "button" && node.textContent === "Cancel").at(-1).click();
|
||||
await saving;
|
||||
assert.equal(posts.length, 1, "Cancel never creates SR");
|
||||
assert.equal(confirmation(), undefined);
|
||||
assert.equal(h.form().parent.inert, false);
|
||||
assert.equal(save().disabled, false);
|
||||
assert.equal(h.nodes().find(node => node.name === "description").value, "Dynamic description");
|
||||
assert.equal(h.document.activeElement, save());
|
||||
assert.equal(notices.length, 0);
|
||||
|
||||
const escaped = submit();
|
||||
posts[1].resolve({ ok: true, payload: preparedPayload });
|
||||
await flush();
|
||||
confirmation().listeners.keydown({ key: "Escape", preventDefault() {}, stopPropagation() {} });
|
||||
await escaped;
|
||||
assert.equal(posts.length, 2);
|
||||
assert.ok(h.form(), "Escape closes only the confirmation");
|
||||
|
||||
const failed = submit();
|
||||
posts[2].resolve({ ok: true, payload: preparedPayload });
|
||||
await flush();
|
||||
const confirmButton = confirm();
|
||||
confirmButton.click();
|
||||
confirmButton.click();
|
||||
await flush();
|
||||
assert.equal(posts.length, 4, "double confirm sends only one creation request");
|
||||
assert.equal(posts[3].message.phase, "confirm");
|
||||
assert.deepEqual(posts[3].message.payload, preparedPayload);
|
||||
assert.equal(posts[3].message.payload.AccountPartyId, 100000000000101);
|
||||
assert.equal(save().textContent, "Saving...");
|
||||
posts[3].resolve({ ok: false, error: "Synthetic failure" });
|
||||
await failed;
|
||||
assert.equal(notices.at(-1)[1], true);
|
||||
const retry = submit();
|
||||
posts[4].resolve({ ok: true, payload: preparedPayload });
|
||||
await flush();
|
||||
confirm().click();
|
||||
await flush();
|
||||
posts[5].resolve({ ok: true, result: { SrId: 505, SrNumber: "SR505" } });
|
||||
await retry;
|
||||
assert.equal(h.form(), undefined);
|
||||
assert.equal(notices.at(-1)[0], "Service Request saved successfully.");
|
||||
});
|
||||
|
||||
test("only plus opens the nine-field form; a missing workload stays disabled without a save operation", () => {
|
||||
const h = setup();
|
||||
const badge = h.app.opportunityWorkloadActions.createBadge("A");
|
||||
|
||||
Reference in New Issue
Block a user