diff --git a/.github/workflows/e2e-shell-tests.yml b/.github/workflows/e2e-shell-tests.yml index 8b62449f..13a48d7c 100644 --- a/.github/workflows/e2e-shell-tests.yml +++ b/.github/workflows/e2e-shell-tests.yml @@ -80,6 +80,7 @@ jobs: SFCC_SANDBOX_API_HOST: ${{ vars.SFCC_SANDBOX_API_HOST }} SFCC_SHORTCODE: ${{ vars.SFCC_SHORTCODE }} TEST_REALM: ${{ vars.TEST_REALM }} + SFCC_EXTRA_HEADERS: ${{ secrets.SFCC_EXTRA_HEADERS }} run: | echo "Running E2E shell tests with realm: ${TEST_REALM}" cd packages/b2c-cli diff --git a/packages/b2c-tooling-sdk/src/cli/base-command.ts b/packages/b2c-tooling-sdk/src/cli/base-command.ts index bb9e62c4..5a977cd6 100644 --- a/packages/b2c-tooling-sdk/src/cli/base-command.ts +++ b/packages/b2c-tooling-sdk/src/cli/base-command.ts @@ -75,11 +75,19 @@ export abstract class BaseCommand extends Command { }), 'extra-query': Flags.string({ description: 'Extra query parameters as JSON (e.g., \'{"debug":"true"}\')', + env: 'SFCC_EXTRA_QUERY', helpGroup: 'GLOBAL', hidden: true, }), 'extra-body': Flags.string({ description: 'Extra body fields to merge as JSON (e.g., \'{"_internal":true}\')', + env: 'SFCC_EXTRA_BODY', + helpGroup: 'GLOBAL', + hidden: true, + }), + 'extra-headers': Flags.string({ + description: 'Extra HTTP headers as JSON (e.g., \'{"X-Custom-Header": "value"}\')', + env: 'SFCC_EXTRA_HEADERS', helpGroup: 'GLOBAL', hidden: true, }), @@ -307,7 +315,7 @@ export abstract class BaseCommand extends Command { } /** - * Parse extra params from --extra-query and --extra-body flags. + * Parse extra params from --extra-query, --extra-body, and --extra-headers flags. * Returns undefined if no extra params are specified. * * @returns ExtraParamsConfig or undefined @@ -315,8 +323,9 @@ export abstract class BaseCommand extends Command { protected getExtraParams(): ExtraParamsConfig | undefined { const extraQuery = this.flags['extra-query']; const extraBody = this.flags['extra-body']; + const extraHeaders = this.flags['extra-headers']; - if (!extraQuery && !extraBody) { + if (!extraQuery && !extraBody && !extraHeaders) { return undefined; } @@ -338,6 +347,14 @@ export abstract class BaseCommand extends Command { } } + if (extraHeaders) { + try { + config.headers = JSON.parse(extraHeaders) as Record; + } catch { + this.error(`Invalid JSON for --extra-headers: ${extraHeaders}`); + } + } + return config; } } diff --git a/packages/b2c-tooling-sdk/src/clients/middleware.ts b/packages/b2c-tooling-sdk/src/clients/middleware.ts index 101fa10f..30822c2e 100644 --- a/packages/b2c-tooling-sdk/src/clients/middleware.ts +++ b/packages/b2c-tooling-sdk/src/clients/middleware.ts @@ -23,6 +23,8 @@ export interface ExtraParamsConfig { query?: Record; /** Extra body fields to merge into JSON request bodies */ body?: Record; + /** Extra HTTP headers to add to all requests */ + headers?: Record; } /** @@ -206,6 +208,21 @@ export function createExtraParamsMiddleware(config: ExtraParamsConfig): Middlewa async onRequest({request}) { let modifiedRequest = request; + // Add extra headers first (before other modifications) + if (config.headers && Object.keys(config.headers).length > 0) { + const newHeaders = new Headers(modifiedRequest.headers); + for (const [key, value] of Object.entries(config.headers)) { + newHeaders.set(key, value); + } + logger.trace({extraHeaders: config.headers}, '[ExtraParams] Adding extra headers to request'); + modifiedRequest = new Request(modifiedRequest.url, { + method: modifiedRequest.method, + headers: newHeaders, + body: modifiedRequest.body, + duplex: modifiedRequest.body ? 'half' : undefined, + } as RequestInit); + } + // Add extra query parameters if (config.query && Object.keys(config.query).length > 0) { const url = new URL(request.url); diff --git a/packages/b2c-tooling-sdk/test/cli/base-command.test.ts b/packages/b2c-tooling-sdk/test/cli/base-command.test.ts index be88137c..2182b4cf 100644 --- a/packages/b2c-tooling-sdk/test/cli/base-command.test.ts +++ b/packages/b2c-tooling-sdk/test/cli/base-command.test.ts @@ -449,6 +449,73 @@ describe('cli/base-command', () => { cmd.error = originalError; cmd.parse = originalParse; }); + + it('parses extra-headers flag', async () => { + const cmd = command as MockableBaseCommand; + const originalParse = cmd.parse.bind(command); + cmd.parse = (async () => ({ + args: {}, + flags: {'extra-headers': '{"X-Custom-Header":"value"}'}, + metadata: {}, + })) as typeof cmd.parse; + + await cmd.init(); + const params = command.testGetExtraParams(); + expect(params?.headers).to.deep.equal({'X-Custom-Header': 'value'}); + + cmd.parse = originalParse; + }); + + it('parses extra-query, extra-body, and extra-headers together', async () => { + const cmd = command as MockableBaseCommand; + const originalParse = cmd.parse.bind(command); + cmd.parse = (async () => ({ + args: {}, + flags: { + 'extra-query': '{"debug":"true"}', + 'extra-body': '{"_internal":true}', + 'extra-headers': '{"X-Custom":"value"}', + }, + metadata: {}, + })) as typeof cmd.parse; + + await cmd.init(); + const params = command.testGetExtraParams(); + expect(params?.query).to.deep.equal({debug: 'true'}); + expect(params?.body).to.deep.equal({_internal: true}); + expect(params?.headers).to.deep.equal({'X-Custom': 'value'}); + + cmd.parse = originalParse; + }); + + it('throws error for invalid JSON in extra-headers', async () => { + const cmd = command as MockableBaseCommand; + const originalParse = cmd.parse.bind(command); + cmd.parse = (async () => ({ + args: {}, + flags: {'extra-headers': 'invalid-json'}, + metadata: {}, + })) as typeof cmd.parse; + + await cmd.init(); + let errorCalled = false; + const originalError = cmd.error.bind(command); + cmd.error = () => { + errorCalled = true; + throw new Error('Expected error'); + }; + + try { + command.testGetExtraParams(); + } catch { + // Expected + } + + expect(errorCalled).to.be.true; + + cmd.error = originalError; + cmd.parse = originalParse; + }); }); describe('baseCommandTest', () => { diff --git a/packages/b2c-tooling-sdk/test/clients/middleware.test.ts b/packages/b2c-tooling-sdk/test/clients/middleware.test.ts index 5e7ba2a1..48e75471 100644 --- a/packages/b2c-tooling-sdk/test/clients/middleware.test.ts +++ b/packages/b2c-tooling-sdk/test/clients/middleware.test.ts @@ -256,6 +256,89 @@ describe('clients/middleware', () => { const body = JSON.parse(await modifiedRequest.text()) as Record; expect(body.forced).to.equal(true); }); + + it('adds extra headers to request', async () => { + const middleware = createExtraParamsMiddleware({headers: {'X-Custom': 'value'}}); + type OnRequestParams = Parameters>[0]; + + const request = new Request('https://example.com/items', {method: 'GET'}); + const modifiedRequest = await middleware.onRequest!({request} as unknown as OnRequestParams); + + if (!modifiedRequest) { + throw new Error('Expected middleware to return a Request'); + } + + expect(modifiedRequest.headers.get('X-Custom')).to.equal('value'); + }); + + it('overwrites existing headers with extra headers', async () => { + const middleware = createExtraParamsMiddleware({headers: {'X-Custom': 'new-value'}}); + type OnRequestParams = Parameters>[0]; + + const request = new Request('https://example.com/items', { + method: 'GET', + headers: {'X-Custom': 'old-value'}, + }); + const modifiedRequest = await middleware.onRequest!({request} as unknown as OnRequestParams); + + if (!modifiedRequest) { + throw new Error('Expected middleware to return a Request'); + } + + expect(modifiedRequest.headers.get('X-Custom')).to.equal('new-value'); + }); + + it('preserves other headers when adding extra headers', async () => { + const middleware = createExtraParamsMiddleware({headers: {'X-Custom': 'value'}}); + type OnRequestParams = Parameters>[0]; + + const request = new Request('https://example.com/items', { + method: 'GET', + headers: {'Content-Type': 'application/json'}, + }); + const modifiedRequest = await middleware.onRequest!({request} as unknown as OnRequestParams); + + if (!modifiedRequest) { + throw new Error('Expected middleware to return a Request'); + } + + expect(modifiedRequest.headers.get('X-Custom')).to.equal('value'); + expect(modifiedRequest.headers.get('Content-Type')).to.equal('application/json'); + }); + + it('does nothing when headers config is empty', async () => { + const middleware = createExtraParamsMiddleware({headers: {}}); + type OnRequestParams = Parameters>[0]; + + const request = new Request('https://example.com/items', {method: 'GET'}); + const modifiedRequest = await middleware.onRequest!({request} as unknown as OnRequestParams); + + if (!modifiedRequest) { + throw new Error('Expected middleware to return a Request'); + } + + expect(modifiedRequest.url).to.equal(request.url); + }); + + it('adds multiple extra headers', async () => { + const middleware = createExtraParamsMiddleware({ + headers: { + 'CF-Access-Client-Id': 'client-id', + 'CF-Access-Client-Secret': 'client-secret', + }, + }); + type OnRequestParams = Parameters>[0]; + + const request = new Request('https://example.com/items', {method: 'GET'}); + const modifiedRequest = await middleware.onRequest!({request} as unknown as OnRequestParams); + + if (!modifiedRequest) { + throw new Error('Expected middleware to return a Request'); + } + + expect(modifiedRequest.headers.get('CF-Access-Client-Id')).to.equal('client-id'); + expect(modifiedRequest.headers.get('CF-Access-Client-Secret')).to.equal('client-secret'); + }); }); describe('createLoggingMiddleware', () => {