diff --git a/src/docs/asciidoc/docs/release_notes.adoc b/src/docs/asciidoc/docs/release_notes.adoc index 84c024b555..061002e533 100644 --- a/src/docs/asciidoc/docs/release_notes.adoc +++ b/src/docs/asciidoc/docs/release_notes.adoc @@ -7,21 +7,9 @@ = Version 1.3.0.RELEASE == Issue - -== Bug - -* [OC-414] Issue with using user data in templates -* [OC-857] Users - PUT /groups/{id}/users doesn't delete former group members -* [OC-883] Need a click on button "log in using keyCloak" after login in implicit mode - -== Task * [OC-844] Add a UI style for day and a UI style for night with a button to permute. + This functionality is optional and can be activate/desactivate via configuration + https://opfab.github.io/documentation/current/deployment/#_web_ui -* [OC-846] Update documentation for "OC-737 : Add an object entity in the user service" -* [OC-737] Add an object entity in the user service + -Below, you will find information about our new object "Entity" and its endpoints : + -https://opfab.github.io/documentation/current/api/users/#/entities * [OC-736] Add a field "name" to the object Group [WARNING] @@ -41,9 +29,10 @@ Group{ Please, in the config service, make sure to modify the file users.yml, replacing the field "name" by "id", so that the Groups objects contain the mandatory field id. ==== -* [OC-858] Refactoring UI Timeline code for circle content and position computing -* [OC-870] Refactoring cardWriteSerice class in cardPublication service -* [OC-869] Small improvements in karate test +* [OC-737] Add an object entity in the user service + +Below, you will find information about our new object "Entity" and its endpoints : + +https://opfab.github.io/documentation/current/api/users/#/entities + * [OC-744] Replace web-ui spring component with Nginx [NOTE] @@ -64,20 +53,39 @@ cf Web-ui README.adoc, or the chapter link:https://opfab.github.io/documentation of Setting up your development environment within Development Environment documentation, or the entry about [OC-744] of this current file. ==== -* [OC-658] gradle assemble shouldn't run the ui unit tests +* [OC-748] Remove button "log in using keyCloak", go directly to login page * [OC-738] Add a group perimeter object in the user service [NOTE] ==== Work is still in progress. The object is not usable at the moment. ==== -* [OC-878] remove unnecessary utilities time classes -* [OC-547] remove unnecessary UI test -* [OC-880] modify KeyCloak configuration to facilitate dev -* [OC-882] Remove unused publishTestData method in cardConsultation service + * [OC-877] User Service : Implement endpoint GET /CurrentUserWithPerimeters [NOTE] ==== Work is still in progress. The endpoint is not usable at the moment. ==== + +== Bug + +* [OC-414] Issue with using user data in templates +* [OC-857] Users - PUT /groups/{id}/users doesn't delete former group members +* [OC-883] Need a click on button "log in using keyCloak" after login in implicit mode + +== Task + +* [OC-846] Update documentation for "OC-737 : Add an object entity in the user service" +* [OC-858] Refactoring UI Timeline code for circle content and position computing +* [OC-870] Refactoring cardWriteSerice class in cardPublication service +* [OC-869] Small improvements in karate test +* [OC-658] gradle assemble shouldn't run the ui unit tests +* [OC-878] remove unnecessary utilities time classes +* [OC-547] remove unnecessary UI test +* [OC-880] modify KeyCloak configuration to facilitate dev +* [OC-882] Remove unused publishTestData method in cardConsultation service + + + + diff --git a/ui/main/src/app/app.component.html b/ui/main/src/app/app.component.html index b7a2846521..84c16e1d26 100644 --- a/ui/main/src/app/app.component.html +++ b/ui/main/src/app/app.component.html @@ -9,7 +9,7 @@
- +
Application is loading ... diff --git a/ui/main/src/app/app.component.spec.ts b/ui/main/src/app/app.component.spec.ts deleted file mode 100644 index 579ed3f83c..0000000000 --- a/ui/main/src/app/app.component.spec.ts +++ /dev/null @@ -1,178 +0,0 @@ -/* Copyright (c) 2020, RTE (http://www.rte-france.com) - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - - -import {async, TestBed} from '@angular/core/testing'; -import {AppComponent} from './app.component'; -import {RouterTestingModule} from '@angular/router/testing'; -import {Store, StoreModule} from '@ngrx/store'; -import {appReducer, AppState} from '@ofStore/index'; -import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing'; -import {of} from 'rxjs'; -import {NavbarComponent} from './components/navbar/navbar.component'; -import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; -import {selectCurrentUrl} from '@ofSelectors/router.selectors'; -import {IconComponent} from './components/navbar/icon/icon.component'; -import {TranslateModule} from '@ngx-translate/core'; -import {NO_ERRORS_SCHEMA} from '@angular/core'; -import {I18nService} from '@ofServices/i18n.service'; -import {Title} from '@angular/platform-browser'; -import {MockStore, provideMockStore} from '@ngrx/store/testing'; -import {emptyAppState4Test} from '@tests/helpers'; -import {AuthenticationImportHelperForSpecs} from '@ofServices/authentication/authentication.service.spec'; -import {HttpClientTestingModule} from '@angular/common/http/testing'; -import createSpyObj = jasmine.createSpyObj; -import {AuthenticationService} from '@ofServices/authentication/authentication.service'; - - -describe('AppComponent', () => { - - let store: Store; - let fixture; - - beforeEach(async(() => { - TestBed.resetTestEnvironment(); - TestBed.initTestEnvironment(BrowserDynamicTestingModule, - platformBrowserDynamicTesting()); - TestBed.configureTestingModule({ - imports: [ - NgbModule.forRoot(), - StoreModule.forRoot(appReducer), - TranslateModule.forRoot(), - // solution 4 RouterTestingModule: https://github.com/coreui/coreui-free-bootstrap-admin-template/issues/202 - RouterTestingModule, - HttpClientTestingModule - ], - declarations: [AppComponent, NavbarComponent, IconComponent], - providers: [{provide: store, useClass: Store}, I18nService, AuthenticationImportHelperForSpecs], - schemas: [ NO_ERRORS_SCHEMA ] - }).compileComponents(); - store = TestBed.get(Store); - spyOn(store, 'dispatch').and.callThrough(); - // avoid exceptions during construction and init of the component - spyOn(store, 'select').and.callFake((obj) => { - if (obj === selectCurrentUrl) { - // called in ngOnInit and passed to mat-tab-url - return of('/test/url'); - } - return of(null); - } - ); - fixture = TestBed.createComponent(AppComponent); - })); - - it('should create the app', async(() => { - const app = fixture.debugElement.componentInstance; - expect(app).toBeTruthy(); - })); - - it(`should have the title 'OperatorFabric' by default`, async(() => { - const app = fixture.debugElement.componentInstance; - expect(app.title).toEqual('OperatorFabric'); - })); - - it('should init the app', async(() => { - const app = fixture.debugElement.componentInstance; - app.ngOnInit(); - fixture.detectChanges(); - expect(app).toBeTruthy(); - })); - -}); - -describe('AppComponent', () => { - - let store: MockStore; - let fixture; - const i18nServiceSpy = createSpyObj('i18NService', ['changeLocale']); - const authServiceSpy = createSpyObj('AuthenticationService', ['initializeAuthentication', - 'linkAuthenticationStatus']); - - let titleService: Title; - - const initialState = { - ...emptyAppState4Test - , authentication: { expirationDate: null} - } as AppState; - /* Using authInitialState to declare expirationDate (required) caused an error because it is not - * defined in this scope.*/ - - beforeEach(async(() => { - TestBed.resetTestEnvironment(); - TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); - - TestBed.configureTestingModule({ - imports: [ - NgbModule.forRoot(), - // solution 4 RouterTestingModule: https://github.com/coreui/coreui-free-bootstrap-admin-template/issues/202 - RouterTestingModule - ], - - declarations: [AppComponent], - providers: [ - provideMockStore({initialState}), - {provide: I18nService, useValue: i18nServiceSpy}, - Title, - {provide: AuthenticationService, useValue: authServiceSpy}], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); - store = TestBed.get(Store); - fixture = TestBed.createComponent(AppComponent); - titleService = TestBed.get(Title); - })); - - it(`should have the title 'toto' as set in Config service`, async(() => { - - store.setState({ - ...initialState - , config: { - loading: false, - loaded: false, - error: null, - retry: 0, - config: {title: 'toto'} - } - }); - fixture.detectChanges(); - expect(titleService.getTitle()).toEqual('toto'); - })); - - it(`should have the default title 'OperatorFabric' when title is set to '' in Config service`, - async(() => { - - store.setState({ - ...initialState - , config: { - loading: false, - loaded: false, - error: null, - retry: 0, - config: {title: ''} - } - }); - fixture.detectChanges(); - expect(titleService.getTitle()).toEqual('OperatorFabric'); - })); - - it(`should have the default title 'OperatorFabric' when title parameter not present in Config service`, - async(() => { - - store.setState({ - ...initialState - , config: { - loading: false, - loaded: false, - error: null, - retry: 0, - config: {} - } - }); - fixture.detectChanges(); - expect(titleService.getTitle()).toEqual('OperatorFabric'); - })); -}); - diff --git a/ui/main/src/app/app.component.ts b/ui/main/src/app/app.component.ts index c975abd650..b6e7dc4e14 100644 --- a/ui/main/src/app/app.component.ts +++ b/ui/main/src/app/app.component.ts @@ -6,16 +6,15 @@ */ -import {Component, OnInit} from '@angular/core'; -import {Title} from '@angular/platform-browser'; -import {select, Store} from '@ngrx/store'; -import {Observable} from 'rxjs'; -import {AppState} from '@ofStore/index'; -import {selectCurrentUrl, selectRouterState} from '@ofSelectors/router.selectors'; -import {AuthenticationService} from '@ofServices/authentication/authentication.service'; -import {LoadConfig} from '@ofActions/config.actions'; -import {buildConfigSelector, selectConfigLoaded, selectMaxedRetries} from '@ofSelectors/config.selectors'; -import {I18nService} from '@ofServices/i18n.service'; +import { Component, OnInit } from '@angular/core'; +import { Title } from '@angular/platform-browser'; +import { Store } from '@ngrx/store'; +import { AppState } from '@ofStore/index'; +import { AuthenticationService } from '@ofServices/authentication/authentication.service'; +import { LoadConfig } from '@ofActions/config.actions'; +import { buildConfigSelector, selectConfigLoaded, selectMaxedRetries } from '@ofSelectors/config.selectors'; +import { selectIdentifier } from '@ofSelectors/authentication.selectors'; +import { I18nService } from '@ofServices/i18n.service'; @Component({ selector: 'of-root', @@ -24,54 +23,60 @@ import {I18nService} from '@ofServices/i18n.service'; }) export class AppComponent implements OnInit { readonly title = 'OperatorFabric'; - getRoutePE: Observable; - currentPath: any; isAuthenticated$ = false; configLoaded = false; + useCodeOrImplicitFlow = true; private maxedRetries = false; /** * NB: I18nService is injected to trigger its constructor at application startup - * @param store - * @param i18nService */ constructor(private store: Store, - private i18nService: I18nService, - private titleService: Title + private i18nService: I18nService, + private titleService: Title , private authenticationService: AuthenticationService) { - this.getRoutePE = this.store.pipe(select(selectRouterState)); } - public setTitle(newTitle: string) { - this.titleService.setTitle(newTitle); - } - - /** - * On Init the app take trace of the current url and of the authentication status - * Once the subscription done, send an Action to Check the current authentication status. - */ ngOnInit() { - this.store.pipe(select(selectCurrentUrl)).subscribe(url => this.currentPath = url); + this.loadConfiguration(); + this.launchAuthenticationProcessWhenConfigurationLoaded(); + this.waitForUserTobeAuthenticated(); + this.setTitle(); + } + + private loadConfiguration() { + this.store.dispatch(new LoadConfig()); + this.store + .select(selectMaxedRetries) + .subscribe((maxedRetries => this.maxedRetries = maxedRetries)); + } + private launchAuthenticationProcessWhenConfigurationLoaded() { this.store .select(selectConfigLoaded) .subscribe(loaded => { - this.authenticationService.initializeAuthentication(); - this.authenticationService.linkAuthenticationStatus( - (isAuthenticated: boolean) => { - this.isAuthenticated$ = isAuthenticated; + if (loaded) { + this.authenticationService.initializeAuthentication(); + this.useCodeOrImplicitFlow = this.authenticationService.isAuthModeCodeOrImplicitFlow(); + } + this.configLoaded = loaded; }); - this.configLoaded = loaded - }); + } + + private waitForUserTobeAuthenticated() { this.store - .select(selectMaxedRetries) - .subscribe((maxedRetries => this.maxedRetries = maxedRetries)); - this.store.dispatch(new LoadConfig()); + .select(selectIdentifier) + .subscribe(identifier => { + if (identifier) this.isAuthenticated$ = true; + }); + } - const sTitle = this.store.select(buildConfigSelector('title', this.title)); - sTitle.subscribe(data => { - this.setTitle(data); - }); + private setTitle() { + this.store + .select(buildConfigSelector('title', this.title)) + .subscribe(data => { + this.titleService.setTitle(data); + }); } } diff --git a/ui/main/src/app/components/login/login.component.html b/ui/main/src/app/components/login/login.component.html index e8f71f481c..aca3aa3c24 100644 --- a/ui/main/src/app/components/login/login.component.html +++ b/ui/main/src/app/components/login/login.component.html @@ -9,7 +9,7 @@
-
diff --git a/ui/main/src/app/components/login/login.component.ts b/ui/main/src/app/components/login/login.component.ts index 9cf3db670e..de0444048a 100644 --- a/ui/main/src/app/components/login/login.component.ts +++ b/ui/main/src/app/components/login/login.component.ts @@ -11,13 +11,10 @@ import {FormControl, FormGroup} from '@angular/forms'; import {Store} from '@ngrx/store'; import {TryToLogIn} from '@ofActions/authentication.actions'; import {AppState} from '@ofStore/index'; -import {buildConfigSelector} from "@ofSelectors/config.selectors"; -import {filter, map} from "rxjs/operators"; -import {Observable} from "rxjs"; -import {AuthenticationService} from "@ofServices/authentication/authentication.service"; +import {filter} from "rxjs/operators"; import {selectMessage} from "@ofSelectors/authentication.selectors"; import {Message, MessageLevel} from "@ofModel/message.model"; -import {OAuthService} from "angular-oauth2-oidc"; + @Component({ selector: 'of-login', @@ -26,23 +23,16 @@ import {OAuthService} from "angular-oauth2-oidc"; }) export class LoginComponent implements OnInit { - hide: boolean; userForm: FormGroup; - useCodeOrImplicitFlow: boolean; loginMessage: Message; - codeProvider: any; /* istanbul ignore next */ - constructor(private store: Store, private service: AuthenticationService) { + constructor(private store: Store) { } ngOnInit() { - this.useCodeOrImplicitFlow = this.service.isAuthModeCodeOrImplicitFlow(); this.store.select(selectMessage).pipe(filter(m => m != null && m.level === MessageLevel.ERROR)) .subscribe(m => this.loginMessage = m); - this.store.select(buildConfigSelector('security.oauth2.flow.provider')) - .subscribe(provider => this.codeProvider = {name: provider}); - this.hide = true; this.userForm = new FormGroup({ identifier: new FormControl(''), password: new FormControl('') @@ -62,8 +52,4 @@ export class LoginComponent implements OnInit { this.userForm.reset(); } - codeFlow(): void { - this.service.move(); - } - } diff --git a/ui/main/src/app/components/navbar/navbar.component.spec.ts b/ui/main/src/app/components/navbar/navbar.component.spec.ts index 5909510ba7..08b7f5082b 100644 --- a/ui/main/src/app/components/navbar/navbar.component.spec.ts +++ b/ui/main/src/app/components/navbar/navbar.component.spec.ts @@ -21,7 +21,7 @@ import {By} from '@angular/platform-browser'; import {InfoComponent} from './info/info.component'; import {TimeService} from '@ofServices/time.service'; import clock = jasmine.clock; -import { emptyAppState4Test } from '@tests/helpers'; +import { emptyAppState4Test,AuthenticationImportHelperForSpecs} from '@tests/helpers'; import { configInitialState } from '@ofStore/states/config.state'; import { of } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -32,7 +32,6 @@ import { authInitialState } from '@ofStore/states/authentication.state'; import { selectCurrentUrl } from '@ofStore/selectors/router.selectors'; import {MenuLinkComponent} from './menus/menu-link/menu-link.component'; import { CustomLogoComponent } from './custom-logo/custom-logo.component'; -import {AuthenticationImportHelperForSpecs} from '@ofServices/authentication/authentication.service.spec'; import {FontAwesomeIconsModule} from "../../modules/utilities/fontawesome-icons.module"; import {GlobalStyleService} from '@ofServices/global-style.service'; diff --git a/ui/main/src/app/modules/archives/archive.component.spec.ts b/ui/main/src/app/modules/archives/archive.component.spec.ts index 4842ba523a..79c41c729f 100644 --- a/ui/main/src/app/modules/archives/archive.component.spec.ts +++ b/ui/main/src/app/modules/archives/archive.component.spec.ts @@ -23,7 +23,6 @@ import {ArchiveListComponent} from './components/archive-list/archive-list.compo import {ArchiveFiltersComponent} from './components/archive-filters/archive-filters.component'; import {getRandomPage} from '@tests/helpers'; import {HttpClientTestingModule} from '@angular/common/http/testing'; -import {AuthenticationImportHelperForSpecs} from '@ofServices/authentication/authentication.service.spec'; describe('ArchivesComponent', () => { let component: ArchivesComponent; @@ -46,8 +45,7 @@ describe('ArchivesComponent', () => { ], providers: [ {provide: Store, useClass: Store}, - {provide: RouterStateSerializer, useClass: CustomRouterStateSerializer}, - AuthenticationImportHelperForSpecs + {provide: RouterStateSerializer, useClass: CustomRouterStateSerializer} ], schemas: [ NO_ERRORS_SCHEMA ] }).compileComponents(); diff --git a/ui/main/src/app/modules/archives/components/archive-filters/archive-filters.component.spec.ts b/ui/main/src/app/modules/archives/components/archive-filters/archive-filters.component.spec.ts index 4c3c236933..27e61dd395 100644 --- a/ui/main/src/app/modules/archives/components/archive-filters/archive-filters.component.spec.ts +++ b/ui/main/src/app/modules/archives/components/archive-filters/archive-filters.component.spec.ts @@ -27,7 +27,7 @@ import createSpyObj = jasmine.createSpyObj; import { getRandomPage } from '@tests/helpers'; import * as fromStore from '@ofStore/selectors/archive.selectors'; import { ArchiveQuerySuccess } from '@ofStore/actions/archive.actions'; -import {AuthenticationImportHelperForSpecs} from '@ofServices/authentication/authentication.service.spec'; +import {AuthenticationImportHelperForSpecs} from '@tests/helpers' describe('ArchiveFiltersComponent', () => { let component: ArchiveFiltersComponent; diff --git a/ui/main/src/app/modules/cards/components/card-details/card-details.component.spec.ts b/ui/main/src/app/modules/cards/components/card-details/card-details.component.spec.ts index 729c44ae82..61e9164803 100644 --- a/ui/main/src/app/modules/cards/components/card-details/card-details.component.spec.ts +++ b/ui/main/src/app/modules/cards/components/card-details/card-details.component.spec.ts @@ -15,7 +15,7 @@ import {Store, StoreModule} from '@ngrx/store'; import {appReducer, AppState} from '@ofStore/index'; import {RouterTestingModule} from '@angular/router/testing'; import {LoadCardSuccess} from '@ofActions/card.actions'; -import {getOneRandomCard, getOneRandomThird, getRandomI18nData} from '@tests/helpers'; +import {getOneRandomCard, getOneRandomThird, getRandomI18nData,AuthenticationImportHelperForSpecs} from '@tests/helpers'; import {By} from '@angular/platform-browser'; import {ThirdsI18nLoaderFactory, ThirdsService} from '../../../../services/thirds.service'; import {ServicesModule} from '@ofServices/services.module'; @@ -27,7 +27,6 @@ import {of} from 'rxjs'; import {Process, State} from '@ofModel/thirds.model'; import {Detail} from '@ofModel/card.model'; import {Map as OfMap} from '@ofModel/map'; -import {AuthenticationImportHelperForSpecs} from '@ofServices/authentication/authentication.service.spec'; describe('CardDetailsComponent', () => { let component: CardDetailsComponent; diff --git a/ui/main/src/app/modules/cards/components/card/card.component.spec.ts b/ui/main/src/app/modules/cards/components/card/card.component.spec.ts index 944e8fdb37..b075f3cb01 100644 --- a/ui/main/src/app/modules/cards/components/card/card.component.spec.ts +++ b/ui/main/src/app/modules/cards/components/card/card.component.spec.ts @@ -27,7 +27,6 @@ import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; import SpyObj = jasmine.SpyObj; import createSpyObj = jasmine.createSpyObj; import { AddActionsAppear } from '@ofStore/actions/card.actions'; -import {AuthenticationImportHelperForSpecs} from "@ofServices/authentication/authentication.service.spec"; describe('CardComponent', () => { let lightCardDetailsComp: CardComponent; @@ -63,7 +62,7 @@ describe('CardComponent', () => { {provide: Router, useValue: routerSpy}, ThirdsService, {provide: 'TimeEventSource', useValue: null}, - TimeService, I18nService, AuthenticationImportHelperForSpecs + TimeService, I18nService ]}).compileComponents(); store = TestBed.get(Store); spyOn(store, 'dispatch').and.callThrough(); diff --git a/ui/main/src/app/modules/cards/components/detail/detail.component.spec.ts b/ui/main/src/app/modules/cards/components/detail/detail.component.spec.ts index 95d3043675..216d193ddf 100644 --- a/ui/main/src/app/modules/cards/components/detail/detail.component.spec.ts +++ b/ui/main/src/app/modules/cards/components/detail/detail.component.spec.ts @@ -14,7 +14,8 @@ import { getOneRandomCardWithRandomDetails, getOneRandomThird, getRandomI18nData, - getRandomIndex + getRandomIndex, + AuthenticationImportHelperForSpecs } from '@tests/helpers'; import {ThirdsI18nLoaderFactory, ThirdsService} from '../../../../services/thirds.service'; import {ServicesModule} from '@ofServices/services.module'; @@ -29,7 +30,7 @@ import {By} from '@angular/platform-browser'; import {of} from 'rxjs'; import {Action, ActionType, Process, State} from '@ofModel/thirds.model'; import {Map as OfMap} from '@ofModel/map'; -import {AuthenticationImportHelperForSpecs} from '@ofServices/authentication/authentication.service.spec'; +import {RouterTestingModule} from '@angular/router/testing'; describe('DetailComponent', () => { let component: DetailComponent; @@ -44,6 +45,7 @@ describe('DetailComponent', () => { StoreModule.forRoot(appReducer), ServicesModule, HttpClientTestingModule, + RouterTestingModule, TranslateModule.forRoot({ loader: { provide: TranslateLoader, diff --git a/ui/main/src/app/modules/cards/services/handlebars.service.spec.ts b/ui/main/src/app/modules/cards/services/handlebars.service.spec.ts index 73b5386cab..1c4adc26cb 100644 --- a/ui/main/src/app/modules/cards/services/handlebars.service.spec.ts +++ b/ui/main/src/app/modules/cards/services/handlebars.service.spec.ts @@ -15,17 +15,15 @@ import {TranslateLoader, TranslateModule, TranslateService} from "@ngx-translate import {RouterTestingModule} from "@angular/router/testing"; import {Store, StoreModule} from "@ngrx/store"; import {appReducer, AppState} from "@ofStore/index"; -import {getOneRandomCard} from "@tests/helpers"; +import {getOneRandomCard,AuthenticationImportHelperForSpecs} from "@tests/helpers"; import {LightCard} from "@ofModel/light-card.model"; import {ServicesModule} from "@ofServices/services.module"; import {HandlebarsService} from "./handlebars.service"; import {Guid} from "guid-typescript"; -import {TimeService} from "@ofServices/time.service"; import {I18n} from "@ofModel/i18n.model"; import * as moment from "moment"; import {UserContext} from "@ofModel/user-context.model"; import {DetailContext} from "@ofModel/detail-context.model"; -import {AuthenticationImportHelperForSpecs} from "@ofServices/authentication/authentication.service.spec"; function computeTemplateUri(templateName) { return `${environment.urls.thirds}/testPublisher/templates/${templateName}`; diff --git a/ui/main/src/app/modules/feed/components/card-list/filters/time-filter/time-filter.component.spec.ts b/ui/main/src/app/modules/feed/components/card-list/filters/time-filter/time-filter.component.spec.ts index 9b4e0dccee..a904fa32a3 100644 --- a/ui/main/src/app/modules/feed/components/card-list/filters/time-filter/time-filter.component.spec.ts +++ b/ui/main/src/app/modules/feed/components/card-list/filters/time-filter/time-filter.component.spec.ts @@ -20,7 +20,6 @@ import {By} from "@angular/platform-browser"; import {I18nService} from "@ofServices/i18n.service"; import {TranslateModule} from "@ngx-translate/core"; import {HttpClientTestingModule} from "@angular/common/http/testing"; -import {AuthenticationImportHelperForSpecs} from "@ofServices/authentication/authentication.service.spec"; import {FontAwesomeIconsModule} from "../../../../../utilities/fontawesome-icons.module"; import { FlatpickrModule } from 'angularx-flatpickr'; import * as moment from 'moment-timezone'; @@ -45,8 +44,7 @@ describe('TimeFilterComponent', () => { ], declarations: [TimeFilterComponent], providers:[ I18nService, - {provide: 'TimeEventSource', useValue: null}, - AuthenticationImportHelperForSpecs] + {provide: 'TimeEventSource', useValue: null}] }) .compileComponents(); })); diff --git a/ui/main/src/app/modules/feed/components/time-line/custom-timeline-chart/custom-timeline-chart.component.spec.ts b/ui/main/src/app/modules/feed/components/time-line/custom-timeline-chart/custom-timeline-chart.component.spec.ts index 2b9a16431f..6fbd965692 100644 --- a/ui/main/src/app/modules/feed/components/time-line/custom-timeline-chart/custom-timeline-chart.component.spec.ts +++ b/ui/main/src/app/modules/feed/components/time-line/custom-timeline-chart/custom-timeline-chart.component.spec.ts @@ -21,7 +21,6 @@ import {CustomRouterStateSerializer} from '@ofStates/router.state'; import {TimeService} from '@ofServices/time.service'; import {appReducer, storeConfig} from '@ofStore/index'; import {RouterTestingModule} from '@angular/router/testing'; -import {AuthenticationImportHelperForSpecs} from '@ofServices/authentication/authentication.service.spec'; import {HttpClientTestingModule} from '@angular/common/http/testing'; describe('CustomTimelineChartComponent', () => { @@ -46,8 +45,7 @@ describe('CustomTimelineChartComponent', () => { providers: [{provide: APP_BASE_HREF, useValue: '/'}, {provide: Store, useClass: Store}, {provide: RouterStateSerializer, useClass: CustomRouterStateSerializer}, - {provide: TimeService, useClass: TimeService}, - AuthenticationImportHelperForSpecs], + {provide: TimeService, useClass: TimeService}], schemas: [ NO_ERRORS_SCHEMA ] }) .compileComponents(); diff --git a/ui/main/src/app/modules/feed/components/time-line/custom-timeline-chart/custom-timeline-chart.component.ts b/ui/main/src/app/modules/feed/components/time-line/custom-timeline-chart/custom-timeline-chart.component.ts index 66ab8e356e..3512eeac4e 100644 --- a/ui/main/src/app/modules/feed/components/time-line/custom-timeline-chart/custom-timeline-chart.component.ts +++ b/ui/main/src/app/modules/feed/components/time-line/custom-timeline-chart/custom-timeline-chart.component.ts @@ -257,7 +257,6 @@ export class CustomTimelineChartComponent extends BaseChartComponent implements if (tickIndex + 1 === this.xTicks.length) { endLimit += 1; // Include the limit domain value by adding 1ms - if (cards[cardIndex]) console.log("endLimit =" , endLimit , ",cards[cardIndex].date=",cards[cardIndex].date.valueOf()); } diff --git a/ui/main/src/app/modules/feed/feed.component.spec.ts b/ui/main/src/app/modules/feed/feed.component.spec.ts index 40cc7d1d5d..8c848e385d 100644 --- a/ui/main/src/app/modules/feed/feed.component.spec.ts +++ b/ui/main/src/app/modules/feed/feed.component.spec.ts @@ -31,7 +31,6 @@ import {TranslateModule} from "@ngx-translate/core"; import {NO_ERRORS_SCHEMA} from "@angular/core"; import {ServicesModule} from "@ofServices/services.module"; import {compareByPublishDate} from "@ofStates/feed.state"; -import {AuthenticationImportHelperForSpecs} from "@ofServices/authentication/authentication.service.spec"; describe('FeedComponent', () => { let component: FeedComponent; @@ -51,8 +50,7 @@ describe('FeedComponent', () => { declarations: [CardListComponent, FeedComponent, TimeLineComponent], providers: [ Store, - {provide: RouterStateSerializer, useClass: CustomRouterStateSerializer}, - AuthenticationImportHelperForSpecs], + {provide: RouterStateSerializer, useClass: CustomRouterStateSerializer}], schemas: [ NO_ERRORS_SCHEMA ] }) .compileComponents(); diff --git a/ui/main/src/app/services/authentication/authentication.service.spec.ts b/ui/main/src/app/services/authentication/authentication.service.spec.ts deleted file mode 100644 index a6d1ac0fc7..0000000000 --- a/ui/main/src/app/services/authentication/authentication.service.spec.ts +++ /dev/null @@ -1,506 +0,0 @@ -/* Copyright (c) 2020, RTE (http://www.rte-france.com) - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - - -import {TestBed} from '@angular/core/testing'; - -import { - AuthenticationService, - AuthObject, - CheckTokenResponse, ImplicitAuthenticationHandler, - isInTheFuture, - LocalStorageAuthContent, - PasswordOrCodeAuthenticationHandler -} from './authentication.service'; -import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing'; -import { - ImplicitlyAuthenticated, - PayloadForSuccessfulAuthentication, - UnAuthenticationFromImplicitFlow, - UnableToRefreshOrGetToken -} from '@ofActions/authentication.actions'; -import {Guid} from 'guid-typescript'; -import {getPositiveRandomNumberWithinRange, getRandomAlphanumericValue} from '@tests/helpers'; -import {GuidService} from '@ofServices/guid.service'; -import {Store, StoreModule} from '@ngrx/store'; -import {appReducer} from '@ofStore/index'; -import {environment} from '@env/environment'; -import { - OAuthLogger, - OAuthService, - UrlHelperService, - EventType as OauthEventType, - OAuthSuccessEvent -} from 'angular-oauth2-oidc'; -import createSpyObj = jasmine.createSpyObj; -import {create} from "domain"; -import any = jasmine.any; - - -export const AuthenticationImportHelperForSpecs = [AuthenticationService, - GuidService, - OAuthService, - UrlHelperService, - OAuthLogger]; - -describe('AuthenticationService', () => { - - let httpMock: HttpTestingController; - const token = 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJSbXFOVTNLN0x4ck5SRmtIVTJxcTZZcTEya1RDaXNtRkw5U2N' + - 'wbkNPeDBjIn0.eyJqdGkiOiI5ODIzMDA0MC05YjNiLTQxZmUtYWJlZi0xNGUwYmM5M2YyZmEiLCJleHAiOjE1NTgwMTUwODYsIm' + - '5iZiI6MCwiaWF0IjoxNTU4MDE0Nzg2LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0Ojg5L2F1dGgvcmVhbG1zL2RldiIsImF1ZCI6I' + - 'mFjY291bnQiLCJzdWIiOiI1ZTVhNDY5Zi04ZDIxLTQzMDgtODIyOS0zNTMyZmU2ZTUyZjYiLCJ0eXAiOiJCZWFyZXIiLCJhenAi' + - 'OiJvcGZhYi1jbGllbnQiLCJhdXRoX3RpbWUiOjE1NTgwMTQ3NTgsInNlc3Npb25fc3RhdGUiOiJjYTI5ZTU0My04NWMyLTQ1Nzk' + - 'tYmU2NS0wZmVjOTRmZThhYzIiLCJhY3IiOiIwIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwidW' + - '1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50I' + - 'iwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzdWIiOiJy' + - 'dGUtb3BlcmF0b3IiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InJ0ZS1vcGVyYXRvciJ9.C5' + - 'FPw-UkG1_oKThscTJEvkNNJ6R6b59X8FdQeb1dJUtdFYobdewoPZTb790wfZJuyd2CButBlnUX372QqZETakiKFsH8I2ZnDdkuR' + - 'l0OHuSXCLZayNY8mNADYKvXuvvn4eG6eyHLQ_Ol5T0BHYA9fUuBONRDYXgOJmHrQNKy1lAMUoI6JhRw2pzzju0bHFOIr6P9Dt5G' + - 'eICy5Kxb35vToxzvXXln8A60Bcnx-Aw2NQlx82rfawBxqM28Zm11nWk6Rn2D67woUBca9V6MJDEi12yMqJhaigNja4yXJzuv4eD' + - '_FVzhfd38jzvi_RCUhTNWSVNXirKIoPMYMUP5Ehe1ng'; - const securityConf = {}; - securityConf['oauth2'] = {}; - securityConf['jwt'] = {}; - securityConf['oauth2']['client-id'] = 'opfab-client'; - securityConf['oauth2']['client-secret'] = 'opfab-client-secret'; - securityConf['jwt']['login-claim'] = 'preferred_username'; - securityConf['jwt']['expire-claim'] = 'exp'; - let service: AuthenticationService; - - beforeEach(() => { - - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule, - StoreModule.forRoot(appReducer)], - providers: [AuthenticationImportHelperForSpecs] - }); - httpMock = TestBed.get(HttpTestingController); - service = TestBed.get(AuthenticationService); - }); - - afterEach(() => { - httpMock.verify(); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); - - it('should reject a stored date in the past', () => { - spyOn(localStorage, 'getItem').and.callFake(() => '10'); - expect(service.verifyExpirationDate()).toEqual(false); - expect(service.isExpirationDateOver()).toEqual(true); - }); - - it('should reject a stored date in the past', () => { - spyOn(localStorage, 'getItem').and.callFake(() => '10'); - expect(service.verifyExpirationDate()).toEqual(false); - expect(service.isExpirationDateOver()).toEqual(true); - }); - - it('should reject a stored date isNaN', () => { - spyOn(localStorage, 'getItem').and.callFake(() => 'abcd'); - expect(service.verifyExpirationDate()).toEqual(false); - expect(service.isExpirationDateOver()).toEqual(true); - }); - - it('should reject a stored date if it\'s now', () => { - spyOn(localStorage, 'getItem').and.callFake(() => Date.now().toString()); - expect(service.verifyExpirationDate()).toEqual(false); - expect(service.isExpirationDateOver()).toEqual(true); - }); - - it('should accept a stored date if it\'s in the future', () => { - spyOn(localStorage, 'getItem').and.callFake(() => (Date.now() + 10000).toString()); - expect(service.verifyExpirationDate()).toEqual(true); - expect(service.isExpirationDateOver()).toEqual(false); - }); - - - it('should clear the local storage when clearAuthenticationInformation', () => { - spyOn(localStorage, 'removeItem').and.callThrough(); - service.clearAuthenticationInformation(); - expect(localStorage.removeItem).toHaveBeenCalled(); - }); - - it('should set items in localStorage when save Authentication Information', () => { - spyOn(localStorage, 'setItem').and.callThrough(); - const mockPayload = new PayloadForSuccessfulAuthentication('identifier', - Guid.create(), 'token', new Date()); - service.saveAuthenticationInformation(mockPayload); - expect(localStorage.setItem).toHaveBeenCalled(); - }); - - it('should extract the token from the localstorage', () => { - spyOn(localStorage, 'getItem').and.callThrough(); - service.extractToken(); - expect(localStorage.getItem).toHaveBeenCalled(); - }); - - it('should create a congruent payload object from AuthObject', () => { - service.assignConfigurationProperties(securityConf); - const guid = Guid.create(); - const expirationTime = getPositiveRandomNumberWithinRange(12, 2500); - const authObj = { - identifier: 'rte-operator', - access_token: token, - expires_in: expirationTime, - clientId: guid - } as AuthObject; - const result = service.convert(authObj); - expect(result.token).toEqual(token); - expect(result.identifier).toEqual('rte-operator'); - expect(result.clientId).toEqual(guid); - expect(result.expirationDate.getTime()).toBeGreaterThanOrEqual(Date.now() + expirationTime); - - }); - - it('should give a security header corresponding to "Authorization" containing the current token stored in local storage', () => { - const fakeToken = getRandomAlphanumericValue(254); - localStorage.setItem(LocalStorageAuthContent.token, fakeToken); - const securityHeader = service.getSecurityHeader(); - expect(securityHeader).toBeTruthy(); - const securityHeaderElement = securityHeader['Authorization']; - expect(securityHeaderElement).toBeTruthy(); - expect(securityHeaderElement).toEqual(`Bearer ${fakeToken}`); - }); - describe('#checkAuthentication', () => { - it('should check token successfully', () => { - const response = new CheckTokenResponse('johndoe', 123, 'opfab-client'); - service.checkAuthentication('fake-token').subscribe((next: CheckTokenResponse) => { - expect(next.sub).toEqual('johndoe'); - expect(next.exp).toEqual(123); - expect(next.clientId).toEqual('opfab-client'); - }); - const calls = httpMock.match(req => req.url === `${environment.urls.auth}/check_token`); - expect(calls.length).toEqual(1); - calls[0].flush(response); - - }); - - it('should fail if check token unsuccessfully', () => { - // const response = new CheckTokenResponse('johndoe', 123, 'opfab-client'); - service.checkAuthentication('fake-token').subscribe((next) => { - fail(`unexpected value:${next}`); - }, (err) => { - expect(err).not.toBeNull(); - }); - const calls = httpMock.match(req => req.url === `${environment.urls.auth}/check_token`); - expect(calls.length).toEqual(1); - calls[0].error(new ErrorEvent('invalid token')); - - }); - it('should emit null token empty', () => { - // const response = new CheckTokenResponse('johndoe', 123, 'opfab-client'); - service.checkAuthentication(null).subscribe(next => expect(next).toBeNull()); - const calls = httpMock.match(req => req.url === `${environment.urls.auth}/check_token`); - expect(calls.length).toEqual(0); - - }); - }); - describe('#askTokenFromCode', () => { - beforeEach(() => { - service.assignConfigurationProperties(securityConf); - }); - it('should ask token successfully', () => { - const response = new AuthObject(token, 123, Guid.create(), 'johndoe'); - service.askTokenFromCode('fake-code').subscribe((next: PayloadForSuccessfulAuthentication) => { - expect(next.token).toEqual(token); - expect(next.firstName).toEqual('john'); - expect(next.lastName).toEqual('doe'); - }); - const tokenCalls = httpMock.match(req => req.url === `${environment.urls.auth}/token`); - expect(tokenCalls.length).toEqual(1); - tokenCalls[0].flush(response); - const userCalls = httpMock.match(req => req.url === `${environment.urls.users}/users/rte-operator`); - expect(userCalls.length).toEqual(1); - userCalls[0].flush({firstName: 'john', lastName: 'doe'}); - - }); - - it('should fail if ask token unsuccessfully', () => { - service.askTokenFromCode('fake-code').subscribe((next) => { - fail(`unexpected value:${next}`); - }, (err) => { - expect(err).not.toBeNull(); - }); - const calls = httpMock.match(req => req.url === `${environment.urls.auth}/token`); - expect(calls.length).toEqual(1); - calls[0].error(new ErrorEvent('invalid code')); - - }); - it('should fail if not properly configured', () => { - service.assignConfigurationProperties({...securityConf, oauth2: {}}); - service.askTokenFromCode('fake-code').subscribe((next) => { - fail(`unexpected value:${next}`); - }, (err) => { - expect(err).not.toBeNull(); - }); - const calls = httpMock.match(req => req.url === `${environment.urls.auth}/token`); - expect(calls.length).toEqual(0); - - }); - }); - describe('#askTokenFromPassword', () => { - beforeEach(() => { - service.assignConfigurationProperties(securityConf); - }); - it('should ask token successfully', () => { - const response = new AuthObject(token, 123, Guid.create(), 'johndoe'); - service.askTokenFromPassword('fake-login', 'fake-pwd').subscribe((next: PayloadForSuccessfulAuthentication) => { - expect(next.token).toEqual(token); - expect(next.firstName).toEqual('john'); - expect(next.lastName).toEqual('doe'); - }); - const calls = httpMock.match(req => req.url === `${environment.urls.auth}/token`); - expect(calls.length).toEqual(1); - calls[0].flush(response); - const userCalls = httpMock.match(req => req.url === `${environment.urls.users}/users/rte-operator`); - expect(userCalls.length).toEqual(1); - userCalls[0].flush({firstName: 'john', lastName: 'doe'}); - - }); - - it('should fail if ask token unsuccessfully', () => { - service.askTokenFromPassword('fake-login', 'fake-pwd').subscribe((next) => { - fail(`unexpected value:${next}`); - }, (err) => { - expect(err).not.toBeNull(); - }); - const calls = httpMock.match(req => req.url === `${environment.urls.auth}/token`); - expect(calls.length).toEqual(1); - calls[0].error(new ErrorEvent('invalid code')); - - }); - it('should fail if not properly configured', () => { - service.assignConfigurationProperties({...securityConf, oauth2: {}}); - service.askTokenFromPassword('fake-login', 'fake-pwd').subscribe((next) => { - fail(`unexpected value:${next}`); - }, (err) => { - expect(err).not.toBeNull(); - }); - const calls = httpMock.match(req => req.url === `${environment.urls.auth}/token`); - expect(calls.length).toEqual(0); - - }); - }); - describe('convert', () => { - beforeEach(() => { - service.assignConfigurationProperties(securityConf); - }); - it('convert using expiration payload', () => { - const authObj = new AuthObject(token, 123, Guid.create(), 'johndoe'); - const conversion = service.convert(authObj); - expect(conversion.identifier).toEqual('rte-operator'); - expect(conversion.token).toEqual(token); - expect(conversion.clientId).toEqual(authObj.clientId); - expect(conversion.expirationDate.valueOf() / 100).toBeCloseTo((Date.now() + 123000) / 100, 1); - }); - it('convert using expiration payload', () => { - const authObj = new AuthObject(token, null, Guid.create(), 'johndoe'); - const conversion = service.convert(authObj); - expect(conversion.identifier).toEqual('rte-operator'); - expect(conversion.token).toEqual(token); - expect(conversion.clientId).toEqual(authObj.clientId); - expect(conversion.expirationDate.valueOf()).toEqual(1558015086); - }); - }); - -}); - -describe('isInTheFuture', () => { - - it('should find 0 UTC time to be wrong', () => { - expect(isInTheFuture(0)).not.toEqual(true); - }); - - it('should find current time to be wrong', () => { - expect(isInTheFuture(Date.now())).not.toEqual(true); - }); - - it('should find tomorrow time to be true', () => { - const tomorrow = new Date(); - tomorrow.setDate(tomorrow.getDate() + 1); - expect(isInTheFuture(tomorrow.getTime())).toEqual(true); - }); - -}); - -describe('AuthenticationService', () => { - const httpClientMock = createSpyObj('HttpClient', ['request']); - const guidServiceMock = createSpyObj('GuidService', ['getCurrentGuid', 'getCurrentGuidString']); - const store = createSpyObj('Store', ['select', 'dispatch']); - store.select.and.returnValue({ - subscribe: (func: () => {}) => { - } - }); - const oAuthServiceMock = createSpyObj('OAuthService', ['configure' - , 'restartSessionChecksIfStillLoggedIn' - , 'setupAutomaticSilentRefresh' - , 'loadDiscoveryDocumentAndTryLogin' - , 'setStorage' - , 'fetchTokenUsingPasswordFlowAndLoadUserProfile' - , 'loadUserProfile' - , 'fetchTokenUsingPasswordFlow' - , 'refreshToken' - , 'silentRefresh' - , 'initImplicitFlowInPopup' - , 'initImplicitFlowInternal' - , 'initImplicitFlow' - , 'resetImplicitFlow' - , 'tryLogin' - , 'tryLoginCodeFlow' - , 'tryLoginImplicitFlow' - , 'processIdToken' - , 'getIdentityClaims' - , 'getGrantedScopes' - , 'getGrantedScopes' - , 'getAccessToken' - , 'getRefreshToken' - , 'getAccessTokenExpiration' - ]); - const service = new AuthenticationService(httpClientMock, guidServiceMock, store, oAuthServiceMock); - describe('dispatchAppStateActionFromOAuth2Events', () => { - it('should dispatch an `ImplicitlyAuthenticated` on `token_received` events', - () => { - const tokenReceivedEvent = new OAuthSuccessEvent('token_received'); - service.dispatchAppStateActionFromOAuth2Events(tokenReceivedEvent); - expect(store.dispatch).toHaveBeenCalledWith(any(ImplicitlyAuthenticated)); - }); - it('should dispatch an `UnableToRefreshOrGetToken` on `token_error` events', - () => { - const tokenReceivedEvent = new OAuthSuccessEvent('token_error'); - service.dispatchAppStateActionFromOAuth2Events(tokenReceivedEvent); - expect(store.dispatch).toHaveBeenCalledWith(any(UnableToRefreshOrGetToken)); - }); - it('should dispatch an `UnableToRefreshOrGetToken` on `token_refresh_error` events', - () => { - const tokenReceivedEvent = new OAuthSuccessEvent('token_refresh_error'); - service.dispatchAppStateActionFromOAuth2Events(tokenReceivedEvent); - expect(store.dispatch).toHaveBeenCalledWith(any(UnableToRefreshOrGetToken)); - }); - it('should dispatch an `UnAuthenticationFromImplicitFlow` on `logout` events', - () => { - const tokenReceivedEvent = new OAuthSuccessEvent('logout'); - service.dispatchAppStateActionFromOAuth2Events(tokenReceivedEvent); - expect(store.dispatch).toHaveBeenCalledWith(any(UnAuthenticationFromImplicitFlow)); - }); - }); - describe('instantiateAuthModeHandler', () => { - it('should instantiate a `ImplicitFlowConfiguration` and the `Implicit Configuration` ' + - 'when mode is `implicit`', () => { - const spy = spyOn(service, 'instantiateImplicitFlowConfiguration').and.callThrough(); - const result = service.instantiateAuthModeHandler('implicit'); - expect(result).toEqual(new ImplicitAuthenticationHandler(service, store, sessionStorage)); - expect(spy).toHaveBeenCalled(); - }); - it('should return a `PasswordOrCodeFlowAuthenticationHandler` ' + - 'when mode is different from `implicit`', () => { - function stringDifferentFromImplicit() { - const mode = getRandomAlphanumericValue(4, 12); - return (mode === 'implicit') ? stringDifferentFromImplicit() : mode; - } - - const result = service.instantiateAuthModeHandler(stringDifferentFromImplicit()); - expect(result).toEqual(new PasswordOrCodeAuthenticationHandler(service, store)); - }); - }); - -}); - -describe('AuthenticationModeHandler', () => { - const store = createSpyObj('Store', ['dispatch', 'pipe']); - store.pipe.and.returnValue({ - subscribe: (func: (t: boolean) => {}) => { - func(true); - } - }); - - const authServiceSpy = createSpyObj('AuthenticationService', - ['initializeAuthentication', - 'linkAuthenticationStatus', - 'getAuthenticationMode', - 'initAndLoadAuth', - 'moveToCodeFlowLoginPage' - ]); - let test: boolean; - const linker = (isAuthenticated: boolean) => test = isAuthenticated; - - beforeEach(() => { - store.pipe.calls.reset(); - }); - describe('implemented as PasswordOrCodeAuthenticationHandler', () => { - const underTest = new PasswordOrCodeAuthenticationHandler(authServiceSpy, store); - beforeEach(() => { - store.dispatch.calls.reset(); - }); - describe('initializeAuthentication', () => { - it(`should dispatch an InitAuthStatus Action when 'window.location.href' contains 'code=' as query parameter`, () => { - underTest.initializeAuthentication('http://abcdef.com?code=authentication'); - expect(store.dispatch).toHaveBeenCalled(); - }); - it(`should dispatch any action when 'window.location.href' contains no queryParameters - of type 'code=' `, () => { - underTest.initializeAuthentication('http://abcdefgh.com'); - expect(store.dispatch).not.toHaveBeenCalled(); - }); - }); - describe('linkAuthenticationStatus', () => { - - it(`should subscribe linker to expirationTime changes when called`, () => { - underTest.linkAuthenticationStatus(linker); - expect(store.pipe).toHaveBeenCalled(); - }); - }); - describe('move', () => { - it('should use moveToCodeFlowLoginPage', () => { - underTest.move(); - expect(authServiceSpy.moveToCodeFlowLoginPage).toHaveBeenCalled(); - }); - }); - }); - describe('implemented as ImplicitAuthentication', () => { - const storage = createSpyObj('sessionStorage', ['getItem']); - const underTest = new ImplicitAuthenticationHandler(authServiceSpy, store, storage); - beforeEach(() => { - authServiceSpy.initAndLoadAuth.calls.reset(); - }); - describe('initializedAuthentication', () => { - it('should call getAuthenticationMode and initAndLoadAuth methods from `authenticationService`' + - ' when AuthenticationMode is set to `IMPLICIT', - () => { - authServiceSpy.getAuthenticationMode.and.returnValue('IMPLICIT'); - underTest.initializeAuthentication(getRandomAlphanumericValue(1, 12)); - expect(authServiceSpy.getAuthenticationMode).toHaveBeenCalled(); - expect(authServiceSpy.initAndLoadAuth).toHaveBeenCalled(); - - }); - it('should do nothing when `getAuthenticationMode` of `AuthenticationService` return something ' + - ' else than `IMPLICIT`', () => { - authServiceSpy.getAuthenticationMode.and.returnValue(getRandomAlphanumericValue(9, 15)); - underTest.initializeAuthentication(getRandomAlphanumericValue(5, 12)); - expect(authServiceSpy.getAuthenticationMode).toHaveBeenCalled(); - expect(authServiceSpy.initAndLoadAuth).not.toHaveBeenCalled(); - - }); - - }); - describe('linkAuthenticationStatus', () => { - it('should call subscribe linker on the slice of selectIsImplicitlyAuthenticated', () => { - underTest.linkAuthenticationStatus(linker); - expect(store.pipe).toHaveBeenCalled(); - }); - }); - describe('extractToken', () => { - it('should ask for `access_token` item from `sessionStorage`', () => { - underTest.extractToken(); - expect(storage.getItem).toHaveBeenCalled(); - }); - }); - }); -}); diff --git a/ui/main/src/app/services/authentication/authentication.service.ts b/ui/main/src/app/services/authentication/authentication.service.ts index eb801db4bc..a816bc20eb 100644 --- a/ui/main/src/app/services/authentication/authentication.service.ts +++ b/ui/main/src/app/services/authentication/authentication.service.ts @@ -12,11 +12,12 @@ import {Observable, of, throwError} from 'rxjs'; import {catchError, filter, map, switchMap, tap} from 'rxjs/operators'; import {Guid} from 'guid-typescript'; import { - ImplicitlyAuthenticated, + CheckAuthenticationStatus, InitAuthStatus, PayloadForSuccessfulAuthentication, UnableToRefreshOrGetToken, - UnAuthenticationFromImplicitFlow + UnAuthenticationFromImplicitFlow, + AcceptLogIn } from '@ofActions/authentication.actions'; import {environment} from '@env/environment'; import {GuidService} from '@ofServices/guid.service'; @@ -28,7 +29,8 @@ import * as _ from 'lodash'; import {User} from '@ofModel/user.model'; import {EventType as OAuthType, JwksValidationHandler, OAuthEvent, OAuthService} from 'angular-oauth2-oidc'; import {implicitAuthenticationConfigFallback} from '@ofServices/authentication/auth-implicit-flow.config'; -import {selectExpirationTime, selectIsImplicitlyAuthenticated} from '@ofSelectors/authentication.selectors'; +import {redirectToCurrentLocation} from "../../app-routing.module"; +import {Router} from '@angular/router'; export enum LocalStorageAuthContent { token = 'token', @@ -38,6 +40,7 @@ export enum LocalStorageAuthContent { } export const ONE_SECOND = 1000; +export const MILLIS_TO_WAIT_BETWEEN_TOKEN_EXPIRATION_DATE_CONTROLS= 5000; @Injectable() export class AuthenticationService { @@ -68,6 +71,7 @@ export class AuthenticationService { , private guidService: GuidService , private store: Store , private oauthService: OAuthService + , private router: Router ) { store.select(buildConfigSelector('security')) .subscribe(oauth2Conf => { @@ -77,17 +81,22 @@ export class AuthenticationService { } - regularCheckTokenValidity() { - if (this.verifyExpirationDate()) { - setTimeout(() => { - this.regularCheckTokenValidity(); - }, 5000); - } else {// Will send Logout if token is expired - this.store.dispatch(new UnableToRefreshOrGetToken()); - } + /** + * extract Oauth 2.0 configuration from settings and store it in the service + * @param oauth2Conf - settings return by the back-end config service + */ + assignConfigurationProperties(oauth2Conf) { + this.clientId = _.get(oauth2Conf, 'oauth2.client-id', null); + this.delegateUrl = _.get(oauth2Conf, 'oauth2.flow.delagate-url', null); + this.loginClaim = _.get(oauth2Conf, 'jwt.login-claim', 'sub'); + this.givenNameClaim = _.get(oauth2Conf, 'jwt.given-name-claim', 'given_name'); + this.familyNameClaim = _.get(oauth2Conf, 'jwt.family-name-claim', 'family_name'); + this.expireClaim = _.get(oauth2Conf, 'jwt.expire-claim', 'exp'); + this.mode = _.get(oauth2Conf, 'oauth2.flow.mode', 'PASSWORD'); } + /** * Choose to handle the OAuth 2.0 using implicit workflow * if mode equals to 'implicit' other manage password grand @@ -96,31 +105,41 @@ export class AuthenticationService { */ instantiateAuthModeHandler(mode: string): AuthenticationModeHandler { if (mode.toLowerCase() === 'implicit') { - this.instantiateImplicitFlowConfiguration(); - return new ImplicitAuthenticationHandler(this, this.store, sessionStorage); + this.implicitConf = {...this.implicitConf, issuer: this.delegateUrl, clientId: this.clientId,clearHashAfterLogin: false}; + return new ImplicitAuthenticationHandler(this, this.store, sessionStorage,this.oauthService,this.guidService,this.router,this.implicitConf,); } return new PasswordOrCodeAuthenticationHandler(this, this.store); } + + regularCheckTokenValidity() { + if (this.verifyExpirationDate()) { + setTimeout(() => { + this.regularCheckTokenValidity(); + }, MILLIS_TO_WAIT_BETWEEN_TOKEN_EXPIRATION_DATE_CONTROLS); + } else {// Will send Logout if token is expired + this.store.dispatch(new UnableToRefreshOrGetToken()); + } + + } + /** - * extract Oauth 2.0 configuration from settings and store it in the service - * @param oauth2Conf - settings return by the back-end config service + * @return true if the expiration date stored in the `localestorage` is still running, false otherwise. */ - assignConfigurationProperties(oauth2Conf) { - this.clientId = _.get(oauth2Conf, 'oauth2.client-id', null); - this.delegateUrl = _.get(oauth2Conf, 'oauth2.flow.delagate-url', null); - this.loginClaim = _.get(oauth2Conf, 'jwt.login-claim', 'sub'); - this.givenNameClaim = _.get(oauth2Conf, 'jwt.given-name-claim', 'given_name'); - this.familyNameClaim = _.get(oauth2Conf, 'jwt.family-name-claim', 'family_name'); - this.expireClaim = _.get(oauth2Conf, 'jwt.expire-claim', 'exp'); - this.mode = _.get(oauth2Conf, 'oauth2.flow.mode', 'PASSWORD'); + verifyExpirationDate(): boolean { + // + to convert the stored number as a string back to number + const expirationDate = +localStorage.getItem(LocalStorageAuthContent.expirationDate); + const isNotANumber = isNaN(expirationDate); + const stillValid = (expirationDate> Date.now()); + return !isNotANumber && stillValid; } - // TODO push this part into ImplicitAuthenticationHandler class - instantiateImplicitFlowConfiguration(): void { - this.implicitConf = {...this.implicitConf, issuer: this.delegateUrl, clientId: this.clientId}; + /** + * @return true if the expiration date stored in the `localstorage` is over, false otherwise + */ + isExpirationDateOver(): boolean { + return !this.verifyExpirationDate(); } - /** * Call the web service which checks the authentication token. A valid token gives back the authentication information * and an invalid one an message. @@ -131,7 +150,6 @@ export class AuthenticationService { */ checkAuthentication(token: string): Observable { if (!!token) { - // const postData = new FormData(); const postData = new URLSearchParams(); postData.append('token', token); const headers = new HttpHeaders({'Content-type': 'application/x-www-form-urlencoded; charset=utf-8'}); @@ -222,27 +240,9 @@ export class AuthenticationService { * extract the jwt authentication token from the localstorage */ public extractToken(): string { - const currentAuthModeHandler = this.authModeHandler; - return currentAuthModeHandler.extractToken(); - } - - /** - * @return true if the expiration date stored in the `localestorage` is still running, false otherwise. - */ - verifyExpirationDate(): boolean { - // + to convert the stored number as a string back to number - const expirationDate = +localStorage.getItem(LocalStorageAuthContent.expirationDate); - const isNotANumber = isNaN(expirationDate); - const stillValid = isInTheFuture(expirationDate); - return !isNotANumber && stillValid; + return this.authModeHandler.extractToken(); } - /** - * @return true if the expiration date stored in the `localstorage` is over, false otherwise - */ - isExpirationDateOver(): boolean { - return !this.verifyExpirationDate(); - } /** * clear the `localstorage` from all its content. @@ -326,35 +326,6 @@ export class AuthenticationService { } } - public async moveToImplicitFlowLoginPage() { - this.oauthService.configure(this.implicitConf); - await this.oauthService.loadDiscoveryDocument(); - sessionStorage.setItem('flow', 'implicit'); - this.oauthService.initImplicitFlow(); - } - - public async initAndLoadAuth() { - this.oauthService.configure(this.implicitConf); - this.oauthService.setupAutomaticSilentRefresh(); - this.oauthService.tokenValidationHandler = new JwksValidationHandler(); - await this.oauthService.loadDiscoveryDocument() - .then(() => { - this.oauthService.tryLogin(); - } - ).then(() => { - if (this.oauthService.hasValidAccessToken()) { - return Promise.resolve(); - } - }); - - this.oauthService.events - .pipe(filter(e => e.type === 'session_terminated')) - .subscribe(() => { - console.log('Your session has been terminated!'); - }); - this.askForOauthAccessToken(); - this.dispatchAuthActionFromOAuthEvents(); - } computeRedirectUri() { const uriBase = location.origin; @@ -370,49 +341,6 @@ export class AuthenticationService { } } - public dispatchAuthActionFromOAuthEvents() { - this.oauthService.events.subscribe(e => { - this.dispatchAppStateActionFromOAuth2Events(e); - }); - } - - public askForOauthAccessToken() { - return this.oauthService.getAccessToken(); - - } - - public providePayloadForSuccessfulAuthenticationFromImplicitFlow(): PayloadForSuccessfulAuthentication { - const identityClaims = this.oauthService.getIdentityClaims(); - const identifier = identityClaims['sub']; - const clientId = this.guidService.getCurrentGuid(); - const token = this.askForOauthAccessToken(); - const expirationDate = new Date(this.oauthService.getAccessTokenExpiration()); - return new PayloadForSuccessfulAuthentication(identifier, clientId, token, expirationDate); - } - - dispatchAppStateActionFromOAuth2Events(event: OAuthEvent): void { - const eventType: OAuthType = event.type; - console.log(new Date(), 'Authentication event', event); - switch (eventType) { - case ('token_received'): { - this.store.dispatch(new ImplicitlyAuthenticated()); - break; - } - // We can have a token_error or token_refresh_error when it is not possible to refresh token - // This case arise for example when using a SSO and the session is not valid anymore (session timeout) - case ('token_error'): - case('token_refresh_error'): - this.store.dispatch(new UnableToRefreshOrGetToken()); - break; - case('logout'): { - this.store.dispatch(new UnAuthenticationFromImplicitFlow()); - break; - } - default: { - // console.log('no action to dispatch for:', eventType); - } - } - } public getAuthenticationMode(): string { return this.mode; @@ -422,13 +350,6 @@ export class AuthenticationService { this.authModeHandler.initializeAuthentication(window.location.href); } - public linkAuthenticationStatus(linker: (isAuthenticated: boolean) => void): void { - this.authModeHandler.linkAuthenticationStatus(linker); - } - - public move(): void { - this.authModeHandler.move(); - } public isAuthModeCodeOrImplicitFlow(): boolean { const mode = this.getAuthenticationMode(); @@ -465,27 +386,14 @@ export class CheckTokenResponse { } } -/** - * helper @method to confirm or not if the number corresponding to a given time is in the futur regarding the present - * moment. - * @param time - a number corresponding to an UTC time to test - * @return true if time is in the future regarding the present moment, false otherwise - */ -export function isInTheFuture(time: number): boolean { - return time > Date.now(); -} - /** * interface to handle the mode of authentication */ export interface AuthenticationModeHandler { initializeAuthentication(currentHrefLocation: string): void; - linkAuthenticationStatus(linker: (isAuthenticated: boolean) => void): void; - extractToken(): string; - move(): void; } /** @@ -501,25 +409,19 @@ export class PasswordOrCodeAuthenticationHandler implements AuthenticationModeHa initializeAuthentication(currentLocationHref: string) { const searchCodeString = 'code='; - const foundIndex = currentLocationHref.indexOf(searchCodeString); + const foundIndex = currentLocationHref.indexOf(searchCodeString); if (foundIndex !== -1) { this.store.dispatch( new InitAuthStatus({code: currentLocationHref.substring(foundIndex + searchCodeString.length)})); } - } - - linkAuthenticationStatus(linker: (isAuthenticated: boolean) => void): void { - this.store.pipe(select(selectExpirationTime), map(isInTheFuture)) - .subscribe(linker); + this.store.dispatch( + new CheckAuthenticationStatus()); } public extractToken(): string { return localStorage.getItem(LocalStorageAuthContent.token); } - move(): void { - this.authenticationService.moveToCodeFlowLoginPage(); - } } /** @@ -529,24 +431,75 @@ export class PasswordOrCodeAuthenticationHandler implements AuthenticationModeHa export class ImplicitAuthenticationHandler implements AuthenticationModeHandler { constructor(private authenticationService: AuthenticationService , private store: Store - , private storage: Storage) { + , private storage: Storage + , private oauthService: OAuthService + , private guidService: GuidService + , private router :Router + , private implicitConf) { } initializeAuthentication(currentLocationHref: string) { - if (this.authenticationService.getAuthenticationMode().toLowerCase() === 'implicit') { - this.authenticationService.initAndLoadAuth(); - } + this.initFlow(); } - linkAuthenticationStatus(linker: (isAuthenticated: boolean) => void): void { - this.store.pipe(select(selectIsImplicitlyAuthenticated)).subscribe(linker); + + public async initFlow() { + this.oauthService.configure(this.implicitConf); + this.oauthService.setupAutomaticSilentRefresh(); + this.oauthService.tokenValidationHandler = new JwksValidationHandler(); + await this.oauthService.loadDiscoveryDocument() + .then(() => { + this.tryToLogin(); + } + ); + this.oauthService.events.subscribe(e => { + this.dispatchAppStateActionFromOAuth2Events(e); + }); + } + + public async tryToLogin() { + await this.oauthService.tryLogin() + .then(() => { + if (this.oauthService.hasValidAccessToken()) { + this.store.dispatch(new AcceptLogIn(this.providePayloadForSuccessfulAuthentication())); + redirectToCurrentLocation(this.router); + } + else { + sessionStorage.setItem('flow', 'implicit'); + this.oauthService.initImplicitFlow(); + } + + }); } + + public providePayloadForSuccessfulAuthentication(): PayloadForSuccessfulAuthentication { + const identityClaims = this.oauthService.getIdentityClaims(); + const identifier = identityClaims['sub']; + const clientId = this.guidService.getCurrentGuid(); + const token = this.oauthService.getAccessToken(); + const expirationDate = new Date(this.oauthService.getAccessTokenExpiration()); + return new PayloadForSuccessfulAuthentication(identifier, clientId, token, expirationDate); + } + + + dispatchAppStateActionFromOAuth2Events(event: OAuthEvent): void { + const eventType: OAuthType = event.type; + switch (eventType) { + // We can have a token_error or token_refresh_error when it is not possible to refresh token + // This case arise for example when using a SSO and the session is not valid anymore (session timeout) + case ('token_error'): + case('token_refresh_error'): + this.store.dispatch(new UnableToRefreshOrGetToken()); + break; + case('logout'): { + this.store.dispatch(new UnAuthenticationFromImplicitFlow()); + break; + } + } + } public extractToken(): string { return this.storage.getItem('access_token'); } - move() { - this.authenticationService.moveToImplicitFlowLoginPage(); - } } diff --git a/ui/main/src/app/services/time.service.spec.ts b/ui/main/src/app/services/time.service.spec.ts index f922c65020..329e2bf31a 100644 --- a/ui/main/src/app/services/time.service.spec.ts +++ b/ui/main/src/app/services/time.service.spec.ts @@ -15,7 +15,6 @@ import {RouterTestingModule} from '@angular/router/testing'; import {StoreModule} from '@ngrx/store'; import {appReducer} from '@ofStore/index'; import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing'; -import {AuthenticationImportHelperForSpecs} from '@ofServices/authentication/authentication.service.spec'; describe('TimeService', () => { @@ -27,7 +26,7 @@ describe('TimeService', () => { }); beforeEach(() => { TestBed.configureTestingModule({ - providers: [TimeService, AuthenticationImportHelperForSpecs], + providers: [TimeService], imports:[ HttpClientTestingModule, RouterTestingModule, StoreModule.forRoot(appReducer) diff --git a/ui/main/src/app/store/effects/authentication.effects.spec.ts b/ui/main/src/app/store/effects/authentication.effects.spec.ts index accc0eea5b..862a970d79 100644 --- a/ui/main/src/app/store/effects/authentication.effects.spec.ts +++ b/ui/main/src/app/store/effects/authentication.effects.spec.ts @@ -32,7 +32,6 @@ import {Store} from '@ngrx/store'; import {AppState} from '@ofStore/index'; import {Router} from '@angular/router'; import {hot} from 'jasmine-marbles'; -import {LoadConfigSuccess} from '@ofActions/config.actions'; import * as moment from 'moment'; import {Message} from '@ofModel/message.model'; import {CardService} from '@ofServices/card.service'; @@ -40,8 +39,7 @@ import {EmptyLightCards} from '@ofActions/light-card.actions'; import {ClearCard} from '@ofActions/card.actions'; import SpyObj = jasmine.SpyObj; import createSpyObj = jasmine.createSpyObj; -import {TranslateLoader, TranslateModule, TranslateService} from "@ngx-translate/core"; -import { transformAll } from '@angular/compiler/src/render3/r3_ast'; +import { TranslateModule, TranslateService} from "@ngx-translate/core"; describe('AuthenticationEffects', () => { let actions$: Observable; @@ -99,18 +97,6 @@ describe('AuthenticationEffects', () => { expect(effects).toBeTruthy(); }); - it('returns CheckAuthenticationStatus on LoadConfigSuccess', () => { - const localActions$ = new Actions(hot('-a--', - {a: new LoadConfigSuccess({config: {security: {oauth2: {flow: {mode: 'CODE'}}}}})})); - effects = new AuthenticationEffects(mockStore, localActions$, null, null, null,translate); - expect(effects).toBeTruthy(); - effects.checkAuthenticationWhenReady - .subscribe((action: AuthenticationActions) => { - expect(action.type).toEqual(AuthenticationActionTypes.CheckAuthenticationStatus) - }); - - }); - describe('TryToLogIn', () => { it('should success if JWT is generated from backend', () => { const localAction$ = new Actions(hot('-a--', {a: new TryToLogIn({username: 'johndoe', password: 'pwd'})})); @@ -163,16 +149,7 @@ describe('AuthenticationEffects', () => { describe('CheckAuthentication', () => { it('should success if has valid token', () => { - /*export class PayloadForSuccessfulAuthentication { - constructor(public identifier: string, - public clientId: Guid, - public token: string, - public expirationDate: Date, - public firstName?: string, - public lastName?: string) { - } -}*/ - const localAction$ = new Actions(hot('-a--', {a: new CheckAuthenticationStatus()})); + const localAction$ = new Actions(hot('-a--', {a: new CheckAuthenticationStatus()})); setStorageWithUserData(moment().add(1, 'days').valueOf()); authenticationService.checkAuthentication.and.returnValue(of( new CheckTokenResponse('johndoe', 123, Guid.create().toString()) @@ -200,30 +177,6 @@ describe('AuthenticationEffects', () => { }); }); - it('should fail if has no valid token and no code', () => { - const localAction$ = new Actions(hot('-a--', {a: new CheckAuthenticationStatus()})); - authenticationService.checkAuthentication.and.returnValue(throwError('no valid token')); - mockStore.select.and.returnValue(of(null)); - effects = new AuthenticationEffects(mockStore, localAction$, authenticationService, null, router,translate); - expect(effects).toBeTruthy(); - effects.CheckAuthentication.subscribe((action: AuthenticationActions) => { - expect(action.type).toEqual(AuthenticationActionTypes.RejectLogIn); - expect(authenticationService.clearAuthenticationInformation).toHaveBeenCalled(); - }); - }); - it('should success if has no valid token and a valid code', () => { - const localAction$ = new Actions(hot('-a--', {a: new CheckAuthenticationStatus()})); - authenticationService.checkAuthentication.and.returnValue(throwError('no valid token')); - authenticationService.askTokenFromCode.and.returnValue(of( - new PayloadForSuccessfulAuthentication('johndoe', Guid.create(), 'fake-token', moment().add(1, 'days').toDate()) - )); - mockStore.select.and.returnValue(of('code')); - effects = new AuthenticationEffects(mockStore, localAction$, authenticationService, null, router,translate); - expect(effects).toBeTruthy(); - effects.CheckAuthentication.subscribe((action: AuthenticationActions) => { - expect(action.type).toEqual(AuthenticationActionTypes.AcceptLogIn); - }); - }); it('should fail if has no valid token and an invalid code', () => { const localAction$ = new Actions(hot('-a--', {a: new CheckAuthenticationStatus()})); authenticationService.checkAuthentication.and.returnValue(throwError('no valid token')); @@ -238,21 +191,6 @@ describe('AuthenticationEffects', () => { }); - // it('should send accept loginAction when handling successful login attempt', () => { - // const mockCheckTokenResponse = {sub:"",exp:0,clientId:""} as CheckTokenResponse; - // const mockIdInfo = new PayloadForSuccessfulAuthentication("",Guid.create(),"",new Date()); - // authenticationService.extractIdentificationInformation.and.callFake(() => mockIdInfo); - // const expectedAction = new AcceptLogIn(mockIdInfo); - // expect(effects.handleLogInAttempt(mockCheckTokenResponse)).toEqual(expectedAction); - // expect(authenticationService.extractIdentificationInformation).toHaveBeenCalled(); - // - // }) - - // it('should clear local storage of auth information when handling a failing login attempt', () => { - // expect(effects.handleLogInAttempt(null)).toEqual(new RejectLogIn( { message: 'invalid token'})); - // expect(authenticationService.clearAuthenticationInformation).toHaveBeenCalled(); - // }) -// TODO it('should clear local storage of auth information when sending RejectLogIn Action', () => { const errorMsg = new Message('test'); expect(effects.handleRejectedLogin(errorMsg)).toEqual(new RejectLogIn({error: errorMsg})); diff --git a/ui/main/src/app/store/effects/authentication.effects.ts b/ui/main/src/app/store/effects/authentication.effects.ts index 4b9fcbfcf2..d71ae33373 100644 --- a/ui/main/src/app/store/effects/authentication.effects.ts +++ b/ui/main/src/app/store/effects/authentication.effects.ts @@ -18,9 +18,11 @@ import { AuthenticationActionTypes, CheckAuthenticationStatus, CheckImplicitFlowAuthenticationStatus, + InitAuthStatus, RejectLogIn, TryToLogIn, - TryToLogOut + TryToLogOut, + UnAuthenticationFromImplicitFlow } from '@ofActions/authentication.actions'; import {AuthenticationService} from '@ofServices/authentication/authentication.service'; import {catchError, flatMap, map, switchMap, tap, withLatestFrom} from 'rxjs/operators'; @@ -36,7 +38,6 @@ import {EmptyLightCards} from '@ofActions/light-card.actions'; import {ClearCard} from '@ofActions/card.actions'; import { buildConfigSelector } from '@ofStore/selectors/config.selectors'; import {redirectToCurrentLocation} from "../../app-routing.module"; -import { combineLatest } from 'rxjs'; import {TranslateService} from "@ngx-translate/core"; /** @@ -63,23 +64,6 @@ export class AuthenticationEffects { private translate: TranslateService) { } - /** - * Triggers Authentication Check when the application is ready - */ - @Effect() - checkAuthenticationWhenReady: Observable = - this.actions$ - .pipe( - ofType(ConfigActionTypes.LoadConfigSuccess), - map((loadConfigSuccess: LoadConfigSuccess) => { - const flowMode = loadConfigSuccess.payload.config.security.oauth2.flow.mode; - if (flowMode && flowMode === 'IMPLICIT') { - return new CheckImplicitFlowAuthenticationStatus(); - } - return new CheckAuthenticationStatus(); - }) - ); - /** * This {Observable} of {AuthenticationActions} listen {AuthenticationActionTypes.TryToLogIn} type and uses * the login payload to get an authentication token from the authentication service if the authentication is @@ -199,8 +183,9 @@ export class AuthenticationEffects { }), withLatestFrom(this.store.select(selectCode)), switchMap(([payload, code]) => { - // no token stored or token invalid - if (!payload) { + // no token stored or token invalid + if (!payload) { + if (this.authService.isAuthModeCodeOrImplicitFlow()) { if (!!code) { return this.authService.askTokenFromCode(code).pipe( tap(() => { @@ -211,30 +196,32 @@ export class AuthenticationEffects { return new AcceptLogIn(authenticationInfo) }), catchError(errorResponse => { - return this.handleErrorOnTokenGeneration(errorResponse, 'code'); - } + return this.handleErrorOnTokenGeneration(errorResponse, 'code'); + } )); } + this.authService.moveToCodeFlowLoginPage(); return of(this.handleRejectedLogin(new Message('The stored token is invalid', MessageLevel.ERROR, new I18n('login.error.token.invalid')))); - } else { - if (!this.authService.isExpirationDateOver()) { - const authInfo = this.authService.extractIdentificationInformation(); - this.authService.regularCheckTokenValidity(); - return this.authService.loadUserData(authInfo) - .pipe( - map(auth => { - redirectToCurrentLocation(this.router); - return new AcceptLogIn(auth); - }) - ); - } - return of(this.handleRejectedLogin(new Message('The stored token has expired', - MessageLevel.ERROR, - new I18n('login.error.token.expiration')))); } + } else { + if (!this.authService.isExpirationDateOver()) { + const authInfo = this.authService.extractIdentificationInformation(); + this.authService.regularCheckTokenValidity(); + return this.authService.loadUserData(authInfo) + .pipe( + map(auth => { + redirectToCurrentLocation(this.router); + return new AcceptLogIn(auth); + }) + ); + } + return of(this.handleRejectedLogin(new Message('The stored token has expired', + MessageLevel.ERROR, + new I18n('login.error.token.expiration')))); } + } ), catchError(err => { console.error(err); @@ -248,23 +235,6 @@ export class AuthenticationEffects { ); - @Effect() - CheckImplicitFlowAuthentication: Observable = - this.actions$ - .pipe(ofType(AuthenticationActionTypes.CheckImplicitFlowAuthenticationStatus), - flatMap(() => from(this.authService.initAndLoadAuth()).pipe( - map ( response => { - return response ; - }), - catchError( error => { - return of(error); - }) - )), - // due to implicit flow mode an explicit rerouting to `/feed` is needed once authenticated - tap ( () => redirectToCurrentLocation(this.router)), - map(() => { - return new AcceptLogIn(this.authService.providePayloadForSuccessfulAuthenticationFromImplicitFlow()); - })); @Effect() UnableToRefreshToken: Observable = this.actions$.pipe( diff --git a/ui/main/src/app/store/effects/feed-filters.effects.spec.ts b/ui/main/src/app/store/effects/feed-filters.effects.spec.ts index 18ef0bf08f..17223fe204 100644 --- a/ui/main/src/app/store/effects/feed-filters.effects.spec.ts +++ b/ui/main/src/app/store/effects/feed-filters.effects.spec.ts @@ -9,7 +9,6 @@ import {Actions} from '@ngrx/effects'; import {hot} from 'jasmine-marbles'; import {FeedFiltersEffects} from "@ofEffects/feed-filters.effects"; -import {Filter} from "@ofModel/feed-filter.model"; import {ApplyFilter} from "@ofActions/feed.actions"; import {LoadSettingsSuccess} from "@ofActions/settings.actions"; import {of} from "rxjs"; @@ -17,16 +16,13 @@ import {FilterService, FilterType} from "@ofServices/filter.service"; import {async, TestBed} from "@angular/core/testing"; import {Store} from "@ngrx/store"; import {AppState} from "@ofStore/index"; -import {emptyAppState4Test} from "@tests/helpers"; import createSpyObj = jasmine.createSpyObj; import SpyObj = jasmine.SpyObj; describe('FeedFilterEffects', () => { let effects: FeedFiltersEffects; - let localMockFeedFilterService:SpyObj; let mockStore:SpyObj>; - let emptyAppState: AppState = emptyAppState4Test; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -38,7 +34,6 @@ describe('FeedFilterEffects', () => { })); beforeEach(() => { - localMockFeedFilterService = TestBed.get(FilterService); mockStore = TestBed.get(Store); }); @@ -49,7 +44,7 @@ describe('FeedFilterEffects', () => { const localExpected = hot('', {}); mockStore.select.and.returnValue(of(null)); - effects = new FeedFiltersEffects(mockStore, localActions$, localMockFeedFilterService); + effects = new FeedFiltersEffects(mockStore, localActions$); expect(effects).toBeTruthy(); expect(effects.initTagFilterOnLoadedSettings).toBeObservable(localExpected); @@ -60,7 +55,7 @@ describe('FeedFilterEffects', () => { const localExpected = hot('', {c:new ApplyFilter({name:FilterType.TAG_FILTER,active:true,status:{tags:['test1']}})}); mockStore.select.and.returnValue(of(null)); - effects = new FeedFiltersEffects(mockStore, localActions$, localMockFeedFilterService); + effects = new FeedFiltersEffects(mockStore, localActions$); expect(effects).toBeTruthy(); expect(effects.initTagFilterOnLoadedSettings).toBeObservable(localExpected); @@ -71,7 +66,7 @@ describe('FeedFilterEffects', () => { const localExpected = hot('', {c:new ApplyFilter({name:FilterType.TAG_FILTER,active:true,status:{tags:['test1']}})}); mockStore.select.and.returnValue(of(null)); - effects = new FeedFiltersEffects(mockStore, localActions$, localMockFeedFilterService); + effects = new FeedFiltersEffects(mockStore, localActions$); expect(effects).toBeTruthy(); expect(effects.initTagFilterOnLoadedSettings).toBeObservable(localExpected); @@ -82,7 +77,7 @@ describe('FeedFilterEffects', () => { const localExpected = hot('-c', {c:new ApplyFilter({name:FilterType.TAG_FILTER,active:true,status:{tags:['test1']}})}); mockStore.select.and.returnValue(of(null)); - effects = new FeedFiltersEffects(mockStore, localActions$, localMockFeedFilterService); + effects = new FeedFiltersEffects(mockStore, localActions$); expect(effects).toBeTruthy(); expect(effects.initTagFilterOnLoadedSettings).toBeObservable(localExpected); @@ -93,7 +88,7 @@ describe('FeedFilterEffects', () => { const localExpected = hot('-c', {c:new ApplyFilter({name:FilterType.TAG_FILTER,active:true,status:{tags:['test1']}})}); mockStore.select.and.returnValue(of(['test2'])); - effects = new FeedFiltersEffects(mockStore, localActions$, localMockFeedFilterService); + effects = new FeedFiltersEffects(mockStore, localActions$); expect(effects).toBeTruthy(); expect(effects.initTagFilterOnLoadedSettings).toBeObservable(localExpected); @@ -104,7 +99,7 @@ describe('FeedFilterEffects', () => { const localExpected = hot('-c', {c:new ApplyFilter({name:FilterType.TAG_FILTER,active:true,status:{tags:['test2']}})}); mockStore.select.and.returnValue(of(['test2'])); - effects = new FeedFiltersEffects(mockStore, localActions$, localMockFeedFilterService); + effects = new FeedFiltersEffects(mockStore, localActions$); expect(effects).toBeTruthy(); expect(effects.initTagFilterOnLoadedSettings).toBeObservable(localExpected); @@ -115,7 +110,7 @@ describe('FeedFilterEffects', () => { const localExpected = hot('-c', {c:new ApplyFilter({name:FilterType.TAG_FILTER,active:true,status:{tags:['test2']}})}); mockStore.select.and.returnValue(of(['test2'])); - effects = new FeedFiltersEffects(mockStore, localActions$, localMockFeedFilterService); + effects = new FeedFiltersEffects(mockStore, localActions$); expect(effects).toBeTruthy(); expect(effects.initTagFilterOnLoadedSettings).toBeObservable(localExpected); diff --git a/ui/main/src/app/store/effects/feed-filters.effects.ts b/ui/main/src/app/store/effects/feed-filters.effects.ts index 5a8848d5c8..89275da571 100644 --- a/ui/main/src/app/store/effects/feed-filters.effects.ts +++ b/ui/main/src/app/store/effects/feed-filters.effects.ts @@ -12,7 +12,7 @@ import {Observable} from 'rxjs'; import {filter, map, withLatestFrom} from 'rxjs/operators'; import {Action, Store} from "@ngrx/store"; import {AppState} from "@ofStore/index"; -import {FilterService, FilterType} from "@ofServices/filter.service"; +import {FilterType} from "@ofServices/filter.service"; import {ApplyFilter} from "@ofActions/feed.actions"; import {LoadSettingsSuccess, SettingsActionTypes} from "@ofActions/settings.actions"; import {buildConfigSelector} from "@ofSelectors/config.selectors"; @@ -23,18 +23,16 @@ export class FeedFiltersEffects { /* istanbul ignore next */ constructor(private store: Store, - private actions$: Actions, - private service: FilterService) { + private actions$: Actions) { } @Effect() initTagFilterOnLoadedSettings: Observable = this.actions$ .pipe( - // tap(v=>console.log("initTagFilterOnLoadedSettings: action start", v)), + ofType(SettingsActionTypes.LoadSettingsSuccess), withLatestFrom(this.store.select(buildConfigSelector('settings.defaultTags'))), - // tap(v=>console.log("initTagFilterOnLoadedSettings: latest config", v)), map(([action,configTags])=>{ if(action.payload.settings.defaultTags && action.payload.settings.defaultTags.length>0) return action.payload.settings.defaultTags; @@ -42,13 +40,10 @@ export class FeedFiltersEffects { return configTags; return null; }), - // tap(v=>console.log("initTagFilterOnLoadedSettings: mapped tag array", v)), filter(v=>!!v), - // tap(v=>console.log("initTagFilterOnLoadedSettings: filtered empty array", v)), map(v=> { console.log(new Date().toISOString(),"BUG OC-604 feed_filters.effects.ts initTagFilterOnLoadedSettings "); return new ApplyFilter({name:FilterType.TAG_FILTER,active:true,status:{tags:v}}); }) - // tap(v=>console.log("initTagFilterOnLoadedSettings: mapped action", v)) ); } diff --git a/ui/main/src/app/store/effects/user.effects.ts b/ui/main/src/app/store/effects/user.effects.ts index 280d783121..a987630602 100644 --- a/ui/main/src/app/store/effects/user.effects.ts +++ b/ui/main/src/app/store/effects/user.effects.ts @@ -68,7 +68,6 @@ export class UserEffects { ofType(UserActionsTypes.UserApplicationNotRegistered), map((action: UserApplicationNotRegistered) => { const userDataPayload = action.payload.user; - // console.log("transitionCreateUserApplication userPayload : " + userDataPayload); return new CreateUserApplication({user: userDataPayload}); }) ); @@ -86,11 +85,9 @@ export class UserEffects { return this.userService.askCreateUser(user) .pipe( map(currentUser => { - // console.log("ok creation user " + user.login); return new CreateUserApplicationOnSuccess({user: currentUser}); }), catchError((error, caught) => { - // console.log(error, caught, "error on creation user application for the user ") this.authService.clearAuthenticationInformation(); this.store.dispatch(new CreateUserApplicationOnFailure({error: error})); return caught; diff --git a/ui/main/src/tests/helpers.ts b/ui/main/src/tests/helpers.ts index 20ce450ef0..bababcdb26 100644 --- a/ui/main/src/tests/helpers.ts +++ b/ui/main/src/tests/helpers.ts @@ -14,6 +14,9 @@ import {Map as OfMap, Map} from "@ofModel/map"; import {Action, ActionType, Process, State, Third, ThirdMenu, ThirdMenuEntry} from "@ofModel/thirds.model"; import {Page} from '@ofModel/page.model'; import {AppState} from "@ofStore/index"; +import {AuthenticationService} from '@ofServices/authentication/authentication.service'; +import {GuidService} from '@ofServices/guid.service'; +import {OAuthLogger,OAuthService,UrlHelperService}from 'angular-oauth2-oidc'; export const emptyAppState4Test:AppState = { router: null, @@ -28,6 +31,12 @@ export const emptyAppState4Test:AppState = { user:null }; +export const AuthenticationImportHelperForSpecs = [AuthenticationService, + GuidService, + OAuthService, + UrlHelperService, + OAuthLogger]; + export function getOneRandomMenu(): ThirdMenu { let entries: ThirdMenuEntry[]=[];