From b5fb7e6e7fffba125c92ff42de0f8b0002c6358d Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 16 Aug 2022 14:42:03 -0400 Subject: [PATCH 01/42] WIP: ODP client shell --- .../lib/plugins/odp/odpClient.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 packages/optimizely-sdk/lib/plugins/odp/odpClient.ts diff --git a/packages/optimizely-sdk/lib/plugins/odp/odpClient.ts b/packages/optimizely-sdk/lib/plugins/odp/odpClient.ts new file mode 100644 index 000000000..885777759 --- /dev/null +++ b/packages/optimizely-sdk/lib/plugins/odp/odpClient.ts @@ -0,0 +1,30 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LogHandler } from '../../modules/logging'; + +export interface IOdpClient { + querySegments(apiKey: string, apiHost: string, userKey: string, userValue: string): string; +} + +export class OdpClient implements IOdpClient { + constructor(logger: LogHandler, client: unknown) { + } + + querySegments(apiKey: string, apiHost: string, userKey: string, userValue: string): string { + return ''; + } +} From 5dfb285b3714ab2670b26bc4f1c4f0fe902e452d Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Thu, 18 Aug 2022 11:45:31 -0400 Subject: [PATCH 02/42] Initial implementation of GraphQLManager --- .../lib/plugins/odp/graphqlManager.ts | 66 +++++++++++++++++++ .../lib/plugins/odp/odp_types.ts | 57 ++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 packages/optimizely-sdk/lib/plugins/odp/graphqlManager.ts create mode 100644 packages/optimizely-sdk/lib/plugins/odp/odp_types.ts diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphqlManager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphqlManager.ts new file mode 100644 index 000000000..362a27642 --- /dev/null +++ b/packages/optimizely-sdk/lib/plugins/odp/graphqlManager.ts @@ -0,0 +1,66 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConsoleLogHandler, LogHandler, LogLevel } from '../../modules/logging'; +import { Response } from './odp_types'; +import { IOdpClient, OdpClient } from './odpClient'; + +const QUALIFIED = 'qualified'; + +export interface IGraphQLManager { + fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string): string[]; +} + +export class GraphQLManager implements IGraphQLManager { + private readonly _logger: LogHandler; + private readonly _odpClient: IOdpClient; + + constructor(logger: LogHandler, client: IOdpClient) { + this._logger = logger ?? new ConsoleLogHandler(); + this._odpClient = client ?? new OdpClient(this._logger, client); + } + + public fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string): string[] { + const segmentsResponse = this._odpClient.querySegments(apiKey, apiHost, userKey, userValue); + if (!segmentsResponse) { + return [] as string[]; + } + + const parsedSegments = this._parseSegmentsResponseJson(segmentsResponse); + if (!parsedSegments) { + return [] as string[]; + } + + if (parsedSegments.errors?.length > 0) { + const errors = parsedSegments.errors.map((e) => e.message).join(';'); + + this._logger.log(LogLevel.WARNING, `Audience segments fetch failed (${errors})`); + + return [] as string[]; + } + + if (parsedSegments?.data?.customer?.audiences?.edges === null) { + this._logger.log(LogLevel.WARNING, 'Audience segments fetch failed (decode error)'); + + return [] as string[]; + } + + return parsedSegments.data.customer.audiences.edges.filter(edge => edge.node.state == QUALIFIED).map(edge => edge.node.name); + } + + private _parseSegmentsResponseJson = (jsonResponse: string): Response | undefined => + jsonResponse ? JSON.parse(jsonResponse) as Response : undefined; +} diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_types.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_types.ts new file mode 100644 index 000000000..7203c833b --- /dev/null +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_types.ts @@ -0,0 +1,57 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Response { + data: Data; + errors: Error[]; +} + +export interface Data { + customer: Customer; +} + +export interface Error { + message: string; + locations: Location[]; + path: string[]; + extensions: Extension; +} + +export interface Customer { + audiences: Audience; +} + +export interface Location { + line: number; + column: number; +} + +export interface Extension { + classification: string; +} + +export interface Audience { + edges: Edge[]; +} + +export interface Edge { + node: Node; +} + +export interface Node { + name: string; + state: string; +} From f0e4af895b32a14157196a27809f84ec4a054657 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Thu, 18 Aug 2022 13:29:46 -0400 Subject: [PATCH 03/42] Rename files --- .../lib/plugins/odp/graphql_manager.ts | 66 +++++++++++++++++++ .../lib/plugins/odp/odp_client.ts | 30 +++++++++ 2 files changed, 96 insertions(+) create mode 100644 packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts create mode 100644 packages/optimizely-sdk/lib/plugins/odp/odp_client.ts diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts new file mode 100644 index 000000000..95107c6d0 --- /dev/null +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -0,0 +1,66 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConsoleLogHandler, LogHandler, LogLevel } from '../../modules/logging'; +import { Response } from './odp_types'; +import { IOdpClient, OdpClient } from './odp_client'; + +const QUALIFIED = 'qualified'; + +export interface IGraphQLManager { + fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string): string[]; +} + +export class GraphqlManager implements IGraphQLManager { + private readonly _logger: LogHandler; + private readonly _odpClient: IOdpClient; + + constructor(logger: LogHandler, client: IOdpClient) { + this._logger = logger ?? new ConsoleLogHandler(); + this._odpClient = client ?? new OdpClient(this._logger, client); + } + + public fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string): string[] { + const segmentsResponse = this._odpClient.querySegments(apiKey, apiHost, userKey, userValue); + if (!segmentsResponse) { + return [] as string[]; + } + + const parsedSegments = this._parseSegmentsResponseJson(segmentsResponse); + if (!parsedSegments) { + return [] as string[]; + } + + if (parsedSegments.errors?.length > 0) { + const errors = parsedSegments.errors.map((e) => e.message).join(';'); + + this._logger.log(LogLevel.WARNING, `Audience segments fetch failed (${errors})`); + + return [] as string[]; + } + + if (parsedSegments?.data?.customer?.audiences?.edges === null) { + this._logger.log(LogLevel.WARNING, 'Audience segments fetch failed (decode error)'); + + return [] as string[]; + } + + return parsedSegments.data.customer.audiences.edges.filter(edge => edge.node.state == QUALIFIED).map(edge => edge.node.name); + } + + private _parseSegmentsResponseJson = (jsonResponse: string): Response | undefined => + jsonResponse ? JSON.parse(jsonResponse) as Response : undefined; +} diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts new file mode 100644 index 000000000..885777759 --- /dev/null +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -0,0 +1,30 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LogHandler } from '../../modules/logging'; + +export interface IOdpClient { + querySegments(apiKey: string, apiHost: string, userKey: string, userValue: string): string; +} + +export class OdpClient implements IOdpClient { + constructor(logger: LogHandler, client: unknown) { + } + + querySegments(apiKey: string, apiHost: string, userKey: string, userValue: string): string { + return ''; + } +} From d32b9caa8b5dfceeb6589fe97f4d9b7ef0db8365 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Thu, 18 Aug 2022 13:30:06 -0400 Subject: [PATCH 04/42] WIP schema definition for ODP --- .../lib/plugins/odp/odp_response_schema.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts new file mode 100644 index 000000000..d3a303bbd --- /dev/null +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { JSONSchema4 } from 'json-schema'; + +/** + * ODP Response JSON Schema file used to validate the project json datafile + */ +const definition = { + $schema: 'http://json-schema.org/draft-04/schema#', + type: 'object', + properties: { + data: { + type: 'object', + required: true, + }, + errors: { + type: 'object', + required: false, + }, + }, +}; + +export default definition as JSONSchema4 From 9e7cbe72337fc57721e81dc2aa0870ac5753dea5 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Thu, 18 Aug 2022 15:44:43 -0400 Subject: [PATCH 05/42] Remove renamed files --- .../lib/plugins/odp/graphqlManager.ts | 66 ------------------- .../lib/plugins/odp/odpClient.ts | 30 --------- 2 files changed, 96 deletions(-) delete mode 100644 packages/optimizely-sdk/lib/plugins/odp/graphqlManager.ts delete mode 100644 packages/optimizely-sdk/lib/plugins/odp/odpClient.ts diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphqlManager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphqlManager.ts deleted file mode 100644 index 362a27642..000000000 --- a/packages/optimizely-sdk/lib/plugins/odp/graphqlManager.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright 2022, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ConsoleLogHandler, LogHandler, LogLevel } from '../../modules/logging'; -import { Response } from './odp_types'; -import { IOdpClient, OdpClient } from './odpClient'; - -const QUALIFIED = 'qualified'; - -export interface IGraphQLManager { - fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string): string[]; -} - -export class GraphQLManager implements IGraphQLManager { - private readonly _logger: LogHandler; - private readonly _odpClient: IOdpClient; - - constructor(logger: LogHandler, client: IOdpClient) { - this._logger = logger ?? new ConsoleLogHandler(); - this._odpClient = client ?? new OdpClient(this._logger, client); - } - - public fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string): string[] { - const segmentsResponse = this._odpClient.querySegments(apiKey, apiHost, userKey, userValue); - if (!segmentsResponse) { - return [] as string[]; - } - - const parsedSegments = this._parseSegmentsResponseJson(segmentsResponse); - if (!parsedSegments) { - return [] as string[]; - } - - if (parsedSegments.errors?.length > 0) { - const errors = parsedSegments.errors.map((e) => e.message).join(';'); - - this._logger.log(LogLevel.WARNING, `Audience segments fetch failed (${errors})`); - - return [] as string[]; - } - - if (parsedSegments?.data?.customer?.audiences?.edges === null) { - this._logger.log(LogLevel.WARNING, 'Audience segments fetch failed (decode error)'); - - return [] as string[]; - } - - return parsedSegments.data.customer.audiences.edges.filter(edge => edge.node.state == QUALIFIED).map(edge => edge.node.name); - } - - private _parseSegmentsResponseJson = (jsonResponse: string): Response | undefined => - jsonResponse ? JSON.parse(jsonResponse) as Response : undefined; -} diff --git a/packages/optimizely-sdk/lib/plugins/odp/odpClient.ts b/packages/optimizely-sdk/lib/plugins/odp/odpClient.ts deleted file mode 100644 index 885777759..000000000 --- a/packages/optimizely-sdk/lib/plugins/odp/odpClient.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright 2022, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { LogHandler } from '../../modules/logging'; - -export interface IOdpClient { - querySegments(apiKey: string, apiHost: string, userKey: string, userValue: string): string; -} - -export class OdpClient implements IOdpClient { - constructor(logger: LogHandler, client: unknown) { - } - - querySegments(apiKey: string, apiHost: string, userKey: string, userValue: string): string { - return ''; - } -} From 1dedf29237d22777e04bcafafe8203dd641bdc7a Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Thu, 18 Aug 2022 15:45:05 -0400 Subject: [PATCH 06/42] ODP response json schema --- .../lib/plugins/odp/odp_response_schema.ts | 131 +++++++++++++++++- 1 file changed, 124 insertions(+), 7 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts index d3a303bbd..dfd96dc63 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts @@ -1,14 +1,14 @@ /** * Copyright 2022, Optimizely * - * Licensed under the Apache License, Version 2.0 (the "License"); + * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * distributed under the License is distributed on an 'AS IS' BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. @@ -20,18 +20,135 @@ import { JSONSchema4 } from 'json-schema'; * ODP Response JSON Schema file used to validate the project json datafile */ const definition = { - $schema: 'http://json-schema.org/draft-04/schema#', + $schema: 'https://json-schema.org/draft/2019-09/schema', + $id: 'http://example.com/example.json', + title: 'Root Schema', type: 'object', + default: {}, + required: [ ], properties: { data: { + title: 'The data Schema', type: 'object', - required: true, + default: {}, + required: [ + 'customer', + ], + properties: { + customer: { + title: 'The customer Schema', + type: 'object', + default: {}, + required: [ + 'audiences', + ], + properties: { + audiences: { + title: 'The audiences Schema', + type: 'object', + default: {}, + required: [ + 'edges', + ], + properties: { + edges: { + title: 'The edges Schema', + type: 'array', + default: [], + items: { + title: 'A Schema', + type: 'object', + required: [ + 'node', + ], + properties: { + node: { + title: 'The node Schema', + type: 'object', + required: [ + 'name', + 'state', + ], + properties: { + name: { + title: 'The name Schema', + type: 'string', + examples: [ + 'has_email', + 'has_email_opted_in', + ], + }, + state: { + title: 'The state Schema', + type: 'string', + examples: [ + 'qualified', + ], + }, + }, + examples: [], + }, + }, + examples: [], + }, + examples: [], + }, + }, + examples: [], + }, + }, + examples: [], + }, + }, + examples: [], }, errors: { - type: 'object', - required: false, + title: 'The errors Schema', + type: 'array', + default: [], + items: { + title: 'A Schema', + type: 'object', + default: {}, + required: [ + 'message', + 'extensions' + ], + properties: { + message: { + title: 'The message Schema', + type: 'string', + default: '', + examples: [ + 'Exception while fetching data (/customer) : java.lang.RuntimeException: could not resolve _fs_user_id = invalid-user' + ] + }, + extensions: { + title: 'The extensions Schema', + type: 'object', + default: {}, + required: [ + 'classification' + ], + properties: { + classification: { + title: 'The classification Schema', + type: 'string', + default: '', + examples: [ + 'InvalidIdentifierException' + ] + } + }, + examples: [] + } + }, + examples: [] + }, + examples: [] }, }, + examples: [], }; -export default definition as JSONSchema4 +export default definition as JSONSchema4; From 68327eceacf87f85a7b7ff07d19e782f91664355 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Thu, 18 Aug 2022 15:52:28 -0400 Subject: [PATCH 07/42] Add missing props to ODP response schema --- .../lib/plugins/odp/odp_response_schema.ts | 58 +++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts index dfd96dc63..735b8b7b8 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts @@ -22,7 +22,7 @@ import { JSONSchema4 } from 'json-schema'; const definition = { $schema: 'https://json-schema.org/draft/2019-09/schema', $id: 'http://example.com/example.json', - title: 'Root Schema', + title: 'ODP Response Schema', type: 'object', default: {}, required: [ ], @@ -39,9 +39,7 @@ const definition = { title: 'The customer Schema', type: 'object', default: {}, - required: [ - 'audiences', - ], + required: [ ], properties: { audiences: { title: 'The audiences Schema', @@ -112,6 +110,8 @@ const definition = { default: {}, required: [ 'message', + 'locations', + 'path', 'extensions' ], properties: { @@ -120,9 +120,57 @@ const definition = { type: 'string', default: '', examples: [ - 'Exception while fetching data (/customer) : java.lang.RuntimeException: could not resolve _fs_user_id = invalid-user' + 'Exception while fetching data (/customer) : java.lang.RuntimeException: could not resolve _fs_user_id = asdsdaddddd' ] }, + locations: { + title: 'The locations Schema', + type: 'array', + default: [], + items: { + title: 'A Schema', + type: 'object', + default: {}, + required: [ + 'line', + 'column' + ], + properties: { + line: { + title: 'The line Schema', + type: 'integer', + default: 0, + examples: [ + 2 + ] + }, + column: { + title: 'The column Schema', + type: 'integer', + default: 0, + examples: [ + 3 + ] + } + }, + examples: [] + }, + examples: [] + }, + path: { + title: 'The path Schema', + type: 'array', + default: [], + items: { + title: 'A Schema', + type: 'string', + default: '', + examples: [ + 'customer' + ] + }, + examples: [] + }, extensions: { title: 'The extensions Schema', type: 'object', From 0d3818def526c85cc5e4153231c2ffe9ea71493f Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Thu, 18 Aug 2022 16:14:22 -0400 Subject: [PATCH 08/42] Adjust schema validator to handle injected schemas --- .../project_config/project_config_schema.ts | 1 + .../lib/plugins/odp/odp_response_schema.ts | 8 ++--- .../lib/utils/json_schema_validator/index.ts | 33 ++++++++++++------- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/packages/optimizely-sdk/lib/core/project_config/project_config_schema.ts b/packages/optimizely-sdk/lib/core/project_config/project_config_schema.ts index 5830807a7..27fbb0aad 100644 --- a/packages/optimizely-sdk/lib/core/project_config/project_config_schema.ts +++ b/packages/optimizely-sdk/lib/core/project_config/project_config_schema.ts @@ -21,6 +21,7 @@ import { JSONSchema4 } from 'json-schema'; var schemaDefinition = { $schema: 'http://json-schema.org/draft-04/schema#', + title: 'Project Config JSON Schema', type: 'object', properties: { projectId: { diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts index 735b8b7b8..b4ec577cc 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts @@ -19,9 +19,9 @@ import { JSONSchema4 } from 'json-schema'; /** * ODP Response JSON Schema file used to validate the project json datafile */ -const definition = { +export const OdpResponseSchema = { $schema: 'https://json-schema.org/draft/2019-09/schema', - $id: 'http://example.com/example.json', + $id: 'https://example.com/example.json', title: 'ODP Response Schema', type: 'object', default: {}, @@ -197,6 +197,4 @@ const definition = { }, }, examples: [], -}; - -export default definition as JSONSchema4; +} as JSONSchema4; diff --git a/packages/optimizely-sdk/lib/utils/json_schema_validator/index.ts b/packages/optimizely-sdk/lib/utils/json_schema_validator/index.ts index 95e4c504f..10d4f88b5 100644 --- a/packages/optimizely-sdk/lib/utils/json_schema_validator/index.ts +++ b/packages/optimizely-sdk/lib/utils/json_schema_validator/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { sprintf } from '../fns'; -import { validate as jsonSchemaValidator } from 'json-schema'; +import { JSONSchema4, validate as jsonSchemaValidator } from 'json-schema'; import { ERROR_MESSAGES } from '../enums'; import schema from '../../core/project_config/project_config_schema'; @@ -23,23 +23,32 @@ const MODULE_NAME = 'JSON_SCHEMA_VALIDATOR'; /** * Validate the given json object against the specified schema - * @param {unknown} jsonObject The object to validate against the schema - * @return {boolean} true if the given object is valid + * @param {unknown} jsonObject The object to validate against the schema + * @param {JSONSchema4} validationSchema Provided schema to use for validation + * @param {boolean} shouldThrow Should validation throw if invalid JSON object + * @return {boolean} true if the given object is valid; throws or false if invalid */ -export function validate(jsonObject: unknown): boolean { +export function validate(jsonObject: unknown, validationSchema: JSONSchema4 = schema, shouldThrow = true): boolean { if (typeof jsonObject !== 'object' || jsonObject === null) { throw new Error(sprintf(ERROR_MESSAGES.NO_JSON_PROVIDED, MODULE_NAME)); } - const result = jsonSchemaValidator(jsonObject, schema); + const result = jsonSchemaValidator(jsonObject, validationSchema); if (result.valid) { return true; - } else { - if (Array.isArray(result.errors)) { - throw new Error( - sprintf(ERROR_MESSAGES.INVALID_DATAFILE, MODULE_NAME, result.errors[0].property, result.errors[0].message) - ); - } - throw new Error(sprintf(ERROR_MESSAGES.INVALID_JSON, MODULE_NAME)); } + + if (!shouldThrow) { + return false; + } + + const moduleTitle = `${MODULE_NAME} (${validationSchema.title})`; + + if (Array.isArray(result.errors)) { + throw new Error( + sprintf(ERROR_MESSAGES.INVALID_DATAFILE, moduleTitle, result.errors[0].property, result.errors[0].message), + ); + } + + throw new Error(sprintf(ERROR_MESSAGES.INVALID_JSON, moduleTitle)); } From d6ce18a33c57a9255f627a18997180fc26ec765c Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Thu, 18 Aug 2022 16:15:56 -0400 Subject: [PATCH 09/42] Implement schema validation for ODP JSON response --- .../lib/plugins/odp/graphql_manager.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index 95107c6d0..6917a969b 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -16,7 +16,9 @@ import { ConsoleLogHandler, LogHandler, LogLevel } from '../../modules/logging'; import { Response } from './odp_types'; -import { IOdpClient, OdpClient } from './odp_client'; +import { IOdpClient, NodeOdpClient } from './odp_client'; +import { validate } from '../../utils/json_schema_validator'; +import { OdpResponseSchema } from './odp_response_schema'; const QUALIFIED = 'qualified'; @@ -30,7 +32,7 @@ export class GraphqlManager implements IGraphQLManager { constructor(logger: LogHandler, client: IOdpClient) { this._logger = logger ?? new ConsoleLogHandler(); - this._odpClient = client ?? new OdpClient(this._logger, client); + this._odpClient = client ?? new NodeOdpClient(this._logger, client); } public fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string): string[] { @@ -61,6 +63,10 @@ export class GraphqlManager implements IGraphQLManager { return parsedSegments.data.customer.audiences.edges.filter(edge => edge.node.state == QUALIFIED).map(edge => edge.node.name); } - private _parseSegmentsResponseJson = (jsonResponse: string): Response | undefined => - jsonResponse ? JSON.parse(jsonResponse) as Response : undefined; + private _parseSegmentsResponseJson(jsonResponse: string): Response | undefined { + if (validate(jsonResponse, OdpResponseSchema, false)) { + return JSON.parse(jsonResponse) as Response; + } + return undefined; + } } From 6ee6cb942e5169a8c1138e61cb756c62e3d41490 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Thu, 18 Aug 2022 17:52:06 -0400 Subject: [PATCH 10/42] WIP Building ODP clients for browser and node --- .../lib/plugins/odp/graphql_manager.ts | 15 +++-- .../lib/plugins/odp/odp_client.ts | 58 ++++++++++++++-- .../plugins/odp/query_segments_parameters.ts | 66 +++++++++++++++++++ 3 files changed, 131 insertions(+), 8 deletions(-) create mode 100644 packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index 6917a969b..28d8fb4b7 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -16,14 +16,14 @@ import { ConsoleLogHandler, LogHandler, LogLevel } from '../../modules/logging'; import { Response } from './odp_types'; -import { IOdpClient, NodeOdpClient } from './odp_client'; +import { IOdpClient, NodeOdpClient, QuerySegmentsParameters } from './odp_client'; import { validate } from '../../utils/json_schema_validator'; import { OdpResponseSchema } from './odp_response_schema'; const QUALIFIED = 'qualified'; export interface IGraphQLManager { - fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string): string[]; + fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentToCheck: string[]): string[]; } export class GraphqlManager implements IGraphQLManager { @@ -35,8 +35,15 @@ export class GraphqlManager implements IGraphQLManager { this._odpClient = client ?? new NodeOdpClient(this._logger, client); } - public fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string): string[] { - const segmentsResponse = this._odpClient.querySegments(apiKey, apiHost, userKey, userValue); + public fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentToCheck: string[]): string[] { + const parameters = new QuerySegmentsParameters({ + ApiKey: apiKey, + ApiHost: apiHost, + UserKey: userKey, + UserValue: userValue, + SegmentToCheck: segmentToCheck, + }); + const segmentsResponse = this._odpClient.querySegments(parameters); if (!segmentsResponse) { return [] as string[]; } diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index 885777759..3d0ec939a 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -15,16 +15,66 @@ */ import { LogHandler } from '../../modules/logging'; +import https from 'https'; +import { QuerySegmentsParameters } from './query_segments_parameters'; export interface IOdpClient { - querySegments(apiKey: string, apiHost: string, userKey: string, userValue: string): string; + querySegments(parameters: QuerySegmentsParameters): string; } -export class OdpClient implements IOdpClient { - constructor(logger: LogHandler, client: unknown) { +export class NodeOdpClient implements IOdpClient { + private readonly _logger: LogHandler; + + constructor(logger: LogHandler) { + this._logger = logger; + } + + public querySegments(parameters: QuerySegmentsParameters): string { + const data = JSON.stringify({ + todo: 'Buy the milk', + }); + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': data.length, + 'x-api-key': apiKey, + }, + }; + + const req = https.request(apiHost, options, res => { + console.log(`statusCode: ${res.statusCode}`); + + res.on('data', d => { + process.stdout.write(d); + }); + }); + + req.on('error', error => { + console.error(error); + }); + + req.write(data); + req.end(); + + return ''; + } + + private BuildRequestMessage(jsonQuery: string, parameters: QuerySegmentsParameters): unknown { + // TODO: Implement + return undefined; + } +} + +export class BrowserOdpClient implements IOdpClient { + private readonly _logger: LogHandler; + + constructor(logger: LogHandler) { + this._logger = logger; } - querySegments(apiKey: string, apiHost: string, userKey: string, userValue: string): string { + public querySegments(parameters: QuerySegmentsParameters): string { return ''; } } + diff --git a/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts b/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts new file mode 100644 index 000000000..03ab0cae9 --- /dev/null +++ b/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts @@ -0,0 +1,66 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Handles parameters used in querying ODP segments + */ +export class QuerySegmentsParameters { + /** + * Optimizely Data Platform API key + */ + public ApiKey: string | undefined; + + /** + * Fully-qualified URL to ODP endpoint + */ + public ApiHost: string | undefined; + + /** + * 'vuid' or 'fs_user_id' (client device id or fullstack id) + */ + public UserKey: string | undefined; + + /** + * Value for the user key + */ + public UserValue: string | undefined; + + /** + * Audience segments to check for inclusion in the experiment + */ + public SegmentToCheck: string[] | undefined; + + constructor(parameters: { UserValue: string; ApiKey: string; UserKey: string; SegmentToCheck: string[]; ApiHost: string }) { + Object.assign(this, parameters); + } + + /** + * Converts the QuerySegmentsParameters into JSON + * @returns GraphQL JSON payload + */ + public ToJson(): string { + const segmentsArrayJson = JSON.stringify(this.SegmentToCheck); + + const json: string[] = []; + json.push('{"query" : "query {customer"'); + json.push(`(${this.UserKey} : "${this.UserValue}") `); + json.push('{audiences'); + json.push(`(subset: ${segmentsArrayJson})`); + json.push('{edges {node {name state}}}}}"}'); + + return json.join(''); + } +} From c1328264df08c608a9ab42772d51012b4d04f654 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 19 Aug 2022 11:48:09 -0400 Subject: [PATCH 11/42] Use Axios to support browser and node ODP calls --- .../lib/plugins/odp/graphql_manager.ts | 11 +-- .../lib/plugins/odp/odp_client.ts | 77 +++++++------------ packages/optimizely-sdk/package-lock.json | 67 +++++++++++----- packages/optimizely-sdk/package.json | 5 +- 4 files changed, 86 insertions(+), 74 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index 28d8fb4b7..67204570b 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -16,14 +16,15 @@ import { ConsoleLogHandler, LogHandler, LogLevel } from '../../modules/logging'; import { Response } from './odp_types'; -import { IOdpClient, NodeOdpClient, QuerySegmentsParameters } from './odp_client'; +import { IOdpClient, OdpClient } from './odp_client'; import { validate } from '../../utils/json_schema_validator'; import { OdpResponseSchema } from './odp_response_schema'; +import { QuerySegmentsParameters } from './query_segments_parameters'; const QUALIFIED = 'qualified'; export interface IGraphQLManager { - fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentToCheck: string[]): string[]; + fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentToCheck: string[]): Promise; } export class GraphqlManager implements IGraphQLManager { @@ -32,10 +33,10 @@ export class GraphqlManager implements IGraphQLManager { constructor(logger: LogHandler, client: IOdpClient) { this._logger = logger ?? new ConsoleLogHandler(); - this._odpClient = client ?? new NodeOdpClient(this._logger, client); + this._odpClient = client ?? new OdpClient(this._logger); } - public fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentToCheck: string[]): string[] { + public async fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentToCheck: string[]): Promise { const parameters = new QuerySegmentsParameters({ ApiKey: apiKey, ApiHost: apiHost, @@ -43,7 +44,7 @@ export class GraphqlManager implements IGraphQLManager { UserValue: userValue, SegmentToCheck: segmentToCheck, }); - const segmentsResponse = this._odpClient.querySegments(parameters); + const segmentsResponse = await this._odpClient.querySegments(parameters); if (!segmentsResponse) { return [] as string[]; } diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index 3d0ec939a..cd5954b5c 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -14,67 +14,46 @@ * limitations under the License. */ -import { LogHandler } from '../../modules/logging'; -import https from 'https'; +import { LogHandler, LogLevel } from '../../modules/logging'; import { QuerySegmentsParameters } from './query_segments_parameters'; +import axios from 'axios'; export interface IOdpClient { - querySegments(parameters: QuerySegmentsParameters): string; + querySegments(parameters: QuerySegmentsParameters): Promise; } -export class NodeOdpClient implements IOdpClient { +export class OdpClient implements IOdpClient { private readonly _logger: LogHandler; constructor(logger: LogHandler) { this._logger = logger; } - public querySegments(parameters: QuerySegmentsParameters): string { - const data = JSON.stringify({ - todo: 'Buy the milk', - }); - const options = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Content-Length': data.length, - 'x-api-key': apiKey, - }, - }; - - const req = https.request(apiHost, options, res => { - console.log(`statusCode: ${res.statusCode}`); - - res.on('data', d => { - process.stdout.write(d); + public async querySegments(parameters: QuerySegmentsParameters): Promise { + const data = parameters.ToJson(); + + if (!parameters?.ApiHost || !parameters?.ApiKey) { + this._logger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments'); + return; + } + + const response = await axios(parameters.ApiHost, + { + method: 'post', + url: parameters.ApiHost, + headers: { + 'Content-Type': 'application/json', + 'Content-Length': data.length, + 'x-api-key': parameters.ApiKey, + }, + data, }); - }); - - req.on('error', error => { - console.error(error); - }); - - req.write(data); - req.end(); - - return ''; - } - - private BuildRequestMessage(jsonQuery: string, parameters: QuerySegmentsParameters): unknown { - // TODO: Implement - return undefined; - } -} - -export class BrowserOdpClient implements IOdpClient { - private readonly _logger: LogHandler; - - constructor(logger: LogHandler) { - this._logger = logger; - } - public querySegments(parameters: QuerySegmentsParameters): string { - return ''; + if (response.status !== 200) { + this._logger.log(LogLevel.ERROR, `Error while querying segments. Response (${response.status}): ${response.statusText}.`); + return; + } + + return response.data; } } - diff --git a/packages/optimizely-sdk/package-lock.json b/packages/optimizely-sdk/package-lock.json index fadc7ed7e..eccda8203 100644 --- a/packages/optimizely-sdk/package-lock.json +++ b/packages/optimizely-sdk/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@optimizely/js-sdk-datafile-manager": "^0.9.5", - "@optimizely/js-sdk-event-processor": "^0.9.2", + "axios": "^0.27.2", "json-schema": "^0.4.0", "murmurhash": "^2.0.1", "uuid": "^8.3.2" @@ -5415,8 +5415,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/atob": { "version": "2.1.2", @@ -5445,6 +5444,28 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, + "node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -6821,7 +6842,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -7421,7 +7441,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -8930,7 +8949,6 @@ "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", - "dev": true, "funding": [ { "type": "individual", @@ -14532,7 +14550,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "devOptional": true, "engines": { "node": ">= 0.6" } @@ -14541,7 +14558,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "devOptional": true, "dependencies": { "mime-db": "1.52.0" }, @@ -24535,8 +24551,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "atob": { "version": "2.1.2", @@ -24556,6 +24571,27 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, + "axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "requires": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -25683,7 +25719,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -26179,8 +26214,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "denodeify": { "version": "1.2.1", @@ -27350,8 +27384,7 @@ "follow-redirects": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", - "dev": true + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" }, "for-each": { "version": "0.3.3", @@ -31832,14 +31865,12 @@ "mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "devOptional": true + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, "mime-types": { "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "devOptional": true, "requires": { "mime-db": "1.52.0" } diff --git a/packages/optimizely-sdk/package.json b/packages/optimizely-sdk/package.json index 028414316..c4dec303b 100644 --- a/packages/optimizely-sdk/package.json +++ b/packages/optimizely-sdk/package.json @@ -41,6 +41,7 @@ "homepage": "https://github.com/optimizely/javascript-sdk/tree/master/packages/optimizely-sdk", "dependencies": { "@optimizely/js-sdk-datafile-manager": "^0.9.5", + "axios": "^0.27.2", "json-schema": "^0.4.0", "murmurhash": "^2.0.1", "uuid": "^8.3.2" @@ -87,9 +88,9 @@ "webpack": "^5.74.0" }, "peerDependencies": { - "@react-native-community/netinfo": "5.9.4", + "@babel/runtime": "^7.0.0", "@react-native-async-storage/async-storage": "^1.2.0", - "@babel/runtime": "^7.0.0" + "@react-native-community/netinfo": "5.9.4" }, "peerDependenciesMeta": { "@react-native-async-storage/async-storage": { From a903a7021cb4dfe21c82fad5f2cbbbc662498cf9 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 19 Aug 2022 17:26:27 -0400 Subject: [PATCH 12/42] WIP GraphQLManager testing --- .../lib/plugins/odp/graphql_manager.ts | 18 +- .../lib/plugins/odp/odp_client.ts | 10 +- .../lib/plugins/odp/odp_response_schema.ts | 4 +- .../lib/utils/json_schema_validator/index.ts | 6 +- .../tests/graphQLManager.spec.ts | 211 ++++++++++++++++++ 5 files changed, 236 insertions(+), 13 deletions(-) create mode 100644 packages/optimizely-sdk/tests/graphQLManager.spec.ts diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index 67204570b..52493cd1d 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -49,8 +49,9 @@ export class GraphqlManager implements IGraphQLManager { return [] as string[]; } - const parsedSegments = this._parseSegmentsResponseJson(segmentsResponse); + const parsedSegments = this.parseSegmentsResponseJson(segmentsResponse); if (!parsedSegments) { + this._logger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)'); return [] as string[]; } @@ -71,10 +72,17 @@ export class GraphqlManager implements IGraphQLManager { return parsedSegments.data.customer.audiences.edges.filter(edge => edge.node.state == QUALIFIED).map(edge => edge.node.name); } - private _parseSegmentsResponseJson(jsonResponse: string): Response | undefined { - if (validate(jsonResponse, OdpResponseSchema, false)) { - return JSON.parse(jsonResponse) as Response; + private parseSegmentsResponseJson(jsonResponse: string): Response | undefined { + let jsonObject = {}; + try { + jsonObject = JSON.parse(jsonResponse); + } catch { + this._logger.log(LogLevel.ERROR, 'Attempted to parse invalid segment response JSON.'); + return; } - return undefined; + if (validate(jsonObject, OdpResponseSchema, false)) { + return jsonObject as Response; + } + return; } } diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index cd5954b5c..1994e076f 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -16,7 +16,7 @@ import { LogHandler, LogLevel } from '../../modules/logging'; import { QuerySegmentsParameters } from './query_segments_parameters'; -import axios from 'axios'; +import axios, { AxiosResponse } from 'axios'; export interface IOdpClient { querySegments(parameters: QuerySegmentsParameters): Promise; @@ -47,13 +47,15 @@ export class OdpClient implements IOdpClient { 'x-api-key': parameters.ApiKey, }, data, - }); + }).catch(() => { + this._logger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)'); + }) as AxiosResponse; if (response.status !== 200) { - this._logger.log(LogLevel.ERROR, `Error while querying segments. Response (${response.status}): ${response.statusText}.`); + this._logger.log(LogLevel.ERROR, `Audience segments fetch failed (${response.status})`); return; } - + return response.data; } } diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts index b4ec577cc..e7e16e56a 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts @@ -25,7 +25,9 @@ export const OdpResponseSchema = { title: 'ODP Response Schema', type: 'object', default: {}, - required: [ ], + required: [ + 'data', + ], properties: { data: { title: 'The data Schema', diff --git a/packages/optimizely-sdk/lib/utils/json_schema_validator/index.ts b/packages/optimizely-sdk/lib/utils/json_schema_validator/index.ts index 10d4f88b5..1e44e9e85 100644 --- a/packages/optimizely-sdk/lib/utils/json_schema_validator/index.ts +++ b/packages/optimizely-sdk/lib/utils/json_schema_validator/index.ts @@ -29,8 +29,10 @@ const MODULE_NAME = 'JSON_SCHEMA_VALIDATOR'; * @return {boolean} true if the given object is valid; throws or false if invalid */ export function validate(jsonObject: unknown, validationSchema: JSONSchema4 = schema, shouldThrow = true): boolean { + const moduleTitle = `${MODULE_NAME} (${validationSchema.title})`; + if (typeof jsonObject !== 'object' || jsonObject === null) { - throw new Error(sprintf(ERROR_MESSAGES.NO_JSON_PROVIDED, MODULE_NAME)); + throw new Error(sprintf(ERROR_MESSAGES.NO_JSON_PROVIDED, moduleTitle)); } const result = jsonSchemaValidator(jsonObject, validationSchema); @@ -42,8 +44,6 @@ export function validate(jsonObject: unknown, validationSchema: JSONSchema4 = sc return false; } - const moduleTitle = `${MODULE_NAME} (${validationSchema.title})`; - if (Array.isArray(result.errors)) { throw new Error( sprintf(ERROR_MESSAGES.INVALID_DATAFILE, moduleTitle, result.errors[0].property, result.errors[0].message), diff --git a/packages/optimizely-sdk/tests/graphQLManager.spec.ts b/packages/optimizely-sdk/tests/graphQLManager.spec.ts new file mode 100644 index 000000000..3ec2a8613 --- /dev/null +++ b/packages/optimizely-sdk/tests/graphQLManager.spec.ts @@ -0,0 +1,211 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/// + +import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; +import { IOdpClient, OdpClient } from '../lib/plugins/odp/odp_client'; +import { LogHandler, LogLevel } from '../lib/modules/logging'; +import { GraphqlManager } from '../lib/plugins/odp/graphql_manager'; +import { Response } from '../lib/plugins/odp/odp_types'; + +describe('GraphQLManager', () => { + const VALID_ODP_PUBLIC_KEY = 'W4WzcEs-ABgXorzY7h1LCQ'; + const ODP_GRAPHQL_URL = 'https://api.zaius.com/v3/graphql'; + const FS_USER_ID = 'fs_user_id'; + const VALID_FS_USER_ID = 'tester-101'; + const SEGMENTS_TO_CHECK = [ + 'has_email', + 'has_email_opted_in', + 'push_on_sale', + ]; + + let mockLogger: LogHandler; + let mockOdpClient: IOdpClient; + + beforeAll(() => { + mockLogger = mock(); + mockOdpClient = mock(); + }); + + beforeEach(() => { + resetCalls(mockLogger); + resetCalls(mockOdpClient); + }); + + it('should parse a successful response', () => { + const validJsonResponse = `{ + "data": { + "customer": { + "audiences": { + "edges": [ + { + "node": { + "name": "has_email", + "state": "qualified" + } + }, + { + "node": { + "name": "has_email_opted_in", + "state": "not-qualified" + } + } + ] + } + } + } + }`; + const manager = new GraphqlManager(instance(mockLogger), instance(mockOdpClient)); + + const response = manager['parseSegmentsResponseJson'](validJsonResponse) as Response; + + expect(response).not.toBeUndefined(); + expect(response.errors.length).toEqual(0); + expect(response?.data?.customer?.audiences?.edges).not.toBeNull(); + expect(response.data.customer.audiences.edges.length).toEqual(2); + let node = response.data.customer.audiences.edges[0].node; + expect(node.name).toEqual('has_email'); + expect(node.state).toEqual('qualified'); + node = response.data.customer.audiences.edges[1].node; + expect(node.name).toEqual('has_email_opted_in'); + expect(node.state).not.toEqual('qualified'); + }); + + it('should parse an error response', () => { + const errorJsonResponse = `{ + "errors": [ + { + "message": "Exception while fetching data (/customer) : java.lang.RuntimeException: could not resolve _fs_user_id = asdsdaddddd", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "customer" + ], + "extensions": { + "classification": "InvalidIdentifierException" + } + } + ], + "data": { + "customer": null + } +}`; + const manager = new GraphqlManager(instance(mockLogger), instance(mockOdpClient)); + + const response = manager['parseSegmentsResponseJson'](errorJsonResponse) as Response; + + expect(response).not.toBeUndefined(); + expect(response.data.customer).toBeNull(); + expect(response?.errors).not.toBeNull(); + expect(response.errors[0].extensions.classification).toEqual('InvalidIdentifierException'); + }); + + it('should fetch valid qualified segments', async () => { + const responseJsonWithQualifiedSegments = '{"data":{"customer":{"audiences":' + + '{"edges":[{"node":{"name":"has_email",' + + '"state":"qualified"}},{"node":{"name":' + + '"has_email_opted_in","state":"qualified"}}]}}}}'; + when(mockOdpClient.querySegments(anything())).thenResolve(responseJsonWithQualifiedSegments); + const manager = new GraphqlManager(instance(mockLogger), instance(mockOdpClient)); + + const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); + + expect(segments.length).toEqual(2); + expect(segments).toContain('has_email'); + expect(segments).toContain('has_email_opted_in'); + verify(mockLogger.log(anything(), anyString())).never(); + }); + + it('should handle empty qualified segments', async () => { + const responseJsonWithNoQualifiedSegments = '{"data":{"customer":{"audiences":' + + '{"edges":[ ]}}}}'; + when(mockOdpClient.querySegments(anything())).thenResolve(responseJsonWithNoQualifiedSegments); + const manager = new GraphqlManager(instance(mockLogger), instance(mockOdpClient)); + + const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); + + expect(segments.length).toEqual(0); + verify(mockLogger.log(anything(), anyString())).never(); + }); + + it('should handle error with invalid identifier', async () => { + const INVALID_USER_ID = 'invalid-user'; + const errorJsonResponse = '{"errors":[{"message":' + + '"Exception while fetching data (/customer) : ' + + `java.lang.RuntimeException: could not resolve _fs_user_id = ${INVALID_USER_ID}",` + + '"locations":[{"line":1,"column":8}],"path":["customer"],' + + '"extensions":{"classification":"DataFetchingException"}}],' + + '"data":{"customer":null}}'; + when(mockOdpClient.querySegments(anything())).thenResolve(errorJsonResponse); + const manager = new GraphqlManager(instance(mockLogger), instance(mockOdpClient)); + + const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, INVALID_USER_ID, SEGMENTS_TO_CHECK); + + expect(segments.length).toEqual(0); + verify(mockLogger.log(anything(), anyString())).once(); + }); + + it('should handle unrecognized JSON responses', async () => { + const unrecognizedJson = '{"unExpectedObject":{ "withSome": "value", "thatIsNotParseable": "true" }}'; + when(mockOdpClient.querySegments(anything())).thenResolve(unrecognizedJson); + const manager = new GraphqlManager(instance(mockLogger), instance(mockOdpClient)); + + const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); + + expect(segments.length).toEqual(0); + verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)')).once(); + }); + + it('should handle other exception types', async () => { + const errorJsonResponse = '{"errors":[{"message":"Validation error of type ' + + 'UnknownArgument: Unknown field argument not_real_userKey @ ' + + '\'customer\'","locations":[{"line":1,"column":17}],' + + '"extensions":{"classification":"ValidationError"}}]}'; + when(mockOdpClient.querySegments(anything())).thenResolve(errorJsonResponse); + const manager = new GraphqlManager(instance(mockLogger), instance(mockOdpClient)); + + const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); + + expect(segments.length).toEqual(0); + verify(mockLogger.log(anything(), anyString())).once(); + }); + + it('should handle bad responses', async () => { + const badResponse = '{"data":{ }}'; + when(mockOdpClient.querySegments(anything())).thenResolve(badResponse); + const manager = new GraphqlManager(instance(mockLogger), instance(mockOdpClient)); + + const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); + + expect(segments.length).toEqual(0); + verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)')).once(); + + }); + + it('should handle 400 HTTP status code response', async () => { + // TODO: implement + }); + + it('should handle 500 HTTP status code response', async () => { + // TODO: implement + }); +}); + From 2c7b898182dd4ff071155e60582e3e556a5bfd5b Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 19 Aug 2022 17:43:54 -0400 Subject: [PATCH 13/42] Corrected ODP response schema --- .../lib/plugins/odp/odp_response_schema.ts | 16 ---------------- .../optimizely-sdk/tests/graphQLManager.spec.ts | 3 +-- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts index e7e16e56a..98a16b4c3 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_response_schema.ts @@ -24,7 +24,6 @@ export const OdpResponseSchema = { $id: 'https://example.com/example.json', title: 'ODP Response Schema', type: 'object', - default: {}, required: [ 'data', ], @@ -32,7 +31,6 @@ export const OdpResponseSchema = { data: { title: 'The data Schema', type: 'object', - default: {}, required: [ 'customer', ], @@ -40,13 +38,11 @@ export const OdpResponseSchema = { customer: { title: 'The customer Schema', type: 'object', - default: {}, required: [ ], properties: { audiences: { title: 'The audiences Schema', type: 'object', - default: {}, required: [ 'edges', ], @@ -54,7 +50,6 @@ export const OdpResponseSchema = { edges: { title: 'The edges Schema', type: 'array', - default: [], items: { title: 'A Schema', type: 'object', @@ -109,18 +104,15 @@ export const OdpResponseSchema = { items: { title: 'A Schema', type: 'object', - default: {}, required: [ 'message', 'locations', - 'path', 'extensions' ], properties: { message: { title: 'The message Schema', type: 'string', - default: '', examples: [ 'Exception while fetching data (/customer) : java.lang.RuntimeException: could not resolve _fs_user_id = asdsdaddddd' ] @@ -128,11 +120,9 @@ export const OdpResponseSchema = { locations: { title: 'The locations Schema', type: 'array', - default: [], items: { title: 'A Schema', type: 'object', - default: {}, required: [ 'line', 'column' @@ -141,7 +131,6 @@ export const OdpResponseSchema = { line: { title: 'The line Schema', type: 'integer', - default: 0, examples: [ 2 ] @@ -149,7 +138,6 @@ export const OdpResponseSchema = { column: { title: 'The column Schema', type: 'integer', - default: 0, examples: [ 3 ] @@ -162,11 +150,9 @@ export const OdpResponseSchema = { path: { title: 'The path Schema', type: 'array', - default: [], items: { title: 'A Schema', type: 'string', - default: '', examples: [ 'customer' ] @@ -176,7 +162,6 @@ export const OdpResponseSchema = { extensions: { title: 'The extensions Schema', type: 'object', - default: {}, required: [ 'classification' ], @@ -184,7 +169,6 @@ export const OdpResponseSchema = { classification: { title: 'The classification Schema', type: 'string', - default: '', examples: [ 'InvalidIdentifierException' ] diff --git a/packages/optimizely-sdk/tests/graphQLManager.spec.ts b/packages/optimizely-sdk/tests/graphQLManager.spec.ts index 3ec2a8613..7b52622fc 100644 --- a/packages/optimizely-sdk/tests/graphQLManager.spec.ts +++ b/packages/optimizely-sdk/tests/graphQLManager.spec.ts @@ -74,7 +74,7 @@ describe('GraphQLManager', () => { const response = manager['parseSegmentsResponseJson'](validJsonResponse) as Response; expect(response).not.toBeUndefined(); - expect(response.errors.length).toEqual(0); + expect(response?.errors?.length).toEqual(0); expect(response?.data?.customer?.audiences?.edges).not.toBeNull(); expect(response.data.customer.audiences.edges.length).toEqual(2); let node = response.data.customer.audiences.edges[0].node; @@ -197,7 +197,6 @@ describe('GraphQLManager', () => { expect(segments.length).toEqual(0); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)')).once(); - }); it('should handle 400 HTTP status code response', async () => { From 4162506016a1e140904509df316462bdb710b0d1 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 19 Aug 2022 17:57:04 -0400 Subject: [PATCH 14/42] Handle all non-200 HTTP statuses --- .../lib/plugins/odp/graphql_manager.ts | 1 + packages/optimizely-sdk/tests/graphQLManager.spec.ts | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index 52493cd1d..d14ea0205 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -46,6 +46,7 @@ export class GraphqlManager implements IGraphQLManager { }); const segmentsResponse = await this._odpClient.querySegments(parameters); if (!segmentsResponse) { + this._logger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)'); return [] as string[]; } diff --git a/packages/optimizely-sdk/tests/graphQLManager.spec.ts b/packages/optimizely-sdk/tests/graphQLManager.spec.ts index 7b52622fc..806bba77b 100644 --- a/packages/optimizely-sdk/tests/graphQLManager.spec.ts +++ b/packages/optimizely-sdk/tests/graphQLManager.spec.ts @@ -199,12 +199,14 @@ describe('GraphQLManager', () => { verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)')).once(); }); - it('should handle 400 HTTP status code response', async () => { - // TODO: implement - }); + it('should handle non 200 HTTP status code response', async () => { + when(mockOdpClient.querySegments(anything())).thenResolve(undefined); + const manager = new GraphqlManager(instance(mockLogger), instance(mockOdpClient)); - it('should handle 500 HTTP status code response', async () => { - // TODO: implement + const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); + + expect(segments.length).toEqual(0); + verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)')).once(); }); }); From 37003447648b3914b60bc0200bbfc4d156fc0386 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 19 Aug 2022 18:06:08 -0400 Subject: [PATCH 15/42] WIP ODP client test starts --- .../optimizely-sdk/tests/odpClient.spec.ts | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 packages/optimizely-sdk/tests/odpClient.spec.ts diff --git a/packages/optimizely-sdk/tests/odpClient.spec.ts b/packages/optimizely-sdk/tests/odpClient.spec.ts new file mode 100644 index 000000000..0e3847c47 --- /dev/null +++ b/packages/optimizely-sdk/tests/odpClient.spec.ts @@ -0,0 +1,63 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/// + +import { anyString, anything, mock, resetCalls, verify } from 'ts-mockito'; +import { LogHandler, LogLevel } from '../lib/modules/logging'; + +describe('OdpClient', () => { + let mockLogger: LogHandler; + + beforeAll(() => { + mockLogger = mock(); + }); + + beforeEach(() => { + resetCalls(mockLogger); + }); + + it('should segments successfully', () => { + + verify(mockLogger.log(anything(), anyString())).never(); + }); + + it('should handle missing API Host', () => { + + verify(mockLogger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments')).once(); + }); + + it('should handle missing API Key', () => { + + verify(mockLogger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments')).once(); + }); + + it('should handle 400 HTTP response', () => { + + verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (400)')).once(); + }); + + it('should handle 500 HTTP response', () => { + + verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (500)')).once(); + }); + + it('should other types of unsuccessful HTTP responses', () => { + + verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)')).once(); + }); +}); + From 562581bf3d046d5c45e08b1319fbd5e16a2c158b Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 22 Aug 2022 10:49:53 -0400 Subject: [PATCH 16/42] Add ODP tests --- .../lib/plugins/odp/graphql_manager.ts | 2 +- .../lib/plugins/odp/odp_client.ts | 37 +++---- .../plugins/odp/query_segments_parameters.ts | 2 +- packages/optimizely-sdk/package-lock.json | 55 +++++++++++ packages/optimizely-sdk/package.json | 1 + .../optimizely-sdk/tests/odpClient.spec.ts | 97 +++++++++++++++++-- 6 files changed, 167 insertions(+), 27 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index d14ea0205..e1950f689 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -42,7 +42,7 @@ export class GraphqlManager implements IGraphQLManager { ApiHost: apiHost, UserKey: userKey, UserValue: userValue, - SegmentToCheck: segmentToCheck, + SegmentsToCheck: segmentToCheck, }); const segmentsResponse = await this._odpClient.querySegments(parameters); if (!segmentsResponse) { diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index 1994e076f..d5f513166 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -16,7 +16,7 @@ import { LogHandler, LogLevel } from '../../modules/logging'; import { QuerySegmentsParameters } from './query_segments_parameters'; -import axios, { AxiosResponse } from 'axios'; +import axios, { AxiosError, AxiosResponse } from 'axios'; export interface IOdpClient { querySegments(parameters: QuerySegmentsParameters): Promise; @@ -37,22 +37,25 @@ export class OdpClient implements IOdpClient { return; } - const response = await axios(parameters.ApiHost, - { - method: 'post', - url: parameters.ApiHost, - headers: { - 'Content-Type': 'application/json', - 'Content-Length': data.length, - 'x-api-key': parameters.ApiKey, - }, - data, - }).catch(() => { - this._logger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)'); - }) as AxiosResponse; - - if (response.status !== 200) { - this._logger.log(LogLevel.ERROR, `Audience segments fetch failed (${response.status})`); + const fetchFailureMessage = 'Audience segments fetch failed'; + + let response: AxiosResponse; + + try { + response = await axios(parameters.ApiHost, + { + method: 'post', + url: parameters.ApiHost, + headers: { + 'Content-Type': 'application/json', + 'Content-Length': data.length, + 'x-api-key': parameters.ApiKey, + }, + data, + }); + } catch (error) { + const response = (error as AxiosError).response; + this._logger.log(LogLevel.ERROR, `${fetchFailureMessage} (${response?.status ?? 'network error'})`); return; } diff --git a/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts b/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts index 03ab0cae9..483bf48c8 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts @@ -43,7 +43,7 @@ export class QuerySegmentsParameters { */ public SegmentToCheck: string[] | undefined; - constructor(parameters: { UserValue: string; ApiKey: string; UserKey: string; SegmentToCheck: string[]; ApiHost: string }) { + constructor(parameters: { UserValue: string; ApiKey: string; UserKey: string; SegmentsToCheck: string[]; ApiHost: string }) { Object.assign(this, parameters); } diff --git a/packages/optimizely-sdk/package-lock.json b/packages/optimizely-sdk/package-lock.json index eccda8203..5c98f6a57 100644 --- a/packages/optimizely-sdk/package-lock.json +++ b/packages/optimizely-sdk/package-lock.json @@ -26,6 +26,7 @@ "@types/uuid": "^3.4.4", "@typescript-eslint/eslint-plugin": "^5.33.0", "@typescript-eslint/parser": "^5.33.0", + "axios-mock-adapter": "^1.21.2", "bluebird": "^3.4.6", "chai": "^4.2.0", "coveralls": "^3.0.2", @@ -5453,6 +5454,42 @@ "form-data": "^4.0.0" } }, + "node_modules/axios-mock-adapter": { + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.21.2.tgz", + "integrity": "sha512-jzyNxU3JzB2XVhplZboUcF0YDs7xuExzoRSHXPHr+UQajaGmcTqvkkUADgkVI2WkGlpZ1zZlMVdcTMU0ejV8zQ==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "is-buffer": "^2.0.5" + }, + "peerDependencies": { + "axios": ">= 0.17.0" + } + }, + "node_modules/axios-mock-adapter/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, "node_modules/axios/node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -24592,6 +24629,24 @@ } } }, + "axios-mock-adapter": { + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.21.2.tgz", + "integrity": "sha512-jzyNxU3JzB2XVhplZboUcF0YDs7xuExzoRSHXPHr+UQajaGmcTqvkkUADgkVI2WkGlpZ1zZlMVdcTMU0ejV8zQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "is-buffer": "^2.0.5" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true + } + } + }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", diff --git a/packages/optimizely-sdk/package.json b/packages/optimizely-sdk/package.json index c4dec303b..00698cb26 100644 --- a/packages/optimizely-sdk/package.json +++ b/packages/optimizely-sdk/package.json @@ -57,6 +57,7 @@ "@types/uuid": "^3.4.4", "@typescript-eslint/eslint-plugin": "^5.33.0", "@typescript-eslint/parser": "^5.33.0", + "axios-mock-adapter": "^1.21.2", "bluebird": "^3.4.6", "chai": "^4.2.0", "coveralls": "^3.0.2", diff --git a/packages/optimizely-sdk/tests/odpClient.spec.ts b/packages/optimizely-sdk/tests/odpClient.spec.ts index 0e3847c47..936dc8cf9 100644 --- a/packages/optimizely-sdk/tests/odpClient.spec.ts +++ b/packages/optimizely-sdk/tests/odpClient.spec.ts @@ -16,47 +16,128 @@ /// -import { anyString, anything, mock, resetCalls, verify } from 'ts-mockito'; +import { anyString, anything, instance, mock, resetCalls, verify } from 'ts-mockito'; import { LogHandler, LogLevel } from '../lib/modules/logging'; +import MockAdapter from 'axios-mock-adapter'; +import axios from 'axios'; +import { OdpClient } from '../lib/plugins/odp/odp_client'; +import { QuerySegmentsParameters } from '../lib/plugins/odp/query_segments_parameters'; describe('OdpClient', () => { + const MOCK_QUERY_PARAMETERS = new QuerySegmentsParameters({ + ApiKey: 'not-real-api-key', + ApiHost: 'https://api.example.com/v3/graphql', + UserKey: 'fs_user_id', + UserValue: 'mock-user-id', + SegmentsToCheck: [ + 'has_email', + 'has_email_opted_in', + 'push_on_sale', + ], + }); + let mockLogger: LogHandler; + let mockAxios: MockAdapter; beforeAll(() => { mockLogger = mock(); + mockAxios = new MockAdapter(axios); }); beforeEach(() => { resetCalls(mockLogger); + mockAxios.reset(); }); - it('should segments successfully', () => { - + it('should get mocked segments successfully', async () => { + const responseJson = { + 'data': { + 'customer': { + 'audiences': { + 'edges': [ + { + 'node': { + 'name': 'has_email', + 'state': 'qualified', + }, + }, + { + 'node': { + 'name': 'has_email_opted_in', + 'state': 'qualified', + }, + }, + ], + }, + }, + }, + }; + mockAxios.onPost(/.*/).reply(200, responseJson); + const client = new OdpClient(instance(mockLogger)); + + const response = await client.querySegments(MOCK_QUERY_PARAMETERS); + + expect(response).toEqual(responseJson); verify(mockLogger.log(anything(), anyString())).never(); }); - it('should handle missing API Host', () => { + it('should handle missing API Host', async () => { + const missingApiHost = new QuerySegmentsParameters({ + ApiKey: 'apiKey', + ApiHost: '', + UserKey: 'userKey', + UserValue: 'userValue', + SegmentsToCheck: ['segmentToCheck'], + }); + const client = new OdpClient(instance(mockLogger)); + + await client.querySegments(missingApiHost); verify(mockLogger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments')).once(); }); - it('should handle missing API Key', () => { + it('should handle missing API Key', async () => { + const missingApiHost = new QuerySegmentsParameters({ + ApiKey: '', + ApiHost: 'apiHost', + UserKey: 'userKey', + UserValue: 'userValue', + SegmentsToCheck: ['segmentToCheck'], + }); + const client = new OdpClient(instance(mockLogger)); + + await client.querySegments(missingApiHost); verify(mockLogger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments')).once(); }); - it('should handle 400 HTTP response', () => { + it('should handle 400 HTTP response', async () => { + mockAxios.onPost(/.*/).reply(400, { throwAway: 'data' }); + const client = new OdpClient(instance(mockLogger)); + + const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); + expect(responseJson).toBeUndefined(); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (400)')).once(); }); - it('should handle 500 HTTP response', () => { + it('should handle 500 HTTP response', async () => { + mockAxios.onPost(/.*/).reply(500, { throwAway: 'data' }); + const client = new OdpClient(instance(mockLogger)); + const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); + + expect(responseJson).toBeUndefined(); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (500)')).once(); }); - it('should other types of unsuccessful HTTP responses', () => { + it('should handle a network timeout', async () => { + mockAxios.onPost(/.*/).timeout(); + const client = new OdpClient(instance(mockLogger)); + + const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); + expect(responseJson).toBeUndefined(); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)')).once(); }); }); From 3ffd5b180b41de4e195ba3370425a60b725c208b Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 22 Aug 2022 11:53:28 -0400 Subject: [PATCH 17/42] Switch to using toHaveLength --- .../tests/graphQLManager.spec.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/optimizely-sdk/tests/graphQLManager.spec.ts b/packages/optimizely-sdk/tests/graphQLManager.spec.ts index 806bba77b..6d2d85898 100644 --- a/packages/optimizely-sdk/tests/graphQLManager.spec.ts +++ b/packages/optimizely-sdk/tests/graphQLManager.spec.ts @@ -74,9 +74,9 @@ describe('GraphQLManager', () => { const response = manager['parseSegmentsResponseJson'](validJsonResponse) as Response; expect(response).not.toBeUndefined(); - expect(response?.errors?.length).toEqual(0); + expect(response?.errors).toHaveLength(0); expect(response?.data?.customer?.audiences?.edges).not.toBeNull(); - expect(response.data.customer.audiences.edges.length).toEqual(2); + expect(response.data.customer.audiences.edges).toHaveLength(2); let node = response.data.customer.audiences.edges[0].node; expect(node.name).toEqual('has_email'); expect(node.state).toEqual('qualified'); @@ -128,7 +128,7 @@ describe('GraphQLManager', () => { const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); - expect(segments.length).toEqual(2); + expect(segments).toHaveLength(2); expect(segments).toContain('has_email'); expect(segments).toContain('has_email_opted_in'); verify(mockLogger.log(anything(), anyString())).never(); @@ -142,7 +142,7 @@ describe('GraphQLManager', () => { const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); - expect(segments.length).toEqual(0); + expect(segments).toHaveLength(0); verify(mockLogger.log(anything(), anyString())).never(); }); @@ -159,7 +159,7 @@ describe('GraphQLManager', () => { const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, INVALID_USER_ID, SEGMENTS_TO_CHECK); - expect(segments.length).toEqual(0); + expect(segments).toHaveLength(0); verify(mockLogger.log(anything(), anyString())).once(); }); @@ -170,7 +170,7 @@ describe('GraphQLManager', () => { const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); - expect(segments.length).toEqual(0); + expect(segments).toHaveLength(0); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)')).once(); }); @@ -184,7 +184,7 @@ describe('GraphQLManager', () => { const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); - expect(segments.length).toEqual(0); + expect(segments).toHaveLength(0); verify(mockLogger.log(anything(), anyString())).once(); }); @@ -195,7 +195,7 @@ describe('GraphQLManager', () => { const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); - expect(segments.length).toEqual(0); + expect(segments).toHaveLength(0); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)')).once(); }); @@ -205,7 +205,7 @@ describe('GraphQLManager', () => { const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); - expect(segments.length).toEqual(0); + expect(segments).toHaveLength(0); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)')).once(); }); }); From f76e131892a8511cc0fe2faea7128bae827d508a Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 22 Aug 2022 14:26:42 -0400 Subject: [PATCH 18/42] Fix tests --- .../lib/core/project_config/project_config_manager.tests.js | 2 +- .../lib/utils/json_schema_validator/index.tests.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/optimizely-sdk/lib/core/project_config/project_config_manager.tests.js b/packages/optimizely-sdk/lib/core/project_config/project_config_manager.tests.js index 25969facc..ff278f1e3 100644 --- a/packages/optimizely-sdk/lib/core/project_config/project_config_manager.tests.js +++ b/packages/optimizely-sdk/lib/core/project_config/project_config_manager.tests.js @@ -97,7 +97,7 @@ describe('lib/core/project_config/project_config_manager', function() { var errorMessage = globalStubErrorHandler.handleError.lastCall.args[0].message; assert.strictEqual( errorMessage, - sprintf(ERROR_MESSAGES.INVALID_DATAFILE, 'JSON_SCHEMA_VALIDATOR', 'projectId', 'is missing and it is required') + sprintf(ERROR_MESSAGES.INVALID_DATAFILE, 'JSON_SCHEMA_VALIDATOR (Project Config JSON Schema)', 'projectId', 'is missing and it is required'), ); return manager.onReady().then(function(result) { assert.include(result, { diff --git a/packages/optimizely-sdk/lib/utils/json_schema_validator/index.tests.js b/packages/optimizely-sdk/lib/utils/json_schema_validator/index.tests.js index 00d9e712b..597ce15b7 100644 --- a/packages/optimizely-sdk/lib/utils/json_schema_validator/index.tests.js +++ b/packages/optimizely-sdk/lib/utils/json_schema_validator/index.tests.js @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { sprintf } from '../../utils/fns'; +import { sprintf } from '../fns'; import { assert } from 'chai'; import { validate } from './'; @@ -33,7 +33,7 @@ describe('lib/utils/json_schema_validator', function() { it('should throw an error if no json object is passed in', function() { assert.throws(function() { validate(); - }, sprintf(ERROR_MESSAGES.NO_JSON_PROVIDED, 'JSON_SCHEMA_VALIDATOR')); + }, sprintf(ERROR_MESSAGES.NO_JSON_PROVIDED, 'JSON_SCHEMA_VALIDATOR (Project Config JSON Schema)')); }); it('should validate specified Optimizely datafile', function() { From 2f5af449a98b94ad5b9b4d54e90e3e2464a98acb Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 23 Aug 2022 16:41:21 -0400 Subject: [PATCH 19/42] Exchange Axios for fetch --- .../lib/plugins/odp/odp_client.ts | 36 ++-- packages/optimizely-sdk/package-lock.json | 190 +++++++----------- packages/optimizely-sdk/package.json | 3 +- .../optimizely-sdk/tests/odpClient.spec.ts | 42 +++- 4 files changed, 126 insertions(+), 145 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index d5f513166..e59ffab17 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -16,7 +16,6 @@ import { LogHandler, LogLevel } from '../../modules/logging'; import { QuerySegmentsParameters } from './query_segments_parameters'; -import axios, { AxiosError, AxiosResponse } from 'axios'; export interface IOdpClient { querySegments(parameters: QuerySegmentsParameters): Promise; @@ -30,35 +29,36 @@ export class OdpClient implements IOdpClient { } public async querySegments(parameters: QuerySegmentsParameters): Promise { - const data = parameters.ToJson(); + const emptyResponse = undefined; if (!parameters?.ApiHost || !parameters?.ApiKey) { this._logger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments'); - return; + return emptyResponse; } const fetchFailureMessage = 'Audience segments fetch failed'; - let response: AxiosResponse; + const method = 'POST'; + const headers = { + 'Content-Type': 'application/json', + 'x-api-key': parameters.ApiKey, + }; + const body = parameters.ToJson(); + + let response: Response; try { - response = await axios(parameters.ApiHost, - { - method: 'post', - url: parameters.ApiHost, - headers: { - 'Content-Type': 'application/json', - 'Content-Length': data.length, - 'x-api-key': parameters.ApiKey, - }, - data, - }); + response = await fetch(parameters.ApiHost, { method, headers, body }); } catch (error) { - const response = (error as AxiosError).response; + this._logger.log(LogLevel.ERROR, `${fetchFailureMessage} (network error)`); + return emptyResponse; + } + + if (!response.ok) { this._logger.log(LogLevel.ERROR, `${fetchFailureMessage} (${response?.status ?? 'network error'})`); - return; + return emptyResponse; } - return response.data; + return response.json(); } } diff --git a/packages/optimizely-sdk/package-lock.json b/packages/optimizely-sdk/package-lock.json index 5c98f6a57..6f293d2d7 100644 --- a/packages/optimizely-sdk/package-lock.json +++ b/packages/optimizely-sdk/package-lock.json @@ -10,7 +10,6 @@ "license": "Apache-2.0", "dependencies": { "@optimizely/js-sdk-datafile-manager": "^0.9.5", - "axios": "^0.27.2", "json-schema": "^0.4.0", "murmurhash": "^2.0.1", "uuid": "^8.3.2" @@ -26,12 +25,12 @@ "@types/uuid": "^3.4.4", "@typescript-eslint/eslint-plugin": "^5.33.0", "@typescript-eslint/parser": "^5.33.0", - "axios-mock-adapter": "^1.21.2", "bluebird": "^3.4.6", "chai": "^4.2.0", "coveralls": "^3.0.2", "eslint": "^8.21.0", "jest": "^23.6.0", + "jest-fetch-mock": "^3.0.3", "json-loader": "^0.5.4", "karma": "^6.4.0", "karma-browserstack-launcher": "^1.5.1", @@ -5416,7 +5415,8 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true }, "node_modules/atob": { "version": "2.1.2", @@ -5445,64 +5445,6 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, - "node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - } - }, - "node_modules/axios-mock-adapter": { - "version": "1.21.2", - "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.21.2.tgz", - "integrity": "sha512-jzyNxU3JzB2XVhplZboUcF0YDs7xuExzoRSHXPHr+UQajaGmcTqvkkUADgkVI2WkGlpZ1zZlMVdcTMU0ejV8zQ==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "is-buffer": "^2.0.5" - }, - "peerDependencies": { - "axios": ">= 0.17.0" - } - }, - "node_modules/axios-mock-adapter/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "engines": { - "node": ">=4" - } - }, - "node_modules/axios/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -6879,6 +6821,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -7180,6 +7123,15 @@ "node": ">=6" } }, + "node_modules/cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dev": true, + "dependencies": { + "node-fetch": "2.6.7" + } + }, "node_modules/cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -7478,6 +7430,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, "engines": { "node": ">=0.4.0" } @@ -8986,6 +8939,7 @@ "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "dev": true, "funding": [ { "type": "individual", @@ -10951,6 +10905,22 @@ "jest-util": "^23.4.0" } }, + "node_modules/jest-fetch-mock": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", + "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", + "dev": true, + "dependencies": { + "cross-fetch": "^3.0.4", + "promise-polyfill": "^8.1.3" + } + }, + "node_modules/jest-fetch-mock/node_modules/promise-polyfill": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.3.tgz", + "integrity": "sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg==", + "dev": true + }, "node_modules/jest-get-type": { "version": "22.4.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", @@ -14587,6 +14557,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "devOptional": true, "engines": { "node": ">= 0.6" } @@ -14595,6 +14566,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "devOptional": true, "dependencies": { "mime-db": "1.52.0" }, @@ -14999,7 +14971,6 @@ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "devOptional": true, - "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -15019,22 +14990,19 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "devOptional": true, - "peer": true + "devOptional": true }, "node_modules/node-fetch/node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "devOptional": true, - "peer": true + "devOptional": true }, "node_modules/node-fetch/node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "devOptional": true, - "peer": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -24588,7 +24556,8 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true }, "atob": { "version": "2.1.2", @@ -24608,45 +24577,6 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, - "axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "requires": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - }, - "dependencies": { - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } - } - }, - "axios-mock-adapter": { - "version": "1.21.2", - "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.21.2.tgz", - "integrity": "sha512-jzyNxU3JzB2XVhplZboUcF0YDs7xuExzoRSHXPHr+UQajaGmcTqvkkUADgkVI2WkGlpZ1zZlMVdcTMU0ejV8zQ==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.3", - "is-buffer": "^2.0.5" - }, - "dependencies": { - "is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "dev": true - } - } - }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -25774,6 +25704,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -26031,6 +25962,15 @@ "request": "^2.88.2" } }, + "cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dev": true, + "requires": { + "node-fetch": "2.6.7" + } + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -26269,7 +26209,8 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true }, "denodeify": { "version": "1.2.1", @@ -27439,7 +27380,8 @@ "follow-redirects": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "dev": true }, "for-each": { "version": "0.3.3", @@ -28994,6 +28936,24 @@ "jest-util": "^23.4.0" } }, + "jest-fetch-mock": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", + "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", + "dev": true, + "requires": { + "cross-fetch": "^3.0.4", + "promise-polyfill": "^8.1.3" + }, + "dependencies": { + "promise-polyfill": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.3.tgz", + "integrity": "sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg==", + "dev": true + } + } + }, "jest-get-type": { "version": "22.4.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", @@ -31920,12 +31880,14 @@ "mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "devOptional": true }, "mime-types": { "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "devOptional": true, "requires": { "mime-db": "1.52.0" } @@ -32257,7 +32219,6 @@ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "devOptional": true, - "peer": true, "requires": { "whatwg-url": "^5.0.0" }, @@ -32266,22 +32227,19 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "devOptional": true, - "peer": true + "devOptional": true }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "devOptional": true, - "peer": true + "devOptional": true }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "devOptional": true, - "peer": true, "requires": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" diff --git a/packages/optimizely-sdk/package.json b/packages/optimizely-sdk/package.json index 00698cb26..b70f4de24 100644 --- a/packages/optimizely-sdk/package.json +++ b/packages/optimizely-sdk/package.json @@ -41,7 +41,6 @@ "homepage": "https://github.com/optimizely/javascript-sdk/tree/master/packages/optimizely-sdk", "dependencies": { "@optimizely/js-sdk-datafile-manager": "^0.9.5", - "axios": "^0.27.2", "json-schema": "^0.4.0", "murmurhash": "^2.0.1", "uuid": "^8.3.2" @@ -57,12 +56,12 @@ "@types/uuid": "^3.4.4", "@typescript-eslint/eslint-plugin": "^5.33.0", "@typescript-eslint/parser": "^5.33.0", - "axios-mock-adapter": "^1.21.2", "bluebird": "^3.4.6", "chai": "^4.2.0", "coveralls": "^3.0.2", "eslint": "^8.21.0", "jest": "^23.6.0", + "jest-fetch-mock": "^3.0.3", "json-loader": "^0.5.4", "karma": "^6.4.0", "karma-browserstack-launcher": "^1.5.1", diff --git a/packages/optimizely-sdk/tests/odpClient.spec.ts b/packages/optimizely-sdk/tests/odpClient.spec.ts index 936dc8cf9..bd4884655 100644 --- a/packages/optimizely-sdk/tests/odpClient.spec.ts +++ b/packages/optimizely-sdk/tests/odpClient.spec.ts @@ -16,13 +16,14 @@ /// +import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; import { anyString, anything, instance, mock, resetCalls, verify } from 'ts-mockito'; import { LogHandler, LogLevel } from '../lib/modules/logging'; -import MockAdapter from 'axios-mock-adapter'; -import axios from 'axios'; import { OdpClient } from '../lib/plugins/odp/odp_client'; import { QuerySegmentsParameters } from '../lib/plugins/odp/query_segments_parameters'; +enableFetchMocks(); + describe('OdpClient', () => { const MOCK_QUERY_PARAMETERS = new QuerySegmentsParameters({ ApiKey: 'not-real-api-key', @@ -37,16 +38,14 @@ describe('OdpClient', () => { }); let mockLogger: LogHandler; - let mockAxios: MockAdapter; beforeAll(() => { mockLogger = mock(); - mockAxios = new MockAdapter(axios); }); beforeEach(() => { resetCalls(mockLogger); - mockAxios.reset(); + fetchMock.resetMocks(); }); it('should get mocked segments successfully', async () => { @@ -72,7 +71,12 @@ describe('OdpClient', () => { }, }, }; - mockAxios.onPost(/.*/).reply(200, responseJson); + jest.spyOn(global, 'fetch').mockImplementation(jest.fn(() => { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(responseJson), + } as Response); + })); const client = new OdpClient(instance(mockLogger)); const response = await client.querySegments(MOCK_QUERY_PARAMETERS); @@ -112,7 +116,14 @@ describe('OdpClient', () => { }); it('should handle 400 HTTP response', async () => { - mockAxios.onPost(/.*/).reply(400, { throwAway: 'data' }); + const errorResponse = { + ok: false, + status: 400, + statusText: 'Mock 400 error message which is still a Promise.resolve()', + }; + jest.spyOn(global, 'fetch').mockImplementation(jest.fn(() => { + return Promise.resolve(errorResponse as Response); + })); const client = new OdpClient(instance(mockLogger)); const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); @@ -122,7 +133,14 @@ describe('OdpClient', () => { }); it('should handle 500 HTTP response', async () => { - mockAxios.onPost(/.*/).reply(500, { throwAway: 'data' }); + const errorResponse = { + ok: false, + status: 500, + statusText: 'Mock 500 error message which is still a Promise.resolve()', + }; + jest.spyOn(global, 'fetch').mockImplementation(jest.fn(() => { + return Promise.resolve(errorResponse as Response); + })); const client = new OdpClient(instance(mockLogger)); const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); @@ -132,7 +150,13 @@ describe('OdpClient', () => { }); it('should handle a network timeout', async () => { - mockAxios.onPost(/.*/).timeout(); + const errorResponse = { + ok: false, + statusText: 'Unexpected mock network issue which causes a Promise.reject()', + }; + jest.spyOn(global, 'fetch').mockImplementation(jest.fn(() => { + return Promise.reject(errorResponse as Response); + })); const client = new OdpClient(instance(mockLogger)); const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); From 808daac06047078487e6a470456e663f22abe945 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Wed, 24 Aug 2022 08:41:55 -0400 Subject: [PATCH 20/42] Resolve code review requests --- .../lib/plugins/odp/graphql_manager.ts | 39 +++++++++++-------- .../lib/plugins/odp/odp_client.ts | 30 +++++++------- .../plugins/odp/query_segments_parameters.ts | 22 +++++------ .../lib/utils/json_schema_validator/index.ts | 6 +-- 4 files changed, 51 insertions(+), 46 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index e1950f689..b2d9fe600 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -22,9 +22,11 @@ import { OdpResponseSchema } from './odp_response_schema'; import { QuerySegmentsParameters } from './query_segments_parameters'; const QUALIFIED = 'qualified'; +const EMPTY_SEGMENTS_COLLECTION: string[] = []; +const EMPTY_JSON_RESPONSE = null; export interface IGraphQLManager { - fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentToCheck: string[]): Promise; + fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise; } export class GraphqlManager implements IGraphQLManager { @@ -36,24 +38,24 @@ export class GraphqlManager implements IGraphQLManager { this._odpClient = client ?? new OdpClient(this._logger); } - public async fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentToCheck: string[]): Promise { + public async fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise { const parameters = new QuerySegmentsParameters({ - ApiKey: apiKey, - ApiHost: apiHost, - UserKey: userKey, - UserValue: userValue, - SegmentsToCheck: segmentToCheck, + apiKey, + apiHost, + userKey, + userValue, + segmentsToCheck, }); const segmentsResponse = await this._odpClient.querySegments(parameters); if (!segmentsResponse) { this._logger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)'); - return [] as string[]; + return EMPTY_SEGMENTS_COLLECTION; } const parsedSegments = this.parseSegmentsResponseJson(segmentsResponse); if (!parsedSegments) { this._logger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)'); - return [] as string[]; + return EMPTY_SEGMENTS_COLLECTION; } if (parsedSegments.errors?.length > 0) { @@ -61,29 +63,32 @@ export class GraphqlManager implements IGraphQLManager { this._logger.log(LogLevel.WARNING, `Audience segments fetch failed (${errors})`); - return [] as string[]; + return EMPTY_SEGMENTS_COLLECTION; } - if (parsedSegments?.data?.customer?.audiences?.edges === null) { + const edges = parsedSegments?.data?.customer?.audiences?.edges; + if (edges === undefined) { this._logger.log(LogLevel.WARNING, 'Audience segments fetch failed (decode error)'); - - return [] as string[]; + return EMPTY_SEGMENTS_COLLECTION; } - return parsedSegments.data.customer.audiences.edges.filter(edge => edge.node.state == QUALIFIED).map(edge => edge.node.name); + return edges.filter(edge => edge.node.state == QUALIFIED).map(edge => edge.node.name); } - private parseSegmentsResponseJson(jsonResponse: string): Response | undefined { + private parseSegmentsResponseJson(jsonResponse: string): Response | null { let jsonObject = {}; + try { jsonObject = JSON.parse(jsonResponse); } catch { this._logger.log(LogLevel.ERROR, 'Attempted to parse invalid segment response JSON.'); - return; + return EMPTY_JSON_RESPONSE; } + if (validate(jsonObject, OdpResponseSchema, false)) { return jsonObject as Response; } - return; + + return EMPTY_JSON_RESPONSE; } } diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index e59ffab17..21a5bda4b 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -17,46 +17,46 @@ import { LogHandler, LogLevel } from '../../modules/logging'; import { QuerySegmentsParameters } from './query_segments_parameters'; +const FETCH_FAILURE_MESSAGE = 'Audience segments fetch failed'; +const EMPTY_JSON_RESPONSE = null; + export interface IOdpClient { - querySegments(parameters: QuerySegmentsParameters): Promise; + querySegments(parameters: QuerySegmentsParameters): Promise; } export class OdpClient implements IOdpClient { + private readonly _logger: LogHandler; constructor(logger: LogHandler) { this._logger = logger; } - public async querySegments(parameters: QuerySegmentsParameters): Promise { - const emptyResponse = undefined; - - if (!parameters?.ApiHost || !parameters?.ApiKey) { + public async querySegments(parameters: QuerySegmentsParameters): Promise { + if (!parameters?.apiHost || !parameters?.apiKey) { this._logger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments'); - return emptyResponse; + return EMPTY_JSON_RESPONSE; } - const fetchFailureMessage = 'Audience segments fetch failed'; - const method = 'POST'; const headers = { 'Content-Type': 'application/json', - 'x-api-key': parameters.ApiKey, + 'x-api-key': parameters.apiKey, }; - const body = parameters.ToJson(); + const body = parameters.toGraphQLJson(); let response: Response; try { - response = await fetch(parameters.ApiHost, { method, headers, body }); + response = await fetch(parameters.apiHost, { method, headers, body }); } catch (error) { - this._logger.log(LogLevel.ERROR, `${fetchFailureMessage} (network error)`); - return emptyResponse; + this._logger.log(LogLevel.ERROR, `${FETCH_FAILURE_MESSAGE} (network error)`); + return EMPTY_JSON_RESPONSE; } if (!response.ok) { - this._logger.log(LogLevel.ERROR, `${fetchFailureMessage} (${response?.status ?? 'network error'})`); - return emptyResponse; + this._logger.log(LogLevel.ERROR, `${FETCH_FAILURE_MESSAGE} (${response?.status ?? 'network error'})`); + return EMPTY_JSON_RESPONSE; } return response.json(); diff --git a/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts b/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts index 483bf48c8..0fc62c30c 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts @@ -21,42 +21,42 @@ export class QuerySegmentsParameters { /** * Optimizely Data Platform API key */ - public ApiKey: string | undefined; + public apiKey: string | undefined; /** * Fully-qualified URL to ODP endpoint */ - public ApiHost: string | undefined; + public apiHost: string | undefined; /** * 'vuid' or 'fs_user_id' (client device id or fullstack id) */ - public UserKey: string | undefined; + public userKey: string | undefined; /** * Value for the user key */ - public UserValue: string | undefined; + public userValue: string | undefined; /** * Audience segments to check for inclusion in the experiment */ - public SegmentToCheck: string[] | undefined; + public segmentsToCheck: string[] | undefined; - constructor(parameters: { UserValue: string; ApiKey: string; UserKey: string; SegmentsToCheck: string[]; ApiHost: string }) { + constructor(parameters: { apiKey: string, apiHost: string, userKey: string, userValue: string, segmentsToCheck: string[] }) { Object.assign(this, parameters); } /** - * Converts the QuerySegmentsParameters into JSON - * @returns GraphQL JSON payload + * Converts the QuerySegmentsParameters to a GraphQL JSON payload + * @returns GraphQL JSON string */ - public ToJson(): string { - const segmentsArrayJson = JSON.stringify(this.SegmentToCheck); + public toGraphQLJson(): string { + const segmentsArrayJson = JSON.stringify(this.segmentsToCheck); const json: string[] = []; json.push('{"query" : "query {customer"'); - json.push(`(${this.UserKey} : "${this.UserValue}") `); + json.push(`(${this.userKey} : "${this.userValue}") `); json.push('{audiences'); json.push(`(subset: ${segmentsArrayJson})`); json.push('{edges {node {name state}}}}}"}'); diff --git a/packages/optimizely-sdk/lib/utils/json_schema_validator/index.ts b/packages/optimizely-sdk/lib/utils/json_schema_validator/index.ts index 1e44e9e85..96c3dc485 100644 --- a/packages/optimizely-sdk/lib/utils/json_schema_validator/index.ts +++ b/packages/optimizely-sdk/lib/utils/json_schema_validator/index.ts @@ -25,10 +25,10 @@ const MODULE_NAME = 'JSON_SCHEMA_VALIDATOR'; * Validate the given json object against the specified schema * @param {unknown} jsonObject The object to validate against the schema * @param {JSONSchema4} validationSchema Provided schema to use for validation - * @param {boolean} shouldThrow Should validation throw if invalid JSON object + * @param {boolean} shouldThrowOnError Should validation throw if invalid JSON object * @return {boolean} true if the given object is valid; throws or false if invalid */ -export function validate(jsonObject: unknown, validationSchema: JSONSchema4 = schema, shouldThrow = true): boolean { +export function validate(jsonObject: unknown, validationSchema: JSONSchema4 = schema, shouldThrowOnError = true): boolean { const moduleTitle = `${MODULE_NAME} (${validationSchema.title})`; if (typeof jsonObject !== 'object' || jsonObject === null) { @@ -40,7 +40,7 @@ export function validate(jsonObject: unknown, validationSchema: JSONSchema4 = sc return true; } - if (!shouldThrow) { + if (!shouldThrowOnError) { return false; } From 5737d21518fd966c8c2263277d961203852b7172 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Wed, 24 Aug 2022 09:46:39 -0400 Subject: [PATCH 21/42] More code review changes --- .../lib/plugins/odp/graphql_manager.ts | 12 ++-- .../lib/plugins/odp/odp_client.ts | 10 ++- .../tests/graphQLManager.spec.ts | 42 ++++++++----- .../optimizely-sdk/tests/odpClient.spec.ts | 61 +++++++++++-------- 4 files changed, 77 insertions(+), 48 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index b2d9fe600..a6d5bab05 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -14,13 +14,14 @@ * limitations under the License. */ -import { ConsoleLogHandler, LogHandler, LogLevel } from '../../modules/logging'; +import { ConsoleLogHandler, ErrorHandler, LogHandler, LogLevel, NoopErrorHandler } from '../../modules/logging'; import { Response } from './odp_types'; import { IOdpClient, OdpClient } from './odp_client'; import { validate } from '../../utils/json_schema_validator'; import { OdpResponseSchema } from './odp_response_schema'; import { QuerySegmentsParameters } from './query_segments_parameters'; + const QUALIFIED = 'qualified'; const EMPTY_SEGMENTS_COLLECTION: string[] = []; const EMPTY_JSON_RESPONSE = null; @@ -30,12 +31,14 @@ export interface IGraphQLManager { } export class GraphqlManager implements IGraphQLManager { + private readonly _errorHandler: ErrorHandler; private readonly _logger: LogHandler; private readonly _odpClient: IOdpClient; - constructor(logger: LogHandler, client: IOdpClient) { + constructor(errorHandler: ErrorHandler, logger: LogHandler, client: IOdpClient) { + this._errorHandler = errorHandler ?? new NoopErrorHandler(); this._logger = logger ?? new ConsoleLogHandler(); - this._odpClient = client ?? new OdpClient(this._logger); + this._odpClient = client ?? new OdpClient(this._errorHandler, this._logger); } public async fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise { @@ -80,7 +83,8 @@ export class GraphqlManager implements IGraphQLManager { try { jsonObject = JSON.parse(jsonResponse); - } catch { + } catch (error) { + this._errorHandler.handleError(error); this._logger.log(LogLevel.ERROR, 'Attempted to parse invalid segment response JSON.'); return EMPTY_JSON_RESPONSE; } diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index 21a5bda4b..bbbb3d19f 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -14,8 +14,9 @@ * limitations under the License. */ -import { LogHandler, LogLevel } from '../../modules/logging'; +import { ErrorHandler, LogHandler, LogLevel, NoopErrorHandler } from '../../modules/logging'; import { QuerySegmentsParameters } from './query_segments_parameters'; +import { NoOpLogger } from '../logger'; const FETCH_FAILURE_MESSAGE = 'Audience segments fetch failed'; const EMPTY_JSON_RESPONSE = null; @@ -26,10 +27,12 @@ export interface IOdpClient { export class OdpClient implements IOdpClient { + private readonly _errorHandler: ErrorHandler; private readonly _logger: LogHandler; - constructor(logger: LogHandler) { - this._logger = logger; + constructor(errorHandler: ErrorHandler, logger: LogHandler) { + this._errorHandler = errorHandler ?? new NoopErrorHandler(); + this._logger = logger ?? new NoOpLogger(); } public async querySegments(parameters: QuerySegmentsParameters): Promise { @@ -50,6 +53,7 @@ export class OdpClient implements IOdpClient { try { response = await fetch(parameters.apiHost, { method, headers, body }); } catch (error) { + this._errorHandler.handleError(error); this._logger.log(LogLevel.ERROR, `${FETCH_FAILURE_MESSAGE} (network error)`); return EMPTY_JSON_RESPONSE; } diff --git a/packages/optimizely-sdk/tests/graphQLManager.spec.ts b/packages/optimizely-sdk/tests/graphQLManager.spec.ts index 6d2d85898..4a877c1bc 100644 --- a/packages/optimizely-sdk/tests/graphQLManager.spec.ts +++ b/packages/optimizely-sdk/tests/graphQLManager.spec.ts @@ -18,13 +18,13 @@ import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; import { IOdpClient, OdpClient } from '../lib/plugins/odp/odp_client'; -import { LogHandler, LogLevel } from '../lib/modules/logging'; +import { ErrorHandler, LogHandler, LogLevel } from '../lib/modules/logging'; import { GraphqlManager } from '../lib/plugins/odp/graphql_manager'; import { Response } from '../lib/plugins/odp/odp_types'; describe('GraphQLManager', () => { - const VALID_ODP_PUBLIC_KEY = 'W4WzcEs-ABgXorzY7h1LCQ'; - const ODP_GRAPHQL_URL = 'https://api.zaius.com/v3/graphql'; + const VALID_ODP_PUBLIC_KEY = 'not-real-api-key'; + const ODP_GRAPHQL_URL = 'https://some.example.com/graphql/endpoint'; const FS_USER_ID = 'fs_user_id'; const VALID_FS_USER_ID = 'tester-101'; const SEGMENTS_TO_CHECK = [ @@ -33,15 +33,20 @@ describe('GraphQLManager', () => { 'push_on_sale', ]; + const makeManagerInstance = () => new GraphqlManager(instance(mockErrorHandler), instance(mockLogger), instance(mockOdpClient)); + + let mockErrorHandler: ErrorHandler; let mockLogger: LogHandler; let mockOdpClient: IOdpClient; beforeAll(() => { + mockErrorHandler = mock(); mockLogger = mock(); mockOdpClient = mock(); }); beforeEach(() => { + resetCalls(mockErrorHandler); resetCalls(mockLogger); resetCalls(mockOdpClient); }); @@ -69,7 +74,7 @@ describe('GraphQLManager', () => { } } }`; - const manager = new GraphqlManager(instance(mockLogger), instance(mockOdpClient)); + const manager = makeManagerInstance(); const response = manager['parseSegmentsResponseJson'](validJsonResponse) as Response; @@ -89,7 +94,7 @@ describe('GraphQLManager', () => { const errorJsonResponse = `{ "errors": [ { - "message": "Exception while fetching data (/customer) : java.lang.RuntimeException: could not resolve _fs_user_id = asdsdaddddd", + "message": "Exception while fetching data (/customer) : Exception: could not resolve _fs_user_id = asdsdaddddd", "locations": [ { "line": 2, @@ -108,7 +113,7 @@ describe('GraphQLManager', () => { "customer": null } }`; - const manager = new GraphqlManager(instance(mockLogger), instance(mockOdpClient)); + const manager = makeManagerInstance(); const response = manager['parseSegmentsResponseJson'](errorJsonResponse) as Response; @@ -124,13 +129,14 @@ describe('GraphQLManager', () => { '"state":"qualified"}},{"node":{"name":' + '"has_email_opted_in","state":"qualified"}}]}}}}'; when(mockOdpClient.querySegments(anything())).thenResolve(responseJsonWithQualifiedSegments); - const manager = new GraphqlManager(instance(mockLogger), instance(mockOdpClient)); + const manager = makeManagerInstance(); const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); expect(segments).toHaveLength(2); expect(segments).toContain('has_email'); expect(segments).toContain('has_email_opted_in'); + verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(anything(), anyString())).never(); }); @@ -138,11 +144,12 @@ describe('GraphQLManager', () => { const responseJsonWithNoQualifiedSegments = '{"data":{"customer":{"audiences":' + '{"edges":[ ]}}}}'; when(mockOdpClient.querySegments(anything())).thenResolve(responseJsonWithNoQualifiedSegments); - const manager = new GraphqlManager(instance(mockLogger), instance(mockOdpClient)); + const manager = makeManagerInstance(); const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); expect(segments).toHaveLength(0); + verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(anything(), anyString())).never(); }); @@ -150,27 +157,28 @@ describe('GraphQLManager', () => { const INVALID_USER_ID = 'invalid-user'; const errorJsonResponse = '{"errors":[{"message":' + '"Exception while fetching data (/customer) : ' + - `java.lang.RuntimeException: could not resolve _fs_user_id = ${INVALID_USER_ID}",` + + `Exception: could not resolve _fs_user_id = ${INVALID_USER_ID}",` + '"locations":[{"line":1,"column":8}],"path":["customer"],' + '"extensions":{"classification":"DataFetchingException"}}],' + '"data":{"customer":null}}'; when(mockOdpClient.querySegments(anything())).thenResolve(errorJsonResponse); - const manager = new GraphqlManager(instance(mockLogger), instance(mockOdpClient)); - + const manager = makeManagerInstance(); const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, INVALID_USER_ID, SEGMENTS_TO_CHECK); expect(segments).toHaveLength(0); + verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(anything(), anyString())).once(); }); it('should handle unrecognized JSON responses', async () => { const unrecognizedJson = '{"unExpectedObject":{ "withSome": "value", "thatIsNotParseable": "true" }}'; when(mockOdpClient.querySegments(anything())).thenResolve(unrecognizedJson); - const manager = new GraphqlManager(instance(mockLogger), instance(mockOdpClient)); + const manager = makeManagerInstance(); const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); expect(segments).toHaveLength(0); + verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)')).once(); }); @@ -180,28 +188,30 @@ describe('GraphQLManager', () => { '\'customer\'","locations":[{"line":1,"column":17}],' + '"extensions":{"classification":"ValidationError"}}]}'; when(mockOdpClient.querySegments(anything())).thenResolve(errorJsonResponse); - const manager = new GraphqlManager(instance(mockLogger), instance(mockOdpClient)); + const manager = makeManagerInstance(); const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); expect(segments).toHaveLength(0); + verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(anything(), anyString())).once(); }); it('should handle bad responses', async () => { const badResponse = '{"data":{ }}'; when(mockOdpClient.querySegments(anything())).thenResolve(badResponse); - const manager = new GraphqlManager(instance(mockLogger), instance(mockOdpClient)); + const manager = makeManagerInstance(); const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); expect(segments).toHaveLength(0); + verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)')).once(); }); it('should handle non 200 HTTP status code response', async () => { - when(mockOdpClient.querySegments(anything())).thenResolve(undefined); - const manager = new GraphqlManager(instance(mockLogger), instance(mockOdpClient)); + when(mockOdpClient.querySegments(anything())).thenResolve(null); + const manager = makeManagerInstance(); const segments = await manager.fetchSegments(VALID_ODP_PUBLIC_KEY, ODP_GRAPHQL_URL, FS_USER_ID, VALID_FS_USER_ID, SEGMENTS_TO_CHECK); diff --git a/packages/optimizely-sdk/tests/odpClient.spec.ts b/packages/optimizely-sdk/tests/odpClient.spec.ts index bd4884655..b19f6fc27 100644 --- a/packages/optimizely-sdk/tests/odpClient.spec.ts +++ b/packages/optimizely-sdk/tests/odpClient.spec.ts @@ -18,7 +18,7 @@ import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; import { anyString, anything, instance, mock, resetCalls, verify } from 'ts-mockito'; -import { LogHandler, LogLevel } from '../lib/modules/logging'; +import { ErrorHandler, LogHandler, LogLevel } from '../lib/modules/logging'; import { OdpClient } from '../lib/plugins/odp/odp_client'; import { QuerySegmentsParameters } from '../lib/plugins/odp/query_segments_parameters'; @@ -26,24 +26,29 @@ enableFetchMocks(); describe('OdpClient', () => { const MOCK_QUERY_PARAMETERS = new QuerySegmentsParameters({ - ApiKey: 'not-real-api-key', - ApiHost: 'https://api.example.com/v3/graphql', - UserKey: 'fs_user_id', - UserValue: 'mock-user-id', - SegmentsToCheck: [ + apiKey: 'not-real-api-key', + apiHost: 'https://api.example.com/v3/graphql', + userKey: 'fs_user_id', + userValue: 'mock-user-id', + segmentsToCheck: [ 'has_email', 'has_email_opted_in', 'push_on_sale', ], }); + const makeClientInstance = () => new OdpClient(instance(mockErrorHandler), instance(mockLogger)); + + let mockErrorHandler: ErrorHandler; let mockLogger: LogHandler; beforeAll(() => { + mockErrorHandler = mock(); mockLogger = mock(); }); beforeEach(() => { + resetCalls(mockErrorHandler); resetCalls(mockLogger); fetchMock.resetMocks(); }); @@ -77,41 +82,44 @@ describe('OdpClient', () => { json: () => Promise.resolve(responseJson), } as Response); })); - const client = new OdpClient(instance(mockLogger)); + const client = makeClientInstance(); const response = await client.querySegments(MOCK_QUERY_PARAMETERS); expect(response).toEqual(responseJson); + verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(anything(), anyString())).never(); }); it('should handle missing API Host', async () => { const missingApiHost = new QuerySegmentsParameters({ - ApiKey: 'apiKey', - ApiHost: '', - UserKey: 'userKey', - UserValue: 'userValue', - SegmentsToCheck: ['segmentToCheck'], + apiKey: 'apiKey', + apiHost: '', + userKey: 'userKey', + userValue: 'userValue', + segmentsToCheck: ['segmentToCheck'], }); - const client = new OdpClient(instance(mockLogger)); + const client = makeClientInstance(); await client.querySegments(missingApiHost); + verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments')).once(); }); it('should handle missing API Key', async () => { const missingApiHost = new QuerySegmentsParameters({ - ApiKey: '', - ApiHost: 'apiHost', - UserKey: 'userKey', - UserValue: 'userValue', - SegmentsToCheck: ['segmentToCheck'], + apiKey: '', + apiHost: 'apiHost', + userKey: 'userKey', + userValue: 'userValue', + segmentsToCheck: ['segmentToCheck'], }); - const client = new OdpClient(instance(mockLogger)); + const client = makeClientInstance(); await client.querySegments(missingApiHost); + verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments')).once(); }); @@ -124,11 +132,12 @@ describe('OdpClient', () => { jest.spyOn(global, 'fetch').mockImplementation(jest.fn(() => { return Promise.resolve(errorResponse as Response); })); - const client = new OdpClient(instance(mockLogger)); + const client = makeClientInstance(); const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); - expect(responseJson).toBeUndefined(); + expect(responseJson).toBeNull(); + verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (400)')).once(); }); @@ -141,11 +150,12 @@ describe('OdpClient', () => { jest.spyOn(global, 'fetch').mockImplementation(jest.fn(() => { return Promise.resolve(errorResponse as Response); })); - const client = new OdpClient(instance(mockLogger)); + const client = makeClientInstance(); const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); - expect(responseJson).toBeUndefined(); + expect(responseJson).toBeNull(); + verify(mockErrorHandler.handleError(anything())).never(); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (500)')).once(); }); @@ -157,11 +167,12 @@ describe('OdpClient', () => { jest.spyOn(global, 'fetch').mockImplementation(jest.fn(() => { return Promise.reject(errorResponse as Response); })); - const client = new OdpClient(instance(mockLogger)); + const client = makeClientInstance(); const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); - expect(responseJson).toBeUndefined(); + expect(responseJson).toBeNull(); + verify(mockErrorHandler.handleError(anything())).once(); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)')).once(); }); }); From fa576b2cfe408183ec74a0b6078398b6df80f3ca Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Wed, 24 Aug 2022 10:03:05 -0400 Subject: [PATCH 22/42] Refactors to remove warns --- .../lib/plugins/odp/graphql_manager.ts | 3 +-- .../lib/plugins/odp/odp_client.ts | 2 +- .../optimizely-sdk/tests/odpClient.spec.ts | 24 +++++++------------ 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index a6d5bab05..183b0c7b2 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -21,7 +21,6 @@ import { validate } from '../../utils/json_schema_validator'; import { OdpResponseSchema } from './odp_response_schema'; import { QuerySegmentsParameters } from './query_segments_parameters'; - const QUALIFIED = 'qualified'; const EMPTY_SEGMENTS_COLLECTION: string[] = []; const EMPTY_JSON_RESPONSE = null; @@ -83,7 +82,7 @@ export class GraphqlManager implements IGraphQLManager { try { jsonObject = JSON.parse(jsonResponse); - } catch (error) { + } catch (error: any) { this._errorHandler.handleError(error); this._logger.log(LogLevel.ERROR, 'Attempted to parse invalid segment response JSON.'); return EMPTY_JSON_RESPONSE; diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index bbbb3d19f..ae9c31adf 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -52,7 +52,7 @@ export class OdpClient implements IOdpClient { try { response = await fetch(parameters.apiHost, { method, headers, body }); - } catch (error) { + } catch (error: any) { this._errorHandler.handleError(error); this._logger.log(LogLevel.ERROR, `${FETCH_FAILURE_MESSAGE} (network error)`); return EMPTY_JSON_RESPONSE; diff --git a/packages/optimizely-sdk/tests/odpClient.spec.ts b/packages/optimizely-sdk/tests/odpClient.spec.ts index b19f6fc27..b4337c071 100644 --- a/packages/optimizely-sdk/tests/odpClient.spec.ts +++ b/packages/optimizely-sdk/tests/odpClient.spec.ts @@ -38,6 +38,8 @@ describe('OdpClient', () => { }); const makeClientInstance = () => new OdpClient(instance(mockErrorHandler), instance(mockLogger)); + const setFetchSpy = (mockImplementation: { (): Promise; (...args: unknown[]): any; }) => + jest.spyOn(global, 'fetch').mockImplementation(jest.fn(mockImplementation)); let mockErrorHandler: ErrorHandler; let mockLogger: LogHandler; @@ -76,12 +78,10 @@ describe('OdpClient', () => { }, }, }; - jest.spyOn(global, 'fetch').mockImplementation(jest.fn(() => { - return Promise.resolve({ - ok: true, - json: () => Promise.resolve(responseJson), - } as Response); - })); + setFetchSpy(() => Promise.resolve({ + ok: true, + json: () => Promise.resolve(responseJson), + } as Response)); const client = makeClientInstance(); const response = await client.querySegments(MOCK_QUERY_PARAMETERS); @@ -129,9 +129,7 @@ describe('OdpClient', () => { status: 400, statusText: 'Mock 400 error message which is still a Promise.resolve()', }; - jest.spyOn(global, 'fetch').mockImplementation(jest.fn(() => { - return Promise.resolve(errorResponse as Response); - })); + setFetchSpy(() => Promise.resolve(errorResponse as Response)); const client = makeClientInstance(); const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); @@ -147,9 +145,7 @@ describe('OdpClient', () => { status: 500, statusText: 'Mock 500 error message which is still a Promise.resolve()', }; - jest.spyOn(global, 'fetch').mockImplementation(jest.fn(() => { - return Promise.resolve(errorResponse as Response); - })); + setFetchSpy(() => Promise.resolve(errorResponse as Response)); const client = makeClientInstance(); const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); @@ -164,9 +160,7 @@ describe('OdpClient', () => { ok: false, statusText: 'Unexpected mock network issue which causes a Promise.reject()', }; - jest.spyOn(global, 'fetch').mockImplementation(jest.fn(() => { - return Promise.reject(errorResponse as Response); - })); + setFetchSpy(() => Promise.reject(errorResponse as Response)); const client = makeClientInstance(); const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); From 8e409a8b61a942f238bba6fc028c2c1e3b166b4c Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Thu, 25 Aug 2022 15:48:09 -0400 Subject: [PATCH 23/42] Pull out fetch & put axios back in --- .../lib/plugins/odp/odp_client.ts | 30 +-- packages/optimizely-sdk/package-lock.json | 190 +++++++++++------- packages/optimizely-sdk/package.json | 3 +- .../optimizely-sdk/tests/odpClient.spec.ts | 39 +--- 4 files changed, 146 insertions(+), 116 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index ae9c31adf..5605035d5 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -17,6 +17,7 @@ import { ErrorHandler, LogHandler, LogLevel, NoopErrorHandler } from '../../modules/logging'; import { QuerySegmentsParameters } from './query_segments_parameters'; import { NoOpLogger } from '../logger'; +import axios, { AxiosError, AxiosResponse } from 'axios'; const FETCH_FAILURE_MESSAGE = 'Audience segments fetch failed'; const EMPTY_JSON_RESPONSE = null; @@ -41,28 +42,31 @@ export class OdpClient implements IOdpClient { return EMPTY_JSON_RESPONSE; } - const method = 'POST'; - const headers = { - 'Content-Type': 'application/json', - 'x-api-key': parameters.apiKey, - }; - const body = parameters.toGraphQLJson(); - - let response: Response; + let response: AxiosResponse; try { - response = await fetch(parameters.apiHost, { method, headers, body }); + response = await axios( + { + method: 'post', + url: parameters.apiHost, + headers: { + 'Content-Type': 'application/json', + 'x-api-key': parameters.apiKey, + }, + data: parameters.toGraphQLJson(), + }); } catch (error: any) { this._errorHandler.handleError(error); - this._logger.log(LogLevel.ERROR, `${FETCH_FAILURE_MESSAGE} (network error)`); + + const errorDetails = error as AxiosError; + this._logger.log(LogLevel.ERROR, `${FETCH_FAILURE_MESSAGE} (${errorDetails?.response?.status ?? 'network error'})`); return EMPTY_JSON_RESPONSE; } - if (!response.ok) { - this._logger.log(LogLevel.ERROR, `${FETCH_FAILURE_MESSAGE} (${response?.status ?? 'network error'})`); + if (response.status !== 200) { return EMPTY_JSON_RESPONSE; } - return response.json(); + return response.data; } } diff --git a/packages/optimizely-sdk/package-lock.json b/packages/optimizely-sdk/package-lock.json index 6f293d2d7..5c98f6a57 100644 --- a/packages/optimizely-sdk/package-lock.json +++ b/packages/optimizely-sdk/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@optimizely/js-sdk-datafile-manager": "^0.9.5", + "axios": "^0.27.2", "json-schema": "^0.4.0", "murmurhash": "^2.0.1", "uuid": "^8.3.2" @@ -25,12 +26,12 @@ "@types/uuid": "^3.4.4", "@typescript-eslint/eslint-plugin": "^5.33.0", "@typescript-eslint/parser": "^5.33.0", + "axios-mock-adapter": "^1.21.2", "bluebird": "^3.4.6", "chai": "^4.2.0", "coveralls": "^3.0.2", "eslint": "^8.21.0", "jest": "^23.6.0", - "jest-fetch-mock": "^3.0.3", "json-loader": "^0.5.4", "karma": "^6.4.0", "karma-browserstack-launcher": "^1.5.1", @@ -5415,8 +5416,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/atob": { "version": "2.1.2", @@ -5445,6 +5445,64 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, + "node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "node_modules/axios-mock-adapter": { + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.21.2.tgz", + "integrity": "sha512-jzyNxU3JzB2XVhplZboUcF0YDs7xuExzoRSHXPHr+UQajaGmcTqvkkUADgkVI2WkGlpZ1zZlMVdcTMU0ejV8zQ==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "is-buffer": "^2.0.5" + }, + "peerDependencies": { + "axios": ">= 0.17.0" + } + }, + "node_modules/axios-mock-adapter/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -6821,7 +6879,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -7123,15 +7180,6 @@ "node": ">=6" } }, - "node_modules/cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", - "dev": true, - "dependencies": { - "node-fetch": "2.6.7" - } - }, "node_modules/cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -7430,7 +7478,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -8939,7 +8986,6 @@ "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", - "dev": true, "funding": [ { "type": "individual", @@ -10905,22 +10951,6 @@ "jest-util": "^23.4.0" } }, - "node_modules/jest-fetch-mock": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", - "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", - "dev": true, - "dependencies": { - "cross-fetch": "^3.0.4", - "promise-polyfill": "^8.1.3" - } - }, - "node_modules/jest-fetch-mock/node_modules/promise-polyfill": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.3.tgz", - "integrity": "sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg==", - "dev": true - }, "node_modules/jest-get-type": { "version": "22.4.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", @@ -14557,7 +14587,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "devOptional": true, "engines": { "node": ">= 0.6" } @@ -14566,7 +14595,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "devOptional": true, "dependencies": { "mime-db": "1.52.0" }, @@ -14971,6 +14999,7 @@ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "devOptional": true, + "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -14990,19 +15019,22 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "devOptional": true + "devOptional": true, + "peer": true }, "node_modules/node-fetch/node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "devOptional": true + "devOptional": true, + "peer": true }, "node_modules/node-fetch/node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "devOptional": true, + "peer": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -24556,8 +24588,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "atob": { "version": "2.1.2", @@ -24577,6 +24608,45 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, + "axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "requires": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, + "axios-mock-adapter": { + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.21.2.tgz", + "integrity": "sha512-jzyNxU3JzB2XVhplZboUcF0YDs7xuExzoRSHXPHr+UQajaGmcTqvkkUADgkVI2WkGlpZ1zZlMVdcTMU0ejV8zQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "is-buffer": "^2.0.5" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true + } + } + }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -25704,7 +25774,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -25962,15 +26031,6 @@ "request": "^2.88.2" } }, - "cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", - "dev": true, - "requires": { - "node-fetch": "2.6.7" - } - }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -26209,8 +26269,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "denodeify": { "version": "1.2.1", @@ -27380,8 +27439,7 @@ "follow-redirects": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", - "dev": true + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" }, "for-each": { "version": "0.3.3", @@ -28936,24 +28994,6 @@ "jest-util": "^23.4.0" } }, - "jest-fetch-mock": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", - "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", - "dev": true, - "requires": { - "cross-fetch": "^3.0.4", - "promise-polyfill": "^8.1.3" - }, - "dependencies": { - "promise-polyfill": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.3.tgz", - "integrity": "sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg==", - "dev": true - } - } - }, "jest-get-type": { "version": "22.4.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", @@ -31880,14 +31920,12 @@ "mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "devOptional": true + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, "mime-types": { "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "devOptional": true, "requires": { "mime-db": "1.52.0" } @@ -32219,6 +32257,7 @@ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "devOptional": true, + "peer": true, "requires": { "whatwg-url": "^5.0.0" }, @@ -32227,19 +32266,22 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "devOptional": true + "devOptional": true, + "peer": true }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "devOptional": true + "devOptional": true, + "peer": true }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "devOptional": true, + "peer": true, "requires": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" diff --git a/packages/optimizely-sdk/package.json b/packages/optimizely-sdk/package.json index b70f4de24..00698cb26 100644 --- a/packages/optimizely-sdk/package.json +++ b/packages/optimizely-sdk/package.json @@ -41,6 +41,7 @@ "homepage": "https://github.com/optimizely/javascript-sdk/tree/master/packages/optimizely-sdk", "dependencies": { "@optimizely/js-sdk-datafile-manager": "^0.9.5", + "axios": "^0.27.2", "json-schema": "^0.4.0", "murmurhash": "^2.0.1", "uuid": "^8.3.2" @@ -56,12 +57,12 @@ "@types/uuid": "^3.4.4", "@typescript-eslint/eslint-plugin": "^5.33.0", "@typescript-eslint/parser": "^5.33.0", + "axios-mock-adapter": "^1.21.2", "bluebird": "^3.4.6", "chai": "^4.2.0", "coveralls": "^3.0.2", "eslint": "^8.21.0", "jest": "^23.6.0", - "jest-fetch-mock": "^3.0.3", "json-loader": "^0.5.4", "karma": "^6.4.0", "karma-browserstack-launcher": "^1.5.1", diff --git a/packages/optimizely-sdk/tests/odpClient.spec.ts b/packages/optimizely-sdk/tests/odpClient.spec.ts index b4337c071..44998a16d 100644 --- a/packages/optimizely-sdk/tests/odpClient.spec.ts +++ b/packages/optimizely-sdk/tests/odpClient.spec.ts @@ -16,13 +16,13 @@ /// -import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; import { anyString, anything, instance, mock, resetCalls, verify } from 'ts-mockito'; import { ErrorHandler, LogHandler, LogLevel } from '../lib/modules/logging'; +import MockAdapter from 'axios-mock-adapter'; +import axios from 'axios'; import { OdpClient } from '../lib/plugins/odp/odp_client'; import { QuerySegmentsParameters } from '../lib/plugins/odp/query_segments_parameters'; -enableFetchMocks(); describe('OdpClient', () => { const MOCK_QUERY_PARAMETERS = new QuerySegmentsParameters({ @@ -38,21 +38,21 @@ describe('OdpClient', () => { }); const makeClientInstance = () => new OdpClient(instance(mockErrorHandler), instance(mockLogger)); - const setFetchSpy = (mockImplementation: { (): Promise; (...args: unknown[]): any; }) => - jest.spyOn(global, 'fetch').mockImplementation(jest.fn(mockImplementation)); let mockErrorHandler: ErrorHandler; let mockLogger: LogHandler; + let mockAxios: MockAdapter; beforeAll(() => { mockErrorHandler = mock(); mockLogger = mock(); + mockAxios = new MockAdapter(axios); }); beforeEach(() => { resetCalls(mockErrorHandler); resetCalls(mockLogger); - fetchMock.resetMocks(); + mockAxios.reset(); }); it('should get mocked segments successfully', async () => { @@ -78,10 +78,7 @@ describe('OdpClient', () => { }, }, }; - setFetchSpy(() => Promise.resolve({ - ok: true, - json: () => Promise.resolve(responseJson), - } as Response)); + mockAxios.onPost(/.*/).reply(200, responseJson); const client = makeClientInstance(); const response = await client.querySegments(MOCK_QUERY_PARAMETERS); @@ -124,43 +121,29 @@ describe('OdpClient', () => { }); it('should handle 400 HTTP response', async () => { - const errorResponse = { - ok: false, - status: 400, - statusText: 'Mock 400 error message which is still a Promise.resolve()', - }; - setFetchSpy(() => Promise.resolve(errorResponse as Response)); + mockAxios.onPost(/.*/).reply(400, { throwAway: 'data' }); const client = makeClientInstance(); const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); expect(responseJson).toBeNull(); - verify(mockErrorHandler.handleError(anything())).never(); + verify(mockErrorHandler.handleError(anything())).once(); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (400)')).once(); }); it('should handle 500 HTTP response', async () => { - const errorResponse = { - ok: false, - status: 500, - statusText: 'Mock 500 error message which is still a Promise.resolve()', - }; - setFetchSpy(() => Promise.resolve(errorResponse as Response)); + mockAxios.onPost(/.*/).reply(500, { throwAway: 'data' }); const client = makeClientInstance(); const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); expect(responseJson).toBeNull(); - verify(mockErrorHandler.handleError(anything())).never(); + verify(mockErrorHandler.handleError(anything())).once(); verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (500)')).once(); }); it('should handle a network timeout', async () => { - const errorResponse = { - ok: false, - statusText: 'Unexpected mock network issue which causes a Promise.reject()', - }; - setFetchSpy(() => Promise.reject(errorResponse as Response)); + mockAxios.onPost(/.*/).timeout(); const client = makeClientInstance(); const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); From 99e644b374134232dae7c6e49b49fcbfdf0b4f21 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 26 Aug 2022 08:21:23 -0400 Subject: [PATCH 24/42] Remove Axios again --- packages/optimizely-sdk/package-lock.json | 121 +++------------------- packages/optimizely-sdk/package.json | 2 - 2 files changed, 17 insertions(+), 106 deletions(-) diff --git a/packages/optimizely-sdk/package-lock.json b/packages/optimizely-sdk/package-lock.json index 5c98f6a57..28932dd05 100644 --- a/packages/optimizely-sdk/package-lock.json +++ b/packages/optimizely-sdk/package-lock.json @@ -10,7 +10,6 @@ "license": "Apache-2.0", "dependencies": { "@optimizely/js-sdk-datafile-manager": "^0.9.5", - "axios": "^0.27.2", "json-schema": "^0.4.0", "murmurhash": "^2.0.1", "uuid": "^8.3.2" @@ -26,7 +25,6 @@ "@types/uuid": "^3.4.4", "@typescript-eslint/eslint-plugin": "^5.33.0", "@typescript-eslint/parser": "^5.33.0", - "axios-mock-adapter": "^1.21.2", "bluebird": "^3.4.6", "chai": "^4.2.0", "coveralls": "^3.0.2", @@ -5416,7 +5414,8 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true }, "node_modules/atob": { "version": "2.1.2", @@ -5445,64 +5444,6 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, - "node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - } - }, - "node_modules/axios-mock-adapter": { - "version": "1.21.2", - "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.21.2.tgz", - "integrity": "sha512-jzyNxU3JzB2XVhplZboUcF0YDs7xuExzoRSHXPHr+UQajaGmcTqvkkUADgkVI2WkGlpZ1zZlMVdcTMU0ejV8zQ==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "is-buffer": "^2.0.5" - }, - "peerDependencies": { - "axios": ">= 0.17.0" - } - }, - "node_modules/axios-mock-adapter/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "engines": { - "node": ">=4" - } - }, - "node_modules/axios/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -6879,6 +6820,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -7478,6 +7420,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, "engines": { "node": ">=0.4.0" } @@ -8986,6 +8929,7 @@ "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "dev": true, "funding": [ { "type": "individual", @@ -14587,6 +14531,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "devOptional": true, "engines": { "node": ">= 0.6" } @@ -14595,6 +14540,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "devOptional": true, "dependencies": { "mime-db": "1.52.0" }, @@ -24588,7 +24534,8 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true }, "atob": { "version": "2.1.2", @@ -24608,45 +24555,6 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, - "axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "requires": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - }, - "dependencies": { - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } - } - }, - "axios-mock-adapter": { - "version": "1.21.2", - "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.21.2.tgz", - "integrity": "sha512-jzyNxU3JzB2XVhplZboUcF0YDs7xuExzoRSHXPHr+UQajaGmcTqvkkUADgkVI2WkGlpZ1zZlMVdcTMU0ejV8zQ==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.3", - "is-buffer": "^2.0.5" - }, - "dependencies": { - "is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "dev": true - } - } - }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -25774,6 +25682,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -26269,7 +26178,8 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true }, "denodeify": { "version": "1.2.1", @@ -27439,7 +27349,8 @@ "follow-redirects": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "dev": true }, "for-each": { "version": "0.3.3", @@ -31920,12 +31831,14 @@ "mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "devOptional": true }, "mime-types": { "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "devOptional": true, "requires": { "mime-db": "1.52.0" } diff --git a/packages/optimizely-sdk/package.json b/packages/optimizely-sdk/package.json index 00698cb26..06bd03479 100644 --- a/packages/optimizely-sdk/package.json +++ b/packages/optimizely-sdk/package.json @@ -41,7 +41,6 @@ "homepage": "https://github.com/optimizely/javascript-sdk/tree/master/packages/optimizely-sdk", "dependencies": { "@optimizely/js-sdk-datafile-manager": "^0.9.5", - "axios": "^0.27.2", "json-schema": "^0.4.0", "murmurhash": "^2.0.1", "uuid": "^8.3.2" @@ -57,7 +56,6 @@ "@types/uuid": "^3.4.4", "@typescript-eslint/eslint-plugin": "^5.33.0", "@typescript-eslint/parser": "^5.33.0", - "axios-mock-adapter": "^1.21.2", "bluebird": "^3.4.6", "chai": "^4.2.0", "coveralls": "^3.0.2", From 3c201abcb1587be8c6eb034ed43a6bf6a50040fa Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 26 Aug 2022 08:27:23 -0400 Subject: [PATCH 25/42] Remove axios from code (WIP tests failing) --- .../lib/plugins/odp/graphql_manager.ts | 1 + .../lib/plugins/odp/odp_client.ts | 27 +++++++++---------- .../optimizely-sdk/tests/odpClient.spec.ts | 14 +++------- 3 files changed, 18 insertions(+), 24 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index 183b0c7b2..e362103cc 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -82,6 +82,7 @@ export class GraphqlManager implements IGraphQLManager { try { jsonObject = JSON.parse(jsonResponse); + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { this._errorHandler.handleError(error); this._logger.log(LogLevel.ERROR, 'Attempted to parse invalid segment response JSON.'); diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index 5605035d5..c8712efcf 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -17,7 +17,6 @@ import { ErrorHandler, LogHandler, LogLevel, NoopErrorHandler } from '../../modules/logging'; import { QuerySegmentsParameters } from './query_segments_parameters'; import { NoOpLogger } from '../logger'; -import axios, { AxiosError, AxiosResponse } from 'axios'; const FETCH_FAILURE_MESSAGE = 'Audience segments fetch failed'; const EMPTY_JSON_RESPONSE = null; @@ -42,24 +41,24 @@ export class OdpClient implements IOdpClient { return EMPTY_JSON_RESPONSE; } - let response: AxiosResponse; + const method = 'POST'; + const url = parameters.apiHost; + const headers = { + 'Content-Type': 'application/json', + 'x-api-key': parameters.apiKey, + }; + const data = parameters.toGraphQLJson(); + let response: any; try { - response = await axios( - { - method: 'post', - url: parameters.apiHost, - headers: { - 'Content-Type': 'application/json', - 'x-api-key': parameters.apiKey, - }, - data: parameters.toGraphQLJson(), - }); + throw new Error('Implementation needed'); + response = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { this._errorHandler.handleError(error); - const errorDetails = error as AxiosError; - this._logger.log(LogLevel.ERROR, `${FETCH_FAILURE_MESSAGE} (${errorDetails?.response?.status ?? 'network error'})`); + // const errorDetails = error as AxiosError; + // this._logger.log(LogLevel.ERROR, `${FETCH_FAILURE_MESSAGE} (${errorDetails?.response?.status ?? 'network error'})`); return EMPTY_JSON_RESPONSE; } diff --git a/packages/optimizely-sdk/tests/odpClient.spec.ts b/packages/optimizely-sdk/tests/odpClient.spec.ts index 44998a16d..c796fff0f 100644 --- a/packages/optimizely-sdk/tests/odpClient.spec.ts +++ b/packages/optimizely-sdk/tests/odpClient.spec.ts @@ -18,12 +18,9 @@ import { anyString, anything, instance, mock, resetCalls, verify } from 'ts-mockito'; import { ErrorHandler, LogHandler, LogLevel } from '../lib/modules/logging'; -import MockAdapter from 'axios-mock-adapter'; -import axios from 'axios'; import { OdpClient } from '../lib/plugins/odp/odp_client'; import { QuerySegmentsParameters } from '../lib/plugins/odp/query_segments_parameters'; - describe('OdpClient', () => { const MOCK_QUERY_PARAMETERS = new QuerySegmentsParameters({ apiKey: 'not-real-api-key', @@ -41,18 +38,15 @@ describe('OdpClient', () => { let mockErrorHandler: ErrorHandler; let mockLogger: LogHandler; - let mockAxios: MockAdapter; beforeAll(() => { mockErrorHandler = mock(); mockLogger = mock(); - mockAxios = new MockAdapter(axios); }); beforeEach(() => { resetCalls(mockErrorHandler); resetCalls(mockLogger); - mockAxios.reset(); }); it('should get mocked segments successfully', async () => { @@ -78,7 +72,7 @@ describe('OdpClient', () => { }, }, }; - mockAxios.onPost(/.*/).reply(200, responseJson); + //mockAxios.onPost(/.*/).reply(200, responseJson); const client = makeClientInstance(); const response = await client.querySegments(MOCK_QUERY_PARAMETERS); @@ -121,7 +115,7 @@ describe('OdpClient', () => { }); it('should handle 400 HTTP response', async () => { - mockAxios.onPost(/.*/).reply(400, { throwAway: 'data' }); + //mockAxios.onPost(/.*/).reply(400, { throwAway: 'data' }); const client = makeClientInstance(); const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); @@ -132,7 +126,7 @@ describe('OdpClient', () => { }); it('should handle 500 HTTP response', async () => { - mockAxios.onPost(/.*/).reply(500, { throwAway: 'data' }); + //mockAxios.onPost(/.*/).reply(500, { throwAway: 'data' }); const client = makeClientInstance(); const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); @@ -143,7 +137,7 @@ describe('OdpClient', () => { }); it('should handle a network timeout', async () => { - mockAxios.onPost(/.*/).timeout(); + //mockAxios.onPost(/.*/).timeout(); const client = makeClientInstance(); const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); From 766eb876447595c6ff02e2fc844e2a82bca1ad35 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 26 Aug 2022 14:53:48 -0400 Subject: [PATCH 26/42] WIP (tests failing) Browser vs Node request handling --- .../lib/plugins/odp/odp_client.ts | 26 ++- .../browser_request_handler.ts | 102 +++++++++++ .../lib/utils/http_request_handler/config.ts | 17 ++ .../lib/utils/http_request_handler/http.ts | 42 +++++ .../node_request_handler.ts | 171 ++++++++++++++++++ 5 files changed, 353 insertions(+), 5 deletions(-) create mode 100644 packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts create mode 100644 packages/optimizely-sdk/lib/utils/http_request_handler/config.ts create mode 100644 packages/optimizely-sdk/lib/utils/http_request_handler/http.ts create mode 100644 packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index c8712efcf..d875a31ed 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -17,6 +17,9 @@ import { ErrorHandler, LogHandler, LogLevel, NoopErrorHandler } from '../../modules/logging'; import { QuerySegmentsParameters } from './query_segments_parameters'; import { NoOpLogger } from '../logger'; +import { BrowserRequestHandler } from '../../utils/http_request_handler/browser_request_handler'; +import { NodeRequestHandler } from '../../utils/http_request_handler/node_request_handler'; +import { RequestHandler, Response } from '../../utils/http_request_handler/http'; const FETCH_FAILURE_MESSAGE = 'Audience segments fetch failed'; const EMPTY_JSON_RESPONSE = null; @@ -29,10 +32,15 @@ export class OdpClient implements IOdpClient { private readonly _errorHandler: ErrorHandler; private readonly _logger: LogHandler; + private readonly _isNodeContext: boolean; + private readonly _isBrowserContext: boolean; constructor(errorHandler: ErrorHandler, logger: LogHandler) { this._errorHandler = errorHandler ?? new NoopErrorHandler(); this._logger = logger ?? new NoOpLogger(); + + this._isNodeContext = typeof process === 'object'; + this._isBrowserContext = typeof window === 'object'; } public async querySegments(parameters: QuerySegmentsParameters): Promise { @@ -49,10 +57,18 @@ export class OdpClient implements IOdpClient { }; const data = parameters.toGraphQLJson(); - let response: any; + let requestHandler: RequestHandler; + if (this._isBrowserContext) { + requestHandler = new BrowserRequestHandler(this._logger); + } else if (this._isNodeContext) { + requestHandler = new NodeRequestHandler(this._logger); + } else { + // this will be a factory soon + return EMPTY_JSON_RESPONSE; + } + let response: Response; try { - throw new Error('Implementation needed'); - response = {}; + response = await requestHandler.makeRequest(url, headers, method, data).responsePromise; // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { this._errorHandler.handleError(error); @@ -62,10 +78,10 @@ export class OdpClient implements IOdpClient { return EMPTY_JSON_RESPONSE; } - if (response.status !== 200) { + if (response.statusCode !== 200) { return EMPTY_JSON_RESPONSE; } - return response.data; + return response.body; } } diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts new file mode 100644 index 000000000..c6c419fc6 --- /dev/null +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts @@ -0,0 +1,102 @@ +/** + * Copyright 2019-2020, 2022 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AbortableRequest, Headers, RequestHandler, Response } from './http'; +import { REQUEST_TIMEOUT_MS } from './config'; +import { LogHandler, LogLevel } from '../../modules/logging'; +import { NoOpLogger } from '../../plugins/logger'; + +const READY_STATE_DONE = 4; + +export class BrowserRequestHandler implements RequestHandler { + private readonly logger: LogHandler; + + public constructor(logger?: LogHandler) { + this.logger = logger ?? new NoOpLogger(); + } + + public makeRequest(reqUrl: string, headers: Headers, method: string, data?: object): AbortableRequest { + const req = new XMLHttpRequest(); + + const responsePromise: Promise = new Promise((resolve, reject) => { + req.open(method, reqUrl, true); + + this.setHeadersInXhr(headers, req); + + req.onreadystatechange = (): void => { + if (req.readyState === READY_STATE_DONE) { + const statusCode = req.status; + if (statusCode === 0) { + reject(new Error('Request error')); + return; + } + + const headers = this.parseHeadersFromXhr(req); + const resp: Response = { + statusCode: req.status, + body: req.responseText, + headers, + }; + resolve(resp); + } + }; + + req.timeout = REQUEST_TIMEOUT_MS; + + req.ontimeout = (): void => { + this.logger.log(LogLevel.WARNING, 'Request timed out'); + }; + + req.send(data); + }); + + return { + responsePromise, + abort(): void { + req.abort(); + }, + }; + } + + private parseHeadersFromXhr(req: XMLHttpRequest): Headers { + const allHeadersString = req.getAllResponseHeaders(); + + if (allHeadersString === null) { + return {}; + } + + const headerLines = allHeadersString.split('\r\n'); + const headers: Headers = {}; + headerLines.forEach(headerLine => { + const separatorIndex = headerLine.indexOf(': '); + if (separatorIndex > -1) { + const headerName = headerLine.slice(0, separatorIndex); + const headerValue = headerLine.slice(separatorIndex + 2); + if (headerValue.length > 0) { + headers[headerName] = headerValue; + } + } + }); + return headers; + } + + private setHeadersInXhr(headers: Headers, req: XMLHttpRequest): void { + Object.keys(headers).forEach(headerName => { + const header = headers[headerName]; + req.setRequestHeader(headerName, header!); + }); + } +} diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/config.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/config.ts new file mode 100644 index 000000000..830bec432 --- /dev/null +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/config.ts @@ -0,0 +1,17 @@ +/** + * Copyright 2019-2020, 2022 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const REQUEST_TIMEOUT_MS = 60 * 1000; // 1 minute diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/http.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/http.ts new file mode 100644 index 000000000..bdc4b160c --- /dev/null +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/http.ts @@ -0,0 +1,42 @@ +/** + * Copyright 2019-2020, 2022 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Headers is the interface that bridges between the abstract datafile manager and + * any Node-or-browser-specific http header types. + * It's simplified and can only store one value per header name. + * We can extend or replace this type if requirements change, and we need + * to work with multiple values per header name. + */ +export interface Headers { + [header: string]: string | undefined; +} + +export interface Response { + statusCode?: number; + body: string; + headers: Headers; +} + +export interface AbortableRequest { + abort(): void; + + responsePromise: Promise; +} + +export interface RequestHandler { + makeRequest(reqUrl: string, headers: Headers, method: string, data?: object): AbortableRequest; +} diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts new file mode 100644 index 000000000..1e1d0381b --- /dev/null +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts @@ -0,0 +1,171 @@ +/** + * Copyright 2019-2020, 2022 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import http from 'http'; +import https from 'https'; +import url from 'url'; +import { AbortableRequest, Headers, RequestHandler, Response } from './http'; +import { REQUEST_TIMEOUT_MS } from './config'; +import decompressResponse from 'decompress-response'; +import { LogHandler } from '../../modules/logging'; +import { NoOpLogger } from '../../plugins/logger'; + +// Shared signature between http.request and https.request +type ClientRequestCreator = (options: http.RequestOptions) => http.ClientRequest; + +export class NodeRequestHandler implements RequestHandler { + private readonly logger: LogHandler; + + public constructor(logger?: LogHandler) { + this.logger = logger ?? new NoOpLogger(); + } + + public makeRequest(reqUrl: string, headers: Headers, method: string, data?: object): AbortableRequest { + // TODO: Use non-legacy URL parsing when we drop support for Node 6 + const parsedUrl = url.parse(reqUrl); + + let requester: ClientRequestCreator; + if (parsedUrl.protocol === 'http:') { + requester = http.request; + } else if (parsedUrl.protocol === 'https:') { + requester = https.request; + } else { + return { + responsePromise: Promise.reject(new Error(`Unsupported protocol: ${parsedUrl.protocol}`)), + abort(): void { + }, + }; + } + + const requestOptions: http.RequestOptions = { + ...this.getRequestOptionsFromUrl(parsedUrl), + method: 'GET', + headers: { + ...headers, + 'accept-encoding': 'gzip,deflate', + }, + }; + + const request = requester(requestOptions); + const responsePromise = this.getResponseFromRequest(request); + + request.end(); + + return { + abort(): void { + request.abort(); + }, + responsePromise, + }; + } + + private getRequestOptionsFromUrl(url: url.UrlWithStringQuery): http.RequestOptions { + return { + hostname: url.hostname, + path: url.path, + port: url.port, + protocol: url.protocol, + }; + } + + /** + * Convert incomingMessage.headers (which has type http.IncomingHttpHeaders) into our Headers type defined in src/http.ts. + * + * Our Headers type is simplified and can't represent mutliple values for the same header name. + * + * We don't currently need multiple values support, and the consumer code becomes simpler if it can assume at-most 1 value + * per header name. + * + */ + private createHeadersFromNodeIncomingMessage(incomingMessage: http.IncomingMessage): Headers { + const headers: Headers = {}; + Object.keys(incomingMessage.headers).forEach(headerName => { + const headerValue = incomingMessage.headers[headerName]; + if (typeof headerValue === 'string') { + headers[headerName] = headerValue; + } else if (typeof headerValue === 'undefined') { + // no value provided for this header + } else { + // array + if (headerValue.length > 0) { + // We don't care about multiple values - just take the first one + headers[headerName] = headerValue[0]; + } + } + }); + return headers; + } + + + private getResponseFromRequest(request: http.ClientRequest): Promise { + // TODO: When we drop support for Node 6, consider using util.promisify instead of + // constructing own Promise + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + request.abort(); + reject(new Error('Request timed out')); + }, REQUEST_TIMEOUT_MS); + + request.once('response', (incomingMessage: http.IncomingMessage) => { + if (request.aborted) { + return; + } + + const response = decompressResponse(incomingMessage); + + response.setEncoding('utf8'); + + let responseData = ''; + response.on('data', (chunk: string) => { + if (!request.aborted) { + responseData += chunk; + } + }); + + response.on('end', () => { + if (request.aborted) { + return; + } + + clearTimeout(timeout); + + resolve({ + statusCode: incomingMessage.statusCode, + body: responseData, + headers: this.createHeadersFromNodeIncomingMessage(incomingMessage), + }); + }); + }); + + request.on('error', (err: any) => { + clearTimeout(timeout); + + if (err instanceof Error) { + reject(err); + } else if (typeof err === 'string') { + reject(new Error(err)); + } else { + reject(new Error('Request error')); + } + }); + }); + } +} + + + + + From 45d4ce163ccbf5e3377b7db807b80bdfb7e0567c Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 26 Aug 2022 16:53:14 -0400 Subject: [PATCH 27/42] WIP still: cleaned code a bit; tests need attn --- .../lib/plugins/odp/odp_client.ts | 42 +++++++++------ .../browser_request_handler.ts | 54 ++++++++++--------- .../lib/utils/http_request_handler/http.ts | 2 +- .../node_request_handler.ts | 51 +++++++----------- .../request_handler_factory.ts | 31 +++++++++++ 5 files changed, 105 insertions(+), 75 deletions(-) create mode 100644 packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index d875a31ed..c7e66bccb 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -17,9 +17,9 @@ import { ErrorHandler, LogHandler, LogLevel, NoopErrorHandler } from '../../modules/logging'; import { QuerySegmentsParameters } from './query_segments_parameters'; import { NoOpLogger } from '../logger'; -import { BrowserRequestHandler } from '../../utils/http_request_handler/browser_request_handler'; -import { NodeRequestHandler } from '../../utils/http_request_handler/node_request_handler'; -import { RequestHandler, Response } from '../../utils/http_request_handler/http'; +import { Response } from '../../utils/http_request_handler/http'; +import { RequestHandlerFactory } from '../../utils/http_request_handler/request_handler_factory'; +import { REQUEST_TIMEOUT_MS } from '../../utils/http_request_handler/config'; const FETCH_FAILURE_MESSAGE = 'Audience segments fetch failed'; const EMPTY_JSON_RESPONSE = null; @@ -28,19 +28,31 @@ export interface IOdpClient { querySegments(parameters: QuerySegmentsParameters): Promise; } +export enum ExecutionContextType { + notDefined, + browser, + node, +} + export class OdpClient implements IOdpClient { private readonly _errorHandler: ErrorHandler; private readonly _logger: LogHandler; - private readonly _isNodeContext: boolean; - private readonly _isBrowserContext: boolean; + private readonly _timeout: number; + private readonly _executionContextType: ExecutionContextType; + - constructor(errorHandler: ErrorHandler, logger: LogHandler) { + constructor(errorHandler?: ErrorHandler, logger?: LogHandler, timeout?: number) { this._errorHandler = errorHandler ?? new NoopErrorHandler(); this._logger = logger ?? new NoOpLogger(); + this._timeout = timeout ?? REQUEST_TIMEOUT_MS; - this._isNodeContext = typeof process === 'object'; - this._isBrowserContext = typeof window === 'object'; + this._executionContextType = ExecutionContextType.notDefined; + if (typeof window === 'object') { + this._executionContextType = ExecutionContextType.browser; + } else if (typeof process === 'object') { + this._executionContextType = ExecutionContextType.node; + } } public async querySegments(parameters: QuerySegmentsParameters): Promise { @@ -57,15 +69,12 @@ export class OdpClient implements IOdpClient { }; const data = parameters.toGraphQLJson(); - let requestHandler: RequestHandler; - if (this._isBrowserContext) { - requestHandler = new BrowserRequestHandler(this._logger); - } else if (this._isNodeContext) { - requestHandler = new NodeRequestHandler(this._logger); - } else { - // this will be a factory soon + const requestHandler = RequestHandlerFactory.createHandler(this._executionContextType.toString(), this._logger, this._timeout); + if (!requestHandler) { + this._logger.log(LogLevel.ERROR, 'Unable to determine execution context'); return EMPTY_JSON_RESPONSE; } + let response: Response; try { response = await requestHandler.makeRequest(url, headers, method, data).responsePromise; @@ -73,12 +82,11 @@ export class OdpClient implements IOdpClient { } catch (error: any) { this._errorHandler.handleError(error); - // const errorDetails = error as AxiosError; - // this._logger.log(LogLevel.ERROR, `${FETCH_FAILURE_MESSAGE} (${errorDetails?.response?.status ?? 'network error'})`); return EMPTY_JSON_RESPONSE; } if (response.statusCode !== 200) { + this._logger.log(LogLevel.ERROR, `${FETCH_FAILURE_MESSAGE} (${response.statusCode ?? 'network error'})`); return EMPTY_JSON_RESPONSE; } diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts index c6c419fc6..ad1f3ae86 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts @@ -22,55 +22,66 @@ import { NoOpLogger } from '../../plugins/logger'; const READY_STATE_DONE = 4; export class BrowserRequestHandler implements RequestHandler { - private readonly logger: LogHandler; + private readonly _logger: LogHandler; + private readonly _timeout: number; - public constructor(logger?: LogHandler) { - this.logger = logger ?? new NoOpLogger(); + public constructor(logger?: LogHandler, timeout: number = REQUEST_TIMEOUT_MS) { + this._logger = logger ?? new NoOpLogger(); + this._timeout = timeout; } - public makeRequest(reqUrl: string, headers: Headers, method: string, data?: object): AbortableRequest { - const req = new XMLHttpRequest(); + public makeRequest(reqUrl: string, headers: Headers, method: string, data?: string): AbortableRequest { + const request = new XMLHttpRequest(); const responsePromise: Promise = new Promise((resolve, reject) => { - req.open(method, reqUrl, true); + request.open(method, reqUrl, true); - this.setHeadersInXhr(headers, req); + this.setHeadersInXhr(headers, request); - req.onreadystatechange = (): void => { - if (req.readyState === READY_STATE_DONE) { - const statusCode = req.status; + request.onreadystatechange = (): void => { + if (request.readyState === READY_STATE_DONE) { + const statusCode = request.status; if (statusCode === 0) { reject(new Error('Request error')); return; } - const headers = this.parseHeadersFromXhr(req); + const headers = this.parseHeadersFromXhr(request); const resp: Response = { - statusCode: req.status, - body: req.responseText, + statusCode: request.status, + body: request.responseText, headers, }; resolve(resp); } }; - req.timeout = REQUEST_TIMEOUT_MS; + request.timeout = this._timeout; - req.ontimeout = (): void => { - this.logger.log(LogLevel.WARNING, 'Request timed out'); + request.ontimeout = (): void => { + this._logger.log(LogLevel.WARNING, 'Request timed out'); }; - req.send(data); + request.send(data); }); return { responsePromise, abort(): void { - req.abort(); + request.abort(); }, }; } + private setHeadersInXhr(headers: Headers, req: XMLHttpRequest): void { + Object.keys(headers).forEach(headerName => { + const header = headers[headerName]; + if (typeof header === 'string') { + req.setRequestHeader(headerName, header); + } + }); + } + private parseHeadersFromXhr(req: XMLHttpRequest): Headers { const allHeadersString = req.getAllResponseHeaders(); @@ -92,11 +103,4 @@ export class BrowserRequestHandler implements RequestHandler { }); return headers; } - - private setHeadersInXhr(headers: Headers, req: XMLHttpRequest): void { - Object.keys(headers).forEach(headerName => { - const header = headers[headerName]; - req.setRequestHeader(headerName, header!); - }); - } } diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/http.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/http.ts index bdc4b160c..a775bf714 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/http.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/http.ts @@ -38,5 +38,5 @@ export interface AbortableRequest { } export interface RequestHandler { - makeRequest(reqUrl: string, headers: Headers, method: string, data?: object): AbortableRequest; + makeRequest(reqUrl: string, headers: Headers, method: string, data?: string): AbortableRequest; } diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts index 1e1d0381b..dd3de04ac 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts @@ -23,26 +23,19 @@ import decompressResponse from 'decompress-response'; import { LogHandler } from '../../modules/logging'; import { NoOpLogger } from '../../plugins/logger'; -// Shared signature between http.request and https.request -type ClientRequestCreator = (options: http.RequestOptions) => http.ClientRequest; - export class NodeRequestHandler implements RequestHandler { private readonly logger: LogHandler; + private readonly timeout: number; - public constructor(logger?: LogHandler) { + public constructor(logger?: LogHandler, timeout: number = REQUEST_TIMEOUT_MS) { this.logger = logger ?? new NoOpLogger(); + this.timeout = timeout; } - public makeRequest(reqUrl: string, headers: Headers, method: string, data?: object): AbortableRequest { - // TODO: Use non-legacy URL parsing when we drop support for Node 6 + public makeRequest(reqUrl: string, headers: Headers, method: string, data?: string): AbortableRequest { const parsedUrl = url.parse(reqUrl); - let requester: ClientRequestCreator; - if (parsedUrl.protocol === 'http:') { - requester = http.request; - } else if (parsedUrl.protocol === 'https:') { - requester = https.request; - } else { + if (parsedUrl.protocol !== 'https:') { return { responsePromise: Promise.reject(new Error(`Unsupported protocol: ${parsedUrl.protocol}`)), abort(): void { @@ -50,29 +43,26 @@ export class NodeRequestHandler implements RequestHandler { }; } - const requestOptions: http.RequestOptions = { + const request = https.request({ ...this.getRequestOptionsFromUrl(parsedUrl), - method: 'GET', + method, headers: { ...headers, 'accept-encoding': 'gzip,deflate', }, - }; - - const request = requester(requestOptions); + }); const responsePromise = this.getResponseFromRequest(request); + request.write(data); request.end(); return { - abort(): void { - request.abort(); - }, + abort: () => request.abort(), responsePromise, }; } - private getRequestOptionsFromUrl(url: url.UrlWithStringQuery): http.RequestOptions { + private getRequestOptionsFromUrl(url: url.UrlWithStringQuery): https.RequestOptions { return { hostname: url.hostname, path: url.path, @@ -82,13 +72,12 @@ export class NodeRequestHandler implements RequestHandler { } /** - * Convert incomingMessage.headers (which has type http.IncomingHttpHeaders) into our Headers type defined in src/http.ts. + * Convert IncomingMessage.headers (which has type http.IncomingHttpHeaders) into our Headers type defined in src/http.ts. * - * Our Headers type is simplified and can't represent mutliple values for the same header name. + * Our Headers type is simplified and can't represent multiple values for the same header name. * * We don't currently need multiple values support, and the consumer code becomes simpler if it can assume at-most 1 value * per header name. - * */ private createHeadersFromNodeIncomingMessage(incomingMessage: http.IncomingMessage): Headers { const headers: Headers = {}; @@ -109,18 +98,15 @@ export class NodeRequestHandler implements RequestHandler { return headers; } - private getResponseFromRequest(request: http.ClientRequest): Promise { - // TODO: When we drop support for Node 6, consider using util.promisify instead of - // constructing own Promise return new Promise((resolve, reject) => { const timeout = setTimeout(() => { request.abort(); reject(new Error('Request timed out')); - }, REQUEST_TIMEOUT_MS); + }, this.timeout); request.once('response', (incomingMessage: http.IncomingMessage) => { - if (request.aborted) { + if (request.destroyed) { return; } @@ -130,13 +116,13 @@ export class NodeRequestHandler implements RequestHandler { let responseData = ''; response.on('data', (chunk: string) => { - if (!request.aborted) { + if (!request.destroyed) { responseData += chunk; } }); response.on('end', () => { - if (request.aborted) { + if (request.destroyed) { return; } @@ -149,7 +135,8 @@ export class NodeRequestHandler implements RequestHandler { }); }); }); - + + // eslint-disable-next-line @typescript-eslint/no-explicit-any request.on('error', (err: any) => { clearTimeout(timeout); diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts new file mode 100644 index 000000000..ecab65521 --- /dev/null +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2022 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { LogHandler } from '../../modules/logging'; +import { RequestHandler } from './http'; +import { NodeRequestHandler } from './node_request_handler'; +import { BrowserRequestHandler } from './browser_request_handler'; + +export class RequestHandlerFactory { + public static createHandler(type: string, logger?: LogHandler, timeout?: number): RequestHandler { + if (type === 'node') { + return new NodeRequestHandler(logger, timeout); + } else if (type === 'browser') { + return new BrowserRequestHandler(logger, timeout); + } + + return null as unknown as RequestHandler; + } +} From f8557f879341cb72757a565838f500f4d75f77e6 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 26 Aug 2022 17:02:59 -0400 Subject: [PATCH 28/42] Fix enum string --- packages/optimizely-sdk/lib/plugins/odp/odp_client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index c7e66bccb..3bf9d5836 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -69,7 +69,7 @@ export class OdpClient implements IOdpClient { }; const data = parameters.toGraphQLJson(); - const requestHandler = RequestHandlerFactory.createHandler(this._executionContextType.toString(), this._logger, this._timeout); + const requestHandler = RequestHandlerFactory.createHandler(ExecutionContextType[this._executionContextType], this._logger, this._timeout); if (!requestHandler) { this._logger.log(LogLevel.ERROR, 'Unable to determine execution context'); return EMPTY_JSON_RESPONSE; From c4a873b54c1d6147e72484e2357452451a561a84 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 26 Aug 2022 17:13:46 -0400 Subject: [PATCH 29/42] Trying to get timeout test working I need to mock up the RequestHandlers --- packages/optimizely-sdk/tests/odpClient.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/optimizely-sdk/tests/odpClient.spec.ts b/packages/optimizely-sdk/tests/odpClient.spec.ts index c796fff0f..a79e8a870 100644 --- a/packages/optimizely-sdk/tests/odpClient.spec.ts +++ b/packages/optimizely-sdk/tests/odpClient.spec.ts @@ -137,8 +137,7 @@ describe('OdpClient', () => { }); it('should handle a network timeout', async () => { - //mockAxios.onPost(/.*/).timeout(); - const client = makeClientInstance(); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), 1); const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); From dba12f3dacf4e55a22ceb5f6321d13e00cf57a58 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 29 Aug 2022 15:11:52 -0400 Subject: [PATCH 30/42] ODP tests --- .../lib/plugins/odp/odp_client.ts | 36 ++-- .../node_request_handler.ts | 10 +- .../optimizely-sdk/tests/odpClient.spec.ts | 196 ++++++++++++++---- 3 files changed, 169 insertions(+), 73 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index 3bf9d5836..89dfb2c47 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -17,7 +17,7 @@ import { ErrorHandler, LogHandler, LogLevel, NoopErrorHandler } from '../../modules/logging'; import { QuerySegmentsParameters } from './query_segments_parameters'; import { NoOpLogger } from '../logger'; -import { Response } from '../../utils/http_request_handler/http'; +import { RequestHandler, Response } from '../../utils/http_request_handler/http'; import { RequestHandlerFactory } from '../../utils/http_request_handler/request_handler_factory'; import { REQUEST_TIMEOUT_MS } from '../../utils/http_request_handler/config'; @@ -28,30 +28,29 @@ export interface IOdpClient { querySegments(parameters: QuerySegmentsParameters): Promise; } -export enum ExecutionContextType { +enum ExecutionContextType { notDefined, browser, node, } export class OdpClient implements IOdpClient { - private readonly _errorHandler: ErrorHandler; private readonly _logger: LogHandler; private readonly _timeout: number; - private readonly _executionContextType: ExecutionContextType; - + private readonly _requestHandler: RequestHandler; - constructor(errorHandler?: ErrorHandler, logger?: LogHandler, timeout?: number) { + constructor(errorHandler?: ErrorHandler, logger?: LogHandler, requestHandler?: RequestHandler, timeout?: number) { this._errorHandler = errorHandler ?? new NoopErrorHandler(); this._logger = logger ?? new NoOpLogger(); this._timeout = timeout ?? REQUEST_TIMEOUT_MS; - this._executionContextType = ExecutionContextType.notDefined; - if (typeof window === 'object') { - this._executionContextType = ExecutionContextType.browser; - } else if (typeof process === 'object') { - this._executionContextType = ExecutionContextType.node; + if (requestHandler) { + this._requestHandler = requestHandler; + } else { + let executionContextType = typeof window === 'object' ? ExecutionContextType.browser : ExecutionContextType.notDefined; + executionContextType = typeof process === 'object' ? ExecutionContextType.node : executionContextType; + this._requestHandler = RequestHandlerFactory.createHandler(ExecutionContextType[executionContextType], this._logger, this._timeout); } } @@ -69,27 +68,18 @@ export class OdpClient implements IOdpClient { }; const data = parameters.toGraphQLJson(); - const requestHandler = RequestHandlerFactory.createHandler(ExecutionContextType[this._executionContextType], this._logger, this._timeout); - if (!requestHandler) { - this._logger.log(LogLevel.ERROR, 'Unable to determine execution context'); - return EMPTY_JSON_RESPONSE; - } - let response: Response; try { - response = await requestHandler.makeRequest(url, headers, method, data).responsePromise; + const request = this._requestHandler.makeRequest(url, headers, method, data); + response = await request.responsePromise; // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { this._errorHandler.handleError(error); + this._logger.log(LogLevel.ERROR, `${FETCH_FAILURE_MESSAGE} (${error.statusCode ?? 'network error'})`); return EMPTY_JSON_RESPONSE; } - if (response.statusCode !== 200) { - this._logger.log(LogLevel.ERROR, `${FETCH_FAILURE_MESSAGE} (${response.statusCode ?? 'network error'})`); - return EMPTY_JSON_RESPONSE; - } - return response.body; } } diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts index dd3de04ac..53181d960 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts @@ -24,12 +24,12 @@ import { LogHandler } from '../../modules/logging'; import { NoOpLogger } from '../../plugins/logger'; export class NodeRequestHandler implements RequestHandler { - private readonly logger: LogHandler; - private readonly timeout: number; + private readonly _logger: LogHandler; + private readonly _timeout: number; public constructor(logger?: LogHandler, timeout: number = REQUEST_TIMEOUT_MS) { - this.logger = logger ?? new NoOpLogger(); - this.timeout = timeout; + this._logger = logger ?? new NoOpLogger(); + this._timeout = timeout; } public makeRequest(reqUrl: string, headers: Headers, method: string, data?: string): AbortableRequest { @@ -103,7 +103,7 @@ export class NodeRequestHandler implements RequestHandler { const timeout = setTimeout(() => { request.abort(); reject(new Error('Request timed out')); - }, this.timeout); + }, this._timeout); request.once('response', (incomingMessage: http.IncomingMessage) => { if (request.destroyed) { diff --git a/packages/optimizely-sdk/tests/odpClient.spec.ts b/packages/optimizely-sdk/tests/odpClient.spec.ts index a79e8a870..4e1a1e97e 100644 --- a/packages/optimizely-sdk/tests/odpClient.spec.ts +++ b/packages/optimizely-sdk/tests/odpClient.spec.ts @@ -16,10 +16,12 @@ /// -import { anyString, anything, instance, mock, resetCalls, verify } from 'ts-mockito'; +import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; import { ErrorHandler, LogHandler, LogLevel } from '../lib/modules/logging'; import { OdpClient } from '../lib/plugins/odp/odp_client'; import { QuerySegmentsParameters } from '../lib/plugins/odp/query_segments_parameters'; +import { BrowserRequestHandler } from '../lib/utils/http_request_handler/browser_request_handler'; +import { NodeRequestHandler } from '../lib/utils/http_request_handler/node_request_handler'; describe('OdpClient', () => { const MOCK_QUERY_PARAMETERS = new QuerySegmentsParameters({ @@ -33,53 +35,46 @@ describe('OdpClient', () => { 'push_on_sale', ], }); - - const makeClientInstance = () => new OdpClient(instance(mockErrorHandler), instance(mockLogger)); + const VALID_RESPONSE_JSON = { + 'data': { + 'customer': { + 'audiences': { + 'edges': [ + { + 'node': { + 'name': 'has_email', + 'state': 'qualified', + }, + }, + { + 'node': { + 'name': 'has_email_opted_in', + 'state': 'qualified', + }, + }, + ], + }, + }, + }, + }; let mockErrorHandler: ErrorHandler; let mockLogger: LogHandler; + let mockBrowserRequestHandler: BrowserRequestHandler; + let mockNodeRequestHandler: NodeRequestHandler; beforeAll(() => { mockErrorHandler = mock(); mockLogger = mock(); + mockBrowserRequestHandler = mock(); + mockNodeRequestHandler = mock(); }); beforeEach(() => { resetCalls(mockErrorHandler); resetCalls(mockLogger); - }); - - it('should get mocked segments successfully', async () => { - const responseJson = { - 'data': { - 'customer': { - 'audiences': { - 'edges': [ - { - 'node': { - 'name': 'has_email', - 'state': 'qualified', - }, - }, - { - 'node': { - 'name': 'has_email_opted_in', - 'state': 'qualified', - }, - }, - ], - }, - }, - }, - }; - //mockAxios.onPost(/.*/).reply(200, responseJson); - const client = makeClientInstance(); - - const response = await client.querySegments(MOCK_QUERY_PARAMETERS); - - expect(response).toEqual(responseJson); - verify(mockErrorHandler.handleError(anything())).never(); - verify(mockLogger.log(anything(), anyString())).never(); + resetCalls(mockBrowserRequestHandler); + resetCalls(mockNodeRequestHandler); }); it('should handle missing API Host', async () => { @@ -90,7 +85,7 @@ describe('OdpClient', () => { userValue: 'userValue', segmentsToCheck: ['segmentToCheck'], }); - const client = makeClientInstance(); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger)); await client.querySegments(missingApiHost); @@ -106,7 +101,7 @@ describe('OdpClient', () => { userValue: 'userValue', segmentsToCheck: ['segmentToCheck'], }); - const client = makeClientInstance(); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger)); await client.querySegments(missingApiHost); @@ -114,9 +109,84 @@ describe('OdpClient', () => { verify(mockLogger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments')).once(); }); - it('should handle 400 HTTP response', async () => { - //mockAxios.onPost(/.*/).reply(400, { throwAway: 'data' }); - const client = makeClientInstance(); + it('Browser: should get mocked segments successfully', async () => { + when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.resolve({ + statusCode: 200, + body: JSON.stringify(VALID_RESPONSE_JSON), + headers: { + 'Content-Type': 'application/json', + }, + }), + }); + + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); + + const response = await client.querySegments(MOCK_QUERY_PARAMETERS) ?? ''; + + expect(response).toEqual(JSON.stringify(VALID_RESPONSE_JSON)); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(anything(), anyString())).never(); + }); + + it('Node: should get mocked segments successfully', async () => { + when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.resolve({ + statusCode: 200, + body: JSON.stringify(VALID_RESPONSE_JSON), + headers: { + 'Content-Type': 'application/json', + }, + }), + }); + + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); + + const response = await client.querySegments(MOCK_QUERY_PARAMETERS) ?? ''; + + expect(response).toEqual(JSON.stringify(VALID_RESPONSE_JSON)); + verify(mockErrorHandler.handleError(anything())).never(); + verify(mockLogger.log(anything(), anyString())).never(); + }); + + it('Browser should handle 400 HTTP response', async () => { + when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.reject({ + statusCode: 400, + body: '', + headers: { + 'Content-Type': 'application/json', + }, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); + + const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); + + expect(responseJson).toBeNull(); + verify(mockErrorHandler.handleError(anything())).once(); + verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (400)')).once(); + }); + + it('Node should handle 400 HTTP response', async () => { + when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.reject({ + statusCode: 400, + body: '', + headers: { + 'Content-Type': 'application/json', + }, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); @@ -125,9 +195,40 @@ describe('OdpClient', () => { verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (400)')).once(); }); - it('should handle 500 HTTP response', async () => { - //mockAxios.onPost(/.*/).reply(500, { throwAway: 'data' }); - const client = makeClientInstance(); + it('Browser should handle 500 HTTP response', async () => { + when(mockBrowserRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.reject({ + statusCode: 500, + body: '', + headers: { + 'Content-Type': 'application/json', + }, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); + + const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); + + expect(responseJson).toBeNull(); + verify(mockErrorHandler.handleError(anything())).once(); + verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (500)')).once(); + }); + + it('Node should handle 500 HTTP response', async () => { + when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.reject({ + statusCode: 500, + body: '', + headers: { + 'Content-Type': 'application/json', + }, + }), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); @@ -137,7 +238,12 @@ describe('OdpClient', () => { }); it('should handle a network timeout', async () => { - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), 1); + when(mockNodeRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ + abort: () => { + }, + responsePromise: Promise.reject(new Error('Request timed out')), + }); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler), 10); const responseJson = await client.querySegments(MOCK_QUERY_PARAMETERS); From 738f9ec6d134b20d490cdf477d3eecd0d8f122a8 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 29 Aug 2022 17:24:45 -0400 Subject: [PATCH 31/42] WIP nodeRequestHandler tests --- packages/optimizely-sdk/package-lock.json | 259 ++---------------- packages/optimizely-sdk/package.json | 2 +- .../tests/nodeRequestHandler.spec.ts | 229 ++++++++++++++++ .../optimizely-sdk/tests/odpClient.spec.ts | 24 +- packages/optimizely-sdk/tests/testUtils.ts | 29 ++ 5 files changed, 288 insertions(+), 255 deletions(-) create mode 100644 packages/optimizely-sdk/tests/nodeRequestHandler.spec.ts create mode 100644 packages/optimizely-sdk/tests/testUtils.ts diff --git a/packages/optimizely-sdk/package-lock.json b/packages/optimizely-sdk/package-lock.json index 28932dd05..ee42c4a7c 100644 --- a/packages/optimizely-sdk/package-lock.json +++ b/packages/optimizely-sdk/package-lock.json @@ -40,7 +40,7 @@ "lodash": "^4.17.11", "mocha": "^5.2.0", "mocha-lcov-reporter": "^1.3.0", - "nock": "^7.7.2", + "nock": "^13.2.9", "nyc": "^15.0.1", "promise-polyfill": "8.1.0", "rollup": "2.2.0", @@ -7276,23 +7276,6 @@ "node": ">=0.12" } }, - "node_modules/deep-equal": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", - "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", - "dev": true, - "dependencies": { - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.1", - "is-regex": "^1.0.4", - "object-is": "^1.0.1", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.2.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -10047,22 +10030,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -14844,87 +14811,18 @@ } }, "node_modules/nock": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/nock/-/nock-7.7.3.tgz", - "integrity": "sha512-YZawhm3nkypLhoVpQq/8h1CilR2/B2vuSrA7cM/8wlZsMYi/czkENRR05MO2rI6vhPT4YCVjbP8R8C0uhY80lw==", + "version": "13.2.9", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.2.9.tgz", + "integrity": "sha512-1+XfJNYF1cjGB+TKMWi29eZ0b82QOvQs2YoLNzbpWGqFMtRQHTa57osqdGj4FrFPgkO4D4AZinzUJR9VvW3QUA==", "dev": true, - "engines": [ - "node >= 0.10.0" - ], "dependencies": { - "chai": ">=1.9.2 <4.0.0", - "debug": "^2.2.0", - "deep-equal": "^1.0.0", + "debug": "^4.1.0", "json-stringify-safe": "^5.0.1", - "lodash": "^3.10.1", - "mkdirp": "^0.5.0", - "propagate": "0.3.x", - "qs": "^6.0.2" - } - }, - "node_modules/nock/node_modules/chai": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", - "integrity": "sha512-eRYY0vPS2a9zt5w5Z0aCeWbrXTEyvk7u/Xf71EzNObrjSCPgMm1Nku/D/u2tiqHBX5j40wWhj54YJLtgn8g55A==", - "dev": true, - "dependencies": { - "assertion-error": "^1.0.1", - "deep-eql": "^0.1.3", - "type-detect": "^1.0.0" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/nock/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/nock/node_modules/deep-eql": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", - "integrity": "sha512-6sEotTRGBFiNcqVoeHwnfopbSpi5NbH1VWJmYCVkmxMmaVTT0bUTrNaGyBwhgP4MZL012W/mkzIn3Da+iDYweg==", - "dev": true, - "dependencies": { - "type-detect": "0.1.1" + "lodash": "^4.17.21", + "propagate": "^2.0.0" }, "engines": { - "node": "*" - } - }, - "node_modules/nock/node_modules/deep-eql/node_modules/type-detect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", - "integrity": "sha512-5rqszGVwYgBoDkIm2oUtvkfZMQ0vk29iDMU0W2qCa3rG0vPDNczCMT4hV/bLBgLg8k8ri6+u3Zbt+S/14eMzlA==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/nock/node_modules/lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha512-9mDDwqVIma6OZX79ZlDACZl8sBm0TEnkf99zV3iMA4GzkIT/9hiqP5mY0HoT1iNLCrKc/R1HByV+yJfRWVJryQ==", - "dev": true - }, - "node_modules/nock/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/nock/node_modules/type-detect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", - "integrity": "sha512-f9Uv6ezcpvCQjJU0Zqbg+65qdcszv3qUQsZfjdRbWiZ7AMenrX1u0lNk9EoWWX6e1F+NULyg27mtdeZ5WhpljA==", - "dev": true, - "engines": { - "node": "*" + "node": ">= 10.13" } }, "node_modules/node-dir": { @@ -15603,22 +15501,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -16378,13 +16260,13 @@ } }, "node_modules/propagate": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-0.3.1.tgz", - "integrity": "sha512-GOc8Eoa3MWbN905b5l2PNNt1Pf+I/CF6uAPt3IGT+v9WExDE7WPT/kDfLe7vYXtG11KbTnBhTNfcoq9+umDrSw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", "dev": true, - "engines": [ - "node >= 0.8.1" - ] + "engines": { + "node": ">= 8" + } }, "node_modules/ps-tree": { "version": "1.2.0", @@ -26066,20 +25948,6 @@ "type-detect": "^4.0.0" } }, - "deep-equal": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", - "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", - "dev": true, - "requires": { - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.1", - "is-regex": "^1.0.4", - "object-is": "^1.0.1", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.2.0" - } - }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -28179,16 +28047,6 @@ "kind-of": "^3.0.2" } }, - "is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -32083,76 +31941,15 @@ "peer": true }, "nock": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/nock/-/nock-7.7.3.tgz", - "integrity": "sha512-YZawhm3nkypLhoVpQq/8h1CilR2/B2vuSrA7cM/8wlZsMYi/czkENRR05MO2rI6vhPT4YCVjbP8R8C0uhY80lw==", + "version": "13.2.9", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.2.9.tgz", + "integrity": "sha512-1+XfJNYF1cjGB+TKMWi29eZ0b82QOvQs2YoLNzbpWGqFMtRQHTa57osqdGj4FrFPgkO4D4AZinzUJR9VvW3QUA==", "dev": true, "requires": { - "chai": ">=1.9.2 <4.0.0", - "debug": "^2.2.0", - "deep-equal": "^1.0.0", + "debug": "^4.1.0", "json-stringify-safe": "^5.0.1", - "lodash": "^3.10.1", - "mkdirp": "^0.5.0", - "propagate": "0.3.x", - "qs": "^6.0.2" - }, - "dependencies": { - "chai": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", - "integrity": "sha512-eRYY0vPS2a9zt5w5Z0aCeWbrXTEyvk7u/Xf71EzNObrjSCPgMm1Nku/D/u2tiqHBX5j40wWhj54YJLtgn8g55A==", - "dev": true, - "requires": { - "assertion-error": "^1.0.1", - "deep-eql": "^0.1.3", - "type-detect": "^1.0.0" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-eql": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", - "integrity": "sha512-6sEotTRGBFiNcqVoeHwnfopbSpi5NbH1VWJmYCVkmxMmaVTT0bUTrNaGyBwhgP4MZL012W/mkzIn3Da+iDYweg==", - "dev": true, - "requires": { - "type-detect": "0.1.1" - }, - "dependencies": { - "type-detect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", - "integrity": "sha512-5rqszGVwYgBoDkIm2oUtvkfZMQ0vk29iDMU0W2qCa3rG0vPDNczCMT4hV/bLBgLg8k8ri6+u3Zbt+S/14eMzlA==", - "dev": true - } - } - }, - "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha512-9mDDwqVIma6OZX79ZlDACZl8sBm0TEnkf99zV3iMA4GzkIT/9hiqP5mY0HoT1iNLCrKc/R1HByV+yJfRWVJryQ==", - "dev": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "type-detect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", - "integrity": "sha512-f9Uv6ezcpvCQjJU0Zqbg+65qdcszv3qUQsZfjdRbWiZ7AMenrX1u0lNk9EoWWX6e1F+NULyg27mtdeZ5WhpljA==", - "dev": true - } + "lodash": "^4.17.21", + "propagate": "^2.0.0" } }, "node-dir": { @@ -32682,16 +32479,6 @@ "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", "dev": true }, - "object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -33274,9 +33061,9 @@ } }, "propagate": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-0.3.1.tgz", - "integrity": "sha512-GOc8Eoa3MWbN905b5l2PNNt1Pf+I/CF6uAPt3IGT+v9WExDE7WPT/kDfLe7vYXtG11KbTnBhTNfcoq9+umDrSw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", "dev": true }, "ps-tree": { diff --git a/packages/optimizely-sdk/package.json b/packages/optimizely-sdk/package.json index 06bd03479..d86db89b9 100644 --- a/packages/optimizely-sdk/package.json +++ b/packages/optimizely-sdk/package.json @@ -71,7 +71,7 @@ "lodash": "^4.17.11", "mocha": "^5.2.0", "mocha-lcov-reporter": "^1.3.0", - "nock": "^7.7.2", + "nock": "^13.2.9", "nyc": "^15.0.1", "promise-polyfill": "8.1.0", "rollup": "2.2.0", diff --git a/packages/optimizely-sdk/tests/nodeRequestHandler.spec.ts b/packages/optimizely-sdk/tests/nodeRequestHandler.spec.ts new file mode 100644 index 000000000..6a2de39f6 --- /dev/null +++ b/packages/optimizely-sdk/tests/nodeRequestHandler.spec.ts @@ -0,0 +1,229 @@ +/** + * Copyright 2019-2020, 2022 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/// + +import nock from 'nock'; +import zlib from 'zlib'; +import { NodeRequestHandler } from '../lib/utils/http_request_handler/node_request_handler'; +import { advanceTimersByTime } from './testUtils'; +import { NoOpLogger } from '../lib/plugins/logger'; + +beforeAll(() => { + nock.disableNetConnect(); +}); + +afterAll(() => { + nock.enableNetConnect(); +}); + +describe('NodeRequestHandler', () => { + const host = 'https://endpoint.example.com'; + const path = '/api/query'; + + afterEach(async () => { + nock.cleanAll(); + }); + + describe('makeRequest', () => { + it('should handle a 200 response back from a post', async () => { + const data = '{"foo":"bar"}'; + const scope = nock(host) + .post(path) + .reply(200, data); + + const request = new NodeRequestHandler().makeRequest(`${host}${path}`, {}, 'post', data); + const response = await request.responsePromise; + + expect(response).toEqual({ + statusCode: 200, + body: data, + headers: {}, + }); + scope.done(); + }); + + it('should handle a 400 response back ', async () => { + const scope = nock(host) + .post(path) + .reply(400, ''); + + const request = new NodeRequestHandler().makeRequest(`${host}${path}`, {}, 'post'); + const response = await request.responsePromise; + + expect(response).toEqual({ + statusCode: 400, + body: '', + headers: {}, + }); + scope.done(); + }); + + it('should include headers from the headers argument in the request', async () => { + const scope = nock(host) + .matchHeader('if-modified-since', 'Fri, 08 Mar 2019 18:57:18 GMT') + .get(path) + .reply(304, ''); + const rrequestq = new NodeRequestHandler().makeRequest(`${host}${path}`, { + 'if-modified-since': 'Fri, 08 Mar 2019 18:57:18 GMT', + }, 'get'); + const response = await rrequestq.responsePromise; + expect(response).toEqual({ + statusCode: 304, + body: '', + headers: {}, + }); + scope.done(); + }); + + it('should add Accept-Encoding request header and unzips a gzipped response body', async () => { + const data = '{"foo":"bar"}'; + const scope = nock(host) + .matchHeader('accept-encoding', 'gzip,deflate') + .get(path) + .reply(200, () => zlib.gzipSync(data), { 'content-encoding': 'gzip' }); + + const request = new NodeRequestHandler().makeRequest(`${host}${path}`, {}, 'get'); + const response = await request.responsePromise; + + expect(response).toMatchObject({ + statusCode: 200, + body: data, + }); + scope.done(); + }); + + it('should include headers from the response in the eventual response in the return value', async () => { + const scope = nock(host) + .get(path) + .reply( + 200, + { foo: 'bar' }, + { + 'last-modified': 'Fri, 08 Mar 2019 18:57:18 GMT', + }, + ); + + const request = new NodeRequestHandler().makeRequest(`${host}${path}`, {}, 'get'); + const response = await request.responsePromise; + + expect(response).toEqual({ + statusCode: 200, + body: '{"foo":"bar"}', + headers: { + 'content-type': 'application/json', + 'last-modified': 'Fri, 08 Mar 2019 18:57:18 GMT', + }, + }); + scope.done(); + }); + + it('should handle a URL with a query string', async () => { + const pathWithQuery = '/datafiles/123.json?from_my_app=true'; + const scope = nock(host) + .get(pathWithQuery) + .reply(200, { foo: 'bar' }); + + const request = new NodeRequestHandler().makeRequest(`${host}${pathWithQuery}`, {}, 'get'); + await request.responsePromise; + + scope.done(); + }); + + it('should throw error for a URL with http protocol (not https)', async () => { + const invalidHttpProtocolUrl = 'http://some.example.com'; + + const request = new NodeRequestHandler().makeRequest(invalidHttpProtocolUrl, {}, 'get'); + + await expect(request.responsePromise).rejects.toThrow(); + }); + + it('should returns a rejected response promise when the URL protocol is unsupported', async () => { + const invalidProtocolUrl = 'ftp://something/datafiles/123.json'; + + const request = new NodeRequestHandler().makeRequest(invalidProtocolUrl, {}, 'get'); + + await expect(request.responsePromise).rejects.toThrow(); + }); + + it('should return a rejected promise when there is a request error', async () => { + const scope = nock(host) + .get(path) + .replyWithError({ + message: 'Connection error', + code: 'CONNECTION_ERROR', + }); + const req = new NodeRequestHandler().makeRequest(`${host}${path}`, {}, 'get'); + await expect(req.responsePromise).rejects.toThrow(); + scope.done(); + }); + + it('should handle a url with a host and a port', async () => { + const hostWithPort = 'https://datafiles:44311'; + const path = '/12/345.json'; + const scope = nock(hostWithPort) + .get(path) + .reply(200, '{"foo":"bar"}'); + + const req = new NodeRequestHandler().makeRequest(`${hostWithPort}${path}`, {}, 'get'); + const resp = await req.responsePromise; + + expect(resp).toEqual({ + statusCode: 200, + body: '{"foo":"bar"}', + headers: {}, + }); + scope.done(); + }); + + xdescribe('timeout', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + it('should reject the response promise and abort the request when the response is not received before the timeout', async () => { + const scope = nock(host) + .get(path) + .delay(61000) + .reply(200, '{"foo":"bar"}'); + + const abortEventListener = jest.fn(); + let emittedReq: any; + const requestListener = (request: any): void => { + emittedReq = request; + emittedReq.once('abort', abortEventListener); + }; + scope.on('request', requestListener); + + const request = new NodeRequestHandler(new NoOpLogger(), 10).makeRequest(`${host}${path}`, {}, 'get'); + await advanceTimersByTime(60000); + + await expect(request.responsePromise).rejects.toThrow(); + expect(abortEventListener).toBeCalledTimes(1); + + scope.done(); + if (emittedReq) { + emittedReq.off('abort', abortEventListener); + } + scope.off('request', requestListener); + }); + }); + }); +}); diff --git a/packages/optimizely-sdk/tests/odpClient.spec.ts b/packages/optimizely-sdk/tests/odpClient.spec.ts index 4e1a1e97e..a89840fb9 100644 --- a/packages/optimizely-sdk/tests/odpClient.spec.ts +++ b/packages/optimizely-sdk/tests/odpClient.spec.ts @@ -116,9 +116,7 @@ describe('OdpClient', () => { responsePromise: Promise.resolve({ statusCode: 200, body: JSON.stringify(VALID_RESPONSE_JSON), - headers: { - 'Content-Type': 'application/json', - }, + headers: {}, }), }); @@ -138,9 +136,7 @@ describe('OdpClient', () => { responsePromise: Promise.resolve({ statusCode: 200, body: JSON.stringify(VALID_RESPONSE_JSON), - headers: { - 'Content-Type': 'application/json', - }, + headers: {}, }), }); @@ -160,9 +156,7 @@ describe('OdpClient', () => { responsePromise: Promise.reject({ statusCode: 400, body: '', - headers: { - 'Content-Type': 'application/json', - }, + headers: {}, }), }); const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); @@ -181,9 +175,7 @@ describe('OdpClient', () => { responsePromise: Promise.reject({ statusCode: 400, body: '', - headers: { - 'Content-Type': 'application/json', - }, + headers: {}, }), }); const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); @@ -202,9 +194,7 @@ describe('OdpClient', () => { responsePromise: Promise.reject({ statusCode: 500, body: '', - headers: { - 'Content-Type': 'application/json', - }, + headers: {}, }), }); const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockBrowserRequestHandler)); @@ -223,9 +213,7 @@ describe('OdpClient', () => { responsePromise: Promise.reject({ statusCode: 500, body: '', - headers: { - 'Content-Type': 'application/json', - }, + headers: {}, }), }); const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); diff --git a/packages/optimizely-sdk/tests/testUtils.ts b/packages/optimizely-sdk/tests/testUtils.ts new file mode 100644 index 000000000..9dcf23796 --- /dev/null +++ b/packages/optimizely-sdk/tests/testUtils.ts @@ -0,0 +1,29 @@ +/** + * Copyright 2019-2020, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/// + +export function advanceTimersByTime(waitMs: number): Promise { + const timeoutPromise: Promise = new Promise(res => setTimeout(res, waitMs)); + jest.advanceTimersByTime(waitMs); + return timeoutPromise; +} + +export function getTimerCount(): number { + // Type definition for jest doesn't include this, but it exists + // https://jestjs.io/docs/en/jest-object#jestgettimercount + return (jest as any).getTimerCount(); +} From 807e257bf99e57553b0cc41271c81b61de5d1a35 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 30 Aug 2022 08:41:36 -0400 Subject: [PATCH 32/42] Handle node request timeouts with tests --- .../node_request_handler.ts | 49 ++++++++----------- .../tests/nodeRequestHandler.spec.ts | 43 ++++++++-------- packages/optimizely-sdk/tests/testUtils.ts | 29 ----------- 3 files changed, 41 insertions(+), 80 deletions(-) delete mode 100644 packages/optimizely-sdk/tests/testUtils.ts diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts index 53181d960..ce49763a1 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts @@ -38,7 +38,7 @@ export class NodeRequestHandler implements RequestHandler { if (parsedUrl.protocol !== 'https:') { return { responsePromise: Promise.reject(new Error(`Unsupported protocol: ${parsedUrl.protocol}`)), - abort(): void { + abort: () => { }, }; } @@ -50,14 +50,17 @@ export class NodeRequestHandler implements RequestHandler { ...headers, 'accept-encoding': 'gzip,deflate', }, + timeout: this._timeout, }); const responsePromise = this.getResponseFromRequest(request); - request.write(data); + if (data) { + request.write(data); + } request.end(); return { - abort: () => request.abort(), + abort: () => request.destroy(), responsePromise, }; } @@ -71,14 +74,6 @@ export class NodeRequestHandler implements RequestHandler { }; } - /** - * Convert IncomingMessage.headers (which has type http.IncomingHttpHeaders) into our Headers type defined in src/http.ts. - * - * Our Headers type is simplified and can't represent multiple values for the same header name. - * - * We don't currently need multiple values support, and the consumer code becomes simpler if it can assume at-most 1 value - * per header name. - */ private createHeadersFromNodeIncomingMessage(incomingMessage: http.IncomingMessage): Headers { const headers: Headers = {}; Object.keys(incomingMessage.headers).forEach(headerName => { @@ -100,10 +95,21 @@ export class NodeRequestHandler implements RequestHandler { private getResponseFromRequest(request: http.ClientRequest): Promise { return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - request.abort(); + request.on('timeout', () => { + request.destroy(); reject(new Error('Request timed out')); - }, this._timeout); + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + request.on('error', (err: any) => { + if (err instanceof Error) { + reject(err); + } else if (typeof err === 'string') { + reject(new Error(err)); + } else { + reject(new Error('Request error')); + } + }); request.once('response', (incomingMessage: http.IncomingMessage) => { if (request.destroyed) { @@ -126,8 +132,6 @@ export class NodeRequestHandler implements RequestHandler { return; } - clearTimeout(timeout); - resolve({ statusCode: incomingMessage.statusCode, body: responseData, @@ -135,19 +139,6 @@ export class NodeRequestHandler implements RequestHandler { }); }); }); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - request.on('error', (err: any) => { - clearTimeout(timeout); - - if (err instanceof Error) { - reject(err); - } else if (typeof err === 'string') { - reject(new Error(err)); - } else { - reject(new Error('Request error')); - } - }); }); } } diff --git a/packages/optimizely-sdk/tests/nodeRequestHandler.spec.ts b/packages/optimizely-sdk/tests/nodeRequestHandler.spec.ts index 6a2de39f6..e48267afb 100644 --- a/packages/optimizely-sdk/tests/nodeRequestHandler.spec.ts +++ b/packages/optimizely-sdk/tests/nodeRequestHandler.spec.ts @@ -19,7 +19,6 @@ import nock from 'nock'; import zlib from 'zlib'; import { NodeRequestHandler } from '../lib/utils/http_request_handler/node_request_handler'; -import { advanceTimersByTime } from './testUtils'; import { NoOpLogger } from '../lib/plugins/logger'; beforeAll(() => { @@ -33,6 +32,7 @@ afterAll(() => { describe('NodeRequestHandler', () => { const host = 'https://endpoint.example.com'; const path = '/api/query'; + const body = '{"foo":"bar"}'; afterEach(async () => { nock.cleanAll(); @@ -40,17 +40,16 @@ describe('NodeRequestHandler', () => { describe('makeRequest', () => { it('should handle a 200 response back from a post', async () => { - const data = '{"foo":"bar"}'; const scope = nock(host) .post(path) - .reply(200, data); + .reply(200, body); - const request = new NodeRequestHandler().makeRequest(`${host}${path}`, {}, 'post', data); + const request = new NodeRequestHandler().makeRequest(`${host}${path}`, {}, 'post', body); const response = await request.responsePromise; expect(response).toEqual({ statusCode: 200, - body: data, + body, headers: {}, }); scope.done(); @@ -77,10 +76,10 @@ describe('NodeRequestHandler', () => { .matchHeader('if-modified-since', 'Fri, 08 Mar 2019 18:57:18 GMT') .get(path) .reply(304, ''); - const rrequestq = new NodeRequestHandler().makeRequest(`${host}${path}`, { + const request = new NodeRequestHandler().makeRequest(`${host}${path}`, { 'if-modified-since': 'Fri, 08 Mar 2019 18:57:18 GMT', }, 'get'); - const response = await rrequestq.responsePromise; + const response = await request.responsePromise; expect(response).toEqual({ statusCode: 304, body: '', @@ -90,18 +89,17 @@ describe('NodeRequestHandler', () => { }); it('should add Accept-Encoding request header and unzips a gzipped response body', async () => { - const data = '{"foo":"bar"}'; const scope = nock(host) .matchHeader('accept-encoding', 'gzip,deflate') .get(path) - .reply(200, () => zlib.gzipSync(data), { 'content-encoding': 'gzip' }); + .reply(200, () => zlib.gzipSync(body), { 'content-encoding': 'gzip' }); const request = new NodeRequestHandler().makeRequest(`${host}${path}`, {}, 'get'); const response = await request.responsePromise; expect(response).toMatchObject({ statusCode: 200, - body: data, + body: body, }); scope.done(); }); @@ -111,7 +109,7 @@ describe('NodeRequestHandler', () => { .get(path) .reply( 200, - { foo: 'bar' }, + JSON.parse(body), { 'last-modified': 'Fri, 08 Mar 2019 18:57:18 GMT', }, @@ -122,7 +120,7 @@ describe('NodeRequestHandler', () => { expect(response).toEqual({ statusCode: 200, - body: '{"foo":"bar"}', + body, headers: { 'content-type': 'application/json', 'last-modified': 'Fri, 08 Mar 2019 18:57:18 GMT', @@ -135,7 +133,7 @@ describe('NodeRequestHandler', () => { const pathWithQuery = '/datafiles/123.json?from_my_app=true'; const scope = nock(host) .get(pathWithQuery) - .reply(200, { foo: 'bar' }); + .reply(200, JSON.parse(body)); const request = new NodeRequestHandler().makeRequest(`${host}${pathWithQuery}`, {}, 'get'); await request.responsePromise; @@ -176,20 +174,20 @@ describe('NodeRequestHandler', () => { const path = '/12/345.json'; const scope = nock(hostWithPort) .get(path) - .reply(200, '{"foo":"bar"}'); + .reply(200, body); const req = new NodeRequestHandler().makeRequest(`${hostWithPort}${path}`, {}, 'get'); const resp = await req.responsePromise; expect(resp).toEqual({ statusCode: 200, - body: '{"foo":"bar"}', + body, headers: {}, }); scope.done(); }); - xdescribe('timeout', () => { + describe('timeout', () => { beforeEach(() => { jest.useFakeTimers(); }); @@ -201,26 +199,27 @@ describe('NodeRequestHandler', () => { it('should reject the response promise and abort the request when the response is not received before the timeout', async () => { const scope = nock(host) .get(path) - .delay(61000) - .reply(200, '{"foo":"bar"}'); + .delay({ head: 2000, body: 2000 }) + .reply(200, body); const abortEventListener = jest.fn(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any let emittedReq: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const requestListener = (request: any): void => { emittedReq = request; - emittedReq.once('abort', abortEventListener); + emittedReq.once('timeout', abortEventListener); }; scope.on('request', requestListener); - const request = new NodeRequestHandler(new NoOpLogger(), 10).makeRequest(`${host}${path}`, {}, 'get'); - await advanceTimersByTime(60000); + const request = new NodeRequestHandler(new NoOpLogger(), 100).makeRequest(`${host}${path}`, {}, 'get'); await expect(request.responsePromise).rejects.toThrow(); expect(abortEventListener).toBeCalledTimes(1); scope.done(); if (emittedReq) { - emittedReq.off('abort', abortEventListener); + emittedReq.off('timeout', abortEventListener); } scope.off('request', requestListener); }); diff --git a/packages/optimizely-sdk/tests/testUtils.ts b/packages/optimizely-sdk/tests/testUtils.ts deleted file mode 100644 index 9dcf23796..000000000 --- a/packages/optimizely-sdk/tests/testUtils.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright 2019-2020, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/// - -export function advanceTimersByTime(waitMs: number): Promise { - const timeoutPromise: Promise = new Promise(res => setTimeout(res, waitMs)); - jest.advanceTimersByTime(waitMs); - return timeoutPromise; -} - -export function getTimerCount(): number { - // Type definition for jest doesn't include this, but it exists - // https://jestjs.io/docs/en/jest-object#jestgettimercount - return (jest as any).getTimerCount(); -} From c3c8540015cc173200d365f878fe3c2fb753b561 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 30 Aug 2022 09:06:17 -0400 Subject: [PATCH 33/42] BroswerRequestHandler tests --- packages/optimizely-sdk/package-lock.json | 100 ++++++++++++ packages/optimizely-sdk/package.json | 2 + .../tests/browserRequestHandler.spec.ts | 142 ++++++++++++++++++ 3 files changed, 244 insertions(+) create mode 100644 packages/optimizely-sdk/tests/browserRequestHandler.spec.ts diff --git a/packages/optimizely-sdk/package-lock.json b/packages/optimizely-sdk/package-lock.json index ee42c4a7c..64a02bde5 100644 --- a/packages/optimizely-sdk/package-lock.json +++ b/packages/optimizely-sdk/package-lock.json @@ -22,6 +22,7 @@ "@types/chai": "^4.2.11", "@types/jest": "^23.3.12", "@types/mocha": "^5.2.7", + "@types/nise": "^1.4.0", "@types/uuid": "^3.4.4", "@typescript-eslint/eslint-plugin": "^5.33.0", "@typescript-eslint/parser": "^5.33.0", @@ -40,6 +41,7 @@ "lodash": "^4.17.11", "mocha": "^5.2.0", "mocha-lcov-reporter": "^1.3.0", + "nise": "^5.1.1", "nock": "^13.2.9", "nyc": "^15.0.1", "promise-polyfill": "8.1.0", @@ -4148,6 +4150,30 @@ "devOptional": true, "peer": true }, + "node_modules/@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", + "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "node_modules/@types/chai": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.3.tgz", @@ -4253,6 +4279,12 @@ "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", "dev": true }, + "node_modules/@types/nise": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@types/nise/-/nise-1.4.0.tgz", + "integrity": "sha512-DPxmjiDwubsNmguG5X4fEJ+XCyzWM3GXWsqQlvUcjJKa91IOoJUy51meDr0GkzK64qqNcq85ymLlyjoct9tInw==", + "dev": true + }, "node_modules/@types/node": { "version": "18.7.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.2.tgz", @@ -11864,6 +11896,12 @@ "node": ">=0.6.0" } }, + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, "node_modules/karma": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.0.tgz", @@ -14800,6 +14838,19 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "devOptional": true }, + "node_modules/nise": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.1.tgz", + "integrity": "sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": ">=5", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, "node_modules/nocache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/nocache/-/nocache-3.0.4.tgz", @@ -23388,6 +23439,30 @@ "devOptional": true, "peer": true }, + "@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", + "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "@types/chai": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.3.tgz", @@ -23493,6 +23568,12 @@ "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", "dev": true }, + "@types/nise": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@types/nise/-/nise-1.4.0.tgz", + "integrity": "sha512-DPxmjiDwubsNmguG5X4fEJ+XCyzWM3GXWsqQlvUcjJKa91IOoJUy51meDr0GkzK64qqNcq85ymLlyjoct9tInw==", + "dev": true + }, "@types/node": { "version": "18.7.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.2.tgz", @@ -29573,6 +29654,12 @@ "verror": "1.10.0" } }, + "just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, "karma": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.0.tgz", @@ -31933,6 +32020,19 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "devOptional": true }, + "nise": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.1.tgz", + "integrity": "sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": ">=5", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, "nocache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/nocache/-/nocache-3.0.4.tgz", diff --git a/packages/optimizely-sdk/package.json b/packages/optimizely-sdk/package.json index d86db89b9..f636bad51 100644 --- a/packages/optimizely-sdk/package.json +++ b/packages/optimizely-sdk/package.json @@ -53,6 +53,7 @@ "@types/chai": "^4.2.11", "@types/jest": "^23.3.12", "@types/mocha": "^5.2.7", + "@types/nise": "^1.4.0", "@types/uuid": "^3.4.4", "@typescript-eslint/eslint-plugin": "^5.33.0", "@typescript-eslint/parser": "^5.33.0", @@ -71,6 +72,7 @@ "lodash": "^4.17.11", "mocha": "^5.2.0", "mocha-lcov-reporter": "^1.3.0", + "nise": "^5.1.1", "nock": "^13.2.9", "nyc": "^15.0.1", "promise-polyfill": "8.1.0", diff --git a/packages/optimizely-sdk/tests/browserRequestHandler.spec.ts b/packages/optimizely-sdk/tests/browserRequestHandler.spec.ts new file mode 100644 index 000000000..e11215abb --- /dev/null +++ b/packages/optimizely-sdk/tests/browserRequestHandler.spec.ts @@ -0,0 +1,142 @@ +/** + * Copyright 2019-2020, 2022 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/// + +import { FakeXMLHttpRequest, FakeXMLHttpRequestStatic, fakeXhr } from 'nise'; +import { BrowserRequestHandler } from '../lib/utils/http_request_handler/browser_request_handler'; +import { NoOpLogger } from '../lib/plugins/logger'; + +describe('BrowserRequestHandler', () => { + const host = 'https://endpoint.example.com/api/query'; + const body = '{"foo":"bar"}'; + const dateString = 'Fri, 08 Mar 2019 18:57:18 GMT'; + + describe('makeRequest', () => { + let mockXHR: FakeXMLHttpRequestStatic; + let xhrs: FakeXMLHttpRequest[]; + + beforeEach(() => { + xhrs = []; + mockXHR = fakeXhr.useFakeXMLHttpRequest(); + mockXHR.onCreate = (request): number => xhrs.push(request); + }); + + afterEach(() => { + mockXHR.restore(); + }); + + it('should make a GET request to the argument URL', async () => { + const request = new BrowserRequestHandler().makeRequest(host, {}, 'get'); + + expect(xhrs.length).toBe(1); + const xhr = xhrs[0]; + const { url, method } = xhr; + expect({ url, method }).toEqual({ + url: host, + method: 'get', + }); + xhr.respond(200, {}, body); + + const response = await request.responsePromise; + + expect(response.body).toEqual(body); + }); + + it('should return a 200 response', async () => { + const req = new BrowserRequestHandler().makeRequest(host, {}, 'get'); + + const xhr = xhrs[0]; + xhr.respond(200, {}, body); + + const resp = await req.responsePromise; + expect(resp).toEqual({ + statusCode: 200, + headers: {}, + body, + }); + }); + + it('should return a 404 response', async () => { + const req = new BrowserRequestHandler().makeRequest(host, {}, 'get'); + + const xhr = xhrs[0]; + xhr.respond(404, {}, ''); + + const resp = await req.responsePromise; + expect(resp).toEqual({ + statusCode: 404, + headers: {}, + body: '', + }); + }); + + it('should include headers from the headers argument in the request', async () => { + const req = new BrowserRequestHandler().makeRequest(host, { + 'if-modified-since': dateString, + }, 'get'); + + expect(xhrs.length).toBe(1); + expect(xhrs[0].requestHeaders['if-modified-since']).toBe(dateString); + + xhrs[0].respond(404, {}, ''); + + await req.responsePromise; + }); + + it('should include headers from the response in the eventual response in the return value', async () => { + const req = new BrowserRequestHandler().makeRequest(host, {}, 'get'); + const xhr = xhrs[0]; + xhr.respond( + 200, + { + 'content-type': 'application/json', + 'last-modified': dateString, + }, + body, + ); + + const resp = await req.responsePromise; + + expect(resp).toEqual({ + statusCode: 200, + body, + headers: { + 'content-type': 'application/json', + 'last-modified': dateString, + }, + }); + }); + + it('should return a rejected promise when there is a request error', async () => { + const req = new BrowserRequestHandler().makeRequest(host, {}, 'get'); + xhrs[0].error(); + + await expect(req.responsePromise).rejects.toThrow(); + }); + + it('should set a timeout on the request object', () => { + const timeout = 60000; + const onCreateMock = jest.fn(); + mockXHR.onCreate = onCreateMock; + + new BrowserRequestHandler(new NoOpLogger(), timeout).makeRequest(host, {}, 'get'); + + expect(onCreateMock).toBeCalledTimes(1); + expect(onCreateMock.mock.calls[0][0].timeout).toBe(timeout); + }); + }); +}); From c0e427b162fd6965de7ea3dd604d8815a40f0ea1 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 30 Aug 2022 09:33:43 -0400 Subject: [PATCH 34/42] Document RequestHandlers --- .../browser_request_handler.ts | 31 ++++++++++++++++--- .../lib/utils/http_request_handler/config.ts | 3 ++ .../lib/utils/http_request_handler/http.ts | 15 ++++++--- .../node_request_handler.ts | 29 +++++++++++++++++ .../request_handler_factory.ts | 4 +++ 5 files changed, 73 insertions(+), 9 deletions(-) diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts index ad1f3ae86..0d3a95a2b 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts @@ -21,6 +21,9 @@ import { NoOpLogger } from '../../plugins/logger'; const READY_STATE_DONE = 4; +/** + * Handles sending requests and receiving responses over HTTP via XMLHttpRequest + */ export class BrowserRequestHandler implements RequestHandler { private readonly _logger: LogHandler; private readonly _timeout: number; @@ -30,6 +33,14 @@ export class BrowserRequestHandler implements RequestHandler { this._timeout = timeout; } + /** + * Builds an XMLHttpRequest + * @param reqUrl Fully-qualified URL to which to send the request + * @param headers List of headers to include in the request + * @param method HTTP method to use + * @param data stringified version of data to POST, PUT, etc + * @returns AbortableRequest contains both the response Promise and capability to abort() + */ public makeRequest(reqUrl: string, headers: Headers, method: string, data?: string): AbortableRequest { const request = new XMLHttpRequest(); @@ -73,17 +84,29 @@ export class BrowserRequestHandler implements RequestHandler { }; } - private setHeadersInXhr(headers: Headers, req: XMLHttpRequest): void { + /** + * Sets the header collection for an XHR + * @param headers Headers to set + * @param request Request into which headers are to be set + * @private + */ + private setHeadersInXhr(headers: Headers, request: XMLHttpRequest): void { Object.keys(headers).forEach(headerName => { const header = headers[headerName]; if (typeof header === 'string') { - req.setRequestHeader(headerName, header); + request.setRequestHeader(headerName, header); } }); } - private parseHeadersFromXhr(req: XMLHttpRequest): Headers { - const allHeadersString = req.getAllResponseHeaders(); + /** + * Parses headers from an XHR + * @param request Request containing headers to be retrieved + * @private + * @returns List of headers without duplicates + */ + private parseHeadersFromXhr(request: XMLHttpRequest): Headers { + const allHeadersString = request.getAllResponseHeaders(); if (allHeadersString === null) { return {}; diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/config.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/config.ts index 830bec432..76ab92f82 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/config.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/config.ts @@ -14,4 +14,7 @@ * limitations under the License. */ +/** + * Default milliseconds before request timeout + */ export const REQUEST_TIMEOUT_MS = 60 * 1000; // 1 minute diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/http.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/http.ts index a775bf714..4eafb07fd 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/http.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/http.ts @@ -15,28 +15,33 @@ */ /** - * Headers is the interface that bridges between the abstract datafile manager and - * any Node-or-browser-specific http header types. - * It's simplified and can only store one value per header name. - * We can extend or replace this type if requirements change, and we need - * to work with multiple values per header name. + * List of key-value pairs to be used in an HTTP requests */ export interface Headers { [header: string]: string | undefined; } +/** + * Simplified Response object containing only needed information + */ export interface Response { statusCode?: number; body: string; headers: Headers; } +/** + * Cancellable request wrapper around a Promised response + */ export interface AbortableRequest { abort(): void; responsePromise: Promise; } +/** + * Client that handles sending requests and receiving responses + */ export interface RequestHandler { makeRequest(reqUrl: string, headers: Headers, method: string, data?: string): AbortableRequest; } diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts index ce49763a1..4de88043e 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts @@ -23,6 +23,9 @@ import decompressResponse from 'decompress-response'; import { LogHandler } from '../../modules/logging'; import { NoOpLogger } from '../../plugins/logger'; +/** + * Handles sending requests and receiving responses over HTTP via NodeJS http module + */ export class NodeRequestHandler implements RequestHandler { private readonly _logger: LogHandler; private readonly _timeout: number; @@ -32,6 +35,14 @@ export class NodeRequestHandler implements RequestHandler { this._timeout = timeout; } + /** + * Builds an XMLHttpRequest + * @param reqUrl Fully-qualified URL to which to send the request + * @param headers List of headers to include in the request + * @param method HTTP method to use + * @param data stringified version of data to POST, PUT, etc + * @returns AbortableRequest contains both the response Promise and capability to abort() + */ public makeRequest(reqUrl: string, headers: Headers, method: string, data?: string): AbortableRequest { const parsedUrl = url.parse(reqUrl); @@ -65,6 +76,12 @@ export class NodeRequestHandler implements RequestHandler { }; } + /** + * Parses a URL into its constituent parts + * @param url URL object to parse + * @private + * @returns https.RequestOptions Standard request options dictionary + */ private getRequestOptionsFromUrl(url: url.UrlWithStringQuery): https.RequestOptions { return { hostname: url.hostname, @@ -74,6 +91,12 @@ export class NodeRequestHandler implements RequestHandler { }; } + /** + * Parses headers from an http response + * @param incomingMessage Incoming response message to parse + * @private + * @returns Headers Dictionary of headers without duplicates + */ private createHeadersFromNodeIncomingMessage(incomingMessage: http.IncomingMessage): Headers { const headers: Headers = {}; Object.keys(incomingMessage.headers).forEach(headerName => { @@ -93,6 +116,12 @@ export class NodeRequestHandler implements RequestHandler { return headers; } + /** + * Sends a built request handling response, errors, and events around the transmission + * @param request Request to send + * @private + * @returns Response Promise-wrapped, simplified response object + */ private getResponseFromRequest(request: http.ClientRequest): Promise { return new Promise((resolve, reject) => { request.on('timeout', () => { diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts index ecab65521..ecd5abf88 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts @@ -13,11 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { LogHandler } from '../../modules/logging'; import { RequestHandler } from './http'; import { NodeRequestHandler } from './node_request_handler'; import { BrowserRequestHandler } from './browser_request_handler'; +/** + * Factory to create the appropriate type of RequestHandler based on a provided context + */ export class RequestHandlerFactory { public static createHandler(type: string, logger?: LogHandler, timeout?: number): RequestHandler { if (type === 'node') { From b8ba3476f5be74538357b346bcc6d07db43a9c54 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 30 Aug 2022 09:59:38 -0400 Subject: [PATCH 35/42] ODP Client & GraphQL jsdoc --- .../lib/plugins/odp/graphql_manager.ts | 35 +++++++++++++++++++ .../lib/plugins/odp/odp_client.ts | 27 ++++++++++++++ .../lib/plugins/odp/odp_types.ts | 27 ++++++++++++++ 3 files changed, 89 insertions(+) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index e362103cc..d95cf0393 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -21,25 +21,54 @@ import { validate } from '../../utils/json_schema_validator'; import { OdpResponseSchema } from './odp_response_schema'; import { QuerySegmentsParameters } from './query_segments_parameters'; +/** + * Expected value for a qualified/valid segment + */ const QUALIFIED = 'qualified'; +/** + * Return value when no valid segments found + */ const EMPTY_SEGMENTS_COLLECTION: string[] = []; +/** + * Return value for scenarios with no valid JSON + */ const EMPTY_JSON_RESPONSE = null; +/** + * Manager for communicating with the Optimizely Data Platform GraphQL endpoint + */ export interface IGraphQLManager { fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise; } +/** + * Concrete implementation for communicating with the Optimizely Data Platform GraphQL endpoint + */ export class GraphqlManager implements IGraphQLManager { private readonly _errorHandler: ErrorHandler; private readonly _logger: LogHandler; private readonly _odpClient: IOdpClient; + /** + * Retrieves the audience segments from the Optimizely Data Platform (ODP) + * @param errorHandler Handler to record exceptions + * @param logger Collect and record events/errors for this GraphQL implementation + * @param client Client to use to send queries to ODP + */ constructor(errorHandler: ErrorHandler, logger: LogHandler, client: IOdpClient) { this._errorHandler = errorHandler ?? new NoopErrorHandler(); this._logger = logger ?? new ConsoleLogHandler(); this._odpClient = client ?? new OdpClient(this._errorHandler, this._logger); } + /** + * Retrieves the audience segments from ODP + * @param apiKey ODP public key + * @param apiHost Fully-qualified URL of ODP + * @param userKey 'vuid' or 'fs_user_id key' + * @param userValue Associated value to query for the user key + * @param segmentsToCheck Audience segments to check for experiment inclusion + */ public async fetchSegments(apiKey: string, apiHost: string, userKey: string, userValue: string, segmentsToCheck: string[]): Promise { const parameters = new QuerySegmentsParameters({ apiKey, @@ -77,6 +106,12 @@ export class GraphqlManager implements IGraphQLManager { return edges.filter(edge => edge.node.state == QUALIFIED).map(edge => edge.node.name); } + /** + * Parses JSON response + * @param jsonResponse JSON response from ODP + * @private + * @returns Response Strongly-typed ODP Response object + */ private parseSegmentsResponseJson(jsonResponse: string): Response | null { let jsonObject = {}; diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index 89dfb2c47..443998c7b 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -21,25 +21,47 @@ import { RequestHandler, Response } from '../../utils/http_request_handler/http' import { RequestHandlerFactory } from '../../utils/http_request_handler/request_handler_factory'; import { REQUEST_TIMEOUT_MS } from '../../utils/http_request_handler/config'; +/** + * Standard failure message for fetch errors + */ const FETCH_FAILURE_MESSAGE = 'Audience segments fetch failed'; +/** + * Return value for scenarios with no valid JSON + */ const EMPTY_JSON_RESPONSE = null; +/** + * Interface for sending requests and handling responses to Optimizely Data Platform + */ export interface IOdpClient { querySegments(parameters: QuerySegmentsParameters): Promise; } +/** + * Valid types of Javascript contexts in which this code is executing + */ enum ExecutionContextType { notDefined, browser, node, } +/** + * Http implementation for sending requests and handling responses to Optimizely Data Platform + */ export class OdpClient implements IOdpClient { private readonly _errorHandler: ErrorHandler; private readonly _logger: LogHandler; private readonly _timeout: number; private readonly _requestHandler: RequestHandler; + /** + * An implementation for sending requests and handling responses to Optimizely Data Platform (ODP) + * @param errorHandler Handler to record exceptions + * @param logger Collect and record events/errors for this ODP client + * @param requestHandler Client implementation to send/receive requests over HTTP + * @param timeout Maximum milliseconds before requests are considered timed out + */ constructor(errorHandler?: ErrorHandler, logger?: LogHandler, requestHandler?: RequestHandler, timeout?: number) { this._errorHandler = errorHandler ?? new NoopErrorHandler(); this._logger = logger ?? new NoOpLogger(); @@ -54,6 +76,11 @@ export class OdpClient implements IOdpClient { } } + /** + * Handler for querying the ODP GraphQL endpoint + * @param parameters + * @returns JSON response string from ODP + */ public async querySegments(parameters: QuerySegmentsParameters): Promise { if (!parameters?.apiHost || !parameters?.apiKey) { this._logger.log(LogLevel.ERROR, 'No ApiHost or ApiKey set before querying segments'); diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_types.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_types.ts index 7203c833b..cb034a3c9 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_types.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_types.ts @@ -14,15 +14,24 @@ * limitations under the License. */ +/** + * Wrapper around valid data and error responses + */ export interface Response { data: Data; errors: Error[]; } +/** + * GraphQL response data returned from a valid query + */ export interface Data { customer: Customer; } +/** + * GraphQL response from an errant query + */ export interface Error { message: string; locations: Location[]; @@ -30,27 +39,45 @@ export interface Error { extensions: Extension; } +/** + * Profile used to group/segment an addressable market + */ export interface Customer { audiences: Audience; } +/** + * Specifies the precise place in code or data where the error occurred + */ export interface Location { line: number; column: number; } +/** + * Extended error information + */ export interface Extension { classification: string; } +/** + * Segment of a customer base + */ export interface Audience { edges: Edge[]; } +/** + * Grouping of nodes within an audience + */ export interface Edge { node: Node; } +/** + * Atomic grouping an audience + */ export interface Node { name: string; state: string; From ef8dc5231d85af4a565c95a4429b99ea99cf00f4 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 30 Aug 2022 10:23:54 -0400 Subject: [PATCH 36/42] Remove `any` in `catch` --- packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts | 3 +-- packages/optimizely-sdk/lib/plugins/odp/odp_client.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index d95cf0393..a7b06a2ed 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -117,8 +117,7 @@ export class GraphqlManager implements IGraphQLManager { try { jsonObject = JSON.parse(jsonResponse); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { + } catch (error) { this._errorHandler.handleError(error); this._logger.log(LogLevel.ERROR, 'Attempted to parse invalid segment response JSON.'); return EMPTY_JSON_RESPONSE; diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index 443998c7b..f55eff6d6 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -99,8 +99,7 @@ export class OdpClient implements IOdpClient { try { const request = this._requestHandler.makeRequest(url, headers, method, data); response = await request.responsePromise; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { + } catch (error) { this._errorHandler.handleError(error); this._logger.log(LogLevel.ERROR, `${FETCH_FAILURE_MESSAGE} (${error.statusCode ?? 'network error'})`); From e825064747a4f96f52443cd8d7db7448bfefbec9 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 30 Aug 2022 13:16:51 -0400 Subject: [PATCH 37/42] Revert "Remove `any` in `catch`" This reverts commit ef8dc5231d85af4a565c95a4429b99ea99cf00f4. --- packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts | 3 ++- packages/optimizely-sdk/lib/plugins/odp/odp_client.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index a7b06a2ed..d95cf0393 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -117,7 +117,8 @@ export class GraphqlManager implements IGraphQLManager { try { jsonObject = JSON.parse(jsonResponse); - } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { this._errorHandler.handleError(error); this._logger.log(LogLevel.ERROR, 'Attempted to parse invalid segment response JSON.'); return EMPTY_JSON_RESPONSE; diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index f55eff6d6..443998c7b 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -99,7 +99,8 @@ export class OdpClient implements IOdpClient { try { const request = this._requestHandler.makeRequest(url, headers, method, data); response = await request.responsePromise; - } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { this._errorHandler.handleError(error); this._logger.log(LogLevel.ERROR, `${FETCH_FAILURE_MESSAGE} (${error.statusCode ?? 'network error'})`); From a07c90578080b08385fab84854e623059b42cd00 Mon Sep 17 00:00:00 2001 From: Mike Chu <104384559+mikechu-optimizely@users.noreply.github.com> Date: Tue, 30 Aug 2022 16:15:57 -0400 Subject: [PATCH 38/42] Update packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts Co-authored-by: Zeeshan Ashraf <35262377+zashraf1985@users.noreply.github.com> --- packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index d95cf0393..d39a081c3 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -98,7 +98,7 @@ export class GraphqlManager implements IGraphQLManager { } const edges = parsedSegments?.data?.customer?.audiences?.edges; - if (edges === undefined) { + if (!edges) { this._logger.log(LogLevel.WARNING, 'Audience segments fetch failed (decode error)'); return EMPTY_SEGMENTS_COLLECTION; } From 6134d844f296204680a31dac99ecb24468f3e015 Mon Sep 17 00:00:00 2001 From: Mike Chu <104384559+mikechu-optimizely@users.noreply.github.com> Date: Tue, 30 Aug 2022 16:16:12 -0400 Subject: [PATCH 39/42] Update packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts Co-authored-by: Zeeshan Ashraf <35262377+zashraf1985@users.noreply.github.com> --- packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index d39a081c3..8e3483cff 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -103,7 +103,7 @@ export class GraphqlManager implements IGraphQLManager { return EMPTY_SEGMENTS_COLLECTION; } - return edges.filter(edge => edge.node.state == QUALIFIED).map(edge => edge.node.name); + return edges.filter?(edge => edge.node.state == QUALIFIED).map(edge => edge.node.name) || []; } /** From 5d0ebb23dc0db08957d2d35023453c1562efc9a6 Mon Sep 17 00:00:00 2001 From: Mike Chu <104384559+mikechu-optimizely@users.noreply.github.com> Date: Tue, 30 Aug 2022 16:16:20 -0400 Subject: [PATCH 40/42] Update packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts Co-authored-by: Zeeshan Ashraf <35262377+zashraf1985@users.noreply.github.com> --- .../optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts b/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts index 0fc62c30c..aa12df677 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/query_segments_parameters.ts @@ -21,7 +21,7 @@ export class QuerySegmentsParameters { /** * Optimizely Data Platform API key */ - public apiKey: string | undefined; + public apiKey?: string; /** * Fully-qualified URL to ODP endpoint From 8308a535a479b828619e31064741ed2175b99009 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Wed, 31 Aug 2022 12:53:31 -0400 Subject: [PATCH 41/42] Code review changes --- packages/optimizely-sdk/lib/index.browser.ts | 15 +++++-- packages/optimizely-sdk/lib/index.node.ts | 12 +++-- .../optimizely-sdk/lib/index.react_native.ts | 12 +++-- .../lib/plugins/odp/graphql_manager.ts | 19 +++++--- .../lib/plugins/odp/odp_client.ts | 31 +++---------- .../optimizely-sdk/lib/utils/enums/index.ts | 12 ++++- .../lib/utils/execution_context/index.ts | 45 +++++++++++++++++++ .../optimizely-sdk/lib/utils/fns/index.ts | 21 +++++---- .../browser_request_handler.ts | 28 +++++++----- .../node_request_handler.ts | 8 ++-- .../request_handler_factory.ts | 17 ++++--- .../tests/browserRequestHandler.spec.ts | 34 +++++++------- .../tests/nodeRequestHandler.spec.ts | 36 ++++++++------- .../optimizely-sdk/tests/odpClient.spec.ts | 4 +- 14 files changed, 188 insertions(+), 106 deletions(-) create mode 100644 packages/optimizely-sdk/lib/utils/execution_context/index.ts diff --git a/packages/optimizely-sdk/lib/index.browser.ts b/packages/optimizely-sdk/lib/index.browser.ts index 0db641b27..5749ebf43 100644 --- a/packages/optimizely-sdk/lib/index.browser.ts +++ b/packages/optimizely-sdk/lib/index.browser.ts @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -18,9 +18,9 @@ import { getLogger, setErrorHandler, getErrorHandler, - LogLevel + LogLevel, } from './modules/logging'; -import { LocalStoragePendingEventsDispatcher } from '../lib/modules/event_processor'; +import { LocalStoragePendingEventsDispatcher } from './modules/event_processor'; import configValidator from './utils/config_validator'; import defaultErrorHandler from './plugins/error_handler'; import defaultEventDispatcher from './plugins/event_dispatcher/index.browser'; @@ -32,6 +32,8 @@ import { createNotificationCenter } from './core/notification_center'; import { default as eventProcessor } from './plugins/event_processor'; import { OptimizelyDecideOption, Client, Config } from './shared_types'; import { createHttpPollingDatafileManager } from './plugins/datafile_manager/http_polling_datafile_manager'; +import { EXECUTION_CONTEXT_TYPE } from './utils/enums'; +import { ExecutionContext } from './utils/execution_context'; const logger = getLogger(); logHelper.setLogHandler(loggerPlugin.createLogger()); @@ -44,11 +46,13 @@ const DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000; let hasRetriedEvents = false; +ExecutionContext.Current = EXECUTION_CONTEXT_TYPE.BROWSER; + /** * Creates an instance of the Optimizely class * @param {Config} config * @return {Client|null} the Optimizely client object - * null on error + * null on error */ const createInstance = function(config: Config): Client | null { try { @@ -70,6 +74,7 @@ const createInstance = function(config: Config): Client | null { try { configValidator.validate(config); isValidInstance = true; + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (ex: any) { logger.error(ex); } @@ -141,11 +146,13 @@ const createInstance = function(config: Config): Client | null { false ); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { logger.error(enums.LOG_MESSAGES.UNABLE_TO_ATTACH_UNLOAD, MODULE_NAME, e.message); } return optimizely; + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { logger.error(e); return null; diff --git a/packages/optimizely-sdk/lib/index.node.ts b/packages/optimizely-sdk/lib/index.node.ts index a92fe1e91..6fe59eb22 100644 --- a/packages/optimizely-sdk/lib/index.node.ts +++ b/packages/optimizely-sdk/lib/index.node.ts @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * * You may obtain a copy of the License at * * * - * http://www.apache.org/licenses/LICENSE-2.0 * + * https://www.apache.org/licenses/LICENSE-2.0 * * * * Unless required by applicable law or agreed to in writing, software * * distributed under the License is distributed on an "AS IS" BASIS, * @@ -32,6 +32,8 @@ import { createNotificationCenter } from './core/notification_center'; import { createEventProcessor } from './plugins/event_processor'; import { OptimizelyDecideOption, Client, Config } from './shared_types'; import { createHttpPollingDatafileManager } from './plugins/datafile_manager/http_polling_datafile_manager'; +import { ExecutionContext } from './utils/execution_context'; +import { EXECUTION_CONTEXT_TYPE } from './utils/enums'; const logger = getLogger(); setLogLevel(LogLevel.ERROR); @@ -40,13 +42,15 @@ const DEFAULT_EVENT_BATCH_SIZE = 10; const DEFAULT_EVENT_FLUSH_INTERVAL = 30000; // Unit is ms, default is 30s const DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000; +ExecutionContext.Current = EXECUTION_CONTEXT_TYPE.NODE; + /** * Creates an instance of the Optimizely class * @param {Config} config * @return {Client|null} the Optimizely client object - * null on error + * null on error */ - const createInstance = function(config: Config): Client | null { +const createInstance = function(config: Config): Client | null { try { let hasLogger = false; let isValidInstance = false; @@ -68,6 +72,7 @@ const DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000; try { configValidator.validate(config); isValidInstance = true; + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (ex: any) { if (hasLogger) { logger.error(ex); @@ -117,6 +122,7 @@ const DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000; }; return new Optimizely(optimizelyOptions); + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { logger.error(e); return null; diff --git a/packages/optimizely-sdk/lib/index.react_native.ts b/packages/optimizely-sdk/lib/index.react_native.ts index 41ed88e46..cece646f2 100644 --- a/packages/optimizely-sdk/lib/index.react_native.ts +++ b/packages/optimizely-sdk/lib/index.react_native.ts @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -32,6 +32,8 @@ import { createNotificationCenter } from './core/notification_center'; import { createEventProcessor } from './plugins/event_processor/index.react_native'; import { OptimizelyDecideOption, Client, Config } from './shared_types'; import { createHttpPollingDatafileManager } from './plugins/datafile_manager/http_polling_datafile_manager'; +import { EXECUTION_CONTEXT_TYPE } from './utils/enums'; +import { ExecutionContext } from './utils/execution_context'; const logger = getLogger(); setLogHandler(loggerPlugin.createLogger()); @@ -41,13 +43,15 @@ const DEFAULT_EVENT_BATCH_SIZE = 10; const DEFAULT_EVENT_FLUSH_INTERVAL = 1000; // Unit is ms, default is 1s const DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000; +ExecutionContext.Current = EXECUTION_CONTEXT_TYPE.BROWSER; + /** * Creates an instance of the Optimizely class * @param {Config} config * @return {Client|null} the Optimizely client object - * null on error + * null on error */ - const createInstance = function(config: Config): Client | null { +const createInstance = function(config: Config): Client | null { try { // TODO warn about setting per instance errorHandler / logger / logLevel let isValidInstance = false; @@ -67,6 +71,7 @@ const DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000; try { configValidator.validate(config); isValidInstance = true; + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (ex: any) { logger.error(ex); } @@ -117,6 +122,7 @@ const DEFAULT_EVENT_MAX_QUEUE_SIZE = 10000; } return new Optimizely(optimizelyOptions); + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { logger.error(e); return null; diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index 8e3483cff..c2f2dbb7c 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -14,12 +14,14 @@ * limitations under the License. */ -import { ConsoleLogHandler, ErrorHandler, LogHandler, LogLevel, NoopErrorHandler } from '../../modules/logging'; +import { ErrorHandler, LogHandler, LogLevel } from '../../modules/logging'; import { Response } from './odp_types'; import { IOdpClient, OdpClient } from './odp_client'; import { validate } from '../../utils/json_schema_validator'; import { OdpResponseSchema } from './odp_response_schema'; import { QuerySegmentsParameters } from './query_segments_parameters'; +import { throwError } from '../../utils/fns'; +import { RequestHandlerFactory } from '../../utils/http_request_handler/request_handler_factory'; /** * Expected value for a qualified/valid segment @@ -55,10 +57,13 @@ export class GraphqlManager implements IGraphQLManager { * @param logger Collect and record events/errors for this GraphQL implementation * @param client Client to use to send queries to ODP */ - constructor(errorHandler: ErrorHandler, logger: LogHandler, client: IOdpClient) { - this._errorHandler = errorHandler ?? new NoopErrorHandler(); - this._logger = logger ?? new ConsoleLogHandler(); - this._odpClient = client ?? new OdpClient(this._errorHandler, this._logger); + constructor(errorHandler: ErrorHandler, logger: LogHandler, client?: IOdpClient) { + this._errorHandler = errorHandler ?? throwError('Error Handler is required'); + this._logger = logger ?? throwError('Logger is required'); + + this._odpClient = client ?? new OdpClient(this._errorHandler, + this._logger, + RequestHandlerFactory.createHandler(this._logger)); } /** @@ -90,7 +95,7 @@ export class GraphqlManager implements IGraphQLManager { } if (parsedSegments.errors?.length > 0) { - const errors = parsedSegments.errors.map((e) => e.message).join(';'); + const errors = parsedSegments.errors.map((e) => e.message).join('; '); this._logger.log(LogLevel.WARNING, `Audience segments fetch failed (${errors})`); @@ -103,7 +108,7 @@ export class GraphqlManager implements IGraphQLManager { return EMPTY_SEGMENTS_COLLECTION; } - return edges.filter?(edge => edge.node.state == QUALIFIED).map(edge => edge.node.name) || []; + return edges.filter(edge => edge.node.state == QUALIFIED).map(edge => edge.node.name); } /** diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index 443998c7b..9c780ce29 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -14,12 +14,11 @@ * limitations under the License. */ -import { ErrorHandler, LogHandler, LogLevel, NoopErrorHandler } from '../../modules/logging'; +import { ErrorHandler, LogHandler, LogLevel } from '../../modules/logging'; import { QuerySegmentsParameters } from './query_segments_parameters'; -import { NoOpLogger } from '../logger'; import { RequestHandler, Response } from '../../utils/http_request_handler/http'; -import { RequestHandlerFactory } from '../../utils/http_request_handler/request_handler_factory'; import { REQUEST_TIMEOUT_MS } from '../../utils/http_request_handler/config'; +import { throwError } from '../../utils/fns'; /** * Standard failure message for fetch errors @@ -37,15 +36,6 @@ export interface IOdpClient { querySegments(parameters: QuerySegmentsParameters): Promise; } -/** - * Valid types of Javascript contexts in which this code is executing - */ -enum ExecutionContextType { - notDefined, - browser, - node, -} - /** * Http implementation for sending requests and handling responses to Optimizely Data Platform */ @@ -62,18 +52,11 @@ export class OdpClient implements IOdpClient { * @param requestHandler Client implementation to send/receive requests over HTTP * @param timeout Maximum milliseconds before requests are considered timed out */ - constructor(errorHandler?: ErrorHandler, logger?: LogHandler, requestHandler?: RequestHandler, timeout?: number) { - this._errorHandler = errorHandler ?? new NoopErrorHandler(); - this._logger = logger ?? new NoOpLogger(); - this._timeout = timeout ?? REQUEST_TIMEOUT_MS; - - if (requestHandler) { - this._requestHandler = requestHandler; - } else { - let executionContextType = typeof window === 'object' ? ExecutionContextType.browser : ExecutionContextType.notDefined; - executionContextType = typeof process === 'object' ? ExecutionContextType.node : executionContextType; - this._requestHandler = RequestHandlerFactory.createHandler(ExecutionContextType[executionContextType], this._logger, this._timeout); - } + constructor(errorHandler: ErrorHandler, logger: LogHandler, requestHandler: RequestHandler, timeout: number = REQUEST_TIMEOUT_MS) { + this._errorHandler = errorHandler ?? throwError('Error Handler is required'); + this._logger = logger ?? throwError('Logger is required'); + this._requestHandler = requestHandler ?? throwError('Implementation of RequestHandler is required'); + this._timeout = timeout; } /** diff --git a/packages/optimizely-sdk/lib/utils/enums/index.ts b/packages/optimizely-sdk/lib/utils/enums/index.ts index fe4abb242..e9eecdcf4 100644 --- a/packages/optimizely-sdk/lib/utils/enums/index.ts +++ b/packages/optimizely-sdk/lib/utils/enums/index.ts @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * * You may obtain a copy of the License at * * * - * http://www.apache.org/licenses/LICENSE-2.0 * + * https://www.apache.org/licenses/LICENSE-2.0 * * * * Unless required by applicable law or agreed to in writing, software * * distributed under the License is distributed on an "AS IS" BASIS, * @@ -291,3 +291,13 @@ export enum NOTIFICATION_TYPES { OPTIMIZELY_CONFIG_UPDATE = 'OPTIMIZELY_CONFIG_UPDATE', TRACK = 'TRACK:event_key, user_id, attributes, event_tags, event', } + + +/** + * Valid types of Javascript contexts in which this code is executing + */ +export enum EXECUTION_CONTEXT_TYPE { + NOT_DEFINED, + BROWSER, + NODE, +} diff --git a/packages/optimizely-sdk/lib/utils/execution_context/index.ts b/packages/optimizely-sdk/lib/utils/execution_context/index.ts new file mode 100644 index 000000000..f9f3e2457 --- /dev/null +++ b/packages/optimizely-sdk/lib/utils/execution_context/index.ts @@ -0,0 +1,45 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { EXECUTION_CONTEXT_TYPE } from '../enums'; + +/** + * Determine the running or execution context for JavaScript + * Note: React Native is considered a browser context + */ +export class ExecutionContext { + /** + * Holds the current value of the execution context + * @private + */ + private static _currentContext: EXECUTION_CONTEXT_TYPE = EXECUTION_CONTEXT_TYPE.NOT_DEFINED; + + /** + * Gets the current running context + * @constructor + */ + public static get Current(): EXECUTION_CONTEXT_TYPE { + return this._currentContext; + } + + /** + * Sets the current running context ideally from package initialization + * @param newValue The new execution context + * @constructor + */ + public static set Current(newValue: EXECUTION_CONTEXT_TYPE) { + this._currentContext = newValue; + } +} diff --git a/packages/optimizely-sdk/lib/utils/fns/index.ts b/packages/optimizely-sdk/lib/utils/fns/index.ts index 59084a130..c3120f114 100644 --- a/packages/optimizely-sdk/lib/utils/fns/index.ts +++ b/packages/optimizely-sdk/lib/utils/fns/index.ts @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -141,18 +141,22 @@ export function keyByUtil(arr: K[], keyByFn: (item: K) => string): { [key: st export function sprintf(format: string, ...args: any[]): string { let i = 0 return format.replace(/%s/g, function() { - const arg = args[i++] - const type = typeof arg + const arg = args[i++]; + const type = typeof arg; if (type === 'function') { - return arg() + return arg(); } else if (type === 'string') { - return arg + return arg; } else { - return String(arg) + return String(arg); } }) } +export function throwError(errorMessage: string): never { + throw new Error(errorMessage); +} + export default { assign, currentTimestamp, @@ -167,5 +171,6 @@ export default { objectEntries, find, keyByUtil, - sprintf -} + sprintf, + throwExpression: throwError, +}; diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts index 0d3a95a2b..946bbe0a9 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts @@ -1,5 +1,5 @@ /** - * Copyright 2019-2020, 2022 Optimizely + * Copyright 2022 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ import { AbortableRequest, Headers, RequestHandler, Response } from './http'; import { REQUEST_TIMEOUT_MS } from './config'; import { LogHandler, LogLevel } from '../../modules/logging'; -import { NoOpLogger } from '../../plugins/logger'; +import { throwError } from '../fns'; const READY_STATE_DONE = 4; @@ -28,8 +28,8 @@ export class BrowserRequestHandler implements RequestHandler { private readonly _logger: LogHandler; private readonly _timeout: number; - public constructor(logger?: LogHandler, timeout: number = REQUEST_TIMEOUT_MS) { - this._logger = logger ?? new NoOpLogger(); + public constructor(logger: LogHandler, timeout: number = REQUEST_TIMEOUT_MS) { + this._logger = logger ?? throwError('Logger is required.'); this._timeout = timeout; } @@ -58,12 +58,12 @@ export class BrowserRequestHandler implements RequestHandler { } const headers = this.parseHeadersFromXhr(request); - const resp: Response = { + const response: Response = { statusCode: request.status, body: request.responseText, headers, }; - resolve(resp); + resolve(response); } }; @@ -115,13 +115,17 @@ export class BrowserRequestHandler implements RequestHandler { const headerLines = allHeadersString.split('\r\n'); const headers: Headers = {}; headerLines.forEach(headerLine => { - const separatorIndex = headerLine.indexOf(': '); - if (separatorIndex > -1) { - const headerName = headerLine.slice(0, separatorIndex); - const headerValue = headerLine.slice(separatorIndex + 2); - if (headerValue.length > 0) { - headers[headerName] = headerValue; + try { + const separatorIndex = headerLine.indexOf(': '); + if (separatorIndex > -1) { + const headerName = headerLine.slice(0, separatorIndex); + const headerValue = headerLine.slice(separatorIndex + 2); + if (headerName && headerValue) { + headers[headerName] = headerValue; + } } + } catch { + this._logger.log(LogLevel.WARNING, `Unable to parse & skipped header item '${headerLine}'`); } }); return headers; diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts index 4de88043e..c6a43e04f 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts @@ -1,5 +1,5 @@ /** - * Copyright 2019-2020, 2022 Optimizely + * Copyright 2022 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import { AbortableRequest, Headers, RequestHandler, Response } from './http'; import { REQUEST_TIMEOUT_MS } from './config'; import decompressResponse from 'decompress-response'; import { LogHandler } from '../../modules/logging'; -import { NoOpLogger } from '../../plugins/logger'; +import { throwError } from '../fns'; /** * Handles sending requests and receiving responses over HTTP via NodeJS http module @@ -30,8 +30,8 @@ export class NodeRequestHandler implements RequestHandler { private readonly _logger: LogHandler; private readonly _timeout: number; - public constructor(logger?: LogHandler, timeout: number = REQUEST_TIMEOUT_MS) { - this._logger = logger ?? new NoOpLogger(); + public constructor(logger: LogHandler, timeout: number = REQUEST_TIMEOUT_MS) { + this._logger = logger ?? throwError('Logger is required.'); this._timeout = timeout; } diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts index ecd5abf88..455754cb5 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/request_handler_factory.ts @@ -18,18 +18,21 @@ import { LogHandler } from '../../modules/logging'; import { RequestHandler } from './http'; import { NodeRequestHandler } from './node_request_handler'; import { BrowserRequestHandler } from './browser_request_handler'; +import { ExecutionContext } from '../execution_context'; +import { EXECUTION_CONTEXT_TYPE } from '../enums'; /** * Factory to create the appropriate type of RequestHandler based on a provided context */ export class RequestHandlerFactory { - public static createHandler(type: string, logger?: LogHandler, timeout?: number): RequestHandler { - if (type === 'node') { - return new NodeRequestHandler(logger, timeout); - } else if (type === 'browser') { - return new BrowserRequestHandler(logger, timeout); + public static createHandler(logger: LogHandler, timeout?: number): RequestHandler { + switch (ExecutionContext.Current) { + case EXECUTION_CONTEXT_TYPE.BROWSER: + return new BrowserRequestHandler(logger, timeout); + case EXECUTION_CONTEXT_TYPE.NODE: + return new NodeRequestHandler(logger, timeout); + default: + return null as unknown as RequestHandler; } - - return null as unknown as RequestHandler; } } diff --git a/packages/optimizely-sdk/tests/browserRequestHandler.spec.ts b/packages/optimizely-sdk/tests/browserRequestHandler.spec.ts index e11215abb..24624bebe 100644 --- a/packages/optimizely-sdk/tests/browserRequestHandler.spec.ts +++ b/packages/optimizely-sdk/tests/browserRequestHandler.spec.ts @@ -1,11 +1,11 @@ /** - * Copyright 2019-2020, 2022 Optimizely + * Copyright 2022 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -28,11 +28,13 @@ describe('BrowserRequestHandler', () => { describe('makeRequest', () => { let mockXHR: FakeXMLHttpRequestStatic; let xhrs: FakeXMLHttpRequest[]; + let browserRequestHandler: BrowserRequestHandler; beforeEach(() => { xhrs = []; mockXHR = fakeXhr.useFakeXMLHttpRequest(); mockXHR.onCreate = (request): number => xhrs.push(request); + browserRequestHandler = new BrowserRequestHandler(new NoOpLogger()); }); afterEach(() => { @@ -40,7 +42,7 @@ describe('BrowserRequestHandler', () => { }); it('should make a GET request to the argument URL', async () => { - const request = new BrowserRequestHandler().makeRequest(host, {}, 'get'); + const request = browserRequestHandler.makeRequest(host, {}, 'get'); expect(xhrs.length).toBe(1); const xhr = xhrs[0]; @@ -57,13 +59,13 @@ describe('BrowserRequestHandler', () => { }); it('should return a 200 response', async () => { - const req = new BrowserRequestHandler().makeRequest(host, {}, 'get'); + const request = browserRequestHandler.makeRequest(host, {}, 'get'); const xhr = xhrs[0]; xhr.respond(200, {}, body); - const resp = await req.responsePromise; - expect(resp).toEqual({ + const response = await request.responsePromise; + expect(response).toEqual({ statusCode: 200, headers: {}, body, @@ -71,13 +73,13 @@ describe('BrowserRequestHandler', () => { }); it('should return a 404 response', async () => { - const req = new BrowserRequestHandler().makeRequest(host, {}, 'get'); + const request = browserRequestHandler.makeRequest(host, {}, 'get'); const xhr = xhrs[0]; xhr.respond(404, {}, ''); - const resp = await req.responsePromise; - expect(resp).toEqual({ + const response = await request.responsePromise; + expect(response).toEqual({ statusCode: 404, headers: {}, body: '', @@ -85,7 +87,7 @@ describe('BrowserRequestHandler', () => { }); it('should include headers from the headers argument in the request', async () => { - const req = new BrowserRequestHandler().makeRequest(host, { + const request = browserRequestHandler.makeRequest(host, { 'if-modified-since': dateString, }, 'get'); @@ -94,11 +96,11 @@ describe('BrowserRequestHandler', () => { xhrs[0].respond(404, {}, ''); - await req.responsePromise; + await request.responsePromise; }); it('should include headers from the response in the eventual response in the return value', async () => { - const req = new BrowserRequestHandler().makeRequest(host, {}, 'get'); + const request = browserRequestHandler.makeRequest(host, {}, 'get'); const xhr = xhrs[0]; xhr.respond( 200, @@ -109,9 +111,9 @@ describe('BrowserRequestHandler', () => { body, ); - const resp = await req.responsePromise; + const response = await request.responsePromise; - expect(resp).toEqual({ + expect(response).toEqual({ statusCode: 200, body, headers: { @@ -122,10 +124,10 @@ describe('BrowserRequestHandler', () => { }); it('should return a rejected promise when there is a request error', async () => { - const req = new BrowserRequestHandler().makeRequest(host, {}, 'get'); + const request = browserRequestHandler.makeRequest(host, {}, 'get'); xhrs[0].error(); - await expect(req.responsePromise).rejects.toThrow(); + await expect(request.responsePromise).rejects.toThrow(); }); it('should set a timeout on the request object', () => { diff --git a/packages/optimizely-sdk/tests/nodeRequestHandler.spec.ts b/packages/optimizely-sdk/tests/nodeRequestHandler.spec.ts index e48267afb..a5166bd1a 100644 --- a/packages/optimizely-sdk/tests/nodeRequestHandler.spec.ts +++ b/packages/optimizely-sdk/tests/nodeRequestHandler.spec.ts @@ -1,11 +1,11 @@ /** - * Copyright 2019-2020, 2022 Optimizely + * Copyright 2022 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -34,6 +34,12 @@ describe('NodeRequestHandler', () => { const path = '/api/query'; const body = '{"foo":"bar"}'; + let nodeRequestHandler: NodeRequestHandler; + + beforeEach(() => { + nodeRequestHandler = new NodeRequestHandler(new NoOpLogger()); + }); + afterEach(async () => { nock.cleanAll(); }); @@ -44,7 +50,7 @@ describe('NodeRequestHandler', () => { .post(path) .reply(200, body); - const request = new NodeRequestHandler().makeRequest(`${host}${path}`, {}, 'post', body); + const request = nodeRequestHandler.makeRequest(`${host}${path}`, {}, 'post', body); const response = await request.responsePromise; expect(response).toEqual({ @@ -60,7 +66,7 @@ describe('NodeRequestHandler', () => { .post(path) .reply(400, ''); - const request = new NodeRequestHandler().makeRequest(`${host}${path}`, {}, 'post'); + const request = nodeRequestHandler.makeRequest(`${host}${path}`, {}, 'post'); const response = await request.responsePromise; expect(response).toEqual({ @@ -76,7 +82,7 @@ describe('NodeRequestHandler', () => { .matchHeader('if-modified-since', 'Fri, 08 Mar 2019 18:57:18 GMT') .get(path) .reply(304, ''); - const request = new NodeRequestHandler().makeRequest(`${host}${path}`, { + const request = nodeRequestHandler.makeRequest(`${host}${path}`, { 'if-modified-since': 'Fri, 08 Mar 2019 18:57:18 GMT', }, 'get'); const response = await request.responsePromise; @@ -94,7 +100,7 @@ describe('NodeRequestHandler', () => { .get(path) .reply(200, () => zlib.gzipSync(body), { 'content-encoding': 'gzip' }); - const request = new NodeRequestHandler().makeRequest(`${host}${path}`, {}, 'get'); + const request = nodeRequestHandler.makeRequest(`${host}${path}`, {}, 'get'); const response = await request.responsePromise; expect(response).toMatchObject({ @@ -115,7 +121,7 @@ describe('NodeRequestHandler', () => { }, ); - const request = new NodeRequestHandler().makeRequest(`${host}${path}`, {}, 'get'); + const request = nodeRequestHandler.makeRequest(`${host}${path}`, {}, 'get'); const response = await request.responsePromise; expect(response).toEqual({ @@ -135,7 +141,7 @@ describe('NodeRequestHandler', () => { .get(pathWithQuery) .reply(200, JSON.parse(body)); - const request = new NodeRequestHandler().makeRequest(`${host}${pathWithQuery}`, {}, 'get'); + const request = nodeRequestHandler.makeRequest(`${host}${pathWithQuery}`, {}, 'get'); await request.responsePromise; scope.done(); @@ -144,7 +150,7 @@ describe('NodeRequestHandler', () => { it('should throw error for a URL with http protocol (not https)', async () => { const invalidHttpProtocolUrl = 'http://some.example.com'; - const request = new NodeRequestHandler().makeRequest(invalidHttpProtocolUrl, {}, 'get'); + const request = nodeRequestHandler.makeRequest(invalidHttpProtocolUrl, {}, 'get'); await expect(request.responsePromise).rejects.toThrow(); }); @@ -152,7 +158,7 @@ describe('NodeRequestHandler', () => { it('should returns a rejected response promise when the URL protocol is unsupported', async () => { const invalidProtocolUrl = 'ftp://something/datafiles/123.json'; - const request = new NodeRequestHandler().makeRequest(invalidProtocolUrl, {}, 'get'); + const request = nodeRequestHandler.makeRequest(invalidProtocolUrl, {}, 'get'); await expect(request.responsePromise).rejects.toThrow(); }); @@ -164,8 +170,8 @@ describe('NodeRequestHandler', () => { message: 'Connection error', code: 'CONNECTION_ERROR', }); - const req = new NodeRequestHandler().makeRequest(`${host}${path}`, {}, 'get'); - await expect(req.responsePromise).rejects.toThrow(); + const request = nodeRequestHandler.makeRequest(`${host}${path}`, {}, 'get'); + await expect(request.responsePromise).rejects.toThrow(); scope.done(); }); @@ -176,10 +182,10 @@ describe('NodeRequestHandler', () => { .get(path) .reply(200, body); - const req = new NodeRequestHandler().makeRequest(`${hostWithPort}${path}`, {}, 'get'); - const resp = await req.responsePromise; + const request = nodeRequestHandler.makeRequest(`${hostWithPort}${path}`, {}, 'get'); + const response = await request.responsePromise; - expect(resp).toEqual({ + expect(response).toEqual({ statusCode: 200, body, headers: {}, diff --git a/packages/optimizely-sdk/tests/odpClient.spec.ts b/packages/optimizely-sdk/tests/odpClient.spec.ts index a89840fb9..d0a7bbb57 100644 --- a/packages/optimizely-sdk/tests/odpClient.spec.ts +++ b/packages/optimizely-sdk/tests/odpClient.spec.ts @@ -85,7 +85,7 @@ describe('OdpClient', () => { userValue: 'userValue', segmentsToCheck: ['segmentToCheck'], }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger)); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); await client.querySegments(missingApiHost); @@ -101,7 +101,7 @@ describe('OdpClient', () => { userValue: 'userValue', segmentsToCheck: ['segmentToCheck'], }); - const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger)); + const client = new OdpClient(instance(mockErrorHandler), instance(mockLogger), instance(mockNodeRequestHandler)); await client.querySegments(missingApiHost); From b4ed7bdc811c72fc0e0305956633b65cb4cbcb0b Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Wed, 31 Aug 2022 13:45:36 -0400 Subject: [PATCH 42/42] Remove throwError --- packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts | 5 ++--- packages/optimizely-sdk/lib/plugins/odp/odp_client.ts | 7 +++---- packages/optimizely-sdk/lib/utils/fns/index.ts | 5 ----- .../utils/http_request_handler/browser_request_handler.ts | 3 +-- .../lib/utils/http_request_handler/node_request_handler.ts | 3 +-- 5 files changed, 7 insertions(+), 16 deletions(-) diff --git a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts index c2f2dbb7c..fc0160d05 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/graphql_manager.ts @@ -20,7 +20,6 @@ import { IOdpClient, OdpClient } from './odp_client'; import { validate } from '../../utils/json_schema_validator'; import { OdpResponseSchema } from './odp_response_schema'; import { QuerySegmentsParameters } from './query_segments_parameters'; -import { throwError } from '../../utils/fns'; import { RequestHandlerFactory } from '../../utils/http_request_handler/request_handler_factory'; /** @@ -58,8 +57,8 @@ export class GraphqlManager implements IGraphQLManager { * @param client Client to use to send queries to ODP */ constructor(errorHandler: ErrorHandler, logger: LogHandler, client?: IOdpClient) { - this._errorHandler = errorHandler ?? throwError('Error Handler is required'); - this._logger = logger ?? throwError('Logger is required'); + this._errorHandler = errorHandler; + this._logger = logger; this._odpClient = client ?? new OdpClient(this._errorHandler, this._logger, diff --git a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts index 9c780ce29..416f312b5 100644 --- a/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts +++ b/packages/optimizely-sdk/lib/plugins/odp/odp_client.ts @@ -18,7 +18,6 @@ import { ErrorHandler, LogHandler, LogLevel } from '../../modules/logging'; import { QuerySegmentsParameters } from './query_segments_parameters'; import { RequestHandler, Response } from '../../utils/http_request_handler/http'; import { REQUEST_TIMEOUT_MS } from '../../utils/http_request_handler/config'; -import { throwError } from '../../utils/fns'; /** * Standard failure message for fetch errors @@ -53,9 +52,9 @@ export class OdpClient implements IOdpClient { * @param timeout Maximum milliseconds before requests are considered timed out */ constructor(errorHandler: ErrorHandler, logger: LogHandler, requestHandler: RequestHandler, timeout: number = REQUEST_TIMEOUT_MS) { - this._errorHandler = errorHandler ?? throwError('Error Handler is required'); - this._logger = logger ?? throwError('Logger is required'); - this._requestHandler = requestHandler ?? throwError('Implementation of RequestHandler is required'); + this._errorHandler = errorHandler; + this._logger = logger; + this._requestHandler = requestHandler; this._timeout = timeout; } diff --git a/packages/optimizely-sdk/lib/utils/fns/index.ts b/packages/optimizely-sdk/lib/utils/fns/index.ts index c3120f114..c769f147b 100644 --- a/packages/optimizely-sdk/lib/utils/fns/index.ts +++ b/packages/optimizely-sdk/lib/utils/fns/index.ts @@ -153,10 +153,6 @@ export function sprintf(format: string, ...args: any[]): string { }) } -export function throwError(errorMessage: string): never { - throw new Error(errorMessage); -} - export default { assign, currentTimestamp, @@ -172,5 +168,4 @@ export default { find, keyByUtil, sprintf, - throwExpression: throwError, }; diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts index 946bbe0a9..3103b5322 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/browser_request_handler.ts @@ -17,7 +17,6 @@ import { AbortableRequest, Headers, RequestHandler, Response } from './http'; import { REQUEST_TIMEOUT_MS } from './config'; import { LogHandler, LogLevel } from '../../modules/logging'; -import { throwError } from '../fns'; const READY_STATE_DONE = 4; @@ -29,7 +28,7 @@ export class BrowserRequestHandler implements RequestHandler { private readonly _timeout: number; public constructor(logger: LogHandler, timeout: number = REQUEST_TIMEOUT_MS) { - this._logger = logger ?? throwError('Logger is required.'); + this._logger = logger; this._timeout = timeout; } diff --git a/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts b/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts index c6a43e04f..5a6c647f1 100644 --- a/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts +++ b/packages/optimizely-sdk/lib/utils/http_request_handler/node_request_handler.ts @@ -21,7 +21,6 @@ import { AbortableRequest, Headers, RequestHandler, Response } from './http'; import { REQUEST_TIMEOUT_MS } from './config'; import decompressResponse from 'decompress-response'; import { LogHandler } from '../../modules/logging'; -import { throwError } from '../fns'; /** * Handles sending requests and receiving responses over HTTP via NodeJS http module @@ -31,7 +30,7 @@ export class NodeRequestHandler implements RequestHandler { private readonly _timeout: number; public constructor(logger: LogHandler, timeout: number = REQUEST_TIMEOUT_MS) { - this._logger = logger ?? throwError('Logger is required.'); + this._logger = logger; this._timeout = timeout; }