-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat: support custom APIs @W-15111169@ (#149)
* add custom endpoint helper * refactor * implement runFetchHelper * use runFetchHelper in operations handlebar template * fix broken calls * fix linting errors and most type errors * Refactor and add tsdoc comments * refactor and add unit test * add comment for test coverage * add unit test and update changelog * update type for body * address PR comments * add example in README * update types and allow baseUri as argument * lint * add check in test for response status code * combine params into 1 object and pull out custom path params into options * default application/json as content type and add test * add check for clientConfig headers * lint * use siteId from clientConfig * update README * remove comment * pull out default base URI into config file
- Loading branch information
1 parent
fa49335
commit 93358e3
Showing
11 changed files
with
724 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
/* | ||
* Copyright (c) 2024, Salesforce, Inc. | ||
* All rights reserved. | ||
* SPDX-License-Identifier: BSD-3-Clause | ||
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
*/ | ||
// eslint-disable-next-line import/prefer-default-export | ||
export const CUSTOM_API_DEFAULT_BASE_URI = | ||
'https://{shortCode}.api.commercecloud.salesforce.com/custom/{apiName}/{apiVersion}'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,252 @@ | ||
/* | ||
* Copyright (c) 2024, salesforce.com, inc. | ||
* All rights reserved. | ||
* SPDX-License-Identifier: BSD-3-Clause | ||
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
*/ | ||
|
||
import nock from 'nock'; | ||
import {callCustomEndpoint, CustomParams} from './customApi'; | ||
import * as fetchHelper from './fetchHelper'; | ||
import ClientConfig from '../clientConfig'; | ||
|
||
describe('callCustomEndpoint', () => { | ||
beforeEach(() => { | ||
jest.restoreAllMocks(); | ||
nock.cleanAll(); | ||
}); | ||
|
||
const clientConfig = new ClientConfig<CustomParams>({ | ||
parameters: { | ||
shortCode: 'short_code', | ||
organizationId: 'organization_id', | ||
clientId: 'client_id', | ||
siteId: 'site_id', | ||
}, | ||
}); | ||
|
||
const options = { | ||
method: 'POST', | ||
parameters: { | ||
queryParam1: 'query parameter 1', | ||
queryParam2: 'query parameter 2', | ||
}, | ||
customApiPathParameters: { | ||
apiName: 'api_name', | ||
apiVersion: 'v2', | ||
endpointPath: 'endpoint_path', | ||
}, | ||
headers: { | ||
'Content-Type': 'text/plain', | ||
authorization: 'Bearer token', | ||
}, | ||
body: 'Hello World', | ||
}; | ||
|
||
const queryParamString = new URLSearchParams({ | ||
...options.parameters, | ||
siteId: clientConfig.parameters.siteId as string, | ||
}).toString(); | ||
|
||
// helper function that creates a copy of the options object | ||
// and adds siteId to the parameters object that comes from clientConfig | ||
const addSiteIdToOptions = (optionsObj: Record<string, unknown>) => ({ | ||
...optionsObj, | ||
parameters: { | ||
...(optionsObj.parameters as Record<string, unknown>), | ||
siteId: clientConfig.parameters.siteId, | ||
}, | ||
}); | ||
|
||
test('throws an error when required path parameters are not passed', () => { | ||
const copyOptions = { | ||
...options, | ||
// omit endpointPath | ||
customApiPathParameters: { | ||
apiName: 'api_name', | ||
}, | ||
}; | ||
|
||
expect(async () => { | ||
// eslint-disable-next-line | ||
// @ts-ignore <-- we know it'll complain since we removed endpointPath | ||
await callCustomEndpoint({options: copyOptions, clientConfig}); | ||
}) | ||
.rejects.toThrow( | ||
'Missing required property needed in options.customApiPathParameters or clientConfig.parameters: endpointPath' | ||
) | ||
.finally(() => 'resolve promise'); | ||
}); | ||
|
||
test('sets api version to "v1" if not provided', async () => { | ||
const copyOptions = { | ||
...options, | ||
// omit apiVersion | ||
customApiPathParameters: { | ||
endpointPath: 'endpoint_path', | ||
apiName: 'api_name', | ||
}, | ||
}; | ||
|
||
const {shortCode, organizationId} = clientConfig.parameters; | ||
const {apiName, endpointPath} = copyOptions.customApiPathParameters; | ||
|
||
const nockBasePath = `https://${shortCode}.api.commercecloud.salesforce.com`; | ||
const nockEndpointPath = `/custom/${apiName}/v1/organizations/${ | ||
organizationId as string | ||
}/${endpointPath}`; | ||
nock(nockBasePath).post(nockEndpointPath).query(true).reply(200); | ||
|
||
const expectedUrl = `${ | ||
nockBasePath + nockEndpointPath | ||
}?${queryParamString}`; | ||
const expectedOptions = addSiteIdToOptions(copyOptions); | ||
|
||
const doFetchSpy = jest.spyOn(fetchHelper, 'doFetch'); | ||
|
||
const response = (await callCustomEndpoint({ | ||
options: copyOptions, | ||
clientConfig, | ||
rawResponse: true, | ||
})) as Response; | ||
|
||
expect(response.status).toBe(200); | ||
expect(doFetchSpy).toBeCalledTimes(1); | ||
expect(doFetchSpy).toBeCalledWith( | ||
expectedUrl, | ||
expectedOptions, | ||
expect.anything(), | ||
true | ||
); | ||
expect(expectedUrl).toContain('/v1/'); | ||
}); | ||
|
||
test('doFetch is called with the correct arguments', async () => { | ||
const {shortCode, organizationId} = clientConfig.parameters; | ||
const {apiName, endpointPath} = options.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).reply(200); | ||
|
||
const expectedUrl = `${ | ||
nockBasePath + nockEndpointPath | ||
}?${queryParamString}`; | ||
const expectedOptions = addSiteIdToOptions(options); | ||
|
||
const expectedClientConfig = { | ||
...clientConfig, | ||
baseUri: | ||
'https://{shortCode}.api.commercecloud.salesforce.com/custom/{apiName}/{apiVersion}', | ||
}; | ||
|
||
const doFetchSpy = jest.spyOn(fetchHelper, 'doFetch'); | ||
await callCustomEndpoint({options, clientConfig, rawResponse: true}); | ||
expect(doFetchSpy).toBeCalledTimes(1); | ||
expect(doFetchSpy).toBeCalledWith( | ||
expectedUrl, | ||
expectedOptions, | ||
expectedClientConfig, | ||
true | ||
); | ||
}); | ||
|
||
test('uses path params from options and clientConfig, prioritizing options', async () => { | ||
const copyClientConfig = { | ||
...clientConfig, | ||
// Only shortCode will be used | ||
parameters: { | ||
endpointPath: 'clientConfig_endpoint_path', | ||
apiName: 'clientConfig_api_name', | ||
shortCode: 'clientconfig_shortcode', | ||
apiVersion: 'v2', | ||
organizationId: 'clientConfig_organizationId', | ||
siteId: 'site_id', | ||
}, | ||
}; | ||
|
||
const copyOptions = { | ||
...options, | ||
// these parameters will be prioritzed | ||
customApiPathParameters: { | ||
endpointPath: 'customApiPathParameters_endpoint_path', | ||
apiName: 'customApiPathParameters_api_name', | ||
apiVersion: 'v3', | ||
organizationId: 'customApiPathParameters_organizationId', | ||
}, | ||
}; | ||
|
||
// nock interception should be using custom API path parameters from options | ||
const {apiName, endpointPath, organizationId, apiVersion} = | ||
copyOptions.customApiPathParameters; | ||
// except shortcode since we didn't implement it in copyOptions.customApiPathParameters | ||
const {shortCode} = copyClientConfig.parameters; | ||
|
||
const nockBasePath = `https://${shortCode}.api.commercecloud.salesforce.com`; | ||
const nockEndpointPath = `/custom/${apiName}/${apiVersion}/organizations/${organizationId}/${endpointPath}`; | ||
nock(nockBasePath).post(nockEndpointPath).query(true).reply(200); | ||
|
||
// expected URL is a mix of both params | ||
const expectedUrl = `${ | ||
nockBasePath + nockEndpointPath | ||
}?${queryParamString}`; | ||
|
||
const doFetchSpy = jest.spyOn(fetchHelper, 'doFetch'); | ||
await callCustomEndpoint({ | ||
options: copyOptions, | ||
clientConfig: copyClientConfig, | ||
}); | ||
expect(doFetchSpy).toBeCalledTimes(1); | ||
expect(doFetchSpy).toBeCalledWith( | ||
expectedUrl, | ||
expect.anything(), | ||
expect.anything(), | ||
undefined | ||
); | ||
}); | ||
|
||
test('uses application/json as default content type if not provided', async () => { | ||
const copyOptions = { | ||
...options, | ||
// exclude Content-Type | ||
headers: { | ||
authorization: 'Bearer token', | ||
}, | ||
}; | ||
|
||
const {apiName, endpointPath, apiVersion} = | ||
copyOptions.customApiPathParameters; | ||
const {shortCode, organizationId} = clientConfig.parameters; | ||
|
||
const expectedJsonHeaders = { | ||
authorization: 'Bearer token', | ||
'Content-Type': 'application/json', | ||
}; | ||
|
||
const nockBasePath = `https://${shortCode}.api.commercecloud.salesforce.com`; | ||
const nockEndpointPath = `/custom/${apiName}/${apiVersion}/organizations/${ | ||
organizationId as string | ||
}/${endpointPath}`; | ||
nock(nockBasePath, { | ||
reqheaders: expectedJsonHeaders, | ||
}) | ||
.post(nockEndpointPath) | ||
.query(true) | ||
.reply(200); | ||
|
||
const doFetchSpy = jest.spyOn(fetchHelper, 'doFetch'); | ||
await callCustomEndpoint({ | ||
options: copyOptions, | ||
clientConfig, | ||
}); | ||
expect(doFetchSpy).toBeCalledTimes(1); | ||
expect(doFetchSpy).toBeCalledWith( | ||
expect.any(String), | ||
expect.objectContaining({headers: expectedJsonHeaders}), | ||
expect.anything(), | ||
undefined | ||
); | ||
}); | ||
}); |
Oops, something went wrong.