From 358e15f886ecd52232caaa19c9397a455bf2b320 Mon Sep 17 00:00:00 2001 From: Kamaz Date: Thu, 8 Jan 2026 09:53:16 +0000 Subject: [PATCH] feat: add configurable auth and API endpoints Add optional authUrl and apiUrl configuration options to CommercetoolsBaseConfig to allow custom endpoint URLs. This enables pointing to mock servers for integration testing while maintaining full backward compatibility. Changes: - Add authUrl and apiUrl optional properties to CommercetoolsBaseConfig - Update CommercetoolsAuthApi to use custom endpoints when provided - Update CommercetoolsApi to use custom endpoints when provided - Both classes fall back to region-based URLs when custom URLs not specified - Add comprehensive test coverage for custom endpoint functionality - Add integration tests verifying requests use custom endpoints correctly All existing tests pass, confirming full backward compatibility. No breaking changes - custom endpoints are optional and default behavior is preserved when not specified. --- src/lib/api/CommercetoolsApi.ts | 5 +- src/lib/auth/CommercetoolsAuthApi.ts | 5 +- src/lib/types.ts | 16 ++++++ src/test/api/CommercetoolsApi.test.ts | 58 ++++++++++++++++++++++ src/test/auth/CommercetoolsAuth.test.ts | 49 ++++++++++++++++++ src/test/auth/CommercetoolsAuthApi.test.ts | 52 +++++++++++++++++++ 6 files changed, 183 insertions(+), 2 deletions(-) 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', () => {