diff --git a/documentation/Configs.md b/documentation/Configs.md index 0223ccf5b..c4a4a12fe 100644 --- a/documentation/Configs.md +++ b/documentation/Configs.md @@ -84,3 +84,18 @@ for the reset API and how to use it. **Purpose**: This config fetches default settings from a remote URL. The default URL is set to `https://raw.githubusercontent.com/GPII/universal/master/testData/defaultSettings/defaultSettings.win32.json5` in this config. See [Reset Computer](ResetComputer.md) for what are default settings. + +### Load Solutions from the Repository in Production Contexts + +**Config file**: [`%flowManager/configs/gpii.config.local.flowManager.loadSolutionsFromRepository.json5`](../gpii/node_modules/flowManager/configs/gpii.config.local.flowManager.loadSolutionsFromRepository.json5) + +**Purpose**: This add-on config is for use in production where the cloud based +flow manager supports a valid `/revision` end point, and where the local +flow manager loads solutions registries from both its local hard drive and from +the source code repository. See [SolutionsRegistryDataSource](SolutionsRegistryDataSource.md#local-flow-manager-solutions-registry-data-source) +for more details. + +In addition, for development testing, the default is to assume there is no valid +revision, and no request for the revision is made (see [GpiiRevisionRequester.js](../gpii/node_modules/flowManager/src/GpiiRevisionRequester.js)). +This is done by setting the requester's `cloudURL` option to `null`. This +effectively stops the process of loading solutions from the repository. diff --git a/documentation/FlowManager.md b/documentation/FlowManager.md index 6b65e7bef..cdcbe4dea 100644 --- a/documentation/FlowManager.md +++ b/documentation/FlowManager.md @@ -115,11 +115,19 @@ payload structure to start a matchMaking process. cloud based components of the GPII. * **route:** `/revision` * **method:** `GET` -* **return:** A JSON document containing the revision: +* **return:** On success, an http status code of 200 and a payload containing the revision, e.g.: -```json +```json5 +{"sha256": "2602bdf868aec49993d8780feec42d4e9f995e21"} +``` + +Otherwise, returns status code 404 and an error payload: + +```json5 { - "sha256": "2602bdf868aec49993d8780feec42d4e9f995e21" + "isError": true, + "message": "Error retrieving full git revision: %reason", + "statusCode": 404 } ``` diff --git a/documentation/SolutionsRegistryDataSource.md b/documentation/SolutionsRegistryDataSource.md new file mode 100644 index 000000000..179976134 --- /dev/null +++ b/documentation/SolutionsRegistryDataSource.md @@ -0,0 +1,134 @@ +# Solutions Registry Data Source + +The solutions registry data source provides a RESTful means of fetching +solutions registries via the [FlowManager](FlowManager.md) in order to determine +which solutions are available and appropriate for a user's preferences. A +solution is an application, such as the NVDA screen reader, or an operating +system feature set using a control panel, such as the Windows high contrast +theme. Each solution entry in the registry declares a set of preferences that +it supports and describes how to configure the solution, launch it, reset it, +and stop it. + +The solutions are listed in `JSON` files called "solutions registries". The +structure of a solutions registry is documented in +[SolutionsRegistryFormat](SolutionsRegistryFormat.md). Since solutions are +frequently specific to an operating system, or "platform", there are separate +solution registries for each platform. Examples of platforms are Windows, +GNOME-Linux, MacOS, and Android. + +The SolutionsRegistryDataSource is a component that loads and caches +solutions registries when the GPII starts up, and then serves solutions to the +rest of the system upon request. The flow manager coordinates retrieval of +solutions, user preferences, device information, and so on, passing these to the +[MatchMaker Framework](MatchMakerFramework.md) and [LifecycleManager](LifecycleManager.md). + +There are two initialization workflows with respect to solutions registries +depending on whether the flow manager is running in the cloud or locally on the +client device. The Cloud Based Flow Manager uses a Solutions Registry Data +Source implemented to run in that context, whereas the Local Flow Manager uses a +Solutions Registry Data Source appropriate for running on client machines. +These two scenarios are described in the following two sections. + +## Cloud Based Flow Manager Solutions Registry Data Source + +With respect to the Cloud Based Flow Manager, the solutions registries are +included in the distribution of `gpii-universal` along with the other components +of the GPII -- the flow manager, lifecycle manager and so on. The Cloud Based +FlowManager is a central service for GPII clients, and these clients run on a +variety of platforms. As such, the platform that the Cloud Based FlowManager is +executing on is irrelevant in terms of providing solutions for the client. When +a request for a solutionsvregistry is made, the flow manager needs to have +access to all platform solutions in order to respond with the solutions relevant +to a particular client. + +Here, the SolutionsRegistryDataSource component loads the solutions +registries from the local file system at system startup. In this context, +"local" refers to the file system associated with machine that the Cloud Based +Flow Manager is running on. The SolutionsRegistryDataSource is a +subcomponent of the flow manager, and the sequence of operations and +events that occur during its instantiation are: + +
    +
  1. loadSolutions.loadFromLocalDisk, + +
  2. +
  3. loadSolutions.solutionsLoaded + +
  4. +
+ +The `solutionsRegistryReady` informs the flow manager that its +SolutionsRegistryDataSource is ready to provide solutions upon request. + +## Local Flow Manager Solutions Registry Data Source + +As in the case of the Cloud Based FlowManager, the solutions registries are +included in the distribution of GPII for the client. However, they are not +updated as frequently as those in the cloud. In particular, the solutions +registry may be stale for the platform that the client is running on. However, +the registry associated with the latest version of the cloud based GPII is +available in `gpii-universal`'s source code repository (github), and can be +downloaded from there. In this regard, the Cloud Based FlowManager provides a +`/revision` end-point that responds with the full `SHA256` of the revision of +the source associated with the latest solutions registries. + +The SolutionsRegistryDataSource associated with the Local FlowManager uses the +following sequence of events and operations at startup: + +
    +
  1. loadSolutions.loadFromLocalDisk, + +
  2. +
  3. loadSolutions.getRevision, + +
  4. +
  5. loadSolutions.loadFromRepository, + +
  6. +
  7. loadSolutions.solutionsLoaded + +
  8. +
