From e88baa1995d86f34d70b0fde13e078ce3c67ea37 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Thu, 3 Jun 2021 14:29:50 +0200 Subject: [PATCH] 79700: specs for modal, auth check for idleness tracking & stop blocking at token success --- src/app/core/auth/auth.effects.ts | 1 - src/app/core/auth/auth.reducer.ts | 1 + src/app/root/root.component.ts | 30 ++-- .../idle-modal/idle-modal.component.spec.ts | 128 ++++++++++++++++++ src/assets/i18n/en.json5 | 2 +- src/environments/environment.common.ts | 7 +- 6 files changed, 151 insertions(+), 18 deletions(-) create mode 100644 src/app/shared/idle-modal/idle-modal.component.spec.ts diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts index c133310471..f7b81dc4ef 100644 --- a/src/app/core/auth/auth.effects.ts +++ b/src/app/core/auth/auth.effects.ts @@ -263,7 +263,6 @@ export class AuthEffects { filter((action: Action) => !IDLE_TIMER_IGNORE_TYPES.includes(action.type)), // Using switchMap the timer will be interrupted and restarted if a new action comes in, so idleness timer restarts switchMap(() => { - this.authService.isAuthenticated(); return timer(environment.auth.ui.timeUntilIdle); }), map(() => { diff --git a/src/app/core/auth/auth.reducer.ts b/src/app/core/auth/auth.reducer.ts index 0424a58898..f26ddb0182 100644 --- a/src/app/core/auth/auth.reducer.ts +++ b/src/app/core/auth/auth.reducer.ts @@ -193,6 +193,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut return Object.assign({}, state, { authToken: (action as RefreshTokenSuccessAction).payload, refreshing: false, + blocking: false }); case AuthActionTypes.ADD_MESSAGE: diff --git a/src/app/root/root.component.ts b/src/app/root/root.component.ts index c2d3c96951..81ae1a745c 100644 --- a/src/app/root/root.component.ts +++ b/src/app/root/root.component.ts @@ -2,7 +2,12 @@ import { map, take } from 'rxjs/operators'; import { Component, Inject, OnInit, Optional, Input } from '@angular/core'; import { Router } from '@angular/router'; -import { combineLatest as combineLatestObservable, Observable, of } from 'rxjs'; +import { + combineLatest as observableCombineLatest, + combineLatest as combineLatestObservable, + Observable, + of +} from 'rxjs'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; @@ -84,18 +89,19 @@ export class RootComponent implements OnInit { map(([collapsed, mobile]) => collapsed || mobile) ); - this.authService.isUserIdle().subscribe((userIdle: boolean) => { - if (userIdle) { - if (!this.idleModalOpen) { - const modalRef = this.modalService.open(IdleModalComponent); - this.idleModalOpen = true; - modalRef.componentInstance.response.pipe(take(1)).subscribe((closed: boolean) => { - if (closed) { - this.idleModalOpen = false; - } - }); + observableCombineLatest([this.authService.isUserIdle(), this.authService.isAuthenticated()]) + .subscribe(([userIdle, authenticated]) => { + if (userIdle && authenticated) { + if (!this.idleModalOpen) { + const modalRef = this.modalService.open(IdleModalComponent); + this.idleModalOpen = true; + modalRef.componentInstance.response.pipe(take(1)).subscribe((closed: boolean) => { + if (closed) { + this.idleModalOpen = false; + } + }); + } } - } }); } } diff --git a/src/app/shared/idle-modal/idle-modal.component.spec.ts b/src/app/shared/idle-modal/idle-modal.component.spec.ts new file mode 100644 index 0000000000..639cbd6ad1 --- /dev/null +++ b/src/app/shared/idle-modal/idle-modal.component.spec.ts @@ -0,0 +1,128 @@ +import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; +import { IdleModalComponent } from './idle-modal.component'; +import { AuthService } from '../../core/auth/auth.service'; +import { By } from '@angular/platform-browser'; + +describe('IdleModalComponent', () => { + let component: IdleModalComponent; + let fixture: ComponentFixture; + let debugElement: DebugElement; + + const modalStub = jasmine.createSpyObj('modalStub', ['close']); + const authServiceStub = jasmine.createSpyObj('authService', ['setIdle', 'logout']); + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [IdleModalComponent], + providers: [ + { provide: NgbActiveModal, useValue: modalStub }, + { provide: AuthService, useValue: authServiceStub } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(IdleModalComponent); + component = fixture.componentInstance; + debugElement = fixture.debugElement; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('extendSessionPressed', () => { + beforeEach(fakeAsync(() => { + spyOn(component.response, 'next'); + component.extendSessionPressed(); + })); + it('should set idle to false', () => { + expect(authServiceStub.setIdle).toHaveBeenCalledWith(false); + }); + it('should close the modal', () => { + expect(modalStub.close).toHaveBeenCalled(); + }); + it('response \'closed\' should have true as next', () => { + expect(component.response.next).toHaveBeenCalledWith(true); + }); + }); + + describe('logOutPressed', () => { + beforeEach(() => { + component.logOutPressed(); + }); + it('should logout', () => { + expect(authServiceStub.logout).toHaveBeenCalled(); + }); + it('should close the modal', () => { + expect(modalStub.close).toHaveBeenCalled(); + }); + }); + + describe('closePressed', () => { + beforeEach(fakeAsync(() => { + spyOn(component.response, 'next'); + component.closePressed(); + })); + it('should set idle to false', () => { + expect(authServiceStub.setIdle).toHaveBeenCalledWith(false); + }); + it('should close the modal', () => { + expect(modalStub.close).toHaveBeenCalled(); + }); + it('response \'closed\' should have true as next', () => { + expect(component.response.next).toHaveBeenCalledWith(true); + }); + }); + + describe('when the click method emits on extend session button', () => { + beforeEach(fakeAsync(() => { + spyOn(component, 'extendSessionPressed'); + debugElement.query(By.css('button.confirm')).triggerEventHandler('click', { + preventDefault: () => {/**/ + } + }); + tick(); + fixture.detectChanges(); + })); + it('should call the extendSessionPressed method on the component', () => { + expect(component.extendSessionPressed).toHaveBeenCalled(); + }); + }); + + describe('when the click method emits on log out button', () => { + beforeEach(fakeAsync(() => { + spyOn(component, 'logOutPressed'); + debugElement.query(By.css('button.cancel')).triggerEventHandler('click', { + preventDefault: () => {/**/ + } + }); + tick(); + fixture.detectChanges(); + })); + it('should call the logOutPressed method on the component', () => { + expect(component.logOutPressed).toHaveBeenCalled(); + }); + }); + + describe('when the click method emits on close button', () => { + beforeEach(fakeAsync(() => { + spyOn(component, 'closePressed'); + debugElement.query(By.css('.close')).triggerEventHandler('click', { + preventDefault: () => {/**/ + } + }); + tick(); + fixture.detectChanges(); + })); + it('should call the closePressed method on the component', () => { + expect(component.closePressed).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 6105c79cd9..5501b92aa7 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -3697,7 +3697,7 @@ "idle-modal.header": "Session will expire soon", - "idle-modal.info": "For security reasons, user sessions expire after {{ timeToExpire }} minutes of inactivity. Your session will expire soon. Would you like to extend it or log out?”", + "idle-modal.info": "For security reasons, user sessions expire after {{ timeToExpire }} minutes of inactivity. Your session will expire soon. Would you like to extend it or log out?", "idle-modal.log-out": "Log out", diff --git a/src/environments/environment.common.ts b/src/environments/environment.common.ts index a7d4ec8a00..24496386e9 100644 --- a/src/environments/environment.common.ts +++ b/src/environments/environment.common.ts @@ -48,17 +48,16 @@ export const environment: GlobalConfig = { ui: { // the amount of time before the idle warning is shown // timeUntilIdle: 15 * 60 * 1000, // 15 minutes - timeUntilIdle: 1 * 60 * 1000, // 1 minutes + timeUntilIdle: 30 * 1000, // 30 seconds // the amount of time the user has to react after the idle warning is shown before they are logged out. // idleGracePeriod: 5 * 60 * 1000, // 5 minutes - idleGracePeriod: 1 * 60 * 1000, // 1 minutes + idleGracePeriod: 1 * 60 * 1000, // 1 minute }, // Authority REST settings rest: { // If the rest token expires in less than this amount of time, it will be refreshed automatically. // This is independent from the idle warning. - // timeLeftBeforeTokenRefresh: 2 * 60 * 1000, // 2 minutes - timeLeftBeforeTokenRefresh: 0.25 * 60 * 1000, // 25 seconds + timeLeftBeforeTokenRefresh: 2 * 60 * 1000, // 2 minutes }, }, // Form settings