Release version 1.0.1

This commit is contained in:
2026-09-08 18:58:30 -03:00
parent 5b1a35430a
commit 46f6b195e9
9 changed files with 549 additions and 49 deletions

View File

@@ -1,4 +1,4 @@
# Opportunities Extension 1.0.0 # Opportunities Extension 1.0.1
Browser extension that adds the "Opportunities Extension" shortcut to the permitted Oracle Fusion pages, retrieves opportunities, and displays them in a filterable table. Browser extension that adds the "Opportunities Extension" shortcut to the permitted Oracle Fusion pages, retrieves opportunities, and displays them in a filterable table.
@@ -29,6 +29,12 @@ The build recreates `dist/chromium` and `dist/firefox` from the same source.
- Light and dark themes based on Oracle Redwood. - Light and dark themes based on Oracle Redwood.
- Separate outputs for Chromium/Edge and Firefox. - Separate outputs for Chromium/Edge and Firefox.
## Release 1.0.1
- Retries the consumer token service when it returns HTTP 200 with an HTML fallback.
- Opens the SPA session page in an inactive temporary tab and closes it as soon as valid JSON is received.
- Limits the recovery flow to five attempts and records the session bootstrap details in debug mode.
## Loading in Development Mode ## Loading in Development Mode
### Chrome ou Microsoft Edge ### Chrome ou Microsoft Edge

View File

