From 27a36362cfb8abd1de23f90ba42fd8eef1004839 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 15 May 2018 16:54:27 +0200 Subject: [PATCH] Added tests --- .../auth-response-parsing.service.spec.ts | 117 +++++++ src/app/core/auth/auth.reducer.spec.ts | 9 +- .../auth-nav-menu.component.html | 4 +- .../auth-nav-menu.component.spec.ts | 297 ++++++++++++++++++ .../shared/log-in/log-in.component.spec.ts | 69 ++-- .../shared/log-out/log-out.component.spec.ts | 100 ++++-- src/app/shared/testing/auth-service-stub.ts | 6 + 7 files changed, 547 insertions(+), 55 deletions(-) create mode 100644 src/app/core/auth/auth-response-parsing.service.spec.ts create mode 100644 src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts create mode 100644 src/app/shared/testing/auth-service-stub.ts diff --git a/src/app/core/auth/auth-response-parsing.service.spec.ts b/src/app/core/auth/auth-response-parsing.service.spec.ts new file mode 100644 index 0000000000..f7d899a9bc --- /dev/null +++ b/src/app/core/auth/auth-response-parsing.service.spec.ts @@ -0,0 +1,117 @@ +import { AuthStatusResponse } from '../cache/response-cache.models'; + +import { ObjectCacheService } from '../cache/object-cache.service'; +import { GlobalConfig } from '../../../config/global-config.interface'; + +import { Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; +import { AuthStatus } from './models/auth-status.model'; +import { AuthResponseParsingService } from './auth-response-parsing.service'; +import { AuthGetRequest, AuthPostRequest } from '../data/request.models'; + +describe('ConfigResponseParsingService', () => { + let service: AuthResponseParsingService; + + const EnvConfig = {} as GlobalConfig; + const store = {} as Store; + const objectCacheService = new ObjectCacheService(store); + + beforeEach(() => { + service = new AuthResponseParsingService(EnvConfig, objectCacheService); + }); + + describe('parse', () => { + const validRequest = new AuthPostRequest( + '69f375b5-19f4-4453-8c7a-7dc5c55aafbb', + 'https://rest.api/dspace-spring-rest/api/authn/login', + 'password=test&user=myself@testshib.org'); + + const validRequest2 = new AuthGetRequest( + '69f375b5-19f4-4453-8c7a-7dc5c55aafbb', + 'https://rest.api/dspace-spring-rest/api/authn/status'); + + const validResponse = { + payload: { + authenticated: true, + id: null, + okay: true, + token: { + accessToken: 'eyJhbGciOiJIUzI1NiJ9.eyJlaWQiOiI0ZGM3MGFiNS1jZDczLTQ5MmYtYjAwNy0zMTc5ZDJkOTI5NmIiLCJzZyI6W10sImV4cCI6MTUyNjMxODMyMn0.ASmvcbJFBfzhN7D5ncloWnaVZr5dLtgTuOgHaCKiimc', + expires: 1526318322000 + }, + } as AuthStatus, + statusCode: '200' + }; + + const validResponse1 = { + payload: {}, + statusCode: '404' + }; + + const validResponse2 = { + payload: { + authenticated: true, + id: null, + okay: true, + type: 'status', + _embedded: { + eperson: { + canLogIn: true, + email: 'myself@testshib.org', + groups: [], + handle: null, + id: '4dc70ab5-cd73-492f-b007-3179d2d9296b', + lastActive: '2018-05-14T17:03:31.277+0000', + metadata: [ + { + key: 'eperson.firstname', + language: null, + value: 'User' + }, + { + key: 'eperson.lastname', + language: null, + value: 'Test' + }, + { + key: 'eperson.language', + language: null, + value: 'en' + } + ], + name: 'User Test', + netid: 'myself@testshib.org', + requireCertificate: false, + selfRegistered: false, + type: 'eperson', + uuid: '4dc70ab5-cd73-492f-b007-3179d2d9296b', + _links: { + self: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b' + } + } + }, + _links: { + eperson: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b', + self: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/authn/status' + } + }, + statusCode: '200' + }; + + it('should return a AuthStatusResponse if data contains a valid AuthStatus object as payload', () => { + const response = service.parse(validRequest, validResponse); + expect(response.constructor).toBe(AuthStatusResponse); + }); + + it('should return a AuthStatusResponse if data contains a valid endpoint response', () => { + const response = service.parse(validRequest2, validResponse2); + expect(response.constructor).toBe(AuthStatusResponse); + }); + + it('should return a AuthStatusResponse if data contains an empty 404 endpoint response', () => { + const response = service.parse(validRequest, validResponse1); + expect(response.constructor).toBe(AuthStatusResponse); + }); + + }); +}); diff --git a/src/app/core/auth/auth.reducer.spec.ts b/src/app/core/auth/auth.reducer.spec.ts index 411e1064c6..d64aa824e5 100644 --- a/src/app/core/auth/auth.reducer.spec.ts +++ b/src/app/core/auth/auth.reducer.spec.ts @@ -12,14 +12,15 @@ import { LogOutAction, LogOutErrorAction, LogOutSuccessAction, - RedirectWhenAuthenticationIsRequiredAction, RedirectWhenTokenExpiredAction, + RedirectWhenAuthenticationIsRequiredAction, + RedirectWhenTokenExpiredAction, RefreshTokenAction, RefreshTokenErrorAction, - RefreshTokenSuccessAction, ResetAuthenticationMessagesAction, SetRedirectUrlAction + RefreshTokenSuccessAction, + ResetAuthenticationMessagesAction, + SetRedirectUrlAction } from './auth.actions'; import { AuthTokenInfo } from './models/auth-token-info.model'; -import { Eperson } from '../eperson/models/eperson.model'; -import { Group } from '../eperson/models/group.model'; import { EpersonMock } from '../../shared/testing/eperson-mock'; describe('authReducer', () => { diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.html b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html index e67e49a2f0..7203dc07ab 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.html +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html @@ -8,7 +8,7 @@ diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts new file mode 100644 index 0000000000..fb63389206 --- /dev/null +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts @@ -0,0 +1,297 @@ +import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; +import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; + +import { By } from '@angular/platform-browser'; +import { Store, StoreModule } from '@ngrx/store'; + +import { authReducer, AuthState } from '../../core/auth/auth.reducers'; +import { EpersonMock } from '../testing/eperson-mock'; +import { TranslateModule } from '@ngx-translate/core'; +import { AppState } from '../../app.reducer'; +import { AuthNavMenuComponent } from './auth-nav-menu.component'; +import { HostWindowServiceStub } from '../testing/host-window-service-stub'; +import { HostWindowService } from '../host-window.service'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model'; + +describe('AuthNavMenuComponent', () => { + + let component: AuthNavMenuComponent; + let deNavMenu: DebugElement; + let deNavMenuItem: DebugElement; + let fixture: ComponentFixture; + + const notAuthState: AuthState = { + authenticated: false, + loaded: false, + loading: false + }; + const authState: AuthState = { + authenticated: true, + loaded: true, + loading: false, + authToken: new AuthTokenInfo('test_token'), + user: EpersonMock + }; + let routerState = { + url: '/home' + }; + + describe('when is a not mobile view', () => { + beforeEach(async(() => { + const window = new HostWindowServiceStub(800); + + // refine the test module by declaring the test component + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + StoreModule.forRoot(authReducer), + TranslateModule.forRoot() + ], + declarations: [ + AuthNavMenuComponent + ], + providers: [ + {provide: HostWindowService, useValue: window}, + ], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ] + }) + .compileComponents(); + + })); + + describe('when route is /login and user is not authenticated', () => { + routerState = { + url: '/login' + }; + beforeEach(inject([Store], (store: Store) => { + store + .subscribe((state) => { + (state as any).router = Object.create({}); + (state as any).router.state = routerState; + (state as any).core = Object.create({}); + (state as any).core.auth = notAuthState; + }); + + // create component and test fixture + fixture = TestBed.createComponent(AuthNavMenuComponent); + + // get test component from the fixture + component = fixture.componentInstance; + + fixture.detectChanges(); + + const navMenuSelector = '.navbar-nav'; + deNavMenu = fixture.debugElement.query(By.css(navMenuSelector)); + + const navMenuItemSelector = 'li'; + deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector)); + })); + + it('should not render', () => { + expect(component).toBeTruthy(); + expect(deNavMenu.nativeElement).toBeDefined(); + expect(deNavMenuItem).toBeNull(); + }); + + }); + + describe('when route is /logout and user is authenticated', () => { + routerState = { + url: '/logout' + }; + beforeEach(inject([Store], (store: Store) => { + store + .subscribe((state) => { + (state as any).router = Object.create({}); + (state as any).router.state = routerState; + (state as any).core = Object.create({}); + (state as any).core.auth = authState; + }); + + // create component and test fixture + fixture = TestBed.createComponent(AuthNavMenuComponent); + + // get test component from the fixture + component = fixture.componentInstance; + + fixture.detectChanges(); + + const navMenuSelector = '.navbar-nav'; + deNavMenu = fixture.debugElement.query(By.css(navMenuSelector)); + + const navMenuItemSelector = 'li'; + deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector)); + })); + + it('should not render', () => { + expect(component).toBeTruthy(); + expect(deNavMenu.nativeElement).toBeDefined(); + expect(deNavMenuItem).toBeNull(); + }); + + }); + + describe('when route is not /login neither /logout', () => { + describe('when user is not authenticated', () => { + + beforeEach(inject([Store], (store: Store) => { + routerState = { + url: '/home' + }; + store + .subscribe((state) => { + (state as any).router = Object.create({}); + (state as any).router.state = routerState; + (state as any).core = Object.create({}); + (state as any).core.auth = notAuthState; + }); + + // create component and test fixture + fixture = TestBed.createComponent(AuthNavMenuComponent); + + // get test component from the fixture + component = fixture.componentInstance; + + fixture.detectChanges(); + + const navMenuSelector = '.navbar-nav'; + deNavMenu = fixture.debugElement.query(By.css(navMenuSelector)); + + const navMenuItemSelector = 'li'; + deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector)); + })); + + it('should render login dropdown menu', () => { + const loginDropdownMenu = deNavMenuItem.query(By.css('div[id=loginDropdownMenu]')); + expect(loginDropdownMenu.nativeElement).toBeDefined(); + }); + }); + + describe('when user is authenticated', () => { + beforeEach(inject([Store], (store: Store) => { + routerState = { + url: '/home' + }; + store + .subscribe((state) => { + (state as any).router = Object.create({}); + (state as any).router.state = routerState; + (state as any).core = Object.create({}); + (state as any).core.auth = authState; + }); + + // create component and test fixture + fixture = TestBed.createComponent(AuthNavMenuComponent); + + // get test component from the fixture + component = fixture.componentInstance; + + fixture.detectChanges(); + + const navMenuSelector = '.navbar-nav'; + deNavMenu = fixture.debugElement.query(By.css(navMenuSelector)); + + const navMenuItemSelector = 'li'; + deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector)); + })); + + it('should render logout dropdown menu', () => { + const logoutDropdownMenu = deNavMenuItem.query(By.css('div[id=logoutDropdownMenu]')); + expect(logoutDropdownMenu.nativeElement).toBeDefined(); + }); + }) + }) + }); + + describe('when is a mobile view', () => { + beforeEach(async(() => { + const window = new HostWindowServiceStub(300); + + // refine the test module by declaring the test component + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + StoreModule.forRoot(authReducer), + TranslateModule.forRoot() + ], + declarations: [ + AuthNavMenuComponent + ], + providers: [ + {provide: HostWindowService, useValue: window}, + ], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ] + }) + .compileComponents(); + + })); + + describe('when user is not authenticated', () => { + + beforeEach(inject([Store], (store: Store) => { + store + .subscribe((state) => { + (state as any).router = Object.create({}); + (state as any).router.state = routerState; + (state as any).core = Object.create({}); + (state as any).core.auth = notAuthState; + }); + + // create component and test fixture + fixture = TestBed.createComponent(AuthNavMenuComponent); + + // get test component from the fixture + component = fixture.componentInstance; + + fixture.detectChanges(); + + const navMenuSelector = '.navbar-nav'; + deNavMenu = fixture.debugElement.query(By.css(navMenuSelector)); + + const navMenuItemSelector = 'li'; + deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector)); + })); + + it('should render login link', () => { + const loginDropdownMenu = deNavMenuItem.query(By.css('a[id=loginLink]')); + expect(loginDropdownMenu.nativeElement).toBeDefined(); + }); + }); + + describe('when user is authenticated', () => { + beforeEach(inject([Store], (store: Store) => { + store + .subscribe((state) => { + (state as any).router = Object.create({}); + (state as any).router.state = routerState; + (state as any).core = Object.create({}); + (state as any).core.auth = authState; + }); + + // create component and test fixture + fixture = TestBed.createComponent(AuthNavMenuComponent); + + // get test component from the fixture + component = fixture.componentInstance; + + fixture.detectChanges(); + + const navMenuSelector = '.navbar-nav'; + deNavMenu = fixture.debugElement.query(By.css(navMenuSelector)); + + const navMenuItemSelector = 'li'; + deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector)); + })); + + it('should render logout link', inject([Store], (store: Store) => { + const logoutDropdownMenu = deNavMenuItem.query(By.css('a[id=logoutLink]')); + expect(logoutDropdownMenu.nativeElement).toBeDefined(); + })); + }) + }) +}); diff --git a/src/app/shared/log-in/log-in.component.spec.ts b/src/app/shared/log-in/log-in.component.spec.ts index c73a8e481d..eab1d65ad0 100644 --- a/src/app/shared/log-in/log-in.component.spec.ts +++ b/src/app/shared/log-in/log-in.component.spec.ts @@ -1,33 +1,34 @@ -/* tslint:disable:no-unused-variable */ -/*import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; -import { ComponentFixture, TestBed, async } from '@angular/core/testing'; -import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { MaterialModule } from '@angular/material'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; +import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; + import { By } from '@angular/platform-browser'; import { Store, StoreModule } from '@ngrx/store'; -import { go } from '@ngrx/router-store'; -// reducers -import { reducer } from '../../app.reducers'; - -// models -import { User } from '../../core/models/user'; - -// services -import { MOCK_USER } from '../../core/services/user.service'; - -// this component to test import { LogInComponent } from './log-in.component'; +import { authReducer } from '../../core/auth/auth.reducers'; +import { EpersonMock } from '../testing/eperson-mock'; +import { Eperson } from '../../core/eperson/models/eperson.model'; +import { TranslateModule } from '@ngx-translate/core'; +import { AuthService } from '../../core/auth/auth.service'; +import { AuthServiceStub } from '../testing/auth-service-stub'; +import { AppState } from '../../app.reducer'; describe('LogInComponent', () => { let component: LogInComponent; let fixture: ComponentFixture; let page: Page; - let user: User = new User(); + let user: Eperson; + + const authState = { + authenticated: false, + loaded: false, + loading: false, + }; beforeEach(() => { - user = MOCK_USER; + user = EpersonMock; }); beforeEach(async(() => { @@ -35,27 +36,37 @@ describe('LogInComponent', () => { TestBed.configureTestingModule({ imports: [ FormsModule, - MaterialModule, ReactiveFormsModule, - StoreModule.provideStore(reducer) + StoreModule.forRoot(authReducer), + TranslateModule.forRoot() ], declarations: [ LogInComponent ], + providers: [ + {provide: AuthService, useClass: AuthServiceStub} + ], schemas: [ CUSTOM_ELEMENTS_SCHEMA ] }) - .compileComponents(); + .compileComponents(); + + })); + + beforeEach(inject([Store], (store: Store) => { + store + .subscribe((state) => { + (state as any).core = Object.create({}); + (state as any).core.auth = authState; + }); // create component and test fixture fixture = TestBed.createComponent(LogInComponent); // get test component from the fixture component = fixture.componentInstance; - })); - beforeEach(() => { // create page page = new Page(component, fixture); @@ -63,7 +74,8 @@ describe('LogInComponent', () => { fixture.whenStable().then(() => { page.addPageElements(); }); - }); + + })); it('should create a FormGroup comprised of FormControls', () => { fixture.detectChanges(); @@ -74,8 +86,8 @@ describe('LogInComponent', () => { fixture.detectChanges(); // set FormControl values - component.form.controls['email'].setValue(user.email); - component.form.controls['password'].setValue(user.password); + component.form.controls.email.setValue('user'); + component.form.controls.password.setValue('password'); // submit form component.submit(); @@ -90,7 +102,6 @@ describe('LogInComponent', () => { * * @class Page */ -/* class Page { public emailInput: HTMLInputElement; @@ -103,16 +114,14 @@ class Page { const store = injector.get(Store); // add spies - this.navigateSpy = spyOn(store, 'dispatch'); + this.navigateSpy = spyOn(store, 'dispatch'); } public addPageElements() { const emailInputSelector = 'input[formcontrolname=\'email\']'; - // console.log(this.fixture.debugElement.query(By.css(emailInputSelector))); this.emailInput = this.fixture.debugElement.query(By.css(emailInputSelector)).nativeElement; const passwordInputSelector = 'input[formcontrolname=\'password\']'; this.passwordInput = this.fixture.debugElement.query(By.css(passwordInputSelector)).nativeElement; } } -*/ diff --git a/src/app/shared/log-out/log-out.component.spec.ts b/src/app/shared/log-out/log-out.component.spec.ts index cb83b96892..7e07e1b7cc 100644 --- a/src/app/shared/log-out/log-out.component.spec.ts +++ b/src/app/shared/log-out/log-out.component.spec.ts @@ -1,46 +1,108 @@ -/* tslint:disable:no-unused-variable */ -/*import { DebugElement, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { Router } from '@angular/router'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -// import ngrx import { Store, StoreModule } from '@ngrx/store'; -// reducers -import { reducer } from '../../app.reducers'; +import { authReducer } from '../../core/auth/auth.reducers'; +import { EpersonMock } from '../testing/eperson-mock'; +import { Eperson } from '../../core/eperson/models/eperson.model'; +import { TranslateModule } from '@ngx-translate/core'; +import { Router } from '@angular/router'; +import { AppState } from '../../app.reducer'; +import { LogOutComponent } from './log-out.component'; +import { RouterStub } from '../testing/router-stub'; -// test this component -import { SignOutComponent } from './log-out.component'; +describe('LogOutComponent', () => { -describe('Component: Signout', () => { - let component: SignOutComponent; - let fixture: ComponentFixture; + let component: LogOutComponent; + let fixture: ComponentFixture; + let page: Page; + let user: Eperson; + + const authState = { + authenticated: false, + loaded: false, + loading: false, + }; + const routerStub = new RouterStub(); + + beforeEach(() => { + user = EpersonMock; + }); beforeEach(async(() => { // refine the test module by declaring the test component TestBed.configureTestingModule({ imports: [ - StoreModule.provideStore(reducer) + FormsModule, + ReactiveFormsModule, + StoreModule.forRoot(authReducer), + TranslateModule.forRoot() ], declarations: [ - SignOutComponent + LogOutComponent + ], + providers: [ + {provide: Router, useValue: routerStub}, ], schemas: [ CUSTOM_ELEMENTS_SCHEMA ] }) - .compileComponents(); + .compileComponents(); + + })); + + beforeEach(inject([Store], (store: Store) => { + store + .subscribe((state) => { + (state as any).core = Object.create({}); + (state as any).core.auth = authState; + }); // create component and test fixture - fixture = TestBed.createComponent(SignOutComponent); + fixture = TestBed.createComponent(LogOutComponent); // get test component from the fixture component = fixture.componentInstance; + + // create page + page = new Page(component, fixture); + })); it('should create an instance', () => { - expect(component).toBeTruthy(); + expect(component).toBeTruthy(); + }); + + it('should log out', () => { + fixture.detectChanges(); + + // submit form + component.logOut(); + + // verify Store.dispatch() is invoked + expect(page.navigateSpy.calls.any()).toBe(true, 'Store.dispatch not invoked'); }); }); -*/ + +/** + * I represent the DOM elements and attach spies. + * + * @class Page + */ +class Page { + + public navigateSpy: jasmine.Spy; + + constructor(private component: LogOutComponent, private fixture: ComponentFixture) { + // use injector to get services + const injector = fixture.debugElement.injector; + const store = injector.get(Store); + + // add spies + this.navigateSpy = spyOn(store, 'dispatch'); + } + +} diff --git a/src/app/shared/testing/auth-service-stub.ts b/src/app/shared/testing/auth-service-stub.ts new file mode 100644 index 0000000000..5c5f2c67dd --- /dev/null +++ b/src/app/shared/testing/auth-service-stub.ts @@ -0,0 +1,6 @@ +export class AuthServiceStub { + + public redirectToPreviousUrl() { + return; + } +}