Skip to content
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 6 additions & 4 deletions src/lib/api/CommercetoolsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion src/lib/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { CommercetoolsAuthConfig } from '../auth/index.js'
*/
export interface CommercetoolsApiConfig extends CommercetoolsAuthConfig {
httpsAgent?: https.Agent
clientScopes: string[]
clientScopes?: string[]
}

/**
Expand Down
7 changes: 1 addition & 6 deletions src/lib/auth/CommercetoolsAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

Expand Down
9 changes: 7 additions & 2 deletions src/lib/auth/CommercetoolsAuthApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CommercetoolsGrantResponse> {
public async getClientGrant(scopes?: string[]): Promise<CommercetoolsGrantResponse> {
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),
})
}

Expand Down
2 changes: 1 addition & 1 deletion src/lib/auth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import https from 'https'
export interface CommercetoolsAuthConfig extends CommercetoolsBaseConfig {
refreshIfWithinSecs?: number
customerScopes?: string[]
clientScopes: string[]
clientScopes?: string[]
}

/**
Expand Down
29 changes: 27 additions & 2 deletions src/test/api/CommercetoolsApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
52 changes: 49 additions & 3 deletions src/test/auth/CommercetoolsAuth.test.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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)
Expand Down
28 changes: 28 additions & 0 deletions src/test/auth/CommercetoolsAuthApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down