From 39b47ea580aa85335b2d3b894c51406d7d1adcca Mon Sep 17 00:00:00 2001 From: Brian Kopp Date: Tue, 11 Dec 2018 14:05:58 -0700 Subject: [PATCH] Progress on example app (#4) Updating tests and packaging --- package-lock.json | 11 + projects/ngrx-cognito/package.json | 14 +- .../src/lib/services/cognito.service.spec.ts | 7 +- .../require-logged-in-guard.service.spec.ts | 10 +- .../require-logged-out-guard.service.spec.ts | 10 +- .../src/lib/state/cognito.effects.ts | 336 +++++++++--------- src/app/app.component.spec.ts | 5 +- .../login-form/login-form.component.spec.ts | 7 +- .../components/login/login.component.spec.ts | 12 +- .../signup-form/signup-form.component.spec.ts | 7 +- .../signup/signup.component.spec.ts | 12 +- 11 files changed, 245 insertions(+), 186 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8f687c8..510b2b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -131,6 +131,17 @@ "rxjs": "6.3.3" } }, + "@angular-devkit/build-webpack": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.11.2.tgz", + "integrity": "sha512-biKjwoFXazNmgPiFJbXDbIMaZGjAV2VAauV3CWs8xHdIdC1Q2yVxF3I+babvgQnHEQWozykK/DuLoZn4kCzQUQ==", + "dev": true, + "requires": { + "@angular-devkit/architect": "0.11.2", + "@angular-devkit/core": "7.1.2", + "rxjs": "6.3.3" + } + }, "@angular-devkit/core": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-7.1.2.tgz", diff --git a/projects/ngrx-cognito/package.json b/projects/ngrx-cognito/package.json index bc0f843..074b4b2 100644 --- a/projects/ngrx-cognito/package.json +++ b/projects/ngrx-cognito/package.json @@ -1,12 +1,22 @@ { "name": "ngrx-cognito", + "description": "Cognito workflow in a simple API, maintained in ngrx", "version": "0.0.1", + "author": "Brian Kopp", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/briankopp/ngrx-cognito.git" + }, + "bugs": { + "url": "https://github.com/briankopp/ngrx-cognito/issues" + }, + "homepage": "https://github.com/briankopp/ngrx-cognito", "peerDependencies": { "@angular/common": "^7.1.2", "@angular/core": "^7.1.2", "@ngrx/effects": "^6.1.2", "@ngrx/store": "^6.1.2", "amazon-cognito-identity-js": "^3.0.5" - }, - "dependencies": {} + } } diff --git a/projects/ngrx-cognito/src/lib/services/cognito.service.spec.ts b/projects/ngrx-cognito/src/lib/services/cognito.service.spec.ts index c2e1e13..380e3ef 100644 --- a/projects/ngrx-cognito/src/lib/services/cognito.service.spec.ts +++ b/projects/ngrx-cognito/src/lib/services/cognito.service.spec.ts @@ -1,9 +1,14 @@ import { TestBed } from '@angular/core/testing'; import { CognitoService } from './cognito.service'; +import { CognitoConfigService } from './cognito-config.service'; describe('CognitoService', () => { - beforeEach(() => TestBed.configureTestingModule({})); + beforeEach(() => + TestBed.configureTestingModule({ + providers: [{ provide: CognitoConfigService, useValue: {} }] + }) + ); it('should be created', () => { const service: CognitoService = TestBed.get(CognitoService); diff --git a/projects/ngrx-cognito/src/lib/services/require-logged-in-guard.service.spec.ts b/projects/ngrx-cognito/src/lib/services/require-logged-in-guard.service.spec.ts index 2022d31..e80ffad 100644 --- a/projects/ngrx-cognito/src/lib/services/require-logged-in-guard.service.spec.ts +++ b/projects/ngrx-cognito/src/lib/services/require-logged-in-guard.service.spec.ts @@ -1,9 +1,17 @@ import { TestBed } from '@angular/core/testing'; import { RequireLoggedInGuardService } from './require-logged-in-guard.service'; +import { CognitoConfigService } from './cognito-config.service'; +import { RouterTestingModule } from '@angular/router/testing'; +import { StoreModule } from '@ngrx/store'; describe('RequireLoggedInGuardService', () => { - beforeEach(() => TestBed.configureTestingModule({})); + beforeEach(() => + TestBed.configureTestingModule({ + imports: [RouterTestingModule.withRoutes([]), StoreModule.forRoot({})], + providers: [{ provide: CognitoConfigService, useValue: {} }] + }) + ); it('should be created', () => { const service: RequireLoggedInGuardService = TestBed.get(RequireLoggedInGuardService); diff --git a/projects/ngrx-cognito/src/lib/services/require-logged-out-guard.service.spec.ts b/projects/ngrx-cognito/src/lib/services/require-logged-out-guard.service.spec.ts index e482861..cd62147 100644 --- a/projects/ngrx-cognito/src/lib/services/require-logged-out-guard.service.spec.ts +++ b/projects/ngrx-cognito/src/lib/services/require-logged-out-guard.service.spec.ts @@ -1,9 +1,17 @@ import { TestBed } from '@angular/core/testing'; import { RequireLoggedOutGuardService } from './require-logged-out-guard.service'; +import { RouterTestingModule } from '@angular/router/testing'; +import { CognitoConfigService } from './cognito-config.service'; +import { StoreModule } from '@ngrx/store'; describe('RequireLoggedOutGuardService', () => { - beforeEach(() => TestBed.configureTestingModule({})); + beforeEach(() => + TestBed.configureTestingModule({ + imports: [RouterTestingModule.withRoutes([]), StoreModule.forRoot({})], + providers: [{ provide: CognitoConfigService, useValue: {} }] + }) + ); it('should be created', () => { const service: RequireLoggedOutGuardService = TestBed.get(RequireLoggedOutGuardService); diff --git a/projects/ngrx-cognito/src/lib/state/cognito.effects.ts b/projects/ngrx-cognito/src/lib/state/cognito.effects.ts index ca2e8d9..14a5126 100644 --- a/projects/ngrx-cognito/src/lib/state/cognito.effects.ts +++ b/projects/ngrx-cognito/src/lib/state/cognito.effects.ts @@ -1,168 +1,168 @@ -import { Injectable, Inject } from '@angular/core'; -import { Router } from '@angular/router'; -import { Actions, Effect, ofType } from '@ngrx/effects'; -import { Store } from '@ngrx/store'; -import { map, switchMap, catchError, withLatestFrom, tap } from 'rxjs/operators'; -import { of, defer, Observable, Subscription } from 'rxjs'; - -import { CognitoService } from '../services/cognito.service'; -import * as cog from './cognito.actions'; -import { CognitoFacade } from './cognito.facade'; -import { CognitoState } from './cognito.reducer'; -import { - LoginResponseCodes, - SignupResponse, - ConfirmationCodeResponse, - LoginResponse, - LoadUserFromStorageResponse, - CognitoConfig -} from '../model'; -import { CognitoConfigService } from '../services/cognito-config.service'; - -@Injectable() -export class CognitoEffects { - constructor( - private actions$: Actions, - private store: Store, - private cognitoService: CognitoService, - private authFacade: CognitoFacade, - private router: Router, - @Inject(CognitoConfigService) private config: CognitoConfig - ) {} - - @Effect() - login$ = this.actions$.pipe(ofType(cog.CognitoActionTypes.LOGIN)).pipe( - map((action: cog.LoginAction) => action.payload), - tap(({ username, password }) => { - const user = this.cognitoService.createUserWithCredentials(username); - const authDetails = this.cognitoService.createAuthDetails(username, password); - this.store.dispatch(new cog.LoginWaitingAction({ user, authDetails })); - }), - switchMap(({ username, password, redirectUrl }) => { - return this.cognitoService.loginUser(username, password).pipe( - map(response => { - switch (response.code) { - case LoginResponseCodes.SUCCESS: - return new cog.LoginSuccessAction({ redirectUrl }); - case LoginResponseCodes.INVALID_CREDENTIALS: - return new cog.LoginFailureAction({ errorMessage: 'Incorrect username or password' }); - case LoginResponseCodes.MFA_REQUIRED: - return new cog.RequireMFACodeAction(); - case LoginResponseCodes.NOT_CONFIRMED: - return new cog.RequireUserConfirmationAction(); - case LoginResponseCodes.REQUIRE_PASSWORD_RESET: - return new cog.RequireNewPasswordAction(); - case LoginResponseCodes.UNKNOWN: - default: - return new cog.LoginFailureAction({ errorMessage: 'Login failed' }); - } - }), - catchError(error => of(new cog.LoginFailureAction({ errorMessage: error }))) - ); - }) - ); - - loginSuccess$ = this.actions$.pipe(ofType(cog.CognitoActionTypes.LOGIN_SUCCESS)).subscribe(_ => { - this.router.navigate([this.config.loginDidSucceedUrl]); - }); - - @Effect() - signup$ = this.actions$.pipe(ofType(cog.CognitoActionTypes.SIGNUP)).pipe( - map((action: cog.SignupAction) => action.payload), - tap(({ username }) => { - const user = this.cognitoService.createUserWithCredentials(username); - this.store.dispatch(new cog.SignupWaitingAction({ user })); - }), - switchMap(({ username, password, email, attributeData }) => { - return this.cognitoService.signupUser(username, password, email, attributeData).pipe( - map((response: SignupResponse) => { - if (response.errorMessage) { - return new cog.SignupFailureAction({ errorMessage: response.errorMessage }); - } else if (response.userIsConfirmed || response.userIsConfirmed === null) { - return new cog.SignupSuccessAction({ - cognitoUser: response.user - }); - } else { - return new cog.RequireUserConfirmationAction(); - } - }), - catchError(err => of(new cog.SignupFailureAction({ errorMessage: err }))) - ); - }) - ); - - @Effect() - logout$ = this.actions$.pipe(ofType(cog.CognitoActionTypes.LOGOUT)).pipe( - withLatestFrom(this.authFacade.cognitoUser$), - tap(([_, user]) => { - this.cognitoService.logoutUser(user); - }), - switchMap(_ => { - return of(new cog.LogoutSuccessAction()); - }) - ); - - logoutSuccess$ = this.actions$.pipe(ofType(cog.CognitoActionTypes.LOGOUT_SUCCESS)).subscribe(_ => { - this.router.navigate([this.config.logoutDidSucceedUrl]); - }); - - @Effect() - submitConfirmationCode$ = this.actions$.pipe(ofType(cog.CognitoActionTypes.SUBMIT_CONFIRMATION_CODE)).pipe( - map((action: cog.SubmitConfirmationCodeAction) => action.payload.confirmationCode), - withLatestFrom(this.authFacade.cognitoUser$), - switchMap(([code, user]) => { - return this.cognitoService.submitConfirmationCode(user, code).pipe( - map((response: ConfirmationCodeResponse) => { - if (response.errorMessage || !response.success) { - const errMsg = response.errorMessage ? response.errorMessage : 'Error submitting confirmation code'; - return new cog.SubmitConfirmationCodeFailureAction({ errorMessage: errMsg }); - } else { - return new cog.SubmitConfirmationCodeSuccessAction(); - } - }), - catchError(err => of(new cog.SubmitConfirmationCodeFailureAction({ errorMessage: err }))) - ); - }) - ); - - submitConfirmationCodeSuccess$ = this.actions$ - .pipe(ofType(cog.CognitoActionTypes.SUBMIT_CONFIRMATION_CODE_SUCCESS)) - .subscribe((_: cog.SubmitConfirmationCodeSuccessAction) => { - this.router.navigate([this.config.loginDidSucceedUrl]); - }); - - @Effect() - submitMfaCode$ = this.actions$.pipe(ofType(cog.CognitoActionTypes.SUBMIT_MFA)).pipe( - map((action: cog.SubmitMFACodeAction) => action.payload.mfaCode), - withLatestFrom(this.authFacade.cognitoUser$), - switchMap(([code, user]) => { - return this.cognitoService.submitMfaCode(user, code).pipe( - map((response: LoginResponse) => { - if (response.code === LoginResponseCodes.MFA_REQUIRED) { - return new cog.SubmitMFACodeFailureInvalidAction({ errorMessage: 'Code invalid' }); - } else { - return new cog.SubmitMFACodeSuccessAction(); - } - }), - catchError(err => of(new cog.SubmitMFACodeFailureInvalidAction({ errorMessage: err }))) - ); - }) - ); - - @Effect({ dispatch: false }) - initAuth$ = defer(() => { - return this.cognitoService.loadUserFromStorage().pipe( - map((response: LoadUserFromStorageResponse) => { - if (response.user && response.accessToken && response.idToken) { - this.store.dispatch( - new cog.InitAuthUserRememberedAction({ - user: response.user, - accessToken: response.accessToken, - idToken: response.idToken - }) - ); - } - }) - ); - }); -} +import { Injectable, Inject } from '@angular/core'; +import { Router } from '@angular/router'; +import { Actions, Effect, ofType } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; +import { map, switchMap, catchError, withLatestFrom, tap } from 'rxjs/operators'; +import { of, defer, Observable, Subscription } from 'rxjs'; + +import { CognitoService } from '../services/cognito.service'; +import * as cog from './cognito.actions'; +import { CognitoFacade } from './cognito.facade'; +import { CognitoState } from './cognito.reducer'; +import { + LoginResponseCodes, + SignupResponse, + ConfirmationCodeResponse, + LoginResponse, + LoadUserFromStorageResponse, + CognitoConfig +} from '../model'; +import { CognitoConfigService } from '../services/cognito-config.service'; + +@Injectable() +export class CognitoEffects { + constructor( + private actions$: Actions, + private store: Store, + private cognitoService: CognitoService, + private cognitoFacade: CognitoFacade, + private router: Router, + @Inject(CognitoConfigService) private config: CognitoConfig + ) {} + + @Effect() + login$ = this.actions$.pipe(ofType(cog.CognitoActionTypes.LOGIN)).pipe( + map((action: cog.LoginAction) => action.payload), + tap(({ username, password }) => { + const user = this.cognitoService.createUserWithCredentials(username); + const authDetails = this.cognitoService.createAuthDetails(username, password); + this.store.dispatch(new cog.LoginWaitingAction({ user, authDetails })); + }), + switchMap(({ username, password, redirectUrl }) => { + return this.cognitoService.loginUser(username, password).pipe( + map(response => { + switch (response.code) { + case LoginResponseCodes.SUCCESS: + return new cog.LoginSuccessAction({ redirectUrl }); + case LoginResponseCodes.INVALID_CREDENTIALS: + return new cog.LoginFailureAction({ errorMessage: 'Incorrect username or password' }); + case LoginResponseCodes.MFA_REQUIRED: + return new cog.RequireMFACodeAction(); + case LoginResponseCodes.NOT_CONFIRMED: + return new cog.RequireUserConfirmationAction(); + case LoginResponseCodes.REQUIRE_PASSWORD_RESET: + return new cog.RequireNewPasswordAction(); + case LoginResponseCodes.UNKNOWN: + default: + return new cog.LoginFailureAction({ errorMessage: 'Login failed' }); + } + }), + catchError(error => of(new cog.LoginFailureAction({ errorMessage: error }))) + ); + }) + ); + + loginSuccess$ = this.actions$.pipe(ofType(cog.CognitoActionTypes.LOGIN_SUCCESS)).subscribe(_ => { + this.router.navigate([this.config.loginDidSucceedUrl]); + }); + + @Effect() + signup$ = this.actions$.pipe(ofType(cog.CognitoActionTypes.SIGNUP)).pipe( + map((action: cog.SignupAction) => action.payload), + tap(({ username }) => { + const user = this.cognitoService.createUserWithCredentials(username); + this.store.dispatch(new cog.SignupWaitingAction({ user })); + }), + switchMap(({ username, password, email, attributeData }) => { + return this.cognitoService.signupUser(username, password, email, attributeData).pipe( + map((response: SignupResponse) => { + if (response.errorMessage) { + return new cog.SignupFailureAction({ errorMessage: response.errorMessage }); + } else if (response.userIsConfirmed || response.userIsConfirmed === null) { + return new cog.SignupSuccessAction({ + cognitoUser: response.user + }); + } else { + return new cog.RequireUserConfirmationAction(); + } + }), + catchError(err => of(new cog.SignupFailureAction({ errorMessage: err }))) + ); + }) + ); + + @Effect() + logout$ = this.actions$.pipe(ofType(cog.CognitoActionTypes.LOGOUT)).pipe( + withLatestFrom(this.cognitoFacade.cognitoUser$), + tap(([_, user]) => { + this.cognitoService.logoutUser(user); + }), + switchMap(_ => { + return of(new cog.LogoutSuccessAction()); + }) + ); + + logoutSuccess$ = this.actions$.pipe(ofType(cog.CognitoActionTypes.LOGOUT_SUCCESS)).subscribe(_ => { + this.router.navigate([this.config.logoutDidSucceedUrl]); + }); + + @Effect() + submitConfirmationCode$ = this.actions$.pipe(ofType(cog.CognitoActionTypes.SUBMIT_CONFIRMATION_CODE)).pipe( + map((action: cog.SubmitConfirmationCodeAction) => action.payload.confirmationCode), + withLatestFrom(this.cognitoFacade.cognitoUser$), + switchMap(([code, user]) => { + return this.cognitoService.submitConfirmationCode(user, code).pipe( + map((response: ConfirmationCodeResponse) => { + if (response.errorMessage || !response.success) { + const errMsg = response.errorMessage ? response.errorMessage : 'Error submitting confirmation code'; + return new cog.SubmitConfirmationCodeFailureAction({ errorMessage: errMsg }); + } else { + return new cog.SubmitConfirmationCodeSuccessAction(); + } + }), + catchError(err => of(new cog.SubmitConfirmationCodeFailureAction({ errorMessage: err }))) + ); + }) + ); + + submitConfirmationCodeSuccess$ = this.actions$ + .pipe(ofType(cog.CognitoActionTypes.SUBMIT_CONFIRMATION_CODE_SUCCESS)) + .subscribe((_: cog.SubmitConfirmationCodeSuccessAction) => { + this.router.navigate([this.config.loginDidSucceedUrl]); + }); + + @Effect() + submitMfaCode$ = this.actions$.pipe(ofType(cog.CognitoActionTypes.SUBMIT_MFA)).pipe( + map((action: cog.SubmitMFACodeAction) => action.payload.mfaCode), + withLatestFrom(this.cognitoFacade.cognitoUser$), + switchMap(([code, user]) => { + return this.cognitoService.submitMfaCode(user, code).pipe( + map((response: LoginResponse) => { + if (response.code === LoginResponseCodes.MFA_REQUIRED) { + return new cog.SubmitMFACodeFailureInvalidAction({ errorMessage: 'Code invalid' }); + } else { + return new cog.SubmitMFACodeSuccessAction(); + } + }), + catchError(err => of(new cog.SubmitMFACodeFailureInvalidAction({ errorMessage: err }))) + ); + }) + ); + + @Effect({ dispatch: false }) + initAuth$ = defer(() => { + return this.cognitoService.loadUserFromStorage().pipe( + map((response: LoadUserFromStorageResponse) => { + if (response.user && response.accessToken && response.idToken) { + this.store.dispatch( + new cog.InitAuthUserRememberedAction({ + user: response.user, + accessToken: response.accessToken, + idToken: response.idToken + }) + ); + } + }) + ); + }); +} diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 6003c0b..78d7166 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -1,11 +1,14 @@ import { TestBed, async } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { AppComponent } from './app.component'; +import { CognitoFacade } from 'ngrx-cognito'; +import { StoreModule } from '@ngrx/store'; describe('AppComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [RouterTestingModule], + imports: [RouterTestingModule, StoreModule.forRoot([])], + providers: [CognitoFacade], declarations: [AppComponent] }).compileComponents(); })); diff --git a/src/app/components/login-form/login-form.component.spec.ts b/src/app/components/login-form/login-form.component.spec.ts index c5f08d4..57b8533 100644 --- a/src/app/components/login-form/login-form.component.spec.ts +++ b/src/app/components/login-form/login-form.component.spec.ts @@ -1,6 +1,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { LoginFormComponent } from './login-form.component'; +import { ReactiveFormsModule } from '@angular/forms'; describe('LoginFormComponent', () => { let component: LoginFormComponent; @@ -8,9 +9,9 @@ describe('LoginFormComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ LoginFormComponent ] - }) - .compileComponents(); + imports: [ReactiveFormsModule], + declarations: [LoginFormComponent] + }).compileComponents(); })); beforeEach(() => { diff --git a/src/app/components/login/login.component.spec.ts b/src/app/components/login/login.component.spec.ts index d6d85a8..d32f191 100644 --- a/src/app/components/login/login.component.spec.ts +++ b/src/app/components/login/login.component.spec.ts @@ -1,6 +1,11 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { LoginComponent } from './login.component'; +import { LoginFormComponent } from '../login-form/login-form.component'; +import { ConfirmationCodeFormComponent } from '../confirmation-code-form/confirmation-code-form.component'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CognitoFacade } from 'ngrx-cognito'; +import { StoreModule } from '@ngrx/store'; describe('LoginComponent', () => { let component: LoginComponent; @@ -8,9 +13,10 @@ describe('LoginComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ LoginComponent ] - }) - .compileComponents(); + imports: [ReactiveFormsModule, StoreModule.forRoot([])], + providers: [CognitoFacade], + declarations: [LoginComponent, LoginFormComponent, ConfirmationCodeFormComponent] + }).compileComponents(); })); beforeEach(() => { diff --git a/src/app/components/signup-form/signup-form.component.spec.ts b/src/app/components/signup-form/signup-form.component.spec.ts index 53e36f9..f770bf7 100644 --- a/src/app/components/signup-form/signup-form.component.spec.ts +++ b/src/app/components/signup-form/signup-form.component.spec.ts @@ -1,6 +1,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { SignupFormComponent } from './signup-form.component'; +import { ReactiveFormsModule } from '@angular/forms'; describe('SignupFormComponent', () => { let component: SignupFormComponent; @@ -8,9 +9,9 @@ describe('SignupFormComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ SignupFormComponent ] - }) - .compileComponents(); + imports: [ReactiveFormsModule], + declarations: [SignupFormComponent] + }).compileComponents(); })); beforeEach(() => { diff --git a/src/app/components/signup/signup.component.spec.ts b/src/app/components/signup/signup.component.spec.ts index 43e46a5..862126f 100644 --- a/src/app/components/signup/signup.component.spec.ts +++ b/src/app/components/signup/signup.component.spec.ts @@ -1,6 +1,11 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { SignupComponent } from './signup.component'; +import { ReactiveFormsModule } from '@angular/forms'; +import { StoreModule } from '@ngrx/store'; +import { CognitoFacade } from 'ngrx-cognito'; +import { SignupFormComponent } from '../signup-form/signup-form.component'; +import { ConfirmationCodeFormComponent } from '../confirmation-code-form/confirmation-code-form.component'; describe('SignupComponent', () => { let component: SignupComponent; @@ -8,9 +13,10 @@ describe('SignupComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ SignupComponent ] - }) - .compileComponents(); + imports: [ReactiveFormsModule, StoreModule.forRoot([])], + providers: [CognitoFacade], + declarations: [SignupComponent, SignupFormComponent, ConfirmationCodeFormComponent] + }).compileComponents(); })); beforeEach(() => {