Skip to content
Open
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
50 changes: 50 additions & 0 deletions src/static/helpers/customApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,4 +249,54 @@ describe('callCustomEndpoint', () => {
undefined
);
});

test('doFetch is called with signal from options and throws aborted error when aborted', async () => {
const controller = new AbortController();

const copyOptions = {
...options,
signal: controller.signal,
};

const {shortCode, organizationId} = clientConfig.parameters;
const {apiName, endpointPath} = copyOptions.customApiPathParameters;

const nockBasePath = `https://${shortCode}.api.commercecloud.salesforce.com`;
const nockEndpointPath = `/custom/${apiName}/v2/organizations/${
organizationId as string
}/${endpointPath}`;
nock(nockBasePath)
.post(nockEndpointPath)
.query(true)
.delayConnection(200)
.reply(200);

const expectedUrl = `${
nockBasePath + nockEndpointPath
}?${queryParamString}`;
const expectedOptions = addSiteIdToOptions(copyOptions);

const expectedClientConfig = {
...clientConfig,
baseUri:
'https://{shortCode}.api.commercecloud.salesforce.com/custom/{apiName}/{apiVersion}',
};

const doFetchSpy = jest.spyOn(fetchHelper, 'doFetch');
setTimeout(() => controller.abort(), 100);
await expect(
callCustomEndpoint({
options: copyOptions,
clientConfig,
rawResponse: true,
})
).rejects.toThrow('The user aborted a request.');
expect(doFetchSpy).toBeCalledTimes(1);
expect(doFetchSpy).toBeCalledWith(
expectedUrl,
expectedOptions,
expectedClientConfig,
true
);
});
});
1 change: 1 addition & 0 deletions src/static/helpers/customApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface CustomParams {
* @param args.options.parameters? - Query parameters that are added to the request
* @param args.options.customApiPathParameters? - Path parameters used for custom API. Required path parameters (apiName, endpointPath, organizationId, and shortCode) can be in this object, or args.clientConfig.parameters. apiVersion is defaulted to 'v1' if not provided.
* @param args.options.headers? - Headers that are added to the request. Authorization header should be in this parameter or in the clientConfig.headers. If "Content-Type" is not provided in either header, it will be defaulted to "application/json".
* @param args.options.signal? - An AbortSignal object that can be used to cancel the fetch request
* @param args.options.body? - Body that is used for the request
* @param args.clientConfig - Client Configuration object used by the SDK with properties that can affect the fetch call
* @param args.clientConfig.parameters - Path parameters used for custom API endpoints. The required properties are: apiName, endpointPath, organizationId, and shortCode. An error will be thrown if these are not provided.
Expand Down
67 changes: 67 additions & 0 deletions src/static/helpers/fetchHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,71 @@ describe('doFetch', () => {
expect.objectContaining(clientConfig.fetchOptions)
);
});

test('throws error when fetchOptions.signal is passed and aborted during fetch call', async () => {
nock(basePath)
.post(endpointPath)
.query(true)
.delayConnection(200)
.reply(200, responseBody);

const controller = new AbortController();
const copyClientConfig = {
...clientConfig,
fetchOptions: {...clientConfig.fetchOptions, signal: controller.signal},
};
setTimeout(() => controller.abort(), 100);
const spy = jest.spyOn(environment, 'fetch');
await expect(
doFetch(url, options, copyClientConfig, false)
).rejects.toThrow('The user aborted a request.');
expect(spy).toBeCalledTimes(1);
expect(spy).toBeCalledWith(
expect.any(String),
expect.objectContaining({signal: controller.signal})
);
});

test('throws error when options.signal is passed and aborted during fetch call', async () => {
nock(basePath)
.post(endpointPath)
.query(true)
.delayConnection(200)
.reply(200, responseBody);

const controller = new AbortController();
const copyOptions = {...options, signal: controller.signal};
setTimeout(() => controller.abort(), 100);
const spy = jest.spyOn(environment, 'fetch');
await expect(
doFetch(url, copyOptions, clientConfig, false)
).rejects.toThrow('The user aborted a request.');
expect(spy).toBeCalledTimes(1);
expect(spy).toBeCalledWith(
expect.any(String),
expect.objectContaining({signal: controller.signal})
);
});

