aboutsummaryrefslogtreecommitdiff
path: root/Timeline/ClientApp/src
diff options
context:
space:
mode:
author杨宇千 <crupest@outlook.com>2019-03-11 19:52:29 +0800
committerGitHub <noreply@github.com>2019-03-11 19:52:29 +0800
commit6f02dc7094d1304501e7ffd6c39ecf89369202c7 (patch)
treed031e2d14933574de04c892accb59560bdbea395 /Timeline/ClientApp/src
parent028af6f786ac2664d301614d57bbff053c3dc9c0 (diff)
parent5d1d884635713278a792f99bb32cbe6d7471b0df (diff)
downloadtimeline-6f02dc7094d1304501e7ffd6c39ecf89369202c7.tar.gz
timeline-6f02dc7094d1304501e7ffd6c39ecf89369202c7.tar.bz2
timeline-6f02dc7094d1304501e7ffd6c39ecf89369202c7.zip
Merge pull request #11 from crupest/7-user-route
Use named route in user dialog.
Diffstat (limited to 'Timeline/ClientApp/src')
-rw-r--r--Timeline/ClientApp/src/app/app.component.ts9
-rw-r--r--Timeline/ClientApp/src/app/test-utilities/activated-route.mock.ts66
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-service/http-entities.ts11
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-service/todo.service.spec.ts8
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-service/todo.service.ts14
-rw-r--r--Timeline/ClientApp/src/app/user/entities.ts9
-rw-r--r--Timeline/ClientApp/src/app/user/internal-user-service/errors.ts25
-rw-r--r--Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts20
-rw-r--r--Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.mock.ts5
-rw-r--r--Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts116
-rw-r--r--Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts96
-rw-r--r--Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.html7
-rw-r--r--Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts86
-rw-r--r--Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts47
-rw-r--r--Timeline/ClientApp/src/app/user/user-info.ts4
-rw-r--r--Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts40
-rw-r--r--Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts12
-rw-r--r--Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts75
-rw-r--r--Timeline/ClientApp/src/app/user/user-login/user-login.component.ts25
-rw-r--r--Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts114
-rw-r--r--Timeline/ClientApp/src/app/user/user-service/user.service.ts116
-rw-r--r--Timeline/ClientApp/src/app/user/user.module.ts11
-rw-r--r--Timeline/ClientApp/src/app/user/user.service.ts33
-rw-r--r--Timeline/ClientApp/src/app/utilities/debounce-click.directive.spec.ts (renamed from Timeline/ClientApp/src/app/utility/debounce-click.directive.spec.ts)0
-rw-r--r--Timeline/ClientApp/src/app/utilities/debounce-click.directive.ts (renamed from Timeline/ClientApp/src/app/utility/debounce-click.directive.ts)0
-rw-r--r--Timeline/ClientApp/src/app/utilities/utility.module.ts (renamed from Timeline/ClientApp/src/app/utility/utility.module.ts)0
-rw-r--r--Timeline/ClientApp/src/tsconfig.app.json4
-rw-r--r--Timeline/ClientApp/src/tsconfig.spec.json4
28 files changed, 583 insertions, 374 deletions
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/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<MockActivatedRouteSnapshot>(new MockActivatedRouteSnapshot());
+
+ get paramMap(): Observable<ParamMap> {
+ 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/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<TodoItem> {
- return this.client.get<IssueResponse>(`${this.baseUrl}/issues`, {
+ return this.client.get<IssueResponse>(`${githubBaseUrl}/issues`, {
params: {
state: 'all'
}
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/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..5664cf7c
--- /dev/null
+++ b/Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts
@@ -0,0 +1,20 @@
+import { UserCredentials, UserInfo } from '../entities';
+
+export const createTokenUrl = '/api/User/CreateToken';
+export const validateTokenUrl = '/api/User/ValidateToken';
+
+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.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<InternalUserService> {
+ return jasmine.createSpyObj('InternalUserService', ['userRouteNavigate', 'refreshAndGetUserState', 'tryLogin']);
+}
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..4db28768
--- /dev/null
+++ b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts
@@ -0,0 +1,116 @@
+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 {
+ createTokenUrl, validateTokenUrl, CreateTokenRequest,
+ CreateTokenResponse, ValidateTokenRequest, ValidateTokenResponse
+} from './http-entities';
+import { InternalUserService, UserLoginState } from './internal-user.service';
+
+describe('InternalUserService', () => {
+ const mockUserCredentials: UserCredentials = {
+ username: 'user',
+ password: 'user'
+ };
+
+ beforeEach(() => TestBed.configureTestingModule({
+ imports: [HttpClientTestingModule],
+ providers: [{ provide: Router, useValue: null }]
+ }));
+
+ 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);
+ expect(service.currentUserInfo).toBe(null);
+ service.refreshAndGetUserState().subscribe(result => {
+ expect(result).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<CreateTokenRequest>) =>
+ request.url === createTokenUrl &&
+ request.body.username === 'user' &&
+ request.body.password === 'user').flush(<CreateTokenResponse>{
+ token: 'test-token',
+ userInfo: mockUserInfo
+ });
+
+ expect(service.currentUserInfo).toEqual(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<ValidateTokenRequest>) => {
+ return req.url === validateTokenUrl && req.body.token === mockToken;
+ };
+
+ beforeEach(() => {
+ service = TestBed.get(InternalUserService);
+ httpController = TestBed.get(HttpTestingController);
+
+ service.tryLogin(mockUserCredentials).subscribe(); // subscribe to activate login
+
+ httpController.expectOne(createTokenUrl).flush(<CreateTokenResponse>{
+ token: mockToken,
+ userInfo: mockUserInfo
+ });
+ });
+
+ it('success should work well', () => {
+ service.refreshAndGetUserState().subscribe((result: UserLoginState) => {
+ expect(result).toEqual(<UserLoginState>'success');
+ });
+
+ httpController.expectOne(tokenValidateRequestMatcher).flush(<ValidateTokenResponse>{
+ isValid: true,
+ userInfo: mockUserInfo
+ });
+
+ httpController.verify();
+ });
+
+ it('invalid should work well', () => {
+ service.refreshAndGetUserState().subscribe((result: UserLoginState) => {
+ expect(result).toEqual(<UserLoginState>'invalidlogin');
+ });
+
+ httpController.expectOne(tokenValidateRequestMatcher).flush(<ValidateTokenResponse>{ 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..91a67e5b
--- /dev/null
+++ b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts
@@ -0,0 +1,96 @@
+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 {
+ createTokenUrl, validateTokenUrl, 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<UserInfo | null>(null);
+
+ get currentUserInfo(): UserInfo | null {
+ return this.userInfoSubject.value;
+ }
+
+ get userInfo$(): Observable<UserInfo | null> {
+ return this.userInfoSubject;
+ }
+
+ constructor(private httpClient: HttpClient, private router: Router) { }
+
+ userRouteNavigate(commands: any[]) {
+ this.router.navigate([{
+ outlets: {
+ user: commands
+ }
+ }]);
+ }
+
+ refreshAndGetUserState(): Observable<UserLoginState> {
+ if (this.token === undefined || this.token === null) {
+ return of(<UserLoginState>'nologin');
+ }
+
+ return this.httpClient.post<ValidateTokenResponse>(validateTokenUrl, <ValidateTokenRequest>{ 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 <UserLoginState>'success';
+ } else {
+ this.token = null;
+ this.userInfoSubject.next(null);
+ return <UserLoginState>'invalidlogin';
+ }
+ })
+ );
+ }
+
+ tryLogin(credentials: UserCredentials): Observable<UserInfo> {
+ if (this.token) {
+ return throwError(new AlreadyLoginError());
+ }
+
+ return this.httpClient.post<CreateTokenResponse>(createTokenUrl, <CreateTokenRequest>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.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 @@
-<div [ngSwitch]="state" class="container">
- <mat-progress-spinner *ngSwitchCase="'loading'" mode="indeterminate" diameter="50"></mat-progress-spinner>
- <app-user-login *ngSwitchCase="'login'" (login)="login($event)" [message]="loginMessage"></app-user-login>
- <app-user-login-success *ngSwitchCase="'success'" [userInfo]="userInfo" [displayLoginSuccessMessage]="displayLoginSuccessMessage"></app-user-login-success>
+<div class="container">
+ <mat-progress-spinner *ngIf="isLoading" mode="indeterminate" diameter="50"></mat-progress-spinner>
+ <router-outlet name="user"></router-outlet>
</div>
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..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
@@ -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 '../user-info';
import { UserDialogComponent } from './user-dialog.component';
-import { UserService, UserLoginState } from '../user-service/user.service';
-import { LoginEvent } from '../user-login/user-login.component';
+import { createMockInternalUserService } from '../internal-user-service/internal-user.service.mock';
+import { InternalUserService, UserLoginState } from '../internal-user-service/internal-user.service';
@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<LoginEvent>();
-}
+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<UserDialogComponent>;
- let mockUserService: jasmine.SpyObj<UserService>;
+ let mockInternalUserService: jasmine.SpyObj<InternalUserService>;
+
beforeEach(async(() => {
- mockUserService = jasmine.createSpyObj('UserService', ['validateUserLoginState', 'tryLogin']);
+ mockInternalUserService = createMockInternalUserService();
TestBed.configureTestingModule({
- declarations: [UserDialogComponent, MatProgressSpinnerStubComponent,
- UserLoginStubComponent, UserLoginSuccessStubComponent],
- providers: [{ provide: UserService, useValue: mockUserService }]
+ declarations: [UserDialogComponent, MatProgressSpinnerStubComponent, RouterOutletStubComponent],
+ providers: [{ provide: InternalUserService, useValue: mockInternalUserService },
+ { // for the workaround
+ provide: Router, useValue: {
+ events: new Observable<Event>()
+ }
+ }]
})
.compileComponents();
}));
@@ -57,7 +51,7 @@ describe('UserDialogComponent', () => {
});
it('progress spinner should work well', fakeAsync(() => {
- mockUserService.validateUserLoginState.and.returnValue(of(<UserLoginState>{ state: 'nologin' }).pipe(delay(10)));
+ mockInternalUserService.refreshAndGetUserState.and.returnValue(of(<UserLoginState>'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.validateUserLoginState.and.returnValue(of(<UserLoginState>{ state: 'nologin' }));
+ mockInternalUserService.refreshAndGetUserState.and.returnValue(of(<UserLoginState>'nologin'));
fixture.detectChanges();
- expect(mockUserService.validateUserLoginState).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.validateUserLoginState.and.returnValue(of(<UserLoginState>{ state: 'success', userInfo: {} }));
+ it('invalid login should work well', () => {
+ mockInternalUserService.refreshAndGetUserState.and.returnValue(of(<UserLoginState>'invalidlogin'));
fixture.detectChanges();
- expect(mockUserService.validateUserLoginState).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.validateUserLoginState.and.returnValue(of(<UserLoginState>{ state: 'nologin' }));
-
- fixture.detectChanges();
- expect(mockUserService.validateUserLoginState).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(<UserInfo>{
- username: 'user',
- roles: ['user']
- }));
-
- (fixture.debugElement.query(By.css('app-user-login')).componentInstance as
- UserLoginStubComponent).login.emit(<LoginEvent>{
- username: 'user',
- password: 'user'
- });
+ it('success should work well', () => {
+ mockInternalUserService.refreshAndGetUserState.and.returnValue(of(<UserLoginState>'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-dialog/user-dialog.component.ts b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts
index 7511de16..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,43 +1,40 @@
-import { Component, OnInit } from '@angular/core';
-import { UserInfo } from '../user-info';
-import { UserService } from '../user-service/user.service';
-import { LoginEvent, LoginMessage } from '../user-login/user-login.component';
+import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
+import { InternalUserService } from '../internal-user-service/internal-user.service';
+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: InternalUserService, private router: Router) { }
- state: 'loading' | 'login' | 'success' = 'loading';
+ @ViewChild(RouterOutlet) outlet: RouterOutlet;
- loginMessage: LoginMessage;
-
- displayLoginSuccessMessage = false;
- userInfo: UserInfo;
+ isLoading = true;
ngOnInit() {
- this.userService.validateUserLoginState().subscribe(result => {
- if (result.state === 'success') {
- this.userInfo = result.userInfo;
- this.state = 'success';
+ // 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.refreshAndGetUserState().subscribe(result => {
+ this.isLoading = false;
+ if (result === 'success') {
+ this.userService.userRouteNavigate(['success', { reason: 'already' }]);
} else {
- this.loginMessage = result.state;
- this.state = 'login';
+ this.userService.userRouteNavigate(['login', { reason: result }]);
}
});
}
- 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.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<UserLoginSuccessComponent>;
+ let mockInternalUserService: jasmine.SpyObj<InternalUserService>;
+ 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.
+ (<any>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-success/user-login-success.component.ts b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts
index 99de5970..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,5 +1,8 @@
import { Component, OnInit, Input } from '@angular/core';
-import { UserInfo } from '../user-info';
+import { ActivatedRoute } from '@angular/router';
+
+import { UserInfo } from '../entities';
+import { InternalUserService } from '../internal-user-service/internal-user.service';
@Component({
selector: 'app-user-login-success',
@@ -8,15 +11,14 @@ import { UserInfo } from '../user-info';
})
export class UserLoginSuccessComponent implements OnInit {
- @Input()
displayLoginSuccessMessage = false;
- @Input()
userInfo: UserInfo;
- constructor() { }
+ constructor(private route: ActivatedRoute, private userService: InternalUserService) { }
ngOnInit() {
+ 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.spec.ts b/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts
index acd13721..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,17 +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 { 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<UserLoginComponent>;
+ let mockInternalUserService: jasmine.SpyObj<InternalUserService>;
+ let mockActivatedRoute: MockActivatedRoute;
beforeEach(async(() => {
+ mockInternalUserService = createMockInternalUserService();
+ mockActivatedRoute = new MockActivatedRoute();
+
TestBed.configureTestingModule({
declarations: [UserLoginComponent],
+ providers: [
+ { provide: InternalUserService, useValue: mockInternalUserService },
+ { provide: ActivatedRoute, useValue: mockActivatedRoute }
+ ],
imports: [ReactiveFormsModule],
schemas: [NO_ERRORS_SCHEMA]
})
@@ -21,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;
@@ -45,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(<UserInfo>{ 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 da642cb8..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,32 +1,35 @@
-import { Component, Output, OnInit, EventEmitter, Input } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
+import { ActivatedRoute } from '@angular/router';
+
+import { InternalUserService } from '../internal-user-service/internal-user.service';
export type LoginMessage = 'nologin' | 'invalidlogin' | string;
-export class LoginEvent {
- username: string;
- password: string;
-}
@Component({
selector: 'app-user-login',
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: InternalUserService) { }
- @Output()
- login = new EventEmitter<LoginEvent>();
+ message: LoginMessage;
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
deleted file mode 100644
index 0095f031..00000000
--- a/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-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 {
- UserService, UserCredentials, CreateTokenResult,
- UserLoginState, TokenValidationRequest, TokenValidationResult
-} from './user.service';
-
-describe('UserService', () => {
- const tokenCreateUrl = '/api/User/CreateToken';
-
- 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('user', 'user').subscribe(result => {
- expect(result).toEqual(mockUserInfo);
- });
-
- const httpController = TestBed.get(HttpTestingController) as HttpTestingController;
-
- httpController.expectOne((request: HttpRequest<UserCredentials>) =>
- request.url === tokenCreateUrl &&
- request.body.username === 'user' &&
- request.body.password === 'user').flush(<CreateTokenResult>{
- 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<TokenValidationRequest>) => {
- return req.url === '/api/User/ValidateToken' && req.body.token === mockToken;
- };
-
- beforeEach(() => {
- service = TestBed.get(UserService);
- httpController = TestBed.get(HttpTestingController);
-
- service.tryLogin('user', 'user').subscribe(); // subscribe to activate login
-
- httpController.expectOne(tokenCreateUrl).flush(<CreateTokenResult>{
- token: mockToken,
- userInfo: mockUserInfo
- });
- });
-
- it('success should work well', () => {
- service.validateUserLoginState().subscribe((result: UserLoginState) => {
- expect(result).toEqual(<UserLoginState>{
- state: 'success',
- userInfo: mockUserInfo
- });
- });
-
- httpController.expectOne(tokenValidateRequestMatcher).flush(<TokenValidationResult>{
- isValid: true,
- userInfo: mockUserInfo
- });
-
- httpController.verify();
- });
-
- it('invalid should work well', () => {
- service.validateUserLoginState().subscribe((result: UserLoginState) => {
- expect(result).toEqual(<UserLoginState>{
- state: 'invalidlogin'
- });
- });
-
- httpController.expectOne(tokenValidateRequestMatcher).flush(<TokenValidationResult>{
- 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 009e5292..00000000
--- a/Timeline/ClientApp/src/app/user/user-service/user.service.ts
+++ /dev/null
@@ -1,116 +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 { UserInfo } from '../user-info';
-
-export interface UserCredentials {
- username: string;
- password: string;
-}
-
-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;
- private userInfo: UserInfo;
-
- constructor(private httpClient: HttpClient) { }
-
- validateUserLoginState(): Observable<UserLoginState> {
- if (this.token === undefined || this.token === null) {
- return of(<UserLoginState>{ state: 'nologin' });
- }
-
- return this.httpClient.post<TokenValidationResult>('/api/User/ValidateToken', <TokenValidationRequest>{ 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 <UserLoginState>{
- state: 'success',
- userInfo: result.userInfo
- };
- } else {
- this.token = null;
- this.userInfo = null;
- return <UserLoginState>{
- state: 'invalidlogin'
- };
- }
- })
- );
- }
-
- tryLogin(username: string, password: string): Observable<UserInfo> {
- if (this.token) {
- return throwError(new AlreadyLoginException());
- }
-
- return this.httpClient.post<CreateTokenResult>('/api/User/CreateToken', <UserCredentials>{
- username, password
- }).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.module.ts b/Timeline/ClientApp/src/app/user/user.module.ts
index 67de90a2..c399c9e0 100644
--- a/Timeline/ClientApp/src/app/user/user.module.ts
+++ b/Timeline/ClientApp/src/app/user/user.module.ts
@@ -10,15 +10,22 @@ 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';
@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 { }
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<UserInfo | null> {
+ return this.internalService.userInfo$;
+ }
+
+ openUserDialog() {
+ this.dialog.open(UserDialogComponent, {
+ width: '300px'
+ });
+ }
+}
diff --git a/Timeline/ClientApp/src/app/utility/debounce-click.directive.spec.ts b/Timeline/ClientApp/src/app/utilities/debounce-click.directive.spec.ts
index 75710d0c..75710d0c 100644
--- a/Timeline/ClientApp/src/app/utility/debounce-click.directive.spec.ts
+++ b/Timeline/ClientApp/src/app/utilities/debounce-click.directive.spec.ts
diff --git a/Timeline/ClientApp/src/app/utility/debounce-click.directive.ts b/Timeline/ClientApp/src/app/utilities/debounce-click.directive.ts
index feb0404e..feb0404e 100644
--- a/Timeline/ClientApp/src/app/utility/debounce-click.directive.ts
+++ b/Timeline/ClientApp/src/app/utilities/debounce-click.directive.ts
diff --git a/Timeline/ClientApp/src/app/utility/utility.module.ts b/Timeline/ClientApp/src/app/utilities/utility.module.ts
index dd686bf7..dd686bf7 100644
--- a/Timeline/ClientApp/src/app/utility/utility.module.ts
+++ b/Timeline/ClientApp/src/app/utilities/utility.module.ts
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"
]
}