From cfc1a24bcb782721780eb79cc45260db16ffad64 Mon Sep 17 00:00:00 2001
From: crupest
Date: Sat, 16 Mar 2019 20:57:31 +0800
Subject: Add remember me.
---
.../src/app/test-utilities/activated-route.mock.ts | 10 ++-
.../internal-user-service/internal-user.service.ts | 81 +++++++++++++---------
Timeline/ClientApp/src/app/user/user.module.ts | 4 +-
.../src/app/utilities/language-untilities.ts | 4 +-
4 files changed, 61 insertions(+), 38 deletions(-)
(limited to 'Timeline/ClientApp/src')
diff --git a/Timeline/ClientApp/src/app/test-utilities/activated-route.mock.ts b/Timeline/ClientApp/src/app/test-utilities/activated-route.mock.ts
index 1743e615..72707c5e 100644
--- a/Timeline/ClientApp/src/app/test-utilities/activated-route.mock.ts
+++ b/Timeline/ClientApp/src/app/test-utilities/activated-route.mock.ts
@@ -1,11 +1,15 @@
-import { ParamMap } from '@angular/router';
+import { ParamMap, ActivatedRouteSnapshot, ActivatedRoute } from '@angular/router';
import { Observable, BehaviorSubject } from 'rxjs';
import { map } from 'rxjs/operators';
+export type PartialMock = {
+ [P in keyof T]?: T[P] | PartialMock;
+};
+
export interface ParamMapCreator { [name: string]: string | string[]; }
-export class MockActivatedRouteSnapshot {
+export class MockActivatedRouteSnapshot implements PartialMock {
private paramMapInternal: ParamMap;
@@ -44,7 +48,7 @@ export class MockActivatedRouteSnapshot {
}
}
-export class MockActivatedRoute {
+export class MockActivatedRoute implements PartialMock {
snapshot$ = new BehaviorSubject(new MockActivatedRouteSnapshot());
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..604393f4 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
@@ -2,10 +2,8 @@ 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 { nullIfUndefined } from '../../utilities/language-untilities';
+import { Observable, throwError, BehaviorSubject, of } from 'rxjs';
+import { map, catchError, retry, switchMap, tap } from 'rxjs/operators';
import { AlreadyLoginError, BadCredentialsError, BadNetworkError, UnknownError } from './errors';
import {
@@ -13,10 +11,9 @@ import {
CreateTokenResponse, ValidateTokenRequest, ValidateTokenResponse
} from './http-entities';
import { UserCredentials, UserInfo } from '../entities';
+import { MatSnackBar } from '@angular/material';
-export type UserLoginState = 'nologin' | 'invalidlogin' | 'success';
-
/**
* This service is only used internal in user module.
*/
@@ -36,41 +33,60 @@ export class InternalUserService {
return this.userInfoSubject;
}
- constructor(private httpClient: HttpClient, private router: Router) { }
+ constructor(private httpClient: HttpClient, private router: Router, private snackBar: MatSnackBar) {
+ const savedToken = window.localStorage.getItem('token');
+ if (savedToken === null) {
+ setTimeout(() => snackBar.open('No login before!', 'ok', { duration: 2000 }), 0);
+ } else {
+ this.validateToken(savedToken).subscribe(result => {
+ if (result === null) {
+ window.localStorage.removeItem('token');
+ setTimeout(() => snackBar.open('Last login is no longer invalid!', 'ok', { duration: 2000 }), 0);
+ } else {
+ this.token = savedToken;
+ this.userInfoSubject.next(result);
+ setTimeout(() => snackBar.open('You have login already!', 'ok', { duration: 2000 }), 0);
+ }
+ }, _ => {
+ setTimeout(() => snackBar.open('Failed to check last login', 'ok', { duration: 2000 }), 0);
+ });
+ }
- userRouteNavigate(commands: any[] | null) {
- this.router.navigate([{
- outlets: {
- user: commands
- }
- }]);
}
- refreshAndGetUserState(): Observable {
- if (this.token === undefined || this.token === null) {
- return of('nologin');
- }
-
- return this.httpClient.post(validateTokenUrl, { token: this.token }).pipe(
+ private validateToken(token: string): Observable {
+ return this.httpClient.post(validateTokenUrl, { 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 '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 'invalidlogin';
+ return of(null);
}
- })
+ }),
+ tap({
+ error: error => {
+ console.error('Failed to validate token.');
+ console.error(error);
+ }
+ }),
);
}
- tryLogin(credentials: UserCredentials): Observable {
+ userRouteNavigate(commands: any[] | null) {
+ this.router.navigate([{
+ outlets: {
+ user: commands
+ }
+ }]);
+ }
+
+ tryLogin(credentials: UserCredentials, options: { remember: boolean } = { remember: true }): Observable {
if (this.token) {
return throwError(new AlreadyLoginError());
}
@@ -90,6 +106,9 @@ export class InternalUserService {
}),
map(result => {
this.token = result.token;
+ if (options.remember) {
+ window.localStorage.setItem('token', result.token);
+ }
this.userInfoSubject.next(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 8f3b9a9c..7645d61d 100644
--- a/Timeline/ClientApp/src/app/user/user.module.ts
+++ b/Timeline/ClientApp/src/app/user/user.module.ts
@@ -6,7 +6,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule } from '@angular/router';
import {
MatFormFieldModule, MatProgressSpinnerModule,
- MatDialogModule, MatInputModule, MatButtonModule
+ MatDialogModule, MatInputModule, MatButtonModule, MatSnackBarModule
} from '@angular/material';
import { RequireNoLoginGuard, RequireLoginGuard } from './auth.guard';
@@ -25,7 +25,7 @@ import { UtilityModule } from '../utilities/utility.module';
{ path: '**', component: RedirectComponent, outlet: 'user' }
]),
CommonModule, HttpClientModule, ReactiveFormsModule, BrowserAnimationsModule,
- MatFormFieldModule, MatProgressSpinnerModule, MatDialogModule, MatInputModule, MatButtonModule,
+ MatFormFieldModule, MatProgressSpinnerModule, MatDialogModule, MatInputModule, MatButtonModule, MatSnackBarModule,
UtilityModule
],
exports: [RouterModule],
diff --git a/Timeline/ClientApp/src/app/utilities/language-untilities.ts b/Timeline/ClientApp/src/app/utilities/language-untilities.ts
index be9df2dc..7f38f3e4 100644
--- a/Timeline/ClientApp/src/app/utilities/language-untilities.ts
+++ b/Timeline/ClientApp/src/app/utilities/language-untilities.ts
@@ -3,9 +3,9 @@ export function nullIfUndefined(value: T | undefined): T | null {
}
export function throwIfNullOrUndefined(value: T | null | undefined,
- lazyMessage: () => string = () => 'Value mustn\'t be falsy'): T | never {
+ message: string | (() => string) = 'Value mustn\'t be null or undefined'): T | never {
if (value === null || value === undefined) {
- throw new Error(lazyMessage());
+ throw new Error(typeof message === 'string' ? message : message());
} else {
return value;
}
--
cgit v1.2.3
From ec9efc7edc3133459612e6e799e68a454e8148ba Mon Sep 17 00:00:00 2001
From: crupest
Date: Sun, 17 Mar 2019 21:57:24 +0800
Subject: Add unit test.
---
.../src/app/test-utilities/activated-route.mock.ts | 4 +-
Timeline/ClientApp/src/app/test-utilities/mock.ts | 7 +
.../src/app/test-utilities/storage.mock.ts | 28 ++++
.../internal-user.service.spec.ts | 149 ++++++++++-----------
.../internal-user-service/internal-user.service.ts | 35 +++--
Timeline/ClientApp/src/app/user/user.module.ts | 2 +
.../ClientApp/src/app/user/window-inject-token.ts | 3 +
.../src/app/utilities/language-untilities.ts | 6 +
Timeline/ClientApp/src/tsconfig.app.json | 1 +
Timeline/ClientApp/src/tsconfig.spec.json | 1 +
10 files changed, 148 insertions(+), 88 deletions(-)
create mode 100644 Timeline/ClientApp/src/app/test-utilities/mock.ts
create mode 100644 Timeline/ClientApp/src/app/test-utilities/storage.mock.ts
create mode 100644 Timeline/ClientApp/src/app/user/window-inject-token.ts
(limited to 'Timeline/ClientApp/src')
diff --git a/Timeline/ClientApp/src/app/test-utilities/activated-route.mock.ts b/Timeline/ClientApp/src/app/test-utilities/activated-route.mock.ts
index 72707c5e..40484387 100644
--- a/Timeline/ClientApp/src/app/test-utilities/activated-route.mock.ts
+++ b/Timeline/ClientApp/src/app/test-utilities/activated-route.mock.ts
@@ -3,9 +3,7 @@ import { ParamMap, ActivatedRouteSnapshot, ActivatedRoute } from '@angular/route
import { Observable, BehaviorSubject } from 'rxjs';
import { map } from 'rxjs/operators';
-export type PartialMock = {
- [P in keyof T]?: T[P] | PartialMock;
-};
+import { PartialMock } from './mock';
export interface ParamMapCreator { [name: string]: string | string[]; }
diff --git a/Timeline/ClientApp/src/app/test-utilities/mock.ts b/Timeline/ClientApp/src/app/test-utilities/mock.ts
new file mode 100644
index 00000000..c3e368f0
--- /dev/null
+++ b/Timeline/ClientApp/src/app/test-utilities/mock.ts
@@ -0,0 +1,7 @@
+export type Mock = {
+ [P in keyof T]: T[P] extends Function ? T[P] : T[P] | Mock;
+};
+
+export type PartialMock = {
+ [P in keyof T]?: T[P] extends Function ? T[P] : T[P] | PartialMock | Mock;
+};
diff --git a/Timeline/ClientApp/src/app/test-utilities/storage.mock.ts b/Timeline/ClientApp/src/app/test-utilities/storage.mock.ts
new file mode 100644
index 00000000..0ba5aa35
--- /dev/null
+++ b/Timeline/ClientApp/src/app/test-utilities/storage.mock.ts
@@ -0,0 +1,28 @@
+import { Mock } from './mock';
+import { nullIfUndefined } from '../utilities/language-untilities';
+
+export function createMockStorage(): Mock {
+ const map: { [key: string]: string } = {};
+ return {
+ get length(): number {
+ return Object.keys(map).length;
+ },
+ key(index: number): string | null {
+ const keys = Object.keys(map);
+ if (index >= keys.length) { return null; }
+ return keys[index];
+ },
+ clear() {
+ Object.keys(map).forEach(key => delete map.key);
+ },
+ getItem(key: string): string | null {
+ return nullIfUndefined(map[key]);
+ },
+ setItem(key: string, value: string) {
+ map[key] = value;
+ },
+ removeItem(key: string) {
+ delete map[key];
+ }
+ };
+}
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..be6631eb 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,47 +1,95 @@
-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 { HttpClientTestingModule, HttpTestingController, TestRequest } 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;
+ let mockSnackBar: jasmine.SpyObj;
+
+ 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');
- });
+ const mockUserInfo: UserInfo = {
+ username: 'user',
+ roles: ['user', 'other']
+ };
+
+ const mockToken = 'mock-token';
+
+ describe('validate token', () => {
+ const validateTokenRequestMatcher = (req: HttpRequest): 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());
+ });
+ }
+
+ it('no login should work well', createTest('noLogin', false));
+ it('already login should work well', createTest('alreadyLogin', true,
+ controller => controller.expectOne(validateTokenRequestMatcher).flush(
+ { isValid: true, userInfo: mockUserInfo })));
+ it('invalid login should work well', createTest('invalidLogin', true,
+ controller => controller.expectOne(validateTokenRequestMatcher).flush({ 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' }));
+ })));
});
it('login should work well', () => {
- const service: InternalUserService = TestBed.get(InternalUserService);
-
- const mockUserInfo: UserInfo = {
+ const mockUserCredentials: UserCredentials = {
username: 'user',
- roles: ['user', 'other']
+ password: 'user'
};
+ const service: InternalUserService = TestBed.get(InternalUserService);
+
service.tryLogin(mockUserCredentials).subscribe(result => {
expect(result).toEqual(mockUserInfo);
});
@@ -50,66 +98,17 @@ describe('InternalUserService', () => {
httpController.expectOne((request: HttpRequest) =>
request.url === createTokenUrl && request.body !== null &&
- request.body.username === 'user' &&
- request.body.password === 'user').flush({
- token: 'test-token',
+ request.body.username === mockUserCredentials.username &&
+ request.body.password === mockUserCredentials.password).flush({
+ token: mockToken,
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) => {
- return req.url === validateTokenUrl && req.body !== null && 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({
- token: mockToken,
- userInfo: mockUserInfo
- });
- });
- it('success should work well', () => {
- service.refreshAndGetUserState().subscribe((result: UserLoginState) => {
- expect(result).toEqual('success');
- });
-
- httpController.expectOne(tokenValidateRequestMatcher).flush({
- isValid: true,
- userInfo: mockUserInfo
- });
-
- httpController.verify();
- });
-
- it('invalid should work well', () => {
- service.refreshAndGetUserState().subscribe((result: UserLoginState) => {
- expect(result).toEqual('invalidlogin');
- });
-
- httpController.expectOne(tokenValidateRequestMatcher).flush({ isValid: false });
-
- httpController.verify();
- });
+ expect(mockLocalStorage.getItem(TOKEN_STORAGE_KEY)).toBe(mockToken);
});
// 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 604393f4..2098391e 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,4 +1,4 @@
-import { Injectable } from '@angular/core';
+import { Injectable, Inject } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Router } from '@angular/router';
@@ -12,7 +12,19 @@ import {
} 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;
+
+export const TOKEN_STORAGE_KEY = 'token';
/**
* This service is only used internal in user module.
@@ -33,25 +45,28 @@ export class InternalUserService {
return this.userInfoSubject;
}
- constructor(private httpClient: HttpClient, private router: Router, private snackBar: MatSnackBar) {
- const savedToken = window.localStorage.getItem('token');
+ private openSnackBar(snackBar: MatSnackBar, textKey: SnackBarTextKey) {
+ setTimeout(() => snackBar.open(snackBarText[textKey], snackBarText.ok, { duration: 2000 }), 0);
+ }
+
+ 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) {
- setTimeout(() => snackBar.open('No login before!', 'ok', { duration: 2000 }), 0);
+ this.openSnackBar(snackBar, 'noLogin');
} else {
this.validateToken(savedToken).subscribe(result => {
if (result === null) {
- window.localStorage.removeItem('token');
- setTimeout(() => snackBar.open('Last login is no longer invalid!', 'ok', { duration: 2000 }), 0);
+ this.window.localStorage.removeItem(TOKEN_STORAGE_KEY);
+ this.openSnackBar(snackBar, 'invalidLogin');
} else {
this.token = savedToken;
this.userInfoSubject.next(result);
- setTimeout(() => snackBar.open('You have login already!', 'ok', { duration: 2000 }), 0);
+ this.openSnackBar(snackBar, 'alreadyLogin');
}
}, _ => {
- setTimeout(() => snackBar.open('Failed to check last login', 'ok', { duration: 2000 }), 0);
+ this.openSnackBar(snackBar, 'checkFail');
});
}
-
}
private validateToken(token: string): Observable {
@@ -107,7 +122,7 @@ export class InternalUserService {
map(result => {
this.token = result.token;
if (options.remember) {
- window.localStorage.setItem('token', result.token);
+ this.window.localStorage.setItem(TOKEN_STORAGE_KEY, result.token);
}
this.userInfoSubject.next(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 7645d61d..dcb61736 100644
--- a/Timeline/ClientApp/src/app/user/user.module.ts
+++ b/Timeline/ClientApp/src/app/user/user.module.ts
@@ -15,6 +15,7 @@ import { UserLoginComponent } from './user-login/user-login.component';
import { UserLoginSuccessComponent } from './user-login-success/user-login-success.component';
import { RedirectComponent } from './redirect.component';
import { UtilityModule } from '../utilities/utility.module';
+import { WINDOW } from './window-inject-token';
@NgModule({
declarations: [UserDialogComponent, UserLoginComponent, UserLoginSuccessComponent, RedirectComponent],
@@ -28,6 +29,7 @@ import { UtilityModule } from '../utilities/utility.module';
MatFormFieldModule, MatProgressSpinnerModule, MatDialogModule, MatInputModule, MatButtonModule, MatSnackBarModule,
UtilityModule
],
+ providers: [{provide: WINDOW, useValue: window}],
exports: [RouterModule],
entryComponents: [UserDialogComponent]
})
diff --git a/Timeline/ClientApp/src/app/user/window-inject-token.ts b/Timeline/ClientApp/src/app/user/window-inject-token.ts
new file mode 100644
index 00000000..9f8723f6
--- /dev/null
+++ b/Timeline/ClientApp/src/app/user/window-inject-token.ts
@@ -0,0 +1,3 @@
+import { InjectionToken } from '@angular/core';
+
+export const WINDOW = new InjectionToken('global window');
diff --git a/Timeline/ClientApp/src/app/utilities/language-untilities.ts b/Timeline/ClientApp/src/app/utilities/language-untilities.ts
index 7f38f3e4..94434665 100644
--- a/Timeline/ClientApp/src/app/utilities/language-untilities.ts
+++ b/Timeline/ClientApp/src/app/utilities/language-untilities.ts
@@ -10,3 +10,9 @@ export function throwIfNullOrUndefined(value: T | null | undefined,
return value;
}
}
+
+export function repeat(time: number, action: (index?: number) => void) {
+ for (let i = 0; i < time; i++) {
+ action(i);
+ }
+}
diff --git a/Timeline/ClientApp/src/tsconfig.app.json b/Timeline/ClientApp/src/tsconfig.app.json
index 13151ca4..ec3cc7f4 100644
--- a/Timeline/ClientApp/src/tsconfig.app.json
+++ b/Timeline/ClientApp/src/tsconfig.app.json
@@ -7,6 +7,7 @@
},
"exclude": [
"src/test.ts",
+ "test-utilities/**",
"**/*.spec.ts",
"**/*.mock.ts",
"**/*.test.ts"
diff --git a/Timeline/ClientApp/src/tsconfig.spec.json b/Timeline/ClientApp/src/tsconfig.spec.json
index 6e4460f8..ccf4b2ee 100644
--- a/Timeline/ClientApp/src/tsconfig.spec.json
+++ b/Timeline/ClientApp/src/tsconfig.spec.json
@@ -13,6 +13,7 @@
"polyfills.ts"
],
"include": [
+ "test-utilities/**",
"**/*.spec.ts",
"**/*.d.ts",
"**/*.mock.ts",
--
cgit v1.2.3
From e973ad02680f9d9ffdb9f7ac5aff9283484d2f46 Mon Sep 17 00:00:00 2001
From: crupest
Date: Sun, 17 Mar 2019 22:18:56 +0800
Subject: Fix a wrong wildcard in ts config file.
---
Timeline/ClientApp/src/tsconfig.app.json | 2 +-
Timeline/ClientApp/src/tsconfig.spec.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
(limited to 'Timeline/ClientApp/src')
diff --git a/Timeline/ClientApp/src/tsconfig.app.json b/Timeline/ClientApp/src/tsconfig.app.json
index ec3cc7f4..0d3b876e 100644
--- a/Timeline/ClientApp/src/tsconfig.app.json
+++ b/Timeline/ClientApp/src/tsconfig.app.json
@@ -7,7 +7,7 @@
},
"exclude": [
"src/test.ts",
- "test-utilities/**",
+ "test-utilities/**/*",
"**/*.spec.ts",
"**/*.mock.ts",
"**/*.test.ts"
diff --git a/Timeline/ClientApp/src/tsconfig.spec.json b/Timeline/ClientApp/src/tsconfig.spec.json
index ccf4b2ee..3bcc8926 100644
--- a/Timeline/ClientApp/src/tsconfig.spec.json
+++ b/Timeline/ClientApp/src/tsconfig.spec.json
@@ -13,7 +13,7 @@
"polyfills.ts"
],
"include": [
- "test-utilities/**",
+ "test-utilities/**/*",
"**/*.spec.ts",
"**/*.d.ts",
"**/*.mock.ts",
--
cgit v1.2.3
From 69ede25976c11f0624036251523d5f1d28811740 Mon Sep 17 00:00:00 2001
From: crupest
Date: Mon, 18 Mar 2019 21:21:56 +0800
Subject: Add logout. Fix a bug. The bug is it always goes to login page
whether you have login or not before when user is presented in url.
---
Timeline/ClientApp/src/app/user/auth.guard.ts | 33 +++++++++++-----------
.../internal-user-service/internal-user.service.ts | 23 ++++++++++-----
.../user-login-success.component.html | 1 +
.../app/user/user-logout/user-logout.component.css | 0
.../user/user-logout/user-logout.component.html | 1 +
.../user/user-logout/user-logout.component.spec.ts | 25 ++++++++++++++++
.../app/user/user-logout/user-logout.component.ts | 17 +++++++++++
Timeline/ClientApp/src/app/user/user.module.ts | 6 ++--
Timeline/ClientApp/src/app/user/user.service.ts | 2 +-
9 files changed, 82 insertions(+), 26 deletions(-)
create mode 100644 Timeline/ClientApp/src/app/user/user-logout/user-logout.component.css
create mode 100644 Timeline/ClientApp/src/app/user/user-logout/user-logout.component.html
create mode 100644 Timeline/ClientApp/src/app/user/user-logout/user-logout.component.spec.ts
create mode 100644 Timeline/ClientApp/src/app/user/user-logout/user-logout.component.ts
(limited to 'Timeline/ClientApp/src')
diff --git a/Timeline/ClientApp/src/app/user/auth.guard.ts b/Timeline/ClientApp/src/app/user/auth.guard.ts
index 561a0c53..1fc7a7c0 100644
--- a/Timeline/ClientApp/src/app/user/auth.guard.ts
+++ b/Timeline/ClientApp/src/app/user/auth.guard.ts
@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
+import { take, map } from 'rxjs/operators';
import { InternalUserService } from './internal-user-service/internal-user.service';
@@ -23,26 +24,26 @@ export abstract class AuthGuard implements CanActivate {
return true;
}
- const { currentUserInfo } = this.internalUserService;
-
- if (currentUserInfo === null) {
- if (authStrategy === 'requirenologin') {
- return true;
- }
- } else {
- if (authStrategy === 'requirelogin') {
- return true;
- } else if (authStrategy instanceof Array) {
- const { roles } = currentUserInfo;
- if (authStrategy.every(value => roles.includes(value))) {
+ return this.internalUserService.userInfo$.pipe(take(1), map(userInfo => {
+ if (userInfo === null) {
+ if (authStrategy === 'requirenologin') {
return true;
}
+ } else {
+ if (authStrategy === 'requirelogin') {
+ return true;
+ } else if (authStrategy instanceof Array) {
+ const { roles } = userInfo;
+ if (authStrategy.every(value => roles.includes(value))) {
+ return true;
+ }
+ }
}
- }
- // reach here means auth fails
- this.onAuthFailed();
- return false;
+ // reach here means auth fails
+ this.onAuthFailed();
+ return false;
+ }));
}
}
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 2098391e..d82e9613 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
@@ -3,7 +3,7 @@ import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Router } from '@angular/router';
import { Observable, throwError, BehaviorSubject, of } from 'rxjs';
-import { map, catchError, retry, switchMap, tap } from 'rxjs/operators';
+import { map, catchError, retry, switchMap, tap, filter } from 'rxjs/operators';
import { AlreadyLoginError, BadCredentialsError, BadNetworkError, UnknownError } from './errors';
import {
@@ -35,14 +35,13 @@ export const TOKEN_STORAGE_KEY = 'token';
export class InternalUserService {
private token: string | null = null;
- private userInfoSubject = new BehaviorSubject(null);
+ private userInfoSubject = new BehaviorSubject(undefined);
- get currentUserInfo(): UserInfo | null {
- return this.userInfoSubject.value;
- }
+ readonly userInfo$: Observable =
+ >this.userInfoSubject.pipe(filter(value => value !== undefined));
- get userInfo$(): Observable {
- return this.userInfoSubject;
+ get currentUserInfo(): UserInfo | null | undefined {
+ return this.userInfoSubject.value;
}
private openSnackBar(snackBar: MatSnackBar, textKey: SnackBarTextKey) {
@@ -129,4 +128,14 @@ export class InternalUserService {
})
);
}
+
+ 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);
+ }
}
diff --git a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html
index e156f0f8..8599a91d 100644
--- a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html
+++ b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html
@@ -3,3 +3,4 @@
You have been login as {{ userInfo.username }}.
Your roles are {{ userInfo.roles.join(', ') }}.
+Logout
diff --git a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.css b/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.css
new file mode 100644
index 00000000..e69de29b
diff --git a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.html b/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.html
new file mode 100644
index 00000000..56d96b83
--- /dev/null
+++ b/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.html
@@ -0,0 +1 @@
+Log out succeeded!
diff --git a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.spec.ts b/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.spec.ts
new file mode 100644
index 00000000..91369e01
--- /dev/null
+++ b/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { UserLogoutComponent } from './user-logout.component';
+
+describe('UserLogoutComponent', () => {
+ let component: UserLogoutComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ UserLogoutComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UserLogoutComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.ts b/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.ts
new file mode 100644
index 00000000..24002c84
--- /dev/null
+++ b/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.ts
@@ -0,0 +1,17 @@
+import { Component, OnInit } from '@angular/core';
+import { InternalUserService } from '../internal-user-service/internal-user.service';
+
+@Component({
+ selector: 'app-user-logout',
+ templateUrl: './user-logout.component.html',
+ styleUrls: ['./user-logout.component.css']
+})
+export class UserLogoutComponent implements OnInit {
+
+ constructor(private userService: InternalUserService) { }
+
+ ngOnInit() {
+ this.userService.logout();
+ }
+
+}
diff --git a/Timeline/ClientApp/src/app/user/user.module.ts b/Timeline/ClientApp/src/app/user/user.module.ts
index dcb61736..50c59662 100644
--- a/Timeline/ClientApp/src/app/user/user.module.ts
+++ b/Timeline/ClientApp/src/app/user/user.module.ts
@@ -16,20 +16,22 @@ import { UserLoginSuccessComponent } from './user-login-success/user-login-succe
import { RedirectComponent } from './redirect.component';
import { UtilityModule } from '../utilities/utility.module';
import { WINDOW } from './window-inject-token';
+import { UserLogoutComponent } from './user-logout/user-logout.component';
@NgModule({
- declarations: [UserDialogComponent, UserLoginComponent, UserLoginSuccessComponent, RedirectComponent],
+ declarations: [UserDialogComponent, UserLoginComponent, UserLoginSuccessComponent, RedirectComponent, UserLogoutComponent],
imports: [
RouterModule.forChild([
{ path: 'login', canActivate: [RequireNoLoginGuard], component: UserLoginComponent, outlet: 'user' },
{ path: 'success', canActivate: [RequireLoginGuard], component: UserLoginSuccessComponent, outlet: 'user' },
+ { path: 'logout', canActivate: [RequireLoginGuard], component: UserLogoutComponent, outlet: 'user' },
{ path: '**', component: RedirectComponent, outlet: 'user' }
]),
CommonModule, HttpClientModule, ReactiveFormsModule, BrowserAnimationsModule,
MatFormFieldModule, MatProgressSpinnerModule, MatDialogModule, MatInputModule, MatButtonModule, MatSnackBarModule,
UtilityModule
],
- providers: [{provide: WINDOW, useValue: window}],
+ providers: [{ provide: WINDOW, useValue: window }],
exports: [RouterModule],
entryComponents: [UserDialogComponent]
})
diff --git a/Timeline/ClientApp/src/app/user/user.service.ts b/Timeline/ClientApp/src/app/user/user.service.ts
index e7d50dd2..6cae2d31 100644
--- a/Timeline/ClientApp/src/app/user/user.service.ts
+++ b/Timeline/ClientApp/src/app/user/user.service.ts
@@ -29,7 +29,7 @@ export class UserService {
});
}
- get currentUserInfo(): UserInfo | null {
+ get currentUserInfo(): UserInfo | null | undefined {
return this.internalService.currentUserInfo;
}
--
cgit v1.2.3
From b47d61c59c399f45d7fa59f600c58e9089cb1dab Mon Sep 17 00:00:00 2001
From: crupest
Date: Mon, 18 Mar 2019 21:47:19 +0800
Subject: Design login success UI.
---
.../user-login-success/user-login-success.component.css | 15 +++++++++++++++
.../user-login-success/user-login-success.component.html | 2 +-
2 files changed, 16 insertions(+), 1 deletion(-)
(limited to 'Timeline/ClientApp/src')
diff --git a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.css b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.css
index 6486142b..b1101e2a 100644
--- a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.css
+++ b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.css
@@ -5,3 +5,18 @@
.username {
color: blue;
}
+
+:host {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+:host p {
+ margin-top: 0.3em;
+ margin-bottom: 0.3em;
+ width: 100%;
+}
+
+.logout-button {
+ margin-left: auto;
+}
diff --git a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html
index 8599a91d..685f6299 100644
--- a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html
+++ b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html
@@ -3,4 +3,4 @@
You have been login as {{ userInfo.username }}.
Your roles are {{ userInfo.roles.join(', ') }}.
-Logout
+Logout
--
cgit v1.2.3
From bfff5579382ba5671a531cbab5ff14b7207dd15e Mon Sep 17 00:00:00 2001
From: crupest
Date: Mon, 18 Mar 2019 21:57:02 +0800
Subject: Fix a bug. Set userInfo to null when nologin.
---
.../src/app/user/internal-user-service/internal-user.service.ts | 3 +++
1 file changed, 3 insertions(+)
(limited to 'Timeline/ClientApp/src')
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 d82e9613..3f6147af 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
@@ -52,11 +52,13 @@ export class InternalUserService {
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);
@@ -64,6 +66,7 @@ export class InternalUserService {
}
}, _ => {
this.openSnackBar(snackBar, 'checkFail');
+ this.userInfoSubject.next(null);
});
}
}
--
cgit v1.2.3
From d7bfb99d41ac74763f1a1b96a850633994e1efff Mon Sep 17 00:00:00 2001
From: crupest
Date: Tue, 26 Mar 2019 19:21:31 +0800
Subject: Add unit test.
---
.../src/app/test-utilities/router-link.mock.ts | 9 +++++++
Timeline/ClientApp/src/app/user/auth.guard.spec.ts | 30 ++++++++++++++--------
.../user-login-success.component.spec.ts | 12 +++++++--
.../user/user-logout/user-logout.component.html | 2 +-
.../user/user-logout/user-logout.component.spec.ts | 16 +++++++++---
.../app/user/user-logout/user-logout.component.ts | 3 +--
6 files changed, 53 insertions(+), 19 deletions(-)
create mode 100644 Timeline/ClientApp/src/app/test-utilities/router-link.mock.ts
(limited to 'Timeline/ClientApp/src')
diff --git a/Timeline/ClientApp/src/app/test-utilities/router-link.mock.ts b/Timeline/ClientApp/src/app/test-utilities/router-link.mock.ts
new file mode 100644
index 00000000..7f4cde4d
--- /dev/null
+++ b/Timeline/ClientApp/src/app/test-utilities/router-link.mock.ts
@@ -0,0 +1,9 @@
+import { Directive, Input } from '@angular/core';
+
+@Directive({
+ /* tslint:disable-next-line:directive-selector*/
+ selector: '[routerLink]'
+})
+export class RouterLinkStubDirective {
+ @Input('routerLink') linkParams: any;
+}
diff --git a/Timeline/ClientApp/src/app/user/auth.guard.spec.ts b/Timeline/ClientApp/src/app/user/auth.guard.spec.ts
index 42e35bf7..6a36fea6 100644
--- a/Timeline/ClientApp/src/app/user/auth.guard.spec.ts
+++ b/Timeline/ClientApp/src/app/user/auth.guard.spec.ts
@@ -1,3 +1,5 @@
+import { Observable, of } from 'rxjs';
+
import { AuthGuard, AuthStrategy } from './auth.guard';
import { UserInfo } from './entities';
@@ -8,10 +10,9 @@ describe('AuthGuard', () => {
}
authStrategy: AuthStrategy = 'all';
- onAuthFailed: () => void = () => { };
}
- let mockUserService: { currentUserInfo: UserInfo | null };
+ let mockUserService: { userInfo$: Observable };
let guard: ConfiurableAuthGuard;
let onAuthFialedSpy: jasmine.Spy;
@@ -28,19 +29,27 @@ describe('AuthGuard', () => {
return () => {
guard.authStrategy = authStrategy;
- mockUserService.currentUserInfo = null;
- expect(guard.canActivate(null, null)).toBe(result.nologin);
+ function testWith(userInfo: UserInfo | null, r: boolean) {
+ mockUserService.userInfo$ = of(userInfo);
- mockUserService.currentUserInfo = { username: 'user', roles: [] };
- expect(guard.canActivate(null, null)).toBe(result.loginWithNoRole);
+ const rawResult = guard.canActivate(null, null);
+ if (typeof rawResult === 'boolean') {
+ expect(rawResult).toBe(r);
+ } else if (rawResult instanceof Observable) {
+ rawResult.subscribe(next => expect(next).toBe(r));
+ } else {
+ throw new Error('Unsupported return type.');
+ }
+ }
- mockUserService.currentUserInfo = { username: 'user', roles: mockRoles };
- expect(guard.canActivate(null, null)).toBe(result.loginWithMockRoles);
+ testWith(null, result.nologin);
+ testWith({ username: 'user', roles: [] }, result.loginWithNoRole);
+ testWith({ username: 'user', roles: mockRoles }, result.loginWithMockRoles);
};
}
beforeEach(() => {
- mockUserService = { currentUserInfo: null };
+ mockUserService = { userInfo$: of(null) };
guard = new ConfiurableAuthGuard(mockUserService);
onAuthFialedSpy = spyOn(guard, 'onAuthFailed');
});
@@ -54,8 +63,7 @@ describe('AuthGuard', () => {
it('auth failed callback should be called', () => {
guard.authStrategy = 'requirelogin';
- mockUserService.currentUserInfo = null;
- guard.canActivate(null, null);
+ (>guard.canActivate(null, null)).subscribe();
expect(onAuthFialedSpy).toHaveBeenCalled();
});
});
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 ff253add..3eba2696 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
@@ -2,7 +2,8 @@ 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 { RouterLinkStubDirective } from '../../test-utilities/router-link.mock';
+import { MockActivatedRoute } from '../../test-utilities/activated-route.mock';
import { createMockInternalUserService } from '../internal-user-service/internal-user.service.mock';
import { UserLoginSuccessComponent } from './user-login-success.component';
@@ -29,7 +30,7 @@ describe('UserLoginSuccessComponent', () => {
(mockInternalUserService).currentUserInfo = mockUserInfo;
TestBed.configureTestingModule({
- declarations: [UserLoginSuccessComponent],
+ declarations: [UserLoginSuccessComponent, RouterLinkStubDirective],
providers: [
{ provide: InternalUserService, useValue: mockInternalUserService },
{ provide: ActivatedRoute, useValue: mockActivatedRoute }
@@ -64,4 +65,11 @@ describe('UserLoginSuccessComponent', () => {
fixture.detectChanges();
expect((fixture.debugElement.query(By.css('p.login-success-message')))).toBeTruthy();
});
+
+ it('logout button should be set well', () => {
+ fixture.detectChanges();
+ const routerLinkDirective: RouterLinkStubDirective =
+ fixture.debugElement.query(By.css('a')).injector.get(RouterLinkStubDirective);
+ expect(routerLinkDirective.linkParams).toEqual(['..', 'logout']);
+ });
});
diff --git a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.html b/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.html
index 56d96b83..309e5c83 100644
--- a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.html
+++ b/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.html
@@ -1 +1 @@
-Log out succeeded!
+Logout successfully!
diff --git a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.spec.ts b/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.spec.ts
index 91369e01..855ea4a1 100644
--- a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.spec.ts
+++ b/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.spec.ts
@@ -1,25 +1,35 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { UserLogoutComponent } from './user-logout.component';
+import { InternalUserService } from '../internal-user-service/internal-user.service';
describe('UserLogoutComponent', () => {
let component: UserLogoutComponent;
let fixture: ComponentFixture;
+ let mockInternalUserService: jasmine.SpyObj;
+
beforeEach(async(() => {
+ mockInternalUserService = jasmine.createSpyObj('InternalUserService', ['logout']);
+
TestBed.configureTestingModule({
- declarations: [ UserLogoutComponent ]
+ declarations: [UserLogoutComponent],
+ providers: [{ provide: InternalUserService, useValue: mockInternalUserService }]
})
- .compileComponents();
+ .compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(UserLogoutComponent);
component = fixture.componentInstance;
- fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
+
+ it('should logout on init', () => {
+ fixture.detectChanges();
+ expect(mockInternalUserService.logout).toHaveBeenCalled();
+ });
});
diff --git a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.ts b/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.ts
index 24002c84..e004196f 100644
--- a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.ts
+++ b/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.ts
@@ -1,4 +1,5 @@
import { Component, OnInit } from '@angular/core';
+
import { InternalUserService } from '../internal-user-service/internal-user.service';
@Component({
@@ -7,11 +8,9 @@ import { InternalUserService } from '../internal-user-service/internal-user.serv
styleUrls: ['./user-logout.component.css']
})
export class UserLogoutComponent implements OnInit {
-
constructor(private userService: InternalUserService) { }
ngOnInit() {
this.userService.logout();
}
-
}
--
cgit v1.2.3
From 842995aaed29cba1ac17e9a671e4b0782ad65c99 Mon Sep 17 00:00:00 2001
From: crupest
Date: Thu, 11 Apr 2019 19:18:37 +0800
Subject: Add remember me.
---
.../internal-user.service.spec.ts | 39 +++++++++++++---------
.../internal-user-service/internal-user.service.ts | 10 ++++--
.../app/user/user-login/user-login.component.html | 7 ++--
.../user/user-login/user-login.component.spec.ts | 21 +++++++-----
.../app/user/user-login/user-login.component.ts | 3 +-
Timeline/ClientApp/src/app/user/user.module.ts | 4 +--
6 files changed, 51 insertions(+), 33 deletions(-)
(limited to 'Timeline/ClientApp/src')
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 be6631eb..8d081402 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
@@ -82,33 +82,40 @@ describe('InternalUserService', () => {
})));
});
- it('login should work well', () => {
+ describe('login should work well', () => {
const mockUserCredentials: UserCredentials = {
username: 'user',
password: 'user'
};
- const service: InternalUserService = TestBed.get(InternalUserService);
+ function createTest(rememberMe: boolean) {
+ return () => {
+ const service: InternalUserService = TestBed.get(InternalUserService);
- service.tryLogin(mockUserCredentials).subscribe(result => {
- expect(result).toEqual(mockUserInfo);
- });
+ service.tryLogin({ ...mockUserCredentials, rememberMe: rememberMe }).subscribe(result => {
+ expect(result).toEqual(mockUserInfo);
+ });
- const httpController = TestBed.get(HttpTestingController) as HttpTestingController;
+ const httpController = TestBed.get(HttpTestingController) as HttpTestingController;
- httpController.expectOne((request: HttpRequest) =>
- request.url === createTokenUrl && request.body !== null &&
- request.body.username === mockUserCredentials.username &&
- request.body.password === mockUserCredentials.password).flush({
- token: mockToken,
- userInfo: mockUserInfo
- });
+ httpController.expectOne((request: HttpRequest) =>
+ request.url === createTokenUrl && request.body !== null &&
+ request.body.username === mockUserCredentials.username &&
+ request.body.password === mockUserCredentials.password).flush({
+ token: mockToken,
+ userInfo: mockUserInfo
+ });
- expect(service.currentUserInfo).toEqual(mockUserInfo);
+ expect(service.currentUserInfo).toEqual(mockUserInfo);
- httpController.verify();
+ httpController.verify();
+
+ expect(mockLocalStorage.getItem(TOKEN_STORAGE_KEY)).toBe(rememberMe ? mockToken : null);
+ }
+ }
- expect(mockLocalStorage.getItem(TOKEN_STORAGE_KEY)).toBe(mockToken);
+ 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 3f6147af..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
@@ -26,6 +26,10 @@ export type SnackBarTextKey = Exclude;
export const TOKEN_STORAGE_KEY = 'token';
+export interface LoginInfo extends UserCredentials {
+ rememberMe: boolean;
+}
+
/**
* This service is only used internal in user module.
*/
@@ -103,12 +107,12 @@ export class InternalUserService {
}]);
}
- tryLogin(credentials: UserCredentials, options: { remember: boolean } = { remember: true }): Observable {
+ tryLogin(info: LoginInfo): Observable {
if (this.token) {
return throwError(new AlreadyLoginError());
}
- return this.httpClient.post(createTokenUrl, credentials).pipe(
+ return this.httpClient.post(createTokenUrl, info).pipe(
catchError((error: HttpErrorResponse) => {
if (error.error instanceof ErrorEvent) {
console.error('An error occurred when login: ' + error.error.message);
@@ -123,7 +127,7 @@ export class InternalUserService {
}),
map(result => {
this.token = result.token;
- if (options.remember) {
+ if (info.rememberMe) {
this.window.localStorage.setItem(TOKEN_STORAGE_KEY, result.token);
}
this.userInfoSubject.next(result.userInfo);
diff --git a/Timeline/ClientApp/src/app/user/user-login/user-login.component.html b/Timeline/ClientApp/src/app/user/user-login/user-login.component.html
index b1dd289d..7398ece7 100644
--- a/Timeline/ClientApp/src/app/user/user-login/user-login.component.html
+++ b/Timeline/ClientApp/src/app/user/user-login/user-login.component.html
@@ -1,8 +1,8 @@
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 693d5b6e..f010e4b7 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,7 +2,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
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 { of, throwError } from 'rxjs';
@@ -10,6 +9,7 @@ import { createMockInternalUserService } from '../internal-user-service/internal
import { UserLoginComponent } from './user-login.component';
import { InternalUserService } from '../internal-user-service/internal-user.service';
import { UserInfo } from '../entities';
+import { MatCheckboxModule } from '@angular/material';
describe('UserLoginComponent', () => {
let component: UserLoginComponent;
@@ -27,7 +27,7 @@ describe('UserLoginComponent', () => {
providers: [
{ provide: InternalUserService, useValue: mockInternalUserService }
],
- imports: [ReactiveFormsModule],
+ imports: [ReactiveFormsModule, MatCheckboxModule],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
@@ -48,17 +48,20 @@ describe('UserLoginComponent', () => {
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;
+ const rememberMeCheckbox = fixture.debugElement.query(By.css('input[type=checkbox]')).nativeElement as HTMLInputElement;
usernameInput.value = 'user';
usernameInput.dispatchEvent(new Event('input'));
passwordInput.value = 'user';
passwordInput.dispatchEvent(new Event('input'));
+ rememberMeCheckbox.dispatchEvent(new MouseEvent('click'));
fixture.detectChanges();
expect(component.form.value).toEqual({
username: 'user',
- password: 'user'
+ password: 'user',
+ rememberMe: true
});
});
@@ -67,7 +70,8 @@ describe('UserLoginComponent', () => {
const mockValue = {
username: 'user',
- password: 'user'
+ password: 'user',
+ rememberMe: true
};
mockInternalUserService.tryLogin.withArgs(mockValue).and.returnValue(of({ username: 'user', roles: ['user'] }));
@@ -84,7 +88,7 @@ describe('UserLoginComponent', () => {
fixture.detectChanges();
component.message = 'nologin';
fixture.detectChanges();
- expect((fixture.debugElement.query(By.css('p.mat-body')).nativeElement as
+ expect((fixture.debugElement.query(By.css('p')).nativeElement as
HTMLParagraphElement).textContent).toBe('You haven\'t login.');
});
@@ -92,7 +96,7 @@ describe('UserLoginComponent', () => {
fixture.detectChanges();
component.message = 'invalidlogin';
fixture.detectChanges();
- expect((fixture.debugElement.query(By.css('p.mat-body')).nativeElement as
+ expect((fixture.debugElement.query(By.css('p')).nativeElement as
HTMLParagraphElement).textContent).toBe('Your login is no longer valid.');
});
@@ -103,7 +107,8 @@ describe('UserLoginComponent', () => {
const mockValue = {
username: 'user',
- password: 'user'
+ password: 'user',
+ rememberMe: false
};
mockInternalUserService.tryLogin.withArgs(mockValue).and.returnValue(throwError(new Error(customMessage)));
component.form.setValue(mockValue);
@@ -111,7 +116,7 @@ describe('UserLoginComponent', () => {
fixture.detectChanges();
expect(component.message).toBe(customMessage);
- expect((fixture.debugElement.query(By.css('p.mat-body')).nativeElement as
+ expect((fixture.debugElement.query(By.css('p')).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 836202de..4395c5cf 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
@@ -19,7 +19,8 @@ export class UserLoginComponent implements OnInit {
form = new FormGroup({
username: new FormControl(''),
- password: new FormControl('')
+ password: new FormControl(''),
+ rememberMe: new FormControl(false)
});
ngOnInit() {
diff --git a/Timeline/ClientApp/src/app/user/user.module.ts b/Timeline/ClientApp/src/app/user/user.module.ts
index 50c59662..59193380 100644
--- a/Timeline/ClientApp/src/app/user/user.module.ts
+++ b/Timeline/ClientApp/src/app/user/user.module.ts
@@ -6,7 +6,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule } from '@angular/router';
import {
MatFormFieldModule, MatProgressSpinnerModule,
- MatDialogModule, MatInputModule, MatButtonModule, MatSnackBarModule
+ MatDialogModule, MatInputModule, MatButtonModule, MatSnackBarModule, MatCheckboxModule
} from '@angular/material';
import { RequireNoLoginGuard, RequireLoginGuard } from './auth.guard';
@@ -28,7 +28,7 @@ import { UserLogoutComponent } from './user-logout/user-logout.component';
{ path: '**', component: RedirectComponent, outlet: 'user' }
]),
CommonModule, HttpClientModule, ReactiveFormsModule, BrowserAnimationsModule,
- MatFormFieldModule, MatProgressSpinnerModule, MatDialogModule, MatInputModule, MatButtonModule, MatSnackBarModule,
+ MatFormFieldModule, MatProgressSpinnerModule, MatDialogModule, MatInputModule, MatButtonModule, MatCheckboxModule, MatSnackBarModule,
UtilityModule
],
providers: [{ provide: WINDOW, useValue: window }],
--
cgit v1.2.3
From d328e1eac76d9e28563b118e42f8ee5cf5fe43d8 Mon Sep 17 00:00:00 2001
From: crupest
Date: Thu, 11 Apr 2019 19:21:15 +0800
Subject: Run lint.
---
.../src/app/user/internal-user-service/internal-user.service.spec.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
(limited to 'Timeline/ClientApp/src')
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 8d081402..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,6 +1,6 @@
import { TestBed, fakeAsync, tick } from '@angular/core/testing';
import { HttpRequest } from '@angular/common/http';
-import { HttpClientTestingModule, HttpTestingController, TestRequest } from '@angular/common/http/testing';
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { Router } from '@angular/router';
import { MatSnackBar } from '@angular/material';
@@ -111,7 +111,7 @@ describe('InternalUserService', () => {
httpController.verify();
expect(mockLocalStorage.getItem(TOKEN_STORAGE_KEY)).toBe(rememberMe ? mockToken : null);
- }
+ };
}
it('remember me should work well', createTest(true));
--
cgit v1.2.3