diff options
Diffstat (limited to 'src/app/services')
-rw-r--r-- | src/app/services/auth.service.ts | 94 | ||||
-rw-r--r-- | src/app/services/authconfig.service.ts | 47 | ||||
-rw-r--r-- | src/app/services/cacheservice/request-cache.service.spec.ts | 35 | ||||
-rw-r--r-- | src/app/services/cacheservice/request-cache.service.ts | 89 | ||||
-rw-r--r-- | src/app/services/fullscreen.service.ts | 58 | ||||
-rw-r--r-- | src/app/services/history.service.ts | 67 | ||||
-rw-r--r-- | src/app/services/loading-indicator.service.ts | 47 | ||||
-rw-r--r-- | src/app/services/logging.service.ts | 34 | ||||
-rw-r--r-- | src/app/services/shortcut.service.ts | 49 | ||||
-rw-r--r-- | src/app/services/tileservice/tiles.service.spec.ts | 488 | ||||
-rw-r--r-- | src/app/services/tileservice/tiles.service.ts | 88 | ||||
-rw-r--r-- | src/app/services/unsubscribe/unsubscribe.service.ts | 32 | ||||
-rw-r--r-- | src/app/services/user-settings.service.ts | 111 |
13 files changed, 1239 insertions, 0 deletions
diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts new file mode 100644 index 0000000..8988196 --- /dev/null +++ b/src/app/services/auth.service.ts @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { Injectable } from '@angular/core'; +import { OAuthService, UserInfo } from 'angular-oauth2-oidc'; +import { catchError, filter, map } from 'rxjs/operators'; +import { NavigationStart, Router } from '@angular/router'; +import { AlertService } from '../modules/alerting'; +import { TranslateService } from '@ngx-translate/core'; +import { BehaviorSubject, EMPTY, Observable, of } from 'rxjs'; + +/** + * Provides check for roles and token + */ +@Injectable({ + providedIn: 'root', +}) +export class AuthService { + userProfile$ = new BehaviorSubject<UserInfo | undefined>(undefined); + constructor(private readonly oauthService: OAuthService, private router: Router, private alertService: AlertService, private translateService: TranslateService) { + //renew cache every page reload + router.events.pipe( + filter((e): e is NavigationStart => e instanceof NavigationStart) + ).subscribe(() => { + try { + return this.loadUserProfile().then(userInfo => this.userProfile$.next(userInfo)).catch(e => {throw e}) + }catch (e) { + this.alertService.error(this.translateService.instant('common.messages.keycloakAccessTokenNotValid'), {id: "keycloak", keepAfterRouteChange: true}) + return Promise.resolve(null); + } + }); + } + + userProfileCache: UserInfo | undefined = undefined; + + /** + * Convenience method of `hasValidAccessToken()` and `hasSufficientRoles()` + * Asynchronous because the needed UserInfo is fetched from Keycloak + */ + hasPermissions(): Observable<boolean> { + if (this.hasValidAccessToken()) { + return this.hasSufficientRoles(); + } + return of(false); + } + + /** + * This answers: 'What if the user has an account, but without any permissions?' + * Asynchronous because the needed UserInfo is fetched from Keycloak + */ + hasSufficientRoles(): Observable<boolean> { + return this.loadCachedUserProfile().pipe(map(info => info?.roles.join(',') !== 'offline_access')); + } + + loadCachedUserProfile(): Observable<UserInfo | undefined> { + return this.userProfile$.pipe( + filter(userProfile => userProfile !== undefined), + catchError(err => { + console.error(err); + return EMPTY + })); + } + + /** + * Wrapper for `hasValidAccessToken` from OAuthService + */ + hasValidAccessToken(): boolean { + return this.oauthService.hasValidAccessToken(); + } + /* + * Private method = should not be used outside of this class, because it triggers additional request for userprofile + * */ + private loadUserProfile():Promise<UserInfo> { + // in version 12.2 loadUserProfile() Promise returns data of type object instead of UserInfo + //@ts-ignore + return this.oauthService.loadUserProfile().then(userInfo => userInfo.info as UserInfo) + } +} diff --git a/src/app/services/authconfig.service.ts b/src/app/services/authconfig.service.ts new file mode 100644 index 0000000..5ced9a1 --- /dev/null +++ b/src/app/services/authconfig.service.ts @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { Injectable } from '@angular/core'; +import { AuthConfig, NullValidationHandler, OAuthService } from 'angular-oauth2-oidc'; + +@Injectable() +export class AuthConfigService { + constructor(private readonly oauthService: OAuthService, private readonly authConfig: AuthConfig) {} + + async initAuth(): Promise<any> { + return new Promise<void>((resolveFn, rejectFn) => { + // setup oauthService + this.oauthService.configure(this.authConfig); + this.oauthService.tokenValidationHandler = new NullValidationHandler(); + + this.oauthService.loadDiscoveryDocumentAndLogin().then(isLoggedIn => { + if (isLoggedIn) { + this.oauthService.setupAutomaticSilentRefresh(); + resolveFn(); + } else { + this.oauthService.initImplicitFlow(); + rejectFn(); + } + }).catch(() => { + //@ts-ignore + window.location.href = './keycloak-error.html' + }); + }); + } +} diff --git a/src/app/services/cacheservice/request-cache.service.spec.ts b/src/app/services/cacheservice/request-cache.service.spec.ts new file mode 100644 index 0000000..c9f931e --- /dev/null +++ b/src/app/services/cacheservice/request-cache.service.spec.ts @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { TestBed } from '@angular/core/testing'; + +import { RequestCacheService } from './request-cache.service'; + +describe('RequestCacheService', () => { + let service: RequestCacheService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(RequestCacheService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/cacheservice/request-cache.service.ts b/src/app/services/cacheservice/request-cache.service.ts new file mode 100644 index 0000000..3d2047f --- /dev/null +++ b/src/app/services/cacheservice/request-cache.service.ts @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { HttpRequest, HttpResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +const maxAge = 60000; + +@Injectable({ + providedIn: 'root', +}) +// https://github.com/angular/angular/blob/master/aio/content/examples/http/src/app/request-cache.service.ts +export class RequestCacheService implements RequestCache { + // The cache is a Map of (most importantly but not exclusively) HttpResponses + cache = new Map<string, RequestCacheEntry>(); + + /** + * Get an http request from cache + * @param request the http request that should be retrieved from cache + */ + get(request: HttpRequest<any>): HttpResponse<any> | undefined { + const requestUrl = request.urlWithParams; + const cachedResponse = this.cache.get(requestUrl); + + if (!cachedResponse) { + return undefined; + } + + const isExpired = cachedResponse.lastRead < Date.now() - maxAge; + + + return isExpired ? undefined : cachedResponse.response; + } + + /** + * Put a http response for a given request url (taken from the request object) in the cache + * @param request the http request that should be associated with the http response + * @param response the http response that should be stored in cache + */ + put(request: HttpRequest<any>, response: HttpResponse<any>): void { + const requestUrl = request.urlWithParams; + + // Map a request url to an object + const newEntry = { requestUrl, response, lastRead: Date.now() }; + this.cache.set(requestUrl, newEntry); + + // Remove expired entries from the cache + const expired = Date.now() - maxAge; + this.cache.forEach(entry => { + if (entry.lastRead < expired) { + this.cache.delete(entry.requestUrl); + } + }); + } +} + +/** + * Service that manages the cache. + * `get()` HttpResponses from cache and `put()` responses into the cache + */ +export abstract class RequestCache { + abstract get(request: HttpRequest<any>): HttpResponse<any> | undefined; + abstract put(request: HttpRequest<any>, response: HttpResponse<any>): void; +} + +/** + * Wrapper Object that stores the HttpResponse together with the `requestUrl` of the request and the `lastRead` time it was cached + */ +export interface RequestCacheEntry { + requestUrl: string; + response: HttpResponse<any>; + lastRead: number; +} diff --git a/src/app/services/fullscreen.service.ts b/src/app/services/fullscreen.service.ts new file mode 100644 index 0000000..91ceec9 --- /dev/null +++ b/src/app/services/fullscreen.service.ts @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class FullscreenService { + private doc = <FullScreenDocument>document; + + enter() { + const el = this.doc.documentElement; + if (el.requestFullscreen) el.requestFullscreen(); + else if (el.msRequestFullscreen) el.msRequestFullscreen(); + else if (el.mozRequestFullScreen) el.mozRequestFullScreen(); + else if (el.webkitRequestFullscreen) el.webkitRequestFullscreen(); + } + + leave() { + if (this.doc.exitFullscreen) this.doc.exitFullscreen(); + else if (this.doc.msExitFullscreen) this.doc.msExitFullscreen(); + else if (this.doc.mozCancelFullScreen) this.doc.mozCancelFullScreen(); + else if (this.doc.webkitExitFullscreen) this.doc.webkitExitFullscreen(); + } +} + +interface FullScreenDocument extends HTMLDocument { + documentElement: FullScreenDocumentElement; + mozFullScreenElement?: Element; + msFullscreenElement?: Element; + webkitFullscreenElement?: Element; + msExitFullscreen?: () => void; + mozCancelFullScreen?: () => void; + webkitExitFullscreen?: () => void; +} + +interface FullScreenDocumentElement extends HTMLElement { + msRequestFullscreen?: () => void; + mozRequestFullScreen?: () => void; + webkitRequestFullscreen?: () => void; +} diff --git a/src/app/services/history.service.ts b/src/app/services/history.service.ts new file mode 100644 index 0000000..2924f39 --- /dev/null +++ b/src/app/services/history.service.ts @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { Injectable } from '@angular/core'; +import { + ActionType, + CreateActionModel, + EntityType, + EntityUserHistoryActionModel, +} from '../model/user-last-action.model'; +import { ActionsListResponse, ActionsResponse, ActionsService } from '../../../openapi/output'; +import { OAuthService } from 'angular-oauth2-oidc'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class HistoryService { + constructor(private readonly actionService: ActionsService, private readonly oauthService: OAuthService) {} + + public getUserActions(interval: number | undefined): Observable<ActionsListResponse> { + const userId = Object(this.oauthService.getIdentityClaims()).sub; + return this.actionService.getActions(userId, 1, 1000, interval); + } + + public createUserHistoryAction(action: CreateActionModel<EntityUserHistoryActionModel>): Observable<ActionsResponse> { + let mappedAction = { + type: action.type, + entity: action.entity, + entityParams: { + userName: action.entityParams.userName, + userId: action.entityParams.userId, + }, + }; + return this.createAction(mappedAction); + } + + private createAction(action: { + type: ActionType; + entity: EntityType; + entityParams: { [key: string]: string | undefined}; + }): Observable<ActionsResponse> { + const userId = Object(this.oauthService.getIdentityClaims()).sub; + const actionCreatedAt = new Date().toISOString(); + return this.actionService.createAction(userId, undefined, { + userId, + actionCreatedAt, + action, + }); + } +} diff --git a/src/app/services/loading-indicator.service.ts b/src/app/services/loading-indicator.service.ts new file mode 100644 index 0000000..727edd4 --- /dev/null +++ b/src/app/services/loading-indicator.service.ts @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; + +@Injectable() +export class LoadingIndicatorService { + private isVisible$ = new BehaviorSubject<boolean>(false); + private timeDelay = 500; + private counter = 0; + + show(): void { + this.counter++; + if (this.counter > 0) { + setTimeout(() => this.isVisible$.next(true), 0); + } + } + + hide(): void { + this.counter--; + if (this.counter === 0) { + setTimeout(() => this.isVisible$.next(false), 0); + } + } + + isVisible(): Observable<boolean> { + return this.isVisible$.pipe(debounceTime(this.timeDelay)); + } +} diff --git a/src/app/services/logging.service.ts b/src/app/services/logging.service.ts new file mode 100644 index 0000000..5250fda --- /dev/null +++ b/src/app/services/logging.service.ts @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class LoggingService { + constructor(private readonly httpClient: HttpClient) {} + + writeLog(message: string): Observable<string> { + return this.httpClient.post(environment.loggingUrl, message, { responseType: 'text' }); + } +} diff --git a/src/app/services/shortcut.service.ts b/src/app/services/shortcut.service.ts new file mode 100644 index 0000000..750ab5b --- /dev/null +++ b/src/app/services/shortcut.service.ts @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; + +export enum KeyboardShortcuts { + SHORTCUT_0 = '0', + SHORTCUT_1 = '1', + SHORTCUT_2 = '2', + SHORTCUT_4 = '4', + SHORTCUT_6 = '6', +} + +@Injectable({ + providedIn: 'root' +}) + +export class ShortcutService { + + private shortcuts = new Map<KeyboardShortcuts, string>([ + [KeyboardShortcuts.SHORTCUT_0, this.translateService.instant('layout.header.shortcuts.details')], + [KeyboardShortcuts.SHORTCUT_1, this.translateService.instant('layout.header.shortcuts.home')], + [KeyboardShortcuts.SHORTCUT_2, this.translateService.instant('layout.header.shortcuts.main')], + [KeyboardShortcuts.SHORTCUT_4, this.translateService.instant('layout.header.shortcuts.search')], + [KeyboardShortcuts.SHORTCUT_6, this.translateService.instant('layout.header.shortcuts.menu')], + ]); + + constructor(private translateService: TranslateService) { } + + public getShortcuts(): Map<KeyboardShortcuts,string> { + return this.shortcuts; + } +} diff --git a/src/app/services/tileservice/tiles.service.spec.ts b/src/app/services/tileservice/tiles.service.spec.ts new file mode 100644 index 0000000..f7f4369 --- /dev/null +++ b/src/app/services/tileservice/tiles.service.spec.ts @@ -0,0 +1,488 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { TestBed, waitForAsync } from '@angular/core/testing'; +import { Tile } from '../../model/tile'; +import { TilesService } from './tiles.service'; +import { HttpErrorResponse } from '@angular/common/http'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { environment } from '../../../environments/environment'; +import { OAuthLogger, OAuthService, UrlHelperService } from 'angular-oauth2-oidc'; + +// https://dev.to/coly010/unit-testing-angular-services-1anm + +/** + * describe sets up the Test Suite for the TileService + */ +describe('TilesService', () => { + /** + * let service declares a Test Suite-scoped variable where we will store a reference to our service + */ + let service: TilesService; + let mockTile: Tile; + let httpmock: HttpTestingController; + let errmsg: string; + + const backendServerUrlTest = environment.backendServerUrl + '/tiles'; + /** + * beforeEach tells the test runner to run this code before every test in the Test Suite + * It is using Angular's TestBed to create the testing environment. Finally it is injecting the TilesService + * and placing a reference to it in the service variable defined earlier. + */ + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [TilesService, OAuthService, UrlHelperService, OAuthLogger], + }); + service = TestBed.inject(TilesService); + httpmock = TestBed.inject(HttpTestingController); + mockTile = { + id: 1, + title: 'NewTile1', + description: 'New Tile for frontend test', + imageUrl: 'https://www.onap.org/wp-content/uploads/sites/20/2017/02/logo_onap_2017.png', + imageAltText: 'Onap Image', + redirectUrl: 'www.onap.org', + headers: 'This is a header', + groups: [], + roles: [], + }; + // responseTile = { + // id: 2, + // title: 'NewTile1', + // description: 'New Tile for frontend test', + // imageUrl: 'https://www.onap.org/wp-content/uploads/sites/20/2017/02/logo_onap_2017.png', + // imageAltText: 'Onap Image', + // redirectUrl: 'www.onap.org', + // headers: 'This is a header', + // groups: [], + // roles: [], + // }; + }); + + /** + * After every test, assert that there are no more pending requests. + */ + afterEach(() => { + httpmock.verify(); + }); + + /** + * the it() function creates a new test with the title 'should be created' + * This test is expecting the service varibale to truthy, in otherwords, + * it should have been instantiated correctly by the Angular TestBed. + */ + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + /** + * TileService method tests begin + * Testing getTiles + */ + describe('#getTiles', () => { + // let expectedTiles: Tile[]; + + beforeEach(() => { + // expectedTiles = [mockTile, responseTile]; + }); + + /** + * testing method getTiles() to get all existing tiles + */ + /* + it('should return expected tiles (called once)', (done: DoneFn) => { + service.getTiles().subscribe(response => { + expect(response).toEqual(expectedTiles, 'should return expected tiles'); + // done() to be called after asynchronous calls (https://angular.io/guide/testing-services) + done(); + }); + + // TileService should have made one request to GET tiles from expected URL + const req = httpmock.expectOne(backendServerUrlTest); + expect(req.request.method).toEqual('GET'); + + // Respond with the expected mock tiles + req.flush(expectedTiles); + }); +*/ + /** + * TODO: Maybe it makes sense to inform the user that no tiles are displayed + * testing method getTiles() in case there are no tiles in the database + */ + /* + it('should be OK returning no tiles', (done: DoneFn) => { + service.getTiles().subscribe(response => { + expect(response.length).toEqual(0, 'should have empty tiles array'); + done(); + }); + + const req = httpmock.expectOne(backendServerUrlTest); + expect(req.request.method).toEqual('GET'); + + req.flush([]); // Respond with no tile + }); +*/ + /** + * testing method getTiles() in case the backend responds with 404 Not Found + * This service reports the error but finds a way to let the app keep going. + */ + + /* + it('should handle 404 error', (done: DoneFn) => { + errmsg = '404 error'; + + service.getTiles().subscribe( + response => fail('should fail with the 404 error'), + (err: HttpErrorResponse) => { + expect(err.status).toEqual(404); + expect(err.error).toEqual(errmsg); + }, + ); + + // Make an HTTP Get Request + // service.getTiles().then( + // response => fail('should have failed with the 404 error'), + // (err: HttpErrorResponse) => { + // expect(err.status).toEqual(404); + // expect(err.error).toEqual(errmsg); + // } + // ); + + const req = httpmock.expectOne(backendServerUrlTest); + expect(req.request.method).toEqual('GET'); + + // respond with a 404 and the error message in the body --> TODO Frontend GUI must react correctly + req.flush(errmsg, { status: 404, statusText: 'Not Found' }); + }); + */ + + /** + * testing getTiles() when method is called multiple times + * TODO: expect cached results + */ + /* + it('should return expected tiles (called multiple times)', () => { + service.getTiles().subscribe(); + service.getTiles().subscribe(); + service.getTiles().subscribe(response => { + expect(response).toEqual(expectedTiles, 'should return expected tiles'); + }); + + const req = httpmock.match(backendServerUrlTest); + expect(req.length).toEqual(3, 'calls to getTiles()'); + + // Respond to each request with different mock tile results + req[0].flush([]); + req[1].flush([mockTile]); + req[2].flush(expectedTiles); + });*/ + }); + + /** + * Tests for getTileByID() + */ + describe('#getTileByID', () => { + /** + * testing method getTilesById() to return the specific tile with right id + */ + it('should return expected tile by id', () => { + service.getTileById(mockTile.id).then(response => { + expect(response).toEqual(mockTile, 'should return expected tile'); + }); + + const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id); + expect(req.request.method).toEqual('GET'); + + // Respond with the mock tiles + req.flush(mockTile); + }); + + /** + * testing method getTileByID() in case the backend responds with 404 Not Found and the tile does not exist + */ + it( + 'getTileById(): should handle 404 error', + waitForAsync(() => { + errmsg = '404 error'; + // Make an HTTP Get Request + service.getTileById(mockTile.id).then( + () => fail('should have failed with the 404 error'), + (err: HttpErrorResponse) => { + expect(err.status).toEqual(404); + expect(err.error).toEqual(errmsg); + }, + ); + + const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id); + expect(req.request.method).toEqual('GET'); + + req.flush(errmsg, { status: 404, statusText: 'Not Found' }); + }), + ); + }); + /** + * Tests for update an existing tile + */ + describe('#updateTiles', () => { + /** + * testing method updateTiles() + */ + it('should update a tile and return it', () => { + mockTile.title = 'Update title'; + + service.updateTiles(mockTile).then(response => { + expect(response.title).toEqual('Update title', 'should return tile'); + }); + // TileService should have made one request to PUT + const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id); + expect(req.request.method).toEqual('PUT'); + expect(req.request.body).toEqual(mockTile); + + req.flush(mockTile); + }); + + /** + * testing method updateTiles() in case the backend responds with 404 Not Found and the tile does not exist + */ + it( + 'updateTiles(): should handle 404 error', + waitForAsync(() => { + errmsg = '404 error'; + // Make an HTTP Get Request + service.updateTiles(mockTile).then( + () => fail('should have failed with the 404 error'), + (err: HttpErrorResponse) => { + expect(err.status).toEqual(404); + expect(err.error).toEqual(errmsg); + }, + ); + + const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id); + expect(req.request.method).toEqual('PUT'); + + req.flush(errmsg, { status: 404, statusText: 'Not Found' }); + }), + ); + + /** + * testing method updateTiles() in case the backend responds with 401 Unauthorized + */ + it( + 'updateTiles(): should handle 401 error', + waitForAsync(() => { + errmsg = '401 error'; + // Make an HTTP Get Request + service.updateTiles(mockTile).then( + () => fail('should have failed with the 401 error'), + (err: HttpErrorResponse) => { + expect(err.status).toEqual(401); + expect(err.error).toEqual(errmsg); + }, + ); + + const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id); + expect(req.request.method).toEqual('PUT'); + + req.flush(errmsg, { status: 401, statusText: 'Not Found' }); + }), + ); + + /** + * testing method updateTiles() in case the backend responds with 403 Forbidden + */ + it( + 'updateTiles(): should handle 403 error', + waitForAsync(() => { + errmsg = '403 error'; + // Make an HTTP Get Request + service.updateTiles(mockTile).then( + () => fail('should have failed with the 404 error'), + (err: HttpErrorResponse) => { + expect(err.status).toEqual(403); + }, + ); + + const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id); + expect(req.request.method).toEqual('PUT'); + + req.flush(errmsg, { status: 403, statusText: 'Not Found' }); + }), + ); + }); + /* + * Test save a new tile + */ + describe('#saveTiles', () => { + /* + * testing saveTiles() to save a new tile + */ + it( + 'should save a tile correctly (mocked http post request)', + waitForAsync(() => { + service.saveTiles(mockTile).then(response => { + expect(response.id).toBe(1); + expect(response.title).toBe('NewTile1'); + expect(response.redirectUrl).toBe('www.onap.org'); + expect(response.imageAltText).toBe('Onap Image'); + expect(response.imageUrl).toBe('https://www.onap.org/wp-content/uploads/sites/20/2017/02/logo_onap_2017.png'); + expect(response.description).toBe('New Tile for frontend test'); + expect(response.headers).toBe('This is a header'); + }); + /* + * Checking that there ist just one request and check the type of request + * 'flush'/ respond with mock data, run then-block in line 64 and check the except commands + */ + const req = httpmock.expectOne(backendServerUrlTest); + expect(req.request.method).toEqual('POST'); + req.flush(mockTile); + }), + ); + /** + * testing method saveTiles() in case the backend answers with an 401 responds + */ + it( + 'saveTiles(): should handle 401 error', + waitForAsync(() => { + errmsg = '401 error'; + // Make an HTTP Get Request + service.saveTiles(mockTile).then( + () => fail('should have failed with the 401 error'), + (err: HttpErrorResponse) => { + expect(err.status).toEqual(401); + expect(err.error).toEqual(errmsg); + }, + ); + + const req = httpmock.expectOne(backendServerUrlTest); + expect(req.request.method).toEqual('POST'); + + req.flush(errmsg, { status: 401, statusText: 'Not Found' }); + }), + ); + + it( + 'saveTiles(): should handle 403 error', + waitForAsync(() => { + errmsg = '403 error'; + // Make an HTTP Get Request + service.saveTiles(mockTile).then( + () => fail('should have failed with the 401 error'), + (err: HttpErrorResponse) => { + expect(err.status).toEqual(403); + expect(err.error).toEqual(errmsg); + }, + ); + + const req = httpmock.expectOne(backendServerUrlTest); + expect(req.request.method).toEqual('POST'); + + req.flush(errmsg, { status: 403, statusText: 'Forbidden' }); + }), + ); + }); + /** + * testing delete a tile + */ + describe('#deleteTiles', () => { + /** + * testing method deleteTile() + */ + it( + 'should delete a tile correctly (mocked http delete request)', + waitForAsync(() => { + service.deleteTile(mockTile).then(response => { + expect(response).toBeDefined(); + }); + const req = httpmock.expectOne(environment.backendServerUrl + '/tiles/' + mockTile.id); + expect(req.request.method).toEqual('DELETE'); + req.flush({}); + }), + ); + + /** + * testing method deleteTiles() in case the backend responds with 404 Not Found and the tile does not exist + */ + it( + 'deleteTiles(): should handle 404 error', + waitForAsync(() => { + errmsg = '404 error'; + // Make an HTTP Get Request + service.deleteTile(mockTile).then( + () => fail('should have failed with the 404 error'), + (err: HttpErrorResponse) => { + expect(err.status).toEqual(404); + expect(err.error).toEqual(errmsg); + }, + ); + + const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id); + expect(req.request.method).toEqual('DELETE'); + + req.flush(errmsg, { status: 404, statusText: 'Not Found' }); + }), + ); + + /** + * testing method deleteTiles() in case the backend responds with 401 Unauthorized + */ + it( + 'deleteTiles(): should handle 401 error', + waitForAsync(() => { + errmsg = '401 error'; + // Make an HTTP Get Request + service.deleteTile(mockTile).then( + () => fail('should have failed with the 401 error'), + (err: HttpErrorResponse) => { + expect(err.status).toEqual(401); + expect(err.error).toEqual(errmsg); + }, + ); + + const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id); + expect(req.request.method).toEqual('DELETE'); + + req.flush(errmsg, { status: 401, statusText: 'Unauthorized' }); + }), + ); + + /** + * testing method deleteTiles() in case the backend responds with 403 Forbidden + */ + it( + 'deleteTiles(): should handle 403 error', + waitForAsync(() => { + errmsg = '403 error'; + // Make an HTTP Get Request + service.deleteTile(mockTile).then( + () => fail('should have failed with the 404 error'), + (err: HttpErrorResponse) => { + expect(err.status).toEqual(403); + expect(err.error).toEqual(errmsg); + }, + ); + + const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id); + expect(req.request.method).toEqual('DELETE'); + + req.flush(errmsg, { status: 403, statusText: 'Forbidden' }); + }), + ); + }); +}); diff --git a/src/app/services/tileservice/tiles.service.ts b/src/app/services/tileservice/tiles.service.ts new file mode 100644 index 0000000..167e42a --- /dev/null +++ b/src/app/services/tileservice/tiles.service.ts @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { environment } from 'src/environments/environment'; +import { Tile, TilesListResponse } from '../../model/tile'; +import { map } from 'rxjs/operators'; + +export const urlTileApi = environment.backendServerUrl + '/tiles'; + +@Injectable({ + providedIn: 'root', +}) +// Tutorial on the http client: https://angular.io/tutorial/toh-pt6#get-heroes-with-httpclient +export class TilesService { + constructor(private httpClient: HttpClient) {} + /** + * GET tiles from the server + */ + getTiles(refresh = false): Observable<Tile[]> { + if (refresh) { + const headers = new HttpHeaders({ 'x-refresh': 'true' }); + return this.httpClient + .get<TilesListResponse>(urlTileApi, { headers }) + .pipe(map(tilesListResponse => tilesListResponse.items)); + } + + return this.httpClient.get<TilesListResponse>(urlTileApi).pipe(map(tilesListResponse => tilesListResponse.items)); + } + + /** + * GET tile by id + * @param id to get specific tile + */ + getTileById(id: number): Promise<Tile> { + return this.httpClient.get<Tile>(urlTileApi + '/' + id).toPromise(); + } + + /** + * POST: add a new tile to the database + * @param tile + * @returns the new saved tile + */ + saveTiles(tile: Tile): Promise<Tile> { + const options = { + headers: new HttpHeaders({ 'Content-Type': 'application/json' }), + }; + return this.httpClient.post<Tile>(urlTileApi, tile, options).toPromise(); + } + + /** + * PUT: update the tile on the server + * @returns the updated hero + * @param tile + */ + updateTiles(tile: Tile): Promise<Tile> { + const options = { + headers: new HttpHeaders({ 'Content-Type': 'application/json' }), + }; + return this.httpClient.put<Tile>(urlTileApi + '/' + tile.id, tile, options).toPromise(); + } + + /** + * DELETE: delete the tile from the server + * @param tile to delete + */ + deleteTile(tile: Tile): Promise<void> { + return this.httpClient.delete<void>(urlTileApi + '/' + tile.id).toPromise(); + } +} diff --git a/src/app/services/unsubscribe/unsubscribe.service.ts b/src/app/services/unsubscribe/unsubscribe.service.ts new file mode 100644 index 0000000..b27f6d8 --- /dev/null +++ b/src/app/services/unsubscribe/unsubscribe.service.ts @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { Injectable, OnDestroy } from '@angular/core'; +import { Subject } from 'rxjs'; + +@Injectable() +export class UnsubscribeService implements OnDestroy { + private readonly sub$ = new Subject<void>(); + public readonly unsubscribe$ = this.sub$.asObservable(); + + ngOnDestroy(): void { + this.sub$.next(); + this.sub$.complete(); + } +} diff --git a/src/app/services/user-settings.service.ts b/src/app/services/user-settings.service.ts new file mode 100644 index 0000000..cbaa992 --- /dev/null +++ b/src/app/services/user-settings.service.ts @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { distinctUntilChanged, map, pluck, switchMap, take } from 'rxjs/operators'; +import { PreferencesResponse, PreferencesService } from '../../../openapi/output'; +import { + DashboardAppsModel, + DashboardModel, + DashboardTileSettings, + defaultUserSettings, + LastUserActionSettings, STATE_KEYS, + UpdateUserPreferenceModel, + UserPreferencesModel, +} from '../model/user-preferences.model'; +import { BehaviorSubject, Observable, pipe, UnaryFunction } from 'rxjs'; +import { mergeWith as _mergeWith, isObject as _isObject } from 'lodash'; +import { isString } from '../helpers/helpers'; +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class UserSettingsService { + private userSettings: UserPreferencesModel = defaultUserSettings; + private preferencesTracker$ = new BehaviorSubject<UserPreferencesModel>(this.userSettings); + + constructor(private preferencesService: PreferencesService) { + this.getPreferences(); + } + + getPreferences$(): Observable<UserPreferencesModel> { + return this.preferencesTracker$.asObservable(); + } + + selectDashboard = () => + this.getPreferences$().pipe(selectDistinctState<UserPreferencesModel, DashboardModel>(STATE_KEYS.DASHBOARD)); + selectDashboardApps = () => + this.selectDashboard().pipe(selectDistinctState<DashboardModel, DashboardAppsModel>(STATE_KEYS.APPS)); + selectDashboardAvailableTiles = () => + this.selectDashboardApps().pipe(selectDistinctState<DashboardAppsModel, DashboardTileSettings[]>(STATE_KEYS.TILES)); + selectLastUserAction = () => + this.selectDashboardApps().pipe( + selectDistinctState<DashboardAppsModel, LastUserActionSettings>(STATE_KEYS.USER_ACTIONS), + ); + + getPreferences(): void { + this.preferencesService + .getPreferences() + .pipe( + map(preferences => { + return _mergeWith({}, defaultUserSettings, preferences.properties, (objValue, srcValue) => { + if ( + (Array.isArray(srcValue) && !srcValue.some(_isObject)) || + isString(srcValue) || + typeof srcValue === 'boolean' || + Number.isInteger(srcValue) + ) { + return srcValue; + } + }) as UserPreferencesModel; + }), + ) + .subscribe(userPreferences => { + this.preferencesTracker$.next(userPreferences); + }); + } + + updatePreferences(preferences: UpdateUserPreferenceModel): Observable<PreferencesResponse> { + return this.getPreferences$().pipe( + take(1), + switchMap(data => { + const properties = _mergeWith({}, data, preferences, (objValue, srcValue) => { + if ( + Array.isArray(srcValue) || + isString(srcValue) || + typeof srcValue === 'boolean' || + Number.isInteger(srcValue) + ) { + return srcValue; + } + }) as UserPreferencesModel; + this.preferencesTracker$.next(properties); + return this.preferencesService.savePreferences({ properties }); + }), + ); + } + + removePreferences(): Observable<PreferencesResponse> { + return this.preferencesService.updatePreferences({ properties: {} }); + } +} + +export function selectDistinctState<T, I>(key: string): UnaryFunction<Observable<T>, Observable<I>> { + return pipe(pluck<T, I>(key), distinctUntilChanged<I>()); +} |