Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/e2e-shell-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 19 additions & 2 deletions packages/b2c-tooling-sdk/src/cli/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,19 @@ export abstract class BaseCommand<T extends typeof Command> 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,
}),
Expand Down Expand Up @@ -307,16 +315,17 @@ export abstract class BaseCommand<T extends typeof Command> 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
*/
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;
}

Expand All @@ -338,6 +347,14 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
}
}

if (extraHeaders) {
try {
config.headers = JSON.parse(extraHeaders) as Record<string, string>;
} catch {
this.error(`Invalid JSON for --extra-headers: ${extraHeaders}`);
}
}

return config;
}
}
17 changes: 17 additions & 0 deletions packages/b2c-tooling-sdk/src/clients/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export interface ExtraParamsConfig {
query?: Record<string, string | number | boolean | undefined>;
/** Extra body fields to merge into JSON request bodies */
body?: Record<string, unknown>;
/** Extra HTTP headers to add to all requests */
headers?: Record<string, string>;
}

/**
Expand Down Expand Up @@ -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);
Expand Down
67 changes: 67 additions & 0 deletions packages/b2c-tooling-sdk/test/cli/base-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
83 changes: 83 additions & 0 deletions packages/b2c-tooling-sdk/test/clients/middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,89 @@ describe('clients/middleware', () => {
const body = JSON.parse(await modifiedRequest.text()) as Record<string, unknown>;
expect(body.forced).to.equal(true);
});

it('adds extra headers to request', async () => {
const middleware = createExtraParamsMiddleware({headers: {'X-Custom': 'value'}});
type OnRequestParams = Parameters<NonNullable<typeof middleware.onRequest>>[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<NonNullable<typeof middleware.onRequest>>[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<NonNullable<typeof middleware.onRequest>>[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<NonNullable<typeof middleware.onRequest>>[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<NonNullable<typeof middleware.onRequest>>[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', () => {
Expand Down