test('options.signal overrides fetchOptions.signal', async () => {
nock(basePath).post(endpointPath).query(true).reply(200, responseBody);

const clientConfigController = new AbortController();
const optionsController = new AbortController();
const copyClientConfig = {
...clientConfig,
fetchOptions: {
...clientConfig.fetchOptions,
signal: clientConfigController.signal,
},
};
const copyOptions = {...options, signal: optionsController.signal};
const spy = jest.spyOn(environment, 'fetch');
await doFetch(url, copyOptions, copyClientConfig, false);
expect(spy).toBeCalledTimes(1);
expect(spy).toBeCalledWith(
expect.any(String),
expect.objectContaining({signal: optionsController.signal})
);
});
});
3 changes: 3 additions & 0 deletions src/static/helpers/fetchHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {ClientConfigInit} from '../clientConfig';
* @param options.method? - The request HTTP operation. 'GET' is the default if no method is provided.
* @param options.headers? - Headers that are added to the request. Authorization header should be in this argument or in the clientConfig.headers
* @param options.body? - Body that is used for the request
* @param options.signal? - An AbortSignal object that can be used to cancel the fetch request
* @param clientConfig? - Client Configuration object used by the SDK with properties that can affect the fetch call
* @param clientConfig.headers? - Additional headers that are added to the request. Authorization header should be in this argument or in the options?.headers. options?.headers will override any duplicate properties.
* @param clientConfig.fetchOptions? - fetchOptions that are passed onto the fetch request
Expand All @@ -34,6 +35,7 @@ export const doFetch = async <Params extends BaseUriParameters>(
authorization?: string;
} & {[key: string]: string};
body?: BodyInit | globalThis.BodyInit | unknown;
signal?: AbortSignal;
},
clientConfig?: ClientConfigInit<Params>,
rawResponse?: boolean
Expand All @@ -50,6 +52,7 @@ export const doFetch = async <Params extends BaseUriParameters>(
| (BodyInit & (globalThis.BodyInit | null))
| undefined,
method: options?.method ?? 'GET',
...(options?.signal ? {signal: options.signal} : {}),
};

const response = await fetch(url, requestOptions);
Expand Down
31 changes: 31 additions & 0 deletions src/test/crossFetchNode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,3 +372,34 @@ test('throwOnBadResponse flag defaults to false', async () => {

expect(response).toEqual({content: 'not empty'});
});

test('should use signal from fetch options and throw aborted error', async () => {
nock('https://localhost:3000')
.get(
`/search/shopper-search/v1/organizations/${config.parameters.organizationId}/product-search?siteId=${config.parameters.siteId}&q=sony`
)
.matchHeader('authorization', 'Bearer test-auth')
.delayConnection(200)
.reply(200, {}, {'content-type': 'application-json charset=UTF-8'});

const abortController = new AbortController();
const clientConfig: ClientConfigInit<TestConfigParameters> = {
...config,
fetchOptions: {
signal: abortController.signal,
},
};

expect.assertions(1);
const client = new ShopperSearch(clientConfig);
setTimeout(() => abortController.abort(), 100);
await expect(
client.productSearch({
parameters: {q: 'sony'},
headers: {authorization: 'Bearer test-auth'},
})
).rejects.toEqual({
message: 'The user aborted a request.',
type: 'aborted',
});
});
7 changes: 7 additions & 0 deletions templates/operations.ts.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
{{/each}}
* @param headers - An object literal of key value pairs of the headers to be
* sent with this request.
* @param signal - An AbortSignal object that can be used to cancel the fetch request
{{#if (isRequestWithPayload request)}}
* @param body - The data to send as the request body.
{{/if}}
Expand All @@ -36,6 +37,7 @@
{{/each}}
} & { [key in `c_${string}`]: any }, ConfigParameters>,
headers?: { [key: string]: string },
signal?: AbortSignal,
{{#if (isRequestWithPayload request)}}
body: {{{getPayloadTypeFromRequest request}}}
{{/if}}
Expand All @@ -55,6 +57,7 @@
{{/each}}
* @param headers - An object literal of key value pairs of the headers to be
* sent with this request.
* @param signal - An AbortSignal object that can be used to cancel the fetch request
{{#if (isRequestWithPayload request)}}
* @param body - The data to send as the request body.
{{/if}}
Expand All @@ -75,6 +78,7 @@
{{/each}}
} & { [key in `c_${string}`]: any }, ConfigParameters>,
headers?: { [key: string]: string },
signal?: AbortSignal,
{{#if (isRequestWithPayload request)}}
body: {{{getPayloadTypeFromRequest request}}}
{{/if}}
Expand All @@ -95,6 +99,7 @@
{{/each}}
* @param headers - An object literal of key value pairs of the headers to be
* sent with this request.
* @param signal - An AbortSignal object that can be used to cancel the fetch request
{{#if (isRequestWithPayload request)}}
* @param body - The data to send as the request body.
{{/if}}
Expand All @@ -116,6 +121,7 @@
{{/each}}
} & { [key in `c_${string}`]: any }, ConfigParameters>,
headers?: { [key: string]: string },
signal?: AbortSignal,
{{#if (isRequestWithPayload request)}}
body: {{{getPayloadTypeFromRequest request}}}
{{/if}}
Expand Down Expand Up @@ -193,6 +199,7 @@
{
method: "{{loud method}}",
headers,
signal: options?.signal,
{{#if (isRequestWithPayload request)}}body: this.clientConfig.transformRequest(options.body, headers){{/if}}
},
this.clientConfig,
Expand Down