Merge pull request #1 in ~VLE2FE/dfop-ui from validation to development
* commit 'fd7cf7b77ab5f7921b6df97df30e3dc9740f6d11': finished tests update to 8 before update
This commit is contained in:
commit
0c241bafc9
@ -22,7 +22,7 @@
|
|||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
"polyfills": "src/polyfills.ts",
|
"polyfills": "src/polyfills.ts",
|
||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "tsconfig.app.json",
|
||||||
"aot": false,
|
"aot": true,
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/favicon.ico",
|
"src/favicon.ico",
|
||||||
"src/assets",
|
"src/assets",
|
||||||
@ -46,7 +46,6 @@
|
|||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
"extractCss": true,
|
"extractCss": true,
|
||||||
"namedChunks": false,
|
"namedChunks": false,
|
||||||
"aot": true,
|
|
||||||
"extractLicenses": true,
|
"extractLicenses": true,
|
||||||
"vendorChunk": false,
|
"vendorChunk": false,
|
||||||
"buildOptimizer": true,
|
"buildOptimizer": true,
|
||||||
@ -68,7 +67,8 @@
|
|||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "UI:build"
|
"browserTarget": "UI:build",
|
||||||
|
"proxyConfig": "src/proxy.conf.json"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
@ -89,7 +89,6 @@
|
|||||||
"polyfills": "src/polyfills.ts",
|
"polyfills": "src/polyfills.ts",
|
||||||
"tsConfig": "tsconfig.spec.json",
|
"tsConfig": "tsconfig.spec.json",
|
||||||
"karmaConfig": "karma.conf.js",
|
"karmaConfig": "karma.conf.js",
|
||||||
"codeCoverage": true,
|
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/favicon.ico",
|
"src/favicon.ico",
|
||||||
"src/assets"
|
"src/assets"
|
||||||
|
7008
package-lock.json
generated
7008
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
35
package.json
35
package.json
@ -12,30 +12,31 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "~8.2.14",
|
"@angular/animations": "~9.1.7",
|
||||||
"@angular/common": "~8.2.14",
|
"@angular/common": "~9.1.7",
|
||||||
"@angular/compiler": "~8.2.14",
|
"@angular/compiler": "~9.1.7",
|
||||||
"@angular/core": "~8.2.14",
|
"@angular/core": "~9.1.7",
|
||||||
"@angular/forms": "~8.2.14",
|
"@angular/forms": "~9.1.7",
|
||||||
"@angular/platform-browser": "~8.2.14",
|
"@angular/platform-browser": "~9.1.7",
|
||||||
"@angular/platform-browser-dynamic": "~8.2.14",
|
"@angular/platform-browser-dynamic": "~9.1.7",
|
||||||
"@angular/router": "~8.2.14",
|
"@angular/router": "~9.1.7",
|
||||||
"@hapi/joi": "^17.1.1",
|
"@hapi/joi": "^17.1.1",
|
||||||
"@inst-iot/bosch-angular-ui-components": "^0.5.30",
|
"@inst-iot/bosch-angular-ui-components": "^0.5.30",
|
||||||
|
"angular-2-local-storage": "^3.0.2",
|
||||||
"flatpickr": "^4.6.3",
|
"flatpickr": "^4.6.3",
|
||||||
"rxjs": "~6.4.0",
|
"rxjs": "~6.5.5",
|
||||||
"tslib": "^1.10.0",
|
"tslib": "^1.10.0",
|
||||||
"zone.js": "~0.9.1"
|
"zone.js": "~0.10.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "~0.803.22",
|
"@angular-devkit/build-angular": "~0.901.6",
|
||||||
"@angular/cli": "~8.3.22",
|
"@angular/cli": "~9.1.6",
|
||||||
"@angular/compiler-cli": "~8.2.14",
|
"@angular/compiler-cli": "~9.1.7",
|
||||||
"@angular/language-service": "~8.2.14",
|
"@angular/language-service": "~9.1.7",
|
||||||
"@types/node": "~8.9.4",
|
|
||||||
"@types/jasmine": "~3.3.8",
|
"@types/jasmine": "~3.3.8",
|
||||||
"@types/jasminewd2": "~2.0.3",
|
"@types/jasminewd2": "~2.0.3",
|
||||||
"codelyzer": "^5.0.0",
|
"@types/node": "^12.11.1",
|
||||||
|
"codelyzer": "^5.1.2",
|
||||||
"jasmine-core": "~3.4.0",
|
"jasmine-core": "~3.4.0",
|
||||||
"jasmine-spec-reporter": "~4.2.1",
|
"jasmine-spec-reporter": "~4.2.1",
|
||||||
"karma": "~4.1.0",
|
"karma": "~4.1.0",
|
||||||
@ -46,6 +47,6 @@
|
|||||||
"protractor": "~5.4.0",
|
"protractor": "~5.4.0",
|
||||||
"ts-node": "~7.0.0",
|
"ts-node": "~7.0.0",
|
||||||
"tslint": "~5.15.0",
|
"tslint": "~5.15.0",
|
||||||
"typescript": "~3.5.3"
|
"typescript": "~3.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
53
src/app/api.service.spec.ts
Normal file
53
src/app/api.service.spec.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { ApiService } from './api.service';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {LocalStorageService} from 'angular-2-local-storage';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
|
|
||||||
|
let apiService: ApiService;
|
||||||
|
let httpClientSpy: jasmine.SpyObj<HttpClient>;
|
||||||
|
let localStorageServiceSpy: jasmine.SpyObj<LocalStorageService>;
|
||||||
|
|
||||||
|
describe('ApiService', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const httpSpy = jasmine.createSpyObj('HttpClient', ['get']);
|
||||||
|
const localStorageSpy = jasmine.createSpyObj('LocalStorageService', ['get']);
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
ApiService,
|
||||||
|
{provide: HttpClient, useValue: httpSpy},
|
||||||
|
{provide: LocalStorageService, useValue: localStorageSpy}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
apiService = TestBed.inject(ApiService);
|
||||||
|
httpClientSpy = TestBed.inject(HttpClient) as jasmine.SpyObj<HttpClient>;
|
||||||
|
localStorageServiceSpy = TestBed.inject(LocalStorageService) as jasmine.SpyObj<LocalStorageService>;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(apiService).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do get requests without auth if not available', () => {
|
||||||
|
const getReturn = new Observable();
|
||||||
|
httpClientSpy.get.and.returnValue(getReturn);
|
||||||
|
localStorageServiceSpy.get.and.returnValue(undefined);
|
||||||
|
|
||||||
|
const result = apiService.get('/testurl');
|
||||||
|
expect(result).toBe(getReturn);
|
||||||
|
expect(httpClientSpy.get).toHaveBeenCalledWith('/testurl', {});
|
||||||
|
expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth');
|
||||||
|
});
|
||||||
|
it('should do get requests with basic auth if available', () => {
|
||||||
|
const getReturn = new Observable();
|
||||||
|
httpClientSpy.get.and.returnValue(getReturn);
|
||||||
|
localStorageServiceSpy.get.and.returnValue('basicAuth');
|
||||||
|
|
||||||
|
const result = apiService.get('/testurl');
|
||||||
|
expect(result).toBe(getReturn);
|
||||||
|
expect(httpClientSpy.get).toHaveBeenCalledWith('/testurl', jasmine.any(Object)); // could not test http headers better
|
||||||
|
expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth');
|
||||||
|
});
|
||||||
|
});
|
28
src/app/api.service.ts
Normal file
28
src/app/api.service.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {HttpClient, HttpHeaders} from '@angular/common/http';
|
||||||
|
import {LocalStorageService} from 'angular-2-local-storage';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ApiService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private http: HttpClient,
|
||||||
|
private storage: LocalStorageService
|
||||||
|
) { }
|
||||||
|
|
||||||
|
get(url) {
|
||||||
|
return this.http.get(url, this.authOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
private authOptions() {
|
||||||
|
const auth = this.storage.get('basicAuth');
|
||||||
|
if (auth) {
|
||||||
|
return {headers: new HttpHeaders({Authorization: 'Basic ' + auth})};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,17 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { Routes, RouterModule } from '@angular/router';
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
import {HomeComponent} from './home/home.component';
|
import {HomeComponent} from './home/home.component';
|
||||||
|
import {LoginService} from './login.service';
|
||||||
|
import {SamplesComponent} from './samples/samples.component';
|
||||||
|
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{path: '', component: HomeComponent}
|
{path: '', component: HomeComponent},
|
||||||
|
{path: 'samples', component: SamplesComponent},
|
||||||
|
{path: 'replace-me', component: HomeComponent, canActivate: [LoginService]},
|
||||||
|
|
||||||
|
// if not authenticated
|
||||||
|
{ path: '**', redirectTo: '' }
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<rb-full-header>
|
<rb-full-header>
|
||||||
<nav *rbMainNavItems>
|
<nav *rbMainNavItems>
|
||||||
<a routerLink="/" routerLinkActive="active" rbLoadingLink>Home</a>
|
<a routerLink="/" routerLinkActive="active" rbLoadingLink>Home</a>
|
||||||
|
<a routerLink="/samples" routerLinkActive="active" rbLoadingLink>Samples</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div *rbSubBrandHeader>Digital Fingerprint of Plastics</div>
|
<div *rbSubBrandHeader>Digital Fingerprint of Plastics</div>
|
||||||
</rb-full-header>
|
</rb-full-header>
|
||||||
|
@ -1,30 +1,45 @@
|
|||||||
import { TestBed, async } from '@angular/core/testing';
|
import {TestBed, async, ComponentFixture} from '@angular/core/testing';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
import {By} from '@angular/platform-browser';
|
||||||
|
import {RbUiComponentsModule} from '@inst-iot/bosch-angular-ui-components';
|
||||||
|
import {RouterTestingModule} from '@angular/router/testing';
|
||||||
|
|
||||||
describe('AppComponent', () => {
|
describe('AppComponent', () => {
|
||||||
|
let component: AppComponent;
|
||||||
|
let fixture: ComponentFixture<AppComponent>;
|
||||||
|
let css; // get native element by css selector
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ AppComponent ],
|
||||||
imports: [
|
imports: [
|
||||||
|
RbUiComponentsModule,
|
||||||
RouterTestingModule
|
RouterTestingModule
|
||||||
],
|
]
|
||||||
declarations: [
|
|
||||||
AppComponent
|
|
||||||
],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should create the app', () => {
|
beforeEach(() => {
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
fixture = TestBed.createComponent(AppComponent);
|
||||||
const app = fixture.debugElement.componentInstance;
|
component = fixture.componentInstance;
|
||||||
expect(app).toBeTruthy();
|
fixture.detectChanges();
|
||||||
|
css = (selector) => fixture.debugElement.query(By.css(selector)).nativeElement;
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should have the correct title'`, () => {
|
it('should create the app', () => {
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
expect(component).toBeTruthy();
|
||||||
const app = fixture.debugElement.componentInstance;
|
|
||||||
expect(app.title).toEqual('UI');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should have the header', () => {
|
||||||
|
expect(css('rb-full-header')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have the correct app title', () => {
|
||||||
|
expect(css('rb-full-header div.sub-brand-content > div').innerText).toBe('Digital Fingerprint of Plastics');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have the page container', () => {
|
||||||
|
expect(css('.container')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -6,5 +6,4 @@ import { Component } from '@angular/core';
|
|||||||
styleUrls: ['./app.component.scss']
|
styleUrls: ['./app.component.scss']
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
title = 'UI';
|
|
||||||
}
|
}
|
||||||
|
@ -7,18 +7,27 @@ import {RbUiComponentsModule} from '@inst-iot/bosch-angular-ui-components';
|
|||||||
import { LoginComponent } from './login/login.component';
|
import { LoginComponent } from './login/login.component';
|
||||||
import { HomeComponent } from './home/home.component';
|
import { HomeComponent } from './home/home.component';
|
||||||
import {FormsModule} from '@angular/forms';
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {LocalStorageModule} from 'angular-2-local-storage';
|
||||||
|
import {HttpClientModule} from '@angular/common/http';
|
||||||
|
import { SamplesComponent } from './samples/samples.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
LoginComponent,
|
LoginComponent,
|
||||||
HomeComponent
|
HomeComponent,
|
||||||
|
SamplesComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
|
LocalStorageModule.forRoot({
|
||||||
|
prefix: 'dfop',
|
||||||
|
storageType: 'localStorage'
|
||||||
|
}),
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
RbUiComponentsModule,
|
RbUiComponentsModule,
|
||||||
FormsModule
|
FormsModule,
|
||||||
|
HttpClientModule
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { HomeComponent } from './home.component';
|
import { HomeComponent } from './home.component';
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {By} from '@angular/platform-browser';
|
||||||
|
|
||||||
|
@Component({selector: 'app-login', template: ''})
|
||||||
|
class LoginStubComponent {}
|
||||||
|
|
||||||
describe('HomeComponent', () => {
|
describe('HomeComponent', () => {
|
||||||
let component: HomeComponent;
|
let component: HomeComponent;
|
||||||
@ -8,7 +12,10 @@ describe('HomeComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ HomeComponent ]
|
declarations: [
|
||||||
|
HomeComponent,
|
||||||
|
LoginStubComponent
|
||||||
|
]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
}));
|
}));
|
||||||
@ -22,4 +29,8 @@ describe('HomeComponent', () => {
|
|||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should load the login component', () => {
|
||||||
|
expect(fixture.debugElement.query(By.css('app-login'))).toBeTruthy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
81
src/app/login.service.spec.ts
Normal file
81
src/app/login.service.spec.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LoginService } from './login.service';
|
||||||
|
import {LocalStorageService} from 'angular-2-local-storage';
|
||||||
|
import {ApiService} from './api.service';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
|
|
||||||
|
let loginService: LoginService;
|
||||||
|
let apiServiceSpy: jasmine.SpyObj<ApiService>;
|
||||||
|
let localStorageServiceSpy: jasmine.SpyObj<LocalStorageService>;
|
||||||
|
|
||||||
|
describe('LoginService', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const apiSpy = jasmine.createSpyObj('ApiService', ['get']);
|
||||||
|
const localStorageSpy = jasmine.createSpyObj('LocalStorageService', ['set', 'remove']);
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
LoginService,
|
||||||
|
{provide: ApiService, useValue: apiSpy},
|
||||||
|
{provide: LocalStorageService, useValue: localStorageSpy}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
loginService = TestBed.inject(LoginService);
|
||||||
|
apiServiceSpy = TestBed.inject(ApiService) as jasmine.SpyObj<ApiService>;
|
||||||
|
localStorageServiceSpy = TestBed.inject(LocalStorageService) as jasmine.SpyObj<LocalStorageService>;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(loginService).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('login', () => {
|
||||||
|
it('should store the basic auth', () => {
|
||||||
|
localStorageServiceSpy.set.and.returnValue(true);
|
||||||
|
apiServiceSpy.get.and.returnValue(new Observable());
|
||||||
|
loginService.login('username', 'password');
|
||||||
|
expect(localStorageServiceSpy.set).toHaveBeenCalledWith('basicAuth', 'dXNlcm5hbWU6cGFzc3dvcmQ=');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove the basic auth if login fails', () => {
|
||||||
|
localStorageServiceSpy.set.and.returnValue(true);
|
||||||
|
localStorageServiceSpy.remove.and.returnValue(true);
|
||||||
|
apiServiceSpy.get.and.returnValue(new Observable(o => o.error()));
|
||||||
|
loginService.login('username', 'password');
|
||||||
|
expect(localStorageServiceSpy.remove.calls.count()).toBe(1);
|
||||||
|
expect(localStorageServiceSpy.remove).toHaveBeenCalledWith('basicAuth');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve true when login succeeds', async () => {
|
||||||
|
localStorageServiceSpy.set.and.returnValue(true);
|
||||||
|
apiServiceSpy.get.and.returnValue(new Observable(o => o.next({status: 'Authorization successful', method: 'basic'})));
|
||||||
|
expect(await loginService.login('username', 'password')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve false when a wrong result comes in', async () => {
|
||||||
|
localStorageServiceSpy.set.and.returnValue(true);
|
||||||
|
apiServiceSpy.get.and.returnValue(new Observable(o => o.next({status: 'xxx', method: 'basic'})));
|
||||||
|
expect(await loginService.login('username', 'password')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve false on an error', async () => {
|
||||||
|
localStorageServiceSpy.set.and.returnValue(true);
|
||||||
|
apiServiceSpy.get.and.returnValue(new Observable(o => o.error()));
|
||||||
|
expect(await loginService.login('username', 'password')).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('canActivate', () => {
|
||||||
|
it('should return false at first', () => {
|
||||||
|
expect(loginService.canActivate(null, null)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true if login was successful', async () => {
|
||||||
|
localStorageServiceSpy.set.and.returnValue(true);
|
||||||
|
apiServiceSpy.get.and.returnValue(new Observable(o => o.next({status: 'Authorization successful', method: 'basic'})));
|
||||||
|
await loginService.login('username', 'password');
|
||||||
|
expect(loginService.canActivate(null, null)).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
43
src/app/login.service.ts
Normal file
43
src/app/login.service.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {ApiService} from './api.service';
|
||||||
|
import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router';
|
||||||
|
import {LocalStorageService} from 'angular-2-local-storage';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class LoginService implements CanActivate {
|
||||||
|
|
||||||
|
private loggedIn = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private api: ApiService,
|
||||||
|
private storage: LocalStorageService
|
||||||
|
) { }
|
||||||
|
|
||||||
|
login(username, password) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.storage.set('basicAuth', btoa(username + ':' + password));
|
||||||
|
this.api.get('/authorized').subscribe((data: any) => {
|
||||||
|
if (data.status === 'Authorization successful') {
|
||||||
|
this.loggedIn = true;
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.loggedIn = false;
|
||||||
|
this.storage.remove('basicAuth');
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.loggedIn = false;
|
||||||
|
this.storage.remove('basicAuth');
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||||
|
return this.loggedIn;
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,8 @@
|
|||||||
<div class="login-wrapper">
|
<div class="login-wrapper">
|
||||||
<h2>Please log in</h2>
|
<h2>Please log in</h2>
|
||||||
|
|
||||||
<rb-form-input name="username" label="username">
|
<rb-form-input name="username" label="username" [(ngModel)]="username"></rb-form-input>
|
||||||
</rb-form-input>
|
<rb-form-input type="password" name="password" label="password" [(ngModel)]="password"></rb-form-input>
|
||||||
<rb-form-input type="password" name="password" label="password">
|
<span class="message">{{message}}</span>
|
||||||
</rb-form-input>
|
<button class="rb-btn rb-primary login-button" (click)="login()">Login</button>
|
||||||
<button class="rb-btn rb-primary">Login</button>
|
|
||||||
<span>{{message}}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,3 +1,12 @@
|
|||||||
.login-wrapper {
|
.login-wrapper {
|
||||||
max-width: 220px;
|
max-width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button {
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,107 @@
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
|
||||||
|
|
||||||
import { LoginComponent } from './login.component';
|
import { LoginComponent } from './login.component';
|
||||||
|
import {LoginService} from '../login.service';
|
||||||
|
import {ValidationService} from '../validation.service';
|
||||||
|
import {RbUiComponentsModule} from '@inst-iot/bosch-angular-ui-components';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {By} from '@angular/platform-browser';
|
||||||
|
|
||||||
|
let validationServiceSpy: jasmine.SpyObj<ValidationService>;
|
||||||
|
let loginServiceSpy: jasmine.SpyObj<LoginService>;
|
||||||
|
|
||||||
describe('LoginComponent', () => {
|
describe('LoginComponent', () => {
|
||||||
let component: LoginComponent;
|
let component: LoginComponent;
|
||||||
let fixture: ComponentFixture<LoginComponent>;
|
let fixture: ComponentFixture<LoginComponent>;
|
||||||
|
let css; // get native element by css selector
|
||||||
|
let cssd; // get debug element by css selector
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
|
const validationSpy = jasmine.createSpyObj('ValidationService', ['username', 'password']);
|
||||||
|
const loginSpy = jasmine.createSpyObj('LoginService', ['login']);
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ LoginComponent ]
|
declarations: [ LoginComponent ],
|
||||||
|
imports: [
|
||||||
|
RbUiComponentsModule,
|
||||||
|
FormsModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{provide: ValidationService, useValue: validationSpy},
|
||||||
|
{provide: LoginService, useValue: loginSpy}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
|
|
||||||
|
validationServiceSpy = TestBed.inject(ValidationService) as jasmine.SpyObj<ValidationService>;
|
||||||
|
loginServiceSpy = TestBed.inject(LoginService) as jasmine.SpyObj<LoginService>;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(LoginComponent);
|
fixture = TestBed.createComponent(LoginComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
cssd = (selector) => fixture.debugElement.query(By.css(selector));
|
||||||
|
css = (selector) => fixture.debugElement.query(By.css(selector)).nativeElement;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should have the `Please log in` heading', () => {
|
||||||
|
expect(css('h2').innerText).toBe('Please log in');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have empty credential inputs', () => {
|
||||||
|
expect(css('rb-form-input[label=username] input').value).toBe('');
|
||||||
|
expect(css('rb-form-input[label=password] input').value).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have an empty message at the beginning', () => {
|
||||||
|
expect(css('.message').innerText).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have a login button', () => {
|
||||||
|
expect(css('.login-button')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display a message when a wrong username was entered', () => {
|
||||||
|
validationServiceSpy.username.and.returnValue({ok: false, error: 'username must only contain a-z0-9-_.'});
|
||||||
|
validationServiceSpy.password.and.returnValue({ok: true, error: ''});
|
||||||
|
|
||||||
|
cssd('.login-button').triggerEventHandler('click', null);
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(css('.message').innerText).toBe('username must only contain a-z0-9-_.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display a message when a wrong password was entered', () => {
|
||||||
|
validationServiceSpy.username.and.returnValue({ok: true, error: ''});
|
||||||
|
validationServiceSpy.password.and.returnValue({ok: false, error: 'password must only contain a-zA-Z0-9!"#%&\'()*+,-./:;<=>?@[]^_`{|}~'});
|
||||||
|
|
||||||
|
cssd('.login-button').triggerEventHandler('click', null);
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(css('.message').innerText).toBe('password must only contain a-zA-Z0-9!"#%&\'()*+,-./:;<=>?@[]^_`{|}~');
|
||||||
|
expect(loginServiceSpy.login).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call the LoginService with valid credentials', () => {
|
||||||
|
validationServiceSpy.username.and.returnValue({ok: true, error: ''});
|
||||||
|
validationServiceSpy.password.and.returnValue({ok: true, error: ''});
|
||||||
|
loginServiceSpy.login.and.returnValue(new Promise(r => r(true)));
|
||||||
|
|
||||||
|
cssd('.login-button').triggerEventHandler('click', null);
|
||||||
|
expect(loginServiceSpy.login.calls.count()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display an error if the LoginService could not authenticate', fakeAsync(() => {
|
||||||
|
validationServiceSpy.username.and.returnValue({ok: true, error: ''});
|
||||||
|
validationServiceSpy.password.and.returnValue({ok: true, error: ''});
|
||||||
|
loginServiceSpy.login.and.returnValue(new Promise(r => r(false)));
|
||||||
|
|
||||||
|
cssd('.login-button').triggerEventHandler('click', null);
|
||||||
|
expect(loginServiceSpy.login.calls.count()).toBe(1);
|
||||||
|
tick();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(css('.message').innerText).toBe('Wrong credentials! Try again.');
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import {ValidationService} from '../validation.service';
|
||||||
|
import {LoginService} from '../login.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-login',
|
selector: 'app-login',
|
||||||
@ -7,13 +9,33 @@ import { Component, OnInit } from '@angular/core';
|
|||||||
})
|
})
|
||||||
export class LoginComponent implements OnInit {
|
export class LoginComponent implements OnInit {
|
||||||
|
|
||||||
message = '';
|
message = ''; // message below login fields
|
||||||
|
username = ''; // credentials
|
||||||
|
password = '';
|
||||||
|
validCredentials = false; // true if entered credentials are valid
|
||||||
|
|
||||||
constructor() { }
|
constructor(
|
||||||
|
private validate: ValidationService,
|
||||||
|
private loginService: LoginService
|
||||||
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
login() {
|
||||||
|
const {ok: userOk, error: userError} = this.validate.username(this.username);
|
||||||
|
const {ok: passwordOk, error: passwordError} = this.validate.password(this.password);
|
||||||
|
this.message = userError + (userError !== '' && passwordError !== '' ? '\n' : '') + passwordError; // display errors
|
||||||
|
if (userOk && passwordOk) {
|
||||||
|
this.loginService.login(this.username, this.password).then(ok => {
|
||||||
|
if (ok) {
|
||||||
|
this.message = 'Login successful'; // TODO: think about following action
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.message = 'Wrong credentials! Try again.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
1
src/app/samples/samples.component.html
Normal file
1
src/app/samples/samples.component.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<p>samples works!</p>
|
0
src/app/samples/samples.component.scss
Normal file
0
src/app/samples/samples.component.scss
Normal file
25
src/app/samples/samples.component.spec.ts
Normal file
25
src/app/samples/samples.component.spec.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SamplesComponent } from './samples.component';
|
||||||
|
|
||||||
|
describe('SamplesComponent', () => {
|
||||||
|
let component: SamplesComponent;
|
||||||
|
let fixture: ComponentFixture<SamplesComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ SamplesComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SamplesComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
15
src/app/samples/samples.component.ts
Normal file
15
src/app/samples/samples.component.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-samples',
|
||||||
|
templateUrl: './samples.component.html',
|
||||||
|
styleUrls: ['./samples.component.scss']
|
||||||
|
})
|
||||||
|
export class SamplesComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,12 +1,35 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { ValidationService } from './validation.service';
|
import { ValidationService } from './validation.service';
|
||||||
|
|
||||||
|
let validationService: ValidationService;
|
||||||
|
|
||||||
describe('ValidationService', () => {
|
describe('ValidationService', () => {
|
||||||
beforeEach(() => TestBed.configureTestingModule({}));
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [ValidationService]
|
||||||
|
});
|
||||||
|
|
||||||
|
validationService = TestBed.inject(ValidationService);
|
||||||
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
const service: ValidationService = TestBed.get(ValidationService);
|
expect(validationService).toBeTruthy();
|
||||||
expect(service).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return true on a correct username', () => {
|
||||||
|
expect(validationService.username('abc')).toEqual({ok: true, error: ''});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an error on an incorrect username', () => {
|
||||||
|
expect(validationService.username('abc#')).toEqual({ok: false, error: 'username must only contain a-z0-9-_.'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true on a correct password', () => {
|
||||||
|
expect(validationService.password('Abc123!#')).toEqual({ok: true, error: ''});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an error on an incorrect password', () => {
|
||||||
|
expect(validationService.password('Abc123')).toEqual({ok: false, error: 'password must only contain a-zA-Z0-9!"#%&\'()*+,-./:;<=>?@[]^_`{|}~'});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,36 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import Joi from '@hapi/joi';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class ValidationService {
|
export class ValidationService {
|
||||||
|
|
||||||
|
private vUsername = Joi.string()
|
||||||
|
.lowercase()
|
||||||
|
.pattern(new RegExp('^[a-z0-9-_.]+$'))
|
||||||
|
.min(1)
|
||||||
|
.max(128);
|
||||||
|
|
||||||
|
private vPassword = Joi.string()
|
||||||
|
.pattern(new RegExp('^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$).{8,}$'))
|
||||||
|
.max(128);
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
|
username(data) {
|
||||||
|
const {ignore, error} = this.vUsername.validate(data);
|
||||||
|
if (error) {
|
||||||
|
return {ok: false, error: 'username must only contain a-z0-9-_.'};
|
||||||
|
}
|
||||||
|
return {ok: true, error: ''};
|
||||||
|
}
|
||||||
|
|
||||||
|
password(data) {
|
||||||
|
const {ignore, error} = this.vPassword.validate(data);
|
||||||
|
if (error) {
|
||||||
|
return {ok: false, error: 'password must only contain a-zA-Z0-9!"#%&\'()*+,-./:;<=>?@[]^_`{|}~'};
|
||||||
|
}
|
||||||
|
return {ok: true, error: ''};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
6
src/proxy.conf.json
Normal file
6
src/proxy.conf.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"/api": {
|
||||||
|
"target": "http://localhost:3000",
|
||||||
|
"secure": false
|
||||||
|
}
|
||||||
|
}
|
@ -9,7 +9,7 @@
|
|||||||
"src/polyfills.ts"
|
"src/polyfills.ts"
|
||||||
],
|
],
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.ts"
|
"src/**/*.d.ts"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"src/test.ts",
|
"src/test.ts",
|
||||||
|
@ -3,6 +3,13 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"array-type": false,
|
"array-type": false,
|
||||||
"arrow-parens": false,
|
"arrow-parens": false,
|
||||||
|
"brace-style": [
|
||||||
|
true,
|
||||||
|
"stroustrup",
|
||||||
|
{
|
||||||
|
"allowSingleLine": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"deprecation": {
|
"deprecation": {
|
||||||
"severity": "warning"
|
"severity": "warning"
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user