+ +The above sequence is embeded within the Local FlowManager's `flowManagerReady` +startup interlock such that all initialization is completed before the GPII +client responds to user interactions. + +Note that both the `getRevision` and/or the `loadFromRepository` steps could +fail. In that case, the latest solutions registry for the client platform will +not be downloaded and cached within the client. When solutions for the client +platform are requested, the SolutionsRegistryDataSource uses a fallback where +the solutions loaded from the local file system during the first +`loadFromLocalDisk` step are provided. + +Further note that the `getRevision` step feeds its result into the +`loadFromRepository` step. The revision is necessary to fetch the +correct solutions registry from the repository. If a developer wants to avoid +that, they can set the `cloudURL` of the `GpiiRevisionRequester` to `null`, +effectively stopping the entire sequence. No solutions registry will be +downloaded from the repository in that case. diff --git a/gpii/node_modules/flowManager/configs/gpii.config.local.flowManager.loadSolutionsFromRepository.json5 b/gpii/node_modules/flowManager/configs/gpii.config.local.flowManager.loadSolutionsFromRepository.json5 new file mode 100644 index 000000000..680c905c8 --- /dev/null +++ b/gpii/node_modules/flowManager/configs/gpii.config.local.flowManager.loadSolutionsFromRepository.json5 @@ -0,0 +1,21 @@ +/** + * This configuration is for the production version of the system where the + * local flowManager provides its solutions registry dataSource with the url to + * the cloud based flowManager's /revision endpoint. + */ +{ + "type": "gpii.config.local.flowManager.loadSolutionsFromRepository", + "options": { + "distributeOptions": { + "flowManager.defaultRevisionCloudURL": { + "record": "http://localhost:8084", + "target": "{that gpii.flowManager.local solutionsRegistryDataSource revisionRequester}.options.cloudURL" + }, + "flowManager.gpiiRevisionCloudURL": { + "record": "@expand:kettle.resolvers.env(GPII_CLOUD_URL)", + "target": "{that gpii.flowManager.local solutionsRegistryDataSource revisionRequester}.options.cloudURL", + "priority": "after:flowManager.defaultRevisionCloudURL" + } + } + } +} diff --git a/gpii/node_modules/flowManager/configs/gpii.flowManager.config.base.json5 b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.base.json5 index 30f65ca07..8e0d1b254 100644 --- a/gpii/node_modules/flowManager/configs/gpii.flowManager.config.base.json5 +++ b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.base.json5 @@ -7,7 +7,6 @@ "distributeOptions": { "flowManager.solutions": { "record": { - "type": "gpii.flowManager.solutionsRegistry.dataSource", "options": { "gradeNames": "gpii.flowManager.solutionsRegistry.dataSource.moduleTerms", "path": "%gpii-universal/testData/solutions/" diff --git a/gpii/node_modules/flowManager/configs/gpii.flowManager.config.local.base.json5 b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.local.base.json5 index b7814ffb0..0e812a221 100644 --- a/gpii/node_modules/flowManager/configs/gpii.flowManager.config.local.base.json5 +++ b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.local.base.json5 @@ -5,6 +5,14 @@ "flowManager.local": { "record": ["gpii.flowManager.local"], "target": "{that flowManager}.options.gradeNames" + }, + "flowManager.local.solutionsRegistryRepositoryPrefix": { + "record": "https://raw.githubusercontent.com/GPII/universal", + "target": "{that flowManager solutionsRegistryDataSource repositorySolutionsLoader}.options.urlPrefix" + }, + "flowManager.local.solutionsRegistryRepositorySuffix": { + "record": "testData/solutions", + "target": "{that flowManager solutionsRegistryDataSource repositorySolutionsLoader}.options.urlSuffix" } } }, diff --git a/gpii/node_modules/flowManager/src/FlowManager.js b/gpii/node_modules/flowManager/src/FlowManager.js index 61fc345d3..1c9f58d93 100644 --- a/gpii/node_modules/flowManager/src/FlowManager.js +++ b/gpii/node_modules/flowManager/src/FlowManager.js @@ -42,9 +42,8 @@ require("gpii-user-errors"); fluid.defaults("gpii.flowManager", { gradeNames: ["kettle.app"], components: { - // TODO: Make this use the solutions registry data source by default? solutionsRegistryDataSource: { - type: "kettle.dataSource", + type: "gpii.flowManager.solutionsRegistry.dataSource.cloudBased", options: { termMap: { "os": "%os", @@ -154,9 +153,23 @@ fluid.defaults("gpii.flowManager.local", { "lifecycleManager.pspChannel.sessionBinder": { record: "gpii.pspChannel.sessionBinder", target: "{that lifecycleManager gpii.lifecycleManager.userSession}.options.gradeNames" + }, + "solutionsRegistryDataSource.platformReporter": { + record: { + platformReporter: "{gpii.flowManager.local}.deviceReporter.platformReporter" + }, + target: "{that solutionsRegistryDataSource}.options.components" } }, components: { + solutionsRegistryDataSource: { + type: "gpii.flowManager.solutionsRegistry.dataSource.local", + options: { + events: { + solutionsRegistryReady: "{gpii.flowManager.local}.events.solutionsRegistryReady" + } + } + }, lifecycleManager: { type: "gpii.lifecycleManager", options: { @@ -222,10 +235,12 @@ fluid.defaults("gpii.flowManager.local", { preferencesSavedSuccess: null, preferencesSavedError: null, noUserLoggedIn: null, + solutionsRegistryReady: null, getDefaultSettingsData: null, defaultSettingsDataLoaded: null, flowManagerReady: { events: { + "solutionsRegistryReady": "solutionsRegistryReady", "defaultSettingsDataLoaded": "defaultSettingsDataLoaded", "kettleReady": "{kettle.server}.events.onListen" } @@ -242,7 +257,6 @@ fluid.defaults("gpii.flowManager.local", { funcName: "gpii.flowManager.local.mountWebSocketsSettingsHandler", args: ["{webSocketsSettingsHandlerComponent}"] }, - // Fire "getDefaultSettingsData" event to trigger promise transform chain that does: // 1. Read default settings from the reset to default file; // 2. Calculate defaultLifecycleInstructions and defaultSnapshot based on the default settings; diff --git a/gpii/node_modules/flowManager/src/GpiiRevisionRequester.js b/gpii/node_modules/flowManager/src/GpiiRevisionRequester.js new file mode 100644 index 000000000..846be9ff1 --- /dev/null +++ b/gpii/node_modules/flowManager/src/GpiiRevisionRequester.js @@ -0,0 +1,79 @@ +/*! +GPII Full SHA Revision DataSource + +Copyright 2020 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +require("kettle"); + +fluid.registerNamespace("gpii.flowmanager.revisionRequester"); + +fluid.defaults("gpii.flowmanager.revisionRequester", { + gradeNames: ["fluid.component"], + + // Prepend CBFM host/port distributed down from, e.g., gpii.flowManager.config.untrusted + // For development tests where there is no valid /revision endpoint in the + // cloud, this is left as null. + cloudURL: null, + urlTemplate: "%cloudURL/revision", + + // Compute url from CBFM base URL and template + revisionGetUrl: { + expander: { + funcName: "fluid.stringTemplate", + args: ["{that}.options.urlTemplate", { + cloudURL: "{that}.options.cloudURL" + }] + } + }, + components: { + gpiiRevisionDataSource: { + type: "kettle.dataSource.URL", + options: { + url: "{revisionRequester}.options.revisionGetUrl" + } + } + }, + invokers: { + getRevision: { + funcName: "gpii.flowmanager.revisionRequester.getRevision", + args: ["{that}"] + } + } +}); + +/** + * Retrieve the respository revision's full SHA256 from the cloud. + * @param {Component} that - An instance of gpii.flowManager.revisionRequester. + * @return {Promise} A promise whose resolved value is eiher the revision or, if + * there is an error, an object with an "isError: true" property. The revision + * has the form { "sha256": "86a83d2f93a6f8f954a4fef618ca6aea1399c980" }. + */ +gpii.flowmanager.revisionRequester.getRevision = function (that) { + var togo = fluid.promise(); + if (that.options.cloudURL !== null) { + var revisionPromise = that.gpiiRevisionDataSource.get(); + revisionPromise.then(function (/*revision*/) { + fluid.promise.follow(revisionPromise, togo); + }, function (err) { + togo.resolve(err); + }); + } else { + // If the url to the CBFM is null, assume this is running in a + // development testing enviroment or, generally, a scenario where + // requests for the revision are to be suppressed. + togo.resolve(null); + } + return togo; +}; diff --git a/gpii/node_modules/flowManager/src/MatchMaking.js b/gpii/node_modules/flowManager/src/MatchMaking.js index f805484db..0d051f5d5 100644 --- a/gpii/node_modules/flowManager/src/MatchMaking.js +++ b/gpii/node_modules/flowManager/src/MatchMaking.js @@ -96,17 +96,15 @@ gpii.flowManager.getSolutions = function (solutionsRegistryDataSource, deviceContext, onSuccessEvent, onErrorEvent) { var promiseTogo = fluid.promise(); - var os = fluid.get(deviceContext, "OS.id"); - var promise = solutionsRegistryDataSource.get({}); - promise.then(function (solutions) { - var solutionsRegistryEntries = gpii.matchMakerFramework.filterSolutions(solutions[os], deviceContext); + var promise = solutionsRegistryDataSource.get({os: fluid.get(deviceContext, "OS.id")}); + promise.then(function (entries) { + var solutionsRegistryEntries = gpii.matchMakerFramework.filterSolutions(entries, deviceContext); fluid.log("Fetched filtered solutions registry entries: ", gpii.renderMegapayload({solutionsRegistryEntries: solutionsRegistryEntries})); promiseTogo.resolve({ - solutionsRegistryEntries: solutionsRegistryEntries, - solutions: solutions + solutionsRegistryEntries: solutionsRegistryEntries }); if (onSuccessEvent) { - onSuccessEvent.fire(solutionsRegistryEntries, solutions); + onSuccessEvent.fire(solutionsRegistryEntries); } }, function (error) { promiseTogo.reject(error); @@ -114,7 +112,6 @@ onErrorEvent.fire(error); } }); - return promiseTogo; }; @@ -187,8 +184,7 @@ gpiiKey: "{that}.gpiiKey", preferences: "{arguments}.preferences.0", deviceContext: "{arguments}.deviceContext.0", - solutionsRegistryEntries: "{arguments}.solutions.0", - fullSolutionsRegistry: "{arguments}.solutions.1" + solutionsRegistryEntries: "{arguments}.solutions.0" }] } }, diff --git a/gpii/node_modules/flowManager/src/RepositorySolutionsLoader.js b/gpii/node_modules/flowManager/src/RepositorySolutionsLoader.js new file mode 100644 index 000000000..f6527a6c9 --- /dev/null +++ b/gpii/node_modules/flowManager/src/RepositorySolutionsLoader.js @@ -0,0 +1,108 @@ +/*! +GPII Full SHA Revision DataSource + +Copyright 2020 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +require("kettle"); + +fluid.registerNamespace("gpii.flowManager.repositorySolutionsLoader"); + +fluid.defaults("gpii.flowManager.repositorySolutionsLoader", { + gradeNames: ["kettle.dataSource.URL"], + urlTemplate: "%prefix/%revision/%suffix/%fileName", + url: { + expander: { + funcName: "fluid.stringTemplate", + args: ["{that}.options.urlTemplate", { + prefix: "{that}.options.urlPrefix", + suffix: "{that}.options.urlSuffix" + }] + } + }, + termMap: { + revision: "%directRevision", + fileName: "%directFileName" + }, + // The URL prefix and suffix are distributed from a config file such as + // e.g., gpii.flowManager.config.local.base. + urlPrefix: "", + urlSuffix: "", + protocol: "https:", // default + members: { + // Contents of the solutions registry file downloaded from the source + // code repository + solutionsRegistry: null + }, + components: { + encoding: { + type: "kettle.dataSource.encoding.JSON5" + } + }, + invokers: { + getSolutions: { + funcName: "gpii.flowManager.repositorySolutionsLoader.getSolutions", + args: ["{that}", "{arguments}.0", "{arguments}.1"] + // gpii revision, platform id + } + } +}); + +/** + * Retrieve the solutions registry JSON file from the source code repository. + * @param {Component} that - An instance of gpii.flowManager.repositorySolutionsLoader: + * @param {Component} that.solutionsRegistry - The contents of the solutions + * registry fetched from the source code repository, set herein (will be + * `null` on failure). + * @param {String} gpiiRevision - the SHA256 revision of the repository to fetch. + * @param {String} platformId - the platform matching the solutions registry + * that this is retrieving, e.g. "win32". It is used + * to construct the name of the solutions registry + * file. + * @return {Promise} A promise whose resolved value is eiher the solutions + * registry, or, if there is an error, an object with an "isError: true" + * property. + */ +gpii.flowManager.repositorySolutionsLoader.getSolutions = function (that, gpiiRevision, platformId) { + var togo = fluid.promise(); + if (gpiiRevision && platformId) { + var fileName = platformId + ".json5"; + var solutionsPromise = that.get({ + directRevision: gpiiRevision, + directFileName: fileName + }); + solutionsPromise.then(function (solutions) { + var taggedRegistry = {}; + taggedRegistry[platformId] = solutions; + that.solutionsRegistry = fluid.freezeRecursive(taggedRegistry); + togo.resolve(that.solutionsRegistry); + }, function (err) { + fluid.log("Error retrieving solutions from repository: ", err, ", ", that.options.url); + togo.reject(err); + }); + } else { + var msg = "Error retrieving solutions from repository: missing "; + if (!gpiiRevision && !platformId) { + msg += "revision and platform ID"; + } + else if (!gpiiRevision) { + msg += "revision"; + } + else { + msg += "platform ID"; + } + togo.reject({isError: true, message: msg, statusCode: 404}); + } + return togo; +}; diff --git a/gpii/node_modules/flowManager/src/SolutionsRegistryDataSource.js b/gpii/node_modules/flowManager/src/SolutionsRegistryDataSource.js index 1f81a4c72..4b6aba9a6 100644 --- a/gpii/node_modules/flowManager/src/SolutionsRegistryDataSource.js +++ b/gpii/node_modules/flowManager/src/SolutionsRegistryDataSource.js @@ -2,6 +2,7 @@ * GPII Solutions Registry Datasource * * Copyright 2016 RtF-I + * Copyright 2020 OCAD University * * Licensed under the New BSD license. You may not use this file except in * compliance with this License. @@ -20,13 +21,20 @@ var fluid = require("infusion"), gpii = fluid.registerNamespace("gpii"), fs = require("fs"); - require("kettle"); +require("./GpiiRevisionRequester.js"); +require("./RepositorySolutionsLoader.js"); fluid.registerNamespace("gpii.flowManager.solutionsRegistry"); +// The base solutions registry data source, containing information and functions +// shared by derived grades. fluid.defaults("gpii.flowManager.solutionsRegistry.dataSource", { gradeNames: ["kettle.dataSource"], + termMap: { + "os": "%os", + "version": "%version" + }, components: { encoding: { type: "kettle.dataSource.encoding.none" @@ -35,21 +43,35 @@ fluid.defaults("gpii.flowManager.solutionsRegistry.dataSource", { members: { fullSolutionsRegistry: null }, - readOnlyGrade: "gpii.flowManager.solutionsRegistry.dataSource", invokers: { - getImpl: { - funcName: "gpii.flowManager.solutionsRegistry.dataSource.handle", - args: ["{that}", "{arguments}.1", "{arguments}.2"] - // options, directModel + loadFromLocalDisk: { + funcName: "gpii.flowManager.solutionsRegistry.dataSource.loadFromLocalDisk", + args: ["{that}"] } }, + events: { + loadSolutions: null, + solutionsRegistryReady: null + }, listeners: { - onCreate: "gpii.flowManager.solutionsRegistry.dataSource.loadSolutionsRegistry" + "onCreate.loadSolutions": { + listener: "fluid.promise.fireTransformEvent", + args: ["{that}.events.loadSolutions"] + } } }); -// TODO: Add an invoker to reload once we are "more live". -gpii.flowManager.solutionsRegistry.dataSource.loadSolutionsRegistry = function (that) { +/** + * Load the solutions registry from the local file system -- the file system + * that this solutions registry data source is running on. + * + * @param {Object} that - The gpii.flowManager.solutionsRegistry.dataSource. + * @param {String} that.options.path - The path to the solutions registry + * directory. + * @param {Object} that.fullSolutionsRegistry - This will be set to the + * collection of solution registries given by path. + */ +gpii.flowManager.solutionsRegistry.dataSource.loadFromLocalDisk = function (that) { if (!that.options.path) { fluid.fail("The solutionsRegistry datasource ", that, " needs a \"path\" option pointing to the solution entries folder"); } @@ -57,33 +79,189 @@ gpii.flowManager.solutionsRegistry.dataSource.loadSolutionsRegistry = function ( if (!fs.existsSync(url)) { fluid.fail("The path provided to the solutionsRegistry datasource (", url, ") has not been found on the file system"); } - that.fullSolutionsRegistry = fluid.freezeRecursive(require(url)); }; +// Solutions registry data source used by the Cloud Based Flow Manager +fluid.defaults("gpii.flowManager.solutionsRegistry.dataSource.cloudBased", { + gradeNames: ["gpii.flowManager.solutionsRegistry.dataSource"], + readOnlyGrade: "gpii.flowManager.solutionsRegistry.dataSource.cloudBased", + invokers: { + getImpl: { + funcName: "gpii.flowManager.solutionsRegistry.dataSource.cloudBased.handle", + args: ["{that}", "{arguments}.1", "{arguments}.2"] + // options, directModel + } + }, + listeners: { + "loadSolutions.loadFromLocalDisk": { + listener: "{that}.loadFromLocalDisk", + priority: "first" + }, + "loadSolutions.solutionsLoaded": { + listener: "{that}.events.solutionsRegistryReady", + priority: "after:loadFromLocalDisk" + } + } +}); + /** - * Handler for get requests of solutions registry. It will return either a full solution registry, - * or if an 'os' is provided in the requestOptions, only the entries for that os will be returned + * Handler for get requests to the solutions registry used by the Cloud Based + * Flow Manager. It will return an object containing the solutions registry for + * the given platform. * - * @param {Object} that - The gpii.flowManager.solutionsRegistry.dataSource. - * @param {Object} requestOptions - Currently the only request option supported is "os". If provided, - * the returned solutions registry will be filtered by OS version. - * @return {Promise} A promise that will be resolved with results (see above) or rejected on error. + * @param {Object} that - The gpii.flowManager.solutionsRegistry.dataSource.cloudBased + instance that is handling the GET request. + * @param {Object} requestOptions - Currently the only request option supported + * is "os". The returned solutions registry will be the solutions for + * the OS version provided. + * @return {Promise} A promise that will be resolved with results (see above) or + * rejected on error. + */ +gpii.flowManager.solutionsRegistry.dataSource.cloudBased.handle = function (that, requestOptions) { + var promise = fluid.promise(); + if (requestOptions.os && requestOptions.os in that.fullSolutionsRegistry) { + promise.resolve(that.fullSolutionsRegistry[requestOptions.os]); + } else { + promise.reject({ + isError: true, + message: "The requested OS (" + requestOptions.os + ") was not present in the solutions registry" + }); + } + return promise; +}; + +// Solutions registry datasource used by the Local Flow Manager. On creation, +// it first loads all of the solution registries from the local file system for +// all of the platforms (win32, linux, darwin, android, etc). Then, the +// solutions for the current platform are downloaded from the source code +// repository. The current platform is the platform on which the Local Flow +// Manager is running. The solutions fetched from the source code repository +// overlay the set retrieved from the local file system at the first step. If +// the download from the source code repository fails, then the solutions loaded +// from the local file system are used. +fluid.defaults("gpii.flowManager.solutionsRegistry.dataSource.local", { + gradeNames: ["gpii.flowManager.solutionsRegistry.dataSource"], + components: { + revisionRequester: { + type: "gpii.flowmanager.revisionRequester" + }, + repositorySolutionsLoader: { + type: "gpii.flowManager.repositorySolutionsLoader" + } +// The platformReporter is distributed down from the local flowManager +// platformReporter: { +// type: "gpii.platformReporter.native" +// } + }, + readOnlyGrade: "gpii.flowManager.solutionsRegistry.dataSource.local", + members: { + repositorySolutionsRegistry: null + }, + invokers: { + getImpl: { + funcName: "gpii.flowManager.solutionsRegistry.dataSource.local.handle", + args: ["{that}", "{arguments}.1"] + // options + } + }, + listeners: { + "loadSolutions.loadFromLocalDisk": { + listener: "{that}.loadFromLocalDisk", + priority: "first" + }, + "loadSolutions.getRevision": { + listener: "{revisionRequester}.getRevision", + priority: "after:loadFromLocalDisk" + }, + "loadSolutions.loadFromRepository": { + listener: "gpii.flowManager.solutionsRegistry.dataSource.local.loadFromRepository", + args: ["{that}", "{arguments}.0", "{that}.platformReporter"], + // revision + priority: "after:getRevision" + }, + "loadSolutions.solutionsLoaded": { + listener: "{that}.events.solutionsRegistryReady", + priority: "after:loadFromRepository" + } + } +}); + +/** + * Retrieve the solutions registry JSON file from the source code repository. + * If successful, sets the repositorySolutionsRegistry member to the fetched + * solutions registry. + * @param {Component} that - An instance of gpii.flowManager.solutionsRegistry.dataSource + * @param {Object} that.repositorySolutionsRegistry - Set to point to the + * solutions registry retrieved from the respository, or null if + * none were retrieved. + * @param {Object} revision - the SHA256 revision of the repository in the form: + * { sha256: ... }. If the sha256 is missing, the + * result of the load is null. + * @param {Component} platformReporter - used to get the name of the platform, + * e.g., "win32". + * @return {Promise} A promise whose resolved value is either the solutions + * registry from the source code repository, or null, if there was an error. + */ +gpii.flowManager.solutionsRegistry.dataSource.local.loadFromRepository = function (that, revision, platformReporter) { + var togo = fluid.promise(); + if (revision) { + var gpiiRevision = revision.sha256; + var platformId = platformReporter.reportPlatform().id; + var repoLoadPromise = that.repositorySolutionsLoader.getSolutions(gpiiRevision, platformId); + + // Either the solutions registry has been successfully retrieved from the + // repository, or it hasn't. Set that.repositorySolutionsRegistry to + // to either the result, or to null. + repoLoadPromise.then( + function (repositorySolutions) { + that.repositorySolutionsRegistry = repositorySolutions; + togo.resolve(repositorySolutions); + }, + function () { + that.repositorySolutionsRegistry = null; + togo.resolve(null); + } + ); + } else { + that.repositorySolutionsRegistry = null; + togo.resolve(null); + } + return togo; +}; + +/** + * Handler for get requests when running with the gpii-app client and the Local + * Flow Manager. If a solutions registry was retrieved from the source code + * repository, it is used to mask the equivalent platform in the full set of + * solutions registries. This handler will return the solutions registry for + * the given platform. + * @param {Object} that - The gpii.flowManager.solutionsRegistry.dataSource.local + * instance. + * @param {Object} requestOptions - Currently the only request option supported + * is "os". The returned solutions registry will be the solutions for the + * given OS version. + * @return {Promise} A promise that will be resolved with results (see above) or + * rejected on error. */ -gpii.flowManager.solutionsRegistry.dataSource.handle = function (that, requestOptions) { +gpii.flowManager.solutionsRegistry.dataSource.local.handle = function (that, requestOptions) { var promise = fluid.promise(); - if (requestOptions.os) { // if "os" is defined, return only solution registry entries for that OS - if (requestOptions.os in that.fullSolutionsRegistry) { + if (requestOptions && requestOptions.os) { + if (that.repositorySolutionsRegistry && (requestOptions.os in that.repositorySolutionsRegistry)) { + promise.resolve(that.repositorySolutionsRegistry[requestOptions.os]); + } else if (requestOptions.os in that.fullSolutionsRegistry) { promise.resolve(that.fullSolutionsRegistry[requestOptions.os]); } else { promise.reject({ isError: true, - message: "The requested OS (" + requestOptions.os + ") was not present in the solutions registry", - statusCode: 404 + message: "The requested OS (" + requestOptions.os + ") was not present in the solutions registry" }); } - } else { // if no "os" is requested, return the full solutions registry - promise.resolve(that.fullSolutionsRegistry); + } else { + promise.reject({ + isError: true, + message: "Missing OS (undefined) for accessing the solutions registry" + }); } return promise; }; diff --git a/gpii/node_modules/flowManager/test/CaptureTests.js b/gpii/node_modules/flowManager/test/CaptureTests.js index 981fafa96..16fe93fc2 100644 --- a/gpii/node_modules/flowManager/test/CaptureTests.js +++ b/gpii/node_modules/flowManager/test/CaptureTests.js @@ -41,9 +41,10 @@ gpii.tests.flowManager.capture.checkForFakeMags = function (payload) { }, payload.fakemag2); }; +gpii.tests.flowManager.capture.platformId = "darwin"; gpii.tests.flowManager.capture.platformReporter = function () { return { - id: "darwin" + id: gpii.tests.flowManager.capture.platformId }; }; @@ -53,9 +54,17 @@ gpii.tests.flowManager.capture.checkForInstalledMags = function (payload) { jqUnit.assertEquals("Check for Fake Mag 2", "Fake Magnifier 2 - fully featured", payload.fakemag2.name); }; +gpii.tests.flowManager.capture.handleSolutionsReadyEvent = function (that) { + that.isReady = true; +}; + +gpii.tests.flowManager.capture.solutionsRegistryIsReady = function (msg, solutionsRegistryDataSource) { + jqUnit.assertTrue(msg, solutionsRegistryDataSource.isReady); +}; + /* * A simple resolver config for testing resolver substituion inside of solutions for - * capture. This only needs to support a single varialbe in test `PWD` that will be + * capture. This only needs to support a single variable in test `PWD` that will be * used to lookup json settings files for the fake magnifier configurations. */ fluid.defaults("gpii.tests.flowManager.capture.standardResolverConfig", { @@ -81,8 +90,18 @@ fluid.defaults("gpii.tests.flowManager.capture.tests", { name: "Simple system capture", tests: [{ name: "Check for existing FakeMag Settings", - expect: 2, + expect: 3, sequence: [{ + // Cannot listen for solutionsRegistryReady event directly + // since it is long gone by the time this executes. Check + // that the handleSolutionsReadyEvent listener heard the + // event. + funcName: "gpii.tests.flowManager.capture.solutionsRegistryIsReady", + args: [ + "Solutions registry ready", + "{config}.server.flowManager.solutionsRegistryDataSource" + ] + }, { task: "{config}.server.flowManager.capture.getSystemSettingsCapture", args: [], resolve: "gpii.tests.flowManager.capture.checkForFakeMags", diff --git a/gpii/node_modules/flowManager/test/GpiiRevisionRequesterTests.js b/gpii/node_modules/flowManager/test/GpiiRevisionRequesterTests.js new file mode 100644 index 000000000..8706617dd --- /dev/null +++ b/gpii/node_modules/flowManager/test/GpiiRevisionRequesterTests.js @@ -0,0 +1,219 @@ +/*! +GPII revision request tests + +Copyright 2020 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = fluid || require("infusion"), + gpii = fluid.registerNamespace("gpii"), + kettle = require("kettle"), + nock = require("nock"); + +fluid.require("%gpii-universal/gpii/node_modules/testing/src/NockUtils.js"); +fluid.require("%flowManager/src/GpiiRevisionRequester.js"); + +kettle.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.revisionRequester"); + +gpii.tests.revisionRequester.hostname = "http://gpii.net"; +gpii.tests.revisionRequester.path = "/revision"; + +// Set up mock cloud request/response +gpii.tests.revisionRequester.setUpNock = function (config) { + var cloudMock = nock(config.hostname); + cloudMock.log(console.log); + + // mock GET request + cloudMock.get(config.path) + .reply(config.status, config.response); +}; + +// Revision requester customized for testing +fluid.defaults("gpii.tests.revisionRequester", { + gradeNames: ["gpii.flowmanager.revisionRequester"], + cloudURL: gpii.tests.revisionRequester.hostname +}); + +// Base testEnvironment +fluid.defaults("gpii.tests.revisionRequesterTests", { + gradeNames: ["fluid.test.testEnvironment"], + testCaseHolderGrade: null, // supplied by individual tests + distributeOptions: { + testCaseHolderGrade: { + source: "{that}.options.testCaseHolderGrade", + target: "{that > testCaseHolder}.type" + } + }, + components: { + revisionRequester: { + type: "gpii.tests.revisionRequester" + }, + testCaseHolder: { + type: "fluid.test.testCaseHolder" + } + } +}); + +// 1. Successful retrieval +gpii.tests.revisionRequester.success = { + nockConfig: { + hostname: gpii.tests.revisionRequester.hostname, + path: gpii.tests.revisionRequester.path, + type: "get", + status: 200, + response: {"sha256": "2602bdf868aec49993d8780feec42d4e9f995e21"} + } +}; + +fluid.defaults("gpii.tests.revisionRequester.testCaseHolder.success", { + gradeNames: "fluid.test.testCaseHolder", + modules: [{ + name: "Revision requester module tests - successful retrieval", + expect: 1, + tests: [{ + name: "Response: valid revision", + sequence: [{ + task: "{revisionRequester}.getRevision", + resolve: "jqUnit.assertDeepEq", + resolveArgs: [ + "The response is the expected revision", + gpii.tests.revisionRequester.success.nockConfig.response, + "{arguments}.0" + ] + }] + }] + }] +}); + +fluid.defaults("gpii.tests.revisionRequesterTests.success", { + gradeNames: ["gpii.tests.revisionRequesterTests", "gpii.test.testWithNock"], + testCaseHolderGrade: "gpii.tests.revisionRequester.testCaseHolder.success", + invokers: { + setUpNock: { + funcName: "gpii.tests.revisionRequester.setUpNock", + args: gpii.tests.revisionRequester.success.nockConfig + } + } +}); + +// 2. Retrieval of malformed or missing revision +gpii.tests.revisionRequester.missingRevision = { + nockConfig: { + hostname: gpii.tests.revisionRequester.hostname, + path: gpii.tests.revisionRequester.path, + type: "get", + status: 404, + response: { + isError: true, + statusCode: 404, + message: "Error retrieving full git revision: Missing revision value" + } + }, + expected: { + isError: true, + statusCode: 404, + // FIXME: Find out where in nock "while executing HTTP GET on url http://gpii.net/revision" + // is coming from, and get rid of it. + message: "Error retrieving full git revision: Missing revision value while executing HTTP GET on url http://gpii.net/revision" + } +}; + +fluid.defaults("gpii.tests.revisionRequester.testCaseHolder.missingRevision", { + gradeNames: "fluid.test.testCaseHolder", + modules: [{ + name: "Revision requester module tests - missing revision", + expect: 1, + tests: [{ + name: "Response: missing revision", + sequence: [{ + task: "{revisionRequester}.getRevision", + resolve: "jqUnit.assertDeepEq", + resolveArgs: [ + "The response is expected as missing", + gpii.tests.revisionRequester.missingRevision.expected, + "{arguments}.0" + ] + }] + }] + }] +}); + +fluid.defaults("gpii.tests.revisionRequesterTests.missingRevision", { + gradeNames: ["gpii.tests.revisionRequesterTests", "gpii.test.testWithNock"], + testCaseHolderGrade: "gpii.tests.revisionRequester.testCaseHolder.missingRevision", + invokers: { + setUpNock: { + funcName: "gpii.tests.revisionRequester.setUpNock", + args: gpii.tests.revisionRequester.missingRevision.nockConfig + } + } +}); + +// 3. Null cloudURL for the revision request +gpii.tests.revisionRequester.nullCloudUrl = { + nockConfig: { + // The hostname should be null here, but nock can't handle it. Taken + // care of in the setup of the GpiiRevisionRquester used in the test: + // see the "components" block of + // "gpii.tests.revisionRequesterTests.nullCloudUrl" below. + hostname: gpii.tests.revisionRequester.hostname, + path: null, + type: "get" + }, + expected: null +}; + +fluid.defaults("gpii.tests.revisionRequester.testCaseHolder.nullCloudUrl", { + gradeNames: "fluid.test.testCaseHolder", + modules: [{ + name: "Revision requester module tests - null cloudURL", + expect: 1, + tests: [{ + name: "No revision request with a null result", + sequence: [{ + task: "{revisionRequester}.getRevision", + resolve: "jqUnit.assertNull", + resolveArgs: [ + "A null result is expected", + gpii.tests.revisionRequester.nullCloudUrl.expected, + "{arguments}.0" + ] + }] + }] + }] +}); + +fluid.defaults("gpii.tests.revisionRequesterTests.nullCloudUrl", { + gradeNames: ["gpii.tests.revisionRequesterTests", "gpii.test.testWithNock"], + testCaseHolderGrade: "gpii.tests.revisionRequester.testCaseHolder.nullCloudUrl", + invokers: { + setUpNock: { + funcName: "gpii.tests.revisionRequester.setUpNock", + args: gpii.tests.revisionRequester.nullCloudUrl.nockConfig + } + }, + components: { + revisionRequester: { + type: "gpii.tests.revisionRequester", + options: { + cloudURL: null + } + } + } +}); + +// Run all tests +fluid.test.runTests([ + "gpii.tests.revisionRequesterTests.success", + "gpii.tests.revisionRequesterTests.missingRevision", + "gpii.tests.revisionRequesterTests.nullCloudUrl" +]); diff --git a/gpii/node_modules/flowManager/test/RepositorySolutionsLoaderTests.js b/gpii/node_modules/flowManager/test/RepositorySolutionsLoaderTests.js new file mode 100644 index 000000000..e557bb0fe --- /dev/null +++ b/gpii/node_modules/flowManager/test/RepositorySolutionsLoaderTests.js @@ -0,0 +1,305 @@ +/*! +GPII download and save solutions registry from source code repositoyr + +Copyright 2020 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = fluid || require("infusion"), + gpii = fluid.registerNamespace("gpii"), + kettle = require("kettle"), + nock = require("nock"); + +fluid.require("%gpii-universal/gpii/node_modules/testing/src/NockUtils.js"); +fluid.require("%flowManager/src/RepositorySolutionsLoader.js"); + +kettle.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.repositorySolutionsLoader"); + +gpii.tests.repositorySolutionsLoader = { + hostname: "http://gpii.net", + prefix: "prefix", + revision: "32759c38f9e1a5a38072f8c5e60b02a5d20969d7", + suffix: "suffix", + platformId: "darwin", + fileName: "darwin.json5", + solutionsRegistryFolderPath: "%flowManager/test/data/", + taggedSolutions: {} // filled in below +}; +gpii.tests.repositorySolutionsLoader.path = "/" + + gpii.tests.repositorySolutionsLoader.prefix + "/" + + gpii.tests.repositorySolutionsLoader.revision + "/" + + gpii.tests.repositorySolutionsLoader.suffix + "/" + + gpii.tests.repositorySolutionsLoader.fileName; + +gpii.tests.repositorySolutionsLoader.solutions = fluid.require( + gpii.tests.repositorySolutionsLoader.solutionsRegistryFolderPath + + gpii.tests.repositorySolutionsLoader.fileName +); + +// The solutions registry files contain a set of solutions as an object. The +// RepositorySolutionsLoader tags the set with a platform ID and returns the +// tagged set as its payload. +fluid.set( + gpii.tests.repositorySolutionsLoader.taggedSolutions, + gpii.tests.repositorySolutionsLoader.platformId, + gpii.tests.repositorySolutionsLoader.solutions +); + +// Set up mock cloud request/response +gpii.tests.repositorySolutionsLoader.setUpNock = function (config) { + var cloudMock = nock(gpii.tests.repositorySolutionsLoader.hostname); + cloudMock.log(console.log); + + // mock GET request + cloudMock.get(gpii.tests.repositorySolutionsLoader.path) + .reply(config.status, config.response); +}; + +// Repository loader customized for testing +fluid.defaults("gpii.tests.repositorySolutionsLoader", { + gradeNames: ["gpii.flowManager.repositorySolutionsLoader"], + urlPrefix: gpii.tests.repositorySolutionsLoader.hostname + "/" + gpii.tests.repositorySolutionsLoader.prefix, + urlSuffix: gpii.tests.repositorySolutionsLoader.suffix, + protocol: "http:" +}); + +// Base testEnvironment +fluid.defaults("gpii.tests.repositorySolutionsLoaderTests", { + gradeNames: ["fluid.test.testEnvironment"], + testCaseHolderGrade: null, // supplied by individual tests + distributeOptions: { + testCaseHolderGrade: { + source: "{that}.options.testCaseHolderGrade", + target: "{that > testCaseHolder}.type" + } + }, + components: { + repositorySolutionsLoader: { + type: "gpii.tests.repositorySolutionsLoader" + }, + testCaseHolder: { + type: "fluid.test.testCaseHolder" + } + } +}); + +// 1. Successful retrieval +gpii.tests.repositorySolutionsLoader.success = { + nockConfig: { + url: gpii.tests.repositorySolutionsLoader.hostname + gpii.tests.repositorySolutionsLoader.path, + type: "get", + status: 200, + response: gpii.tests.repositorySolutionsLoader.solutions + } +}; + +fluid.defaults("gpii.tests.repositorySolutionsLoader.testCaseHolder.success", { + gradeNames: "fluid.test.testCaseHolder", + modules: [{ + name: "Repository solutions loader module tests - successful retrieval", + expect: 1, + tests: [{ + name: "Response: solutions loaded", + sequence: [{ + task: "{repositorySolutionsLoader}.getSolutions", + args: [ + gpii.tests.repositorySolutionsLoader.revision, + gpii.tests.repositorySolutionsLoader.platformId + ], + resolve: "jqUnit.assertDeepEq", + resolveArgs: [ + "Check response for tagged solutions", + gpii.tests.repositorySolutionsLoader.taggedSolutions, + "{arguments}.0" + ] + }] + }] + }] +}); + +fluid.defaults("gpii.tests.repositorySolutionsLoaderTests.success", { + gradeNames: ["gpii.tests.repositorySolutionsLoaderTests", "gpii.test.testWithNock"], + testCaseHolderGrade: "gpii.tests.repositorySolutionsLoader.testCaseHolder.success", + invokers: { + setUpNock: { + funcName: "gpii.tests.repositorySolutionsLoader.setUpNock", + args: gpii.tests.repositorySolutionsLoader.success.nockConfig + } + } +}); + +// 2. Failure due to missing revision +gpii.tests.repositorySolutionsLoader.missingRevision = { + nockConfig: { + url: gpii.tests.repositorySolutionsLoader.hostname + gpii.tests.repositorySolutionsLoader.path, + type: "get", + status: 404, + response: { + isError: true, + statusCode: 404, + message: "Error retrieving solutions from repository: missing revision" + } + }, + expected: { + isError: true, + statusCode: 404, + message: "Error retrieving solutions from repository: missing revision" + } +}; + +fluid.defaults("gpii.tests.repositorySolutionsLoader.testCaseHolder.missingRevision", { + gradeNames: "fluid.test.testCaseHolder", + modules: [{ + name: "Revision requester module tests - missing revision", + expect: 1, + tests: [{ + name: "Response: missing revision", + sequence: [{ + task: "{repositorySolutionsLoader}.getSolutions", + args: [ + null, + gpii.tests.repositorySolutionsLoader.platformId + ], + reject: "jqUnit.assertDeepEq", + rejectArgs: [ + "Check missing revision", + gpii.tests.repositorySolutionsLoader.missingRevision.expected, + "{arguments}.0" + ] + }] + }] + }] +}); + +fluid.defaults("gpii.tests.repositorySolutionsLoaderTests.missingRevision", { + gradeNames: ["gpii.tests.repositorySolutionsLoaderTests", "gpii.test.testWithNock"], + testCaseHolderGrade: "gpii.tests.repositorySolutionsLoader.testCaseHolder.missingRevision", + invokers: { + setUpNock: { + funcName: "gpii.tests.repositorySolutionsLoader.setUpNock", + args: gpii.tests.repositorySolutionsLoader.missingRevision.nockConfig + } + } +}); + +// 3. Failure due to missing platform ID +gpii.tests.repositorySolutionsLoader.missingPlatformId = { + nockConfig: { + url: gpii.tests.repositorySolutionsLoader.hostname + gpii.tests.repositorySolutionsLoader.path, + type: "get", + status: 404, + response: { + isError: true, + statusCode: 404, + message: "Error retrieving solutions from repository: missing platform ID" + } + }, + expected: { + isError: true, + statusCode: 404, + message: "Error retrieving solutions from repository: missing platform ID" + } +}; + +fluid.defaults("gpii.tests.repositorySolutionsLoader.testCaseHolder.missingPlatformId", { + gradeNames: "fluid.test.testCaseHolder", + modules: [{ + name: "Revision requester module tests - missing platform ID", + expect: 1, + tests: [{ + name: "Response: missing platform ID", + sequence: [{ + task: "{repositorySolutionsLoader}.getSolutions", + args: [ + gpii.tests.repositorySolutionsLoader.revision, + null + ], + reject: "jqUnit.assertDeepEq", + rejectArgs: [ + "Check missing platform ID", + gpii.tests.repositorySolutionsLoader.missingPlatformId.expected, + "{arguments}.0" + ] + }] + }] + }] +}); + +fluid.defaults("gpii.tests.repositorySolutionsLoaderTests.missingPlatformId", { + gradeNames: ["gpii.tests.repositorySolutionsLoaderTests", "gpii.test.testWithNock"], + testCaseHolderGrade: "gpii.tests.repositorySolutionsLoader.testCaseHolder.missingPlatformId", + invokers: { + setUpNock: { + funcName: "gpii.tests.repositorySolutionsLoader.setUpNock", + args: gpii.tests.repositorySolutionsLoader.missingPlatformId.nockConfig + } + } +}); + +// 4. Failure due to missing revision and platform ID +gpii.tests.repositorySolutionsLoader.missingRevisionAndPlatformId = { + nockConfig: { + url: gpii.tests.repositorySolutionsLoader.hostname + gpii.tests.repositorySolutionsLoader.path, + type: "get", + status: 404, + response: { + isError: true, + statusCode: 404, + message: "Error retrieving solutions from repository: missing revision and platform ID" + } + }, + expected: { + isError: true, + statusCode: 404, + message: "Error retrieving solutions from repository: missing revision and platform ID" + } +}; + +fluid.defaults("gpii.tests.repositorySolutionsLoader.testCaseHolder.missingRevisionAndPlatformId", { + gradeNames: "fluid.test.testCaseHolder", + modules: [{ + name: "Revision requester module tests - missing revision and platform ID", + expect: 1, + tests: [{ + name: "Response: missing revision and platform ID", + sequence: [{ + task: "{repositorySolutionsLoader}.getSolutions", + args: [null, null], + reject: "jqUnit.assertDeepEq", + rejectArgs: [ + "Check missing both revision and platform ID", + gpii.tests.repositorySolutionsLoader.missingRevisionAndPlatformId.expected, + "{arguments}.0" + ] + }] + }] + }] +}); + +fluid.defaults("gpii.tests.repositorySolutionsLoaderTests.missingRevisionAndPlatformId", { + gradeNames: ["gpii.tests.repositorySolutionsLoaderTests", "gpii.test.testWithNock"], + testCaseHolderGrade: "gpii.tests.repositorySolutionsLoader.testCaseHolder.missingRevisionAndPlatformId", + invokers: { + setUpNock: { + funcName: "gpii.tests.repositorySolutionsLoader.setUpNock", + args: gpii.tests.repositorySolutionsLoader.missingRevisionAndPlatformId.nockConfig + } + } +}); + +// Run all tests +fluid.test.runTests([ + "gpii.tests.repositorySolutionsLoaderTests.success", + "gpii.tests.repositorySolutionsLoaderTests.missingRevision", + "gpii.tests.repositorySolutionsLoaderTests.missingPlatformId", + "gpii.tests.repositorySolutionsLoaderTests.missingRevisionAndPlatformId" +]); diff --git a/gpii/node_modules/flowManager/test/SolutionsRegistryDataSourceTests.js b/gpii/node_modules/flowManager/test/SolutionsRegistryDataSourceTests.js new file mode 100644 index 000000000..d449b2838 --- /dev/null +++ b/gpii/node_modules/flowManager/test/SolutionsRegistryDataSourceTests.js @@ -0,0 +1,328 @@ +/*! +GPII download and save solutions registry from source code repositoyr + +Copyright 2020 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = fluid || require("infusion"), + kettle = require("kettle"), + jqUnit = fluid.registerNamespace("jqUnit"), + gpii = fluid.registerNamespace("gpii"); + +kettle.loadTestingSupport(); + +fluid.require("%flowManager/src/SolutionsRegistryDataSource.js"); +fluid.registerNamespace("gpii.tests.solutionsRegistry"); + +gpii.tests.solutionsRegistry = { + path: "%gpii-universal/testData/solutions", + platformIds: ["win32", "linux", "darwin", "android", "web"], + platformId: "darwin", + repositoryDarwinSolutions: require("./data/darwin.json5"), + requestOptions: { os: "darwin" }, + requestOptionsMissingOs: {foo: "bar"}, + win32PlatformId: "win32" +}; + +// Check that the local solutions for the given platformId matches the result. +gpii.tests.solutionsRegistry.checkLocalRegistry = function (msg, platformId, result) { + var solutions = fluid.require(gpii.tests.solutionsRegistry.path + "/" + platformId + ".json5"); + jqUnit.assertDeepEq(msg, solutions, result); +}; + +// equalityCheck is either jqUnit.assertDeepEq() or jqUnit.assertDeepNeq(). +gpii.tests.solutionsRegistry.checkNamedRegistry = function (msg, equalityCheck, expectedSolutions, result) { + jqUnit.assertValue(msg, result); + equalityCheck(msg, expectedSolutions, result); +}; + +// Check for failures. +gpii.tests.solutionsRegistry.checkRejection = function (msg, result) { + jqUnit.assertTrue(msg, result.isError); +}; + +gpii.tests.solutionsRegistry.handleReadyEvent = function (that) { + that.isReady = true; +}; + +// ======== Testing the solutions registry data source used by the CBFM ======== + +fluid.defaults("gpii.tests.solutionsRegistry.cloud.testCaseHolder", { + gradeNames: "fluid.test.testCaseHolder", + components: { + solutionsRegistryDataSource: { + type: "gpii.flowManager.solutionsRegistry.dataSource.cloudBased", + options: { + path: gpii.tests.solutionsRegistry.path, + members: { + isReady: false + }, + listeners: { + "solutionsRegistryReady": { + listener: "gpii.tests.solutionsRegistry.handleReadyEvent", + args: ["{that}"] + } + } + } + } + }, + modules: [{ + name: "Solutions registry used by CBFM - file-based solutions", + expect: 2, + tests: [{ + name: "Solutions registry used with CBFM", + sequence: [ + { funcName: "fluid.log", args: ["Solutions Registry for CBFM, getting named registry -- START"] }, + { + // Cannot listen for solutionsRegistryReady event directly + // since it is long gone by the time this executes. Check + // that the handleIsReady listener heard the event. + funcName: "jqUnit.assertTrue", + args: [ + "Cloud based solutionsRegistryDataSource is ready", + "{solutionsRegistryDataSource}.isReady" + ] + }, { + task: "{solutionsRegistryDataSource}.get", + args: [gpii.tests.solutionsRegistry.requestOptions], + resolve: "gpii.tests.solutionsRegistry.checkLocalRegistry", + resolveArgs: [ + "Expecting one solutions registry", + gpii.tests.solutionsRegistry.requestOptions.os, + "{arguments}.0" + ] + }, + { funcName: "fluid.log", args: ["Solutions Registry for CBFM, getting named registry -- END"] }, + { funcName: "fluid.log", args: ["Solutions Registry for CBFM, failure to provide platform -- START"] }, + { + task: "{solutionsRegistryDataSource}.get", + args: [gpii.tests.solutionsRegistry.requestOptionsMissingOs], + reject: "gpii.tests.solutionsRegistry.checkRejection", + rejectArgs: [ + "Check missing OS parameter", + "{arguments}.0" + ] + }, + { funcName: "fluid.log", args: ["Solutions Registry for CBFM, failure to provide platform -- END"] } + ] + }] + }] +}); + +// Test environment for CBFM solutions registry data source +fluid.defaults("gpii.tests.solutionsRegistry.dataSource.cloud.env", { + gradeNames: ["fluid.test.testEnvironment"], + components: { + tester: { + type: "gpii.tests.solutionsRegistry.cloud.testCaseHolder" + } + } +}); +kettle.test.bootstrap("gpii.tests.solutionsRegistry.dataSource.cloud.env"); + +// ======== Testing the solutions registry data source used by the LFM ======== + +// Test component for the LFM solutions registry data source mocking a load +// from the source code repository. +fluid.defaults("gpii.tests.solutionsRegistry.dataSource.local.mockLoadFromRepository", { + gradeNames: ["gpii.flowManager.solutionsRegistry.dataSource.local"], + components: { + revisionRequester: { + type: "gpii.flowmanager.revisionRequester", + options: { + cloudURL: "http://gpii.net" + } + } + }, + listeners: { + "loadSolutions.loadFromRepository": { + listener: "gpii.tests.solutionsRegistry.dataSource.local.mockRepoLoad", + args: [ + "{that}", + gpii.tests.solutionsRegistry.platformId, + gpii.tests.solutionsRegistry.repositoryDarwinSolutions + ], + priority: "after:getRevision" + } + } +}); + +gpii.tests.solutionsRegistry.dataSource.local.mockRepoLoad = function (that, platformId, solutions) { + var promise = fluid.promise(); + var solutionsRegistry = {}; + solutionsRegistry[platformId] = solutions; + that.repositorySolutionsRegistry = fluid.freezeRecursive(solutionsRegistry); + promise.resolve(that.repositorySolutionsRegistry); + return promise; +}; + +fluid.defaults("gpii.tests.solutionsRegistry.local.mockRepository.testCaseHolder", { + gradeNames: "fluid.test.testCaseHolder", + components: { + solutionsRegistryMockRepoLoad: { + type: "gpii.tests.solutionsRegistry.dataSource.local.mockLoadFromRepository", + options: { + path: gpii.tests.solutionsRegistry.path + } + } + }, + modules: [{ + name: "Solutions registry used by LFM - repository + file system based solutions", + expect: 3, + tests: [{ + name: "Solutions registry LFM - solutions loaded from repository and looking for named registry", + sequence: [ + { funcName: "fluid.log", args: ["Solutions Registry Mock Repo Load, getting named repository registry -- START"] }, + { + event: "{solutionsRegistryMockRepoLoad}.events.solutionsRegistryReady", + listener: "jqUnit.assert", + args: ["LFM solutionsRegistryMockRepoLoad ready"] + }, { + task: "{solutionsRegistryMockRepoLoad}.get", + args: [gpii.tests.solutionsRegistry.requestOptions], + resolve: "gpii.tests.solutionsRegistry.checkNamedRegistry", + resolveArgs: [ + "Expecting named solutions registry from repository", + jqUnit.assertDeepEq, + gpii.tests.solutionsRegistry.repositoryDarwinSolutions, + "{arguments}.0" + ] + }, + { funcName: "fluid.log", args: ["Solutions Registry Mock Repo Load, getting named repository registry -- END"] }, + { funcName: "fluid.log", args: ["Solutions Registry Mock Repo Load, getting named local registry -- START"] }, + { + task: "{solutionsRegistryMockRepoLoad}.get", + args: [{os: gpii.tests.solutionsRegistry.win32PlatformId}], + resolve: "gpii.tests.solutionsRegistry.checkLocalRegistry", + resolveArgs: [ + "Expecting named solutions registries, but from the local file system", + gpii.tests.solutionsRegistry.win32PlatformId, + "{arguments}.0" + ] + }, + { funcName: "fluid.log", args: ["Solutions Registry Mock Repo Load, getting named local registry -- END"] }, + { funcName: "fluid.log", args: ["Solutions Registry Mock Repo Load, failure to provide platform -- START"] }, + { + task: "{solutionsRegistryMockRepoLoad}.get", + args: [gpii.tests.solutionsRegistry.requestOptionsMissingOs], + reject: "gpii.tests.solutionsRegistry.checkRejection", + rejectArgs: [ + "Check missing OS parameter", + "{arguments}.0" + ] + }, + { funcName: "fluid.log", args: ["Solutions Registry Mock Repo Load, failure to provide platform -- END"] } + ] + }] + }] +}); + +// Test environment for LFM solutions registry data source where solutions are +// fetched from the repository +fluid.defaults("gpii.tests.solutionsRegistry.dataSource.local.mockRepository.env", { + gradeNames: ["fluid.test.testEnvironment"], + components: { + tester: { + type: "gpii.tests.solutionsRegistry.local.mockRepository.testCaseHolder" + } + } +}); +kettle.test.bootstrap("gpii.tests.solutionsRegistry.dataSource.local.mockRepository.env"); + +// Test component for the LFM solutions registry data source where it fails to +// load from the source code repository. +fluid.defaults("gpii.tests.solutionsRegistry.dataSource.local.failLoadFromRepository", { + gradeNames: ["gpii.flowManager.solutionsRegistry.dataSource.local"], + components: { + revisionRequester: { + type: "gpii.flowmanager.revisionRequester", + options: { + cloudURL: "http://gpii.net" + } + } + }, + listeners: { + "loadSolutions.loadFromRepository": { + listener: "gpii.tests.solutionsRegistry.dataSource.local.nullRepoLoad", + args: ["{that}"], + priority: "after:getRevision" + } + } +}); + +gpii.tests.solutionsRegistry.dataSource.local.nullRepoLoad = function (that) { + var promise = fluid.promise(); + that.repositorySolutionsRegistry = null; + promise.resolve(that.repositorySolutionsRegistry); + return promise; +}; + +fluid.defaults("gpii.tests.solutionsRegistry.local.noRepository.testCaseHolder", { + gradeNames: "fluid.test.testCaseHolder", + components: { + solutionsRegistryNullRepoLoad: { + type: "gpii.tests.solutionsRegistry.dataSource.local.failLoadFromRepository", + options: { + path: gpii.tests.solutionsRegistry.path + } + } + }, + modules: [{ + name: "Solutions registry used by LFM - only file system based solutions available", + expect: 2, + tests: [{ + name: "Solutions registry used with LFM - no solutions loaded from repository", + sequence: [ + { funcName: "fluid.log", args: ["Solutions Registry No Repo Load, getting named 'repository' registry -- START"] }, + { + event: "{solutionsRegistryNullRepoLoad}.events.solutionsRegistryReady", + listener: "jqUnit.assert", + args: ["LFM solutionsRegistryNullRepoLoad ready"] + }, { + task: "{solutionsRegistryNullRepoLoad}.get", + args: [gpii.tests.solutionsRegistry.requestOptions], + resolve: "gpii.tests.solutionsRegistry.checkNamedRegistry", + resolveArgs: [ + "Expecting local solutions registry", + jqUnit.assertDeepNeq, + gpii.tests.solutionsRegistry.repositoryDarwinSolutions, + "{arguments}.0" + ] + }, + { funcName: "fluid.log", args: ["Solutions Registry No Repo Load, getting named 'repository' registry -- END"] }, + { funcName: "fluid.log", args: ["Solutions Registry No Repo Load, getting named local registry -- START"] }, + { + task: "{solutionsRegistryNullRepoLoad}.get", + args: [{os: gpii.tests.solutionsRegistry.win32PlatformId}], + resolve: "gpii.tests.solutionsRegistry.checkLocalRegistry", + resolveArgs: [ + "Expecting local named solutions registry", + gpii.tests.solutionsRegistry.win32PlatformId, + "{arguments}.0" + ] + }, + { funcName: "fluid.log", args: ["Solutions Registry No Repo Load, named local registry -- END"] } + ] + }] + }] +}); + +// Test environment for LFM solutions registry data source where solutions are +// NOT fetched from the repository +fluid.defaults("gpii.tests.solutionsRegistry.dataSource.local.noRepository.env", { + gradeNames: ["fluid.test.testEnvironment"], + components: { + tester: { + type: "gpii.tests.solutionsRegistry.local.noRepository.testCaseHolder" + } + } +}); +kettle.test.bootstrap("gpii.tests.solutionsRegistry.dataSource.local.noRepository.env"); diff --git a/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.capture.fakeData.config.json5 b/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.capture.fakeData.config.json5 index d3a12630f..95f502105 100644 --- a/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.capture.fakeData.config.json5 +++ b/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.capture.fakeData.config.json5 @@ -1,6 +1,7 @@ // // This configuration is used for testing the snappingshotting Capture API. Capturing setting will only -// ever happen on a users local machine, so there is only an untrusted test configuration. +// ever happen on a users local machine, so there is only an untrusted test configuration. Also, it +// will use the LFM's version of the solutions registry data source (gpii.flowManager.solutionsRegistry.dataSource.local) // { "type": "gpii.flowManager.tests.capture.fakeData.config", @@ -11,6 +12,15 @@ "target": "{that flowManager solutionsRegistryDataSource}.options.path", "priority": "after:flowManager.solutions" }, + "capture.noteSolutionsRegistryReady": { + "record":{ + "solutionsRegistryReady": { + "listener": "gpii.tests.flowManager.capture.handleSolutionsReadyEvent", + "args": ["{that}"] + } + }, + "target": "{that flowManager solutionsRegistryDataSource}.options.listeners" + }, "capture.deviceReporter": { "record": "%gpii-universal/gpii/node_modules/flowManager/test/data/capture_deviceReporter.json", "target": "{that deviceReporter installedSolutionsDataSource}.options.path", diff --git a/gpii/node_modules/flowManager/test/data/darwin.json5 b/gpii/node_modules/flowManager/test/data/darwin.json5 new file mode 100644 index 000000000..7bd475de8 --- /dev/null +++ b/gpii/node_modules/flowManager/test/data/darwin.json5 @@ -0,0 +1,119 @@ +{ + "fakemag1": { + "name": "Fake Magnifier 1", + "contexts": { + "OS": [ + { + "id": "darwin" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + "settingsHandlers": { + "configuration": { + "type": "gpii.settingsHandlers.JSONSettingsHandler", + "liveness": "live", + "options": { + "filename": "/tmp/fakemag1.settings.json" + }, + "capabilitiesTransformations": { + "magnification": "http://registry\\.gpii\\.net/common/magnification" + } + } + }, + "configure": [ + "settings.configuration" + ], + "restore": [ + "settings.configuration" + ], + "start": [], + "stop": [], + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "fakemag2": { + "name": "Fake Magnifier 2 - fully featured", + "contexts": { + "OS": [ + { + "id": "darwin" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + "settingsHandlers": { + "configuration": { + "type": "gpii.settingsHandlers.JSONSettingsHandler", + "liveness": "live", + "options": { + "filename": "/tmp/fakemag2.settings.json" + }, + "capabilitiesTransformations": { + "magnification": "http://registry\\.gpii\\.net/common/magnification", + "invert": "http://registry\\.gpii\\.net/common/invertColours" + } + } + }, + "configure": [ + "settings.configuration" + ], + "restore": [ + "settings.configuration" + ], + "start": [], + "stop": [], + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "fakescreenreader1": { + "name": "fake screenreader", + "contexts": { + "OS": [ + { + "id": "darwin" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled" + ], + "settingsHandlers": { + "configuration": { + "type": "gpii.settingsHandlers.JSONSettingsHandler", + "liveness": "live", + "options": { + "filename": "/tmp/fakescreenreader1.json" + }, + "capabilitiesTransformations": { + "pitch": "http://registry\\.gpii\\.net/common/pitch", + "volumeTTS": "http://registry\\.gpii\\.net/common/volumeTTS", + "rate": "http://registry\\.gpii\\.net/common/speechRate" + } + } + }, + "configure": [ + "settings.configuration" + ], + "restore": [ + "settings.configuration" + ], + "start": [], + "stop": [], + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + } +} diff --git a/gpii/node_modules/matchMakerFramework/src/MatchMakerUtilities.js b/gpii/node_modules/matchMakerFramework/src/MatchMakerUtilities.js index 2f56767ef..4623a628f 100644 --- a/gpii/node_modules/matchMakerFramework/src/MatchMakerUtilities.js +++ b/gpii/node_modules/matchMakerFramework/src/MatchMakerUtilities.js @@ -50,10 +50,6 @@ var fluid = fluid || require("infusion"), specialPreferences: gpii.matchMakerFramework.utils.findSpecialPreferences(initialPayload.preferences) }, fluid.copy(initialPayload)); gpii.matchMakerFramework.utils.addCapabilitiesInformation(matchMakerInput); - // remove full solutions registry from the payload, now that we've used it - // to avoid sending a too large payload to the matchmaker (see GPII-1880) - delete matchMakerInput.fullSolutionsRegistry; - return matchMakerInput; }; diff --git a/gpii/node_modules/testing/src/RunTestDefs.js b/gpii/node_modules/testing/src/RunTestDefs.js index d17e5f693..eae59890b 100644 --- a/gpii/node_modules/testing/src/RunTestDefs.js +++ b/gpii/node_modules/testing/src/RunTestDefs.js @@ -88,7 +88,8 @@ gpii.test.testDefToEnvironment = function (testDef, environmentType, sequenceGra }; gpii.test.testDefToServerEnvironment = function (testDef) { - return gpii.test.testDefToEnvironment(testDef, "gpii.test.serverEnvironment", "gpii.test.standardServerSequenceGrade"); + var serverEnvironmentGrade = testDef.testEnvironmentGrade ? testDef.testEnvironmentGrade : "gpii.test.serverEnvironment"; + return gpii.test.testDefToEnvironment(testDef, serverEnvironmentGrade, "gpii.test.standardServerSequenceGrade"); }; gpii.test.testDefToCouchEnvironment = function (testDef) { diff --git a/scripts/vagrantCloudBasedContainers.sh b/scripts/vagrantCloudBasedContainers.sh index a13604b61..a47e4669f 100755 --- a/scripts/vagrantCloudBasedContainers.sh +++ b/scripts/vagrantCloudBasedContainers.sh @@ -27,6 +27,22 @@ fi UNIVERSAL_IMAGE=vagrant-universal +# The following SHA256 is guaranteed to be a revison on github, and recent. It +# is not necessarily the latest revision that is used in production -- it +# could be a later revision -- but it is sufficient for the tests. For local +# development, @{upstream} is used in case the local changes have not been +# pushed. CI has no upstream, and uses HEAD instead. The result is written to +# the file "gpii-revision.json" at the root universal folder. See the +# Dockerfile. +GITFULLREV="$(git rev-parse @{upstream})" +if [ $? != 0 ] +then + echo "No upstream, using HEAD for revision.json" + GITFULLREV="$(git rev-parse HEAD)" +else + echo "Using upstream for revision.json" +fi + COUCHDB_IMAGE=couchdb:2.3.1 COUCHDB_PORT=5984 COUCHDB_HEALTHCHECK_DELAY=2 @@ -73,7 +89,7 @@ if [ "$NO_REBUILD" != "true" ] ; then docker rmi -f $UNIVERSAL_IMAGE 2>/dev/null || true # Build image - docker build -t $UNIVERSAL_IMAGE . + docker build --build-arg gitFullRev="$GITFULLREV" -t $UNIVERSAL_IMAGE . fi # Start the CouchDB container diff --git a/tests/JournalIntegrationTests.js b/tests/JournalIntegrationTests.js index 2f3966f90..b2a89df03 100644 --- a/tests/JournalIntegrationTests.js +++ b/tests/JournalIntegrationTests.js @@ -17,6 +17,7 @@ var gpii = fluid.registerNamespace("gpii"); var jqUnit = require("node-jqunit"); var kettle = require("kettle"); + kettle.loadTestingSupport(); fluid.require("%gpii-universal"); @@ -297,50 +298,48 @@ fluid.defaults("gpii.tests.journal.testCaseHolder", { // Used on the first run where the settings handler crashes the whole system gpii.tests.journal.solutionsRegistryOverlay = { - win32: { - "com.microsoft.windows.cursors": { - settingsHandlers: { - explode: { - type: "gpii.tests.journal.explodingSettingsHandler", - supportedSettings: { - AppStarting: {} - }, - capabilitiesTransformations: { - AppStarting: { - transform: { - type: "fluid.transforms.value", - inputPath: "http://registry\\.gpii\\.net/common/cursorSize", - outputPath: "value" - } - } - } + "com.microsoft.windows.cursors": { + settingsHandlers: { + explode: { + type: "gpii.tests.journal.explodingSettingsHandler", + supportedSettings: { + AppStarting: {} }, - maybeThrow: { - type: "gpii.tests.journal.throwingSettingsHandler", - supportedSettings: { - AppStarting: {} - }, - capabilitiesTransformations: { - AppStarting: { - transform: { - type: "fluid.transforms.value", - inputPath: "http://registry\\.gpii\\.net/common/cursorSize", - outputPath: "value" - } + capabilitiesTransformations: { + AppStarting: { + transform: { + type: "fluid.transforms.value", + inputPath: "http://registry\\.gpii\\.net/common/cursorSize", + outputPath: "value" } } + } + }, + maybeThrow: { + type: "gpii.tests.journal.throwingSettingsHandler", + supportedSettings: { + AppStarting: {} }, - configure: { - supportedSettings: { - AppStarting: {} + capabilitiesTransformations: { + AppStarting: { + transform: { + type: "fluid.transforms.value", + inputPath: "http://registry\\.gpii\\.net/common/cursorSize", + outputPath: "value" + } } } }, - restore: ["settings.maybeThrow", "settings.configure"], - // It's necessary to execute settings.maybeThrow otherwise its entry does not appear in the snapshotted solutions - // registry block entered into the journal, and hence it cannot be found on the next turn - configure: ["settings.configure", "settings.maybeThrow", "settings.explode"] - } + configure: { + supportedSettings: { + AppStarting: {} + } + } + }, + restore: ["settings.maybeThrow", "settings.configure"], + // It's necessary to execute settings.maybeThrow otherwise its entry does not appear in the snapshotted solutions + // registry block entered into the journal, and hence it cannot be found on the next turn + configure: ["settings.configure", "settings.maybeThrow", "settings.explode"] } }; diff --git a/tests/all-tests.js b/tests/all-tests.js index f2bf8c474..99d5dbdad 100644 --- a/tests/all-tests.js +++ b/tests/all-tests.js @@ -59,10 +59,12 @@ var testIncludes = [ "../gpii/node_modules/flatMatchMaker/test/FlatMatchMakerTests.js", "../gpii/node_modules/flowManager/test/BrowserChannelTests.js", "../gpii/node_modules/flowManager/test/CaptureTests.js", + "../gpii/node_modules/flowManager/test/SolutionsRegistryDataSourceTests.js", "../gpii/node_modules/flowManager/test/DefaultSettingsLoaderTests.js", "../gpii/node_modules/flowManager/test/PrefsServerDataSourceTests.js", "../gpii/node_modules/flowManager/test/PSPChannelTests.js", "../gpii/node_modules/flowManager/test/SettingsDataSourceTests.js", + "../gpii/node_modules/flowManager/test/RepositorySolutionsLoaderTests.js", "../gpii/node_modules/gpii-db-operation/test/DbDataStoreTests.js", "../gpii/node_modules/gpii-ini-file/test/iniFileTests.js", "../gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/test/authGrantFinderTests.js", diff --git a/tests/configs/gpii.tests.untrusted.production.config.json5 b/tests/configs/gpii.tests.untrusted.production.config.json5 index 14e009b58..8229dc055 100644 --- a/tests/configs/gpii.tests.untrusted.production.config.json5 +++ b/tests/configs/gpii.tests.untrusted.production.config.json5 @@ -3,5 +3,8 @@ "options": { "mainServerPort": "@expand:kettle.resolvers.env(GPII_FLOWMANAGER_LISTEN_PORT)" }, - "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.untrusted.development.json5" + "mergeConfigs": [ + "%gpii-universal/gpii/node_modules/flowManager/configs/gpii.config.local.flowManager.loadSolutionsFromRepository", + "%gpii-universal/gpii/configs/shared/gpii.config.untrusted.development.json5" + ] } diff --git a/tests/production/CloudStatusProductionTests.js b/tests/production/CloudStatusProductionTests.js index 3ab0ca0a1..7f970dd66 100644 --- a/tests/production/CloudStatusProductionTests.js +++ b/tests/production/CloudStatusProductionTests.js @@ -1,5 +1,5 @@ /** -GPII Production Config tests +GPII Production Config tests - Cloud Status Requirements: * an internet connection @@ -10,7 +10,7 @@ Requirements: --- Copyright 2015 Raising the Floor - International -Copyright 2018, 2019 OCAD University +Copyright 2018-2020 OCAD University Licensed under the New BSD license. You may not use this file except in compliance with this License. @@ -36,38 +36,18 @@ gpii.loadTestingSupport(); fluid.registerNamespace("gpii.tests.productionConfigTesting"); -require("../shared/DevelopmentTestDefs.js"); require("./ProductionTestsUtils.js"); -// Flowmanager tests for: -// /user/%gpiiKey/login and /user/%gpiiKey/logout (as defined in gpii.tests.development.testDefs), +gpii.tests.productionConfigTesting.validGpiiRevision = require( + fluid.module.resolvePath( + "%gpii-universal/gpii-revision.json" + ) +); + +// Flowmanager tests for these http endpoints: // /health, // /ready, -gpii.tests.productionConfigTesting.testDefs = fluid.transform(gpii.tests.development.testDefs, function (testDefIn) { - var testDef = fluid.extend(true, {}, testDefIn, { - name: "Flow Manager production tests -- status, login, and logout", - config: gpii.tests.productionConfigTesting.config, - expect: 6, - components: { - healthRequest: { - type: "gpii.tests.productionConfigTesting.cloudStatusRequest", - options: { - path: "/health", - expectedPayload: {"isHealthy": true} - } - }, - readyRequest: { - type: "gpii.tests.productionConfigTesting.cloudStatusRequest", - options: { - path: "/ready", - expectedPayload: {"isReady": true} - } - } - }, - sequenceGrade: "gpii.tests.productionConfigTesting.cloudStatusSequence" - }); - return testDef; -}); +// /revision fluid.defaults("gpii.tests.productionConfigTesting.cloudStatus", { gradeNames: ["fluid.test.sequenceElement"], @@ -83,6 +63,11 @@ fluid.defaults("gpii.tests.productionConfigTesting.cloudStatus", { }, { event: "{readyRequest}.events.onComplete", listener: "gpii.tests.productionConfigTesting.testResponse" + }, { + func: "{revisionRequest}.sendToCBFM" + }, { + event: "{revisionRequest}.events.onComplete", + listener: "gpii.tests.productionConfigTesting.testResponse" }, { funcName: "fluid.log", args: ["Cloud status tests end"]} ] @@ -94,12 +79,38 @@ fluid.defaults("gpii.tests.productionConfigTesting.cloudStatusSequence", { cloudStatus: { gradeNames: "gpii.tests.productionConfigTesting.cloudStatus", priority: "after:startServer" - }, - loginLogout: { - gradeNames: "gpii.tests.development.loginLogout", - priority: "after:cloudStatus" } } }); +gpii.tests.productionConfigTesting.testDefs = [{ + name: "Flow Manager production tests -- Cloud status", + config: gpii.tests.productionConfigTesting.config, + expect: 6, + components: { + healthRequest: { + type: "gpii.tests.productionConfigTesting.cloudStatusRequest", + options: { + path: "/health", + expectedPayload: {"isHealthy": true} + } + }, + readyRequest: { + type: "gpii.tests.productionConfigTesting.cloudStatusRequest", + options: { + path: "/ready", + expectedPayload: {"isReady": true} + } + }, + revisionRequest: { + type: "gpii.tests.productionConfigTesting.cloudStatusRequest", + options: { + path: "/revision", + expectedPayload: gpii.tests.productionConfigTesting.validGpiiRevision + } + } + }, + sequenceGrade: "gpii.tests.productionConfigTesting.cloudStatusSequence" +}]; + gpii.test.runServerTestDefs(gpii.tests.productionConfigTesting.testDefs); diff --git a/tests/production/LoginLogoutProductionTests.js b/tests/production/LoginLogoutProductionTests.js new file mode 100644 index 000000000..568084cda --- /dev/null +++ b/tests/production/LoginLogoutProductionTests.js @@ -0,0 +1,73 @@ +/** +GPII Production Config tests - Login/Logout process + +Requirements: +* an internet connection +* a cloud based flow manager +* a preferences server +* a CouchDB server + +--- + +Copyright 2015 Raising the Floor - International +Copyright 2018-2020 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +WARNING: Do not run these tests directly. They are called from within the +"vagrantCloudBasedContainers.sh" after it has initialized the environment. +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.productionConfigTesting"); + +require("../shared/DevelopmentTestDefs.js"); +require("./ProductionTestsUtils.js"); + +/** Flowmanager tests for the key in and key out processes: + * /user/%gpiiKey/login and /user/%gpiiKey/logout (as defined in gpii.tests.development.testDefs), + * + * Note: When Local Flow Manager starts, it now fetches Solution Registry and default settings file from remote URLs. + * This test needs to ensure the keyin/keyout test sequence starts when LFM is ready and noUser has been keyed into + * the system. To accomplish it, a special testEnvironment component is created to define an aggregate event + * "onSystemReady"that will be fired when: + * 1. The test server has been constructed; + * 2. The local flow manager initial actions have completed and is ready to process login/logout requests. + * The keyin/keyout test sequence will wait until "onSystemReady" is fired. + */ + +fluid.defaults("gpii.tests.productionConfigTesting.loginLogoutSequence", { + gradeNames: ["gpii.test.standardServerSequenceGrade"], + sequenceElements: { + loginLogout: { + gradeNames: "gpii.tests.development.loginLogout", + priority: "after:startServer" + } + } +}); + +gpii.tests.productionConfigTesting.keyInKeyOutTestDefs = fluid.transform(gpii.tests.development.testDefs, function (testDefIn) { + var testDef = fluid.extend(true, {}, testDefIn, { + config: gpii.tests.productionConfigTesting.config, + testEnvironmentGrade: "gpii.tests.productionConfigTesting.testEnvironment", + sequenceGrade: "gpii.tests.productionConfigTesting.loginLogoutSequence" + }); + return testDef; +}); + +gpii.test.runServerTestDefs(gpii.tests.productionConfigTesting.keyInKeyOutTestDefs); diff --git a/tests/production/ProductionTestsUtils.js b/tests/production/ProductionTestsUtils.js index 3d4d73bef..0411dd8d2 100644 --- a/tests/production/ProductionTestsUtils.js +++ b/tests/production/ProductionTestsUtils.js @@ -88,6 +88,40 @@ fluid.defaults("gpii.tests.cloud.oauth2.accessTokensDeleteRequests", { } }); +// The customized testEnvironment component that adds and listens to the aggregate event "onSystemReady" +fluid.defaults("gpii.tests.productionConfigTesting.testEnvironment", { + gradeNames: ["gpii.test.serverEnvironment"], + distributeOptions: { + "resetAtStartSuccess.escalate": { + record: { + resetAtStartSuccess: "{testEnvironment}.events.resetAtStartSuccess" + }, + target: "{that gpii.flowManager.local}.options.events" + }, + "productionConfigTesting.startServerSequence": { + record: [ + { // This sequence point is required because of a QUnit bug - it defers the start of sequence by 13ms "to avoid any current callbacks" in its words + func: "{testEnvironment}.events.constructServer.fire" + }, + { + event: "{testEnvironment}.events.onSystemReady", + listener: "fluid.identity" + } + ], + target: "{that gpii.test.startServerSequence}.options.sequence" + } + }, + events: { + resetAtStartSuccess: null, + onSystemReady: { + events: { + resetAtStartSuccess: "resetAtStartSuccess", + onServerReady: "onServerReady" + } + } + } +}); + // Sequence elements for cleaning up extra access tokens fluid.defaults("gpii.tests.productionConfigTesting.deleteAccessTokensSequence", { gradeNames: ["fluid.test.sequenceElement"], diff --git a/tests/production/SolutionsRegistryLoadSequenceTests.js b/tests/production/SolutionsRegistryLoadSequenceTests.js new file mode 100644 index 000000000..11cde4167 --- /dev/null +++ b/tests/production/SolutionsRegistryLoadSequenceTests.js @@ -0,0 +1,144 @@ +/** +GPII Production Config tests - Loading Solutions Registries + +Requirements: +* an internet connection +* a cloud based flow manager +--- + +Copyright 2020 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +WARNING: Do not run these tests directly. They are called from within the +"vagrantCloudBasedContainers.sh" after it has initialized the environment. +*/ + +"use strict"; + +var fluid = require("infusion"), + jqUnit = fluid.require("node-jqunit", require, "jqUnit"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.productionConfigTesting"); + +require("./ProductionTestsUtils.js"); + +gpii.tests.productionConfigTesting.validGpiiRevision = require( + fluid.module.resolvePath( + "%gpii-universal/gpii-revision.json" + ) +); + +gpii.tests.productionConfigTesting.localSolutionsRegistries = + fluid.module.resolvePath("%gpii-universal/testData/solutions"); + +/** Local FlowManager tests of the solutions registry loading sequence: + * + * When the LFM starts, its SolutionsRegisryDataSource loads solutions + * registries from both the local file system, and from the source code + * repository. The tests need to start running *after* this loading process is + * complete, and are sensitive to the "onSystemReady" event, in that regard. See + * the component "gpii.tests.productionConfigTesting.testEnvironment" in the + * "ProductionTestUtils.js" file. + * + * "onSystemReady" is fired when: + * 1. The test server has been constructed; + * 2. The local flow manager initial actions have completed and is ready for + * requests + */ +fluid.defaults("gpii.tests.productionConfigTesting.loadingSolutionsTransform", { + gradeNames: ["gpii.test.standardServerSequenceGrade"], + sequenceElements: { + testLoadedSequence: { + gradeNames: "gpii.tests.productionConfigTesting.testLoadedSequence", + priority: "after:startServer" + } + } +}); + +fluid.defaults("gpii.tests.productionConfigTesting.testLoadedSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [{ + funcName: "gpii.tests.productionConfigTesting.checkSolutionsRegistries" + }] +}); + +gpii.tests.productionConfigTesting.loadingSolutionsTransform.testDefs = [{ + name: "Flow Manager production tests -- solutions loading sequence", + config: gpii.tests.productionConfigTesting.config, + testEnvironmentGrade: "gpii.tests.productionConfigTesting.testEnvironment", + distributeOptions: { + // Override the default getRevision() to record the value it resolves after + // requesting the revision. + "store.revision": { + "record": { + "loadSolutions.getRevision": { + "listener": "gpii.tests.productionConfigTesting.loadingSolutionsTransform.getRevisionTest", + "args": ["{that}"] + } + }, + "target": "{that gpii.flowManager.local solutionsRegistryDataSource}.options.listeners" + }, + "store.registries": { + "record":{ + "solutionsRegistryReady": { + "listener": "gpii.tests.productionConfigTesting.loadingSolutionsTransform.storeRegistries", + "args": ["{that}"] + } + }, + "target": "{that flowManager solutionsRegistryDataSource}.options.listeners" + } + }, + sequenceGrade: "gpii.tests.productionConfigTesting.loadingSolutionsTransform" +}]; + +// Call the default getRevision() and store the result is the SRDS +gpii.tests.productionConfigTesting.loadingSolutionsTransform.getRevisionTest = function (solutionRegistryDataSource) { + var revisionPromise = solutionRegistryDataSource.revisionRequester.getRevision(); + revisionPromise.then(function (revision) { + gpii.tests.productionConfigTesting.loadingSolutionsTransform.revision = revision; + }); + return revisionPromise; +}; + +// After the SRDS has finished loading the registries, keep a copy of them for +// testing. +gpii.tests.productionConfigTesting.loadingSolutionsTransform.storeRegistries = function (solutionRegistryDataSource) { + gpii.tests.productionConfigTesting.loadingSolutionsTransform.fullSolutionsRegistry = + solutionRegistryDataSource.fullSolutionsRegistry; + + gpii.tests.productionConfigTesting.loadingSolutionsTransform.repositorySolutionsRegistry = + solutionRegistryDataSource.repositorySolutionsRegistry; +}; + +// Check that the solutions were loaded from local file system and repository, +// and that the revision used matches. +gpii.tests.productionConfigTesting.checkSolutionsRegistries = function () { + jqUnit.assertDeepEq( + "Check revision", + gpii.tests.productionConfigTesting.validGpiiRevision, + gpii.tests.productionConfigTesting.loadingSolutionsTransform.revision + ); + jqUnit.assertNotNull( + "Check loading of solutions registries from local file system", + gpii.tests.productionConfigTesting.loadingSolutionsTransform.fullSolutionsRegistry + ); + jqUnit.assertNotNull( + "Check loading from soruce code respository", + gpii.tests.productionConfigTesting.loadingSolutionsTransform.repositorySolutionsRegistry + ); +}; + +gpii.test.runServerTestDefs(gpii.tests.productionConfigTesting.loadingSolutionsTransform.testDefs); diff --git a/tests/production/all-tests.js b/tests/production/all-tests.js index a9fd06dd7..972866a72 100644 --- a/tests/production/all-tests.js +++ b/tests/production/all-tests.js @@ -26,6 +26,8 @@ fluid.require("%gpii-universal", require); var testIncludes = [ "./CloudStatusProductionTests.js", + "./SolutionsRegistryLoadSequenceTests.js", + "./LoginLogoutProductionTests.js", "./SettingsGetProductionTests.js", "./SettingsPutProductionTests.js" ];