@@ -18,6 +18,10 @@
const OPPTY_PRODUCTS_MAX_PAGES = 250; const OPPTY_PRODUCTS_MAX_PAGES = 250;
const OPPTY_PRODUCTS_CONCURRENCY = 4; const OPPTY_PRODUCTS_CONCURRENCY = 4;
const CUSTOMER_TOKEN_SERVICE_URL = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/consumerTokenService"; const CUSTOMER_TOKEN_SERVICE_URL = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/consumerTokenService";
const CUSTOMER_TOKEN_SESSION_URL = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html";
const CUSTOMER_TOKEN_MAX_ATTEMPTS = 5;
const CUSTOMER_TOKEN_RETRY_BASE_DELAY_MS = 750;
const CUSTOMER_TOKEN_RETRY_MAX_DELAY_MS = 4000;
const ACCOUNT_SUMMARY_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-account/service/accountsummary"; const ACCOUNT_SUMMARY_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-account/service/accountsummary";
const ACCOUNT_PLANS_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/account/accountPlans"; const ACCOUNT_PLANS_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/account/accountPlans";
const CONSUMPTION_TREND_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/plan/trend"; const CONSUMPTION_TREND_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/plan/trend";
@@ -129,7 +133,7 @@
} }
if (message.type === "opportunitiesExtension.requestConsumerTokenService") { if (message.type === "opportunitiesExtension.requestConsumerTokenService") {
requestConsumerTokenService() requestConsumerTokenService(sender)
.then(sendResponse) .then(sendResponse)
.catch((error) => { .catch((error) => {
sendResponse({ sendResponse({
@@ -173,20 +177,11 @@
return false; return false;
}); });
async function requestConsumerTokenService() { async function requestConsumerTokenService(sender) {
const debugRequests = []; const debugRequests = [];
try { try {
const tokenResponse = await fetchJson(CUSTOMER_TOKEN_SERVICE_URL, { const tokenResponse = await fetchConsumerTokenServiceUntilJson(sender, debugRequests);
method: "GET",
credentials: "include",
cache: "no-store",
headers: {
Accept: "application/json",
"Cache-Control": "no-cache",
Pragma: "no-cache"
}
}, "requestConsumerTokenService", debugRequests);
const accountSummaryAuthorization = extractTokenServiceAuthorization( const accountSummaryAuthorization = extractTokenServiceAuthorization(
tokenResponse, tokenResponse,
"ACCP-TS", "ACCP-TS",
@@ -239,6 +234,126 @@
} }
} }
async function fetchConsumerTokenServiceUntilJson(sender, debugRequests) {
let attempt = 0;
let sessionTab = null;
let receivedValidJson = false;
try {
while (attempt < CUSTOMER_TOKEN_MAX_ATTEMPTS) {
attempt += 1;
try {
const tokenResponse = await fetchJson(CUSTOMER_TOKEN_SERVICE_URL, {
method: "GET",
credentials: "include",
cache: "no-store",
headers: {
Accept: "application/json",
"Cache-Control": "no-cache, no-store, max-age=0",
Pragma: "no-cache"
}
}, "requestConsumerTokenService", debugRequests);
const debugEntry = debugRequests[debugRequests.length - 1];
if (debugEntry) {
debugEntry.metadata.attempt = String(attempt);
}
receivedValidJson = true;
return tokenResponse;
} catch (error) {
const isHtmlFallback = error
&& error.code === "INVALID_JSON_RESPONSE"
&& error.httpStatus === 200;
const debugEntry = debugRequests[debugRequests.length - 1];
if (!isHtmlFallback || attempt >= CUSTOMER_TOKEN_MAX_ATTEMPTS) {
if (debugEntry) {
debugEntry.metadata.attempt = String(attempt);
if (isHtmlFallback) {
debugEntry.metadata.retryLimitReached = "true";
debugEntry.error = `requestConsumerTokenService returned invalid JSON after ${CUSTOMER_TOKEN_MAX_ATTEMPTS} attempts.`;
error.message = debugEntry.error;
}
}
throw error;
}
if (!sessionTab) {
try {
sessionTab = await openConsumerTokenSessionTab(sender);
if (debugEntry) {
debugEntry.metadata.sessionBootstrapUrl = CUSTOMER_TOKEN_SESSION_URL;
debugEntry.metadata.sessionBootstrapState = "opened-inactive-tab";
}
} catch (sessionError) {
if (debugEntry) {
debugEntry.metadata.sessionBootstrapUrl = CUSTOMER_TOKEN_SESSION_URL;
debugEntry.metadata.sessionBootstrapState = `tab-open-failed: ${sessionError.message || "unknown error"}`;
}
}
} else if (debugEntry) {
debugEntry.metadata.sessionBootstrapState = "waiting-for-valid-session";
}
const retryDelayMs = getConsumerTokenRetryDelay(attempt);
if (debugEntry) {
debugEntry.metadata.attempt = String(attempt);
debugEntry.metadata.retryReason = "HTTP 200 response did not contain valid JSON";
debugEntry.metadata.nextRetryDelayMs = String(retryDelayMs);
}
await wait(retryDelayMs);
}
}
throw new Error("requestConsumerTokenService retry loop ended unexpectedly.");
} finally {
if (sessionTab && Number.isInteger(sessionTab.id)) {
const debugEntry = debugRequests[debugRequests.length - 1];
try {
await tabsRemove(sessionTab.id);
if (debugEntry) {
debugEntry.metadata.sessionBootstrapState = receivedValidJson
? "closed-after-valid-json"
: "closed-after-retry-finished";
}
} catch (error) {
if (debugEntry) {
debugEntry.metadata.sessionBootstrapState = `tab-close-failed: ${error.message || "unknown error"}`;
}
}
}
}
}
function getConsumerTokenRetryDelay(attempt) {
return Math.min(
CUSTOMER_TOKEN_RETRY_BASE_DELAY_MS * (2 ** Math.max(attempt - 1, 0)),
CUSTOMER_TOKEN_RETRY_MAX_DELAY_MS
);
}
function openConsumerTokenSessionTab(sender) {
const details = {
url: CUSTOMER_TOKEN_SESSION_URL,
active: false
};
if (sender && sender.tab && Number.isInteger(sender.tab.windowId)) {
details.windowId = sender.tab.windowId;
}
if (supportsFirefoxCookieStoreTabs() && sender && sender.tab && sender.tab.cookieStoreId) {
details.cookieStoreId = sender.tab.cookieStoreId;
}
return tabsCreate(details);
}
async function requestAccountPlans(message) { async function requestAccountPlans(message) {
const partyNumber = typeof message.partyNumber === "string" ? message.partyNumber.trim() : ""; const partyNumber = typeof message.partyNumber === "string" ? message.partyNumber.trim() : "";
const accountSummaryAuthorization = normalizeMessageAuthorization( const accountSummaryAuthorization = normalizeMessageAuthorization(
@@ -416,7 +531,14 @@
throw new Error(`${requestName} response body could not be read.`); throw new Error(`${requestName} response body could not be read.`);
} }
return parseJsonResponse(responsePayload.text, requestName); try {
return parseJsonResponse(responsePayload.text, requestName);
} catch (error) {
error.httpStatus = response.status;
debugEntry.status = "failed";
debugEntry.error = error.message;
throw error;
}
} }
function createBackgroundDebugEntry(requestName, url, options, startedAt) { function createBackgroundDebugEntry(requestName, url, options, startedAt) {
@@ -491,7 +613,9 @@
try { try {
return JSON.parse(normalizedText); return JSON.parse(normalizedText);
} catch (error) { } catch (error) {
throw new Error(`${requestName} returned an invalid JSON response.`); const invalidJsonError = new Error(`${requestName} returned an invalid JSON response.`);
invalidJsonError.code = "INVALID_JSON_RESPONSE";
throw invalidJsonError;
} }
} }

View File

@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "Opportunities Extension", "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.0", "version": "1.0.1",
"permissions": [ "permissions": [
"cookies", "cookies",
"storage" "storage"

View File

@@ -18,6 +18,10 @@
const OPPTY_PRODUCTS_MAX_PAGES = 250; const OPPTY_PRODUCTS_MAX_PAGES = 250;
const OPPTY_PRODUCTS_CONCURRENCY = 4; const OPPTY_PRODUCTS_CONCURRENCY = 4;
const CUSTOMER_TOKEN_SERVICE_URL = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/consumerTokenService"; const CUSTOMER_TOKEN_SERVICE_URL = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/consumerTokenService";
const CUSTOMER_TOKEN_SESSION_URL = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html";
const CUSTOMER_TOKEN_MAX_ATTEMPTS = 5;
const CUSTOMER_TOKEN_RETRY_BASE_DELAY_MS = 750;
const CUSTOMER_TOKEN_RETRY_MAX_DELAY_MS = 4000;
const ACCOUNT_SUMMARY_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-account/service/accountsummary"; const ACCOUNT_SUMMARY_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-account/service/accountsummary";
const ACCOUNT_PLANS_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/account/accountPlans"; const ACCOUNT_PLANS_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/account/accountPlans";
const CONSUMPTION_TREND_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/plan/trend"; const CONSUMPTION_TREND_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/plan/trend";
@@ -129,7 +133,7 @@
} }
if (message.type === "opportunitiesExtension.requestConsumerTokenService") { if (message.type === "opportunitiesExtension.requestConsumerTokenService") {
requestConsumerTokenService() requestConsumerTokenService(sender)
.then(sendResponse) .then(sendResponse)
.catch((error) => { .catch((error) => {
sendResponse({ sendResponse({
@@ -173,20 +177,11 @@
return false; return false;
}); });
async function requestConsumerTokenService() { async function requestConsumerTokenService(sender) {
const debugRequests = []; const debugRequests = [];
try { try {
const tokenResponse = await fetchJson(CUSTOMER_TOKEN_SERVICE_URL, { const tokenResponse = await fetchConsumerTokenServiceUntilJson(sender, debugRequests);
method: "GET",
credentials: "include",
cache: "no-store",
headers: {
Accept: "application/json",
"Cache-Control": "no-cache",
Pragma: "no-cache"
}
}, "requestConsumerTokenService", debugRequests);
const accountSummaryAuthorization = extractTokenServiceAuthorization( const accountSummaryAuthorization = extractTokenServiceAuthorization(
tokenResponse, tokenResponse,
"ACCP-TS", "ACCP-TS",
@@ -239,6 +234,126 @@
} }
} }
async function fetchConsumerTokenServiceUntilJson(sender, debugRequests) {
let attempt = 0;
let sessionTab = null;
let receivedValidJson = false;
try {
while (attempt < CUSTOMER_TOKEN_MAX_ATTEMPTS) {
attempt += 1;
try {
const tokenResponse = await fetchJson(CUSTOMER_TOKEN_SERVICE_URL, {
method: "GET",
credentials: "include",
cache: "no-store",
headers: {
Accept: "application/json",
"Cache-Control": "no-cache, no-store, max-age=0",
Pragma: "no-cache"
}
}, "requestConsumerTokenService", debugRequests);
const debugEntry = debugRequests[debugRequests.length - 1];
if (debugEntry) {
debugEntry.metadata.attempt = String(attempt);
}
receivedValidJson = true;
return tokenResponse;
} catch (error) {
const isHtmlFallback = error
&& error.code === "INVALID_JSON_RESPONSE"
&& error.httpStatus === 200;
const debugEntry = debugRequests[debugRequests.length - 1];
if (!isHtmlFallback || attempt >= CUSTOMER_TOKEN_MAX_ATTEMPTS) {
if (debugEntry) {
debugEntry.metadata.attempt = String(attempt);
if (isHtmlFallback) {
debugEntry.metadata.retryLimitReached = "true";
debugEntry.error = `requestConsumerTokenService returned invalid JSON after ${CUSTOMER_TOKEN_MAX_ATTEMPTS} attempts.`;
error.message = debugEntry.error;
}
}
throw error;
}
if (!sessionTab) {
try {
sessionTab = await openConsumerTokenSessionTab(sender);
if (debugEntry) {
debugEntry.metadata.sessionBootstrapUrl = CUSTOMER_TOKEN_SESSION_URL;
debugEntry.metadata.sessionBootstrapState = "opened-inactive-tab";
}
} catch (sessionError) {
if (debugEntry) {
debugEntry.metadata.sessionBootstrapUrl = CUSTOMER_TOKEN_SESSION_URL;
debugEntry.metadata.sessionBootstrapState = `tab-open-failed: ${sessionError.message || "unknown error"}`;
}
}
} else if (debugEntry) {
debugEntry.metadata.sessionBootstrapState = "waiting-for-valid-session";
}
const retryDelayMs = getConsumerTokenRetryDelay(attempt);
if (debugEntry) {
debugEntry.metadata.attempt = String(attempt);
debugEntry.metadata.retryReason = "HTTP 200 response did not contain valid JSON";
debugEntry.metadata.nextRetryDelayMs = String(retryDelayMs);
}
await wait(retryDelayMs);
}
}
throw new Error("requestConsumerTokenService retry loop ended unexpectedly.");
} finally {
if (sessionTab && Number.isInteger(sessionTab.id)) {
const debugEntry = debugRequests[debugRequests.length - 1];
try {
await tabsRemove(sessionTab.id);
if (debugEntry) {
debugEntry.metadata.sessionBootstrapState = receivedValidJson
? "closed-after-valid-json"
: "closed-after-retry-finished";
}
} catch (error) {
if (debugEntry) {
debugEntry.metadata.sessionBootstrapState = `tab-close-failed: ${error.message || "unknown error"}`;
}
}
}
}
}
function getConsumerTokenRetryDelay(attempt) {
return Math.min(
CUSTOMER_TOKEN_RETRY_BASE_DELAY_MS * (2 ** Math.max(attempt - 1, 0)),
CUSTOMER_TOKEN_RETRY_MAX_DELAY_MS
);
}
function openConsumerTokenSessionTab(sender) {
const details = {
url: CUSTOMER_TOKEN_SESSION_URL,
active: false
};
if (sender && sender.tab && Number.isInteger(sender.tab.windowId)) {
details.windowId = sender.tab.windowId;
}
if (supportsFirefoxCookieStoreTabs() && sender && sender.tab && sender.tab.cookieStoreId) {
details.cookieStoreId = sender.tab.cookieStoreId;
}
return tabsCreate(details);
}
async function requestAccountPlans(message) { async function requestAccountPlans(message) {
const partyNumber = typeof message.partyNumber === "string" ? message.partyNumber.trim() : ""; const partyNumber = typeof message.partyNumber === "string" ? message.partyNumber.trim() : "";
const accountSummaryAuthorization = normalizeMessageAuthorization( const accountSummaryAuthorization = normalizeMessageAuthorization(
@@ -416,7 +531,14 @@
throw new Error(`${requestName} response body could not be read.`); throw new Error(`${requestName} response body could not be read.`);
} }
return parseJsonResponse(responsePayload.text, requestName); try {
return parseJsonResponse(responsePayload.text, requestName);
} catch (error) {
error.httpStatus = response.status;
debugEntry.status = "failed";
debugEntry.error = error.message;
throw error;
}
} }
function createBackgroundDebugEntry(requestName, url, options, startedAt) { function createBackgroundDebugEntry(requestName, url, options, startedAt) {
@@ -491,7 +613,9 @@
try { try {
return JSON.parse(normalizedText); return JSON.parse(normalizedText);
} catch (error) { } catch (error) {
throw new Error(`${requestName} returned an invalid JSON response.`); const invalidJsonError = new Error(`${requestName} returned an invalid JSON response.`);
invalidJsonError.code = "INVALID_JSON_RESPONSE";
throw invalidJsonError;
} }
} }

View File

@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "Opportunities Extension", "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.0", "version": "1.0.1",
"permissions": [ "permissions": [
"cookies", "cookies",
"storage" "storage"

View File

@@ -1,11 +1,11 @@
{ {
"name": "opportunities-extension", "name": "opportunities-extension",
"version": "1.0.0", "version": "1.0.1",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "node scripts/build.mjs", "build": "node scripts/build.mjs",
"check": "node --check src/content.js && node --check src/background.js && node tests/consumptionTrend.test.cjs && node scripts/build.mjs --check", "check": "node --check src/content.js && node --check src/background.js && node tests/consumptionTrend.test.cjs && node tests/consumerTokenSessionRetry.test.cjs && node scripts/build.mjs --check",
"test": "node tests/consumptionTrend.test.cjs" "test": "node tests/consumptionTrend.test.cjs && node tests/consumerTokenSessionRetry.test.cjs"
} }
} }

View File

@@ -13,7 +13,7 @@ const baseManifest = {
manifest_version: 3, manifest_version: 3,
name: "Opportunities Extension", 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.0", version: "1.0.1",
permissions: [ permissions: [
"cookies", "cookies",
"storage" "storage"

View File

@@ -18,6 +18,10 @@
const OPPTY_PRODUCTS_MAX_PAGES = 250; const OPPTY_PRODUCTS_MAX_PAGES = 250;
const OPPTY_PRODUCTS_CONCURRENCY = 4; const OPPTY_PRODUCTS_CONCURRENCY = 4;
const CUSTOMER_TOKEN_SERVICE_URL = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/consumerTokenService"; const CUSTOMER_TOKEN_SERVICE_URL = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/consumerTokenService";
const CUSTOMER_TOKEN_SESSION_URL = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html";
const CUSTOMER_TOKEN_MAX_ATTEMPTS = 5;
const CUSTOMER_TOKEN_RETRY_BASE_DELAY_MS = 750;
const CUSTOMER_TOKEN_RETRY_MAX_DELAY_MS = 4000;
const ACCOUNT_SUMMARY_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-account/service/accountsummary"; const ACCOUNT_SUMMARY_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-account/service/accountsummary";
const ACCOUNT_PLANS_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/account/accountPlans"; const ACCOUNT_PLANS_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/account/accountPlans";
const CONSUMPTION_TREND_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/plan/trend"; const CONSUMPTION_TREND_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/plan/trend";
@@ -129,7 +133,7 @@
} }
if (message.type === "opportunitiesExtension.requestConsumerTokenService") { if (message.type === "opportunitiesExtension.requestConsumerTokenService") {
requestConsumerTokenService() requestConsumerTokenService(sender)
.then(sendResponse) .then(sendResponse)
.catch((error) => { .catch((error) => {
sendResponse({ sendResponse({
@@ -173,20 +177,11 @@
return false; return false;
}); });
async function requestConsumerTokenService() { async function requestConsumerTokenService(sender) {
const debugRequests = []; const debugRequests = [];
try { try {
const tokenResponse = await fetchJson(CUSTOMER_TOKEN_SERVICE_URL, { const tokenResponse = await fetchConsumerTokenServiceUntilJson(sender, debugRequests);
method: "GET",
credentials: "include",
cache: "no-store",
headers: {
Accept: "application/json",
"Cache-Control": "no-cache",
Pragma: "no-cache"
}
}, "requestConsumerTokenService", debugRequests);
const accountSummaryAuthorization = extractTokenServiceAuthorization( const accountSummaryAuthorization = extractTokenServiceAuthorization(
tokenResponse, tokenResponse,
"ACCP-TS", "ACCP-TS",
@@ -239,6 +234,126 @@
} }
} }
async function fetchConsumerTokenServiceUntilJson(sender, debugRequests) {
let attempt = 0;
let sessionTab = null;
let receivedValidJson = false;
try {
while (attempt < CUSTOMER_TOKEN_MAX_ATTEMPTS) {
attempt += 1;
try {
const tokenResponse = await fetchJson(CUSTOMER_TOKEN_SERVICE_URL, {
method: "GET",
credentials: "include",
cache: "no-store",
headers: {
Accept: "application/json",
"Cache-Control": "no-cache, no-store, max-age=0",
Pragma: "no-cache"
}
}, "requestConsumerTokenService", debugRequests);
const debugEntry = debugRequests[debugRequests.length - 1];
if (debugEntry) {
debugEntry.metadata.attempt = String(attempt);
}
receivedValidJson = true;
return tokenResponse;
} catch (error) {
const isHtmlFallback = error
&& error.code === "INVALID_JSON_RESPONSE"
&& error.httpStatus === 200;
const debugEntry = debugRequests[debugRequests.length - 1];
if (!isHtmlFallback || attempt >= CUSTOMER_TOKEN_MAX_ATTEMPTS) {
if (debugEntry) {
debugEntry.metadata.attempt = String(attempt);
if (isHtmlFallback) {
debugEntry.metadata.retryLimitReached = "true";
debugEntry.error = `requestConsumerTokenService returned invalid JSON after ${CUSTOMER_TOKEN_MAX_ATTEMPTS} attempts.`;
error.message = debugEntry.error;
}
}
throw error;
}
if (!sessionTab) {
try {
sessionTab = await openConsumerTokenSessionTab(sender);
if (debugEntry) {
debugEntry.metadata.sessionBootstrapUrl = CUSTOMER_TOKEN_SESSION_URL;
debugEntry.metadata.sessionBootstrapState = "opened-inactive-tab";
}
} catch (sessionError) {
if (debugEntry) {
debugEntry.metadata.sessionBootstrapUrl = CUSTOMER_TOKEN_SESSION_URL;
debugEntry.metadata.sessionBootstrapState = `tab-open-failed: ${sessionError.message || "unknown error"}`;
}
}
} else if (debugEntry) {
debugEntry.metadata.sessionBootstrapState = "waiting-for-valid-session";
}
const retryDelayMs = getConsumerTokenRetryDelay(attempt);
if (debugEntry) {
debugEntry.metadata.attempt = String(attempt);
debugEntry.metadata.retryReason = "HTTP 200 response did not contain valid JSON";
debugEntry.metadata.nextRetryDelayMs = String(retryDelayMs);
}
await wait(retryDelayMs);
}
}
throw new Error("requestConsumerTokenService retry loop ended unexpectedly.");
} finally {
if (sessionTab && Number.isInteger(sessionTab.id)) {
const debugEntry = debugRequests[debugRequests.length - 1];
try {
await tabsRemove(sessionTab.id);
if (debugEntry) {
debugEntry.metadata.sessionBootstrapState = receivedValidJson
? "closed-after-valid-json"
: "closed-after-retry-finished";
}
} catch (error) {
if (debugEntry) {
debugEntry.metadata.sessionBootstrapState = `tab-close-failed: ${error.message || "unknown error"}`;
}
}
}
}
}
function getConsumerTokenRetryDelay(attempt) {
return Math.min(
CUSTOMER_TOKEN_RETRY_BASE_DELAY_MS * (2 ** Math.max(attempt - 1, 0)),
CUSTOMER_TOKEN_RETRY_MAX_DELAY_MS
);
}
function openConsumerTokenSessionTab(sender) {
const details = {
url: CUSTOMER_TOKEN_SESSION_URL,
active: false
};
if (sender && sender.tab && Number.isInteger(sender.tab.windowId)) {
details.windowId = sender.tab.windowId;
}
if (supportsFirefoxCookieStoreTabs() && sender && sender.tab && sender.tab.cookieStoreId) {
details.cookieStoreId = sender.tab.cookieStoreId;
}
return tabsCreate(details);
}
async function requestAccountPlans(message) { async function requestAccountPlans(message) {
const partyNumber = typeof message.partyNumber === "string" ? message.partyNumber.trim() : ""; const partyNumber = typeof message.partyNumber === "string" ? message.partyNumber.trim() : "";
const accountSummaryAuthorization = normalizeMessageAuthorization( const accountSummaryAuthorization = normalizeMessageAuthorization(
@@ -416,7 +531,14 @@
throw new Error(`${requestName} response body could not be read.`); throw new Error(`${requestName} response body could not be read.`);
} }
return parseJsonResponse(responsePayload.text, requestName); try {
return parseJsonResponse(responsePayload.text, requestName);
} catch (error) {
error.httpStatus = response.status;
debugEntry.status = "failed";
debugEntry.error = error.message;
throw error;
}
} }
function createBackgroundDebugEntry(requestName, url, options, startedAt) { function createBackgroundDebugEntry(requestName, url, options, startedAt) {
@@ -491,7 +613,9 @@
try { try {
return JSON.parse(normalizedText); return JSON.parse(normalizedText);
} catch (error) { } catch (error) {
throw new Error(`${requestName} returned an invalid JSON response.`); const invalidJsonError = new Error(`${requestName} returned an invalid JSON response.`);
invalidJsonError.code = "INVALID_JSON_RESPONSE";
throw invalidJsonError;
} }
} }

View File

@@ -0,0 +1,122 @@
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const vm = require("node:vm");
const root = path.resolve(__dirname, "..");
function createResponse(body, contentType) {
return {
ok: true,
status: 200,
statusText: "OK",
headers: {
get(name) {
return name.toLowerCase() === "content-type" ? contentType : null;
},
forEach(callback) {
callback(contentType, "content-type");
}
},
text: async () => body
};
}
function loadBackground(responses, createdTabs, removedTabs) {
let listener;
const source = fs.readFileSync(path.join(root, "src", "background.js"), "utf8");
const context = {
chrome: {
runtime: {
onMessage: {
addListener(callback) {
listener = callback;
}
}
},
tabs: {
async create(details) {
createdTabs.push(details);
return { id: 27 };
},
async remove(tabId) {
removedTabs.push(tabId);
}
}
},
fetch: async () => {
const response = responses.shift();
if (!response) {
throw new Error("Unexpected fetch call.");
}
return response;
},
Date,
Error,
JSON,
Map,
Set,
Promise,
String,
Array,
Object,
Number,
Math,
setTimeout,
clearTimeout
};
vm.runInNewContext(source, context);
return listener;
}
function dispatch(listener) {
return new Promise((resolve) => {
assert.equal(listener({ type: "opportunitiesExtension.requestConsumerTokenService" }, {
tab: { windowId: 1 }
}, resolve), true);
});
}
async function run() {
const createdTabs = [];
const removedTabs = [];
const listener = loadBackground([
createResponse("<html><body>Fallback</body></html>", "text/html"),
createResponse(JSON.stringify([
{
tokenService: "ACCP-TS",
uiaasHeader: { label: "Account-Ti-Authorization", value: "account-token" }
},
{
tokenService: "OCICONS-TS",
uiaasHeader: { label: "G2m-Authorization", value: "consumption-token" }
}
]), "application/json")
], createdTabs, removedTabs);
const response = await dispatch(listener);
assert.equal(response.ok, true);
assert.equal(response.authorizations.accountSummary.value, "account-token");
assert.equal(response.authorizations.accountPlans.value, "consumption-token");
assert.equal(createdTabs.length, 1);
assert.equal(createdTabs[0].active, false);
assert.equal(
createdTabs[0].url,
"https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html"
);
assert.deepEqual(removedTabs, [27]);
assert.equal(response.debugRequests.length, 2);
assert.equal(response.debugRequests[0].status, "failed");
assert.equal(response.debugRequests[0].metadata.sessionBootstrapState, "opened-inactive-tab");
assert.equal(response.debugRequests[1].metadata.sessionBootstrapState, "closed-after-valid-json");
console.log("Consumer token session retry tests passed.");
}
run().catch((error) => {
console.error(error);
process.exitCode = 1;
});