code improvements

This commit is contained in:
VLE2FE
2020-09-03 15:51:53 +02:00
parent 1440e9a6fc
commit c38d0be457
73 changed files with 276 additions and 1686 deletions

View File

@ -1,143 +1,5 @@
import {async, 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';
import {ModalService} from '@inst-iot/bosch-angular-ui-components';
let apiService: ApiService;
let httpClientSpy: jasmine.SpyObj<HttpClient>;
let localStorageServiceSpy: jasmine.SpyObj<LocalStorageService>;
let modalServiceSpy: jasmine.SpyObj<ModalService>;
let windowServiceSpy: jasmine.SpyObj<Window>;
// TODO
// TODO: test options
describe('ApiService', () => {
beforeEach(() => {
const httpSpy = jasmine.createSpyObj('HttpClient', ['get', 'post', 'put', 'delete']);
const localStorageSpy = jasmine.createSpyObj('LocalStorageService', ['get']);
const modalSpy = jasmine.createSpyObj('ModalService', ['openComponent']);
const windowSpy = jasmine.createSpyObj('Window', ['location']);
TestBed.configureTestingModule({
providers: [
ApiService,
{provide: HttpClient, useValue: httpSpy},
{provide: LocalStorageService, useValue: localStorageSpy},
{provide: ModalService, useValue: modalSpy},
{provide: Window, useValue: windowSpy}
]
});
apiService = TestBed.inject(ApiService);
httpClientSpy = TestBed.inject(HttpClient) as jasmine.SpyObj<HttpClient>;
localStorageServiceSpy = TestBed.inject(LocalStorageService) as jasmine.SpyObj<LocalStorageService>;
modalServiceSpy = TestBed.inject(ModalService) as jasmine.SpyObj<ModalService>;
windowServiceSpy = TestBed.inject(Window) as jasmine.SpyObj<Window>;
});
it('should be created', () => {
expect(apiService).toBeTruthy();
});
it('shows an error message when the request fails', () => {
const getReturn = new Observable(observer => {
observer.error('error');
});
httpClientSpy.get.and.returnValue(getReturn);
localStorageServiceSpy.get.and.returnValue(undefined);
modalServiceSpy.openComponent.and.returnValue({instance: {message: ''}} as any);
apiService.get('/testurl');
expect(httpClientSpy.get).toHaveBeenCalledWith('/api/testurl', jasmine.any(Object));
expect(modalServiceSpy.openComponent.calls.count()).toBe(1);
});
it('returns the error message if the callback function had an error parameter', () => {
const getReturn = new Observable(observer => {
observer.error('error');
});
httpClientSpy.get.and.returnValue(getReturn);
localStorageServiceSpy.get.and.returnValue(undefined);
modalServiceSpy.openComponent.and.returnValue({instance: {message: ''}} as any);
apiService.get('/testurl', (data, error) => {
expect(modalServiceSpy.openComponent.calls.count()).toBe(0);
expect(error).toBe('error');
});
});
it('should do get requests without auth if not available', async(() => {
const getReturn = new Observable(observer => {
observer.next({body: 'data', headers: {keys: () => []}});
});
httpClientSpy.get.and.returnValue(getReturn);
localStorageServiceSpy.get.and.returnValue(undefined);
apiService.get('/testurl', res => {
expect(res).toBe('data');
expect(httpClientSpy.get).toHaveBeenCalledWith('/api/testurl', jasmine.any(Object));
expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth');
});
}));
it('should do get requests with basic auth if available', async(() => {
const getReturn = new Observable(observer => {
observer.next('data');
});
httpClientSpy.get.and.returnValue(getReturn);
localStorageServiceSpy.get.and.returnValue('basicAuth');
apiService.get('/testurl', res => {
expect(res).toBe('data');
expect(httpClientSpy.get).toHaveBeenCalledWith('/api/testurl', jasmine.any(Object));
expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth');
});
}));
it('should do post requests', async(() => {
const resReturn = new Observable(observer => {
observer.next('data');
});
httpClientSpy.post.and.returnValue(resReturn);
localStorageServiceSpy.get.and.returnValue('basicAuth');
apiService.post('/testurl', 'reqData', res => {
expect(res).toBe('data');
expect(httpClientSpy.post).toHaveBeenCalledWith('/api/testurl', 'reqData', jasmine.any(Object));
expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth');
});
}));
it('should do put requests', async(() => {
const resReturn = new Observable(observer => {
observer.next('data');
});
httpClientSpy.put.and.returnValue(resReturn);
localStorageServiceSpy.get.and.returnValue('basicAuth');
apiService.put('/testurl', 'reqData', res => {
expect(res).toBe('data');
expect(httpClientSpy.put).toHaveBeenCalledWith('/api/testurl', 'reqData', jasmine.any(Object));
expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth');
});
}));
it('should do delete requests', async(() => {
const resReturn = new Observable(observer => {
observer.next('data');
});
httpClientSpy.delete.and.returnValue(resReturn);
localStorageServiceSpy.get.and.returnValue('basicAuth');
apiService.delete('/testurl', res => {
expect(res).toBe('data');
expect(httpClientSpy.delete).toHaveBeenCalledWith('/api/testurl', jasmine.any(Object));
expect(localStorageServiceSpy.get).toHaveBeenCalledWith('basicAuth');
});
}));
// TODO: test return headers
});

View File

@ -5,7 +5,6 @@ import {Observable} from 'rxjs';
import {ErrorComponent} from '../error/error.component';
import {ModalService} from '@inst-iot/bosch-angular-ui-components';
// TODO: find solution when client wants to visit subpage but is not logged in to redirect to login without showing request failed errors
@Injectable({
providedIn: 'root'
@ -25,6 +24,7 @@ export class ApiService {
return this.host;
}
// main HTTP methods
get<T>(url, f: (data?: T, err?, headers?) => void = () => {}) {
this.requestErrorHandler<T>(this.http.get(this.url(url), this.options()), f);
}
@ -41,20 +41,25 @@ export class ApiService {
this.requestErrorHandler<T>(this.http.delete(this.url(url), this.options()), f);
}
// execute request and handle errors
private requestErrorHandler<T>(observable: Observable<any>, f: (data?: T, err?, headers?) => void) {
observable.subscribe(data => {
f(data.body, undefined, data.headers.keys().reduce((s, e) => {s[e.toLowerCase()] = data.headers.get(e); return s; }, {}));
}, err => {
if (f.length > 1) {
observable.subscribe(data => { // successful request
f(
data.body,
undefined,
data.headers.keys().reduce((s, e) => {s[e.toLowerCase()] = data.headers.get(e); return s; }, {})
);
}, err => { // error
if (f.length > 1) { // pass on error
f(undefined, err, undefined);
}
else {
else { // handle directly
this.requestError(err);
}
});
}
requestError(err) {
requestError(err) { // network error dialog
const modalRef = this.modalService.openComponent(ErrorComponent);
modalRef.instance.message = 'Network request failed!';
const details = [err.error.status];
@ -67,7 +72,7 @@ export class ApiService {
});
}
private url(url) {
private url(url) { // detect if host was given, otherwise use default host
if (/http[s]?:\/\//.test(url)) {
return url;
}
@ -76,10 +81,12 @@ export class ApiService {
}
}
// generate request options
private options(): {headers: HttpHeaders, observe: 'body'} {
return {headers: this.authOptions(), observe: 'response' as 'body'};
}
// generate Basic Auth
private authOptions(): HttpHeaders {
const auth = this.storage.get('basicAuth');
if (auth) {

View File

@ -1,37 +1,5 @@
import { TestBed } from '@angular/core/testing';
import { AutocompleteService } from './autocomplete.service';
// TODO
let autocompleteService: AutocompleteService;
describe('AutocompleteService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [AutocompleteService]
});
autocompleteService = TestBed.inject(AutocompleteService);
});
it('should be created', () => {
expect(autocompleteService).toBeTruthy();
});
it('should should return a bind function', () => {
expect(autocompleteService.bind('a', ['b'])).toBeTruthy();
});
it('should return search results', () => {
autocompleteService.search(['aa', 'ab', 'bb'], 'a').subscribe(res => {
expect(res).toEqual(['aa', 'ab']);
});
});
it('should return an empty array if no result was found', () => {
autocompleteService.search(['aa', 'ab', 'bb'], 'c').subscribe(res => {
expect(res).toEqual([]);
});
});
});

View File

@ -1,33 +1,5 @@
import { TestBed } from '@angular/core/testing';
import { DataService } from './data.service';
import {ApiService} from './api.service';
import {HttpClient} from '@angular/common/http';
import {LocalStorageService} from 'angular-2-local-storage';
import {ModalService} from '@inst-iot/bosch-angular-ui-components';
// TODO
let apiServiceSpy: jasmine.SpyObj<ApiService>;
describe('DataService', () => {
let service: DataService;
beforeEach(() => {
const apiSpy = jasmine.createSpyObj('ApiService', ['post', 'put']);
TestBed.configureTestingModule({
providers: [
{provide: ApiService, useValue: apiSpy}
]
});
service = TestBed.inject(DataService);
apiServiceSpy = TestBed.inject(ApiService) as jasmine.SpyObj<ApiService>;
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -16,7 +16,7 @@ export class DataService {
private api: ApiService
) { }
private collectionMap = {
private collectionMap = { // list of available collections
materials: {path: '/materials?status[]=validated&status[]=new', model: MaterialModel, type: 'idArray'},
materialSuppliers: {path: '/material/suppliers', model: null, type: 'idArray'},
materialGroups: {path: '/material/groups', model: null, type: 'idArray'},
@ -36,7 +36,7 @@ export class DataService {
id: {[key: string]: {[id: string]: any}} = {}; // data in format _id: data
d: {[key: string]: any} = {}; // data not in array format
contact = {name: 'CR/APS1-Lingenfelser', mail: 'dominic.lingenfelser@bosch.com'};
contact = {name: 'CR/APS1-Lingenfelser', mail: 'dominic.lingenfelser@bosch.com'}; // global contact data
load(collection, f = () => {}) { // load data
if (this.arr[collection]) { // data already loaded
@ -46,7 +46,11 @@ export class DataService {
this.api.get<any>(this.collectionMap[collection].path, data => {
if (this.collectionMap[collection].type !== 'string') { // array data
this.arr[collection] = data
.map(e => this.collectionMap[collection].model ? new this.collectionMap[collection].model().deserialize(e) : e);
.map(
e => this.collectionMap[collection].model ?
new this.collectionMap[collection].model().deserialize(e) : e
);
// load ids
if (this.collectionMap[collection].type === 'idArray' || this.collectionMap[collection].type === 'template') {
this.idReload(collection);
}
@ -59,9 +63,10 @@ export class DataService {
}
}
// generate id object
idReload(collection) {
this.id[collection] = this.arr[collection].reduce((s, e) => {s[e._id] = e; return s; }, {});
if (this.collectionMap[collection].type === 'template') {
if (this.collectionMap[collection].type === 'template') { // generate array with latest templates
const tmpTemplates = {};
this.arr[collection].forEach(template => {
if (tmpTemplates[template.first_id]) { // already found another version

View File

@ -1,95 +1,5 @@
import { TestBed } from '@angular/core/testing';
import { LoginService } from './login.service';
import {LocalStorageService} from 'angular-2-local-storage';
import {ApiService} from './api.service';
import {Router} from '@angular/router';
// TODO
let loginService: LoginService;
let apiServiceSpy: jasmine.SpyObj<ApiService>;
let localStorageServiceSpy: jasmine.SpyObj<LocalStorageService>;
let routerServiceSpy: jasmine.SpyObj<Router>;
describe('LoginService', () => {
beforeEach(() => {
const apiSpy = jasmine.createSpyObj('ApiService', ['get']);
const localStorageSpy = jasmine.createSpyObj('LocalStorageService', ['set', 'remove']);
const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
TestBed.configureTestingModule({
providers: [
LoginService,
{provide: ApiService, useValue: apiSpy},
{provide: LocalStorageService, useValue: localStorageSpy},
{provide: Router, useValue: routerSpy}
]
});
loginService = TestBed.inject(LoginService);
apiServiceSpy = TestBed.inject(ApiService) as jasmine.SpyObj<ApiService>;
localStorageServiceSpy = TestBed.inject(LocalStorageService) as jasmine.SpyObj<LocalStorageService>;
routerServiceSpy = TestBed.inject(Router) as jasmine.SpyObj<Router>;
});
it('should be created', () => {
expect(loginService).toBeTruthy();
});
// describe('login', () => {
// it('should store the basic auth', () => {
// localStorageServiceSpy.set.and.returnValue(true);
// apiServiceSpy.get.and.callFake(() => {});
// 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.callFake((a, b) => {b(undefined, '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.callFake((a, b) => {b({status: 'Authorization successful', method: 'basic'} as any, undefined); });
// 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.callFake((a, b) => {b({status: 'xxx', method: 'basic'} as any, undefined); });
// expect(await loginService.login('username', 'password')).toBeFalsy();
// });
//
// it('should resolve false on an error', async () => {
// localStorageServiceSpy.set.and.returnValue(true);
// apiServiceSpy.get.and.callFake((a, b) => {b(undefined, 'error'); });
// expect(await loginService.login('username', 'password')).toBeFalsy();
// });
// });
//
// describe('canActivate', () => {
// it('should return false at first', done => {
// apiServiceSpy.get.and.callFake((a, b) => {b(undefined, 'error'); });
// loginService.canActivate(null, null).subscribe(res => {
// expect(res).toBeFalsy();
// done();
// });
// });
//
// it('returns true if login was successful', async done => {
// localStorageServiceSpy.set.and.returnValue(true);
// apiServiceSpy.get.and.callFake((a, b) => {b({status: 'Authorization successful', method: 'basic'} as any, undefined); });
// await loginService.login('username', 'password');
// loginService.canActivate(null, null).subscribe(res => {
// expect(res).toBeTruthy();
// done();
// });
// });
// });
});

View File

@ -10,19 +10,20 @@ import {DataService} from './data.service';
})
export class LoginService implements CanActivate {
private pathPermissions = [
private pathPermissions = [ // minimum level needed for the specified paths
{path: 'materials', permission: 'dev'},
{path: 'templates', permission: 'dev'},
{path: 'changelog', permission: 'dev'},
{path: 'users', permission: 'admin'}
];
readonly levels = [
readonly levels = [ // all user levels in ascending permissions order
'predict',
'read',
'write',
'dev',
'admin'
];
// returns true or false depending on whether the user fulfills the minimum level
isLevel: {[level: string]: boolean} = {};
hasPrediction = false; // true if user has prediction models specified
userId = '';
@ -99,6 +100,7 @@ export class LoginService implements CanActivate {
this.hasPrediction = false;
}
// canActivate for Angular routing
canActivate(route: ActivatedRouteSnapshot = null, state: RouterStateSnapshot = null): Observable<boolean> {
return new Observable<boolean>(observer => {
new Promise(resolve => {
@ -112,7 +114,8 @@ export class LoginService implements CanActivate {
}
}).then(res => {
const pathPermission = this.pathPermissions.find(e => e.path.indexOf(route.url[0].path) >= 0);
const ok = res && (!pathPermission || this.isLevel[pathPermission.permission]); // check if level is permitted for path
// check if level is permitted for path
const ok = res && (!pathPermission || this.isLevel[pathPermission.permission]);
observer.next(ok);
observer.complete();
if (!ok) {

View File

@ -1,116 +1,5 @@
import { TestBed } from '@angular/core/testing';
import { ValidationService } from './validation.service';
// TODO
let validationService: ValidationService;
describe('ValidationService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [ValidationService]
});
validationService = TestBed.inject(ValidationService);
});
it('should be created', () => {
expect(validationService).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 a password too short', () => {
// expect(validationService.password('Abc123')).toEqual({ok: false, error: 'password must have at least 8 characters'});
// });
//
// it('should return an error on a password without a lowercase letter', () => {
// expect(validationService.password('ABC123!#')).toEqual({ok: false, error: 'password must have at least one lowercase character'});
// });
//
// it('should return an error on a password without an uppercase letter', () => {
// expect(validationService.password('abc123!#')).toEqual({ok: false, error: 'password must have at least one uppercase character'});
// });
//
// it('should return an error on a password without a number', () => {
// expect(validationService.password('Abcabc!#')).toEqual({ok: false, error: 'password must have at least one number'});
// });
//
// it('should return an error on a password without a special character', () => {
// expect(validationService.password('Abc12345')).toEqual({ok: false, error: 'password must have at least one of the following characters !"#%&\'()*+,-.\\/:;<=>?@[]^_`{|}~'});
// });
//
// it('should return an error on a password with a character not allowed', () => {
// expect(validationService.password('Abc123!€')).toEqual({ok: false, error: 'password must only contain a-zA-Z0-9!"#%&\'()*+,-./:;<=>?@[]^_`{|}~'});
// });
//
// it('should return true on a correct string', () => {
// expect(validationService.string('Abc')).toEqual({ok: true, error: ''});
// });
//
// it('should return an error on a string too long', () => {
// expect(validationService.string('abcabcabcbabcbabcabcabacbabcabcabcbabcbabcabcabacbabcabcabcbabcbabcabcabacbabcabcabcbabcbabcabcabacbabcabcabcbabcbabcabcabacbacab')).toEqual({ok: false, error: 'must contain max 128 characters'});
// });
//
// it('should return true on a string in the list', () => {
// expect(validationService.stringOf('Abc', ['Abc', 'Def'])).toEqual({ok: true, error: ''});
// });
//
// it('should return an error on a string not in the list', () => {
// expect(validationService.stringOf('abc', ['Abc', 'Def'])).toEqual({ok: false, error: 'must be one of Abc, Def'});
// });
//
// it('should return true on a string of correct length', () => {
// expect(validationService.stringLength('Abc', 5)).toEqual({ok: true, error: ''});
// });
//
// it('should return an error on a string longer than specified', () => {
// expect(validationService.stringLength('Abc', 2)).toEqual({ok: false, error: 'must contain max 2 characters'});
// });
//
// it('should return true on a number in the range', () => {
// expect(validationService.minMax(2, -2, 2)).toEqual({ok: true, error: ''});
// });
//
// it('should return an error on a number below the range', () => {
// expect(validationService.minMax(0, 1, 3)).toEqual({ok: false, error: 'must be between 1 and 3'});
// });
//
// it('should return an error on a number above the range', () => {
// expect(validationService.minMax(3.1, 1, 3)).toEqual({ok: false, error: 'must be between 1 and 3'});
// });
//
// it('should return true on a number above min', () => {
// expect(validationService.min(2, -2)).toEqual({ok: true, error: ''});
// });
//
// it('should return an error on a number below min', () => {
// expect(validationService.min(0, 1)).toEqual({ok: false, error: 'must not be below 1'});
// });
//
// it('should return true on a number below max', () => {
// expect(validationService.max(2, 2)).toEqual({ok: true, error: ''});
// });
//
// it('should return an error on a number above max', () => {
// expect(validationService.max(2, 1)).toEqual({ok: false, error: 'must not be above 1'});
// });
//
// it('should return true on a string not in the list', () => {
// expect(validationService.unique('Abc', ['Def', 'Ghi'])).toEqual({ok: true, error: ''});
// });
//
// it('should return an error on a string from the list', () => {
// expect(validationService.unique('Abc', ['Abc', 'Def'])).toEqual({ok: false, error: 'values must be unique'});
// });
});

View File

@ -63,7 +63,7 @@ export class ValidationService {
return {ok: true, error: ''};
}
stringOf(data, list) {
stringOf(data, list) { // string must be in list
const {ignore, error} = Joi.string().allow('').valid(...list.map(e => e.toString())).validate(data);
if (error) {
return {ok: false, error: 'must be one of ' + list.join(', ')};
@ -71,7 +71,7 @@ export class ValidationService {
return {ok: true, error: ''};
}
stringNin(data, list) {
stringNin(data, list) { // string must not be in list
const {ignore, error} = Joi.string().invalid(...list).validate(data);
if (error) {
return {ok: false, error: 'value not allowed'};
@ -79,7 +79,7 @@ export class ValidationService {
return {ok: true, error: ''};
}
stringLength(data, length) {
stringLength(data, length) { // string with maximum length
const {ignore, error} = Joi.string().max(length).allow('').validate(data);
if (error) {
return {ok: false, error: 'must contain max ' + length + ' characters'};
@ -87,7 +87,7 @@ export class ValidationService {
return {ok: true, error: ''};
}
minMax(data, min, max) {
minMax(data, min, max) { // number between min and max
const {ignore, error} = Joi.number().allow('').min(min).max(max).validate(data);
if (error) {
return {ok: false, error: `must be between ${min} and ${max}`};
@ -95,7 +95,7 @@ export class ValidationService {
return {ok: true, error: ''};
}
min(data, min) {
min(data, min) { // number above min
const {ignore, error} = Joi.number().allow('').min(min).validate(data);
if (error) {
return {ok: false, error: `must not be below ${min}`};
@ -103,7 +103,7 @@ export class ValidationService {
return {ok: true, error: ''};
}
max(data, max) {
max(data, max) { // number below max
const {ignore, error} = Joi.number().allow('').max(max).validate(data);
if (error) {
return {ok: false, error: `must not be above ${max}`};