diff --git a/src/app/core/auth/auth.effects.spec.ts b/src/app/core/auth/auth.effects.spec.ts new file mode 100644 index 0000000000..12679fc325 --- /dev/null +++ b/src/app/core/auth/auth.effects.spec.ts @@ -0,0 +1,198 @@ +import { cold, hot } from 'jasmine-marbles'; +import { Observable } from 'rxjs/Observable'; +import { TestBed } from '@angular/core/testing'; +import { provideMockActions } from '@ngrx/effects/testing'; +import { AuthEffects } from './auth.effects'; +import { + AuthActionTypes, + AuthenticatedAction, + AuthenticatedErrorAction, + AuthenticatedSuccessAction, + AuthenticationErrorAction, + AuthenticationSuccessAction, + CheckAuthenticationTokenErrorAction, + LogOutErrorAction, + LogOutSuccessAction, + RefreshTokenErrorAction, + RefreshTokenSuccessAction +} from './auth.actions'; +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 store: Store = jasmine.createSpyObj('store', { + /* tslint:disable:no-empty */ + dispatch: {}, + /* tslint:enable:no-empty */ + select: Observable.of(true) + }); + const token = new AuthTokenInfo('token_test'); + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + AuthEffects, + {provide: AuthService, useClass: AuthServiceStub}, + {provide: Store, useValue: store}, + provideMockActions(() => actions), + // other providers + ], + }); + + authEffects = TestBed.get(AuthEffects); + }); + + describe('authenticate$', () => { + describe('when credentials are correct', () => { + it('should return a AUTHENTICATE_SUCCESS action in response to a AUTHENTICATE action', () => { + actions = hot('--a-', { + a: { + type: AuthActionTypes.AUTHENTICATE, + payload: {email: 'user', password: 'password'} + } + }); + + const expected = cold('--b-', {b: new AuthenticationSuccessAction(new AuthTokenInfo('token_test'))}); + + expect(authEffects.authenticate$).toBeObservable(expected); + }); + }); + + describe('when credentials are wrong', () => { + it('should return a AUTHENTICATE_ERROR action in response to a AUTHENTICATE action', () => { + spyOn((authEffects as any).authService, 'authenticate').and.returnValue(Observable.throw(new Error('Message Error test'))); + + actions = hot('--a-', { + a: { + type: AuthActionTypes.AUTHENTICATE, + payload: {email: 'user', password: 'wrongpassword'} + } + }); + + const expected = cold('--b-', {b: new AuthenticationErrorAction(new Error('Message Error test'))}); + + expect(authEffects.authenticate$).toBeObservable(expected); + }); + }); + }); + + describe('authenticateSuccess$', () => { + + 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'))}); + + expect(authEffects.authenticateSuccess$).toBeObservable(expected); + }); + }); + + describe('authenticated$', () => { + + describe('when token is valid', () => { + it('should return a AUTHENTICATED_SUCCESS action in response to a AUTHENTICATED action', () => { + actions = hot('--a-', {a: {type: AuthActionTypes.AUTHENTICATED, payload: token}}); + + const expected = cold('--b-', {b: new AuthenticatedSuccessAction(true, token, EpersonMock)}); + + expect(authEffects.authenticated$).toBeObservable(expected); + }); + }); + + describe('when token is not valid', () => { + it('should return a AUTHENTICATED_ERROR action in response to a AUTHENTICATED action', () => { + spyOn((authEffects as any).authService, 'authenticatedUser').and.returnValue(Observable.throw(new Error('Message Error test'))); + + actions = hot('--a-', {a: {type: AuthActionTypes.AUTHENTICATED, payload: token}}); + + const expected = cold('--b-', {b: new AuthenticatedErrorAction(new Error('Message Error test'))}); + + expect(authEffects.authenticated$).toBeObservable(expected); + }); + }); + }); + + describe('checkToken$', () => { + + describe('when check token succeeded', () => { + it('should return a AUTHENTICATED action in response to a CHECK_AUTHENTICATION_TOKEN action', () => { + + actions = hot('--a-', {a: {type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN}}); + + const expected = cold('--b-', {b: new AuthenticatedAction(new AuthTokenInfo('token_test'))}); + + expect(authEffects.checkToken$).toBeObservable(expected); + }); + }); + + describe('when check token failed', () => { + it('should return a CHECK_AUTHENTICATION_TOKEN_ERROR action in response to a CHECK_AUTHENTICATION_TOKEN action', () => { + spyOn((authEffects as any).authService, 'hasValidAuthenticationToken').and.returnValue(Observable.throw('')); + + actions = hot('--a-', {a: {type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN, payload: token}}); + + const expected = cold('--b-', {b: new CheckAuthenticationTokenErrorAction()}); + + expect(authEffects.checkToken$).toBeObservable(expected); + }); + }) + }); + + describe('refreshToken$', () => { + + describe('when refresh token succeeded', () => { + it('should return a REFRESH_TOKEN_SUCCESS action in response to a REFRESH_TOKEN action', () => { + + actions = hot('--a-', {a: {type: AuthActionTypes.REFRESH_TOKEN}}); + + const expected = cold('--b-', {b: new RefreshTokenSuccessAction(new AuthTokenInfo('token_test'))}); + + expect(authEffects.refreshToken$).toBeObservable(expected); + }); + }); + + describe('when refresh token failed', () => { + it('should return a REFRESH_TOKEN_ERROR action in response to a REFRESH_TOKEN action', () => { + spyOn((authEffects as any).authService, 'refreshAuthenticationToken').and.returnValue(Observable.throw('')); + + actions = hot('--a-', {a: {type: AuthActionTypes.REFRESH_TOKEN, payload: token}}); + + const expected = cold('--b-', {b: new RefreshTokenErrorAction()}); + + expect(authEffects.refreshToken$).toBeObservable(expected); + }); + }) + }); + + describe('logOut$', () => { + + describe('when refresh token succeeded', () => { + it('should return a LOG_OUT_SUCCESS action in response to a LOG_OUT action', () => { + + actions = hot('--a-', {a: {type: AuthActionTypes.LOG_OUT}}); + + const expected = cold('--b-', {b: new LogOutSuccessAction()}); + + expect(authEffects.logOut$).toBeObservable(expected); + }); + }); + + describe('when refresh token failed', () => { + it('should return a REFRESH_TOKEN_ERROR action in response to a LOG_OUT action', () => { + spyOn((authEffects as any).authService, 'logout').and.returnValue(Observable.throw(new Error('Message Error test'))); + + actions = hot('--a-', {a: {type: AuthActionTypes.LOG_OUT, payload: token}}); + + const expected = cold('--b-', {b: new LogOutErrorAction(new Error('Message Error test'))}); + + expect(authEffects.logOut$).toBeObservable(expected); + }); + }) + }); +}); diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts index f3bebebbcd..60fe04a5d1 100644 --- a/src/app/core/auth/auth.effects.ts +++ b/src/app/core/auth/auth.effects.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; // import @ngrx -import { Effect, Actions } from '@ngrx/effects'; +import { Actions, Effect } from '@ngrx/effects'; import { Action, Store } from '@ngrx/store'; // import rxjs @@ -9,16 +9,22 @@ import { Observable } from 'rxjs/Observable'; // import services import { AuthService } from './auth.service'; - // import actions import { - AuthActionTypes, AuthenticateAction, AuthenticatedAction, + AuthActionTypes, + AuthenticateAction, + AuthenticatedAction, AuthenticatedErrorAction, AuthenticatedSuccessAction, AuthenticationErrorAction, - AuthenticationSuccessAction, CheckAuthenticationTokenErrorAction, + AuthenticationSuccessAction, + CheckAuthenticationTokenErrorAction, LogOutErrorAction, - LogOutSuccessAction, RefreshTokenAction, RefreshTokenErrorAction, RefreshTokenSuccessAction, RegistrationAction, + LogOutSuccessAction, + RefreshTokenAction, + RefreshTokenErrorAction, + RefreshTokenSuccessAction, + RegistrationAction, RegistrationErrorAction, RegistrationSuccessAction } from './auth.actions'; @@ -37,7 +43,7 @@ export class AuthEffects { * @method authenticate */ @Effect() - public authenticate: Observable = this.actions$ + public authenticate$: Observable = this.actions$ .ofType(AuthActionTypes.AUTHENTICATE) .switchMap((action: AuthenticateAction) => { return this.authService.authenticate(action.payload.email, action.payload.password) @@ -47,13 +53,13 @@ export class AuthEffects { }); @Effect() - public authenticateSuccess: Observable = this.actions$ + public authenticateSuccess$: Observable = this.actions$ .ofType(AuthActionTypes.AUTHENTICATE_SUCCESS) .do((action: AuthenticationSuccessAction) => this.authService.storeToken(action.payload)) .map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload)); @Effect() - public authenticated: Observable = this.actions$ + public authenticated$: Observable = this.actions$ .ofType(AuthActionTypes.AUTHENTICATED) .switchMap((action: AuthenticatedAction) => { return this.authService.authenticatedUser(action.payload) @@ -63,12 +69,12 @@ export class AuthEffects { // It means "reacts to this action but don't send another" @Effect({dispatch: false}) - public authenticatedError: Observable = this.actions$ + public authenticatedError$: Observable = this.actions$ .ofType(AuthActionTypes.AUTHENTICATED_ERROR) .do((action: LogOutSuccessAction) => this.authService.removeToken()); @Effect() - public checkToken: Observable = this.actions$ + public checkToken$: Observable = this.actions$ .ofType(AuthActionTypes.CHECK_AUTHENTICATION_TOKEN) .switchMap(() => { return this.authService.hasValidAuthenticationToken() @@ -77,7 +83,7 @@ export class AuthEffects { }); @Effect() - public createUser: Observable = this.actions$ + public createUser$: Observable = this.actions$ .ofType(AuthActionTypes.REGISTRATION) .debounceTime(500) // to remove when functionality is implemented .switchMap((action: RegistrationAction) => { @@ -87,7 +93,7 @@ export class AuthEffects { }); @Effect() - public refreshToken: Observable = this.actions$ + public refreshToken$: Observable = this.actions$ .ofType(AuthActionTypes.REFRESH_TOKEN) .switchMap((action: RefreshTokenAction) => { return this.authService.refreshAuthenticationToken(action.payload) @@ -97,7 +103,7 @@ export class AuthEffects { // It means "reacts to this action but don't send another" @Effect({dispatch: false}) - public refreshTokenSuccess: Observable = this.actions$ + public refreshTokenSuccess$: Observable = this.actions$ .ofType(AuthActionTypes.REFRESH_TOKEN_SUCCESS) .do((action: RefreshTokenSuccessAction) => this.authService.replaceToken(action.payload)); @@ -106,7 +112,7 @@ export class AuthEffects { * clear a possible invalid token or authentication errors */ @Effect({dispatch: false}) - public clearInvalidTokenOnRehydrate = this.actions$ + public clearInvalidTokenOnRehydrate$: Observable = this.actions$ .ofType(StoreActionTypes.REHYDRATE) .switchMap(() => { return this.store.select(isAuthenticated) @@ -117,7 +123,7 @@ export class AuthEffects { }); @Effect() - public logOut: Observable = this.actions$ + public logOut$: Observable = this.actions$ .ofType(AuthActionTypes.LOG_OUT) .switchMap(() => { return this.authService.logout() @@ -126,19 +132,19 @@ export class AuthEffects { }); @Effect({dispatch: false}) - public logOutSuccess: Observable = this.actions$ + public logOutSuccess$: Observable = this.actions$ .ofType(AuthActionTypes.LOG_OUT_SUCCESS) .do(() => this.authService.removeToken()) .do(() => this.authService.refreshAfterLogout()); @Effect({dispatch: false}) - public redirectToLogin: Observable = this.actions$ + public redirectToLogin$: Observable = this.actions$ .ofType(AuthActionTypes.REDIRECT_AUTHENTICATION_REQUIRED) .do(() => this.authService.removeToken()) .do(() => this.authService.redirectToLogin()); @Effect({dispatch: false}) - public redirectToLoginTokenExpired: Observable = this.actions$ + public redirectToLoginTokenExpired$: Observable = this.actions$ .ofType(AuthActionTypes.REDIRECT_TOKEN_EXPIRED) .do(() => this.authService.removeToken()) .do(() => this.authService.redirectToLoginWhenTokenExpired()); diff --git a/src/app/shared/testing/auth-service-stub.ts b/src/app/shared/testing/auth-service-stub.ts index 5c5f2c67dd..2a8e2f467f 100644 --- a/src/app/shared/testing/auth-service-stub.ts +++ b/src/app/shared/testing/auth-service-stub.ts @@ -1,6 +1,54 @@ +import { AuthStatus } from '../../core/auth/models/auth-status.model'; +import { Observable } from 'rxjs/Observable'; +import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model'; +import { EpersonMock } from './eperson-mock'; +import { Eperson } from '../../core/eperson/models/eperson.model'; + export class AuthServiceStub { + 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.eperson = [EpersonMock]; + return Observable.of(authStatus); + } else { + console.log('error'); + throw(new Error('Message Error test')); + } + } + + public authenticatedUser(token: AuthTokenInfo): Observable { + if (token.accessToken === 'token_test') { + return Observable.of(EpersonMock); + } else { + throw(new Error('Message Error test')); + } + } + + public hasValidAuthenticationToken(): Observable { + return Observable.of(new AuthTokenInfo('token_test')); + } + + public logout(): Observable { + return Observable.of(true); + } + + public refreshAuthenticationToken(token: AuthTokenInfo): Observable { + return Observable.of(new AuthTokenInfo('token_test')); + } + public redirectToPreviousUrl() { return; } + + public removeToken() { + return; + } + + public storeToken(token: AuthTokenInfo) { + return; + } }