From 3ae0809ee5a8ceb1b9857369d79b27d8a7a39a7a Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Thu, 9 Sep 2021 04:37:30 +0500 Subject: [PATCH] test(AuthManager): improved coverage --- src/auth/AuthManager.ts | 44 +-- src/auth/spec/AuthManager.spec.ts | 455 ++++++++++++++++++++++++++++-- src/auth/spec/openWebPage.spec.ts | 64 +++++ 3 files changed, 515 insertions(+), 48 deletions(-) create mode 100644 src/auth/spec/openWebPage.spec.ts diff --git a/src/auth/AuthManager.ts b/src/auth/AuthManager.ts index 3c1c0fb..84ae334 100644 --- a/src/auth/AuthManager.ts +++ b/src/auth/AuthManager.ts @@ -66,12 +66,7 @@ export class AuthManager { if (isLoggedIn) { if (this.serverType === ServerType.Sas9) { - const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check` - - await this.requestClient.get( - `/SASLogon/login?service=${casAuthenticationUrl}`, - undefined - ) + await this.performCASSecurityCheck() } const { userName } = await this.fetchUserName() @@ -113,7 +108,8 @@ export class AuthManager { userName: this.userName } } else { - this.logOut() + await this.logOut() + loginForm = await this.getNewLoginForm() } } else this.userName = '' @@ -138,12 +134,7 @@ export class AuthManager { if (isLoggedIn) { if (this.serverType === ServerType.Sas9) { - const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check` - - await this.requestClient.get( - `/SASLogon/login?service=${casAuthenticationUrl}`, - undefined - ) + await this.performCASSecurityCheck() } this.loginCallback() @@ -155,6 +146,15 @@ export class AuthManager { } } + private async performCASSecurityCheck() { + const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check` + + await this.requestClient.get( + `/SASLogon/login?service=${casAuthenticationUrl}`, + undefined + ) + } + private async sendLoginRequest( loginForm: { [key: string]: any }, loginParams: { [key: string]: any } @@ -198,13 +198,7 @@ export class AuthManager { //Residue can happen in case of session expiration await this.logOut() - const { result: formResponse } = await this.requestClient.get( - this.loginUrl.replace('.do', ''), - undefined, - 'text/plain' - ) - - loginForm = await this.getLoginForm(formResponse) + loginForm = await this.getNewLoginForm() } return Promise.resolve({ @@ -214,6 +208,16 @@ export class AuthManager { }) } + private async getNewLoginForm() { + const { result: formResponse } = await this.requestClient.get( + this.loginUrl.replace('.do', ''), + undefined, + 'text/plain' + ) + + return await this.getLoginForm(formResponse) + } + private async fetchUserName(): Promise<{ isLoggedIn: boolean userName: string diff --git a/src/auth/spec/AuthManager.spec.ts b/src/auth/spec/AuthManager.spec.ts index ae04e3c..b17a352 100644 --- a/src/auth/spec/AuthManager.spec.ts +++ b/src/auth/spec/AuthManager.spec.ts @@ -10,6 +10,7 @@ import { import { serialize } from '../../utils' import * as openWebPageModule from '../openWebPage' import * as verifySasViyaLoginModule from '../verifySasViyaLogin' +import * as verifySas9LoginModule from '../verifySas9Login' import { RequestClient } from '../../request/RequestClient' jest.mock('axios') const mockedAxios = axios as jest.Mocked @@ -83,7 +84,64 @@ describe('AuthManager', () => { expect(authCallback).toHaveBeenCalledTimes(1) }) - it('should post a login request to the server if not logged in', async () => { + it('should post a login request to the server when already logged in with other username', async () => { + const authManager = new AuthManager( + serverUrl, + serverType, + requestClient, + authCallback + ) + jest.spyOn(authManager, 'checkSession').mockImplementation(() => + Promise.resolve({ + isLoggedIn: true, + userName: 'someOtherUsername', + loginForm: null + }) + ) + jest + .spyOn(authManager, 'logOut') + .mockImplementation(() => Promise.resolve(true)) + + jest + .spyOn(authManager, 'getNewLoginForm') + .mockImplementation(() => + Promise.resolve({ + name: 'test' + }) + ) + mockedAxios.post.mockImplementation(() => + Promise.resolve({ data: mockLoginSuccessResponse }) + ) + + const loginResponse = await authManager.logIn(userName, password) + + expect(loginResponse.isLoggedIn).toBeTruthy() + expect(loginResponse.userName).toEqual(userName) + + const loginParams = serialize({ + _service: 'default', + username: userName, + password, + name: 'test' + }) + expect(authCallback).toHaveBeenCalledTimes(1) + expect(authManager.logOut).toHaveBeenCalledTimes(1) + expect(authManager['getNewLoginForm']).toHaveBeenCalledTimes(1) + expect(mockedAxios.post).toHaveBeenCalledWith( + `/SASLogon/login`, + loginParams, + { + withCredentials: true, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: '*/*' + } + } + ) + expect(authCallback).toHaveBeenCalledTimes(1) + }) + + it('should post a login request to the server when not logged in', async () => { const authManager = new AuthManager( serverUrl, serverType, @@ -126,6 +184,98 @@ describe('AuthManager', () => { expect(authCallback).toHaveBeenCalledTimes(1) }) + it('should post a login & a cas_security request to the SAS9 server when not logged in', async () => { + const serverType = ServerType.Sas9 + const authManager = new AuthManager( + serverUrl, + serverType, + requestClient, + authCallback + ) + jest.spyOn(authManager, 'checkSession').mockImplementation(() => + Promise.resolve({ + isLoggedIn: false, + userName: '', + loginForm: { name: 'test' } + }) + ) + mockedAxios.post.mockImplementation(() => + Promise.resolve({ data: mockLoginSuccessResponse }) + ) + mockedAxios.get.mockImplementation(() => Promise.resolve({ status: 200 })) + + const loginResponse = await authManager.logIn(userName, password) + + expect(loginResponse.isLoggedIn).toBeTruthy() + expect(loginResponse.userName).toEqual(userName) + + const loginParams = serialize({ + _service: 'default', + username: userName, + password, + name: 'test' + }) + expect(mockedAxios.post).toHaveBeenCalledWith( + `/SASLogon/login`, + loginParams, + { + withCredentials: true, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: '*/*' + } + } + ) + const casAuthenticationUrl = `${serverUrl}/SASStoredProcess/j_spring_cas_security_check` + expect(mockedAxios.get).toHaveBeenCalledWith( + `/SASLogon/login?service=${casAuthenticationUrl}`, + getHeadersJson + ) + expect(authCallback).toHaveBeenCalledTimes(1) + }) + + it('should return empty username if unable to logged in', async () => { + const authManager = new AuthManager( + serverUrl, + serverType, + requestClient, + authCallback + ) + jest.spyOn(authManager, 'checkSession').mockImplementation(() => + Promise.resolve({ + isLoggedIn: false, + userName: '', + loginForm: { name: 'test' } + }) + ) + mockedAxios.post.mockImplementation(() => + Promise.resolve({ data: 'Not Signed in' }) + ) + + const loginResponse = await authManager.logIn(userName, password) + + expect(loginResponse.isLoggedIn).toBeFalsy() + expect(loginResponse.userName).toEqual('') + + const loginParams = serialize({ + _service: 'default', + username: userName, + password, + name: 'test' + }) + expect(mockedAxios.post).toHaveBeenCalledWith( + `/SASLogon/login`, + loginParams, + { + withCredentials: true, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: '*/*' + } + } + ) + }) + it('should parse and submit the authorisation form when necessary', async () => { const authManager = new AuthManager( serverUrl, @@ -177,6 +327,10 @@ describe('AuthManager', () => { jest .spyOn(verifySasViyaLoginModule, 'verifySasViyaLogin') .mockImplementation(() => Promise.resolve({ isLoggedIn: true })) + jest.mock('../verifySas9Login') + jest + .spyOn(verifySas9LoginModule, 'verifySas9Login') + .mockImplementation(() => Promise.resolve({ isLoggedIn: true })) }) it('should call the auth callback and return when already logged in', async () => { @@ -202,7 +356,7 @@ describe('AuthManager', () => { expect(authCallback).toHaveBeenCalledTimes(1) }) - it('should open pop up if not logged in', async () => { + it('should perform login via pop up if not logged in', async () => { const authManager = new AuthManager( serverUrl, serverType, @@ -239,37 +393,282 @@ describe('AuthManager', () => { undefined ) expect(authManager['fetchUserName']).toHaveBeenCalledTimes(2) - + expect(verifySasViyaLoginModule.verifySasViyaLogin).toHaveBeenCalledTimes( + 1 + ) expect(authCallback).toHaveBeenCalledTimes(1) }) + + it('should perform login via pop up if not logged in with server sas9', async () => { + const serverType = ServerType.Sas9 + const authManager = new AuthManager( + serverUrl, + serverType, + requestClient, + authCallback + ) + jest + .spyOn(authManager, 'fetchUserName') + .mockImplementationOnce(() => + Promise.resolve({ + isLoggedIn: false, + userName: '' + }) + ) + .mockImplementationOnce(() => + Promise.resolve({ + isLoggedIn: true, + userName + }) + ) + + const loginResponse = await authManager.redirectedLogIn({}) + + expect(loginResponse.isLoggedIn).toBeTruthy() + expect(loginResponse.userName).toEqual(userName) + + expect(openWebPageModule.openWebPage).toHaveBeenCalledWith( + `/SASLogon/home`, + 'SASLogon', + { + width: 500, + height: 600 + }, + undefined + ) + expect(authManager['fetchUserName']).toHaveBeenCalledTimes(2) + expect(verifySas9LoginModule.verifySas9Login).toHaveBeenCalledTimes(1) + expect(authCallback).toHaveBeenCalledTimes(1) + }) + + it('should return empty username if user unable to re-login via pop up', async () => { + const authManager = new AuthManager( + serverUrl, + serverType, + requestClient, + authCallback + ) + jest + .spyOn(authManager, 'fetchUserName') + .mockImplementationOnce(() => + Promise.resolve({ + isLoggedIn: false, + userName: '' + }) + ) + .mockImplementationOnce(() => + Promise.resolve({ + isLoggedIn: true, + userName + }) + ) + jest + .spyOn(verifySasViyaLoginModule, 'verifySasViyaLogin') + .mockImplementation(() => Promise.resolve({ isLoggedIn: false })) + + const loginResponse = await authManager.redirectedLogIn({}) + + expect(loginResponse.isLoggedIn).toBeFalsy() + expect(loginResponse.userName).toEqual('') + + expect(openWebPageModule.openWebPage).toHaveBeenCalledWith( + `/SASLogon/home`, + 'SASLogon', + { + width: 500, + height: 600 + }, + undefined + ) + expect(authManager['fetchUserName']).toHaveBeenCalledTimes(1) + + expect(authCallback).toHaveBeenCalledTimes(0) + }) + + it('should return empty username if user rejects to re-login', async () => { + const authManager = new AuthManager( + serverUrl, + serverType, + requestClient, + authCallback + ) + jest + .spyOn(authManager, 'fetchUserName') + .mockImplementationOnce(() => + Promise.resolve({ + isLoggedIn: false, + userName: '' + }) + ) + .mockImplementationOnce(() => + Promise.resolve({ + isLoggedIn: true, + userName + }) + ) + jest + .spyOn(openWebPageModule, 'openWebPage') + .mockImplementation(() => Promise.resolve(null)) + + const loginResponse = await authManager.redirectedLogIn({}) + + expect(loginResponse.isLoggedIn).toBeFalsy() + expect(loginResponse.userName).toEqual('') + + expect(openWebPageModule.openWebPage).toHaveBeenCalledWith( + `/SASLogon/home`, + 'SASLogon', + { + width: 500, + height: 600 + }, + undefined + ) + expect(authManager['fetchUserName']).toHaveBeenCalledTimes(1) + + expect(authCallback).toHaveBeenCalledTimes(0) + }) }) - it('should check and return session information if logged in', async () => { - const authManager = new AuthManager( - serverUrl, - serverType, - requestClient, - authCallback - ) - mockedAxios.get.mockImplementation(() => - Promise.resolve({ data: mockedCurrentUserApi(userName) }) - ) + describe('checkSession', () => { + it('return session information when logged in', async () => { + const authManager = new AuthManager( + serverUrl, + serverType, + requestClient, + authCallback + ) + mockedAxios.get.mockImplementation(() => + Promise.resolve({ data: mockedCurrentUserApi(userName) }) + ) - const response = await authManager.checkSession() - expect(response.isLoggedIn).toBeTruthy() - expect(response.userName).toEqual(userName) - expect(mockedAxios.get).toHaveBeenNthCalledWith( - 1, - `http://test-server.com/identities/users/@currentUser`, - { - withCredentials: true, - responseType: 'text', - transformResponse: undefined, - headers: { - Accept: '*/*', - 'Content-Type': 'text/plain' + const response = await authManager.checkSession() + expect(response.isLoggedIn).toBeTruthy() + expect(response.userName).toEqual(userName) + expect(mockedAxios.get).toHaveBeenNthCalledWith( + 1, + `http://test-server.com/identities/users/@currentUser`, + { + withCredentials: true, + responseType: 'text', + transformResponse: undefined, + headers: { + Accept: '*/*', + 'Content-Type': 'text/plain' + } } - } - ) + ) + }) + + it('return session information when logged in - SAS9', async () => { + // username cannot have `-` and cannot be uppercased + const username = 'testusername' + const serverType = ServerType.Sas9 + const authManager = new AuthManager( + serverUrl, + serverType, + requestClient, + authCallback + ) + mockedAxios.get.mockImplementation(() => + Promise.resolve({ + data: `"title":"Log Off ${username}","url":"javascript: clearFrame(\"/SASStoredProcess/do?_action=logoff\")"' })` + }) + ) + + const response = await authManager.checkSession() + expect(response.isLoggedIn).toBeTruthy() + expect(response.userName).toEqual(username) + expect(mockedAxios.get).toHaveBeenNthCalledWith( + 1, + `http://test-server.com/SASStoredProcess`, + { + withCredentials: true, + responseType: 'text', + transformResponse: undefined, + headers: { + Accept: '*/*', + 'Content-Type': 'text/plain' + } + } + ) + }) + + it('return session information when logged in - SAS9 - having full name in html', async () => { + const fullname = 'FirstName LastName' + const username = 'firlas' + const serverType = ServerType.Sas9 + const authManager = new AuthManager( + serverUrl, + serverType, + requestClient, + authCallback + ) + mockedAxios.get.mockImplementation(() => + Promise.resolve({ + data: `"title":"Log Off ${fullname}","url":"javascript: clearFrame(\"/SASStoredProcess/do?_action=logoff\")"' })` + }) + ) + + const response = await authManager.checkSession() + expect(response.isLoggedIn).toBeTruthy() + expect(response.userName).toEqual(username) + expect(mockedAxios.get).toHaveBeenNthCalledWith( + 1, + `http://test-server.com/SASStoredProcess`, + { + withCredentials: true, + responseType: 'text', + transformResponse: undefined, + headers: { + Accept: '*/*', + 'Content-Type': 'text/plain' + } + } + ) + }) + + it('perform logout when not logged in', async () => { + const authManager = new AuthManager( + serverUrl, + serverType, + requestClient, + authCallback + ) + mockedAxios.get + .mockImplementationOnce(() => Promise.resolve({ status: 401 })) + .mockImplementation(() => Promise.resolve({})) + + const response = await authManager.checkSession() + expect(response.isLoggedIn).toBeFalsy() + expect(response.userName).toEqual('') + expect(mockedAxios.get).toHaveBeenNthCalledWith( + 1, + `http://test-server.com/identities/users/@currentUser`, + { + withCredentials: true, + responseType: 'text', + transformResponse: undefined, + headers: { + Accept: '*/*', + 'Content-Type': 'text/plain' + } + } + ) + expect(mockedAxios.get).toHaveBeenNthCalledWith( + 2, + `/SASLogon/logout.do?`, + getHeadersJson + ) + }) }) }) + +const getHeadersJson = { + withCredentials: true, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json' + }, + responseType: 'json' +} diff --git a/src/auth/spec/openWebPage.spec.ts b/src/auth/spec/openWebPage.spec.ts new file mode 100644 index 0000000..a2f72a2 --- /dev/null +++ b/src/auth/spec/openWebPage.spec.ts @@ -0,0 +1,64 @@ +/** + * @jest-environment jsdom + */ +import { openWebPage } from '../openWebPage' +import * as loginPromptModule from '../../utils/loginPrompt' + +describe('openWebPage', () => { + const serverUrl = 'http://test-server.com' + + describe('window.open is not blocked', () => { + const mockedOpen = jest + .fn() + .mockImplementation(() => ({} as unknown as Window)) + const originalOpen = window.open + + beforeAll(() => { + window.open = mockedOpen + }) + afterAll(() => { + window.open = originalOpen + }) + + it(`should return new Window popup - using default adapter's dialog`, async () => { + await expect(openWebPage(serverUrl)).resolves.toBeDefined() + + expect(mockedOpen).toBeCalled() + }) + }) + + describe('window.open is blocked', () => { + const mockedOpen = jest.fn().mockImplementation(() => null) + const originalOpen = window.open + + beforeAll(() => { + window.open = mockedOpen + }) + afterAll(() => { + window.open = originalOpen + }) + + it(`should return new Window popup - using default adapter's dialog`, async () => { + jest.mock('../../utils/loginPrompt') + jest + .spyOn(loginPromptModule, 'openLoginPrompt') + .mockImplementation(() => Promise.resolve(true)) + + await expect(openWebPage(serverUrl)).resolves.toBeDefined() + expect(loginPromptModule.openLoginPrompt).toBeCalled() + expect(mockedOpen).toBeCalled() + }) + + it(`should return new Window popup - using frontend's provided onloggedOut`, async () => { + const onLoggedOut = jest + .fn() + .mockImplementation(() => Promise.resolve(true)) + + await expect( + openWebPage(serverUrl, undefined, undefined, onLoggedOut) + ).resolves.toBeDefined() + expect(onLoggedOut).toBeCalled() + expect(mockedOpen).toBeCalled() + }) + }) +})