initial codebase

This commit is contained in:
anweshknayak
2018-05-28 21:01:27 +05:30
commit 5eedfa13bd
136 changed files with 19812 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { Route, extract } from '@app/core';
import { AboutComponent } from './about.component';
const routes: Routes = [
Route.withShell([
{ path: 'about', component: AboutComponent, data: { title: extract('About') } }
])
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: []
})
export class AboutRoutingModule { }
+10
View File
@@ -0,0 +1,10 @@
<div class="container-fluid">
<div class="jumbotron text-center">
<h1>
<span translate>APP_NAME</span>
</h1>
<p>
<i class="far fa-bookmark"></i> <span translate>Version</span> {{version}}
</p>
</div>
</div>
View File
+25
View File
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AboutComponent } from './about.component';
describe('AboutComponent', () => {
let component: AboutComponent;
let fixture: ComponentFixture<AboutComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AboutComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AboutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+18
View File
@@ -0,0 +1,18 @@
import { Component, OnInit } from '@angular/core';
import { environment } from '@env/environment';
@Component({
selector: 'app-about',
templateUrl: './about.component.html',
styleUrls: ['./about.component.scss']
})
export class AboutComponent implements OnInit {
version: string = environment.version;
constructor() { }
ngOnInit() { }
}
+18
View File
@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { AboutRoutingModule } from './about-routing.module';
import { AboutComponent } from './about.component';
@NgModule({
imports: [
CommonModule,
TranslateModule,
AboutRoutingModule
],
declarations: [
AboutComponent
]
})
export class AboutModule { }
+14
View File
@@ -0,0 +1,14 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
// Fallback when no prior route is matched
{ path: '**', redirectTo: '', pathMatch: 'full' },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
providers: []
})
export class AppRoutingModule { }
+1
View File
@@ -0,0 +1 @@
<router-outlet></router-outlet>
View File
+27
View File
@@ -0,0 +1,27 @@
import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
import { CoreModule } from '@app/core';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
TranslateModule.forRoot(),
CoreModule
],
declarations: [AppComponent],
providers: []
});
TestBed.compileComponents();
}));
it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
});
+61
View File
@@ -0,0 +1,61 @@
import { Component, OnInit } from '@angular/core';
import { Router, NavigationEnd, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { merge } from 'rxjs';
import { filter, map, mergeMap } from 'rxjs/operators';
import { environment } from '@env/environment';
import { Logger, I18nService } from '@app/core';
const log = new Logger('App');
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
constructor(private router: Router,
private activatedRoute: ActivatedRoute,
private titleService: Title,
private translateService: TranslateService,
private i18nService: I18nService) { }
ngOnInit() {
// Setup logger
if (environment.production) {
Logger.enableProductionMode();
}
log.debug('init');
// Setup translations
this.i18nService.init(environment.defaultLanguage, environment.supportedLanguages);
const onNavigationEnd = this.router.events.pipe(filter(event => event instanceof NavigationEnd));
// Change page title on navigation or language change, based on route data
merge(this.translateService.onLangChange, onNavigationEnd)
.pipe(
map(() => {
let route = this.activatedRoute;
while (route.firstChild) {
route = route.firstChild;
}
return route;
}),
filter(route => route.outlet === 'primary'),
mergeMap(route => route.data)
)
.subscribe(event => {
const title = event['title'];
if (title) {
this.titleService.setTitle(this.translateService.instant(title));
}
});
}
}
+40
View File
@@ -0,0 +1,40 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { ServiceWorkerModule } from '@angular/service-worker';
import { TranslateModule } from '@ngx-translate/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { environment } from '@env/environment';
import { CoreModule } from '@app/core';
import { SharedModule } from '@app/shared';
import { HomeModule } from './home/home.module';
import { AboutModule } from './about/about.module';
import { ClientsModule} from './clients/clients.module'
import { LoginModule } from './login/login.module';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
@NgModule({
imports: [
BrowserModule,
ServiceWorkerModule.register('./ngsw-worker.js', { enabled: environment.production }),
FormsModule,
HttpClientModule,
TranslateModule.forRoot(),
NgbModule.forRoot(),
CoreModule,
SharedModule,
HomeModule,
AboutModule,
ClientsModule,
LoginModule,
AppRoutingModule
],
declarations: [AppComponent,],
providers: [
],
bootstrap: [AppComponent]
})
export class AppModule { }
+16
View File
@@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { extract } from '@app/core';
import { ClientsComponent } from './clients.component';
const routes: Routes = [
{ path: 'clients', component: ClientsComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: []
})
export class ClientsRoutingModule { }
+4
View File
@@ -0,0 +1,4 @@
<app-header> </app-header>
<p>
clients works!
</p>
+18
View File
@@ -0,0 +1,18 @@
import { Component, OnInit } from '@angular/core';
import { environment } from '@env/environment';
import { HeaderComponent } from '../core/shell/header/header.component';
@Component({
selector: 'app-clients',
templateUrl: './clients.component.html',
styleUrls: ['./clients.component.scss']
})
export class ClientsComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}
+23
View File
@@ -0,0 +1,23 @@
import { NgModule,CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ClientsComponent } from './clients.component';
import { ClientsRoutingModule } from './clients-routing.module';
import { CoreModule } from '../core/core.module';
@NgModule({
imports: [
CoreModule,
CommonModule,
ClientsRoutingModule,
],
declarations: [
ClientsComponent
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
],
})
export class ClientsModule { }
@@ -0,0 +1,55 @@
import { TestBed, inject } from '@angular/core/testing';
import { Router } from '@angular/router';
import { AuthenticationService } from './authentication.service';
import { MockAuthenticationService } from './authentication.service.mock';
import { AuthenticationGuard } from './authentication.guard';
describe('AuthenticationGuard', () => {
let authenticationGuard: AuthenticationGuard;
let authenticationService: MockAuthenticationService;
let mockRouter: any;
beforeEach(() => {
mockRouter = {
navigate: jasmine.createSpy('navigate')
};
TestBed.configureTestingModule({
providers: [
AuthenticationGuard,
{ provide: AuthenticationService, useClass: MockAuthenticationService },
{ provide: Router, useValue: mockRouter },
]
});
});
beforeEach(inject([
AuthenticationGuard,
AuthenticationService
], (_authenticationGuard: AuthenticationGuard,
_authenticationService: MockAuthenticationService) => {
authenticationGuard = _authenticationGuard;
authenticationService = _authenticationService;
}));
it('should have a canActivate method', () => {
expect(typeof authenticationGuard.canActivate).toBe('function');
});
it('should return true if user is authenticated', () => {
expect(authenticationGuard.canActivate()).toBe(true);
});
it('should return false and redirect to login if user is not authenticated', () => {
// Arrange
authenticationService.credentials = null;
// Act
const result = authenticationGuard.canActivate();
// Assert
expect(mockRouter.navigate).toHaveBeenCalledWith(['/login'], {replaceUrl: true});
expect(result).toBe(false);
});
});
@@ -0,0 +1,25 @@
import { Injectable } from '@angular/core';
import { Router, CanActivate } from '@angular/router';
import { Logger } from '../logger.service';
import { AuthenticationService } from './authentication.service';
const log = new Logger('AuthenticationGuard');
@Injectable()
export class AuthenticationGuard implements CanActivate {
constructor(private router: Router,
private authenticationService: AuthenticationService) { }
canActivate(): boolean {
if (this.authenticationService.isAuthenticated()) {
return true;
}
log.debug('Not authenticated, redirecting...');
this.router.navigate(['/login'], { replaceUrl: true });
return false;
}
}
@@ -0,0 +1,28 @@
import { Observable, of } from 'rxjs';
import { Credentials, LoginContext } from './authentication.service';
export class MockAuthenticationService {
credentials: Credentials | null = {
username: 'test',
token: '123'
};
login(context: LoginContext): Observable<Credentials> {
return of({
username: context.username,
token: '123456'
});
}
logout(): Observable<boolean> {
this.credentials = null;
return of(true);
}
isAuthenticated(): boolean {
return !!this.credentials;
}
}
@@ -0,0 +1,144 @@
import { TestBed, inject, fakeAsync, tick } from '@angular/core/testing';
import { AuthenticationService, Credentials} from './authentication.service';
const credentialsKey = 'credentials';
describe('AuthenticationService', () => {
let authenticationService: AuthenticationService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [AuthenticationService]
});
});
beforeEach(inject([
AuthenticationService
], (_authenticationService: AuthenticationService) => {
authenticationService = _authenticationService;
}));
afterEach(() => {
// Cleanup
localStorage.removeItem(credentialsKey);
sessionStorage.removeItem(credentialsKey);
});
describe('login', () => {
it('should return credentials', fakeAsync(() => {
// Act
const request = authenticationService.login({
username: 'toto',
password: '123'
});
tick();
// Assert
request.subscribe(credentials => {
expect(credentials).toBeDefined();
expect(credentials.token).toBeDefined();
});
}));
it('should authenticate user', fakeAsync(() => {
expect(authenticationService.isAuthenticated()).toBe(false);
// Act
const request = authenticationService.login({
username: 'toto',
password: '123'
});
tick();
// Assert
request.subscribe(() => {
expect(authenticationService.isAuthenticated()).toBe(true);
expect(authenticationService.credentials).toBeDefined();
expect(authenticationService.credentials).not.toBeNull();
expect((<Credentials>authenticationService.credentials).token).toBeDefined();
expect((<Credentials>authenticationService.credentials).token).not.toBeNull();
});
}));
it('should persist credentials for the session', fakeAsync(() => {
// Act
const request = authenticationService.login({
username: 'toto',
password: '123'
});
tick();
// Assert
request.subscribe(() => {
expect(sessionStorage.getItem(credentialsKey)).not.toBeNull();
});
}));
it('should persist credentials across sessions', fakeAsync(() => {
// Act
const request = authenticationService.login({
username: 'toto',
password: '123',
remember: true
});
tick();
// Assert
request.subscribe(() => {
expect(localStorage.getItem(credentialsKey)).not.toBeNull();
});
}));
});
describe('logout', () => {
it('should clear user authentication', fakeAsync(() => {
// Arrange
const loginRequest = authenticationService.login({
username: 'toto',
password: '123'
});
tick();
// Assert
loginRequest.subscribe(() => {
expect(authenticationService.isAuthenticated()).toBe(true);
const request = authenticationService.logout();
tick();
request.subscribe(() => {
expect(authenticationService.isAuthenticated()).toBe(false);
expect(authenticationService.credentials).toBeNull();
expect(sessionStorage.getItem(credentialsKey)).toBeNull();
expect(localStorage.getItem(credentialsKey)).toBeNull();
});
});
}));
it('should clear persisted user authentication', fakeAsync(() => {
// Arrange
const loginRequest = authenticationService.login({
username: 'toto',
password: '123',
remember: true
});
tick();
// Assert
loginRequest.subscribe(() => {
expect(authenticationService.isAuthenticated()).toBe(true);
const request = authenticationService.logout();
tick();
request.subscribe(() => {
expect(authenticationService.isAuthenticated()).toBe(false);
expect(authenticationService.credentials).toBeNull();
expect(sessionStorage.getItem(credentialsKey)).toBeNull();
expect(localStorage.getItem(credentialsKey)).toBeNull();
});
});
}));
});
});
@@ -0,0 +1,94 @@
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
export interface Credentials {
// Customize received credentials here
username: string;
token: string;
}
export interface LoginContext {
username: string;
password: string;
remember?: boolean;
}
const credentialsKey = 'credentials';
/**
* Provides a base for authentication workflow.
* The Credentials interface as well as login/logout methods should be replaced with proper implementation.
*/
@Injectable()
export class AuthenticationService {
private _credentials: Credentials | null;
constructor() {
const savedCredentials = sessionStorage.getItem(credentialsKey) || localStorage.getItem(credentialsKey);
if (savedCredentials) {
this._credentials = JSON.parse(savedCredentials);
}
}
/**
* Authenticates the user.
* @param {LoginContext} context The login parameters.
* @return {Observable<Credentials>} The user credentials.
*/
login(context: LoginContext): Observable<Credentials> {
// Replace by proper authentication call
const data = {
username: context.username,
token: '123456'
};
this.setCredentials(data, context.remember);
return of(data);
}
/**
* Logs out the user and clear credentials.
* @return {Observable<boolean>} True if the user was logged out successfully.
*/
logout(): Observable<boolean> {
// Customize credentials invalidation here
this.setCredentials();
return of(true);
}
/**
* Checks is the user is authenticated.
* @return {boolean} True if the user is authenticated.
*/
isAuthenticated(): boolean {
return !!this.credentials;
}
/**
* Gets the user credentials.
* @return {Credentials} The user credentials or null if the user is not authenticated.
*/
get credentials(): Credentials | null {
return this._credentials;
}
/**
* Sets the user credentials.
* The credentials may be persisted across sessions by setting the `remember` parameter to true.
* Otherwise, the credentials are only persisted for the current session.
* @param {Credentials=} credentials The user credentials.
* @param {boolean=} remember True to remember credentials across sessions.
*/
private setCredentials(credentials?: Credentials, remember?: boolean) {
this._credentials = credentials || null;
if (credentials) {
const storage = remember ? localStorage : sessionStorage;
storage.setItem(credentialsKey, JSON.stringify(credentials));
} else {
sessionStorage.removeItem(credentialsKey);
localStorage.removeItem(credentialsKey);
}
}
}
+63
View File
@@ -0,0 +1,63 @@
import { NgModule, Optional, SkipSelf } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from '@angular/common/http';
import { RouteReuseStrategy, RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { ShellComponent } from './shell/shell.component';
import { HeaderComponent } from './shell/header/header.component';
import { RouteReusableStrategy } from './route-reusable-strategy';
import { AuthenticationService } from './authentication/authentication.service';
import { AuthenticationGuard } from './authentication/authentication.guard';
import { I18nService } from './i18n.service';
import { HttpService } from './http/http.service';
import { HttpCacheService } from './http/http-cache.service';
import { ApiPrefixInterceptor } from './http/api-prefix.interceptor';
import { ErrorHandlerInterceptor } from './http/error-handler.interceptor';
import { CacheInterceptor } from './http/cache.interceptor';
@NgModule({
imports: [
CommonModule,
HttpClientModule,
TranslateModule,
NgbModule,
RouterModule
],
declarations: [
HeaderComponent,
ShellComponent
],
exports: [
HeaderComponent],
providers: [
AuthenticationService,
AuthenticationGuard,
I18nService,
HttpCacheService,
ApiPrefixInterceptor,
ErrorHandlerInterceptor,
CacheInterceptor,
{
provide: HttpClient,
useClass: HttpService
},
{
provide: RouteReuseStrategy,
useClass: RouteReusableStrategy
}
]
})
export class CoreModule {
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
// Import guard
if (parentModule) {
throw new Error(`${parentModule} has already been loaded. Import Core module in the AppModule only.`);
}
}
}
@@ -0,0 +1,44 @@
import { TestBed, inject } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
import { environment } from '@env/environment';
import { ApiPrefixInterceptor } from './api-prefix.interceptor';
describe('ApiPrefixInterceptor', () => {
let http: HttpClient;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [{
provide: HTTP_INTERCEPTORS,
useClass: ApiPrefixInterceptor,
multi: true
}]
});
});
beforeEach(inject([
HttpClient,
HttpTestingController
], (_http: HttpClient,
_httpMock: HttpTestingController) => {
http = _http;
httpMock = _httpMock;
}));
afterEach(() => {
httpMock.verify();
});
it('should prepend environment.serverUrl to the request url', () => {
// Act
http.get('/toto').subscribe();
// Assert
httpMock.expectOne({ url: environment.serverUrl + '/toto' });
});
});
@@ -0,0 +1,18 @@
import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '@env/environment';
/**
* Prefixes all requests with `environment.serverUrl`.
*/
@Injectable()
export class ApiPrefixInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
request = request.clone({ url: environment.serverUrl + request.url });
return next.handle(request);
}
}
+135
View File
@@ -0,0 +1,135 @@
import { TestBed, inject } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HTTP_INTERCEPTORS, HttpClient, HttpResponse } from '@angular/common/http';
import { CacheInterceptor } from './cache.interceptor';
import { HttpCacheService } from './http-cache.service';
describe('CacheInterceptor', () => {
let interceptorOptions: Object | null = {};
let httpCacheService: HttpCacheService;
let cacheInterceptor: CacheInterceptor;
let http: HttpClient;
let httpMock: HttpTestingController;
function createInterceptor(_httpCacheService: HttpCacheService) {
cacheInterceptor = new CacheInterceptor(_httpCacheService).configure(interceptorOptions);
return cacheInterceptor;
}
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
HttpCacheService,
{
provide: HTTP_INTERCEPTORS,
useFactory: createInterceptor,
deps: [HttpCacheService],
multi: true
}
]
});
});
afterEach(() => {
httpCacheService.cleanCache();
httpMock.verify();
});
describe('with default configuration', () => {
beforeEach(() => {
interceptorOptions = null;
});
beforeEach(inject([
HttpClient,
HttpTestingController,
HttpCacheService
], (_http: HttpClient,
_httpMock: HttpTestingController,
_httpCacheService: HttpCacheService) => {
http = _http;
httpMock = _httpMock;
httpCacheService = _httpCacheService;
}));
it('should cache the request', () => {
// Act
http.get('/toto').subscribe(() => {
// Assert
const cachedData = httpCacheService.getCacheData('/toto');
expect(cachedData).toBeDefined();
expect(cachedData ? cachedData.body : null).toEqual('someData');
});
httpMock.expectOne({ url: '/toto' }).flush('someData');
});
it('should respond from the cache', () => {
// Arrange
httpCacheService.setCacheData('/toto', new HttpResponse({ body: 'cachedData' }));
// Act
http.get('/toto').subscribe(response => {
// Assert
expect(response).toEqual('cachedData');
});
httpMock.expectNone({ url: '/toto' });
});
it('should not cache the request in case of error', () => {
// Act
http.get('/toto').subscribe(() => {}, () => {
// Assert
expect(httpCacheService.getCacheData('/toto')).toBeNull();
});
httpMock.expectOne({}).flush(null, {
status: 404,
statusText: 'error'
});
});
});
describe('with update forced configuration', () => {
beforeEach(() => {
interceptorOptions = { update: true };
});
beforeEach(inject([
HttpClient,
HttpTestingController,
HttpCacheService
], (_http: HttpClient,
_httpMock: HttpTestingController,
_httpCacheService: HttpCacheService) => {
http = _http;
httpMock = _httpMock;
httpCacheService = _httpCacheService;
}));
afterEach(() => {
httpCacheService.cleanCache();
httpMock.verify();
});
it('should force cache update', () => {
// Arrange
httpCacheService.setCacheData('/toto', new HttpResponse({ body: 'oldCachedData' }));
cacheInterceptor.configure({ update: true });
// Act
http.get('/toto').subscribe(response => {
// Assert
expect(response).toEqual('newData');
});
httpMock.expectOne({ url: '/toto' }).flush('newData');
});
});
});
+58
View File
@@ -0,0 +1,58 @@
import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse } from '@angular/common/http';
import { Observable, Subscriber } from 'rxjs';
import { HttpCacheService } from './http-cache.service';
/**
* Caches HTTP requests.
* Use ExtendedHttpClient fluent API to configure caching for each request.
*/
@Injectable()
export class CacheInterceptor implements HttpInterceptor {
private forceUpdate = false;
constructor(private httpCacheService: HttpCacheService) { }
/**
* Configures interceptor options
* @param {{update: boolean}} options If update option is enabled, forces request to be made and updates cache entry.
* @return {CacheInterceptor} The configured instance.
*/
configure(options?: { update?: boolean } | null): CacheInterceptor {
const instance = new CacheInterceptor(this.httpCacheService);
if (options && options.update) {
instance.forceUpdate = true;
}
return instance;
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (request.method !== 'GET') {
return next.handle(request);
}
return new Observable((subscriber: Subscriber<HttpEvent<any>>) => {
const cachedData = this.forceUpdate ? null : this.httpCacheService.getCacheData(request.urlWithParams);
if (cachedData !== null) {
// Create new response to avoid side-effects
subscriber.next(new HttpResponse(cachedData as Object));
subscriber.complete();
} else {
next.handle(request)
.subscribe(
event => {
if (event instanceof HttpResponse) {
this.httpCacheService.setCacheData(request.urlWithParams, event);
}
subscriber.next(event);
},
error => subscriber.error(error),
() => subscriber.complete()
);
}
});
}
}
@@ -0,0 +1,59 @@
import { TestBed, inject } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
import { ErrorHandlerInterceptor } from './error-handler.interceptor';
describe('ErrorHandlerInterceptor', () => {
let errorHandlerInterceptor: ErrorHandlerInterceptor;
let http: HttpClient;
let httpMock: HttpTestingController;
function createInterceptor() {
errorHandlerInterceptor = new ErrorHandlerInterceptor();
return errorHandlerInterceptor;
}
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [{
provide: HTTP_INTERCEPTORS,
useFactory: createInterceptor,
multi: true
}]
});
});
beforeEach(inject([
HttpClient,
HttpTestingController
], (_http: HttpClient,
_httpMock: HttpTestingController) => {
http = _http;
httpMock = _httpMock;
}));
afterEach(() => {
httpMock.verify();
});
it('should catch error and call error handler', () => {
// Arrange
// Note: here we spy on private method since target is customization here,
// but you should replace it by actual behavior in your app
spyOn(ErrorHandlerInterceptor.prototype as any, 'errorHandler').and.callThrough();
// Act
http.get('/toto').subscribe(() => fail('should error'), () => {
// Assert
expect(ErrorHandlerInterceptor.prototype['errorHandler']).toHaveBeenCalled();
});
httpMock.expectOne({}).flush(null, {
status: 404,
statusText: 'error'
});
});
});
@@ -0,0 +1,30 @@
import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { environment } from '@env/environment';
import { Logger } from '../logger.service';
const log = new Logger('ErrorHandlerInterceptor');
/**
* Adds a default error handler to all requests.
*/
@Injectable()
export class ErrorHandlerInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(catchError(error => this.errorHandler(error)));
}
// Customize the default error handler here if needed
private errorHandler(response: HttpEvent<any>): Observable<HttpEvent<any>> {
if (!environment.production) {
// Do something with the error
log.error('Request error', response);
}
throw response;
}
}
@@ -0,0 +1,194 @@
import { TestBed, inject } from '@angular/core/testing';
import { HttpResponse } from '@angular/common/http';
import { HttpCacheService, HttpCacheEntry } from './http-cache.service';
const cachePersistenceKey = 'httpCache';
describe('HttpCacheService', () => {
let httpCacheService: HttpCacheService;
let response: HttpResponse<any>;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [HttpCacheService]
});
// Start fresh
window.sessionStorage.removeItem(cachePersistenceKey);
window.localStorage.removeItem(cachePersistenceKey);
});
beforeEach(inject([HttpCacheService], (_httpCacheService: HttpCacheService) => {
httpCacheService = _httpCacheService;
response = new HttpResponse({ body: 'data' });
}));
afterEach(() => {
httpCacheService.cleanCache();
});
describe('setCacheData', () => {
it('should set cache data', () => {
// Act
httpCacheService.setCacheData('/popo', response);
// Assert
expect(httpCacheService.getCacheData('/popo')).toEqual(response);
});
it('should replace existing data', () => {
// Arrange
const newResponse = new HttpResponse({ body: 'new data' });
// Act
httpCacheService.setCacheData('/popo', response);
httpCacheService.setCacheData('/popo', newResponse);
// Assert
expect(httpCacheService.getCacheData('/popo')).toEqual(newResponse);
});
it('should set cache date correctly', () => {
// Act
const date = new Date(123);
httpCacheService.setCacheData('/popo', response, date);
httpCacheService.setCacheData('/hoho', response);
// Assert
expect((<HttpCacheEntry>httpCacheService.getHttpCacheEntry('/popo')).lastUpdated).toBe(date);
expect((<HttpCacheEntry>httpCacheService.getHttpCacheEntry('/hoho')).lastUpdated).not.toBe(date);
});
});
describe('getCacheData', () => {
it('should return null if no cache', () => {
expect(httpCacheService.getCacheData('/hoho')).toBe(null);
});
it('should return cached data if exists', () => {
// Act
httpCacheService.setCacheData('/hoho', response);
// Assert
expect(httpCacheService.getCacheData('/hoho')).toEqual(response);
});
it('should return cached data with url parameters if exists', () => {
// Act
httpCacheService.setCacheData('/hoho?pif=paf', response);
// Assert
expect(httpCacheService.getCacheData('/hoho?pif=paf')).toEqual(response);
});
});
describe('getHttpCacheEntry', () => {
it('should return null if no cache', () => {
expect(httpCacheService.getHttpCacheEntry('/hoho')).toBe(null);
});
it('should return cached data date if exists', () => {
// Arrange
const date = new Date(123);
// Act
httpCacheService.setCacheData('/hoho', response, date);
const entry = httpCacheService.getHttpCacheEntry('/hoho');
// Assert
expect(entry).not.toBeNull();
expect((<HttpCacheEntry>entry).lastUpdated).toEqual(date);
expect((<HttpCacheEntry>entry).data).toEqual(response);
});
});
describe('clearCacheData', () => {
it('should clear existing cache data', () => {
// Set cache
httpCacheService.setCacheData('/hoho', response);
expect(httpCacheService.getCacheData('/hoho')).toEqual(response);
// Clear cache
httpCacheService.clearCache('/hoho');
expect(httpCacheService.getCacheData('/hoho')).toBe(null);
});
it('should do nothing if no cache exists', () => {
expect(httpCacheService.getCacheData('/lolo')).toBe(null);
httpCacheService.clearCache('/hoho');
expect(httpCacheService.getCacheData('/lolo')).toBe(null);
});
});
describe('cleanCache', () => {
it('should clear all cache if no date is specified', () => {
// Set cache
httpCacheService.setCacheData('/hoho', response);
httpCacheService.setCacheData('/popo', response);
expect(httpCacheService.getCacheData('/hoho')).toBe(response);
expect(httpCacheService.getCacheData('/popo')).toBe(response);
// Clean cache
httpCacheService.cleanCache();
expect(httpCacheService.getCacheData('/hoho')).toBe(null);
expect(httpCacheService.getCacheData('/popo')).toBe(null);
});
it('should clear existing since specified date', () => {
// Set cache
httpCacheService.setCacheData('/hoho', response);
expect(httpCacheService.getCacheData('/hoho')).toBe(response);
// Clean cache
httpCacheService.cleanCache(new Date());
expect(httpCacheService.getCacheData('/hoho')).toBe(null);
});
it('should not affect cache entries newer than specified date', () => {
// Set cache
httpCacheService.setCacheData('/hoho', response);
expect(httpCacheService.getCacheData('/hoho')).toBe(response);
// Clean cache
const date = new Date();
httpCacheService.setCacheData('/lolo', response, new Date(date.getTime() + 10));
httpCacheService.cleanCache(date);
// Assert
expect(httpCacheService.getCacheData('/hoho')).toBe(null);
expect(httpCacheService.getCacheData('/lolo')).toBe(response);
});
});
describe('setPersistence', () => {
beforeEach(() => {
httpCacheService.setPersistence();
httpCacheService.cleanCache = jasmine.createSpy('cleanCache');
});
it('should clear previous cache data when persistence value change', () => {
httpCacheService.setPersistence('local');
expect(httpCacheService.cleanCache).toHaveBeenCalledWith();
});
it('should persist cache to local storage', () => {
expect(localStorage.getItem(cachePersistenceKey)).toBeNull();
httpCacheService.setPersistence('local');
httpCacheService.setCacheData('/hoho', response);
expect(localStorage.getItem(cachePersistenceKey)).not.toBeNull();
});
it('should persist cache to session storage', () => {
expect(sessionStorage.getItem(cachePersistenceKey)).toBeNull();
httpCacheService.setPersistence('session');
httpCacheService.setCacheData('/hoho', response);
expect(sessionStorage.getItem(cachePersistenceKey)).not.toBeNull();
});
});
});
+118
View File
@@ -0,0 +1,118 @@
import { Injectable } from '@angular/core';
import { HttpResponse } from '@angular/common/http';
import { each } from 'lodash';
import { Logger } from '../logger.service';
const log = new Logger('HttpCacheService');
const cachePersistenceKey = 'httpCache';
export interface HttpCacheEntry {
lastUpdated: Date;
data: HttpResponse<any>;
}
/**
* Provides a cache facility for HTTP requests with configurable persistence policy.
*/
@Injectable()
export class HttpCacheService {
private cachedData: { [key: string]: HttpCacheEntry; } = {};
private storage: Storage | null = null;
constructor() {
this.loadCacheData();
}
/**
* Sets the cache data for the specified request.
* @param {!string} url The request URL.
* @param {ResponseOptions} data The received data.
* @param {Date=} lastUpdated The cache last update, current date is used if not specified.
*/
setCacheData(url: string, data: HttpResponse<any>, lastUpdated?: Date) {
this.cachedData[url] = {
lastUpdated: lastUpdated || new Date(),
data: data
};
log.debug(`Cache set for key: "${url}"`);
this.saveCacheData();
}
/**
* Gets the cached data for the specified request.
* @param {!string} url The request URL.
* @return {?ResponseOptions} The cached data or null if no cached data exists for this request.
*/
getCacheData(url: string): HttpResponse<any> | null {
const cacheEntry = this.cachedData[url];
if (cacheEntry) {
log.debug(`Cache hit for key: "${url}"`);
return cacheEntry.data;
}
return null;
}
/**
* Gets the cached entry for the specified request.
* @param {!string} url The request URL.
* @return {?HttpCacheEntry} The cache entry or null if no cache entry exists for this request.
*/
getHttpCacheEntry(url: string): HttpCacheEntry | null {
return this.cachedData[url] || null;
}
/**
* Clears the cached entry (if exists) for the specified request.
* @param {!string} url The request URL.
*/
clearCache(url: string): void {
delete this.cachedData[url];
log.debug(`Cache cleared for key: "${url}"`);
this.saveCacheData();
}
/**
* Cleans cache entries older than the specified date.
* @param {date=} expirationDate The cache expiration date. If no date is specified, all cache is cleared.
*/
cleanCache(expirationDate?: Date) {
if (expirationDate) {
each(this.cachedData, (value: HttpCacheEntry, key: string) => {
if (expirationDate >= value.lastUpdated) {
delete this.cachedData[key];
}
});
} else {
this.cachedData = {};
}
this.saveCacheData();
}
/**
* Sets the cache persistence policy.
* Note that changing the cache persistence will also clear the cache from its previous storage.
* @param {'local'|'session'=} persistence How the cache should be persisted, it can be either local or session
* storage, or if no value is provided it will be only in-memory (default).
*/
setPersistence(persistence?: 'local' | 'session') {
this.cleanCache();
this.storage = persistence === 'local' || persistence === 'session' ? window[persistence + 'Storage'] : null;
this.loadCacheData();
}
private saveCacheData() {
if (this.storage) {
this.storage[cachePersistenceKey] = JSON.stringify(this.cachedData);
}
}
private loadCacheData() {
const data = this.storage ? this.storage[cachePersistenceKey] : null;
this.cachedData = data ? JSON.parse(data) : {};
}
}
+140
View File
@@ -0,0 +1,140 @@
import { TestBed, inject } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HttpClient, HttpInterceptor } from '@angular/common/http';
import { HttpService } from './http.service';
import { HttpCacheService } from './http-cache.service';
import { ErrorHandlerInterceptor } from './error-handler.interceptor';
import { CacheInterceptor } from './cache.interceptor';
import { ApiPrefixInterceptor } from './api-prefix.interceptor';
describe('HttpService', () => {
let httpCacheService: HttpCacheService;
let http: HttpClient;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
ErrorHandlerInterceptor,
CacheInterceptor,
ApiPrefixInterceptor,
HttpCacheService,
{
provide: HttpClient,
useClass: HttpService
},
]
});
});
beforeEach(inject([
HttpClient,
HttpTestingController,
HttpCacheService
], (_http: HttpClient,
_httpMock: HttpTestingController,
_httpCacheService: HttpCacheService) => {
http = _http;
httpMock = _httpMock;
httpCacheService = _httpCacheService;
}));
afterEach(() => {
httpCacheService.cleanCache();
httpMock.verify();
});
it('should use error handler, API prefix and no cache by default', () => {
// Arrange
let interceptors: HttpInterceptor[];
const realRequest = http.request;
spyOn(HttpService.prototype, 'request').and.callFake(function(this: any) {
interceptors = this.interceptors;
return realRequest.apply(this, arguments);
});
// Act
const request = http.get('/toto');
// Assert
request.subscribe(() => {
expect(http.request).toHaveBeenCalled();
expect(interceptors.some(i => i instanceof ApiPrefixInterceptor)).toBeTruthy();
expect(interceptors.some(i => i instanceof ErrorHandlerInterceptor)).toBeTruthy();
expect(interceptors.some(i => i instanceof CacheInterceptor)).toBeFalsy();
});
httpMock.expectOne({}).flush({});
});
it('should use cache', () => {
// Arrange
let interceptors: HttpInterceptor[];
const realRequest = http.request;
spyOn(HttpService.prototype, 'request').and.callFake(function(this: any) {
interceptors = this.interceptors;
return realRequest.apply(this, arguments);
});
// Act
const request = http
.cache()
.get('/toto');
// Assert
request.subscribe(() => {
expect(interceptors.some(i => i instanceof ApiPrefixInterceptor)).toBeTruthy();
expect(interceptors.some(i => i instanceof ErrorHandlerInterceptor)).toBeTruthy();
expect(interceptors.some(i => i instanceof CacheInterceptor)).toBeTruthy();
});
httpMock.expectOne({}).flush({});
});
it('should skip error handler', () => {
// Arrange
let interceptors: HttpInterceptor[];
const realRequest = http.request;
spyOn(HttpService.prototype, 'request').and.callFake(function(this: any) {
interceptors = this.interceptors;
return realRequest.apply(this, arguments);
});
// Act
const request = http
.skipErrorHandler()
.get('/toto');
// Assert
request.subscribe(() => {
expect(interceptors.some(i => i instanceof ApiPrefixInterceptor)).toBeTruthy();
expect(interceptors.some(i => i instanceof ErrorHandlerInterceptor)).toBeFalsy();
expect(interceptors.some(i => i instanceof CacheInterceptor)).toBeFalsy();
});
httpMock.expectOne({}).flush({});
});
it('should not use API prefix', () => {
// Arrange
let interceptors: HttpInterceptor[];
const realRequest = http.request;
spyOn(HttpService.prototype, 'request').and.callFake(function(this: any) {
interceptors = this.interceptors;
return realRequest.apply(this, arguments);
});
// Act
const request = http
.disableApiPrefix()
.get('/toto');
// Assert
request.subscribe(() => {
expect(interceptors.some(i => i instanceof ApiPrefixInterceptor)).toBeFalsy();
expect(interceptors.some(i => i instanceof ErrorHandlerInterceptor)).toBeTruthy();
expect(interceptors.some(i => i instanceof CacheInterceptor)).toBeFalsy();
});
httpMock.expectOne({}).flush({});
});
});
+119
View File
@@ -0,0 +1,119 @@
import { Inject, Injectable, InjectionToken, Injector, Optional } from '@angular/core';
import { HttpClient, HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
import { ErrorHandlerInterceptor } from './error-handler.interceptor';
import { CacheInterceptor } from './cache.interceptor';
import { ApiPrefixInterceptor } from './api-prefix.interceptor';
// HttpClient is declared in a re-exported module, so we have to extend the original module to make it work properly
// (see https://github.com/Microsoft/TypeScript/issues/13897)
declare module '@angular/common/http/src/client' {
// Augment HttpClient with the added configuration methods from HttpService, to allow in-place replacement of
// HttpClient with HttpService using dependency injection
export interface HttpClient {
/**
* Enables caching for this request.
* @param {boolean} forceUpdate Forces request to be made and updates cache entry.
* @return {HttpClient} The new instance.
*/
cache(forceUpdate?: boolean): HttpClient;
/**
* Skips default error handler for this request.
* @return {HttpClient} The new instance.
*/
skipErrorHandler(): HttpClient;
/**
* Do not use API prefix for this request.
* @return {HttpClient} The new instance.
*/
disableApiPrefix(): HttpClient;
}
}
// From @angular/common/http/src/interceptor: allows to chain interceptors
class HttpInterceptorHandler implements HttpHandler {
constructor(private next: HttpHandler, private interceptor: HttpInterceptor) { }
handle(request: HttpRequest<any>): Observable<HttpEvent<any>> {
return this.interceptor.intercept(request, this.next);
}
}
/**
* Allows to override default dynamic interceptors that can be disabled with the HttpService extension.
* Except for very specific needs, you should better configure these interceptors directly in the constructor below
* for better readability.
*
* For static interceptors that should always be enabled (like ApiPrefixInterceptor), use the standard
* HTTP_INTERCEPTORS token.
*/
export const HTTP_DYNAMIC_INTERCEPTORS = new InjectionToken<HttpInterceptor>('HTTP_DYNAMIC_INTERCEPTORS');
/**
* Extends HttpClient with per request configuration using dynamic interceptors.
*/
@Injectable()
export class HttpService extends HttpClient {
constructor(private httpHandler: HttpHandler,
private injector: Injector,
@Optional() @Inject(HTTP_DYNAMIC_INTERCEPTORS) private interceptors: HttpInterceptor[] = []) {
super(httpHandler);
if (!this.interceptors) {
// Configure default interceptors that can be disabled here
this.interceptors = [
this.injector.get(ApiPrefixInterceptor),
this.injector.get(ErrorHandlerInterceptor)
];
}
}
cache(forceUpdate?: boolean): HttpClient {
const cacheInterceptor = this.injector.get(CacheInterceptor).configure({ update: forceUpdate });
return this.addInterceptor(cacheInterceptor);
}
skipErrorHandler(): HttpClient {
return this.removeInterceptor(ErrorHandlerInterceptor);
}
disableApiPrefix(): HttpClient {
return this.removeInterceptor(ApiPrefixInterceptor);
}
// Override the original method to wire interceptors when triggering the request.
request(method?: any, url?: any, options?: any): any {
const handler = this.interceptors.reduceRight(
(next, interceptor) => new HttpInterceptorHandler(next, interceptor),
this.httpHandler
);
return new HttpClient(handler).request(method, url, options);
}
private removeInterceptor(interceptorType: Function): HttpService {
return new HttpService(
this.httpHandler,
this.injector,
this.interceptors.filter(i => !(i instanceof interceptorType))
);
}
private addInterceptor(interceptor: HttpInterceptor): HttpService {
return new HttpService(
this.httpHandler,
this.injector,
this.interceptors.concat([interceptor])
);
}
}
+152
View File
@@ -0,0 +1,152 @@
import { TestBed, inject } from '@angular/core/testing';
import { TranslateService, LangChangeEvent } from '@ngx-translate/core';
import { Subject } from 'rxjs';
import { extract, I18nService } from './i18n.service';
const defaultLanguage = 'en-US';
const supportedLanguages = ['eo', 'en-US', 'fr-FR'];
class MockTranslateService {
currentLang: string;
onLangChange = new Subject();
use(language: string) {
this.currentLang = language;
this.onLangChange.next({
lang: this.currentLang,
translations: {}
});
}
getBrowserCultureLang() {
return 'en-US';
}
setTranslation(lang: string, translations: Object, shouldMerge?: boolean) { }
}
describe('I18nService', () => {
let i18nService: I18nService;
let translateService: TranslateService;
let onLangChangeSpy: jasmine.Spy;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
I18nService,
{ provide: TranslateService, useClass: MockTranslateService },
]
});
});
beforeEach(inject([
I18nService,
TranslateService
], (_i18nService: I18nService,
_translateService: TranslateService) => {
i18nService = _i18nService;
translateService = _translateService;
// Create spies
onLangChangeSpy = jasmine.createSpy('onLangChangeSpy');
translateService.onLangChange
.subscribe((event: LangChangeEvent) => {
onLangChangeSpy(event.lang);
});
spyOn(translateService, 'use').and.callThrough();
}));
afterEach(() => {
// Cleanup
localStorage.removeItem('language');
});
describe('extract', () => {
it('should not modify string', () => {
expect(extract('Hello world !')).toEqual('Hello world !');
});
});
describe('init', () => {
it('should init with default language', () => {
// Act
i18nService.init(defaultLanguage, supportedLanguages);
// Assert
expect(translateService.use).toHaveBeenCalledWith(defaultLanguage);
expect(onLangChangeSpy).toHaveBeenCalledWith(defaultLanguage);
});
it('should init with save language', () => {
// Arrange
const savedLanguage = 'eo';
localStorage.setItem('language', savedLanguage);
// Act
i18nService.init(defaultLanguage, supportedLanguages);
// Assert
expect(translateService.use).toHaveBeenCalledWith(savedLanguage);
expect(onLangChangeSpy).toHaveBeenCalledWith(savedLanguage);
});
});
describe('set language', () => {
it('should change current language', () => {
// Arrange
const newLanguage = 'eo';
i18nService.init(defaultLanguage, supportedLanguages);
// Act
i18nService.language = newLanguage;
// Assert
expect(translateService.use).toHaveBeenCalledWith(newLanguage);
expect(onLangChangeSpy).toHaveBeenCalledWith(newLanguage);
});
it('should change current language without a region match', () => {
// Arrange
const newLanguage = 'fr-CA';
i18nService.init(defaultLanguage, supportedLanguages);
// Act
i18nService.language = newLanguage;
// Assert
expect(translateService.use).toHaveBeenCalledWith('fr-FR');
expect(onLangChangeSpy).toHaveBeenCalledWith('fr-FR');
});
it('should change current language to default if unsupported', () => {
// Arrange
const newLanguage = 'es';
i18nService.init(defaultLanguage, supportedLanguages);
// Act
i18nService.language = newLanguage;
// Assert
expect(translateService.use).toHaveBeenCalledWith(defaultLanguage);
expect(onLangChangeSpy).toHaveBeenCalledWith(defaultLanguage);
});
});
describe('get language', () => {
it('should return current language', () => {
// Arrange
i18nService.init(defaultLanguage, supportedLanguages);
// Act
const currentLanguage = i18nService.language;
// Assert
expect(currentLanguage).toEqual(defaultLanguage);
});
});
});
+83
View File
@@ -0,0 +1,83 @@
import { Injectable } from '@angular/core';
import { TranslateService, LangChangeEvent } from '@ngx-translate/core';
import { includes } from 'lodash';
import { Logger } from './logger.service';
import * as enUS from '../../translations/en-US.json';
import * as frFR from '../../translations/fr-FR.json';
const log = new Logger('I18nService');
const languageKey = 'language';
/**
* Pass-through function to mark a string for translation extraction.
* Running `npm translations:extract` will include the given string by using this.
* @param {string} s The string to extract for translation.
* @return {string} The same string.
*/
export function extract(s: string) {
return s;
}
@Injectable()
export class I18nService {
defaultLanguage: string;
supportedLanguages: string[];
constructor(private translateService: TranslateService) {
// Embed languages to avoid extra HTTP requests
translateService.setTranslation('en-US', enUS);
translateService.setTranslation('fr-FR', frFR);
}
/**
* Initializes i18n for the application.
* Loads language from local storage if present, or sets default language.
* @param {!string} defaultLanguage The default language to use.
* @param {Array.<String>} supportedLanguages The list of supported languages.
*/
init(defaultLanguage: string, supportedLanguages: string[]) {
this.defaultLanguage = defaultLanguage;
this.supportedLanguages = supportedLanguages;
this.language = '';
this.translateService.onLangChange
.subscribe((event: LangChangeEvent) => { localStorage.setItem(languageKey, event.lang); });
}
/**
* Sets the current language.
* Note: The current language is saved to the local storage.
* If no parameter is specified, the language is loaded from local storage (if present).
* @param {string} language The IETF language code to set.
*/
set language(language: string) {
language = language || localStorage.getItem(languageKey) || this.translateService.getBrowserCultureLang();
let isSupportedLanguage = includes(this.supportedLanguages, language);
// If no exact match is found, search without the region
if (language && !isSupportedLanguage) {
language = language.split('-')[0];
language = this.supportedLanguages.find(supportedLanguage => supportedLanguage.startsWith(language)) || '';
isSupportedLanguage = Boolean(language);
}
// Fallback if language is not supported
if (!isSupportedLanguage) {
language = this.defaultLanguage;
}
log.debug(`Language set to ${language}`);
this.translateService.use(language);
}
/**
* Gets the current language.
* @return {string} The current language code.
*/
get language(): string {
return this.translateService.currentLang;
}
}
+13
View File
@@ -0,0 +1,13 @@
export * from './core.module';
export * from './authentication/authentication.service';
export * from './authentication/authentication.service.mock';
export * from './authentication/authentication.guard';
export * from './i18n.service';
export * from './http/http.service';
export * from './http/http-cache.service';
export * from './http/api-prefix.interceptor';
export * from './http/cache.interceptor';
export * from './http/error-handler.interceptor';
export * from './route.service';
export * from './route-reusable-strategy';
export * from './logger.service';
+72
View File
@@ -0,0 +1,72 @@
import { Logger, LogLevel, LogOutput } from './logger.service';
const logMethods = ['log', 'info', 'warn', 'error'];
describe('Logger', () => {
let savedConsole: Function[];
let savedLevel: LogLevel;
let savedOutputs: LogOutput[];
beforeAll(() => {
savedConsole = [];
logMethods.forEach((m) => {
savedConsole[m] = console[m];
console[m] = () => {};
});
savedLevel = Logger.level;
savedOutputs = Logger.outputs;
});
afterAll(() => {
logMethods.forEach((m) => { console[m] = savedConsole[m]; });
Logger.level = savedLevel;
Logger.outputs = savedOutputs;
});
it('should create an instance', () => {
expect(new Logger()).toBeTruthy();
});
it('should add a new LogOutput and receives log entries', () => {
// Arrange
const outputSpy = jasmine.createSpy('outputSpy');
const log = new Logger('test');
// Act
Logger.outputs.push(outputSpy);
log.debug('d');
log.info('i');
log.warn('w');
log.error('e', { error: true });
// Assert
expect(outputSpy).toHaveBeenCalled();
expect(outputSpy.calls.count()).toBe(4);
expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Debug, 'd');
expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Info, 'i');
expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Warning, 'w');
expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Error, 'e', { error: true });
});
it('should add a new LogOutput and receives only production log entries', () => {
// Arrange
const outputSpy = jasmine.createSpy('outputSpy');
const log = new Logger('test');
// Act
Logger.outputs.push(outputSpy);
Logger.enableProductionMode();
log.debug('d');
log.info('i');
log.warn('w');
log.error('e', { error: true });
// Assert
expect(outputSpy).toHaveBeenCalled();
expect(outputSpy.calls.count()).toBe(2);
expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Warning, 'w');
expect(outputSpy).toHaveBeenCalledWith('test', LogLevel.Error, 'e', { error: true });
});
});
+113
View File
@@ -0,0 +1,113 @@
/**
* Simple logger system with the possibility of registering custom outputs.
*
* 4 different log levels are provided, with corresponding methods:
* - debug : for debug information
* - info : for informative status of the application (success, ...)
* - warning : for non-critical errors that do not prevent normal application behavior
* - error : for critical errors that prevent normal application behavior
*
* Example usage:
* ```
* import { Logger } from 'app/core/logger.service';
*
* const log = new Logger('myFile');
* ...
* log.debug('something happened');
* ```
*
* To disable debug and info logs in production, add this snippet to your root component:
* ```
* export class AppComponent implements OnInit {
* ngOnInit() {
* if (environment.production) {
* Logger.enableProductionMode();
* }
* ...
* }
* }
*
* If you want to process logs through other outputs than console, you can add LogOutput functions to Logger.outputs.
*/
/**
* The possible log levels.
* LogLevel.Off is never emitted and only used with Logger.level property to disable logs.
*/
export enum LogLevel {
Off = 0,
Error,
Warning,
Info,
Debug
}
/**
* Log output handler function.
*/
export type LogOutput = (source: string, level: LogLevel, ...objects: any[]) => void;
export class Logger {
/**
* Current logging level.
* Set it to LogLevel.Off to disable logs completely.
*/
static level = LogLevel.Debug;
/**
* Additional log outputs.
*/
static outputs: LogOutput[] = [];
/**
* Enables production mode.
* Sets logging level to LogLevel.Warning.
*/
static enableProductionMode() {
Logger.level = LogLevel.Warning;
}
constructor(private source?: string) { }
/**
* Logs messages or objects with the debug level.
* Works the same as console.log().
*/
debug(...objects: any[]) {
this.log(console.log, LogLevel.Debug, objects);
}
/**
* Logs messages or objects with the info level.
* Works the same as console.log().
*/
info(...objects: any[]) {
this.log(console.info, LogLevel.Info, objects);
}
/**
* Logs messages or objects with the warning level.
* Works the same as console.log().
*/
warn(...objects: any[]) {
this.log(console.warn, LogLevel.Warning, objects);
}
/**
* Logs messages or objects with the error level.
* Works the same as console.log().
*/
error(...objects: any[]) {
this.log(console.error, LogLevel.Error, objects);
}
private log(func: Function, level: LogLevel, objects: any[]) {
if (level <= Logger.level) {
const log = this.source ? ['[' + this.source + ']'].concat(objects) : objects;
func.apply(console, log);
Logger.outputs.forEach((output) => output.apply(output, [this.source, level].concat(objects)));
}
}
}
+28
View File
@@ -0,0 +1,28 @@
import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from '@angular/router';
/**
* A route strategy allowing for explicit route reuse.
* Used as a workaround for https://github.com/angular/angular/issues/18374
* To reuse a given route, add `data: { reuse: true }` to the route definition.
*/
export class RouteReusableStrategy extends RouteReuseStrategy {
public shouldDetach(route: ActivatedRouteSnapshot): boolean {
return false;
}
public store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle | null): void { }
public shouldAttach(route: ActivatedRouteSnapshot): boolean {
return false;
}
public retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
return null;
}
public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
return (future.routeConfig === curr.routeConfig) || future.data.reuse;
}
}
+40
View File
@@ -0,0 +1,40 @@
import { TestBed, inject } from '@angular/core/testing';
import { AuthenticationService } from './authentication/authentication.service';
import { MockAuthenticationService } from './authentication/authentication.service.mock';
import { AuthenticationGuard } from './authentication/authentication.guard';
import { ShellComponent } from './shell/shell.component';
import { Route } from './route.service';
describe('Route', () => {
let route: Route;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
AuthenticationGuard,
{ provide: AuthenticationService, useClass: MockAuthenticationService },
Route
]
});
});
beforeEach(inject([Route], (_route: Route) => {
route = _route;
}));
describe('withShell', () => {
it('should create routes as children of shell', () => {
// Prepare
const testRoutes = [{ path: 'test' }];
// Act
const result = Route.withShell(testRoutes);
// Assert
expect(result.path).toBe('');
expect(result.children).toBe(testRoutes);
expect(result.component).toBe(ShellComponent);
});
});
});
+27
View File
@@ -0,0 +1,27 @@
import { Route as ngRoute, Routes } from '@angular/router';
import { ShellComponent } from './shell/shell.component';
import { AuthenticationGuard } from './authentication/authentication.guard';
/**
* Provides helper methods to create routes.
*/
export class Route {
/**
* Creates routes using the shell component and authentication.
* @param routes The routes to add.
* @return {Route} The new route using shell as the base.
*/
static withShell(routes: Routes): ngRoute {
return {
path: '',
component: ShellComponent,
children: routes,
canActivate: [AuthenticationGuard],
// Reuse ShellComponent instance when navigating between child views
data: { reuse: true }
};
}
}
@@ -0,0 +1,71 @@
<!-- <header>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="https://github.com/ngx-rocket" translate>APP_NAME</a>
<button class="navbar-toggler" type="button" aria-controls="navbar-menu"
aria-label="Toggle navigation" (click)="toggleMenu()" [attr.aria-expanded]="!menuHidden">
<span class="navbar-toggler-icon"></span>
</button>
<div id="navbar-menu" class="collapse navbar-collapse float-xs-none" [ngbCollapse]="menuHidden">
<div class="navbar-nav">
<a class="nav-item nav-link text-uppercase" routerLink="/home" routerLinkActive="active">
<i class="fas fa-home"></i>
<span translate>Home</span>
</a>
<a class="nav-item nav-link text-uppercase" routerLink="/about" routerLinkActive="active">
<i class="fas fa-question-circle"></i>
<span translate>About</span>
</a>
</div>
<div class="navbar-nav ml-auto">
<div class="nav-item" ngbDropdown placement="bottom-right">
<a id="language-dropdown" class="nav-link" ngbDropdownToggle>{{currentLanguage}}</a>
<div ngbDropdownMenu aria-labelledby="language-dropdown">
<button class="dropdown-item" *ngFor="let language of languages" (click)="setLanguage(language)">
{{language}}
</button>
</div>
</div>
<div class="nav-item" ngbDropdown placement="bottom-right">
<a id="user-dropdown" class="nav-link" ngbDropdownToggle>
<i class="fas fa-user-circle"></i>
</a>
<div ngbDropdownMenu aria-labelledby="user-dropdown">
<h6 class="dropdown-header">
<span translate>Logged in as</span> <b>{{username}}</b></h6>
<div class="dropdown-divider"></div>
<button class="dropdown-item" (click)="logout()" translate>Logout</button>
</div>
</div>
</div>
</div>
</nav>
</header> -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="#">MifosX</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarColor02" aria-controls="navbarColor02" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarColor02">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="/clients" routerLinkActive="active">Clients</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Groups</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Loans</a>
</li>
</ul>
<form class="form-inline my-2 my-lg-0">
<input class="form-control mr-sm-2" placeholder="Search" type="text">
<button class="btn btn-secondary my-2 mr-sm-2 my-sm-0" type="submit">Search</button>
</form>
<button class="btn btn-secondary my-2 mr-sm-2 my-sm-0" (click)="logout()" type="submit">Logout</button>
</div>
</nav>
@@ -0,0 +1,10 @@
/* @import "src/theme/theme-variables";
.navbar {
margin-bottom: $spacer;
}
.nav-link.dropdown-toggle {
cursor: pointer;
}
*/
@@ -0,0 +1,40 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { RouterTestingModule } from '@angular/router/testing';
import { AuthenticationService } from '../../authentication/authentication.service';
import { MockAuthenticationService } from '../../authentication/authentication.service.mock';
import { I18nService } from '../../i18n.service';
import { HeaderComponent } from './header.component';
describe('HeaderComponent', () => {
let component: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
NgbModule.forRoot(),
TranslateModule.forRoot()
],
declarations: [HeaderComponent],
providers: [
{ provide: AuthenticationService, useClass: MockAuthenticationService },
I18nService
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,48 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AuthenticationService } from '../../authentication/authentication.service';
import { I18nService } from '../../i18n.service';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss']
})
export class HeaderComponent implements OnInit {
menuHidden = true;
constructor(private router: Router,
private authenticationService: AuthenticationService,
private i18nService: I18nService) { }
ngOnInit() { }
toggleMenu() {
this.menuHidden = !this.menuHidden;
}
setLanguage(language: string) {
this.i18nService.language = language;
}
logout() {
this.authenticationService.logout()
.subscribe(() => this.router.navigate(['/login'], { replaceUrl: true }));
}
get currentLanguage(): string {
return this.i18nService.language;
}
get languages(): string[] {
return this.i18nService.supportedLanguages;
}
get username(): string | null {
const credentials = this.authenticationService.credentials;
return credentials ? credentials.username : null;
}
}
+2
View File
@@ -0,0 +1,2 @@
<app-header></app-header>
<router-outlet></router-outlet>
@@ -0,0 +1,39 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { AuthenticationService } from '../authentication/authentication.service';
import { MockAuthenticationService } from '../authentication/authentication.service.mock';
import { CoreModule } from '../core.module';
import { ShellComponent } from './shell.component';
describe('ShellComponent', () => {
let component: ShellComponent;
let fixture: ComponentFixture<ShellComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
TranslateModule.forRoot(),
NgbModule.forRoot(),
CoreModule
],
providers: [
{ provide: AuthenticationService, useClass: MockAuthenticationService }
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ShellComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+14
View File
@@ -0,0 +1,14 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-shell',
templateUrl: './shell.component.html',
styleUrls: ['./shell.component.scss']
})
export class ShellComponent implements OnInit {
constructor() { }
ngOnInit() { }
}
+19
View File
@@ -0,0 +1,19 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { Route, extract } from '@app/core';
import { HomeComponent } from './home.component';
const routes: Routes = [
Route.withShell([
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent, data: { title: extract('Home') } }
])
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: []
})
export class HomeRoutingModule { }
+7
View File
@@ -0,0 +1,7 @@
<div class="container-fluid">
<div class="text-center">
<img class="logo" src="assets/images/mifos_lg-logo.jpg"/>
<!-- <app-loader [isLoading]="isLoading"></app-loader>
<q [hidden]="isLoading">{{quote}}</q> -->
</div>
</div>
+9
View File
@@ -0,0 +1,9 @@
.logo {
width: 500px;
}
q {
font-style: italic;
font-size: 1.2rem;
quotes: "« " " »";
}
+35
View File
@@ -0,0 +1,35 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { CoreModule } from '@app/core';
import { SharedModule } from '@app/shared';
import { HomeComponent } from './home.component';
import { QuoteService } from './quote.service';
describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CoreModule,
SharedModule,
HttpClientTestingModule
],
declarations: [HomeComponent],
providers: [QuoteService]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+25
View File
@@ -0,0 +1,25 @@
import { Component, OnInit } from '@angular/core';
import { finalize } from 'rxjs/operators';
import { QuoteService } from './quote.service';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
quote: string;
isLoading: boolean;
constructor(private quoteService: QuoteService) { }
ngOnInit() {
this.isLoading = true;
this.quoteService.getRandomQuote({ category: 'dev' })
.pipe(finalize(() => { this.isLoading = false; }))
.subscribe((quote: string) => { this.quote = quote; });
}
}
+26
View File
@@ -0,0 +1,26 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { CoreModule } from '@app/core';
import { SharedModule } from '@app/shared';
import { HomeRoutingModule } from './home-routing.module';
import { HomeComponent } from './home.component';
import { QuoteService } from './quote.service';
@NgModule({
imports: [
CommonModule,
TranslateModule,
CoreModule,
SharedModule,
HomeRoutingModule
],
declarations: [
HomeComponent
],
providers: [
QuoteService
]
})
export class HomeModule { }
+72
View File
@@ -0,0 +1,72 @@
import { TestBed, inject, async } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { CoreModule, HttpCacheService } from '@app/core';
import { QuoteService } from './quote.service';
describe('QuoteService', () => {
let quoteService: QuoteService;
let httpMock: HttpTestingController;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CoreModule,
HttpClientTestingModule
],
providers: [
HttpCacheService,
QuoteService
]
});
}));
beforeEach(inject([
HttpCacheService,
QuoteService,
HttpTestingController
], (htttpCacheService: HttpCacheService,
_quoteService: QuoteService,
_httpMock: HttpTestingController) => {
quoteService = _quoteService;
httpMock = _httpMock;
htttpCacheService.cleanCache();
}));
afterEach(() => {
httpMock.verify();
});
describe('getRandomQuote', () => {
it('should return a random Chuck Norris quote', () => {
// Arrange
const mockQuote = { value: 'a random quote' };
// Act
const randomQuoteSubscription = quoteService.getRandomQuote({ category: 'toto' });
// Assert
randomQuoteSubscription.subscribe((quote: string) => {
expect(quote).toEqual(mockQuote.value);
});
httpMock.expectOne({}).flush(mockQuote);
});
it('should return a string in case of error', () => {
// Act
const randomQuoteSubscription = quoteService.getRandomQuote({ category: 'toto' });
// Assert
randomQuoteSubscription.subscribe((quote: string) => {
expect(typeof quote).toEqual('string');
expect(quote).toContain('Error');
});
httpMock.expectOne({}).flush(null, {
status: 500,
statusText: 'error'
});
});
});
});
+30
View File
@@ -0,0 +1,30 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
const routes = {
quote: (c: RandomQuoteContext) => `/jokes/random?category=${c.category}`
};
export interface RandomQuoteContext {
// The quote's category: 'dev', 'explicit'...
category: string;
}
@Injectable()
export class QuoteService {
constructor(private httpClient: HttpClient) { }
getRandomQuote(context: RandomQuoteContext): Observable<string> {
return this.httpClient
.cache()
.get(routes.quote(context))
.pipe(
map((body: any) => body.value),
catchError(() => of('Error, could not load joke :-('))
);
}
}
+16
View File
@@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { extract } from '@app/core';
import { LoginComponent } from './login.component';
const routes: Routes = [
{ path: 'login', component: LoginComponent, data: { title: extract('Login') } }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: []
})
export class LoginRoutingModule { }
+62
View File
@@ -0,0 +1,62 @@
<div class="login-container bg-light">
<div class="login-box">
<img class="text-center img-responsive" src="assets/images/mifos-logo-flat.png" style="width:30%">
<div>
<h6 class="d-inline-block">v{{version}}</h6>
<div class="d-inline-block ml-3" ngbDropdown>
<button id="language-dropdown" class="btn btn-sm btn-secondary" ngbDropdownToggle>
{{currentLanguage}}
</button>
<div ngbDropdownMenu aria-labelledby="language-dropdown">
<button class="dropdown-item" *ngFor="let language of languages" (click)="setLanguage(language)">
{{language}}
</button>
</div>
</div>
</div>
<div class="container">
<div class="card col-md-6 mt-3 mx-auto">
<div class="card-body">
<h4 class="card-title text-center">
<i class="far fa-3x fa-user-circle text-muted"></i>
</h4>
<form (ngSubmit)="login()" [formGroup]="loginForm" novalidate>
<div class="alert alert-danger" [hidden]="!error || isLoading" translate>
Username or password incorrect.
</div>
<div class="form-group">
<label class="d-block">
<input type="text" class="form-control" formControlName="username" autocomplete="username"
[placeholder]="'Username' | translate"/>
<span hidden translate>Username</span>
<small [hidden]="loginForm.controls.username.valid || loginForm.controls.username.untouched"
class="text-danger" translate>
Username is required
</small>
</label>
<label class="d-block">
<input type="password" class="form-control" formControlName="password" autocomplete="current-password"
[placeholder]="'Password' | translate" required/>
<span hidden translate>Password</span>
<small [hidden]="loginForm.controls.password.valid || loginForm.controls.password.untouched"
class="text-danger" translate>
Password is required
</small>
</label>
<div class="form-check">
<label class="form-check-label">
<input type="checkbox" class="form-check-input" formControlName="remember"/>
<span translate>Remember me</span>
</label>
</div>
</div>
<button class="btn btn-primary w-100" type="submit" [disabled]="loginForm.invalid || isLoading">
<i class="fas fa-cog fa-spin" [hidden]="!isLoading"></i>
<span translate>Login</span>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
+29
View File
@@ -0,0 +1,29 @@
@import "src/theme/theme-variables";
@import "~bootstrap/scss/mixins/_breakpoints";
.login-container {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.login-box {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
min-height: 100%;
}
.ng-invalid.ng-touched:not(form) {
border-left: 4px solid theme-color("danger");
}
@include media-breakpoint-down(xs) {
.container {
width: 100%;
}
}
+37
View File
@@ -0,0 +1,37 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { RouterTestingModule } from '@angular/router/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { CoreModule } from '@app/core';
import { LoginComponent } from './login.component';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
NgbModule.forRoot(),
RouterTestingModule,
TranslateModule.forRoot(),
ReactiveFormsModule,
CoreModule
],
declarations: [LoginComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+68
View File
@@ -0,0 +1,68 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { finalize } from 'rxjs/operators';
import { environment } from '@env/environment';
import { Logger, I18nService, AuthenticationService } from '@app/core';
const log = new Logger('Login');
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
version: string = environment.version;
error: string;
loginForm: FormGroup;
isLoading = false;
constructor(private router: Router,
private formBuilder: FormBuilder,
private i18nService: I18nService,
private authenticationService: AuthenticationService) {
this.createForm();
}
ngOnInit() { }
login() {
this.isLoading = true;
this.authenticationService.login(this.loginForm.value)
.pipe(finalize(() => {
this.loginForm.markAsPristine();
this.isLoading = false;
}))
.subscribe(credentials => {
log.debug(`${credentials.username} successfully logged in`);
this.router.navigate(['/'], { replaceUrl: true });
}, error => {
log.debug(`Login error: ${error}`);
this.error = error;
});
}
setLanguage(language: string) {
this.i18nService.language = language;
}
get currentLanguage(): string {
return this.i18nService.language;
}
get languages(): string[] {
return this.i18nService.supportedLanguages;
}
private createForm() {
this.loginForm = this.formBuilder.group({
username: ['', Validators.required],
password: ['', Validators.required],
remember: true
});
}
}
+22
View File
@@ -0,0 +1,22 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { LoginRoutingModule } from './login-routing.module';
import { LoginComponent } from './login.component';
@NgModule({
imports: [
CommonModule,
ReactiveFormsModule,
TranslateModule,
NgbModule,
LoginRoutingModule
],
declarations: [
LoginComponent
]
})
export class LoginModule { }
+2
View File
@@ -0,0 +1,2 @@
export * from './shared.module';
export * from './loader/loader.component';
@@ -0,0 +1,4 @@
<div [hidden]="!isLoading" class="text-xs-center">
<i class="fas fa-cog fa-spin fa-3x"></i> <span>{{message}}</span>
</div>
@@ -0,0 +1,3 @@
.fa {
vertical-align: middle;
}
@@ -0,0 +1,65 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoaderComponent } from './loader.component';
describe('LoaderComponent', () => {
let component: LoaderComponent;
let fixture: ComponentFixture<LoaderComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [LoaderComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should not be visible by default', () => {
// Arrange
const element = fixture.nativeElement;
const div = element.querySelectorAll('div')[0];
// Assert
expect(div.getAttribute('hidden')).not.toBeNull();
});
it('should be visible when app is loading', () => {
// Arrange
const element = fixture.nativeElement;
const div = element.querySelectorAll('div')[0];
// Act
fixture.componentInstance.isLoading = true;
fixture.detectChanges();
// Assert
expect(div.getAttribute('hidden')).toBeNull();
});
it('should not display a message by default', () => {
// Arrange
const element = fixture.nativeElement;
const span = element.querySelectorAll('span')[0];
// Assert
expect(span.innerText).toBe('');
});
it('should display specified message', () => {
// Arrange
const element = fixture.nativeElement;
const span = element.querySelectorAll('span')[0];
// Act
fixture.componentInstance.message = 'testing';
fixture.detectChanges();
// Assert
expect(span.innerText).toBe('testing');
});
});
+17
View File
@@ -0,0 +1,17 @@
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'app-loader',
templateUrl: './loader.component.html',
styleUrls: ['./loader.component.scss']
})
export class LoaderComponent implements OnInit {
@Input() isLoading = false;
@Input() message: string;
constructor() { }
ngOnInit() { }
}
+17
View File
@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LoaderComponent } from './loader/loader.component';
@NgModule({
imports: [
CommonModule
],
declarations: [
LoaderComponent
],
exports: [
LoaderComponent
]
})
export class SharedModule { }