Created
March 24, 2017 01:31
-
-
Save scionwest/04b65dc81ae21429ab928e90e0973265 to your computer and use it in GitHub Desktop.
Account creation and authentication components with spec file for unit-tests.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export enum AccountFormType { | |
SignIn = 1, | |
CreateAccount = 2 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Angular | |
import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms'; | |
import { Injectable } from '@angular/core'; | |
// Custom Enum | |
import { AccountFormType } from './account-form-type.enum'; | |
@Injectable() | |
// Factory responsible for creating the FormGroups used by the components facilitating account sign-in and account creation. | |
export class AccountFormFactory { | |
// Inject a FormBuilder into the factory constructor. | |
constructor(private formBuilder: FormBuilder) {} | |
// Creates a FormGroup representing a 'New Account' form. | |
public createAccountForm(): FormGroup { | |
return this.createForm(AccountFormType.CreateAccount); | |
} | |
// Creates a FormGroup representing a 'Sign-In' form. | |
public signInForm(): FormGroup { | |
return this.createForm(AccountFormType.SignIn); | |
} | |
// Conditionally creates a 'New Account' or 'Sign-In' form based on the FormType provided. | |
private createForm(formType: AccountFormType): FormGroup { | |
var form: FormGroup; | |
let emailRegex = '^[a-z0-9]+(\.[_a-z0-9]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,15})$'; | |
if (formType === AccountFormType.SignIn) { | |
form = this.formBuilder.group({ | |
'emailAddress': new FormControl('', [Validators.required, Validators.pattern(emailRegex)]), | |
'password': new FormControl('', Validators.required), | |
'accountName': new FormControl('', Validators.required) | |
}) | |
} else if (formType === AccountFormType.CreateAccount) { | |
form = this.formBuilder.group({ | |
'emailAddress': new FormControl('', [Validators.required, Validators.pattern(emailRegex)]), | |
'password': new FormControl('', Validators.required), | |
'accountName': new FormControl('', Validators.required), | |
'passwordConfirmation': new FormControl('', Validators.required) | |
}); | |
} | |
// Return the created FormGroup | |
return form; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export interface Authentication { | |
emailAddress: string; | |
password: string; | |
confirmPassword: string; | |
accountName: string; | |
providerTypeId: number; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Injectable, OnInit } from '@angular/core'; | |
import { Http, URLSearchParams, Headers, RequestOptions, Response } from '@angular/http'; | |
import 'rxjs/add/operator/toPromise'; | |
import 'rxjs/add/operator/mergeMap'; | |
import { Authentication } from '../authentication.interface'; | |
import { BearerToken } from './bearer-token'; | |
@Injectable() | |
export class AuthenticationService implements OnInit { | |
private authTokenKey: string = 'auth_token'; | |
private createAccountApi: string = 'Register/ProviderAccount'; | |
private tokenApi: string = 'connect/token'; | |
private access_token: string; | |
private accountHeader: Headers; | |
private tokenHeader: Headers; | |
private accountOptions: RequestOptions; | |
private tokenOptions: RequestOptions; | |
constructor(private http: Http) { | |
this.accountHeader = new Headers({ | |
'Content-Type': 'application/json' | |
}); | |
this.tokenHeader = new Headers({ | |
'Content-Type': 'application/x-www-form-urlencoded' | |
}); | |
// var tokenHeader = new Headers(); | |
// tokenHeader.append('Content-Type', 'application/json'); | |
// tokenHeader.append('Authorization', 'Bearer ' + localStorage.getItem('id_token')); | |
this.accountOptions = new RequestOptions({ headers: this.accountHeader }); | |
this.tokenOptions = new RequestOptions({ headers: this.tokenHeader }); | |
} | |
get isAuthenticated(): boolean { | |
if (this.access_token) { | |
return true; | |
} else { | |
return false; | |
} | |
} | |
ngOnInit() { | |
this.access_token = localStorage.getItem(this.authTokenKey); | |
} | |
createAccount(authentication: Authentication): void { | |
authentication.providerTypeId = 1; | |
let data: any = JSON.stringify(authentication); | |
let data2 = new URLSearchParams(); | |
data2.append('username', authentication.emailAddress); | |
data2.append('password', authentication.password); | |
// data2.append('provider', authentication.accountName); | |
data2.append('grant_type', 'password'); | |
this.http | |
.post(this.createAccountApi, data, this.accountOptions) | |
.flatMap(response => { | |
return this.http.post(this.tokenApi, data2); | |
}) | |
.map(response => response.json()) | |
.catch(this.handleError) | |
.subscribe(this.handleResponse); | |
} | |
public logIn(authentication: Authentication): void { | |
let data = new URLSearchParams(); | |
data.append('username', authentication.emailAddress); | |
data.append('password', authentication.password); | |
// data.append('provider', authentication.accountName); | |
data.append('grant_type', 'password'); | |
this.http.post(this.tokenApi, data) | |
.catch(this.handleError) | |
.subscribe(this.handleResponse); | |
} | |
public logOut(): void { | |
this.access_token = null; | |
localStorage.removeItem(this.authTokenKey); | |
} | |
public refreshToken(): void {} | |
private handleResponse(response: any): void { | |
// let token: BearerToken = new BearerToken(response.access_token, response.expires_in, response.token_type); | |
localStorage.setItem(this.authTokenKey, response.access_token); | |
console.log(response); | |
} | |
private handleError(error: any): Promise<any> { | |
// TODO: Remove after debugging is done | |
console.error('An error occured', error); | |
return Promise.reject(error.message || error); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Guid } from '../guid'; | |
export class BearerToken { | |
constructor( | |
public accessToken: string, | |
public expiresIn: number, | |
public tokenType: string, | |
public providerId: Guid, | |
public userId: Guid) | |
{ } | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<form [formGroup]="createAccountForm" novalidate (ngSubmit)="onSubmit(createAccountForm)" > | |
<div> | |
<input type="email" | |
formControlName="emailAddress" | |
placeholder="Email Address" /> | |
</div> | |
<div> | |
<input type="accountName" | |
formControlName="accountName" | |
placeholder="Provider Name" /> | |
</div> | |
<div> | |
<input type="password" | |
formControlName="password" | |
placeholder="Password" /> | |
</div> | |
<div> | |
<input type="password" | |
formControlName="passwordConfirmation" | |
placeholder="Confirm Password" /> | |
</div> | |
<div> | |
<button type="submit" [disabled]="createAccountForm.invalid">Create</button> | |
<button type="button">Cancel</button> | |
</div> | |
</form> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Component, OnInit } from '@angular/core'; | |
import { FormGroup } from '@angular/forms'; | |
import { Authentication } from '../authentication.interface'; | |
import { AuthenticationService } from './authentication.service'; | |
import { AccountFormFactory } from './account-form.factory'; | |
@Component({ | |
selector: 'app-create-account', | |
templateUrl: './create-account.component.html' | |
}) | |
export class CreateAccountComponent { | |
private authService: AuthenticationService; | |
public createAccountForm: FormGroup; | |
constructor(formFactory: AccountFormFactory, private authenticationService: AuthenticationService) { | |
this.authService = authenticationService; | |
// Create the FormGroup our template will bind to | |
this.createAccountForm = formFactory.createAccountForm(); | |
} | |
onSubmit({ value, valid }: { value: Authentication, valid: boolean }) { | |
this.authService.createAccount(value); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<form [formGroup]="signInForm" novalidate (ngSubmit)="onSubmit(signInForm)"> | |
<div> | |
<input type="email" | |
formControlName="emailAddress" | |
placeholder="Email Address" /> | |
</div> | |
<div> | |
<input type="accountName" | |
formControlName="accountName" | |
placeholder="Provider Name" /> | |
</div> | |
<div> | |
<input type="password" | |
formControlName="password" | |
placeholder="Password" /> | |
</div> | |
<div> | |
<button type="submit" [disabled]="signInForm.invalid">Sign-in</button> | |
<button type="button">Cancel</button> | |
</div> | |
</form> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Angular dependencies | |
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; | |
import { ComponentFixture, TestBed } from '@angular/core/testing'; | |
import { By } from '@angular/platform-browser'; | |
import { DebugElement } from '@angular/core'; | |
// Custom dependencies | |
import { Authentication } from '../authentication.interface'; | |
import { SignInComponent } from './sign-in.component'; | |
import { AccountFormFactory } from './account-form.factory'; | |
import { AuthenticationService } from './authentication.service'; | |
// AuthenticationService Mock | |
let authenticationService = { | |
createAccount(user: Authentication): void {} | |
} | |
// Key/Value Pair lookups | |
let emailControlName: string = 'emailAddress'; | |
let passwordControlName: string = 'password'; | |
let accountNameControlName: string = 'accountName'; | |
describe('Sign-In Component', () => { | |
// References for our component and the elements we are testing. | |
let component: SignInComponent; | |
let fixture: ComponentFixture<SignInComponent>; | |
let debugElement: DebugElement; | |
let htmlElement: HTMLElement; | |
// Initialize each test | |
beforeEach(() => { | |
TestBed.configureTestingModule({ | |
imports: [ReactiveFormsModule, FormsModule], | |
providers: [AccountFormFactory, {provide: AuthenticationService, useValue: authenticationService } ], | |
declarations: [ SignInComponent ], | |
}); | |
fixture = TestBed.createComponent(SignInComponent); | |
component = fixture.componentInstance; | |
debugElement = fixture.debugElement; | |
htmlElement = debugElement.nativeElement; | |
}); | |
it('form invalid when empty', () => { | |
// Assert | |
expect(component.signInForm.valid).toBeFalsy(); | |
}); | |
it('form is valid when filled-out proper', () => { | |
// Arrange | |
let emailControl = component.signInForm.controls[emailControlName]; | |
let passwordControl = component.signInForm.controls[passwordControlName]; | |
let accountNameControl = component.signInForm.controls[accountNameControlName]; | |
// Act | |
emailControl.setValue('[email protected]'); | |
passwordControl.setValue('1234_Foo'); | |
accountNameControl.setValue('my account'); | |
// Assert | |
expect(component.signInForm.valid).toBeTruthy(); | |
}) | |
it('email address required', () => { | |
// Arrange | |
let emailControl = component.signInForm.controls[emailControlName]; | |
// Assert | |
expect(emailControl.valid).toBeFalsy(); | |
}); | |
it('reject invalid email address format', () => { | |
// Arrange | |
let emailControl = component.signInForm.controls[emailControlName]; | |
// Act | |
emailControl.setValue('foo-bar.com'); | |
let errors = emailControl.errors || {}; | |
// Assert | |
expect(errors['pattern']).toBeTruthy(); | |
}); | |
it('accept proper email address format', () => { | |
// Arrange | |
let emailControl = component.signInForm.controls[emailControlName]; | |
// Act | |
emailControl.setValue('[email protected]'); | |
// Assert | |
let errors = emailControl.errors || {}; | |
expect(errors['pattern']).toBeFalsy(); | |
}); | |
it('account name required', () => { | |
// Arrange | |
let accountNameControl = component.signInForm.controls[accountNameControlName]; | |
// Assert | |
expect(accountNameControl.valid).toBeFalsy(); | |
}); | |
it('account name is valid', () => { | |
// Arrange | |
let accountNameControl = component.signInForm.controls[accountNameControlName]; | |
// Act | |
accountNameControl.setValue('my account'); | |
// Assert | |
expect(accountNameControl.valid).toBeTruthy(); | |
}); | |
it('password required', () => { | |
// Arrange | |
let passwordControl = component.signInForm.controls[passwordControlName]; | |
// Assert | |
expect(passwordControl.valid).toBeFalsy(); | |
}); | |
it('password is valid', () => { | |
// Arrange | |
let passwordControl = component.signInForm.controls[passwordControlName]; | |
// Act | |
passwordControl.setValue('123_Foo'); | |
// Assert | |
expect(passwordControl.valid).toBeTruthy(); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Component, OnInit } from '@angular/core'; | |
import { FormGroup } from '@angular/forms'; | |
import { AccountFormFactory } from './account-form.factory'; | |
import { Authentication } from '../authentication.interface'; | |
import { AuthenticationService } from './authentication.service'; | |
@Component({ | |
selector: 'app-sign-in', | |
templateUrl: './sign-in.component.html' | |
}) | |
export class SignInComponent { | |
private authService: AuthenticationService; | |
public signInForm: FormGroup; | |
constructor(formFactory: AccountFormFactory, authenticationService: AuthenticationService) { | |
this.authService = authenticationService; | |
// Create the FormGroup our template will bind to | |
this.signInForm = formFactory.signInForm(); | |
} | |
onSubmit({ value, valid }: { value: Authentication, valid: boolean }) { | |
this.authService.createAccount(value); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Authentication } from './authentication.interface'; | |
export class User implements Authentication { | |
emailAddress: string; | |
password: string; | |
confirmPassword: string; | |
accountName: string; | |
providerTypeId: number; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment