From cd6b60ddcd85ef748057ff865b4db8b88a602426 Mon Sep 17 00:00:00 2001 From: Paulo Porto Date: Tue, 8 Sep 2026 18:58:30 -0300 Subject: [PATCH] Release version 1.0.1 --- README.md | 8 +- dist/chromium/background.js | 152 ++++++++++++++++++++--- dist/chromium/manifest.json | 2 +- dist/firefox/background.js | 152 ++++++++++++++++++++--- dist/firefox/manifest.json | 2 +- package.json | 6 +- scripts/build.mjs | 2 +- src/background.js | 152 ++++++++++++++++++++--- tests/consumerTokenSessionRetry.test.cjs | 122 ++++++++++++++++++ 9 files changed, 549 insertions(+), 49 deletions(-) create mode 100644 tests/consumerTokenSessionRetry.test.cjs diff --git a/README.md b/README.md index 101d0ed..23bc3f2 100644 --- a/README.md +++ b/README.md @@ -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. @@ -29,6 +29,12 @@ The build recreates `dist/chromium` and `dist/firefox` from the same source. - Light and dark themes based on Oracle Redwood. - 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 ### Chrome ou Microsoft Edge diff --git a/dist/chromium/background.js b/dist/chromium/background.js index 4200856..5c380af 100644 --- a/dist/chromium/background.js +++ b/dist/chromium/background.js @@ -18,6 +18,10 @@ const OPPTY_PRODUCTS_MAX_PAGES = 250; const OPPTY_PRODUCTS_CONCURRENCY = 4; 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_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"; @@ -129,7 +133,7 @@ } if (message.type === "opportunitiesExtension.requestConsumerTokenService") { - requestConsumerTokenService() + requestConsumerTokenService(sender) .then(sendResponse) .catch((error) => { sendResponse({ @@ -173,20 +177,11 @@ return false; }); - async function requestConsumerTokenService() { + async function requestConsumerTokenService(sender) { const debugRequests = []; try { - const tokenResponse = await fetchJson(CUSTOMER_TOKEN_SERVICE_URL, { - method: "GET", - credentials: "include", - cache: "no-store", - headers: { - Accept: "application/json", - "Cache-Control": "no-cache", - Pragma: "no-cache" - } - }, "requestConsumerTokenService", debugRequests); + const tokenResponse = await fetchConsumerTokenServiceUntilJson(sender, debugRequests); const accountSummaryAuthorization = extractTokenServiceAuthorization( tokenResponse, "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) { const partyNumber = typeof message.partyNumber === "string" ? message.partyNumber.trim() : ""; const accountSummaryAuthorization = normalizeMessageAuthorization( @@ -416,7 +531,14 @@ 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) { @@ -491,7 +613,9 @@ try { return JSON.parse(normalizedText); } 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; } } diff --git a/dist/chromium/manifest.json b/dist/chromium/manifest.json index 220048f..d9dcb4a 100644 --- a/dist/chromium/manifest.json +++ b/dist/chromium/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Opportunities Extension", "description": "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.", - "version": "1.0.0", + "version": "1.0.1", "permissions": [ "cookies", "storage" diff --git a/dist/firefox/background.js b/dist/firefox/background.js index 4200856..5c380af 100644 --- a/dist/firefox/background.js +++ b/dist/firefox/background.js @@ -18,6 +18,10 @@ const OPPTY_PRODUCTS_MAX_PAGES = 250; const OPPTY_PRODUCTS_CONCURRENCY = 4; 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_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"; @@ -129,7 +133,7 @@ } if (message.type === "opportunitiesExtension.requestConsumerTokenService") { - requestConsumerTokenService() + requestConsumerTokenService(sender) .then(sendResponse) .catch((error) => { sendResponse({ @@ -173,20 +177,11 @@ return false; }); - async function requestConsumerTokenService() { + async function requestConsumerTokenService(sender) { const debugRequests = []; try { - const tokenResponse = await fetchJson(CUSTOMER_TOKEN_SERVICE_URL, { - method: "GET", - credentials: "include", - cache: "no-store", - headers: { - Accept: "application/json", - "Cache-Control": "no-cache", - Pragma: "no-cache" - } - }, "requestConsumerTokenService", debugRequests); + const tokenResponse = await fetchConsumerTokenServiceUntilJson(sender, debugRequests); const accountSummaryAuthorization = extractTokenServiceAuthorization( tokenResponse, "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) { const partyNumber = typeof message.partyNumber === "string" ? message.partyNumber.trim() : ""; const accountSummaryAuthorization = normalizeMessageAuthorization( @@ -416,7 +531,14 @@ 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) { @@ -491,7 +613,9 @@ try { return JSON.parse(normalizedText); } 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; } } diff --git a/dist/firefox/manifest.json b/dist/firefox/manifest.json index 66e0a20..3a568dd 100644 --- a/dist/firefox/manifest.json +++ b/dist/firefox/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Opportunities Extension", "description": "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.", - "version": "1.0.0", + "version": "1.0.1", "permissions": [ "cookies", "storage" diff --git a/package.json b/package.json index 678f9d2..130a217 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "opportunities-extension", - "version": "1.0.0", + "version": "1.0.1", "private": true, "type": "module", "scripts": { "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", - "test": "node tests/consumptionTrend.test.cjs" + "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 && node tests/consumerTokenSessionRetry.test.cjs" } } diff --git a/scripts/build.mjs b/scripts/build.mjs index 2ce768f..85e12e1 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -13,7 +13,7 @@ const baseManifest = { manifest_version: 3, name: "Opportunities Extension", description: "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.", - version: "1.0.0", + version: "1.0.1", permissions: [ "cookies", "storage" diff --git a/src/background.js b/src/background.js index 4200856..5c380af 100644 --- a/src/background.js +++ b/src/background.js @@ -18,6 +18,10 @@ const OPPTY_PRODUCTS_MAX_PAGES = 250; const OPPTY_PRODUCTS_CONCURRENCY = 4; 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_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"; @@ -129,7 +133,7 @@ } if (message.type === "opportunitiesExtension.requestConsumerTokenService") { - requestConsumerTokenService() + requestConsumerTokenService(sender) .then(sendResponse) .catch((error) => { sendResponse({ @@ -173,20 +177,11 @@ return false; }); - async function requestConsumerTokenService() { + async function requestConsumerTokenService(sender) { const debugRequests = []; try { - const tokenResponse = await fetchJson(CUSTOMER_TOKEN_SERVICE_URL, { - method: "GET", - credentials: "include", - cache: "no-store", - headers: { - Accept: "application/json", - "Cache-Control": "no-cache", - Pragma: "no-cache" - } - }, "requestConsumerTokenService", debugRequests); + const tokenResponse = await fetchConsumerTokenServiceUntilJson(sender, debugRequests); const accountSummaryAuthorization = extractTokenServiceAuthorization( tokenResponse, "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) { const partyNumber = typeof message.partyNumber === "string" ? message.partyNumber.trim() : ""; const accountSummaryAuthorization = normalizeMessageAuthorization( @@ -416,7 +531,14 @@ 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) { @@ -491,7 +613,9 @@ try { return JSON.parse(normalizedText); } 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; } } diff --git a/tests/consumerTokenSessionRetry.test.cjs b/tests/consumerTokenSessionRetry.test.cjs new file mode 100644 index 0000000..7ccd6f8 --- /dev/null +++ b/tests/consumerTokenSessionRetry.test.cjs @@ -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("Fallback", "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; +});