From f3684db6a31ed1cca291858601a693f3faf8111c Mon Sep 17 00:00:00 2001 From: tenretC Date: Thu, 22 Mar 2018 15:50:19 +0100 Subject: [PATCH] feat(logging): create StarkLogging module. Fixed imports --- packages/stark-core/package.json | 5 + packages/stark-core/src/common/store/index.ts | 1 + .../common/store/starkCoreApplicationState.ts | 25 ++ .../application/app-config.entity.intf.ts | 150 +++++++ .../entities/application/app-config.entity.ts | 159 ++++++++ .../entities/application/index.ts | 4 + .../src/configuration/entities/index.ts | 1 + packages/stark-core/src/http/index.ts | 2 +- .../http-abstract-repository.spec.ts | 2 +- .../repository/http-abstract-repository.ts | 2 +- .../http.service.intf.ts | 0 .../http.service.spec.ts | 378 ++++++++---------- .../{service => services}/http.service.ts | 0 .../src/http/{service => services}/index.ts | 0 .../stark-core/src/logging/actions/index.ts | 3 + .../src/logging/actions/logging.actions.ts | 46 +++ .../stark-core/src/logging/entities/index.ts | 7 + .../entities/log-message-type.entity.ts | 8 + .../entities/log-message.entity.intf.ts | 11 + .../logging/entities/log-message.entity.ts | 22 + .../logging/entities/logging.entity.intf.ts | 9 + .../src/logging/entities/logging.entity.ts | 12 + packages/stark-core/src/logging/index.ts | 5 +- .../stark-core/src/logging/logging.module.ts | 23 ++ .../stark-core/src/logging/reducers/index.ts | 7 + .../logging/reducers/logging.reducer.spec.ts | 147 +++++++ .../src/logging/reducers/logging.reducer.ts | 37 ++ .../logging/{service => services}/index.ts | 1 + .../logging.service.intf.ts | 5 - .../logging/services/logging.service.spec.ts | 304 ++++++++++++++ .../src/logging/services/logging.service.ts | 288 +++++++++++++ packages/stark-core/src/util/index.ts | 1 + .../src/util/validation-errors.util.spec.ts | 100 +++++ .../src/util/validation-errors.util.ts | 45 +++ packages/stark-core/tsconfig.json | 2 +- 35 files changed, 1601 insertions(+), 211 deletions(-) create mode 100644 packages/stark-core/src/common/store/index.ts create mode 100644 packages/stark-core/src/common/store/starkCoreApplicationState.ts create mode 100644 packages/stark-core/src/configuration/entities/application/app-config.entity.intf.ts create mode 100644 packages/stark-core/src/configuration/entities/application/app-config.entity.ts create mode 100644 packages/stark-core/src/configuration/entities/application/index.ts rename packages/stark-core/src/http/{service => services}/http.service.intf.ts (100%) rename packages/stark-core/src/http/{service => services}/http.service.spec.ts (92%) rename packages/stark-core/src/http/{service => services}/http.service.ts (100%) rename packages/stark-core/src/http/{service => services}/index.ts (100%) create mode 100644 packages/stark-core/src/logging/actions/index.ts create mode 100644 packages/stark-core/src/logging/actions/logging.actions.ts create mode 100644 packages/stark-core/src/logging/entities/index.ts create mode 100644 packages/stark-core/src/logging/entities/log-message-type.entity.ts create mode 100644 packages/stark-core/src/logging/entities/log-message.entity.intf.ts create mode 100644 packages/stark-core/src/logging/entities/log-message.entity.ts create mode 100644 packages/stark-core/src/logging/entities/logging.entity.intf.ts create mode 100644 packages/stark-core/src/logging/entities/logging.entity.ts create mode 100644 packages/stark-core/src/logging/logging.module.ts create mode 100644 packages/stark-core/src/logging/reducers/index.ts create mode 100644 packages/stark-core/src/logging/reducers/logging.reducer.spec.ts create mode 100644 packages/stark-core/src/logging/reducers/logging.reducer.ts rename packages/stark-core/src/logging/{service => services}/index.ts (61%) rename packages/stark-core/src/logging/{service => services}/logging.service.intf.ts (98%) create mode 100644 packages/stark-core/src/logging/services/logging.service.spec.ts create mode 100644 packages/stark-core/src/logging/services/logging.service.ts create mode 100644 packages/stark-core/src/util/validation-errors.util.spec.ts create mode 100644 packages/stark-core/src/util/validation-errors.util.ts diff --git a/packages/stark-core/package.json b/packages/stark-core/package.json index ed79b71cf2..f9896bd772 100644 --- a/packages/stark-core/package.json +++ b/packages/stark-core/package.json @@ -28,11 +28,16 @@ "dependencies": { "@types/core-js": "0.9.46", "@types/node": "6.0.102", + "@types/uuid": "3.4.3", + "@ngrx/store": "5.2.0", "cerialize": "0.1.18", "class-validator": "0.7.3", "core-js": "2.5.3", "deep-freeze-strict": "1.1.1", + "moment": "2.21.0", "rxjs": "5.5.6", + "typescript": "2.6.2", + "uuid": "3.2.1", "zone.js": "0.8.20" }, "devDependencies": { diff --git a/packages/stark-core/src/common/store/index.ts b/packages/stark-core/src/common/store/index.ts new file mode 100644 index 0000000000..bb60785bb8 --- /dev/null +++ b/packages/stark-core/src/common/store/index.ts @@ -0,0 +1 @@ +export { StarkCoreApplicationState } from "./starkCoreApplicationState"; diff --git a/packages/stark-core/src/common/store/starkCoreApplicationState.ts b/packages/stark-core/src/common/store/starkCoreApplicationState.ts new file mode 100644 index 0000000000..d83589df85 --- /dev/null +++ b/packages/stark-core/src/common/store/starkCoreApplicationState.ts @@ -0,0 +1,25 @@ +/** + * Interface defining the shape of the application state of Stark Core (i.e., what's stored in Redux by Stark) + */ +import { StarkLogging } from "../../logging/entities/index"; +// import {StarkSession} from "../../session/entities"; +// import {StarkSettings} from "../../settings/entities"; + +export interface StarkCoreApplicationState + extends StarkLoggingApplicationState /*, StarkSessionApplicationState, StarkSettingsApplicationState*/ { + // starkApplicationMetadata: StarkApplicationMetadata; + // starkLogging: StarkLogging; + // starkSession: StarkSession; + // starkSettings: StarkSettings; + // starkUser: StarkUser; // not stored in Redux +} +export interface StarkLoggingApplicationState { + starkLogging: StarkLogging; +} + +// export interface StarkSessionApplicationState { +// starkSession: StarkSession; +// } +// export interface StarkSettingsApplicationState { +// starkSettings: StarkSettings; +// } diff --git a/packages/stark-core/src/configuration/entities/application/app-config.entity.intf.ts b/packages/stark-core/src/configuration/entities/application/app-config.entity.intf.ts new file mode 100644 index 0000000000..41a38d5f8b --- /dev/null +++ b/packages/stark-core/src/configuration/entities/application/app-config.entity.intf.ts @@ -0,0 +1,150 @@ +"use strict"; + +import { StarkBackend } from "../../../http/entities/backend"; + +export const starkAppConfigConstantName: string = "StarkAppConfig"; +/** + * Minimal set of configuration options for Stark applications. + * An implementation should be instantiated and be available under the name defined in the constants. + */ +export interface StarkApplicationConfig { + /** + * Url of the state defined as root of the router state tree definition + */ + rootStateUrl: string; + + /** + * Name of the state defined as root of the router state tree definition + */ + rootStateName: string; + + /** + * Name of the state defined as home (homepage) + */ + homeStateName: string; + + /** + * Name of the state to be navigated to on generic errors + */ + errorStateName: string; + + /** + * Enable Angular's debug runtime information + * @link https://docs.angularjs.org/guide/production#disabling-debug-data + * @link https://docs.angularjs.org/api/ng/provider/$compileProvider#debugInfoEnabled + */ + angularDebugInfoEnabled: boolean; + + /** + * Enable logging of debug level messages + */ + debugLoggingEnabled: boolean; + + /** + * When the number of log messages reaches the loggingFlushPersistSize value, + * the log messages are sent to the back-end and removed from the redux store. + * Default: 15 + */ + loggingFlushPersistSize: number; + + /** + * The loggingFlushApplicationId uniquely identifies the application. + * It makes that the back-end can recognize your application. + */ + loggingFlushApplicationId: string; + + /** + * The loggingFlushResourceName defines the name of the logging resource on the back-end. Default: "logging" + */ + loggingFlushResourceName: string; + + /** + * Enable router logging + */ + routerLoggingEnabled: boolean; + + /** + * Timeout period before the session is ended if no user interaction occurs + */ + sessionTimeout: number; + + /** + * Seconds before the session is ended (due to no user interaction) when the timeout warning event will be emitted. + * Default: 15 + */ + sessionTimeoutWarningPeriod: number; + + /** + * Interval in seconds between every "keepalive" ping. Default: 15 + */ + keepAliveInterval: number; + + /** + * Url where the "keepalive" pings should be sent to + */ + keepAliveUrl: string; + + /** + * Url to be navigated to logout the user + */ + logoutUrl: string; + + /** + * Base Url of the application + */ + baseUrl: string; + + /** + * The language to be used as default. + * If a translation key is not found in the current language, the one from the default language is used as fallback + */ + defaultLanguage: string; + + /** + * Whether the application is public or private. + * Public applications don't require authentication and usually provide read-only access to information + */ + publicApp: boolean; + + /** + * Option to disable the logging flush if it not needed for the application. + * default: false + */ + loggingFlushDisabled?: boolean; + + /** + * Option to disable the keepAlive if it not needed for the application. + * default: false + */ + keepAliveDisabled?: boolean; + + /** + * Backends that the application will interact to. + */ + backends: Map; + + /** + * Get a back-end by name + * @param name name of the back-end object to get + * @returns StarkBackend The requested backend + */ + getBackend(name: string): StarkBackend; + + /** + * Add a back-end + * @param backend back-end object to add + */ + addBackend(backend: StarkBackend): void; + + /** + * Define all back-ends + * @param backends the array of back-end objects + */ + setBackends(backends: StarkBackend[]): void; + + /*** + * Get all currently defined back-end objects + * @returns Map A Map containing the different backends + */ + getBackends(): Map; +} diff --git a/packages/stark-core/src/configuration/entities/application/app-config.entity.ts b/packages/stark-core/src/configuration/entities/application/app-config.entity.ts new file mode 100644 index 0000000000..81c6aac44e --- /dev/null +++ b/packages/stark-core/src/configuration/entities/application/app-config.entity.ts @@ -0,0 +1,159 @@ +"use strict"; + +import { IsBoolean, IsDefined, IsNotEmpty, IsNumber, IsString, IsUrl, Matches, Min, /* ValidateIf,*/ validateSync } from "class-validator"; +import { autoserialize, autoserializeAs } from "cerialize"; +import { StarkApplicationConfig } from "./app-config.entity.intf"; +import { StarkBackend, StarkBackendImpl } from "../../../http/entities/backend/index"; +import { stringMap } from "../../../serialization/index"; +import { StarkValidationErrorsUtil } from "../../../util/index"; +// import {StarkMapIsValid, StarkMapNotEmpty} from "../../../validation/decorators"; + +export class StarkApplicationConfigImpl implements StarkApplicationConfig { + @IsDefined() + @IsString() + @autoserialize + public rootStateUrl: string; + + @IsDefined() + @IsString() + @autoserialize + public rootStateName: string; + + @IsDefined() + @IsString() + @autoserialize + public homeStateName: string; + + @IsDefined() + @IsString() + @autoserialize + public errorStateName: string; + + @IsDefined() + @IsBoolean() + @autoserialize + public angularDebugInfoEnabled: boolean; + + @IsDefined() + @IsBoolean() + @autoserialize + public debugLoggingEnabled: boolean; + + @IsNumber() + @Min(1) + @autoserialize + public loggingFlushPersistSize: number; + + @IsDefined() + @IsString() + @autoserialize + public loggingFlushApplicationId: string; + + @IsDefined() + @IsString() + @autoserialize + public loggingFlushResourceName: string; + + @IsDefined() + @IsBoolean() + @autoserialize + public routerLoggingEnabled: boolean; + + @IsNotEmpty() + @IsString() + @Matches(/^[a-z]{2}$/) + @autoserialize + public defaultLanguage: string; + + @IsDefined() + @IsNumber() + @autoserialize + public sessionTimeout: number; + + @IsNumber() + @autoserialize + public sessionTimeoutWarningPeriod: number; + + @IsNumber() + @autoserialize + public keepAliveInterval: number; + + // @ValidateIf((keepAliveUrl: string) => typeof keepAliveUrl === "string") + @IsUrl() + @autoserialize + public keepAliveUrl: string; + + @IsDefined() + @IsUrl() + @autoserialize + public logoutUrl: string; + + @IsNotEmpty() + @IsString() + @autoserialize + public baseUrl: string; + + @IsDefined() + @IsBoolean() + @autoserialize + public publicApp: boolean; + + // @ValidateIf((loggingFlushDisabled: boolean) => typeof loggingFlushDisabled === "boolean") + @IsBoolean() + @autoserialize + public loggingFlushDisabled: boolean; + + // @ValidateIf((keepAliveDisabled: boolean) => typeof keepAliveDisabled === "boolean") + @IsBoolean() + @autoserialize + public keepAliveDisabled: boolean; + //FIXME Import StarkMapIsValid & StarkMapNotEmpty from validation/decorators + // @StarkMapNotEmpty() + // @StarkMapIsValid() + @autoserializeAs(stringMap(StarkBackendImpl)) // using custom serialization type (stringMap) to handle ES6 Maps + public backends: Map = new Map(); + + public constructor() { + // Default values + // FIXME: DEVELOPMENT env variable? + /*if (DEVELOPMENT) { + this.loggingFlushPersistSize = 500; + } else {*/ + this.loggingFlushPersistSize = 15; + // } + + this.loggingFlushResourceName = "logging"; + this.sessionTimeoutWarningPeriod = 15; + this.keepAliveInterval = 15; + } + + public addBackend(backend: StarkBackend): void { + if (!backend) { + throw new Error("A backend instance must be provided"); + } + + StarkValidationErrorsUtil.throwOnError(validateSync(backend), "The backend instance provided is not valid."); + + this.backends.set(backend.name, backend); + } + + public setBackends(backends: StarkBackend[]): void { + this.backends = new Map(); + + for (const backend of backends) { + this.addBackend(backend); + } + } + + public getBackend(name: string): StarkBackend { + const backend: StarkBackend | undefined = this.backends.get(name); + if (backend === undefined) { + throw new Error("Backend " + name + " is undefined."); + } + return backend; + } + + public getBackends(): Map { + return this.backends; + } +} diff --git a/packages/stark-core/src/configuration/entities/application/index.ts b/packages/stark-core/src/configuration/entities/application/index.ts new file mode 100644 index 0000000000..9ccf7cf4c6 --- /dev/null +++ b/packages/stark-core/src/configuration/entities/application/index.ts @@ -0,0 +1,4 @@ +"use strict"; + +export * from "./app-config.entity.intf"; +export * from "./app-config.entity"; diff --git a/packages/stark-core/src/configuration/entities/index.ts b/packages/stark-core/src/configuration/entities/index.ts index 2be27a68ba..39b6fe1ab2 100644 --- a/packages/stark-core/src/configuration/entities/index.ts +++ b/packages/stark-core/src/configuration/entities/index.ts @@ -2,3 +2,4 @@ export * from "./language/index"; export * from "./metadata/index"; +export * from "./application/index"; diff --git a/packages/stark-core/src/http/index.ts b/packages/stark-core/src/http/index.ts index fe84c4df8d..efea32b3de 100644 --- a/packages/stark-core/src/http/index.ts +++ b/packages/stark-core/src/http/index.ts @@ -6,5 +6,5 @@ export * from "./entities/index"; export * from "./enumerators/index"; export * from "./repository/index"; export * from "./serializer/index"; -export * from "./service/index"; +export * from "./services/index"; export * from "./http.module"; diff --git a/packages/stark-core/src/http/repository/http-abstract-repository.spec.ts b/packages/stark-core/src/http/repository/http-abstract-repository.spec.ts index f83d1ad900..4e08ae6c49 100644 --- a/packages/stark-core/src/http/repository/http-abstract-repository.spec.ts +++ b/packages/stark-core/src/http/repository/http-abstract-repository.spec.ts @@ -11,7 +11,7 @@ import { StarkResource, StarkSingleItemResponseWrapper } from "../entities/index"; -import { StarkHttpService } from "../service/http.service.intf"; +import { StarkHttpService } from "../services/http.service.intf"; import { StarkLoggingService } from "../../logging"; import { UnitTestingUtils } from "../../test/unit-testing/index"; import { StarkHttpRequestBuilderImpl } from "../builder/index"; diff --git a/packages/stark-core/src/http/repository/http-abstract-repository.ts b/packages/stark-core/src/http/repository/http-abstract-repository.ts index 293721bd0e..3360e6b22b 100644 --- a/packages/stark-core/src/http/repository/http-abstract-repository.ts +++ b/packages/stark-core/src/http/repository/http-abstract-repository.ts @@ -13,7 +13,7 @@ import { StarkHttpSearchRequestParams, StarkHttpUpdateRequestParams } from "../builder/index"; -import { StarkHttpService } from "../service/http.service.intf"; +import { StarkHttpService } from "../services/http.service.intf"; import { StarkLoggingService } from "../../logging/index"; import { StarkSerializable } from "../../serialization/index"; import { StarkHttpSerializer, StarkHttpSerializerImpl } from "../serializer/index"; diff --git a/packages/stark-core/src/http/service/http.service.intf.ts b/packages/stark-core/src/http/services/http.service.intf.ts similarity index 100% rename from packages/stark-core/src/http/service/http.service.intf.ts rename to packages/stark-core/src/http/services/http.service.intf.ts diff --git a/packages/stark-core/src/http/service/http.service.spec.ts b/packages/stark-core/src/http/services/http.service.spec.ts similarity index 92% rename from packages/stark-core/src/http/service/http.service.spec.ts rename to packages/stark-core/src/http/services/http.service.spec.ts index 7efb196306..4c0107646b 100644 --- a/packages/stark-core/src/http/service/http.service.spec.ts +++ b/packages/stark-core/src/http/services/http.service.spec.ts @@ -345,7 +345,7 @@ describe("Service: StarkHttpService", () => { expect(errorWrapper.httpError.errors).toBeDefined(); expect(errorWrapper.httpError.errors.length).toBe(mockHttpErrorsCount); expect(errorWrapper.starkHttpHeaders.size).toBe(3); - expect(errorCounter).toBe(1 + request.retryCount); // error in original request + number of retries + expect(httpMock.get).toHaveBeenCalledTimes(1 + request.retryCount); // original request + number of retries done(); } ); @@ -374,11 +374,9 @@ describe("Service: StarkHttpService", () => { body: undefined, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.delete).and.returnValue(Observable.of(httpResponse)); + ( httpMock.delete).and.returnValue(Observable.of(httpResponse)); - const resultObs: Observable> = starkHttpService.executeSingleItemRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeSingleItemRequest(request); resultObs.subscribe( (result: StarkSingleItemResponseWrapper) => { @@ -391,12 +389,15 @@ describe("Service: StarkHttpService", () => { expect(result.starkHttpHeaders.get(expiresKey)).toBe(expiresValue); expect(httpMock.delete).toHaveBeenCalledTimes(1); - expect(httpMock.delete).toHaveBeenCalledWith("www.awesomeapi.com/mock", { - params: convertMapIntoObject(request.queryParameters), - headers: convertMapIntoObject(headersMap), - observe: "response", - responseType: "json" - }); + expect(httpMock.delete).toHaveBeenCalledWith( + "www.awesomeapi.com/mock", + { + params: convertMapIntoObject(request.queryParameters), + headers: convertMapIntoObject(headersMap), + observe: "response", + responseType: "json" + } + ); }, () => { fail("The 'error' function should not be called in case of success"); @@ -410,11 +411,9 @@ describe("Service: StarkHttpService", () => { body: mockHttpError, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.delete).and.returnValue(Observable.throw(httpResponse)); + ( httpMock.delete).and.returnValue(Observable.throw(httpResponse)); - const resultObs: Observable> = starkHttpService.executeSingleItemRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeSingleItemRequest(request); resultObs.subscribe( () => { @@ -449,12 +448,15 @@ describe("Service: StarkHttpService", () => { expect(errorWrapper.starkHttpHeaders.get(expiresKey)).toBe(expiresValue); expect(httpMock.delete).toHaveBeenCalledTimes(1); - expect(httpMock.delete).toHaveBeenCalledWith("www.awesomeapi.com/mock", { - params: convertMapIntoObject(request.queryParameters), - headers: convertMapIntoObject(headersMap), - observe: "response", - responseType: "json" - }); + expect(httpMock.delete).toHaveBeenCalledWith( + "www.awesomeapi.com/mock", + { + params: convertMapIntoObject(request.queryParameters), + headers: convertMapIntoObject(headersMap), + observe: "response", + responseType: "json" + } + ); } ); }); @@ -476,9 +478,7 @@ describe("Service: StarkHttpService", () => { request.retryCount = 2; - const resultObs: Observable> = starkHttpService.executeSingleItemRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeSingleItemRequest(request); resultObs.subscribe( () => { @@ -490,7 +490,7 @@ describe("Service: StarkHttpService", () => { expect(errorWrapper.httpError.errors).toBeDefined(); expect(errorWrapper.httpError.errors.length).toBe(mockHttpErrorsCount); expect(errorWrapper.starkHttpHeaders.size).toBe(3); - expect(errorCounter).toBe(1 + request.retryCount); // error in original request + number of retries + expect(httpMock.delete).toHaveBeenCalledTimes(1 + request.retryCount); // original request + number of retries done(); } ); @@ -519,13 +519,11 @@ describe("Service: StarkHttpService", () => { body: mockResourceWithEtag, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.post).and.returnValue(Observable.of(httpResponse)); + ( httpMock.post).and.returnValue(Observable.of(httpResponse)); request.requestType = StarkHttpRequestType.UPDATE; - const resultObs: Observable> = starkHttpService.executeSingleItemRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeSingleItemRequest(request); resultObs.subscribe((result: StarkSingleItemResponseWrapper) => { expect(result).toBeDefined(); @@ -549,13 +547,11 @@ describe("Service: StarkHttpService", () => { body: mockResourceWithEtag, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.post).and.returnValue(Observable.of(httpResponse)); + ( httpMock.post).and.returnValue(Observable.of(httpResponse)); request.requestType = StarkHttpRequestType.UPDATE; - const resultObs: Observable> = starkHttpService.executeSingleItemRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeSingleItemRequest(request); resultObs.subscribe( (result: StarkSingleItemResponseWrapper) => { @@ -596,13 +592,11 @@ describe("Service: StarkHttpService", () => { body: mockResourceWithMetadata, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.post).and.returnValue(Observable.of(httpResponse)); + ( httpMock.post).and.returnValue(Observable.of(httpResponse)); request.requestType = StarkHttpRequestType.UPDATE; - const resultObs: Observable> = starkHttpService.executeSingleItemRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeSingleItemRequest(request); resultObs.subscribe( (result: StarkSingleItemResponseWrapper) => { @@ -646,13 +640,11 @@ describe("Service: StarkHttpService", () => { body: mockResourceWithEtag, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.put).and.returnValue(Observable.of(httpResponse)); + ( httpMock.put).and.returnValue(Observable.of(httpResponse)); request.requestType = StarkHttpRequestType.UPDATE_IDEMPOTENT; - const resultObs: Observable> = starkHttpService.executeSingleItemRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeSingleItemRequest(request); resultObs.subscribe( (result: StarkSingleItemResponseWrapper) => { @@ -693,13 +685,11 @@ describe("Service: StarkHttpService", () => { body: mockResourceWithMetadata, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.put).and.returnValue(Observable.of(httpResponse)); + ( httpMock.put).and.returnValue(Observable.of(httpResponse)); request.requestType = StarkHttpRequestType.UPDATE_IDEMPOTENT; - const resultObs: Observable> = starkHttpService.executeSingleItemRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeSingleItemRequest(request); resultObs.subscribe( (result: StarkSingleItemResponseWrapper) => { @@ -743,13 +733,11 @@ describe("Service: StarkHttpService", () => { body: mockHttpError, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.post).and.returnValue(Observable.throw(httpResponse)); + ( httpMock.post).and.returnValue(Observable.throw(httpResponse)); request.requestType = StarkHttpRequestType.UPDATE; - const resultObs: Observable> = starkHttpService.executeSingleItemRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeSingleItemRequest(request); resultObs.subscribe( () => { @@ -816,9 +804,7 @@ describe("Service: StarkHttpService", () => { request.requestType = StarkHttpRequestType.UPDATE; request.retryCount = 2; - const resultObs: Observable> = starkHttpService.executeSingleItemRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeSingleItemRequest(request); resultObs.subscribe( () => { @@ -830,7 +816,7 @@ describe("Service: StarkHttpService", () => { expect(errorWrapper.httpError.errors).toBeDefined(); expect(errorWrapper.httpError.errors.length).toBe(mockHttpErrorsCount); expect(errorWrapper.starkHttpHeaders.size).toBe(3); - expect(errorCounter).toBe(1 + request.retryCount); // error in original request + number of retries + expect(httpMock.post).toHaveBeenCalledTimes(1 + request.retryCount); // original request + number of retries done(); } ); @@ -842,13 +828,11 @@ describe("Service: StarkHttpService", () => { body: mockHttpError, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.put).and.returnValue(Observable.throw(httpResponse)); + ( httpMock.put).and.returnValue(Observable.throw(httpResponse)); request.requestType = StarkHttpRequestType.UPDATE_IDEMPOTENT; - const resultObs: Observable> = starkHttpService.executeSingleItemRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeSingleItemRequest(request); resultObs.subscribe( () => { @@ -915,9 +899,7 @@ describe("Service: StarkHttpService", () => { request.requestType = StarkHttpRequestType.UPDATE_IDEMPOTENT; request.retryCount = 2; - const resultObs: Observable> = starkHttpService.executeSingleItemRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeSingleItemRequest(request); resultObs.subscribe( () => { @@ -929,7 +911,7 @@ describe("Service: StarkHttpService", () => { expect(errorWrapper.httpError.errors).toBeDefined(); expect(errorWrapper.httpError.errors.length).toBe(mockHttpErrorsCount); expect(errorWrapper.starkHttpHeaders.size).toBe(3); - expect(errorCounter).toBe(1 + request.retryCount); // error in original request + number of retries + expect(httpMock.put).toHaveBeenCalledTimes(1 + request.retryCount); // original request + number of retries done(); } ); @@ -958,11 +940,9 @@ describe("Service: StarkHttpService", () => { body: mockResourceWithEtag, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.post).and.returnValue(Observable.of(httpResponse)); + ( httpMock.post).and.returnValue(Observable.of(httpResponse)); - const resultObs: Observable> = starkHttpService.executeSingleItemRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeSingleItemRequest(request); resultObs.subscribe((result: StarkSingleItemResponseWrapper) => { expect(result).toBeDefined(); @@ -986,11 +966,9 @@ describe("Service: StarkHttpService", () => { body: mockResourceWithEtag, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.post).and.returnValue(Observable.of(httpResponse)); + ( httpMock.post).and.returnValue(Observable.of(httpResponse)); - const resultObs: Observable> = starkHttpService.executeSingleItemRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeSingleItemRequest(request); resultObs.subscribe( (result: StarkSingleItemResponseWrapper) => { @@ -1030,11 +1008,9 @@ describe("Service: StarkHttpService", () => { body: mockResourceWithMetadata, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.post).and.returnValue(Observable.of(httpResponse)); + ( httpMock.post).and.returnValue(Observable.of(httpResponse)); - const resultObs: Observable> = starkHttpService.executeSingleItemRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeSingleItemRequest(request); resultObs.subscribe( (result: StarkSingleItemResponseWrapper) => { @@ -1078,11 +1054,9 @@ describe("Service: StarkHttpService", () => { body: mockHttpError, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.post).and.returnValue(Observable.throw(httpResponse)); + ( httpMock.post).and.returnValue(Observable.throw(httpResponse)); - const resultObs: Observable> = starkHttpService.executeSingleItemRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeSingleItemRequest(request); resultObs.subscribe( () => { @@ -1147,9 +1121,7 @@ describe("Service: StarkHttpService", () => { request.retryCount = 2; - const resultObs: Observable> = starkHttpService.executeSingleItemRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeSingleItemRequest(request); resultObs.subscribe( () => { @@ -1161,7 +1133,7 @@ describe("Service: StarkHttpService", () => { expect(errorWrapper.httpError.errors).toBeDefined(); expect(errorWrapper.httpError.errors.length).toBe(mockHttpErrorsCount); expect(errorWrapper.starkHttpHeaders.size).toBe(3); - expect(errorCounter).toBe(1 + request.retryCount); // error in original request + number of retries + expect(httpMock.post).toHaveBeenCalledTimes(1 + request.retryCount); // original request + number of retries done(); } ); @@ -1191,7 +1163,16 @@ describe("Service: StarkHttpService", () => { prop1: 1234, prop2: "whatever", prop3: "2016-03-18T18:25:43.511Z", - prop4: ["some value", "false", "null", "", true, false, 0, { name: "Christopher", surname: "Cortes" }] + prop4: [ + "some value", + "false", + "null", + "", + true, + false, + 0, + {name: "Christopher", surname: "Cortes"} + ] }; describe("with a GetCollection request", () => { @@ -1217,7 +1198,9 @@ describe("Service: StarkHttpService", () => { const httpResponse: Partial>> = { status: StarkHttpStatusCodes.HTTP_200_OK, body: { - items: [mockResourceWithoutEtag], + items: [ + mockResourceWithoutEtag + ], metadata: { sortedBy: [ { @@ -1240,11 +1223,9 @@ describe("Service: StarkHttpService", () => { }, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.get).and.returnValue(Observable.of(httpResponse)); + ( httpMock.get).and.returnValue(Observable.of(httpResponse)); - const resultObs: Observable> = starkHttpService.executeCollectionRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeCollectionRequest(request); resultObs.subscribe( (result: StarkCollectionResponseWrapper) => { @@ -1293,12 +1274,15 @@ describe("Service: StarkHttpService", () => { expect(loggerMock.warn).not.toHaveBeenCalled(); expect(httpMock.get).toHaveBeenCalledTimes(1); - expect(httpMock.get).toHaveBeenCalledWith("www.awesomeapi.com/mock", { - params: convertMapIntoObject(request.queryParameters), - headers: convertMapIntoObject(headersMap), - observe: "response", - responseType: "json" - }); + expect(httpMock.get).toHaveBeenCalledWith( + "www.awesomeapi.com/mock", + { + params: convertMapIntoObject(request.queryParameters), + headers: convertMapIntoObject(headersMap), + observe: "response", + responseType: "json" + } + ); }, () => { fail("The 'error' function should not be called in case of success"); @@ -1310,7 +1294,9 @@ describe("Service: StarkHttpService", () => { const httpResponse: Partial>> = { status: StarkHttpStatusCodes.HTTP_200_OK, body: { - items: [mockResourceWithoutEtag], + items: [ + mockResourceWithoutEtag + ], metadata: { sortedBy: [], pagination: { @@ -1327,11 +1313,9 @@ describe("Service: StarkHttpService", () => { }, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.get).and.returnValue(Observable.of(httpResponse)); + ( httpMock.get).and.returnValue(Observable.of(httpResponse)); - const resultObs: Observable> = starkHttpService.executeCollectionRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeCollectionRequest(request); resultObs.subscribe( (result: StarkCollectionResponseWrapper) => { @@ -1363,7 +1347,9 @@ describe("Service: StarkHttpService", () => { const httpResponse: Partial>> = { status: StarkHttpStatusCodes.HTTP_200_OK, body: { - items: [mockResourceWithoutEtag], + items: [ + mockResourceWithoutEtag + ], metadata: { sortedBy: [], pagination: { @@ -1380,11 +1366,9 @@ describe("Service: StarkHttpService", () => { }, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.get).and.returnValue(Observable.of(httpResponse)); + ( httpMock.get).and.returnValue(Observable.of(httpResponse)); - const resultObs: Observable> = starkHttpService.executeCollectionRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeCollectionRequest(request); resultObs.subscribe( (result: StarkCollectionResponseWrapper) => { @@ -1436,17 +1420,15 @@ describe("Service: StarkHttpService", () => { }, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.get).and.returnValue(Observable.of(httpResponse)); + ( httpMock.get).and.returnValue(Observable.of(httpResponse)); - const resultObs: Observable> = starkHttpService.executeCollectionRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeCollectionRequest(request); resultObs.subscribe( (result: StarkCollectionResponseWrapper) => { expect(result).toBeDefined(); expect(result.starkHttpStatusCode).toBe(StarkHttpStatusCodes.HTTP_200_OK); - expect(result.data).toBeDefined(); // the data is whatever it comes in the "items" property + expect(result.data).toBeDefined(); // the data is whatever it comes in the "items" property expect(result.metadata instanceof StarkCollectionMetadataImpl).toBe(true); expect(result.metadata.sortedBy.length).toBe(0); expect(result.metadata.pagination).toBeDefined(); @@ -1467,8 +1449,7 @@ describe("Service: StarkHttpService", () => { const etags: { [uuid: string]: string } = {}; etags[mockUuid] = mockEtag; - const items: any[] = [ - // non-object item in "items" array + const items: any[] = [ // non-object item in "items" array "some value" ]; @@ -1492,11 +1473,9 @@ describe("Service: StarkHttpService", () => { }, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.get).and.returnValue(Observable.of(httpResponse)); + ( httpMock.get).and.returnValue(Observable.of(httpResponse)); - const resultObs: Observable> = starkHttpService.executeCollectionRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeCollectionRequest(request); resultObs.subscribe( (result: StarkCollectionResponseWrapper) => { @@ -1526,13 +1505,15 @@ describe("Service: StarkHttpService", () => { const etags: { [uuid: string]: string } = {}; etags[mockUuid] = mockEtag; - const mockResourceWithoutUuid: MockResource = { ...mockResourceWithEtag }; + const mockResourceWithoutUuid: MockResource = {...mockResourceWithEtag}; delete mockResourceWithoutUuid.uuid; const httpResponse: Partial>> = { status: StarkHttpStatusCodes.HTTP_200_OK, body: { - items: [mockResourceWithoutUuid], + items: [ + mockResourceWithoutUuid + ], metadata: { sortedBy: [], pagination: { @@ -1549,11 +1530,9 @@ describe("Service: StarkHttpService", () => { }, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.get).and.returnValue(Observable.of(httpResponse)); + ( httpMock.get).and.returnValue(Observable.of(httpResponse)); - const resultObs: Observable> = starkHttpService.executeCollectionRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeCollectionRequest(request); resultObs.subscribe( (result: StarkCollectionResponseWrapper) => { @@ -1589,7 +1568,9 @@ describe("Service: StarkHttpService", () => { const httpResponse: Partial>> = { status: StarkHttpStatusCodes.HTTP_200_OK, body: { - items: [mockResourceWithoutEtag], + items: [ + mockResourceWithoutEtag + ], metadata: { sortedBy: [], pagination: { @@ -1607,11 +1588,9 @@ describe("Service: StarkHttpService", () => { }, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.get).and.returnValue(Observable.of(httpResponse)); + ( httpMock.get).and.returnValue(Observable.of(httpResponse)); - const resultObs: Observable> = starkHttpService.executeCollectionRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeCollectionRequest(request); resultObs.subscribe( (result: StarkCollectionResponseWrapper) => { @@ -1643,16 +1622,16 @@ describe("Service: StarkHttpService", () => { const httpResponse: Partial>> = { status: StarkHttpStatusCodes.HTTP_200_OK, body: { - items: [mockResourceWithoutEtag], + items: [ + mockResourceWithoutEtag + ], metadata: undefined }, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.get).and.returnValue(Observable.of(httpResponse)); + ( httpMock.get).and.returnValue(Observable.of(httpResponse)); - const resultObs: Observable> = starkHttpService.executeCollectionRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeCollectionRequest(request); resultObs.subscribe( (result: StarkCollectionResponseWrapper) => { @@ -1679,11 +1658,9 @@ describe("Service: StarkHttpService", () => { body: mockHttpError, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.get).and.returnValue(Observable.throw(httpResponse)); + ( httpMock.get).and.returnValue(Observable.throw(httpResponse)); - const resultObs: Observable> = starkHttpService.executeCollectionRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeCollectionRequest(request); resultObs.subscribe( () => { @@ -1718,12 +1695,15 @@ describe("Service: StarkHttpService", () => { expect(errorWrapper.starkHttpHeaders.get(expiresKey)).toBe(expiresValue); expect(httpMock.get).toHaveBeenCalledTimes(1); - expect(httpMock.get).toHaveBeenCalledWith("www.awesomeapi.com/mock", { - params: convertMapIntoObject(request.queryParameters), - headers: convertMapIntoObject(headersMap), - observe: "response", - responseType: "json" - }); + expect(httpMock.get).toHaveBeenCalledWith( + "www.awesomeapi.com/mock", + { + params: convertMapIntoObject(request.queryParameters), + headers: convertMapIntoObject(headersMap), + observe: "response", + responseType: "json" + } + ); } ); }); @@ -1745,9 +1725,7 @@ describe("Service: StarkHttpService", () => { request.retryCount = 2; - const resultObs: Observable> = starkHttpService.executeCollectionRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeCollectionRequest(request); resultObs.subscribe( () => { @@ -1759,7 +1737,7 @@ describe("Service: StarkHttpService", () => { expect(errorWrapper.httpError.errors).toBeDefined(); expect(errorWrapper.httpError.errors.length).toBe(mockHttpErrorsCount); expect(errorWrapper.starkHttpHeaders.size).toBe(3); - expect(errorCounter).toBe(1 + request.retryCount); // error in original request + number of retries + expect(httpMock.get).toHaveBeenCalledTimes(1 + request.retryCount); // original request + number of retries done(); } ); @@ -1768,7 +1746,7 @@ describe("Service: StarkHttpService", () => { describe("with a Search request", () => { let request: StarkHttpRequest; - const mockCriteria: { [key: string]: any } = { field1: "anything", field2: "whatever" }; + const mockCriteria: { [key: string]: any } = {field1: "anything", field2: "whatever"}; beforeEach(() => { request = { @@ -1790,7 +1768,9 @@ describe("Service: StarkHttpService", () => { const httpResponse: Partial>> = { status: StarkHttpStatusCodes.HTTP_200_OK, body: { - items: [mockResourceWithoutEtag], + items: [ + mockResourceWithoutEtag + ], metadata: { sortedBy: [ { @@ -1813,11 +1793,9 @@ describe("Service: StarkHttpService", () => { }, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.post).and.returnValue(Observable.of(httpResponse)); + ( httpMock.post).and.returnValue(Observable.of(httpResponse)); - const resultObs: Observable> = starkHttpService.executeCollectionRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeCollectionRequest(request); resultObs.subscribe( (result: StarkCollectionResponseWrapper) => { @@ -1887,7 +1865,9 @@ describe("Service: StarkHttpService", () => { const httpResponse: Partial>> = { status: StarkHttpStatusCodes.HTTP_200_OK, body: { - items: [mockResourceWithoutEtag], + items: [ + mockResourceWithoutEtag + ], metadata: { sortedBy: [], pagination: { @@ -1904,11 +1884,9 @@ describe("Service: StarkHttpService", () => { }, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.post).and.returnValue(Observable.of(httpResponse)); + ( httpMock.post).and.returnValue(Observable.of(httpResponse)); - const resultObs: Observable> = starkHttpService.executeCollectionRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeCollectionRequest(request); resultObs.subscribe( (result: StarkCollectionResponseWrapper) => { @@ -1940,7 +1918,9 @@ describe("Service: StarkHttpService", () => { const httpResponse: Partial>> = { status: StarkHttpStatusCodes.HTTP_200_OK, body: { - items: [mockResourceWithoutEtag], + items: [ + mockResourceWithoutEtag + ], metadata: { sortedBy: [], pagination: { @@ -1957,11 +1937,9 @@ describe("Service: StarkHttpService", () => { }, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.post).and.returnValue(Observable.of(httpResponse)); + ( httpMock.post).and.returnValue(Observable.of(httpResponse)); - const resultObs: Observable> = starkHttpService.executeCollectionRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeCollectionRequest(request); resultObs.subscribe( (result: StarkCollectionResponseWrapper) => { @@ -2013,17 +1991,15 @@ describe("Service: StarkHttpService", () => { }, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.post).and.returnValue(Observable.of(httpResponse)); + ( httpMock.post).and.returnValue(Observable.of(httpResponse)); - const resultObs: Observable> = starkHttpService.executeCollectionRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeCollectionRequest(request); resultObs.subscribe( (result: StarkCollectionResponseWrapper) => { expect(result).toBeDefined(); expect(result.starkHttpStatusCode).toBe(StarkHttpStatusCodes.HTTP_200_OK); - expect(result.data).toBeDefined(); // the data is whatever it comes in the "items" property + expect(result.data).toBeDefined(); // the data is whatever it comes in the "items" property expect(result.metadata instanceof StarkCollectionMetadataImpl).toBe(true); expect(result.metadata.sortedBy.length).toBe(0); expect(result.metadata.pagination).toBeDefined(); @@ -2044,8 +2020,7 @@ describe("Service: StarkHttpService", () => { const etags: { [uuid: string]: string } = {}; etags[mockUuid] = mockEtag; - const items: any[] = [ - // non-object item in "items" array + const items: any[] = [ // non-object item in "items" array "some value" ]; @@ -2069,11 +2044,9 @@ describe("Service: StarkHttpService", () => { }, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.post).and.returnValue(Observable.of(httpResponse)); + ( httpMock.post).and.returnValue(Observable.of(httpResponse)); - const resultObs: Observable> = starkHttpService.executeCollectionRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeCollectionRequest(request); resultObs.subscribe( (result: StarkCollectionResponseWrapper) => { @@ -2103,13 +2076,15 @@ describe("Service: StarkHttpService", () => { const etags: { [uuid: string]: string } = {}; etags[mockUuid] = mockEtag; - const mockResourceWithoutUuid: MockResource = { ...mockResourceWithEtag }; + const mockResourceWithoutUuid: MockResource = {...mockResourceWithEtag}; delete mockResourceWithoutUuid.uuid; const httpResponse: Partial>> = { status: StarkHttpStatusCodes.HTTP_200_OK, body: { - items: [mockResourceWithoutUuid], + items: [ + mockResourceWithoutUuid + ], metadata: { sortedBy: [], pagination: { @@ -2126,11 +2101,9 @@ describe("Service: StarkHttpService", () => { }, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.post).and.returnValue(Observable.of(httpResponse)); + ( httpMock.post).and.returnValue(Observable.of(httpResponse)); - const resultObs: Observable> = starkHttpService.executeCollectionRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeCollectionRequest(request); resultObs.subscribe( (result: StarkCollectionResponseWrapper) => { @@ -2166,7 +2139,9 @@ describe("Service: StarkHttpService", () => { const httpResponse: Partial>> = { status: StarkHttpStatusCodes.HTTP_200_OK, body: { - items: [mockResourceWithoutEtag], + items: [ + mockResourceWithoutEtag + ], metadata: { sortedBy: [], pagination: { @@ -2184,11 +2159,9 @@ describe("Service: StarkHttpService", () => { }, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.post).and.returnValue(Observable.of(httpResponse)); + ( httpMock.post).and.returnValue(Observable.of(httpResponse)); - const resultObs: Observable> = starkHttpService.executeCollectionRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeCollectionRequest(request); resultObs.subscribe( (result: StarkCollectionResponseWrapper) => { @@ -2220,16 +2193,16 @@ describe("Service: StarkHttpService", () => { const httpResponse: Partial>> = { status: StarkHttpStatusCodes.HTTP_200_OK, body: { - items: [mockResourceWithoutEtag], + items: [ + mockResourceWithoutEtag + ], metadata: undefined }, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.post).and.returnValue(Observable.of(httpResponse)); + ( httpMock.post).and.returnValue(Observable.of(httpResponse)); - const resultObs: Observable> = starkHttpService.executeCollectionRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeCollectionRequest(request); resultObs.subscribe( (result: StarkCollectionResponseWrapper) => { @@ -2256,11 +2229,9 @@ describe("Service: StarkHttpService", () => { body: mockHttpError, headers: httpHeadersGetter(httpHeaders) }; - (httpMock.post).and.returnValue(Observable.throw(httpResponse)); + ( httpMock.post).and.returnValue(Observable.throw(httpResponse)); - const resultObs: Observable> = starkHttpService.executeCollectionRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeCollectionRequest(request); resultObs.subscribe( () => { @@ -2326,9 +2297,7 @@ describe("Service: StarkHttpService", () => { request.retryCount = 2; - const resultObs: Observable> = starkHttpService.executeCollectionRequest( - request - ); + const resultObs: Observable> = starkHttpService.executeCollectionRequest(request); resultObs.subscribe( () => { @@ -2340,7 +2309,7 @@ describe("Service: StarkHttpService", () => { expect(errorWrapper.httpError.errors).toBeDefined(); expect(errorWrapper.httpError.errors.length).toBe(mockHttpErrorsCount); expect(errorWrapper.starkHttpHeaders.size).toBe(3); - expect(errorCounter).toBe(1 + request.retryCount); // error in original request + number of retries + expect(httpMock.post).toHaveBeenCalledTimes(1 + request.retryCount); // original request + number of retries done(); } ); @@ -2429,19 +2398,25 @@ describe("Service: StarkHttpService", () => { @inheritSerialization(StarkSingleItemMetadataImpl) class MockResourceMetadata extends StarkSingleItemMetadataImpl { - @autoserialize public someValue?: string; + @autoserialize + public someValue?: string; } class MockResource implements StarkResource { - @autoserialize public uuid: string; + @autoserialize + public uuid: string; - @autoserialize public etag: string; + @autoserialize + public etag: string; - @autoserializeAs(MockResourceMetadata) public metadata: MockResourceMetadata; + @autoserializeAs(MockResourceMetadata) + public metadata: MockResourceMetadata; - @autoserialize public property1: string; + @autoserialize + public property1: string; - @autoserialize public property2: string; + @autoserialize + public property2: string; public constructor(uuid: string) { this.uuid = uuid; @@ -2465,6 +2440,7 @@ function convertMapIntoObject(map: Map): object { } class HttpServiceHelper

extends StarkHttpServiceImpl

{ + public retryDelay: number; public constructor(logger: StarkLoggingService, sessionService: StarkSessionService, $http: HttpClient) { diff --git a/packages/stark-core/src/http/service/http.service.ts b/packages/stark-core/src/http/services/http.service.ts similarity index 100% rename from packages/stark-core/src/http/service/http.service.ts rename to packages/stark-core/src/http/services/http.service.ts diff --git a/packages/stark-core/src/http/service/index.ts b/packages/stark-core/src/http/services/index.ts similarity index 100% rename from packages/stark-core/src/http/service/index.ts rename to packages/stark-core/src/http/services/index.ts diff --git a/packages/stark-core/src/logging/actions/index.ts b/packages/stark-core/src/logging/actions/index.ts new file mode 100644 index 0000000000..575e7f2a3e --- /dev/null +++ b/packages/stark-core/src/logging/actions/index.ts @@ -0,0 +1,3 @@ +"use strict"; + +export * from "./logging.actions"; diff --git a/packages/stark-core/src/logging/actions/logging.actions.ts b/packages/stark-core/src/logging/actions/logging.actions.ts new file mode 100644 index 0000000000..41125b1068 --- /dev/null +++ b/packages/stark-core/src/logging/actions/logging.actions.ts @@ -0,0 +1,46 @@ +"use strict"; + +import { Action } from "@ngrx/store"; +import { StarkLogMessage } from "../entities/index"; + +/** + * Actions related to stark logging service + */ + +export enum StarkLoggingActionTypes { + LOGGING_SET_APPLICATION_ID = "LOGGING_SET_APPLICATION_ID", + LOG_MESSAGE = "LOG_MESSAGE", + FLUSH_LOG = "FLUSH_LOG" +} + +/** + * Add the Application Name to the logging object + * @returns The created action object + */ +export class SetApplicationId implements Action { + readonly type = StarkLoggingActionTypes.LOGGING_SET_APPLICATION_ID; + + constructor(public applicationId: string) {} +} + +/** + * Store a debug/info/warning/error message + * @param message - The message to log + * @returns The created action object + */ +export class LogMessage implements Action { + readonly type = StarkLoggingActionTypes.LOG_MESSAGE; + + constructor(public message: StarkLogMessage) {} +} + +/** + * Persists the log messages in the redux store to the back-end + * @returns The created action object + */ +export class FlushLogMessages implements Action { + readonly type = StarkLoggingActionTypes.FLUSH_LOG; + + constructor(public numberOfMessagesToFlush: number) {} +} +export type StarkLoggingActions = SetApplicationId | LogMessage | FlushLogMessages; diff --git a/packages/stark-core/src/logging/entities/index.ts b/packages/stark-core/src/logging/entities/index.ts new file mode 100644 index 0000000000..46d28e7be5 --- /dev/null +++ b/packages/stark-core/src/logging/entities/index.ts @@ -0,0 +1,7 @@ +"use strict"; + +export * from "./logging.entity"; +export * from "./logging.entity.intf"; +export * from "./log-message.entity"; +export * from "./log-message.entity.intf"; +export * from "./log-message-type.entity"; diff --git a/packages/stark-core/src/logging/entities/log-message-type.entity.ts b/packages/stark-core/src/logging/entities/log-message-type.entity.ts new file mode 100644 index 0000000000..c88195080b --- /dev/null +++ b/packages/stark-core/src/logging/entities/log-message-type.entity.ts @@ -0,0 +1,8 @@ +"use strict"; + +export class StarkLogMessageType { + public static DEBUG: string = "DEBUG"; + public static INFO: string = "INFO"; + public static WARNING: string = "WARNING"; + public static ERROR: string = "ERROR"; +} diff --git a/packages/stark-core/src/logging/entities/log-message.entity.intf.ts b/packages/stark-core/src/logging/entities/log-message.entity.intf.ts new file mode 100644 index 0000000000..fd2ecec213 --- /dev/null +++ b/packages/stark-core/src/logging/entities/log-message.entity.intf.ts @@ -0,0 +1,11 @@ +"use strict"; + +import { StarkLogMessageType } from "./log-message-type.entity"; + +export interface StarkLogMessage { + timestamp: string; + message: string; + type: StarkLogMessageType; + correlationId: string; + error: string | undefined; +} diff --git a/packages/stark-core/src/logging/entities/log-message.entity.ts b/packages/stark-core/src/logging/entities/log-message.entity.ts new file mode 100644 index 0000000000..832cf1cfda --- /dev/null +++ b/packages/stark-core/src/logging/entities/log-message.entity.ts @@ -0,0 +1,22 @@ +"use strict"; + +import * as moment from "moment"; +import { autoserialize, autoserializeAs } from "cerialize"; +import { StarkLogMessageType } from "./log-message-type.entity"; +import { StarkLogMessage } from "./log-message.entity.intf"; + +export class StarkLogMessageImpl implements StarkLogMessage { + @autoserialize public timestamp: string; + @autoserialize public message: string; + @autoserializeAs(StarkLogMessageType) public type: StarkLogMessageType; + @autoserialize public correlationId: string; + @autoserialize public error: string | undefined; + + public constructor(type: StarkLogMessageType, message: string, correlationId: string, error?: string | undefined) { + this.timestamp = moment().format(); // ISO-8601 format + this.type = type; + this.message = message; + this.error = error; + this.correlationId = correlationId; + } +} diff --git a/packages/stark-core/src/logging/entities/logging.entity.intf.ts b/packages/stark-core/src/logging/entities/logging.entity.intf.ts new file mode 100644 index 0000000000..3df552e1da --- /dev/null +++ b/packages/stark-core/src/logging/entities/logging.entity.intf.ts @@ -0,0 +1,9 @@ +"use strict"; + +import { StarkLogMessage } from "./log-message.entity.intf"; + +export interface StarkLogging { + uuid: string; + applicationId: string; + messages: StarkLogMessage[]; +} diff --git a/packages/stark-core/src/logging/entities/logging.entity.ts b/packages/stark-core/src/logging/entities/logging.entity.ts new file mode 100644 index 0000000000..cc8dd90167 --- /dev/null +++ b/packages/stark-core/src/logging/entities/logging.entity.ts @@ -0,0 +1,12 @@ +"use strict"; + +import { StarkLogMessage } from "./log-message.entity.intf"; +import { StarkLogging } from "./logging.entity.intf"; +import { autoserialize, autoserializeAs } from "cerialize"; +import { StarkLogMessageImpl } from "./log-message.entity"; + +export class StarkLoggingImpl implements StarkLogging { + @autoserialize public uuid: string; + @autoserialize public applicationId: string; + @autoserializeAs(StarkLogMessageImpl) public messages: StarkLogMessage[]; +} diff --git a/packages/stark-core/src/logging/index.ts b/packages/stark-core/src/logging/index.ts index b2f5d16850..8ef723006f 100644 --- a/packages/stark-core/src/logging/index.ts +++ b/packages/stark-core/src/logging/index.ts @@ -1,3 +1,6 @@ "use strict"; -export * from "./service/index"; +export * from "./actions/index"; +export * from "./entities/index"; +export * from "./reducers/index"; +export * from "./services/index"; diff --git a/packages/stark-core/src/logging/logging.module.ts b/packages/stark-core/src/logging/logging.module.ts new file mode 100644 index 0000000000..ec0a468063 --- /dev/null +++ b/packages/stark-core/src/logging/logging.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from "@angular/core"; +// import {ActionReducerMap, StoreModule} from "@ngrx/store"; + +// import {loggingReducer} from "./reducers/index"; +// import {StarkLoggingActions} from "./actions/index"; +import { StarkLoggingServiceImpl, starkLoggingServiceName } from "./services/index"; +import { starkAppConfigConstantName, StarkApplicationConfigImpl } from "../configuration/entities/index"; +// import {StarkLoggingApplicationState} from "../common/store/starkCoreApplicationState"; + +// const reducers:ActionReducerMap = { +// starkLogging: loggingReducer +// }; + +@NgModule({ + // imports: [StoreModule.forRoot(reducers)], + imports: [], + declarations: [], + providers: [ + { provide: starkLoggingServiceName, useClass: StarkLoggingServiceImpl }, + { provide: starkAppConfigConstantName, useClass: StarkApplicationConfigImpl } + ] +}) +export class LoggingModule {} diff --git a/packages/stark-core/src/logging/reducers/index.ts b/packages/stark-core/src/logging/reducers/index.ts new file mode 100644 index 0000000000..8e8eeeb33c --- /dev/null +++ b/packages/stark-core/src/logging/reducers/index.ts @@ -0,0 +1,7 @@ +import { StarkLogging } from "../entities/index"; + +export { loggingReducer, starkLoggingStoreKey } from "./logging.reducer"; + +export interface StarkLoggingState { + starkLogging: StarkLogging; +} diff --git a/packages/stark-core/src/logging/reducers/logging.reducer.spec.ts b/packages/stark-core/src/logging/reducers/logging.reducer.spec.ts new file mode 100644 index 0000000000..fe66b298a9 --- /dev/null +++ b/packages/stark-core/src/logging/reducers/logging.reducer.spec.ts @@ -0,0 +1,147 @@ +"use strict"; + +import { StarkLogging, StarkLogMessageImpl, StarkLogMessageType } from "../entities/index"; +import { loggingReducer } from "./logging.reducer"; +import { SetApplicationId, LogMessage, FlushLogMessages } from "../actions/index"; + +const deepFreeze: Function = require("deep-freeze-strict"); + +describe("Reducer: LoggingReducer", () => { + let starkLogging: StarkLogging; + + beforeEach(() => { + starkLogging = { uuid: "dummyUuid", applicationId: "dummyAppId", messages: [] }; + }); + + describe("on LOG_MESSAGE", () => { + it("should add the given messages to the array when state given", () => { + // create the initial state object + const initialState: StarkLogging = starkLogging; + initialState.messages = [ + new StarkLogMessageImpl(StarkLogMessageType.DEBUG, "Message 1", ""), + new StarkLogMessageImpl(StarkLogMessageType.DEBUG, "Message 2", "") + ]; + + expect(initialState.messages.length).toBe(2); + + deepFreeze(initialState); //Enforce immutability + // const payload: any = { message: new StarkLogMessageImpl(StarkLogMessageType.DEBUG, "Message N", "") }; + // deepFreeze(payload); //Enforce immutability + // + // // Send the LOG_MESSAGE action to the loggingReducer + // const changedState: StarkLogging = loggingReducer(initialState, { + // type: StarkLoggingActions.LOG_MESSAGE, + // payload + // }); + + const changedState: StarkLogging = loggingReducer( + initialState, + new LogMessage(new StarkLogMessageImpl(StarkLogMessageType.DEBUG, "Message N", "")) + ); + + expect(changedState.messages.length).toBe(3); + expect(changedState.messages[0].message).toBe("Message 1"); + expect(changedState.messages[1].message).toBe("Message 2"); + expect(changedState.messages[2].message).toBe("Message N"); + }); + + it("should add the given messages to the array even if the state is not defined", () => { + // const payload: any = { message: new StarkLogMessageImpl(StarkLogMessageType.DEBUG, "Message N", "") }; + // deepFreeze(payload); //Enforce immutability + // + // // Send the LOG_MESSAGE action to the loggingReducer + // const changedState: StarkLogging = loggingReducer(undefined, { + // type: StarkLoggingActions.LOG_MESSAGE, + // payload + // }); + const changedState: StarkLogging = loggingReducer( + undefined, + new LogMessage(new StarkLogMessageImpl(StarkLogMessageType.DEBUG, "Message N", "")) + ); + + expect(changedState.messages.length).toBe(1); + expect(changedState.messages[0].message).toBe("Message N"); + }); + }); + + describe("on FLUSH_LOG", () => { + it("should remove from the log messages array the first X number of messages specified in the payload", () => { + // create the initial state object + const initialState: StarkLogging = starkLogging; + initialState.messages = [ + new StarkLogMessageImpl(StarkLogMessageType.DEBUG, "Message 1", ""), + new StarkLogMessageImpl(StarkLogMessageType.DEBUG, "Message 2", ""), + new StarkLogMessageImpl(StarkLogMessageType.DEBUG, "Message 3", ""), + new StarkLogMessageImpl(StarkLogMessageType.DEBUG, "Message 4", ""), + new StarkLogMessageImpl(StarkLogMessageType.DEBUG, "Message 5", "") + ]; + + expect(initialState.messages.length).toBe(5); + + deepFreeze(initialState); //Enforce immutability + // const payload: any = { numberOfMessagesToFlush: 3 }; + // deepFreeze(payload); //Enforce immutability + // + // // Send the FLUSH_LOG action to the loggingReducer + // const changedState: StarkLogging = loggingReducer(initialState, { + // type: StarkLoggingActions.FLUSH_LOG, + // payload + // }); + const changedState: StarkLogging = loggingReducer(initialState, new FlushLogMessages(3)); + + expect(changedState.messages.length).toBe(2); + expect(changedState.messages[0].message).toBe("Message 4"); + expect(changedState.messages[1].message).toBe("Message 5"); + }); + }); + + describe("on LOGGING_SET_APPLICATION_ID", () => { + it("should set the application id when state given", () => { + // create the initial state object + const initialState: StarkLogging = starkLogging; + initialState.applicationId = "whatever"; + + deepFreeze(initialState); //Enforce immutability + // const payload: any = { applicationId: "new appID" }; + // deepFreeze(payload); //Enforce immutability + + // Send the LOGGING_SET_APPLICATION_ID action to the loggingReducer + // const changedState: StarkLogging = loggingReducer(initialState, { + // type: StarkLoggingActions.LOGGING_SET_APPLICATION_ID, + // payload + // }); + const changedState: StarkLogging = loggingReducer(initialState, new SetApplicationId("new appID")); + + expect(changedState.applicationId).toBe("new appID"); + }); + it("should set the application id even if the state is not defined", () => { + // const payload: any = { applicationId: "new appID" }; + // deepFreeze(payload); //Enforce immutability + // + // // Send the LOGGING_SET_APPLICATION_ID action to the loggingReducer + // const changedState: StarkLogging = loggingReducer(undefined, { + // type: StarkLoggingActions.LOGGING_SET_APPLICATION_ID, + // payload + // }); + const changedState: StarkLogging = loggingReducer(undefined, new SetApplicationId("new appID")); + + expect(changedState.applicationId).toBe("new appID"); + }); + }); + + // describe("on any other Action", () => { + // it("should invoke the default state", () => { + // const initialState: StarkLogging = starkLogging; + // deepFreeze(initialState); //Enforce immutability + // + // // Send the MOCK_ACTION action to the loggingReducer + // const changedState: StarkLogging = loggingReducer(initialState, { + // type: "MOCK_ACTION" + // }); + // + // loggingReducer + // + // expect(changedState).toBe(initialState); + // }); + // }); +}); diff --git a/packages/stark-core/src/logging/reducers/logging.reducer.ts b/packages/stark-core/src/logging/reducers/logging.reducer.ts new file mode 100644 index 0000000000..4c244ffa1e --- /dev/null +++ b/packages/stark-core/src/logging/reducers/logging.reducer.ts @@ -0,0 +1,37 @@ +"use strict"; + +import { StarkLoggingActions, StarkLoggingActionTypes } from "../actions/index"; +import { StarkLogging, StarkLogMessage } from "../entities/index"; + +export const starkLoggingStoreKey: string = "starkLogging"; + +const INITIAL_STATE: Readonly = { + uuid: "", + applicationId: "", + messages: [] +}; + +export function loggingReducer( + state: Readonly = INITIAL_STATE, + action: Readonly +): Readonly { + // the new state will be calculated from the data coming in the actions + switch (action.type) { + case StarkLoggingActionTypes.LOG_MESSAGE: + const message: StarkLogMessage = action.message; + return { ...state, messages: [...state.messages, message] }; + + case StarkLoggingActionTypes.FLUSH_LOG: + const numberOfMessagesToFlush: number = action.numberOfMessagesToFlush; + const numberOfMessages: number = state.messages.length; + const messages: StarkLogMessage[] = state.messages.slice(numberOfMessagesToFlush, numberOfMessages); + + return { ...state, messages: [...messages] }; + + case StarkLoggingActionTypes.LOGGING_SET_APPLICATION_ID: + return { ...state, applicationId: action.applicationId }; + + default: + return state; + } +} diff --git a/packages/stark-core/src/logging/service/index.ts b/packages/stark-core/src/logging/services/index.ts similarity index 61% rename from packages/stark-core/src/logging/service/index.ts rename to packages/stark-core/src/logging/services/index.ts index 5851a0a1b6..909432bd60 100644 --- a/packages/stark-core/src/logging/service/index.ts +++ b/packages/stark-core/src/logging/services/index.ts @@ -1,3 +1,4 @@ "use strict"; +export * from "./logging.service"; export * from "./logging.service.intf"; diff --git a/packages/stark-core/src/logging/service/logging.service.intf.ts b/packages/stark-core/src/logging/services/logging.service.intf.ts similarity index 98% rename from packages/stark-core/src/logging/service/logging.service.intf.ts rename to packages/stark-core/src/logging/services/logging.service.intf.ts index 9e4bdc63e8..e635d97f4f 100644 --- a/packages/stark-core/src/logging/service/logging.service.intf.ts +++ b/packages/stark-core/src/logging/services/logging.service.intf.ts @@ -8,14 +8,9 @@ export const starkLoggingServiceName: string = "StarkLoggingService"; */ export interface StarkLoggingService { readonly correlationId: string; - generateNewCorrelationId(): string; - debug(...args: any[]): void; - info(...args: any[]): void; - warn(...args: any[]): void; - error(message: string, error?: Error): void; } diff --git a/packages/stark-core/src/logging/services/logging.service.spec.ts b/packages/stark-core/src/logging/services/logging.service.spec.ts new file mode 100644 index 0000000000..418e40e365 --- /dev/null +++ b/packages/stark-core/src/logging/services/logging.service.spec.ts @@ -0,0 +1,304 @@ +"use strict"; + +import Spy = jasmine.Spy; +import { Action, Store } from "@ngrx/store"; +import { Observable } from "rxjs/Observable"; +import "rxjs/add/observable/of"; +import "rxjs/add/observable/throw"; +import { Serialize } from "cerialize"; + +import { StarkLoggingActionTypes, FlushLogMessages } from "../../logging/actions/index"; +import { StarkLoggingServiceImpl } from "./logging.service"; +import { StarkApplicationConfig, StarkApplicationConfigImpl } from "../../configuration/entities/index"; +import { StarkLogging, StarkLoggingImpl, StarkLogMessage, StarkLogMessageImpl, StarkLogMessageType } from "../../logging/entities/index"; +import { StarkBackend } from "../../http/entities/backend"; +import { StarkCoreApplicationState } from "../../common/store/starkCoreApplicationState"; +// import {StarkXSRFService} from "../../xsrf"; +// import {UnitTestingUtils} from "../../test/unit-testing"; + +describe("Service: StarkLoggingService", () => { + let appConfig: StarkApplicationConfig; + let mockStore: Store; + // let mockXSRFService: StarkXSRFService; + let loggingService: LoggingServiceHelper; + const loggingBackend: StarkBackend = { + name: "logging", + url: "http://localhost:5000", + authenticationType: 1, + fakePreAuthenticationEnabled: true, + fakePreAuthenticationRolePrefix: "", + loginResource: "logging", + token: "" + }; + const dummyObject: object = { + prop1: "dummy prop", + prop2: 123, + method1: () => "dummy method" + }; + let mockStarkLogging: StarkLogging; + const loggingFlushPersistSize: number = 11; + + beforeEach(() => { + mockStore = jasmine.createSpyObj("store", ["dispatch", "select"]); + appConfig = new StarkApplicationConfigImpl(); + appConfig.debugLoggingEnabled = true; + appConfig.loggingFlushDisabled = false; + appConfig.loggingFlushApplicationId = "TEST"; + appConfig.loggingFlushPersistSize = loggingFlushPersistSize; + appConfig.addBackend(loggingBackend); + appConfig.sessionTimeout = 123; + appConfig.sessionTimeoutWarningPeriod = 13; + appConfig.keepAliveDisabled = true; + appConfig.logoutUrl = "http://localhost:5000/logout"; + appConfig.rootStateUrl = ""; + appConfig.rootStateName = ""; + appConfig.homeStateName = ""; + appConfig.errorStateName = ""; + appConfig.angularDebugInfoEnabled = false; + appConfig.defaultLanguage = "fr"; + appConfig.baseUrl = "/"; + appConfig.publicApp = false; + appConfig.routerLoggingEnabled = false; + + // mockXSRFService = UnitTestingUtils.getMockedXSRFService(); + mockStarkLogging = { + uuid: "dummy uuid", + applicationId: "dummy app id", + messages: [] + }; + (mockStore.select).and.returnValue(Observable.of(mockStarkLogging)); + loggingService = new LoggingServiceHelper(mockStore, appConfig /*, mockXSRFService*/); + // reset the calls counter because there is a log in the constructor + (mockStore.dispatch).calls.reset(); + }); + + describe("on initialization", () => { + it("should throw an error in case the logging flushing is enabled but the backend config is missing", () => { + appConfig.loggingFlushDisabled = false; + appConfig.backends.delete("logging"); + + expect(() => new LoggingServiceHelper(mockStore, appConfig /*, mockXSRFService*/)).toThrowError(/backend/); + }); + + it("should throw an error in case the logging flushing is enabled but the loggingFlushPersistSize option is not greater than 0", () => { + appConfig.loggingFlushPersistSize = 0; + + expect(() => new LoggingServiceHelper(mockStore, appConfig /*, mockXSRFService*/)).toThrowError(/loggingFlushPersistSize/); + + appConfig.loggingFlushPersistSize = -1; + + expect(() => new LoggingServiceHelper(mockStore, appConfig /*, mockXSRFService*/)).toThrowError(/loggingFlushPersistSize/); + }); + + it("should generate a new correlation id", () => { + expect(loggingService.correlationId).toBeDefined(); + expect(typeof loggingService.correlationId).toBe("string"); + expect(loggingService.correlationId).not.toBe(""); + }); + }); + + describe("debug", () => { + it("should dispatch LOG_MESSAGE action with an StarkLogMessageType of type DEBUG", () => { + loggingService.debug("dummy debug message", dummyObject, "end of message"); + + expect(mockStore.dispatch).toHaveBeenCalledTimes(1); + + const dispatchedAction: Action = (mockStore.dispatch).calls.mostRecent().args[0]; + expect(dispatchedAction.type).toBe(StarkLoggingActionTypes.LOG_MESSAGE); + + const { message }: any = dispatchedAction; + expect(message instanceof StarkLogMessageImpl).toBe(true); + expect((message).type).toBe(StarkLogMessageType.DEBUG); + expect((message).message).toContain("dummy debug message"); + expect((message).message).toContain(JSON.stringify(dummyObject)); + expect((message).message).toContain("end of message"); + expect((message).timestamp).toBeDefined(); + expect((message).error).toBeUndefined(); + }); + + it("should NOT dispatch any action nor log any message if appConfig,debugLoggingEnabled property is FALSE", () => { + appConfig.debugLoggingEnabled = false; + loggingService.debug("dummy debug message"); + + expect(mockStore.dispatch).not.toHaveBeenCalled(); + }); + }); + + describe("info", () => { + it("should dispatch LOG_MESSAGE action with an StarkLogMessageType of type INFO", () => { + loggingService.info("dummy info message", dummyObject, "end of message"); + + expect(mockStore.dispatch).toHaveBeenCalledTimes(1); + + const dispatchedAction: Action = (mockStore.dispatch).calls.mostRecent().args[0]; + expect(dispatchedAction.type).toBe(StarkLoggingActionTypes.LOG_MESSAGE); + + const { message }: any = dispatchedAction; + expect(message instanceof StarkLogMessageImpl).toBe(true); + expect((message).type).toBe(StarkLogMessageType.INFO); + expect((message).message).toContain("dummy info message"); + expect((message).message).toContain(JSON.stringify(dummyObject)); + expect((message).message).toContain("end of message"); + expect((message).timestamp).toBeDefined(); + expect((message).error).toBeUndefined(); + }); + }); + + describe("warn", () => { + it("should dispatch LOG_MESSAGE action with an StarkLogMessageType of type WARNING", () => { + loggingService.warn("dummy warning message", dummyObject, "end of message"); + + expect(mockStore.dispatch).toHaveBeenCalledTimes(1); + + const dispatchedAction: Action = (mockStore.dispatch).calls.mostRecent().args[0]; + expect(dispatchedAction.type).toBe(StarkLoggingActionTypes.LOG_MESSAGE); + + const { message }: any = dispatchedAction; + expect(message instanceof StarkLogMessageImpl).toBe(true); + expect((message).type).toBe(StarkLogMessageType.WARNING); + expect((message).message).toContain("dummy warning message"); + expect((message).message).toContain(JSON.stringify(dummyObject)); + expect((message).message).toContain("end of message"); + expect((message).timestamp).toBeDefined(); + expect((message).error).toBeUndefined(); + }); + }); + + describe("error", () => { + it("should dispatch LOG_MESSAGE action with an StarkLogMessageType of type ERROR", () => { + const errorMsg: string = "dummy error message" + JSON.stringify(dummyObject) + "end of message"; + loggingService.error(errorMsg, new Error("this is the error")); + expect(mockStore.dispatch).toHaveBeenCalledTimes(1); + const dispatchedAction: Action = (mockStore.dispatch).calls.mostRecent().args[0]; + expect(dispatchedAction.type).toBe(StarkLoggingActionTypes.LOG_MESSAGE); + const { message }: any = dispatchedAction; + expect(message instanceof StarkLogMessageImpl).toBe(true); + expect((message).type).toBe(StarkLogMessageType.ERROR); + expect((message).message).toContain("dummy error message"); + expect((message).message).toContain(JSON.stringify(dummyObject)); + expect((message).message).toContain("end of message"); + expect((message).timestamp).toBeDefined(); + expect((message).error).toBeDefined(); + expect((message).error).toContain("this is the error"); + }); + }); + + describe("constructLogMessage", () => { + describe("Debug / Info / Warning messages ", () => { + it("should return an StarkLogMessage instance of the type specified and the message containing the arguments", () => { + let message: StarkLogMessage; + message = (loggingService).constructLogMessageHelper(StarkLogMessageType.INFO, "arg1", dummyObject); + + expect(message.type).toBe(StarkLogMessageType.INFO); + expect(message.message).toContain("arg1 | " + JSON.stringify(dummyObject)); + expect(message.timestamp).toBeDefined(); + expect(message.error).toBeUndefined(); + + message = (loggingService).constructLogMessageHelper(StarkLogMessageType.DEBUG, "arg3", dummyObject); + + expect(message.type).toBe(StarkLogMessageType.DEBUG); + expect(message.message).toContain("arg3 | " + JSON.stringify(dummyObject)); + expect(message.timestamp).toBeDefined(); + expect(message.error).toBeUndefined(); + + message = (loggingService).constructLogMessageHelper( + StarkLogMessageType.WARNING, + "arg5", + dummyObject + ); + + expect(message.type).toBe(StarkLogMessageType.WARNING); + expect(message.message).toContain("arg5 | " + JSON.stringify(dummyObject)); + expect(message.timestamp).toBeDefined(); + expect(message.error).toBeUndefined(); + }); + }); + + describe("Error messages ", () => { + it("should return an StarkLogMessage instance of the type ERROR containing a message and the error string", () => { + let message: StarkLogMessage; + const error: Error = new Error("this is the error"); + message = (loggingService).constructLogMessageHelper( + StarkLogMessageType.ERROR, + "dummy error message", + error + ); + + expect(message.type).toBe(StarkLogMessageType.ERROR); + expect(message.message).toContain("dummy error message"); + expect(message.timestamp).toBeDefined(); + expect(message.error).toBeDefined(); + expect(message.error).toContain("this is the error"); + }); + }); + }); + + describe("persistLogMessages", () => { + beforeEach(() => { + for (let i: number = 1; i <= loggingFlushPersistSize; i++) { + const message: StarkLogMessage = (loggingService).constructLogMessageHelper( + StarkLogMessageType.INFO, + "Message " + i, + dummyObject + ); + mockStarkLogging.messages = [...mockStarkLogging.messages, message]; + } + }); + + it("should persist messages to the back-end when the persist size exceeds", () => { + expect(mockStarkLogging.messages.length).toBe(loggingFlushPersistSize); + + const sendRequestSpy: Spy = spyOn(loggingService, "sendRequest").and.callFake(() => Observable.of("ok")); + const data: string = JSON.stringify(Serialize(mockStarkLogging, StarkLoggingImpl)); + (loggingService).persistLogMessagesHelper(); + expect(sendRequestSpy).toHaveBeenCalledTimes(1); + expect(sendRequestSpy.calls.mostRecent().args[0]).toBe(loggingBackend.url + "/" + loggingBackend.loginResource); + expect(sendRequestSpy.calls.mostRecent().args[1]).toBe(data); + expect(sendRequestSpy.calls.mostRecent().args[2]).toBe(true); + + expect(mockStore.dispatch).toHaveBeenCalledTimes(1); + expect(mockStore.dispatch).toHaveBeenCalledWith(new FlushLogMessages(loggingFlushPersistSize)); + }); + + it("should fail to persist messages when the back-end fails", () => { + expect(mockStarkLogging.messages.length).toBe(loggingFlushPersistSize); + + const sendRequestSpy: Spy = spyOn(loggingService, "sendRequest").and.callFake(() => Observable.throw("ko")); + const errorSpy: Spy = spyOn(loggingService, "error"); + const data: string = JSON.stringify(Serialize(mockStarkLogging, StarkLoggingImpl)); + (loggingService).persistLogMessagesHelper(); + expect(sendRequestSpy).toHaveBeenCalledTimes(1); + expect(sendRequestSpy.calls.mostRecent().args[0]).toBe(loggingBackend.url + "/" + loggingBackend.loginResource); + expect(sendRequestSpy.calls.mostRecent().args[1]).toBe(data); + expect(sendRequestSpy.calls.mostRecent().args[2]).toBe(true); + expect(errorSpy).toHaveBeenCalledTimes(1); + expect(errorSpy.calls.mostRecent().args[0]).toContain("an error occurred while persisting log messages. (retry 1)"); + }); + }); +}); + +class LoggingServiceHelper extends StarkLoggingServiceImpl { + public constructor(store: Store, appConfig: StarkApplicationConfig /*, xsrfService: StarkXSRFService*/) { + super(store, appConfig /*, xsrfService*/); + } + public constructLogMessageHelper(messageType: StarkLogMessageType, ...args: any[]): StarkLogMessage { + return this.constructLogMessage(messageType, ...args); + } + + public persistLogMessagesHelper(isForced: boolean = false): void { + this.persistLogMessages(isForced); + } + + // override parent's implementation to prevent actual HTTP request to be sent! + public sendRequest(): Observable { + /* dummy function to be mocked */ + return Observable.of(undefined); + } + + // override parent's implementation to prevent logging to the console + public getConsole(): Function { + return () => { + /* noop */ + }; + } +} diff --git a/packages/stark-core/src/logging/services/logging.service.ts b/packages/stark-core/src/logging/services/logging.service.ts new file mode 100644 index 0000000000..119e08895d --- /dev/null +++ b/packages/stark-core/src/logging/services/logging.service.ts @@ -0,0 +1,288 @@ +"use strict"; + +import * as uuid from "uuid"; + +import { Serialize } from "cerialize"; +import { validateSync } from "class-validator"; + +import { Store } from "@ngrx/store"; +import { Observable } from "rxjs/Observable"; +import { Subject } from "rxjs/Subject"; + +import { Inject, Injectable } from "@angular/core"; + +import { StarkLoggingService, starkLoggingServiceName } from "./logging.service.intf"; +import { StarkApplicationConfig, starkAppConfigConstantName } from "../../configuration/entities/index"; +import { StarkBackend } from "../../http/entities/backend/index"; +import { StarkCoreApplicationState } from "../../common/store/starkCoreApplicationState"; +import { StarkHttpStatusCodes } from "../../http/enumerators/index"; +import { StarkHttpHeaders } from "../../http/constants/index"; +// import {StarkXSRFService, starkXSRFServiceName} from "../../xsrf"; +import { StarkValidationErrorsUtil } from "../../util/validation-errors.util"; +import { StarkLogging, StarkLoggingImpl, StarkLogMessage, StarkLogMessageImpl, StarkLogMessageType } from "../entities/index"; +import { LogMessage, FlushLogMessages } from "../actions/index"; + +const _noop: Function = require("lodash/noop"); + +/** + * @ngdoc service + * @name stark-core.service:StarkLoggingService + * @description Basic logging service implementation. + * Integrates logging with the Redux store + * + * @requires ngrx-store.Store + * @requires StarkApplicationConfig + * @requires StarkXSRFService + */ +@Injectable() +export class StarkLoggingServiceImpl implements StarkLoggingService { + private backend: StarkBackend; + private logUrl: string; + private logPersistSize: number; + private isPersisting: boolean; + private retryCounter: number; + private consoleDebug: Function; + private consoleInfo: Function; + private consoleWarn: Function; + private consoleError: Function; + private consoleTrace: Function; + private starkLogging: StarkLogging; + /** @internal */ + private _correlationId: string; + // FIXME: uncomment these lines once XSRF Service is implemented + public constructor( + private store: Store, + @Inject(starkAppConfigConstantName) + private appConfig: StarkApplicationConfig /*, + @Inject(starkXSRFServiceName) private xsrfService: StarkXSRFService*/ + ) { + this.isPersisting = false; + this.retryCounter = 0; + this.consoleDebug = this.getConsole("debug"); + this.consoleInfo = this.getConsole("info"); + this.consoleWarn = this.getConsole("warn"); + this.consoleError = this.getConsole("error"); + this.consoleTrace = this.getConsole("trace"); + + if (!this.appConfig.loggingFlushDisabled) { + // ensuring that the app config is valid before configuring the automatic logging flush + StarkValidationErrorsUtil.throwOnError( + validateSync(this.appConfig), + starkLoggingServiceName + ": " + starkAppConfigConstantName + " constant is not valid." + ); + + if (!(this.backend = this.appConfig.getBackend("logging"))) { + throw new Error(starkLoggingServiceName + ': no configuration found for "logging" backend.'); + } + + this.logPersistSize = this.appConfig.loggingFlushPersistSize; + this.logUrl = this.backend.url + "/" + this.appConfig.loggingFlushResourceName; + this.generateNewCorrelationId(); + + this.store.select(state => state.starkLogging).subscribe((starkLogging: StarkLogging) => { + this.starkLogging = starkLogging; + this.persistLogMessages(); + }); + + if (window) { + window.addEventListener("beforeunload", () => { + //ev: BeforeUnloadEvent + // Persist the remaining log entries that are still in the store, before leaving the application. + // We need to call the REST service synchronously, + // because the browser has to wait for the REST service to complete. + + const data: string = JSON.stringify(Serialize(this.starkLogging, StarkLoggingImpl)); + this.sendRequest(this.logUrl, data, false); + }); + } + + this.debug(starkLoggingServiceName + " loaded"); + } + } + + public debug(...args: any[]): void { + if (this.appConfig.debugLoggingEnabled) { + const debugMessage: StarkLogMessage = this.constructLogMessage(StarkLogMessageType.DEBUG, ...args); + this.store.dispatch(new LogMessage(debugMessage)); + // also log the message to the console + this.consoleDebug(...args); + } + } + + public info(...args: any[]): void { + const infoMessage: StarkLogMessage = this.constructLogMessage(StarkLogMessageType.INFO, ...args); + this.store.dispatch(new LogMessage(infoMessage)); + // also log the message to the console + this.consoleInfo(...args); + } + + public warn(...args: any[]): void { + const warningMessage: StarkLogMessage = this.constructLogMessage(StarkLogMessageType.WARNING, ...args); + this.store.dispatch(new LogMessage(warningMessage)); + // also log the message to the console + this.consoleWarn(...args); + } + + public error(message: string, error?: Error): void { + const errorMessage: StarkLogMessage = this.constructLogMessage(StarkLogMessageType.ERROR, message, error); + this.store.dispatch(new LogMessage(errorMessage)); + // also log the message to the console + this.consoleError(message, error); + // IE Errors don't have an "stack" property so we log it manually via console.trace() + if (error && !error.stack) { + this.consoleTrace(message, error); + } + } + + public get correlationId(): string { + return this._correlationId; + } + + public generateNewCorrelationId(): string { + this._correlationId = uuid.v4(); + return this._correlationId; + } + + protected constructLogMessage(messageType: StarkLogMessageType, ...args: any[]): StarkLogMessage { + let parsedMessage: string; + let parsedError: string | undefined; + + if (messageType !== StarkLogMessageType.ERROR) { + const parsedArgs: string[] = args.map((arg: any) => this.parseArg(arg)); + parsedMessage = parsedArgs.join(" | "); + } else { + const error: Error = args[1]; + parsedMessage = args[0]; + if (error) { + parsedError = this.formatError(error); + } + } + + return new StarkLogMessageImpl(messageType, parsedMessage, this._correlationId, parsedError); + } + + protected persistLogMessages(isForced: boolean = false): void { + const numberOfMessages: number = this.starkLogging.messages.length; + + if (numberOfMessages > 0 && this.starkLogging.messages[numberOfMessages - 1].type === StarkLogMessageType.ERROR) { + isForced = true; + } + + if ((numberOfMessages >= this.logPersistSize && !this.isPersisting) || isForced) { + if (this.retryCounter < 5) { + this.isPersisting = true; + + const data: string = JSON.stringify(Serialize(this.starkLogging, StarkLoggingImpl)); + + this.sendRequest(this.logUrl, data, true).subscribe( + () => { + this.isPersisting = false; + this.retryCounter = 0; + this.store.dispatch(new FlushLogMessages(numberOfMessages)); + }, + (error: Error) => { + this.isPersisting = false; + this.retryCounter++; + const errorMsg: string = + starkLoggingServiceName + + ": an error occurred while persisting log messages." + + " (retry " + + this.retryCounter + + ")"; + this.error(errorMsg, error); + } + ); + } else { + // still no success after retrying 5 times, will be tried again in the next logged message + } + } + } + + protected sendRequest(url: string, serializedData: string, async: boolean = true): Observable { + const httpRequest$: Subject = new Subject(); + + const emitXhrResult: Function = (xhrRequest: XMLHttpRequest) => { + if (xhrRequest.readyState === XMLHttpRequest.DONE) { + if (xhrRequest.status === StarkHttpStatusCodes.HTTP_200_OK || xhrRequest.status === StarkHttpStatusCodes.HTTP_201_CREATED) { + httpRequest$.next(); + httpRequest$.complete(); + } else { + httpRequest$.error(xhrRequest.status); + } + } + }; + + const xhr: XMLHttpRequest = new XMLHttpRequest(); + + if (async) { + xhr.onreadystatechange = () => { + emitXhrResult(xhr); + }; + } else { + emitXhrResult(xhr); + } + + // catch any error raised by the browser while opening the connection. for example: + // Chrome "mixed content" error: https://developers.google.com/web/fundamentals/security/prevent-mixed-content/what-is-mixed-content + // IE "Access is denied" error: https://stackoverflow.com/questions/22098259/access-denied-in-ie-10-and-11-when-ajax-target-is-localhost + try { + xhr.open("POST", url, async); + // this.xsrfService.configureXHR(xhr); + xhr.setRequestHeader(StarkHttpHeaders.CONTENT_TYPE, "application/json"); + xhr.send(serializedData); + } catch (e) { + httpRequest$.error(e); + } + + return httpRequest$; + } + + private parseArg(arg: any): string { + if (arg instanceof Error) { + return this.formatError(arg); + } else if (typeof arg === "string") { + return arg; + } else { + // catch potential "circular reference" error + try { + return JSON.stringify(arg); + } catch (e) { + return arg; // return the arg "as is" in case of error + } + } + } + + private formatError(error: Error): string { + if (error.stack && error.message && error.stack.indexOf(error.message) === -1) { + return "Error: " + error.message + "\n" + error.stack; + } else if (error.stack) { + return error.stack; + } else if (error["sourceURL"]) { + return error.message + "\n" + error["sourceURL"] + ":" + error["line"]; + } else { + return error.message; + } + } + + /** + * Returns the specified window console method if it exists (debug, warn, info, error, trace), + * otherwise returns console.log or empty function + * @param type Type of console to be used: info, debug, warn, error, trace + */ + protected getConsole(type: string): Function { + const console: any = window && window.console ? window.console : {}; + const logFn: Function = console[type] || console.log || _noop; + + return (...args: any[]): any => { + const consoleArgs: any[] = []; + for (const arg of args) { + if (arg instanceof Error) { + consoleArgs.push(this.formatError(arg)); + } else { + consoleArgs.push(arg); + } + } + return logFn.apply(console, consoleArgs); + }; + } +} diff --git a/packages/stark-core/src/util/index.ts b/packages/stark-core/src/util/index.ts index c8adfbf720..613b85f382 100644 --- a/packages/stark-core/src/util/index.ts +++ b/packages/stark-core/src/util/index.ts @@ -1,3 +1,4 @@ "use strict"; export * from "./url-util"; +export * from "./validation-errors.util"; diff --git a/packages/stark-core/src/util/validation-errors.util.spec.ts b/packages/stark-core/src/util/validation-errors.util.spec.ts new file mode 100644 index 0000000000..7311095c1d --- /dev/null +++ b/packages/stark-core/src/util/validation-errors.util.spec.ts @@ -0,0 +1,100 @@ +import { ArrayNotEmpty, IsNotEmpty, Matches, ValidateNested, validateSync, ValidationError } from "class-validator"; +import { StarkValidationErrorsUtil } from "./validation-errors.util"; + +describe("Util: ValidationErrorsUtil", () => { + const customIsNotEmptyMessage: string = "The property $property should not be empty"; + const customArrayNotEmptyMessage: string = "The array $property should not be empty"; + const customMatchesMessage: string = "The property $property should be exactly 2 lower case characters"; + + class LanguageClass { + @Matches(/^[a-z]{2}$/, { message: customMatchesMessage }) + public id: string; + + @IsNotEmpty({ message: customIsNotEmptyMessage }) + public description: string; + + public constructor(id: string, description: string) { + this.id = id; + this.description = description; + } + } + + class MyClass { + @IsNotEmpty({ message: customIsNotEmptyMessage }) + public name: string; + + @IsNotEmpty({ message: customIsNotEmptyMessage }) + public lastName: string; + + @ArrayNotEmpty({ message: customArrayNotEmptyMessage }) + @ValidateNested({ each: true }) // validate each item of the array + public languages: LanguageClass[] = []; + } + + let myClass: MyClass; + + beforeEach(() => { + myClass = new MyClass(); + }); + + describe("when there are validation errors", () => { + it("should throw an error", () => { + const errors: ValidationError[] = validateSync(myClass); + + expect(errors.length).toBe(3); + expect(() => StarkValidationErrorsUtil.throwOnError(errors)).toThrowError(); + }); + + it("should throw an error with a message containing the description of every validation error", () => { + const errorNameRegExp: RegExp = new RegExp(customIsNotEmptyMessage.replace("$property", "name"), "g"); + const errorLastNameRegExp: RegExp = new RegExp(customIsNotEmptyMessage.replace("$property", "lastName"), "g"); + const errorLanguagesRegExp: RegExp = new RegExp(customArrayNotEmptyMessage.replace("$property", "languages"), "g"); + + const errors: ValidationError[] = validateSync(myClass); + expect(errors.length).toBe(3); + + try { + StarkValidationErrorsUtil.throwOnError(errors); + } catch (e) { + expect(e.message).toContain("Validation errors"); + expect(e.message.match(errorNameRegExp).length).toBe(1); + expect(e.message.match(errorLastNameRegExp).length).toBe(1); + expect(e.message.match(errorLanguagesRegExp).length).toBe(1); + } + }); + + it("should throw an error with a message containing the full validation error tree", () => { + const errorIdRegExp: RegExp = new RegExp(customMatchesMessage.replace("$property", "id"), "g"); + const errorDescriptionRegExp: RegExp = new RegExp(customIsNotEmptyMessage.replace("$property", "description"), "g"); + + myClass.name = "valid name"; + myClass.lastName = "valid lastName"; + myClass.languages.push(new LanguageClass("eng", "English")); // invalid id + myClass.languages.push(new LanguageClass("french", "")); // invalid description + const errors: ValidationError[] = validateSync(myClass); + expect(errors.length).toBe(1); + expect(errors[0].children.length).toBe(myClass.languages.length); + + try { + StarkValidationErrorsUtil.throwOnError(errors); + } catch (e) { + expect(e.message).toContain("Validation errors"); + expect(e.message.match(errorIdRegExp).length).toBe(2); + expect(e.message.match(errorDescriptionRegExp).length).toBe(1); + } + }); + }); + + describe("when there are NO validation errors", () => { + it("should NOT throw", () => { + myClass.name = "valid name"; + myClass.lastName = "valid lastName"; + myClass.languages.push(new LanguageClass("en", "English")); + myClass.languages.push(new LanguageClass("fr", "French")); + const errors: ValidationError[] = validateSync(myClass); + + expect(errors.length).toBe(0); + expect(() => StarkValidationErrorsUtil.throwOnError(errors)).not.toThrowError(); + }); + }); +}); diff --git a/packages/stark-core/src/util/validation-errors.util.ts b/packages/stark-core/src/util/validation-errors.util.ts new file mode 100644 index 0000000000..20a441df6c --- /dev/null +++ b/packages/stark-core/src/util/validation-errors.util.ts @@ -0,0 +1,45 @@ +import { ValidationError } from "class-validator"; + +export class StarkValidationErrorsUtil { + /** + * Throws an error in case there are validation errors. The error contains the description of the different errors. + * @param validationErrors - Array containing validation results + * @param errorMessagePrefix - (Optional) A prefix to be added to the error message + * @throws Error + */ + public static throwOnError(validationErrors: ValidationError[], errorMessagePrefix?: string): void { + if (validationErrors.length) { + let validationMessage: string = errorMessagePrefix ? errorMessagePrefix + " " : ""; + validationMessage += "Validation errors:\n\n" + StarkValidationErrorsUtil.toString(validationErrors); + + throw new Error(validationMessage); + } + } + + /** + * Extracts the description of all the validation errors and generates a single string containing such descriptions + * @param validationErrors - Array containing validation results + * @param errorMessagePrefix - (Optional) A prefix to be added to the generated string + */ + public static toString(validationErrors: ValidationError[], errorMessagePrefix?: string): string { + let validationMessage: string = errorMessagePrefix ? errorMessagePrefix + "\n" : ""; + + if (validationErrors instanceof Array) { + for (const error of validationErrors) { + for (const constraint in error.constraints) { + if (error.constraints.hasOwnProperty(constraint)) { + validationMessage += "- " + error.constraints[constraint] + "\n"; + } + } + + if (error.children && error.children.length) { + validationMessage += error.property ? "-- " + error.property + ":" : ""; + // overriding the complete message (the current one is passed in the prefix param) + validationMessage = StarkValidationErrorsUtil.toString(error.children, validationMessage); + } + } + } + + return validationMessage; + } +} diff --git a/packages/stark-core/tsconfig.json b/packages/stark-core/tsconfig.json index 8cc970f378..97658f10c1 100644 --- a/packages/stark-core/tsconfig.json +++ b/packages/stark-core/tsconfig.json @@ -29,7 +29,7 @@ "allowEmptyCodegenFiles": false, "annotateForClosureCompiler": true, "skipTemplateCodegen": true, - "flatModuleOutFile": "stark-core.js", + // "flatModuleOutFile": "stark-core.js", "flatModuleId": "@nationalbankbelgium/stark-core" } }