diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..2a38c8c --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "eslint-config-fluid" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2a5953e --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# Commenting this out is preferred by some people, see +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- +node_modules + +# Users Environment Variables +.lock-wscript +bower_components +reports +.vagrant diff --git a/.istanbul.yml b/.istanbul.yml new file mode 100644 index 0000000..1ed00a7 --- /dev/null +++ b/.istanbul.yml @@ -0,0 +1,8 @@ +reporting: + print: none + dir: ./reports + reports: + - lcov + - cobertura + report-config: + cobertura: {file: report.cov } diff --git a/.qi.yml b/.qi.yml new file mode 100644 index 0000000..a3527ad --- /dev/null +++ b/.qi.yml @@ -0,0 +1,19 @@ +# A configuration file for our Quality Infrastructure builds: https://wiki.gpii.net/w/Quality_Infrastructure +email: tony@raisingthefloor.org +env_runtime: linux-desktop + +apps: + - app_name: gpii-json-schema + git_repository: https://github.com/the-t-in-rtf/gpii-json-schema.git + git_branch: GPII-1336 + software_stack: nodejs + software_stack_version: lts + folder: + dest: /app/gpii-json-schema + run_in: fedora + deploy: false + setup: + - sudo yum install -y chromedriver + - yarn install + test_cmds: + - BROWSERS=chrome yarn test diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..d597b83 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: node_js +node_js: "6.9.1" +addons: + apt: + packages: + - xvfb +install: + - export DISPLAY=':99.0' + - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + - npm install -g yarn + - yarn install +script: + - BROWSERS=firefox yarn test \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..64dd02a --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,19 @@ +/* eslint-env node */ +"use strict"; + +module.exports = function (grunt) { + grunt.initConfig({ + eslint: { + src: ["./src/**/*.js", "./tests/**/*.js", "./*.js"] + }, + jsonlint: { + src: ["src/**/*.json", "tests/**/*.json", "./*.json"] + } + }); + + grunt.loadNpmTasks("fluid-grunt-eslint"); + grunt.loadNpmTasks("grunt-jsonlint"); + + grunt.registerTask("lint", "Apply jshint and jsonlint", ["eslint", "jsonlint"]); +}; + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3138d58 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2015, Raising the Floor International +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +* Neither the name of the {organization} nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index dba80d9..2d49af7 100644 --- a/README.md +++ b/README.md @@ -1 +1,49 @@ -# gpii-json-schema +# What is this? + +[JSON Schemas](http://json-schema.org) are JSON documents that describe how a JSON object should be structured. The +JSON Schema standard includes rules about what fields are required, what type and format of data is allowed in a field, +and many other complex rules that allow you to do things like limit the length of a text field, or require between one +and three entries in an array. + +This package provides a series of [Fluid components](https://github.com/fluid-project/infusion-docs/blob/master/src/documents/UnderstandingInfusionComponents.md) +to help add the power of [JSON Schemas](http://json-schema.org) to your project. + +This package is intended to help with three key use cases: + +1. Validating arbitrary JSON data and reporting problems to the end user. See the [validator documentation](./docs/validator.md) and [parser documentation](./docs/parser.md) for more details. +2. Rejecting invalid data sent to a REST endpoint (presumably via a POST or PUT request). See the [middleware documentation](./docs/middleware) for more details. +3. Adding appropriate headers to JSON responses so that it is clear what JSON Schema they adhere to. See the [handler documentation](./docs/handler.md) for more details. + +# Running the tests + +Before you can successfully run the tests, you will need to have the following installed: + +# `node` (4.x or 6.x) +# `npm` or [`yarn`](http://yarnpkg.com/) +# [`chromedriver`](https://sites.google.com/a/chromium.org/chromedriver/) + +*Note:* You cannot use the [https://www.npmjs.com/package/chromedriver](https://www.npmjs.com/package/chromedriver) npm +package to install chromedriver globally, as this will not be detected on Windows until +[this bug](https://github.com/giggio/node-chromedriver/issues/90) is resolved. + +Once you have these installed, you can run the tests using commands like: + +1. `yarn install` +2. `yarn test` + +Or, if you're using `npm`, you can use commands like: + +1. `npm install` +2. `npm test` + +NOTE: When using `npm` version 3.10.10, the `test` script does not work properly. You can still run the tests with +that version of `npm` using commands like: + +1. `npm install` +2. `node tests/all-tests.js` + +# Using these components in a browser + +This package depends on AJV. AJV can be used on the client-side, but must first be bundled using `browserify`. The +AJV package takes care of this automatically when it's installed, the required client-side bundle can be found in +`./node_modules/ajv/dist/ajv.bundle.js` once you've installed this package's dependencies. \ No newline at end of file diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000..ff6a56b --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,16 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +# This file loads a generic Vagrantfile that will build the environment +# specified in the .qi.yml file. +# +# DO NOT MODIFY THIS FILE +# +# if you want to change the VMs specifications go to README.md + +VAGRANT_VMENV_PATH = `node -e "require('vagrant-vmenv')"`.delete!("\n") +puts "\nCan not find the vagrant-vmenv module, please install it:\n" + + "npm install -g https://github.com/amatas/vagrant-vmenv.git" if $?.to_i != 0 + +require_relative( VAGRANT_VMENV_PATH + '/Vagrantfile.rb') + diff --git a/docs/errorBinder.md b/docs/errorBinder.md new file mode 100644 index 0000000..7e89385 --- /dev/null +++ b/docs/errorBinder.md @@ -0,0 +1,91 @@ +# `gpii.schemas.client.errorBinder` + +The `gpii-json-schema` package provides both [server and client-side validation](validator.md). The `errorBinder` +component provides a consistent means of associating errors returned by the validator with onscreen elements. + +This component does not expose any invokers. Instead, it listens for changes to the `fieldErrors` model variable and +updates the display if needed. + +## How do "bindings" work? + +The `errorBinder` uses model->view bindings like those used with [`gpii-binder`](https://github.com/GPII/gpii-binder) to +associate validation errors reported by the validator with onscreen elements. That "binding" structure looks something +like: + +``` +bindings: { + "key": { + selector: "selector1", + path: "path1" + }, + "selector2": "path2" +} +``` + +The map of bindings used by the base component are stored under `options.errorBindings`. By default, the component +tries to pick up the existing value from `options.bindings`, so that you can easily reuse existing bindings from +grades like 'templateFormControl`. + +## Requirements + +The `errorBinder` component requires both `gpii-binder` and `gpii-handlebars`. + +## Component options + + +| Option | Type | Description | +| ------------------ | -------- | ----------- | +| `selectors.fieldError` | `Selector` | The selector representing the view that will display our error summary. | +| `templates.inlineError` | `String` | The filename/id of the template that will be used to produce the inline error output. | + + +# `gpii.schemas.client.errorAwareForm` + +This is an extended version of the `templateFormControl` grade provided by the `gpii-handlebars` package. +It passes on validation errors returned by the server to an instance of the `errorBinder`, which displays +the errors next to each field. It also displays a summary of all errors. + +## Component options + +Here are the unique options you will likely want to customize when using the `errorAwareForm` component. + +| Option | Type | Description | +| ------------------ | -------- | ----------- | +| `templates.error` | `String` | The filename/id of the template that will be used to produce the error summary. | +| `rules` | `Object` | The [model transformation rules](https://wiki.fluidproject.org/display/docs/fluid.model.transformWithRules) that control what information is passed to the server on form submit, and how the response is handled. See the `ajaxCapable` documentation in the `gpii-handlebars` package for details. | + + +See the `templateFormControl`documentation in the [`gpii-handlebars`](https://github.com/GPII/gpii-handlebars) package +for more details about supported options. + + +# `gpii.schemas.client.errorAwareForm.clientSideValidation` + +This is an extended version of the `errorAwareForm` grade that adds client-side validation. It validates the model +content before submitting and prevents form submission if there are any errors. + +## Component options + +| Option | Type | Description | +| ------------------ | -------- | ----------- | +| `rules.modelToRequestPayload` | `Object` | The [model transformation rules](https://wiki.fluidproject.org/display/docs/fluid.model.transformWithRules) that control what information is submitted by the form are also used in validating the form data. | + +## Invokers + +### `{gpii.schemas.client.errorAwareForm.clientSideValidation}.submitForm(event)` +* `event {Object}`: The [jQuery event object](http://api.jquery.com/Types/#Event) passed to us by the DOM elements we're bound to. +* Returns: Nothing. + +A gatekeeper function that only allows the form to be submitted if client-side validation succeeds. For details on +binding this to your own elements, see the `templateFormControl` documentation in the `gpii-handlebars` package. + +### `{gpii.schemas.client.errorAwareForm.clientSideValidation}.validateContent()` +* Returns: Nothing. + +Validate client-side model content and display any errors. It expects to validate the same transformed model data the +form transmits (see the `rules.modelToRequestPayload` option above). + +# `gpii.schemas.client.errorAwareForm.clientSideValidation.realTime` + +This is an extended version of the `clientSideValidation` grade that listens for all model changes, revalidates the +model and updates the onscreen error messages in real time. diff --git a/docs/evolveErrors.md b/docs/evolveErrors.md new file mode 100644 index 0000000..ab37a79 --- /dev/null +++ b/docs/evolveErrors.md @@ -0,0 +1,222 @@ +# The `gpii.schema.errors.evolveError` static function. + +The [draft v5 JSON Schema standard](https://github.com/json-schema/json-schema/wiki/Custom-error-messages-%28v5-proposal%29) +adds an `errors` keyword, which is a map of relative JSON pointers and replacement message definitions. It can be used +either the document level, or when defining an individual field. Here is an example of using `errors` at the document +level, taken from the v5 draft proposal: + +``` +{ + "properties": { + "age": { "minimum": 13 }, + "gender": { "enum": ["male", "female"] } + }, + "errors": { + "#/properties/age/minimum": "Should be at least ${schema} years, ${data} years is too young.", + "#/properties/gender/enum": { + "text": "Gender should be ${schema/0} or ${schema/1}", + "action": "replace" + } + } +} +``` + +Here is an example of using `errors` within an individual field, again taken from the v5 draft proposal: + +``` +{ + "properties": { + "age": { + "minimum": 13, + "errors": { + "minimum": "Should be at least ${schema} years, ${data} years is too young." + } + }, + "gender": { + "enum": ["male", "female"], + "errors": { + "enum": { + "text": "Gender should be ${schema/0} or ${schema/1}", + "action": "replace" + } + } + } + } +} +``` + +## So what does the new field actually _do_? + +Let's say that we have the following schema: + +``` +{ + "properties": { + "field": { + "type": "string", + "pattern": "[A-Z]+" + } + } +} +``` + +If we were to validate the simply JSON document `{ "field": "lowercase" }`, the validator would return output like: + +``` +[ + { + "keyword": "pattern", + "dataPath": ".field", + "schemaPath": "#/properties/field/pattern", + "params": { + "pattern": "^[A-Z]+$" + }, + "message": "should match pattern \"^[A-Z]+$\"" + } +] +``` + +Here's the same schema with the v5 error definition added. + +``` +{ + "properties": { + "field": { + "type": "string", + "pattern": "^[A-Z]+$", + "errors": { + "pattern": "You must enter an uppercase string." + } + } + } +} +``` + +Now the validator (with our help) can return the following output: + +``` +[ + { + "keyword": "pattern", + "dataPath": ".field", + "schemaPath": "#/properties/field/pattern", + "params": { + "pattern": "^[A-Z]+$" + }, + "message": "You must enter an uppercase string." + } +] +``` + +## Differences from the draft v5 proposal + +## No support for variables + +The draft v5 proposal promises support for using schema variables and user data in the text of an error message. This +function does not support this. As such, we also do not support the longer form of the message definition (where the +error is an object that contains a `text` and `action`). Instead, we support only the simplest of the forms outlined +above, as in this updated snippet: + +``` +"age": { + "minimum": 13, + "errors": { + "minimum": "Should be at least 13 years." + } +}, +``` + +The working draft is not clear on how "required" fields should be evolved to use custom errors. This module uses +the schema proposed [in this GitHub issue](https://github.com/json-schema/json-schema/issues/222). Here is an example +of how this module expects `errors` data to be entered for required fields: + +``` +{ + "properties": { + "shallowlyRequired": { + "type": "string", + "properties": { + "deeplyRequired": { "type": "string" } + }, + "required": ["deeplyRequired"], + "errors": { + "required/0": "This field is required and I'm telling you about it from within a field definition." + } + } + }, + "required": ["shallowlyRequired"], + "errors": { + "required/0": "This field is required and I'll tell you about it at the document level.", + "shallowlyRequired/required/0": "This deep field is required." + } +} +``` + +Note that all of the "keys" used in an `errors` block are [relative JSON Pointers](https://tools.ietf.org/html/rfc6901). +Slashes within keys should be escaped as `~1`. Tildes within keys should be escaped as `~0`. + +## The `errors` block and inheritance. + +Although this has not yet been established in the standard or in AJV, we have chosen to resolve +`errors` in a way that will allow overlaying errors on an existing schema, as in the following +example: + +``` +{ + "id": "person.json", + "definitions": { + "firstname": { type: "string" }, + "lastname": { type: "string" } + }, + "properties": { + "firstname": { "$ref": "#/definitions/firstname" }, + "lastname": { "$ref": "#/definitions/lastname" } + } +} +``` + +Here is a schema with overlayed error messages in English: + +``` +{ + "id": "person-en.json", + "properties": { + "firstname": { "$ref": "person.json#/definitions/firstname" }, + "lastname": { "$ref": "person.json#/definitions/lastname" } + }, + "errors": { + "#/definitions/firstname/type": "The first name must be a string.", + "#/definitions/lastname/type": "The last name must be a string." + } +} +``` + +If you're having trouble figuring out what key to use in the document's `errors` block, use the schema to validate a +document in which the rule has been broken, and look at the `schemaPath` variable in the raw validator output. + +# `gpii.schema.errors.evolveError(schemaContent, error)` + +* `schemaContent {Object}`: The derefrenced content of the schema, as produce by [the parser](parser.md). +* `error {object}`: The original error returned by AJV. +* Returns: `{Object}` The "evolved" error with an updated message based on the `errors` definitions (if there are any). + +This function used by the validator to transform the full set of raw validation results using `fluid.transform`. It +tries to look up the v5 draft proposal `errors` definition for a given `error`. A copy of the original `error` is +returned, with an updated `error.message` if a custom error definition was found. + +The parser starts by looking for the relevant error definition in the document, i.e. at `#/errors`. If that fails, look +for a relevant definition at the field. This involves navigating from the failing rule to the relevant `errors` block. +There are two variations: + +1. All required data has been entered but a field is invalid. In those cases, `schemaPath` is something like + `#/properties/password/allOf/1/pattern` and the relevant `errors` block is something like + `#/properties/password/allOf/1/errors/pattern`. + +2. Required data is missing. In those cases, `schemaPath` is something like `#/properties/deep/required` + or `#/required` and the relevant `errors` block is something like `#/properties/deep/errors` or + `#/errors`. The `errors` block should contain an entry for the field based on its position in the + `required` array, as in `/required/0`, `/required/1`, et cetera. + +In both cases we navigate to the parent element immediately above the reported failure, and then to its `errors` +block. If we find an error definition that matches the failure, we add that to a modified copy of the error and return +that. \ No newline at end of file diff --git a/docs/parser.md b/docs/parser.md new file mode 100644 index 0000000..2bc0376 --- /dev/null +++ b/docs/parser.md @@ -0,0 +1,53 @@ + +# What is the parser? + +[JSON Schema](http://json-schema.org/) is a standard for describing JSON data formats using JSON notation. Simple +schemas are more or less human-readable descriptions of what structure is expected and what data is allowed at each +level. + +You can reuse content within or between schemas using the `$ref` notation, which allows you to refer to content found +elsewhere. Schemas that make use of the `$ref` notation are not generally human readable, especially when there is a +chain of inherited definitions that must each be retrieved and understood before you reach the level of "simple" rules. + +This component parses a JSON Schema and dereferences all of its `$ref` links (including chains of references). The +final result is a JSON Schema that enforces the same structure as the original, but which is composed only of "simple" +rules. + +# What is it used for? + +The parser can be used to present a simplified (dereferenced schema). Our first use case is adding partial support for +custom error messages as outlined in [draft v5 of the JSON Schema standard](https://github.com/json-schema/json-schema/wiki/Custom-error-messages-(v5-proposal)). +See [the documentation for the `evolveErrors` function](evolveErrors.md) for details. + +# Requirements + +This component uses [json-schema-ref-parser](https://github.com/BigstickCarpet/json-schema-ref-parser). We configure +it using `options.parserOptions` (empty by default). Read the library's documentation for details of [what options +are available](https://github.com/BigstickCarpet/json-schema-ref-parser/blob/master/docs/options.md). + +# `gpii.schema.parser` + +A server-side parser which loads and dereferences schema content on startup. + +## Component Options + +The following component configuration options are supported: + +| Option | Type | Description | +| ------------ | -------- | ----------- | +| `schemaDirs` | `String` or `Array` | The location of the directory or directories containing all schemas used by the parser. Each entry should be a package-relative path such as `%gpii-handlebars/tests/schemas`.| + + +## Invokers + +The `parser` component provides the following invokers: + +### `{parser}.dereferenceSchema(schemaPath, schemaKey)` + +* `schemaPath {String}`: A path (server side )or URI (client side) to the directory or base URL where the schema can be found. Will be combined with `schemaKey` to construct the full location of the schema. +* `schemaKey {String}`: The filename of the schema relative to `schemaPath`. +* Returns: A `promise` that will be satisfied when the parser finishes its work. The promise itself does not return any value. + +Dereference a single schema. As the process is asynchronous, this function returns a promise. Typically accessed +using the parser component's `dereferenceSchema` invoker and the `schemaKey` attribute. + diff --git a/docs/schemaLinkMiddleware.md b/docs/schemaLinkMiddleware.md new file mode 100644 index 0000000..1d8be81 --- /dev/null +++ b/docs/schemaLinkMiddleware.md @@ -0,0 +1,77 @@ +# The JSON Schema Link Middleware + +When writing REST interfaces, we also commonly return JSON data in response to a request. JSON Schemas can also be +used to provide hints about the output format we are using. + +The working group that writes the JSON Schema standard [has outlined two approaches for labeling outgoing responses](http://json-schema.org/latest/json-schema-core.html#anchor33). +Both of these involve setting HTTP headers in the outgoing response, as in: + + Content-Type: application/my-media-type+json; profile="http://example.com/my-hyper-schema#" + Link: ; rel="describedBy" + +The schema link middleware provided in this package adds these headers to all requests it is allowed to work with. +There are two grades, one for normal (non-error) requests, and one for error requests. To use either of these, they +must be allowed to work with the request before whatever router or middleware sends the final response to the user. The +ordering of middleware components is controlled using +[priorities and namespaces](http://docs.fluidproject.org/infusion/development/Priorities.html). + +These grades do not send a response to the user. You are expected to wire a `gpii.express.router` instance into the +middleware chain that will respond to the user. Otherwise, the error will be passed along to the default Express +error handler. + +# `gpii.schema.schemaLinkMiddleware` + +The middleware grade for non-error requests. + +## Component Options + +In addition to the standard options available for an instance of +[`gpii.express.middleware`](https://github.com/GPII/gpii-express/blob/master/docs/middleware.md), this grade supports +the following unique options: + +| Option | Type | Description | +| ----------- | ---------- | ----------- | +| `schemaKey` | `{String}` | A short key for the schema that will represent it in the `Content-Type` header. | +| `schemaUrl` | `{String}` | A URL for the schema, which will be included in both the `Link` and `Content-Type` headers. | + + +## Component Invokers + +### `{that}.middleware(request, response, next)` + +* `request`: An object representing the individual user's request. See [the `gpii-express` documentation](https://github.com/GPII/gpii-express/blob/master/docs/express.md#the-express-request-object) for details. +* `response`: The response object, which can be used to send information to the requesting user. See [the `gpii-express` documentation](https://github.com/GPII/gpii-express/blob/master/docs/express.md#the-express-response-object) for details. +* `next`: The next Express middleware or router function in the chain. See [the `gpii-express` documentation for details](https://github.com/GPII/gpii-express/blob/master/docs/middleware.md#what-is-middleware). +* Returns: Nothing. + +This invoker fulfills the standard contract for a `gpii.express.middleware` component. It adds the two headers outlined +above and then allows processing to continue along the chain of middleware. + + +# `gpii.schema.schemaLinkMiddleware.error` + +The middleware grade for error requests. + +## Component Options + +In addition to the options outlined above, and the options supported for an instance of +[`gpii.express.middleware.error`](https://github.com/GPII/gpii-express/blob/master/docs/errorMiddleware.md), this grade +supports the following options: + +| Option | Type | Description | +| ------------ | ---------- | ----------- | +| `statusCode` | `{Number}` | The [HTTP status code](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) to send via the `response` object. | + + +## Component Invokers + +### `{that}.middleware(error, request, response, body)` + +* `error`: The error payload returned by upstream middleware. See [the `gpii-express` documentation](https://github.com/GPII/gpii-express/blob/master/docs/middleware.md#error-handling-middleware) for details. +* `request`: An object representing the individual user's request. See [the `gpii-express` documentation](https://github.com/GPII/gpii-express/blob/master/docs/express.md#the-express-request-object) for details. +* `response`: The response object, which can be used to send information to the requesting user. See [the `gpii-express` documentation](https://github.com/GPII/gpii-express/blob/master/docs/express.md#the-express-response-object) for details. +* `next`: The next Express middleware or router function in the chain. See [the `gpii-express` documentation for details](https://github.com/GPII/gpii-express/blob/master/docs/middleware.md#what-is-middleware). +* Returns: Nothing. + +This invoker fulfills the standard contract for a `gpii.express.middleware.error` component. It adds the two headers +outlined above and then allows processing to continue along the chain of error middleware. diff --git a/docs/schemaValidationMiddleware.md b/docs/schemaValidationMiddleware.md new file mode 100644 index 0000000..d463397 --- /dev/null +++ b/docs/schemaValidationMiddleware.md @@ -0,0 +1,126 @@ +# The Schema "Gatekeeper" Validation Middleware + +Although you usually will build some fault tolerance into your components, on some level they expect to deal with +data that has a particular structure, and which contains the expected type of information (strings, booleans, dates, +etc). Although it cannot confirm whether data supplied by an end user is accurate or meaningful, [the JSON Schema validation component](validator.md) +provided by this package can at least verify whether the supplied data is "valid", i.e. that it matches the rules +outlined in a particular JSON Schema. + +The `gpii.schema.validationMiddleware` component provided with this package validates all incoming requests, and rejects +invalid payloads. This allows your server-side components to at least assume they will only receive JSON data that is +valid according to the supplied schema. The "gatekeeper" doesn't know anything about how you intend to use the data. +It only examines the payload and steps in if the payload is not valid according to the configured JSON Schema. + +The base middleware is intended to be used with a `gpii.express` or `gpii.express.router` instance. The +`gpii.schema.validationMiddleware.requestAware.router` wrapper is provided as a convenient starting point. With that router, +you can created "gated" REST endpoints that only pass through valid payloads to the underlying handlers, as show here: + + var fluid = require("infusion"); + var gpii = fluid.registerNamespace("gpii"); + + require("gpii-express"); + require("gpii-json-schema"); + + fluid.defaults("gpii.schema.tests.handler", { + gradeNames: ["gpii.express.handler"], + invokers: { + handleRequest: { + funcName: "{that}.sendResponse", + args: [200, "Someone sent me a valid JSON payload."] + } + } + ); + + gpii.express({ + gradeNames: ["gpii.schema.validationMiddleware.requestAware.router"], + handlerGrades: ["gpii.schema.tests.handler"], + schemaKey: "valid.json", + schemaDirs: ["%my-package/src/schemas"], + responseSchemaKey: "message.json", + responseSchemaUrl: "http://my.site/schemas/" + path: "/gatekeeper" + }); + +If you were to launch this example, you would have a REST endpoint `/gatekeeper` that compares all POST request payloads +to the schema `valid.json`, which can be found in `%my-package/src/schemas`. If a payload is valid according to the +schema, the handler defined above would output a canned "success" message. If the payload is invalid, the underlying +`gpii.express.middleware` instance steps in and responds with a failure message. + +# Displaying validation messages onscreen + +The [`errorBinder`](errorBinder.md) component included with this package is designed to associate the validation error +messages produced by `gpii.schema.validationMiddleware` with on-screen elements. See that component's documentation for details. + +# Components + +## `gpii.schema.validationMiddleware` + +Validates information available in the request object. The incoming request is first transformed using +`fluid.model.transformWithRules` and`options.rules.requestContentToValidate`. The results are validated against +`options.schemaKey`. + +The default options validate the request body, as expected with a `POST` or `PUT` request. See the mix-in grades +below for examples of how different rules can handle different types of request data. + +The transformed request data is validated against the schema. Any validation errors are then transformed using +`options.rules.validationErrorsToResponse` before they are sent to the user. The default format looks roughly like: + + { + ok: false, + message: "The JSON you have provided is not valid.", + errors: { + field1: ["This field is required."] + } + } + +The rejection output of this middleware is delivered using a [`schemaHandler`](./handler.md). It should match the JSON +Schema specified in `options.responseSchemaKey` and `options.responseSchemaUrl`. + +### Component Options + +The following component configuration options are supported: + +| Option | Type | Description | +| ------------------- | -------- | ----------- | +| `handlerGrades` | `Array` | An array of grade names that will be used in constructing our request handler. | +| `method` | `String` | The method(s) the inner router will respond to. These should be lowercase strings corresponding to the methods exposed by Express routers. The default is to use the `POST` method, there are convenience grades for each method. | +| `responseSchemaKey` | `String` | The schema key that [our handler](./handler.md) will use in constructing response headers. | +| `responseSchemaUrl` | `String` | The base URL where `responseSchemaKey` can be found. | +| `rules.requestContentToValidate` | `Object` | The [rules to use in transforming](http://docs.fluidproject.org/infusion/development/ModelTransformationAPI.html#fluid-model-transformwithrules-source-rules-options-) the incoming data before validation (see below for more details). | +| `rules.validationErrorsToResponse` | `Object` | The [rules to use in transforming](http://docs.fluidproject.org/infusion/development/ModelTransformationAPI.html#fluid-model-transformwithrules-source-rules-options-) validation errors before they are sent to the user (see above). | +| `schemaKey` | `String` | The key (also the filename) of the schema to be used for validation. | +| `schemaDirs` | `String` | The path to the schema directories that contain a file matching `options.schemaKey`. This is expected to be an array of package-relative paths such as `%gpii-handlebars/tests/schemas`. | + +The default `rules.requestContentToValidate` in this grade are intended for use with `PUT` or `POST` body data. This +can be represented as follows: + + requestContentToValidate: { + "": "body" + } + +See `gpii.schema.validationMiddleware.handlesGetMethod` below for an example of working with query data. + +### Invokers + +#### `{middleware}.middleware(request, response, next)` + +* `request`: An object representing the individual user's request. See [the `gpii-express` documentation](https://github.com/GPII/gpii-express/blob/master/docs/express.md#the-express-request-object) for details. +* `response`: The response object, which can be used to send information to the requesting user. See [the `gpii-express` documentation](https://github.com/GPII/gpii-express/blob/master/docs/express.md#the-express-response-object) for details. +* `next`: The next Express middleware or router function in the chain. See [the `gpii-express` documentation for details](https://github.com/GPII/gpii-express/blob/master/docs/middleware.md#what-is-middleware). +* Returns: Nothing. + +This invoker fulfills the standard contract for a `gpii.express.middleware` component. It examines the `request` +content and interrupts the conversation if the JSON data supplied as part of `request` is not valid according to the +JSON Schema `options.schemaKey` found at `options.schemaPath`. If the content is valid, execute the supplied `next` +function and let some other downstream piece of middleware continue the conversation. + +This function is expected to be called by Express (or by an instance of `gpii.express`). + +## `gpii.schema.validationMiddleware.handlesQueryData` + +A mix-in grade that configures an instance of `gpii.schema.validationMiddleware` to validate query data. +Sets `rules.requestContentToValidate` to the following: + + requestContentToValidate: { + "": "query" + } diff --git a/docs/validator.md b/docs/validator.md new file mode 100644 index 0000000..4057fbd --- /dev/null +++ b/docs/validator.md @@ -0,0 +1,208 @@ +# What is the validator? + +JSON is a very flexible format, which allows it to cover a wide range of use cases. However, in most cases you will +want to impose some practical limitations on the data you will accept. + +[JSON Schema](http://json-schema.org/) is a standard for describing JSON data formats using JSON notation. The +structure of a JSON schema roughly mirrors the structure of the JSON data being examined. + +JSON Schemas provide a clear way to indicate what is and is not valid within a given JSON document. You might use JSON +Schemas to validate a form's data on the client side before attempting to submit it to the server. You might also +validate data and report errors whenever a component's model changes. + +# How does it work? + +This package provides client and server-side components that use [AJV](https://github.com/epoberezkin/ajv) to validate +supplied JSON data against a JSON Schema + +The main work of the validator component is performed by its `validate(schemaKey, JSON)` invoker. Here is an example +of how the validator might be used from within node: + + var fluid = require("infusion""); + var gpii = fluid.registerNamespace("gpii"); + + require("gpii-json-schema"); + var validator = gpii.schema.validator({ + schemaContents: { + sample: { "type": "object", "properties": { "required": { "type": "boolean" } }, "required": ["required"]} + } + }); + + var errors = validator.validate("sample", { foo: "bar" }); + if (errors) { + // Complain + } + else { + // Rejoice + } + + +If there are no validation errors, we return `undefined`. If there are validation errors, the raw output generated by +AJV is transformed using the static function `sanitizeValidationErrors` (see below). + +This component is configured to work together with schemas that have been dereferenced, i. e. where all references +to remote definitions have been reduced to the underlying JSON Schema rules. See [the parser documentation](parser.md) +for details. + +On the server-side, this is handled directly by a `gpii.schema.parser` instance. On the client side, the +validator works with content retrieved from [a `gpii.schema.inline.router` instance](router.md), which is already +dereferenced. + +# Components + +## gpii.schema.validator.ajv + +The common grade, designed for use on the client and server-side. + +### Component Options + +The following component configuration options are supported: + +| Option | Type | Description | +| ------------------ | -------- | ----------- | +| `validatorOptions` | `Object` | Options to be passed to our AJV validator instance (check [the AJV options documentation](https://github.com/epoberezkin/ajv#options) for the syntax and available options). | + + +### Invokers + +#### `{validator}.validate(key, content)` + +* `key {String}`: The schema key we are validating against. +* `content {Object}`: The JSON content we are validating. +* Returns: An `{Object}` describing the errors, or `undefined` if there are no errors. + +The raw output provided by AJV is run through `sanitizeValidationErrors` (see below) to provide more user-friendly +feedback where possible. + +### Static Functions + + +#### `gpii.schema.validator.ajv.sanitizeValidationErrors(schemaKey, errors)` + +* `schemaKey {String}`: The schema key we are validating against. +* `errors`: A map of existing error results to be sanitized (see below for examples). + +AJV gives us output like: + +``` + [ + { + "keyword": "minLength", + "schemaPath": "/password/minLength", + "dataPath": ".password", + "message": "should NOT be shorter than 8 characters" + }, + { + "keyword": "pattern", + "schemaPath": "/password/allOf/1/pattern", + "dataPath": ".password", + "message": "should match pattern \"[A-Z]+\"" + }, + { + "keyword": "required", + "schemaPath": "/deep/required/0", + "dataPath": ".deep.required", + "message": "is a required property" + } + ] +``` + +This function uses [`gpii.schema.errors.evolveError`](evolveErrors.md) to look for `errors` definitions that match the `schemaPath` output +from the validator. If it finds them, it replaces `message` with alternate wording, so that we end up with output like: + +``` + [ + { + "keyword": "minLength", + "schemaPath": "/password/minLength", + "dataPath": ".password", + "message": "Passwords must be at least 8 characters long." + }, + { + "keyword": "pattern", + "schemaPath": "/password/allOf/1/pattern", + "dataPath": ".password", + "message": "Passwords must contain at least one uppercase character." + }, + { + "keyword": "required", + "schemaPath": "/deep/required/0", + "dataPath": ".deep.required", + "message": "You must enter a value for the 'deeply required' field." + } + ] +``` +See [the documentation for `evolveError`](evolveErrors.md) for more details. + +Note that `dataPath` values are ["Javascript property access notation"](https://github.com/epoberezkin/ajv#validation-errors). +These values are URI encoded along the way, and as such, slashes in element names are replaced with `~1`. To avoid +mangling `/1`, tildes are escaped as `~0`. See [the relevant AJV functions for more information](https://github.com/epoberezkin/ajv/blob/3806b9d8a3d11a23ee505bcc8e9eb4907b4cd328/lib/compile/util.js#L270). + +The `schemaPath` values are JSON Pointer notation, see [RFC 6901](https://tools.ietf.org/html/rfc6901) for details. + + +# gpii.schema.validator.ajv.server + +A server-side component which loads and dereferences all schemas on startup. + +## Component options + +Supports all options from `gpii.schema.validator.ajv` as well as the following unique options: + +| Option | Type | Description | +| ------------ | -------- | ----------- | +| `schemaDirs` | `Object` | The path to one or more schema directories that contain the files we can validate against. This is expected to be an array of package-relative paths such as `%gpii-handlebars/tests/schemas`. | + +# Displaying validation messages onscreen + +The [`errorBinder`](errorBinder.md) component included with this package is designed to associate the validation error +messages produced by `gpii.schema.validator` with on-screen elements. See that component's documentation for details. + +# Reusing Content between Schemas + +One of the key strengths of JSON Schema is that it allows you to compose a complex schema out of parts taken from +other schemas. The validator in this package supports references between JSON Schemas. + +Inheritance is still a sticking point at least in v4 of the draft standard. You cannot safely expect to overlay +multiple schemas on top of each other. Best practice for now is to only reuse individual definitions between schemas, +and to explicitly specify each schema's required properties. + +Inheritance within and between schemas is handled using `$ref` references, as in: + + $ref: "filename.json#/definitions/field" + +These values are expected to be relative or absolute URLs, in our case the exactly correspond to the filename of the schema. + +To give an example, suppose we have a simple `person` schema: + +``` +{ + "id": "person.json", + "definitions": { + "firstname": { type: "string" }, + "lastname": { type: "string" } + }, + properties: { + "firstname": { "$ref": "#/definitions/firstname" }, + "lastname": { "$ref": "#/definitions/lastname" } + } +} +``` + +We can add our own properties in a number of ways, but here is the approach we favor at the moment: + +``` +{ + "id": "reachable-person.json", + "definitions": { + "email": { "type": "string", "format": "email" } + }, + properties: { + "firstname": { "$ref": "person.json#/definitions/firstname" }, + "lastname": { "$ref": "person.json#/definitions/lastname" }, + "email": { "$ref": "#/definitions/email" } + } +} +``` + +See the test schemas (particularly `derived.json` and `evolved-overlay.json`) for additional examples. \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..10a16d1 --- /dev/null +++ b/index.js @@ -0,0 +1,18 @@ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); + +// Expose this package's content so that relative paths can be resolved using `fluid.module.resolvePath`. +fluid.module.register("gpii-json-schema", __dirname, require); + +// Require all of the server-side components at once. +require("./src/js/common/evolveErrors"); +require("./src/js/common/hasRequiredOptions"); +require("./src/js/common/pointers"); +require("./src/js/common/validator"); +require("./src/js/server/validator"); +require("./src/js/server/parser"); +require("./src/js/server/schemaInlineMiddleware"); +require("./src/js/server/schemaLinkMiddleware"); +require("./src/js/server/schemaValidationMiddleware"); + diff --git a/package.json b/package.json new file mode 100644 index 0000000..441a523 --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "gpii-json-schema", + "version": "1.0.0", + "description": "Fluid components to add support for JSON Schema validation.", + "main": "", + "scripts": { + "test": "node_modules/.bin/istanbul cover tests/all-tests.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/GPII/gpii-json-schema" + }, + "author": "Tony Atkins ", + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "4.11.2", + "gpii-binder": "1.0.0-dev.20170127T144054Z.54a5ef2", + "gpii-express": "1.0.2", + "gpii-handlebars": "1.0.0-dev.20170127T160953Z.4d15347", + "infusion": "3.0.0-dev.20170127T130413Z.103de6e", + "json-schema-ref-parser": "3.1.2", + "jsonpointer.js": "0.4.0", + "quoted-printable": "1.0.1" + }, + "devDependencies": { + "eslint": "3.15.0", + "eslint-config-fluid": "1.1.0", + "fluid-grunt-eslint": "18.1.2", + "foundation-sites": "6.3.0", + "gpii-webdriver": "1.0.0-dev.20170127T135806Z.aa30519", + "grunt": "1.0.1", + "grunt-jsonlint": "1.1.0", + "handlebars": "4.0.6", + "istanbul": "0.4.5", + "kettle": "1.3.2", + "node-jqunit": "1.1.4", + "pagedown": "1.1.0", + "request": "2.79.0" + } +} diff --git a/src/js/client/errorBinder.js b/src/js/client/errorBinder.js new file mode 100644 index 0000000..6e113b0 --- /dev/null +++ b/src/js/client/errorBinder.js @@ -0,0 +1,303 @@ +/* + + This package provides client side components that: + + 1. Bind validation errors to onscreen elements. + 2. Display server-side validation feedback. + 3. Perform client-side validation and prevent submission if the data is invalid. + + See the documentation for more details: + + https://github.com/the-t-in-rtf/gpii-json-schema/blob/master/docs/validator.md + + */ +/* globals fluid */ +(function () { + "use strict"; + var gpii = fluid.registerNamespace("gpii"); + fluid.registerNamespace("gpii.schemas.client.errorBinder"); + + // The base component used to actually display validation errors. + fluid.defaults("gpii.schemas.client.errorBinder", { + gradeNames: ["fluid.component"], // TODO: This should really be a more specific grade + errorBindings: "{that}.options.bindings", + selectors: { + "fieldError": ".fieldError" + }, + templates: { + inlineError: "validation-error-inline" + }, + model: { + fieldErrors: [] + }, + components: { + renderer: { + type: "gpii.handlebars.renderer" + } + }, + invokers: { + renderErrors: { + funcName: "gpii.schemas.client.errorAwareForm.renderErrors", + args: ["{that}", "{renderer}"] + } + }, + modelListeners: { + // TODO: This is a problem, as it rips the rug out from under the form before it can submit. + // Split into two steps, one that renders the initial markup, the other that renders the errors (if any) + fieldErrors: { + funcName: "{that}.renderErrors", + excludeSource: "init", + args: ["{that}"] + } + } + }); + + fluid.registerNamespace("gpii.schemas.client.errorAwareForm"); + + // We need to ensure that both our own markup and the field errors are rendered before we fire `onMarkupRendered`. + gpii.schemas.client.errorAwareForm.renderMarkup = function (that, renderer, selector, template, data, manipulator) { + manipulator = manipulator ? manipulator : "html"; + if (renderer) { + var mainElement = that.locate(selector); + renderer[manipulator](mainElement, template, data); + gpii.handlebars.templateAware.refreshDom(that); + + // Let everyone else know that the markup has been updated. + that.events.onMarkupRendered.fire(that); + } + else { + fluid.fail("I cannot render content without a renderer."); + } + }; + + // We need to ensure that both our own markup and the field errors are rendered before we fire `onMarkupRendered`. + gpii.schemas.client.errorAwareForm.renderErrors = function (that, renderer) { + if (renderer) { + // var mainElement = that.locate(selector); + // renderer[manipulator](mainElement, template, data); + // Refresh the DOM once without firing any events. This intermediate refresh is required before we can + // render our field-specific errors. + // gpii.handlebars.templateAware.refreshDom(that); + + // Get rid of any previous validation errors. + that.locate("fieldError").remove(); + + // Step through the list of bindings and look for anything that matches the current validation errors. + fluid.each(that.options.errorBindings, function (value, key) { + var selector = typeof value === "string" ? key : value.selector; + var fieldElement = that.locate(selector); + if (fieldElement) { + var expectedPath = "." + (typeof value === "string" ? value : value.path); + fluid.each(that.model.fieldErrors, function (error) { + var errorDataPath = error.keyword === "required" ? error.dataPath + "." + error.params.missingProperty : error.dataPath; + if (errorDataPath === expectedPath) { + // element, key, context + that.renderer.before(fieldElement, that.options.templates.inlineError, error); + } + }); + } + }); + } + else { + fluid.fail("I cannot render errors without a renderer."); + } + }; + + // An instance of `templateFormControl` that uses the `errorBinder` to display server-side errors. + fluid.defaults("gpii.schemas.client.errorAwareForm", { + gradeNames: ["gpii.schemas.client.errorBinder", "gpii.handlebars.templateFormControl"], + templates: { + error: "validation-error-summary" + }, + events: { + onTemplatesLoaded: null, + onReady: { + events: { + onTemplatesLoaded: "onTemplatesLoaded" + } + } + }, + invokers: { + renderMarkup: { + funcName: "gpii.schemas.client.errorAwareForm.renderMarkup", + args: ["{that}", "{renderer}", "{arguments}.0", "{arguments}.1", "{arguments}.2", "{arguments}.3"] // renderer, selector, template, data, manipulator + } + }, + listeners: { + "onReady.renderMarkup": { + func: "{that}.renderInitialMarkup" + } + }, + rules: { + successResponseToModel: { + successMessage: "responseJSON.message", + fieldErrors: { literalValue: [] }, + errorMessage: { literalValue: false } + }, + errorResponseToModel: { + successMessage: { literalValue: false}, + errorMessage: "responseJSON.message", + fieldErrors: "responseJSON.fieldErrors" + } + }, + components: { + renderer: { + type: "gpii.handlebars.renderer.serverAware", + options: { + listeners: { + "onTemplatesLoaded.notifyParent": { + func: "{gpii.schemas.client.errorAwareForm}.events.onTemplatesLoaded.fire" + } + } + } + }, + success: { + options: { + listeners: { + "onCreate.renderMarkup": { + func: "{that}.renderInitialMarkup" + } + }, + model: { + message: "{gpii.schemas.client.errorAwareForm}.model.successMessage" + } + } + }, + error: { + options: { + listeners: { + "onCreate.renderMarkup": { + func: "{that}.renderInitialMarkup" + } + }, + model: { + message: "{gpii.schemas.client.errorAwareForm}.model.errorMessage", + fieldErrors: "{gpii.schemas.client.errorAwareForm}.model.fieldErrors" + } + } + } + } + }); + + fluid.registerNamespace("gpii.schemas.client.errorAwareForm.clientSideValidation"); + + /** + * + * A gatekeeper function that only allows form submission if there are no validation errors. + * + * @param that - The clientSideValidation component itself. + * @param event - The jQuery Event (see http://api.jquery.com/Types/#Event) passed by the DOM element we're bound to. + */ + gpii.schemas.client.errorAwareForm.clientSideValidation.submitForm = function (that, event) { + if (event) { event.preventDefault(); } + + // Validate our data at least once before attempting to submit it. + that.validateContent(false); + + if (!that.model.fieldErrors || that.model.fieldErrors.length === 0) { + // Let the `ajaxCapable` grade handle the request and response. + that.makeRequest(); + } + }; + + /** + * + * Validate client-side model content and display any errors. + * + * @param that - The clientSideValidation component itself. + * + */ + gpii.schemas.client.errorAwareForm.clientSideValidation.validateContent = function (that) { + // We assume that the content we will transmit is governed by the rule system from the `ajaxCapable` grade. + var dataToValidate = fluid.model.transformWithRules(that.model, that.options.rules.modelToRequestPayload); + // The trailing empty array seems to avoid problems in relaying the data to the errors component. + var validatorResults = that.validator.validate(that.options.schemaKey, dataToValidate) || []; + + that.applier.fireChangeRequest({ path: "fieldErrors", value: validatorResults, source: "validator"}); + + // Clear out any previous "success" messages if there are validation errors. + if (validatorResults && validatorResults.length > 0) { + that.applier.fireChangeRequest({ path: "successMessage", value: false, source: "validator"}); + } + }; + + // A grade that adds client-side validation before the form is submitted. The form cannot be submitted if there are validation errors. + // + // You must be able to reach an instance of the `inlineSchema` router as well as individual schemas to use this grade. + fluid.defaults("gpii.schemas.client.errorAwareForm.clientSideValidation", { + gradeNames: ["gpii.hasRequiredOptions", "gpii.schemas.client.errorAwareForm"], + requiredOptions: ["inlineSchemaUrl", "schemaKey", "rules.modelToRequestPayload"], + inlineSchemaUrl: "/allSchemas", + events: { + onSchemasLoaded: null, + onReady: { + events: { + onSchemasLoaded: "onSchemasLoaded" + } + } + }, + listeners: { + "onReady.validateContent": { + func: "{that}.validateContent" + } + }, + invokers: { + submitForm: { + funcName: "gpii.schemas.client.errorAwareForm.clientSideValidation.submitForm", + args: ["{that}", "{arguments}.0"] + }, + validateContent: { + funcName: "gpii.schemas.client.errorAwareForm.clientSideValidation.validateContent", + args: ["{that}"] + } + }, + components: { + validator: { + type: "gpii.schema.validator.ajv.client", + options: { + inlineSchemaUrl: "{gpii.schemas.client.errorAwareForm.clientSideValidation}.options.inlineSchemaUrl", + listeners: { + "onSchemasLoaded.notifyParent": { + func: "{gpii.schemas.client.errorAwareForm.clientSideValidation}.events.onSchemasLoaded.fire" + } + } + } + } + } + }); + + // A "real time" validation grade that extends the `clientSideValidation` grade to revalidate when the model changes. + fluid.defaults("gpii.schemas.client.errorAwareForm.clientSideValidation.realTime", { + gradeNames: ["gpii.schemas.client.errorAwareForm.clientSideValidation"], + events: { + onReady: { + events: { + onSchemasLoaded: "onSchemasLoaded", + onTemplatesLoaded: "onTemplatesLoaded" + } + } + }, + // // TODO: Remove these once we figure out our timing problems. + // invokers: { + // logEvent: { + // funcName: "fluid.log", args: ["event", "{arguments}.0", " fired for component ", "{that}.typename", ":", "{that}.id"] + // } + // }, + // listeners: { + // onReady: { func: "{that}.logEvent", args: "onReady"}, + // onSchemasLoaded: { func: "{that}.logEvent", args: "onSchemasLoaded"}, + // onTemplatesLoaded: { func: "{that}.logEvent", args: "onTemplatesLoaded"}, + // onMarkupRendered: { func: "{that}.logEvent", args: "onMarkupRendered"}, + // onDomChange: { func: "{that}.logEvent", args: "onDomChange"} + // }, + modelListeners: { + "": { + func: "{that}.validateContent", + excludeSource: [ + "init", // The validator will take care of the first pass once it's ready. + "validator" // Do not validate if the validator itself is saving its results. + ] + } + } + }); +})(); diff --git a/src/js/client/validator.js b/src/js/client/validator.js new file mode 100644 index 0000000..9aa81f5 --- /dev/null +++ b/src/js/client/validator.js @@ -0,0 +1,63 @@ +/* + + A client-side wrapper for the validation component. See the documentation for details: + + https://github.com/the-t-in-rtf/gpii-json-schema/blob/master/docs/validator.md + + */ +/* globals fluid, $ */ +(function () { + "use strict"; + var gpii = fluid.registerNamespace("gpii"); + + fluid.registerNamespace("gpii.schema.validator.ajv.client"); + + /** + * + * Fire off a jQuery AJAX request to retrieve our schemas from the server-side `inlineSchema` router. + * + * @param that - The client-side validator component. + */ + gpii.schema.validator.ajv.client.retrieveSchemas = function (that) { + $.ajax({ + url: that.options.inlineSchemaUrl, + success: that.saveSchemas, + error: fluid.fail, + json: true + }); + }; + + /** + * + * Save the schemas returned by the server-side `inlineSchema` router to our model. + * + * @param that - The client-side validator component. + * @param jqXHR - The full jQuery jqXHR response, including the JSON payload containing our schemas. + */ + gpii.schema.validator.ajv.client.saveSchemas = function (that, jqXHR) { + var schemas = jqXHR.responseJSON; + if (schemas) { + that.applier.change("schemas", schemas); + that.events.onSchemasLoaded.fire(); + } + }; + + fluid.defaults("gpii.schema.validator.ajv.client", { + gradeNames: ["gpii.schema.validator.ajv", "gpii.hasRequiredOptions"], + requiredFields: { + "inlineSchemaUrl": true + }, + listeners: { + "onCreate.retrieveSchemas": { + funcName: "gpii.schema.validator.ajv.client.retrieveSchemas", + args: ["{that}"] + } + }, + invokers: { + saveSchemas: { + funcName: "gpii.schema.validator.ajv.client.saveSchemas", + args: ["{that}", "{arguments}.2"] + } + } + }); +})(); diff --git a/src/js/common/evolveErrors.js b/src/js/common/evolveErrors.js new file mode 100644 index 0000000..a065bba --- /dev/null +++ b/src/js/common/evolveErrors.js @@ -0,0 +1,52 @@ +// A static function to "evolve" an error using the proposed v5 "errors" option. See the documentation for details: +// +// https://github.com/the-t-in-rtf/gpii-json-schema/blob/master/docs/errors.md +// +/* eslint-env node */ +"use strict"; +var fluid = fluid || require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.schema.errors"); + +/** + * + * Look up the error metadata for a single field and use that to "evolve" the raw validator output. + * + * @param schemaContent {Object} - The full dereferenced content of the schema we are working with. + * @param error {Object} - The validator error to be evolved. + * @returns An "evolved" copy of the original error. + */ +gpii.schema.errors.evolveError = function (schemaContent, error) { + var evolvedError = fluid.copy(error); + + // We start with `error.schemaPath`, which will be something like `#/properties/password/allOf/1/pattern`. + if (error.schemaPath) { + var isRequiredError = error.schemaPath.match("/required$"); + //gpii.schema.pointers.getRequiredFieldPointer = function (schemaContent, failurePointer, propertyToMatch) { + var failurePointer = isRequiredError ? gpii.schema.pointers.getRequiredFieldPointer(schemaContent, error.schemaPath, error.params.missingProperty) : error.schemaPath; + + // Check the document level for definitions first. + var documentErrorDefinitions = gpii.schema.pointers.resolveJsonPointer(schemaContent, "#/errors"); + if (documentErrorDefinitions && documentErrorDefinitions[failurePointer]) { + evolvedError.message = documentErrorDefinitions[failurePointer]; + } + // If we have not found anything at the document level, inspect the field itself. + else { + //gpii.schema.pointers.getFieldErrorsFromFailure = function (failurePointer) { + var fieldErrorsPointer = gpii.schema.pointers.getFieldErrorsFromFailure(error.schemaPath); + // TODO; Make into a static call + //gpii.schema.pointers.resolveJsonPointer = function (schemaContent, jsonPointer) { + var fieldErrorsDefinition = gpii.schema.pointers.resolveJsonPointer(schemaContent, fieldErrorsPointer); + //gpii.schema.pointers.getLastJsonPointerSegment = function (jsonPointer) { + var failureKey = gpii.schema.pointers.getLastJsonPointerSegment(failurePointer); + + if (fieldErrorsDefinition && fieldErrorsDefinition[failureKey]) { + evolvedError.message = fieldErrorsDefinition[failureKey]; + } + } + } + + return evolvedError; +}; + diff --git a/src/js/common/hasRequiredOptions.js b/src/js/common/hasRequiredOptions.js new file mode 100644 index 0000000..262236b --- /dev/null +++ b/src/js/common/hasRequiredOptions.js @@ -0,0 +1,51 @@ +// A grade that wires in a function that checks for the existing of required `options` on startup and fails with an +// error message if they are missing. This is automatically called when a component that has this grade is created. +// +// This is intended to test for the presence of required options. More complex options validation (for example using a +// validator) is not supported by this package. +// +// This grade looks for a single configuration option, `options.requiredFields`, an object whose keys represent relative +// paths to definitions, as in: +// +// requiredFields: { +// "path.relative.to.that.options": true +// } +// +/* eslint-env node */ +"use strict"; +var fluid = fluid || require("infusion"); // Can also be used within client-side components. +var gpii = fluid.registerNamespace("gpii"); + +/** + * + * Check a component's options to ensure that required options are set. Throws `fluid.fail` if one or more fields is + * missing. + * + * @param options {Object} - The component options to be checked. + * @param requiredFields {Array} - The field paths we expect to be found in the options. + * @param component - The component whose options we are checking. + */ +gpii.checkRequiredOptions = function (options, requiredFields, component) { + var errors = []; + + fluid.each(requiredFields, function (value, path) { + var requiredValue = fluid.get(options, path); + if (requiredValue === undefined) { + errors.push("You have not supplied the required option '" + path + "' in component '" + component.typeName + "'..."); + } + }); + + if (errors.length > 0) { + fluid.fail(errors); + } +}; + +fluid.defaults("gpii.hasRequiredOptions", { + gradeNames: ["fluid.component"], + listeners: { + "onCreate.checkRequiredOptions": { + funcName: "gpii.checkRequiredOptions", + args: ["{that}.options", "{that}.options.requiredFields", "{that}"] + } + } +}); diff --git a/src/js/common/pointers.js b/src/js/common/pointers.js new file mode 100644 index 0000000..546c9c9 --- /dev/null +++ b/src/js/common/pointers.js @@ -0,0 +1,112 @@ +/** + * + * Static functions used to work with JSON pointers. + * + */ +/* eslint-env node */ +"use strict"; +var fluid = fluid || require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +var jsonpointer = jsonpointer || require("jsonpointer.js"); + +fluid.registerNamespace("gpii.schema.pointers"); + +/** + * + * @param schemaContent {Object} - The full dereferenced content of the schema we are working with. + * @param rawJsonPointer {String} - The JSON pointer to resolve. + * @returns The piece of the JSON Schema referred to by rawJonPointer, or undefined if that cannot be found. + * + */ +gpii.schema.pointers.resolveJsonPointer = function (schemaContent, jsonPointer) { + return jsonpointer.get(schemaContent, jsonPointer); +}; + +/** + * + * Static function to strip the last part of a JSON pointer. If we are already at the top (i.e. `#/`), we will stay at the top. + * + * @param jsonPointer {String} - The original JSON pointer. + * @returns A {string} representing the immediate parent of the original pointer. + */ +gpii.schema.pointers.getParentJsonPointer = function (jsonPointer) { + if (jsonPointer) { + var allButLastSegment = jsonPointer.split("/").slice(0, -1).join("/"); + // We have to append a trailing slash if we are already at the top of the chain + return allButLastSegment === "#" ? "#/" : allButLastSegment; + } +}; + +/** + * + * Static function to add a `childPath` to an existing JSON pointer. If keys in the path contain literal slashes + * or tildes, you are expected to escape them yourself, ~0 in place of literal tildes, and ~1 in place of literal + * slashes in a key name. + * + * @param jsonPointer {String} - The original JSON pointer. + * @param childPath {String} - The child segment(s) to add to the original pointer. + * @returns A {string} representing the child JSON pointer. + * + */ +gpii.schema.pointers.getChildJsonPointer = function (jsonPointer, childPath) { + var pointerSegments = (jsonPointer === "#/") ? ["#"] : jsonPointer.split("/"); + return pointerSegments.concat(childPath).join("/"); +}; + +/** + * + * Static function to determine the JSON pointer to an error definition given the JSON pointer to the failure returned + * by AJV. We will begin with something like `#/field1/type` and return something like `#/field1/errors`. + * + * @param failurePointer {String} - A JSON pointer representing the failing rule as reporting by AJV. + * @returns An {Object} representing the `errors` block for the given field. + * + */ +gpii.schema.pointers.getFieldErrorsFromFailure = function (failurePointer) { + var parentJsonPointer = gpii.schema.pointers.getParentJsonPointer(failurePointer); + return gpii.schema.pointers.getChildJsonPointer(parentJsonPointer, "errors"); +}; + + +/** + * + * Static function to determine the JSON pointer that points to a missing required field. Since required fields are + * defined in arrays within the enclosing object, these will be references like `#/required/1`. + * + * @param schemaContent {Object} - The full dereferenced content of the schema we are working with. + * @param failurePointer {String} - A JSON pointer representing the validation rule that was broken. + * @param propertyToMatch {String} - The missing property as reported by AJV. + * @returns A JSON pointer {String} that can be used to look up the relevant error data from the schema. + * + */ +gpii.schema.pointers.getRequiredFieldPointer = function (schemaContent, failurePointer, propertyToMatch) { + var requireDefinitions = gpii.schema.pointers.resolveJsonPointer(schemaContent, failurePointer); + + var requirementIndex = fluid.find(requireDefinitions, function (value, index) { + if (value === propertyToMatch) { return index; } + }); + + // The path to the message is relative to the parent + if (requirementIndex !== undefined && requirementIndex !== null) { + var parentPointer = gpii.schema.pointers.getParentJsonPointer(failurePointer); + var errorsPointer = gpii.schema.pointers.getChildJsonPointer(parentPointer, "required"); + return gpii.schema.pointers.getChildJsonPointer(errorsPointer, requirementIndex); + } + else { + return undefined; + } +}; + +/** + * + * A static function to return the last segment of a given JSON pointer. + * + * @param jsonPointer {String} - The original JSON pointer. + * @returns A {String} representing the last segment of the original pointer. + */ +gpii.schema.pointers.getLastJsonPointerSegment = function (jsonPointer) { + var segments = jsonPointer.split("/"); + return segments[segments.length - 1]; +}; + diff --git a/src/js/common/validator.js b/src/js/common/validator.js new file mode 100644 index 0000000..0fde304 --- /dev/null +++ b/src/js/common/validator.js @@ -0,0 +1,122 @@ +/* + + A Fluid component to handle JSON Schema validation. See the documentation for details: + + https://github.com/the-t-in-rtf/gpii-json-schema/blob/master/docs/validator.md + + */ +/* eslint-env node */ +"use strict"; +var fluid = fluid || require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +var Ajv = Ajv || require("ajv"); + +fluid.registerNamespace("gpii.schema.validator.ajv"); + +gpii.schema.validator.ajv.init = function (that) { + // We persist a single AJV instance so that we can take advantage of its automatic compiling and caching. + that.ajv = Ajv(that.options.validatorOptions); /* eslint new-cap: "off" */ + + gpii.schema.validator.ajv.refreshSchemas(that); +}; + +/** + * + * @param that {Object} the validator component itself. + * @param key {String} The key of the JSON Schema we are validating against. + * @param content {Object} The JSON data to be validated. + * @returns `{Object}` sanitized validation errors, if there are any, or `undefined` if there are no validation errors. + * + */ +gpii.schema.validator.ajv.validate = function (that, key, content) { + // TODO: Review with Antranig, this is meant to ensure that validation only takes place when already we have a valid schema. + if (that.model.schemas[key]) { + var contentValid = that.ajv.validate(key, content); + + if (!contentValid) { + return (gpii.schema.validator.ajv.sanitizeValidationErrors(that, key, that.ajv.errors)); + } + } + + return undefined; +}; + + +/** + * + * Iterate through the raw validation errors and evolve them if possible. This is gated and will abort if the parser is + * not yet ready. This function is called once the parser is ready. + * + * @param that - The validator component itself. + * @param schemaKey {String} - The filename/id of the schema we are working with. + * @param rawErrors {Object} - The raw error data returned by AJV. + * @returns An {Object} representing the original error data combined with any "evolved" error messages we were able to find. + */ +gpii.schema.validator.ajv.sanitizeValidationErrors = function (that, schemaKey, rawErrors) { + var schemaContent = that.model.schemas[schemaKey]; + var evolvedErrors = fluid.transform(rawErrors, function (error) { + return gpii.schema.errors.evolveError(schemaContent, error); + }); + + return evolvedErrors; +}; + +/** + * + * If we receive new schemas, make the validator aware of them so that we can simply validate using their key. + * + * @param that - The validator component itself. + * + */ +gpii.schema.validator.ajv.refreshSchemas = function (that) { + // Update the list of schemas using the supplied content + fluid.each(that.model.schemas, function (schemaContent, schemaKey) { + // AJV will not let us overwrite an existing schema , so we have to remove the current content first. + if (that.ajv.getSchema(schemaKey)) { + that.ajv.removeSchema(schemaKey); + } + + try { + that.ajv.addSchema(schemaContent, schemaKey); + } + catch (e) { + fluid.fail("There was an error loading one of your JSON Schemas:", e); + } + }); + + that.events.onSchemasRefreshed.fire(that); +}; + +fluid.defaults("gpii.schema.validator.ajv", { + gradeNames: ["fluid.modelComponent"], + validatorOptions: { + v5: true, // enable "v5" support for $data references + verbose: false, // Prevent invalid data (such as passwords) from being displayed in error messages + messages: true, // Display human-readable error messages + allErrors: true // Generate a complete list of errors and not just the first failure. + }, + model: { + schemas: {} + }, + events: { + onSchemasLoaded: null, + onSchemasRefreshed: null + }, + invokers: { + validate: { + funcName: "gpii.schema.validator.ajv.validate", + args: ["{that}", "{arguments}.0", "{arguments}.1"] // schemaKey, schemaContent + } + }, + listeners: { + "onCreate.init": { + funcName: "gpii.schema.validator.ajv.init", + args: ["{that}"] + }, + "onSchemasLoaded.refreshSchemas": { + funcName: "gpii.schema.validator.ajv.refreshSchemas", + args: ["{that}"] + } + } +}); diff --git a/src/js/server/parser.js b/src/js/server/parser.js new file mode 100644 index 0000000..154bf8d --- /dev/null +++ b/src/js/server/parser.js @@ -0,0 +1,110 @@ +/* + + A parser that resolves `$ref` references in JSON Schema definitions. See the documentation for details: + + https://github.com/the-t-in-rtf/gpii-json-schema/blob/master/docs/parser.md + +*/ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +var fs = require("fs"); +var path = require("path"); + +var $RefParser = require("json-schema-ref-parser"); + +fluid.registerNamespace("gpii.schema.parser"); + +/** + * + * @param that - The parser component itself + * + * Starts the process of dereferencing all schemas found in `options.schemaDirs`. Wraps them in a sequence to ensure + * that `onSchemasDereferenced` is fired once all of the schemas are dereferenced. + */ +gpii.schema.parser.loadSchemas = function (that) { + var promises = []; + fluid.each(fluid.makeArray(that.options.schemaDirs), function (schemaDir) { + var resolvePathToDir = fluid.module.resolvePath(schemaDir); + fluid.each(fs.readdirSync(resolvePathToDir), function (filename) { + if (filename.match(/.json$/i)) { + var fullPathToFile = path.resolve(resolvePathToDir, filename); + promises.push(gpii.schema.parser.dereferenceSchema(that, fullPathToFile, filename)); + } + }); + }); + + fluid.promise.sequence(promises).then( + function () { that.events.onSchemasDereferenced.fire(that); }, + function (error) { + fluid.fail(error.message || error); + } + ); +}; + + +/** + * Dereference all `$ref` links for a single schema. See the documentation for details. + * + * https://github.com/the-t-in-rtf/gpii-json-schema/blob/master/docs/parser.md#gpiischemaparserdereferenceschemathat-schemapath-schemakey + * + * @param that - The parser component itself. + * @param schemaPath {String} - The full path to the schema we are derferencing. + * @param schemaKey {String} - The filename/id of the schema we are dereferencing. + * @returns A `fluid.promise` that will be resolved once the parser has finished dereferencing the schema. + * + */ +gpii.schema.parser.dereferenceSchema = function (that, schemaPath, schemaKey) { + var promise = fluid.promise(); + var parser = new $RefParser(); // jshint ignore:line + + parser.dereference(schemaPath, that.options.parserOptions, gpii.schema.parser.getParserCallback(that, schemaKey, promise)); + return promise; +}; + +/** + * + * Wrap the normal callback used by the parser in a `fluid.promise`. + * + * @param that - The parser component itself. + * @param schemaKey - The filename/id of the schema we are dereferencing. + * @param promise - The promise the parser will either resolve or reject once it completes its work. + * @returns A {Function} that can be passed directly to the parser as a callback. + * + */ +gpii.schema.parser.getParserCallback = function (that, schemaKey, promise) { + return function (error, schema) { + if (error) { + promise.reject(error); + } + else { + // Our keys have periods in them, which the change applier will interpret as a deeper part of the path + // unless we escape the key. + var escapedSchemaKey = schemaKey.replace(/\./g, "\\."); + that.applier.change("dereferencedSchemas." + escapedSchemaKey , schema); + promise.resolve(schema); + } + }; +}; + +fluid.defaults("gpii.schema.parser", { + gradeNames: ["fluid.modelComponent", "gpii.hasRequiredOptions"], + requiredFields: { + "schemaDirs": true + }, + parserOptions: {}, + events: { + onSchemasDereferenced: null + }, + model: { + dereferencedSchemas: {} + }, + listeners: { + "onCreate.loadSchemas": { + funcName: "gpii.schema.parser.loadSchemas", + args: ["{that}"] + } + } +}); diff --git a/src/js/server/schemaInlineMiddleware.js b/src/js/server/schemaInlineMiddleware.js new file mode 100644 index 0000000..55bafa2 --- /dev/null +++ b/src/js/server/schemaInlineMiddleware.js @@ -0,0 +1,69 @@ +/* + + "Inline Schema" router that packages up all schemas and returns them in a single JSON bundle, keyed by filename. + + See the documentation for details: + + https://github.com/the-t-in-rtf/gpii-json-schema/blob/master/docs/schemaValidationMiddleware.md + + */ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +require("../common/hasRequiredOptions"); + +fluid.registerNamespace("gpii.schema.inlineMiddleware"); + +/** + * + * Send our map of JSON Schemas to the client. + * + * @param that - The router component. + * @param req {Object} - The Express `request` object: http://expressjs.com/en/api.html#req + * @param res {Object} - The Express `response` object: http://expressjs.com/en/api.html#res + * + */ +gpii.schema.inlineMiddleware.sendSchemaData = function (that, req, res) { + res.status(200).send(that.model.schemas); +}; + +/* + + The `gpii.express.router` that delivers schema data to client side components. + + */ +fluid.defaults("gpii.schema.inlineMiddleware", { + gradeNames: ["gpii.express.middleware", "gpii.hasRequiredOptions", "fluid.modelComponent"], + path: "/allSchemas", + namespace: "allSchemas", // Namespace to allow other routers to put themselves in the chain before or after us. + events: { + onSchemasDereferenced: null + }, + requiredFields: { + "schemaDirs": true + }, + model: { + schemas: "{parser}.model.dereferencedSchemas" + }, + invokers: { + middleware: { + funcName: "gpii.schema.inlineMiddleware.sendSchemaData", + args: ["{that}", "{arguments}.0", "{arguments}.1"] // `request`, `response` + } + }, + components: { + parser: { + type: "gpii.schema.parser", + options: { + schemaDirs: "{gpii.schema.inlineMiddleware}.options.schemaDirs", + listeners: { + "onSchemasDereferenced.notifyRouter": { + func: "{gpii.schema.inlineMiddleware}.events.onSchemasDereferenced.fire" + } + } + } + } + } +}); diff --git a/src/js/server/schemaLinkMiddleware.js b/src/js/server/schemaLinkMiddleware.js new file mode 100644 index 0000000..6586592 --- /dev/null +++ b/src/js/server/schemaLinkMiddleware.js @@ -0,0 +1,82 @@ +/* + + An extension of `gpii.express.middleware` (and `gpii.express.middleware.error`) that adds JSON Schema headers to + the outgoing response. See the component's documentation for more details: + + https://github.com/the-t-in-rtf/gpii-json-schema/blob/master/docs/schemaLinkMiddleware.md + + */ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +require("gpii-express"); +require("../common/hasRequiredOptions"); + +fluid.registerNamespace("gpii.schema.schemaLinkMiddleware"); + +var quotedPrintable = require("quoted-printable"); + +/** + * + * Send the appropriate headers and then let the underlying grade's `sendResponse` function take over. + * + * @param that - The handler component itself. + * @param response {Object} - The Express `response` object: http://expressjs.com/en/api.html#res + * @param statusCode - The numeric HTTP status code. + * @param body {Object} - The response payload to send to the browser. + */ +gpii.schema.schemaLinkMiddleware.addHeaders = function (that, error, request, response, next) { + if (response.headersSent) { + if (error) { + next(error); + + fluid.fail("Could not set headers, they have already been sent to the response. There was already an error, so I couldn't even pass it on!"); + } + else { + next(that.options.errors.headersSent); + } + } + else { + response.type("application/" + quotedPrintable.encode(that.options.schemaKey) + "+json; profile=\"" + that.options.schemaUrl + "\""); + response.set("Link", that.options.schemaUrl + "; rel=\"describedBy\""); + + if (error) { + next(error); + } + else { + next(); + } + } +}; + +fluid.defaults("gpii.schema.schemaLinkMiddleware.base", { + gradeNames: ["gpii.hasRequiredOptions"], + requiredFields: { + "schemaKey": true, + "schemaUrl": true + } +}); + +fluid.defaults("gpii.schema.schemaLinkMiddleware", { + gradeNames: ["gpii.schema.schemaLinkMiddleware.base", "gpii.express.middleware"], + namespace: "schemaLinkMiddleware", + invokers: { + middleware: { + funcName: "gpii.schema.schemaLinkMiddleware.addHeaders", + args: ["{that}", null, "{arguments}.0", "{arguments}.1", "{arguments}.2"] // error, request, response, next + } + } +}); + +fluid.defaults("gpii.schema.schemaLinkMiddleware.error", { + gradeNames: ["gpii.schema.schemaLinkMiddleware.base", "gpii.express.middleware.error"], + namespace: "schemaLinkErrorMiddleware", + invokers: { + middleware: { + funcName: "gpii.schema.schemaLinkMiddleware.addHeaders", + args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2", "{arguments}.3"] // error, request, response, next + } + } +}); diff --git a/src/js/server/schemaValidationMiddleware.js b/src/js/server/schemaValidationMiddleware.js new file mode 100644 index 0000000..dba5889 --- /dev/null +++ b/src/js/server/schemaValidationMiddleware.js @@ -0,0 +1,108 @@ +/* + + "Gatekeeper" middleware that rejects any request whose JSON payloads are not valid. See this component's + documentation for more details: + + https://github.com/the-t-in-rtf/gpii-json-schema/blob/master/docs/schemaValidationMiddleware.md + + */ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +require("../common/hasRequiredOptions"); +fluid.registerNamespace("gpii.schema.validationMiddleware"); + +/** + * + * @param that {Object} The middleware component itself. + * @param req {Object} The Express request object. + * @param res {Object} The Express response object. + * @param next {Function} The function to be executed next in the middleware chain. + */ +gpii.schema.validationMiddleware.rejectOrForward = function (that, req, res, next) { + var toValidate = fluid.model.transformWithRules(req, that.options.rules.requestContentToValidate); + var results = that.validator.validate(that.options.schemaKey, toValidate); + if (results) { + var transformedValidationErrors = fluid.model.transformWithRules(results, that.options.rules.validationErrorsToResponse); + next(transformedValidationErrors); + } + else { + next(); + } +}; + +/* + + The `gpii.express.middleware` that fields invalid responses itself and passes valid ones through to the `next` + Express router or middleware function. Must be combined with either the `requestAware` or `contentAware` grades + to function properly. See the grades below for an example. + + */ +fluid.defaults("gpii.schema.validationMiddleware", { + gradeNames: ["gpii.express.middleware", "gpii.hasRequiredOptions"], + namespace: "validationMiddleware", // A namespace that can be used to order other middleware relative to this component. + requiredFields: { + "rules.requestContentToValidate": true, + "rules.validationErrorsToResponse": true, + schemaKey: true + }, + responseSchemaKey: "message.json", + responseSchemaUrl: "http://terms.raisingthefloor.org/schema/message.json", + messages: { + error: "The JSON you have provided is not valid." + }, + // We prevent merging of individual options, but allow them to be individually replaced. + mergeOptions: { + "rules.validationErrorsToResponse": "nomerge", + "rules.requestContentToValidate": "nomerge" + }, + rules: { + requestContentToValidate: { + "": "body" + }, + validationErrorsToResponse: { + isError: { literalValue: true}, + message: { + literalValue: "{that}.options.messages.error" + }, + fieldErrors: "" + } + }, + events: { + onSchemasDereferenced: null + }, + components: { + validator: { + type: "gpii.schema.validator.ajv.server", + options: { + schemaDirs: "{gpii.schema.validationMiddleware}.options.schemaDirs", + listeners: { + "onSchemasDereferenced.notifyMiddleware": { + func: "{gpii.schema.validationMiddleware}.events.onSchemasDereferenced.fire" + } + } + } + } + }, + invokers: { + middleware: { + funcName: "gpii.schema.validationMiddleware.rejectOrForward", + args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] // request, response, next + } + } +}); + +/* + + A mix-in grade to configure an instance of `gpii.schema.validationMiddleware` to work with query data. + + */ +fluid.defaults("gpii.schema.validationMiddleware.handlesQueryData", { + rules: { + requestContentToValidate: { + "": "query" + } + } +}); diff --git a/src/js/server/validator.js b/src/js/server/validator.js new file mode 100644 index 0000000..dfadd8b --- /dev/null +++ b/src/js/server/validator.js @@ -0,0 +1,47 @@ +/* + + A server-side wrapper for the validation component. The server-side parser loads the schema content used here. See + the documentation for details: + + https://github.com/the-t-in-rtf/gpii-json-schema/blob/master/docs/validator.md + + */ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); + +require("../../../index"); + +fluid.registerNamespace("gpii.schema.validator.ajv.server"); + +fluid.defaults("gpii.schema.validator.ajv.server", { + gradeNames: ["gpii.schema.validator.ajv"], + events: { + onSchemasDereferenced: null, + // Map the common `onSchemasLoaded` event to `onSchemasReferenced`, as the parser loaded them for us. + onSchemasLoaded: { + events: { + onSchemasDereferenced: "onSchemasDereferenced" + } + } + }, + model: { + schemas: {} + }, + components: { + parser: { + type: "gpii.schema.parser", + options: { + schemaDirs: "{gpii.schema.validator.ajv}.options.schemaDirs", + model: { + dereferencedSchemas: "{gpii.schema.validator.ajv.server}.model.schemas" + }, + listeners: { + "onSchemasDereferenced.notifyValidator": { + func: "{gpii.schema.validator.ajv.server}.events.onSchemasDereferenced.fire" + } + } + } + } + } +}); diff --git a/src/templates/pages/validation-error.handlebars b/src/templates/pages/validation-error.handlebars new file mode 100644 index 0000000..22ea6d3 --- /dev/null +++ b/src/templates/pages/validation-error.handlebars @@ -0,0 +1 @@ +{{>validation-error-summary}} \ No newline at end of file diff --git a/src/templates/partials/validation-error-inline.handlebars b/src/templates/partials/validation-error-inline.handlebars new file mode 100644 index 0000000..4cb67ab --- /dev/null +++ b/src/templates/partials/validation-error-inline.handlebars @@ -0,0 +1,3 @@ +
+ {{message}} +
\ No newline at end of file diff --git a/src/templates/partials/validation-error-summary.handlebars b/src/templates/partials/validation-error-summary.handlebars new file mode 100644 index 0000000..41fe5b8 --- /dev/null +++ b/src/templates/partials/validation-error-summary.handlebars @@ -0,0 +1,14 @@ +{{#if message}} +
{{message}}
+{{/if}} +{{#if fieldErrors}} +
+

The information you provided is incomplete or incorrect. Please check the following:

+ +
    + {{#each fieldErrors}} +
  • {{message}}
  • + {{/each}} +
+
+{{/if}} diff --git a/tests/all-tests.js b/tests/all-tests.js new file mode 100644 index 0000000..f770495 --- /dev/null +++ b/tests/all-tests.js @@ -0,0 +1,6 @@ +/* eslint-env node */ +"use strict"; + +// TODO: A browser test fails when run from this file, but not when run on its own. Review with Antranig. +require("./js/server/"); +require("./js/browser"); diff --git a/tests/badSchemas/black-hole.json b/tests/badSchemas/black-hole.json new file mode 100644 index 0000000..2efe324 --- /dev/null +++ b/tests/badSchemas/black-hole.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "base.json", + "title": "Schema with an unresolvable reference", + "type": "object", + "properties": { + "bogus": { "$ref": "bogus.json#/definitions/required" } + } +} \ No newline at end of file diff --git a/tests/js/browser/environment.js b/tests/js/browser/environment.js new file mode 100644 index 0000000..46358ca --- /dev/null +++ b/tests/js/browser/environment.js @@ -0,0 +1,43 @@ +// A test environment for browser + express tests +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +require("../lib/harness"); + +require("gpii-webdriver"); +gpii.webdriver.loadTestingSupport(); + +fluid.defaults("gpii.tests.schema.browser.environment", { + gradeNames: ["gpii.test.webdriver.testEnvironment.withExpress"], + port: 6984, + url: { + expander: { + funcName: "fluid.stringTemplate", + args: ["http://localhost:%port%endpoint", { port: "{that}.options.port", endpoint: "{that}.options.endpoint" }] + } + }, + events: { + onHarnessReady: null, + onFixturesConstructed: { + events: { + onDriverReady: "onDriverReady", + onExpressReady: "onHarnessReady" + } + } + }, + components: { + express: { + type: "gpii.test.schema.harness", + options: { + port: "{testEnvironment}.options.port", + listeners: { + "onAllReady.notifyEnvironment": { + func: "{testEnvironment}.events.onHarnessReady.fire" + } + } + } + } + } +}); diff --git a/tests/js/browser/errorBinder-client-tests.js b/tests/js/browser/errorBinder-client-tests.js new file mode 100644 index 0000000..bf87a7b --- /dev/null +++ b/tests/js/browser/errorBinder-client-tests.js @@ -0,0 +1,389 @@ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +fluid.setLogging(true); +var gpii = fluid.registerNamespace("gpii"); + +require("./environment"); + +fluid.registerNamespace("gpii.tests.schema.errorBinder"); + +fluid.defaults("gpii.tests.schema.errorBinder.caseHolder", { + gradeNames: ["gpii.test.webdriver.caseHolder"], + // All of our tests follow the same pattern, start everything, then open a page. + sequenceStart: [ + { + func: "{testEnvironment}.events.constructFixtures.fire" + }, + { + event: "{testEnvironment}.events.onFixturesConstructed", + listener: "fluid.identity" + }, + // These must be separate otherwise the framework will complain that the browser might not exist yet. (Even though it should). + { + func: "{testEnvironment}.webdriver.get", + args: ["{testEnvironment}.options.url"] + }, + { + event: "{testEnvironment}.webdriver.events.onGetComplete", + listener: "{testEnvironment}.webdriver.wait", + args: [gpii.webdriver.until.elementLocated({css: ".fieldError"})] + }, + { + event: "{testEnvironment}.webdriver.events.onWaitComplete", + listener: "fluid.identity" + } + ], + rawModules: [{ + name: "Testing the client-side error binder component...", + tests: [ + { + name: "Confirm that initial client-side validation errors appear correctly after startup...", + sequence: [ + { + func: "{testEnvironment}.webdriver.findElement", + args: [{ css: ".errorBinder-clientSideValidation-viewport .fieldErrors"}] + }, + { + listener: "gpii.test.webdriver.inspectElement", + event: "{testEnvironment}.webdriver.events.onFindElementComplete", + args: ["The error summary should be as expected...", "{arguments}.0", "getText", "The information you provided is incomplete or incorrect. Please check the following:\nThe 'shallowlyRequired' field is required."] // message, element, elementFn, expectedValue, jqUnitFn + }, + { + func: "{testEnvironment}.webdriver.findElement", + args: [{ css: ".errorBinder-clientSideValidation-viewport .shallowlyRequired-block .fieldError"}] + }, + { + listener: "gpii.test.webdriver.inspectElement", + event: "{testEnvironment}.webdriver.events.onFindElementComplete", + args: ["An inline error should be as expected...", "{arguments}.0", "getText", "The 'shallowlyRequired' field is required."] // message, element, elementFn, expectedValue, jqUnitFn + } + ] + }, + { + name: "Confirm that feedback on a required field is set and unset as needed...", + sequence: [ + { + func: "{testEnvironment}.webdriver.findElement", + args: [gpii.webdriver.By.css(".errorBinder-clientSideValidation-viewport input[name='shallowlyRequired']")] + }, + { + event: "{testEnvironment}.webdriver.events.onFindElementComplete", + listener: "{testEnvironment}.webdriver.actionsHelper", + args: [[ + { fn: "click", args: ["{arguments}.0"]}, + { fn: "sendKeys", args: ["There is text now.", gpii.webdriver.Key.TAB]} + ]] + }, + { + event: "{testEnvironment}.webdriver.events.onActionsHelperComplete", + listener: "{testEnvironment}.webdriver.findElement", + args: [gpii.webdriver.By.css(".errorBinder-clientSideValidation-viewport .fieldErrors")] + }, + { + listener: "jqUnit.assert", + event: "{testEnvironment}.webdriver.events.onError", + args: ["There should no longer be an error summary..."] + }, + { + func: "{testEnvironment}.webdriver.findElement", + args: [gpii.webdriver.By.css(".errorBinder-clientSideValidation-viewport .fieldError")] + }, + { + listener: "jqUnit.assert", + event: "{testEnvironment}.webdriver.events.onError", + args: ["There should no longer be any field-level errors..."] + }, + { + func: "{testEnvironment}.webdriver.findElement", + args: [gpii.webdriver.By.css(".errorBinder-clientSideValidation-viewport input[name='shallowlyRequired']")] + }, + { + event: "{testEnvironment}.webdriver.events.onFindElementComplete", + listener: "{testEnvironment}.webdriver.actionsHelper", + args: [[ + { fn: "click", args: ["{arguments}.0"]}, + { fn: "sendKeys", args: fluid.generate(20, gpii.webdriver.Key.ARROW_RIGHT) }, + { fn: "sendKeys", args: fluid.generate(20, gpii.webdriver.Key.BACK_SPACE) }, + { fn: "sendKeys", args: [gpii.webdriver.Key.TAB]} + ]] + }, + { + event: "{testEnvironment}.webdriver.events.onActionsHelperComplete", + listener: "{testEnvironment}.webdriver.findElement", + args: [gpii.webdriver.By.css(".errorBinder-clientSideValidation-viewport .fieldErrors")] + }, + { + listener: "jqUnit.assert", + event: "{testEnvironment}.webdriver.events.onFindElementComplete", + args: ["There should now be an error summary..."] + }, + { + func: "{testEnvironment}.webdriver.findElement", + args: [gpii.webdriver.By.css(".errorBinder-clientSideValidation-viewport .fieldError")] + }, + { + listener: "jqUnit.assert", + event: "{testEnvironment}.webdriver.events.onFindElementComplete", + args: ["There should now be field-level errors..."] + } + ] + }, + { + name: "Confirm that multiple errors can be set and cleared in real time...", + sequence: [ + { + func: "{testEnvironment}.webdriver.findElement", + args: [gpii.webdriver.By.css(".errorBinder-clientSideValidation-viewport input[name='testAllOf']")] + }, + { + event: "{testEnvironment}.webdriver.events.onFindElementComplete", + listener: "{testEnvironment}.webdriver.actionsHelper", + args: [[ + { fn: "click", args: ["{arguments}.0"]}, + { fn: "sendKeys", args: ["CAT", gpii.webdriver.Key.TAB]} + ]] + }, + { + event: "{testEnvironment}.webdriver.events.onActionsHelperComplete", + listener: "{testEnvironment}.webdriver.findElements", + args: [gpii.webdriver.By.css(".fieldError")] + }, + { + listener: "jqUnit.assertEquals", + event: "{testEnvironment}.webdriver.events.onFindElementsComplete", + args: ["There should now be two field errors...", 2, "{arguments}.0.length"] + }, + { + func: "{testEnvironment}.webdriver.findElement", + args: [gpii.webdriver.By.css(".errorBinder-clientSideValidation-viewport input[name='shallowlyRequired']")] + }, + { + event: "{testEnvironment}.webdriver.events.onFindElementComplete", + listener: "{testEnvironment}.webdriver.actionsHelper", + args: [[ + { fn: "click", args: ["{arguments}.0"]}, + { fn: "sendKeys", args: ["There is text now.", gpii.webdriver.Key.TAB]} + ]] + }, + { + func: "{testEnvironment}.webdriver.findElement", + args: [gpii.webdriver.By.css(".errorBinder-clientSideValidation-viewport input[name='testAllOf']")] + }, + { + event: "{testEnvironment}.webdriver.events.onFindElementComplete", + listener: "{testEnvironment}.webdriver.actionsHelper", + args: [[ + { fn: "click", args: ["{arguments}.0"]}, + { fn: "sendKeys", args: [" NAP", gpii.webdriver.Key.TAB]} + ]] + }, + { + event: "{testEnvironment}.webdriver.events.onActionsHelperComplete", + listener: "{testEnvironment}.webdriver.findElements", + args: [gpii.webdriver.By.css(".fieldError")] + }, + { + listener: "jqUnit.assertEquals", + event: "{testEnvironment}.webdriver.events.onFindElementsComplete", + args: ["There should now be no field errors...", 0, "{arguments}.0.length"] + } + ] + }, + { + name: "Confirm that form submission is prevented if there are validation errors...", + sequence: [ + { + func: "{testEnvironment}.webdriver.findElement", + args: [gpii.webdriver.By.css(".errorBinder-clientSideValidation-viewport .submit")] + }, + { + event: "{testEnvironment}.webdriver.events.onFindElementComplete", + listener: "{testEnvironment}.webdriver.actionsHelper", + args: [[{ fn: "click", args: ["{arguments}.0"]}]] + }, + { + event: "{testEnvironment}.webdriver.events.onActionsHelperComplete", + listener: "{testEnvironment}.webdriver.findElements", + args: [gpii.webdriver.By.css(".errorBinder-clientSideValidation-viewport .fieldError")] + }, + { + listener: "jqUnit.assertEquals", + event: "{testEnvironment}.webdriver.events.onFindElementsComplete", + args: ["There should still be 1 field error...", 1, "{arguments}.0.length"] + }, + { + func: "{testEnvironment}.webdriver.findElements", + args: [gpii.webdriver.By.css(".errorBinder-clientSideValidation-viewport .templateFormControl-success .callout")] + }, + { + listener: "jqUnit.assertEquals", + event: "{testEnvironment}.webdriver.events.onFindElementsComplete", + args: ["There should still be no new success message...", 0, "{arguments}.0.length"] + }, + { + func: "{testEnvironment}.webdriver.findElements", + args: [gpii.webdriver.By.css(".errorBinder-clientSideValidation-viewport .templateFormControl-error .callout")] + }, + { + listener: "jqUnit.assertEquals", + event: "{testEnvironment}.webdriver.events.onFindElementsComplete", + args: ["There should still be only one top level error message...", 1, "{arguments}.0.length"] + } + ] + }, + { + name: "Confirm that underlying server-side messages are displayed correctly...", + sequence: [ + { + func: "{testEnvironment}.webdriver.wait", + args: [gpii.webdriver.until.elementLocated({css: ".errorBinder-clientSideValidation-viewport .callout.alert"})] + }, + { + event: "{testEnvironment}.webdriver.events.onWaitComplete", + listener: "{testEnvironment}.webdriver.findElement", + args: [gpii.webdriver.By.css(".errorBinder-clientSideValidation-viewport input[name='shallowlyRequired']")] + }, + { + event: "{testEnvironment}.webdriver.events.onFindElementComplete", + listener: "{testEnvironment}.webdriver.actionsHelper", + args: [[ + { fn: "click", args: ["{arguments}.0"]}, + { fn: "sendKeys", args: ["Here is some text.", gpii.webdriver.Key.ENTER]} + ]] + }, + { + event: "{testEnvironment}.webdriver.events.onActionsHelperComplete", + listener: "{testEnvironment}.webdriver.findElement", + args: [gpii.webdriver.By.css(".errorBinder-clientSideValidation-viewport input[name='succeed']")] + }, + { + event: "{testEnvironment}.webdriver.events.onFindElementComplete", + listener: "{testEnvironment}.webdriver.actionsHelper", + args: [[{ fn: "click", args: ["{arguments}.0"]}]] + }, + { + event: "{testEnvironment}.webdriver.events.onActionsHelperComplete", + listener: "{testEnvironment}.webdriver.findElement", + args: [gpii.webdriver.By.css(".errorBinder-clientSideValidation-viewport .submit")] + }, + { + event: "{testEnvironment}.webdriver.events.onFindElementComplete", + listener: "{testEnvironment}.webdriver.actionsHelper", + args: [[{ fn: "click", args: ["{arguments}.0"]}]] + }, + { + event: "{testEnvironment}.webdriver.events.onActionsHelperComplete", + listener: "{testEnvironment}.webdriver.wait", + args: [gpii.webdriver.until.elementLocated({ css: ".errorBinder-clientSideValidation-viewport .templateFormControl-error .callout"})] + }, + { + listener: "jqUnit.assert", + event: "{testEnvironment}.webdriver.events.onWaitComplete", + args: ["There should now be an error message..."] + }, + { + func: "{testEnvironment}.webdriver.findElements", + args: [gpii.webdriver.By.css(".errorBinder-clientSideValidation-viewport .templateFormControl-success .callout")] + }, + { + listener: "jqUnit.assertEquals", + event: "{testEnvironment}.webdriver.events.onFindElementsComplete", + args: ["There should no longer be a success message...", 0, "{arguments}.0.length"] + }, + { + func: "{testEnvironment}.webdriver.findElement", + args: [gpii.webdriver.By.css(".errorBinder-clientSideValidation-viewport input[name='succeed']")] + }, + { + event: "{testEnvironment}.webdriver.events.onFindElementComplete", + listener: "{testEnvironment}.webdriver.actionsHelper", + args: [[{ fn: "click", args: ["{arguments}.0"]}]] + }, + { + event: "{testEnvironment}.webdriver.events.onActionsHelperComplete", + listener: "{testEnvironment}.webdriver.findElement", + args: [gpii.webdriver.By.css(".errorBinder-clientSideValidation-viewport .submit")] + }, + { + event: "{testEnvironment}.webdriver.events.onFindElementComplete", + listener: "{testEnvironment}.webdriver.actionsHelper", + args: [[{ fn: "click", args: ["{arguments}.0"]}]] + }, + { + event: "{testEnvironment}.webdriver.events.onActionsHelperComplete", + listener: "{testEnvironment}.webdriver.wait", + args: [gpii.webdriver.until.elementLocated({ css: ".errorBinder-clientSideValidation-viewport .success.callout"})] + }, + { + listener: "jqUnit.assert", + event: "{testEnvironment}.webdriver.events.onWaitComplete", + args: ["There should be a success message after the second submission..."] + }, + { + func: "{testEnvironment}.webdriver.findElements", + args: [gpii.webdriver.By.css(".errorBinder-clientSideValidation-viewport .templateFormControl-error .callout")] + }, + { + listener: "jqUnit.assertEquals", + event: "{testEnvironment}.webdriver.events.onFindElementsComplete", + args: ["There should no longer be an error message after the second submission...", 0, "{arguments}.0.length"] + } + ] + }, + { + name: "Confirm that server-side validation errors are displayed correctly...", + sequence: [ + { + func: "{testEnvironment}.webdriver.findElement", + args: [gpii.webdriver.By.css(".errorBinder-viewport input[name='testAllOf']")] + }, + { + event: "{testEnvironment}.webdriver.events.onFindElementComplete", + listener: "{testEnvironment}.webdriver.actionsHelper", + args: [[ + { fn: "click", args: ["{arguments}.0"]}, + { fn: "sendKeys", args: ["CAT", gpii.webdriver.Key.ENTER]} + ]] + }, + { + event: "{testEnvironment}.webdriver.events.onActionsHelperComplete", + listener: "{testEnvironment}.webdriver.wait", + args: [gpii.webdriver.until.elementLocated({ css: ".errorBinder-viewport .callout.alert"})] + }, + { + event: "{testEnvironment}.webdriver.events.onWaitComplete", + listener: "{testEnvironment}.webdriver.findElements", + args: [gpii.webdriver.By.css(".errorBinder-viewport .templateFormControl-error > .callout.alert")] + }, + { + listener: "jqUnit.assertEquals", + event: "{testEnvironment}.webdriver.events.onFindElementsComplete", + args: ["There should be an error summary and the server's `message` as well...", 2, "{arguments}.0.length"] + }, + { + func: "{testEnvironment}.webdriver.findElements", + args: [gpii.webdriver.By.css(".errorBinder-viewport .fieldError")] + }, + { + listener: "jqUnit.assertEquals", + event: "{testEnvironment}.webdriver.events.onFindElementsComplete", + args: ["There should be two field-level errors...", 2, "{arguments}.0.length"] + } + ] + } + ] + }] +}); + +fluid.defaults("gpii.tests.schema.errorBinder.environment", { + gradeNames: ["gpii.tests.schema.browser.environment"], + port: 6984, + endpoint: "/content/errorBinder-tests.html", + components: { + caseHolder: { + type: "gpii.tests.schema.errorBinder.caseHolder" + } + } +}); +gpii.test.webdriver.allBrowsers({ baseTestEnvironment: "gpii.tests.schema.errorBinder.environment"}); diff --git a/tests/js/browser/index.js b/tests/js/browser/index.js new file mode 100644 index 0000000..c3a5150 --- /dev/null +++ b/tests/js/browser/index.js @@ -0,0 +1,4 @@ +/* eslint-env node */ +"use strict"; +require("./validate-client-tests"); +require("./errorBinder-client-tests"); diff --git a/tests/js/browser/validate-client-tests.js b/tests/js/browser/validate-client-tests.js new file mode 100644 index 0000000..dc1d4cf --- /dev/null +++ b/tests/js/browser/validate-client-tests.js @@ -0,0 +1,167 @@ +/* eslint-env node */ +"use strict"; +// Test "validator" components using browser and filesystem content. +// +// browser provides two means of interrogating globals within the browser, namely: +// +// 1. `browser.assert.global(name, expected, message)`, which tests the global with `name` to confirm that it matches +// `expected`, and throws `message` if it does not. +// +// 2. `browser.window[fieldName]` provides direct access to the global variable `fieldName`. +// +// We will use the latter in these tests and using `jqUnit` assertions. The page itself will use jQuery to manipulate +// all controls, browser is only inspecting the final results. +// +// We have to load this via a `gpii.express` instance because file URLs don't work on windows: +// +// https://github.com/assaf/browser/issues/915 + +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +require("../lib/errors"); +require("../lib/harness"); +require("./environment"); +require("../common/validate-common-test-definitions"); + +require("../../../"); + +// var jqUnit = require("node-jqunit"); +// jqUnit.asyncTest("Waiting for stuff to start....", function () { +// setTimeout(function () { jqUnit.start(); jqUnit.assert("Stuff has started"); }, 1000); +// }); + +fluid.registerNamespace("gpii.tests.schema.validator.browser"); + +// A client-side function to submit JSON content to the client-side component via `gpii-test-browser` and return the output. +// +// Expects to be passed: +// +// 1. `validatorPath`: The namespaced path to the test component. +// 2. `schema`: The id of the schema to validate against. +// 3. `content`: The JSON content to validate. +// +// Returns the output in JSON format. +// +gpii.tests.schema.validator.browser.validateContent = function (schema, content) { + /* globals clientValidator */ + return clientValidator.validate(schema, content); +}; + +// A function to wire up a series of test sequences based on our "common" (to this package) test format: +// +// emptyDerived: { +// message: "Validate an empty 'derived' record....", +// schema: "derived.json", +// content: {}, +// errors: true, +// errorPaths: [".required", ".deeply.nested.additionalRequired"] +// } +// +// This would result in a test sequence that loads a page and then evaluates various statements like: +// +// { +// name: "Validate an empty 'derived' record...", +// sequence: [ +// { +// func: "{gpii.handlebars.tests.browser.environment}.browser.goto", +// args: ["{gpii.handlebars.tests.browser.environment}.options.url"] +// }, +// { +// event: "{gpii.handlebars.tests.browser.environment}.browser.events.onLoaded", +// listener: "{gpii.handlebars.tests.browser.environment}.browser.evaluate", +// args: [gpii.tests.schema.validator.browser.validateContent, "derived.json", {}] +// }, +// { +// event: "{gpii.handlebars.tests.browser.environment}.browser.events.onEvaluateComplete", +// listener: "gpii.test.schema.hasFieldErrors", +// args: ["The correct errors should be returned...", [".required", ".deeply.nested.additionalRequired"]] +// }, +// ] +// } +// +// The checks for multiples are similar, but add a final `true` argument to the call to +// `gpii.test.schema.hasFieldErrors`. +// +// The checks for tests that do not have errors would look even simpler, as we are expecting `undefined` to be the +// output and can simply use `jqUnit.assertUndefined` to test that.. +// +// The generated sequences are eventually run through the normal process that the `addRequiredSequences` function +// provided by `gpii.express` uses. Thus, you can use the same `sequenceStart` and `sequenceEnd` options. +// +gpii.tests.schema.validator.browser.constructTestSequences = function (that) { + var generatedTests = []; + + // iterate through the test definitions and generate sequences as outlined above. + fluid.each(that.options.commonTests, function (testDefinition) { + var hasMultiples = Boolean(testDefinition.multipleErrorPaths); + var sequence = [ + { + func: "{testEnvironment}.webdriver.get", + args: ["{testEnvironment}.options.url"] + }, + // TODO: Listen for the client-side component's `onTemplatesLoaded` event. + { + event: "{testEnvironment}.webdriver.events.onGetComplete", + listener: "{testEnvironment}.webdriver.sleep", + args: [500] + }, + { + event: "{testEnvironment}.webdriver.events.onSleepComplete", + listener: "{testEnvironment}.webdriver.executeScript", + args: [gpii.tests.schema.validator.browser.validateContent, testDefinition.schema, testDefinition.content] + } + ]; + + if (testDefinition.errors) { + var errorPaths = hasMultiples ? testDefinition.multipleErrorPaths : testDefinition.errorPaths; + + sequence.push({ + event: "{testEnvironment}.webdriver.events.onExecuteScriptComplete", + listener: "gpii.test.schema.hasFieldErrors", + args: ["{arguments}.0", errorPaths, hasMultiples] + }); + } + else { + sequence.push({ + event: "{testEnvironment}.webdriver.events.onExecuteScriptComplete", + listener: "jqUnit.assertNull", + args: ["There should be no errors...", "{arguments}.0"] + }); + } + var generatedTest = { + name: testDefinition.message, + sequence: sequence + }; + generatedTests.push(generatedTest); + }); + + // Finish off each sequence by running it through the `gpii.express` function that prepends and appends sequence steps. + var modulesWithStartAndEnd = gpii.test.express.helpers.addRequiredSequences([{ name: "Testing client-side validation...", tests: generatedTests }], that.options.sequenceStart, that.options.sequenceEnd); + var transformedSequences = fluid.transform(modulesWithStartAndEnd, that.prepareModule); + return transformedSequences; +}; + + +// Use the standard `gpii-test-browser` caseHolder, but use a more complex function to rehydrate the "common" tests +// before wiring in the standard start and end sequence steps. +fluid.defaults("gpii.tests.schema.validator.browser.caseHolder", { + gradeNames: ["gpii.test.webdriver.caseHolder", "gpii.test.schema.validator.hasDehydratedTests"], + moduleSource: { + funcName: "gpii.tests.schema.validator.browser.constructTestSequences", + args: ["{that}"] + } +}); + +fluid.defaults("gpii.tests.schema.validator.browser.environment", { + gradeNames: ["gpii.tests.schema.browser.environment"], + port: 6985, + endpoint: "/content/validate-client-tests.html", + components: { + caseHolder: { + type: "gpii.tests.schema.validator.browser.caseHolder" + } + } +}); + +gpii.test.webdriver.allBrowsers({ baseTestEnvironment: "gpii.tests.schema.validator.browser.environment"}); diff --git a/tests/js/common/validate-common-test-definitions.js b/tests/js/common/validate-common-test-definitions.js new file mode 100644 index 0000000..59b62b5 --- /dev/null +++ b/tests/js/common/validate-common-test-definitions.js @@ -0,0 +1,88 @@ +// Tests to be used with the validator on both the client and server side. This grade cannot be used by itself. You +// must mix it in with an existing test grade. See `validate-server-tests.js` for an example. +// +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); + +// A convenience grade that holds our "dehydrated" test definitions. +fluid.defaults("gpii.test.schema.validator.hasDehydratedTests", { + commonTests: { + validBase: { + message: "Validate the simplest valid 'base' record....", + schema: "base.json", + content: { required: true }, + errors: false + }, + validDerived: { + message: "Validate the simplest valid 'derived' record....", + schema: "derived.json", + content: { required: true, additionalRequired: true }, + errors: false + }, + emptyBase: { + message: "Validate an empty 'base' record....", + schema: "base.json", + content: {}, + errors: true, + errorPaths: ["#/required"] + }, + emptyDerived: { + message: "Validate an empty 'derived' record....", + schema: "derived.json", + content: {}, + errors: true, + multipleErrorPaths: ["#/required"] + }, + deeplyInvalid: { + message: "Test handling of 'deep' validation error....", + schema: "deep.json", + content: { deep: {}}, + errors: true, + errorPaths: ["#/properties/deep/required"] + }, + invalidBase: { + message: "Validate an invalid 'base' record....", + schema: "base.json", + content: { required: "bogus"}, + errors: true, + errorPaths: ["#/properties/required/type"] + }, + invalidDerived: { + message: "Validate an invalid 'derived' record....", + schema: "derived.json", + content: { required: "bogus", additionalRequired: "also bogus" }, + errors: true, + errorPaths: ["#/properties/required/type", "#/properties/additionalRequired/type"] + }, + badRawMultiple: { + message: "Validate a field that fails multiple rules (raw output)...", + schema: "base.json", + content: { required: true, rawMultiple: "bogus" }, + errors: true, + errorPaths: ["#/properties/rawMultiple/allOf/0/minLength", "#/properties/rawMultiple/allOf/1/pattern", "#/properties/rawMultiple/allOf/3/pattern"] + }, + goodMultiple: { + message: "Validate a field that passes multiple rules...", + schema: "base.json", + content: { required: true, password: "Password1" }, + errors: false + }, + // In theory any validation error should be caught using already tested methods, but we want to break things + // in a range of ways to check. + complexFraud: { + message: "Test a variety of invalid field types and formats...", + schema: "base.json", + content: { + required: true, + number: "bogus", + date: "bogus", + "boolean": "bogus", + array: "bogus", + regex: "bogus" + }, + errors: true, + errorPaths: ["#/properties/number/type", "#/properties/date/format", "#/properties/array/type", "#/properties/boolean/type", "#/properties/regex/pattern"] + } + } +}); diff --git a/tests/js/launch-test-harness.js b/tests/js/launch-test-harness.js new file mode 100644 index 0000000..ef738b3 --- /dev/null +++ b/tests/js/launch-test-harness.js @@ -0,0 +1,14 @@ +/* eslint-env node */ +// Launch the test harness as a standalone server to assist in browser debugging. +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +fluid.setLogging(true); + +require("./lib/harness"); + +gpii.test.schema.harness({ + "expressPort" : 6904, + "baseUrl": "http://localhost:6904/" +}); diff --git a/tests/js/lib/checkResponseHeaders.js b/tests/js/lib/checkResponseHeaders.js new file mode 100644 index 0000000..ed1f995 --- /dev/null +++ b/tests/js/lib/checkResponseHeaders.js @@ -0,0 +1,25 @@ +/* + + Static function to check responses for the standard headers. + + */ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); +var jqUnit = require("node-jqunit"); + +fluid.registerNamespace("gpii.test.schema"); +gpii.test.schema.checkResponseHeaders = function (response, body, typePattern, linkPattern) { + if (typePattern) { + var contentType = response.headers["content-type"]; + jqUnit.assertTrue("The content type header should contain our key...", contentType && contentType.match(typePattern)); + } + + if (linkPattern) { + var link = response.headers.link; + jqUnit.assertTrue("The link header should contain our key...", link && link.match(linkPattern) !== -1); + } + + jqUnit.assertNotUndefined("There should be body content", body); +}; diff --git a/tests/js/lib/errors.js b/tests/js/lib/errors.js new file mode 100644 index 0000000..4217d19 --- /dev/null +++ b/tests/js/lib/errors.js @@ -0,0 +1,34 @@ +// Simple function to examine validation errors. +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +var jqUnit = require("node-jqunit"); + +require("../../../"); + +fluid.registerNamespace("gpii.test.schema"); + +// Inspects a response body (`results`) looking for errors that match the paths specified in `fieldPaths` (dot notation paths within `fieldErrors`. If `multiple` +// is specified, there should be more than one error at each `fieldPath`. +gpii.test.schema.hasFieldErrors = function (results, fieldPointers, multiple) { + if (fieldPointers) { + var errorsFound = {}; + fluid.each(results, function (error) { + errorsFound[error.schemaPath] = !isNaN(errorsFound[error.schemaPath]) ? errorsFound[error.schemaPath]++ : 1; + }); + + fluid.each(fieldPointers, function (pointer) { + if (multiple) { + jqUnit.assertTrue("There should be multiple errors for field '" + pointer + "'...", errorsFound[pointer] >= 1); + } + else { + jqUnit.assertEquals("There should be a single error for field '" + pointer + "'...", 1, errorsFound[pointer]); + } + }); + } + else { + jqUnit.assertTrue("There should be field errors...", results.fieldErrors && results.fieldErrors.length > 0); + } +}; diff --git a/tests/js/lib/fixtures.js b/tests/js/lib/fixtures.js new file mode 100644 index 0000000..724c35d --- /dev/null +++ b/tests/js/lib/fixtures.js @@ -0,0 +1,61 @@ +/* + Test fixtures for use in a range of tests. + */ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +var jqUnit = require("node-jqunit"); + +require("../../../"); +require("gpii-express"); +gpii.express.loadTestingSupport(); + +require("gpii-handlebars"); +var kettle = require("kettle"); +kettle.loadTestingSupport(); + +require("./harness"); + +fluid.registerNamespace("gpii.test.schema"); +gpii.test.schema.checkHtmlResponse = function (message, expected, body) { + jqUnit.assertTrue(message, body.indexOf(expected) !== -1); +}; + +// A testEnvironment with the standard harness wired in. +fluid.defaults("gpii.test.schema.testEnvironment", { + gradeNames: ["gpii.test.express.testEnvironment"], + events: { + onHarnessReady: null, + onFixturesConstructed: { + events: { + onHarnessReady: "onHarnessReady" + } + } + }, + components: { + express: { + type: "gpii.test.schema.harness", + options: { + listeners: { + "onAllReady.notifyEnvironment": { + func: "{testEnvironment}.events.onHarnessReady.fire" + } + } + } + } + } +}); + +// A wrapper for `kettle.request.http` designed for use with the above `testEnvironment`. +fluid.defaults("gpii.test.schema.request", { + gradeNames: ["kettle.test.request.http"], + port: "{testEnvironment}.options.port", + path: { + expander: { + funcName: "fluid.stringTemplate", + args: ["http://localhost:%port/%endpoint", { port: "{testEnvironment}.options.port", endpoint: "{that}.options.endpoint"}] + } + } +}); diff --git a/tests/js/lib/harness.js b/tests/js/lib/harness.js new file mode 100644 index 0000000..e2cfb57 --- /dev/null +++ b/tests/js/lib/harness.js @@ -0,0 +1,186 @@ +/* + Test harness common to all tests that use `gpii-express`. Loads all required server-side components. + */ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); + +require("../../../"); +require("gpii-express"); + +require("gpii-handlebars"); + +require("./middleware-fixtures.js"); + +fluid.defaults("gpii.test.schema.harness", { + gradeNames: ["gpii.express"], + port: 6194, + events: { + onInlineRouterReady: null, + onGatedContentAwareRouterReady: null, + onGatedRequestAwareRouterReady: null, + onAllReady: { + events: { + "onStarted": "onStarted", + "onInlineRouterReady": "onInlineRouterReady", + "onGatedContentAwareRouterReady": "onGatedContentAwareRouterReady", + "onGatedRequestAwareRouterReady": "onGatedRequestAwareRouterReady" + } + } + }, + baseUrl: { + expander: { + funcName: "fluid.stringTemplate", + args: ["http://localhost:%port/", { port: "{that}.options.port" }] + } + }, + config: { + express: { + "port" : "{that}.options.port", + baseUrl: "{that}.options.url" + } + }, + components: { + json: { + type: "gpii.express.middleware.bodyparser.json", + options: { + priority: "first" + } + }, + urlencoded: { + type: "gpii.express.middleware.bodyparser.urlencoded", + options: { + priority: "after:json" + } + }, + handlebars: { + type: "gpii.express.hb", + options: { + priority: "after:urlencoded", + templateDirs: ["%gpii-json-schema/tests/templates", "%gpii-json-schema/src/templates"] + } + }, + gated: { + type: "gpii.test.schema.middleware.router", + options: { + priority: "after:handlebars", + listeners: { + "onSchemasDereferenced.notifyEnvironment": { + func: "{gpii.test.schema.harness}.events.onGatedRequestAwareRouterReady.fire" + } + } + } + }, + gatedContentAware: { + type: "gpii.test.schema.contentAware", + options: { + namespace: "gatedContentAware", + priority: "after:gated", + listeners: { + "onSchemasDereferenced.notifyEnvironment": { + func: "{gpii.test.schema.harness}.events.onGatedContentAwareRouterReady.fire" + } + } + } + }, + build: { + type: "gpii.express.router.static", + options: { + namespace: "build", + path: "/build", + content: "%gpii-json-schema/build" + } + }, + js: { + type: "gpii.express.router.static", + options: { + path: "/src", + content: "%gpii-json-schema/src" + } + }, + modules: { + type: "gpii.express.router.static", + options: { + path: "/modules", + content: "%gpii-json-schema/node_modules" + } + }, + content: { + type: "gpii.express.router.static", + options: { + path: "/content", + content: "%gpii-json-schema/tests/static" + } + }, + inline: { + type: "gpii.handlebars.inlineTemplateBundlingMiddleware", + options: { + path: "/hbs", + templateDirs: ["%gpii-json-schema/src/templates", "%gpii-json-schema/tests/templates"] + } + }, + inlineSchemas: { + type: "gpii.schema.inlineMiddleware", + options: { + schemaDirs: "%gpii-json-schema/tests/schemas", + listeners: { + "onSchemasDereferenced.notifyEnvironment": { + func: "{gpii.test.schema.harness}.events.onInlineRouterReady.fire" + } + } + } + }, + htmlHeaderMiddleware: { + type: "gpii.express.middleware.headerSetter.error", + options: { + priority: "after:gatedContentAware", + namespace: "htmlHeaderMiddleware", + headers: { + contentType: { + fieldName: "Content-Type", + template: "text/html", + dataRules: {} + } + } + } + }, + htmlErrorHandler: { + type: "gpii.handlebars.errorRenderingMiddleware", + options: { + priority: "after:htmlHeaderMiddleware", + namespace: "htmlErrorHandler", + statusCode: 400, + templateKey: "partials/validation-error-summary" + } + }, + jsonHeaderMiddleware: { + type: "gpii.express.middleware.headerSetter.error", + options: { + priority: "after:htmlErrorHandler", + namespace: "jsonHeaderMiddleware", + headers: { + contentType: { + fieldName: "Content-Type", + template: "application/json", + dataRules: {} + } + } + } + }, + validationErrorHeaderMiddleware: { + type: "gpii.schema.schemaLinkMiddleware.error", + options: { + schemaKey: "message.json", + schemaUrl: "http://some.fake.site/schemas/message.json", + priority: "after:jsonHeaderMiddleware" + } + }, + defaultErrorHandler: { + type: "gpii.express.middleware.error", + options: { + priority: "after:validationErrorHeaderMiddleware", + defaultStatusCode: 400 + } + } + } +}); diff --git a/tests/js/lib/middleware-contentAware-fixtures.js b/tests/js/lib/middleware-contentAware-fixtures.js new file mode 100644 index 0000000..33b78ff --- /dev/null +++ b/tests/js/lib/middleware-contentAware-fixtures.js @@ -0,0 +1,93 @@ +// "gated" test middleware (and an underlying test handler that lies beyond the gate). Used in testing the +// `contentAware` wrapper with our `schemaMiddleware`. +// +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +require("gpii-express"); +require("../../../"); + +fluid.registerNamespace("gpii.test.schema.contentAware.error.defaultHandler"); +gpii.test.schema.contentAware.error.defaultHandler.sendSummary = function (that) { + var output = []; + output.push(that.options.validationErrors.message); + fluid.each(that.options.validationErrors.fieldErrors, function (error) { + var label = error.dataPath.length ? error.dataPath.substring(1) : error.params.missingProperty; + output.push(" * " + label + ": " + error.message); + }); + + that.sendResponse(400, output.join("\n")); +}; + +// Convert the JSON output to a string before sending it onward +// +fluid.defaults("gpii.test.schema.contentAware.error.defaultHandler", { + gradeNames: ["gpii.express.handler"], + invokers: { + handleRequest: { + funcName: "gpii.test.schema.contentAware.error.defaultHandler.sendSummary", + args: ["{that}"] + } + } +}); + +fluid.defaults("gpii.test.schema.contentAware.success.defaultHandler", { + gradeNames: ["gpii.express.handler"], + timeout: 1000000, + invokers: { + handleRequest: { + func: "{that}.sendResponse", + args: [200, "I am happy to hear from you."] + } + } +}); + +fluid.defaults("gpii.test.schema.contentAware.success.jsonHandler", { + gradeNames: ["gpii.express.handler"], + invokers: { + handleRequest: { + func: "{that}.sendResponse", + args: [ 200, { ok: true, message: "I am happy to hear from you."} ] + } + } +}); + +fluid.defaults("gpii.test.schema.contentAware", { + gradeNames: ["gpii.express.router"], + path: "/gatedContentAware", + events: { + onSchemasDereferenced: null + }, + components: { + validationMiddleware: { + type: "gpii.schema.validationMiddleware", + options: { + schemaDirs: "%gpii-json-schema/tests/schemas", + schemaKey: "gated.json", + listeners: { + "onSchemasDereferenced.notifyParent": { + func: "{gpii.test.schema.contentAware}.events.onSchemasDereferenced.fire" + } + } + } + }, + contentAwareMiddleware: { + type: "gpii.express.middleware.contentAware", + options: { + priority: "after:validationMiddleware", + handlers: { + "html": { + contentType: "text/html", + handlerGrades: ["gpii.test.schema.contentAware.success.defaultHandler"] + }, + json: { + contentType: "application/json", + handlerGrades: ["gpii.test.schema.contentAware.success.jsonHandler"] + } + } + } + } + } +}); diff --git a/tests/js/lib/middleware-fixtures.js b/tests/js/lib/middleware-fixtures.js new file mode 100644 index 0000000..b4e397b --- /dev/null +++ b/tests/js/lib/middleware-fixtures.js @@ -0,0 +1,4 @@ +/* eslint-env node */ +"use strict"; +require("./middleware-requestAware-fixtures"); +require("./middleware-contentAware-fixtures"); diff --git a/tests/js/lib/middleware-requestAware-fixtures.js b/tests/js/lib/middleware-requestAware-fixtures.js new file mode 100644 index 0000000..8fee155 --- /dev/null +++ b/tests/js/lib/middleware-requestAware-fixtures.js @@ -0,0 +1,180 @@ +// "gated" test middleware (and an underlying test handler that lies beyond the gate). Used in testing the +// `requestAware` wrapper with our `schemaMiddleware`. +// +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +require("gpii-express"); +require("../../../"); + +// A handler which delivers a "success" or "failure" message depending on a single user-supplied flag. +fluid.registerNamespace("gpii.test.schema.middleware.underlyingHandler"); + +gpii.test.schema.middleware.underlyingHandler.handleRequest = function (that) { + // Reuse the validation rules to get a consistent payload across all methods. + var data = fluid.model.transformWithRules(that.options.request, that.options.rules.requestContentToValidate); + // TODO: Simplify this once the binder properly supports checkboxes: https://issues.gpii.net/browse/GPII-1577 + if (data.succeed || data["succeed[]"]) { + that.sendResponse(200, { ok: true, message: that.options.messages.success}); + } + else { + that.sendResponse(400, { ok: false, message: that.options.messages.failure}); + } +}; + +fluid.defaults("gpii.test.schema.middleware.underlyingHandler", { + gradeNames: ["gpii.express.handler"], + rules: "{gpii.schema.validationMiddleware}.options.rules", + invokers: { + handleRequest: { + funcName: "gpii.test.schema.middleware.underlyingHandler.handleRequest", + args: ["{that}"] + } + } +}); + +// A base grade for all the "method" variations on our router. +fluid.defaults("gpii.test.schema.middleware.router.base", { + gradeNames: ["gpii.express.router"], + events: { + onSchemasDereferenced: null + }, + components: { + validationMiddleware: { + type: "gpii.schema.validationMiddleware", + options: { + namespace: "validationMiddleware", + schemaDirs: "%gpii-json-schema/tests/schemas", + schemaKey: "gated.json", + listeners: { + "onSchemasDereferenced.notifyParent": { + func: "{gpii.test.schema.middleware.router.base}.events.onSchemasDereferenced.fire" + } + } + } + }, + requestAwareMiddleware: { + type: "gpii.express.middleware.requestAware", + options: { + priority: "after:validationMiddleware", + method: "use", + handlerGrades: ["gpii.test.schema.middleware.underlyingHandler"], + messages: { + success: { + expander: { + funcName: "fluid.stringTemplate", + args: ["You were able to '%method' content.", { method: "{gpii.test.schema.middleware.router.base}.options.method"}] + } + }, + failure: { + expander: { + funcName: "fluid.stringTemplate", + args: ["You failed to '%method' content.", { method: "{gpii.test.schema.middleware.router.base}.options.method"}] + } + } + }, + distributeOptions: [ + { + source: "{that}.options.messages", + target: "{that gpii.express.handler}.options.messages" + }, + { + source: "{that}.options.rules", + target: "{that gpii.express.handler}.options.rules" + } + ] + } + } + } +}); + +// POST +fluid.defaults("gpii.test.schema.middleware.router.post", { + gradeNames: ["gpii.test.schema.middleware.router.base"], + method: "post", + path: "/POST" +}); + +// PUT +fluid.defaults("gpii.test.schema.middleware.router.put", { + gradeNames: ["gpii.test.schema.middleware.router.base"], + method: "put", + path: "/PUT" +}); + +// GET +fluid.defaults("gpii.test.schema.middleware.router.get", { + gradeNames: ["gpii.test.schema.middleware.router.base"], + method: "get", + path: "/GET", + components: { + validationMiddleware: { + options: { + gradeNames: ["gpii.schema.validationMiddleware.handlesQueryData"] + } + } + } +}); + +// A common container for all of the different "method" variations +fluid.defaults("gpii.test.schema.middleware.router", { + gradeNames: ["gpii.express.router"], + path: "/gated", + method: "use", + events: { + onGetSchemasDereferenced: null, + onPostSchemasDereferenced: null, + onPutSchemasDereferenced: null, + onSchemasDereferenced: { + events: { + onGetSchemasDereferenced: "onGetSchemasDereferenced", + onPostSchemasDereferenced: "onPostSchemasDereferenced", + onPutSchemasDereferenced: "onPutSchemasDereferenced" + } + } + }, + components: { + linkHeaderMiddleware: { + type: "gpii.schema.schemaLinkMiddleware", + options: { + schemaKey: "message.json", + schemaUrl: "http://terms.raisingthefloor.org/schema/message.json" + } + }, + get: { + type: "gpii.test.schema.middleware.router.get", + options: { + priority: "after:schemaLinkMiddleware", + listeners: { + "onSchemasDereferenced.notifyParent": { + func: "{gpii.test.schema.middleware.router}.events.onGetSchemasDereferenced.fire" + } + } + } + }, + post: { + type: "gpii.test.schema.middleware.router.post", + options: { + priority: "after:schemaLinkMiddleware", + listeners: { + "onSchemasDereferenced.notifyParent": { + func: "{gpii.test.schema.middleware.router}.events.onPostSchemasDereferenced.fire" + } + } + } + }, + put: { + type: "gpii.test.schema.middleware.router.put", + options: { + priority: "after:schemaLinkMiddleware", + listeners: { + "onSchemasDereferenced.notifyParent": { + func: "{gpii.test.schema.middleware.router}.events.onPutSchemasDereferenced.fire" + } + } + } + } + } +}); diff --git a/tests/js/server/hasRequiredOptions-tests.js b/tests/js/server/hasRequiredOptions-tests.js new file mode 100644 index 0000000..8877999 --- /dev/null +++ b/tests/js/server/hasRequiredOptions-tests.js @@ -0,0 +1,49 @@ +// Tests for the `hasRequiredOptions` grade that enforces required options. +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +require("../../../"); + +var jqUnit = require("node-jqunit"); + +jqUnit.module("Testing hasRequiredOptions grade..."); + +jqUnit.test("A grade that has all the required options should be created successfully...", function () { + gpii.hasRequiredOptions({ + requiredFields: { + "topLevel": true, + "deep.field": true + }, + topLevel: {}, + deep: { field: {} } + }); + + jqUnit.assert("The component should have been created successfully..."); +}); + +jqUnit.test("A grade with all no required fields should be created successfully...", function () { + gpii.hasRequiredOptions({}); + + jqUnit.assert("The component should have been created successfully..."); +}); + +jqUnit.test("A grade that is missing required options should not be created successfully...", function () { + fluid.failureEvent.addListener(function () { + jqUnit.assert("A component that is missing options should throw an error..."); + }, "jqUnit", "before:fail"); + + try { + gpii.hasRequiredOptions({ + requiredFields: { + "topLevel": true, + "deep.field": true + } + }); + } + catch (e) { + jqUnit.assert("An exception should have been thrown..."); + } +}); + diff --git a/tests/js/server/index.js b/tests/js/server/index.js new file mode 100644 index 0000000..c3b7866 --- /dev/null +++ b/tests/js/server/index.js @@ -0,0 +1,11 @@ +"use strict"; +/* eslint-env node */ +require("./parser-update-failure-tests"); +require("./middleware-contentAware-tests"); +require("./middleware-requestAware-tests"); +require("./parser-tests"); +require("./pointer-function-tests"); +require("./schema-inline-router-tests"); +require("./validate-server-tests"); +require("./validator-evolved-error-tests"); +require("./hasRequiredOptions-tests"); diff --git a/tests/js/server/middleware-contentAware-tests.js b/tests/js/server/middleware-contentAware-tests.js new file mode 100644 index 0000000..7dffef0 --- /dev/null +++ b/tests/js/server/middleware-contentAware-tests.js @@ -0,0 +1,232 @@ +/* + + Tests for the "schema Middleware" that rejects requests with invalid JSON payloads. + +*/ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +require("gpii-express"); +gpii.express.loadTestingSupport(); + +require("../lib/errors"); +require("../lib/checkResponseHeaders"); +require("../lib/fixtures"); + +var kettle = require("kettle"); +kettle.loadTestingSupport(); + +require("../../../"); + +fluid.defaults("gpii.test.schema.middleware.contentAware.request", { + gradeNames: ["kettle.test.request.http"], + path: "/gatedContentAware", + port: "{testEnvironment}.options.port", + method: "POST" +}); + +fluid.defaults("gpii.test.schema.middleware.contentAware.request.html", { + gradeNames: ["gpii.test.schema.middleware.contentAware.request"], + headers: { + "accept": "text/html" + } +}); + +fluid.defaults("gpii.test.schema.middleware.contentAware.request.json", { + gradeNames: ["gpii.test.schema.middleware.contentAware.request"], + headers: { + "accept": "application/json" + } +}); + +// Wire in an instance of kettle.requests.request.http for each test and wire the check to its onError or onSuccess event +fluid.defaults("gpii.test.schema.middleware.contentAware.caseHolder", { + gradeNames: ["gpii.test.express.caseHolder"], + validPayload: { + shallowlyRequired: true, + testString: "CATs", + testAllOf: "CATs" + }, + invalidPayload: { + testString: "CATs", + testAllOf: "CATs" + }, + expected: { + invalidText: "should have required property", + invalidJson: { + "isError": true, + "message": "The JSON you have provided is not valid.", + "fieldErrors": [ + { + "keyword": "required", + "dataPath": "", + "schemaPath": "#/required", + "params": { + "missingProperty": "shallowlyRequired" + }, + "message": "should have required property 'shallowlyRequired'" + } + ] + }, + validText: "I am happy to hear from you.", + validJson: { + ok: true, + "message": "I am happy to hear from you." + } + }, + rawModules: [ + { + name: "Testing validation with 'content aware' middleware...", + tests: [ + { + name: "Testing a valid POST response with no 'Accept' header...", + type: "test", + sequence: [ + { + func: "{validPostNoHeader}.send", + args: ["{that}.options.validPayload"] + }, + { + listener: "jqUnit.assertEquals", + event: "{validPostNoHeader}.events.onComplete", + args: ["We should receive a text message indicating 'success'...", "{that}.options.expected.validText", "{arguments}.0"] + }, + { + func: "jqUnit.assertEquals", + args: ["The status code should indicate that we were successful...", 200, "{validPostNoHeader}.nativeResponse.statusCode"] + } + ] + }, + { + name: "Testing a valid POST response with 'Accept: text/html'...", + type: "test", + sequence: [ + { + func: "{validPostHtmlHeader}.send", + args: ["{that}.options.validPayload"] + }, + { + listener: "jqUnit.assertEquals", + event: "{validPostHtmlHeader}.events.onComplete", + args: ["We should receive a text message indicating 'success'...", "{that}.options.expected.validText", "{arguments}.0"] + }, + { + func: "jqUnit.assertEquals", + args: ["The status code should indicate that we were successful...", 200, "{validPostHtmlHeader}.nativeResponse.statusCode"] + } + ] + }, + { + name: "Testing a valid POST response with 'Accept: application/json'...", + type: "test", + sequence: [ + { + func: "{validPostJsonHeader}.send", + args: ["{that}.options.validPayload"] + }, + { + listener: "jqUnit.assertDeepEq", + event: "{validPostJsonHeader}.events.onComplete", + args: ["We should receive a JSON payload indicating 'success'...", "{that}.options.expected.validJson", "@expand:JSON.parse({arguments}.0)"] + }, + { + func: "jqUnit.assertEquals", + args: ["The status code should indicate that we were successful...", 200, "{validPostJsonHeader}.nativeResponse.statusCode"] + } + ] + }, + { + name: "Testing an invalid POST response with no 'Accept' header...", + type: "test", + sequence: [ + { + func: "{invalidPostNoHeader}.send", + args: ["{that}.options.invalidPayload"] + }, + { + listener: "gpii.test.schema.checkHtmlResponse", + event: "{invalidPostNoHeader}.events.onComplete", + args: ["We should receive a text message indicating 'failure'...", "{that}.options.expected.invalidText", "{arguments}.0"] + }, + { + func: "jqUnit.assertEquals", + args: ["The status code should indicate that we were unsuccessful...", 400, "{invalidPostNoHeader}.nativeResponse.statusCode"] + } + ] + }, + { + name: "Testing an invalid POST response with 'Accept: text/html'...", + type: "test", + sequence: [ + { + func: "{invalidPostHtmlHeader}.send", + args: ["{that}.options.invalidPayload"] + }, + { + listener: "gpii.test.schema.checkHtmlResponse", + event: "{invalidPostHtmlHeader}.events.onComplete", + args: ["We should receive a text message indicating 'failure'...", "{that}.options.expected.invalidText", "{arguments}.0"] + }, + { + func: "jqUnit.assertEquals", + args: ["The status code should indicate that we were unsuccessful...", 400, "{invalidPostHtmlHeader}.nativeResponse.statusCode"] + } + ] + }, + { + name: "Testing an invalid POST response with 'Accept: application/json'...", + type: "test", + sequence: [ + { + func: "{invalidPostJsonHeader}.send", + args: ["{that}.options.invalidPayload"] + }, + { + listener: "jqUnit.assertDeepEq", + event: "{invalidPostJsonHeader}.events.onComplete", + args: ["We should receive a JSON payload indicating 'failure'...", "{that}.options.expected.invalidJson", "@expand:JSON.parse({arguments}.0)"] + }, + { + func: "jqUnit.assertEquals", + args: ["The status code should indicate that we were unsuccessful...", 400, "{invalidPostJsonHeader}.nativeResponse.statusCode"] + } + ] + } + ] + } + ], + components: { + validPostNoHeader: { + type: "gpii.test.schema.middleware.contentAware.request" + }, + validPostHtmlHeader: { + type: "gpii.test.schema.middleware.contentAware.request.html" + }, + validPostJsonHeader: { + type: "gpii.test.schema.middleware.contentAware.request.json" + }, + invalidPostNoHeader: { + type: "gpii.test.schema.middleware.contentAware.request" + }, + invalidPostHtmlHeader: { + type: "gpii.test.schema.middleware.contentAware.request.html" + }, + invalidPostJsonHeader: { + type: "gpii.test.schema.middleware.contentAware.request.json" + } + } +}); + +fluid.defaults("gpii.test.schema.middleware.contentAware.testEnvironment", { + gradeNames: ["gpii.test.schema.testEnvironment"], + port: 7593, + components: { + caseHolder: { + type: "gpii.test.schema.middleware.contentAware.caseHolder" + } + } +}); + +fluid.test.runTests("gpii.test.schema.middleware.contentAware.testEnvironment"); diff --git a/tests/js/server/middleware-requestAware-tests.js b/tests/js/server/middleware-requestAware-tests.js new file mode 100644 index 0000000..0ea933f --- /dev/null +++ b/tests/js/server/middleware-requestAware-tests.js @@ -0,0 +1,263 @@ +/* + + Tests for the "schema Middleware" that rejects requests with invalid JSON payloads. + +*/ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +require("gpii-express"); +gpii.express.loadTestingSupport(); + +require("../lib/errors"); +require("../lib/checkResponseHeaders"); +require("../lib/fixtures"); + +var kettle = require("kettle"); +kettle.loadTestingSupport(); + +require("../../../"); + +fluid.registerNamespace("gpii.tests.schema.middleware.caseHolder"); +gpii.tests.schema.middleware.caseHolder.examineResponse = function (response, body, shouldBeValid) { + + if (shouldBeValid) { + gpii.test.schema.checkResponseHeaders(response, body); + } + else { + gpii.test.schema.checkResponseHeaders(response, body, "message.json", "message.json"); + + try { + var jsonData = typeof body === "string" ? JSON.parse(body) : body; + gpii.test.schema.hasFieldErrors(jsonData); + } + catch (e) { + fluid.fail("There should be no parsing errors:\n" + e); + } + } +}; + +fluid.defaults("gpii.tests.schema.middleware.request", { + gradeNames: ["kettle.test.request.http"], + path: { + expander: { + funcName: "fluid.stringTemplate", + args: ["/gated/%method", { method: "{that}.options.method"}] + } + }, + headers: { + accept: "application/json" + }, + port: "{testEnvironment}.options.port" +}); + +fluid.defaults("gpii.tests.schema.middleware.request.post", { + gradeNames: ["gpii.tests.schema.middleware.request"], + method: "POST" +}); + +fluid.defaults("gpii.tests.schema.middleware.request.get", { + gradeNames: ["gpii.tests.schema.middleware.request"], + method: "GET" +}); + +fluid.defaults("gpii.tests.schema.middleware.request.put", { + gradeNames: ["gpii.tests.schema.middleware.request"], + method: "PUT" +}); + +// Wire in an instance of kettle.requests.request.http for each test and wire the check to its onError or onSuccess event +fluid.defaults("gpii.tests.schema.middleware.caseHolder", { + gradeNames: ["gpii.test.express.caseHolder"], + rawModules: [ + { + name: "Testing the schema validation middleware in combination with the 'request aware' middleware...", + tests: [ + { + name: "Testing a GET request with no body...", + type: "test", + sequence: [ + { + func: "{emptyGetRequest}.send" + }, + { + listener: "gpii.tests.schema.middleware.caseHolder.examineResponse", + event: "{emptyGetRequest}.events.onComplete", + args: ["{emptyGetRequest}.nativeResponse", "{arguments}.0", true] + } + ] + }, + { + name: "Testing a GET request with bad JSON data...", + type: "test", + sequence: [ + { + func: "{badJsonGetRequest}.send", + args: [{}] + }, + { + listener: "gpii.tests.schema.middleware.caseHolder.examineResponse", + event: "{badJsonGetRequest}.events.onComplete", + args: ["{badJsonGetRequest}.nativeResponse", "{arguments}.0", false] + } + ] + }, + { + name: "Testing a GET request with valid JSON data...", + type: "test", + sequence: [ + { + func: "{goodJsonGetRequest}.send", + args: [{required: true}] + }, + { + listener: "gpii.tests.schema.middleware.caseHolder.examineResponse", + event: "{goodJsonGetRequest}.events.onComplete", + args: ["{goodJsonGetRequest}.nativeResponse", "{arguments}.0", true] + } + ] + }, + { + name: "Testing a POST request with no body...", + type: "test", + sequence: [ + { + func: "{emptyPostRequest}.send" + }, + { + listener: "gpii.tests.schema.middleware.caseHolder.examineResponse", + event: "{emptyPostRequest}.events.onComplete", + args: ["{emptyPostRequest}.nativeResponse", "{arguments}.0", true] + } + ] + }, + { + name: "Testing a POST request with bad JSON data...", + type: "test", + sequence: [ + { + func: "{badJsonPostRequest}.send", + args: [{}] + }, + { + listener: "gpii.tests.schema.middleware.caseHolder.examineResponse", + event: "{badJsonPostRequest}.events.onComplete", + args: ["{badJsonPostRequest}.nativeResponse", "{arguments}.0", false] + } + ] + }, + { + name: "Testing a POST request with valid JSON data...", + type: "test", + sequence: [ + { + func: "{goodJsonPostRequest}.send", + args: [{required: true}] + }, + { + listener: "gpii.tests.schema.middleware.caseHolder.examineResponse", + event: "{goodJsonPostRequest}.events.onComplete", + args: ["{goodJsonPostRequest}.nativeResponse", "{arguments}.0", true] + } + ] + }, + { + name: "Testing a PUT request with no body...", + type: "test", + sequence: [ + { + func: "{emptyPutRequest}.send" + }, + { + listener: "gpii.tests.schema.middleware.caseHolder.examineResponse", + event: "{emptyPutRequest}.events.onComplete", + args: ["{emptyPutRequest}.nativeResponse", "{arguments}.0", true] + } + ] + }, + { + name: "Testing a PUT request with bad JSON data...", + type: "test", + sequence: [ + { + func: "{badJsonPutRequest}.send", + args: [{}] + }, + { + listener: "gpii.tests.schema.middleware.caseHolder.examineResponse", + event: "{badJsonPutRequest}.events.onComplete", + args: ["{badJsonPutRequest}.nativeResponse", "{arguments}.0", false] + } + ] + }, + { + name: "Testing a PUT request with valid JSON data...", + type: "test", + sequence: [ + { + func: "{goodJsonPutRequest}.send", + args: [{required: true}] + }, + { + listener: "gpii.tests.schema.middleware.caseHolder.examineResponse", + event: "{goodJsonPutRequest}.events.onComplete", + args: ["{goodJsonPutRequest}.nativeResponse", "{arguments}.0", true] + } + ] + } + ] + } + ], + components: { + emptyGetRequest: { + type: "gpii.tests.schema.middleware.request.get" + }, + nonJsonGetRequest: { + type: "gpii.tests.schema.middleware.request.get" + }, + badJsonGetRequest: { + type: "gpii.tests.schema.middleware.request.get" + }, + goodJsonGetRequest: { + type: "gpii.tests.schema.middleware.request.get" + }, + emptyPostRequest: { + type: "gpii.tests.schema.middleware.request.post" + }, + nonJsonPostRequest: { + type: "gpii.tests.schema.middleware.request.post" + }, + badJsonPostRequest: { + type: "gpii.tests.schema.middleware.request.post" + }, + goodJsonPostRequest: { + type: "gpii.tests.schema.middleware.request.post" + }, + emptyPutRequest: { + type: "gpii.tests.schema.middleware.request.put" + }, + nonJsonPutRequest: { + type: "gpii.tests.schema.middleware.request.put" + }, + badJsonPutRequest: { + type: "gpii.tests.schema.middleware.request.put" + }, + goodJsonPutRequest: { + type: "gpii.tests.schema.middleware.request.put" + } + } +}); + +fluid.defaults("gpii.tests.schema.middleware.testEnvironment", { + gradeNames: ["gpii.test.schema.testEnvironment"], + port: 7533, + components: { + caseHolder: { + type: "gpii.tests.schema.middleware.caseHolder" + } + } +}); + +gpii.tests.schema.middleware.testEnvironment(); diff --git a/tests/js/server/parser-tests.js b/tests/js/server/parser-tests.js new file mode 100644 index 0000000..e232a4c --- /dev/null +++ b/tests/js/server/parser-tests.js @@ -0,0 +1,84 @@ +/* + + Test the parser by itself to confirm that it loads schemas and dereferences them as expected. + + */ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); +var jqUnit = require("node-jqunit"); + +require("../../../"); + +require("gpii-express"); +gpii.express.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.schema.parser"); + +gpii.tests.schema.parser.testSchemaCaching = function (that) { + jqUnit.assertTrue("There should be dereferenced schemas...", that.model && that.model.dereferencedSchemas && Object.keys(that.model.dereferencedSchemas).length > 0); + + // There should no longer be any $ref values in any of our schemas. We check the `properties` structure, which in + // our original schemas contains $ref values. + // + fluid.each(that.model.dereferencedSchemas, function (schemaContent) { + fluid.each(schemaContent.properties, function (property) { + jqUnit.assertUndefined("There should not be a $ref property after dereferencing...", property.$ref); + }); + }); +}; + +fluid.defaults("gpii.tests.schema.parser.caseHolder", { + gradeNames: ["gpii.test.express.caseHolder"], + rawModules: [ + { + name: "Testing the parser in isolation...", + tests: [ + { + name: "Testing the initial dereferencing of schema content...", + type: "test", + sequence: [ + { + funcName: "gpii.tests.schema.parser.testSchemaCaching", + args: ["{testEnvironment}.parser"] + } + ] + } + ] + } + ] +}); + +fluid.defaults("gpii.tests.schema.parser.environment", { + gradeNames: ["fluid.test.testEnvironment"], + events: { + constructFixtures: null, + onSchemasDereferenced: null, + onFixturesConstructed: { + events: { + onSchemasDereferenced: "onSchemasDereferenced" + } + } + }, + distributeOptions: { + target: "{that > gpii.schema.parser}.options.listeners.onSchemasDereferenced", + record: { + func: "{testEnvironment}.events.onSchemasDereferenced.fire" + } + }, + components: { + parser: { + type: "gpii.schema.parser", + createOnEvent: "constructFixtures", + options: { + schemaDirs: "%gpii-json-schema/tests/schemas" + } + }, + caseHolder: { + type: "gpii.tests.schema.parser.caseHolder" + } + } +}); + +fluid.test.runTests("gpii.tests.schema.parser.environment"); diff --git a/tests/js/server/parser-update-failure-tests.js b/tests/js/server/parser-update-failure-tests.js new file mode 100644 index 0000000..4f66bb3 --- /dev/null +++ b/tests/js/server/parser-update-failure-tests.js @@ -0,0 +1,95 @@ +/* + + Pass the parser a bad JSON Schema and confirm that it throws an error. + + */ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); +fluid.loadTestingSupport(); + +var kettle = require("kettle"); +kettle.loadTestingSupport(); + +var jqUnit = require("node-jqunit"); + +require("../../../index"); + +fluid.defaults("gpii.tests.schema.parser.bornToDie", { + gradeNames: ["gpii.schema.parser"], + schemaDirs: "%gpii-json-schema/tests/badSchemas" +}); + +/* + + Global error handling used in this specific test, adapted from: + + https://github.com/amb26/kettle/blob/KETTLE-32/tests/ErrorTests.js#L111 + https://github.com/amb26/kettle/blob/KETTLE-32/tests/ErrorTests.js#L115 + + */ + +fluid.registerNamespace("gpii.tests.schema.parser.failure"); +gpii.tests.schema.parser.failure.confirmErrorFired = function (error) { + jqUnit.assertTrue("Attempting to load bad remote schemas fails as expected...", error && error.message && (error.message.indexOf("ENOENT") !== -1)); +}; + +gpii.tests.schema.awaitGlobalError = function (priority, message) { + jqUnit.assert(message); +}; + +fluid.defaults("gpii.tests.schema.globalErrorHandler", { + gradeNames: ["fluid.component", "fluid.resolveRootSingle"], + singleRootType: "kettle.tests.logNotifierHolder", + events: { + onError: null + } +}); + +var globalErrorHandler = gpii.tests.schema.globalErrorHandler(); + +gpii.tests.schema.notifyGlobalError = function () { + globalErrorHandler.events.onError.fire(fluid.makeArray(arguments)); +}; + +fluid.defaults("gpii.tests.schema.parser.failure.caseholder", { + gradeNames: ["fluid.test.testCaseHolder"], + modules: { + name: "Testing failure modes in parser...", + tests: [ + { + name: "Confirm that bad schemas eventually trigger a failure...", + type: "test", + sequence: [ + { + funcName: "kettle.test.pushInstrumentedErrors", + args: ["gpii.tests.schema.notifyGlobalError"] + }, + { + funcName: "gpii.tests.schema.parser.bornToDie", + args: [] + }, + { + event: "{globalErrorHandler}.events.onError", + listener: "gpii.tests.schema.awaitGlobalError" + }, + { + funcName: "kettle.test.popInstrumentedErrors" + } + ] + } + ] + } +}); + +fluid.defaults("gpii.tests.schema.parser.failure.testEnvironment", { + gradeNames: ["fluid.test.testEnvironment"], + components: { + caseHolder: { + type: "gpii.tests.schema.parser.failure.caseholder" + } + } +}); + +fluid.test.runTests("gpii.tests.schema.parser.failure.testEnvironment"); diff --git a/tests/js/server/pointer-function-tests.js b/tests/js/server/pointer-function-tests.js new file mode 100644 index 0000000..350222f --- /dev/null +++ b/tests/js/server/pointer-function-tests.js @@ -0,0 +1,111 @@ +/* + + Tests for the static functions used within the.pointers. + + */ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +var jqUnit = require("node-jqunit"); + +require("../../../"); + +fluid.registerNamespace("gpii.tests.schema.pointers"); + +/* + + Run a single test. Tests must be of the form: + + { + message: "The message passed to jqUnit.test and used in the assertion...", + func: gpii.schema.pointers.getChildJsonPointer, // The static function under test. + args: [], // the arguments to be passed to the function + expected: "" // The expected output as compared with assertDeepEq + } + + */ +gpii.tests.schema.pointers.runSingleTest = function (test) { + jqUnit.test(test.message, function () { + jqUnit.assertDeepEq(test.message, test.expected, test.func.apply(null, test.args)); + }); +}; + +fluid.defaults("gpii.tests.schema.pointers", { + gradeNames: ["fluid.component"], + tests: [ + // gpii.schema.pointers.getParentJsonPointer (jsonPointer) + { + message: "The parent for a deep element should be correctly resolved...", + func: gpii.schema.pointers.getParentJsonPointer, + args: ["#/definitions/testString/minLength"], + expected: "#/definitions/testString" + }, + { + message: "The parent for an immediate child of the root element should be correctly resolved...", + func: gpii.schema.pointers.getParentJsonPointer, + args: ["#/required"], + expected: "#/" + }, + { + message: "The parent for the root itself should be correctly resolved...", + func: gpii.schema.pointers.getParentJsonPointer, + args: ["#/"], + expected: "#/" + }, + // gpii.schema.pointers.getChildJsonPointer (jsonPointer, childPath) + { + message: "A child path should be resolved correctly from the root...", + func: gpii.schema.pointers.getChildJsonPointer, + args: ["#/", "errors"], + expected: "#/errors" + }, + { + message: "A simple child path should be resolved correctly...", + func: gpii.schema.pointers.getChildJsonPointer, + args: ["#/definitions/testString", "minLength"], + expected: "#/definitions/testString/minLength" + }, + // These tests are here to ensure that we do not double escape or strip escaping. + { + message: "A child path with slashes should be resolved correctly...", + func: gpii.schema.pointers.getChildJsonPointer, + args: ["#/definitions", "testString/minLength"], + expected: "#/definitions/testString/minLength" + }, + { + message: "A child path with escaped slashes and tildes should be correctly resolved...", + func: gpii.schema.pointers.getChildJsonPointer, + args: ["#/definitions/testString", "this~0and~1that"], + expected: "#/definitions/testString/this~0and~1that" + }, + // gpii.schema.pointers.getFieldErrorsFromFailure(failurePointer) + { + message: "Root required errors should be resolved correctly...", + func: gpii.schema.pointers.getFieldErrorsFromFailure, + args: ["#/required"], + expected: "#/errors" + }, + { + message: "Deep required errors should be resolved correctly...", + func: gpii.schema.pointers.getFieldErrorsFromFailure, + args: ["#/field1/allOf/1/required"], + expected: "#/field1/allOf/1/errors" + }, + { + message: "Deep field errors should be resolved correctly...", + func: gpii.schema.pointers.getFieldErrorsFromFailure, + args: ["#/field1/allOf/1/maxLength"], + expected: "#/field1/allOf/1/errors" + } + ], + listeners: { + "onCreate.runTests": { + funcName: "fluid.each", + args: ["{that}.options.tests", gpii.tests.schema.pointers.runSingleTest] + } + } +}); + +gpii.tests.schema.pointers(); diff --git a/tests/js/server/schema-inline-router-tests.js b/tests/js/server/schema-inline-router-tests.js new file mode 100644 index 0000000..dcacb4f --- /dev/null +++ b/tests/js/server/schema-inline-router-tests.js @@ -0,0 +1,83 @@ +/* + + Test the router that dereferences and delivers all schemas "inline" as a single JSON object. + */ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); +var jqUnit = require("node-jqunit"); + +require("../../../"); + +require("../lib/fixtures"); + +require("gpii-express"); +gpii.express.loadTestingSupport(); + +fluid.registerNamespace("gpii.schema.inline.tests"); + +gpii.schema.inline.tests.checkInlineSchemaPayload = function (response, body) { + jqUnit.assertEquals("The status code should be correct...", 200, response.statusCode); + + var payload = JSON.parse(body); + + jqUnit.assertTrue("There should be schema content in the payload...", payload && (Object.keys(payload).length > 0)); + + fluid.each(payload, function (schemaContent, schemaKey) { + jqUnit.assertTrue("The schema '" + schemaKey + "' should have at least one top-level key...", schemaContent && Object.keys(schemaContent).length > 0); + + // There should no longer be any $ref values in any of our schemas. We check the `properties` structure, which in + // our original schemas contains $ref values. + // + fluid.each(schemaContent.properties, function (property) { + jqUnit.assertUndefined("The schema '" + schemaKey + "' should not contain $ref properties after dereferencing...", property.$ref); + }); + }); +}; + +fluid.defaults("gpii.schema.inline.tests.caseHolder", { + gradeNames: ["gpii.test.express.caseHolder"], + rawModules: [ + { + name: "Testing the router that bundles and delivers all schemas at once...", + tests: [ + { + name: "Testing the initial dereferencing of schema content...", + type: "test", + sequence: [ + { + func: "{schemaContentRequest}.send", + args: [] + }, + { + event: "{schemaContentRequest}.events.onComplete", + listener: "gpii.schema.inline.tests.checkInlineSchemaPayload", + args: ["{schemaContentRequest}.nativeResponse", "{arguments}.0"] // response, body + } + ] + } + ] + } + ], + components: { + schemaContentRequest: { + type: "gpii.test.schema.request", + options: { + endpoint: "allSchemas" + } + } + } +}); + +fluid.defaults("gpii.schema.inline.tests.environment", { + gradeNames: ["gpii.test.schema.testEnvironment"], + port: 7654, + components: { + caseHolder: { + type: "gpii.schema.inline.tests.caseHolder" + } + } +}); + +fluid.test.runTests("gpii.schema.inline.tests.environment"); diff --git a/tests/js/server/validate-server-tests.js b/tests/js/server/validate-server-tests.js new file mode 100644 index 0000000..aff6f21 --- /dev/null +++ b/tests/js/server/validate-server-tests.js @@ -0,0 +1,218 @@ +// Tests of the core validator. +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +var jqUnit = require("node-jqunit"); + +require("../../../"); +require("../common/validate-common-test-definitions"); +require("../lib/errors"); +require("../lib/fixtures"); + +fluid.registerNamespace("gpii.tests.schema.validator.server"); + +// We are working with test definitions that look like: +// +// emptyDerived: { +// message: "Validate an empty 'derived' record....", +// schema: "derived.json", +// content: {}, +// errors: true, +// errorPaths: [".required", ".deeply.nested.additionalRequired"] +// } +// +// See `options.tests` below for examples. +// +gpii.tests.schema.validator.server.singleTest = function (that, test) { + var result = that.validate(test.schema, test.content); + + if (test.errors) { + if (test.errorPaths) { + gpii.test.schema.hasFieldErrors(result, test.errorPaths); + } + if (test.multipleErrorPaths) { + gpii.test.schema.hasFieldErrors(result, test.multipleErrorPaths, true); + } + } + else { + jqUnit.assertUndefined("There should be no validation errors...", result); + } +}; + + +// This last test is the only one that can't use the common definitions or `hasFieldErrors` function... +gpii.tests.schema.validator.server.invalidJsonTest = function (that) { + var result = that.validate("base.json", "{}"); + jqUnit.assertNotUndefined("There should be validation errors...", result); +}; + +// We need a custom test sequence constructor. We rehydrate the common test cases here. +gpii.tests.schema.validator.server.constructTestSequences = function (that) { + var generatedTests = []; + + // iterate through the test definitions and generate sequences as outlined above. + fluid.each(that.options.commonTests, function (testDefinition) { + var generatedCommonTest = { + name: testDefinition.message, + sequence: [{ + func: "gpii.tests.schema.validator.server.singleTest", + args: ["{testEnvironment}.validator", testDefinition] + }] + }; + generatedTests.push(generatedCommonTest); + }); + + // This one isn't part of the commmon test definitions, so we generate and add it manually. + var generatedBonusTest = { + name: "Test invalid JSON content...", + sequence: [{ + func: "gpii.tests.schema.validator.server.invalidJsonTest", + args: ["{testEnvironment}.validator"] + }] + }; + + generatedTests.push(generatedBonusTest); + + var testsWithStartAndEnd = gpii.test.express.helpers.addRequiredSequences([{ + name: "Testing server-side validation...", + tests: generatedTests + }], that.options.sequenceStart, that.options.sequenceEnd); + + return testsWithStartAndEnd; +}; + +gpii.tests.schema.validator.server.standardStartSequence = [ + { + func: "{testEnvironment}.events.onConstructFixtures.fire" + }, + { + event: "{testEnvironment}.events.onSchemasLoaded", + listener: "fluid.identity" + } +]; + +// Use the standard `gpii-test-browser` caseHolder, but use a more complex function to rehydrate the "common" tests +// before wiring in the standard start and end sequence steps. +fluid.defaults("gpii.tests.schema.validator.server.caseHolder", { + gradeNames: ["fluid.test.testCaseHolder", "gpii.test.schema.validator.hasDehydratedTests"], + sequenceStart: gpii.tests.schema.validator.server.standardStartSequence, + mergePolicy: { + rawModules: "noexpand", + sequenceStart: "noexpand", + sequenceEnd: "noexpand" + }, + moduleSource: { + funcName: "gpii.tests.schema.validator.server.constructTestSequences", + args: ["{that}"] + } +}); + +fluid.defaults("gpii.tests.schema.validator.server.environment.base", { + gradeNames: ["gpii.test.schema.testEnvironment"], + events: { + onSchemasLoaded: null, + onConstructFixtures: null + }, + components: { + validator: { + type: "gpii.schema.validator.ajv.server", + createOnEvent: "onConstructFixtures", + options: { + schemaDirs: "%gpii-json-schema/tests/schemas", + schemaKey: "base.json", + listeners: { + "onSchemasLoaded.notifyEnvironment": { + func: "{testEnvironment}.events.onSchemasLoaded.fire" + } + } + } + } + } +}); + +fluid.defaults("gpii.tests.schema.validator.server.environment.standard", { + gradeNames: ["gpii.tests.schema.validator.server.environment.base"], + components: { + caseHolder: { + type: "gpii.tests.schema.validator.server.caseHolder" + } + } +}); + +fluid.test.runTests("gpii.tests.schema.validator.server.environment.standard"); + +// The next tests can use the standard harness from gpii-express +fluid.registerNamespace("gpii.tests.schema.validator.server.caseHolder.replay"); +gpii.tests.schema.validator.server.caseHolder.replay.validateMultiples = function (validator, schemaKey, message, inputs, expected) { + var results = []; + fluid.each(inputs, function (input) { + var result = validator.validate(schemaKey, input); + results.push(result === undefined); + }); + + jqUnit.assertDeepEq(message, expected, results); +}; + +fluid.defaults("gpii.tests.schema.validator.server.caseHolder.replay", { + gradeNames: ["gpii.test.express.caseHolder.base"], + sequenceStart: gpii.tests.schema.validator.server.standardStartSequence, + schemaKey: "base.json", + rawModules: [{ + name: "Testing multiple validations with a single component...", + tests: [ + { + name: "Fail twice in a row...", + type: "test", + sequence: [ + { + func: "gpii.tests.schema.validator.server.caseHolder.replay.validateMultiples", + args: ["{testEnvironment}.validator", "{that}.options.schemaKey", "We should be able to fail after a failure...", [{},{}], [false, false]] + } + ] + }, + { + name: "Succeed twice in a row...", + type: "test", + sequence: [ + { + func: "gpii.tests.schema.validator.server.caseHolder.replay.validateMultiples", + args: ["{testEnvironment}.validator", "{that}.options.schemaKey", "We should be able to succeed after a success...", [{ required: true },{ required: true }], [true, true]] + } + ] + }, + { + name: "Succeed, then fail...", + type: "test", + sequence: [ + { + func: "gpii.tests.schema.validator.server.caseHolder.replay.validateMultiples", + args: ["{testEnvironment}.validator", "{that}.options.schemaKey", "We should be able to fail after a success...", [{ required: true },{}], [true, false]] + } + ] + }, + { + name: "Fail, then succeed...", + type: "test", + sequence: [ + { + func: "gpii.tests.schema.validator.server.caseHolder.replay.validateMultiples", + args: ["{testEnvironment}.validator", "{that}.options.schemaKey", "We should be able to succeed after a failure...", [{},{ required: true }], [false, true]] + } + ] + } + ] + }] +}); + +fluid.defaults("gpii.tests.schema.validator.server.environment.replay", { + gradeNames: ["gpii.tests.schema.validator.server.environment.base"], + components: { + caseHolder: { + type: "gpii.tests.schema.validator.server.caseHolder.replay" + } + } +}); + +fluid.test.runTests("gpii.tests.schema.validator.server.environment.replay"); diff --git a/tests/js/server/validator-evolved-error-tests.js b/tests/js/server/validator-evolved-error-tests.js new file mode 100644 index 0000000..66e0eab --- /dev/null +++ b/tests/js/server/validator-evolved-error-tests.js @@ -0,0 +1,265 @@ +/* + + Test the parser by itself with as little interaction with the validator as possible (only static validator functions used). + + Client-side tests and integration are covered by the validator tests, which make use of the parser internally. + + */ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); +var jqUnit = require("node-jqunit"); + +require("../../../"); + +require("gpii-express"); +gpii.express.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.schema.parser.server"); + +gpii.tests.schema.parser.server.validateAndTest = function (validator, schemaKey, content, expected) { + var output = validator.validate(schemaKey, content); + jqUnit.assertDeepEq("The output should be as expected...", expected, output); +}; + +fluid.defaults("gpii.tests.schema.parser.server.caseHolder", { + gradeNames: ["gpii.test.express.caseHolder"], + rawModules: [ + { + name: "Testing 'evolved' error messages...", + tests: [ + { + name: "Testing a single unevolved failure...", + type: "test", + sequence: [ + { + funcName: "gpii.tests.schema.parser.server.validateAndTest", + args: ["{testEnvironment}.validator", "base.json", "{that}.options.input.singleRawFailure", "{that}.options.expected.singleRawFailure"] // validator, schemaKey, content, expected + } + ] + }, + { + name: "Testing multiple unevolved `allOf` failures within a single field...", + type: "test", + sequence: [ + { + funcName: "gpii.tests.schema.parser.server.validateAndTest", + args: ["{testEnvironment}.validator", "base.json", "{that}.options.input.multipleRawFailures", "{that}.options.expected.multipleRawFailures"] // validator, schemaKey, content, expected + } + ] + }, + { + name: "Testing evolving a single failure in an immediate child of the root...", + type: "test", + sequence: [ + { + funcName: "gpii.tests.schema.parser.server.validateAndTest", + args: ["{testEnvironment}.validator", "evolved.json", "{that}.options.input.evolvedRootFailure", "{that}.options.expected.evolvedRootFailure"] // validator, schemaKey, content, expected + } + ] + }, + { + name: "Testing evolving a failure within a top-level required field...", + type: "test", + sequence: [ + { + funcName: "gpii.tests.schema.parser.server.validateAndTest", + args: ["{testEnvironment}.validator", "evolved.json", "{that}.options.input.evolvedRequiredFailure", "{that}.options.expected.evolvedRequiredFailure"] // validator, schemaKey, content, expected + } + ] + }, + { + name: "Testing evolving the message for a deep required field...", + type: "test", + sequence: [ + { + funcName: "gpii.tests.schema.parser.server.validateAndTest", + args: ["{testEnvironment}.validator", "evolved-overlay.json", "{that}.options.input.evolvedDeepFailure", "{that}.options.expected.evolvedDeepFailure"] // validator, schemaKey, content, expected + } + ] + }, + { + name: "Testing evolving a failure within an 'allOf' field...", + type: "test", + sequence: [ + { + funcName: "gpii.tests.schema.parser.server.validateAndTest", + args: ["{testEnvironment}.validator", "evolved.json", "{that}.options.input.evolvedArrayFailure", "{that}.options.expected.evolvedArrayFailure"] // validator, schemaKey, content, expected + } + ] + }, + { + name: "Confirm that a valid record still validates when there is error metadata...", + type: "test", + sequence: [ + { + funcName: "gpii.tests.schema.parser.server.validateAndTest", + args: ["{testEnvironment}.validator", "evolved.json", "{that}.options.input.evolvedButValid", "{that}.options.expected.evolvedButValid"] // validator, schemaKey, content, expected + } + ] + }, + { + name: "Testing replacing a message in an underlying schema...", + type: "test", + sequence: [ + { + funcName: "gpii.tests.schema.parser.server.validateAndTest", + args: ["{testEnvironment}.validator", "evolved-overlay.json", "{that}.options.input.overlayedRootFailure", "{that}.options.expected.overlayedRootFailure"] // validator, schemaKey, content, expected + } + ] + }, + { + name: "Testing preserving a message in an underlying schema...", + type: "test", + sequence: [ + { + funcName: "gpii.tests.schema.parser.server.validateAndTest", + args: ["{testEnvironment}.validator", "evolved-overlay.json", "{that}.options.input.inheritedFailure", "{that}.options.expected.inheritedFailure"] // validator, schemaKey, content, expected + } + ] + } + ] + } + ], + input: { + singleRawFailure: {}, + multipleRawFailures: { required: true, password: "pass" }, + evolvedRootFailure: { shallowlyRequired: true, testString: "ThunderCAT"}, + overlayedRootFailure: { shallowlyRequired: true, testString: "ThunderCAT"}, + inheritedFailure: { shallowlyRequired: true, testString: "UnderDOG"}, + evolvedDeepFailure: { shallowlyRequired: true, deep: {}}, + evolvedArrayFailure: { shallowlyRequired: true, testAllOf: "ThunderCAT"}, + evolvedRequiredFailure: {}, + evolvedButValid: { shallowlyRequired: true } + }, + expected: { + singleRawFailure: [ + { + "keyword": "required", + "dataPath": "", + "schemaPath": "#/required", + "params": { + "missingProperty": "required" + }, + "message": "should have required property 'required'" + } + ], + multipleRawFailures: [ + { + "keyword": "minLength", + "dataPath": ".password", + "schemaPath": "#/properties/password/allOf/0/minLength", + "params": { + "limit": 8 + }, + "message": "should NOT be shorter than 8 characters" + }, + { + "keyword": "pattern", + "dataPath": ".password", + "schemaPath": "#/properties/password/allOf/1/pattern", + "params": { + "pattern": "[A-Z]+" + }, + "message": "should match pattern \"[A-Z]+\"" + }, + { + "keyword": "pattern", + "dataPath": ".password", + "schemaPath": "#/properties/password/allOf/3/pattern", + "params": { + "pattern": "[^a-zA-Z]" + }, + "message": "should match pattern \"[^a-zA-Z]\"" + } + ], + evolvedRootFailure: [{ + "keyword": "maxLength", + "dataPath": ".testString", + "schemaPath": "#/properties/testString/maxLength", + "params": { + "limit": 9 + }, + "message": "You must enter a test string that is no more than nine characters long." + }], + overlayedRootFailure: [{ + "keyword": "maxLength", + "dataPath": ".testString", + "schemaPath": "#/properties/testString/maxLength", + "params": { + "limit": 9 + }, + "message": "You must enter a BETTER string that is no more than nine characters long." + }], + inheritedFailure: [{ + "keyword": "pattern", + "dataPath": ".testString", + "schemaPath": "#/properties/testString/pattern", + "params": { + "pattern": ".*CAT.*" + }, + "message": "You must enter a test string which contains the word \"CAT\"." + }], + evolvedDeepFailure: [{ + "keyword": "required", + "dataPath": ".deep", + "schemaPath": "#/properties/deep/required", + "params": { + "missingProperty": "deeplyRequired" + }, + "message": "should have required property 'deeplyRequired'" + }], + evolvedArrayFailure: [{ + "keyword": "maxLength", + "dataPath": ".testAllOf", + "schemaPath": "#/properties/testAllOf/allOf/2/maxLength", + "params": { + "limit": 9 + }, + "message": "The 'allOf' string cannot be longer than nine characters." + }], + evolvedRequiredFailure: [{ + "keyword": "required", + "dataPath": "", + "schemaPath": "#/required", + "params": { + "missingProperty": "shallowlyRequired" + }, + "message": "The 'shallowlyRequired' field is required." + }], + evolvedButValid: undefined + } +}); + +fluid.defaults("gpii.tests.schema.parser.server.environment", { + gradeNames: ["gpii.test.express.testEnvironment"], + events: { + onSchemasLoaded: null, + onFixturesConstructed: { + events: { + onSchemasLoaded: "onSchemasLoaded" + } + } + }, + distributeOptions: { + target: "{that > gpii.schema.validator.ajv.server}.options.listeners.onSchemasLoaded", + record: { + func: "{testEnvironment}.events.onSchemasLoaded.fire" + } + }, + components: { + validator: { + type: "gpii.schema.validator.ajv.server", + createOnEvent: "constructFixtures", + options: { + schemaDirs: "%gpii-json-schema/tests/schemas" + } + }, + caseHolder: { + type: "gpii.tests.schema.parser.server.caseHolder" + } + } +}); + +fluid.test.runTests("gpii.tests.schema.parser.server.environment"); diff --git a/tests/schemas/README.md b/tests/schemas/README.md new file mode 100644 index 0000000..822d7f3 --- /dev/null +++ b/tests/schemas/README.md @@ -0,0 +1,2 @@ +This directory contains schemas used in our tests. This file is also part of our tests, confirming that non-JSON +content will not break anything. \ No newline at end of file diff --git a/tests/schemas/base.json b/tests/schemas/base.json new file mode 100644 index 0000000..8165e01 --- /dev/null +++ b/tests/schemas/base.json @@ -0,0 +1,60 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "base.json", + "title": "Example Base Schema", + "type": "object", + "definitions": { + "required": { + "type": "boolean" + }, + "boolean": { + "type": "boolean" + }, + "date": { + "type": "string", + "format": "date" + }, + "number": { + "type": "number", + "minValue": 1, + "maxValue": 10 + }, + "array": { + "type": "array", + "items": {"type": "boolean" } + }, + "regex": { + "description": "The string should be five characters long, begin with 'v' and end with 'd'.", + "type": "string", + "pattern": "^v...d$" + }, + "password": { + "description": "Password must be 8 or more characters, and have at least one uppercase letter, at least one lowercase letter, and at least one number or special character.", + "allOf": [ + { "type": "string", "minLength": 8 }, + { "type": "string", "pattern": "[A-Z]+"}, + { "type": "string", "pattern": "[a-z]+"}, + { "type": "string", "pattern": "[^a-zA-Z]"} + ] + }, + "rawMultiple": { + "allOf": [ + { "type": "string", "minLength": 8 }, + { "type": "string", "pattern": "[A-Z]+"}, + { "type": "string", "pattern": "[a-z]+"}, + { "type": "string", "pattern": "[^a-zA-Z]"} + ] + } + }, + "properties": { + "required": { "$ref": "#/definitions/required" }, + "boolean": { "$ref": "#/definitions/boolean" }, + "date": { "$ref": "#/definitions/date"}, + "number": { "$ref": "#/definitions/number"}, + "array": { "$ref": "#/definitions/array"}, + "password": { "$ref": "#/definitions/password"}, + "rawMultiple": { "$ref": "#/definitions/rawMultiple"}, + "regex": { "$ref": "#/definitions/regex"} + }, + "required": ["required"] +} \ No newline at end of file diff --git a/tests/schemas/deep.json b/tests/schemas/deep.json new file mode 100644 index 0000000..4c346ec --- /dev/null +++ b/tests/schemas/deep.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Deep schema to test variable nesting and paths.", + "type": "object", + "properties": { + "deep": { + "type": "object", + "properties": { + "required": { + "type": "boolean" + } + }, + "required": ["required"] + } + } +} \ No newline at end of file diff --git a/tests/schemas/derived.json b/tests/schemas/derived.json new file mode 100644 index 0000000..0db03f6 --- /dev/null +++ b/tests/schemas/derived.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Schema derived from base schema", + "description": "inheritance in JSON Schema v4 is still weird. This example is the one we test against, and it works with JSON Schema.", + "id": "derived.json", + "type": "object", + "properties": { + "required": { "$ref": "base.json#/definitions/required" }, + "boolean": { "$ref": "base.json#/definitions/boolean" }, + "date": { "$ref": "base.json#/definitions/date"}, + "number": { "$ref": "base.json#/definitions/number"}, + "array": { "$ref": "base.json#/definitions/array"}, + "regex": { "$ref": "base.json#/definitions/array"}, + + "additionalOptional": { + "type": "string" + }, + "additionalRequired": { + "type": "boolean" + } + }, + "required": ["additionalRequired", "required"] +} \ No newline at end of file diff --git a/tests/schemas/escaped.json b/tests/schemas/escaped.json new file mode 100644 index 0000000..0fd3976 --- /dev/null +++ b/tests/schemas/escaped.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Schema to test names that would otherwise conflict with the path handling", + "type": "object", + "properties": { + "this.that": { + "type": "object", + "properties": { + "th'other": { + "type": "object", + "description": "How do increasingly sloppy variable names make you feel?", + "properties": { + "required": { + "type": "boolean" + } + }, + "required": ["required"] + } + }, + "required": ["th'other"] + }, + "[x][x]": { + "type": "string", + "description": "How do textual cross marks make you feel?" + } + }, + "required" : ["[x][x]"] +} \ No newline at end of file diff --git a/tests/schemas/evolved-overlay.json b/tests/schemas/evolved-overlay.json new file mode 100644 index 0000000..0d8e86e --- /dev/null +++ b/tests/schemas/evolved-overlay.json @@ -0,0 +1,30 @@ +{ + "type": "object", + "id": "evolved-overlay.json", + "title": "'Evolved' schema plus overlay...", + "properties": { + "testString": { + "$ref": "evolved.json#/definitions/testString" + }, + "testAllOf": { + "$ref": "evolved.json#/definitions/testAllOf" + }, + "deep": { + "$ref": "evolved.json#/definitions/deep" + }, + "hasNoErrorsMetadata": { + "$ref": "evolved.json#/definitions/hasNoErrorsMetadata" + } + }, + "required": [ + "shallowlyRequired" + ], + "errors": { + "#/properties/testString/minLength": "You must enter a BETTER string that is at least four characters long.", + "#/properties/testString/maxLength": "You must enter a BETTER string that is no more than nine characters long.", + "#/properties/testAllOf/allOf/0/type": "You must enter a BETTER string.", + "#/properties/testAllOf/allOf/1/minLength": "The BETTER string must be at least four characters long.", + "#/properties/testAllOf/allOf/2/maxLength": "The BETTER string cannot be longer than nine characters.", + "#/properties/testAllOf/allOf/3/pattern": "The BETTER string must contain the word 'CAT'." + } +} diff --git a/tests/schemas/evolved.json b/tests/schemas/evolved.json new file mode 100644 index 0000000..3bf84f8 --- /dev/null +++ b/tests/schemas/evolved.json @@ -0,0 +1,82 @@ +{ + "type": "object", + "id": "evolved.json", + "title": "'Evolved' schema for use in testing v5 emulation...", + "definitions": { + "testString": { + "type": "string", + "minLength": 4, + "maxLength": 9, + "pattern": ".*CAT.*", + "errors": { + "minLength": "You must enter a test string that is at least four characters long.", + "maxLength": "You must enter a test string that is no more than nine characters long.", + "pattern": "You must enter a test string which contains the word \"CAT\"." + } + }, + "testAllOf": { + "allOf": [ + { + "type": "string", + "errors": { "type" : "The 'allOf' field must be a string." } + }, + { + "minLength": 4, + "errors": { "minLength": "The 'allOf' string must be at least four characters long." } + }, + { + "maxLength": 9, + "errors": { "maxLength": "The 'allOf' string cannot be longer than nine characters." } + }, + { + "pattern": ".*CAT.*", + "errors": { "pattern": "The 'allOf' string must contain the word 'CAT'." } + } + ] + }, + "deep": { + "type": "object", + "properties": { + "deeplyRequired": { + "type": "string" + } + }, + "required": [ + "deeplyRequired" + ] + }, + "hasNoErrorsMetadata": { + "type": "string", + "minLength": 3 + } + }, + "properties": { + "testString": { + "$ref": "#/definitions/testString" + }, + "testAllOf": { + "$ref": "#/definitions/testAllOf" + }, + "deep": { + "$ref": "#/definitions/deep" + }, + "hasNoErrorsMetadata": { + "$ref": "#/definitions/hasNoErrorsMetadata" + }, + "definedDirectly": { + "type": "string", + "maxLength": 1, + "errors": { "maxLength": "The 'definedDirectly' field must contain a single character."} + }, + "definedDirectlyHasNoErrorsMetadata": { + "type": "string", + "maxLength": 1 + } + }, + "required": [ + "shallowlyRequired" + ], + "errors": { + "#/required/0": "The 'shallowlyRequired' field is required." + } +} diff --git a/tests/schemas/gated.json b/tests/schemas/gated.json new file mode 100644 index 0000000..0f06ae5 --- /dev/null +++ b/tests/schemas/gated.json @@ -0,0 +1,49 @@ +{ + "type": "object", + "id": "gated.json", + "title": "'Gated' schema...", + "description": "Schema for use in exercising our middleware and client-side components.", + "definitions": { + "testString": { + "type": "string", + "minLength": 4, + "maxLength": 9, + "pattern": ".*CAT.*" + }, + "testAllOf": { + "allOf": [ + { + "type": "string" + }, + { + "minLength": 4 + }, + { + "maxLength": 9 + }, + { + "pattern": ".*CAT.*" + } + ] + }, + "deep": { + "type": "object", + "properties": { + "deeplyRequired": { + "type": "string" + } + }, + "required": [ + "deeplyRequired" + ] + } + }, + "properties": { + "testString": { "$ref": "#/definitions/testString" }, + "testAllOf": { "$ref": "#/definitions/testAllOf" }, + "deep": { "$ref": "#/definitions/deep" } + }, + "required": [ + "shallowlyRequired" + ] +} diff --git a/tests/static/errorBinder-tests.html b/tests/static/errorBinder-tests.html new file mode 100644 index 0000000..7441ed6 --- /dev/null +++ b/tests/static/errorBinder-tests.html @@ -0,0 +1,101 @@ + + + Client-side tests for validator... + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Client side validation

+ +
+
+
+ +
+ +

Server side validation

+ +
+
+
+ +
+ +
+

+ This sample form is used to test the binding between validation errors and onscreen elements, in response + to both client-side and server-side feedback. +

+
+ + + + + \ No newline at end of file diff --git a/tests/static/js/testErrorBinder.js b/tests/static/js/testErrorBinder.js new file mode 100644 index 0000000..5a1dfa5 --- /dev/null +++ b/tests/static/js/testErrorBinder.js @@ -0,0 +1,110 @@ +/* global fluid, jQuery */ +(function () { + "use strict"; + + fluid.defaults("gpii.tests.schema.errorBinder.base", { + hideOnSuccess: false, + inlineSchemaUrl: "/allSchemas", + ajaxOptions: { + url: "/gated/POST", + method: "POST", + headers: { + "Accept": "application/json" + } + }, + schemaKey: "evolved.json", + templates: { + initial: "errorBinder-viewport" + }, + rules: { + modelToRequestPayload: { + "": "notfound", + shallowlyRequired: "shallowlyRequired", + testString: "testString", + testAllOf: "testAllOf", + succeed: "succeed", + deep: { + deeplyRequired: "deeplyRequired" + } + } + }, + model: { + }, + bindings: { + // We use both styles of bindings to confirm that they each work with the `errorBinder`. + shallowlyRequired: { + selector: "shallowlyRequired", + path: "shallowlyRequired", + rules: { + domToModel: { + "": { + transform: { + type: "gpii.binder.transforms.stripEmptyString", + inputPath: "" + } + } + } + } + }, + testString: { + selector: "testString", + path: "testString", + rules: { + domToModel: { + "": { + transform: { + type: "gpii.binder.transforms.stripEmptyString", + inputPath: "" + } + } + } + } + }, + testAllOf: { + selector: "testAllOf", + path: "testAllOf", + rules: { + domToModel: { + "": { + transform: { + type: "gpii.binder.transforms.stripEmptyString", + inputPath: "" + } + } + } + } + }, + deeplyRequired: { + selector: "deeplyRequired", + path: "deeplyRequired", + rules: { + domToModel: { + "": { + transform: { + type: "gpii.binder.transforms.stripEmptyString", + inputPath: "" + } + } + } + } + }, + succeed: "succeed" + }, + selectors: { + shallowlyRequired: "input[name='shallowlyRequired']", + testString: "input[name='testString']", + testAllOf: "input[name='testAllOf']", + deeplyRequired: "input[name='deeplyRequired']", + succeed: "input[name='succeed']" + } + }); + + fluid.defaults("gpii.tests.schema.errorBinder", { + gradeNames: ["gpii.schemas.client.errorAwareForm", "gpii.tests.schema.errorBinder.base"] + }); + + fluid.defaults("gpii.tests.schema.errorBinder.clientSideValidation", { + gradeNames: ["gpii.schemas.client.errorAwareForm.clientSideValidation.realTime", "gpii.tests.schema.errorBinder.base"] + }); +})(jQuery); + diff --git a/tests/static/validate-client-tests.html b/tests/static/validate-client-tests.html new file mode 100644 index 0000000..5d200a2 --- /dev/null +++ b/tests/static/validate-client-tests.html @@ -0,0 +1,44 @@ + + + Client-side tests for validator... + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

This page instantiates a client-side validator, which we will exercise using a test browser. This page must be hosted using gpii.express or another web server.

+ + + + + \ No newline at end of file diff --git a/tests/templates/layouts/main.handlebars b/tests/templates/layouts/main.handlebars new file mode 100644 index 0000000..6d42a49 --- /dev/null +++ b/tests/templates/layouts/main.handlebars @@ -0,0 +1,16 @@ + + + GPII JSON Schema Test Content + + +

Main Layout

+ +

This is content coming from the layout.

+ +
+ + {{{body}}} + + + + diff --git a/tests/templates/partials/common-error.handlebars b/tests/templates/partials/common-error.handlebars new file mode 100644 index 0000000..a33c6d5 --- /dev/null +++ b/tests/templates/partials/common-error.handlebars @@ -0,0 +1,3 @@ +{{#if message}} +
{{message}}
+{{/if}} diff --git a/tests/templates/partials/common-success.handlebars b/tests/templates/partials/common-success.handlebars new file mode 100644 index 0000000..77bf800 --- /dev/null +++ b/tests/templates/partials/common-success.handlebars @@ -0,0 +1,3 @@ +{{#if message}} +
{{message}}
+{{/if}} diff --git a/tests/templates/partials/errorBinder-viewport.handlebars b/tests/templates/partials/errorBinder-viewport.handlebars new file mode 100644 index 0000000..9f466f9 --- /dev/null +++ b/tests/templates/partials/errorBinder-viewport.handlebars @@ -0,0 +1,60 @@ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..1f9ddbd --- /dev/null +++ b/yarn.lock @@ -0,0 +1,2375 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"JSV@>= 4.0.x": + version "4.0.2" + resolved "https://registry.yarnpkg.com/JSV/-/JSV-4.0.2.tgz#d077f6825571f82132f9dffaed587b4029feff57" + +abbrev@1, abbrev@1.0.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" + +accepts@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" + dependencies: + mime-types "~2.1.11" + negotiator "0.6.1" + +accessibility-developer-tools@2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/accessibility-developer-tools/-/accessibility-developer-tools-2.11.0.tgz#2b669c802671672aed5c61c42a782082a2fc7d2c" + +acorn-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" + dependencies: + acorn "^3.0.4" + +acorn@4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.4.tgz#17a8d6a7a6c4ef538b814ec9abac2779293bf30a" + +acorn@^3.0.4, acorn@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" + +adm-zip@^0.4.7: + version "0.4.7" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.7.tgz#8606c2cbf1c426ce8c8ec00174447fd49b6eafc1" + +ajv-keywords@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.1.1.tgz#02550bc605a3e576041565628af972e06c549d50" + +ajv@4.11.2, ajv@^4.7.0: + version "4.11.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.2.tgz#f166c3c11cbc6cb9dcc102a5bcfe5b72c95287e6" + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + +align-text@^0.1.1, align-text@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" + dependencies: + kind-of "^3.0.2" + longest "^1.0.1" + repeat-string "^1.5.2" + +amdefine@>=0.0.4: + version "1.0.0" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.0.tgz#fd17474700cb5cc9c2b709f0be9d23ce3c198c33" + +ansi-escapes@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" + +ansi-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.0.0.tgz#c5061b6e0ef8a81775e50f5d66151bf6bf371107" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansi-styles@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178" + +argparse@^1.0.2, argparse@^1.0.7: + version "1.0.9" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" + dependencies: + sprintf-js "~1.0.2" + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + +arrify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + +asap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/asap/-/asap-1.0.0.tgz#b2a45da5fdfa20b0496fc3768cc27c12fa916a7d" + +asn1@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + +assert-plus@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" + +assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +async@1.x, async@^1.4.0, async@~1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + +async@~0.2.6: + version "0.2.10" + resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +aws-sign2@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" + +aws4@^1.2.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.5.0.tgz#0a29ffb79c31c9e712eeb087e8e7a64b4a56d755" + +axe-core@2.1.7: + version "2.1.7" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-2.1.7.tgz#4f66f2b3ee3b58ec2d3db4339dd124c5b33b79c3" + +babel-code-frame@^6.16.0: + version "6.16.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.16.0.tgz#f90e60da0862909d3ce098733b5d3987c97cb8de" + dependencies: + chalk "^1.1.0" + esutils "^2.0.2" + js-tokens "^2.0.0" + +balanced-match@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" + +base64-url@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/base64-url/-/base64-url-1.3.3.tgz#f8b6c537f09a4fc58c99cb86e0b0e9c61461a20f" + +bcrypt-pbkdf@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.0.tgz#3ca76b85241c7170bf7d9703e7b9aa74630040d4" + dependencies: + tweetnacl "^0.14.3" + +body-parser@1.16.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.16.0.tgz#924a5e472c6229fb9d69b85a20d5f2532dec788b" + dependencies: + bytes "2.4.0" + content-type "~1.0.2" + debug "2.6.0" + depd "~1.1.0" + http-errors "~1.5.1" + iconv-lite "0.4.15" + on-finished "~2.3.0" + qs "6.2.1" + raw-body "~2.2.0" + type-is "~1.6.14" + +boom@2.x.x: + version "2.10.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + dependencies: + hoek "2.x.x" + +brace-expansion@^1.0.0: + version "1.1.6" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9" + dependencies: + balanced-match "^0.4.1" + concat-map "0.0.1" + +builtin-modules@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + +bytes@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" + +call-me-maybe@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" + +caller-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" + dependencies: + callsites "^0.2.0" + +callsites@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" + +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" + +camelcase@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + +caseless@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" + +center-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" + dependencies: + align-text "^0.1.3" + lazy-cache "^1.0.3" + +chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3, chalk@~1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f" + dependencies: + ansi-styles "~1.0.0" + has-color "~0.1.0" + strip-ansi "~0.1.0" + +chromedriver@2.27.2: + version "2.27.2" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-2.27.2.tgz#fd8ee03e6f8dde47d83b951bf99ea268538179c3" + dependencies: + adm-zip "^0.4.7" + kew "^0.7.0" + mkdirp "^0.5.1" + rimraf "^2.5.4" + +circular-json@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d" + +cli-cursor@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" + dependencies: + restore-cursor "^1.0.1" + +cli-width@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a" + +cliui@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" + dependencies: + center-align "^0.1.1" + right-align "^0.1.1" + wordwrap "0.0.2" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +code-point-at@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.0.1.tgz#1104cd34f9b5b45d3eba88f1babc1924e1ce35fb" + dependencies: + number-is-nan "^1.0.0" + +coffee-script@~1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/coffee-script/-/coffee-script-1.10.0.tgz#12938bcf9be1948fa006f92e0c4c9e81705108c0" + +colors@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" + +combined-stream@^1.0.5, combined-stream@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" + dependencies: + delayed-stream "~1.0.0" + +commander@^2.7.1, commander@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" + dependencies: + graceful-readlink ">= 1.0.0" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@^1.4.6: + version "1.5.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" + dependencies: + inherits "~2.0.1" + readable-stream "~2.0.0" + typedarray "~0.0.5" + +content-disposition@0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.1.tgz#87476c6a67c8daa87e32e87616df883ba7fb071b" + +content-type@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" + +cookie-parser@1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.3.tgz#0fe31fa19d000b95f4aadf1f53fdc2b8a203baa5" + dependencies: + cookie "0.3.1" + cookie-signature "1.0.6" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +crc@3.4.4: + version "3.4.4" + resolved "https://registry.yarnpkg.com/crc/-/crc-3.4.4.tgz#9da1e980e3bd44fc5c93bf5ab3da3378d85e466b" + +cryptiles@2.x.x: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + dependencies: + boom "2.x.x" + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + dependencies: + array-find-index "^1.0.1" + +d@^0.1.1, d@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309" + dependencies: + es5-ext "~0.10.2" + +dashdash@^1.12.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.0.tgz#29e486c5418bf0f356034a993d51686a33e84141" + dependencies: + assert-plus "^1.0.0" + +dateformat@~1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9" + dependencies: + get-stdin "^4.0.1" + meow "^3.3.0" + +debug@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b" + dependencies: + ms "0.7.2" + +debug@^2.1.1, debug@^2.2.0, debug@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" + dependencies: + ms "0.7.1" + +decamelize@^1.0.0, decamelize@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + +del@^2.0.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" + dependencies: + globby "^5.0.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + rimraf "^2.2.8" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +depd@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + +doctrine@^1.2.1, doctrine@^1.2.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" + dependencies: + esutils "^2.0.2" + isarray "^1.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + dependencies: + jsbn "~0.1.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + +encodeurl@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" + +error-ex@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.0.tgz#e67b43f3e82c96ea3a584ffee0b9fc3325d802d9" + dependencies: + is-arrayish "^0.2.1" + +es5-ext@^0.10.7, es5-ext@^0.10.8, es5-ext@~0.10.11, es5-ext@~0.10.2, es5-ext@~0.10.7: + version "0.10.12" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.12.tgz#aa84641d4db76b62abba5e45fd805ecbab140047" + dependencies: + es6-iterator "2" + es6-symbol "~3.1" + +es6-iterator@2: + version "2.0.0" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.0.tgz#bd968567d61635e33c0b80727613c9cb4b096bac" + dependencies: + d "^0.1.1" + es5-ext "^0.10.7" + es6-symbol "3" + +es6-map@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.4.tgz#a34b147be224773a4d7da8072794cefa3632b897" + dependencies: + d "~0.1.1" + es5-ext "~0.10.11" + es6-iterator "2" + es6-set "~0.1.3" + es6-symbol "~3.1.0" + event-emitter "~0.3.4" + +es6-promise@^3.1.2: + version "3.3.1" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" + +es6-set@~0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.4.tgz#9516b6761c2964b92ff479456233a247dc707ce8" + dependencies: + d "~0.1.1" + es5-ext "~0.10.11" + es6-iterator "2" + es6-symbol "3" + event-emitter "~0.3.4" + +es6-symbol@3, es6-symbol@~3.1, es6-symbol@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.0.tgz#94481c655e7a7cad82eba832d97d5433496d7ffa" + dependencies: + d "~0.1.1" + es5-ext "~0.10.11" + +es6-weak-map@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.1.tgz#0d2bbd8827eb5fb4ba8f97fbfea50d43db21ea81" + dependencies: + d "^0.1.1" + es5-ext "^0.10.8" + es6-iterator "2" + es6-symbol "3" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +escodegen@1.8.x: + version "1.8.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018" + dependencies: + esprima "^2.7.1" + estraverse "^1.9.1" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.2.0" + +escope@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" + dependencies: + es6-map "^0.1.3" + es6-weak-map "^2.0.1" + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-config-fluid@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/eslint-config-fluid/-/eslint-config-fluid-1.1.0.tgz#82b8bff6d208ff2492938cbe1a011d4ca2362a97" + +eslint@3.15.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.15.0.tgz#bdcc6a6c5ffe08160e7b93c066695362a91e30f2" + dependencies: + babel-code-frame "^6.16.0" + chalk "^1.1.3" + concat-stream "^1.4.6" + debug "^2.1.1" + doctrine "^1.2.2" + escope "^3.6.0" + espree "^3.4.0" + estraverse "^4.2.0" + esutils "^2.0.2" + file-entry-cache "^2.0.0" + glob "^7.0.3" + globals "^9.14.0" + ignore "^3.2.0" + imurmurhash "^0.1.4" + inquirer "^0.12.0" + is-my-json-valid "^2.10.0" + is-resolvable "^1.0.0" + js-yaml "^3.5.1" + json-stable-stringify "^1.0.0" + levn "^0.3.0" + lodash "^4.0.0" + mkdirp "^0.5.0" + natural-compare "^1.4.0" + optionator "^0.8.2" + path-is-inside "^1.0.1" + pluralize "^1.2.1" + progress "^1.1.8" + require-uncached "^1.0.2" + shelljs "^0.7.5" + strip-bom "^3.0.0" + strip-json-comments "~2.0.1" + table "^3.7.8" + text-table "~0.2.0" + user-home "^2.0.0" + +espree@3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.1.4.tgz#0726d7ac83af97a7c8498da9b363a3609d2a68a1" + dependencies: + acorn "^3.1.0" + acorn-jsx "^3.0.0" + +espree@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.4.0.tgz#41656fa5628e042878025ef467e78f125cb86e1d" + dependencies: + acorn "4.0.4" + acorn-jsx "^3.0.0" + +esprima@2.7.x, esprima@^2.6.0, esprima@^2.7.1: + version "2.7.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" + +esrecurse@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.1.0.tgz#4713b6536adf7f2ac4f327d559e7756bff648220" + dependencies: + estraverse "~4.1.0" + object-assign "^4.0.1" + +estraverse@^1.9.1: + version "1.9.3" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" + +estraverse@^4.1.1, estraverse@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + +estraverse@~4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.1.1.tgz#f6caca728933a850ef90661d0e17982ba47111a2" + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +etag@~1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8" + +event-emitter@~0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.4.tgz#8d63ddfb4cfe1fae3b32ca265c4c720222080bb5" + dependencies: + d "~0.1.1" + es5-ext "~0.10.7" + +eventemitter2@~0.4.13: + version "0.4.14" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-0.4.14.tgz#8f61b75cde012b2e9eb284d4545583b5643b61ab" + +exit-hook@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" + +exit@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + +express-handlebars@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/express-handlebars/-/express-handlebars-1.2.2.tgz#945530aea626f0b87130dc8dbea2b6cea9c43460" + dependencies: + glob "^5.0.0" + graceful-fs "^3.0.2" + handlebars "^2.0.0" + promise "^6.0.0" + semver "^3.0.1" + +express-session@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.15.0.tgz#67131dd5b78a42bc57b50af0a14880265c03f919" + dependencies: + cookie "0.3.1" + cookie-signature "1.0.6" + crc "3.4.4" + debug "2.6.0" + depd "~1.1.0" + on-headers "~1.0.1" + parseurl "~1.3.1" + uid-safe "~2.1.3" + utils-merge "1.0.0" + +express@4.14.0: + version "4.14.0" + resolved "https://registry.yarnpkg.com/express/-/express-4.14.0.tgz#c1ee3f42cdc891fb3dc650a8922d51ec847d0d66" + dependencies: + accepts "~1.3.3" + array-flatten "1.1.1" + content-disposition "0.5.1" + content-type "~1.0.2" + cookie "0.3.1" + cookie-signature "1.0.6" + debug "~2.2.0" + depd "~1.1.0" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.7.0" + finalhandler "0.5.0" + fresh "0.3.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.1" + path-to-regexp "0.1.7" + proxy-addr "~1.1.2" + qs "6.2.0" + range-parser "~1.2.0" + send "0.14.1" + serve-static "~1.11.1" + type-is "~1.6.13" + utils-merge "1.0.0" + vary "~1.1.0" + +extend@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4" + +extsprintf@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" + +fast-levenshtein@~2.0.4: + version "2.0.5" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.5.tgz#bd33145744519ab1c36c3ee9f31f08e9079b67f2" + +figures@^1.3.5: + version "1.7.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + +file-entry-cache@^1.1.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-1.3.1.tgz#44c61ea607ae4be9c1402f41f44270cbfe334ff8" + dependencies: + flat-cache "^1.2.1" + object-assign "^4.0.1" + +file-entry-cache@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" + dependencies: + flat-cache "^1.2.1" + object-assign "^4.0.1" + +finalhandler@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-0.5.0.tgz#e9508abece9b6dba871a6942a1d7911b91911ac7" + dependencies: + debug "~2.2.0" + escape-html "~1.0.3" + on-finished "~2.3.0" + statuses "~1.3.0" + unpipe "~1.0.0" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +findup-sync@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.3.0.tgz#37930aa5d816b777c03445e1966cc6790a4c0b16" + dependencies: + glob "~5.0.0" + +flat-cache@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.1.tgz#6c837d6225a7de5659323740b36d5361f71691ff" + dependencies: + circular-json "^0.3.0" + del "^2.0.2" + graceful-fs "^4.1.2" + write "^0.2.1" + +fluid-eslint@^2.0.0: + version "2.10.2" + resolved "https://registry.yarnpkg.com/fluid-eslint/-/fluid-eslint-2.10.2.tgz#fe76984b0a2d14c650fdd0d1328c3ccb9113f9ec" + dependencies: + chalk "^1.1.3" + concat-stream "^1.4.6" + debug "^2.1.1" + doctrine "^1.2.1" + es6-map "^0.1.3" + escope "^3.6.0" + espree "3.1.4" + estraverse "^4.2.0" + esutils "^2.0.2" + file-entry-cache "^1.1.1" + glob "^7.0.3" + globals "^9.2.0" + ignore "^3.1.2" + imurmurhash "^0.1.4" + inquirer "^0.12.0" + is-my-json-valid "^2.10.0" + is-resolvable "^1.0.0" + js-yaml "^3.5.1" + json-stable-stringify "^1.0.0" + lodash "^4.0.0" + mkdirp "^0.5.0" + optionator "^0.8.1" + path-is-absolute "^1.0.0" + path-is-inside "^1.0.1" + pluralize "^1.2.1" + progress "^1.1.8" + require-uncached "^1.0.2" + shelljs "^0.6.0" + strip-json-comments "~1.0.1" + table "^3.7.8" + text-table "~0.2.0" + user-home "^2.0.0" + +fluid-grunt-eslint@18.1.2: + version "18.1.2" + resolved "https://registry.yarnpkg.com/fluid-grunt-eslint/-/fluid-grunt-eslint-18.1.2.tgz#fffa645d468a432090458ed055dd486bb9437f6c" + dependencies: + chalk "^1.0.0" + fluid-eslint "^2.0.0" + +fluid-resolve@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fluid-resolve/-/fluid-resolve-1.2.0.tgz#9bf0f3e47a84d591d4ccc5fcda4909dfaea23590" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.2.tgz#89c3534008b97eada4cbb157d58f6f5df025eae4" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + +forwarded@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" + +foundation-sites@6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/foundation-sites/-/foundation-sites-6.3.0.tgz#19b2d6e5edced8418a4a7b4a1d65287ee1c8d512" + dependencies: + jquery "^2.2.0" + what-input "^4.0.3" + +fresh@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.3.0.tgz#651f838e22424e7566de161d8358caa199f83d4f" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +generate-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74" + +generate-object-property@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" + dependencies: + is-property "^1.0.0" + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + +getobject@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/getobject/-/getobject-0.1.0.tgz#047a449789fa160d018f5486ed91320b6ec7885c" + +getpass@^0.1.1: + version "0.1.6" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.6.tgz#283ffd9fc1256840875311c1b60e8c40187110e6" + dependencies: + assert-plus "^1.0.0" + +glob@^5.0.0, glob@^5.0.15, glob@~5.0.0: + version "5.0.15" + resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@~7.0.0: + version "7.0.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.6.tgz#211bafaf49e525b8cd93260d14ab136152b3f57a" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^9.14.0, globals@^9.2.0: + version "9.14.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.14.0.tgz#8859936af0038741263053b39d0e76ca241e4034" + +globby@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" + dependencies: + array-union "^1.0.1" + arrify "^1.0.0" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +gpii-binder@1.0.0-dev.20170127T144054Z.54a5ef2: + version "1.0.0-dev.20170127T144054Z.54a5ef2" + resolved "https://registry.yarnpkg.com/gpii-binder/-/gpii-binder-1.0.0-dev.20170127T144054Z.54a5ef2.tgz#a56985dcfdbf0a52970a9b42fe3f39d375a50d63" + dependencies: + infusion "3.0.0-dev.20170127T130413Z.103de6e" + +gpii-express@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/gpii-express/-/gpii-express-1.0.2.tgz#be71f0d453c536b66de1b86af0056831ed738105" + dependencies: + body-parser "1.16.0" + cookie-parser "1.4.3" + express "4.14.0" + express-session "1.15.0" + infusion "3.0.0-dev.20170127T130413Z.103de6e" + +gpii-handlebars@1.0.0-dev.20170127T160953Z.4d15347: + version "1.0.0-dev.20170127T160953Z.4d15347" + resolved "https://registry.yarnpkg.com/gpii-handlebars/-/gpii-handlebars-1.0.0-dev.20170127T160953Z.4d15347.tgz#bc3296bc9c9eb5136f8799ea3bf7291505106175" + dependencies: + express-handlebars "1.2.2" + gpii-binder "1.0.0-dev.20170127T144054Z.54a5ef2" + gpii-express "1.0.2" + handlebars "4.0.6" + infusion "3.0.0-dev.20170127T130413Z.103de6e" + marked "0.3.6" + pagedown "1.1.0" + underscore-node "0.1.2" + +gpii-webdriver@1.0.0-dev.20170127T135806Z.aa30519: + version "1.0.0-dev.20170127T135806Z.aa30519" + resolved "https://registry.yarnpkg.com/gpii-webdriver/-/gpii-webdriver-1.0.0-dev.20170127T135806Z.aa30519.tgz#5fab3f1276c33005924b3d23417556ff9e74447c" + dependencies: + accessibility-developer-tools "2.11.0" + axe-core "2.1.7" + infusion "3.0.0-dev.20170127T130413Z.103de6e" + selenium-webdriver "3.0.1" + +graceful-fs@^3.0.2: + version "3.0.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-3.0.11.tgz#7613c778a1afea62f25c630a086d7f3acbbdd818" + dependencies: + natives "^1.1.0" + +graceful-fs@^4.1.2: + version "4.1.9" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.9.tgz#baacba37d19d11f9d146d3578bc99958c3787e29" + +"graceful-readlink@>= 1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" + +grunt-cli@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/grunt-cli/-/grunt-cli-1.2.0.tgz#562b119ebb069ddb464ace2845501be97b35b6a8" + dependencies: + findup-sync "~0.3.0" + grunt-known-options "~1.1.0" + nopt "~3.0.6" + resolve "~1.1.0" + +grunt-jsonlint@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/grunt-jsonlint/-/grunt-jsonlint-1.1.0.tgz#a31ee97240aee3f343ca263c45bd532063127db2" + dependencies: + jsonlint "1.6.2" + strip-json-comments "^2.0.0" + +grunt-known-options@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/grunt-known-options/-/grunt-known-options-1.1.0.tgz#a4274eeb32fa765da5a7a3b1712617ce3b144149" + +grunt-legacy-log-utils@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/grunt-legacy-log-utils/-/grunt-legacy-log-utils-1.0.0.tgz#a7b8e2d0fb35b5a50f4af986fc112749ebc96f3d" + dependencies: + chalk "~1.1.1" + lodash "~4.3.0" + +grunt-legacy-log@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/grunt-legacy-log/-/grunt-legacy-log-1.0.0.tgz#fb86f1809847bc07dc47843f9ecd6cacb62df2d5" + dependencies: + colors "~1.1.2" + grunt-legacy-log-utils "~1.0.0" + hooker "~0.2.3" + lodash "~3.10.1" + underscore.string "~3.2.3" + +grunt-legacy-util@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/grunt-legacy-util/-/grunt-legacy-util-1.0.0.tgz#386aa78dc6ed50986c2b18957265b1b48abb9b86" + dependencies: + async "~1.5.2" + exit "~0.1.1" + getobject "~0.1.0" + hooker "~0.2.3" + lodash "~4.3.0" + underscore.string "~3.2.3" + which "~1.2.1" + +grunt@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/grunt/-/grunt-1.0.1.tgz#e8778764e944b18f32bb0f10b9078475c9dfb56b" + dependencies: + coffee-script "~1.10.0" + dateformat "~1.0.12" + eventemitter2 "~0.4.13" + exit "~0.1.1" + findup-sync "~0.3.0" + glob "~7.0.0" + grunt-cli "~1.2.0" + grunt-known-options "~1.1.0" + grunt-legacy-log "~1.0.0" + grunt-legacy-util "~1.0.0" + iconv-lite "~0.4.13" + js-yaml "~3.5.2" + minimatch "~3.0.0" + nopt "~3.0.6" + path-is-absolute "~1.0.0" + rimraf "~2.2.8" + +handlebars@4.0.6, handlebars@^4.0.1: + version "4.0.6" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.6.tgz#2ce4484850537f9c97a8026d5399b935c4ed4ed7" + dependencies: + async "^1.4.0" + optimist "^0.6.1" + source-map "^0.4.4" + optionalDependencies: + uglify-js "^2.6" + +handlebars@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-2.0.0.tgz#6e9d7f8514a3467fa5e9f82cc158ecfc1d5ac76f" + dependencies: + optimist "~0.3" + optionalDependencies: + uglify-js "~2.3" + +har-validator@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" + dependencies: + chalk "^1.1.1" + commander "^2.9.0" + is-my-json-valid "^2.12.4" + pinkie-promise "^2.0.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-color@~0.1.0: + version "0.1.7" + resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f" + +has-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + +hawk@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + dependencies: + boom "2.x.x" + cryptiles "2.x.x" + hoek "2.x.x" + sntp "1.x.x" + +hoek@2.x.x: + version "2.16.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + +hooker@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/hooker/-/hooker-0.2.3.tgz#b834f723cc4a242aa65963459df6d984c5d3d959" + +hosted-git-info@^2.1.4: + version "2.1.5" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.1.5.tgz#0ba81d90da2e25ab34a332e6ec77936e1598118b" + +http-errors@~1.5.0, http-errors@~1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750" + dependencies: + inherits "2.0.3" + setprototypeof "1.0.2" + statuses ">= 1.3.1 < 2" + +http-signature@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" + dependencies: + assert-plus "^0.2.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +iconv-lite@0.4.15: + version "0.4.15" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb" + +iconv-lite@~0.4.13: + version "0.4.13" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" + +ignore@^3.1.2, ignore@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.0.tgz#8d88f03c3002a0ac52114db25d2c673b0bf1e435" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + dependencies: + repeating "^2.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +infusion@2.0.0-dev.20151130T180545Z.18ee3f8: + version "2.0.0-dev.20151130T180545Z.18ee3f8" + resolved "https://registry.yarnpkg.com/infusion/-/infusion-2.0.0-dev.20151130T180545Z.18ee3f8.tgz#07ade23574f012178be2218a8f5141bab14bdaa2" + dependencies: + resolve "1.1.6" + +infusion@3.0.0-dev.20170127T130413Z.103de6e: + version "3.0.0-dev.20170127T130413Z.103de6e" + resolved "https://registry.yarnpkg.com/infusion/-/infusion-3.0.0-dev.20170127T130413Z.103de6e.tgz#59d5163cd6c55d4c5b3c34af54b74fdb331b1a1d" + dependencies: + fluid-resolve "1.2.0" + +infusion@3.0.0-dev.20170131T153243Z.6aab53a: + version "3.0.0-dev.20170131T153243Z.6aab53a" + resolved "https://registry.yarnpkg.com/infusion/-/infusion-3.0.0-dev.20170131T153243Z.6aab53a.tgz#79080e54636b9ede32ac6a69ceb18ab6fd248a19" + dependencies: + fluid-resolve "1.2.0" + +inherits@2, inherits@2.0.3, inherits@~2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +inquirer@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" + dependencies: + ansi-escapes "^1.1.0" + ansi-regex "^2.0.0" + chalk "^1.0.0" + cli-cursor "^1.0.1" + cli-width "^2.0.0" + figures "^1.3.5" + lodash "^4.3.0" + readline2 "^1.0.1" + run-async "^0.1.0" + rx-lite "^3.1.2" + string-width "^1.0.1" + strip-ansi "^3.0.0" + through "^2.3.6" + +interpret@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c" + +ipaddr.js@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.1.1.tgz#c791d95f52b29c1247d5df80ada39b8a73647230" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + +is-buffer@^1.0.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.4.tgz#cfc86ccd5dc5a52fa80489111c6920c457e2d98b" + +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + dependencies: + builtin-modules "^1.0.0" + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4: + version "2.15.0" + resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz#936edda3ca3c211fd98f3b2d3e08da43f7b2915b" + dependencies: + generate-function "^2.0.0" + generate-object-property "^1.1.0" + jsonpointer "^4.0.0" + xtend "^4.0.0" + +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + +is-path-in-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" + dependencies: + is-path-inside "^1.0.0" + +is-path-inside@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f" + dependencies: + path-is-inside "^1.0.1" + +is-property@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + +is-resolvable@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62" + dependencies: + tryit "^1.0.1" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + +isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isexe@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +istanbul@0.4.5: + version "0.4.5" + resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-0.4.5.tgz#65c7d73d4c4da84d4f3ac310b918fb0b8033733b" + dependencies: + abbrev "1.0.x" + async "1.x" + escodegen "1.8.x" + esprima "2.7.x" + glob "^5.0.15" + handlebars "^4.0.1" + js-yaml "3.x" + mkdirp "0.5.x" + nopt "3.x" + once "1.x" + resolve "1.1.x" + supports-color "^3.1.0" + which "^1.1.1" + wordwrap "^1.0.0" + +jodid25519@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/jodid25519/-/jodid25519-1.0.2.tgz#06d4912255093419477d425633606e0e90782967" + dependencies: + jsbn "~0.1.0" + +jquery@^2.2.0: + version "2.2.4" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.2.4.tgz#2c89d6889b5eac522a7eea32c14521559c6cbf02" + +js-tokens@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-2.0.0.tgz#79903f5563ee778cc1162e6dcf1a0027c97f9cb5" + +js-yaml@3.x, js-yaml@^3.5.1, js-yaml@^3.6.0: + version "3.6.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.6.1.tgz#6e5fe67d8b205ce4d22fad05b7781e8dadcc4b30" + dependencies: + argparse "^1.0.7" + esprima "^2.6.0" + +js-yaml@~3.5.2: + version "3.5.5" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.5.5.tgz#0377c38017cabc7322b0d1fbcd25a491641f2fbe" + dependencies: + argparse "^1.0.2" + esprima "^2.6.0" + +jsbn@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.0.tgz#650987da0dd74f4ebf5a11377a2aa2d273e97dfd" + +json-schema-ref-parser@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/json-schema-ref-parser/-/json-schema-ref-parser-3.1.2.tgz#a38ecb7774f87f32e7eb9723d5921390e76a9a42" + dependencies: + call-me-maybe "^1.0.1" + debug "^2.2.0" + es6-promise "^3.1.2" + js-yaml "^3.6.0" + ono "^2.2.1" + z-schema "^3.17.0" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + dependencies: + jsonify "~0.0.0" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +json5@0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + +jsonlint@1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/jsonlint/-/jsonlint-1.6.2.tgz#5737045085f55eb455c68b1ff4ebc01bd50e8830" + dependencies: + JSV ">= 4.0.x" + nomnom ">= 1.5.x" + +jsonpointer.js@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/jsonpointer.js/-/jsonpointer.js-0.4.0.tgz#002cb123f767aafdeb0196132ce5c4f9941ccaba" + +jsonpointer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.0.tgz#6661e161d2fc445f19f98430231343722e1fcbd5" + +jsprim@^1.2.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.3.1.tgz#2a7256f70412a29ee3670aaca625994c4dcff252" + dependencies: + extsprintf "1.0.2" + json-schema "0.2.3" + verror "1.3.6" + +kettle@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/kettle/-/kettle-1.3.2.tgz#fecf522e290c93a7e50dbd4a9fabf9b57d4ade3f" + dependencies: + body-parser "1.16.0" + cookie-parser "1.4.3" + express "4.14.0" + express-session "1.15.0" + fluid-resolve "1.2.0" + infusion "3.0.0-dev.20170131T153243Z.6aab53a" + json5 "0.5.1" + jsonlint "1.6.2" + path-to-regexp "1.7.0" + serve-static "1.11.2" + ws "1.1.1" + +kew@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b" + +kind-of@^3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.0.4.tgz#7b8ecf18a4e17f8269d73b501c9f232c96887a74" + dependencies: + is-buffer "^1.0.2" + +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +lodash.get@^4.1.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + +lodash@^4.0.0, lodash@^4.3.0: + version "4.16.4" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.16.4.tgz#01ce306b9bad1319f2a5528674f88297aeb70127" + +lodash@~3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" + +lodash@~4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.3.0.tgz#efd9c4a6ec53f3b05412429915c3e4824e4d25a4" + +longest@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + +marked@0.3.6: + version "0.3.6" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.6.tgz#b2c6c618fccece4ef86c4fc6cb8a7cbf5aeda8d7" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + +meow@^3.3.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + +mime-db@~1.24.0: + version "1.24.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.24.0.tgz#e2d13f939f0016c6e4e9ad25a8652f126c467f0c" + +mime-db@~1.26.0: + version "1.26.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.26.0.tgz#eaffcd0e4fc6935cf8134da246e2e6c35305adff" + +mime-types@^2.1.12, mime-types@~2.1.7: + version "2.1.12" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.12.tgz#152ba256777020dd4663f54c2e7bc26381e71729" + dependencies: + mime-db "~1.24.0" + +mime-types@~2.1.11, mime-types@~2.1.13: + version "2.1.14" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.14.tgz#f7ef7d97583fcaf3b7d282b6f8b5679dab1e94ee" + dependencies: + mime-db "~1.26.0" + +mime@1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" + +"minimatch@2 || 3", minimatch@^3.0.2, minimatch@~3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" + dependencies: + brace-expansion "^1.0.0" + +minimist@0.0.8, minimist@~0.0.1: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minimist@^1.1.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +ms@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" + +ms@0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" + +mute-stream@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" + +natives@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.0.tgz#e9ff841418a6b2ec7a495e939984f78f163e6e31" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + +negotiator@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + +node-jqunit@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/node-jqunit/-/node-jqunit-1.1.4.tgz#682956a4adab3258e1da9b6d98cac488ae122744" + dependencies: + infusion "2.0.0-dev.20151130T180545Z.18ee3f8" + +"nomnom@>= 1.5.x": + version "1.8.1" + resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7" + dependencies: + chalk "~0.4.0" + underscore "~1.6.0" + +nopt@3.x, nopt@~3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" + dependencies: + abbrev "1" + +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: + version "2.3.5" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.3.5.tgz#8d924f142960e1777e7ffe170543631cc7cb02df" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +oauth-sign@~0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + +object-assign@^4.0.1, object-assign@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" + +once@1.x, once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +onetime@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + +ono@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ono/-/ono-2.2.1.tgz#722fc7e8429ff733887a80e743fb38bbd985b003" + +optimist@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +optimist@~0.3, optimist@~0.3.5: + version "0.3.7" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.3.7.tgz#c90941ad59e4273328923074d2cf2e7cbc6ec0d9" + dependencies: + wordwrap "~0.0.2" + +optionator@^0.8.1, optionator@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.4" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + wordwrap "~1.0.0" + +options@>=0.0.5: + version "0.0.6" + resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-tmpdir@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +pagedown@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pagedown/-/pagedown-1.1.0.tgz#2b46c119d5d5b583cc3c9210a000b3d40e4e6f80" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + dependencies: + error-ex "^1.2.0" + +parseurl@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + dependencies: + pinkie-promise "^2.0.0" + +path-is-absolute@^1.0.0, path-is-absolute@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-is-inside@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + +path-to-regexp@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + dependencies: + isarray "0.0.1" + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +pluralize@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + +progress@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" + +promise@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/promise/-/promise-6.1.0.tgz#2ce729f6b94b45c26891ad0602c5c90e04c6eef6" + dependencies: + asap "~1.0.0" + +proxy-addr@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.2.tgz#b4cc5f22610d9535824c123aef9d3cf73c40ba37" + dependencies: + forwarded "~0.1.0" + ipaddr.js "1.1.1" + +qs@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.0.tgz#3b7848c03c2dece69a9522b0fae8c4126d745f3b" + +qs@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.1.tgz#ce03c5ff0935bc1d9d69a9f14cbd18e568d67625" + +qs@~6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442" + +quoted-printable@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/quoted-printable/-/quoted-printable-1.0.1.tgz#9eebf5eb3d11eef022b264fd2d2b6b2bb3b84cc3" + dependencies: + utf8 "^2.1.0" + +random-bytes@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" + +range-parser@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + +raw-body@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.2.0.tgz#994976cf6a5096a41162840492f0bdc5d6e7fb96" + dependencies: + bytes "2.4.0" + iconv-lite "0.4.15" + unpipe "1.0.0" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +readable-stream@~2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + +readline2@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + mute-stream "0.0.5" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + dependencies: + resolve "^1.1.6" + +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + +repeat-string@^1.5.2: + version "1.5.4" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.5.4.tgz#64ec0c91e0f4b475f90d5b643651e3e6e5b6c2d5" + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + dependencies: + is-finite "^1.0.0" + +request@2.79.0: + version "2.79.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.11.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~2.0.6" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + oauth-sign "~0.8.1" + qs "~6.3.0" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "~0.4.1" + uuid "^3.0.0" + +require-uncached@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.2.tgz#67dad3b733089e77030124678a459589faf6a7ec" + dependencies: + caller-path "^0.1.0" + resolve-from "^1.0.0" + +resolve-from@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" + +resolve@1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.6.tgz#d3492ad054ca800f5befa612e61beac1eec98f8f" + +resolve@1.1.x, resolve@^1.1.6, resolve@~1.1.0: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + +restore-cursor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" + dependencies: + exit-hook "^1.0.0" + onetime "^1.0.0" + +right-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" + dependencies: + align-text "^0.1.1" + +rimraf@^2.2.8, rimraf@^2.5.4: + version "2.5.4" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" + dependencies: + glob "^7.0.5" + +rimraf@~2.2.8: + version "2.2.8" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" + +run-async@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" + dependencies: + once "^1.3.0" + +rx-lite@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" + +sax@>=0.6.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" + +selenium-webdriver@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-3.0.1.tgz#a2dea5da4a97f6672e89e7ca7276cefa365147a7" + dependencies: + adm-zip "^0.4.7" + rimraf "^2.5.4" + tmp "0.0.30" + xml2js "^0.4.17" + +"semver@2 || 3 || 4 || 5", semver@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-3.0.1.tgz#720ac012515a252f91fb0dd2e99a56a70d6cf078" + +send@0.14.1: + version "0.14.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.14.1.tgz#a954984325392f51532a7760760e459598c89f7a" + dependencies: + debug "~2.2.0" + depd "~1.1.0" + destroy "~1.0.4" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.7.0" + fresh "0.3.0" + http-errors "~1.5.0" + mime "1.3.4" + ms "0.7.1" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.3.0" + +send@0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.14.2.tgz#39b0438b3f510be5dc6f667a11f71689368cdeef" + dependencies: + debug "~2.2.0" + depd "~1.1.0" + destroy "~1.0.4" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.7.0" + fresh "0.3.0" + http-errors "~1.5.1" + mime "1.3.4" + ms "0.7.2" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.3.1" + +serve-static@1.11.2, serve-static@~1.11.1: + version "1.11.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.11.2.tgz#2cf9889bd4435a320cc36895c9aa57bd662e6ac7" + dependencies: + encodeurl "~1.0.1" + escape-html "~1.0.3" + parseurl "~1.3.1" + send "0.14.2" + +setprototypeof@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.2.tgz#81a552141ec104b88e89ce383103ad5c66564d08" + +shelljs@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.6.1.tgz#ec6211bed1920442088fe0f70b2837232ed2c8a8" + +shelljs@^0.7.5: + version "0.7.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.5.tgz#2eef7a50a21e1ccf37da00df767ec69e30ad0675" + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +signal-exit@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.1.tgz#5a4c884992b63a7acd9badb7894c3ee9cfccad81" + +slice-ansi@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" + +sntp@1.x.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + dependencies: + hoek "2.x.x" + +source-map@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" + dependencies: + amdefine ">=0.0.4" + +source-map@~0.1.7: + version "0.1.43" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" + dependencies: + amdefine ">=0.0.4" + +source-map@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d" + dependencies: + amdefine ">=0.0.4" + +source-map@~0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" + +spdx-correct@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" + dependencies: + spdx-license-ids "^1.0.2" + +spdx-expression-parse@~1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" + +spdx-license-ids@^1.0.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +sshpk@^1.7.0: + version "1.10.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.10.1.tgz#30e1a5d329244974a1af61511339d595af6638b0" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jodid25519 "^1.0.0" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +"statuses@>= 1.3.1 < 2", statuses@~1.3.0, statuses@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.0.0.tgz#635c5436cc72a6e0c387ceca278d4e2eec52687e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^3.0.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + +stringstream@~0.0.4: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" + +strip-ansi@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + dependencies: + is-utf8 "^0.2.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + dependencies: + get-stdin "^4.0.1" + +strip-json-comments@^2.0.0, strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +strip-json-comments@~1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@^3.1.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" + dependencies: + has-flag "^1.0.0" + +table@^3.7.8: + version "3.8.3" + resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" + dependencies: + ajv "^4.7.0" + ajv-keywords "^1.0.0" + chalk "^1.1.1" + lodash "^4.0.0" + slice-ansi "0.0.4" + string-width "^2.0.0" + +text-table@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +tmp@0.0.30: + version "0.0.30" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.30.tgz#72419d4a8be7d6ce75148fd8b324e593a711c2ed" + dependencies: + os-tmpdir "~1.0.1" + +tough-cookie@~2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.1.tgz#99c77dfbb7d804249e8a299d4cb0fd81fef083fd" + +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + +tryit@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.2.tgz#c196b0073e6b1c595d93c9c830855b7acc32a453" + +tunnel-agent@~0.4.1: + version "0.4.3" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.3" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.3.tgz#3da382f670f25ded78d7b3d1792119bca0b7132d" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + dependencies: + prelude-ls "~1.1.2" + +type-is@~1.6.13, type-is@~1.6.14: + version "1.6.14" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.14.tgz#e219639c17ded1ca0789092dd54a03826b817cb2" + dependencies: + media-typer "0.3.0" + mime-types "~2.1.13" + +typedarray@~0.0.5: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +uglify-js@^2.6: + version "2.7.3" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.7.3.tgz#39b3a7329b89f5ec507e344c6e22568698ef4868" + dependencies: + async "~0.2.6" + source-map "~0.5.1" + uglify-to-browserify "~1.0.0" + yargs "~3.10.0" + +uglify-js@~2.3: + version "2.3.6" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.3.6.tgz#fa0984770b428b7a9b2a8058f46355d14fef211a" + dependencies: + async "~0.2.6" + optimist "~0.3.5" + source-map "~0.1.7" + +uglify-to-browserify@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" + +uid-safe@~2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.3.tgz#077e264a00b3187936b270bb7376a26473631071" + dependencies: + base64-url "1.3.3" + random-bytes "~1.0.0" + +ultron@1.0.x: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa" + +underscore-node@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/underscore-node/-/underscore-node-0.1.2.tgz#0ce8c7a5737ca6dc8e71315dc4da246949d48278" + +underscore.string@~3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-3.2.3.tgz#806992633665d5e5fcb4db1fb3a862eb68e9e6da" + +underscore@~1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8" + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + +user-home@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" + dependencies: + os-homedir "^1.0.0" + +utf8@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/utf8/-/utf8-2.1.2.tgz#1fa0d9270e9be850d9b05027f63519bf46457d96" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +utils-merge@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" + +uuid@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.0.tgz#6728fc0459c450d796a99c31837569bdf672d728" + +validate-npm-package-license@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + dependencies: + spdx-correct "~1.0.0" + spdx-expression-parse "~1.0.0" + +validator@^5.0.0: + version "5.7.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-5.7.0.tgz#7a87a58146b695ac486071141c0c49d67da05e5c" + +vary@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.0.tgz#e1e5affbbd16ae768dd2674394b9ad3022653140" + +verror@1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c" + dependencies: + extsprintf "1.0.2" + +what-input@^4.0.3: + version "4.0.6" + resolved "https://registry.yarnpkg.com/what-input/-/what-input-4.0.6.tgz#bbe3cd1e1eb5ee16e441c08af7294740f94614ec" + +which@^1.1.1, which@~1.2.1: + version "1.2.11" + resolved "https://registry.yarnpkg.com/which/-/which-1.2.11.tgz#c8b2eeea6b8c1659fa7c1dd4fdaabe9533dc5e8b" + dependencies: + isexe "^1.1.1" + +window-size@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" + +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + +wordwrap@^1.0.0, wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +write@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" + dependencies: + mkdirp "^0.5.1" + +ws@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.1.tgz#082ddb6c641e85d4bb451f03d52f06eabdb1f018" + dependencies: + options ">=0.0.5" + ultron "1.0.x" + +xml2js@^0.4.17: + version "0.4.17" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.17.tgz#17be93eaae3f3b779359c795b419705a8817e868" + dependencies: + sax ">=0.6.0" + xmlbuilder "^4.1.0" + +xmlbuilder@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-4.2.1.tgz#aa58a3041a066f90eaa16c2f5389ff19f3f461a5" + dependencies: + lodash "^4.0.0" + +xtend@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + +yargs@~3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" + dependencies: + camelcase "^1.0.2" + cliui "^2.1.0" + decamelize "^1.0.0" + window-size "0.1.0" + +z-schema@^3.17.0: + version "3.18.0" + resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-3.18.0.tgz#70d037a6c0de12df63cc121ce6ae96eeff7f8784" + dependencies: + lodash.get "^4.1.2" + validator "^5.0.0" + optionalDependencies: + commander "^2.7.1"