diff --git a/src/app/core/auth/auth.effects.spec.ts b/src/app/core/auth/auth.effects.spec.ts index 12679fc325..fae5015184 100644 --- a/src/app/core/auth/auth.effects.spec.ts +++ b/src/app/core/auth/auth.effects.spec.ts @@ -1,7 +1,12 @@ -import { cold, hot } from 'jasmine-marbles'; -import { Observable } from 'rxjs/Observable'; import { TestBed } from '@angular/core/testing'; + import { provideMockActions } from '@ngrx/effects/testing'; +import { Store } from '@ngrx/store'; +import { cold, hot } from 'jasmine-marbles'; + +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/of' + import { AuthEffects } from './auth.effects'; import { AuthActionTypes, @@ -20,25 +25,27 @@ import { AuthTokenInfo } from './models/auth-token-info.model'; import { AuthServiceStub } from '../../shared/testing/auth-service-stub'; import { AuthService } from './auth.service'; import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer'; -import { Store } from '@ngrx/store'; + import { EpersonMock } from '../../shared/testing/eperson-mock'; describe('AuthEffects', () => { let authEffects: AuthEffects; let actions: Observable; + + const authServiceStub = new AuthServiceStub(); const store: Store = jasmine.createSpyObj('store', { /* tslint:disable:no-empty */ dispatch: {}, /* tslint:enable:no-empty */ select: Observable.of(true) }); - const token = new AuthTokenInfo('token_test'); + const token = authServiceStub.getToken(); beforeEach(() => { TestBed.configureTestingModule({ providers: [ AuthEffects, - {provide: AuthService, useClass: AuthServiceStub}, + {provide: AuthService, useValue: authServiceStub}, {provide: Store, useValue: store}, provideMockActions(() => actions), // other providers @@ -58,7 +65,8 @@ describe('AuthEffects', () => { } }); - const expected = cold('--b-', {b: new AuthenticationSuccessAction(new AuthTokenInfo('token_test'))}); + + const expected = cold('--b-', {b: new AuthenticationSuccessAction(token)}); expect(authEffects.authenticate$).toBeObservable(expected); }); @@ -87,7 +95,7 @@ describe('AuthEffects', () => { it('should return a AUTHENTICATED action in response to a AUTHENTICATE_SUCCESS action', () => { actions = hot('--a-', {a: {type: AuthActionTypes.AUTHENTICATE_SUCCESS, payload: token}}); - const expected = cold('--b-', {b: new AuthenticatedAction(new AuthTokenInfo('token_test'))}); + const expected = cold('--b-', {b: new AuthenticatedAction(token)}); expect(authEffects.authenticateSuccess$).toBeObservable(expected); }); @@ -125,7 +133,7 @@ describe('AuthEffects', () => { actions = hot('--a-', {a: {type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN}}); - const expected = cold('--b-', {b: new AuthenticatedAction(new AuthTokenInfo('token_test'))}); + const expected = cold('--b-', {b: new AuthenticatedAction(token)}); expect(authEffects.checkToken$).toBeObservable(expected); }); @@ -151,7 +159,7 @@ describe('AuthEffects', () => { actions = hot('--a-', {a: {type: AuthActionTypes.REFRESH_TOKEN}}); - const expected = cold('--b-', {b: new RefreshTokenSuccessAction(new AuthTokenInfo('token_test'))}); + const expected = cold('--b-', {b: new RefreshTokenSuccessAction(token)}); expect(authEffects.refreshToken$).toBeObservable(expected); }); diff --git a/src/app/core/auth/auth.interceptor.spec.ts b/src/app/core/auth/auth.interceptor.spec.ts new file mode 100644 index 0000000000..528c2cfab3 --- /dev/null +++ b/src/app/core/auth/auth.interceptor.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController, } from '@angular/common/http/testing'; +import { HTTP_INTERCEPTORS } from '@angular/common/http'; +import { Router } from '@angular/router'; + +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs/Observable'; + +import { AuthInterceptor } from './auth.interceptor'; +import { AuthService } from './auth.service'; +import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { RestRequestMethod } from '../data/request.models'; +import { RouterStub } from '../../shared/testing/router-stub'; +import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer'; +import { AuthServiceStub } from '../../shared/testing/auth-service-stub'; + +describe(`AuthInterceptor`, () => { + let service: DSpaceRESTv2Service; + let httpMock: HttpTestingController; + + const authServiceStub = new AuthServiceStub(); + const store: Store = jasmine.createSpyObj('store', { + /* tslint:disable:no-empty */ + dispatch: {}, + /* tslint:enable:no-empty */ + select: Observable.of(true) + }); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + DSpaceRESTv2Service, + {provide: AuthService, useValue: authServiceStub}, + {provide: Router, useClass: RouterStub}, + { + provide: HTTP_INTERCEPTORS, + useClass: AuthInterceptor, + multi: true, + }, + {provide: Store, useValue: store}, + ], + }); + + service = TestBed.get(DSpaceRESTv2Service); + httpMock = TestBed.get(HttpTestingController); + }); + + describe('when has a valid token', () => { + + it('should not add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint', () => { + service.request(RestRequestMethod.Post, 'dspace-spring-rest/api/authn/login', 'password=password&user=user').subscribe((response) => { + expect(response).toBeTruthy(); + }); + + const httpRequest = httpMock.expectOne(`dspace-spring-rest/api/authn/login`); + + const token = httpRequest.request.headers.get('authorization'); + expect(token).toBeNull(); + }); + + it('should add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint', () => { + service.request(RestRequestMethod.Post, 'dspace-spring-rest/api/submission/workspaceitems', 'test').subscribe((response) => { + expect(response).toBeTruthy(); + }); + + const httpRequest = httpMock.expectOne(`dspace-spring-rest/api/submission/workspaceitems`); + + expect(httpRequest.request.headers.has('authorization')); + const token = httpRequest.request.headers.get('authorization'); + expect(token).toBe('Bearer token_test'); + }); + + }); + + describe('when has an expired token', () => { + + beforeEach(() => { + authServiceStub.setTokenAsExpired(); + }); + + afterEach(() => { + authServiceStub.setTokenAsNotExpired(); + }); + + it('should redirect to login', () => { + + service.request(RestRequestMethod.Post, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user').subscribe((response) => { + expect(response).toBeTruthy(); + }); + + service.request(RestRequestMethod.Post, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user'); + + httpMock.expectNone('dspace-spring-rest/api/submission/workspaceitems'); + }); + }) + +}); diff --git a/src/app/shared/testing/auth-request-service-stub.ts b/src/app/shared/testing/auth-request-service-stub.ts index b07b7eb406..2c47068af4 100644 --- a/src/app/shared/testing/auth-request-service-stub.ts +++ b/src/app/shared/testing/auth-request-service-stub.ts @@ -4,39 +4,10 @@ import { AuthStatus } from '../../core/auth/models/auth-status.model'; import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model'; import { Eperson } from '../../core/eperson/models/eperson.model'; import { isNotEmpty } from '../empty.util'; +import { EpersonMock } from './eperson-mock'; export class AuthRequestServiceStub { - protected mockUser: Eperson = Object.assign(new Eperson(), { - id: 'test', - uuid: 'test', - name: 'User Test', - handle: 'test', - metadata: [ - { - key: 'eperson.firstname', - value: 'User', - language: null, - authority: null, - confidence: 0 - }, - { - key: 'eperson.lastname', - value: 'Test', - language: null, - authority: null, - confidence: 0 - } - ], - groups: [], - netid: 'test', - lastActive: '', - canLogIn: true, - email: 'test@test.com', - requireCertificate: true, - selfRegistered: false, - self: {}, - type: 'eperson' - }); + protected mockUser: Eperson = EpersonMock; protected mockTokenInfo = new AuthTokenInfo('test_token'); public postToEndpoint(method: string, body: any, options?: HttpOptions): Observable { diff --git a/src/app/shared/testing/auth-service-stub.ts b/src/app/shared/testing/auth-service-stub.ts index 2a8e2f467f..c7d5556910 100644 --- a/src/app/shared/testing/auth-service-stub.ts +++ b/src/app/shared/testing/auth-service-stub.ts @@ -6,12 +6,19 @@ import { Eperson } from '../../core/eperson/models/eperson.model'; export class AuthServiceStub { + token: AuthTokenInfo = new AuthTokenInfo('token_test'); + private _tokenExpired = false; + + constructor() { + this.token.expires = Date.now() + (1000 * 60 * 60); + } + public authenticate(user: string, password: string): Observable { if (user === 'user' && password === 'password') { const authStatus = new AuthStatus(); authStatus.okay = true; authStatus.authenticated = true; - authStatus.token = new AuthTokenInfo('token_test'); + authStatus.token = this.token; authStatus.eperson = [EpersonMock]; return Observable.of(authStatus); } else { @@ -28,16 +35,46 @@ export class AuthServiceStub { } } + public buildAuthHeader(token?: AuthTokenInfo): string { + return `Bearer ${token.accessToken}`; + } + + public getToken(): AuthTokenInfo { + return this.token; + } + public hasValidAuthenticationToken(): Observable { - return Observable.of(new AuthTokenInfo('token_test')); + return Observable.of(this.token); } public logout(): Observable { return Observable.of(true); } + public isTokenExpired(token?: AuthTokenInfo): boolean { + return this._tokenExpired; + } + + /** + * This method is used to ease testing + */ + public setTokenAsExpired() { + this._tokenExpired = true + } + + /** + * This method is used to ease testing + */ + public setTokenAsNotExpired() { + this._tokenExpired = false + } + + public isTokenExpiring(): Observable { + return Observable.of(false); + } + public refreshAuthenticationToken(token: AuthTokenInfo): Observable { - return Observable.of(new AuthTokenInfo('token_test')); + return Observable.of(this.token); } public redirectToPreviousUrl() { @@ -48,6 +85,10 @@ export class AuthServiceStub { return; } + setRedirectUrl(url: string) { + return; + } + public storeToken(token: AuthTokenInfo) { return; }