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 @@
- - -

{{ message }}

+ + +

{{ message }}

Username @@ -13,6 +13,7 @@ Password + Remember me!
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