From fdd05d2fecfd9ef768d2f23f4c0e8b86db8cfc52 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 2 Jan 2020 18:21:47 +0100 Subject: [PATCH] Refactored components' name and added missing tests --- src/app/core/auth/auth.actions.ts | 6 +- src/app/core/auth/auth.effects.spec.ts | 74 +++++++++- src/app/core/auth/auth.effects.ts | 92 ++++++------ src/app/core/auth/auth.interceptor.ts | 97 +++++++++--- src/app/core/auth/auth.reducer.spec.ts | 8 +- src/app/core/auth/auth.reducer.ts | 10 +- src/app/core/auth/auth.service.spec.ts | 22 +++ src/app/core/auth/auth.service.ts | 15 +- src/app/core/auth/models/auth-status.model.ts | 4 +- .../auth/models/auth.method-type.ts} | 0 .../{auth-method.model.ts => auth.method.ts} | 4 +- .../models/normalized-auth-status.model.ts | 1 - src/app/core/auth/server-auth.service.ts | 10 +- ...t.html => log-in-container.component.html} | 0 ...t.scss => log-in-container.component.scss} | 0 .../log-in-container.component.spec.ts | 108 ++++++++++++++ .../container/log-in-container.component.ts | 51 +++++++ .../container/login-container.component.ts | 52 ------- src/app/shared/log-in/log-in.component.html | 6 +- .../shared/log-in/log-in.component.spec.ts | 139 ++++++++---------- src/app/shared/log-in/log-in.component.ts | 29 ++-- ...corator.ts => log-in.methods-decorator.ts} | 2 +- .../log-in-password.component.spec.ts | 9 +- .../password/log-in-password.component.ts | 33 ++--- .../log-in-shibboleth.component.spec.ts | 105 +++++++++++++ .../shibboleth/log-in-shibboleth.component.ts | 20 ++- src/app/shared/shared.module.ts | 5 +- .../testing/auth-request-service-stub.ts | 5 +- src/app/shared/testing/auth-service-stub.ts | 15 +- 29 files changed, 637 insertions(+), 285 deletions(-) rename src/app/{shared/log-in/methods/authMethods-type.ts => core/auth/models/auth.method-type.ts} (100%) rename src/app/core/auth/models/{auth-method.model.ts => auth.method.ts} (86%) rename src/app/shared/log-in/container/{login-container.component.html => log-in-container.component.html} (100%) rename src/app/shared/log-in/container/{login-container.component.scss => log-in-container.component.scss} (100%) create mode 100644 src/app/shared/log-in/container/log-in-container.component.spec.ts create mode 100644 src/app/shared/log-in/container/log-in-container.component.ts delete mode 100644 src/app/shared/log-in/container/login-container.component.ts rename src/app/shared/log-in/methods/{authMethods-decorator.ts => log-in.methods-decorator.ts} (83%) create mode 100644 src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.spec.ts diff --git a/src/app/core/auth/auth.actions.ts b/src/app/core/auth/auth.actions.ts index 0e082f414e..7cff3fd1c7 100644 --- a/src/app/core/auth/auth.actions.ts +++ b/src/app/core/auth/auth.actions.ts @@ -7,7 +7,7 @@ import { type } from '../../shared/ngrx/type'; // import models import { EPerson } from '../eperson/models/eperson.model'; import { AuthTokenInfo } from './models/auth-token-info.model'; -import { AuthMethodModel } from './models/auth-method.model'; +import { AuthMethod } from './models/auth.method'; import { AuthStatus } from './models/auth-status.model'; export const AuthActionTypes = { @@ -340,9 +340,9 @@ export class RetrieveAuthMethodsAction implements Action { */ export class RetrieveAuthMethodsSuccessAction implements Action { public type: string = AuthActionTypes.RETRIEVE_AUTH_METHODS_SUCCESS; - payload: AuthMethodModel[]; + payload: AuthMethod[]; - constructor(authMethods: AuthMethodModel[] ) { + constructor(authMethods: AuthMethod[] ) { this.payload = authMethods; } } diff --git a/src/app/core/auth/auth.effects.spec.ts b/src/app/core/auth/auth.effects.spec.ts index a29be0b342..8b18a2b5a4 100644 --- a/src/app/core/auth/auth.effects.spec.ts +++ b/src/app/core/auth/auth.effects.spec.ts @@ -18,13 +18,14 @@ import { LogOutErrorAction, LogOutSuccessAction, RefreshTokenErrorAction, - RefreshTokenSuccessAction + RefreshTokenSuccessAction, RetrieveAuthMethodsAction, RetrieveAuthMethodsErrorAction, RetrieveAuthMethodsSuccessAction } from './auth.actions'; -import { AuthServiceStub } from '../../shared/testing/auth-service-stub'; +import { authMethodsMock, AuthServiceStub } from '../../shared/testing/auth-service-stub'; import { AuthService } from './auth.service'; import { AuthState } from './auth.reducer'; import { EPersonMock } from '../../shared/testing/eperson-mock'; +import { AuthStatus } from './models/auth-status.model'; describe('AuthEffects', () => { let authEffects: AuthEffects; @@ -153,6 +154,49 @@ describe('AuthEffects', () => { }) }); + describe('checkTokenCookie$', () => { + + describe('when check token succeeded', () => { + it('should return a AUTHENTICATED action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action when authenticated is true', () => { + spyOn((authEffects as any).authService, 'checkAuthenticationCookie').and.returnValue( + observableOf( + { authenticated: true, + token + }) + ); + actions = hot('--a-', {a: {type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE}}); + + const expected = cold('--b-', {b: new AuthenticatedAction(token)}); + + expect(authEffects.checkTokenCookie$).toBeObservable(expected); + }); + + it('should return a RETRIEVE_AUTH_METHODS action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action when authenticated is false', () => { + spyOn((authEffects as any).authService, 'checkAuthenticationCookie').and.returnValue( + observableOf( + { authenticated: false }) + ); + actions = hot('--a-', {a: {type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE}}); + + const expected = cold('--b-', {b: new RetrieveAuthMethodsAction({ authenticated: false } as AuthStatus)}); + + expect(authEffects.checkTokenCookie$).toBeObservable(expected); + }); + }); + + describe('when check token failed', () => { + it('should return a AUTHENTICATED_ERROR action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action', () => { + spyOn((authEffects as any).authService, 'checkAuthenticationCookie').and.returnValue(observableThrow(new Error('Message Error test'))); + + actions = hot('--a-', {a: {type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE, payload: token}}); + + const expected = cold('--b-', {b: new AuthenticatedErrorAction(new Error('Message Error test'))}); + + expect(authEffects.checkTokenCookie$).toBeObservable(expected); + }); + }) + }); + describe('refreshToken$', () => { describe('when refresh token succeeded', () => { @@ -204,4 +248,30 @@ describe('AuthEffects', () => { }); }) }); + + describe('retrieveMethods$', () => { + + describe('when retrieve authentication methods succeeded', () => { + it('should return a RETRIEVE_AUTH_METHODS_SUCCESS action in response to a RETRIEVE_AUTH_METHODS action', () => { + + actions = hot('--a-', {a: {type: AuthActionTypes.RETRIEVE_AUTH_METHODS}}); + + const expected = cold('--b-', {b: new RetrieveAuthMethodsSuccessAction(authMethodsMock)}); + + expect(authEffects.retrieveMethods$).toBeObservable(expected); + }); + }); + + describe('when retrieve authentication methods failed', () => { + it('should return a RETRIEVE_AUTH_METHODS_ERROR action in response to a RETRIEVE_AUTH_METHODS action', () => { + spyOn((authEffects as any).authService, 'retrieveAuthMethods').and.returnValue(observableThrow('')); + + actions = hot('--a-', {a: {type: AuthActionTypes.RETRIEVE_AUTH_METHODS}}); + + const expected = cold('--b-', {b: new RetrieveAuthMethodsErrorAction()}); + + expect(authEffects.retrieveMethods$).toBeObservable(expected); + }); + }) + }); }); diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts index 0643896320..5d58e8f752 100644 --- a/src/app/core/auth/auth.effects.ts +++ b/src/app/core/auth/auth.effects.ts @@ -2,9 +2,11 @@ import { Observable, of as observableOf } from 'rxjs'; import { catchError, debounceTime, filter, map, switchMap, take, tap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; + // import @ngrx import { Actions, Effect, ofType } from '@ngrx/effects'; import { Action, select, Store } from '@ngrx/store'; + // import services import { AuthService } from './auth.service'; // import actions @@ -35,7 +37,7 @@ import { AuthTokenInfo } from './models/auth-token-info.model'; import { AppState } from '../../app.reducer'; import { isAuthenticated } from './selectors'; import { StoreActionTypes } from '../../store.actions'; -import { AuthMethodModel } from './models/auth-method.model'; +import { AuthMethod } from './models/auth.method'; @Injectable() export class AuthEffects { @@ -46,39 +48,39 @@ export class AuthEffects { */ @Effect() public authenticate$: Observable = this.actions$.pipe( - ofType(AuthActionTypes.AUTHENTICATE), - switchMap((action: AuthenticateAction) => { - return this.authService.authenticate(action.payload.email, action.payload.password).pipe( - take(1), - map((response: AuthStatus) => new AuthenticationSuccessAction(response.token)), - catchError((error) => observableOf(new AuthenticationErrorAction(error))) - ); - }) - ); + ofType(AuthActionTypes.AUTHENTICATE), + switchMap((action: AuthenticateAction) => { + return this.authService.authenticate(action.payload.email, action.payload.password).pipe( + take(1), + map((response: AuthStatus) => new AuthenticationSuccessAction(response.token)), + catchError((error) => observableOf(new AuthenticationErrorAction(error))) + ); + }) + ); @Effect() public authenticateSuccess$: Observable = this.actions$.pipe( - ofType(AuthActionTypes.AUTHENTICATE_SUCCESS), - tap((action: AuthenticationSuccessAction) => this.authService.storeToken(action.payload)), - map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload)) - ); + ofType(AuthActionTypes.AUTHENTICATE_SUCCESS), + tap((action: AuthenticationSuccessAction) => this.authService.storeToken(action.payload)), + map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload)) + ); @Effect() public authenticated$: Observable = this.actions$.pipe( - ofType(AuthActionTypes.AUTHENTICATED), - switchMap((action: AuthenticatedAction) => { - return this.authService.authenticatedUser(action.payload).pipe( - map((user: EPerson) => new AuthenticatedSuccessAction((user !== null), action.payload, user)), - catchError((error) => observableOf(new AuthenticatedErrorAction(error))),); - }) - ); + ofType(AuthActionTypes.AUTHENTICATED), + switchMap((action: AuthenticatedAction) => { + return this.authService.authenticatedUser(action.payload).pipe( + map((user: EPerson) => new AuthenticatedSuccessAction((user !== null), action.payload, user)), + catchError((error) => observableOf(new AuthenticatedErrorAction(error))),); + }) + ); // It means "reacts to this action but don't send another" @Effect({ dispatch: false }) public authenticatedError$: Observable = this.actions$.pipe( - ofType(AuthActionTypes.AUTHENTICATED_ERROR), - tap((action: LogOutSuccessAction) => this.authService.removeToken()) - ); + ofType(AuthActionTypes.AUTHENTICATED_ERROR), + tap((action: LogOutSuccessAction) => this.authService.removeToken()) + ); @Effect() public checkToken$: Observable = this.actions$.pipe(ofType(AuthActionTypes.CHECK_AUTHENTICATION_TOKEN), @@ -91,7 +93,7 @@ export class AuthEffects { ); @Effect() - public checkTokenError$: Observable = this.actions$.pipe( + public checkTokenCookie$: Observable = this.actions$.pipe( ofType(AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE), switchMap(() => { return this.authService.checkAuthenticationCookie().pipe( @@ -109,32 +111,32 @@ export class AuthEffects { @Effect() public createUser$: Observable = this.actions$.pipe( - ofType(AuthActionTypes.REGISTRATION), - debounceTime(500), // to remove when functionality is implemented - switchMap((action: RegistrationAction) => { - return this.authService.create(action.payload).pipe( - map((user: EPerson) => new RegistrationSuccessAction(user)), - catchError((error) => observableOf(new RegistrationErrorAction(error))) - ); - }) - ); + ofType(AuthActionTypes.REGISTRATION), + debounceTime(500), // to remove when functionality is implemented + switchMap((action: RegistrationAction) => { + return this.authService.create(action.payload).pipe( + map((user: EPerson) => new RegistrationSuccessAction(user)), + catchError((error) => observableOf(new RegistrationErrorAction(error))) + ); + }) + ); @Effect() public refreshToken$: Observable = this.actions$.pipe(ofType(AuthActionTypes.REFRESH_TOKEN), - switchMap((action: RefreshTokenAction) => { - return this.authService.refreshAuthenticationToken(action.payload).pipe( - map((token: AuthTokenInfo) => new RefreshTokenSuccessAction(token)), - catchError((error) => observableOf(new RefreshTokenErrorAction())) - ); - }) - ); + switchMap((action: RefreshTokenAction) => { + return this.authService.refreshAuthenticationToken(action.payload).pipe( + map((token: AuthTokenInfo) => new RefreshTokenSuccessAction(token)), + catchError((error) => observableOf(new RefreshTokenErrorAction())) + ); + }) + ); // It means "reacts to this action but don't send another" @Effect({ dispatch: false }) public refreshTokenSuccess$: Observable = this.actions$.pipe( - ofType(AuthActionTypes.REFRESH_TOKEN_SUCCESS), - tap((action: RefreshTokenSuccessAction) => this.authService.replaceToken(action.payload)) - ); + ofType(AuthActionTypes.REFRESH_TOKEN_SUCCESS), + tap((action: RefreshTokenSuccessAction) => this.authService.replaceToken(action.payload)) + ); /** * When the store is rehydrated in the browser, @@ -195,7 +197,7 @@ export class AuthEffects { switchMap((action: RetrieveAuthMethodsAction) => { return this.authService.retrieveAuthMethods(action.payload) .pipe( - map((authMethodModels: AuthMethodModel[]) => new RetrieveAuthMethodsSuccessAction(authMethodModels)), + map((authMethodModels: AuthMethod[]) => new RetrieveAuthMethodsSuccessAction(authMethodModels)), catchError((error) => observableOf(new RetrieveAuthMethodsErrorAction())) ) }) diff --git a/src/app/core/auth/auth.interceptor.ts b/src/app/core/auth/auth.interceptor.ts index 09ac357ac9..6d2cd73d89 100644 --- a/src/app/core/auth/auth.interceptor.ts +++ b/src/app/core/auth/auth.interceptor.ts @@ -22,8 +22,8 @@ import { isNotEmpty, isNotNull, isUndefined } from '../../shared/empty.util'; import { RedirectWhenTokenExpiredAction, RefreshTokenAction } from './auth.actions'; import { Store } from '@ngrx/store'; import { Router } from '@angular/router'; -import { AuthMethodModel } from './models/auth-method.model'; -import { AuthMethodType } from '../../shared/log-in/methods/authMethods-type'; +import { AuthMethod } from './models/auth.method'; +import { AuthMethodType } from './models/auth.method-type'; @Injectable() export class AuthInterceptor implements HttpInterceptor { @@ -36,15 +36,30 @@ export class AuthInterceptor implements HttpInterceptor { constructor(private inj: Injector, private router: Router, private store: Store) { } + /** + * Check if response status code is 401 + * + * @param response + */ private isUnauthorized(response: HttpResponseBase): boolean { // invalid_token The access token provided is expired, revoked, malformed, or invalid for other reasons return response.status === 401; } + /** + * Check if response status code is 200 or 204 + * + * @param response + */ private isSuccess(response: HttpResponseBase): boolean { return (response.status === 200 || response.status === 204); } + /** + * Check if http request is to authn endpoint + * + * @param http + */ private isAuthRequest(http: HttpRequest | HttpResponseBase): boolean { return http && http.url && (http.url.endsWith('/authn/login') @@ -52,29 +67,47 @@ export class AuthInterceptor implements HttpInterceptor { || http.url.endsWith('/authn/status')); } + /** + * Check if response is from a login request + * + * @param http + */ private isLoginResponse(http: HttpRequest | HttpResponseBase): boolean { return http.url && http.url.endsWith('/authn/login') } + /** + * Check if response is from a logout request + * + * @param http + */ private isLogoutResponse(http: HttpRequest | HttpResponseBase): boolean { return http.url && http.url.endsWith('/authn/logout'); } - private parseLocation(unparsedLocation: string): string { - unparsedLocation = unparsedLocation.trim(); - unparsedLocation = unparsedLocation.replace('location="', ''); - unparsedLocation = unparsedLocation.replace('"', ''); + /** + * Extract location url from the WWW-Authenticate header + * + * @param header + */ + private parseLocation(header: string): string { + let location = header.trim(); + location = location.replace('location="', ''); + location = location.replace('"', ''); let re = /%3A%2F%2F/g; - unparsedLocation = unparsedLocation.replace(re, '://'); - re = /%3A/g - unparsedLocation = unparsedLocation.replace(re, ':') - const parsedLocation = unparsedLocation.trim(); // + '/shibboleth'; - - return parsedLocation; + location = location.replace(re, '://'); + re = /%3A/g; + location = location.replace(re, ':'); + return location.trim(); } - private sortAuthMethods(authMethodModels: AuthMethodModel[]): AuthMethodModel[] { - const sortedAuthMethodModels: AuthMethodModel[] = new Array(); + /** + * Sort authentication methods list + * + * @param authMethodModels + */ + private sortAuthMethods(authMethodModels: AuthMethod[]): AuthMethod[] { + const sortedAuthMethodModels: AuthMethod[] = []; authMethodModels.forEach((method) => { if (method.authMethodType === AuthMethodType.Password) { sortedAuthMethodModels.push(method); @@ -90,10 +123,14 @@ export class AuthInterceptor implements HttpInterceptor { return sortedAuthMethodModels; } - private parseAuthMethodsfromHeaders(headers: HttpHeaders): AuthMethodModel[] { - let authMethodModels: AuthMethodModel[] = []; + /** + * Extract authentication methods list from the WWW-Authenticate headers + * + * @param headers + */ + private parseAuthMethodsFromHeaders(headers: HttpHeaders): AuthMethod[] { + let authMethodModels: AuthMethod[] = []; if (isNotEmpty(headers.get('www-authenticate'))) { - const parts: string[] = headers.get('www-authenticate').split(','); // get the realms from the header - a realm is a single auth method const completeWWWauthenticateHeader = headers.get('www-authenticate'); const regex = /(\w+ (\w+=((".*?")|[^,]*)(, )?)*)/g; @@ -105,15 +142,14 @@ export class AuthInterceptor implements HttpInterceptor { const splittedRealm = realms[j].split(', '); const methodName = splittedRealm[0].split(' ')[0].trim(); - let authMethodModel: AuthMethodModel; + let authMethodModel: AuthMethod; if (splittedRealm.length === 1) { - authMethodModel = new AuthMethodModel(methodName); + authMethodModel = new AuthMethod(methodName); authMethodModels.push(authMethodModel); } else if (splittedRealm.length > 1) { let location = splittedRealm[1]; location = this.parseLocation(location); - authMethodModel = new AuthMethodModel(methodName, location); - // console.log('location: ', location); + authMethodModel = new AuthMethod(methodName, location); authMethodModels.push(authMethodModel); } } @@ -121,17 +157,25 @@ export class AuthInterceptor implements HttpInterceptor { // make sure the email + password login component gets rendered first authMethodModels = this.sortAuthMethods(authMethodModels); } else { - authMethodModels.push(new AuthMethodModel(AuthMethodType.Password)); + authMethodModels.push(new AuthMethod(AuthMethodType.Password)); } return authMethodModels; } + /** + * Generate an AuthStatus object + * + * @param authenticated + * @param accessToken + * @param error + * @param httpHeaders + */ private makeAuthStatusObject(authenticated: boolean, accessToken ?: string, error ?: string, httpHeaders ?: HttpHeaders): AuthStatus { const authStatus = new AuthStatus(); // let authMethods: AuthMethodModel[]; if (httpHeaders) { - authStatus.authMethods = this.parseAuthMethodsfromHeaders(httpHeaders); + authStatus.authMethods = this.parseAuthMethodsFromHeaders(httpHeaders); } authStatus.id = null; @@ -149,6 +193,11 @@ export class AuthInterceptor implements HttpInterceptor { return authStatus; } + /** + * Intercept method + * @param req + * @param next + */ intercept(req: HttpRequest, next: HttpHandler): Observable> { const authService = this.inj.get(AuthService); @@ -182,7 +231,7 @@ export class AuthInterceptor implements HttpInterceptor { newReq = req.clone({withCredentials: true}); } -// Pass on the new request instead of the original request. + // Pass on the new request instead of the original request. return next.handle(newReq).pipe( // tap((response) => console.log('next.handle: ', response)), map((response) => { diff --git a/src/app/core/auth/auth.reducer.spec.ts b/src/app/core/auth/auth.reducer.spec.ts index 5c67174f69..b7073e3fa5 100644 --- a/src/app/core/auth/auth.reducer.spec.ts +++ b/src/app/core/auth/auth.reducer.spec.ts @@ -26,8 +26,8 @@ import { import { AuthTokenInfo } from './models/auth-token-info.model'; import { EPersonMock } from '../../shared/testing/eperson-mock'; import { AuthStatus } from './models/auth-status.model'; -import { AuthMethodModel } from './models/auth-method.model'; -import { AuthMethodType } from '../../shared/log-in/methods/authMethods-type'; +import { AuthMethod } from './models/auth.method'; +import { AuthMethodType } from './models/auth.method-type'; describe('authReducer', () => { @@ -441,8 +441,8 @@ describe('authReducer', () => { authMethods: [] }; const authMethods = [ - new AuthMethodModel(AuthMethodType.Password), - new AuthMethodModel(AuthMethodType.Shibboleth, 'location') + new AuthMethod(AuthMethodType.Password), + new AuthMethod(AuthMethodType.Shibboleth, 'location') ]; const action = new RetrieveAuthMethodsSuccessAction(authMethods); const newState = authReducer(initialState, action); diff --git a/src/app/core/auth/auth.reducer.ts b/src/app/core/auth/auth.reducer.ts index 2019920670..7e6db94eab 100644 --- a/src/app/core/auth/auth.reducer.ts +++ b/src/app/core/auth/auth.reducer.ts @@ -8,14 +8,14 @@ import { LogOutErrorAction, RedirectWhenAuthenticationIsRequiredAction, RedirectWhenTokenExpiredAction, - RefreshTokenSuccessAction, RetrieveAuthMethodsSuccessAction, + RefreshTokenSuccessAction, + RetrieveAuthMethodsSuccessAction, SetRedirectUrlAction } from './auth.actions'; // import models import { EPerson } from '../eperson/models/eperson.model'; import { AuthTokenInfo } from './models/auth-token-info.model'; -import { AuthMethodModel } from './models/auth-method.model'; -import { AuthMethodType } from '../../shared/log-in/methods/authMethods-type'; +import { AuthMethod } from './models/auth.method'; /** * The auth state. @@ -50,8 +50,8 @@ export interface AuthState { // the authenticated user user?: EPerson; - // all authenticationMethods enabled at the backend - authMethods?: AuthMethodModel[]; + // all authentication Methods enabled at the backend + authMethods?: AuthMethod[]; } diff --git a/src/app/core/auth/auth.service.spec.ts b/src/app/core/auth/auth.service.spec.ts index 86794f257b..f138947c7a 100644 --- a/src/app/core/auth/auth.service.spec.ts +++ b/src/app/core/auth/auth.service.spec.ts @@ -25,6 +25,8 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service'; import { routeServiceStub } from '../../shared/testing/route-service-stub'; import { RouteService } from '../services/route.service'; +import { authMethodsMock } from '../../shared/testing/auth-service-stub'; +import { AuthMethod } from './models/auth.method'; describe('AuthService test', () => { @@ -128,6 +130,26 @@ describe('AuthService test', () => { expect(authService.logout.bind(null)).toThrow(); }); + it('should return the authentication status object to check an Authentication Cookie', () => { + authService.checkAuthenticationCookie().subscribe((status: AuthStatus) => { + expect(status).toBeDefined(); + }); + }); + + it('should return the authentication methods available', () => { + const authStatus = new AuthStatus(); + + authService.retrieveAuthMethods(authStatus).subscribe((authMethods: AuthMethod[]) => { + expect(authMethods).toBeDefined(); + expect(authMethods.length).toBe(0); + }); + + authStatus.authMethods = authMethodsMock; + authService.retrieveAuthMethods(authStatus).subscribe((authMethods: AuthMethod[]) => { + expect(authMethods).toBeDefined(); + expect(authMethods.length).toBe(2); + }); + }); }); describe('', () => { diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index e958e2b5f2..cb7a0bb486 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -27,9 +27,7 @@ import { NativeWindowRef, NativeWindowService } from '../services/window.service import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RouteService } from '../services/route.service'; -import { GlobalConfig } from '../../../config/global-config.interface'; -import { GLOBAL_CONFIG } from '../../../config'; -import { AuthMethodModel } from './models/auth-method.model'; +import { AuthMethod } from './models/auth.method'; export const LOGIN_ROUTE = '/login'; export const LOGOUT_ROUTE = '/logout'; @@ -213,8 +211,8 @@ export class AuthService { * Retrieve authentication methods available * @returns {User} */ - public retrieveAuthMethods(status: AuthStatus): Observable { - let authMethods: AuthMethodModel[] = []; + public retrieveAuthMethods(status: AuthStatus): Observable { + let authMethods: AuthMethod[] = []; if (isNotEmpty(status.authMethods)) { authMethods = status.authMethods; } @@ -241,7 +239,7 @@ export class AuthService { // Send a request that sign end the session let headers = new HttpHeaders(); headers = headers.append('Content-Type', 'application/x-www-form-urlencoded'); - const options: HttpOptions = Object.create({headers, responseType: 'text'}); + const options: HttpOptions = Object.create({ headers, responseType: 'text' }); return this.authRequestService.getRequest('logout', options).pipe( map((status: AuthStatus) => { if (!status.authenticated) { @@ -317,7 +315,7 @@ export class AuthService { // Set the cookie expire date const expires = new Date(expireDate); - const options: CookieAttributes = {expires: expires}; + const options: CookieAttributes = { expires: expires }; // Save cookie with the token return this.storage.set(TOKENITEM, token, options); @@ -384,7 +382,6 @@ export class AuthService { // For standalone login pages, use the previous route. redirUrl = history[history.length - 2] || ''; } else { - // console.log('isStandAlonePage: ', isStandalonePage); redirUrl = history[history.length - 1] || ''; } this.navigateToRedirectUrl(redirUrl); @@ -438,7 +435,7 @@ export class AuthService { // Set the cookie expire date const expires = new Date(expireDate); - const options: CookieAttributes = {expires: expires}; + const options: CookieAttributes = { expires: expires }; this.storage.set(REDIRECT_COOKIE, url, options); this.store.dispatch(new SetRedirectUrlAction(isNotUndefined(url) ? url : '')); } diff --git a/src/app/core/auth/models/auth-status.model.ts b/src/app/core/auth/models/auth-status.model.ts index e4db209f30..9e5b27be95 100644 --- a/src/app/core/auth/models/auth-status.model.ts +++ b/src/app/core/auth/models/auth-status.model.ts @@ -5,7 +5,7 @@ import { RemoteData } from '../../data/remote-data'; import { Observable } from 'rxjs'; import { CacheableObject } from '../../cache/object-cache.reducer'; import { ResourceType } from '../../shared/resource-type'; -import {AuthMethodModel} from './auth-method.model'; +import { AuthMethod } from './auth.method'; /** * Object that represents the authenticated status of a user @@ -56,6 +56,6 @@ export class AuthStatus implements CacheableObject { /** * All authentication methods enabled at the backend */ - authMethods: AuthMethodModel[]; + authMethods: AuthMethod[]; } diff --git a/src/app/shared/log-in/methods/authMethods-type.ts b/src/app/core/auth/models/auth.method-type.ts similarity index 100% rename from src/app/shared/log-in/methods/authMethods-type.ts rename to src/app/core/auth/models/auth.method-type.ts diff --git a/src/app/core/auth/models/auth-method.model.ts b/src/app/core/auth/models/auth.method.ts similarity index 86% rename from src/app/core/auth/models/auth-method.model.ts rename to src/app/core/auth/models/auth.method.ts index 76b9f51aea..617154080b 100644 --- a/src/app/core/auth/models/auth-method.model.ts +++ b/src/app/core/auth/models/auth.method.ts @@ -1,6 +1,6 @@ -import { AuthMethodType } from '../../../shared/log-in/methods/authMethods-type'; +import { AuthMethodType } from './auth.method-type'; -export class AuthMethodModel { +export class AuthMethod { authMethodType: AuthMethodType; location?: string; diff --git a/src/app/core/auth/models/normalized-auth-status.model.ts b/src/app/core/auth/models/normalized-auth-status.model.ts index 672cd5d062..d8d0c0b952 100644 --- a/src/app/core/auth/models/normalized-auth-status.model.ts +++ b/src/app/core/auth/models/normalized-auth-status.model.ts @@ -4,7 +4,6 @@ import { mapsTo, relationship } from '../../cache/builders/build-decorators'; import { NormalizedObject } from '../../cache/models/normalized-object.model'; import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer'; import { EPerson } from '../../eperson/models/eperson.model'; -import {AuthMethodModel} from './auth-method.model'; @mapsTo(AuthStatus) @inheritSerialization(NormalizedObject) diff --git a/src/app/core/auth/server-auth.service.ts b/src/app/core/auth/server-auth.service.ts index 00c114bbfb..aee5d3a4b7 100644 --- a/src/app/core/auth/server-auth.service.ts +++ b/src/app/core/auth/server-auth.service.ts @@ -1,16 +1,16 @@ import { filter, map, switchMap, take } from 'rxjs/operators'; import { Injectable } from '@angular/core'; +import { HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs'; -import { HttpHeaders } from '@angular/common/http'; + import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { AuthStatus } from './models/auth-status.model'; import { isNotEmpty } from '../../shared/empty.util'; import { AuthService } from './auth.service'; import { AuthTokenInfo } from './models/auth-token-info.model'; -import { CheckAuthenticationTokenAction } from './auth.actions'; import { EPerson } from '../eperson/models/eperson.model'; -import { AuthMethodModel } from './models/auth-method.model'; +import { AuthMethod } from './models/auth.method'; /** * The auth service. @@ -76,7 +76,7 @@ export class ServerAuthService extends AuthService { * Retrieve authentication methods available * @returns {User} */ - public retrieveAuthMethods(): Observable { + public retrieveAuthMethods(): Observable { const options: HttpOptions = Object.create({}); if (isNotEmpty(this.req.headers) && isNotEmpty(this.req.headers.referer)) { let headers = new HttpHeaders(); @@ -86,7 +86,7 @@ export class ServerAuthService extends AuthService { return this.authRequestService.postToEndpoint('login', {}, options).pipe( map((status: AuthStatus) => { - let authMethods: AuthMethodModel[]; + let authMethods: AuthMethod[]; if (isNotEmpty(status.authMethods)) { authMethods = status.authMethods; } diff --git a/src/app/shared/log-in/container/login-container.component.html b/src/app/shared/log-in/container/log-in-container.component.html similarity index 100% rename from src/app/shared/log-in/container/login-container.component.html rename to src/app/shared/log-in/container/log-in-container.component.html diff --git a/src/app/shared/log-in/container/login-container.component.scss b/src/app/shared/log-in/container/log-in-container.component.scss similarity index 100% rename from src/app/shared/log-in/container/login-container.component.scss rename to src/app/shared/log-in/container/log-in-container.component.scss diff --git a/src/app/shared/log-in/container/log-in-container.component.spec.ts b/src/app/shared/log-in/container/log-in-container.component.spec.ts new file mode 100644 index 0000000000..c819b0cc8d --- /dev/null +++ b/src/app/shared/log-in/container/log-in-container.component.spec.ts @@ -0,0 +1,108 @@ +import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { StoreModule } from '@ngrx/store'; +import { TranslateModule } from '@ngx-translate/core'; + +import { LogInContainerComponent } from './log-in-container.component'; +import { authReducer } from '../../../core/auth/auth.reducer'; +import { SharedModule } from '../../shared.module'; +import { createTestComponent } from '../../testing/utils'; +import { AuthService } from '../../../core/auth/auth.service'; +import { AuthMethod } from '../../../core/auth/models/auth.method'; +import { AuthServiceStub } from '../../testing/auth-service-stub'; + +describe('LogInContainerComponent', () => { + + let component: LogInContainerComponent; + let fixture: ComponentFixture; + + const authMethod = new AuthMethod('password'); + + beforeEach(async(() => { + // refine the test module by declaring the test component + TestBed.configureTestingModule({ + imports: [ + FormsModule, + ReactiveFormsModule, + StoreModule.forRoot(authReducer), + SharedModule, + TranslateModule.forRoot() + ], + declarations: [ + TestComponent + ], + providers: [ + {provide: AuthService, useClass: AuthServiceStub}, + LogInContainerComponent + ], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ] + }) + .compileComponents(); + + })); + + describe('', () => { + let testComp: TestComponent; + let testFixture: ComponentFixture; + + // synchronous beforeEach + beforeEach(() => { + const html = ` `; + + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + afterEach(() => { + testFixture.destroy(); + }); + + it('should create LogInContainerComponent', inject([LogInContainerComponent], (app: LogInContainerComponent) => { + + expect(app).toBeDefined(); + + })); + }); + + describe('', () => { + beforeEach(() => { + fixture = TestBed.createComponent(LogInContainerComponent); + component = fixture.componentInstance; + + spyOn(component, 'getAuthMethodContent').and.callThrough(); + component.authMethod = authMethod; + fixture.detectChanges(); + }); + + afterEach(() => { + fixture.destroy(); + component = null; + }); + + it('should inject component properly', () => { + + component.ngOnInit(); + fixture.detectChanges(); + + expect(component.getAuthMethodContent).toHaveBeenCalled(); + + }); + + }); + +}); + +// declare a test component +@Component({ + selector: 'ds-test-cmp', + template: `` +}) +class TestComponent { + + isStandalonePage = true; + +} diff --git a/src/app/shared/log-in/container/log-in-container.component.ts b/src/app/shared/log-in/container/log-in-container.component.ts new file mode 100644 index 0000000000..660e616b9d --- /dev/null +++ b/src/app/shared/log-in/container/log-in-container.component.ts @@ -0,0 +1,51 @@ +import { Component, Injector, Input, OnInit } from '@angular/core'; + +import { rendersAuthMethodType } from '../methods/log-in.methods-decorator'; +import { AuthMethod } from '../../../core/auth/models/auth.method'; + +/** + * This component represents a component container for log-in methods available. + */ +@Component({ + selector: 'ds-log-in-container', + templateUrl: './log-in-container.component.html', + styleUrls: ['./log-in-container.component.scss'] +}) +export class LogInContainerComponent implements OnInit { + + @Input() authMethod: AuthMethod; + + /** + * Injector to inject a section component with the @Input parameters + * @type {Injector} + */ + public objectInjector: Injector; + + /** + * Initialize instance variables + * + * @param {Injector} injector + */ + constructor(private injector: Injector) { + } + + /** + * Initialize all instance variables + */ + ngOnInit() { + this.objectInjector = Injector.create({ + providers: [ + { provide: 'authMethodProvider', useFactory: () => (this.authMethod), deps: [] }, + ], + parent: this.injector + }); + } + + /** + * Find the correct component based on the AuthMethod's type + */ + getAuthMethodContent(): string { + return rendersAuthMethodType(this.authMethod.authMethodType) + } + +} diff --git a/src/app/shared/log-in/container/login-container.component.ts b/src/app/shared/log-in/container/login-container.component.ts deleted file mode 100644 index eb713abacb..0000000000 --- a/src/app/shared/log-in/container/login-container.component.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Component, Injector, Input, OnInit} from '@angular/core'; -import { rendersAuthMethodType } from '../methods/authMethods-decorator'; -import { Store } from '@ngrx/store'; -import { CoreState } from '../../../core/core.reducers'; -import { AuthMethodModel } from '../../../core/auth/models/auth-method.model'; - -/** - * This component represents a section that contains the submission license form. - */ -@Component({ - selector: 'ds-login-container', - templateUrl: './login-container.component.html', - styleUrls: ['./login-container.component.scss'] -}) -export class LoginContainerComponent implements OnInit { - - @Input() authMethodModel: AuthMethodModel; - - /** - * Injector to inject a section component with the @Input parameters - * @type {Injector} - */ - public objectInjector: Injector; - - /** - * Initialize instance variables - * - * @param {Injector} injector - */ - constructor(private injector: Injector, private store: Store) { - } - - /** - * Initialize all instance variables - */ - ngOnInit() { - this.objectInjector = Injector.create({ - providers: [ - {provide: 'authMethodModelProvider', useFactory: () => (this.authMethodModel), deps: []}, - ], - parent: this.injector - }); - } - - /** - * Find the correct component based on the AuthMethod's type - */ - getAuthMethodContent(): string { - return rendersAuthMethodType(this.authMethodModel.authMethodType) - } - -} diff --git a/src/app/shared/log-in/log-in.component.html b/src/app/shared/log-in/log-in.component.html index 8648681ada..faceadf1bd 100644 --- a/src/app/shared/log-in/log-in.component.html +++ b/src/app/shared/log-in/log-in.component.html @@ -1,10 +1,10 @@ -