Implemented the functionality to create Service Requests
This commit is contained in:
193
dist/chromium/background.js
vendored
193
dist/chromium/background.js
vendored
@@ -19,6 +19,7 @@ function createMessageTypes() {
|
|||||||
requestMgmtAction: "opportunitiesExtension.requestMgmtAction",
|
requestMgmtAction: "opportunitiesExtension.requestMgmtAction",
|
||||||
requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest",
|
requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest",
|
||||||
requestSrLookupPrimaryPillar: "opportunitiesExtension.requestSrLookupPrimaryPillar",
|
requestSrLookupPrimaryPillar: "opportunitiesExtension.requestSrLookupPrimaryPillar",
|
||||||
|
requestPostServiceRequest: "opportunitiesExtension.requestPostServiceRequest",
|
||||||
prepareSrSession: "opportunitiesExtension.prepareSrSession",
|
prepareSrSession: "opportunitiesExtension.prepareSrSession",
|
||||||
requestSrLookupLanguage: "opportunitiesExtension.requestSrLookupLanguage",
|
requestSrLookupLanguage: "opportunitiesExtension.requestSrLookupLanguage",
|
||||||
requestSrLookupTechPlatform: "opportunitiesExtension.requestSrLookupTechPlatform",
|
requestSrLookupTechPlatform: "opportunitiesExtension.requestSrLookupTechPlatform",
|
||||||
@@ -430,6 +431,90 @@ function createAuthSpaCredentials({
|
|||||||
return { normalizeMessageAuthorization, extractTokenServiceAuthorization };
|
return { normalizeMessageAuthorization, extractTokenServiceAuthorization };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Source: src/auth/sr-request-headers.js
|
||||||
|
function createSrRequestHeaders({ runtimeApi }) {
|
||||||
|
const origin = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com";
|
||||||
|
const appPath = "/ic/builder/rt/SeaaS_initiation/";
|
||||||
|
const storageKey = "opportunitiesExtension.srAppBuilderClientId.v1";
|
||||||
|
const maxAge = 60 * 60 * 1000;
|
||||||
|
let observed = null;
|
||||||
|
let started = false;
|
||||||
|
function valid(record) {
|
||||||
|
return record?.origin === origin && typeof record.value === "string"
|
||||||
|
&& /^[\x21-\x7e]{1,2048}$/.test(record.value)
|
||||||
|
&& Number.isFinite(record.observedAt) && record.observedAt <= Date.now()
|
||||||
|
&& Date.now() - record.observedAt < maxAge;
|
||||||
|
}
|
||||||
|
function observeHeaders(rawUrl, headers) {
|
||||||
|
let url;
|
||||||
|
try { url = new URL(rawUrl); } catch { return; }
|
||||||
|
if (url.origin !== origin || !/^\/ic\/builder\/rt\/SeaaS_initiation\/live(?:\/|;)/.test(url.pathname)) return;
|
||||||
|
// Read only the client ID, not cookies, bodies or authorization.
|
||||||
|
const value = typeof headers?.get === "function" ? headers.get("x-appbuilder-client-id")
|
||||||
|
: headers?.find(item => item.name.toLowerCase() === "x-appbuilder-client-id")?.value;
|
||||||
|
const record = { origin, value: value?.trim(), observedAt: Date.now() };
|
||||||
|
if (!valid(record)) return;
|
||||||
|
observed = record;
|
||||||
|
// Session storage survives service-worker suspension without persisting across browser sessions.
|
||||||
|
runtimeApi.storage?.session?.set({ [storageKey]: record }).catch(() => {});
|
||||||
|
}
|
||||||
|
function start() {
|
||||||
|
if (started || !runtimeApi?.webRequest) return;
|
||||||
|
started = true;
|
||||||
|
const filter = { urls: [`${origin}${appPath}*`], types: ["xmlhttprequest"] };
|
||||||
|
runtimeApi.webRequest.onBeforeSendHeaders?.addListener(details => {
|
||||||
|
if (details.tabId >= 0 && !details.incognito) observeHeaders(details.url, details.requestHeaders);
|
||||||
|
}, filter, ["requestHeaders"]);
|
||||||
|
runtimeApi.webRequest.onHeadersReceived?.addListener(details => {
|
||||||
|
if (!details.incognito) observeHeaders(details.url, details.responseHeaders);
|
||||||
|
}, filter, ["responseHeaders"]);
|
||||||
|
}
|
||||||
|
async function previewTokenHeaders(url) {
|
||||||
|
const target = new URL(url);
|
||||||
|
if (target.origin !== origin || !target.pathname.startsWith(appPath) || !/\/Catalogue_c(?:\/\d+)?$/.test(target.pathname)) {
|
||||||
|
throw new Error("Unexpected Service Request token URL.");
|
||||||
|
}
|
||||||
|
if (!valid(observed)) {
|
||||||
|
try {
|
||||||
|
const stored = await runtimeApi.storage?.session?.get(storageKey);
|
||||||
|
if (valid(stored?.[storageKey]) && !valid(observed)) observed = stored[storageKey];
|
||||||
|
} catch { /* A missing capture must not prevent trying the other required headers. */ }
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
Accept: "application/json", "Content-Type": "application/vnd.oracle.adf.action+json",
|
||||||
|
Authorization: "Session", Origin: target.origin,
|
||||||
|
...(valid(observed) ? { "x-appbuilder-client-id": observed.value } : {})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async function tokenHeaders(url, serviceRequestUrl) {
|
||||||
|
const headers = await previewTokenHeaders(url);
|
||||||
|
if (!/\/Catalogue_c\/\d+$/.test(new URL(url).pathname)) throw new Error("Missing Catalogue Id.");
|
||||||
|
const serviceTarget = new URL(serviceRequestUrl);
|
||||||
|
if (serviceTarget.origin !== headers.Origin || !serviceTarget.pathname.startsWith(appPath)
|
||||||
|
|| !serviceTarget.pathname.endsWith("/set/seaas/mgmt/serviceRequests") || serviceTarget.search || serviceTarget.hash) {
|
||||||
|
throw new Error("Unexpected Service Request URL.");
|
||||||
|
}
|
||||||
|
if (!runtimeApi?.declarativeNetRequest?.updateSessionRules || !runtimeApi.runtime?.getURL) {
|
||||||
|
throw new Error("Reload the extension to enable the Service Request token header permissions.");
|
||||||
|
}
|
||||||
|
const extensionDomain = new URL(runtimeApi.runtime.getURL("/")).hostname;
|
||||||
|
const catalogUrl = url.slice(0, url.lastIndexOf("/") + 1);
|
||||||
|
const escapedCatalogUrl = catalogUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
const escapedServiceUrl = serviceTarget.href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
await runtimeApi.declarativeNetRequest.updateSessionRules({
|
||||||
|
removeRuleIds: [7201, 7202],
|
||||||
|
addRules: [`^${escapedCatalogUrl}[0-9]+$`, `^${escapedServiceUrl}$`].map((regexFilter, index) => ({
|
||||||
|
id: 7201 + index, priority: 1,
|
||||||
|
action: { type: "modifyHeaders", requestHeaders: [{ header: "Origin", operation: "set", value: headers.Origin }] },
|
||||||
|
condition: { regexFilter, initiatorDomains: [extensionDomain],
|
||||||
|
requestMethods: ["post"], resourceTypes: ["xmlhttprequest"] }
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
return { start, tokenHeaders, previewTokenHeaders, observeResponseHeaders: observeHeaders };
|
||||||
|
}
|
||||||
|
|
||||||
// Source: src/services/spa/accounts.js
|
// Source: src/services/spa/accounts.js
|
||||||
function createSpaAccounts({
|
function createSpaAccounts({
|
||||||
ACCOUNT_PLANS_PAYLOAD,
|
ACCOUNT_PLANS_PAYLOAD,
|
||||||
@@ -951,6 +1036,96 @@ function createFusionSrDetails({ fetchJson, tabsCreate, tabsRemove, supportsFire
|
|||||||
requestSrLookupTechPlatform: () => requestLookup("SENGG_TECH_PLATFORM", "requestSrLookupTechPlatform") };
|
requestSrLookupTechPlatform: () => requestLookup("SENGG_TECH_PLATFORM", "requestSrLookupTechPlatform") };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Source: src/services/fusion/sr-create.js
|
||||||
|
function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders }) {
|
||||||
|
const base = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live;profile=PROD/services/auth/1.1/proxy/";
|
||||||
|
const catalog = base + "crmRestApiResources/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/11.13.18.05/Catalogue_c";
|
||||||
|
const resourceUsers = base + "oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/resourceUsers";
|
||||||
|
const endpoint = base + "oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/mgmt/serviceRequests";
|
||||||
|
function numericId(value, name) {
|
||||||
|
if (!/^\d+$/.test(String(value ?? "")) || !Number.isSafeInteger(Number(value)) || Number(value) <= 0) throw new Error(`Missing or invalid ${name}.`);
|
||||||
|
return Number(value);
|
||||||
|
}
|
||||||
|
function text(value, name) {
|
||||||
|
if (typeof value !== "string" || !value.trim()) throw new Error(`Missing ${name}.`);
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
function payloadFor(input) {
|
||||||
|
const payload = {};
|
||||||
|
for (const name of ["AccountPartyId", "OpportunityName_Id_c", "ServiceNew_Id_c"]) payload[name] = numericId(input?.[name], name);
|
||||||
|
for (const name of ["Title", "RegionText_c", "ProblemDescription", "Language_c", "CustomerDeadline_c", "TechPlatformLookUp_c", "CountryText_c", "PrimaryPillarNew_c"]) payload[name] = text(input?.[name], name);
|
||||||
|
const date = payload.CustomerDeadline_c;
|
||||||
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(Date.parse(`${date}T00:00:00Z`))
|
||||||
|
|| new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) !== date) throw new Error("Invalid Customer deadline.");
|
||||||
|
return { ...payload, SvcQuestionsCollection_c: [], SourceCd: "SVC_OPPORTUNITY_SCREEN", SeverityCd: "ORA_SVC_SEV2", ChannelTypeCd: "ORA_SVC_WEB" };
|
||||||
|
}
|
||||||
|
async function requestPostServiceRequest(message) {
|
||||||
|
const debugRequests = [];
|
||||||
|
try {
|
||||||
|
if (message.phase !== "prepare" && message.phase !== "confirm") throw new Error("Service Request confirmation is required.");
|
||||||
|
const payload = payloadFor(message.payload);
|
||||||
|
if (message.phase === "prepare") {
|
||||||
|
const emailAddress = text(message.emailAddress, "current user email");
|
||||||
|
const resource = await fetchJson(`${resourceUsers}?emailAddress=${encodeURIComponent(emailAddress)}`, {
|
||||||
|
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
|
||||||
|
}, "requestResourceUser", debugRequests);
|
||||||
|
payload.ReportedByPartyId = numericId(resource?.resourcePartyId, "resourcePartyId");
|
||||||
|
const sharedHeaders = await previewTokenHeaders(catalog);
|
||||||
|
return { ok: true, payload, debugRequests, headers: {
|
||||||
|
requestSrTokenCatalog: sharedHeaders,
|
||||||
|
requestPostServiceRequest: { ...sharedHeaders, "vb-proxy-header-x-oracle-ectoken": "(generated after confirmation)" }
|
||||||
|
} };
|
||||||
|
}
|
||||||
|
payload.ReportedByPartyId = numericId(message.payload?.ReportedByPartyId, "ReportedByPartyId");
|
||||||
|
let id = message.catalogId;
|
||||||
|
if (id == null) {
|
||||||
|
const result = await fetchJson(`${catalog}?onlyData=true&fields=Id&limit=1`, {
|
||||||
|
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
|
||||||
|
}, "requestSrCatalog", debugRequests);
|
||||||
|
id = result?.items?.[0]?.Id;
|
||||||
|
}
|
||||||
|
id = numericId(id, "Catalogue Id");
|
||||||
|
const tokenUrl = `${catalog}/${id}`;
|
||||||
|
let catalogHeaders = await tokenHeaders(tokenUrl, endpoint);
|
||||||
|
const requestToken = () => fetchJson(tokenUrl, {
|
||||||
|
method: "POST", credentials: "include", headers: catalogHeaders, body: JSON.stringify({ name: "getJwtToken" })
|
||||||
|
}, "requestSrTokenCatalog", debugRequests);
|
||||||
|
let token;
|
||||||
|
try {
|
||||||
|
token = await requestToken();
|
||||||
|
} catch (error) {
|
||||||
|
if (error.httpStatus !== 403) throw error;
|
||||||
|
const refreshed = await previewTokenHeaders(tokenUrl);
|
||||||
|
if (!refreshed["x-appbuilder-client-id"] || refreshed["x-appbuilder-client-id"] === catalogHeaders["x-appbuilder-client-id"]) throw error;
|
||||||
|
catalogHeaders = refreshed;
|
||||||
|
token = await requestToken();
|
||||||
|
}
|
||||||
|
if (debugRequests.at(-1)?.httpStatus !== 200 || typeof token?.result !== "string" || !token.result.trim()) {
|
||||||
|
throw new Error("requestSrTokenCatalog did not return HTTP 200 with a token.");
|
||||||
|
}
|
||||||
|
const result = await fetchJson(endpoint, {
|
||||||
|
method: "POST", credentials: "include", headers: { ...catalogHeaders, "vb-proxy-header-x-oracle-ectoken": token.result.trim() },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
}, "requestPostServiceRequest", debugRequests, { allowEmpty: true });
|
||||||
|
return { ok: true, result, debugRequests };
|
||||||
|
} catch (error) {
|
||||||
|
error.debugRequests = debugRequests;
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// Never forward the signing token to content scripts, even with Debug secrets enabled.
|
||||||
|
for (const entry of debugRequests) {
|
||||||
|
if (entry.name === "requestSrTokenCatalog") {
|
||||||
|
entry.metadata.appBuilderClientId = entry.requestHeaders?.["x-appbuilder-client-id"] ? "Observed in SeaaS requests" : "Not observed in SeaaS requests";
|
||||||
|
entry.responseBody = JSON.stringify({ result: "(redacted)" });
|
||||||
|
entry.responseBodyTruncated = false;
|
||||||
|
}
|
||||||
|
if (entry.requestHeaders?.["vb-proxy-header-x-oracle-ectoken"]) entry.requestHeaders["vb-proxy-header-x-oracle-ectoken"] = "(redacted)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { requestPostServiceRequest };
|
||||||
|
}
|
||||||
|
|
||||||
// Source: src/services/spa/current-user.js
|
// Source: src/services/spa/current-user.js
|
||||||
function createSpaCurrentUser({ fetchJson }) {
|
function createSpaCurrentUser({ fetchJson }) {
|
||||||
async function requestCurrentUser() {
|
async function requestCurrentUser() {
|
||||||
@@ -1156,6 +1331,7 @@ function createSpaResponseShapes() {
|
|||||||
function createHttpBackgroundJson({
|
function createHttpBackgroundJson({
|
||||||
previewDebugBody,
|
previewDebugBody,
|
||||||
DEBUG_BODY_LIMIT,
|
DEBUG_BODY_LIMIT,
|
||||||
|
observeResponseHeaders = () => {},
|
||||||
environment
|
environment
|
||||||
}) {
|
}) {
|
||||||
const { fetch } = environment;
|
const { fetch } = environment;
|
||||||
@@ -1172,6 +1348,7 @@ function createHttpBackgroundJson({
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
observeResponseHeaders(response.url || url, response.headers);
|
||||||
const responsePayload = await readResponsePayload(response);
|
const responsePayload = await readResponsePayload(response);
|
||||||
const responseBody = createDebugBody(responsePayload.text);
|
const responseBody = createDebugBody(responsePayload.text);
|
||||||
completeBackgroundDebugEntry(debugEntry, response, responseBody, "");
|
completeBackgroundDebugEntry(debugEntry, response, responseBody, "");
|
||||||
@@ -2049,6 +2226,7 @@ function registerBackgroundMessages({
|
|||||||
requestActionTeamMembers,
|
requestActionTeamMembers,
|
||||||
requestActionFindLookup,
|
requestActionFindLookup,
|
||||||
requestMgmtAction,
|
requestMgmtAction,
|
||||||
|
requestPostServiceRequest,
|
||||||
requestInternalServiceRequest,
|
requestInternalServiceRequest,
|
||||||
requestSrLookupPrimaryPillar,
|
requestSrLookupPrimaryPillar,
|
||||||
requestSrLookupWithSession,
|
requestSrLookupWithSession,
|
||||||
@@ -2068,6 +2246,11 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
if (!message || !message.type) {
|
if (!message || !message.type) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (message.type === messageTypes.requestPostServiceRequest) {
|
||||||
|
requestPostServiceRequest(message).then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||||
|
error: error.message, debugRequests: error.debugRequests || [] }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (message.type === messageTypes.prepareSrSession) {
|
if (message.type === messageTypes.prepareSrSession) {
|
||||||
prepareSrSession(sender).then(sendResponse).catch(error => sendResponse({ ok: false,
|
prepareSrSession(sender).then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||||
error: error.message, debugRequests: error.debugRequests || [] }));
|
error: error.message, debugRequests: error.debugRequests || [] }));
|
||||||
@@ -2306,6 +2489,7 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
});
|
});
|
||||||
const spaResponseShapes = createSpaResponseShapes();
|
const spaResponseShapes = createSpaResponseShapes();
|
||||||
const httpBackgroundJson = createHttpBackgroundJson({
|
const httpBackgroundJson = createHttpBackgroundJson({
|
||||||
|
observeResponseHeaders: (...args) => srRequestHeaders.observeResponseHeaders(...args),
|
||||||
previewDebugBody: createDebugBodyPreview,
|
previewDebugBody: createDebugBodyPreview,
|
||||||
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
|
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
|
||||||
environment
|
environment
|
||||||
@@ -2375,6 +2559,12 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
const requestRatecardByPlan = (...args) => spaConsumption.requestRatecardByPlan(...args);
|
const requestRatecardByPlan = (...args) => spaConsumption.requestRatecardByPlan(...args);
|
||||||
const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args);
|
const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args);
|
||||||
const runtimeApi = environment.browser || environment.chrome;
|
const runtimeApi = environment.browser || environment.chrome;
|
||||||
|
const srRequestHeaders = createSrRequestHeaders({ runtimeApi });
|
||||||
|
const fusionSrCreate = createFusionSrCreate({
|
||||||
|
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args),
|
||||||
|
tokenHeaders: (...args) => srRequestHeaders.tokenHeaders(...args),
|
||||||
|
previewTokenHeaders: url => srRequestHeaders.previewTokenHeaders(url)
|
||||||
|
});
|
||||||
const fusionSrDetails = createFusionSrDetails({
|
const fusionSrDetails = createFusionSrDetails({
|
||||||
cookieChanges: runtimeApi?.cookies?.onChanged,
|
cookieChanges: runtimeApi?.cookies?.onChanged,
|
||||||
cookiesGetAll: (...args) => platformBackgroundApi.cookiesGetAll(...args),
|
cookiesGetAll: (...args) => platformBackgroundApi.cookiesGetAll(...args),
|
||||||
@@ -2385,7 +2575,8 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args)
|
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args)
|
||||||
});
|
});
|
||||||
function start() {
|
function start() {
|
||||||
startBackgroundApplication({ prepareSrSession: sender => fusionSrDetails.prepareSession(sender), requestSrLookupWithSession: (...args) => fusionSrDetails.requestLookupWithSession(...args), messageTypes, requestSrLookupLanguage: () => fusionSrDetails.requestSrLookupLanguage(), requestSrLookupTechPlatform: () => fusionSrDetails.requestSrLookupTechPlatform(), requestSrLookupOpportunityBusiness: message => fusionSrDetails.requestSrLookupOpportunityBusiness(message), requestSrLookupPrimaryPillar: () => fusionSrDetails.requestSrLookupPrimaryPillar(), requestInternalServiceRequest: (message, sender) => fusionSrDetails.requestInternalServiceRequest(message, sender), requestMgmtAction: message => spaActionOptions.requestMgmtAction(message), invalidateProductsCache: () => productsCache.invalidate(), requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestActionTeamMembers, requestActionFindLookup, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi });
|
srRequestHeaders.start();
|
||||||
|
startBackgroundApplication({ requestPostServiceRequest: message => fusionSrCreate.requestPostServiceRequest(message), prepareSrSession: sender => fusionSrDetails.prepareSession(sender), requestSrLookupWithSession: (...args) => fusionSrDetails.requestLookupWithSession(...args), messageTypes, requestSrLookupLanguage: () => fusionSrDetails.requestSrLookupLanguage(), requestSrLookupTechPlatform: () => fusionSrDetails.requestSrLookupTechPlatform(), requestSrLookupOpportunityBusiness: message => fusionSrDetails.requestSrLookupOpportunityBusiness(message), requestSrLookupPrimaryPillar: () => fusionSrDetails.requestSrLookupPrimaryPillar(), requestInternalServiceRequest: (message, sender) => fusionSrDetails.requestInternalServiceRequest(message, sender), requestMgmtAction: message => spaActionOptions.requestMgmtAction(message), invalidateProductsCache: () => productsCache.invalidate(), requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestActionTeamMembers, requestActionFindLookup, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi });
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
start, contracts,
|
start, contracts,
|
||||||
|
|||||||
210
dist/chromium/content.js
vendored
210
dist/chromium/content.js
vendored
File diff suppressed because one or more lines are too long
4
dist/chromium/manifest.json
vendored
4
dist/chromium/manifest.json
vendored
@@ -5,7 +5,9 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"cookies",
|
"cookies",
|
||||||
"storage"
|
"storage",
|
||||||
|
"webRequest",
|
||||||
|
"declarativeNetRequestWithHostAccess"
|
||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"https://eeho.fa.us2.oraclecloud.com/*",
|
"https://eeho.fa.us2.oraclecloud.com/*",
|
||||||
|
|||||||
193
dist/firefox/background.js
vendored
193
dist/firefox/background.js
vendored
@@ -19,6 +19,7 @@ function createMessageTypes() {
|
|||||||
requestMgmtAction: "opportunitiesExtension.requestMgmtAction",
|
requestMgmtAction: "opportunitiesExtension.requestMgmtAction",
|
||||||
requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest",
|
requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest",
|
||||||
requestSrLookupPrimaryPillar: "opportunitiesExtension.requestSrLookupPrimaryPillar",
|
requestSrLookupPrimaryPillar: "opportunitiesExtension.requestSrLookupPrimaryPillar",
|
||||||
|
requestPostServiceRequest: "opportunitiesExtension.requestPostServiceRequest",
|
||||||
prepareSrSession: "opportunitiesExtension.prepareSrSession",
|
prepareSrSession: "opportunitiesExtension.prepareSrSession",
|
||||||
requestSrLookupLanguage: "opportunitiesExtension.requestSrLookupLanguage",
|
requestSrLookupLanguage: "opportunitiesExtension.requestSrLookupLanguage",
|
||||||
requestSrLookupTechPlatform: "opportunitiesExtension.requestSrLookupTechPlatform",
|
requestSrLookupTechPlatform: "opportunitiesExtension.requestSrLookupTechPlatform",
|
||||||
@@ -430,6 +431,90 @@ function createAuthSpaCredentials({
|
|||||||
return { normalizeMessageAuthorization, extractTokenServiceAuthorization };
|
return { normalizeMessageAuthorization, extractTokenServiceAuthorization };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Source: src/auth/sr-request-headers.js
|
||||||
|
function createSrRequestHeaders({ runtimeApi }) {
|
||||||
|
const origin = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com";
|
||||||
|
const appPath = "/ic/builder/rt/SeaaS_initiation/";
|
||||||
|
const storageKey = "opportunitiesExtension.srAppBuilderClientId.v1";
|
||||||
|
const maxAge = 60 * 60 * 1000;
|
||||||
|
let observed = null;
|
||||||
|
let started = false;
|
||||||
|
function valid(record) {
|
||||||
|
return record?.origin === origin && typeof record.value === "string"
|
||||||
|
&& /^[\x21-\x7e]{1,2048}$/.test(record.value)
|
||||||
|
&& Number.isFinite(record.observedAt) && record.observedAt <= Date.now()
|
||||||
|
&& Date.now() - record.observedAt < maxAge;
|
||||||
|
}
|
||||||
|
function observeHeaders(rawUrl, headers) {
|
||||||
|
let url;
|
||||||
|
try { url = new URL(rawUrl); } catch { return; }
|
||||||
|
if (url.origin !== origin || !/^\/ic\/builder\/rt\/SeaaS_initiation\/live(?:\/|;)/.test(url.pathname)) return;
|
||||||
|
// Read only the client ID, not cookies, bodies or authorization.
|
||||||
|
const value = typeof headers?.get === "function" ? headers.get("x-appbuilder-client-id")
|
||||||
|
: headers?.find(item => item.name.toLowerCase() === "x-appbuilder-client-id")?.value;
|
||||||
|
const record = { origin, value: value?.trim(), observedAt: Date.now() };
|
||||||
|
if (!valid(record)) return;
|
||||||
|
observed = record;
|
||||||
|
// Session storage survives service-worker suspension without persisting across browser sessions.
|
||||||
|
runtimeApi.storage?.session?.set({ [storageKey]: record }).catch(() => {});
|
||||||
|
}
|
||||||
|
function start() {
|
||||||
|
if (started || !runtimeApi?.webRequest) return;
|
||||||
|
started = true;
|
||||||
|
const filter = { urls: [`${origin}${appPath}*`], types: ["xmlhttprequest"] };
|
||||||
|
runtimeApi.webRequest.onBeforeSendHeaders?.addListener(details => {
|
||||||
|
if (details.tabId >= 0 && !details.incognito) observeHeaders(details.url, details.requestHeaders);
|
||||||
|
}, filter, ["requestHeaders"]);
|
||||||
|
runtimeApi.webRequest.onHeadersReceived?.addListener(details => {
|
||||||
|
if (!details.incognito) observeHeaders(details.url, details.responseHeaders);
|
||||||
|
}, filter, ["responseHeaders"]);
|
||||||
|
}
|
||||||
|
async function previewTokenHeaders(url) {
|
||||||
|
const target = new URL(url);
|
||||||
|
if (target.origin !== origin || !target.pathname.startsWith(appPath) || !/\/Catalogue_c(?:\/\d+)?$/.test(target.pathname)) {
|
||||||
|
throw new Error("Unexpected Service Request token URL.");
|
||||||
|
}
|
||||||
|
if (!valid(observed)) {
|
||||||
|
try {
|
||||||
|
const stored = await runtimeApi.storage?.session?.get(storageKey);
|
||||||
|
if (valid(stored?.[storageKey]) && !valid(observed)) observed = stored[storageKey];
|
||||||
|
} catch { /* A missing capture must not prevent trying the other required headers. */ }
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
Accept: "application/json", "Content-Type": "application/vnd.oracle.adf.action+json",
|
||||||
|
Authorization: "Session", Origin: target.origin,
|
||||||
|
...(valid(observed) ? { "x-appbuilder-client-id": observed.value } : {})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async function tokenHeaders(url, serviceRequestUrl) {
|
||||||
|
const headers = await previewTokenHeaders(url);
|
||||||
|
if (!/\/Catalogue_c\/\d+$/.test(new URL(url).pathname)) throw new Error("Missing Catalogue Id.");
|
||||||
|
const serviceTarget = new URL(serviceRequestUrl);
|
||||||
|
if (serviceTarget.origin !== headers.Origin || !serviceTarget.pathname.startsWith(appPath)
|
||||||
|
|| !serviceTarget.pathname.endsWith("/set/seaas/mgmt/serviceRequests") || serviceTarget.search || serviceTarget.hash) {
|
||||||
|
throw new Error("Unexpected Service Request URL.");
|
||||||
|
}
|
||||||
|
if (!runtimeApi?.declarativeNetRequest?.updateSessionRules || !runtimeApi.runtime?.getURL) {
|
||||||
|
throw new Error("Reload the extension to enable the Service Request token header permissions.");
|
||||||
|
}
|
||||||
|
const extensionDomain = new URL(runtimeApi.runtime.getURL("/")).hostname;
|
||||||
|
const catalogUrl = url.slice(0, url.lastIndexOf("/") + 1);
|
||||||
|
const escapedCatalogUrl = catalogUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
const escapedServiceUrl = serviceTarget.href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
await runtimeApi.declarativeNetRequest.updateSessionRules({
|
||||||
|
removeRuleIds: [7201, 7202],
|
||||||
|
addRules: [`^${escapedCatalogUrl}[0-9]+$`, `^${escapedServiceUrl}$`].map((regexFilter, index) => ({
|
||||||
|
id: 7201 + index, priority: 1,
|
||||||
|
action: { type: "modifyHeaders", requestHeaders: [{ header: "Origin", operation: "set", value: headers.Origin }] },
|
||||||
|
condition: { regexFilter, initiatorDomains: [extensionDomain],
|
||||||
|
requestMethods: ["post"], resourceTypes: ["xmlhttprequest"] }
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
return { start, tokenHeaders, previewTokenHeaders, observeResponseHeaders: observeHeaders };
|
||||||
|
}
|
||||||
|
|
||||||
// Source: src/services/spa/accounts.js
|
// Source: src/services/spa/accounts.js
|
||||||
function createSpaAccounts({
|
function createSpaAccounts({
|
||||||
ACCOUNT_PLANS_PAYLOAD,
|
ACCOUNT_PLANS_PAYLOAD,
|
||||||
@@ -951,6 +1036,96 @@ function createFusionSrDetails({ fetchJson, tabsCreate, tabsRemove, supportsFire
|
|||||||
requestSrLookupTechPlatform: () => requestLookup("SENGG_TECH_PLATFORM", "requestSrLookupTechPlatform") };
|
requestSrLookupTechPlatform: () => requestLookup("SENGG_TECH_PLATFORM", "requestSrLookupTechPlatform") };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Source: src/services/fusion/sr-create.js
|
||||||
|
function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders }) {
|
||||||
|
const base = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live;profile=PROD/services/auth/1.1/proxy/";
|
||||||
|
const catalog = base + "crmRestApiResources/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/11.13.18.05/Catalogue_c";
|
||||||
|
const resourceUsers = base + "oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/resourceUsers";
|
||||||
|
const endpoint = base + "oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/mgmt/serviceRequests";
|
||||||
|
function numericId(value, name) {
|
||||||
|
if (!/^\d+$/.test(String(value ?? "")) || !Number.isSafeInteger(Number(value)) || Number(value) <= 0) throw new Error(`Missing or invalid ${name}.`);
|
||||||
|
return Number(value);
|
||||||
|
}
|
||||||
|
function text(value, name) {
|
||||||
|
if (typeof value !== "string" || !value.trim()) throw new Error(`Missing ${name}.`);
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
function payloadFor(input) {
|
||||||
|
const payload = {};
|
||||||
|
for (const name of ["AccountPartyId", "OpportunityName_Id_c", "ServiceNew_Id_c"]) payload[name] = numericId(input?.[name], name);
|
||||||
|
for (const name of ["Title", "RegionText_c", "ProblemDescription", "Language_c", "CustomerDeadline_c", "TechPlatformLookUp_c", "CountryText_c", "PrimaryPillarNew_c"]) payload[name] = text(input?.[name], name);
|
||||||
|
const date = payload.CustomerDeadline_c;
|
||||||
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(Date.parse(`${date}T00:00:00Z`))
|
||||||
|
|| new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) !== date) throw new Error("Invalid Customer deadline.");
|
||||||
|
return { ...payload, SvcQuestionsCollection_c: [], SourceCd: "SVC_OPPORTUNITY_SCREEN", SeverityCd: "ORA_SVC_SEV2", ChannelTypeCd: "ORA_SVC_WEB" };
|
||||||
|
}
|
||||||
|
async function requestPostServiceRequest(message) {
|
||||||
|
const debugRequests = [];
|
||||||
|
try {
|
||||||
|
if (message.phase !== "prepare" && message.phase !== "confirm") throw new Error("Service Request confirmation is required.");
|
||||||
|
const payload = payloadFor(message.payload);
|
||||||
|
if (message.phase === "prepare") {
|
||||||
|
const emailAddress = text(message.emailAddress, "current user email");
|
||||||
|
const resource = await fetchJson(`${resourceUsers}?emailAddress=${encodeURIComponent(emailAddress)}`, {
|
||||||
|
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
|
||||||
|
}, "requestResourceUser", debugRequests);
|
||||||
|
payload.ReportedByPartyId = numericId(resource?.resourcePartyId, "resourcePartyId");
|
||||||
|
const sharedHeaders = await previewTokenHeaders(catalog);
|
||||||
|
return { ok: true, payload, debugRequests, headers: {
|
||||||
|
requestSrTokenCatalog: sharedHeaders,
|
||||||
|
requestPostServiceRequest: { ...sharedHeaders, "vb-proxy-header-x-oracle-ectoken": "(generated after confirmation)" }
|
||||||
|
} };
|
||||||
|
}
|
||||||
|
payload.ReportedByPartyId = numericId(message.payload?.ReportedByPartyId, "ReportedByPartyId");
|
||||||
|
let id = message.catalogId;
|
||||||
|
if (id == null) {
|
||||||
|
const result = await fetchJson(`${catalog}?onlyData=true&fields=Id&limit=1`, {
|
||||||
|
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
|
||||||
|
}, "requestSrCatalog", debugRequests);
|
||||||
|
id = result?.items?.[0]?.Id;
|
||||||
|
}
|
||||||
|
id = numericId(id, "Catalogue Id");
|
||||||
|
const tokenUrl = `${catalog}/${id}`;
|
||||||
|
let catalogHeaders = await tokenHeaders(tokenUrl, endpoint);
|
||||||
|
const requestToken = () => fetchJson(tokenUrl, {
|
||||||
|
method: "POST", credentials: "include", headers: catalogHeaders, body: JSON.stringify({ name: "getJwtToken" })
|
||||||
|
}, "requestSrTokenCatalog", debugRequests);
|
||||||
|
let token;
|
||||||
|
try {
|
||||||
|
token = await requestToken();
|
||||||
|
} catch (error) {
|
||||||
|
if (error.httpStatus !== 403) throw error;
|
||||||
|
const refreshed = await previewTokenHeaders(tokenUrl);
|
||||||
|
if (!refreshed["x-appbuilder-client-id"] || refreshed["x-appbuilder-client-id"] === catalogHeaders["x-appbuilder-client-id"]) throw error;
|
||||||
|
catalogHeaders = refreshed;
|
||||||
|
token = await requestToken();
|
||||||
|
}
|
||||||
|
if (debugRequests.at(-1)?.httpStatus !== 200 || typeof token?.result !== "string" || !token.result.trim()) {
|
||||||
|
throw new Error("requestSrTokenCatalog did not return HTTP 200 with a token.");
|
||||||
|
}
|
||||||
|
const result = await fetchJson(endpoint, {
|
||||||
|
method: "POST", credentials: "include", headers: { ...catalogHeaders, "vb-proxy-header-x-oracle-ectoken": token.result.trim() },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
}, "requestPostServiceRequest", debugRequests, { allowEmpty: true });
|
||||||
|
return { ok: true, result, debugRequests };
|
||||||
|
} catch (error) {
|
||||||
|
error.debugRequests = debugRequests;
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// Never forward the signing token to content scripts, even with Debug secrets enabled.
|
||||||
|
for (const entry of debugRequests) {
|
||||||
|
if (entry.name === "requestSrTokenCatalog") {
|
||||||
|
entry.metadata.appBuilderClientId = entry.requestHeaders?.["x-appbuilder-client-id"] ? "Observed in SeaaS requests" : "Not observed in SeaaS requests";
|
||||||
|
entry.responseBody = JSON.stringify({ result: "(redacted)" });
|
||||||
|
entry.responseBodyTruncated = false;
|
||||||
|
}
|
||||||
|
if (entry.requestHeaders?.["vb-proxy-header-x-oracle-ectoken"]) entry.requestHeaders["vb-proxy-header-x-oracle-ectoken"] = "(redacted)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { requestPostServiceRequest };
|
||||||
|
}
|
||||||
|
|
||||||
// Source: src/services/spa/current-user.js
|
// Source: src/services/spa/current-user.js
|
||||||
function createSpaCurrentUser({ fetchJson }) {
|
function createSpaCurrentUser({ fetchJson }) {
|
||||||
async function requestCurrentUser() {
|
async function requestCurrentUser() {
|
||||||
@@ -1156,6 +1331,7 @@ function createSpaResponseShapes() {
|
|||||||
function createHttpBackgroundJson({
|
function createHttpBackgroundJson({
|
||||||
previewDebugBody,
|
previewDebugBody,
|
||||||
DEBUG_BODY_LIMIT,
|
DEBUG_BODY_LIMIT,
|
||||||
|
observeResponseHeaders = () => {},
|
||||||
environment
|
environment
|
||||||
}) {
|
}) {
|
||||||
const { fetch } = environment;
|
const { fetch } = environment;
|
||||||
@@ -1172,6 +1348,7 @@ function createHttpBackgroundJson({
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
observeResponseHeaders(response.url || url, response.headers);
|
||||||
const responsePayload = await readResponsePayload(response);
|
const responsePayload = await readResponsePayload(response);
|
||||||
const responseBody = createDebugBody(responsePayload.text);
|
const responseBody = createDebugBody(responsePayload.text);
|
||||||
completeBackgroundDebugEntry(debugEntry, response, responseBody, "");
|
completeBackgroundDebugEntry(debugEntry, response, responseBody, "");
|
||||||
@@ -2049,6 +2226,7 @@ function registerBackgroundMessages({
|
|||||||
requestActionTeamMembers,
|
requestActionTeamMembers,
|
||||||
requestActionFindLookup,
|
requestActionFindLookup,
|
||||||
requestMgmtAction,
|
requestMgmtAction,
|
||||||
|
requestPostServiceRequest,
|
||||||
requestInternalServiceRequest,
|
requestInternalServiceRequest,
|
||||||
requestSrLookupPrimaryPillar,
|
requestSrLookupPrimaryPillar,
|
||||||
requestSrLookupWithSession,
|
requestSrLookupWithSession,
|
||||||
@@ -2068,6 +2246,11 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
if (!message || !message.type) {
|
if (!message || !message.type) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (message.type === messageTypes.requestPostServiceRequest) {
|
||||||
|
requestPostServiceRequest(message).then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||||
|
error: error.message, debugRequests: error.debugRequests || [] }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (message.type === messageTypes.prepareSrSession) {
|
if (message.type === messageTypes.prepareSrSession) {
|
||||||
prepareSrSession(sender).then(sendResponse).catch(error => sendResponse({ ok: false,
|
prepareSrSession(sender).then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||||
error: error.message, debugRequests: error.debugRequests || [] }));
|
error: error.message, debugRequests: error.debugRequests || [] }));
|
||||||
@@ -2306,6 +2489,7 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
});
|
});
|
||||||
const spaResponseShapes = createSpaResponseShapes();
|
const spaResponseShapes = createSpaResponseShapes();
|
||||||
const httpBackgroundJson = createHttpBackgroundJson({
|
const httpBackgroundJson = createHttpBackgroundJson({
|
||||||
|
observeResponseHeaders: (...args) => srRequestHeaders.observeResponseHeaders(...args),
|
||||||
previewDebugBody: createDebugBodyPreview,
|
previewDebugBody: createDebugBodyPreview,
|
||||||
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
|
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
|
||||||
environment
|
environment
|
||||||
@@ -2375,6 +2559,12 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
const requestRatecardByPlan = (...args) => spaConsumption.requestRatecardByPlan(...args);
|
const requestRatecardByPlan = (...args) => spaConsumption.requestRatecardByPlan(...args);
|
||||||
const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args);
|
const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args);
|
||||||
const runtimeApi = environment.browser || environment.chrome;
|
const runtimeApi = environment.browser || environment.chrome;
|
||||||
|
const srRequestHeaders = createSrRequestHeaders({ runtimeApi });
|
||||||
|
const fusionSrCreate = createFusionSrCreate({
|
||||||
|
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args),
|
||||||
|
tokenHeaders: (...args) => srRequestHeaders.tokenHeaders(...args),
|
||||||
|
previewTokenHeaders: url => srRequestHeaders.previewTokenHeaders(url)
|
||||||
|
});
|
||||||
const fusionSrDetails = createFusionSrDetails({
|
const fusionSrDetails = createFusionSrDetails({
|
||||||
cookieChanges: runtimeApi?.cookies?.onChanged,
|
cookieChanges: runtimeApi?.cookies?.onChanged,
|
||||||
cookiesGetAll: (...args) => platformBackgroundApi.cookiesGetAll(...args),
|
cookiesGetAll: (...args) => platformBackgroundApi.cookiesGetAll(...args),
|
||||||
@@ -2385,7 +2575,8 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args)
|
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args)
|
||||||
});
|
});
|
||||||
function start() {
|
function start() {
|
||||||
startBackgroundApplication({ prepareSrSession: sender => fusionSrDetails.prepareSession(sender), requestSrLookupWithSession: (...args) => fusionSrDetails.requestLookupWithSession(...args), messageTypes, requestSrLookupLanguage: () => fusionSrDetails.requestSrLookupLanguage(), requestSrLookupTechPlatform: () => fusionSrDetails.requestSrLookupTechPlatform(), requestSrLookupOpportunityBusiness: message => fusionSrDetails.requestSrLookupOpportunityBusiness(message), requestSrLookupPrimaryPillar: () => fusionSrDetails.requestSrLookupPrimaryPillar(), requestInternalServiceRequest: (message, sender) => fusionSrDetails.requestInternalServiceRequest(message, sender), requestMgmtAction: message => spaActionOptions.requestMgmtAction(message), invalidateProductsCache: () => productsCache.invalidate(), requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestActionTeamMembers, requestActionFindLookup, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi });
|
srRequestHeaders.start();
|
||||||
|
startBackgroundApplication({ requestPostServiceRequest: message => fusionSrCreate.requestPostServiceRequest(message), prepareSrSession: sender => fusionSrDetails.prepareSession(sender), requestSrLookupWithSession: (...args) => fusionSrDetails.requestLookupWithSession(...args), messageTypes, requestSrLookupLanguage: () => fusionSrDetails.requestSrLookupLanguage(), requestSrLookupTechPlatform: () => fusionSrDetails.requestSrLookupTechPlatform(), requestSrLookupOpportunityBusiness: message => fusionSrDetails.requestSrLookupOpportunityBusiness(message), requestSrLookupPrimaryPillar: () => fusionSrDetails.requestSrLookupPrimaryPillar(), requestInternalServiceRequest: (message, sender) => fusionSrDetails.requestInternalServiceRequest(message, sender), requestMgmtAction: message => spaActionOptions.requestMgmtAction(message), invalidateProductsCache: () => productsCache.invalidate(), requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestActionTeamMembers, requestActionFindLookup, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi });
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
start, contracts,
|
start, contracts,
|
||||||
|
|||||||
210
dist/firefox/content.js
vendored
210
dist/firefox/content.js
vendored
File diff suppressed because one or more lines are too long
4
dist/firefox/manifest.json
vendored
4
dist/firefox/manifest.json
vendored
@@ -5,7 +5,9 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"cookies",
|
"cookies",
|
||||||
"storage"
|
"storage",
|
||||||
|
"webRequest",
|
||||||
|
"declarativeNetRequestWithHostAccess"
|
||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"https://eeho.fa.us2.oraclecloud.com/*",
|
"https://eeho.fa.us2.oraclecloud.com/*",
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ const baseManifest = {
|
|||||||
version: "1.0.2",
|
version: "1.0.2",
|
||||||
permissions: [
|
permissions: [
|
||||||
"cookies",
|
"cookies",
|
||||||
"storage"
|
"storage",
|
||||||
|
"webRequest",
|
||||||
|
"declarativeNetRequestWithHostAccess"
|
||||||
],
|
],
|
||||||
host_permissions: [
|
host_permissions: [
|
||||||
"https://eeho.fa.us2.oraclecloud.com/*",
|
"https://eeho.fa.us2.oraclecloud.com/*",
|
||||||
|
|||||||
@@ -85,12 +85,14 @@ module.exports = {
|
|||||||
"src/features/products/cache-state.js",
|
"src/features/products/cache-state.js",
|
||||||
"src/auth/consumer-session.js",
|
"src/auth/consumer-session.js",
|
||||||
"src/auth/spa-credentials.js",
|
"src/auth/spa-credentials.js",
|
||||||
|
"src/auth/sr-request-headers.js",
|
||||||
"src/services/spa/accounts.js",
|
"src/services/spa/accounts.js",
|
||||||
"src/services/spa/install-base.js",
|
"src/services/spa/install-base.js",
|
||||||
"src/services/spa/customer-service-requests.js",
|
"src/services/spa/customer-service-requests.js",
|
||||||
"src/services/spa/workload-actions.js",
|
"src/services/spa/workload-actions.js",
|
||||||
"src/services/spa/action-options.js",
|
"src/services/spa/action-options.js",
|
||||||
"src/services/fusion/sr-details.js",
|
"src/services/fusion/sr-details.js",
|
||||||
|
"src/services/fusion/sr-create.js",
|
||||||
"src/services/spa/current-user.js",
|
"src/services/spa/current-user.js",
|
||||||
"src/services/spa/consumption.js",
|
"src/services/spa/consumption.js",
|
||||||
"src/services/spa/response-shapes.js",
|
"src/services/spa/response-shapes.js",
|
||||||
|
|||||||
82
src/auth/sr-request-headers.js
Normal file
82
src/auth/sr-request-headers.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
function createSrRequestHeaders({ runtimeApi }) {
|
||||||
|
const origin = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com";
|
||||||
|
const appPath = "/ic/builder/rt/SeaaS_initiation/";
|
||||||
|
const storageKey = "opportunitiesExtension.srAppBuilderClientId.v1";
|
||||||
|
const maxAge = 60 * 60 * 1000;
|
||||||
|
let observed = null;
|
||||||
|
let started = false;
|
||||||
|
function valid(record) {
|
||||||
|
return record?.origin === origin && typeof record.value === "string"
|
||||||
|
&& /^[\x21-\x7e]{1,2048}$/.test(record.value)
|
||||||
|
&& Number.isFinite(record.observedAt) && record.observedAt <= Date.now()
|
||||||
|
&& Date.now() - record.observedAt < maxAge;
|
||||||
|
}
|
||||||
|
function observeHeaders(rawUrl, headers) {
|
||||||
|
let url;
|
||||||
|
try { url = new URL(rawUrl); } catch { return; }
|
||||||
|
if (url.origin !== origin || !/^\/ic\/builder\/rt\/SeaaS_initiation\/live(?:\/|;)/.test(url.pathname)) return;
|
||||||
|
// Read only the client ID, not cookies, bodies or authorization.
|
||||||
|
const value = typeof headers?.get === "function" ? headers.get("x-appbuilder-client-id")
|
||||||
|
: headers?.find(item => item.name.toLowerCase() === "x-appbuilder-client-id")?.value;
|
||||||
|
const record = { origin, value: value?.trim(), observedAt: Date.now() };
|
||||||
|
if (!valid(record)) return;
|
||||||
|
observed = record;
|
||||||
|
// Session storage survives service-worker suspension without persisting across browser sessions.
|
||||||
|
runtimeApi.storage?.session?.set({ [storageKey]: record }).catch(() => {});
|
||||||
|
}
|
||||||
|
function start() {
|
||||||
|
if (started || !runtimeApi?.webRequest) return;
|
||||||
|
started = true;
|
||||||
|
const filter = { urls: [`${origin}${appPath}*`], types: ["xmlhttprequest"] };
|
||||||
|
runtimeApi.webRequest.onBeforeSendHeaders?.addListener(details => {
|
||||||
|
if (details.tabId >= 0 && !details.incognito) observeHeaders(details.url, details.requestHeaders);
|
||||||
|
}, filter, ["requestHeaders"]);
|
||||||
|
runtimeApi.webRequest.onHeadersReceived?.addListener(details => {
|
||||||
|
if (!details.incognito) observeHeaders(details.url, details.responseHeaders);
|
||||||
|
}, filter, ["responseHeaders"]);
|
||||||
|
}
|
||||||
|
async function previewTokenHeaders(url) {
|
||||||
|
const target = new URL(url);
|
||||||
|
if (target.origin !== origin || !target.pathname.startsWith(appPath) || !/\/Catalogue_c(?:\/\d+)?$/.test(target.pathname)) {
|
||||||
|
throw new Error("Unexpected Service Request token URL.");
|
||||||
|
}
|
||||||
|
if (!valid(observed)) {
|
||||||
|
try {
|
||||||
|
const stored = await runtimeApi.storage?.session?.get(storageKey);
|
||||||
|
if (valid(stored?.[storageKey]) && !valid(observed)) observed = stored[storageKey];
|
||||||
|
} catch { /* A missing capture must not prevent trying the other required headers. */ }
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
Accept: "application/json", "Content-Type": "application/vnd.oracle.adf.action+json",
|
||||||
|
Authorization: "Session", Origin: target.origin,
|
||||||
|
...(valid(observed) ? { "x-appbuilder-client-id": observed.value } : {})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async function tokenHeaders(url, serviceRequestUrl) {
|
||||||
|
const headers = await previewTokenHeaders(url);
|
||||||
|
if (!/\/Catalogue_c\/\d+$/.test(new URL(url).pathname)) throw new Error("Missing Catalogue Id.");
|
||||||
|
const serviceTarget = new URL(serviceRequestUrl);
|
||||||
|
if (serviceTarget.origin !== headers.Origin || !serviceTarget.pathname.startsWith(appPath)
|
||||||
|
|| !serviceTarget.pathname.endsWith("/set/seaas/mgmt/serviceRequests") || serviceTarget.search || serviceTarget.hash) {
|
||||||
|
throw new Error("Unexpected Service Request URL.");
|
||||||
|
}
|
||||||
|
if (!runtimeApi?.declarativeNetRequest?.updateSessionRules || !runtimeApi.runtime?.getURL) {
|
||||||
|
throw new Error("Reload the extension to enable the Service Request token header permissions.");
|
||||||
|
}
|
||||||
|
const extensionDomain = new URL(runtimeApi.runtime.getURL("/")).hostname;
|
||||||
|
const catalogUrl = url.slice(0, url.lastIndexOf("/") + 1);
|
||||||
|
const escapedCatalogUrl = catalogUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
const escapedServiceUrl = serviceTarget.href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
await runtimeApi.declarativeNetRequest.updateSessionRules({
|
||||||
|
removeRuleIds: [7201, 7202],
|
||||||
|
addRules: [`^${escapedCatalogUrl}[0-9]+$`, `^${escapedServiceUrl}$`].map((regexFilter, index) => ({
|
||||||
|
id: 7201 + index, priority: 1,
|
||||||
|
action: { type: "modifyHeaders", requestHeaders: [{ header: "Origin", operation: "set", value: headers.Origin }] },
|
||||||
|
condition: { regexFilter, initiatorDomains: [extensionDomain],
|
||||||
|
requestMethods: ["post"], resourceTypes: ["xmlhttprequest"] }
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
return { start, tokenHeaders, previewTokenHeaders, observeResponseHeaders: observeHeaders };
|
||||||
|
}
|
||||||
@@ -56,6 +56,7 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
});
|
});
|
||||||
const spaResponseShapes = createSpaResponseShapes();
|
const spaResponseShapes = createSpaResponseShapes();
|
||||||
const httpBackgroundJson = createHttpBackgroundJson({
|
const httpBackgroundJson = createHttpBackgroundJson({
|
||||||
|
observeResponseHeaders: (...args) => srRequestHeaders.observeResponseHeaders(...args),
|
||||||
previewDebugBody: createDebugBodyPreview,
|
previewDebugBody: createDebugBodyPreview,
|
||||||
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
|
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
|
||||||
environment
|
environment
|
||||||
@@ -125,6 +126,12 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
const requestRatecardByPlan = (...args) => spaConsumption.requestRatecardByPlan(...args);
|
const requestRatecardByPlan = (...args) => spaConsumption.requestRatecardByPlan(...args);
|
||||||
const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args);
|
const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args);
|
||||||
const runtimeApi = environment.browser || environment.chrome;
|
const runtimeApi = environment.browser || environment.chrome;
|
||||||
|
const srRequestHeaders = createSrRequestHeaders({ runtimeApi });
|
||||||
|
const fusionSrCreate = createFusionSrCreate({
|
||||||
|
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args),
|
||||||
|
tokenHeaders: (...args) => srRequestHeaders.tokenHeaders(...args),
|
||||||
|
previewTokenHeaders: url => srRequestHeaders.previewTokenHeaders(url)
|
||||||
|
});
|
||||||
const fusionSrDetails = createFusionSrDetails({
|
const fusionSrDetails = createFusionSrDetails({
|
||||||
cookieChanges: runtimeApi?.cookies?.onChanged,
|
cookieChanges: runtimeApi?.cookies?.onChanged,
|
||||||
cookiesGetAll: (...args) => platformBackgroundApi.cookiesGetAll(...args),
|
cookiesGetAll: (...args) => platformBackgroundApi.cookiesGetAll(...args),
|
||||||
@@ -135,7 +142,8 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args)
|
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args)
|
||||||
});
|
});
|
||||||
function start() {
|
function start() {
|
||||||
startBackgroundApplication({ prepareSrSession: sender => fusionSrDetails.prepareSession(sender), requestSrLookupWithSession: (...args) => fusionSrDetails.requestLookupWithSession(...args), messageTypes, requestSrLookupLanguage: () => fusionSrDetails.requestSrLookupLanguage(), requestSrLookupTechPlatform: () => fusionSrDetails.requestSrLookupTechPlatform(), requestSrLookupOpportunityBusiness: message => fusionSrDetails.requestSrLookupOpportunityBusiness(message), requestSrLookupPrimaryPillar: () => fusionSrDetails.requestSrLookupPrimaryPillar(), requestInternalServiceRequest: (message, sender) => fusionSrDetails.requestInternalServiceRequest(message, sender), requestMgmtAction: message => spaActionOptions.requestMgmtAction(message), invalidateProductsCache: () => productsCache.invalidate(), requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestActionTeamMembers, requestActionFindLookup, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi });
|
srRequestHeaders.start();
|
||||||
|
startBackgroundApplication({ requestPostServiceRequest: message => fusionSrCreate.requestPostServiceRequest(message), prepareSrSession: sender => fusionSrDetails.prepareSession(sender), requestSrLookupWithSession: (...args) => fusionSrDetails.requestLookupWithSession(...args), messageTypes, requestSrLookupLanguage: () => fusionSrDetails.requestSrLookupLanguage(), requestSrLookupTechPlatform: () => fusionSrDetails.requestSrLookupTechPlatform(), requestSrLookupOpportunityBusiness: message => fusionSrDetails.requestSrLookupOpportunityBusiness(message), requestSrLookupPrimaryPillar: () => fusionSrDetails.requestSrLookupPrimaryPillar(), requestInternalServiceRequest: (message, sender) => fusionSrDetails.requestInternalServiceRequest(message, sender), requestMgmtAction: message => spaActionOptions.requestMgmtAction(message), invalidateProductsCache: () => productsCache.invalidate(), requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestActionTeamMembers, requestActionFindLookup, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi });
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
start, contracts,
|
start, contracts,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ function registerBackgroundMessages({
|
|||||||
requestActionTeamMembers,
|
requestActionTeamMembers,
|
||||||
requestActionFindLookup,
|
requestActionFindLookup,
|
||||||
requestMgmtAction,
|
requestMgmtAction,
|
||||||
|
requestPostServiceRequest,
|
||||||
requestInternalServiceRequest,
|
requestInternalServiceRequest,
|
||||||
requestSrLookupPrimaryPillar,
|
requestSrLookupPrimaryPillar,
|
||||||
requestSrLookupWithSession,
|
requestSrLookupWithSession,
|
||||||
@@ -30,6 +31,11 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
if (!message || !message.type) {
|
if (!message || !message.type) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (message.type === messageTypes.requestPostServiceRequest) {
|
||||||
|
requestPostServiceRequest(message).then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||||
|
error: error.message, debugRequests: error.debugRequests || [] }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (message.type === messageTypes.prepareSrSession) {
|
if (message.type === messageTypes.prepareSrSession) {
|
||||||
prepareSrSession(sender).then(sendResponse).catch(error => sendResponse({ ok: false,
|
prepareSrSession(sender).then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||||
error: error.message, debugRequests: error.debugRequests || [] }));
|
error: error.message, debugRequests: error.debugRequests || [] }));
|
||||||
|
|||||||
@@ -240,6 +240,13 @@ function createContentApplication(overrides = {}) {
|
|||||||
return result.items;
|
return result.items;
|
||||||
}
|
}
|
||||||
const crmSrForm = createCrmSrForm({
|
const crmSrForm = createCrmSrForm({
|
||||||
|
requestSave: async fields => {
|
||||||
|
const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestPostServiceRequest, ...fields, emailAddress: currentUser.getUserId() });
|
||||||
|
debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []);
|
||||||
|
if (!result?.ok) throw new Error(result?.error || "Unable to save Service Request.");
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
notify: (...args) => opportunitiesLinks.showCopyConfirmation(...args),
|
||||||
getOpportunity: number => opportunitiesTableState.opportunitiesTableState.items.find(item => String(item.OptyNumber) === String(number)),
|
getOpportunity: number => opportunitiesTableState.opportunitiesTableState.items.find(item => String(item.OptyNumber) === String(number)),
|
||||||
formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value),
|
formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value),
|
||||||
requestLanguage: () => requestSrFormLookup(messageTypes.requestSrLookupLanguage),
|
requestLanguage: () => requestSrFormLookup(messageTypes.requestSrLookupLanguage),
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function createDebugContentView({
|
|||||||
if (["requestWlActions", "requestActionTeamMembers", "requestActionFindLookup", "requestMgmtAction"].includes(entry.name)) return "actions";
|
if (["requestWlActions", "requestActionTeamMembers", "requestActionFindLookup", "requestMgmtAction"].includes(entry.name)) return "actions";
|
||||||
if (entry.name === "requestGenerateAiSummary") return "ai";
|
if (entry.name === "requestGenerateAiSummary") return "ai";
|
||||||
if (entry.name === "requestInstallBase") return "installBase";
|
if (entry.name === "requestInstallBase") return "installBase";
|
||||||
if (["requestCustomerServiceRequest", "resquestCrmServiceRequest", "requestInternalServiceRequest", "requestSrLookupPrimaryPillar", "requestSrLookupLanguage", "requestSrLookupTechPlatform", "requestSrLookupOpportunityBusiness"].includes(entry.name)) return "serviceRequests";
|
if (["requestCustomerServiceRequest", "resquestCrmServiceRequest", "requestInternalServiceRequest", "requestSrLookupPrimaryPillar", "requestSrLookupLanguage", "requestSrLookupTechPlatform", "requestSrLookupOpportunityBusiness", "requestResourceUser", "requestSrCatalog", "requestSrTokenCatalog", "requestPostServiceRequest"].includes(entry.name)) return "serviceRequests";
|
||||||
if (/^request(?:AccountSummary|AccountPlans|ConsumptionService|ConsumptionPlanDetail|RatecardByPlan)/.test(entry.name)) return "consumption";
|
if (/^request(?:AccountSummary|AccountPlans|ConsumptionService|ConsumptionPlanDetail|RatecardByPlan)/.test(entry.name)) return "consumption";
|
||||||
return "other";
|
return "other";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportunity, formatDate, requestPillars, requestLanguage, requestPlatform, requestServices, createCloseIcon, searchableSelect, environment }) {
|
function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportunity, requestSave, notify, formatDate, requestPillars, requestLanguage, requestPlatform, requestServices, createCloseIcon, searchableSelect, environment }) {
|
||||||
const { document } = environment;
|
const { document } = environment;
|
||||||
let active = null;
|
let active = null;
|
||||||
function node(tag, className, text) {
|
function node(tag, className, text) {
|
||||||
@@ -9,6 +9,7 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
|||||||
}
|
}
|
||||||
function close(restoreFocus = true) {
|
function close(restoreFocus = true) {
|
||||||
if (!active) return false;
|
if (!active) return false;
|
||||||
|
if (active.saving) return true;
|
||||||
const previous = active;
|
const previous = active;
|
||||||
active = null;
|
active = null;
|
||||||
previous.modal.remove();
|
previous.modal.remove();
|
||||||
@@ -25,6 +26,7 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
|||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
function open(number, trigger) {
|
function open(number, trigger) {
|
||||||
|
if (active?.saving) return;
|
||||||
const host = document.getElementById(MODAL_ID);
|
const host = document.getElementById(MODAL_ID);
|
||||||
if (!host || host.hidden || !allowsCrmActivities() || !isMissing(number)) return;
|
if (!host || host.hidden || !allowsCrmActivities() || !isMissing(number)) return;
|
||||||
close(false);
|
close(false);
|
||||||
@@ -55,23 +57,36 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
|||||||
dismiss.addEventListener("click", () => close());
|
dismiss.addEventListener("click", () => close());
|
||||||
header.append(heading, dismiss);
|
header.append(heading, dismiss);
|
||||||
const form = node("form", "opportunities-extension-action-form");
|
const form = node("form", "opportunities-extension-action-form");
|
||||||
form.addEventListener("submit", event => event.preventDefault());
|
form.noValidate = true;
|
||||||
|
const fields = new Map();
|
||||||
|
const combos = new Map();
|
||||||
|
const loadingFields = new Set(["primaryPillar", "language", "platform", "services"]);
|
||||||
|
const availableFields = new Set();
|
||||||
|
let serviceItems = [];
|
||||||
|
const serviceRadios = [];
|
||||||
|
let serviceGroup;
|
||||||
const field = node("label", "opportunities-extension-action-form-field");
|
const field = node("label", "opportunities-extension-action-form-field");
|
||||||
const select = node("select");
|
const select = node("select");
|
||||||
select.id = "opportunities-extension-sr-primary-pillar";
|
select.id = "opportunities-extension-sr-primary-pillar";
|
||||||
select.name = "primaryPillar";
|
select.name = "primaryPillar";
|
||||||
|
select.required = true;
|
||||||
field.htmlFor = select.id;
|
field.htmlFor = select.id;
|
||||||
field.append(node("span", "", "Primary pillar"), select);
|
field.append(node("span", "", "Primary pillar (required)"), select);
|
||||||
const autocomplete = searchableSelect(select, field);
|
const autocomplete = searchableSelect(select, field);
|
||||||
|
fields.set("primaryPillar", select);
|
||||||
|
combos.set(select, autocomplete);
|
||||||
const status = node("p", "opportunities-extension-action-form-status opportunities-extension-action-form-wide");
|
const status = node("p", "opportunities-extension-action-form-status opportunities-extension-action-form-wide");
|
||||||
status.setAttribute("role", "status");
|
status.setAttribute("role", "status");
|
||||||
const footer = node("footer", "opportunities-extension-action-form-footer");
|
const footer = node("footer", "opportunities-extension-action-form-footer");
|
||||||
const retry = node("button", "", "Retry");
|
const retry = node("button", "", "Retry");
|
||||||
retry.type = "button";
|
retry.type = "button";
|
||||||
const cancel = node("button", "", "Cancel");
|
const cancel = node("button", "", "Cancel");
|
||||||
|
const save = node("button", "opportunities-extension-action-form-save", "Save");
|
||||||
|
save.type = "submit";
|
||||||
|
save.disabled = true;
|
||||||
cancel.type = "button";
|
cancel.type = "button";
|
||||||
cancel.addEventListener("click", () => close());
|
cancel.addEventListener("click", () => close());
|
||||||
footer.append(retry, cancel);
|
footer.append(retry, save, cancel);
|
||||||
form.append(field);
|
form.append(field);
|
||||||
const extraControls = [];
|
const extraControls = [];
|
||||||
const extraLoads = [];
|
const extraLoads = [];
|
||||||
@@ -82,8 +97,10 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
|||||||
else input.type = type;
|
else input.type = type;
|
||||||
input.id = `opportunities-extension-sr-${name}`;
|
input.id = `opportunities-extension-sr-${name}`;
|
||||||
input.name = name;
|
input.name = name;
|
||||||
|
input.required = true;
|
||||||
|
fields.set(name, input);
|
||||||
wrapper.htmlFor = input.id;
|
wrapper.htmlFor = input.id;
|
||||||
wrapper.append(node("span", "", label), input);
|
wrapper.append(node("span", "", `${label} (required)`), input);
|
||||||
form.append(wrapper);
|
form.append(wrapper);
|
||||||
extraControls.push(input);
|
extraControls.push(input);
|
||||||
}
|
}
|
||||||
@@ -98,15 +115,18 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
|||||||
retryOption.hidden = true;
|
retryOption.hidden = true;
|
||||||
extraControls.push(retryOption);
|
extraControls.push(retryOption);
|
||||||
let input, combo;
|
let input, combo;
|
||||||
if (radios) wrapper.append(node("legend", "", label));
|
if (radios) { wrapper.append(node("legend", "", `${label} (required)`)); serviceGroup = wrapper; }
|
||||||
else {
|
else {
|
||||||
const field = node("label", "opportunities-extension-action-form-field");
|
const field = node("label", "opportunities-extension-action-form-field");
|
||||||
input = node("select");
|
input = node("select");
|
||||||
input.id = `opportunities-extension-sr-${name}`;
|
input.id = `opportunities-extension-sr-${name}`;
|
||||||
input.name = name;
|
input.name = name;
|
||||||
|
input.required = true;
|
||||||
|
fields.set(name, input);
|
||||||
field.htmlFor = input.id;
|
field.htmlFor = input.id;
|
||||||
field.append(node("span", "", label), input);
|
field.append(node("span", "", `${label} (required)`), input);
|
||||||
combo = searchableSelect(input, field);
|
combo = searchableSelect(input, field);
|
||||||
|
combos.set(input, combo);
|
||||||
content.append(field);
|
content.append(field);
|
||||||
extraControls.push(combo.input);
|
extraControls.push(combo.input);
|
||||||
}
|
}
|
||||||
@@ -116,6 +136,9 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
|||||||
async function loadOptions() {
|
async function loadOptions() {
|
||||||
if (loading || !current()) return;
|
if (loading || !current()) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
|
loadingFields.add(name);
|
||||||
|
availableFields.delete(name);
|
||||||
|
updateSave();
|
||||||
retryOption.hidden = true;
|
retryOption.hidden = true;
|
||||||
message.hidden = false;
|
message.hidden = false;
|
||||||
message.setAttribute("role", "status");
|
message.setAttribute("role", "status");
|
||||||
@@ -125,6 +148,7 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
|||||||
try {
|
try {
|
||||||
const items = await request();
|
const items = await request();
|
||||||
if (!current()) return;
|
if (!current()) return;
|
||||||
|
if (items.length) availableFields.add(name);
|
||||||
if (input) {
|
if (input) {
|
||||||
const blank = node("option", "", items.length ? `Select ${label.toLowerCase()}` : "No options available");
|
const blank = node("option", "", items.length ? `Select ${label.toLowerCase()}` : "No options available");
|
||||||
blank.value = "";
|
blank.value = "";
|
||||||
@@ -134,6 +158,8 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
|||||||
input.disabled = !items.length;
|
input.disabled = !items.length;
|
||||||
} else {
|
} else {
|
||||||
content.textContent = "";
|
content.textContent = "";
|
||||||
|
serviceItems = items;
|
||||||
|
serviceRadios.length = 0;
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
items.forEach((item, index) => {
|
items.forEach((item, index) => {
|
||||||
if (seen.has(String(item.Id))) return;
|
if (seen.has(String(item.Id))) return;
|
||||||
@@ -142,6 +168,8 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
|||||||
const radio = node("input");
|
const radio = node("input");
|
||||||
radio.type = "radio";
|
radio.type = "radio";
|
||||||
radio.name = "service";
|
radio.name = "service";
|
||||||
|
radio.required = true;
|
||||||
|
serviceRadios.push(radio);
|
||||||
radio.value = String(item.Id);
|
radio.value = String(item.Id);
|
||||||
radio.id = `opportunities-extension-sr-service-${index}`;
|
radio.id = `opportunities-extension-sr-service-${index}`;
|
||||||
const copy = node("span");
|
const copy = node("span");
|
||||||
@@ -164,6 +192,8 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
|||||||
retryOption.hidden = false;
|
retryOption.hidden = false;
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
|
loadingFields.delete(name);
|
||||||
|
if (current()) updateSave();
|
||||||
if (current()) { combo?.sync(); wrapper.setAttribute("aria-busy", "false"); }
|
if (current()) { combo?.sync(); wrapper.setAttribute("aria-busy", "false"); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,10 +211,156 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
|||||||
const view = { modal, trigger, loading: false };
|
const view = { modal, trigger, loading: false };
|
||||||
active = view;
|
active = view;
|
||||||
const current = () => active === view && allowsCrmActivities();
|
const current = () => active === view && allowsCrmActivities();
|
||||||
|
function updateSave() { save.disabled = loadingFields.size > 0 || availableFields.size < 4 || Boolean(view.saving); }
|
||||||
|
function confirmPayload(payload, requestHeaders = {}) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const overlay = node("section", "opportunities-extension-action-form-modal opportunities-extension-sr-confirmation");
|
||||||
|
overlay.setAttribute("role", "dialog");
|
||||||
|
overlay.setAttribute("aria-modal", "true");
|
||||||
|
overlay.setAttribute("aria-labelledby", "opportunities-extension-sr-confirm-title");
|
||||||
|
const panel = node("div", "opportunities-extension-action-form-dialog opportunities-extension-sr-confirm-dialog");
|
||||||
|
const heading = node("header", "opportunities-extension-account-plans-header");
|
||||||
|
const title = node("h2", "", "Confirm Service Request");
|
||||||
|
title.id = "opportunities-extension-sr-confirm-title";
|
||||||
|
heading.append(title);
|
||||||
|
const body = node("div", "opportunities-extension-sr-confirm-body");
|
||||||
|
const label = node("p", "", "requestPostServiceRequest");
|
||||||
|
label.id = "opportunities-extension-sr-confirm-payload-label";
|
||||||
|
const preview = node("pre", "opportunities-extension-sr-confirm-payload", JSON.stringify(payload, null, 2));
|
||||||
|
preview.tabIndex = 0;
|
||||||
|
preview.setAttribute("aria-labelledby", label.id);
|
||||||
|
body.append(label, preview);
|
||||||
|
const headerPreviews = [];
|
||||||
|
for (const name of ["requestSrTokenCatalog", "requestPostServiceRequest"]) {
|
||||||
|
if (!requestHeaders[name]) continue;
|
||||||
|
const headerLabel = node("p", "", `${name} - Headers`);
|
||||||
|
headerLabel.id = `opportunities-extension-sr-confirm-${name}-headers`;
|
||||||
|
const headerPreview = node("pre", "opportunities-extension-sr-confirm-payload", JSON.stringify(requestHeaders[name], null, 2));
|
||||||
|
headerPreview.tabIndex = 0;
|
||||||
|
headerPreview.setAttribute("aria-labelledby", headerLabel.id);
|
||||||
|
headerPreviews.push(headerPreview);
|
||||||
|
body.append(headerLabel, headerPreview);
|
||||||
|
if (name === "requestSrTokenCatalog" && !requestHeaders[name]["x-appbuilder-client-id"]) {
|
||||||
|
body.append(node("p", "opportunities-extension-action-form-status", "x-appbuilder-client-id: not yet observed in SeaaS requests."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const footer = node("footer", "opportunities-extension-action-form-footer");
|
||||||
|
const cancel = node("button", "", "Cancel");
|
||||||
|
const confirm = node("button", "", "Confirm Save");
|
||||||
|
cancel.type = confirm.type = "button";
|
||||||
|
footer.append(cancel, confirm);
|
||||||
|
panel.append(heading, body, footer);
|
||||||
|
overlay.append(panel);
|
||||||
|
dialog.inert = true;
|
||||||
|
dialog.setAttribute("aria-hidden", "true");
|
||||||
|
modal.setAttribute("aria-modal", "false");
|
||||||
|
modal.append(overlay);
|
||||||
|
let settled = false;
|
||||||
|
function finish(confirmed) {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
overlay.remove();
|
||||||
|
dialog.inert = false;
|
||||||
|
dialog.setAttribute("aria-hidden", "false");
|
||||||
|
modal.setAttribute("aria-modal", "true");
|
||||||
|
resolve(confirmed);
|
||||||
|
}
|
||||||
|
cancel.addEventListener("click", () => finish(false));
|
||||||
|
confirm.addEventListener("click", () => finish(true));
|
||||||
|
overlay.addEventListener("keydown", event => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (event.key === "Escape") { event.preventDefault(); finish(false); }
|
||||||
|
if (event.key === "Tab") {
|
||||||
|
const controls = [preview, ...headerPreviews, cancel, confirm];
|
||||||
|
const index = controls.indexOf(document.activeElement);
|
||||||
|
if (index < 0 || (event.shiftKey ? index === 0 : index === controls.length - 1)) {
|
||||||
|
event.preventDefault(); controls[event.shiftKey ? controls.length - 1 : 0].focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cancel.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
form.addEventListener("submit", async event => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!current() || view.saving || save.disabled) return;
|
||||||
|
const invalid = [];
|
||||||
|
const value = name => String(fields.get(name)?.value || "").trim();
|
||||||
|
for (const [name, input] of fields) {
|
||||||
|
let valid = !!value(name);
|
||||||
|
if (combos.has(input)) valid = valid && Array.from(input.children).some(option => option.value === value(name));
|
||||||
|
if (name === "customerDeadline") {
|
||||||
|
const date = value(name);
|
||||||
|
valid = /^\d{4}-\d{2}-\d{2}$/.test(date) && Number.isFinite(Date.parse(`${date}T00:00:00Z`))
|
||||||
|
&& new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) === date;
|
||||||
|
}
|
||||||
|
const control = combos.get(input)?.input || input;
|
||||||
|
control.setAttribute("aria-invalid", String(!valid));
|
||||||
|
if (!valid) invalid.push(control);
|
||||||
|
}
|
||||||
|
const selected = serviceRadios.find(radio => radio.checked);
|
||||||
|
const service = serviceItems.find(item => String(item.Id) === selected?.value);
|
||||||
|
serviceGroup.setAttribute("aria-invalid", String(!service));
|
||||||
|
if (!service) invalid.push(serviceRadios[0]);
|
||||||
|
if (invalid.length) {
|
||||||
|
status.hidden = false;
|
||||||
|
status.setAttribute("role", "alert");
|
||||||
|
status.textContent = "Complete the highlighted required fields with valid values.";
|
||||||
|
invalid[0]?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const payload = {
|
||||||
|
AccountPartyId: opportunity?.CustomerAccount?.PartyId,
|
||||||
|
OpportunityName_Id_c: opportunity?.OptyId,
|
||||||
|
Title: `${service.RecordName}-${number}-${opportunity?.CustomerAccount?.PartyUniqueName || ""}`,
|
||||||
|
RegionText_c: opportunity?.CustomerAccount?.Region_c,
|
||||||
|
CountryText_c: opportunity?.CustomerAccount?.PrimaryAddress?.CountryCode,
|
||||||
|
ServiceNew_Id_c: service.Id,
|
||||||
|
ProblemDescription: value("description"), Language_c: value("language"),
|
||||||
|
CustomerDeadline_c: value("customerDeadline"), TechPlatformLookUp_c: value("platform"),
|
||||||
|
PrimaryPillarNew_c: value("primaryPillar")
|
||||||
|
};
|
||||||
|
view.saving = true;
|
||||||
|
const controls = [dismiss, retry, cancel, ...fields.values(), ...serviceRadios, ...extraControls];
|
||||||
|
const disabled = controls.map(control => [control, control.disabled]);
|
||||||
|
controls.forEach(control => { control.disabled = true; });
|
||||||
|
combos.forEach(combo => combo.sync());
|
||||||
|
updateSave();
|
||||||
|
save.textContent = "Preparing...";
|
||||||
|
form.setAttribute("aria-busy", "true");
|
||||||
|
status.hidden = true;
|
||||||
|
try {
|
||||||
|
const prepared = await requestSave({ phase: "prepare", payload });
|
||||||
|
if (!current() || host.hidden || !await confirmPayload(prepared.payload, prepared.headers)) return;
|
||||||
|
save.textContent = "Saving...";
|
||||||
|
await requestSave({ phase: "confirm", payload: prepared.payload });
|
||||||
|
view.saving = false;
|
||||||
|
close();
|
||||||
|
notify("Service Request saved successfully.");
|
||||||
|
} catch (error) {
|
||||||
|
status.hidden = false;
|
||||||
|
status.setAttribute("role", "alert");
|
||||||
|
status.textContent = error.message || "Unable to save Service Request.";
|
||||||
|
notify(status.textContent, true);
|
||||||
|
} finally {
|
||||||
|
view.saving = false;
|
||||||
|
if (current()) {
|
||||||
|
disabled.forEach(([control, wasDisabled]) => { control.disabled = wasDisabled; });
|
||||||
|
combos.forEach(combo => combo.sync());
|
||||||
|
updateSave();
|
||||||
|
save.textContent = "Save";
|
||||||
|
form.setAttribute("aria-busy", "false");
|
||||||
|
save.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
function option(value, text) { const item = node("option", "", text); item.value = value; select.append(item); }
|
function option(value, text) { const item = node("option", "", text); item.value = value; select.append(item); }
|
||||||
async function load() {
|
async function load() {
|
||||||
if (view.loading || !current()) return;
|
if (view.loading || !current()) return;
|
||||||
view.loading = true;
|
view.loading = true;
|
||||||
|
loadingFields.add("primaryPillar");
|
||||||
|
availableFields.delete("primaryPillar");
|
||||||
|
updateSave();
|
||||||
retry.hidden = true;
|
retry.hidden = true;
|
||||||
select.disabled = true;
|
select.disabled = true;
|
||||||
select.textContent = "";
|
select.textContent = "";
|
||||||
@@ -203,6 +379,7 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
|||||||
items.forEach(item => option(item.code, item.name));
|
items.forEach(item => option(item.code, item.name));
|
||||||
select.value = "";
|
select.value = "";
|
||||||
select.disabled = !items.length;
|
select.disabled = !items.length;
|
||||||
|
if (items.length) availableFields.add("primaryPillar");
|
||||||
form.setAttribute("data-state", "ready");
|
form.setAttribute("data-state", "ready");
|
||||||
status.hidden = true;
|
status.hidden = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -215,6 +392,8 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
|||||||
retry.hidden = false;
|
retry.hidden = false;
|
||||||
} finally {
|
} finally {
|
||||||
view.loading = false;
|
view.loading = false;
|
||||||
|
loadingFields.delete("primaryPillar");
|
||||||
|
if (current()) updateSave();
|
||||||
if (current()) { autocomplete.sync(); form.setAttribute("aria-busy", "false"); }
|
if (current()) { autocomplete.sync(); form.setAttribute("aria-busy", "false"); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,7 +402,7 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
|||||||
modal.addEventListener("keydown", event => {
|
modal.addEventListener("keydown", event => {
|
||||||
if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(); }
|
if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(); }
|
||||||
if (event.key !== "Tab") return;
|
if (event.key !== "Tab") return;
|
||||||
const controls = [dismiss, autocomplete.input, ...extraControls, retry, cancel].filter(control => !control.disabled && !control.hidden);
|
const controls = [dismiss, autocomplete.input, ...extraControls, retry, save, cancel].filter(control => !control.disabled && !control.hidden);
|
||||||
const index = controls.indexOf(document.activeElement);
|
const index = controls.indexOf(document.activeElement);
|
||||||
if (index < 0 || (event.shiftKey ? index === 0 : index === controls.length - 1)) {
|
if (index < 0 || (event.shiftKey ? index === 0 : index === controls.length - 1)) {
|
||||||
event.preventDefault(); controls[event.shiftKey ? controls.length - 1 : 0].focus();
|
event.preventDefault(); controls[event.shiftKey ? controls.length - 1 : 0].focus();
|
||||||
|
|||||||
@@ -310,6 +310,17 @@
|
|||||||
.opportunities-extension-action-form-field select { height: 38px; }
|
.opportunities-extension-action-form-field select { height: 38px; }
|
||||||
.opportunities-extension-action-form-field select[hidden] { display: none; }
|
.opportunities-extension-action-form-field select[hidden] { display: none; }
|
||||||
.opportunities-extension-sr-form-dialog { width: min(900px, 100%); }
|
.opportunities-extension-sr-form-dialog { width: min(900px, 100%); }
|
||||||
|
.opportunities-extension-sr-confirmation { z-index: 6; }
|
||||||
|
.opportunities-extension-sr-confirm-dialog { width: min(800px, 100%); display: flex; flex-direction: column; overflow: hidden; }
|
||||||
|
.opportunities-extension-sr-confirm-dialog > header,
|
||||||
|
.opportunities-extension-sr-confirm-dialog > footer { flex-shrink: 0; }
|
||||||
|
.opportunities-extension-sr-confirm-dialog > footer { padding: 16px 20px; }
|
||||||
|
.opportunities-extension-sr-confirm-body { min-height: 0; overflow: auto; padding: 0 20px; }
|
||||||
|
.opportunities-extension-sr-confirm-body > p { font-size: 13px; font-weight: 600; }
|
||||||
|
.opportunities-extension-sr-confirm-payload { margin: 0; padding: 16px; border: 1px solid #b9b4ae; border-radius: 4px; background: #f6f5f4; color: #312d2a; font: 13px/1.6 Consolas, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||||
|
.opportunities-extension-sr-confirmation :focus-visible { outline: 2px solid #00758f; outline-offset: 2px; }
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-sr-confirm-payload { background: #211f1d; color: #eeeae6; border-color: #756f68; }
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-sr-confirmation :focus-visible { outline-color: #80d6e8; }
|
||||||
.opportunities-extension-sr-form-dialog > .opportunities-extension-account-plans-header { padding: 20px 24px; }
|
.opportunities-extension-sr-form-dialog > .opportunities-extension-account-plans-header { padding: 20px 24px; }
|
||||||
.opportunities-extension-sr-form-dialog > .opportunities-extension-action-form { gap: 20px 24px; padding: 24px; align-items: start; }
|
.opportunities-extension-sr-form-dialog > .opportunities-extension-action-form { gap: 20px 24px; padding: 24px; align-items: start; }
|
||||||
.opportunities-extension-sr-form-dialog .opportunities-extension-action-form-field { gap: 8px; }
|
.opportunities-extension-sr-form-dialog .opportunities-extension-action-form-field { gap: 8px; }
|
||||||
|
|||||||
@@ -150,7 +150,8 @@ function createFusionOpportunities({
|
|||||||
"OptyId",
|
"OptyId",
|
||||||
"CustomerAccount.PartyUniqueName",
|
"CustomerAccount.PartyUniqueName",
|
||||||
"CustomerAccount.PartyId",
|
"CustomerAccount.PartyId",
|
||||||
"CustomerAccount.PartyNumber"
|
"CustomerAccount.PartyNumber",
|
||||||
|
"CustomerAccount.Region_c"
|
||||||
],
|
],
|
||||||
sort: [],
|
sort: [],
|
||||||
language: "en",
|
language: "en",
|
||||||
|
|||||||
88
src/services/fusion/sr-create.js
Normal file
88
src/services/fusion/sr-create.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders }) {
|
||||||
|
const base = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live;profile=PROD/services/auth/1.1/proxy/";
|
||||||
|
const catalog = base + "crmRestApiResources/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/11.13.18.05/Catalogue_c";
|
||||||
|
const resourceUsers = base + "oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/resourceUsers";
|
||||||
|
const endpoint = base + "oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/mgmt/serviceRequests";
|
||||||
|
function numericId(value, name) {
|
||||||
|
if (!/^\d+$/.test(String(value ?? "")) || !Number.isSafeInteger(Number(value)) || Number(value) <= 0) throw new Error(`Missing or invalid ${name}.`);
|
||||||
|
return Number(value);
|
||||||
|
}
|
||||||
|
function text(value, name) {
|
||||||
|
if (typeof value !== "string" || !value.trim()) throw new Error(`Missing ${name}.`);
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
function payloadFor(input) {
|
||||||
|
const payload = {};
|
||||||
|
for (const name of ["AccountPartyId", "OpportunityName_Id_c", "ServiceNew_Id_c"]) payload[name] = numericId(input?.[name], name);
|
||||||
|
for (const name of ["Title", "RegionText_c", "ProblemDescription", "Language_c", "CustomerDeadline_c", "TechPlatformLookUp_c", "CountryText_c", "PrimaryPillarNew_c"]) payload[name] = text(input?.[name], name);
|
||||||
|
const date = payload.CustomerDeadline_c;
|
||||||
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(Date.parse(`${date}T00:00:00Z`))
|
||||||
|
|| new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) !== date) throw new Error("Invalid Customer deadline.");
|
||||||
|
return { ...payload, SvcQuestionsCollection_c: [], SourceCd: "SVC_OPPORTUNITY_SCREEN", SeverityCd: "ORA_SVC_SEV2", ChannelTypeCd: "ORA_SVC_WEB" };
|
||||||
|
}
|
||||||
|
async function requestPostServiceRequest(message) {
|
||||||
|
const debugRequests = [];
|
||||||
|
try {
|
||||||
|
if (message.phase !== "prepare" && message.phase !== "confirm") throw new Error("Service Request confirmation is required.");
|
||||||
|
const payload = payloadFor(message.payload);
|
||||||
|
if (message.phase === "prepare") {
|
||||||
|
const emailAddress = text(message.emailAddress, "current user email");
|
||||||
|
const resource = await fetchJson(`${resourceUsers}?emailAddress=${encodeURIComponent(emailAddress)}`, {
|
||||||
|
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
|
||||||
|
}, "requestResourceUser", debugRequests);
|
||||||
|
payload.ReportedByPartyId = numericId(resource?.resourcePartyId, "resourcePartyId");
|
||||||
|
const sharedHeaders = await previewTokenHeaders(catalog);
|
||||||
|
return { ok: true, payload, debugRequests, headers: {
|
||||||
|
requestSrTokenCatalog: sharedHeaders,
|
||||||
|
requestPostServiceRequest: { ...sharedHeaders, "vb-proxy-header-x-oracle-ectoken": "(generated after confirmation)" }
|
||||||
|
} };
|
||||||
|
}
|
||||||
|
payload.ReportedByPartyId = numericId(message.payload?.ReportedByPartyId, "ReportedByPartyId");
|
||||||
|
let id = message.catalogId;
|
||||||
|
if (id == null) {
|
||||||
|
const result = await fetchJson(`${catalog}?onlyData=true&fields=Id&limit=1`, {
|
||||||
|
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
|
||||||
|
}, "requestSrCatalog", debugRequests);
|
||||||
|
id = result?.items?.[0]?.Id;
|
||||||
|
}
|
||||||
|
id = numericId(id, "Catalogue Id");
|
||||||
|
const tokenUrl = `${catalog}/${id}`;
|
||||||
|
let catalogHeaders = await tokenHeaders(tokenUrl, endpoint);
|
||||||
|
const requestToken = () => fetchJson(tokenUrl, {
|
||||||
|
method: "POST", credentials: "include", headers: catalogHeaders, body: JSON.stringify({ name: "getJwtToken" })
|
||||||
|
}, "requestSrTokenCatalog", debugRequests);
|
||||||
|
let token;
|
||||||
|
try {
|
||||||
|
token = await requestToken();
|
||||||
|
} catch (error) {
|
||||||
|
if (error.httpStatus !== 403) throw error;
|
||||||
|
const refreshed = await previewTokenHeaders(tokenUrl);
|
||||||
|
if (!refreshed["x-appbuilder-client-id"] || refreshed["x-appbuilder-client-id"] === catalogHeaders["x-appbuilder-client-id"]) throw error;
|
||||||
|
catalogHeaders = refreshed;
|
||||||
|
token = await requestToken();
|
||||||
|
}
|
||||||
|
if (debugRequests.at(-1)?.httpStatus !== 200 || typeof token?.result !== "string" || !token.result.trim()) {
|
||||||
|
throw new Error("requestSrTokenCatalog did not return HTTP 200 with a token.");
|
||||||
|
}
|
||||||
|
const result = await fetchJson(endpoint, {
|
||||||
|
method: "POST", credentials: "include", headers: { ...catalogHeaders, "vb-proxy-header-x-oracle-ectoken": token.result.trim() },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
}, "requestPostServiceRequest", debugRequests, { allowEmpty: true });
|
||||||
|
return { ok: true, result, debugRequests };
|
||||||
|
} catch (error) {
|
||||||
|
error.debugRequests = debugRequests;
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// Never forward the signing token to content scripts, even with Debug secrets enabled.
|
||||||
|
for (const entry of debugRequests) {
|
||||||
|
if (entry.name === "requestSrTokenCatalog") {
|
||||||
|
entry.metadata.appBuilderClientId = entry.requestHeaders?.["x-appbuilder-client-id"] ? "Observed in SeaaS requests" : "Not observed in SeaaS requests";
|
||||||
|
entry.responseBody = JSON.stringify({ result: "(redacted)" });
|
||||||
|
entry.responseBodyTruncated = false;
|
||||||
|
}
|
||||||
|
if (entry.requestHeaders?.["vb-proxy-header-x-oracle-ectoken"]) entry.requestHeaders["vb-proxy-header-x-oracle-ectoken"] = "(redacted)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { requestPostServiceRequest };
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
function createHttpBackgroundJson({
|
function createHttpBackgroundJson({
|
||||||
previewDebugBody,
|
previewDebugBody,
|
||||||
DEBUG_BODY_LIMIT,
|
DEBUG_BODY_LIMIT,
|
||||||
|
observeResponseHeaders = () => {},
|
||||||
environment
|
environment
|
||||||
}) {
|
}) {
|
||||||
const { fetch } = environment;
|
const { fetch } = environment;
|
||||||
@@ -17,6 +18,7 @@ function createHttpBackgroundJson({
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
observeResponseHeaders(response.url || url, response.headers);
|
||||||
const responsePayload = await readResponsePayload(response);
|
const responsePayload = await readResponsePayload(response);
|
||||||
const responseBody = createDebugBody(responsePayload.text);
|
const responseBody = createDebugBody(responsePayload.text);
|
||||||
completeBackgroundDebugEntry(debugEntry, response, responseBody, "");
|
completeBackgroundDebugEntry(debugEntry, response, responseBody, "");
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ function createMessageTypes() {
|
|||||||
requestMgmtAction: "opportunitiesExtension.requestMgmtAction",
|
requestMgmtAction: "opportunitiesExtension.requestMgmtAction",
|
||||||
requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest",
|
requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest",
|
||||||
requestSrLookupPrimaryPillar: "opportunitiesExtension.requestSrLookupPrimaryPillar",
|
requestSrLookupPrimaryPillar: "opportunitiesExtension.requestSrLookupPrimaryPillar",
|
||||||
|
requestPostServiceRequest: "opportunitiesExtension.requestPostServiceRequest",
|
||||||
prepareSrSession: "opportunitiesExtension.prepareSrSession",
|
prepareSrSession: "opportunitiesExtension.prepareSrSession",
|
||||||
requestSrLookupLanguage: "opportunitiesExtension.requestSrLookupLanguage",
|
requestSrLookupLanguage: "opportunitiesExtension.requestSrLookupLanguage",
|
||||||
requestSrLookupTechPlatform: "opportunitiesExtension.requestSrLookupTechPlatform",
|
requestSrLookupTechPlatform: "opportunitiesExtension.requestSrLookupTechPlatform",
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ test("distribution contracts: manifests, execution boundaries and deterministic
|
|||||||
const base = {
|
const base = {
|
||||||
manifest_version: 3, name: "Opportunities Extension",
|
manifest_version: 3, name: "Opportunities Extension",
|
||||||
description: "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.",
|
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/*"],
|
host_permissions: ["https://eeho.fa.us2.oraclecloud.com/*", "https://spa.oracle.com/*", "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/*"],
|
||||||
content_scripts: [{
|
content_scripts: [{
|
||||||
matches: ["https://eeho.fa.us2.oraclecloud.com/hcmUI/faces/FuseWelcome*", "https://eeho.fa.us2.oraclecloud.com/fscmUI/faces/FuseWelcome*"],
|
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: "$or", criteria: types.map(value => ({ op: "$eq", attribute: "RecordSet", value })) },
|
||||||
{ op: "$wi", attribute: "EffectiveDate", value1: range.startCloseDate, value2: range.endCloseDate, dynamicDate: false }
|
{ op: "$wi", attribute: "EffectiveDate", value1: range.startCloseDate, value2: range.endCloseDate, dynamicDate: false }
|
||||||
] }, keywords: null, keywordsFields: fields,
|
] }, 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,
|
sort: [], language: "en", skipInValidFields: true, skipHiddenFromUIFields: true,
|
||||||
copiedFrom: "queries/d5dbcd01-9ff3-40be-9b5b-64744cbf7162", limit: 15, offset: 15
|
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);
|
assert.equal(badge(), undefined);
|
||||||
for (const [values, expected] of [
|
for (const [values, expected] of [
|
||||||
[[], null], [[null], "N/D"], [[null, undefined, "", " "], "N/D"], [[6], "6"],
|
[[], 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", {
|
h.app.productsContentState.opptyProductsByNumber.set("TEST-1", {
|
||||||
fromCache: true, items: values.map(RampMonths_c => ({ RampMonths_c }))
|
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 {
|
else {
|
||||||
assert.equal(badge().textContent, expected);
|
assert.equal(badge().textContent, expected);
|
||||||
assert.equal(badge().attributes["data-missing"], String(expected.includes("N/D")));
|
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(badge().attributes["aria-label"], badge().title);
|
||||||
assert.equal(h.cell("name").children[0].children[1].children[1], badge());
|
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, /td\[data-column="customer"\] \{\s*position: relative;/);
|
||||||
assert.match(css, /grid-template-columns: minmax\(0, 1fr\) 12px/);
|
assert.match(css, /grid-template-columns: minmax\(0, 1fr\) 12px/);
|
||||||
const tableRules = css.replace(/[^{}]*\.opportunities-extension-products-ramp\s*\{[^}]*\}/g, "")
|
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.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, /\.opportunities-extension-products-ramp\s*\{[^}]*background: #f0e8f7/);
|
||||||
assert.match(css, /\[data-theme="dark"\] \.opportunities-extension-products-ramp\s*\{[^}]*color: #ead5fc/);
|
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 time = clock();
|
||||||
const listeners = [];
|
const listeners = [];
|
||||||
const progress = [];
|
const progress = [];
|
||||||
@@ -65,7 +65,17 @@ function loadBackground({ family = "chrome", fetch = unexpectedFetch, storage =
|
|||||||
const tabs = [];
|
const tabs = [];
|
||||||
const removedTabs = [];
|
const removedTabs = [];
|
||||||
const cookieQueries = [];
|
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 readCookies = details => { cookieQueries.push(plain(details)); return cookies(details); };
|
||||||
const getStored = key => ({ [key]: storage[key] === undefined ? undefined : plain(storage[key]) });
|
const getStored = key => ({ [key]: storage[key] === undefined ? undefined : plain(storage[key]) });
|
||||||
const store = value => Object.assign(storage, plain(value));
|
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" }]); }
|
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 = {
|
const context = {
|
||||||
[family === "firefox" ? "browser" : "chrome"]: api,
|
[family === "firefox" ? "browser" : "chrome"]: api,
|
||||||
fetch, URL, Date: time.Date,
|
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")); }
|
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 };
|
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);
|
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", () => {
|
test("only plus opens the nine-field form; a missing workload stays disabled without a save operation", () => {
|
||||||
const h = setup();
|
const h = setup();
|
||||||
const badge = h.app.opportunityWorkloadActions.createBadge("A");
|
const badge = h.app.opportunityWorkloadActions.createBadge("A");
|
||||||
|
|||||||
Reference in New Issue
Block a user