From d6e7d702db4e600d2298376ba9310e985ee69079 Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 9 Mar 2019 01:42:38 +0800 Subject: User named route in dialog. --- Timeline/ClientApp/src/app/user/entities.ts | 9 +++++ .../user/user-dialog/user-dialog.component.html | 7 ++-- .../app/user/user-dialog/user-dialog.component.ts | 41 ++++++++++------------ Timeline/ClientApp/src/app/user/user-info.ts | 4 --- .../user-login-success.component.ts | 11 +++--- .../app/user/user-login/user-login.component.ts | 20 +++++++---- .../src/app/user/user-service/user.service.spec.ts | 13 ++++--- .../src/app/user/user-service/user.service.ts | 26 +++++++------- Timeline/ClientApp/src/app/user/user.module.ts | 9 ++++- 9 files changed, 81 insertions(+), 59 deletions(-) create mode 100644 Timeline/ClientApp/src/app/user/entities.ts delete mode 100644 Timeline/ClientApp/src/app/user/user-info.ts diff --git a/Timeline/ClientApp/src/app/user/entities.ts b/Timeline/ClientApp/src/app/user/entities.ts new file mode 100644 index 00000000..6d432ec6 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/entities.ts @@ -0,0 +1,9 @@ +export interface UserCredentials { + username: string; + password: string; +} + +export interface UserInfo { + username: string; + roles: string[]; +} diff --git a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.html b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.html index 50d6ba56..58dff0e4 100644 --- a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.html +++ b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.html @@ -1,5 +1,4 @@ -
- - - +
+ +
diff --git a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts index 7511de16..0edde924 100644 --- a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts +++ b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts @@ -1,43 +1,40 @@ -import { Component, OnInit } from '@angular/core'; -import { UserInfo } from '../user-info'; +import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; import { UserService } from '../user-service/user.service'; -import { LoginEvent, LoginMessage } from '../user-login/user-login.component'; +import { RouterOutlet, Router, ActivationStart } from '@angular/router'; @Component({ selector: 'app-user-dialog', templateUrl: './user-dialog.component.html', styleUrls: ['./user-dialog.component.css'] }) -export class UserDialogComponent implements OnInit { +export class UserDialogComponent implements OnInit, OnDestroy { - constructor(private userService: UserService) { } + constructor(private userService: UserService, private router: Router) { } - state: 'loading' | 'login' | 'success' = 'loading'; + @ViewChild(RouterOutlet) outlet: RouterOutlet; - loginMessage: LoginMessage; - - displayLoginSuccessMessage = false; - userInfo: UserInfo; + isLoading = true; ngOnInit() { + // this is a workaround for a bug. see https://github.com/angular/angular/issues/20694 + this.router.events.subscribe(e => { + if (e instanceof ActivationStart && e.snapshot.outlet === 'user') { + this.outlet.deactivate(); + } + }); + + this.userService.validateUserLoginState().subscribe(result => { + this.isLoading = false; if (result.state === 'success') { - this.userInfo = result.userInfo; - this.state = 'success'; + this.userService.userRouteNavigate(['success', { reason: 'already' }]); } else { - this.loginMessage = result.state; - this.state = 'login'; + this.userService.userRouteNavigate(['login', { reason: result.state }]); } }); } - login(event: LoginEvent) { - this.userService.tryLogin(event.username, event.password).subscribe(result => { - this.userInfo = result; - this.displayLoginSuccessMessage = true; - this.state = 'success'; - }, (error: Error) => { - this.loginMessage = error.message; - }); + ngOnDestroy() { + this.userService.userRouteNavigate(null); } } diff --git a/Timeline/ClientApp/src/app/user/user-info.ts b/Timeline/ClientApp/src/app/user/user-info.ts deleted file mode 100644 index 490b00ba..00000000 --- a/Timeline/ClientApp/src/app/user/user-info.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface UserInfo { - username: string; - roles: string[]; -} diff --git a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts index 99de5970..d141b3b6 100644 --- a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts +++ b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts @@ -1,5 +1,7 @@ import { Component, OnInit, Input } from '@angular/core'; -import { UserInfo } from '../user-info'; +import { UserInfo } from '../entities'; +import { UserService } from '../user-service/user.service'; +import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-user-login-success', @@ -8,15 +10,14 @@ import { UserInfo } from '../user-info'; }) export class UserLoginSuccessComponent implements OnInit { - @Input() displayLoginSuccessMessage = false; - @Input() userInfo: UserInfo; - constructor() { } + constructor(private route: ActivatedRoute, private userService: UserService) { } ngOnInit() { + this.userInfo = this.userService.userInfo; + this.displayLoginSuccessMessage = this.route.snapshot.paramMap.get('reason') === 'login'; } - } diff --git a/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts b/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts index da642cb8..971d57ce 100644 --- a/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts +++ b/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts @@ -1,5 +1,7 @@ -import { Component, Output, OnInit, EventEmitter, Input } from '@angular/core'; +import { Component, Output, OnInit, EventEmitter } from '@angular/core'; import { FormGroup, FormControl } from '@angular/forms'; +import { UserService } from '../user-service/user.service'; +import { ActivatedRoute } from '@angular/router'; export type LoginMessage = 'nologin' | 'invalidlogin' | string; @@ -13,20 +15,24 @@ export class LoginEvent { templateUrl: './user-login.component.html', styleUrls: ['./user-login.component.css'] }) -export class UserLoginComponent { +export class UserLoginComponent implements OnInit { - @Input() - message: LoginMessage; + constructor(private route: ActivatedRoute, private userService: UserService) { } - @Output() - login = new EventEmitter(); + message: string; form = new FormGroup({ username: new FormControl(''), password: new FormControl('') }); + ngOnInit() { + this.message = this.route.snapshot.paramMap.get('reason'); + } + onLoginButtonClick() { - this.login.emit(this.form.value); + this.userService.tryLogin(this.form.value).subscribe(_ => { + this.userService.userRouteNavigate(['success', { reason: 'login' }]); + }, (error: Error) => this.message = error.message); } } diff --git a/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts b/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts index 0095f031..9effe000 100644 --- a/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts +++ b/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts @@ -2,15 +2,20 @@ import { TestBed } from '@angular/core/testing'; import { HttpRequest } from '@angular/common/http'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { UserInfo } from '../user-info'; +import { UserInfo, UserCredentials } from '../entities'; import { - UserService, UserCredentials, CreateTokenResult, + UserService, CreateTokenResult, UserLoginState, TokenValidationRequest, TokenValidationResult } from './user.service'; describe('UserService', () => { const tokenCreateUrl = '/api/User/CreateToken'; + const mockUserCredentials: UserCredentials = { + username: 'user', + password: 'user' + }; + beforeEach(() => TestBed.configureTestingModule({ imports: [HttpClientTestingModule] })); @@ -35,7 +40,7 @@ describe('UserService', () => { roles: ['user', 'other'] }; - service.tryLogin('user', 'user').subscribe(result => { + service.tryLogin(mockUserCredentials).subscribe(result => { expect(result).toEqual(mockUserInfo); }); @@ -71,7 +76,7 @@ describe('UserService', () => { service = TestBed.get(UserService); httpController = TestBed.get(HttpTestingController); - service.tryLogin('user', 'user').subscribe(); // subscribe to activate login + service.tryLogin(mockUserCredentials).subscribe(); // subscribe to activate login httpController.expectOne(tokenCreateUrl).flush({ token: mockToken, diff --git a/Timeline/ClientApp/src/app/user/user-service/user.service.ts b/Timeline/ClientApp/src/app/user/user-service/user.service.ts index 009e5292..e535537d 100644 --- a/Timeline/ClientApp/src/app/user/user-service/user.service.ts +++ b/Timeline/ClientApp/src/app/user/user-service/user.service.ts @@ -3,12 +3,8 @@ import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Observable, of, throwError } from 'rxjs'; import { map, catchError, retry } from 'rxjs/operators'; -import { UserInfo } from '../user-info'; - -export interface UserCredentials { - username: string; - password: string; -} +import { UserCredentials, UserInfo } from '../entities'; +import { Router } from '@angular/router'; export interface CreateTokenResult { token: string; @@ -53,9 +49,17 @@ export class BadCredentialsException extends Error { export class UserService { private token: string; - private userInfo: UserInfo; + userInfo: UserInfo; - constructor(private httpClient: HttpClient) { } + constructor(private httpClient: HttpClient, private router: Router) { } + + userRouteNavigate(commands: any[]) { + this.router.navigate([{ + outlets: { + user: commands + } + }]); + } validateUserLoginState(): Observable { if (this.token === undefined || this.token === null) { @@ -86,14 +90,12 @@ export class UserService { ); } - tryLogin(username: string, password: string): Observable { + tryLogin(credentials: UserCredentials): Observable { if (this.token) { return throwError(new AlreadyLoginException()); } - return this.httpClient.post('/api/User/CreateToken', { - username, password - }).pipe( + return this.httpClient.post('/api/User/CreateToken', credentials).pipe( catchError((error: HttpErrorResponse) => { if (error.error instanceof ErrorEvent) { console.error('An error occurred when login: ' + error.error.message); diff --git a/Timeline/ClientApp/src/app/user/user.module.ts b/Timeline/ClientApp/src/app/user/user.module.ts index 67de90a2..1e70d33d 100644 --- a/Timeline/ClientApp/src/app/user/user.module.ts +++ b/Timeline/ClientApp/src/app/user/user.module.ts @@ -11,14 +11,21 @@ import { UserDialogComponent } from './user-dialog/user-dialog.component'; import { UserLoginComponent } from './user-login/user-login.component'; import { UserLoginSuccessComponent } from './user-login-success/user-login-success.component'; import { UtilityModule } from '../utility/utility.module'; +import { RouterModule } from '@angular/router'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @NgModule({ declarations: [UserDialogComponent, UserLoginComponent, UserLoginSuccessComponent], imports: [ - CommonModule, HttpClientModule, ReactiveFormsModule, + RouterModule.forChild([ + { path: 'login', component: UserLoginComponent, outlet: 'user' }, + { path: 'success', component: UserLoginSuccessComponent, outlet: 'user' } + ]), + CommonModule, HttpClientModule, ReactiveFormsModule, BrowserAnimationsModule, MatFormFieldModule, MatProgressSpinnerModule, MatDialogModule, MatInputModule, MatButtonModule, UtilityModule ], + exports: [RouterModule], entryComponents: [UserDialogComponent] }) export class UserModule { } -- cgit v1.2.3 From f65ac5d592e4e449dd513ad01cfd2b980324f240 Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 9 Mar 2019 20:44:39 +0800 Subject: Upgrade packages. --- Timeline/ClientApp/yarn.lock | 472 ++++++++++++++++++++----------------------- 1 file changed, 224 insertions(+), 248 deletions(-) diff --git a/Timeline/ClientApp/yarn.lock b/Timeline/ClientApp/yarn.lock index fcb3c00f..cd6328eb 100644 --- a/Timeline/ClientApp/yarn.lock +++ b/Timeline/ClientApp/yarn.lock @@ -10,12 +10,12 @@ "@angular-devkit/core" "7.2.4" rxjs "6.3.3" -"@angular-devkit/architect@0.13.3": - version "0.13.3" - resolved "http://registry.npm.taobao.org/@angular-devkit/architect/download/@angular-devkit/architect-0.13.3.tgz#28813279c546cdcb709ad55038bb2051736de668" - integrity sha1-KIEyecVGzctwmtVQOLsgUXNt5mg= +"@angular-devkit/architect@0.13.5": + version "0.13.5" + resolved "http://registry.npm.taobao.org/@angular-devkit/architect/download/@angular-devkit/architect-0.13.5.tgz#ecd41e3bc37337b9b8884cebc761a367b06cb697" + integrity sha1-7NQeO8NzN7m4iEzrx2GjZ7Bstpc= dependencies: - "@angular-devkit/core" "7.3.3" + "@angular-devkit/core" "7.3.5" rxjs "6.3.3" "@angular-devkit/build-angular@^0.12.3": @@ -102,10 +102,10 @@ rxjs "6.3.3" source-map "0.7.3" -"@angular-devkit/core@7.3.3": - version "7.3.3" - resolved "http://registry.npm.taobao.org/@angular-devkit/core/download/@angular-devkit/core-7.3.3.tgz#cd6d5a8eca25ef07b6394bc2b08133d90d08d39f" - integrity sha1-zW1ajsol7we2OUvCsIEz2Q0I058= +"@angular-devkit/core@7.3.5": + version "7.3.5" + resolved "http://registry.npm.taobao.org/@angular-devkit/core/download/@angular-devkit/core-7.3.5.tgz#2a59a913eab358e2385f52ba28132b81435e63b3" + integrity sha1-KlmpE+qzWOI4X1K6KBMrgUNeY7M= dependencies: ajv "6.9.1" chokidar "2.0.4" @@ -113,18 +113,18 @@ rxjs "6.3.3" source-map "0.7.3" -"@angular-devkit/schematics@7.3.3": - version "7.3.3" - resolved "http://registry.npm.taobao.org/@angular-devkit/schematics/download/@angular-devkit/schematics-7.3.3.tgz#80e9dc3197a3181f32edfb5c07e7ac016eace7d3" - integrity sha1-gOncMZejGB8y7ftcB+esAW6s59M= +"@angular-devkit/schematics@7.3.5": + version "7.3.5" + resolved "http://registry.npm.taobao.org/@angular-devkit/schematics/download/@angular-devkit/schematics-7.3.5.tgz#7b007f8a86dea76e93eef007d4fb6e7d8324b7bb" + integrity sha1-ewB/iobep26T7vAH1PtufYMkt7s= dependencies: - "@angular-devkit/core" "7.3.3" + "@angular-devkit/core" "7.3.5" rxjs "6.3.3" "@angular/animations@^7.2.4": - version "7.2.6" - resolved "http://registry.npm.taobao.org/@angular/animations/download/@angular/animations-7.2.6.tgz#5153d68da7f7dd08e26490f72f97679a93e78f34" - integrity sha1-UVPWjaf33QjiZJD3L5dnmpPnjzQ= + version "7.2.8" + resolved "http://registry.npm.taobao.org/@angular/animations/download/@angular/animations-7.2.8.tgz#0285364c839c660a934ab0f753ec21424bfb292e" + integrity sha1-AoU2TIOcZgqTSrD3U+whQkv7KS4= dependencies: tslib "^1.9.0" @@ -138,15 +138,15 @@ parse5 "^5.0.0" "@angular/cli@^7.3.1": - version "7.3.3" - resolved "http://registry.npm.taobao.org/@angular/cli/download/@angular/cli-7.3.3.tgz#b357000385aa6c75b001cb9fa7982ef3ce02c423" - integrity sha1-s1cAA4WqbHWwAcufp5gu884CxCM= - dependencies: - "@angular-devkit/architect" "0.13.3" - "@angular-devkit/core" "7.3.3" - "@angular-devkit/schematics" "7.3.3" - "@schematics/angular" "7.3.3" - "@schematics/update" "0.13.3" + version "7.3.5" + resolved "http://registry.npm.taobao.org/@angular/cli/download/@angular/cli-7.3.5.tgz#7aa0294410e0a9ae18b9e686803d9b0997d8c5fa" + integrity sha1-eqApRBDgqa4YueaGgD2bCZfYxfo= + dependencies: + "@angular-devkit/architect" "0.13.5" + "@angular-devkit/core" "7.3.5" + "@angular-devkit/schematics" "7.3.5" + "@schematics/angular" "7.3.5" + "@schematics/update" "0.13.5" "@yarnpkg/lockfile" "1.1.0" ini "1.3.5" inquirer "6.2.1" @@ -157,16 +157,16 @@ symbol-observable "1.2.0" "@angular/common@^7.2.4": - version "7.2.6" - resolved "http://registry.npm.taobao.org/@angular/common/download/@angular/common-7.2.6.tgz#876aaf1f9021807b306faba39334b30917f4c3a8" - integrity sha1-h2qvH5AhgHswb6ujkzSzCRf0w6g= + version "7.2.8" + resolved "http://registry.npm.taobao.org/@angular/common/download/@angular/common-7.2.8.tgz#660c816b6f08cd2919a6efb7465397e4ff14d265" + integrity sha1-ZgyBa28IzSkZpu+3RlOX5P8U0mU= dependencies: tslib "^1.9.0" "@angular/compiler-cli@^7.2.4": - version "7.2.6" - resolved "http://registry.npm.taobao.org/@angular/compiler-cli/download/@angular/compiler-cli-7.2.6.tgz#50c4d2fed21292b40b7e14d20a2133312a09d2cd" - integrity sha1-UMTS/tISkrQLfhTSCiEzMSoJ0s0= + version "7.2.8" + resolved "http://registry.npm.taobao.org/@angular/compiler-cli/download/@angular/compiler-cli-7.2.8.tgz#167d106ea002a580e1ed0651e9bcce87e4e14fb3" + integrity sha1-Fn0QbqACpYDh7QZR6bzOh+ThT7M= dependencies: canonical-path "1.0.0" chokidar "^2.1.1" @@ -181,37 +181,37 @@ yargs "9.0.1" "@angular/compiler@^7.2.4": - version "7.2.6" - resolved "http://registry.npm.taobao.org/@angular/compiler/download/@angular/compiler-7.2.6.tgz#c1047f0be52d19f39239b0f3ff9a85caf759c855" - integrity sha1-wQR/C+UtGfOSObDz/5qFyvdZyFU= + version "7.2.8" + resolved "http://registry.npm.taobao.org/@angular/compiler/download/@angular/compiler-7.2.8.tgz#9d9c1515e99914399e6915c1c90484b1d255560b" + integrity sha1-nZwVFemZFDmeaRXByQSEsdJVVgs= dependencies: tslib "^1.9.0" "@angular/core@^7.2.4": - version "7.2.6" - resolved "http://registry.npm.taobao.org/@angular/core/download/@angular/core-7.2.6.tgz#0142bba2b08d6b49811e12d04cb50586f9c5a1f0" - integrity sha1-AUK7orCNa0mBHhLQTLUFhvnFofA= + version "7.2.8" + resolved "http://registry.npm.taobao.org/@angular/core/download/@angular/core-7.2.8.tgz#6586d9b6c6321c80119b3f3e2bd0edbb32d0b649" + integrity sha1-ZYbZtsYyHIARmz8+K9DtuzLQtkk= dependencies: tslib "^1.9.0" "@angular/forms@^7.2.4": - version "7.2.6" - resolved "http://registry.npm.taobao.org/@angular/forms/download/@angular/forms-7.2.6.tgz#4db5238a5aa462927be29c90bc60ce4585ed939f" - integrity sha1-TbUjilqkYpJ74pyQvGDORYXtk58= + version "7.2.8" + resolved "http://registry.npm.taobao.org/@angular/forms/download/@angular/forms-7.2.8.tgz#adf194088495822d55dcf3e5bf69196dcf19465d" + integrity sha1-rfGUCISVgi1V3PPlv2kZbc8ZRl0= dependencies: tslib "^1.9.0" "@angular/http@^7.2.4": - version "7.2.6" - resolved "http://registry.npm.taobao.org/@angular/http/download/@angular/http-7.2.6.tgz#13cdddb561bc54d98efa5279221707eca064f5ae" - integrity sha1-E83dtWG8VNmO+lJ5IhcH7KBk9a4= + version "7.2.8" + resolved "http://registry.npm.taobao.org/@angular/http/download/@angular/http-7.2.8.tgz#bba2ca9c80d3475c4e3fb47427fdfd25f6969241" + integrity sha1-u6LKnIDTR1xOP7R0J/39JfaWkkE= dependencies: tslib "^1.9.0" "@angular/language-service@^7.2.4": - version "7.2.6" - resolved "http://registry.npm.taobao.org/@angular/language-service/download/@angular/language-service-7.2.6.tgz#cc4fc6427667c57f3745de889d30bd751bb4c325" - integrity sha1-zE/GQnZnxX83Rd6InTC9dRu0wyU= + version "7.2.8" + resolved "http://registry.npm.taobao.org/@angular/language-service/download/@angular/language-service-7.2.8.tgz#f08d50cd24c294441d8ebf0d4050c439f6ae86dc" + integrity sha1-8I1QzSTClEQdjr8NQFDEOfauhtw= "@angular/material@^7.3.2": version "7.3.3" @@ -221,32 +221,32 @@ tslib "^1.7.1" "@angular/platform-browser-dynamic@^7.2.4": - version "7.2.6" - resolved "http://registry.npm.taobao.org/@angular/platform-browser-dynamic/download/@angular/platform-browser-dynamic-7.2.6.tgz#3a1cc79b40a1ab487cf035ca679f21b406677c09" - integrity sha1-OhzHm0Chq0h88DXKZ58htAZnfAk= + version "7.2.8" + resolved "http://registry.npm.taobao.org/@angular/platform-browser-dynamic/download/@angular/platform-browser-dynamic-7.2.8.tgz#e82768900cedfa75bf453263f931a9f90f7aaab2" + integrity sha1-6CdokAzt+nW/RTJj+TGp+Q96qrI= dependencies: tslib "^1.9.0" "@angular/platform-browser@^7.2.4": - version "7.2.6" - resolved "http://registry.npm.taobao.org/@angular/platform-browser/download/@angular/platform-browser-7.2.6.tgz#7587dfc60c3af77943c0343658e770f706017f7a" - integrity sha1-dYffxgw693lDwDQ2WOdw9wYBf3o= + version "7.2.8" + resolved "http://registry.npm.taobao.org/@angular/platform-browser/download/@angular/platform-browser-7.2.8.tgz#11096727b99bf3d7fd82a00a3a468b933c9713bd" + integrity sha1-EQlnJ7mb89f9gqAKOkaLkzyXE70= dependencies: tslib "^1.9.0" "@angular/platform-server@^7.2.4": - version "7.2.6" - resolved "http://registry.npm.taobao.org/@angular/platform-server/download/@angular/platform-server-7.2.6.tgz#5fa02d8ae6723da234230292f61f33fb3e80a325" - integrity sha1-X6AtiuZyPaI0IwKS9h8z+z6AoyU= + version "7.2.8" + resolved "http://registry.npm.taobao.org/@angular/platform-server/download/@angular/platform-server-7.2.8.tgz#b6a83bbe92613a689989524cbff5613d33bd06b1" + integrity sha1-tqg7vpJhOmiZiVJMv/VhPTO9BrE= dependencies: domino "^2.1.0" tslib "^1.9.0" xhr2 "^0.1.4" "@angular/router@^7.2.4": - version "7.2.6" - resolved "http://registry.npm.taobao.org/@angular/router/download/@angular/router-7.2.6.tgz#d469384922ccc1c5d7e9d32d40e8855e67833149" - integrity sha1-1Gk4SSLMwcXX6dMtQOiFXmeDMUk= + version "7.2.8" + resolved "http://registry.npm.taobao.org/@angular/router/download/@angular/router-7.2.8.tgz#3ae38abd95cf045f5abd988e039b5253c979a094" + integrity sha1-OuOKvZXPBF9avZiOA5tSU8l5oJQ= dependencies: tslib "^1.9.0" @@ -257,12 +257,12 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/generator@^7.0.0", "@babel/generator@^7.2.2": - version "7.3.3" - resolved "http://registry.npm.taobao.org/@babel/generator/download/@babel/generator-7.3.3.tgz#185962ade59a52e00ca2bdfcfd1d58e528d4e39e" - integrity sha1-GFlireWaUuAMor38/R1Y5SjU454= +"@babel/generator@^7.0.0", "@babel/generator@^7.3.4": + version "7.3.4" + resolved "http://registry.npm.taobao.org/@babel/generator/download/@babel/generator-7.3.4.tgz#9aa48c1989257877a9d971296e5b73bfe72e446e" + integrity sha1-mqSMGYkleHep2XEpbltzv+cuRG4= dependencies: - "@babel/types" "^7.3.3" + "@babel/types" "^7.3.4" jsesc "^2.5.1" lodash "^4.17.11" source-map "^0.5.0" @@ -300,10 +300,10 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.2.2", "@babel/parser@^7.2.3": - version "7.3.3" - resolved "http://registry.npm.taobao.org/@babel/parser/download/@babel/parser-7.3.3.tgz#092d450db02bdb6ccb1ca8ffd47d8774a91aef87" - integrity sha1-CS1FDbAr22zLHKj/1H2HdKka74c= +"@babel/parser@^7.0.0", "@babel/parser@^7.2.2", "@babel/parser@^7.3.4": + version "7.3.4" + resolved "http://registry.npm.taobao.org/@babel/parser/download/@babel/parser-7.3.4.tgz#a43357e4bbf4b92a437fb9e465c192848287f27c" + integrity sha1-pDNX5Lv0uSpDf7nkZcGShIKH8nw= "@babel/template@^7.0.0", "@babel/template@^7.1.0": version "7.2.2" @@ -315,24 +315,24 @@ "@babel/types" "^7.2.2" "@babel/traverse@^7.0.0": - version "7.2.3" - resolved "http://registry.npm.taobao.org/@babel/traverse/download/@babel/traverse-7.2.3.tgz#7ff50cefa9c7c0bd2d81231fdac122f3957748d8" - integrity sha1-f/UM76nHwL0tgSMf2sEi85V3SNg= + version "7.3.4" + resolved "http://registry.npm.taobao.org/@babel/traverse/download/@babel/traverse-7.3.4.tgz#1330aab72234f8dea091b08c4f8b9d05c7119e06" + integrity sha1-EzCqtyI0+N6gkbCMT4udBccRngY= dependencies: "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.2.2" + "@babel/generator" "^7.3.4" "@babel/helper-function-name" "^7.1.0" "@babel/helper-split-export-declaration" "^7.0.0" - "@babel/parser" "^7.2.3" - "@babel/types" "^7.2.2" + "@babel/parser" "^7.3.4" + "@babel/types" "^7.3.4" debug "^4.1.0" globals "^11.1.0" - lodash "^4.17.10" + lodash "^4.17.11" -"@babel/types@^7.0.0", "@babel/types@^7.2.2", "@babel/types@^7.3.3": - version "7.3.3" - resolved "http://registry.npm.taobao.org/@babel/types/download/@babel/types-7.3.3.tgz#6c44d1cdac2a7625b624216657d5bc6c107ab436" - integrity sha1-bETRzawqdiW2JCFmV9W8bBB6tDY= +"@babel/types@^7.0.0", "@babel/types@^7.2.2", "@babel/types@^7.3.4": + version "7.3.4" + resolved "http://registry.npm.taobao.org/@babel/types/download/@babel/types-7.3.4.tgz#bf482eaeaffb367a28abbf9357a94963235d90ed" + integrity sha1-v0gurq/7Nnooq7+TV6lJYyNdkO0= dependencies: esutils "^2.0.2" lodash "^4.17.11" @@ -350,26 +350,26 @@ webpack-sources "1.2.0" "@nguniversal/module-map-ngfactory-loader@^7.1.0": - version "7.1.0" - resolved "http://registry.npm.taobao.org/@nguniversal/module-map-ngfactory-loader/download/@nguniversal/module-map-ngfactory-loader-7.1.0.tgz#70ea905c1b32c2edc484cb77aa7a3f3208069966" - integrity sha1-cOqQXBsywu3EhMt3qno/MggGmWY= + version "7.1.1" + resolved "http://registry.npm.taobao.org/@nguniversal/module-map-ngfactory-loader/download/@nguniversal/module-map-ngfactory-loader-7.1.1.tgz#488a6f8c5890d44b7ad8468180919644a4d1a1ae" + integrity sha1-SIpvjFiQ1Et62EaBgJGWRKTRoa4= -"@schematics/angular@7.3.3": - version "7.3.3" - resolved "http://registry.npm.taobao.org/@schematics/angular/download/@schematics/angular-7.3.3.tgz#aaa63331365bf67b1b908cc18cfc5d7097ec8377" - integrity sha1-qqYzMTZb9nsbkIzBjPxdcJfsg3c= +"@schematics/angular@7.3.5": + version "7.3.5" + resolved "http://registry.npm.taobao.org/@schematics/angular/download/@schematics/angular-7.3.5.tgz#7af1cd446b051b2be3fbe59cb4ba140ec06e2d87" + integrity sha1-evHNRGsFGyvj++WctLoUDsBuLYc= dependencies: - "@angular-devkit/core" "7.3.3" - "@angular-devkit/schematics" "7.3.3" + "@angular-devkit/core" "7.3.5" + "@angular-devkit/schematics" "7.3.5" typescript "3.2.4" -"@schematics/update@0.13.3": - version "0.13.3" - resolved "http://registry.npm.taobao.org/@schematics/update/download/@schematics/update-0.13.3.tgz#7c325b1f723e538ed932b3e344a4a51ea123ffb7" - integrity sha1-fDJbH3I+U47ZMrPjRKSlHqEj/7c= +"@schematics/update@0.13.5": + version "0.13.5" + resolved "http://registry.npm.taobao.org/@schematics/update/download/@schematics/update-0.13.5.tgz#6019accecd2bca5efc931cc9832ecb234009ceb2" + integrity sha1-YBmszs0ryl78kxzJgy7LI0AJzrI= dependencies: - "@angular-devkit/core" "7.3.3" - "@angular-devkit/schematics" "7.3.3" + "@angular-devkit/core" "7.3.5" + "@angular-devkit/schematics" "7.3.5" "@yarnpkg/lockfile" "1.1.0" ini "1.3.5" pacote "9.4.0" @@ -390,14 +390,14 @@ "@types/jasmine" "*" "@types/node@*": - version "11.9.4" - resolved "http://registry.npm.taobao.org/@types/node/download/@types/node-11.9.4.tgz#ceb0048a546db453f6248f2d1d95e937a6f00a14" - integrity sha1-zrAEilRttFP2JI8tHZXpN6bwChQ= + version "11.11.0" + resolved "http://registry.npm.taobao.org/@types/node/download/@types/node-11.11.0.tgz#070e9ce7c90e727aca0e0c14e470f9a93ffe9390" + integrity sha1-Bw6c58kOcnrKDgwU5HD5qT/+k5A= "@types/node@^10.12.19": - version "10.12.26" - resolved "http://registry.npm.taobao.org/@types/node/download/@types/node-10.12.26.tgz#2dec19f1f7981c95cb54bab8f618ecb5dc983d0e" - integrity sha1-LewZ8feYHJXLVLq49hjstdyYPQ4= + version "10.12.30" + resolved "http://registry.npm.taobao.org/@types/node/download/@types/node-10.12.30.tgz#4c2b4f0015f214f8158a347350481322b3b29b2f" + integrity sha1-TCtPABXyFPgVijRzUEgTIrOymy8= "@types/q@^0.0.32": version "0.0.32" @@ -629,7 +629,7 @@ after@0.8.2: resolved "http://registry.npm.taobao.org/after/download/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= -agent-base@4, agent-base@^4.1.0, agent-base@~4.2.0: +agent-base@4, agent-base@^4.1.0, agent-base@~4.2.1: version "4.2.1" resolved "http://registry.npm.taobao.org/agent-base/download/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" integrity sha1-2J5ZmfeXh1Z0wH2H8mD8Qeg+jKk= @@ -663,7 +663,7 @@ ajv@6.6.2: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@6.9.1, ajv@^6.1.0, ajv@^6.5.5: +ajv@6.9.1: version "6.9.1" resolved "http://registry.npm.taobao.org/ajv/download/ajv-6.9.1.tgz#a4d3683d74abc5670e75f0b16520f70a20ea8dc1" integrity sha1-pNNoPXSrxWcOdfCxZSD3CiDqjcE= @@ -683,15 +683,25 @@ ajv@^5.0.0: fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.3.0" +ajv@^6.1.0, ajv@^6.5.5: + version "6.10.0" + resolved "http://registry.npm.taobao.org/ajv/download/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1" + integrity sha1-kNDVRDnaWHzX6EO/twRfUL0ivfE= + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + amdefine@>=0.0.4: version "1.0.1" resolved "http://registry.npm.taobao.org/amdefine/download/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= ansi-colors@^3.0.0: - version "3.2.3" - resolved "http://registry.npm.taobao.org/ansi-colors/download/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813" - integrity sha1-V9NbhoboUeLMBMQD8cACA5dqGBM= + version "3.2.4" + resolved "http://registry.npm.taobao.org/ansi-colors/download/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" + integrity sha1-46PaS/uubIapwoViXeEkojQCb78= ansi-escapes@^3.0.0: version "3.2.0" @@ -713,10 +723,10 @@ ansi-regex@^3.0.0: resolved "http://registry.npm.taobao.org/ansi-regex/download/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= -ansi-regex@^4.0.0: - version "4.0.0" - resolved "http://registry.npm.taobao.org/ansi-regex/download/ansi-regex-4.0.0.tgz#70de791edf021404c3fd615aa89118ae0432e5a9" - integrity sha1-cN55Ht8CFATD/WFaqJEYrgQy5ak= +ansi-regex@^4.1.0: + version "4.1.0" + resolved "http://registry.npm.taobao.org/ansi-regex/download/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha1-i5+PCM8ay4Q3Vqg5yox+MWjFGZc= ansi-styles@^2.2.1: version "2.2.1" @@ -800,11 +810,6 @@ array-flatten@^2.1.0: resolved "http://registry.npm.taobao.org/array-flatten/download/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" integrity sha1-JO+AoowaiTYX4hSbDG0NeIKTsJk= -array-slice@^0.2.3: - version "0.2.3" - resolved "http://registry.npm.taobao.org/array-slice/download/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5" - integrity sha1-3Tz7gO15c6dRF82sabC5nshhhvU= - array-union@^1.0.1: version "1.0.2" resolved "http://registry.npm.taobao.org/array-union/download/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" @@ -817,11 +822,6 @@ array-uniq@^1.0.1: resolved "http://registry.npm.taobao.org/array-uniq/download/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= -array-unique@^0.2.1: - version "0.2.1" - resolved "http://registry.npm.taobao.org/array-unique/download/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" - integrity sha1-odl8yvy8JiXMcPrc6zalDFiwGlM= - array-unique@^0.3.2: version "0.3.2" resolved "http://registry.npm.taobao.org/array-unique/download/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" @@ -1157,13 +1157,6 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^0.1.2: - version "0.1.5" - resolved "http://registry.npm.taobao.org/braces/download/braces-0.1.5.tgz#c085711085291d8b75fdd74eab0f8597280711e6" - integrity sha1-wIVxEIUpHYt1/ddOqw+FlygHEeY= - dependencies: - expand-range "^0.1.0" - braces@^2.3.0, braces@^2.3.1, braces@^2.3.2: version "2.3.2" resolved "http://registry.npm.taobao.org/braces/download/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" @@ -1245,13 +1238,13 @@ browserify-zlib@^0.2.0: pako "~1.0.5" browserslist@^4.3.6: - version "4.4.1" - resolved "http://registry.npm.taobao.org/browserslist/download/browserslist-4.4.1.tgz#42e828954b6b29a7a53e352277be429478a69062" - integrity sha1-QugolUtrKaelPjUid75ClHimkGI= + version "4.4.2" + resolved "http://registry.npm.taobao.org/browserslist/download/browserslist-4.4.2.tgz#6ea8a74d6464bb0bd549105f659b41197d8f0ba2" + integrity sha1-bqinTWRkuwvVSRBfZZtBGX2PC6I= dependencies: - caniuse-lite "^1.0.30000929" - electron-to-chromium "^1.3.103" - node-releases "^1.1.3" + caniuse-lite "^1.0.30000939" + electron-to-chromium "^1.3.113" + node-releases "^1.1.8" browserstack@^1.5.1: version "1.5.2" @@ -1404,10 +1397,10 @@ camelcase@^4.1.0: resolved "http://registry.npm.taobao.org/camelcase/download/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= -caniuse-lite@^1.0.30000921, caniuse-lite@^1.0.30000929: - version "1.0.30000938" - resolved "http://registry.npm.taobao.org/caniuse-lite/download/caniuse-lite-1.0.30000938.tgz#b64bf1427438df40183fce910fe24e34feda7a3f" - integrity sha1-tkvxQnQ430AYP86RD+JONP7aej8= +caniuse-lite@^1.0.30000921, caniuse-lite@^1.0.30000939: + version "1.0.30000942" + resolved "http://registry.npm.taobao.org/caniuse-lite/download/caniuse-lite-1.0.30000942.tgz#454139b28274bce70bfe1d50c30970df7430c6e4" + integrity sha1-RUE5soJ0vOcL/h1Qwwlw33QwxuQ= canonical-path@1.0.0: version "1.0.0" @@ -1508,11 +1501,6 @@ circular-dependency-plugin@5.0.2: resolved "http://registry.npm.taobao.org/circular-dependency-plugin/download/circular-dependency-plugin-5.0.2.tgz#da168c0b37e7b43563fb9f912c1c007c213389ef" integrity sha1-2haMCzfntDVj+5+RLBwAfCEzie8= -circular-json@^0.5.5: - version "0.5.9" - resolved "http://registry.npm.taobao.org/circular-json/download/circular-json-0.5.9.tgz#932763ae88f4f7dead7a0d09c8a51a4743a53b1d" - integrity sha1-kydjroj0996teg0JyKUaR0OlOx0= - class-utils@^0.3.5: version "0.3.6" resolved "http://registry.npm.taobao.org/class-utils/download/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" @@ -1627,13 +1615,6 @@ colors@^1.1.0: resolved "http://registry.npm.taobao.org/colors/download/colors-1.3.3.tgz#39e005d546afe01e01f9c4ca8fa50f686a01205d" integrity sha1-OeAF1Uav4B4B+cTKj6UPaGoBIF0= -combine-lists@^1.0.0: - version "1.0.1" - resolved "http://registry.npm.taobao.org/combine-lists/download/combine-lists-1.0.1.tgz#458c07e09e0d900fc28b70a3fec2dacd1d2cb7f6" - integrity sha1-RYwH4J4NkA/Ci3Cj/sLazR0st/Y= - dependencies: - lodash "^4.5.0" - combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.7" resolved "http://registry.npm.taobao.org/combined-stream/download/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" @@ -1947,10 +1928,10 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -date-format@^1.2.0: - version "1.2.0" - resolved "http://registry.npm.taobao.org/date-format/download/date-format-1.2.0.tgz#615e828e233dd1ab9bb9ae0950e0ceccfa6ecad8" - integrity sha1-YV6CjiM90aubua4JUODOzPpuytg= +date-format@^2.0.0: + version "2.0.0" + resolved "http://registry.npm.taobao.org/date-format/download/date-format-2.0.0.tgz#7cf7b172f1ec564f0003b39ea302c5498fb98c8f" + integrity sha1-fPexcvHsVk8AA7OeowLFSY+5jI8= date-now@^0.1.4: version "0.1.4" @@ -2205,9 +2186,9 @@ domain-task@^3.0.0: isomorphic-fetch "^2.2.1" domino@^2.1.0: - version "2.1.2" - resolved "http://registry.npm.taobao.org/domino/download/domino-2.1.2.tgz#70e8367839ee8ad8bde3aeb4857cf3d93bd98b85" - integrity sha1-cOg2eDnuiti94660hXzz2TvZi4U= + version "2.1.3" + resolved "http://registry.npm.taobao.org/domino/download/domino-2.1.3.tgz#0ca1ad02cbd316ebe2e99e0ac9fb0010407d4601" + integrity sha1-DKGtAsvTFuvi6Z4KyfsAEEB9RgE= duplexify@^3.4.2, duplexify@^3.6.0: version "3.7.1" @@ -2232,7 +2213,7 @@ ee-first@1.1.1: resolved "http://registry.npm.taobao.org/ee-first/download/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.3.103: +electron-to-chromium@^1.3.113: version "1.3.113" resolved "http://registry.npm.taobao.org/electron-to-chromium/download/electron-to-chromium-1.3.113.tgz#b1ccf619df7295aea17bc6951dc689632629e4a9" integrity sha1-scz2Gd9yla6he8aVHcaJYyYp5Kk= @@ -2382,9 +2363,9 @@ escodegen@1.8.x: source-map "~0.2.0" eslint-scope@^4.0.0: - version "4.0.0" - resolved "http://registry.npm.taobao.org/eslint-scope/download/eslint-scope-4.0.0.tgz#50bf3071e9338bcdc43331794a0cb533f0136172" - integrity sha1-UL8wcekzi83EMzF5Sgy1M/ATYXI= + version "4.0.2" + resolved "http://registry.npm.taobao.org/eslint-scope/download/eslint-scope-4.0.2.tgz#5f10cd6cabb1965bf479fa65745673439e21cb0e" + integrity sha1-XxDNbKuxllv0efpldFZzQ54hyw4= dependencies: esrecurse "^4.1.0" estraverse "^4.1.1" @@ -2495,15 +2476,6 @@ exit@^0.1.2: resolved "http://registry.npm.taobao.org/exit/download/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= -expand-braces@^0.1.1: - version "0.1.2" - resolved "http://registry.npm.taobao.org/expand-braces/download/expand-braces-0.1.2.tgz#488b1d1d2451cb3d3a6b192cfc030f44c5855fea" - integrity sha1-SIsdHSRRyz06axks/AMPRMWFX+o= - dependencies: - array-slice "^0.2.3" - array-unique "^0.2.1" - braces "^0.1.2" - expand-brackets@^2.1.4: version "2.1.4" resolved "http://registry.npm.taobao.org/expand-brackets/download/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" @@ -2517,14 +2489,6 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" -expand-range@^0.1.0: - version "0.1.1" - resolved "http://registry.npm.taobao.org/expand-range/download/expand-range-0.1.1.tgz#4cb8eda0993ca56fa4f41fc42f3cbb4ccadff044" - integrity sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ= - dependencies: - is-number "^0.1.1" - repeat-string "^0.2.2" - express@^4.16.2: version "4.16.4" resolved "http://registry.npm.taobao.org/express/download/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e" @@ -2840,6 +2804,15 @@ fs-access@^1.0.0: dependencies: null-check "^1.0.0" +fs-extra@^7.0.0: + version "7.0.1" + resolved "http://registry.npm.taobao.org/fs-extra/download/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" + integrity sha1-TxicRKoSO4lfcigE9V6iPq3DSOk= + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-minipass@^1.2.5: version "1.2.5" resolved "http://registry.npm.taobao.org/fs-minipass/download/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" @@ -3037,7 +3010,7 @@ globule@^1.0.0: lodash "~4.17.10" minimatch "~3.0.2" -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6: version "4.1.15" resolved "http://registry.npm.taobao.org/graceful-fs/download/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" integrity sha1-/7cD4QZuig7qpMi4C6klPu77+wA= @@ -3567,11 +3540,6 @@ is-glob@^4.0.0: dependencies: is-extglob "^2.1.1" -is-number@^0.1.1: - version "0.1.1" - resolved "http://registry.npm.taobao.org/is-number/download/is-number-0.1.1.tgz#69a7af116963d47206ec9bd9b48a14216f1e3806" - integrity sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY= - is-number@^3.0.0: version "3.0.0" resolved "http://registry.npm.taobao.org/is-number/download/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -3848,9 +3816,9 @@ js-tokens@^3.0.2: integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= js-yaml@3.x, js-yaml@^3.12.0, js-yaml@^3.7.0, js-yaml@^3.9.0: - version "3.12.1" - resolved "http://registry.npm.taobao.org/js-yaml/download/js-yaml-3.12.1.tgz#295c8632a18a23e054cf5c9d3cecafe678167600" - integrity sha1-KVyGMqGKI+BUz1ydPOyv5ngWdgA= + version "3.12.2" + resolved "http://registry.npm.taobao.org/js-yaml/download/js-yaml-3.12.2.tgz#ef1d067c5a9d9cb65bd72f285b5d8105c77f14fc" + integrity sha1-7x0GfFqdnLZb1y8oW12BBcd/FPw= dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -3917,6 +3885,13 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" +jsonfile@^4.0.0: + version "4.0.0" + resolved "http://registry.npm.taobao.org/jsonfile/download/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + optionalDependencies: + graceful-fs "^4.1.6" + jsonparse@^1.2.0: version "1.3.1" resolved "http://registry.npm.taobao.org/jsonparse/download/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" @@ -3986,27 +3961,26 @@ karma-source-map-support@1.3.0: source-map-support "^0.5.5" karma@^4.0.0: - version "4.0.0" - resolved "http://registry.npm.taobao.org/karma/download/karma-4.0.0.tgz#f28e38a2b66243fde3f98e12a8dcaa2c6ff8ca9c" - integrity sha1-8o44orZiQ/3j+Y4SqNyqLG/4ypw= + version "4.0.1" + resolved "http://registry.npm.taobao.org/karma/download/karma-4.0.1.tgz#2581d6caa0d4cd28b65131561b47bad6d5478773" + integrity sha1-JYHWyqDUzSi2UTFWG0e61tVHh3M= dependencies: bluebird "^3.3.0" body-parser "^1.16.1" + braces "^2.3.2" chokidar "^2.0.3" colors "^1.1.0" - combine-lists "^1.0.0" connect "^3.6.0" core-js "^2.2.0" di "^0.0.1" dom-serialize "^2.2.0" - expand-braces "^0.1.1" flatted "^2.0.0" glob "^7.1.1" graceful-fs "^4.1.2" http-proxy "^1.13.0" isbinaryfile "^3.0.0" - lodash "^4.17.5" - log4js "^3.0.0" + lodash "^4.17.11" + log4js "^4.0.0" mime "^2.3.1" minimatch "^3.0.2" optimist "^0.6.1" @@ -4195,21 +4169,21 @@ lodash.tail@^4.1.1: resolved "http://registry.npm.taobao.org/lodash.tail/download/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" integrity sha1-0jM6NtnncXyK0vfKyv7HwytERmQ= -lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.5.0, lodash@~4.17.10: +lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.10: version "4.17.11" resolved "http://registry.npm.taobao.org/lodash/download/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" integrity sha1-s56mIp72B+zYniyN8SU2iRysm40= -log4js@^3.0.0: - version "3.0.6" - resolved "http://registry.npm.taobao.org/log4js/download/log4js-3.0.6.tgz#e6caced94967eeeb9ce399f9f8682a4b2b28c8ff" - integrity sha1-5srO2Uln7uuc45n5+GgqSysoyP8= +log4js@^4.0.0: + version "4.0.2" + resolved "http://registry.npm.taobao.org/log4js/download/log4js-4.0.2.tgz#0c73e623ca4448669653eb0e9f629beacc7fbbe3" + integrity sha1-DHPmI8pESGaWU+sOn2Kb6sx/u+M= dependencies: - circular-json "^0.5.5" - date-format "^1.2.0" + date-format "^2.0.0" debug "^3.1.0" + flatted "^2.0.0" rfdc "^1.1.2" - streamroller "0.7.0" + streamroller "^1.0.1" loglevel@^1.4.1: version "1.6.1" @@ -4711,10 +4685,10 @@ node-pre-gyp@^0.10.0: semver "^5.3.0" tar "^4" -node-releases@^1.1.3: - version "1.1.8" - resolved "http://registry.npm.taobao.org/node-releases/download/node-releases-1.1.8.tgz#32a63fff63c5e51b7e0f540ac95947d220fc6862" - integrity sha1-MqY//2PF5Rt+D1QKyVlH0iD8aGI= +node-releases@^1.1.8: + version "1.1.10" + resolved "http://registry.npm.taobao.org/node-releases/download/node-releases-1.1.10.tgz#5dbeb6bc7f4e9c85b899e2e7adcc0635c9b2adf7" + integrity sha1-Xb62vH9OnIW4meLnrcwGNcmyrfc= dependencies: semver "^5.3.0" @@ -5060,9 +5034,9 @@ p-limit@^1.0.0, p-limit@^1.1.0: p-try "^1.0.0" p-limit@^2.0.0: - version "2.1.0" - resolved "http://registry.npm.taobao.org/p-limit/download/p-limit-2.1.0.tgz#1d5a0d20fb12707c758a655f6bbc4386b5930d68" - integrity sha1-HVoNIPsScHx1imVfa7xDhrWTDWg= + version "2.2.0" + resolved "http://registry.npm.taobao.org/p-limit/download/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2" + integrity sha1-QXyZQeYCepq8ulCS3SkE4lW1+8I= dependencies: p-try "^2.0.0" @@ -5129,9 +5103,9 @@ pacote@9.4.0: which "^1.3.1" pako@~1.0.2, pako@~1.0.5: - version "1.0.8" - resolved "http://registry.npm.taobao.org/pako/download/pako-1.0.8.tgz#6844890aab9c635af868ad5fecc62e8acbba3ea4" - integrity sha1-aESJCqucY1r4aK1f7MYuisu6PqQ= + version "1.0.10" + resolved "http://registry.npm.taobao.org/pako/download/pako-1.0.10.tgz#4328badb5086a426aa90f541977d4955da5c9732" + integrity sha1-Qyi621CGpCaqkPVBl31JVdpclzI= parallel-transform@^1.1.0: version "1.1.0" @@ -5658,7 +5632,7 @@ read-pkg@^2.0.0: normalize-package-data "^2.3.2" path-type "^2.0.0" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.6" resolved "http://registry.npm.taobao.org/readable-stream/download/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" integrity sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8= @@ -5672,9 +5646,9 @@ read-pkg@^2.0.0: util-deprecate "~1.0.1" readable-stream@^3.0.6: - version "3.1.1" - resolved "http://registry.npm.taobao.org/readable-stream/download/readable-stream-3.1.1.tgz#ed6bbc6c5ba58b090039ff18ce670515795aeb06" - integrity sha1-7Wu8bFuliwkAOf8YzmcFFXla6wY= + version "3.2.0" + resolved "http://registry.npm.taobao.org/readable-stream/download/readable-stream-3.2.0.tgz#de17f229864c120a9f56945756e4f32c4045245d" + integrity sha1-3hfyKYZMEgqfVpRXVuTzLEBFJF0= dependencies: inherits "^2.0.3" string_decoder "^1.1.1" @@ -5758,11 +5732,6 @@ repeat-element@^1.1.2: resolved "http://registry.npm.taobao.org/repeat-element/download/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" integrity sha1-eC4NglwMWjuzlzH4Tv7mt0Lmsc4= -repeat-string@^0.2.2: - version "0.2.2" - resolved "http://registry.npm.taobao.org/repeat-string/download/repeat-string-0.2.2.tgz#c7a8d3236068362059a7e4651fc6884e8b1fb4ae" - integrity sha1-x6jTI2BoNiBZp+RlH8aITosftK4= - repeat-string@^1.6.1: version "1.6.1" resolved "http://registry.npm.taobao.org/repeat-string/download/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" @@ -6292,17 +6261,17 @@ sockjs@0.3.19: uuid "^3.0.1" socks-proxy-agent@^4.0.0: - version "4.0.1" - resolved "http://registry.npm.taobao.org/socks-proxy-agent/download/socks-proxy-agent-4.0.1.tgz#5936bf8b707a993079c6f37db2091821bffa6473" - integrity sha1-WTa/i3B6mTB5xvN9sgkYIb/6ZHM= + version "4.0.2" + resolved "http://registry.npm.taobao.org/socks-proxy-agent/download/socks-proxy-agent-4.0.2.tgz#3c8991f3145b2799e70e11bd5fbc8b1963116386" + integrity sha1-PImR8xRbJ5nnDhG9X7yLGWMRY4Y= dependencies: - agent-base "~4.2.0" - socks "~2.2.0" + agent-base "~4.2.1" + socks "~2.3.2" -socks@~2.2.0: - version "2.2.3" - resolved "http://registry.npm.taobao.org/socks/download/socks-2.2.3.tgz#7399ce11e19b2a997153c983a9ccb6306721f2dc" - integrity sha1-c5nOEeGbKplxU8mDqcy2MGch8tw= +socks@~2.3.2: + version "2.3.2" + resolved "http://registry.npm.taobao.org/socks/download/socks-2.3.2.tgz#ade388e9e6d87fdb11649c15746c578922a5883e" + integrity sha1-reOI6ebYf9sRZJwVdGxXiSKliD4= dependencies: ip "^1.1.5" smart-buffer "4.0.2" @@ -6581,15 +6550,16 @@ stream-shift@^1.0.0: resolved "http://registry.npm.taobao.org/stream-shift/download/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI= -streamroller@0.7.0: - version "0.7.0" - resolved "http://registry.npm.taobao.org/streamroller/download/streamroller-0.7.0.tgz#a1d1b7cf83d39afb0d63049a5acbf93493bdf64b" - integrity sha1-odG3z4PTmvsNYwSaWsv5NJO99ks= +streamroller@^1.0.1: + version "1.0.3" + resolved "http://registry.npm.taobao.org/streamroller/download/streamroller-1.0.3.tgz#cb51e7e382f799a9381a5d7490ce3053b325fba3" + integrity sha1-y1Hn44L3mak4Gl10kM4wU7Ml+6M= dependencies: - date-format "^1.2.0" + async "^2.6.1" + date-format "^2.0.0" debug "^3.1.0" - mkdirp "^0.5.1" - readable-stream "^2.3.0" + fs-extra "^7.0.0" + lodash "^4.17.10" string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" @@ -6637,11 +6607,11 @@ strip-ansi@^4.0.0: ansi-regex "^3.0.0" strip-ansi@^5.0.0: - version "5.0.0" - resolved "http://registry.npm.taobao.org/strip-ansi/download/strip-ansi-5.0.0.tgz#f78f68b5d0866c20b2c9b8c61b5298508dc8756f" - integrity sha1-949otdCGbCCyybjGG1KYUI3IdW8= + version "5.1.0" + resolved "http://registry.npm.taobao.org/strip-ansi/download/strip-ansi-5.1.0.tgz#55aaa54e33b4c0649a7338a43437b1887d153ec4" + integrity sha1-VaqlTjO0wGSaczikNDexiH0VPsQ= dependencies: - ansi-regex "^4.0.0" + ansi-regex "^4.1.0" strip-bom@^2.0.0: version "2.0.0" @@ -6774,9 +6744,9 @@ terser-webpack-plugin@1.2.1: worker-farm "^1.5.2" terser-webpack-plugin@^1.1.0: - version "1.2.2" - resolved "http://registry.npm.taobao.org/terser-webpack-plugin/download/terser-webpack-plugin-1.2.2.tgz#9bff3a891ad614855a7dde0d707f7db5a927e3d9" - integrity sha1-m/86iRrWFIVafd4NcH99takn49k= + version "1.2.3" + resolved "http://registry.npm.taobao.org/terser-webpack-plugin/download/terser-webpack-plugin-1.2.3.tgz#3f98bc902fac3e5d0de730869f50668561262ec8" + integrity sha1-P5i8kC+sPl0N5zCGn1BmhWEmLsg= dependencies: cacache "^11.0.2" find-cache-dir "^2.0.0" @@ -6930,9 +6900,9 @@ tslib@^1.7.1, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0: integrity sha1-1+TdeSRdhUKMTX5IIqeZF5VMooY= tslint@^5.12.1: - version "5.12.1" - resolved "http://registry.npm.taobao.org/tslint/download/tslint-5.12.1.tgz#8cec9d454cf8a1de9b0a26d7bdbad6de362e52c1" - integrity sha1-jOydRUz4od6bCibXvbrW3jYuUsE= + version "5.13.1" + resolved "http://registry.npm.taobao.org/tslint/download/tslint-5.13.1.tgz#fbc0541c425647a33cd9108ce4fd4cd18d7904ed" + integrity sha1-+8BUHEJWR6M82RCM5P1M0Y15BO0= dependencies: babel-code-frame "^6.22.0" builtin-modules "^1.1.1" @@ -6942,6 +6912,7 @@ tslint@^5.12.1: glob "^7.1.1" js-yaml "^3.7.0" minimatch "^3.0.4" + mkdirp "^0.5.1" resolve "^1.3.2" semver "^5.3.0" tslib "^1.8.0" @@ -7033,6 +7004,11 @@ unique-slug@^2.0.0: dependencies: imurmurhash "^0.1.4" +universalify@^0.1.0: + version "0.1.2" + resolved "http://registry.npm.taobao.org/universalify/download/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha1-tkb2m+OULavOzJ1mOcgNwQXvqmY= + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "http://registry.npm.taobao.org/unpipe/download/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -7047,9 +7023,9 @@ unset-value@^1.0.0: isobject "^3.0.0" upath@^1.0.5, upath@^1.1.0: - version "1.1.0" - resolved "http://registry.npm.taobao.org/upath/download/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd" - integrity sha1-NSVll+RqWB20eT0M5H+prr/J+r0= + version "1.1.1" + resolved "http://registry.npm.taobao.org/upath/download/upath-1.1.1.tgz#497f7c1090b0818f310bbfb06783586a68d28014" + integrity sha1-SX98EJCwgY8xC7+wZ4NYamjSgBQ= uri-js@^4.2.2: version "4.2.2" -- cgit v1.2.3 From 14a799cc17f16ab93ee652bd9a2973c60cb3697c Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 9 Mar 2019 21:36:55 +0800 Subject: Seperate internal and public user service. --- Timeline/ClientApp/src/app/app.component.ts | 9 +- .../src/app/user/internal-user-service/errors.ts | 25 +++++ .../user/internal-user-service/http-entities.ts | 17 +++ .../internal-user.service.spec.ts | 119 +++++++++++++++++++++ .../internal-user-service/internal-user.service.ts | 93 ++++++++++++++++ .../user/user-dialog/user-dialog.component.spec.ts | 22 ++-- .../app/user/user-dialog/user-dialog.component.ts | 10 +- .../user-login-success.component.ts | 9 +- .../app/user/user-login/user-login.component.ts | 7 +- .../src/app/user/user-service/user.service.spec.ts | 119 --------------------- .../src/app/user/user-service/user.service.ts | 118 -------------------- Timeline/ClientApp/src/app/user/user.service.ts | 33 ++++++ 12 files changed, 315 insertions(+), 266 deletions(-) create mode 100644 Timeline/ClientApp/src/app/user/internal-user-service/errors.ts create mode 100644 Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts create mode 100644 Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts create mode 100644 Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts delete mode 100644 Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts delete mode 100644 Timeline/ClientApp/src/app/user/user-service/user.service.ts create mode 100644 Timeline/ClientApp/src/app/user/user.service.ts diff --git a/Timeline/ClientApp/src/app/app.component.ts b/Timeline/ClientApp/src/app/app.component.ts index 0e2a9799..ee02f833 100644 --- a/Timeline/ClientApp/src/app/app.component.ts +++ b/Timeline/ClientApp/src/app/app.component.ts @@ -1,6 +1,5 @@ import { Component } from '@angular/core'; -import { MatDialog } from '@angular/material'; -import { UserDialogComponent } from './user/user-dialog/user-dialog.component'; +import { UserService } from './user/user.service'; @Component({ selector: 'app-root', @@ -9,11 +8,9 @@ import { UserDialogComponent } from './user/user-dialog/user-dialog.component'; }) export class AppComponent { - constructor(private dialog: MatDialog) { } + constructor(private userService: UserService) { } openUserDialog() { - this.dialog.open(UserDialogComponent, { - width: '300px' - }); + this.userService.openUserDialog(); } } diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/errors.ts b/Timeline/ClientApp/src/app/user/internal-user-service/errors.ts new file mode 100644 index 00000000..22e44dd6 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/internal-user-service/errors.ts @@ -0,0 +1,25 @@ +export abstract class LoginError extends Error { } + +export class BadNetworkError extends LoginError { + constructor() { + super('Network is bad.'); + } +} + +export class AlreadyLoginError extends LoginError { + constructor() { + super('Internal logical error. There is already a token saved. Please call validateUserLoginState first.'); + } +} + +export class BadCredentialsError extends LoginError { + constructor() { + super('Username or password is wrong.'); + } +} + +export class UnknownError extends LoginError { + constructor(public internalError?: any) { + super('Sorry, unknown error occured!'); + } +} diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts b/Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts new file mode 100644 index 00000000..1335b407 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts @@ -0,0 +1,17 @@ +import { UserCredentials, UserInfo } from '../entities'; + +export type CreateTokenRequest = UserCredentials; + +export interface CreateTokenResponse { + token: string; + userInfo: UserInfo; +} + +export interface ValidateTokenRequest { + token: string; +} + +export interface ValidateTokenResponse { + isValid: boolean; + userInfo?: UserInfo; +} diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts new file mode 100644 index 00000000..4a2c78f8 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts @@ -0,0 +1,119 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpRequest } from '@angular/common/http'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { UserInfo, UserCredentials } from '../entities'; +import { + InternalUserService, CreateTokenResult, + UserLoginState, TokenValidationRequest, TokenValidationResult +} from './internal-user.service'; + +describe('UserService', () => { + const tokenCreateUrl = '/api/User/CreateToken'; + + const mockUserCredentials: UserCredentials = { + username: 'user', + password: 'user' + }; + + beforeEach(() => TestBed.configureTestingModule({ + imports: [HttpClientTestingModule] + })); + + it('should be created', () => { + const service: InternalUserService = TestBed.get(InternalUserService); + expect(service).toBeTruthy(); + }); + + it('should be nologin at first', () => { + const service: InternalUserService = TestBed.get(InternalUserService); + service.refreshAndGetUserState().subscribe(result => { + expect(result.state).toBe('nologin'); + }); + }); + + it('login should work well', () => { + const service: InternalUserService = TestBed.get(InternalUserService); + + const mockUserInfo: UserInfo = { + username: 'user', + roles: ['user', 'other'] + }; + + service.tryLogin(mockUserCredentials).subscribe(result => { + expect(result).toEqual(mockUserInfo); + }); + + const httpController = TestBed.get(HttpTestingController) as HttpTestingController; + + httpController.expectOne((request: HttpRequest) => + request.url === tokenCreateUrl && + request.body.username === 'user' && + request.body.password === 'user').flush({ + token: 'test-token', + userInfo: mockUserInfo + }); + + httpController.verify(); + }); + + describe('validateUserLoginState', () => { + let service: InternalUserService; + let httpController: HttpTestingController; + + const mockUserInfo: UserInfo = { + username: 'user', + roles: ['user', 'other'] + }; + + const mockToken = 'mock-token'; + + const tokenValidateRequestMatcher = (req: HttpRequest) => { + return req.url === '/api/User/ValidateToken' && req.body.token === mockToken; + }; + + beforeEach(() => { + service = TestBed.get(InternalUserService); + httpController = TestBed.get(HttpTestingController); + + service.tryLogin(mockUserCredentials).subscribe(); // subscribe to activate login + + httpController.expectOne(tokenCreateUrl).flush({ + token: mockToken, + userInfo: mockUserInfo + }); + }); + + it('success should work well', () => { + service.refreshAndGetUserState().subscribe((result: UserLoginState) => { + expect(result).toEqual({ + state: 'success', + userInfo: mockUserInfo + }); + }); + + httpController.expectOne(tokenValidateRequestMatcher).flush({ + isValid: true, + userInfo: mockUserInfo + }); + + httpController.verify(); + }); + + it('invalid should work well', () => { + service.refreshAndGetUserState().subscribe((result: UserLoginState) => { + expect(result).toEqual({ + state: 'invalidlogin' + }); + }); + + httpController.expectOne(tokenValidateRequestMatcher).flush({ + isValid: false + }); + + httpController.verify(); + }); + }); + + // TODO: test on error situations. +}); diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts new file mode 100644 index 00000000..f6987d7d --- /dev/null +++ b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts @@ -0,0 +1,93 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { Router } from '@angular/router'; + +import { Observable, of, throwError, BehaviorSubject } from 'rxjs'; +import { map, catchError, retry } from 'rxjs/operators'; + +import { AlreadyLoginError, BadCredentialsError, BadNetworkError, UnknownError } from './errors'; +import { CreateTokenRequest, CreateTokenResponse, ValidateTokenRequest, ValidateTokenResponse } from './http-entities'; +import { UserCredentials, UserInfo } from '../entities'; + + +export type UserLoginState = 'nologin' | 'invalidlogin' | 'success'; + +/** + * This service is only used internal in user module. + */ +@Injectable({ + providedIn: 'root' +}) +export class InternalUserService { + + private token: string; + private userInfoSubject = new BehaviorSubject(null); + + get currentUserInfo(): UserInfo | null { + return this.userInfoSubject.value; + } + + get userInfo$(): Observable { + return this.userInfoSubject; + } + + constructor(private httpClient: HttpClient, private router: Router) { } + + userRouteNavigate(commands: any[]) { + this.router.navigate([{ + outlets: { + user: commands + } + }]); + } + + refreshAndGetUserState(): Observable { + if (this.token === undefined || this.token === null) { + return of('nologin'); + } + + return this.httpClient.post('/api/User/ValidateToken', { token: this.token }).pipe( + retry(3), + catchError(error => { + console.error('Failed to validate token.'); + return throwError(error); + }), + map(result => { + if (result.isValid) { + this.userInfoSubject.next(result.userInfo); + return 'success'; + } else { + this.token = null; + this.userInfoSubject.next(null); + return 'invalidlogin'; + } + }) + ); + } + + tryLogin(credentials: UserCredentials): Observable { + if (this.token) { + return throwError(new AlreadyLoginError()); + } + + return this.httpClient.post('/api/User/CreateToken', credentials).pipe( + catchError((error: HttpErrorResponse) => { + if (error.error instanceof ErrorEvent) { + console.error('An error occurred when login: ' + error.error.message); + return throwError(new BadNetworkError()); + } else if (error.status === 400) { + console.error('An error occurred when login: wrong credentials.'); + return throwError(new BadCredentialsError()); + } else { + console.error('An unknown error occurred when login: ' + error); + return throwError(new UnknownError(error)); + } + }), + map(result => { + this.token = result.token; + this.userInfoSubject.next(result.userInfo); + return result.userInfo; + }) + ); + } +} diff --git a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts index d24c0cd2..dd7af6ca 100644 --- a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts +++ b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts @@ -4,9 +4,9 @@ import { By } from '@angular/platform-browser'; import { of } from 'rxjs'; import { delay } from 'rxjs/operators'; -import { UserInfo } from '../user-info'; +import { UserInfo } from '../entities'; import { UserDialogComponent } from './user-dialog.component'; -import { UserService, UserLoginState } from '../user-service/user.service'; +import { InternalUserService, UserLoginState } from '../internal-user-service/internal-user.service'; import { LoginEvent } from '../user-login/user-login.component'; @Component({ @@ -38,7 +38,7 @@ class UserLoginSuccessStubComponent { } describe('UserDialogComponent', () => { let component: UserDialogComponent; let fixture: ComponentFixture; - let mockUserService: jasmine.SpyObj; + let mockUserService: jasmine.SpyObj; beforeEach(async(() => { mockUserService = jasmine.createSpyObj('UserService', ['validateUserLoginState', 'tryLogin']); @@ -46,7 +46,7 @@ describe('UserDialogComponent', () => { TestBed.configureTestingModule({ declarations: [UserDialogComponent, MatProgressSpinnerStubComponent, UserLoginStubComponent, UserLoginSuccessStubComponent], - providers: [{ provide: UserService, useValue: mockUserService }] + providers: [{ provide: InternalUserService, useValue: mockUserService }] }) .compileComponents(); })); @@ -57,7 +57,7 @@ describe('UserDialogComponent', () => { }); it('progress spinner should work well', fakeAsync(() => { - mockUserService.validateUserLoginState.and.returnValue(of({ state: 'nologin' }).pipe(delay(10))); + mockUserService.refreshAndGetUserState.and.returnValue(of({ state: 'nologin' }).pipe(delay(10))); fixture.detectChanges(); expect(fixture.debugElement.query(By.css('mat-progress-spinner'))).toBeTruthy(); tick(10); @@ -66,30 +66,30 @@ describe('UserDialogComponent', () => { })); it('nologin should work well', () => { - mockUserService.validateUserLoginState.and.returnValue(of({ state: 'nologin' })); + mockUserService.refreshAndGetUserState.and.returnValue(of({ state: 'nologin' })); fixture.detectChanges(); - expect(mockUserService.validateUserLoginState).toHaveBeenCalled(); + expect(mockUserService.refreshAndGetUserState).toHaveBeenCalled(); expect(fixture.debugElement.query(By.css('app-user-login'))).toBeTruthy(); expect(fixture.debugElement.query(By.css('app-user-login-success'))).toBeFalsy(); }); it('success should work well', () => { - mockUserService.validateUserLoginState.and.returnValue(of({ state: 'success', userInfo: {} })); + mockUserService.refreshAndGetUserState.and.returnValue(of({ state: 'success', userInfo: {} })); fixture.detectChanges(); - expect(mockUserService.validateUserLoginState).toHaveBeenCalled(); + expect(mockUserService.refreshAndGetUserState).toHaveBeenCalled(); expect(fixture.debugElement.query(By.css('app-user-login'))).toBeFalsy(); expect(fixture.debugElement.query(By.css('app-user-login-success'))).toBeTruthy(); }); it('login should work well', () => { - mockUserService.validateUserLoginState.and.returnValue(of({ state: 'nologin' })); + mockUserService.refreshAndGetUserState.and.returnValue(of({ state: 'nologin' })); fixture.detectChanges(); - expect(mockUserService.validateUserLoginState).toHaveBeenCalled(); + expect(mockUserService.refreshAndGetUserState).toHaveBeenCalled(); expect(fixture.debugElement.query(By.css('app-user-login'))).toBeTruthy(); expect(fixture.debugElement.query(By.css('app-user-login-success'))).toBeFalsy(); diff --git a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts index 0edde924..498ffaa1 100644 --- a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts +++ b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; -import { UserService } from '../user-service/user.service'; +import { InternalUserService } from '../internal-user-service/internal-user.service'; import { RouterOutlet, Router, ActivationStart } from '@angular/router'; @Component({ @@ -9,7 +9,7 @@ import { RouterOutlet, Router, ActivationStart } from '@angular/router'; }) export class UserDialogComponent implements OnInit, OnDestroy { - constructor(private userService: UserService, private router: Router) { } + constructor(private userService: InternalUserService, private router: Router) { } @ViewChild(RouterOutlet) outlet: RouterOutlet; @@ -24,12 +24,12 @@ export class UserDialogComponent implements OnInit, OnDestroy { }); - this.userService.validateUserLoginState().subscribe(result => { + this.userService.refreshAndGetUserState().subscribe(result => { this.isLoading = false; - if (result.state === 'success') { + if (result === 'success') { this.userService.userRouteNavigate(['success', { reason: 'already' }]); } else { - this.userService.userRouteNavigate(['login', { reason: result.state }]); + this.userService.userRouteNavigate(['login', { reason: result }]); } }); } diff --git a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts index d141b3b6..48e331d6 100644 --- a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts +++ b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts @@ -1,8 +1,9 @@ import { Component, OnInit, Input } from '@angular/core'; -import { UserInfo } from '../entities'; -import { UserService } from '../user-service/user.service'; import { ActivatedRoute } from '@angular/router'; +import { UserInfo } from '../entities'; +import { InternalUserService } from '../internal-user-service/internal-user.service'; + @Component({ selector: 'app-user-login-success', templateUrl: './user-login-success.component.html', @@ -14,10 +15,10 @@ export class UserLoginSuccessComponent implements OnInit { userInfo: UserInfo; - constructor(private route: ActivatedRoute, private userService: UserService) { } + constructor(private route: ActivatedRoute, private userService: InternalUserService) { } ngOnInit() { - this.userInfo = this.userService.userInfo; + this.userInfo = this.userService.currentUserInfo; this.displayLoginSuccessMessage = this.route.snapshot.paramMap.get('reason') === 'login'; } } diff --git a/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts b/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts index 971d57ce..082f879c 100644 --- a/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts +++ b/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts @@ -1,8 +1,9 @@ import { Component, Output, OnInit, EventEmitter } from '@angular/core'; import { FormGroup, FormControl } from '@angular/forms'; -import { UserService } from '../user-service/user.service'; import { ActivatedRoute } from '@angular/router'; +import { InternalUserService } from '../internal-user-service/internal-user.service'; + export type LoginMessage = 'nologin' | 'invalidlogin' | string; export class LoginEvent { @@ -17,9 +18,9 @@ export class LoginEvent { }) export class UserLoginComponent implements OnInit { - constructor(private route: ActivatedRoute, private userService: UserService) { } + constructor(private route: ActivatedRoute, private userService: InternalUserService) { } - message: string; + message: LoginMessage; form = new FormGroup({ username: new FormControl(''), diff --git a/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts b/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts deleted file mode 100644 index 9effe000..00000000 --- a/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { HttpRequest } from '@angular/common/http'; -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; - -import { UserInfo, UserCredentials } from '../entities'; -import { - UserService, CreateTokenResult, - UserLoginState, TokenValidationRequest, TokenValidationResult -} from './user.service'; - -describe('UserService', () => { - const tokenCreateUrl = '/api/User/CreateToken'; - - const mockUserCredentials: UserCredentials = { - username: 'user', - password: 'user' - }; - - beforeEach(() => TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] - })); - - it('should be created', () => { - const service: UserService = TestBed.get(UserService); - expect(service).toBeTruthy(); - }); - - it('should be nologin at first', () => { - const service: UserService = TestBed.get(UserService); - service.validateUserLoginState().subscribe(result => { - expect(result.state).toBe('nologin'); - }); - }); - - it('login should work well', () => { - const service: UserService = TestBed.get(UserService); - - const mockUserInfo: UserInfo = { - username: 'user', - roles: ['user', 'other'] - }; - - service.tryLogin(mockUserCredentials).subscribe(result => { - expect(result).toEqual(mockUserInfo); - }); - - const httpController = TestBed.get(HttpTestingController) as HttpTestingController; - - httpController.expectOne((request: HttpRequest) => - request.url === tokenCreateUrl && - request.body.username === 'user' && - request.body.password === 'user').flush({ - token: 'test-token', - userInfo: mockUserInfo - }); - - httpController.verify(); - }); - - describe('validateUserLoginState', () => { - let service: UserService; - let httpController: HttpTestingController; - - const mockUserInfo: UserInfo = { - username: 'user', - roles: ['user', 'other'] - }; - - const mockToken = 'mock-token'; - - const tokenValidateRequestMatcher = (req: HttpRequest) => { - return req.url === '/api/User/ValidateToken' && req.body.token === mockToken; - }; - - beforeEach(() => { - service = TestBed.get(UserService); - httpController = TestBed.get(HttpTestingController); - - service.tryLogin(mockUserCredentials).subscribe(); // subscribe to activate login - - httpController.expectOne(tokenCreateUrl).flush({ - token: mockToken, - userInfo: mockUserInfo - }); - }); - - it('success should work well', () => { - service.validateUserLoginState().subscribe((result: UserLoginState) => { - expect(result).toEqual({ - state: 'success', - userInfo: mockUserInfo - }); - }); - - httpController.expectOne(tokenValidateRequestMatcher).flush({ - isValid: true, - userInfo: mockUserInfo - }); - - httpController.verify(); - }); - - it('invalid should work well', () => { - service.validateUserLoginState().subscribe((result: UserLoginState) => { - expect(result).toEqual({ - state: 'invalidlogin' - }); - }); - - httpController.expectOne(tokenValidateRequestMatcher).flush({ - isValid: false - }); - - httpController.verify(); - }); - }); - - // TODO: test on error situations. -}); diff --git a/Timeline/ClientApp/src/app/user/user-service/user.service.ts b/Timeline/ClientApp/src/app/user/user-service/user.service.ts deleted file mode 100644 index e535537d..00000000 --- a/Timeline/ClientApp/src/app/user/user-service/user.service.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Injectable } from '@angular/core'; -import { HttpClient, HttpErrorResponse } from '@angular/common/http'; -import { Observable, of, throwError } from 'rxjs'; -import { map, catchError, retry } from 'rxjs/operators'; - -import { UserCredentials, UserInfo } from '../entities'; -import { Router } from '@angular/router'; - -export interface CreateTokenResult { - token: string; - userInfo: UserInfo; -} - -export interface TokenValidationRequest { - token: string; -} - -export interface TokenValidationResult { - isValid: boolean; - userInfo?: UserInfo; -} - -export interface UserLoginState { - state: 'nologin' | 'invalidlogin' | 'success'; - userInfo?: UserInfo; -} - -export class BadNetworkException extends Error { - constructor() { - super('Network is bad.'); - } -} - -export class AlreadyLoginException extends Error { - constructor() { - super('There is already a token saved. Please call validateUserLoginState first.'); - } -} - -export class BadCredentialsException extends Error { - constructor() { - super(`Username or password is wrong.`); - } -} - -@Injectable({ - providedIn: 'root' -}) -export class UserService { - - private token: string; - userInfo: UserInfo; - - constructor(private httpClient: HttpClient, private router: Router) { } - - userRouteNavigate(commands: any[]) { - this.router.navigate([{ - outlets: { - user: commands - } - }]); - } - - validateUserLoginState(): Observable { - if (this.token === undefined || this.token === null) { - return of({ state: 'nologin' }); - } - - return this.httpClient.post('/api/User/ValidateToken', { token: this.token }).pipe( - retry(3), - catchError(error => { - console.error('Failed to validate token.'); - return throwError(error); - }), - map(result => { - if (result.isValid) { - this.userInfo = result.userInfo; - return { - state: 'success', - userInfo: result.userInfo - }; - } else { - this.token = null; - this.userInfo = null; - return { - state: 'invalidlogin' - }; - } - }) - ); - } - - tryLogin(credentials: UserCredentials): Observable { - if (this.token) { - return throwError(new AlreadyLoginException()); - } - - return this.httpClient.post('/api/User/CreateToken', credentials).pipe( - catchError((error: HttpErrorResponse) => { - if (error.error instanceof ErrorEvent) { - console.error('An error occurred when login: ' + error.error.message); - return throwError(new BadNetworkException()); - } else if (error.status === 400) { - console.error('An error occurred when login: wrong credentials.'); - return throwError(new BadCredentialsException()); - } else { - console.error('An unknown error occurred when login: ' + error); - return throwError(error); - } - }), - map(result => { - this.token = result.token; - this.userInfo = result.userInfo; - return result.userInfo; - }) - ); - } -} diff --git a/Timeline/ClientApp/src/app/user/user.service.ts b/Timeline/ClientApp/src/app/user/user.service.ts new file mode 100644 index 00000000..e876706c --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; +import { MatDialog } from '@angular/material'; + +import { Observable } from 'rxjs'; + +import { UserInfo } from './entities'; +import { InternalUserService } from './internal-user-service/internal-user.service'; +import { UserDialogComponent } from './user-dialog/user-dialog.component'; + + +/** + * This service provides public api of user module. + */ +@Injectable({ + providedIn: 'root' +}) +export class UserService { + constructor(private dialog: MatDialog, private internalService: InternalUserService) { } + + get currentUserInfo(): UserInfo | null { + return this.internalService.currentUserInfo; + } + + get userInfo$(): Observable { + return this.internalService.userInfo$; + } + + openUserDialog() { + this.dialog.open(UserDialogComponent, { + width: '300px' + }); + } +} -- cgit v1.2.3 From 233de3a11027fd88130882f20764ee5f2952abe0 Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 9 Mar 2019 23:40:06 +0800 Subject: Half work! --- .../internal-user.service.spec.ts | 40 +++++------ .../mock-internal-user-service.ts | 5 ++ .../ClientApp/src/app/user/mock-activated-route.ts | 43 +++++++++++ .../user/user-dialog/user-dialog.component.spec.ts | 84 ++++++++-------------- .../user/user-login/user-login.component.spec.ts | 11 +++ 5 files changed, 106 insertions(+), 77 deletions(-) create mode 100644 Timeline/ClientApp/src/app/user/internal-user-service/mock-internal-user-service.ts create mode 100644 Timeline/ClientApp/src/app/user/mock-activated-route.ts diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts index 4a2c78f8..8aadd873 100644 --- a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts +++ b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts @@ -1,14 +1,13 @@ import { TestBed } from '@angular/core/testing'; import { HttpRequest } from '@angular/common/http'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { Router } from '@angular/router'; import { UserInfo, UserCredentials } from '../entities'; -import { - InternalUserService, CreateTokenResult, - UserLoginState, TokenValidationRequest, TokenValidationResult -} from './internal-user.service'; +import { CreateTokenRequest, CreateTokenResponse, ValidateTokenRequest, ValidateTokenResponse } from './http-entities'; +import { InternalUserService, UserLoginState } from './internal-user.service'; -describe('UserService', () => { +describe('InternalUserService', () => { const tokenCreateUrl = '/api/User/CreateToken'; const mockUserCredentials: UserCredentials = { @@ -17,7 +16,8 @@ describe('UserService', () => { }; beforeEach(() => TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] + imports: [HttpClientTestingModule], + providers: [{ provide: Router, useValue: null }] })); it('should be created', () => { @@ -27,8 +27,9 @@ describe('UserService', () => { it('should be nologin at first', () => { const service: InternalUserService = TestBed.get(InternalUserService); + expect(service.currentUserInfo).toBe(null); service.refreshAndGetUserState().subscribe(result => { - expect(result.state).toBe('nologin'); + expect(result).toBe('nologin'); }); }); @@ -46,14 +47,16 @@ describe('UserService', () => { const httpController = TestBed.get(HttpTestingController) as HttpTestingController; - httpController.expectOne((request: HttpRequest) => + httpController.expectOne((request: HttpRequest) => request.url === tokenCreateUrl && request.body.username === 'user' && - request.body.password === 'user').flush({ + request.body.password === 'user').flush({ token: 'test-token', userInfo: mockUserInfo }); + expect(service.currentUserInfo).toEqual(mockUserInfo); + httpController.verify(); }); @@ -68,7 +71,7 @@ describe('UserService', () => { const mockToken = 'mock-token'; - const tokenValidateRequestMatcher = (req: HttpRequest) => { + const tokenValidateRequestMatcher = (req: HttpRequest) => { return req.url === '/api/User/ValidateToken' && req.body.token === mockToken; }; @@ -78,7 +81,7 @@ describe('UserService', () => { service.tryLogin(mockUserCredentials).subscribe(); // subscribe to activate login - httpController.expectOne(tokenCreateUrl).flush({ + httpController.expectOne(tokenCreateUrl).flush({ token: mockToken, userInfo: mockUserInfo }); @@ -86,13 +89,10 @@ describe('UserService', () => { it('success should work well', () => { service.refreshAndGetUserState().subscribe((result: UserLoginState) => { - expect(result).toEqual({ - state: 'success', - userInfo: mockUserInfo - }); + expect(result).toEqual('success'); }); - httpController.expectOne(tokenValidateRequestMatcher).flush({ + httpController.expectOne(tokenValidateRequestMatcher).flush({ isValid: true, userInfo: mockUserInfo }); @@ -102,14 +102,10 @@ describe('UserService', () => { it('invalid should work well', () => { service.refreshAndGetUserState().subscribe((result: UserLoginState) => { - expect(result).toEqual({ - state: 'invalidlogin' - }); + expect(result).toEqual('invalidlogin'); }); - httpController.expectOne(tokenValidateRequestMatcher).flush({ - isValid: false - }); + httpController.expectOne(tokenValidateRequestMatcher).flush({ isValid: false }); httpController.verify(); }); diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/mock-internal-user-service.ts b/Timeline/ClientApp/src/app/user/internal-user-service/mock-internal-user-service.ts new file mode 100644 index 00000000..f4a85262 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/internal-user-service/mock-internal-user-service.ts @@ -0,0 +1,5 @@ +import { InternalUserService } from './internal-user.service'; + +export function createMockInternalUserService(): jasmine.SpyObj { + return jasmine.createSpyObj('InternalUserService', ['userRouteNavigate', 'refreshAndGetUserState', 'tryLogin']); +} diff --git a/Timeline/ClientApp/src/app/user/mock-activated-route.ts b/Timeline/ClientApp/src/app/user/mock-activated-route.ts new file mode 100644 index 00000000..9e516e83 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/mock-activated-route.ts @@ -0,0 +1,43 @@ +import { ParamMap } from '@angular/router'; + +interface MockActivatedRoute { + snapshot: MockActivatedRouteSnapshot; +} + +interface MockActivatedRouteSnapshot { + paramMap: ParamMap; +} + +export function createMockActivatedRoute(mockParamMap: { [name: string]: string | string[] }): MockActivatedRoute { + return { + snapshot: { + paramMap: { + keys: Object.keys(mockParamMap), + get(name: string): string | null { + const param = mockParamMap[name]; + if (typeof param === 'string') { + return param; + } else if (param instanceof Array) { + if (param.length === 0) { + return null; + } + return param[0]; + } + return null; + }, + getAll(name: string): string[] { + const param = mockParamMap[name]; + if (typeof param === 'string') { + return [param]; + } else if (param instanceof Array) { + return param; + } + return []; + }, + has(name: string): boolean { + return mockParamMap.hasOwnProperty(name); + } + } + } + } +} diff --git a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts index dd7af6ca..ca7c024d 100644 --- a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts +++ b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts @@ -1,13 +1,13 @@ -import { Component, Output, EventEmitter } from '@angular/core'; +import { Component } from '@angular/core'; import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { of } from 'rxjs'; +import { Router, Event } from '@angular/router'; +import { of, Observable } from 'rxjs'; import { delay } from 'rxjs/operators'; -import { UserInfo } from '../entities'; import { UserDialogComponent } from './user-dialog.component'; +import { createMockInternalUserService } from '../internal-user-service/mock-internal-user-service'; import { InternalUserService, UserLoginState } from '../internal-user-service/internal-user.service'; -import { LoginEvent } from '../user-login/user-login.component'; @Component({ /* tslint:disable-next-line:component-selector*/ @@ -17,36 +17,30 @@ import { LoginEvent } from '../user-login/user-login.component'; class MatProgressSpinnerStubComponent { } @Component({ - selector: 'app-user-login', - /* tslint:disable-next-line:use-input-property-decorator*/ - inputs: ['message'], + /* tslint:disable-next-line:component-selector*/ + selector: 'router-outlet', template: '' }) -class UserLoginStubComponent { - @Output() - login = new EventEmitter(); -} +class RouterOutletStubComponent { } -@Component({ - selector: 'app-user-login-success', - /* tslint:disable-next-line:use-input-property-decorator*/ - inputs: ['userInfo', 'displayLoginSuccessMessage'], - template: '' -}) -class UserLoginSuccessStubComponent { } describe('UserDialogComponent', () => { let component: UserDialogComponent; let fixture: ComponentFixture; - let mockUserService: jasmine.SpyObj; + let mockInternalUserService: jasmine.SpyObj; + beforeEach(async(() => { - mockUserService = jasmine.createSpyObj('UserService', ['validateUserLoginState', 'tryLogin']); + mockInternalUserService = createMockInternalUserService(); TestBed.configureTestingModule({ - declarations: [UserDialogComponent, MatProgressSpinnerStubComponent, - UserLoginStubComponent, UserLoginSuccessStubComponent], - providers: [{ provide: InternalUserService, useValue: mockUserService }] + declarations: [UserDialogComponent, MatProgressSpinnerStubComponent, RouterOutletStubComponent], + providers: [{ provide: InternalUserService, useValue: mockInternalUserService }, + { // for the workaround + provide: Router, useValue: { + events: new Observable() + } + }] }) .compileComponents(); })); @@ -57,7 +51,7 @@ describe('UserDialogComponent', () => { }); it('progress spinner should work well', fakeAsync(() => { - mockUserService.refreshAndGetUserState.and.returnValue(of({ state: 'nologin' }).pipe(delay(10))); + mockInternalUserService.refreshAndGetUserState.and.returnValue(of('nologin').pipe(delay(10))); fixture.detectChanges(); expect(fixture.debugElement.query(By.css('mat-progress-spinner'))).toBeTruthy(); tick(10); @@ -66,49 +60,29 @@ describe('UserDialogComponent', () => { })); it('nologin should work well', () => { - mockUserService.refreshAndGetUserState.and.returnValue(of({ state: 'nologin' })); + mockInternalUserService.refreshAndGetUserState.and.returnValue(of('nologin')); fixture.detectChanges(); - expect(mockUserService.refreshAndGetUserState).toHaveBeenCalled(); - expect(fixture.debugElement.query(By.css('app-user-login'))).toBeTruthy(); - expect(fixture.debugElement.query(By.css('app-user-login-success'))).toBeFalsy(); + expect(mockInternalUserService.refreshAndGetUserState).toHaveBeenCalled(); + expect(mockInternalUserService.userRouteNavigate).toHaveBeenCalledWith(['login', { reason: 'nologin' }]); }); - it('success should work well', () => { - mockUserService.refreshAndGetUserState.and.returnValue(of({ state: 'success', userInfo: {} })); + it('invalid login should work well', () => { + mockInternalUserService.refreshAndGetUserState.and.returnValue(of('invalidlogin')); fixture.detectChanges(); - expect(mockUserService.refreshAndGetUserState).toHaveBeenCalled(); - expect(fixture.debugElement.query(By.css('app-user-login'))).toBeFalsy(); - expect(fixture.debugElement.query(By.css('app-user-login-success'))).toBeTruthy(); + expect(mockInternalUserService.refreshAndGetUserState).toHaveBeenCalled(); + expect(mockInternalUserService.userRouteNavigate).toHaveBeenCalledWith(['login', { reason: 'invalidlogin' }]); }); - it('login should work well', () => { - mockUserService.refreshAndGetUserState.and.returnValue(of({ state: 'nologin' })); - - fixture.detectChanges(); - expect(mockUserService.refreshAndGetUserState).toHaveBeenCalled(); - expect(fixture.debugElement.query(By.css('app-user-login'))).toBeTruthy(); - expect(fixture.debugElement.query(By.css('app-user-login-success'))).toBeFalsy(); - - mockUserService.tryLogin.withArgs('user', 'user').and.returnValue(of({ - username: 'user', - roles: ['user'] - })); - - (fixture.debugElement.query(By.css('app-user-login')).componentInstance as - UserLoginStubComponent).login.emit({ - username: 'user', - password: 'user' - }); + it('success should work well', () => { + mockInternalUserService.refreshAndGetUserState.and.returnValue(of('success')); fixture.detectChanges(); - expect(mockUserService.tryLogin).toHaveBeenCalledWith('user', 'user'); - - expect(fixture.debugElement.query(By.css('app-user-login'))).toBeFalsy(); - expect(fixture.debugElement.query(By.css('app-user-login-success'))).toBeTruthy(); + expect(mockInternalUserService.refreshAndGetUserState).toHaveBeenCalled(); + expect(mockInternalUserService.userRouteNavigate).toHaveBeenCalledWith(['success', { reason: 'already' }]); }); }); diff --git a/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts b/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts index acd13721..3d431ce7 100644 --- a/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts +++ b/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts @@ -2,16 +2,27 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; +import { ActivatedRoute } from '@angular/router'; +import { createMockInternalUserService } from '../internal-user-service/mock-internal-user-service'; +import { createMockActivatedRoute } from '../mock-activated-route'; import { UserLoginComponent, LoginEvent } from './user-login.component'; +import { InternalUserService } from '../internal-user-service/internal-user.service'; describe('UserLoginComponent', () => { let component: UserLoginComponent; let fixture: ComponentFixture; + let mockInternalUserService: jasmine.SpyObj; beforeEach(async(() => { + mockInternalUserService = createMockInternalUserService(); + TestBed.configureTestingModule({ declarations: [UserLoginComponent], + providers: [ + {provide: InternalUserService, useValue: mockInternalUserService}, + {provide: ActivatedRoute, useValue:} // TODO: custom route snapshot param later. + ] imports: [ReactiveFormsModule], schemas: [NO_ERRORS_SCHEMA] }) -- cgit v1.2.3 From f8cfca136a69c6589bb610a66ea5342fc585f19b Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 11 Mar 2019 00:07:59 +0800 Subject: Write unit tests. --- .../src/app/test-utilities/activated-route.mock.ts | 66 +++++++++++ .../internal-user.service.mock.ts | 5 + .../mock-internal-user-service.ts | 5 - .../ClientApp/src/app/user/mock-activated-route.ts | 43 ------- .../user/user-dialog/user-dialog.component.spec.ts | 2 +- .../user-login-success.component.spec.ts | 40 ++++++- .../user/user-login/user-login.component.spec.ts | 74 +++++++++--- .../app/user/user-login/user-login.component.ts | 6 +- Timeline/ClientApp/src/app/user/user.module.ts | 2 +- .../app/utilities/debounce-click.directive.spec.ts | 124 +++++++++++++++++++++ .../src/app/utilities/debounce-click.directive.ts | 39 +++++++ .../ClientApp/src/app/utilities/utility.module.ts | 11 ++ .../app/utility/debounce-click.directive.spec.ts | 124 --------------------- .../src/app/utility/debounce-click.directive.ts | 39 ------- .../ClientApp/src/app/utility/utility.module.ts | 11 -- 15 files changed, 344 insertions(+), 247 deletions(-) create mode 100644 Timeline/ClientApp/src/app/test-utilities/activated-route.mock.ts create mode 100644 Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.mock.ts delete mode 100644 Timeline/ClientApp/src/app/user/internal-user-service/mock-internal-user-service.ts delete mode 100644 Timeline/ClientApp/src/app/user/mock-activated-route.ts create mode 100644 Timeline/ClientApp/src/app/utilities/debounce-click.directive.spec.ts create mode 100644 Timeline/ClientApp/src/app/utilities/debounce-click.directive.ts create mode 100644 Timeline/ClientApp/src/app/utilities/utility.module.ts delete mode 100644 Timeline/ClientApp/src/app/utility/debounce-click.directive.spec.ts delete mode 100644 Timeline/ClientApp/src/app/utility/debounce-click.directive.ts delete mode 100644 Timeline/ClientApp/src/app/utility/utility.module.ts diff --git a/Timeline/ClientApp/src/app/test-utilities/activated-route.mock.ts b/Timeline/ClientApp/src/app/test-utilities/activated-route.mock.ts new file mode 100644 index 00000000..1743e615 --- /dev/null +++ b/Timeline/ClientApp/src/app/test-utilities/activated-route.mock.ts @@ -0,0 +1,66 @@ +import { ParamMap } from '@angular/router'; + +import { Observable, BehaviorSubject } from 'rxjs'; +import { map } from 'rxjs/operators'; + +export interface ParamMapCreator { [name: string]: string | string[]; } + +export class MockActivatedRouteSnapshot { + + private paramMapInternal: ParamMap; + + constructor({ mockParamMap }: { mockParamMap: ParamMapCreator } = { mockParamMap: {} }) { + this.paramMapInternal = { + keys: Object.keys(mockParamMap), + get(name: string): string | null { + const param = mockParamMap[name]; + if (typeof param === 'string') { + return param; + } else if (param instanceof Array) { + if (param.length === 0) { + return null; + } + return param[0]; + } + return null; + }, + getAll(name: string): string[] { + const param = mockParamMap[name]; + if (typeof param === 'string') { + return [param]; + } else if (param instanceof Array) { + return param; + } + return []; + }, + has(name: string): boolean { + return mockParamMap.hasOwnProperty(name); + } + }; + } + + get paramMap(): ParamMap { + return this.paramMapInternal; + } +} + +export class MockActivatedRoute { + + snapshot$ = new BehaviorSubject(new MockActivatedRouteSnapshot()); + + get paramMap(): Observable { + return this.snapshot$.pipe(map(snapshot => snapshot.paramMap)); + } + + get snapshot(): MockActivatedRouteSnapshot { + return this.snapshot$.value; + } + + pushSnapshot(snapshot: MockActivatedRouteSnapshot) { + this.snapshot$.next(snapshot); + } + + pushSnapshotWithParamMap(mockParamMap: ParamMapCreator) { + this.pushSnapshot(new MockActivatedRouteSnapshot({mockParamMap})); + } +} diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.mock.ts b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.mock.ts new file mode 100644 index 00000000..f4a85262 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.mock.ts @@ -0,0 +1,5 @@ +import { InternalUserService } from './internal-user.service'; + +export function createMockInternalUserService(): jasmine.SpyObj { + return jasmine.createSpyObj('InternalUserService', ['userRouteNavigate', 'refreshAndGetUserState', 'tryLogin']); +} diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/mock-internal-user-service.ts b/Timeline/ClientApp/src/app/user/internal-user-service/mock-internal-user-service.ts deleted file mode 100644 index f4a85262..00000000 --- a/Timeline/ClientApp/src/app/user/internal-user-service/mock-internal-user-service.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { InternalUserService } from './internal-user.service'; - -export function createMockInternalUserService(): jasmine.SpyObj { - return jasmine.createSpyObj('InternalUserService', ['userRouteNavigate', 'refreshAndGetUserState', 'tryLogin']); -} diff --git a/Timeline/ClientApp/src/app/user/mock-activated-route.ts b/Timeline/ClientApp/src/app/user/mock-activated-route.ts deleted file mode 100644 index 9e516e83..00000000 --- a/Timeline/ClientApp/src/app/user/mock-activated-route.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { ParamMap } from '@angular/router'; - -interface MockActivatedRoute { - snapshot: MockActivatedRouteSnapshot; -} - -interface MockActivatedRouteSnapshot { - paramMap: ParamMap; -} - -export function createMockActivatedRoute(mockParamMap: { [name: string]: string | string[] }): MockActivatedRoute { - return { - snapshot: { - paramMap: { - keys: Object.keys(mockParamMap), - get(name: string): string | null { - const param = mockParamMap[name]; - if (typeof param === 'string') { - return param; - } else if (param instanceof Array) { - if (param.length === 0) { - return null; - } - return param[0]; - } - return null; - }, - getAll(name: string): string[] { - const param = mockParamMap[name]; - if (typeof param === 'string') { - return [param]; - } else if (param instanceof Array) { - return param; - } - return []; - }, - has(name: string): boolean { - return mockParamMap.hasOwnProperty(name); - } - } - } - } -} diff --git a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts index ca7c024d..c56e1ed1 100644 --- a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts +++ b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts @@ -6,7 +6,7 @@ import { of, Observable } from 'rxjs'; import { delay } from 'rxjs/operators'; import { UserDialogComponent } from './user-dialog.component'; -import { createMockInternalUserService } from '../internal-user-service/mock-internal-user-service'; +import { createMockInternalUserService } from '../internal-user-service/internal-user.service.mock'; import { InternalUserService, UserLoginState } from '../internal-user-service/internal-user.service'; @Component({ diff --git a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts index ba015ae6..1efbb5c7 100644 --- a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts +++ b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts @@ -1,20 +1,39 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { ActivatedRoute } from '@angular/router'; + +import { MockActivatedRoute } from 'src/app/test-utilities/activated-route.mock'; +import { createMockInternalUserService } from '../internal-user-service/internal-user.service.mock'; import { UserLoginSuccessComponent } from './user-login-success.component'; -import { By } from '@angular/platform-browser'; +import { InternalUserService } from '../internal-user-service/internal-user.service'; + describe('UserLoginSuccessComponent', () => { let component: UserLoginSuccessComponent; let fixture: ComponentFixture; + let mockInternalUserService: jasmine.SpyObj; + let mockActivatedRoute: MockActivatedRoute; + const mockUserInfo = { username: 'crupest', roles: ['superman', 'coder'] }; beforeEach(async(() => { + mockInternalUserService = createMockInternalUserService(); + mockActivatedRoute = new MockActivatedRoute(); + + // mock currentUserInfo property. because it only has a getter so cast it to any first. + (mockInternalUserService).currentUserInfo = mockUserInfo; + TestBed.configureTestingModule({ - declarations: [UserLoginSuccessComponent] + declarations: [UserLoginSuccessComponent], + providers: [ + { provide: InternalUserService, useValue: mockInternalUserService }, + { provide: ActivatedRoute, useValue: mockActivatedRoute } + ] }) .compileComponents(); })); @@ -22,18 +41,29 @@ describe('UserLoginSuccessComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(UserLoginSuccessComponent); component = fixture.componentInstance; - component.userInfo = mockUserInfo; - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); - it('should work well', () => { + it('user info should work well', () => { + fixture.detectChanges(); + + expect((fixture.debugElement.query(By.css('p.login-success-message')))).toBeFalsy(); + expect((fixture.debugElement.query(By.css('span.username')).nativeElement as HTMLSpanElement).textContent) .toBe(mockUserInfo.username); expect((fixture.debugElement.query(By.css('span.roles')).nativeElement as HTMLSpanElement).textContent) .toBe(mockUserInfo.roles.join(', ')); }); + + it('login success message should display well', () => { + mockActivatedRoute.pushSnapshotWithParamMap({ reason: 'login' }); + + fixture.detectChanges(); + + expect((fixture.debugElement.query(By.css('p.login-success-message')))).toBeTruthy(); + }); }); diff --git a/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts b/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts index 3d431ce7..9c9ee1dc 100644 --- a/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts +++ b/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts @@ -1,28 +1,33 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; -import { createMockInternalUserService } from '../internal-user-service/mock-internal-user-service'; -import { createMockActivatedRoute } from '../mock-activated-route'; -import { UserLoginComponent, LoginEvent } from './user-login.component'; +import { of, throwError } from 'rxjs'; + +import { createMockInternalUserService } from '../internal-user-service/internal-user.service.mock'; +import { MockActivatedRoute } from '../../test-utilities/activated-route.mock'; +import { UserLoginComponent } from './user-login.component'; import { InternalUserService } from '../internal-user-service/internal-user.service'; +import { UserInfo } from '../entities'; describe('UserLoginComponent', () => { let component: UserLoginComponent; let fixture: ComponentFixture; let mockInternalUserService: jasmine.SpyObj; + let mockActivatedRoute: MockActivatedRoute; beforeEach(async(() => { mockInternalUserService = createMockInternalUserService(); + mockActivatedRoute = new MockActivatedRoute(); TestBed.configureTestingModule({ declarations: [UserLoginComponent], providers: [ - {provide: InternalUserService, useValue: mockInternalUserService}, - {provide: ActivatedRoute, useValue:} // TODO: custom route snapshot param later. - ] + { provide: InternalUserService, useValue: mockInternalUserService }, + { provide: ActivatedRoute, useValue: mockActivatedRoute } + ], imports: [ReactiveFormsModule], schemas: [NO_ERRORS_SCHEMA] }) @@ -32,14 +37,16 @@ describe('UserLoginComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(UserLoginComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); it('reactive form should work well', () => { + fixture.detectChanges(); + const usernameInput = fixture.debugElement.query(By.css('input[type=text]')).nativeElement as HTMLInputElement; const passwordInput = fixture.debugElement.query(By.css('input[type=password]')).nativeElement as HTMLInputElement; @@ -56,16 +63,57 @@ describe('UserLoginComponent', () => { }); }); - it('login event should work well', fakeAsync(() => { - let userCredential: LoginEvent; - component.login.subscribe((e: LoginEvent) => { userCredential = e; }); + it('login should work well', () => { fixture.detectChanges(); + const mockValue = { username: 'user', password: 'user' }; + + mockInternalUserService.tryLogin.withArgs(mockValue).and.returnValue(of({ username: 'user', roles: ['user'] })); + component.form.setValue(mockValue); component.onLoginButtonClick(); - expect(userCredential).toEqual(mockValue); - })); + + expect(mockInternalUserService.tryLogin).toHaveBeenCalledWith(mockValue); + expect(mockInternalUserService.userRouteNavigate).toHaveBeenCalledWith(['success', { reason: 'login' }]); + }); + + describe('message display', () => { + it('nologin reason should display', () => { + mockActivatedRoute.pushSnapshotWithParamMap({ reason: 'nologin' }); + fixture.detectChanges(); + expect(component.message).toBe('nologin'); + expect((fixture.debugElement.query(By.css('p.mat-body')).nativeElement as + HTMLParagraphElement).textContent).toBe('You haven\'t login.'); + }); + + it('invalid login reason should display', () => { + mockActivatedRoute.pushSnapshotWithParamMap({ reason: 'invalidlogin' }); + fixture.detectChanges(); + expect(component.message).toBe('invalidlogin'); + expect((fixture.debugElement.query(By.css('p.mat-body')).nativeElement as + HTMLParagraphElement).textContent).toBe('Your login is no longer valid.'); + }); + + it('custom error message should display', () => { + const customMessage = 'custom message'; + + fixture.detectChanges(); + + const mockValue = { + username: 'user', + password: 'user' + }; + mockInternalUserService.tryLogin.withArgs(mockValue).and.returnValue(throwError(new Error(customMessage))); + component.form.setValue(mockValue); + component.onLoginButtonClick(); + + fixture.detectChanges(); + expect(component.message).toBe(customMessage); + expect((fixture.debugElement.query(By.css('p.mat-body')).nativeElement as + HTMLParagraphElement).textContent).toBe(customMessage); + }); + }); }); diff --git a/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts b/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts index 082f879c..79a788de 100644 --- a/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts +++ b/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts @@ -1,4 +1,4 @@ -import { Component, Output, OnInit, EventEmitter } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { FormGroup, FormControl } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; @@ -6,10 +6,6 @@ import { InternalUserService } from '../internal-user-service/internal-user.serv export type LoginMessage = 'nologin' | 'invalidlogin' | string; -export class LoginEvent { - username: string; - password: string; -} @Component({ selector: 'app-user-login', diff --git a/Timeline/ClientApp/src/app/user/user.module.ts b/Timeline/ClientApp/src/app/user/user.module.ts index 1e70d33d..c399c9e0 100644 --- a/Timeline/ClientApp/src/app/user/user.module.ts +++ b/Timeline/ClientApp/src/app/user/user.module.ts @@ -10,7 +10,7 @@ import { import { UserDialogComponent } from './user-dialog/user-dialog.component'; import { UserLoginComponent } from './user-login/user-login.component'; import { UserLoginSuccessComponent } from './user-login-success/user-login-success.component'; -import { UtilityModule } from '../utility/utility.module'; +import { UtilityModule } from '../utilities/utility.module'; import { RouterModule } from '@angular/router'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; diff --git a/Timeline/ClientApp/src/app/utilities/debounce-click.directive.spec.ts b/Timeline/ClientApp/src/app/utilities/debounce-click.directive.spec.ts new file mode 100644 index 00000000..75710d0c --- /dev/null +++ b/Timeline/ClientApp/src/app/utilities/debounce-click.directive.spec.ts @@ -0,0 +1,124 @@ +import { Component, ViewChild } from '@angular/core'; +import { async, TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { DebounceClickDirective } from './debounce-click.directive'; + +interface TestComponent { + clickHandler: () => void; +} + +@Component({ + selector: 'app-default-test', + template: '' +}) +class DefaultDebounceTimeTestComponent { + @ViewChild(DebounceClickDirective) + directive: DebounceClickDirective; + + clickHandler: () => void = () => { }; +} + +@Component({ + selector: 'app-default-test', + template: '' +}) +class CustomDebounceTimeTestComponent { + debounceTime: number; + + @ViewChild(DebounceClickDirective) + directive: DebounceClickDirective; + + clickHandler: () => void = () => { }; +} + + +describe('DebounceClickDirective', () => { + let counter: number; + + function initComponent(component: TestComponent) { + component.clickHandler = () => counter++; + } + + beforeEach(() => { + counter = 0; + }); + + describe('default debounce time', () => { + let component: DefaultDebounceTimeTestComponent; + let componentFixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [DebounceClickDirective, DefaultDebounceTimeTestComponent] + }).compileComponents(); + })); + + beforeEach(() => { + componentFixture = TestBed.createComponent(DefaultDebounceTimeTestComponent); + component = componentFixture.componentInstance; + initComponent(component); + }); + + it('should create an instance', () => { + componentFixture.detectChanges(); + expect(component.directive).toBeTruthy(); + }); + + it('should work well', fakeAsync(() => { + function click() { + (componentFixture.debugElement.query(By.css('button')).nativeElement).dispatchEvent(new MouseEvent('click')); + } + componentFixture.detectChanges(); + expect(counter).toBe(0); + click(); + tick(300); + expect(counter).toBe(0); + click(); + tick(); + expect(counter).toBe(0); + tick(500); + expect(counter).toBe(1); + })); + }); + + + describe('custom debounce time', () => { + let component: CustomDebounceTimeTestComponent; + let componentFixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [DebounceClickDirective, CustomDebounceTimeTestComponent] + }).compileComponents(); + })); + + beforeEach(() => { + componentFixture = TestBed.createComponent(CustomDebounceTimeTestComponent); + component = componentFixture.componentInstance; + initComponent(component); + component.debounceTime = 600; + }); + + it('should create an instance', () => { + componentFixture.detectChanges(); + expect(component.directive).toBeTruthy(); + }); + + it('should work well', fakeAsync(() => { + function click() { + (componentFixture.debugElement.query(By.css('button')).nativeElement).dispatchEvent(new MouseEvent('click')); + } + componentFixture.detectChanges(); + expect(counter).toBe(0); + click(); + tick(300); + expect(counter).toBe(0); + click(); + tick(); + expect(counter).toBe(0); + tick(600); + expect(counter).toBe(1); + })); + }); +}); diff --git a/Timeline/ClientApp/src/app/utilities/debounce-click.directive.ts b/Timeline/ClientApp/src/app/utilities/debounce-click.directive.ts new file mode 100644 index 00000000..feb0404e --- /dev/null +++ b/Timeline/ClientApp/src/app/utilities/debounce-click.directive.ts @@ -0,0 +1,39 @@ +import { Directive, Output, Input, EventEmitter, ElementRef, OnInit, OnDestroy } from '@angular/core'; +import { fromEvent, Subscription } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; + +@Directive({ + selector: '[appDebounceClick]' +}) +export class DebounceClickDirective implements OnInit, OnDestroy { + + private subscription: Subscription; + + @Output('appDebounceClick') clickEvent = new EventEmitter(); + + // tslint:disable-next-line:no-input-rename + @Input('appDebounceClickTime') + set debounceTime(value: number) { + if (this.subscription) { + this.subscription.unsubscribe(); + } + this.subscription = fromEvent(this.element.nativeElement, 'click').pipe( + debounceTime(value) + ).subscribe(o => this.clickEvent.emit(o)); + } + + constructor(private element: ElementRef) { + } + + ngOnInit() { + if (!this.subscription) { + this.subscription = fromEvent(this.element.nativeElement, 'click').pipe( + debounceTime(500) + ).subscribe(o => this.clickEvent.emit(o)); + } + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } +} diff --git a/Timeline/ClientApp/src/app/utilities/utility.module.ts b/Timeline/ClientApp/src/app/utilities/utility.module.ts new file mode 100644 index 00000000..dd686bf7 --- /dev/null +++ b/Timeline/ClientApp/src/app/utilities/utility.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { DebounceClickDirective } from './debounce-click.directive'; + +@NgModule({ + declarations: [DebounceClickDirective], + imports: [CommonModule], + exports: [DebounceClickDirective] +}) +export class UtilityModule { } diff --git a/Timeline/ClientApp/src/app/utility/debounce-click.directive.spec.ts b/Timeline/ClientApp/src/app/utility/debounce-click.directive.spec.ts deleted file mode 100644 index 75710d0c..00000000 --- a/Timeline/ClientApp/src/app/utility/debounce-click.directive.spec.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { Component, ViewChild } from '@angular/core'; -import { async, TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; - -import { DebounceClickDirective } from './debounce-click.directive'; - -interface TestComponent { - clickHandler: () => void; -} - -@Component({ - selector: 'app-default-test', - template: '' -}) -class DefaultDebounceTimeTestComponent { - @ViewChild(DebounceClickDirective) - directive: DebounceClickDirective; - - clickHandler: () => void = () => { }; -} - -@Component({ - selector: 'app-default-test', - template: '' -}) -class CustomDebounceTimeTestComponent { - debounceTime: number; - - @ViewChild(DebounceClickDirective) - directive: DebounceClickDirective; - - clickHandler: () => void = () => { }; -} - - -describe('DebounceClickDirective', () => { - let counter: number; - - function initComponent(component: TestComponent) { - component.clickHandler = () => counter++; - } - - beforeEach(() => { - counter = 0; - }); - - describe('default debounce time', () => { - let component: DefaultDebounceTimeTestComponent; - let componentFixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [DebounceClickDirective, DefaultDebounceTimeTestComponent] - }).compileComponents(); - })); - - beforeEach(() => { - componentFixture = TestBed.createComponent(DefaultDebounceTimeTestComponent); - component = componentFixture.componentInstance; - initComponent(component); - }); - - it('should create an instance', () => { - componentFixture.detectChanges(); - expect(component.directive).toBeTruthy(); - }); - - it('should work well', fakeAsync(() => { - function click() { - (componentFixture.debugElement.query(By.css('button')).nativeElement).dispatchEvent(new MouseEvent('click')); - } - componentFixture.detectChanges(); - expect(counter).toBe(0); - click(); - tick(300); - expect(counter).toBe(0); - click(); - tick(); - expect(counter).toBe(0); - tick(500); - expect(counter).toBe(1); - })); - }); - - - describe('custom debounce time', () => { - let component: CustomDebounceTimeTestComponent; - let componentFixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [DebounceClickDirective, CustomDebounceTimeTestComponent] - }).compileComponents(); - })); - - beforeEach(() => { - componentFixture = TestBed.createComponent(CustomDebounceTimeTestComponent); - component = componentFixture.componentInstance; - initComponent(component); - component.debounceTime = 600; - }); - - it('should create an instance', () => { - componentFixture.detectChanges(); - expect(component.directive).toBeTruthy(); - }); - - it('should work well', fakeAsync(() => { - function click() { - (componentFixture.debugElement.query(By.css('button')).nativeElement).dispatchEvent(new MouseEvent('click')); - } - componentFixture.detectChanges(); - expect(counter).toBe(0); - click(); - tick(300); - expect(counter).toBe(0); - click(); - tick(); - expect(counter).toBe(0); - tick(600); - expect(counter).toBe(1); - })); - }); -}); diff --git a/Timeline/ClientApp/src/app/utility/debounce-click.directive.ts b/Timeline/ClientApp/src/app/utility/debounce-click.directive.ts deleted file mode 100644 index feb0404e..00000000 --- a/Timeline/ClientApp/src/app/utility/debounce-click.directive.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Directive, Output, Input, EventEmitter, ElementRef, OnInit, OnDestroy } from '@angular/core'; -import { fromEvent, Subscription } from 'rxjs'; -import { debounceTime } from 'rxjs/operators'; - -@Directive({ - selector: '[appDebounceClick]' -}) -export class DebounceClickDirective implements OnInit, OnDestroy { - - private subscription: Subscription; - - @Output('appDebounceClick') clickEvent = new EventEmitter(); - - // tslint:disable-next-line:no-input-rename - @Input('appDebounceClickTime') - set debounceTime(value: number) { - if (this.subscription) { - this.subscription.unsubscribe(); - } - this.subscription = fromEvent(this.element.nativeElement, 'click').pipe( - debounceTime(value) - ).subscribe(o => this.clickEvent.emit(o)); - } - - constructor(private element: ElementRef) { - } - - ngOnInit() { - if (!this.subscription) { - this.subscription = fromEvent(this.element.nativeElement, 'click').pipe( - debounceTime(500) - ).subscribe(o => this.clickEvent.emit(o)); - } - } - - ngOnDestroy() { - this.subscription.unsubscribe(); - } -} diff --git a/Timeline/ClientApp/src/app/utility/utility.module.ts b/Timeline/ClientApp/src/app/utility/utility.module.ts deleted file mode 100644 index dd686bf7..00000000 --- a/Timeline/ClientApp/src/app/utility/utility.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -import { DebounceClickDirective } from './debounce-click.directive'; - -@NgModule({ - declarations: [DebounceClickDirective], - imports: [CommonModule], - exports: [DebounceClickDirective] -}) -export class UtilityModule { } -- cgit v1.2.3 From 92cd9a980075fa482bd7f67412c618e54ac9501c Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 11 Mar 2019 00:14:48 +0800 Subject: Extract out http entities. --- .../ClientApp/src/app/todo/todo-service/http-entities.ts | 11 +++++++++++ .../src/app/todo/todo-service/todo.service.spec.ts | 8 ++++---- .../ClientApp/src/app/todo/todo-service/todo.service.ts | 14 ++------------ .../src/app/user/internal-user-service/http-entities.ts | 3 +++ .../internal-user-service/internal-user.service.spec.ts | 13 +++++++------ .../user/internal-user-service/internal-user.service.ts | 9 ++++++--- 6 files changed, 33 insertions(+), 25 deletions(-) create mode 100644 Timeline/ClientApp/src/app/todo/todo-service/http-entities.ts diff --git a/Timeline/ClientApp/src/app/todo/todo-service/http-entities.ts b/Timeline/ClientApp/src/app/todo/todo-service/http-entities.ts new file mode 100644 index 00000000..3971617c --- /dev/null +++ b/Timeline/ClientApp/src/app/todo/todo-service/http-entities.ts @@ -0,0 +1,11 @@ +export const githubBaseUrl = 'https://api.github.com/repos/crupest/Timeline'; + +export interface IssueResponseItem { + number: number; + title: string; + state: string; + html_url: string; + pull_request?: any; +} + +export type IssueResponse = IssueResponseItem[]; diff --git a/Timeline/ClientApp/src/app/todo/todo-service/todo.service.spec.ts b/Timeline/ClientApp/src/app/todo/todo-service/todo.service.spec.ts index b0b35f7b..679dc8b7 100644 --- a/Timeline/ClientApp/src/app/todo/todo-service/todo.service.spec.ts +++ b/Timeline/ClientApp/src/app/todo/todo-service/todo.service.spec.ts @@ -3,7 +3,8 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/ import { toArray } from 'rxjs/operators'; import { TodoItem } from '../todo-item'; -import { TodoService, IssueResponse } from './todo.service'; +import { TodoService } from './todo.service'; +import { IssueResponse, githubBaseUrl } from './http-entities'; describe('TodoService', () => { @@ -19,8 +20,6 @@ describe('TodoService', () => { it('should work well', () => { const service: TodoService = TestBed.get(TodoService); - const baseUrl = service.baseUrl; - const mockIssueList: IssueResponse = [{ number: 1, title: 'Issue title 1', @@ -47,7 +46,8 @@ describe('TodoService', () => { const httpController: HttpTestingController = TestBed.get(HttpTestingController); - httpController.expectOne(request => request.url === baseUrl + '/issues' && request.params.get('state') === 'all').flush(mockIssueList); + httpController.expectOne(request => request.url === githubBaseUrl + '/issues' && + request.params.get('state') === 'all').flush(mockIssueList); httpController.verify(); }); diff --git a/Timeline/ClientApp/src/app/todo/todo-service/todo.service.ts b/Timeline/ClientApp/src/app/todo/todo-service/todo.service.ts index ed1f2cbe..df63636d 100644 --- a/Timeline/ClientApp/src/app/todo/todo-service/todo.service.ts +++ b/Timeline/ClientApp/src/app/todo/todo-service/todo.service.ts @@ -3,29 +3,19 @@ import { HttpClient } from '@angular/common/http'; import { Observable, from } from 'rxjs'; import { switchMap, map, filter } from 'rxjs/operators'; +import { IssueResponse, githubBaseUrl } from './http-entities'; import { TodoItem } from '../todo-item'; -export interface IssueResponseItem { - number: number; - title: string; - state: string; - html_url: string; - pull_request?: any; -} - -export type IssueResponse = IssueResponseItem[]; @Injectable({ providedIn: 'root' }) export class TodoService { - readonly baseUrl = 'https://api.github.com/repos/crupest/Timeline'; - constructor(private client: HttpClient) { } getWorkItemList(): Observable { - return this.client.get(`${this.baseUrl}/issues`, { + return this.client.get(`${githubBaseUrl}/issues`, { params: { state: 'all' } diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts b/Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts index 1335b407..5664cf7c 100644 --- a/Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts +++ b/Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts @@ -1,5 +1,8 @@ import { UserCredentials, UserInfo } from '../entities'; +export const createTokenUrl = '/api/User/CreateToken'; +export const validateTokenUrl = '/api/User/ValidateToken'; + export type CreateTokenRequest = UserCredentials; export interface CreateTokenResponse { diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts index 8aadd873..4db28768 100644 --- a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts +++ b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts @@ -4,12 +4,13 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/ import { Router } from '@angular/router'; import { UserInfo, UserCredentials } from '../entities'; -import { CreateTokenRequest, CreateTokenResponse, ValidateTokenRequest, ValidateTokenResponse } from './http-entities'; +import { + createTokenUrl, validateTokenUrl, CreateTokenRequest, + CreateTokenResponse, ValidateTokenRequest, ValidateTokenResponse +} from './http-entities'; import { InternalUserService, UserLoginState } from './internal-user.service'; describe('InternalUserService', () => { - const tokenCreateUrl = '/api/User/CreateToken'; - const mockUserCredentials: UserCredentials = { username: 'user', password: 'user' @@ -48,7 +49,7 @@ describe('InternalUserService', () => { const httpController = TestBed.get(HttpTestingController) as HttpTestingController; httpController.expectOne((request: HttpRequest) => - request.url === tokenCreateUrl && + request.url === createTokenUrl && request.body.username === 'user' && request.body.password === 'user').flush({ token: 'test-token', @@ -72,7 +73,7 @@ describe('InternalUserService', () => { const mockToken = 'mock-token'; const tokenValidateRequestMatcher = (req: HttpRequest) => { - return req.url === '/api/User/ValidateToken' && req.body.token === mockToken; + return req.url === validateTokenUrl && req.body.token === mockToken; }; beforeEach(() => { @@ -81,7 +82,7 @@ describe('InternalUserService', () => { service.tryLogin(mockUserCredentials).subscribe(); // subscribe to activate login - httpController.expectOne(tokenCreateUrl).flush({ + httpController.expectOne(createTokenUrl).flush({ token: mockToken, userInfo: mockUserInfo }); diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts index f6987d7d..91a67e5b 100644 --- a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts +++ b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts @@ -6,7 +6,10 @@ import { Observable, of, throwError, BehaviorSubject } from 'rxjs'; import { map, catchError, retry } from 'rxjs/operators'; import { AlreadyLoginError, BadCredentialsError, BadNetworkError, UnknownError } from './errors'; -import { CreateTokenRequest, CreateTokenResponse, ValidateTokenRequest, ValidateTokenResponse } from './http-entities'; +import { + createTokenUrl, validateTokenUrl, CreateTokenRequest, + CreateTokenResponse, ValidateTokenRequest, ValidateTokenResponse +} from './http-entities'; import { UserCredentials, UserInfo } from '../entities'; @@ -46,7 +49,7 @@ export class InternalUserService { return of('nologin'); } - return this.httpClient.post('/api/User/ValidateToken', { token: this.token }).pipe( + return this.httpClient.post(validateTokenUrl, { token: this.token }).pipe( retry(3), catchError(error => { console.error('Failed to validate token.'); @@ -70,7 +73,7 @@ export class InternalUserService { return throwError(new AlreadyLoginError()); } - return this.httpClient.post('/api/User/CreateToken', credentials).pipe( + return this.httpClient.post(createTokenUrl, credentials).pipe( catchError((error: HttpErrorResponse) => { if (error.error instanceof ErrorEvent) { console.error('An error occurred when login: ' + error.error.message); -- cgit v1.2.3 From 5d1d884635713278a792f99bb32cbe6d7471b0df Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 11 Mar 2019 19:41:08 +0800 Subject: Configure build config. Specify mock and test files. --- Timeline/ClientApp/src/tsconfig.app.json | 4 +++- Timeline/ClientApp/src/tsconfig.spec.json | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Timeline/ClientApp/src/tsconfig.app.json b/Timeline/ClientApp/src/tsconfig.app.json index 722c370d..13151ca4 100644 --- a/Timeline/ClientApp/src/tsconfig.app.json +++ b/Timeline/ClientApp/src/tsconfig.app.json @@ -7,6 +7,8 @@ }, "exclude": [ "src/test.ts", - "**/*.spec.ts" + "**/*.spec.ts", + "**/*.mock.ts", + "**/*.test.ts" ] } diff --git a/Timeline/ClientApp/src/tsconfig.spec.json b/Timeline/ClientApp/src/tsconfig.spec.json index 8f7cedec..6e4460f8 100644 --- a/Timeline/ClientApp/src/tsconfig.spec.json +++ b/Timeline/ClientApp/src/tsconfig.spec.json @@ -14,6 +14,8 @@ ], "include": [ "**/*.spec.ts", - "**/*.d.ts" + "**/*.d.ts", + "**/*.mock.ts", + "**/*.test.ts" ] } -- cgit v1.2.3