aboutsummaryrefslogtreecommitdiff
path: root/Timeline/ClientApp/src/app/user/internal-user-service
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
commit8caef17dd3e455de27f44d13751c27ee4dfe2e1e (patch)
tree5b485ad438c9be9c180d425453588ff1c575a42d /Timeline/ClientApp/src/app/user/internal-user-service
parent17d90077b289c6b2203a34de727dd77c1985f146 (diff)
parentb26342764046d188d223aa494c3bbbf76deb4927 (diff)
downloadtimeline-8caef17dd3e455de27f44d13751c27ee4dfe2e1e.tar.gz
timeline-8caef17dd3e455de27f44d13751c27ee4dfe2e1e.tar.bz2
timeline-8caef17dd3e455de27f44d13751c27ee4dfe2e1e.zip
Merge pull request #11 from crupest/7-user-route
Use named route in user dialog.
Diffstat (limited to 'Timeline/ClientApp/src/app/user/internal-user-service')
-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
5 files changed, 262 insertions, 0 deletions
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;
+ })
+ );
+ }
+}