aboutsummaryrefslogtreecommitdiff
path: root/Timeline/ClientApp/src/app/user/internal-user-service
diff options
context:
space:
mode:
author杨宇千 <crupest@outlook.com>2019-04-11 20:02:33 +0800
committerGitHub <noreply@github.com>2019-04-11 20:02:33 +0800
commit5b5ca3acb1b9decb5ad13798dc79ba2d58f2ce95 (patch)
tree695f7fc0bd2f6d940f64739a1f1f500c36806cef /Timeline/ClientApp/src/app/user/internal-user-service
parent1c9edc5914869a3bbde20742c483182636ee4d43 (diff)
parentc28941c6d86f8ea33521bba49d811bf3ff60b3d1 (diff)
downloadtimeline-5b5ca3acb1b9decb5ad13798dc79ba2d58f2ce95.tar.gz
timeline-5b5ca3acb1b9decb5ad13798dc79ba2d58f2ce95.tar.bz2
timeline-5b5ca3acb1b9decb5ad13798dc79ba2d58f2ce95.zip
Merge pull request #17 from crupest/15-user
Remember me and log out feature.
Diffstat (limited to 'Timeline/ClientApp/src/app/user/internal-user-service')
-rw-r--r--Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts168
-rw-r--r--Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts124
2 files changed, 174 insertions, 118 deletions
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 613a8fa6..6906ed60 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,115 +1,121 @@
-import { TestBed } from '@angular/core/testing';
+import { TestBed, fakeAsync, tick } from '@angular/core/testing';
import { HttpRequest } from '@angular/common/http';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { Router } from '@angular/router';
+import { MatSnackBar } from '@angular/material';
+
+import { Mock } from 'src/app/test-utilities/mock';
+import { createMockStorage } from 'src/app/test-utilities/storage.mock';
+import { WINDOW } from '../window-inject-token';
import { UserInfo, UserCredentials } from '../entities';
import {
createTokenUrl, validateTokenUrl, CreateTokenRequest,
CreateTokenResponse, ValidateTokenRequest, ValidateTokenResponse
} from './http-entities';
-import { InternalUserService, UserLoginState } from './internal-user.service';
+import { InternalUserService, SnackBarTextKey, snackBarText, TOKEN_STORAGE_KEY } from './internal-user.service';
+import { repeat } from 'src/app/utilities/language-untilities';
-describe('InternalUserService', () => {
- const mockUserCredentials: UserCredentials = {
- username: 'user',
- password: 'user'
- };
- beforeEach(() => TestBed.configureTestingModule({
- imports: [HttpClientTestingModule],
- providers: [{ provide: Router, useValue: null }]
- }));
+describe('InternalUserService', () => {
+ let mockLocalStorage: Mock<Storage>;
+ let mockSnackBar: jasmine.SpyObj<MatSnackBar>;
+
+ beforeEach(() => {
+ mockLocalStorage = createMockStorage();
+ mockSnackBar = jasmine.createSpyObj('MatSnackBar', ['open']);
+ TestBed.configureTestingModule({
+ imports: [HttpClientTestingModule],
+ providers: [
+ { provide: WINDOW, useValue: { localStorage: mockLocalStorage } },
+ { provide: Router, useValue: null },
+ { provide: MatSnackBar, useValue: mockSnackBar }
+ ]
+ });
+ });
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;
+ const mockUserInfo: UserInfo = {
+ username: 'user',
+ roles: ['user', 'other']
+ };
- httpController.expectOne((request: HttpRequest<CreateTokenRequest>) =>
- request.url === createTokenUrl && request.body !== null &&
- request.body.username === 'user' &&
- request.body.password === 'user').flush(<CreateTokenResponse>{
- token: 'test-token',
- userInfo: mockUserInfo
+ const mockToken = 'mock-token';
+
+ describe('validate token', () => {
+ const validateTokenRequestMatcher = (req: HttpRequest<ValidateTokenRequest>): boolean =>
+ req.url === validateTokenUrl && req.body !== null && req.body.token === mockToken;
+
+ function createTest(
+ expectSnackBarTextKey: SnackBarTextKey,
+ setStorageToken: boolean,
+ setHttpController?: (controller: HttpTestingController) => void
+ ): () => void {
+ return fakeAsync(() => {
+ if (setStorageToken) {
+ mockLocalStorage.setItem(TOKEN_STORAGE_KEY, mockToken);
+ }
+ TestBed.get(InternalUserService);
+ const controller = TestBed.get(HttpTestingController) as HttpTestingController;
+ if (setHttpController) {
+ setHttpController(controller);
+ }
+ controller.verify();
+ tick();
+ expect(mockSnackBar.open).toHaveBeenCalledWith(snackBarText[expectSnackBarTextKey], jasmine.anything(), jasmine.anything());
});
-
- expect(service.currentUserInfo).toEqual(mockUserInfo);
-
- httpController.verify();
+ }
+
+ it('no login should work well', createTest('noLogin', false));
+ it('already login should work well', createTest('alreadyLogin', true,
+ controller => controller.expectOne(validateTokenRequestMatcher).flush(
+ <ValidateTokenResponse>{ isValid: true, userInfo: mockUserInfo })));
+ it('invalid login should work well', createTest('invalidLogin', true,
+ controller => controller.expectOne(validateTokenRequestMatcher).flush(<ValidateTokenResponse>{ isValid: false })));
+ it('check fail should work well', createTest('checkFail', true,
+ controller => repeat(4, () => {
+ controller.expectOne(validateTokenRequestMatcher).error(new ErrorEvent('Network error', { message: 'simulated network error' }));
+ })));
});
- describe('validateUserLoginState', () => {
- let service: InternalUserService;
- let httpController: HttpTestingController;
-
- const mockUserInfo: UserInfo = {
+ describe('login should work well', () => {
+ const mockUserCredentials: UserCredentials = {
username: 'user',
- roles: ['user', 'other']
+ password: 'user'
};
- const mockToken = 'mock-token';
+ function createTest(rememberMe: boolean) {
+ return () => {
+ const service: InternalUserService = TestBed.get(InternalUserService);
- const tokenValidateRequestMatcher = (req: HttpRequest<ValidateTokenRequest>) => {
- return req.url === validateTokenUrl && req.body !== null && req.body.token === mockToken;
- };
+ service.tryLogin({ ...mockUserCredentials, rememberMe: rememberMe }).subscribe(result => {
+ expect(result).toEqual(mockUserInfo);
+ });
- beforeEach(() => {
- service = TestBed.get(InternalUserService);
- httpController = TestBed.get(HttpTestingController);
+ const httpController = TestBed.get(HttpTestingController) as HttpTestingController;
- service.tryLogin(mockUserCredentials).subscribe(); // subscribe to activate login
+ httpController.expectOne((request: HttpRequest<CreateTokenRequest>) =>
+ request.url === createTokenUrl && request.body !== null &&
+ request.body.username === mockUserCredentials.username &&
+ request.body.password === mockUserCredentials.password).flush(<CreateTokenResponse>{
+ token: mockToken,
+ userInfo: mockUserInfo
+ });
- httpController.expectOne(createTokenUrl).flush(<CreateTokenResponse>{
- token: mockToken,
- userInfo: mockUserInfo
- });
- });
+ expect(service.currentUserInfo).toEqual(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.verify();
- httpController.expectOne(tokenValidateRequestMatcher).flush(<ValidateTokenResponse>{ isValid: false });
+ expect(mockLocalStorage.getItem(TOKEN_STORAGE_KEY)).toBe(rememberMe ? mockToken : null);
+ };
+ }
- httpController.verify();
- });
+ it('remember me should work well', createTest(true));
+ it('not remember me should work well', createTest(false));
});
// 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
index 4767bd16..6de355f2 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
@@ -1,11 +1,9 @@
-import { Injectable } from '@angular/core';
+import { Injectable, Inject } 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 { nullIfUndefined } from '../../utilities/language-untilities';
+import { Observable, throwError, BehaviorSubject, of } from 'rxjs';
+import { map, catchError, retry, switchMap, tap, filter } from 'rxjs/operators';
import { AlreadyLoginError, BadCredentialsError, BadNetworkError, UnknownError } from './errors';
import {
@@ -13,9 +11,24 @@ import {
CreateTokenResponse, ValidateTokenRequest, ValidateTokenResponse
} from './http-entities';
import { UserCredentials, UserInfo } from '../entities';
+import { MatSnackBar } from '@angular/material';
+import { WINDOW } from '../window-inject-token';
+
+export const snackBarText = {
+ checkFail: 'Failed to check last login',
+ noLogin: 'No login before!',
+ alreadyLogin: 'You have login already!',
+ invalidLogin: 'Last login is no longer invalid!',
+ ok: 'ok'
+};
+export type SnackBarTextKey = Exclude<keyof typeof snackBarText, 'ok'>;
-export type UserLoginState = 'nologin' | 'invalidlogin' | 'success';
+export const TOKEN_STORAGE_KEY = 'token';
+
+export interface LoginInfo extends UserCredentials {
+ rememberMe: boolean;
+}
/**
* This service is only used internal in user module.
@@ -26,56 +39,80 @@ export type UserLoginState = 'nologin' | 'invalidlogin' | 'success';
export class InternalUserService {
private token: string | null = null;
- private userInfoSubject = new BehaviorSubject<UserInfo | null>(null);
+ private userInfoSubject = new BehaviorSubject<UserInfo | null | undefined>(undefined);
+
+ readonly userInfo$: Observable<UserInfo | null> =
+ <Observable<UserInfo | null>>this.userInfoSubject.pipe(filter(value => value !== undefined));
- get currentUserInfo(): UserInfo | null {
+ get currentUserInfo(): UserInfo | null | undefined {
return this.userInfoSubject.value;
}
- get userInfo$(): Observable<UserInfo | null> {
- return this.userInfoSubject;
+ private openSnackBar(snackBar: MatSnackBar, textKey: SnackBarTextKey) {
+ setTimeout(() => snackBar.open(snackBarText[textKey], snackBarText.ok, { duration: 2000 }), 0);
}
- constructor(private httpClient: HttpClient, private router: Router) { }
-
- userRouteNavigate(commands: any[] | null) {
- this.router.navigate([{
- outlets: {
- user: commands
- }
- }]);
- }
-
- refreshAndGetUserState(): Observable<UserLoginState> {
- if (this.token === undefined || this.token === null) {
- return of(<UserLoginState>'nologin');
+ constructor(@Inject(WINDOW) private window: Window, private httpClient: HttpClient, private router: Router, snackBar: MatSnackBar) {
+ const savedToken = this.window.localStorage.getItem(TOKEN_STORAGE_KEY);
+ if (savedToken === null) {
+ this.openSnackBar(snackBar, 'noLogin');
+ this.userInfoSubject.next(null);
+ } else {
+ this.validateToken(savedToken).subscribe(result => {
+ if (result === null) {
+ this.window.localStorage.removeItem(TOKEN_STORAGE_KEY);
+ this.openSnackBar(snackBar, 'invalidLogin');
+ this.userInfoSubject.next(null);
+ } else {
+ this.token = savedToken;
+ this.userInfoSubject.next(result);
+ this.openSnackBar(snackBar, 'alreadyLogin');
+ }
+ }, _ => {
+ this.openSnackBar(snackBar, 'checkFail');
+ this.userInfoSubject.next(null);
+ });
}
+ }
- return this.httpClient.post<ValidateTokenResponse>(validateTokenUrl, <ValidateTokenRequest>{ token: this.token }).pipe(
+ private validateToken(token: string): Observable<UserInfo | null> {
+ return this.httpClient.post<ValidateTokenResponse>(validateTokenUrl, <ValidateTokenRequest>{ token: token }).pipe(
retry(3),
- catchError(error => {
- console.error('Failed to validate token.');
- return throwError(error);
- }),
- map(result => {
+ switchMap(result => {
if (result.isValid) {
- this.userInfoSubject.next(nullIfUndefined(result.userInfo));
- return <UserLoginState>'success';
+ const { userInfo } = result;
+ if (userInfo) {
+ return of(userInfo);
+ } else {
+ return throwError(new Error('Wrong server response. IsValid is true but UserInfo is null.'));
+ }
} else {
- this.token = null;
- this.userInfoSubject.next(null);
- return <UserLoginState>'invalidlogin';
+ return of(null);
}
- })
+ }),
+ tap({
+ error: error => {
+ console.error('Failed to validate token.');
+ console.error(error);
+ }
+ }),
);
}
- tryLogin(credentials: UserCredentials): Observable<UserInfo> {
+ userRouteNavigate(commands: any[] | null) {
+ this.router.navigate([{
+ outlets: {
+ user: commands
+ }
+ }]);
+ }
+
+ tryLogin(info: LoginInfo): Observable<UserInfo> {
if (this.token) {
return throwError(new AlreadyLoginError());
}
- return this.httpClient.post<CreateTokenResponse>(createTokenUrl, <CreateTokenRequest>credentials).pipe(
+ return this.httpClient.post<CreateTokenResponse>(createTokenUrl, <CreateTokenRequest>info).pipe(
catchError((error: HttpErrorResponse) => {
if (error.error instanceof ErrorEvent) {
console.error('An error occurred when login: ' + error.error.message);
@@ -90,9 +127,22 @@ export class InternalUserService {
}),
map(result => {
this.token = result.token;
+ if (info.rememberMe) {
+ this.window.localStorage.setItem(TOKEN_STORAGE_KEY, result.token);
+ }
this.userInfoSubject.next(result.userInfo);
return result.userInfo;
})
);
}
+
+ logout() {
+ if (this.currentUserInfo === null) {
+ throw new Error('No login now. You can\'t logout.');
+ }
+
+ this.window.localStorage.removeItem(TOKEN_STORAGE_KEY);
+ this.token = null;
+ this.userInfoSubject.next(null);
+ }
}