diff --git a/src/lib/api/CommercetoolsApi.ts b/src/lib/api/CommercetoolsApi.ts index 84910a0226..2055e5c43d 100644 --- a/src/lib/api/CommercetoolsApi.ts +++ b/src/lib/api/CommercetoolsApi.ts @@ -313,7 +313,10 @@ export class CommercetoolsApi { CommercetoolsApi.validateConfig(config) this.config = config this.auth = new CommercetoolsAuth(config) - this.endpoints = REGION_URLS[this.config.region] + this.endpoints = { + auth: config.authUrl ?? REGION_URLS[this.config.region].auth, + api: config.apiUrl ?? REGION_URLS[this.config.region].api, + } this.requestExecutor = getRequestExecutor({ timeoutMs: config.timeoutMs, httpsAgent: config.httpsAgent, diff --git a/src/lib/auth/CommercetoolsAuthApi.ts b/src/lib/auth/CommercetoolsAuthApi.ts index dc65011021..843428e122 100644 --- a/src/lib/auth/CommercetoolsAuthApi.ts +++ b/src/lib/auth/CommercetoolsAuthApi.ts @@ -39,7 +39,10 @@ export class CommercetoolsAuthApi { constructor(config: CommercetoolsAuthApiConfig) { this.config = config - this.endpoints = REGION_URLS[this.config.region] + this.endpoints = { + auth: config.authUrl ?? REGION_URLS[this.config.region].auth, + api: config.apiUrl ?? REGION_URLS[this.config.region].api, + } this.requestExecutor = getRequestExecutor({ timeoutMs: config.timeoutMs, httpsAgent: config.httpsAgent, diff --git a/src/lib/types.ts b/src/lib/types.ts index d2cced01e6..08688ecc45 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -45,6 +45,22 @@ export interface CommercetoolsBaseConfig extends CommercetoolsHooks { * identify the source of incoming requests. */ systemIdentifier?: string + + /** + * Optional custom authentication endpoint URL. + * If provided, this will be used instead of the region-based auth URL. + * Useful for pointing to mock servers during integration tests. + * @example 'http://localhost:3000/auth' + */ + authUrl?: string + + /** + * Optional custom API endpoint URL. + * If provided, this will be used instead of the region-based API URL. + * Useful for pointing to mock servers during integration tests. + * @example 'http://localhost:3000/api' + */ + apiUrl?: string } export interface CommercetoolsHooks { diff --git a/src/test/api/CommercetoolsApi.test.ts b/src/test/api/CommercetoolsApi.test.ts index b55ca9dc4d..39a65c1bc6 100644 --- a/src/test/api/CommercetoolsApi.test.ts +++ b/src/test/api/CommercetoolsApi.test.ts @@ -96,6 +96,40 @@ describe('CommercetoolsApi', () => { it('should bubble up the error if `validateConfig` method throws an error', () => { expect(() => new CommercetoolsApi({ ...defaultConfig, projectKey: '' })).toThrowError() }) + + it('should allow custom API endpoint to be configured', () => { + const api = new CommercetoolsApi({ + ...defaultConfig, + apiUrl: 'http://localhost:4000/api', + }) + expect(api.endpoints.auth).toBe('https://auth.europe-west1.gcp.commercetools.com') + expect(api.endpoints.api).toBe('http://localhost:4000/api') + }) + + it('should use region-based endpoint when custom apiUrl is not provided', () => { + const api = new CommercetoolsApi(defaultConfig) + expect(api.endpoints.auth).toBe('https://auth.europe-west1.gcp.commercetools.com') + expect(api.endpoints.api).toBe('https://api.europe-west1.gcp.commercetools.com') + }) + + it('should allow both custom auth and api endpoints to be configured', () => { + const api = new CommercetoolsApi({ + ...defaultConfig, + authUrl: 'http://localhost:4000/auth', + apiUrl: 'http://localhost:4000/api', + }) + expect(api.endpoints.api).toBe('http://localhost:4000/api') + expect(api.endpoints.auth).toBe('http://localhost:4000/auth') + }) + + it('should pass custom endpoints to auth instance', () => { + const api = new CommercetoolsApi({ + ...defaultConfig, + authUrl: 'http://localhost:4000/auth', + }) + expect(api.auth.config.authUrl).toBe('http://localhost:4000/auth') + expect(api.endpoints.api).toBe('https://api.europe-west1.gcp.commercetools.com') + }) }) describe('extractCommonRequestOptions', () => { @@ -159,6 +193,30 @@ describe('CommercetoolsApi', () => { expect(product).toEqual({ success: true }) }) + it('should use custom API endpoint when making requests', async () => { + const customApiUrl = 'http://localhost:4000' + const customAuthUrl = 'http://localhost:4000' + nock(customAuthUrl, { + encodedQueryParams: true, + }) + .post('/oauth/token', 'grant_type=client_credentials&scope=defaultClientScope1%3Atest-project-key') + .reply(200, defaultClientGrantResponse) + nock(customApiUrl, { + encodedQueryParams: true, + }) + .get('/test-project-key/stores') + .reply(200, { success: true }) + const api = new CommercetoolsApi({ + ...defaultConfig, + apiUrl: customApiUrl, + authUrl: customAuthUrl, + }) + + const result = await api.queryStores() + + expect(result).toEqual({ success: true }) + }) + it('should make a GET request to the correct endpoint with the passed in parameters in the querystring', async () => { nock('https://api.europe-west1.gcp.commercetools.com', { encodedQueryParams: true, diff --git a/src/test/auth/CommercetoolsAuth.test.ts b/src/test/auth/CommercetoolsAuth.test.ts index 34997dffd2..97d7441571 100644 --- a/src/test/auth/CommercetoolsAuth.test.ts +++ b/src/test/auth/CommercetoolsAuth.test.ts @@ -92,6 +92,29 @@ describe('CommercetoolsAuth', () => { clientScopes: ['scope3', 'scope4'], }) }) + + it('should allow custom auth endpoint to be configured', () => { + const auth = new CommercetoolsAuth({ + ...defaultConfig, + authUrl: 'http://localhost:4000/auth', + }) + expect((auth as any).api.endpoints.auth).toBe('http://localhost:4000/auth') + }) + + it('should use region-based endpoint when custom authUrl is not provided', () => { + const auth = new CommercetoolsAuth(defaultConfig) + expect((auth as any).api.endpoints.auth).toBe('https://auth.us-east-2.aws.commercetools.com') + }) + + it('should allow both custom auth and api endpoints to be configured', () => { + const auth = new CommercetoolsAuth({ + ...defaultConfig, + authUrl: 'http://localhost:4000/auth', + apiUrl: 'http://localhost:4000/api', + }) + expect((auth as any).api.endpoints.auth).toBe('http://localhost:4000/auth') + expect((auth as any).api.endpoints.api).toBe('http://localhost:4000/api') + }) }) describe('getClientGrant', () => { @@ -157,6 +180,32 @@ describe('CommercetoolsAuth', () => { clock.uninstall() }) + it('should use custom auth endpoint when making requests', async () => { + const clock = FakeTimers.install({ now: new Date('2020-01-01T09:35:23.000') }) + const customAuthUrl = 'http://localhost:4000' + const auth = new CommercetoolsAuth({ + ...defaultConfig, + authUrl: customAuthUrl, + }) + const scope = nock(customAuthUrl, { + encodedQueryParams: true, + }) + .post('/oauth/token', 'grant_type=client_credentials&scope=defaultClientScope1%3Atest-project-key') + .reply(200, defaultClientGrantResponse) + + const token = await auth.getClientGrant() + + scope.isDone() + expect(token).toEqual({ + accessToken: 'test-access-token', + scopes: ['scope1', 'scope2', 'scope3'], + expiresIn: 172800, + expiresAt: new Date('2020-01-03T09:35:23.000'), + customerId: '123456', + }) + clock.uninstall() + }) + it('should wait on a single promise when two requests are made at the same time', async () => { const clock = FakeTimers.install({ now: new Date('2020-01-01T09:35:23.000') }) const auth = new CommercetoolsAuth(defaultConfig) diff --git a/src/test/auth/CommercetoolsAuthApi.test.ts b/src/test/auth/CommercetoolsAuthApi.test.ts index cc87b46303..941f5718d1 100644 --- a/src/test/auth/CommercetoolsAuthApi.test.ts +++ b/src/test/auth/CommercetoolsAuthApi.test.ts @@ -35,6 +35,40 @@ describe('CommercetoolsAuthApi', () => { nock.disableNetConnect() }) + describe('constructor', () => { + it('should use region-based endpoint when custom authUrl is not provided', () => { + const auth = new CommercetoolsAuthApi(defaultConfig) + expect(auth.endpoints.auth).toBe('https://auth.us-east-2.aws.commercetools.com') + }) + + it('should use custom auth endpoint when provided', () => { + const auth = new CommercetoolsAuthApi({ + ...defaultConfig, + authUrl: 'http://localhost:4000/auth', + }) + expect(auth.endpoints.auth).toBe('http://localhost:4000/auth') + }) + + it('should use custom auth and api endpoints when both are provided', () => { + const auth = new CommercetoolsAuthApi({ + ...defaultConfig, + authUrl: 'http://localhost:4000/auth', + apiUrl: 'http://localhost:4000/api', + }) + expect(auth.endpoints.auth).toBe('http://localhost:4000/auth') + expect(auth.endpoints.api).toBe('http://localhost:4000/api') + }) + + it('should fall back to region-based API endpoint when only custom authUrl is provided', () => { + const auth = new CommercetoolsAuthApi({ + ...defaultConfig, + authUrl: 'http://localhost:4000/auth', + }) + expect(auth.endpoints.auth).toBe('http://localhost:4000/auth') + expect(auth.endpoints.api).toBe('https://api.us-east-2.aws.commercetools.com') + }) + }) + describe('getClientGrant', () => { it('should call commercetools with the expected request', async () => { const scope = nock('https://auth.us-east-2.aws.commercetools.com', { @@ -49,6 +83,24 @@ describe('CommercetoolsAuthApi', () => { scope.isDone() expect(grant).toEqual(defaultResponseToken) }) + + it('should use custom auth endpoint when making requests', async () => { + const customAuthUrl = 'http://localhost:4000' + const scope = nock(customAuthUrl, { + encodedQueryParams: true, + }) + .post('/oauth/token', 'grant_type=client_credentials&scope=scope1%3Atest-project-key') + .reply(200, defaultResponseToken) + const auth = new CommercetoolsAuthApi({ + ...defaultConfig, + authUrl: customAuthUrl, + }) + + const grant = await auth.getClientGrant(['scope1']) + + scope.isDone() + expect(grant).toEqual(defaultResponseToken) + }) }) describe('refreshGrant', () => {