diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 54f0c86b6..48e5bb2b8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,10 +56,3 @@ jobs: - name: Tests shell: bash run: npm run test:ci - - - name: Upload coverage - if: matrix.node-version == '18.17' - uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # pin@4.5.0 - with: - token: ${{ secrets.CODECOV_TOKEN }} - \ No newline at end of file diff --git a/src/auth/id-token-validator.ts b/src/auth/id-token-validator.ts index 58b927014..a043c7488 100644 --- a/src/auth/id-token-validator.ts +++ b/src/auth/id-token-validator.ts @@ -102,24 +102,12 @@ export class IDTokenValidator { 'Organization Id (org_id) claim must be a string present in the ID token' ); } - - if (payload.org_id !== organization) { - throw new Error( - `Organization Id (org_id) claim value mismatch in the ID token; expected "${organization}", found "${payload.org_id}"'` - ); - } } else { if (!payload.org_name || typeof payload.org_name !== 'string') { throw new Error( 'Organization Name (org_name) claim must be a string present in the ID token' ); } - - if (payload.org_name !== organization.toLowerCase()) { - throw new Error( - `Organization Name (org_name) claim value mismatch in the ID token; expected "${organization}", found "${payload.org_name}"'` - ); - } } } diff --git a/src/management/__generated/managers/organizations-manager.ts b/src/management/__generated/managers/organizations-manager.ts index 2b2d5ff2d..f697f9789 100644 --- a/src/management/__generated/managers/organizations-manager.ts +++ b/src/management/__generated/managers/organizations-manager.ts @@ -9,6 +9,7 @@ import type { GetInvitations200ResponseOneOfInner, GetMembers200Response, GetOrganizationClientGrants200Response, + GetOrganizationClientGrants200ResponseOneOfInner, GetOrganizationMemberRoles200Response, GetOrganizations200Response, GetOrganizations200ResponseOneOfInner, @@ -17,6 +18,7 @@ import type { PostEnabledConnectionsRequest, PostInvitationsRequest, PostMembersRequest, + PostOrganizationClientGrantsRequest, PostOrganizationMemberRolesRequest, PostOrganizations201Response, PostOrganizationsRequest, @@ -25,10 +27,10 @@ import type { GetMembers200ResponseOneOf, GetMembers200ResponseOneOfInner, GetOrganizationClientGrants200ResponseOneOf, - GetOrganizationClientGrants200ResponseOneOfInner, GetOrganizationMemberRoles200ResponseOneOf, GetOrganizationMemberRoles200ResponseOneOfInner, GetOrganizations200ResponseOneOf, + DeleteClientGrantsByGrantIdRequest, DeleteEnabledConnectionsByConnectionIdRequest, DeleteInvitationsByInvitationIdRequest, DeleteMembersOperationRequest, @@ -49,6 +51,7 @@ import type { PostEnabledConnectionsOperationRequest, PostInvitationsOperationRequest, PostMembersOperationRequest, + PostOrganizationClientGrantsOperationRequest, PostOrganizationMemberRolesOperationRequest, } from '../models/index.js'; @@ -58,6 +61,30 @@ const { BaseAPI } = runtime; * */ export class OrganizationsManager extends BaseAPI { + /** + * Remove a client grant from an organization + * + * @throws {RequiredError} + */ + async deleteClientGrantsByGrantId( + requestParameters: DeleteClientGrantsByGrantIdRequest, + initOverrides?: InitOverride + ): Promise> { + runtime.validateRequiredRequestParams(requestParameters, ['id', 'grant_id']); + + const response = await this.request( + { + path: `/organizations/{id}/client-grants/{grant_id}` + .replace('{id}', encodeURIComponent(String(requestParameters.id))) + .replace('{grant_id}', encodeURIComponent(String(requestParameters.grant_id))), + method: 'DELETE', + }, + initOverrides + ); + + return runtime.VoidApiResponse.fromResponse(response); + } + /** * Delete connections from an organization * @@ -859,6 +886,38 @@ export class OrganizationsManager extends BaseAPI { return runtime.VoidApiResponse.fromResponse(response); } + /** + * Associate a client grant with an organization + * + * @throws {RequiredError} + */ + async postOrganizationClientGrants( + requestParameters: PostOrganizationClientGrantsOperationRequest, + bodyParameters: PostOrganizationClientGrantsRequest, + initOverrides?: InitOverride + ): Promise> { + runtime.validateRequiredRequestParams(requestParameters, ['id']); + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + const response = await this.request( + { + path: `/organizations/{id}/client-grants`.replace( + '{id}', + encodeURIComponent(String(requestParameters.id)) + ), + method: 'POST', + headers: headerParameters, + body: bodyParameters, + }, + initOverrides + ); + + return runtime.JSONApiResponse.fromResponse(response); + } + /** * Assign one or more roles to a given user that will be applied in the context of the provided organization * diff --git a/src/management/__generated/models/index.ts b/src/management/__generated/models/index.ts index 9b77657e0..595644a93 100644 --- a/src/management/__generated/models/index.ts +++ b/src/management/__generated/models/index.ts @@ -10513,6 +10513,16 @@ export interface PostMembersRequest { */ members: Array; } +/** + * + */ +export interface PostOrganizationClientGrantsRequest { + /** + * A Client Grant ID to add to the organization. + * + */ + grant_id: string; +} /** * */ @@ -14924,6 +14934,21 @@ export interface GetLogsByIdRequest { */ id: string; } +/** + * + */ +export interface DeleteClientGrantsByGrantIdRequest { + /** + * Organization identifier + * + */ + id: string; + /** + * The Client Grant ID to remove from the organization + * + */ + grant_id: string; +} /** * */ @@ -15319,6 +15344,16 @@ export interface PostMembersOperationRequest { */ id: string; } +/** + * + */ +export interface PostOrganizationClientGrantsOperationRequest { + /** + * Organization identifier + * + */ + id: string; +} /** * */ diff --git a/test/auth/id-token-validator.test.ts b/test/auth/id-token-validator.test.ts index 2f0a32a81..085c905d4 100644 --- a/test/auth/id-token-validator.test.ts +++ b/test/auth/id-token-validator.test.ts @@ -386,60 +386,4 @@ describe('id-token-validator', () => { 'Organization Name (org_name) claim must be a string present in the ID token' ); }); - - it('should throw when org id claim doesnt match org expected', async () => { - const idTokenValidator = new IDTokenValidator({ - domain: DOMAIN, - clientId: CLIENT_ID, - clientSecret: CLIENT_SECRET, - }); - - const jwt = await sign({ payload: { org_id: 'org_1234' } }); - - await expect(idTokenValidator.validate(jwt, { organization: 'org_123' })).rejects.toThrow( - 'Organization Id (org_id) claim value mismatch in the ID token; expected "org_123", found "org_1234' - ); - }); - - it('should throw when org name claim doesnt match org expected', async () => { - const idTokenValidator = new IDTokenValidator({ - domain: DOMAIN, - clientId: CLIENT_ID, - clientSecret: CLIENT_SECRET, - }); - - const jwt = await sign({ payload: { org_name: 'notExpectedOrg' } }); - - await expect(idTokenValidator.validate(jwt, { organization: 'testorg' })).rejects.toThrow( - 'Organization Name (org_name) claim value mismatch in the ID token; expected "testorg", found "notExpectedOrg' - ); - }); - - it('should NOT throw when org_id matches expected organization', async () => { - const idTokenValidator = new IDTokenValidator({ - domain: DOMAIN, - clientId: CLIENT_ID, - clientSecret: CLIENT_SECRET, - }); - - const jwt = await sign({ payload: { org_id: 'org_123' } }); - - await expect( - idTokenValidator.validate(jwt, { organization: 'org_123' }) - ).resolves.not.toThrow(); - }); - - it('should NOT throw when org_name matches expected organization', async () => { - const idTokenValidator = new IDTokenValidator({ - domain: DOMAIN, - clientId: CLIENT_ID, - clientSecret: CLIENT_SECRET, - }); - - const jwt = await sign({ payload: { org_name: 'testorg' } }); - - await expect( - idTokenValidator.validate(jwt, { organization: 'testOrg' }) - ).resolves.not.toThrow(); - }); }); diff --git a/test/auth/oauth.test.ts b/test/auth/oauth.test.ts index 24ca8d13d..f879d2f09 100644 --- a/test/auth/oauth.test.ts +++ b/test/auth/oauth.test.ts @@ -409,38 +409,4 @@ describe('OAuth (with ID Token validation)', () => { ); nockDone(); }); - - it('should throw for invalid organization id', async () => { - const { nockDone } = await nockBack('auth/fixtures/oauth.json', { - before: await withIdToken({ - ...opts, - payload: { org_id: 'org_123' }, - }), - }); - const oauth = new OAuth(opts); - await expect( - oauth.refreshTokenGrant( - { refresh_token: 'test-refresh-token' }, - { idTokenValidateOptions: { organization: 'org_1235' } } - ) - ).rejects.toThrowError(/\(org_id\) claim value mismatch in the ID token/); - nockDone(); - }); - - it('should throw for invalid organization name', async () => { - const { nockDone } = await nockBack('auth/fixtures/oauth.json', { - before: await withIdToken({ - ...opts, - payload: { org_name: 'org123' }, - }), - }); - const oauth = new OAuth(opts); - await expect( - oauth.refreshTokenGrant( - { refresh_token: 'test-refresh-token' }, - { idTokenValidateOptions: { organization: 'org1235' } } - ) - ).rejects.toThrowError(/\(org_name\) claim value mismatch in the ID token/); - nockDone(); - }); }); diff --git a/test/management/organizations.test.ts b/test/management/organizations.test.ts index 1ad799bce..a1a5c5a15 100644 --- a/test/management/organizations.test.ts +++ b/test/management/organizations.test.ts @@ -9,21 +9,21 @@ import { GetOrganizationClientGrantsRequest, GetOrganizationClientGrants200Response, ApiResponse, + DeleteClientGrantsByGrantIdRequest, + GetOrganizationClientGrants200ResponseOneOfInner, } from '../../src/index.js'; -describe('OrganizationsManager', () => { - let organizations: OrganizationsManager; +import { checkMethod } from '../utils/index.js'; +describe('OrganizationsManager', () => { let request: nock.Scope; const token = 'TOKEN'; - beforeAll(() => { - const client = new ManagementClient({ - domain: 'tenant.auth0.com', - token: token, - }); - organizations = client.organizations; + const client = new ManagementClient({ + domain: 'tenant.auth0.com', + token: token, }); + const organizations: OrganizationsManager = client.organizations; describe('#constructor', () => { it('should throw an error when no base URL is provided', () => { @@ -1474,4 +1474,35 @@ describe('OrganizationsManager', () => { }); }); }); + + describe('#deleteClientGrantsById', () => { + const requestParameters: DeleteClientGrantsByGrantIdRequest = { + id: 'org_123', + grant_id: 'grant_id', + }; + const operation = organizations.deleteClientGrantsByGrantId(requestParameters); + const expectedResponse = undefined; + const uri = `/organizations/{id}/client-grants/{grant_id}` + .replace('{id}', encodeURIComponent(String(requestParameters.id))) + .replace('{grant_id}', encodeURIComponent(String(requestParameters.grant_id))); + const method = 'delete'; + + checkMethod({ operation, expectedResponse, uri, method }); + }); + + describe('#postOrganizationClientGrants', () => { + const requestParameters = { id: 'org_123' }; + const requestBody = { grant_id: 'grant_id' }; + const operation = organizations.postOrganizationClientGrants(requestParameters, requestBody); + const expectedResponse: GetOrganizationClientGrants200ResponseOneOfInner = < + GetOrganizationClientGrants200ResponseOneOfInner + >{}; + const uri = `/organizations/{id}/client-grants`.replace( + '{id}', + encodeURIComponent(String(requestParameters.id)) + ); + const method = 'post'; + + checkMethod({ operation, expectedResponse, uri, method, requestBody }); + }); }); diff --git a/test/utils/index.ts b/test/utils/index.ts index fa9c9f608..13c37728b 100644 --- a/test/utils/index.ts +++ b/test/utils/index.ts @@ -1,2 +1,3 @@ export * from './extractParts.js'; export * from './withIdToken.js'; +export * from './wrapperTestUtils.js'; diff --git a/test/utils/wrapperTestUtils.ts b/test/utils/wrapperTestUtils.ts new file mode 100644 index 000000000..2ba06fbf1 --- /dev/null +++ b/test/utils/wrapperTestUtils.ts @@ -0,0 +1,125 @@ +// tests.util.ts +// this file contains commmon test functions that are used to test sdk endpoints +// since the management api managers are essentially wrappers around the REST endpoints, +// these functions end up being repeated for all the managers, this file aims to reduce repetition +// it performs basic sanity checks, input output checks and error handling checks + +import nock, { RequestBodyMatcher } from 'nock'; +import { ApiResponse } from '../../src/lib/models.js'; + +const DOMAIN = `tenant.auth0.com`; +const API_URL = `https://${DOMAIN}/api/v2`; + +// this is not technically not required as type checking will automatically check fot this +// but including it for the sake of completeness +/** + * Checks if the given operation returns a promise when no callback is provided. + * + * @template T - The type of the response expected from the promise. + * @param {any | Promise>} operation - The operation to check, which can be either a promise or any other type. + * @returns {void} + */ +export function checkForPromise(operation: any | Promise>): void { + it('should return a promise if no callback is given', (done) => { + expect(operation instanceof Promise).toBeTruthy(); + operation.then(done.bind(null, null)).catch(done.bind(null, null)); + }); +} + +/** + * Utility function to test if an operation correctly handles errors. + * + * @template T - The type of the response expected from the operation. + * @param {Promise>} operation - The promise representing the operation to be tested. + * @returns {void} - This function does not return anything. + * + * @example + * ```typescript + * checkErrorHandler(someApiOperation); + * ``` + */ +export function checkErrorHandler(operation: Promise>): void { + it('should pass any errors to the promise catch handler', () => { + nock.cleanAll(); + + return operation.catch((err) => { + expect(err).toBeDefined(); + }); + }); +} + +/** + * Verifies that a given operation makes a request to the specified endpoint. + * + * @template T - The type of the result of the operation. + * @param operation - A promise representing the operation to be checked. + * @param request - The nock scope representing the expected request. + */ +export function checkRequestInterceptor(operation: Promise, request: nock.Scope): void { + it(`should make a request to the endpoint`, async () => { + await operation; + expect(request.isDone()).toBeTruthy(); + }); +} + +/** + * Tests an asynchronous operation by comparing its result to an expected response. + * + * @template T - The type of the expected response data. + * @param {Promise>} operation - The asynchronous operation to be tested. + * @param {T} expectedResponse - The expected response data to compare against the operation's result. + */ +export function checkOperation(operation: Promise>, expectedResponse: T): void { + it('should test the method', async () => { + const result = await operation; + expect(result.data).toEqual(expectedResponse); + }); +} + +export type CheckMethodParams = { + operation: Promise>; + expectedResponse: any; + uri: string | RegExp | { (uri: string): boolean }; + method: string; + requestBody?: RequestBodyMatcher | any; +}; + +// this function combines the above functions to check an SDK manager method. +/** + * Checks the given manager method by intercepting the request and validating the response. + * + * Following checks are performed: + * 1. The operation is a promise. + * 2. The operation is rejected in case of an error. + * 3. The request is made to the specified endpoint in the given method. + * 4. The response from the operation is as expected. + * + * @template T - The type of the expected response. + * @param {Object} params - The parameters for the checkMethod function. + * @param {Promise>} params.operation - The operation to be tested. + * @param {any} params.expectedResponse - The expected response from the operation. + * @param {string | RegExp | ((uri: string) => boolean)} params.uri - The URI to intercept. + * @param {string} params.method - The HTTP method to intercept (e.g., 'GET', 'POST'). + * @param {RequestBodyMatcher | any} [params.requestBody] - The optional request body to match. + */ +export const checkMethod = ({ + operation, + expectedResponse, + uri, + method, + requestBody, +}: CheckMethodParams): void => { + // nock the API with success scenario + let request: nock.Scope = nock(API_URL) + .intercept(uri, method, requestBody) + .reply(200, expectedResponse); + + // check for various success checks + checkForPromise(operation); + checkRequestInterceptor(operation, request); + checkOperation(operation, expectedResponse); + + // nock the API with error scenario + request = nock(API_URL).intercept(uri, method, requestBody).reply(500); + checkErrorHandler(operation); +};