diff --git a/package.json b/package.json index dd838acf20..d7125d9c7a 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "prepare": "husky", "prettier": "prettier --check \"**/*.{ts,js,json,graphql}\"", "prettify": "prettier --write \"**/*.{ts,js,json,graphql}\"", - "validate": "npm run prettier && npm run lint && pnpm typecheck && pnpm build && npm run test", + "validate": "pnpm prettier && pnpm lint && pnpm typecheck && pnpm build && pnpm test", "docs": "typedoc --readme ./README.md", "semantic-release": "semantic-release", "typecheck": "tsc --noEmit", diff --git a/src/lib/api/CommercetoolsApi.ts b/src/lib/api/CommercetoolsApi.ts index 5367d38c20..3b0881aacf 100644 --- a/src/lib/api/CommercetoolsApi.ts +++ b/src/lib/api/CommercetoolsApi.ts @@ -3206,10 +3206,12 @@ export class CommercetoolsApi { } else if (typeof config.clientSecret !== 'string') { errors.push('The `clientSecret` property must be a string') } - if (!Array.isArray(config.clientScopes)) { - errors.push('The `clientScopes` property must be an array') - } else if (config.clientScopes.length === 0) { - errors.push('The `clientScopes` property must have at least 1 scope defined') + if (config.clientScopes) { + if (!Array.isArray(config.clientScopes)) { + errors.push('The `clientScopes` property must be an array') + } else if (config.clientScopes.length === 0) { + errors.push('The `clientScopes` property must have at least 1 scope defined') + } } if (!config.region) { errors.push('The `region` property is empty') diff --git a/src/lib/api/types.ts b/src/lib/api/types.ts index e3c5319e8d..4508fd4cba 100644 --- a/src/lib/api/types.ts +++ b/src/lib/api/types.ts @@ -6,7 +6,7 @@ import { CommercetoolsAuthConfig } from '../auth/index.js' */ export interface CommercetoolsApiConfig extends CommercetoolsAuthConfig { httpsAgent?: https.Agent - clientScopes: string[] + clientScopes?: string[] } /** diff --git a/src/lib/auth/CommercetoolsAuth.ts b/src/lib/auth/CommercetoolsAuth.ts index 3a0ca7b94d..e1ced96b9d 100644 --- a/src/lib/auth/CommercetoolsAuth.ts +++ b/src/lib/auth/CommercetoolsAuth.ts @@ -17,7 +17,7 @@ import { CommercetoolsAuthApi } from './CommercetoolsAuthApi.js' * defined on {@see configDefaults}. */ interface Config extends CommercetoolsAuthConfig { - clientScopes: string[] + clientScopes?: string[] refreshIfWithinSecs: number timeoutMs: number storeKey?: string @@ -99,11 +99,6 @@ export class CommercetoolsAuth { */ constructor(config: CommercetoolsAuthConfig) { this.config = { ...configDefaults, ...config } - - if (!this.config.clientScopes.length) { - throw new CommercetoolsError('`config.clientScopes` must contain at least one scope') - } - this.api = new CommercetoolsAuthApi(config) } diff --git a/src/lib/auth/CommercetoolsAuthApi.ts b/src/lib/auth/CommercetoolsAuthApi.ts index 843428e122..5c77a934d0 100644 --- a/src/lib/auth/CommercetoolsAuthApi.ts +++ b/src/lib/auth/CommercetoolsAuthApi.ts @@ -57,10 +57,15 @@ export class CommercetoolsAuthApi { * Get a new client grant: * https://docs.commercetools.com/api/authorization#client-credentials-flow */ - public async getClientGrant(scopes: string[]): Promise { + public async getClientGrant(scopes?: string[]): Promise { + if (scopes?.length) { + return this.post('/token', { + grant_type: GrantType.CLIENT_CREDENTIALS, + scope: scopeArrayToRequestString(scopes, this.config.projectKey), + }) + } return this.post('/token', { grant_type: GrantType.CLIENT_CREDENTIALS, - scope: scopeArrayToRequestString(scopes, this.config.projectKey), }) } diff --git a/src/lib/auth/types.ts b/src/lib/auth/types.ts index 525b470af8..b16abda6d1 100644 --- a/src/lib/auth/types.ts +++ b/src/lib/auth/types.ts @@ -7,7 +7,7 @@ import https from 'https' export interface CommercetoolsAuthConfig extends CommercetoolsBaseConfig { refreshIfWithinSecs?: number customerScopes?: string[] - clientScopes: string[] + clientScopes?: string[] } /** diff --git a/src/test/api/CommercetoolsApi.test.ts b/src/test/api/CommercetoolsApi.test.ts index 933eb28893..2c17c687f2 100644 --- a/src/test/api/CommercetoolsApi.test.ts +++ b/src/test/api/CommercetoolsApi.test.ts @@ -4541,11 +4541,25 @@ describe('CommercetoolsApi', () => { }) it('should throw an error if the `clientScopes` property is not an array', () => { - expect(() => CommercetoolsApi.validateConfig({ ...defaultConfig, clientScopes: 'test' })).toThrowError() + expect(() => CommercetoolsApi.validateConfig({ ...defaultConfig, clientScopes: 'test' })).toThrowError( + 'The `clientScopes` property must be an array', + ) }) it('should throw an error if the `clientScopes` array does not contain at least one item', () => { - expect(() => CommercetoolsApi.validateConfig({ ...defaultConfig, clientScopes: [] })).toThrowError() + expect(() => CommercetoolsApi.validateConfig({ ...defaultConfig, clientScopes: [] })).toThrowError( + 'The `clientScopes` property must have at least 1 scope defined', + ) + }) + + it('should not throw an error if the `clientScopes` property is undefined', () => { + expect(() => CommercetoolsApi.validateConfig({ ...defaultConfig, clientScopes: undefined })).not.toThrowError() + }) + + it('should not throw an error if the `clientScopes` property is omitted', () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { clientScopes: _omitted, ...configWithoutScopes } = defaultConfig + expect(() => CommercetoolsApi.validateConfig(configWithoutScopes)).not.toThrowError() }) it('should throw an error if the `region` property is falsy', () => { @@ -4577,6 +4591,17 @@ describe('CommercetoolsApi', () => { }), ).not.toThrowError() }) + + it('should not throw an error when all required properties are populated and `clientScopes` is omitted', () => { + expect(() => + CommercetoolsApi.validateConfig({ + clientId: 'test', + clientSecret: 'test', + projectKey: 'test', + region: 'europe_gcp', + }), + ).not.toThrowError() + }) }) describe('OrderEdit', () => { diff --git a/src/test/auth/CommercetoolsAuth.test.ts b/src/test/auth/CommercetoolsAuth.test.ts index 6b0edf8582..8af7fec0a6 100644 --- a/src/test/auth/CommercetoolsAuth.test.ts +++ b/src/test/auth/CommercetoolsAuth.test.ts @@ -1,5 +1,5 @@ import nock from 'nock' -import { CommercetoolsAuth, CommercetoolsError, Region } from '../../lib/index.js' +import { CommercetoolsAuth, Region } from '../../lib/index.js' import { CommercetoolsGrantResponse } from '../../lib/auth/types.js' import FakeTimers, { Clock } from '@sinonjs/fake-timers' @@ -80,10 +80,18 @@ describe('CommercetoolsAuth', () => { }) describe('constructor', () => { - it('should throw if there are no client scopes defined on the config object', () => { + it('should not throw if `clientScopes` is an empty array', () => { expect(() => { new CommercetoolsAuth({ ...defaultConfig, clientScopes: [] }) - }).toThrow(new CommercetoolsError('`config.clientScopes` must contain at least one scope')) + }).not.toThrow() + }) + + it('should not throw if `clientScopes` is undefined on the config object', () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { clientScopes: _omitted, ...configWithoutScopes } = defaultConfig + expect(() => { + new CommercetoolsAuth(configWithoutScopes as any) + }).not.toThrow() }) it('should use the expected defaults for optional config properties', () => { @@ -196,6 +204,44 @@ describe('CommercetoolsAuth', () => { clock.uninstall() }) + it('should send no scope parameter when `clientScopes` is undefined on the config', async () => { + const clock = installClock(new Date('2020-01-01T09:35:23.000')) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { clientScopes: _omitted, ...configWithoutScopes } = defaultConfig + const auth = new CommercetoolsAuth(configWithoutScopes as any) + const scope = nockGetClientGrant('grant_type=client_credentials') + + 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 send no scope parameter when `clientScopes` is an empty array on the config', async () => { + const clock = installClock(new Date('2020-01-01T09:35:23.000')) + const auth = new CommercetoolsAuth({ ...defaultConfig, clientScopes: [] }) + const scope = nockGetClientGrant('grant_type=client_credentials') + + 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 return the cached grant if it has not expired', async () => { const clock = installClock(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 b2a685554d..6daa4916c5 100644 --- a/src/test/auth/CommercetoolsAuthApi.test.ts +++ b/src/test/auth/CommercetoolsAuthApi.test.ts @@ -97,6 +97,34 @@ describe('CommercetoolsAuthApi', () => { scope.isDone() expect(grant).toEqual(defaultResponseToken) }) + + it('should send no scope parameter when no scopes are provided', async () => { + const scope = nock('https://auth.us-east-2.aws.commercetools.com', { + encodedQueryParams: true, + }) + .post('/oauth/token', 'grant_type=client_credentials') + .reply(200, defaultResponseToken) + const auth = new CommercetoolsAuthApi(defaultConfig) + + const grant = await auth.getClientGrant() + + scope.isDone() + expect(grant).toEqual(defaultResponseToken) + }) + + it('should send no scope parameter when an empty scope array is provided', async () => { + const scope = nock('https://auth.us-east-2.aws.commercetools.com', { + encodedQueryParams: true, + }) + .post('/oauth/token', 'grant_type=client_credentials') + .reply(200, defaultResponseToken) + const auth = new CommercetoolsAuthApi(defaultConfig) + + const grant = await auth.getClientGrant([]) + + scope.isDone() + expect(grant).toEqual(defaultResponseToken) + }) }) describe('refreshGrant', () => {