From a53ff60f8efe24b61c050700e0c409d103cc035d Mon Sep 17 00:00:00 2001 From: Keaton Sentak <54916859+ksentak@users.noreply.github.com> Date: Fri, 10 Nov 2023 09:04:00 -0500 Subject: [PATCH] SYSTEST-9586 - Enhanced $ref Replacement and Self-Reference Check in Schema Dereferencing (#147) * Modify dereference logic to loop to a maximum depth of 3 * Alter de-referencing logic * Update unit tests * remove maxDereferenceDepth * v1.0.2 --- CHANGELOG.md | 22 + cli/package.json | 2 +- conduit/package.json | 2 +- functional/package.json | 2 +- server/package.json | 2 +- server/src/fireboltOpenRpcDereferencing.mjs | 114 ++- server/test/mocks/mockOpenRpcJson.mjs | 926 +++++++++++++++++ .../fireboltOpenRpcDereferencing.test.mjs | 966 +++--------------- 8 files changed, 1158 insertions(+), 878 deletions(-) create mode 100644 server/test/mocks/mockOpenRpcJson.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 45665da1..5345fbd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,28 @@ All notable changes to this project will be documented in this file. +## [1.0.2] - 2023-11-10 + +### Added + +* New unit tests to ensure the reliability of schema dereferencing functions in `fireboltOpenRpcDereferencing.mjs`. + +### Changed + +* Refined `selfReferenceSchemaCheck()` to return a boolean consistently, enhancing readability and predictability of self-referencing detection logic. + +* Overhauled `replaceRefs()` for improved efficiency: + + * Implemented a set to track replaced $refs, thereby preventing infinite recursion during ref replacement. + + * Expanded the function to handle nested objects and arrays more effectively. + +* Modernized various parts of `fireboltOpenRpcDereferencing.mjs` with arrow functions for a more modern code syntax. + +### Fixed + +* Addressed potential issues with recursive calls and infinite loops in `replaceRefs()` by marking self-referenced schemas as replaced and removing redundant $ref keys. + ## [1.0.1] - 2023-10-18 ### Added diff --git a/cli/package.json b/cli/package.json index b7750c90..f10001a8 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/mock-firebolt-cli", - "version": "1.0.1", + "version": "1.0.2", "description": "Command-line interface for controlling the Mock Firebolt server", "main": "./src/cli.mjs", "scripts": {}, diff --git a/conduit/package.json b/conduit/package.json index bced9b45..4d759df4 100644 --- a/conduit/package.json +++ b/conduit/package.json @@ -1,7 +1,7 @@ { "name": "conduit", "description": "A TV app that works in concert with Mock Firebolt in Reverse Proxy mode", - "version": "1.0.1", + "version": "1.0.2", "private": true, "license": "Apache 2.0", "author": "Mike Fine ", diff --git a/functional/package.json b/functional/package.json index 19e07ac8..2d004332 100644 --- a/functional/package.json +++ b/functional/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/mock-firebolt-functional-tests", - "version": "1.0.1", + "version": "1.0.2", "description": "Command-line interface for controlling the Mock Firebolt server", "scripts": { "test": "NODE_ENV=test npx jest --config=jest.config.js --silent -i", diff --git a/server/package.json b/server/package.json index a28f1efc..2809b018 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/mock-firebolt", - "version": "1.0.1", + "version": "1.0.2", "description": "Controllable mock Firebolt server", "main": "./build/index.mjs", "scripts": { diff --git a/server/src/fireboltOpenRpcDereferencing.mjs b/server/src/fireboltOpenRpcDereferencing.mjs index d8edd25e..ade3f518 100644 --- a/server/src/fireboltOpenRpcDereferencing.mjs +++ b/server/src/fireboltOpenRpcDereferencing.mjs @@ -49,80 +49,101 @@ function replaceRefArr(arrWithItemWithRef, posInArrWithRef, lookedUpSchema) { arrWithItemWithRef[posInArrWithRef] = lookedUpSchema; } -// to check for self-referencing schema objects that can otherwise lead to recursive loops causing maximum call stack size to exceed +/** + * Recursively searches through the provided schema object to detect self-referencing schemas by comparing $ref values against a provided path. + * + * @param {object} schemaObj - The schema object to be checked for self-referencing. + * @param {string} path - The reference path to compare against for self-referencing. + * @returns {boolean} Returns true if a self-reference is detected, false otherwise. + */ function selfReferenceSchemaCheck(schemaObj, path) { if (typeof schemaObj !== 'object' || schemaObj === null) { - return null + return false; } // check if self-refernce is present in current schema object if ('$ref' in schemaObj && schemaObj['$ref'] == path) { - return true + return true; } // check if self-reference is present in any nested objects for (const key in schemaObj) { const refValue = selfReferenceSchemaCheck(schemaObj[key], path); - if (refValue !== null) { - return true + if (refValue === true) { + return true; } } - return null + return false; } -// NOTE: Doesn't handle arrays of arrays -function replaceRefs(metaForSdk, thing, key) { - let xSchema, lookedUpSchema, selfReference = false +/** + * Recursively replaces $ref keys in an object with their corresponding schemas to resolve references. + * It avoids infinite recursion by tracking replaced references using a set. This function handles nested + * objects and arrays but does not handle arrays of arrays. It also accounts for schemas defined under + * "x-schemas" and "components" in the OpenRPC. + * + * @param {object} metaForSdk - The metadata object which may contain the schemas for replacement. + * @param {object|array} thing - The object or array where the replacement should occur. + * @param {string|number} key - The key in the 'thing' object that needs to be checked and potentially replaced. + * @param {Set} [replacedRefs] - A set to keep track of replaced references to prevent recursion. Defaults to a new Set. + * @returns {void} This function does not return a value. It mutates the 'thing' object by reference. + */ +function replaceRefs(metaForSdk, thing, key, replacedRefs = new Set()) { + let xSchema, lookedUpSchema; + if (isObject(thing[key])) { - if ('$ref' in thing[key]) { - // If schema resides under x-schemas object in openRPC - if (thing[key]['$ref'].includes("x-schemas")) { - let xSchemaArray = thing[key]['$ref'].split("/"); - xSchema = xSchemaArray.filter(element => element !== "#" && element !== "x-schemas")[0] - lookedUpSchema = lookupSchema(metaForSdk, ref2schemaName(thing[key]['$ref']), xSchema); + const refKey = thing[key]['$ref']; + + if (refKey) { + // Check if this reference was already replaced to prevent recursion. + if (replacedRefs.has(refKey)) { + // If already replaced, remove the $ref to avoid recursion. + delete thing[key]['$ref']; } else { - // else if schema resides under components object in openRPC - lookedUpSchema = lookupSchema(metaForSdk, ref2schemaName(thing[key]['$ref'])); - } - if (lookedUpSchema) { - if (selfReferenceSchemaCheck(lookedUpSchema, thing[key]['$ref']) == true) { - selfReference = true + // If schema resides under x-schemas object in openRPC. + if (refKey.includes("x-schemas")) { + let xSchemaArray = refKey.split("/"); + xSchema = xSchemaArray.filter(element => element !== "#" && element !== "x-schemas")[0]; + lookedUpSchema = lookupSchema(metaForSdk, ref2schemaName(refKey), xSchema); + } else { + // Else if schema resides under components object in openRPC. + lookedUpSchema = lookupSchema(metaForSdk, ref2schemaName(refKey)); } - } - // replace reference path with the corresponding schema object - replaceRefObj(thing, key, lookedUpSchema); - } if (selfReference !== true) { - // if no self-referencing detected, recursively replace references in nested schema objects, else skip dereferencing the schema object further to avoid infinite loop - for (const key2 in thing[key]) { - replaceRefs(metaForSdk, thing[key], key2); + + if (lookedUpSchema && selfReferenceSchemaCheck(lookedUpSchema, refKey)) { + // If it's a self-reference, mark it as replaced. + replacedRefs.add(refKey); + } + + // Replace the reference with the actual schema. + thing[key] = lookedUpSchema || thing[key]; } } + + // Recursively call replaceRefs on nested objects, passing the replacedRefs set forward. + Object.keys(thing[key]).forEach((nestedKey) => { + replaceRefs(metaForSdk, thing[key], nestedKey, replacedRefs); + }); } else if (isArray(thing[key])) { - for (let idx = 0; ii < thing[key].length; idx += 1) { - if (isObject(thing[key][idx])) { - if ('$ref' in thing[idx]) { - lookedUpSchema = lookupSchema(metaForSdk, ref2schemaName(thing[key][idx]['$ref'])); - replaceRefArr(thing[key], idx, lookedUpSchema); - } + thing[key].forEach((item, idx) => { + if (isObject(item)) { + replaceRefs(metaForSdk, thing[key], idx, replacedRefs); } - } + }); } } function dereferenceSchemas(metaForSdk, methodName) { - let newSchema; const methods = metaForSdk.methods; - const matchMethods = methods.filter(function(method) { return method.name === methodName; }); + const matchMethods = methods.filter((method) => method.name === methodName); const matchMethod = matchMethods[0]; - const result = matchMethod.result; - //replaceRefs(metaForSdk, result, 'schema'); + replaceRefs(metaForSdk, matchMethod, 'result'); replaceRefs(metaForSdk, matchMethod, 'params'); replaceRefs(metaForSdk, matchMethod, 'tags'); - } function dereferenceMeta(_meta) { const meta = JSON.parse(JSON.stringify(_meta)); // Deep copy - config.dotConfig.supportedOpenRPCs.forEach(function(oSdk) { + config.dotConfig.supportedOpenRPCs.forEach((oSdk) => { const sdkName = oSdk.name; if ( sdkName in meta ) { const metaForSdk = meta[sdkName]; @@ -137,11 +158,16 @@ function dereferenceMeta(_meta) { return meta; } - // --- Exports --- - export const testExports = { - replaceRefArr + replaceRefArr, + replaceRefObj, + isObject, + isArray, + ref2schemaName, + lookupSchema, + selfReferenceSchemaCheck, + replaceRefs }; export { diff --git a/server/test/mocks/mockOpenRpcJson.mjs b/server/test/mocks/mockOpenRpcJson.mjs new file mode 100644 index 00000000..a27584cf --- /dev/null +++ b/server/test/mocks/mockOpenRpcJson.mjs @@ -0,0 +1,926 @@ +export const meta = { + core: { + openrpc: '1.2.4', + info: { + title: 'Firebolt', + version: '0.6.1', + }, + methods: [ + { + name: 'rpc.discover', + summary: 'Firebolt OpenRPC schema', + params: [], + result: { + name: 'OpenRPC Schema', + schema: { + type: 'object', + }, + }, + }, + { + name: 'Advertising.advertisingId', + tags: [ + { + name: 'capabilities', + 'x-uses': ['xrn:firebolt:capability:advertising:identifier'], + }, + ], + summary: 'Get the advertising ID', + params: [ + { + name: 'options', + summary: 'AdvertisingId options', + required: false, + schema: { + $ref: '#/components/schemas/AdvertisingIdOptions', + }, + }, + ], + result: { + name: 'advertisingId', + summary: 'the advertising ID', + schema: { + type: 'object', + properties: { + ifa: { + type: 'string', + }, + ifa_type: { + type: 'string', + }, + lmt: { + type: 'string', + }, + }, + required: ['ifa'], + }, + }, + examples: [], + }, + ], + components: { + schemas: { + AdvertisingIdOptions: { + title: 'AdvertisingIdOptions', + type: 'object', + properties: { + scope: { + type: 'object', + description: + 'Provides the options to send scope type and id to select desired advertising id', + required: ['type', 'id'], + properties: { + type: { + type: 'string', + enum: ['browse', 'content'], + default: 'browse', + description: 'The scope type, which will determine where to show advertisement', + }, + id: { + type: 'string', + description: 'A value that identifies a specific scope within the scope type', + }, + }, + }, + }, + }, + }, + }, + }, + manage: { + openrpc: '1.2.4', + info: { + title: 'Firebolt', + version: '0.6.1', + }, + methods: [ + { + name: 'rpc.discover', + summary: 'Firebolt OpenRPC schema', + params: [], + result: { + name: 'OpenRPC Schema', + schema: { + type: 'object', + }, + }, + }, + { + name: 'accessory.pair', + summary: 'Pair an accessory with the device.', + params: [ + { + name: 'type', + schema: { + $ref: '#/components/schemas/AccessoryType', + }, + }, + { + name: 'protocol', + schema: { + $ref: '#/components/schemas/AccessoryProtocol', + }, + }, + { + name: 'timeout', + schema: { + $ref: '#/components/schemas/AccessoryPairingTimeout', + }, + }, + ], + result: { + name: 'pairedAccessory', + summary: 'The state of last paired accessory', + schema: { + $ref: '#/components/schemas/AccessoryInfo', + }, + }, + examples: [ + { + name: 'Pair a Bluetooth Remote', + params: [ + { + name: 'type', + value: 'Remote', + }, + { + name: 'protocol', + value: 'BluetoothLE', + }, + { + name: 'timeout', + value: 180, + }, + ], + result: { + name: 'Bluetooth Remote successful pairing example', + value: { + type: 'Remote', + make: 'UEI', + model: 'PR1', + protocol: 'BluetoothLE', + }, + }, + }, + { + name: 'Pair a Bluetooth Speaker', + params: [ + { + name: 'type', + value: 'Speaker', + }, + { + name: 'protocol', + value: 'BluetoothLE', + }, + { + name: 'timeout', + value: 180, + }, + ], + result: { + name: 'Bluetooth Speaker successful pairing example', + value: { + type: 'Speaker', + make: 'Sonos', + model: 'V120', + protocol: 'BluetoothLE', + }, + }, + }, + { + name: 'Pair a RF Remote', + params: [ + { + name: 'type', + value: 'Remote', + }, + { + name: 'protocol', + value: 'RF4CE', + }, + { + name: 'timeout', + value: 180, + }, + ], + result: { + name: 'RF Remote successful pairing example', + value: { + type: 'Remote', + make: 'UEI', + model: '15', + protocol: 'RF4CE', + }, + }, + }, + ], + }, + { + name: 'Metrics.event', + tags: [ + { + name: 'capabilities', + 'x-uses': ['xrn:firebolt:capability:metrics:distributor'], + }, + ], + summary: 'Inform the platform of 1st party distributor metrics.', + params: [ + { + name: 'schema', + summary: 'The schema URI of the metric type', + schema: { + type: 'string', + format: 'uri', + }, + required: true, + }, + { + name: 'data', + summary: 'A JSON payload conforming the the provided schema', + schema: { + $ref: '#/components/schemas/EventObject', + }, + required: true, + }, + ], + result: { + name: 'results', + schema: { + type: 'null', + }, + }, + }, + ], + components: { + schemas: { + 0: { + enabled: true, + speed: 2, + }, + AccessoryList: { + title: 'AccessoryList', + type: 'object', + description: 'Contains a list of Accessories paired to the device.', + properties: { + list: { + type: 'array', + items: { + $ref: '#/components/schemas/AccessoryInfo', + }, + }, + }, + }, + AccessoryPairingTimeout: { + title: 'AccessoryPairingTimeout', + description: + 'Defines the timeout in seconds. If the threshold for timeout is passed without a result it will throw an error.', + type: 'integer', + default: 0, + minimum: 0, + maximum: 9999, + }, + AccessoryType: { + title: 'AccessoryType', + description: 'Type of the device Remote,Speaker or Other', + type: 'string', + enum: ['Remote', 'Speaker', 'Other'], + }, + AccessoryTypeListParam: { + title: 'AccessoryTypeListParam', + description: 'Type of the device Remote,Speaker or Other', + type: 'string', + enum: ['Remote', 'Speaker', 'All'], + }, + AccessoryProtocol: { + title: 'AccessoryProtocol', + description: 'Mechanism to connect the accessory to the device', + type: 'string', + enum: ['BluetoothLE', 'RF4CE'], + }, + AccessoryProtocolListParam: { + title: 'AccessoryProtocolListParam', + description: 'Mechanism to connect the accessory to the device', + type: 'string', + enum: ['BluetoothLE', 'RF4CE', 'All'], + }, + AccessoryInfo: { + title: 'AccessoryInfo', + description: 'Properties of a paired accessory.', + type: 'object', + properties: { + type: { + $ref: '#/components/schemas/AccessoryType', + }, + make: { + type: 'string', + description: 'Name of the manufacturer of the accessory', + }, + model: { + type: 'string', + description: 'Model name of the accessory', + }, + protocol: { + $ref: '#/components/schemas/AccessoryProtocol', + }, + }, + }, + ChallengeRequestor: { + title: 'ChallengeRequestor', + type: 'object', + required: ['id', 'name'], + properties: { + id: { + type: 'string', + description: 'The id of the app that requested the challenge', + }, + name: { + type: 'string', + description: 'The name of the app that requested the challenge', + }, + }, + }, + Challenge: { + title: 'Challenge', + type: 'object', + required: ['capability', 'requestor'], + properties: { + capability: { + type: 'string', + description: 'The capability that is being requested by the user to approve', + }, + requestor: { + description: 'The identity of which app is requesting access to this capability', + $ref: '#/components/schemas/ChallengeRequestor', + }, + }, + }, + ChallengeProviderRequest: { + title: 'ChallengeProviderRequest', + allOf: [ + { + $ref: '#/components/schemas/ProviderRequest', + }, + { + type: 'object', + required: ['parameters'], + properties: { + parameters: { + description: 'The request to challenge the user', + $ref: '#/components/schemas/Challenge', + }, + }, + }, + ], + }, + EventObjectPrimitives: { + title: 'EventObjectPrimitives', + anyOf: [ + { + type: 'string', + maxLength: 256, + }, + { + type: 'number', + }, + { + type: 'integer', + }, + { + type: 'boolean', + }, + { + type: 'null', + }, + ], + }, + EventObject: { + title: 'EventObject', + type: 'object', + maxProperties: 256, + additionalProperties: { + anyOf: [ + { + $ref: '#/components/schemas/EventObjectPrimitives', + }, + { + type: 'array', + maxItems: 256, + items: { + anyOf: [ + { + $ref: '#/components/schemas/EventObjectPrimitives', + }, + { + $ref: '#/components/schemas/EventObject', + }, + ], + }, + }, + { + $ref: '#/components/schemas/EventObject', + }, + ], + }, + }, + GrantResult: { + title: 'GrantResult', + type: 'object', + required: ['granted'], + properties: { + granted: { + type: 'boolean', + description: 'Whether the user approved or denied the challenge', + }, + }, + examples: [ + { + granted: true, + }, + ], + }, + ListenResponse: { + title: 'ListenResponse', + type: 'object', + required: ['event', 'listening'], + properties: { + event: { + type: 'string', + pattern: '[a-zA-Z]+\\.on[A-Z][a-zA-Z]+', + }, + listening: { + type: 'boolean', + }, + }, + additionalProperties: false, + }, + ProviderResponse: { + title: 'ProviderResponse', + type: 'object', + required: ['correlationId'], + additionalProperties: false, + properties: { + correlationId: { + type: 'string', + description: + 'The id that was passed in to the event that triggered a provider method to be called', + }, + result: { + description: 'The result of the provider response.', + }, + }, + }, + ProviderRequest: { + title: 'ProviderRequest', + type: 'object', + required: ['correlationId'], + additionalProperties: false, + properties: { + correlationId: { + type: 'string', + description: + 'The id that was passed in to the event that triggered a provider method to be called', + }, + parameters: { + description: 'The result of the provider response.', + type: ['object', 'null'], + }, + }, + }, + ClosedCaptionsSettingsProviderRequest: { + title: 'ClosedCaptionsSettingsProviderRequest', + allOf: [ + { + $ref: '#/components/schemas/ProviderRequest', + }, + { + type: 'object', + properties: { + parameters: { + title: 'SettingsParameters', + const: null, + }, + }, + }, + ], + examples: [ + { + correlationId: 'abc', + }, + ], + }, + ClosedCaptionsSettings: { + title: 'ClosedCaptionsSettings', + type: 'object', + required: ['enabled', 'styles'], + properties: { + enabled: { + type: 'boolean', + description: 'Whether or not closed-captions should be enabled by default', + }, + styles: { + $ref: '#/components/schemas/ClosedCaptionsStyles', + }, + }, + examples: [ + { + enabled: true, + styles: { + fontFamily: 'Monospace sans-serif', + fontSize: 1, + fontColor: '#ffffff', + fontEdge: 'none', + fontEdgeColor: '#7F7F7F', + fontOpacity: 100, + backgroundColor: '#000000', + backgroundOpacity: 100, + textAlign: 'center', + textAlignVertical: 'middle', + }, + }, + ], + }, + FontFamily: { + type: 'string', + }, + FontSize: { + type: 'number', + minimum: 0, + }, + Color: { + type: 'string', + }, + FontEdge: { + type: 'string', + }, + Opacity: { + type: 'number', + minimum: 0, + maximum: 100, + }, + HorizontalAlignment: { + type: 'string', + }, + VerticalAlignment: { + type: 'string', + }, + ClosedCaptionsStyles: { + title: 'ClosedCaptionsStyles', + type: 'object', + description: 'The default styles to use when displaying closed-captions', + properties: { + fontFamily: { + $ref: '#/components/schemas/FontFamily', + }, + fontSize: { + $ref: '#/components/schemas/FontSize', + }, + fontColor: { + $ref: '#/components/schemas/Color', + }, + fontEdge: { + $ref: '#/components/schemas/FontEdge', + }, + fontEdgeColor: { + $ref: '#/components/schemas/Color', + }, + fontOpacity: { + $ref: '#/components/schemas/Opacity', + }, + backgroundColor: { + $ref: '#/components/schemas/Color', + }, + backgroundOpacity: { + $ref: '#/components/schemas/Opacity', + }, + textAlign: { + $ref: '#/components/schemas/HorizontalAlignment', + }, + textAlignVertical: { + $ref: '#/components/schemas/VerticalAlignment', + }, + }, + }, + KeyboardType: { + title: 'KeyboardType', + type: 'string', + description: 'The type of keyboard to show to the user', + enum: ['standard', 'email', 'password'], + }, + KeyboardParameters: { + title: 'KeyboardParameters', + type: 'object', + required: ['type', 'message'], + properties: { + type: { + $ref: '#/components/schemas/KeyboardType', + description: 'The type of keyboard', + }, + message: { + description: + 'The message to display to the user so the user knows what they are entering', + type: 'string', + }, + }, + examples: [ + { + type: 'standard', + message: 'Enter your user name.', + }, + ], + }, + KeyboardProviderRequest: { + title: 'KeyboardProviderRequest', + type: 'object', + required: ['correlationId', 'parameters'], + properties: { + correlationId: { + type: 'string', + description: 'An id to correlate the provider response with this request', + }, + parameters: { + description: 'The request to start a keyboard session', + $ref: '#/components/schemas/KeyboardParameters', + }, + }, + }, + KeyboardResult: { + title: 'KeyboardResult', + type: 'object', + required: ['text'], + properties: { + text: { + type: 'string', + description: 'The text the user entered into the keyboard', + }, + canceled: { + type: 'boolean', + description: + 'Whether the user canceled entering text before they were finished typing on the keyboard', + }, + }, + }, + KeyboardResultProviderResponse: { + title: 'KeyboardResultProviderResponse', + type: 'object', + required: ['correlationId', 'result'], + properties: { + correlationId: { + type: 'string', + description: + 'The id that was passed in to the event that triggered a provider method to be called', + }, + result: { + description: + 'The result of the provider response, containing what the user typed in the keyboard', + $ref: '#/components/schemas/KeyboardResult', + }, + }, + examples: [ + { + correlationId: '123', + result: { + text: 'some text', + }, + }, + ], + }, + PowerState: { + title: 'PowerState', + type: 'string', + description: + "Device power states. Note that 'suspended' is not included, because it's impossible for app code to be running during that state.", + enum: ['active', 'activeStandby'], + }, + ActiveEvent: { + title: 'ActiveEvent', + type: 'object', + required: ['reason'], + properties: { + reason: { + type: 'string', + enum: [ + 'firstPowerOn', + 'powerOn', + 'rcu', + 'frontPanel', + 'hdmiCec', + 'dial', + 'motion', + 'farFieldVoice', + ], + }, + }, + }, + StandbyEvent: { + title: 'StandbyEvent', + type: 'object', + required: ['reason'], + properties: { + reason: { + type: 'string', + enum: ['inactivity', 'rcu', 'frontPanel', 'hdmiCec', 'dial', 'farFieldVoice', 'ux'], + }, + }, + }, + ResumeEvent: { + title: 'ResumeEvent', + type: 'object', + required: ['reason'], + properties: { + reason: { + type: 'string', + enum: ['system', 'rcu', 'frontPanel', 'hdmiCec', 'dial', 'motion', 'farFieldVoice'], + }, + }, + }, + SuspendEvent: { + title: 'SuspendEvent', + type: 'object', + required: ['reason'], + properties: { + reason: { + type: 'string', + enum: ['powerOn', 'rcu', 'frontPanel'], + }, + }, + }, + InactivityCancelledEvent: { + title: 'InactivityCancelledEvent', + type: 'object', + required: ['reason'], + properties: { + reason: { + type: 'string', + enum: ['rcu', 'frontPanel', 'farFieldVoice', 'dial', 'hdmiCec', 'motion'], + }, + }, + }, + ContentPolicy: { + title: 'ContentPolicy', + type: 'object', + required: ['enableRecommendations', 'shareWatchHistory', 'rememberWatchedPrograms'], + properties: { + enableRecommendations: { + type: 'boolean', + description: 'Whether or not to the user has enabled history-based recommendations', + }, + shareWatchHistory: { + type: 'boolean', + description: + 'Whether or not the user has enabled app watch history data to be shared with the platform', + }, + rememberWatchedPrograms: { + type: 'boolean', + description: 'Whether or not the user has enabled watch history', + }, + }, + examples: [ + { + enableRecommendations: true, + shareWatchHistory: false, + rememberWatchedPrograms: true, + }, + ], + }, + VoiceGuidanceSettings: { + title: 'VoiceGuidanceSettings', + type: 'object', + required: ['enabled', 'speed'], + properties: { + enabled: { + type: 'boolean', + description: 'Whether or not voice guidance should be enabled by default', + }, + speed: { + type: 'number', + description: 'The speed at which voice guidance speech will be read back to the user', + }, + }, + examples: [ + { + enabled: true, + speed: 2, + }, + ], + }, + VoiceSpeed: { + title: 'VoiceSpeed', + type: 'number', + }, + AccessPointList: { + title: 'AccessPointList', + type: 'object', + description: 'List of scanned Wifi networks available near the device.', + properties: { + list: { + type: 'array', + items: { + $ref: '#/components/schemas/AccessPoint', + }, + }, + }, + }, + WifiSecurityMode: { + title: 'WifiSecurityMode', + description: 'Security Mode supported for Wifi', + type: 'string', + enum: [ + 'none', + 'wep64', + 'wep128', + 'wpaPskTkip', + 'wpaPskAes', + 'wpa2PskTkip', + 'wpa2PskAes', + 'wpaEnterpriseTkip', + 'wpaEnterpriseAes', + 'wpa2EnterpriseTkip', + 'wpa2EnterpriseAes', + 'wpa2Psk', + 'wpa2Enterprise', + 'wpa3PskAes', + 'wpa3Sae', + ], + }, + WifiSignalStrength: { + title: 'WifiSignalStrength', + description: 'Strength of Wifi signal, value is negative based on RSSI specification.', + type: 'integer', + default: -255, + minimum: -255, + maximum: 0, + }, + WifiFrequency: { + title: 'WifiFrequency', + description: 'Wifi Frequency in Ghz, example 2.4Ghz and 5Ghz.', + type: 'number', + default: 0, + minimum: 0, + }, + AccessPoint: { + title: 'AccessPoint', + description: 'Properties of a scanned wifi list item.', + type: 'object', + properties: { + ssid: { + type: 'string', + description: 'Name of the wifi.', + }, + securityMode: { + $ref: '#/components/schemas/WifiSecurityMode', + }, + signalStrength: { + $ref: '#/components/schemas/WifiSignalStrength', + }, + frequency: { + $ref: '#/components/schemas/WifiFrequency', + }, + }, + }, + WPSSecurityPin: { + title: 'WPSSecurityPin', + description: 'Security pin type for WPS(Wifi Protected Setup).', + type: 'string', + enum: ['pushButton', 'pin', 'manufacturerPin'], + }, + WifiConnectRequest: { + title: 'WifiConnectRequest', + description: 'Request object for the wifi connection.', + type: 'object', + properties: { + ssid: { + schema: { + type: 'string', + }, + }, + passphrase: { + schema: { + type: 'string', + }, + }, + securityMode: { + schema: { + $ref: '#/components/schemas/WifiSecurityMode', + }, + }, + timeout: { + schema: { + $ref: '#/components/schemas/Timeout', + }, + }, + }, + }, + Timeout: { + title: 'Timeout', + description: + 'Defines the timeout in seconds. If the threshold for timeout is passed for any operation without a result it will throw an error.', + type: 'integer', + default: 0, + minimum: 0, + maximum: 9999, + }, + }, + }, + }, +}; diff --git a/server/test/suite/fireboltOpenRpcDereferencing.test.mjs b/server/test/suite/fireboltOpenRpcDereferencing.test.mjs index 964a475e..967481e7 100644 --- a/server/test/suite/fireboltOpenRpcDereferencing.test.mjs +++ b/server/test/suite/fireboltOpenRpcDereferencing.test.mjs @@ -20,837 +20,143 @@ "use strict"; -import * as fireboltOpenRpcDereferencing from "../../src/fireboltOpenRpcDereferencing.mjs"; +import * as fireboltOpenRpcDereferencing from '../../src/fireboltOpenRpcDereferencing.mjs'; +import { meta } from '../mocks/mockOpenRpcJson.mjs'; -test(`fireboltOpenRpcDereferencing works properly`, () => { - const meta = { - core: { - openrpc: "1.2.4", - info: { - title: "Firebolt", - version: "0.6.1", - }, - methods: [ - { - name: "rpc.discover", - summary: "Firebolt OpenRPC schema", - params: [], - result: { - name: "OpenRPC Schema", - schema: { - type: "object", - }, - }, - }, - ], - components: {}, - }, - manage: { - openrpc: "1.2.4", - info: { - title: "Firebolt", - version: "0.6.1", - }, - methods: [ - { - name: "rpc.discover", - summary: "Firebolt OpenRPC schema", - params: [], - result: { - name: "OpenRPC Schema", - schema: { - type: "object", - }, - }, - }, - { - name: "accessory.pair", - summary: "Pair an accessory with the device.", - params: [ - { - name: "type", - schema: { - $ref: "#/components/schemas/AccessoryType", - }, - }, - { - name: "protocol", - schema: { - $ref: "#/components/schemas/AccessoryProtocol", - }, - }, - { - name: "timeout", - schema: { - $ref: "#/components/schemas/AccessoryPairingTimeout", - }, - }, - ], - result: { - name: "pairedAccessory", - summary: "The state of last paired accessory", - schema: { - $ref: "#/components/schemas/AccessoryInfo", - }, - }, - examples: [ - { - name: "Pair a Bluetooth Remote", - params: [ - { - name: "type", - value: "Remote", - }, - { - name: "protocol", - value: "BluetoothLE", - }, - { - name: "timeout", - value: 180, - }, - ], - result: { - name: "Bluetooth Remote successful pairing example", - value: { - type: "Remote", - make: "UEI", - model: "PR1", - protocol: "BluetoothLE", - }, - }, - }, - { - name: "Pair a Bluetooth Speaker", - params: [ - { - name: "type", - value: "Speaker", - }, - { - name: "protocol", - value: "BluetoothLE", - }, - { - name: "timeout", - value: 180, - }, - ], - result: { - name: "Bluetooth Speaker successful pairing example", - value: { - type: "Speaker", - make: "Sonos", - model: "V120", - protocol: "BluetoothLE", - }, - }, - }, - { - name: "Pair a RF Remote", - params: [ - { - name: "type", - value: "Remote", - }, - { - name: "protocol", - value: "RF4CE", - }, - { - name: "timeout", - value: 180, - }, - ], - result: { - name: "RF Remote successful pairing example", - value: { - type: "Remote", - make: "UEI", - model: "15", - protocol: "RF4CE", - }, - }, - }, - ], - }, - ], - components: { - schemas: { - 0: { - enabled: true, - speed: 2, - }, - AccessoryList: { - title: "AccessoryList", - type: "object", - description: "Contains a list of Accessories paired to the device.", - properties: { - list: { - type: "array", - items: { - $ref: "#/components/schemas/AccessoryInfo", - }, - }, - }, - }, - AccessoryPairingTimeout: { - title: "AccessoryPairingTimeout", - description: - "Defines the timeout in seconds. If the threshold for timeout is passed without a result it will throw an error.", - type: "integer", - default: 0, - minimum: 0, - maximum: 9999, - }, - AccessoryType: { - title: "AccessoryType", - description: "Type of the device Remote,Speaker or Other", - type: "string", - enum: ["Remote", "Speaker", "Other"], - }, - AccessoryTypeListParam: { - title: "AccessoryTypeListParam", - description: "Type of the device Remote,Speaker or Other", - type: "string", - enum: ["Remote", "Speaker", "All"], - }, - AccessoryProtocol: { - title: "AccessoryProtocol", - description: "Mechanism to connect the accessory to the device", - type: "string", - enum: ["BluetoothLE", "RF4CE"], - }, - AccessoryProtocolListParam: { - title: "AccessoryProtocolListParam", - description: "Mechanism to connect the accessory to the device", - type: "string", - enum: ["BluetoothLE", "RF4CE", "All"], - }, - AccessoryInfo: { - title: "AccessoryInfo", - description: "Properties of a paired accessory.", - type: "object", - properties: { - type: { - $ref: "#/components/schemas/AccessoryType", - }, - make: { - type: "string", - description: "Name of the manufacturer of the accessory", - }, - model: { - type: "string", - description: "Model name of the accessory", - }, - protocol: { - $ref: "#/components/schemas/AccessoryProtocol", - }, - }, - }, - ChallengeRequestor: { - title: "ChallengeRequestor", - type: "object", - required: ["id", "name"], - properties: { - id: { - type: "string", - description: "The id of the app that requested the challenge", - }, - name: { - type: "string", - description: "The name of the app that requested the challenge", - }, - }, - }, - Challenge: { - title: "Challenge", - type: "object", - required: ["capability", "requestor"], - properties: { - capability: { - type: "string", - description: - "The capability that is being requested by the user to approve", - }, - requestor: { - description: - "The identity of which app is requesting access to this capability", - $ref: "#/components/schemas/ChallengeRequestor", - }, - }, - }, - ChallengeProviderRequest: { - title: "ChallengeProviderRequest", - allOf: [ - { - $ref: "#/components/schemas/ProviderRequest", - }, - { - type: "object", - required: ["parameters"], - properties: { - parameters: { - description: "The request to challenge the user", - $ref: "#/components/schemas/Challenge", - }, - }, - }, - ], - }, - GrantResult: { - title: "GrantResult", - type: "object", - required: ["granted"], - properties: { - granted: { - type: "boolean", - description: - "Whether the user approved or denied the challenge", - }, - }, - examples: [ - { - granted: true, - }, - ], - }, - ListenResponse: { - title: "ListenResponse", - type: "object", - required: ["event", "listening"], - properties: { - event: { - type: "string", - pattern: "[a-zA-Z]+\\.on[A-Z][a-zA-Z]+", - }, - listening: { - type: "boolean", - }, - }, - additionalProperties: false, - }, - ProviderResponse: { - title: "ProviderResponse", - type: "object", - required: ["correlationId"], - additionalProperties: false, - properties: { - correlationId: { - type: "string", - description: - "The id that was passed in to the event that triggered a provider method to be called", - }, - result: { - description: "The result of the provider response.", - }, - }, - }, - ProviderRequest: { - title: "ProviderRequest", - type: "object", - required: ["correlationId"], - additionalProperties: false, - properties: { - correlationId: { - type: "string", - description: - "The id that was passed in to the event that triggered a provider method to be called", - }, - parameters: { - description: "The result of the provider response.", - type: ["object", "null"], - }, - }, - }, - ClosedCaptionsSettingsProviderRequest: { - title: "ClosedCaptionsSettingsProviderRequest", - allOf: [ - { - $ref: "#/components/schemas/ProviderRequest", - }, - { - type: "object", - properties: { - parameters: { - title: "SettingsParameters", - const: null, - }, - }, - }, - ], - examples: [ - { - correlationId: "abc", - }, - ], - }, - ClosedCaptionsSettings: { - title: "ClosedCaptionsSettings", - type: "object", - required: ["enabled", "styles"], - properties: { - enabled: { - type: "boolean", - description: - "Whether or not closed-captions should be enabled by default", - }, - styles: { - $ref: "#/components/schemas/ClosedCaptionsStyles", - }, - }, - examples: [ - { - enabled: true, - styles: { - fontFamily: "Monospace sans-serif", - fontSize: 1, - fontColor: "#ffffff", - fontEdge: "none", - fontEdgeColor: "#7F7F7F", - fontOpacity: 100, - backgroundColor: "#000000", - backgroundOpacity: 100, - textAlign: "center", - textAlignVertical: "middle", - }, - }, - ], - }, - FontFamily: { - type: "string", - }, - FontSize: { - type: "number", - minimum: 0, - }, - Color: { - type: "string", - }, - FontEdge: { - type: "string", - }, - Opacity: { - type: "number", - minimum: 0, - maximum: 100, - }, - HorizontalAlignment: { - type: "string", - }, - VerticalAlignment: { - type: "string", - }, - ClosedCaptionsStyles: { - title: "ClosedCaptionsStyles", - type: "object", - description: - "The default styles to use when displaying closed-captions", - properties: { - fontFamily: { - $ref: "#/components/schemas/FontFamily", - }, - fontSize: { - $ref: "#/components/schemas/FontSize", - }, - fontColor: { - $ref: "#/components/schemas/Color", - }, - fontEdge: { - $ref: "#/components/schemas/FontEdge", - }, - fontEdgeColor: { - $ref: "#/components/schemas/Color", - }, - fontOpacity: { - $ref: "#/components/schemas/Opacity", - }, - backgroundColor: { - $ref: "#/components/schemas/Color", - }, - backgroundOpacity: { - $ref: "#/components/schemas/Opacity", - }, - textAlign: { - $ref: "#/components/schemas/HorizontalAlignment", - }, - textAlignVertical: { - $ref: "#/components/schemas/VerticalAlignment", - }, - }, - }, - KeyboardType: { - title: "KeyboardType", - type: "string", - description: "The type of keyboard to show to the user", - enum: ["standard", "email", "password"], - }, - KeyboardParameters: { - title: "KeyboardParameters", - type: "object", - required: ["type", "message"], - properties: { - type: { - $ref: "#/components/schemas/KeyboardType", - description: "The type of keyboard", - }, - message: { - description: - "The message to display to the user so the user knows what they are entering", - type: "string", - }, - }, - examples: [ - { - type: "standard", - message: "Enter your user name.", - }, - ], - }, - KeyboardProviderRequest: { - title: "KeyboardProviderRequest", - type: "object", - required: ["correlationId", "parameters"], - properties: { - correlationId: { - type: "string", - description: - "An id to correlate the provider response with this request", - }, - parameters: { - description: "The request to start a keyboard session", - $ref: "#/components/schemas/KeyboardParameters", - }, - }, - }, - KeyboardResult: { - title: "KeyboardResult", - type: "object", - required: ["text"], - properties: { - text: { - type: "string", - description: "The text the user entered into the keyboard", - }, - canceled: { - type: "boolean", - description: - "Whether the user canceled entering text before they were finished typing on the keyboard", - }, - }, - }, - KeyboardResultProviderResponse: { - title: "KeyboardResultProviderResponse", - type: "object", - required: ["correlationId", "result"], - properties: { - correlationId: { - type: "string", - description: - "The id that was passed in to the event that triggered a provider method to be called", - }, - result: { - description: - "The result of the provider response, containing what the user typed in the keyboard", - $ref: "#/components/schemas/KeyboardResult", - }, - }, - examples: [ - { - correlationId: "123", - result: { - text: "some text", - }, - }, - ], - }, - PowerState: { - title: "PowerState", - type: "string", - description: - "Device power states. Note that 'suspended' is not included, because it's impossible for app code to be running during that state.", - enum: ["active", "activeStandby"], - }, - ActiveEvent: { - title: "ActiveEvent", - type: "object", - required: ["reason"], - properties: { - reason: { - type: "string", - enum: [ - "firstPowerOn", - "powerOn", - "rcu", - "frontPanel", - "hdmiCec", - "dial", - "motion", - "farFieldVoice", - ], - }, - }, - }, - StandbyEvent: { - title: "StandbyEvent", - type: "object", - required: ["reason"], - properties: { - reason: { - type: "string", - enum: [ - "inactivity", - "rcu", - "frontPanel", - "hdmiCec", - "dial", - "farFieldVoice", - "ux", - ], - }, - }, - }, - ResumeEvent: { - title: "ResumeEvent", - type: "object", - required: ["reason"], - properties: { - reason: { - type: "string", - enum: [ - "system", - "rcu", - "frontPanel", - "hdmiCec", - "dial", - "motion", - "farFieldVoice", - ], - }, - }, - }, - SuspendEvent: { - title: "SuspendEvent", - type: "object", - required: ["reason"], - properties: { - reason: { - type: "string", - enum: ["powerOn", "rcu", "frontPanel"], - }, - }, - }, - InactivityCancelledEvent: { - title: "InactivityCancelledEvent", - type: "object", - required: ["reason"], - properties: { - reason: { - type: "string", - enum: [ - "rcu", - "frontPanel", - "farFieldVoice", - "dial", - "hdmiCec", - "motion", - ], - }, - }, - }, - ContentPolicy: { - title: "ContentPolicy", - type: "object", - required: [ - "enableRecommendations", - "shareWatchHistory", - "rememberWatchedPrograms", - ], - properties: { - enableRecommendations: { - type: "boolean", - description: - "Whether or not to the user has enabled history-based recommendations", - }, - shareWatchHistory: { - type: "boolean", - description: - "Whether or not the user has enabled app watch history data to be shared with the platform", - }, - rememberWatchedPrograms: { - type: "boolean", - description: - "Whether or not the user has enabled watch history", - }, - }, - examples: [ - { - enableRecommendations: true, - shareWatchHistory: false, - rememberWatchedPrograms: true, - }, - ], - }, - VoiceGuidanceSettings: { - title: "VoiceGuidanceSettings", - type: "object", - required: ["enabled", "speed"], - properties: { - enabled: { - type: "boolean", - description: - "Whether or not voice guidance should be enabled by default", - }, - speed: { - type: "number", - description: - "The speed at which voice guidance speech will be read back to the user", - }, - }, - examples: [ - { - enabled: true, - speed: 2, - }, - ], - }, - VoiceSpeed: { - title: "VoiceSpeed", - type: "number", - }, - AccessPointList: { - title: "AccessPointList", - type: "object", - description: - "List of scanned Wifi networks available near the device.", - properties: { - list: { - type: "array", - items: { - $ref: "#/components/schemas/AccessPoint", - }, - }, - }, - }, - WifiSecurityMode: { - title: "WifiSecurityMode", - description: "Security Mode supported for Wifi", - type: "string", - enum: [ - "none", - "wep64", - "wep128", - "wpaPskTkip", - "wpaPskAes", - "wpa2PskTkip", - "wpa2PskAes", - "wpaEnterpriseTkip", - "wpaEnterpriseAes", - "wpa2EnterpriseTkip", - "wpa2EnterpriseAes", - "wpa2Psk", - "wpa2Enterprise", - "wpa3PskAes", - "wpa3Sae", - ], - }, - WifiSignalStrength: { - title: "WifiSignalStrength", - description: - "Strength of Wifi signal, value is negative based on RSSI specification.", - type: "integer", - default: -255, - minimum: -255, - maximum: 0, - }, - WifiFrequency: { - title: "WifiFrequency", - description: "Wifi Frequency in Ghz, example 2.4Ghz and 5Ghz.", - type: "number", - default: 0, - minimum: 0, - }, - AccessPoint: { - title: "AccessPoint", - description: "Properties of a scanned wifi list item.", - type: "object", - properties: { - ssid: { - type: "string", - description: "Name of the wifi.", - }, - securityMode: { - $ref: "#/components/schemas/WifiSecurityMode", - }, - signalStrength: { - $ref: "#/components/schemas/WifiSignalStrength", - }, - frequency: { - $ref: "#/components/schemas/WifiFrequency", - }, - }, - }, - WPSSecurityPin: { - title: "WPSSecurityPin", - description: "Security pin type for WPS(Wifi Protected Setup).", - type: "string", - enum: ["pushButton", "pin", "manufacturerPin"], - }, - WifiConnectRequest: { - title: "WifiConnectRequest", - description: "Request object for the wifi connection.", - type: "object", - properties: { - ssid: { - schema: { - type: "string", - }, - }, - passphrase: { - schema: { - type: "string", - }, - }, - securityMode: { - schema: { - $ref: "#/components/schemas/WifiSecurityMode", - }, - }, - timeout: { - schema: { - $ref: "#/components/schemas/Timeout", - }, - }, - }, - }, - Timeout: { - title: "Timeout", - description: - "Defines the timeout in seconds. If the threshold for timeout is passed for any operation without a result it will throw an error.", - type: "integer", - default: 0, - minimum: 0, - maximum: 9999, - }, +describe(`fireboltOpenRpcDereferencing`, () => { + describe(`isObject`, () => { + test(`should identify an object properly`, () => { + expect(fireboltOpenRpcDereferencing.testExports.isObject({})).toBeTruthy(); + expect(fireboltOpenRpcDereferencing.testExports.isObject(null)).toBeFalsy(); + expect(fireboltOpenRpcDereferencing.testExports.isObject('string')).toBeFalsy(); + }); + }) + + describe(`isArray`, () => { + test(`should identify an array`, () => { + expect(fireboltOpenRpcDereferencing.testExports.isArray([])).toBeTruthy(); + expect(fireboltOpenRpcDereferencing.testExports.isArray({})).toBeFalsy(); + expect(fireboltOpenRpcDereferencing.testExports.isArray('string')).toBeFalsy(); + }); + }); + + describe(`ref2schemaName`, () => { + test(`should extract schema name from ref string`, () => { + expect(fireboltOpenRpcDereferencing.testExports.ref2schemaName('#/components/schemas/Example')).toBe('Example'); + }); + }); + + describe(`lookupSchema`, () => { + test(`should lookup schema by ref`, () => { + const metaMock = { + 'x-schemas': { + 'Example': { + 'Example': { 'Property': 'Value' } + } }, - }, - }, - }; - const result = fireboltOpenRpcDereferencing.dereferenceMeta(meta); - expect(result).toEqual(expect.not.objectContaining({ components: {} })); -}); + 'components': { + 'schemas': { + 'Example': { + 'Property': 'Value' + } + } + } + }; + expect(fireboltOpenRpcDereferencing.testExports.lookupSchema(metaMock, '#/components/schemas/Example')).toEqual({ 'Property': 'Value' }); + expect(fireboltOpenRpcDereferencing.testExports.lookupSchema(metaMock, '#/x-schemas/Example', 'Example')).toEqual({ 'Property': 'Value' }); + }); + }); + + describe(`dereferenceMeta`, () => { + test(`fireboltOpenRpcDereferencing works properly`, () => { + const result = fireboltOpenRpcDereferencing.dereferenceMeta(meta); + expect(result).toEqual(expect.not.objectContaining({ components: {} })); + }); + }); + + describe(`replaceRefArr`, () => { + test(`replaceRefArr works properly`, () => { + const testArrWithItemWithRef = []; + const testPosInArrWithRef = 0; + const testLookedUpSchema = { test: "Test" }; + const expectedResult = [{ test: "Test" }]; + fireboltOpenRpcDereferencing.testExports.replaceRefArr( + testArrWithItemWithRef, + testPosInArrWithRef, + testLookedUpSchema + ); + expect(testArrWithItemWithRef).toEqual( + expect.arrayContaining(expectedResult) + ); + }); + }); + + describe(`replaceRefObj`, () => { + test(`replaceRefObj works properly`, () => { + const parentOfObjWithRef = { + foo: { '$ref': 'xxx' } + }; + const keyWithinObjWithRef = 'foo'; + const lookedUpSchema = { type: 'string', description: 'This is a schema' }; + const expectedResult = { + foo: { type: 'string', description: 'This is a schema' } + }; + + fireboltOpenRpcDereferencing.testExports.replaceRefObj(parentOfObjWithRef, keyWithinObjWithRef, lookedUpSchema); + + expect(parentOfObjWithRef).toEqual(expectedResult); + expect(parentOfObjWithRef.foo).not.toHaveProperty('$ref'); + }); + }); + + describe(`selfReferenceSchemaCheck`, () => { + test(`selfReferenceSchemaCheck returns true for self-referencing schema objects`, () => { + const path = '#/components/schemas/SelfRef'; + const schemaObjSelfRef = { + '$ref': path + }; + + expect(fireboltOpenRpcDereferencing.testExports.selfReferenceSchemaCheck(schemaObjSelfRef, path)).toBe(true); + }); + + test(`selfReferenceSchemaCheck returns false for non-self-referencing schema objects`, () => { + const path = '#/components/schemas/NonSelfRef'; + const schemaObjNonSelfRef = { + type: 'object', + properties: { + name: { type: 'string' } + } + }; + + expect(fireboltOpenRpcDereferencing.testExports.selfReferenceSchemaCheck(schemaObjNonSelfRef, path)).toBe(false); + }); + + test(`selfReferenceSchemaCheck returns true for nested self-referencing schema objects`, () => { + const path = '#/components/schemas/NestedSelfRef'; + const schemaObjNestedSelfRef = { + type: 'object', + properties: { + nested: { '$ref': path } + } + }; + + expect(fireboltOpenRpcDereferencing.testExports.selfReferenceSchemaCheck(schemaObjNestedSelfRef, path)).toBe(true); + }); + }); + + describe(`replaceRefs`, () => { + test(`replaceRefs works properly`, () => { + // Usingadvertising.advertisingId example + const method = meta.core.methods[1]; + const expectedSchema = { + ...meta.core.components.schemas.AdvertisingIdOptions, + }; + + // Call replaceRefs on the 'params' of the method + fireboltOpenRpcDereferencing.testExports.replaceRefs(meta.core, method, 'params'); -test(`fireboltOpenRpcDereferencing.replaceRefArr works properly`, () => { - const testArrWithItemWithRef = []; - const testPosInArrWithRef = 0; - const testLookedUpSchema = { test: "Test" }; - const expectedResult = [{ test: "Test" }]; - fireboltOpenRpcDereferencing.testExports.replaceRefArr( - testArrWithItemWithRef, - testPosInArrWithRef, - testLookedUpSchema - ); - expect(testArrWithItemWithRef).toEqual( - expect.arrayContaining(expectedResult) - ); + // 'params' should now be an array with the dereferenced schema as its first element + expect(method.params[0].schema).toEqual(expectedSchema); + }); + }); });