From 682b5bd2e7e7503bc6b792f74783ada8405e00bc Mon Sep 17 00:00:00 2001 From: Aman Kumar Date: Tue, 11 Mar 2025 22:52:57 +0530 Subject: [PATCH 1/6] feat: oauth unit test cases --- test/unit/index.js | 1 + test/unit/oauthHandler-test.js | 325 +++++++++++++++++++++++++++++++++ 2 files changed, 326 insertions(+) create mode 100644 test/unit/oauthHandler-test.js diff --git a/test/unit/index.js b/test/unit/index.js index a49ffca1..6cafbd0c 100644 --- a/test/unit/index.js +++ b/test/unit/index.js @@ -44,3 +44,4 @@ require('./variantGroup-test') require('./ungroupedVariants-test') require('./variantsWithVariantsGroup-test') require('./variants-entry-test') +require('./oauthHandler-test') diff --git a/test/unit/oauthHandler-test.js b/test/unit/oauthHandler-test.js new file mode 100644 index 00000000..f74a8da0 --- /dev/null +++ b/test/unit/oauthHandler-test.js @@ -0,0 +1,325 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import axios from 'axios'; +import OAuthHandler from '../../lib/core/oauthHandler'; + +describe('OAuthHandler', () => { + let axiosInstance; + let oauthHandler; + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + axiosInstance = axios.create({ + uiBaseUrl: 'https://example.com', // Make sure this is correctly set + developerHubBaseUrl: 'https://developer.example.com', + baseURL: 'https://api.example.com', + }); + oauthHandler = new OAuthHandler(axiosInstance, 'appId', 'clientId', 'http://localhost:8184', 'clientSecret'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should initialize OAuthHandler with correct properties', () => { + expect(oauthHandler.appId).to.equal('appId'); + expect(oauthHandler.clientId).to.equal('clientId'); + expect(oauthHandler.redirectUri).to.equal('http://localhost:8184'); + expect(oauthHandler.responseType).to.equal('code'); + expect(oauthHandler.scope).to.equal(''); + expect(oauthHandler.clientSecret).to.equal('clientSecret'); + expect(oauthHandler.OAuthBaseURL).to.equal('https://example.com'); + expect(oauthHandler.axiosInstance).to.equal(axiosInstance); + }); + + it('should generate code verifier', () => { + const codeVerifier = oauthHandler.generateCodeVerifier(); + expect(codeVerifier).to.have.lengthOf(128); + }); + + it('should generate code challenge', async () => { + const codeVerifier = 'testCodeVerifier'; + const codeChallenge = await oauthHandler.generateCodeChallenge(codeVerifier); + expect(codeChallenge).to.exist; + }); + + it('should authorize and return authorization URL', async () => { + const authUrl = await oauthHandler.authorize(); + expect(authUrl).to.include('https://example.com/'); + expect(authUrl).to.include('response_type=code'); + expect(authUrl).to.include('client_id=clientId'); + }); + + it('should exchange code for token', async () => { + const tokenData = { access_token: 'accessToken', refresh_token: 'refreshToken', expires_in: 3600 }; + sandbox.stub(axiosInstance, 'post').resolves({ data: tokenData }); + + const result = await oauthHandler.exchangeCodeForToken('authorization_code'); + expect(result).to.deep.equal(tokenData); + expect(result).to.include('data'); + }); + + it('should refresh access token', async () => { + const tokenData = { access_token: 'newAccessToken', refresh_token: 'newRefreshToken', expires_in: 3600 }; + sandbox.stub(axiosInstance, 'post').resolves({ data: tokenData }); + + const result = await oauthHandler.refreshAccessToken('refreshToken'); + expect(result).to.deep.equal(tokenData); + }); + + it('should logout successfully', async () => { + sandbox.stub(oauthHandler, 'getOauthAppAuthorization').resolves('authorizationId'); + sandbox.stub(oauthHandler, 'revokeOauthAppAuthorization').resolves({}); + + const result = await oauthHandler.logout(); + expect(result).to.equal('Logged out successfully'); + }); + + it('should handle redirect and exchange code for token', async () => { + const exchangeStub = sandbox.stub(oauthHandler, 'exchangeCodeForToken').resolves({}); + + await oauthHandler.handleRedirect('http://localhost:8184?code=authorization_code'); + expect(exchangeStub.calledWith('authorization_code')).to.be.true; + }); + + it('should get access token', () => { + oauthHandler.axiosInstance.oauth = { accessToken: 'accessToken' }; + const accessToken = oauthHandler.getAccessToken(); + expect(accessToken).to.equal('accessToken'); + }); + + it('should get refresh token', () => { + oauthHandler.axiosInstance.oauth = { refreshToken: 'refreshToken' }; + const refreshToken = oauthHandler.getRefreshToken(); + expect(refreshToken).to.equal('refreshToken'); + }); + + it('should get organization UID', () => { + oauthHandler.axiosInstance.oauth = { organizationUID: 'organizationUID' }; + const organizationUID = oauthHandler.getOrganizationUID(); + expect(organizationUID).to.equal('organizationUID'); + }); + + it('should get user UID', () => { + oauthHandler.axiosInstance.oauth = { userUID: 'userUID' }; + const userUID = oauthHandler.getUserUID(); + expect(userUID).to.equal('userUID'); + }); + + it('should get token expiry time', () => { + oauthHandler.axiosInstance.oauth = { tokenExpiryTime: 1234567890 }; + const tokenExpiryTime = oauthHandler.getTokenExpiryTime(); + expect(tokenExpiryTime).to.equal(1234567890); + }); + + it('should set access token', () => { + oauthHandler.setAccessToken('newAccessToken'); + expect(oauthHandler.axiosInstance.oauth.accessToken).to.equal('newAccessToken'); + }); + + it('should set refresh token', () => { + oauthHandler.setRefreshToken('newRefreshToken'); + expect(oauthHandler.axiosInstance.oauth.refreshToken).to.equal('newRefreshToken'); + }); + + it('should set organization UID', () => { + oauthHandler.setOrganizationUID('newOrganizationUID'); + expect(oauthHandler.axiosInstance.oauth.organizationUID).to.equal('newOrganizationUID'); + }); + + it('should set user UID', () => { + oauthHandler.setUserUID('newUserUID'); + expect(oauthHandler.axiosInstance.oauth.userUID).to.equal('newUserUID'); + }); + + it('should set token expiry time', () => { + oauthHandler.setTokenExpiryTime(1234567890); + expect(oauthHandler.axiosInstance.oauth.tokenExpiryTime).to.equal(1234567890); + }); + + it('should generate codeVerifier and set codeChallenge to null if clientSecret is not provided', () => { + const oauthHandlerWithoutClientSecret = new OAuthHandler( + axiosInstance, + 'appId', + 'clientId', + 'http://localhost:8184', + null, // No clientSecret + ); + + // Ensure codeVerifier is generated + expect(oauthHandlerWithoutClientSecret.codeVerifier).to.exist; + + // Ensure codeChallenge is null initially + expect(oauthHandlerWithoutClientSecret.codeChallenge).to.equal(null); + }); + + it('should not generate codeVerifier or codeChallenge if clientSecret is provided', () => { + const oauthHandlerWithClientSecret = new OAuthHandler( + axiosInstance, + 'appId', + 'clientId', + 'http://localhost:8184', + 'clientSecret', // clientSecret is provided + ); + + // codeVerifier and codeChallenge should not be set if clientSecret is provided + expect(oauthHandlerWithClientSecret.codeVerifier).to.equal(undefined); + expect(oauthHandlerWithClientSecret.codeChallenge).to.equal(undefined); + }); + + it('should generate codeChallenge after calling generateCodeChallenge when clientSecret is not provided', async () => { + const oauthHandlerWithoutClientSecret = new OAuthHandler( + axiosInstance, + 'appId', + 'clientId', + 'http://localhost:8184', + null, // No clientSecret + ); + + const codeVerifier = oauthHandlerWithoutClientSecret.codeVerifier; + expect(codeVerifier).to.exist; // Ensure codeVerifier is generated + + const codeChallenge = await oauthHandlerWithoutClientSecret.generateCodeChallenge(codeVerifier); + + // Ensure the codeChallenge is a URL-safe Base64 string + expect(codeChallenge).to.match(/^[A-Za-z0-9-_]+$/); // URL-safe Base64 + }); + + it('should use the Web Crypto API in a browser environment', async () => { + // Mock the browser environment + global.window = { + crypto: { + subtle: { + digest: sinon.stub().resolves(new Uint8Array([1, 2, 3, 4])), // Mock hash result + }, + }, + }; + + const oauthHandlerWithoutClientSecret = new OAuthHandler( + axiosInstance, + 'appId', + 'clientId', + 'http://localhost:8184', + null, // No clientSecret + ); + + const codeVerifier = oauthHandlerWithoutClientSecret.codeVerifier; + expect(codeVerifier).to.exist; // Ensure codeVerifier is generated + + const codeChallenge = await oauthHandlerWithoutClientSecret.generateCodeChallenge(codeVerifier); + + // Ensure the codeChallenge is a URL-safe Base64 string + expect(codeChallenge).to.match(/^[A-Za-z0-9-_]+$/); // URL-safe Base64 + + // Clean up after the test to avoid affecting other tests + delete global.window; + }); + + it('should generate authorization URL with code_challenge when clientSecret is not provided', async () => { + // Mock OAuthHandler without clientSecret + oauthHandler = new OAuthHandler( + axiosInstance, + 'appId', + 'clientId', + 'http://localhost:8184', + null, // No clientSecret (PKCE) + ); + + // Stub the generateCodeChallenge to return a dummy value + const codeChallenge = 'dummyCodeChallenge'; + sandbox.stub(oauthHandler, 'generateCodeChallenge').resolves(codeChallenge); + + const authUrl = await oauthHandler.authorize(); + + // Check that code_challenge and code_challenge_method are included in the URL + expect(authUrl).to.include('https://example.com/'); + expect(authUrl).to.include('response_type=code'); + expect(authUrl).to.include('client_id=clientId'); + expect(authUrl).to.include('code_challenge=dummyCodeChallenge'); + expect(authUrl).to.include('code_challenge_method=S256'); + }); + + // Test cases for getOauthAppAuthorization + describe('getOauthAppAuthorization', () => { + it('should return authorization_uid when authorizations exist for the current user', async () => { + const mockResponse = { + data: { + data: [ + { + user: { uid: 'currentUserUid' }, + authorization_uid: 'authorizationUid1', + }, + ], + }, + }; + + sandbox.stub(axiosInstance, 'get').resolves(mockResponse); + + oauthHandler.axiosInstance.oauth.userUID = 'currentUserUid'; + const authorizationUid = await oauthHandler.getOauthAppAuthorization(); + + expect(authorizationUid).to.equal('authorizationUid1'); + }); + + it('should throw an error when no authorizations found for the current user', async () => { + const mockResponse = { + data: { + data: [ + { + user: { uid: 'otherUserUid' }, + authorization_uid: 'authorizationUid2', + }, + ], + }, + }; + + sandbox.stub(axiosInstance, 'get').resolves(mockResponse); + + oauthHandler.axiosInstance.oauth.userUID = 'currentUserUid'; + + try { + await oauthHandler.getOauthAppAuthorization(); + throw new Error('Expected error not thrown'); + } catch (error) { + expect(error.message).to.equal('No authorizations found for current user!'); + } + }); + + it('should throw an error when no authorizations found for the app', async () => { + const mockResponse = { data: { data: [] } }; + + sandbox.stub(axiosInstance, 'get').resolves(mockResponse); + + try { + await oauthHandler.getOauthAppAuthorization(); + throw new Error('Expected error not thrown'); + } catch (error) { + expect(error.message).to.equal('No authorizations found for the app!'); + } + }); + }); + + describe('revokeOauthAppAuthorization', () => { + it('should make a DELETE request to revoke authorization when valid authorizationId is provided', async () => { + const authorizationId = 'authorizationUid1'; + const mockResponse = { data: { success: true } }; + + sandbox.stub(axiosInstance, 'delete').resolves(mockResponse); + + const result = await oauthHandler.revokeOauthAppAuthorization(authorizationId); + + expect(result.success).to.be.true; + expect(axiosInstance.delete.calledOnce).to.be.true; + }); + + it('should not make a DELETE request when authorizationId is invalid or empty', async () => { + const invalidAuthorizationId = ''; + const deleteStub = sandbox.stub(axiosInstance, 'delete'); + + await oauthHandler.revokeOauthAppAuthorization(invalidAuthorizationId); + + expect(deleteStub.called).to.be.false; + }); + }); +}); From 6fa265a43b1859834b0afd062616e019d507c20c Mon Sep 17 00:00:00 2001 From: Aman Kumar Date: Thu, 13 Mar 2025 16:32:17 +0530 Subject: [PATCH 2/6] fix: oauth unit test cases linting issues --- test/unit/oauthHandler-test.js | 357 +++++++++++++++++---------------- 1 file changed, 182 insertions(+), 175 deletions(-) diff --git a/test/unit/oauthHandler-test.js b/test/unit/oauthHandler-test.js index f74a8da0..dae925e1 100644 --- a/test/unit/oauthHandler-test.js +++ b/test/unit/oauthHandler-test.js @@ -1,142 +1,145 @@ -import { expect } from 'chai'; -import sinon from 'sinon'; -import axios from 'axios'; -import OAuthHandler from '../../lib/core/oauthHandler'; +import { expect } from 'chai' +import sinon from 'sinon' +import axios from 'axios' +import OAuthHandler from '../../lib/core/oauthHandler' +import { describe, it, beforeEach, afterEach } from 'mocha' describe('OAuthHandler', () => { - let axiosInstance; - let oauthHandler; - let sandbox; + let axiosInstance + let oauthHandler + let sandbox beforeEach(() => { - sandbox = sinon.createSandbox(); + sandbox = sinon.createSandbox() axiosInstance = axios.create({ uiBaseUrl: 'https://example.com', // Make sure this is correctly set developerHubBaseUrl: 'https://developer.example.com', - baseURL: 'https://api.example.com', - }); - oauthHandler = new OAuthHandler(axiosInstance, 'appId', 'clientId', 'http://localhost:8184', 'clientSecret'); - }); + baseURL: 'https://api.example.com' + }) + oauthHandler = new OAuthHandler(axiosInstance, 'appId', 'clientId', 'http://localhost:8184', 'clientSecret') + }) afterEach(() => { - sandbox.restore(); - }); + sandbox.restore() + }) it('should initialize OAuthHandler with correct properties', () => { - expect(oauthHandler.appId).to.equal('appId'); - expect(oauthHandler.clientId).to.equal('clientId'); - expect(oauthHandler.redirectUri).to.equal('http://localhost:8184'); - expect(oauthHandler.responseType).to.equal('code'); - expect(oauthHandler.scope).to.equal(''); - expect(oauthHandler.clientSecret).to.equal('clientSecret'); - expect(oauthHandler.OAuthBaseURL).to.equal('https://example.com'); - expect(oauthHandler.axiosInstance).to.equal(axiosInstance); - }); + expect(oauthHandler.appId).to.equal('appId') + expect(oauthHandler.clientId).to.equal('clientId') + expect(oauthHandler.redirectUri).to.equal('http://localhost:8184') + expect(oauthHandler.responseType).to.equal('code') + expect(oauthHandler.scope).to.equal('') + expect(oauthHandler.clientSecret).to.equal('clientSecret') + expect(oauthHandler.OAuthBaseURL).to.equal('https://example.com') + expect(oauthHandler.axiosInstance).to.equal(axiosInstance) + }) it('should generate code verifier', () => { - const codeVerifier = oauthHandler.generateCodeVerifier(); - expect(codeVerifier).to.have.lengthOf(128); - }); + const codeVerifier = oauthHandler.generateCodeVerifier() + expect(codeVerifier).to.have.lengthOf(128) + }) it('should generate code challenge', async () => { - const codeVerifier = 'testCodeVerifier'; - const codeChallenge = await oauthHandler.generateCodeChallenge(codeVerifier); - expect(codeChallenge).to.exist; - }); + const codeVerifier = 'testCodeVerifier' + const codeChallenge = await oauthHandler.generateCodeChallenge(codeVerifier) + // eslint-disable-next-line no-unused-expressions + expect(codeChallenge).to.exist + }) it('should authorize and return authorization URL', async () => { - const authUrl = await oauthHandler.authorize(); - expect(authUrl).to.include('https://example.com/'); - expect(authUrl).to.include('response_type=code'); - expect(authUrl).to.include('client_id=clientId'); - }); + const authUrl = await oauthHandler.authorize() + expect(authUrl).to.include('https://example.com/') + expect(authUrl).to.include('response_type=code') + expect(authUrl).to.include('client_id=clientId') + }) it('should exchange code for token', async () => { - const tokenData = { access_token: 'accessToken', refresh_token: 'refreshToken', expires_in: 3600 }; - sandbox.stub(axiosInstance, 'post').resolves({ data: tokenData }); + const tokenData = { access_token: 'accessToken', refresh_token: 'refreshToken', expires_in: 3600 } + sandbox.stub(axiosInstance, 'post').resolves({ data: tokenData }) - const result = await oauthHandler.exchangeCodeForToken('authorization_code'); - expect(result).to.deep.equal(tokenData); - expect(result).to.include('data'); - }); + const result = await oauthHandler.exchangeCodeForToken('authorization_code') + expect(result).to.deep.equal(tokenData) + expect(result).to.include('data') + }) it('should refresh access token', async () => { - const tokenData = { access_token: 'newAccessToken', refresh_token: 'newRefreshToken', expires_in: 3600 }; - sandbox.stub(axiosInstance, 'post').resolves({ data: tokenData }); + const tokenData = { access_token: 'newAccessToken', refresh_token: 'newRefreshToken', expires_in: 3600 } + sandbox.stub(axiosInstance, 'post').resolves({ data: tokenData }) - const result = await oauthHandler.refreshAccessToken('refreshToken'); - expect(result).to.deep.equal(tokenData); - }); + const result = await oauthHandler.refreshAccessToken('refreshToken') + expect(result).to.deep.equal(tokenData) + }) it('should logout successfully', async () => { - sandbox.stub(oauthHandler, 'getOauthAppAuthorization').resolves('authorizationId'); - sandbox.stub(oauthHandler, 'revokeOauthAppAuthorization').resolves({}); + sandbox.stub(oauthHandler, 'getOauthAppAuthorization').resolves('authorizationId') + sandbox.stub(oauthHandler, 'revokeOauthAppAuthorization').resolves({}) - const result = await oauthHandler.logout(); - expect(result).to.equal('Logged out successfully'); - }); + const result = await oauthHandler.logout() + expect(result).to.equal('Logged out successfully') + }) it('should handle redirect and exchange code for token', async () => { - const exchangeStub = sandbox.stub(oauthHandler, 'exchangeCodeForToken').resolves({}); + const exchangeStub = sandbox.stub(oauthHandler, 'exchangeCodeForToken').resolves({}) - await oauthHandler.handleRedirect('http://localhost:8184?code=authorization_code'); - expect(exchangeStub.calledWith('authorization_code')).to.be.true; - }); + await oauthHandler.handleRedirect('http://localhost:8184?code=authorization_code') + // eslint-disable-next-line no-unused-expressions + expect(exchangeStub.calledWith('authorization_code')).to.be.true + }) it('should get access token', () => { - oauthHandler.axiosInstance.oauth = { accessToken: 'accessToken' }; - const accessToken = oauthHandler.getAccessToken(); - expect(accessToken).to.equal('accessToken'); - }); + oauthHandler.axiosInstance.oauth = { accessToken: 'accessToken' } + const accessToken = oauthHandler.getAccessToken() + expect(accessToken).to.equal('accessToken') + }) it('should get refresh token', () => { - oauthHandler.axiosInstance.oauth = { refreshToken: 'refreshToken' }; - const refreshToken = oauthHandler.getRefreshToken(); - expect(refreshToken).to.equal('refreshToken'); - }); + oauthHandler.axiosInstance.oauth = { refreshToken: 'refreshToken' } + const refreshToken = oauthHandler.getRefreshToken() + expect(refreshToken).to.equal('refreshToken') + }) it('should get organization UID', () => { - oauthHandler.axiosInstance.oauth = { organizationUID: 'organizationUID' }; - const organizationUID = oauthHandler.getOrganizationUID(); - expect(organizationUID).to.equal('organizationUID'); - }); + oauthHandler.axiosInstance.oauth = { organizationUID: 'organizationUID' } + const organizationUID = oauthHandler.getOrganizationUID() + expect(organizationUID).to.equal('organizationUID') + }) it('should get user UID', () => { - oauthHandler.axiosInstance.oauth = { userUID: 'userUID' }; - const userUID = oauthHandler.getUserUID(); - expect(userUID).to.equal('userUID'); - }); + oauthHandler.axiosInstance.oauth = { userUID: 'userUID' } + const userUID = oauthHandler.getUserUID() + expect(userUID).to.equal('userUID') + }) it('should get token expiry time', () => { - oauthHandler.axiosInstance.oauth = { tokenExpiryTime: 1234567890 }; - const tokenExpiryTime = oauthHandler.getTokenExpiryTime(); - expect(tokenExpiryTime).to.equal(1234567890); - }); + oauthHandler.axiosInstance.oauth = { tokenExpiryTime: 1234567890 } + const tokenExpiryTime = oauthHandler.getTokenExpiryTime() + expect(tokenExpiryTime).to.equal(1234567890) + }) it('should set access token', () => { - oauthHandler.setAccessToken('newAccessToken'); - expect(oauthHandler.axiosInstance.oauth.accessToken).to.equal('newAccessToken'); - }); + oauthHandler.setAccessToken('newAccessToken') + expect(oauthHandler.axiosInstance.oauth.accessToken).to.equal('newAccessToken') + }) it('should set refresh token', () => { - oauthHandler.setRefreshToken('newRefreshToken'); - expect(oauthHandler.axiosInstance.oauth.refreshToken).to.equal('newRefreshToken'); - }); + oauthHandler.setRefreshToken('newRefreshToken') + expect(oauthHandler.axiosInstance.oauth.refreshToken).to.equal('newRefreshToken') + }) it('should set organization UID', () => { - oauthHandler.setOrganizationUID('newOrganizationUID'); - expect(oauthHandler.axiosInstance.oauth.organizationUID).to.equal('newOrganizationUID'); - }); + oauthHandler.setOrganizationUID('newOrganizationUID') + expect(oauthHandler.axiosInstance.oauth.organizationUID).to.equal('newOrganizationUID') + }) it('should set user UID', () => { - oauthHandler.setUserUID('newUserUID'); - expect(oauthHandler.axiosInstance.oauth.userUID).to.equal('newUserUID'); - }); + oauthHandler.setUserUID('newUserUID') + expect(oauthHandler.axiosInstance.oauth.userUID).to.equal('newUserUID') + }) it('should set token expiry time', () => { - oauthHandler.setTokenExpiryTime(1234567890); - expect(oauthHandler.axiosInstance.oauth.tokenExpiryTime).to.equal(1234567890); - }); + oauthHandler.setTokenExpiryTime(1234567890) + expect(oauthHandler.axiosInstance.oauth.tokenExpiryTime).to.equal(1234567890) + }) it('should generate codeVerifier and set codeChallenge to null if clientSecret is not provided', () => { const oauthHandlerWithoutClientSecret = new OAuthHandler( @@ -144,15 +147,16 @@ describe('OAuthHandler', () => { 'appId', 'clientId', 'http://localhost:8184', - null, // No clientSecret - ); + null // No clientSecret + ) // Ensure codeVerifier is generated - expect(oauthHandlerWithoutClientSecret.codeVerifier).to.exist; + // eslint-disable-next-line no-unused-expressions + expect(oauthHandlerWithoutClientSecret.codeVerifier).to.exist // Ensure codeChallenge is null initially - expect(oauthHandlerWithoutClientSecret.codeChallenge).to.equal(null); - }); + expect(oauthHandlerWithoutClientSecret.codeChallenge).to.equal(null) + }) it('should not generate codeVerifier or codeChallenge if clientSecret is provided', () => { const oauthHandlerWithClientSecret = new OAuthHandler( @@ -160,13 +164,13 @@ describe('OAuthHandler', () => { 'appId', 'clientId', 'http://localhost:8184', - 'clientSecret', // clientSecret is provided - ); + 'clientSecret' // clientSecret is provided + ) // codeVerifier and codeChallenge should not be set if clientSecret is provided - expect(oauthHandlerWithClientSecret.codeVerifier).to.equal(undefined); - expect(oauthHandlerWithClientSecret.codeChallenge).to.equal(undefined); - }); + expect(oauthHandlerWithClientSecret.codeVerifier).to.equal(undefined) + expect(oauthHandlerWithClientSecret.codeChallenge).to.equal(undefined) + }) it('should generate codeChallenge after calling generateCodeChallenge when clientSecret is not provided', async () => { const oauthHandlerWithoutClientSecret = new OAuthHandler( @@ -174,47 +178,49 @@ describe('OAuthHandler', () => { 'appId', 'clientId', 'http://localhost:8184', - null, // No clientSecret - ); + null // No clientSecret + ) - const codeVerifier = oauthHandlerWithoutClientSecret.codeVerifier; - expect(codeVerifier).to.exist; // Ensure codeVerifier is generated + const codeVerifier = oauthHandlerWithoutClientSecret.codeVerifier + // eslint-disable-next-line no-unused-expressions + expect(codeVerifier).to.exist // Ensure codeVerifier is generated - const codeChallenge = await oauthHandlerWithoutClientSecret.generateCodeChallenge(codeVerifier); + const codeChallenge = await oauthHandlerWithoutClientSecret.generateCodeChallenge(codeVerifier) // Ensure the codeChallenge is a URL-safe Base64 string - expect(codeChallenge).to.match(/^[A-Za-z0-9-_]+$/); // URL-safe Base64 - }); + expect(codeChallenge).to.match(/^[A-Za-z0-9-_]+$/) // URL-safe Base64 + }) it('should use the Web Crypto API in a browser environment', async () => { // Mock the browser environment global.window = { crypto: { subtle: { - digest: sinon.stub().resolves(new Uint8Array([1, 2, 3, 4])), // Mock hash result - }, - }, - }; + digest: sinon.stub().resolves(new Uint8Array([1, 2, 3, 4])) // Mock hash result + } + } + } const oauthHandlerWithoutClientSecret = new OAuthHandler( axiosInstance, 'appId', 'clientId', 'http://localhost:8184', - null, // No clientSecret - ); + null // No clientSecret + ) - const codeVerifier = oauthHandlerWithoutClientSecret.codeVerifier; - expect(codeVerifier).to.exist; // Ensure codeVerifier is generated + const codeVerifier = oauthHandlerWithoutClientSecret.codeVerifier + // eslint-disable-next-line no-unused-expressions + expect(codeVerifier).to.exist // Ensure codeVerifier is generated - const codeChallenge = await oauthHandlerWithoutClientSecret.generateCodeChallenge(codeVerifier); + const codeChallenge = await oauthHandlerWithoutClientSecret.generateCodeChallenge(codeVerifier) // Ensure the codeChallenge is a URL-safe Base64 string - expect(codeChallenge).to.match(/^[A-Za-z0-9-_]+$/); // URL-safe Base64 + expect(codeChallenge).to.match(/^[A-Za-z0-9-_]+$/) // URL-safe Base64 // Clean up after the test to avoid affecting other tests - delete global.window; - }); + delete global.window + }) it('should generate authorization URL with code_challenge when clientSecret is not provided', async () => { // Mock OAuthHandler without clientSecret @@ -223,22 +229,22 @@ describe('OAuthHandler', () => { 'appId', 'clientId', 'http://localhost:8184', - null, // No clientSecret (PKCE) - ); + null // No clientSecret (PKCE) + ) // Stub the generateCodeChallenge to return a dummy value - const codeChallenge = 'dummyCodeChallenge'; - sandbox.stub(oauthHandler, 'generateCodeChallenge').resolves(codeChallenge); + const codeChallenge = 'dummyCodeChallenge' + sandbox.stub(oauthHandler, 'generateCodeChallenge').resolves(codeChallenge) - const authUrl = await oauthHandler.authorize(); + const authUrl = await oauthHandler.authorize() // Check that code_challenge and code_challenge_method are included in the URL - expect(authUrl).to.include('https://example.com/'); - expect(authUrl).to.include('response_type=code'); - expect(authUrl).to.include('client_id=clientId'); - expect(authUrl).to.include('code_challenge=dummyCodeChallenge'); - expect(authUrl).to.include('code_challenge_method=S256'); - }); + expect(authUrl).to.include('https://example.com/') + expect(authUrl).to.include('response_type=code') + expect(authUrl).to.include('client_id=clientId') + expect(authUrl).to.include('code_challenge=dummyCodeChallenge') + expect(authUrl).to.include('code_challenge_method=S256') + }) // Test cases for getOauthAppAuthorization describe('getOauthAppAuthorization', () => { @@ -248,19 +254,19 @@ describe('OAuthHandler', () => { data: [ { user: { uid: 'currentUserUid' }, - authorization_uid: 'authorizationUid1', - }, - ], - }, - }; + authorization_uid: 'authorizationUid1' + } + ] + } + } - sandbox.stub(axiosInstance, 'get').resolves(mockResponse); + sandbox.stub(axiosInstance, 'get').resolves(mockResponse) - oauthHandler.axiosInstance.oauth.userUID = 'currentUserUid'; - const authorizationUid = await oauthHandler.getOauthAppAuthorization(); + oauthHandler.axiosInstance.oauth.userUID = 'currentUserUid' + const authorizationUid = await oauthHandler.getOauthAppAuthorization() - expect(authorizationUid).to.equal('authorizationUid1'); - }); + expect(authorizationUid).to.equal('authorizationUid1') + }) it('should throw an error when no authorizations found for the current user', async () => { const mockResponse = { @@ -268,58 +274,59 @@ describe('OAuthHandler', () => { data: [ { user: { uid: 'otherUserUid' }, - authorization_uid: 'authorizationUid2', - }, - ], - }, - }; + authorization_uid: 'authorizationUid2' + } + ] + } + } - sandbox.stub(axiosInstance, 'get').resolves(mockResponse); + sandbox.stub(axiosInstance, 'get').resolves(mockResponse) - oauthHandler.axiosInstance.oauth.userUID = 'currentUserUid'; + oauthHandler.axiosInstance.oauth.userUID = 'currentUserUid' try { - await oauthHandler.getOauthAppAuthorization(); - throw new Error('Expected error not thrown'); + await oauthHandler.getOauthAppAuthorization() + throw new Error('Expected error not thrown') } catch (error) { - expect(error.message).to.equal('No authorizations found for current user!'); + expect(error.message).to.equal('No authorizations found for current user!') } - }); + }) it('should throw an error when no authorizations found for the app', async () => { - const mockResponse = { data: { data: [] } }; + const mockResponse = { data: { data: [] } } - sandbox.stub(axiosInstance, 'get').resolves(mockResponse); + sandbox.stub(axiosInstance, 'get').resolves(mockResponse) try { - await oauthHandler.getOauthAppAuthorization(); - throw new Error('Expected error not thrown'); + await oauthHandler.getOauthAppAuthorization() + throw new Error('Expected error not thrown') } catch (error) { - expect(error.message).to.equal('No authorizations found for the app!'); + expect(error.message).to.equal('No authorizations found for the app!') } - }); - }); + }) + }) describe('revokeOauthAppAuthorization', () => { it('should make a DELETE request to revoke authorization when valid authorizationId is provided', async () => { - const authorizationId = 'authorizationUid1'; - const mockResponse = { data: { success: true } }; + const authorizationId = 'authorizationUid1' + const mockResponse = { data: { success: true } } - sandbox.stub(axiosInstance, 'delete').resolves(mockResponse); + sandbox.stub(axiosInstance, 'delete').resolves(mockResponse) + const result = await oauthHandler.revokeOauthAppAuthorization(authorizationId) - const result = await oauthHandler.revokeOauthAppAuthorization(authorizationId); - - expect(result.success).to.be.true; - expect(axiosInstance.delete.calledOnce).to.be.true; - }); + // eslint-disable-next-line no-unused-expressions + expect(result.success).to.be.true + // eslint-disable-next-line no-unused-expressions + expect(axiosInstance.delete.calledOnce).to.be.true + }) it('should not make a DELETE request when authorizationId is invalid or empty', async () => { - const invalidAuthorizationId = ''; - const deleteStub = sandbox.stub(axiosInstance, 'delete'); - - await oauthHandler.revokeOauthAppAuthorization(invalidAuthorizationId); - - expect(deleteStub.called).to.be.false; - }); - }); -}); + const invalidAuthorizationId = '' + const deleteStub = sandbox.stub(axiosInstance, 'delete') + + await oauthHandler.revokeOauthAppAuthorization(invalidAuthorizationId) + // eslint-disable-next-line no-unused-expressions + expect(deleteStub.called).to.be.false + }) + }) +}) From c141f42ca618721da953d95a5de6f6919ebc43be Mon Sep 17 00:00:00 2001 From: Aman Kumar Date: Mon, 17 Mar 2025 11:21:00 +0530 Subject: [PATCH 3/6] fix: oauth unit test cases --- lib/core/contentstackHTTPClient.js | 6 +++--- test/unit/ContentstackHTTPClient-test.js | 2 +- test/unit/oauthHandler-test.js | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/core/contentstackHTTPClient.js b/lib/core/contentstackHTTPClient.js index 02f590d5..e5d312c7 100644 --- a/lib/core/contentstackHTTPClient.js +++ b/lib/core/contentstackHTTPClient.js @@ -68,11 +68,11 @@ export default function contentstackHttpClient (options) { let uiHostName = hostname let developerHubBaseUrl = hostname - if (hostname.endsWith('io')) { - uiHostName = hostname.replace('io', 'com') + if (uiHostName?.endsWith('io')) { + uiHostName = uiHostName.replace('io', 'com') } - if (hostname.startsWith('api')) { + if (uiHostName?.startsWith('api')) { uiHostName = uiHostName.replace('api', 'app') } const uiBaseUrl = config.endpoint || `${protocol}://${uiHostName}` diff --git a/test/unit/ContentstackHTTPClient-test.js b/test/unit/ContentstackHTTPClient-test.js index 1b78a290..4015e94c 100644 --- a/test/unit/ContentstackHTTPClient-test.js +++ b/test/unit/ContentstackHTTPClient-test.js @@ -112,7 +112,7 @@ describe('Contentstack HTTP Client', () => { it('Contentstack retryDelayOption base test', done => { const client = contentstackHTTPClient({ - retryDelayOptions: { base: 200 } + retryDelayOptions: { base: 200 }, }) expect(client.defaults.retryDelayOptions).to.not.equal(undefined) expect(client.defaults.retryDelayOptions.base).to.be.equal(200) diff --git a/test/unit/oauthHandler-test.js b/test/unit/oauthHandler-test.js index dae925e1..f1e1e15b 100644 --- a/test/unit/oauthHandler-test.js +++ b/test/unit/oauthHandler-test.js @@ -59,7 +59,6 @@ describe('OAuthHandler', () => { const result = await oauthHandler.exchangeCodeForToken('authorization_code') expect(result).to.deep.equal(tokenData) - expect(result).to.include('data') }) it('should refresh access token', async () => { From a86ec2d3ed205551bcc253647245411ccc79753f Mon Sep 17 00:00:00 2001 From: Aman Kumar Date: Mon, 17 Mar 2025 11:34:28 +0530 Subject: [PATCH 4/6] fix:linting issue in test/unit/ContentstackHTTPClient-test.js --- test/unit/ContentstackHTTPClient-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/ContentstackHTTPClient-test.js b/test/unit/ContentstackHTTPClient-test.js index 4015e94c..1b78a290 100644 --- a/test/unit/ContentstackHTTPClient-test.js +++ b/test/unit/ContentstackHTTPClient-test.js @@ -112,7 +112,7 @@ describe('Contentstack HTTP Client', () => { it('Contentstack retryDelayOption base test', done => { const client = contentstackHTTPClient({ - retryDelayOptions: { base: 200 }, + retryDelayOptions: { base: 200 } }) expect(client.defaults.retryDelayOptions).to.not.equal(undefined) expect(client.defaults.retryDelayOptions.base).to.be.equal(200) From 97876b4c61ab5853ad716045bf9f8120375af57e Mon Sep 17 00:00:00 2001 From: Aman Kumar Date: Mon, 17 Mar 2025 17:35:28 +0530 Subject: [PATCH 5/6] feat: oauth integration test cases --- test/sanity-check/api/oauth-test.js | 144 ++++++++++++++++++++++++++++ test/sanity-check/sanity.js | 1 + 2 files changed, 145 insertions(+) create mode 100644 test/sanity-check/api/oauth-test.js diff --git a/test/sanity-check/api/oauth-test.js b/test/sanity-check/api/oauth-test.js new file mode 100644 index 00000000..c06dc556 --- /dev/null +++ b/test/sanity-check/api/oauth-test.js @@ -0,0 +1,144 @@ +import { expect } from 'chai' +import { describe, it } from 'mocha' +import { contentstackClient } from '../../sanity-check/utility/ContentstackClient' +import axios from 'axios' +import dotenv from 'dotenv' + +dotenv.config() +let accessToken = '' +let loggedinUserID = '' +let authUrl = '' +let codeChallenge = '' +let codeChallengeMethod = '' +let authCode +let authtoken = '' +let redirectUrl = '' +let refreshToken = '' +const client = contentstackClient() +const oauthClient = client.oauth({ + clientId: process.env.CLIENT_ID, + appId: process.env.APP_ID, + redirectUri: process.env.REDIRECT_URI +}) + +describe('OAuth Authentication API Test', () => { + it('should login with credentials', done => { + client.login({ email: process.env.EMAIL, password: process.env.PASSWORD }, { include_orgs: true, include_orgs_roles: true, include_stack_roles: true, include_user_settings: true }).then((response) => { + expect(response.notice).to.be.equal('Login Successful.', 'Login success messsage does not match.') + done() + }) + .catch(done) + }) + + it('should get Current user info test', done => { + client.getUser().then((user) => { + authtoken = user.authtoken + done() + }) + .catch(done) + }) + + it('should fail when trying to login with invalid app credentials', () => { + try { + client.oauth({ + clientId: 'clientId', + appId: 'appId', + redirectUri: 'redirectUri' + }) + } catch (error) { + const jsonMessage = JSON.parse(error.message) + expect(jsonMessage.status).to.be.equal(401, 'Status code does not match for invalid credentials') + expect(jsonMessage.errorMessage).to.not.equal(null, 'Error message not proper') + expect(jsonMessage.errorCode).to.be.equal(104, 'Error code does not match') + } + }) + + it('should generate OAuth authorization URL', async () => { + authUrl = await oauthClient.authorize() + const url = new URL(authUrl) + + codeChallenge = url.searchParams.get('code_challenge') + codeChallengeMethod = url.searchParams.get('code_challenge_method') + + // Ensure they are not empty strings + expect(codeChallenge).to.not.equal('') + expect(codeChallengeMethod).to.not.equal('') + expect(authUrl).to.include(process.env.CLIENT_ID, 'Client ID mismatch') + }) + + it('should simulate calling the authorization URL and receive authorization code', async () => { + try { + const authorizationEndpoint = oauthClient.axiosInstance.defaults.developerHubBaseUrl + axios.defaults.headers.common.authtoken = authtoken + axios.defaults.headers.common.organization_uid = process.env.ORGANIZATION + const response = await axios + .post(`${authorizationEndpoint}/manifests/${process.env.APP_ID}/authorize`, { + client_id: process.env.CLIENT_ID, + redirect_uri: process.env.REDIRECT_URI, + code_challenge: codeChallenge, + code_challenge_method: codeChallengeMethod, + response_type: 'code' + }) + const data = response.data + redirectUrl = data.data.redirect_url + const url = new URL(redirectUrl) + authCode = url.searchParams.get('code') + oauthClient.axiosInstance.oauth.appId = process.env.APP_ID + oauthClient.axiosInstance.oauth.clientId = process.env.CLIENT_ID + oauthClient.axiosInstance.oauth.redirectUri = process.env.REDIRECT_URI + // Ensure they are not empty strings + expect(redirectUrl).to.not.equal('') + expect(url).to.not.equal('') + } catch (error) { + console.log(error) + } + }) + + it('should exchange authorization code for access token', async () => { + const response = await oauthClient.exchangeCodeForToken(authCode) + accessToken = response.access_token + loggedinUserID = response.user_uid + refreshToken = response.refresh_token + + expect(response.organization_uid).to.be.equal(process.env.ORGANIZATION, 'Organization mismatch') + // eslint-disable-next-line no-unused-expressions + expect(response.access_token).to.not.be.null + // eslint-disable-next-line no-unused-expressions + expect(response.refresh_token).to.not.be.null + }) + + it('should get the logged-in user info using the access token', async () => { + const user = await client.getUser({ + authorization: `Bearer ${accessToken}` + }) + expect(user.uid).to.be.equal(loggedinUserID) + expect(user.email).to.be.equal(process.env.EMAIL, 'Email mismatch') + }) + + it('should refresh the access token using refresh token', async () => { + const response = await oauthClient.refreshAccessToken(refreshToken) + + accessToken = response.access_token + refreshToken = response.refresh_token + // eslint-disable-next-line no-unused-expressions + expect(response.access_token).to.not.be.null + // eslint-disable-next-line no-unused-expressions + expect(response.refresh_token).to.not.be.null + }) + + it('should logout successfully after OAuth authentication', async () => { + const response = await oauthClient.logout() + expect(response).to.be.equal('Logged out successfully') + }) + + it('should fail to make an API request with an expired token', async () => { + try { + await client.getUser({ + authorization: `Bearer ${accessToken}` + }) + } catch (error) { + expect(error.status).to.be.equal(401, 'API request should fail with status 401') + expect(error.errorMessage).to.be.equal('The provided access token is invalid or expired or revoked', 'Error message mismatch') + } + }) +}) diff --git a/test/sanity-check/sanity.js b/test/sanity-check/sanity.js index bf309516..8ee08d31 100644 --- a/test/sanity-check/sanity.js +++ b/test/sanity-check/sanity.js @@ -29,3 +29,4 @@ require('./api/contentType-delete-test') require('./api/delete-test') require('./api/team-test') require('./api/auditlog-test') +require('./api/oauth-test') From b310d86b7dad4766566622ffc793a272757c1ddb Mon Sep 17 00:00:00 2001 From: Aman Kumar Date: Mon, 17 Mar 2025 19:41:07 +0530 Subject: [PATCH 6/6] fix: refresh &updated token url --- lib/core/oauthHandler.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/core/oauthHandler.js b/lib/core/oauthHandler.js index 3a42edb6..aa46c8f5 100644 --- a/lib/core/oauthHandler.js +++ b/lib/core/oauthHandler.js @@ -133,7 +133,7 @@ export default class OAuthHandler { this.axiosInstance.defaults.headers = this._getHeaders() try { - const response = await this.axiosInstance.post(`${this.OAuthBaseURL}/apps-api/apps/token`, body) + const response = await this.axiosInstance.post(`${this.developerHubBaseUrl}/token`, body) this._saveTokens(response.data) return response.data @@ -180,7 +180,7 @@ export default class OAuthHandler { this.axiosInstance.defaults.headers = this._getHeaders() try { - const response = await this.axiosInstance.post(`${this.developerHubBaseUrl}/apps/token`, body) + const response = await this.axiosInstance.post(`${this.developerHubBaseUrl}/token`, body) const data = response.data this.axiosInstance.oauth.accessToken = data.access_token