diff --git a/src/app/core/auth/auth.reducer.spec.ts b/src/app/core/auth/auth.reducer.spec.ts new file mode 100644 index 0000000000..411e1064c6 --- /dev/null +++ b/src/app/core/auth/auth.reducer.spec.ts @@ -0,0 +1,410 @@ +import { authReducer, AuthState } from './auth.reducers'; +import { + AddAuthenticationMessageAction, + AuthenticateAction, + AuthenticatedAction, + AuthenticatedErrorAction, + AuthenticatedSuccessAction, + AuthenticationErrorAction, + AuthenticationSuccessAction, + CheckAuthenticationTokenAction, + CheckAuthenticationTokenErrorAction, + LogOutAction, + LogOutErrorAction, + LogOutSuccessAction, + RedirectWhenAuthenticationIsRequiredAction, RedirectWhenTokenExpiredAction, + RefreshTokenAction, + RefreshTokenErrorAction, + 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', () => { + + let initialState: AuthState; + let state: AuthState; + const mockTokenInfo = new AuthTokenInfo('test_token'); + const mockError = new Error('Test error message'); + + it('should properly set the state, in response to a AUTHENTICATE action', () => { + initialState = { + authenticated: false, + loaded: false, + loading: false, + }; + const action = new AuthenticateAction('user', 'password'); + const newState = authReducer(initialState, action); + state = { + authenticated: false, + loaded: false, + error: undefined, + loading: true, + info: undefined + }; + + expect(newState).toEqual(state); + }); + + it('should properly set the state, in response to a AUTHENTICATE_SUCCESS action', () => { + initialState = { + authenticated: false, + loaded: false, + error: undefined, + loading: true, + info: undefined + }; + const action = new AuthenticationSuccessAction(mockTokenInfo); + const newState = authReducer(initialState, action); + + expect(newState).toEqual(initialState); + }); + + it('should properly set the state, in response to a AUTHENTICATE_ERROR action', () => { + initialState = { + authenticated: false, + loaded: false, + error: undefined, + loading: true, + info: undefined + }; + const action = new AuthenticationErrorAction(mockError); + const newState = authReducer(initialState, action); + state = { + authenticated: false, + loaded: false, + loading: false, + info: undefined, + authToken: undefined, + error: 'Test error message' + }; + + expect(newState).toEqual(state); + }); + + it('should properly set the state, in response to a AUTHENTICATED action', () => { + initialState = { + authenticated: false, + loaded: false, + error: undefined, + loading: true, + info: undefined + }; + const action = new AuthenticatedAction(mockTokenInfo); + const newState = authReducer(initialState, action); + + expect(newState).toEqual(initialState); + }); + + it('should properly set the state, in response to a AUTHENTICATED_SUCCESS action', () => { + initialState = { + authenticated: false, + loaded: false, + error: undefined, + loading: true, + info: undefined + }; + const action = new AuthenticatedSuccessAction(true, mockTokenInfo, EpersonMock); + const newState = authReducer(initialState, action); + state = { + authenticated: true, + authToken: mockTokenInfo, + loaded: true, + error: undefined, + loading: false, + info: undefined, + user: EpersonMock + }; + expect(newState).toEqual(state); + }); + + it('should properly set the state, in response to a AUTHENTICATED_ERROR action', () => { + initialState = { + authenticated: false, + loaded: false, + error: undefined, + loading: true, + info: undefined + }; + const action = new AuthenticatedErrorAction(mockError); + const newState = authReducer(initialState, action); + state = { + authenticated: false, + authToken: undefined, + error: 'Test error message', + loaded: true, + loading: false, + info: undefined + }; + expect(newState).toEqual(state); + }); + + it('should properly set the state, in response to a CHECK_AUTHENTICATION_TOKEN action', () => { + initialState = { + authenticated: false, + loaded: false, + loading: false, + }; + const action = new CheckAuthenticationTokenAction(); + const newState = authReducer(initialState, action); + state = { + authenticated: false, + loaded: false, + loading: true, + }; + expect(newState).toEqual(state); + }); + + it('should properly set the state, in response to a CHECK_AUTHENTICATION_TOKEN_ERROR action', () => { + initialState = { + authenticated: false, + loaded: false, + loading: true, + }; + const action = new CheckAuthenticationTokenErrorAction(); + const newState = authReducer(initialState, action); + state = { + authenticated: false, + loaded: false, + loading: false, + }; + expect(newState).toEqual(state); + }); + + it('should properly set the state, in response to a LOG_OUT action', () => { + initialState = { + authenticated: true, + authToken: mockTokenInfo, + loaded: true, + error: undefined, + loading: false, + info: undefined, + user: EpersonMock + }; + + const action = new LogOutAction(); + const newState = authReducer(initialState, action); + + expect(newState).toEqual(initialState); + }); + + it('should properly set the state, in response to a LOG_OUT_SUCCESS action', () => { + initialState = { + authenticated: true, + authToken: mockTokenInfo, + loaded: true, + error: undefined, + loading: false, + info: undefined, + user: EpersonMock + }; + + const action = new LogOutSuccessAction(); + const newState = authReducer(initialState, action); + state = { + authenticated: false, + authToken: undefined, + error: undefined, + loaded: false, + loading: false, + info: undefined, + refreshing: false, + user: undefined + }; + expect(newState).toEqual(state); + }); + + it('should properly set the state, in response to a LOG_OUT_ERROR action', () => { + initialState = { + authenticated: true, + authToken: mockTokenInfo, + loaded: true, + error: undefined, + loading: false, + info: undefined, + user: EpersonMock + }; + + const action = new LogOutErrorAction(mockError); + const newState = authReducer(initialState, action); + state = { + authenticated: true, + authToken: mockTokenInfo, + loaded: true, + error: 'Test error message', + loading: false, + info: undefined, + user: EpersonMock + }; + expect(newState).toEqual(state); + }); + + it('should properly set the state, in response to a REFRESH_TOKEN action', () => { + initialState = { + authenticated: true, + authToken: mockTokenInfo, + loaded: true, + error: undefined, + loading: false, + info: undefined, + user: EpersonMock + }; + const newTokenInfo = new AuthTokenInfo('Refreshed token'); + const action = new RefreshTokenAction(newTokenInfo); + const newState = authReducer(initialState, action); + state = { + authenticated: true, + authToken: mockTokenInfo, + loaded: true, + error: undefined, + loading: false, + info: undefined, + user: EpersonMock, + refreshing: true + }; + expect(newState).toEqual(state); + }); + + it('should properly set the state, in response to a REFRESH_TOKEN_SUCCESS action', () => { + initialState = { + authenticated: true, + authToken: mockTokenInfo, + loaded: true, + error: undefined, + loading: false, + info: undefined, + user: EpersonMock, + refreshing: true + }; + const newTokenInfo = new AuthTokenInfo('Refreshed token'); + const action = new RefreshTokenSuccessAction(newTokenInfo); + const newState = authReducer(initialState, action); + state = { + authenticated: true, + authToken: newTokenInfo, + loaded: true, + error: undefined, + loading: false, + info: undefined, + user: EpersonMock, + refreshing: false + }; + expect(newState).toEqual(state); + }); + + it('should properly set the state, in response to a REFRESH_TOKEN_ERROR action', () => { + initialState = { + authenticated: true, + authToken: mockTokenInfo, + loaded: true, + error: undefined, + loading: false, + info: undefined, + user: EpersonMock, + refreshing: true + }; + const action = new RefreshTokenErrorAction(); + const newState = authReducer(initialState, action); + state = { + authenticated: false, + authToken: undefined, + error: undefined, + loaded: false, + loading: false, + info: undefined, + refreshing: false, + user: undefined + }; + expect(newState).toEqual(state); + }); + + beforeEach(() => { + initialState = { + authenticated: true, + authToken: mockTokenInfo, + loaded: true, + error: undefined, + loading: false, + info: undefined, + user: EpersonMock + }; + + state = { + authenticated: false, + authToken: undefined, + loaded: false, + loading: false, + error: undefined, + info: 'Message', + user: undefined + }; + }); + + it('should properly set the state, in response to a REDIRECT_AUTHENTICATION_REQUIRED action', () => { + const action = new RedirectWhenAuthenticationIsRequiredAction('Message'); + const newState = authReducer(initialState, action); + expect(newState).toEqual(state); + }); + + it('should properly set the state, in response to a REDIRECT_TOKEN_EXPIRED action', () => { + const action = new RedirectWhenTokenExpiredAction('Message'); + const newState = authReducer(initialState, action); + expect(newState).toEqual(state); + }); + + it('should properly set the state, in response to a ADD_MESSAGE action', () => { + initialState = { + authenticated: false, + loaded: false, + loading: false, + }; + const action = new AddAuthenticationMessageAction('Message'); + const newState = authReducer(initialState, action); + state = { + authenticated: false, + loaded: false, + loading: false, + info: 'Message' + }; + expect(newState).toEqual(state); + }); + + it('should properly set the state, in response to a RESET_MESSAGES action', () => { + initialState = { + authenticated: false, + loaded: false, + loading: false, + error: 'Error', + info: 'Message' + }; + const action = new ResetAuthenticationMessagesAction(); + const newState = authReducer(initialState, action); + state = { + authenticated: false, + loaded: false, + loading: false, + error: undefined, + info: undefined + }; + expect(newState).toEqual(state); + }); + + it('should properly set the state, in response to a SET_REDIRECT_URL action', () => { + initialState = { + authenticated: false, + loaded: false, + loading: false + }; + const action = new SetRedirectUrlAction('redirect.url'); + const newState = authReducer(initialState, action); + state = { + authenticated: false, + loaded: false, + loading: false, + redirectUrl: 'redirect.url' + }; + expect(newState).toEqual(state); + }); +}); diff --git a/src/app/core/auth/auth.reducers.ts b/src/app/core/auth/auth.reducers.ts index 9664773ac5..70efddf11e 100644 --- a/src/app/core/auth/auth.reducers.ts +++ b/src/app/core/auth/auth.reducers.ts @@ -98,7 +98,9 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut loading: false }); + case AuthActionTypes.AUTHENTICATED: case AuthActionTypes.AUTHENTICATE_SUCCESS: + case AuthActionTypes.LOG_OUT: return state; case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN: @@ -114,8 +116,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut case AuthActionTypes.LOG_OUT_ERROR: return Object.assign({}, state, { authenticated: true, - error: (action as LogOutErrorAction).payload.message, - user: undefined + error: (action as LogOutErrorAction).payload.message }); case AuthActionTypes.LOG_OUT_SUCCESS: diff --git a/src/app/core/auth/auth.service.spec.ts b/src/app/core/auth/auth.service.spec.ts new file mode 100644 index 0000000000..79d56c58dd --- /dev/null +++ b/src/app/core/auth/auth.service.spec.ts @@ -0,0 +1,221 @@ +import { async, inject, TestBed } from '@angular/core/testing'; +import { CommonModule } from '@angular/common'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { Store, StoreModule } from '@ngrx/store'; +import { Observable } from 'rxjs/Observable'; +import { REQUEST } from '@nguniversal/express-engine/tokens'; +import 'rxjs/add/observable/of'; + +import { authReducer, AuthState } from './auth.reducers'; +import { NativeWindowRef, NativeWindowService } from '../../shared/services/window.service'; +import { AuthService } from './auth.service'; +import { RouterStub } from '../../shared/testing/router-stub'; +import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; + +import { CookieService } from '../../shared/services/cookie.service'; +import { AuthRequestServiceStub } from '../../shared/testing/auth-request-service-stub'; +import { AuthRequestService } from './auth-request.service'; +import { AuthStatus } from './models/auth-status.model'; +import { AuthTokenInfo } from './models/auth-token-info.model'; +import { Eperson } from '../eperson/models/eperson.model'; +import { EpersonMock } from '../../shared/testing/eperson-mock'; +import { AppState } from '../../app.reducer'; +import { ClientCookieService } from '../../shared/services/client-cookie.service'; + +describe('AuthService test', () => { + + const mockStore: Store = jasmine.createSpyObj('store', { + dispatch: {}, + select: Observable.of(true) + }); + let authService: AuthService; + const authRequest = new AuthRequestServiceStub(); + const window = new NativeWindowRef(); + const routerStub = new RouterStub(); + const routeStub = new ActivatedRouteStub(); + let storage: CookieService; + const token: AuthTokenInfo = new AuthTokenInfo('test_token'); + token.expires = Date.now() + (1000 * 60 * 60); + let authenticatedState = { + authenticated: true, + loaded: true, + loading: false, + authToken: token, + user: EpersonMock + }; + + describe('', () => { + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + CommonModule, + StoreModule.forRoot({authReducer}), + ], + declarations: [], + providers: [ + {provide: AuthRequestService, useValue: authRequest}, + {provide: NativeWindowService, useValue: window}, + {provide: REQUEST, useValue: {}}, + {provide: Router, useValue: routerStub}, + {provide: ActivatedRoute, useValue: routeStub}, + {provide: Store, useValue: mockStore}, + CookieService, + AuthService + ], + }); + authService = TestBed.get(AuthService); + }); + + it('should return the authentication status object when user credentials are correct', () => { + authService.authenticate('user', 'password').subscribe((status: AuthStatus) => { + expect(status).toBeDefined(); + }); + }); + + it('should throw an error when user credentials are wrong', () => { + expect(authService.authenticate.bind(null, 'user', 'passwordwrong')).toThrow(); + }); + + it('should return the authenticated user object when user token is valid', () => { + authService.authenticatedUser(new AuthTokenInfo('test_token')).subscribe((user: Eperson) => { + expect(user).toBeDefined(); + }); + }); + + it('should throw an error when user credentials when user token is not valid', () => { + expect(authService.authenticatedUser.bind(null, new AuthTokenInfo('test_token_expired'))).toThrow(); + }); + + it('should return a valid refreshed token', () => { + authService.refreshAuthenticationToken(new AuthTokenInfo('test_token')).subscribe((tokenState: AuthTokenInfo) => { + expect(tokenState).toBeDefined(); + }); + }); + + it('should throw an error when is not possible to refresh token', () => { + expect(authService.refreshAuthenticationToken.bind(null, new AuthTokenInfo('test_token_expired'))).toThrow(); + }); + + it('should return true when logout succeeded', () => { + authService.logout().subscribe((status: boolean) => { + expect(status).toBe(true); + }); + }); + + it('should throw an error when logout is not succeeded', () => { + expect(authService.logout.bind(null)).toThrow(); + }); + + }); + + describe('', () => { + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + StoreModule.forRoot({authReducer}) + ], + providers: [ + {provide: AuthRequestService, useValue: authRequest}, + {provide: REQUEST, useValue: {}}, + {provide: Router, useValue: routerStub}, + CookieService + ] + }).compileComponents(); + })); + + beforeEach(inject([CookieService, AuthRequestService, Store, Router], (cookieService: CookieService, authReqService: AuthRequestService, store: Store, router: Router) => { + store + .subscribe((state) => { + (state as any).core = Object.create({}); + (state as any).core.auth = authenticatedState; + }); + authService = new AuthService({}, window, authReqService, router, cookieService, store); + })); + + it('should return true when user is logged in', () => { + authService.isAuthenticated().subscribe((status: boolean) => { + expect(status).toBe(true); + }); + }); + + it('should return token object when it is valid', () => { + authService.hasValidAuthenticationToken().subscribe((tokenState: AuthTokenInfo) => { + expect(tokenState).toBe(token); + }); + }); + + it('should return a token object', () => { + const result = authService.getToken(); + expect(result).toBe(token); + }); + + it('should return false when token is not expired', () => { + const result = authService.isTokenExpired(); + expect(result).toBe(false); + }); + + }); + + describe('', () => { + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + StoreModule.forRoot({authReducer}) + ], + providers: [ + {provide: AuthRequestService, useValue: authRequest}, + {provide: REQUEST, useValue: {}}, + {provide: Router, useValue: routerStub}, + ClientCookieService, + CookieService + ] + }).compileComponents(); + })); + + beforeEach(inject([ClientCookieService, AuthRequestService, Store, Router], (cookieService: ClientCookieService, authReqService: AuthRequestService, store: Store, router: Router) => { + const expiredToken: AuthTokenInfo = new AuthTokenInfo('test_token'); + expiredToken.expires = Date.now() - (1000 * 60 * 60); + authenticatedState = { + authenticated: true, + loaded: true, + loading: false, + authToken: expiredToken, + user: EpersonMock + }; + store + .subscribe((state) => { + (state as any).core = Object.create({}); + (state as any).core.auth = authenticatedState; + }); + authService = new AuthService({}, window, authReqService, router, cookieService, store); + storage = (authService as any).storage; + spyOn(storage, 'get'); + spyOn(storage, 'remove'); + spyOn(storage, 'set'); + })); + + it('should throw false when token is not valid', () => { + expect(authService.hasValidAuthenticationToken.bind(null)).toThrow(); + }); + + it('should return true when token is expired', () => { + const result = authService.isTokenExpired(); + expect(result).toBe(true); + }); + + it('should save token into storage', () => { + authService.storeToken(token); + expect(storage.set).toHaveBeenCalled(); + }); + + it('should remove token from storage', () => { + authService.removeToken(); + expect(storage.remove).toHaveBeenCalled(); + }); + + }); +}); diff --git a/src/app/shared/testing/auth-request-service-stub.ts b/src/app/shared/testing/auth-request-service-stub.ts new file mode 100644 index 0000000000..b07b7eb406 --- /dev/null +++ b/src/app/shared/testing/auth-request-service-stub.ts @@ -0,0 +1,98 @@ +import { Observable } from 'rxjs/Observable'; +import { HttpOptions } from '../../core/dspace-rest-v2/dspace-rest-v2.service'; +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'; + +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 mockTokenInfo = new AuthTokenInfo('test_token'); + + public postToEndpoint(method: string, body: any, options?: HttpOptions): Observable { + const authStatusStub: AuthStatus = new AuthStatus(); + if (isNotEmpty(body)) { + const parsedBody = this.parseQueryString(body); + authStatusStub.okay = true; + if (parsedBody.user === 'user' && parsedBody.password === 'password') { + authStatusStub.authenticated = true; + authStatusStub.token = this.mockTokenInfo; + } else { + authStatusStub.authenticated = false; + } + } else { + const token = (options.headers as any).lazyUpdate[1].value; + if (this.validateToken(token)) { + authStatusStub.authenticated = true; + authStatusStub.token = this.mockTokenInfo; + authStatusStub.eperson = [this.mockUser]; + } else { + authStatusStub.authenticated = false; + } + } + return Observable.of(authStatusStub); + } + + public getRequest(method: string, options?: HttpOptions): Observable { + const authStatusStub: AuthStatus = new AuthStatus(); + switch (method) { + case 'logout': + authStatusStub.authenticated = false; + break; + case 'status': + const token = (options.headers as any).lazyUpdate[1].value; + if (this.validateToken(token)) { + authStatusStub.authenticated = true; + authStatusStub.token = this.mockTokenInfo; + authStatusStub.eperson = [this.mockUser]; + } else { + authStatusStub.authenticated = false; + } + break; + } + return Observable.of(authStatusStub); + } + + private validateToken(token): boolean { + return (token === 'Bearer test_token'); + } + private parseQueryString(query): any { + const obj = Object.create({}); + const vars = query.split('&'); + for (const param of vars) { + const pair = param.split('='); + obj[pair[0]] = pair[1] + } + return obj; + } +} diff --git a/src/app/shared/testing/eperson-mock.ts b/src/app/shared/testing/eperson-mock.ts new file mode 100644 index 0000000000..9cf938fcf2 --- /dev/null +++ b/src/app/shared/testing/eperson-mock.ts @@ -0,0 +1,34 @@ +import { Eperson } from '../../core/eperson/models/eperson.model'; + +export const EpersonMock: Eperson = Object.assign(new Eperson(),{ + handle: null, + groups: [], + netid: 'test@test.com', + lastActive: '2018-05-14T12:25:42.411+0000', + canLogIn: true, + email: 'test@test.com', + requireCertificate: false, + selfRegistered: false, + self: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/epersons/testid', + id: 'testid', + uuid: 'testid', + type: 'eperson', + name: 'User Test', + metadata: [ + { + key: 'eperson.firstname', + language: null, + value: 'User' + }, + { + key: 'eperson.lastname', + language: null, + value: 'Test' + }, + { + key: 'eperson.language', + language: null, + value: 'en' + } + ] +});