aboutsummaryrefslogtreecommitdiff
path: root/Timeline/ClientApp/src
diff options
context:
space:
mode:
Diffstat (limited to 'Timeline/ClientApp/src')
-rw-r--r--Timeline/ClientApp/src/app/test-utilities/activated-route.mock.ts4
-rw-r--r--Timeline/ClientApp/src/app/test-utilities/mock.ts7
-rw-r--r--Timeline/ClientApp/src/app/test-utilities/storage.mock.ts28
-rw-r--r--Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts149
-rw-r--r--Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts35
-rw-r--r--Timeline/ClientApp/src/app/user/user.module.ts2
-rw-r--r--Timeline/ClientApp/src/app/user/window-inject-token.ts3
-rw-r--r--Timeline/ClientApp/src/app/utilities/language-untilities.ts6
-rw-r--r--Timeline/ClientApp/src/tsconfig.app.json1
-rw-r--r--Timeline/ClientApp/src/tsconfig.spec.json1
10 files changed, 148 insertions, 88 deletions
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<T> = {
- [P in keyof T]?: T[P] | PartialMock<T[P]>;
-};
+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<T> = {
+ [P in keyof T]: T[P] extends Function ? T[P] : T[P] | Mock<T[P]>;
+};
+
+export type PartialMock<T> = {
+ [P in keyof T]?: T[P] extends Function ? T[P] : T[P] | PartialMock<T[P]> | Mock<T[P]>;
+};
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<Storage> {
+ 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<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');
- });
+ const mockUserInfo: UserInfo = {
+ username: 'user',
+ roles: ['user', 'other']
+ };
+
+ 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());
+ });
+ }
+
+ 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' }));
+ })));
});
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<CreateTokenRequest>) =>
request.url === createTokenUrl && request.body !== null &&
- request.body.username === 'user' &&
- request.body.password === 'user').flush(<CreateTokenResponse>{
- token: 'test-token',
+ request.body.username === mockUserCredentials.username &&
+ request.body.password === mockUserCredentials.password).flush(<CreateTokenResponse>{
+ 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<ValidateTokenRequest>) => {
- 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(<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();
- });
+ 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<keyof typeof snackBarText, 'ok'>;
+
+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<UserInfo | null> {
@@ -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<Window>('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<T>(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",