From 415054fe2850f5ca70206bc4ed1a8782ed87c16a Mon Sep 17 00:00:00 2001 From: Shazron Abdullah <36107+shazron@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:20:38 +0800 Subject: [PATCH 1/3] refactor: replace axios and form-data with Node built-in fetch and FormData Node >=20 provides fetch and FormData as globals, making the axios and form-data dependencies unnecessary. Streams are buffered internally before appending to FormData since the native API only accepts Blob/File/string. Co-Authored-By: Claude Sonnet 4.6 --- package.json | 2 -- src/helpers.js | 23 +++++++++++------------ test/helpers.test.js | 33 ++++++++++++++++++++++++++------- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index b486af1..b0ceb28 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,6 @@ "@adobe/aio-lib-core-logging": "^3", "@adobe/aio-lib-core-networking": "^5.0.2", "@adobe/aio-lib-env": "^3", - "axios": "^1.7.9", - "form-data": "^4.0.1", "swagger-client": "^3.31.0" }, "devDependencies": { diff --git a/src/helpers.js b/src/helpers.js index a37de19..a3bcaa6 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -10,8 +10,6 @@ governing permissions and limitations under the License. */ const loggerNamespace = '@adobe/aio-lib-console' const logger = require('@adobe/aio-lib-core-logging')(loggerNamespace, { provider: 'debug', level: process.env.LOG_LEVEL || 'debug' }) -const axios = require('axios') -const FormData = require('form-data') /** * Reduce an Error to a string @@ -118,7 +116,7 @@ function responseInterceptor (res) { } /** - * Use axios lib to directly call console API to create credential + * Use fetch to directly call console API to create credential * * @param {string} url URL string * @param {string} accessToken Token to call the API @@ -126,25 +124,26 @@ function responseInterceptor (res) { * @param {object} certificate A Readable stream with certificate content. eg: fs.createReadStream() * @param {string} name Credential name * @param {string} description Credential description - * @returns {object} The response object + * @returns {Promise} The response object */ async function createCredentialDirect (url, accessToken, apiKey, certificate, name, description) { + const chunks = [] + for await (const chunk of certificate) { + chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk) + } const data = new FormData() - data.append('certificate', certificate) + data.append('certificate', new Blob([Buffer.concat(chunks)])) data.append('name', name) data.append('description', description) - const config = { - method: 'post', - url, + return fetch(url, { + method: 'POST', headers: { Authorization: 'Bearer ' + accessToken, - 'content-type': 'multipart/form-data', 'x-api-key': apiKey }, - data - } - return await axios.request(config) + body: data + }) } module.exports = { diff --git a/test/helpers.test.js b/test/helpers.test.js index b318524..a4790f1 100644 --- a/test/helpers.test.js +++ b/test/helpers.test.js @@ -10,8 +10,6 @@ governing permissions and limitations under the License. */ const AioLogger = require('@adobe/aio-lib-core-logging') const helpers = require('../src/helpers') -const axios = require('axios') -jest.mock('axios') const stream = require('stream') const mockedStream = new stream.Readable() mockedStream._read = function (size) { /* do nothing */ } @@ -162,8 +160,12 @@ describe('responseInterceptor', () => { }) describe('createCredentialDirect', () => { + let fetchSpy beforeEach(() => { - axios.mockReset() + fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({ ok: true, status: 200 }) + }) + afterEach(() => { + fetchSpy.mockRestore() }) test('API call', async () => { const url = 'mockurl' @@ -171,9 +173,26 @@ describe('createCredentialDirect', () => { const apiKey = 'mockKey' const name = 'mockName' const desc = 'mock description' - axios.request.mockImplementation(() => Promise.resolve({ data: {} })) - const ret = await helpers.createCredentialDirect(url, accessToken, apiKey, mockedStream, name, desc) - expect(axios.request).toHaveBeenCalled() - expect(ret).toEqual({ data: {} }) + const certStream = stream.Readable.from(Buffer.from('cert-data')) + const ret = await helpers.createCredentialDirect(url, accessToken, apiKey, certStream, name, desc) + expect(fetchSpy).toHaveBeenCalledWith(url, expect.objectContaining({ + method: 'POST', + headers: expect.objectContaining({ + Authorization: 'Bearer mockToken', + 'x-api-key': 'mockKey' + }) + })) + expect(ret).toEqual({ ok: true, status: 200 }) + }) + test('API call with string chunks', async () => { + const url = 'mockurl' + const accessToken = 'mockToken' + const apiKey = 'mockKey' + const name = 'mockName' + const desc = 'mock description' + const certStream = stream.Readable.from('cert-data-string') + const ret = await helpers.createCredentialDirect(url, accessToken, apiKey, certStream, name, desc) + expect(fetchSpy).toHaveBeenCalled() + expect(ret).toEqual({ ok: true, status: 200 }) }) }) From a3e0cd33ed043c7314a84f4cb7abfefab78752f1 Mon Sep 17 00:00:00 2001 From: Shazron Abdullah <36107+shazron@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:40:10 +0800 Subject: [PATCH 2/3] nit: fix e2e env vars --- e2e/.env.example | 8 ++++---- e2e/README.md | 8 ++++---- e2e/e2e.js | 14 ++++++++++---- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/e2e/.env.example b/e2e/.env.example index d241971..28b647f 100644 --- a/e2e/.env.example +++ b/e2e/.env.example @@ -1,4 +1,4 @@ -CONSOLEAPI_API_KEY= -CONSOLEAPI_ACCESS_TOKEN= -CONSOLEAPI_IMS_ORG_ID= -CONSOLEAPI_ENV= +CONSOLE_API_API_KEY= +CONSOLE_API_ACCESS_TOKEN= +CONSOLE_API_IMS_ORG_ID= +CONSOLE_API_ENV= diff --git a/e2e/README.md b/e2e/README.md index a8839d7..79785fb 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -4,10 +4,10 @@ To run the e2e test you'll need these env variables set (copy `.env.example` to `.env`): - 1. `CONSOLEAPI_API_KEY` (this is the same IMS client id used by the cli) - 2. `CONSOLEAPI_ACCESS_TOKEN` (this is the access token retrieved by an `aio login`) - 3. `CONSOLEAPI_IMS_ORG_ID` (get this from the App Builder project's `.aio` file) - 4. `CONSOLEAPI_ENV` (`prod` (default) or `stage`) + 1. `CONSOLE_API_API_KEY` (this is the same IMS client id used by the cli) + 2. `CONSOLE_API_ACCESS_TOKEN` (this is the access token retrieved by an `aio login`) + 3. `CONSOLE_API_IMS_ORG_ID` (get this from the App Builder project's `.aio` file) + 4. `CONSOLE_API_ENV` (`prod` (default) or `stage`) ## Run diff --git a/e2e/e2e.js b/e2e/e2e.js index dbb8f0e..17d5975 100644 --- a/e2e/e2e.js +++ b/e2e/e2e.js @@ -19,11 +19,17 @@ const tmp = require('tmp') // load .env values in the e2e folder, if any require('dotenv').config({ path: path.join(__dirname, '.env') }) +const missingEnvVars = ['CONSOLE_API_API_KEY', 'CONSOLE_API_ACCESS_TOKEN', 'CONSOLE_API_IMS_ORG_ID'] + .filter(v => !process.env[v]) +if (missingEnvVars.length > 0) { + throw new Error(`Missing required environment variables: ${missingEnvVars.join(', ')}`) +} + let sdkClient = {} -const apiKey = process.env.CONSOLEAPI_API_KEY -const accessToken = process.env.CONSOLEAPI_ACCESS_TOKEN -const imsOrgId = process.env.CONSOLEAPI_IMS_ORG_ID -const env = process.env.CONSOLEAPI_ENV || 'prod' +const apiKey = process.env.CONSOLE_API_API_KEY +const accessToken = process.env.CONSOLE_API_ACCESS_TOKEN +const imsOrgId = process.env.CONSOLE_API_IMS_ORG_ID +const env = process.env.CONSOLE_API_ENV || 'prod' // these ids will be assigned when creating the project and workspace dynamically for the test let fireflyProjectId, projectId, defaultWorkspaceId, workspaceId, orgId, fireflyWorkspaceId From 895af917c97cc526922d0a1fb7effaddeb7952f7 Mon Sep 17 00:00:00 2001 From: Shazron Abdullah <36107+shazron@users.noreply.github.com> Date: Fri, 3 Apr 2026 23:00:51 +0800 Subject: [PATCH 3/3] fix: restore axios failure semantics and set explicit filename in createCredentialDirect - Pass 'certificate' as the filename when appending a Blob to FormData, avoiding the default "blob" filename that browsers/runtimes report - Check res.ok after fetch and throw a shaped error on non-2xx responses so callers retain the same error-path behaviour as the previous axios implementation Co-Authored-By: Claude Sonnet 4.6 --- src/helpers.js | 11 +++++++++-- test/helpers.test.js | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/helpers.js b/src/helpers.js index a3bcaa6..125b5d4 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -132,11 +132,11 @@ async function createCredentialDirect (url, accessToken, apiKey, certificate, na chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk) } const data = new FormData() - data.append('certificate', new Blob([Buffer.concat(chunks)])) + data.append('certificate', new Blob([Buffer.concat(chunks)]), 'certificate') data.append('name', name) data.append('description', description) - return fetch(url, { + const res = await fetch(url, { method: 'POST', headers: { Authorization: 'Bearer ' + accessToken, @@ -144,6 +144,13 @@ async function createCredentialDirect (url, accessToken, apiKey, certificate, na }, body: data }) + if (!res.ok) { + const body = await res.text() + const err = new Error(`${res.status} ${res.statusText}`) + err.response = { status: res.status, statusText: res.statusText, body, headers: Object.fromEntries(res.headers) } + throw err + } + return res } module.exports = { diff --git a/test/helpers.test.js b/test/helpers.test.js index a4790f1..85c92cb 100644 --- a/test/helpers.test.js +++ b/test/helpers.test.js @@ -195,4 +195,24 @@ describe('createCredentialDirect', () => { expect(fetchSpy).toHaveBeenCalled() expect(ret).toEqual({ ok: true, status: 200 }) }) + test('throws on non-2xx response', async () => { + fetchSpy.mockResolvedValue({ + ok: false, + status: 401, + statusText: 'Unauthorized', + text: jest.fn().mockResolvedValue('{"error":"unauthorized"}'), + headers: new Headers({ 'x-request-id': 'req-123' }) + }) + const certStream = stream.Readable.from(Buffer.from('cert-data')) + await expect(helpers.createCredentialDirect('mockurl', 'mockToken', 'mockKey', certStream, 'mockName', 'mock description')) + .rejects.toMatchObject({ + message: '401 Unauthorized', + response: { + status: 401, + statusText: 'Unauthorized', + body: '{"error":"unauthorized"}', + headers: { 'x-request-id': 'req-123' } + } + }) + }) })