diff --git a/.travis.yml b/.travis.yml index 0fe294a..0c81086 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,3 @@ language: node_js node_js: - - "7" + - "16" diff --git a/LICENSE b/LICENSE index 52b3e98..aed4e0d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2019 Polybit Inc. +Copyright (c) 2021 Polybit Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index d219ac4..a0f3a84 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ![travis-ci build](https://travis-ci.org/FunctionScript/FunctionScript.svg?branch=master) ![npm version](https://badge.fury.io/js/functionscript.svg) -## Turn JavaScript Functions into Typed HTTP APIs +## An API gateway and framework for turning functions into web services FunctionScript is a language and specification for turning JavaScript functions into typed HTTP APIs. It allows JavaScript (Node.js) functions to be @@ -19,16 +19,8 @@ interfaces. For this reason, the goals of the language are significantly differe [TypeScript](https://github.com/microsoft/TypeScript). **FunctionScript is intended to provide an easy introduction to API development for those of any skill level, while maintaining professional power and flexibility.** -FunctionScript is the primary specification underpinning the [Standard Library](https://stdlib.com) -API development and integration platform. You can start building with FunctionScript **immediately** using -[Code on Standard Library](https://code.stdlib.com/?sample=t&filename=functions/__main__.js), right in -your web browser. An animated example has been provided below. - -**Note:** *In order to use Code on Standard Library you must have a registered account on [stdlib.com](https://stdlib.com), available for free.* - -https://code.stdlib.com/?sample=t&filename=functions/__main__.js - -[![Demo](/images/demo-api.gif)](https://code.stdlib.com/?sample=t&filename=functions/__main__.js) +FunctionScript is the primary specification underpinning the [Autocode](https://autocode.com) +platform and its standard library of APIs. ### Quick Example of a FunctionScript API @@ -89,7 +81,7 @@ It will return an `object`: ## Background The impetus for creating FunctionScript is simple: it stems from the initial -vision of [Standard Library](https://stdlib.com). We believe the modern web is +vision of [Autocode](https://autocode.com). We believe the modern web is missing a base primitive - the API. Daily, computer systems and developers around the planet make trillions of requests to perform specific tasks: process credit card payments with [Stripe](https://stripe.com), send team messages via @@ -107,11 +99,6 @@ software development instead of an afterthought. This allows teams to be able to deliver high-quality APIs with the same fidelity as organizations like Stripe in a fraction of the time without requiring any additional tooling. -FunctionScript has been developed by the team at Polybit Inc., responsible for -[Standard Library](https://stdlib.com). -Ongoing development is, in part, funded by both [Stripe](https://stripe.com) and -[Slack](https://slack.com) as venture investments in the parent organization. - # Table of Contents 1. [Introduction](#introduction) @@ -144,7 +131,7 @@ Ongoing development is, in part, funded by both [Stripe](https://stripe.com) and To put it simply, FunctionScript defines semantics and rules for turning exported JavaScript (Node.js) functions into strongly-typed, HTTP-accessible web APIs. In order to use FunctionScript, you'd set up your own [FunctionScript Gateway](#functionscript-server-and-gateway-implementation) or you would use an existing FunctionScript-compliant service -like [Standard Library](https://stdlib.com/). +like [Autocode](https://autocode.com/). FunctionScript allows you to turn something like this... @@ -195,7 +182,7 @@ Or, when a type mismatch occurs (like `{"name":10}`): ## Why FunctionScript? FunctionScript is intended primarily to provide a scaffold to build and deliver -APIs easily. It works best in conjunction with the [Standard Library](https://stdlib.com/) +APIs easily. It works best as a part of the [Autocode](https://autocode.com/) platform which consumes the FunctionScript API definitions, hosts the code, generates documentation from the definitions, and automatically handles versioning and environment management. The reason we've open sourced the language specification @@ -689,53 +676,26 @@ is available with this package, simply clone it and run `npm test` or look at the `/tests` folder for more information. The FunctionScript specification is used as the platform specification -for [Standard Library](https://stdlib.com), and is available for local use with the -[Standard Library CLI Package](https://github.com/stdlib/lib) which relies on this +for [Autocode](https://autocode.com), and is available for local use with the +[Autocode CLI](https://github.com/acode/cli) which relies on this repository as a dependency. # Acknowledgements FunctionScript is the result of years of concerted effort working to make API development easier. It would not be possible without the personal and financial -commitments of some very amazing people and companies. - -## Corporate Interests - -Via investments in Polybit Inc., parent of [Standard Library](https://stdlib.com), -the following companies have invested countless hours in and provided financial -support for our team, which has made this project possible. - -[Stripe](https://stripe.com), the global leader in online payments - -[![Stripe Logo](/images/stripe-logo-300.png)](https://stripe.com) - -[Slack](https://slack.com), the online platform for work and communication - -[![Slack Logo](/images/slack-logo-300.png)](https://slack.com) - -## Special Thanks - -There have been a number of helpful supporters and contributors along the way, -and FunctionScript would not be possible without any of them. +commitments of some very amazing people and companies. We'd like to thank our +customers, investors, supporters, friends and family. ### Core Contributors - [**Keith Horwood**](https://twitter.com/keithwhor) - [**Jacob Lee**](https://twitter.com/hacubu) - [**Steve Meyer**](https://twitter.com/notoriaga) - -### Friends and Supporters - -- [**Chad Fowler**](https://twitter.com/chadfowler) -- [**Bear Douglas**](https://twitter.com/beardigsit) -- [**Romain Huet**](https://twitter.com/romainhuet) -- [**Will Gaybrick**](https://twitter.com/gaybrick) -- [**Patrick Collison**](https://twitter.com/patrickc) -- [**Patrick McKenzie**](https://twitter.com/patio11) -- [**David Singleton**](https://twitter.com/dps) +- [**Yusuf Musleh**](https://twitter.com/yusuf-musleh) # Notes The software contained within this repository has been developed and is -copyrighted by the [Standard Library](https://stdlib.com) Team at Polybit Inc. +copyrighted by the [Autocode](https://autocode.com) team (Polybit Inc.) and is MIT licensed. diff --git a/images/demo-api.gif b/images/demo-api.gif deleted file mode 100644 index 49f69fc..0000000 Binary files a/images/demo-api.gif and /dev/null differ diff --git a/lib/background.js b/lib/background_validator.js similarity index 64% rename from lib/background.js rename to lib/background_validator.js index 2e05f19..ce632d2 100644 --- a/lib/background.js +++ b/lib/background_validator.js @@ -1,9 +1,9 @@ module.exports = { modes: { - 'info': (definition, params) => Buffer.from(`Initiated ${definition.name}...`), + 'info': (definition, params) => Buffer.from(`initiated "${definition.name}"...`), 'empty': (definition, params) => Buffer.from([]), 'params': (definition, params) => { - let specifiedBgParams = definition.bg.value + let specifiedBgParams = definition.background.value .split(/\s+/) .filter(param => !!param); return !specifiedBgParams.length @@ -16,11 +16,5 @@ module.exports = { }, {}); } }, - defaultMode: 'info', - generateDefaultValue: function () { - return { - mode: this.defaultMode, - value: '' - }; - } + defaultMode: 'info' }; diff --git a/lib/daemon.js b/lib/daemon.js index 347bfbd..996269c 100644 --- a/lib/daemon.js +++ b/lib/daemon.js @@ -2,6 +2,7 @@ const cluster = require('cluster'); const os = require('os'); const http = require('http'); const fs = require('fs'); +const path = require('path'); const Gateway = require('./gateway.js'); @@ -21,6 +22,8 @@ class Daemon { this._server = null; this._port = null; + this._paused = false; + this.cpus = parseInt(cpus) || os.cpus().length; this.children = []; @@ -46,15 +49,12 @@ class Daemon { if ((process.env.NODE_ENV || 'development') === 'development') { this.watch('', (changes) => { - changes.forEach(change => { console.log(`[${this.name}.Daemon] ${change.event[0].toUpperCase()}${change.event.substr(1)}: ${change.path}`); }); - this.children.forEach(child => child.send({invalidate: true})); this.children = []; !this.children.length && this.unwatch() && this.start(); - }); } @@ -103,7 +103,9 @@ class Daemon { message(data) { - data.error && this.logError(data.error); + if (data.error) { + this.logError(data.error); + } } @@ -161,12 +163,14 @@ class Daemon { /** * Watches a directory tree for changes - * @param {string} path Directory tree to watch + * @param {string} root Directory tree to watch * @param {function} onChange Method to be executed when a change is detected */ - watch(path, onChange) { + watch (root, onChange) { + + let cwd = process.cwd(); - function watchDir(cwd, dirname, watchers) { + function watchDir (dirname, watchers) { if (!watchers) { @@ -176,28 +180,28 @@ class Daemon { } - let path = [cwd, dirname].join(''); - let files = fs.readdirSync(path); + let pathname = path.join(cwd, dirname); + let files = fs.readdirSync(pathname); - watchers.directories[path] = Object.create(null); + watchers.directories[dirname] = Object.create(null); - files.forEach(function(v) { + files.forEach(function (name) { - if (v === 'node_modules' || v.indexOf('.') === 0) { + if (name === 'node_modules' || name.indexOf('.') === 0) { return; } - let filename = [dirname, v].join('/'); - let fullPath = [cwd, filename].join('/'); + let filename = path.join(dirname, name); + let fullPath = path.join(cwd, filename); let stat = fs.statSync(fullPath); if (stat.isDirectory()) { - watchDir(cwd, filename, watchers); + watchDir(filename, watchers); return; } - watchers.directories[path][v] = stat; + watchers.directories[dirname][name] = stat; }); @@ -205,71 +209,93 @@ class Daemon { } - let watchers = watchDir(process.cwd(), path || ''); + let watchers = watchDir(root || ''); let self = this; - watchers.iterate = function(changes) { - - if (changes.length) { - onChange.call(self, changes); + watchers.iterate = function (changes) { + if (!fs.existsSync(path.join(process.cwd(), '.daemon.pause'))) { + // Skip a cycle if just unpaused... + if (!this._paused) { + if (changes.length) { + onChange.call(self, changes); + } + } else { + this._paused = false; + } + } else { + this._paused = true; } - }; watchers.interval = setInterval(function() { let changes = []; - Object.keys(watchers.directories).forEach(function(dirPath) { + Object.keys(watchers.directories).forEach(function (dirname) { - let dir = watchers.directories[dirPath]; - let files = fs.readdirSync(dirPath); - let added = []; + let dir = watchers.directories[dirname]; + let dirPath = path.join(cwd, dirname); - let contents = Object.create(null); + if (!fs.existsSync(dirPath)) { - files.forEach(function(v) { + delete watchers.directories[dirname]; + changes.push({event: 'removed', path: dirPath}); - if (v === 'node_modules' || v.indexOf('.') === 0) { - return; - } + } else { - let fullPath = [dirPath, v].join('/'); - let stat = fs.statSync(fullPath); + let files = fs.readdirSync(dirPath); + let added = []; - if (stat.isDirectory()) { - return; - } + let contents = Object.create(null); - if (!dir[v]) { - added.push([v, stat]); - changes.push({event: 'added', path: fullPath}); - return; - } + files.forEach(function (filename) { - if (stat.mtime.toString() !== dir[v].mtime.toString()) { - dir[v] = stat; - changes.push({event: 'modified', path: fullPath}); - } + if (filename === 'node_modules' || filename.indexOf('.') === 0) { + return; + } - contents[v] = true; + let fullPath = path.join(dirPath, filename); + let stat = fs.statSync(fullPath); - }); + if (stat.isDirectory()) { + let checkPath = path.join(dirname, filename); + if (!watchers.directories[checkPath]) { + watchDir(checkPath, watchers); + } + } else { + if (!dir[filename]) { + added.push([filename, stat]); + changes.push({event: 'added', path: fullPath}); + return; + } - Object.keys(dir).forEach(function(v) { + if (stat.mtime.toString() !== dir[filename].mtime.toString()) { + dir[filename] = stat; + changes.push({event: 'modified', path: fullPath}); + } - let fullPath = [dirPath, v].join('/'); + contents[filename] = true; + } - if (!contents[v]) { - delete dir[v]; - changes.push({event: 'removed', path: fullPath}); - } + }); - }); + Object.keys(dir).forEach(function (filename) { - added.forEach(function(v) { - dir[v[0]] = v[1]; - }); + let fullPath = path.join(cwd, dirname, filename); + + if (!contents[filename]) { + delete dir[filename]; + changes.push({event: 'removed', path: fullPath}); + } + + }); + + added.forEach(function (change) { + let [filename, stat] = change; + dir[filename] = stat; + }); + + } }); diff --git a/lib/gateway.js b/lib/gateway.js index fb73249..7b229ae 100644 --- a/lib/gateway.js +++ b/lib/gateway.js @@ -1,17 +1,33 @@ +const fs = require('fs'); const http = require('http'); const path = require('path'); const url = require('url'); const querystring = require('querystring'); +const zlib = require('zlib'); const EventEmitter = require('events'); +const xmlParser = require('fast-xml-parser'); const uuid = require('uuid'); +const backgroundValidator = require('./background_validator.js'); const types = require('./types.js'); -const background = require('./background.js'); +const wellKnowns = require('./well-knowns.js'); + +const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; + +const relrequire = function (pathname, name) { + let relpath = pathname.split('/').slice(0, -1).join('/'); + let relname = name; + if (!name.startsWith('@') && name.indexOf('/') > 0) { + relname = path.join(process.cwd(), relpath, name); + } + return require(relname); +}; const DEFAULT_PORT = 8170; const DEFAULT_NAME = 'FunctionScript.Gateway'; const DEFAULT_MAX_REQUEST_SIZE_MB = 128; +const FUNCTION_EXECUTION_TIMEOUT = 30000; class Gateway extends EventEmitter { @@ -23,19 +39,26 @@ class Gateway extends EventEmitter { this.port = cfg.port || DEFAULT_PORT; this.name = cfg.name || DEFAULT_NAME; this.maxRequestSizeMB = cfg.maxRequestSizeMB || DEFAULT_MAX_REQUEST_SIZE_MB; + this.defaultTimeout = cfg.defaultTimeout || FUNCTION_EXECUTION_TIMEOUT; this.supportedMethods = {'GET': true, 'POST': true, 'OPTIONS': true, 'HEAD': true}; + this.trailingSlashRedirectMethods = {'GET': true}; this.supportedLogTypes = {'global': '***', 'info': ':::', 'error': '', 'result': '>>>'}; this.defaultLogType = 'info'; - this.supportedBgModes = background.modes; - this.defaultBgMode = background.defaultMode; + this.supportedBackgroundModes = backgroundValidator.modes; + this.defaultBackgroundMode = backgroundValidator.defaultMode; this.server = null; this.definitions = {}; + this.preloadFiles = {}; this.contextHeaders = { 'x-authorization-keys': 'keys', 'x-authorization-providers': 'providers' }; + this._inlineCache = []; this._requests = {}; this._requestCount = 0; + this._staticCache = {}; + this._serverSentEvents = {}; + this._serverSentEventInterval = 100; } routename (req) { @@ -65,10 +88,14 @@ class Gateway extends EventEmitter { this.debug && console.log(this.formatName(this.name), this.formatRequest(req), this.formatMessage(message + '', logType)); } - define (definitions) { + define (definitions, preloadFiles = {}) { if (!definitions || typeof definitions !== 'object') { - throw new Error(`Definitions must be a valid object`); + throw new Error(`definitions must be a valid object`); } + if (!preloadFiles || typeof preloadFiles !== 'object') { + throw new Error(`preloadFiles must be a valid object`); + } + this.preloadFiles = preloadFiles; return this.definitions = definitions; } @@ -127,118 +154,490 @@ class Gateway extends EventEmitter { }, {}); headers['access-control-allow-origin'] = headers['access-control-allow-origin'] || '*'; headers['access-control-allow-headers'] = headers['access-control-allow-headers'] || req.headers['access-control-request-headers'] || ''; - headers['access-control-expose-headers'] = Object.keys(headers).join(', '); headers['x-functionscript'] = 'true'; + headers['access-control-expose-headers'] = Object.keys(headers).concat('x-execution-uuid').join(', '); return headers; } - __background__ (req, res, definition, params) { - let bgResponse = this.supportedBgModes[definition.bg && definition.bg.mode] || - this.supportedBgModes[this.defaultBgMode]; - let value = bgResponse(definition, params); + __beginEmptyExecution__ (req, res) { let headers = this.__createHeaders__(req); - headers['content-type'] = headers['content-type'] || - (value instanceof Buffer ? 'application/octet-stream' : 'application/json'); + headers['content-type'] = 'text/plain'; + let value = Buffer.from('202 accepted'); + res.headersSent || res.writeHead(202, this.__formatHeaders__(headers)); + res.finished || res.end(value); + } + + __beginBackgroundExecution__ (req, res, definition, params, headers) { + let bgResponse = this.supportedBackgroundModes[definition.background && definition.background.mode] || + this.supportedBackgroundModes[this.defaultBackgroundMode]; + let value = bgResponse(definition, params); + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = value instanceof Buffer + ? 'text/plain' + : 'application/json'; value = value instanceof Buffer ? value : JSON.stringify(value); - res.finished || res.writeHead(202, this.__formatHeaders__(headers)); + res.headersSent || res.writeHead(202, this.__formatHeaders__(headers)); res.finished || res.end(value); } - __clientError__ (req, res, message, status) { - let headers = this.__createHeaders__(req, {'content-type': 'application/json'}); - res.finished || res.writeHead(status || 500, this.__formatHeaders__(headers)); - return this.__endRequest__(req, res, JSON.stringify({ - error: { - type: 'ClientError', - message: message + __beginServerSentEvent__ (req, res, definition, params, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'text/event-stream'; + res.headersSent || res.writeHead(200, this.__formatHeaders__(headers)); + // Create SSE Instance + let sseInstance = { + ids: {}, + sent: Buffer.from([]), + buffer: Buffer.from([]), + }; + sseInstance.emptyQueue = function () { + let buffer = sseInstance.buffer; + if (buffer.byteLength) { + if (res && res.headersSent && !res.finished) { + res.write(buffer); + } + sseInstance.sent = Buffer.concat([sseInstance.sent, buffer]); + sseInstance.buffer = Buffer.from([]); } - })); + }; + sseInstance.interval = setInterval(() => { + sseInstance.emptyQueue(); + }, this._serverSentEventInterval); + sseInstance.end = function () { + clearInterval(sseInstance.interval); + sseInstance.interval = null; + sseInstance.emptyQueue(); + }; + this._serverSentEvents[req._uuid] = sseInstance; } - __parameterError__ (req, res, details) { - let headers = this.__createHeaders__(req, {'content-type': 'application/json'}); - res.finished || res.writeHead(400, this.__formatHeaders__(headers)); - return this.__endRequest__(req, res, JSON.stringify({ - error: { - type: 'ParameterError', - message: 'One or more parameters provided did not match the function signature', - details: details - } - })); + __wellKnownResponse__ (req, res, body, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + return this.__endRequest__( + 200, + this.__formatHeaders__(headers), + req, + res, + body + ); } - __accessSourceError__ (req, res, msg) { - let headers = this.__createHeaders__(req, {'content-type': 'application/json'}); - res.finished || res.writeHead(401, this.__formatHeaders__(headers)); - return this.__endRequest__(req, res, JSON.stringify({ - error: { - type: 'AccessSourceError', - message: msg - } - })); + __wellKnownError__ (req, res, message, status, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + return this.__endRequest__( + status || 403, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: { + type: 'WellKnownError', + message: message + } + }) + ); } - __accessPermissionError__ (req, res, msg) { - let headers = this.__createHeaders__(req, {'content-type': 'application/json'}); - res.finished || res.writeHead(401, this.__formatHeaders__(headers)); - return this.__endRequest__(req, res, JSON.stringify({ - error: { - type: 'AccessPermissionError', - message: msg - } - })); + __clientError__ (req, res, message, status, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + return this.__endRequest__( + status || 500, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: { + type: 'ClientError', + message: message + } + }) + ); } - __accessAuthError__ (req, res, msg) { - let headers = this.__createHeaders__(req, {'content-type': 'application/json'}); - res.finished || res.writeHead(401, this.__formatHeaders__(headers)); - return this.__endRequest__(req, res, JSON.stringify({ - error: { - type: 'AccessAuthError', - message: msg - } - })); + __parameterError__ (req, res, details, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + return this.__endRequest__( + 400, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: { + type: 'ParameterError', + message: 'One or more parameters were invalid or missing from your request.', + details: details + } + }) + ); } - __accessSuspendedError__ (req, res, msg) { - let headers = this.__createHeaders__(req, {'content-type': 'application/json'}); - res.finished || res.writeHead(401, this.__formatHeaders__(headers)); - return this.__endRequest__(req, res, JSON.stringify({ - error: { - type: 'AccessSuspendedError', - message: msg - } - })); + __parameterParseError__ (req, res, msg, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + return this.__endRequest__( + 400, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: { + type: 'ParameterParseError', + message: msg + } + }) + ); } - __paymentRequiredError__ (req, res, msg) { - let headers = this.__createHeaders__(req, {'content-type': 'application/json'}); - res.finished || res.writeHead(402, this.__formatHeaders__(headers)); - return this.__endRequest__(req, res, JSON.stringify({ - error: { - type: 'PaymentRequiredError', - message: msg - } - })); + __originError__ (req, res, origin, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + return this.__endRequest__( + 403, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: { + type: 'OriginError', + message: `Provided origin "${origin}" can not access this resource`, + } + }) + ); } - __rateLimitError__ (req, res, count, period) { - let headers = this.__createHeaders__(req, {'content-type': 'application/json'}); - res.finished || res.writeHead(429, this.__formatHeaders__(headers)); - return this.__endRequest__(req, res, JSON.stringify({ - error: { - type: 'RateLimitError', - message: `Too many requests. The rate limit for this endpoint is ${count} requests in ${period} seconds.`, - details: { - rate: {count, period} + __debugError__ (req, res, message, headers) { + headers = headers || {}; + message = message || `You do not have permission to debug this endpoint`; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + return this.__endRequest__( + 403, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: { + type: 'DebugError', + message: message, } - } - })); + }) + ); + } + + __executionModeError__ (req, res, mode, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + return this.__endRequest__( + 403, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: { + type: 'ExecutionModeError', + message: `Execution mode "${mode}" not available for this endpoint`, + } + }) + ); + } + + __streamListenerError__ (req, res, details, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + return this.__endRequest__( + 400, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: { + type: 'StreamListenerError', + message: 'One or more streams you specified to listen to do not exist.', + details: details + } + }) + ); + } + + __accessSourceError__ (req, res, msg, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + return this.__endRequest__( + 401, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: { + type: 'AccessSourceError', + message: msg + } + }) + ); + } + + __accessPermissionError__ (req, res, msg, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + return this.__endRequest__( + 401, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: { + type: 'AccessPermissionError', + message: msg + } + }) + ); + } + + __accessAuthError__ (req, res, msg, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + return this.__endRequest__( + 401, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: { + type: 'AccessAuthError', + message: msg + } + }) + ); + } + + __accessSuspendedError__ (req, res, msg, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + return this.__endRequest__( + 401, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: { + type: 'AccessSuspendedError', + message: msg + } + }) + ); + } + + __ownerSuspendedError__ (req, res, msg, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + return this.__endRequest__( + 401, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: { + type: 'OwnerSuspendedError', + message: msg + } + }) + ); + } + + __ownerPaymentRequiredError__ (req, res, msg, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + return this.__endRequest__( + 401, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: { + type: 'OwnerPaymentRequiredError', + message: msg + } + }) + ); } - __fatalError__ (req, res, msg, stack) { - let headers = this.__createHeaders__(req, {'content-type': 'application/json'}); - res.finished || res.writeHead(500, this.__formatHeaders__(headers)); + __paymentRequiredError__ (req, res, msg, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + return this.__endRequest__( + 402, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: { + type: 'PaymentRequiredError', + message: msg + } + }) + ); + } + + __rateLimitError__ (req, res, message, count, period, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + return this.__endRequest__( + 429, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: { + type: 'RateLimitError', + message: message, + details: { + rate: {count, period} + } + } + }) + ); + } + + __authRateLimitError__ (req, res, message, count, period, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + return this.__endRequest__( + 429, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: { + type: 'AuthRateLimitError', + message: message, + details: { + rate: {count, period} + } + } + }) + ); + } + + __unauthRateLimitError__ (req, res, message, count, period, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + return this.__endRequest__( + 429, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: { + type: 'UnauthRateLimitError', + message: message, + details: { + rate: {count, period} + } + } + }) + ); + } + + __saveError__(req, res, msg, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + return this.__endRequest__( + 503, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: { + type: 'SaveError', + message: msg + } + }) + ); + } + + __maintenanceError__(req, res, msg, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + return this.__endRequest__( + 403, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: { + type: 'MaintenanceError', + message: msg + } + }) + ); + } + + __updateError__(req, res, msg, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + return this.__endRequest__( + 409, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: { + type: 'UpdateError', + message: msg + } + }) + ); + } + + __timeoutError__(req, res, msg, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + let error = { + type: 'TimeoutError', + message: msg || 'Function Timeout Error' + }; + return this.__endRequest__( + 504, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: error + }) + ); + } + + __fatalError__ (req, res, msg, stack, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; let error = { type: 'FatalError', message: msg || 'Fatal Error' @@ -252,52 +651,133 @@ class Gateway extends EventEmitter { ); error.stack = stackLines.join('\n'); } - return this.__endRequest__(req, res, JSON.stringify({ - error: error - })); - } - - __runtimeError__ (req, res, msg, stack) { - let headers = this.__createHeaders__(req, {'content-type': 'application/json'}); - res.finished || res.writeHead(403, this.__formatHeaders__(headers)); - return this.__endRequest__(req, res, JSON.stringify({ - error: { - type: 'RuntimeError', - message: msg || 'Runtime Error', - stack: stack - } - })); - } - - __valueError__ (req, res, details) { - let headers = this.__createHeaders__(req, {'content-type': 'application/json'}); - res.finished || res.writeHead(502, this.__formatHeaders__(headers)); - return this.__endRequest__(req, res, JSON.stringify({ - error: { - type: 'ValueError', - message: 'The value returned by the function did not match the specified type', - details: details - } - })); + return this.__endRequest__( + 500, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: error + }) + ); + } + + __runtimeError__ (req, res, msg, details, stack, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + let error = {}; + error.type = 'RuntimeError'; + error.message = msg || 'Runtime Error'; + if (details) { + error.details = details; + } + error.stack = stack; + return this.__endRequest__( + 403, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({error: error}) + ); + } + + __invalidResponseHeaderError__ (req, res, details, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + return this.__endRequest__( + 502, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: { + type: 'InvalidResponseHeaderError', + message: 'Your service returned invalid response headers', + details: details + } + }) + ); + } + + __valueError__ (req, res, details, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + return this.__endRequest__( + 502, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({ + error: { + type: 'ValueError', + message: 'The value returned by the function did not match the specified type', + details: details + } + }) + ); + } + + __autoformatError__ (req, res, msg, details, stack, headers) { + headers = headers || {}; + headers = this.__createHeaders__(req, headers); + headers['content-type'] = 'application/json'; + let error = {}; + error.type = 'AutoformatError'; + error.message = msg || 'Autoformat Error'; + error.details = { + retry: 'You can try this request again with ?raw=t set in the HTTP query parameters to see the raw file contents' + }; + error.stack = stack; + return this.__endRequest__( + 415, + this.__formatHeaders__(headers), + req, + res, + JSON.stringify({error: error}) + ); } __complete__ (req, res, body, headers, statusCode) { headers = this.__createHeaders__(req, headers); headers['content-type'] = headers['content-type'] || - (Buffer.isBuffer(body) ? 'application/octet-stream' : 'application/json'); + ( + Buffer.isBuffer(body) + ? body.contentType || 'application/octet-stream' + : 'application/json' + ); body = Buffer.isBuffer(body) ? body : JSON.stringify(body); - res.finished || res.writeHead(statusCode || 200, this.__formatHeaders__(headers)); - return this.__endRequest__(req, res, body); + return this.__endRequest__( + statusCode || 200, + this.__formatHeaders__(headers), + req, + res, + body + ); } __redirect__ (req, res, location) { - res.finished || res.writeHead(302, this.__formatHeaders__(this.__createHeaders__(req, {'location': location}))); - return this.__endRequest__(req, res); + let headers = this.__createHeaders__(req, {'location': location}); + return this.__endRequest__( + 302, + this.__formatHeaders__(headers), + req, + res, + null + ); } __options__ (req, res) { - res.finished || res.writeHead(200, this.__formatHeaders__(this.__createHeaders__(req))); - return this.__endRequest__(req, res); + let headers = this.__createHeaders__(req); + return this.__endRequest__( + 200, + this.__formatHeaders__(headers), + req, + res, + null + ); } __parseParameters__ (contentType, contentTypeParameters, convert, query, buffer, definition, proxyParameters) { @@ -306,19 +786,19 @@ class Gateway extends EventEmitter { let params; if (!contentType) { - throw new Error('Must Supply Content-Type'); + throw new Error('Must supply "Content-Type" header'); } else if (!buffer.length) { params = query; convert = true; } else if (buffer.length && Object.keys(query).length) { - throw new Error('Can not specify query parameters and POST data'); + throw new Error('Can not specify both query parameters and POST data'); } else if (contentType === 'application/x-www-form-urlencoded') { try { params = this.__parseParamsFromEncodedURL__(buffer.toString()); buffer = Buffer.from([]); convert = true; } catch (e) { - throw new Error('Invalid URL Encoded Data'); + throw e; } } else if (contentType === 'application/json') { try { @@ -328,13 +808,43 @@ class Gateway extends EventEmitter { throw new Error('Invalid JSON'); } } else if (contentType === 'multipart/form-data') { - params = this.__parseParamsFromMultipartForm__(buffer, contentType, contentTypeParameters) + params = this.__parseParamsFromMultipartForm__(buffer, contentType, contentTypeParameters); + } else if (contentType === 'application/xml' || contentType === 'application/atom+xml') { + try { + params = this.__parseParamsFromXML__(buffer.toString()); + buffer = Buffer.from([]); + } catch (e) { + throw new Error('Invalid XML'); + } + } else if (contentType === 'text/plain') { + // Check to see if text/plain provides valid JSON + // This fallback is meant to handle webhooks that don't send the right + // content-type headers -- eg vimeo + try { + params = JSON.parse(buffer.toString()); + if (typeof params === 'string') { + // vimeo webhook hack + params = JSON.parse(params); + } + if (params && typeof params === 'object' && !Array.isArray(params)) { + buffer = Buffer.from([]); + contentType = 'application/json'; + } else { + throw new Error('Aborting "text/plain" conversion to JSON'); + } + } catch (e) { + // do nothing + } } let type = types.check(params); if (type !== 'object') { - throw new Error('Invalid JSON: Must be Object'); + if (contentType === 'application/json') { + params = {}; + } else { + throw new Error('Invalid JSON: Must be an Object'); + } } if (proxyParameters) { @@ -377,7 +887,7 @@ class Gateway extends EventEmitter { let nameRegex = /name="([^"]*)"/; let contentTypeRegex = /Content-Type: ([^\s]+)*/ - let formString = '\r\n' + formBuffer.toString() + let formString = '\r\n' + formBuffer.toString('latin1') let params = formString .split(this.__getMultipartFormBoundary__([contentType, ...contentTypeParameters].join(';'))) .slice(1, -1) @@ -399,7 +909,7 @@ class Gateway extends EventEmitter { } break; default: - params[key] = Buffer.from(value); + params[key] = Buffer.from(value, 'latin1'); break; } @@ -412,10 +922,117 @@ class Gateway extends EventEmitter { __parseParamsFromEncodedURL__ (str) { let rawParams = querystring.parse(str); - return Object.keys(rawParams).reduce((params, paramName) => { - params[paramName] = rawParams[paramName]; + // We need to grab keys like key1[0][].key2.key3[]=5 + // Which would populate {key1:[[{key2:{key3:[5]}}]]} + // parseKeys is to make sure bracket balancing works as expected + let parseKeys = paramName => { + let keys = []; + let balancing = []; + let curKey = ''; + for (let c = 0; c < paramName.length; c++) { + if (paramName[c] === '[') { + balancing.push('['); + curKey += paramName[c]; + } else if (paramName[c] === ']') { + balancing.pop(); + curKey += paramName[c]; + } else if (paramName[c] === '.' && !balancing.length) { + keys.push(curKey); + curKey = ''; + } else { + curKey += paramName[c]; + } + } + keys.push(curKey); + return keys; + }; + let params = Object.keys(rawParams).reduce((params, paramName) => { + let value = rawParams[paramName]; + let keys = parseKeys(paramName); + let objectScope = params; + for (let i = 0; i < keys.length; i++) { + let objName = keys[i]; + let arrayMatches = objName.match(/^(.*?)((\[([^\]]*)\])+?)$/); + if (arrayMatches) { + let name = arrayMatches[1]; + let curObj = objectScope[name] = objectScope[name] || { + '.type': 'Array', + indexed: [], + pushed: [] + }; + let indices = arrayMatches[2].slice(1, -1).split(']['); + indices.forEach((index, j) => { + let descriptiveName = keys.slice(0, i + 1).join('.') + '[' + indices.slice(0, j + 1).join('][') + ']'; + if (curObj['.type'] !== 'Array') { + throw new Error(`${descriptiveName}: already set, can not set as Array`); + } + let item = (j === indices.length - 1 && i === keys.length - 1) + ? value + : j === indices.length - 1 + ? {} + : { + '.type': 'Array', + indexed: [], + pushed: [] + }; + if (index) { + let n = parseInt(index); + if (isNaN(n) || n !== parseFloat(index)) { + throw new Error(`${descriptiveName}: Array indices in URL encoded values must be integer values`); + } else if (n < 0) { + throw new Error(`${descriptiveName}: Array indices in URL encoded values must be > 0`); + } else if (n > 65535) { + throw new Error(`${descriptiveName}: Array indices in URL encoded values limited to 65535`); + } + curObj.indexed = curObj.indexed.concat( + Array(Math.max(0, n - curObj.indexed.length)).fill(null) + ); + curObj.indexed[n] = item; + curObj = item; + } else { + curObj.pushed = curObj.pushed.concat(item); + curObj = item; + } + }); + objectScope = curObj; + } else if (i < keys.length - 1) { + let curObj = objectScope[objName] = objectScope[objName] || {}; + if (typeof curObj !== 'object') { + throw new Error(`${keys.slice(0, i + 1).join('.')}: can not set subfield "${keys[i + 1]}" on value "${curObj}"`); + } + if (curObj['.type'] === 'Array') { + throw new Error(`${keys.slice(0, i + 1).join('.')}: already set as Array, can not set as Object`); + } + objectScope = curObj; + } else if (objName in objectScope) { + throw new Error(`${keys.slice(0, i + 1).join('.')}: already set`); + } else { + objectScope[objName] = value; + } + } return params; }, {}); + let convertArrays = (obj) => { + if (Array.isArray(obj)) { + return obj.map(convertArrays); + } else if (obj && typeof obj === 'object' && obj['.type'] === 'Array') { + return convertArrays([].concat(obj.indexed, obj.pushed)); + } else if (obj && typeof obj === 'object') { + return Object.keys(obj).reduce((newObj, key) => { + newObj[key] = convertArrays(obj[key]); + return newObj; + }, {}); + } else { + return obj; + } + }; + return convertArrays(params); + } + + __parseParamsFromXML__ (str) { + let validate = true; + let parsedData = xmlParser.parse(str, {arrayMode: false, parseNodeValue: false, ignoreAttributes : false}, validate); + return parsedData; } __validateParameters__ (parsed, definition) { @@ -424,12 +1041,26 @@ class Gateway extends EventEmitter { let convert = parsed.convert; let errors = {}; + // Handle special parameters + let specialParams = ['_background', '_stream', '_debug']; + specialParams.forEach(key => { + if (params[key]) { + try { + params[key] = types.parse('object', null, params[key], convert); + } catch (e) { + params[key] = params[key]; + } + } + }); + let paramsList = definition.params.map(param => { + // Catch where there is a mismatch + let paramMismatch = {}; let nullable = param.defaultValue === null; let value = params[param.name]; value = (value === undefined || value === null) ? param.defaultValue : value; try { - value = types.parse(param.type, value, convert); + value = types.parse(param.type, param.schema, value, convert); } catch (e) { value = value; } @@ -441,14 +1072,19 @@ class Gateway extends EventEmitter { } else if ( !types.validate( param.type, value, nullable, - param.schema || param.members, - param.options && param.options.values + param.members || + (param.alternateSchemas || []).concat(param.schema ? [param.schema] : []), + param.options && param.options.values, + param.range, + [param.name], + paramMismatch ) ) { let type = types.check(value); errors[param.name] = { message: `invalid value: ${Buffer.isBuffer(value) ? `Buffer[${value.length}]` : JSON.stringify(value, null, 2)} (${type}), expected (${param.type})`, invalid: true, + mismatch: paramMismatch.stack && paramMismatch.stack.join('.'), expected: { type: param.type }, @@ -457,21 +1093,29 @@ class Gateway extends EventEmitter { type: type } }; + if (!errors[param.name].mismatch) { + delete errors[param.name].mismatch; + } if (param.schema) { errors[param.name].expected.schema = param.schema; + if (param.alternateSchemas) { + errors[param.name].expected.alternateSchemas = param.alternateSchemas; + } } else if (param.members) { errors[param.name].expected.members = param.members + } else if (param.options && param.options.values) { + errors[param.name].message = `${errors[param.name].message} matching one of ${JSON.stringify(param.options.values)}`; + errors[param.name].expected.values = param.options.values; } } else { try { - value = types.sanitize(param.type, value); + value = types.sanitize(param.type, value, param.range); } catch (e) { errors[param.name] = { message: e.message, invalid: true }; } - if (param.type === 'enum') { let member = param.members.find(m => m[0] === value); value = member ? member[1] : param.defaultValue; @@ -489,6 +1133,101 @@ class Gateway extends EventEmitter { } + __validateDebug__ (debugObject, streams) { + streams = streams || []; + debugObject = typeof debugObject === 'object' + ? Array.isArray(debugObject) || Buffer.isBuffer(debugObject) + ? {} + : debugObject || {} + : {}; + let streamObject = {}; + Object.keys(debugObject).forEach(key => { + if ( + key === '*' || + key === '@begin' || + key === '@stdout' || + key === '@stderr' || + key === '@error' + ) { + return; + } else { + let stream = streams.find(stream => stream.name === key); + if (!stream) { + throw new Error(`Invalid debug listener: "${key}"`); + } else { + return; + } + } + }); + return true; + } + + __validateStreams__ (streamObject, streams) { + streams = streams || []; + streamObject = typeof streamObject === 'object' + ? Array.isArray(streamObject) || Buffer.isBuffer(streamObject) + ? {} + : streamObject || {} + : {}; + let errors = {}; + Object.keys(streamObject).forEach(key => { + if (key === '*') { + return; + } else { + let stream = streams.find(stream => stream.name === key); + if (!stream) { + errors[key] = { + message: `No such stream for this function: "${key}"`, + invalid: true + } + } + } + }); + if (Object.keys(errors).length) { + return {valid: false, errors: errors}; + } else { + return {valid: true}; + } + } + + __validateResponseHeaders__ (responseHeaders) { + let errors = {}; + let validatedHeaders = Object.keys(responseHeaders).reduce((validatedHeaders, headerName) => { + if (typeof headerName !== 'string') { + errors[headerName] = { + message: `Invalid response header name "${headerName}"`, + invalid: true + } + } else if (headerName.match(/\s/)) { + errors[headerName] = { + message: `Response header name "${headerName}" may not contain space characters`, + invalid: true + }; + } else if (headerName.match(/[^A-Za-z0-9\-_]/gi)) { + errors[headerName] = { + message: `Response header name "${headerName}" must only contain alphanumeric values, - and _`, + invalid: true + }; + } else if ( + typeof responseHeaders[headerName] !== 'string' && + typeof responseHeaders[headerName] !== 'boolean' && + typeof responseHeaders[headerName] !== 'number' + ) { + errors[headerName] = { + message: `The value of your "${headerName}" response header is missing or invalid`, + invalid: true + }; + } else { + validatedHeaders[headerName] = responseHeaders[headerName]; + } + return validatedHeaders; + }, {}); + return { + headers: validatedHeaders, + errors: Object.keys(errors).length ? errors: null + }; + } + __httpHandler__ (req, res) { req._uuid = uuid.v4(); @@ -498,15 +1237,22 @@ class Gateway extends EventEmitter { let urlinfo = url.parse(req.url); let pathinfo = urlinfo.pathname.split(':'); let pathname = pathinfo[0]; - let pathquery = this.__parseParamsFromEncodedURL__(pathinfo[1] || ''); req.url = [pathname, urlinfo.search].join(''); - if ('bg' in pathquery) { - req._background = true; - this.log(req, `Background Function Initiated`); + // Legacy, will deprecate + let pathquery; + try { + pathquery = this.__parseParamsFromEncodedURL__(pathinfo[1] || ''); + } catch (e) { + return this.__parameterParseError__(req, res, e.message, 400); } - if (req.headers['user-agent'] && !pathname.endsWith('/') && pathname.split('/').pop().indexOf('.') === -1) { + if ( + this.trailingSlashRedirectMethods[req.method] && + req.headers['user-agent'] && + !pathname.endsWith('/') && + pathname.split('/').pop().indexOf('.') === -1 + ) { this.log(req, `Redirect`); if (pathinfo.length === 2) { pathinfo[0] = pathname + '/'; @@ -524,10 +1270,15 @@ class Gateway extends EventEmitter { return this.__options__(req, res); } + if ('bg' in pathquery) { + this.log(req, `Empty Execution Initiated`); + this.__beginEmptyExecution__(req, res); + } + let buffers = []; let size = 0; req.on('data', chunk => { - size += chunk.length + size += chunk.length; if (size > this.maxRequestSizeMB * 1024 * 1024) { this.log(req, `ClientError: Function Payload Exceeded, Max Size ${this.maxRequestSizeMB}MB`, 'error'); this.__clientError__(req, res, `Function Payload Exceeded, Max Size ${this.maxRequestSizeMB}MB`, 413); @@ -555,28 +1306,90 @@ class Gateway extends EventEmitter { return this.__accessAuthError__(req, res, err.message); } else if (err.accessSuspendedError) { return this.__accessSuspendedError__(req, res, err.message); + } else if (err.ownerSuspendedError) { + return this.__ownerSuspendedError__(req, res, err.message); + } else if (err.ownerPaymentRequiredError) { + return this.__ownerPaymentRequiredError__(req, res, err.message); } else if (err.paymentRequiredError) { return this.__paymentRequiredError__(req, res, err.message); } else if (err.rateLimitError) { - return this.__rateLimitError__(req, res, err.rate && err.rate.count, err.rate && err.rate.period); + return this.__rateLimitError__(req, res, err.message, err.rate && err.rate.count, err.rate && err.rate.period); + } else if (err.authRateLimitError) { + return this.__authRateLimitError__(req, res, err.message, err.rate && err.rate.count, err.rate && err.rate.period); + } else if (err.unauthRateLimitError) { + return this.__unauthRateLimitError__(req, res, err.message, err.rate && err.rate.count, err.rate && err.rate.period); + } else if (err.saveError) { + return this.__saveError__(req, res, err.message); + } else if (err.maintenanceError) { + return this.__maintenanceError__(req, res, err.message); + } else if (err.updateError) { + return this.__updateError__(req, res, err.message); } this.log(req, `ClientError: ${err.message}`, 'error'); return this.__clientError__(req, res, err.message, err.statusCode || 400); } + if (definition.hasOwnProperty('wellKnown')) { + const wellKnown = definition.wellKnown; + const definitions = definition.definitions; + const pluginSupported = !!data.pluginSupported; + const pluginAuthorized = !!data.pluginAuthorized; + if (!pluginSupported) { + return this.__wellKnownError__(req, res, `Plugin not supported for this service`, 404); + } else if (!pluginAuthorized) { + return this.__wellKnownError__(req, res, `Plugin access not authorized`, 403); + } + let plugin; + try { + plugin = wellKnowns.validatePlugin(data.plugin, data.origin); + } catch (e) { + return this.__wellKnownError__(req, res, `Failed to validate plugin: ${e.message}`, 502); + } + const server = data.server; + const origin = data.origin; + const identifier = data.identifier; + let httpResponse; + try { + httpResponse = wellKnowns.handlers[wellKnown](definitions, plugin, server, origin, identifier); + } catch (e) { + return this.__fatalError__(req, res, `Error running .well-known handler for "${wellKnown}"`, e.stack); + } + return this.__wellKnownResponse__(req, res, httpResponse.body, httpResponse.headers); + } + + let headers = {}; + headers['x-execution-uuid'] = req._uuid; + if (definition.origins) { + if ( + req.headers['origin'] && + definition.origins.indexOf(req.headers['origin']) !== -1 + ) { + headers['access-control-allow-origin'] = req.headers['origin']; + } else { + headers['access-control-allow-origin'] = '!'; + return this.__originError__(req, res, req.headers['origin'], headers); + } + } + let [contentType, ...contentTypeParameters] = (req.method === 'GET' || req.method === 'DELETE') ? ['application/x-www-form-urlencoded'] : (req.headers['content-type'] || '').split(';'); let convert = 'x-convert-strings' in req.headers; - let query = this.__parseParamsFromEncodedURL__(urlinfo.query); + + let query; let parsed; + try { + query = this.__parseParamsFromEncodedURL__(urlinfo.query); + } catch (e) { + return this.__parameterParseError__(req, res, e.message); + } try { parsed = this.__parseParameters__(contentType, contentTypeParameters, convert, query, buffer, definition, proxyParameters); } catch (e) { this.log(req, `Bad Request: ${e.message}`, 'error'); - return this.__clientError__(req, res, `Bad Request: ${e.message}`, 400); + return this.__parameterParseError__(req, res, e.message); } let validated = this.__validateParameters__(parsed, definition); @@ -585,22 +1398,78 @@ class Gateway extends EventEmitter { return this.__parameterError__(req, res, validated.errors); } - if (req._background) { - this.log(req, `Background Function Responded to Client`); - this.__background__(req, res, definition, validated.params); + if ( + validated.params._debug === '' || + validated.params._debug + ) { + headers['x-debug'] = true; + if (!data.canDebug) { + this.log(req, `Debug Error`, 'error'); + return this.__debugError__(req, res, null, headers); + } else if ( + validated.params._background === '' || + validated.params._background + ) { + this.log(req, `Debug Error`, 'error'); + return this.__debugError__(req, res, 'Can not debug with "background" mode set', headers); + } else { + try { + this.__validateDebug__(validated.params._debug, definition.streams); + } catch (e) { + this.log(req, `Debug Error`, 'error'); + return this.__debugError__(req, res, e.message, headers); + } + // Set data.debug to true + data.debug = true; + } } + if ( + validated.params._background === '' || + validated.params._background + ) { + if (!definition.background) { + return this.__executionModeError__(req, res, 'background', headers); + } else { + this.log(req, `Background Function Responded to Client`); + this.__beginBackgroundExecution__(req, res, definition, validated.params, headers); + } + } else if ( + validated.params._stream === '' || + validated.params._stream + ) { + if (!definition.streams || !definition.streams.length) { + return this.__executionModeError__(req, res, 'stream', headers); + } else { + let streamValidated = this.__validateStreams__(validated.params._stream, definition.streams); + if (streamValidated.errors) { + this.log(req, `Stream Listener Error`, 'error'); + return this.__streamListenerError__(req, res, streamValidated.errors, headers); + } + this.log(req, `Begin Server-Sent Event`); + this.__beginServerSentEvent__(req, res, definition, validated.params, headers); + } + } else if (data.debug) { + // Always debug in stream mode + this.__beginServerSentEvent__(req, res, definition, validated.params, headers); + } + + let context = this.createContext(req, definition, validated.params, data, buffer); + let functionArgs = definition.context ? - validated.paramsList.concat(this.createContext(req, definition, validated.params, data, buffer)) : + validated.paramsList.concat(context) : validated.paramsList.slice(); + data.context = context; + setImmediate(() => { this.__requestHandler__( req, res, definition, data, - functionArgs + functionArgs, + headers ); }); @@ -610,21 +1479,86 @@ class Gateway extends EventEmitter { } - __endRequest__ (req, res, value) { - res.finished || res.end(value); + __endRequest__ (status, headers, req, res, value) { + if (!res.finished) { + if (!res.headersSent) { + // If we haven't sent headers it's a normal request + let bytes = null; + if (value) { + bytes = value; + let contentType = headers['Content-Type'].split(';')[0]; + let acceptEncoding = req.headers['accept-encoding']; + let canCompress = !!{ + 'text/plain': 1, + 'text/html': 1, + 'text/xml': 1, + 'text/json': 1, + 'text/javascript': 1, + 'application/json': 1, + 'application/xml': 1, + 'application/atom+xml': 1, + 'application/javascript': 1, + 'application/octet-stream': 1 + }[contentType]; + if (canCompress) { + try { + if (acceptEncoding.match(/\bgzip\b/gi)) { + bytes = zlib.gzipSync(bytes); + headers['Content-Encoding'] = 'gzip'; + } else if (acceptEncoding.match(/\bdeflate\b/gi)) { + bytes = zlib.deflateSync(bytes); + headers['Content-Encoding'] = 'deflate'; + } + } catch (e) { + bytes = value; + } + } + headers['Content-Length'] = Buffer.byteLength(bytes); + } + res.writeHead(status, headers); + res.end(bytes); + } else { + // If we have sent headers, it's a Server-Sent Event + this.createServerSentEvent( + req._uuid, + -1, + '@response', + JSON.stringify({ + statusCode: status, + headers: headers, + body: Buffer.isBuffer(value) + ? JSON.stringify({_base64: value.toString('base64')}) + : value + }) + ); + let sseInstance = this._serverSentEvents[req._uuid]; + if (sseInstance) { + sseInstance.end(); + } + res.end(); + } + } this.end(req, value); delete this._requests[req._uuid]; + delete this._serverSentEvents[req._uuid]; this._requestCount -= 1; !this._requestCount && this.emit('empty'); } - __requestHandler__ (req, res, definition, data, functionArgs) { - let nullable = definition.returns.defaultValue === null; + __requestHandler__ (req, res, definition, data, functionArgs, headers) { + let nullable = (definition.returns || {}).defaultValue === null; this.log(req, `Execution Start`); let t = new Date().valueOf(); - this.execute(definition, functionArgs, data, (err, value, headers) => { + let isStatic = ( + definition.format && + definition.format.language && + definition.format.language === 'static' + ); + this.execute(definition, functionArgs, data, headers, (err, value, headers, executionUuid) => { let dt = new Date().valueOf() - t; err = err === undefined ? null : err; + // Catch where there is a mismatch + let returnsMismatch = {}; if (err !== null) { if (!(err instanceof Error)) { let jsonErr; @@ -639,7 +1573,7 @@ class Gateway extends EventEmitter { `Use callback(new Error('description')) to pass an error, or callback(null, 'result') ` + `to pass a result without an error.`; this.log(req, `Runtime Error (${dt}ms): ${msg}`, 'error'); - return this.__runtimeError__(req, res, msg); + return this.__runtimeError__(req, res, msg, null, null, headers); } else if (err.thrown) { let message = err.message; if (err.hasOwnProperty('value')) { @@ -650,19 +1584,26 @@ class Gateway extends EventEmitter { } } this.log(req, `Runtime Error Thrown (${dt}ms): ${message}`, 'error'); - return this.__runtimeError__(req, res, message, err.stack); + return this.__runtimeError__(req, res, message, err.details, err.stack, headers); + } else if (err.timeoutError) { + this.log(req, `Timeout Error (${dt}ms): ${err.message}`, 'error'); + return this.__timeoutError__(req, res, err.message, headers); } else if (err.fatal) { this.log(req, `Fatal Error (${dt}ms): ${err.message}`, 'error'); - return this.__fatalError__(req, res, err.message, err.stack); + return this.__fatalError__(req, res, err.message, err.stack, headers); } else { this.log(req, `Runtime Error (${dt}ms): ${err.message}`, 'error'); - return this.__runtimeError__(req, res, err.message, err.stack); + return this.__runtimeError__(req, res, err.message, err.details, err.stack, headers); } } else if ( !types.validate( definition.returns.type, value, nullable, - definition.returns.schema || definition.returns.members, - definition.returns.options && definition.returns.options.values + definition.returns.members || + (definition.returns.alternateSchemas || []).concat(definition.returns.schema ? [definition.returns.schema] : []), + definition.returns.options && definition.returns.options.values, + definition.returns.range, + [definition.returns.name || '$'], + returnsMismatch ) ) { let returnType = definition.returns.type; @@ -671,6 +1612,7 @@ class Gateway extends EventEmitter { returns: { message: `invalid return value: ${Buffer.isBuffer(value) ? `Buffer[${value.length}]` :JSON.stringify(value, null, 2)} (${type}), expected (${returnType})`, invalid: true, + mismatch: returnsMismatch.stack && returnsMismatch.stack.join('.'), expected: { type: returnType }, @@ -680,16 +1622,22 @@ class Gateway extends EventEmitter { } } }; + if (!details.returns.mismatch) { + delete details.returns.mismatch; + } if (definition.returns.schema) { details.returns.expected.schema = definition.returns.schema; + if (definition.returns.alternateSchemas) { + details.returns.expected.alternateSchemas = definition.returns.alternateSchemas; + } } else if (definition.returns.members) { details.returns.expected.members = definition.returns.members; } this.log(req, `Value Error (${dt}ms): ${details.returns.message}`, 'error'); - return this.__valueError__(req, res, details); + return this.__valueError__(req, res, details, headers); } else { try { - value = types.sanitize(definition.returns.type, value); + value = types.sanitize(definition.returns.type, value, definition.returns.range); } catch (e) { let details = { returns: { @@ -698,29 +1646,65 @@ class Gateway extends EventEmitter { } }; this.log(req, `Value Error (${dt}ms): ${details.returns.message}`, 'error'); - return this.__valueError__(req, res, details); + return this.__valueError__(req, res, details, headers); } if (definition.returns.type === 'enum') { let member = definition.returns.members.find(m => m[0] === value); value = member[1]; } + let httpResponse; + try { + httpResponse = types.httpResponse(definition.returns.type, value, headers); + } catch (e) { + let details = { + returns: { + message: e.message, + invalid: true + } + }; + this.log(req, `Value Error (${dt}ms): ${details.returns.message}`, 'error'); + return this.__valueError__(req, res, details, headers); + } + if (isStatic && !functionArgs.slice().pop().params.raw) { + try { + httpResponse = this.__autoformat__(httpResponse); + } catch (err) { + this.log(req, `Autoformat Error (${dt}ms): ${err.message}`, 'error'); + return this.__autoformatError__(req, res, err.message, err.details, err.stack, httpResponse.headers); + } + } + let validated = this.__validateResponseHeaders__(httpResponse.headers); + if (validated.errors) { + this.log(req, `Invalid Response Header Error (${dt}ms)`, 'error'); + return this.__invalidResponseHeaderError__(req, res, validated.errors, validated.headers); + } this.log(req, `Execution Complete (${dt}ms)`); - let httpResponse = types.httpResponse(definition.returns.type, value, headers); return this.__complete__(req, res, httpResponse.body, httpResponse.headers, httpResponse.statusCode); } }); } + __autoformat__ (httpResponse) { + return httpResponse; + } + findDefinition (definitions, name) { name = name.replace(/^\/?(.*?)\/?$/gi, '$1'); let definition = definitions[name]; if (!definition) { - let subname = name; - definition = definitions[`${subname}:notfound`]; - while (subname && !definition) { - subname = subname.substr(0, subname.lastIndexOf('/')); + if (wellKnowns.handlers[name]) { + return { + wellKnown: name, + definitions: definitions + }; + } else { + let subname = name; definition = definitions[`${subname}:notfound`]; + while (subname && !definition) { + subname = subname.substr(0, subname.lastIndexOf('/')); + definition = definitions[`${subname}:notfound`]; + } } } if (!definition) { @@ -740,7 +1724,15 @@ class Gateway extends EventEmitter { e.statusCode = 404; return callback(e); } - return callback(null, definition, {}, buffer); + let data = {}; + data.canDebug = true; + data.pluginSupported = true; + data.pluginAuthorized = true; + data.plugin = {}; + data.server = 'FunctionScript Gateway'; + data.origin = urlinfo.origin || 'localhost'; + data.identifier = 'service.localhost'; + return callback(null, definition, data, buffer); } createContext (req, definition, params, data, buffer) { @@ -750,11 +1742,17 @@ class Gateway extends EventEmitter { context.path = context.alias.split('/'); context.params = params; context.remoteAddress = req.connection.remoteAddress; + context.uuid = req._uuid; context.http = {}; context.http.url = req.url; context.http.method = req.method; context.http.headers = req.headers; context.http.body = buffer.toString('utf8'); + try { + context.http.json = JSON.parse(buffer.toString('utf8')); + } catch (e) { + context.http.json = null; + } context.user = null; context.service = null; context.function = { @@ -784,52 +1782,426 @@ class Gateway extends EventEmitter { keys[key] = context.keys[key] || null; return keys; }, {}); + context.stream = this.__stream__.bind(this, definition.streams, context.uuid, null); return context; } - execute (definition, functionArgs, data, callback) { - let fn; - try { - let rpath = require.resolve(path.join(process.cwd(), this.root, definition.pathname)); - delete require[rpath]; - fn = require(rpath); - } catch (e) { - if (!(e instanceof Error)) { - let value = e; - e = new Error(e || ''); - e.value = value; + __streamError__ (executionUuid, logId, eventName) { + let req = this._requests[executionUuid]; + this.createServerSentEvent( + executionUuid, + logId, + '@error', + JSON.stringify({ + type: 'StreamError', + message: [ + `No such stream "${eventName}" in function definition.`, + ` Please use the syntax "@stream {string} name description"`, + ` after @params to define a stream.`, + ].join('') + }) + ); + } + + __streamParameterError__ (executionUuid, logId, eventName, details) { + let req = this._requests[executionUuid]; + this.log(req, `Stream Parameter Error: ${details[eventName].message}`, 'error'); + this.createServerSentEvent( + executionUuid, + logId, + '@error', + JSON.stringify({ + type: 'StreamError', + message: [ + `Stream Parameter Error: "${eventName}".`, + ` Please make sure the data type you are sending to the stream matches`, + ` the definition for the stream in the function.` + ].join(''), + details: details + }) + ); + } + + __stream__ (streams, executionUuid, logId, eventName, value) { + value = value === void 0 + ? null + : value; + let stream = (streams || []).find(param => param.name === eventName); + if (!stream) { + if ( + eventName === '@begin' || + eventName === '@stdout' || + eventName === '@stderr' || + eventName === '@error' + ) { + this.createServerSentEvent( + executionUuid, + logId, + eventName, + JSON.stringify(value) + ); + } else { + this.__streamError__(executionUuid, logId, eventName); } - e.fatal = true; - return callback(e); + } else { + let nullable = stream.defaultValue === null; + let details = {}; + let streamMismatch = {}; + if ( + !types.validate( + stream.type, value, nullable, + stream.members || + (stream.alternateSchemas || []).concat(stream.schema ? [stream.schema] : []), + stream.options && stream.options.values, + stream.range, + [stream.name || '$'], + streamMismatch + ) + ) { + let returnType = stream.type; + let type = types.check(value); + let details = {}; + details[eventName] = { + message: `invalid value: ${Buffer.isBuffer(value) ? `Buffer[${value.length}]` :JSON.stringify(value, null, 2)} (${type}), expected (${returnType})`, + invalid: true, + mismatch: streamMismatch.stack && streamMismatch.stack.join('.'), + expected: { + type: returnType + }, + actual: { + value: value, + type: type + } + }; + if (!details[eventName].mismatch) { + delete details[eventName].mismatch; + } + if (stream.schema) { + details[eventName].expected.schema = stream.schema; + if (stream.alternateSchemas) { + details[eventName].expected.alternateSchemas = stream.alternateSchemas; + } + } else if (stream.members) { + details[eventName].expected.members = stream.members; + } + this.__streamParameterError__(executionUuid, logId, eventName, details); + } else { + try { + value = types.sanitize(stream.type, value, stream.range); + } catch (e) { + let details = {}; + details[eventName] = { + message: e.message, + invalid: true + }; + this.__streamParameterError__(executionUuid, logId, eventName, details); + return; + } + if (stream.type === 'enum') { + let member = stream.members.find(m => m[0] === value); + value = member[1]; + } + this.createServerSentEvent( + executionUuid, + logId, + eventName, + JSON.stringify(types.serialize(stream.type, value)) + ); + } + } + } + + createServerSentEvent (executionUuid, logId, eventName, value, silent = false, greaterThanTimestamp = null) { + let sseInstance = this._serverSentEvents[executionUuid]; + let timestamp = new Date().toISOString(); + // Incremenet this counter so IDs don't overlap + let i = 0; + if (greaterThanTimestamp && timestamp <= greaterThanTimestamp) { + let unixTimestamp = new Date(greaterThanTimestamp).valueOf(); + timestamp = new Date(unixTimestamp + 1).toISOString(); } - // Catch unhandled promise rejections once, starting now. - // This applies to local testing only. - process.removeAllListeners('unhandledRejection'); - process.once('unhandledRejection', (err, p) => callback(err)); - if (definition.format.async) { - fn.apply(null, functionArgs) - .then(result => callback(null, result)) - .catch(e => { + if (silent || sseInstance) { + let printId = true; + if (logId === -1) { + printId = false; + logId = null; + } + if (!logId) { + let lpad = (i, len) => { + let s = i.toString(); + return '0'.repeat(Math.max(0, len - s.length)) + s; + }; + let endChar = timestamp[timestamp.length - 1]; + let time = timestamp.slice(0, -1); + logId = [time, lpad(i++, 6), endChar, '/', executionUuid].join(''); + while (!silent && sseInstance.ids[logId]) { + logId = [time, lpad(i++, 6), endChar, '/', executionUuid].join(''); + } + } + if (silent || !sseInstance.ids[logId]) { + if (!silent) { + sseInstance.ids[logId] = true; + sseInstance.buffer = Buffer.concat([ + sseInstance.buffer, + Buffer.from([ + printId ? `id: ${logId}\n` : '', + eventName ? `event: ${eventName}\n` : '', + `data: ${value}\n\n`, + ].join('')) + ]); + } + return { + id: logId, + timestamp: timestamp, + name: eventName, + payload: value + }; + } else { + return null + } + } + } + + jsonify (obj) { + if ( + obj !== null && + typeof obj === 'object' && + !Buffer.isBuffer(obj) + ) { + if (typeof obj.toJSON === 'function') { + obj = obj.toJSON(); + } + if (Array.isArray(obj)) { + obj = obj.map(item => this.jsonify(item)); + } else if ( + obj !== null && + typeof obj === 'object' && + !Buffer.isBuffer(obj) + ) { + Object.keys(obj).forEach(key => obj[key] = this.jsonify(obj[key])); + } + } + return obj; + } + + execute (definition, functionArgs, data, headers, callback) { + headers = headers || {}; + let fn; + let complete = false; + let callbackWrapper = (err, result, headers, executionUuid) => { + if (!complete) { + complete = true; + result = this.jsonify(result); + callback(err, result, headers, executionUuid); + } + }; + setTimeout(() => { + let error = new Error(`Timeout of ${this.defaultTimeout}ms exceeded.`); + error.timeoutError = true; + error.fatal = true; + return callbackWrapper(error, null, headers, executionUuid); + }, this.defaultTimeout); + let executionUuid = data.context.uuid || uuid.v4(); + if (definition.format.language === 'static') { + let buffer = this._staticCache[definition.pathname] = ( + this._staticCache[definition.pathname] || + this.preloadFiles[definition.pathname] || + fs.readFileSync(path.join(process.cwd(), this.root, definition.pathname)) + ); + let statusCode = definition.name.endsWith(':notfound') + ? 404 + : 200; + let contentType = definition.metadata.contentType || 'application/octet-stream'; + if (contentType.split(';')[0].split('/')[0] === 'video') { + let range = data.context.http.headers.range; + let len = buffer.byteLength; + if (range) { + range = range + .replace('bytes=', '') + .split('-') + .map(r => r.trim()); + if (!range.length) { + range = [0, len - 1]; + } else if (range.length === 1) { + range.push(len - 1); + } else if (range[1] === '') { + range[1] = len - 1; + } + if (range[0] === '' && !isNaN(parseInt(range[1]))) { + range = [len - parseInt(range[1]), len - 1]; + } else { + range = [parseInt(range[0]) || 0, parseInt(range[1]) || 0]; + } + buffer = buffer.slice(range[0], range[1] + 1); + } else { + range = [0, len - 1]; + } + if (range[0] !== 0 || range[1] !== len - 1) { + statusCode = 206; + } + headers['Content-Range'] = `bytes ${range[0]}-${range[1]}/${len}`; + headers['Accept-Ranges'] = 'bytes'; + } else if (contentType.split(';')[0].split('/')[0] === 'text') { + contentType = contentType.split(';')[0] + '; charset=utf-8'; + } + headers['Content-Type'] = contentType; + headers['Content-Length'] = buffer.byteLength; + return callbackWrapper( + null, + {statusCode: statusCode, headers: headers, body: buffer}, + headers, + executionUuid + ); + } else if (definition.format.language === 'nodejs') { + if (definition.format.inline) { + fn = this._inlineCache[definition.pathname]; + if (!fn) { + try { + let fnString; + if (this.preloadFiles[definition.pathname]) { + fnString = this.preloadFiles[definition.pathname].toString(); + } else { + fnString = fs.readFileSync(path.join(process.cwd(), this.root, definition.pathname)).toString(); + } + fn = new AsyncFunction('require', 'context', fnString).bind(null, relrequire.bind(null, definition.pathname)); + this._inlineCache[definition.pathname] = fn; + } catch (e) { + e.fatal = true; + return callbackWrapper(e, null, headers, executionUuid); + } + } + } else { + try { + let pathname = definition.pathname; + let rpath; + if (this.preloadFiles[pathname]) { + let filename = path.join(process.cwd(), pathname); + if (fs.existsSync(filename)) { + throw new Error(`Preloaded file "${pathname}" conflicts with existing file`); + } + let pathList = filename.split(path.sep); + let addedDirs = []; + for (let i = 1; i < pathList.length - 1; i++) { + let checkPath = pathList.slice(0, i + 1).join(path.sep); + if (fs.existsSync(checkPath)) { + let stat = fs.statSync(checkPath); + if (!stat.isDirectory()) { + throw new Error(`Preloaded file "${pathname}" can not be created, "${checkPath}" is not a directory`); + } + } else { + addedDirs.push(checkPath); + try { + fs.mkdirSync(checkPath); + } catch (e) { + throw new Error(`Preloaded file "${pathname}" can not be created, "${checkPath}" could not be written`); + } + } + } + fs.writeFileSync(filename, this.preloadFiles[pathname]); + rpath = require.resolve(filename); + delete require[rpath]; + fn = require(rpath); + fs.unlinkSync(filename); + while (addedDirs.length) { + fs.rmdirSync(addedDirs.pop()); + } + } else { + rpath = require.resolve(path.join(process.cwd(), this.root, definition.pathname)); + delete require[rpath]; + fn = require(rpath); + } + } catch (e) { if (!(e instanceof Error)) { let value = e; e = new Error(e || ''); e.value = value; } - e.thrown = true; - callback(e); + e.fatal = true; + return callbackWrapper(e, null, headers, executionUuid); + } + } + // Overwrite internal console to stream output + const stringifyArgs = args => { + return args.map(arg => { + return (typeof arg === 'string') + ? arg + : JSON.stringify(arg) + }).join(' '); + }; + let console = global.console; + if (data.debug) { + let gConsole = {}; + Object.keys(console).forEach(key => { + let fn = console[key]; + if (typeof fn === 'function') { + gConsole[key] = console[key].bind(console); + } else { + gConsole[key] = console[key]; + } }); - } else { - try { - fn.apply(null, functionArgs.concat(callback)); - } catch (e) { - if (!(e instanceof Error)) { - let value = e; - e = new Error(e || ''); - e.value = value; + gConsole.log = function () { + let args = [].slice.call(arguments); + console.log(...args); + data.context.stream('@stdout', stringifyArgs(args)); + }; + gConsole.error = function () { + let args = [].slice.call(arguments); + console.error(...args); + data.context.stream('@stderr', stringifyArgs(args)); + }; + global.console = gConsole; + } + // Catch unhandled promise rejections once, starting now. + // This applies to local testing only. + process.removeAllListeners('unhandledRejection'); + process.once('unhandledRejection', (err, p) => { + global.console = console; + callbackWrapper(err, null, headers, executionUuid); + }); + data.context.stream('@begin', new Date().toISOString()); + if (definition.format.async) { + fn.apply(null, functionArgs) + .then(result => { + global.console = console; + callbackWrapper(null, result, headers, executionUuid); + }) + .catch(e => { + if (!(e instanceof Error)) { + let value = e; + e = new Error(e || ''); + e.value = value; + } + e.thrown = true; + global.console = console; + callbackWrapper(e, null, headers, executionUuid); + }); + } else { + try { + fn.apply(null, functionArgs.concat((err, result, responseHeaders) => { + Object.keys(responseHeaders || {}).forEach(key => { + headers[key.toLowerCase()] = responseHeaders[key]; + }); + global.console = console; + return callbackWrapper(err, result, headers, executionUuid); + })); + } catch (e) { + if (!(e instanceof Error)) { + let value = e; + e = new Error(e || ''); + e.value = value; + } + e.thrown = true; + global.console = console; + return callbackWrapper(e, null, headers, executionUuid); } - e.thrown = true; - return callback(e); } + } else { + return callbackWrapper( + new Error(`Gateway does not support language "${definition.format.language}"`), + null, + headers, + executionUuid + ); } } diff --git a/lib/parser/function_parser.js b/lib/parser/function_parser.js index dda7a69..144d82c 100644 --- a/lib/parser/function_parser.js +++ b/lib/parser/function_parser.js @@ -2,8 +2,9 @@ const fs = require('fs'); const path = require('path'); const minimatch = require('minimatch'); +const mime = require('mime'); -const background = require('../background.js'); +const backgroundValidator = require('../background_validator.js'); const types = require('../types.js'); const DEFAULT_IGNORE = [ @@ -41,8 +42,9 @@ class FunctionParser { return this._parsers[lang]; } - parseDefinition(pathname, buffer, functionsPath) { + parseDefinition (pathname, buffer, functionsPath, isStatic, isPreload) { + isStatic = !!isStatic; functionsPath = functionsPath || ''; functionsPath = (functionsPath && !functionsPath.endsWith('/')) ? `${functionsPath}/` : functionsPath; if (!pathname.startsWith(functionsPath)) { @@ -50,127 +52,236 @@ class FunctionParser { } let names = pathname.split('/'); let filename = names.pop(); - let name; let ext = filename.split('.').pop(); - let parser = this.getParser(`.${ext}`); - let suffix; - name = filename.substr(0, filename.length - ext.length - 1); - if (name === '__main__') { - name = names.join('/'); - } else if (name === '__notfound__') { - name = names.join('/'); - suffix = 'notfound'; + let name = filename.substr(0, filename.length - ext.length - 1); + let definitionList = []; + + if (isStatic) { + // Convert index.html -> mydomain.com/ + // But also preserve mydomain.com/index.html + // Special rules for html files named "index" or "404" + // Which will create default routes + let parser = this.getParser(''); + let definition; + let shouldCreateFileEntry = true; + if ( + (name === 'index' || name === '404') && + (ext === 'htm' || ext === 'html') + ) { + if (name === 'index') { + name = names.join('/').substr(functionsPath.length); + } else if (name === '404') { + name = `${names.join('/').substr(functionsPath.length)}:notfound`; + shouldCreateFileEntry = false; // 404 will trigger 404 anyway + } + try { + definition = parser.parse( + name, + buffer.toString(), + pathname, + buffer + ); + definition.pathname = pathname; + } catch (e) { + throw new Error(`Static file error (${pathname})\n${e.message}`); + } + definitionList.push(definition); + } + if (shouldCreateFileEntry) { + try { + definition = parser.parse( + names.concat(filename).join('/').substr(functionsPath.length), + buffer.toString(), + pathname, + buffer + ); + definition.pathname = pathname; + } catch (e) { + throw new Error(`Static file error (${pathname})\n${e.message}`); + } + definitionList.push(definition); + } } else { - name = names.concat(name).join('/'); - } - name = name.substr(functionsPath.length); - if (name && !name.match(/^([A-Z][A-Z0-9\_\-]*\/)*[A-Z][A-Z0-9\_\-]*$/i)) { - throw new Error( - `Invalid function name: ${name} (${filename})\n` + - `All path segments must be alphanumeric (or -, _) and start with a letter` - ); - } + let parser = this.getParser(`.${ext}`); + let suffix; + if (name === '__main__') { + name = names.join('/'); + } else if (name === '__notfound__') { + name = names.join('/'); + suffix = 'notfound'; + } else { + name = names.concat(name).join('/'); + } + name = name.substr(functionsPath.length); + if (name && !name.match(/^([A-Z][A-Z0-9\_\-]*\/)*[A-Z][A-Z0-9\_\-]*$/i)) { + throw new Error( + `Invalid function name: ${name} (${filename})\n` + + `All path segments must be alphanumeric (or -, _) and start with a letter` + ); + } - name = name + (suffix ? `:${suffix}` : ''); - let definition; + name = name + (suffix ? `:${suffix}` : ''); + let definition; - try { - definition = parser.parse(name, buffer.toString()); - definition.pathname = pathname; - } catch (e) { - e.message = `Function definition error (${pathname})\n${e.message}`; - throw e; - } + try { + definition = parser.parse(name, buffer.toString(), pathname, buffer); + definition.pathname = pathname; + } catch (e) { + e.message = `Function definition error (${pathname})\n${e.message}`; + throw e; + } - let validations = this.constructor.definitionFields; - Object.keys(validations).forEach(field => { - let validate = validations[field]; - let value = definition[field]; - if (!validate(value)) { - throw new Error( - `Function definition error (${pathname})\n`+ - `Invalid field "${field}", unexpected value: ${JSON.stringify(value)}` - ); + if (isPreload) { + definition.preload = true; } + + definitionList.push(definition); + + } + + definitionList.forEach(definition => { + let validations = this.constructor.definitionFields; + Object.keys(validations).forEach(field => { + let validate = validations[field]; + let value = definition[field]; + if (!validate(value)) { + throw new Error( + `FunctionScript endpoint definition error (${pathname})\n`+ + `Invalid field "${field}", unexpected value: ${JSON.stringify(value)}\n` + + `\nThis is likely caused by a FunctionScript misconfiguration.` + ); + } + }); }); - return definition; + return definitionList; } - readDefinitions(files, functionsPath) { + readDefinitions (files, functionsPath, definitions, isStatic, isPreload) { functionsPath = (functionsPath && !functionsPath.endsWith('/')) ? `${functionsPath}/` : (functionsPath || ''); return Object.keys(files).reduce((definitions, pathname) => { if (functionsPath && !pathname.startsWith(functionsPath)) { return definitions; } - let definition = this.parseDefinition(pathname, files[pathname], functionsPath); - if (definitions[definition.name]) { - throw new Error( - `Function ${definition.name} already exists\n` + - `If declaring with [dir]/__main__.js, make sure [dir].js isn't a file in the parent directory.` - ); - } - definitions[definition.name] = definition; + let definitionList = this.parseDefinition(pathname, files[pathname], functionsPath, isStatic, isPreload); + definitionList.forEach(definition => { + if (definitions[definition.name]) { + let existingDefinition = definitions[definition.name]; + throw new Error( + `Endpoint ${definition.name} (${definition.pathname}) was already defined in ${existingDefinition.pathname}\n` + + ( + existingDefinition.preload + ? `This file was preloaded as part of a compilation step, it can not be overwritten.\n` + : '' + ) + + ( + isStatic + ? (!existingDefinition.static && definition.static) + ? [ + `Endpoint functions and static files can not have the same name.`, + `This can often be caused when a function is named "__main__" and a static file is name "index",`, + `or when a function is named "__notfound__" and a static file is named "404".` + ].join('\n') + : `Only one static file per directory can have the name "index" or "404".` + : `If declaring with [dir]/__main__.js, make sure [dir].js isn't a file in the parent directory.` + ) + ); + } + definitions[definition.name] = definition; + }); return definitions; - }, {}); + }, definitions || {}); } - readFiles(rootPath, functionsPath, dirPath, files, ignore) { + readFiles (rootPath, functionsPath, dirPath, files, ignore) { + if (ignore && !Array.isArray(ignore)) { + throw new Error(`Invalid ignore file, received ${typeof ignore} instead of Array`); + } let ignoreList = (ignore || []).concat(DEFAULT_IGNORE); functionsPath = functionsPath || '.'; dirPath = dirPath || '.'; files = files || {}; - return fs.readdirSync(path.join(rootPath, functionsPath, dirPath)).reduce((files, filename) => { - let pathname = path.join(rootPath, functionsPath, dirPath, filename); - let fullPath = path.join(functionsPath, dirPath, filename); - let filePath = path.join(dirPath, filename); - let fullPathNormalized = fullPath.split(path.sep).join('/'); - let isDirectory = fs.statSync(pathname).isDirectory(); - for (let i = 0; i < ignoreList.length; i++) { - if (minimatch(fullPathNormalized, ignoreList[i], {matchBase: true})) { - return files; - } - } - if (isDirectory) { - files = this.readFiles(rootPath, functionsPath, filePath, files, ignore); - } else { - files[fullPathNormalized] = fs.readFileSync(pathname); - } + if (!fs.existsSync(path.join(rootPath, functionsPath, dirPath))) { return files; - }, files); + } else { + return fs.readdirSync(path.join(rootPath, functionsPath, dirPath)).reduce((files, filename) => { + let pathname = path.join(rootPath, functionsPath, dirPath, filename); + let fullPath = path.join(functionsPath, dirPath, filename); + let filePath = path.join(dirPath, filename); + let fullPathNormalized = fullPath.split(path.sep).join('/'); + let isDirectory = fs.statSync(pathname).isDirectory(); + for (let i = 0; i < ignoreList.length; i++) { + if (minimatch(fullPathNormalized, ignoreList[i], {matchBase: true})) { + return files; + } + } + if (isDirectory) { + files = this.readFiles(rootPath, functionsPath, filePath, files, ignore); + } else { + files[fullPathNormalized] = fs.readFileSync(pathname); + } + return files; + }, files); + } } - load(rootPath, functionsPath, ignore) { - let files = this.readFiles(rootPath, functionsPath, null, null, ignore); - return this.readDefinitions(files, functionsPath); + load (rootPath, functionsPath, staticPath, ignore, preloadFiles) { + let definitions = {}; + if (preloadFiles && typeof preloadFiles === 'object') { + definitions = this.readDefinitions(preloadFiles, functionsPath, definitions, false, true); + definitions = this.readDefinitions(preloadFiles, staticPath, definitions, true, true); + } + let functionFiles = this.readFiles(rootPath, functionsPath, null, null, ignore); + definitions = this.readDefinitions(functionFiles, functionsPath, definitions); + if (staticPath) { + let staticFiles = this.readFiles(rootPath, staticPath, null, null, ignore); + definitions = this.readDefinitions(staticFiles, staticPath, definitions, true); + } + return definitions; } } FunctionParser.parsers = { + 'static' : require('./static/static_parser.js'), 'nodejs': require('./nodejs/exported_function_parser.js') }; +FunctionParser.commentParsers = { + 'nodejs': require('./nodejs/comment_definition_parser.js') +}; + FunctionParser.extensions = { + '': 'static', '.js': 'nodejs' }; FunctionParser.definitionFields = { 'name': name => typeof name === 'string', + 'pathname': pathname => typeof pathname === 'string', 'format': format => { return format && typeof format === 'object' && - typeof format.language === 'string'; + typeof format.language === 'string' && + typeof format.inline === 'boolean' }, 'description': description => typeof description === 'string', - 'bg': bg => { - return bg && - typeof bg === 'object' && - 'mode' in bg && - 'value' in bg && - typeof bg.value === 'string' && - bg.mode in background.modes; + 'metadata': metadata => typeof metadata === 'object' && metadata !== null, + 'origins': origins => { + return origins === null || origins === void 0 || Array.isArray(origins); + }, + 'background': background => { + if (background) { + return background && + typeof background === 'object' && + 'mode' in background && + 'value' in background && + typeof background.value === 'string' && + background.mode in backgroundValidator.modes; + } else { + return background === null || background === void 0; + } }, 'keys': keys => { return Array.isArray(keys); @@ -178,12 +289,17 @@ FunctionParser.definitionFields = { 'charge': charge => { return charge >= 0 && charge <= 100 && parseInt(charge) === charge; }, + 'streams': streams => { + return streams === null || + FunctionParser.definitionFields['params'](streams); + }, 'context': context => typeof context === 'object', 'params': params => { return Array.isArray(params) && params.filter(param => { return param && typeof param == 'object' && 'name' in param && + param.name 'description' in param && 'type' in param && ( @@ -204,13 +320,26 @@ FunctionParser.definitionFields = { ) : true ) && + ( + 'alternateSchemas' in param + ? ( + (['object'].indexOf(param.type) !== -1) && + Array.isArray(param.alternateSchemas) && + param.alternateSchemas.filter(schema => { + return FunctionParser.definitionFields['params'](schema); + }).length === param.alternateSchemas.length + ) + : true + ) && types.list.indexOf(param.type) > -1 }).length === params.length; }, 'returns': returns => { return returns && typeof returns === 'object' && - 'description' in returns && 'type' in returns; + 'name' in returns && + 'description' in returns && + 'type' in returns; } }; diff --git a/lib/parser/nodejs/comment_definition_parser.js b/lib/parser/nodejs/comment_definition_parser.js index 3267c33..0987efd 100644 --- a/lib/parser/nodejs/comment_definition_parser.js +++ b/lib/parser/nodejs/comment_definition_parser.js @@ -1,12 +1,16 @@ +const backgroundValidator = require('../../background_validator.js'); const types = require('../../types.js'); +const validateParameterName = require('./validate_parameter_name.js'); const DEFAULT_DEFINITION_FIELD = 'description'; const DEFINITION_FIELDS = [ - 'bg', + 'origin', + 'background', 'keys', 'charge', 'acl', 'param', + 'stream', 'returns' ]; @@ -29,8 +33,16 @@ class CommentDefinitionParser { let splitLine = line.split(' '); let field = splitLine.shift(); line = splitLine.join(' '); - if (!field && previous && (previous.field === 'param' || previous.field === 'returns')) { - previous.schema = (previous.schema || []).concat(line); + if ( + !field && + previous && + ( + previous.field === 'param' || + previous.field === 'stream' || + previous.field === 'returns' + ) + ) { + previous.textSchema = (previous.textSchema || []).concat([[line]]); return semanticsList; } else if (DEFINITION_FIELDS.indexOf(field) === -1) { throw new Error(`Invalid Definition Field: "${field}"`); @@ -50,17 +62,14 @@ class CommentDefinitionParser { values: [line.trim()] }); } else if (previous) { - let lastSchemaItem = previous.schema && previous.schema[previous.schema.length - 1]; - let lastSchemaArray = Array.isArray(lastSchemaItem) - ? lastSchemaItem - : lastSchemaItem - ? [lastSchemaItem.trim()] - : []; - let isEnum = /^{enum}/i.test(lastSchemaArray[0]); + let lastSchemaItem = previous.textSchema + ? previous.textSchema[previous.textSchema.length - 1] + : ['']; + let isEnum = /^{\??enum}/i.test(lastSchemaItem[0].trim()); // Enums inside schemas need to be collected as a single item if (isEnum) { - lastSchemaArray = lastSchemaArray.concat(line.trim()); - previous.schema[previous.schema.length - 1] = lastSchemaArray; + lastSchemaItem = lastSchemaItem.concat(line.trim()); + previous.textSchema[previous.textSchema.length - 1] = lastSchemaItem; } else { previous.values = previous.values.concat(line.trim()); } @@ -113,11 +122,42 @@ class CommentDefinitionParser { }, acl); } - getBg (values) { + getOrigin (values) { + let value = values.join(' ').trim(); + let origins = []; + if (!value.match(/^(https?:\/\/)?([a-z0-9\-]{0,255}\.)*?([a-z0-9\-]{1,255})(:[0-9]{1,5})?$/gi)) { + throw new Error([ + `Invalid origin: "${value}".`, + ` Must be a valid hostname consisting of alphanumeric characters or "-"`, + ` separated by ".".`, + ` Supported protocols are "http://" and "https://", if left absent both`, + ` will be enabled.` + ].join('')); + } + if (!value.match(/^https?:\/\//gi)) { + origins.push(`https://${value}`); + origins.push(`http://${value}`); + } else { + origins.push(value); + } + return origins; + } + + getBackground (values) { values = values.join(' ').split(' '); + let mode = values[0].trim() || backgroundValidator.defaultMode; + let value = values.slice(1).join(' ').trim(); + let modes = Object.keys(backgroundValidator.modes); + if (modes.indexOf(mode) === -1) { + throw new Error([ + `Invalid Background mode: "${mode}". Please specify \`@background MODE\``, + ` where MODE is one of: "${modes.join('", "')}" (without quotes).`, + ` If no mode is provided, the default "${backgroundValidator.defaultMode}" will be used.` + ].join('')); + } return { - mode: values[0], - value: values.slice(1).join(' ').trim() + mode: mode, + value: value }; } @@ -125,7 +165,8 @@ class CommentDefinitionParser { return values.join(' ').split(/\s+/); } - getParameter (values, schema) { + getParameter (values, textSchema = [], depth = 0) { + if (!Array.isArray(values)) { values = [values]; } @@ -133,19 +174,41 @@ class CommentDefinitionParser { let value = values.join(' '); let defaultMetafield = ''; let options = ''; + let range = ''; if (value.indexOf('{?}') !== -1 && value.indexOf('{!}') > value.indexOf('{?}')) { throw new Error('defaultMetafield {!} must come before options {?}'); } + if (value.indexOf('{:}') !== -1) { + if (value.indexOf('{!}') > value.indexOf('{:}')) { + throw new Error(`defaultMetafield {!} must come before range {:}`) + } + if (value.indexOf('{?}') > value.indexOf('{:}')) { + throw new Error(`options {?} must come before range {:}`); + } + } + if (value.indexOf('{!}') !== -1) { defaultMetafield = (value.split('{!}')[1] || '').trim(); value = value.split('{!}')[0].trim(); - options = (defaultMetafield.split('{?}')[1] || '').trim(); - defaultMetafield = defaultMetafield.split('{?}')[0].trim(); - } else { + if (defaultMetafield.indexOf('{?}') !== -1) { + options = (defaultMetafield.split('{?}')[1] || '').trim(); + defaultMetafield = defaultMetafield.split('{?}')[0].trim(); + range = (options.split('{:}')[1] || '').trim(); + options = options.split('{:}')[0].trim(); + } else { + range = (defaultMetafield.split('{:}')[1] || '').trim(); + defaultMetafield = defaultMetafield.split('{:}')[0].trim(); + } + } else if (value.indexOf('{?}') !== -1) { options = (value.split('{?}')[1] || '').trim(); value = value.split('{?}')[0].trim(); + range = (options.split('{:}')[1] || '').trim(); + options = options.split('{:}')[0].trim(); + } else { + range = (value.split('{:}')[1] || '').trim(); + value = value.split('{:}')[0].trim(); } let param = {}; @@ -168,7 +231,7 @@ class CommentDefinitionParser { } if (type === 'enum') { - let splitValue = values[0].split(' ').slice(1); + let splitValue = values[0].trim().split(' ').slice(1); param.name = splitValue.shift(); param.description = splitValue.join(' '); param.members = this.parseEnumMembers(values.slice(1)); @@ -178,11 +241,15 @@ class CommentDefinitionParser { let splitValue = value.split(' '); param.name = splitValue.shift(); param.description = splitValue.join(' '); - if (schema) { + if (textSchema.length) { if (!['object', 'array'].includes(type)) { throw new Error(`Can not provide schema for type: "${type}"`); } - param.schema = this.parseSchema(schema, type === 'object'); + let schemas = this.parseSchemas(textSchema, depth, type === 'object'); + param.schema = schemas.shift(); + if (schemas.length) { + param.alternateSchemas = schemas; + } } if (defaultMetafield) { @@ -197,8 +264,8 @@ class CommentDefinitionParser { throw new Error(`Options {?}: Not allowed for type "${param.type}" for parameter "${param.name}"`); } param.options = {}; - var opt = options.split(' '); - var values; + let opt = options.split(' '); + let values; try { values = JSON.parse(options); } catch (e) { @@ -208,12 +275,12 @@ class CommentDefinitionParser { if (values.length === 0) { throw new Error(`Options {?}: Must provide non-zero options length`); } - var paramType = param.type === 'object.keyql.query' + let paramType = (param.type === 'object.keyql.query' || param.type === 'object.keyql.order') ? 'string' : param.type; values.forEach(v => { if (!types.validate(paramType, v)) { - throw new Error(`Options {?}: "${param.name}", type mismatch for type ${paramType} ("${options}")`); + throw new Error(`Options {?}: "${param.name}", type mismatch for type ${param.type} ("${options}")`); } }); param.options.values = values; @@ -221,12 +288,12 @@ class CommentDefinitionParser { if (!opt[0] || opt[0].indexOf('.') === -1) { throw new Error(`Options {?}: Invalid API call for "${param.name}": "${opt[1]}"`); } else { - var call = /^(.+?)(?:\((.*)\))?$/gi.exec(opt[0]); + let call = /^(.+?)(?:\((.*)\))?$/gi.exec(opt[0]); param.options.lib = call[1]; if (call[2]) { param.options.map = (call[2] || '').split(',').reduce((map, p) => { - var key = p.split('=')[0]; - var value = p.split('=').slice(1).join('='); + let key = p.split('=')[0]; + let value = p.split('=').slice(1).join('='); if (!key.match(/^[a-z0-9\_]+$/i) || !value.match(/^[a-z0-9\_]+$/i)) { throw new Error(`Options {?}: Invalid API call parameters for "${param.name}": "${call[2]}"`); } @@ -244,7 +311,42 @@ class CommentDefinitionParser { } } + if (range) { + if (types.rangeAllowed.indexOf(param.type) === -1) { + throw new Error(`Range {:}: Not allowed for type "${param.type}" for parameter "${param.name}"`); + } + let span; + try { + span = JSON.parse(range); + } catch (e) { + span = null; + } + if (!Array.isArray(span)) { + throw new Error(`Range {:}: "${param.name}" invalid, expecting array [min, max] ("${range}")`); + } + let paramType = (param.type === 'object.keyql.limit') + ? 'integer' + : param.type; + span.forEach(s => { + if (!types.validate(paramType, s)) { + throw new Error(`Range {:}: "${param.name}", type mismatch for type ${param.type} ("${range}")`); + } + }); + if (span.length !== 2) { + throw new Error(`Range {:}: "${param.name}" invalid length, expecting array [min, max] ("${range}")`); + } else if (span[0] > span[1]) { + throw new Error(`Range {:}: "${param.name}" invalid, max must be greater than min in [min, max] ("${range}")`); + } + if (param.type === 'object.keyql.limit') { + if (span[0] < 0 || span[1] < 0) { + throw new Error(`Range {:}: "${param.name}" invalid value, [min, max] must both be greater than or equal to 0 ("${range}")`); + } + } + param.range = {min: span[0], max: span[1]}; + } + return param; + } getCharge (values) { @@ -261,7 +363,7 @@ class CommentDefinitionParser { let field = data.field; let values = data.values; - let schema = data.schema; + let textSchema = data.textSchema; try { @@ -269,11 +371,30 @@ class CommentDefinitionParser { definition.description = values.join('\n'); } else if (field === 'param') { definition.params = definition.params || []; - definition.params.push(this.getParameter(values, schema)); + definition.params.push(this.getParameter(values, textSchema)); + definition.params.forEach(param => { + if (!validateParameterName(param.name)) { + throw new Error(`Invalid parameter name "${param.name}"`); + } + }); + } else if (field === 'stream') { + definition.streams = definition.streams || []; + definition.streams.push(this.getParameter(values, textSchema)); + definition.streams.forEach(stream => { + if (!validateParameterName(stream.name)) { + throw new Error(`Invalid stream name "${stream.name}"`); + } + }); } else if (field === 'returns') { - definition.returns = this.getParameter(values, schema); - } else if (field === 'bg') { - definition.bg = this.getBg(values); + definition.returns = this.getParameter(values, textSchema); + } else if (field === 'origin') { + definition.origins = definition.origins || []; + definition.origins = [].concat( + definition.origins, + this.getOrigin(values) + ); + } else if (field === 'background') { + definition.background = this.getBackground(values); } else if (field === 'acl') { definition.acl = this.getAcl(values); } else if (field === 'charge') { @@ -295,6 +416,7 @@ class CommentDefinitionParser { parse (name, commentString) { + commentString = commentString || ''; return this.getLines(commentString) .map(line => this.stripComments(line)) .reduce((semanticsList, line) => this.reduceLines(semanticsList, line), []) @@ -303,12 +425,15 @@ class CommentDefinitionParser { { name: name, description: '', + origins: null, acl: null, - bg: null, + background: null, keys: [], + streams: null, context: null, params: [], returns: { + name: '', type: 'any', description: '' } @@ -317,64 +442,57 @@ class CommentDefinitionParser { } - parseSchema (schema, multipleKeys) { - if (schema.length && !Array.isArray(schema[0]) && this.getLineDepth(schema[0])) { - throw new Error(`Invalid Schema definition at: "${schema[0]}"`); + parseSchemas (textSchema, depth = 0, allowMultipleEntries = false) { + let schemas = []; + while (textSchema.length) { + let line = textSchema[0][0]; + let params = []; + let schema = this._parseSchema(textSchema, params, depth); + if (!allowMultipleEntries && schema.length > 1) { + throw new Error(`Invalid Schema definition at "${line}": Schema for "array" can only support one top-level key that maps to every element.`); + } + schemas.push(schema); } - - let parsedSchema = this._parseSchema(schema, [], 0); - - if (!multipleKeys && parsedSchema.length > 1) { - throw new Error('Schema for "array" can only support one top-level key that maps to every element.'); + if (!allowMultipleEntries && schemas.length > 1) { + throw new Error(`Invalid Schema definition at "${line}": Schema for "array" does not support OR (alternateSchemas).`); + } else if (schemas.filter(params => !params.length).length > 0) { + throw new Error(`Invalid Schema definition at "${line}": OR (alternateSchemas) requires all schema lengths to be non-zero`); } - - return parsedSchema; - + return schemas; } - _parseSchema (schema, params, lastDepth) { - - if (!schema.length) { - return params; - } - - let line = schema.shift(); + _parseSchema (textSchema, params = [], depth = 0) { - // Just call this.getParameter to collect the enum and continue - if (Array.isArray(line)) { - let param = this.getParameter(line); - params.push(param); - return this._parseSchema(schema, params, lastDepth); + let values = textSchema.shift(); + let line = values[0]; + let curDepth = this.getLineDepth(line); + if (depth !== curDepth) { + throw new Error(`Invalid Schema definition at: "${line}", invalid line depth (expecting ${depth}, found ${curDepth})`); } - - let depth = this.getLineDepth(line); - let param = this.getParameter(line.trim()); - let lastParam = params[params.length - 1]; - let step = depth - lastDepth; - - if (!lastParam || step === 0) { - params.push(param); - return this._parseSchema(schema, params, depth); - } - - if (step === 1) { - if (lastParam.type !== 'object' && lastParam.type !== 'array') { - throw new Error(`Invalid Schema definition at: "${line}", can only define schemas for an object or array`); + if (line.trim() === 'OR') { + if (!textSchema.length) { + throw new Error(`Invalid Schema definition at "${line}": OR (alternateSchemas) can not end a schema definition`); } - lastParam.schema = this._parseSchema(schema, [param], depth); - return this._parseSchema(schema, params, lastDepth); - } - - if (step > 1) { - throw new Error(`Invalid Schema definition at: "${line}", spacing invalid`); + return params; + } else { + let subSchemaEnd = textSchema.findIndex(values => { + let line = values[0]; + return this.getLineDepth(line) <= curDepth; + }); + let subSchema; + if (subSchemaEnd === -1) { + subSchema = textSchema.splice(0, textSchema.length); + } else if (subSchemaEnd) { + subSchema = textSchema.splice(0, subSchemaEnd); + } + params.push(this.getParameter(values, subSchema, depth + 1)); + return textSchema.length + ? this._parseSchema(textSchema, params, depth) + : params; } - - schema.unshift(line); - return params; - } - getLineDepth(line) { + getLineDepth (line) { let depth = (line.length - line.replace(/^\s*/, '').length) / 2; diff --git a/lib/parser/nodejs/exported_function_parser.js b/lib/parser/nodejs/exported_function_parser.js index b1d0b5d..760bc0b 100644 --- a/lib/parser/nodejs/exported_function_parser.js +++ b/lib/parser/nodejs/exported_function_parser.js @@ -1,7 +1,14 @@ -const babylon = require('babylon'); +const babelParser = require('@babel/parser'); const types = require('../../types.js'); -const background = require('../../background.js'); +const validateParameterName = require('./validate_parameter_name.js'); + +const RESERVED_NAMES = [ + '_stream', + '_background', + '_debug', + 'context' +]; const CommentDefinitionParser = require('./comment_definition_parser.js'); @@ -16,7 +23,8 @@ class ExportedFunctionParser { BooleanLiteral: 'boolean', NullLiteral: 'any', ObjectExpression: 'object', - ArrayExpression: 'array' + ArrayExpression: 'array', + UnaryExpression: 'number' }; this.validateExpressions = { ObjectExpression: (node, stack) => { @@ -34,6 +42,19 @@ class ExportedFunctionParser { }, ArrayExpression: (node, stack) => { return node.elements.map((el, i) => this.validateDefaultParameterExpression(i, el, stack)); + }, + UnaryExpression: (node, stack) => { + if (node.argument.type === 'NumericLiteral') { + if (node.operator === '-') { + return -node.argument.value; + } else if (node.operator === '+') { + return node.argument.value; + } else { + throw new Error(`Invalid UnaryExpression`); + } + } else { + throw new Error(`Invalid UnaryExpression`); + } } }; } @@ -43,9 +64,11 @@ class ExportedFunctionParser { stack = (stack || []).slice(0); stack.push(name); - if (!this.literals[node.type]) { + let type = node.type; + + if (!this.literals[type]) { console.log(node); - throw new Error(`(${stack.join('.')}) Expected ${Object.keys(this.literals).join(', ')} in Right-Hand of AssignmentPattern`); + throw new Error(`(${stack.join('.')}) Expected ${Object.keys(this.literals).join(', ')} in Right-Hand of AssignmentPattern, got ${type}.`); } if (this.validateExpressions[node.type]) { @@ -58,11 +81,13 @@ class ExportedFunctionParser { parseModuleExportsStatement(fileString) { - let AST = babylon.parse(fileString, { - plugins: [ - 'objectRestSpread' - ] - }); + let AST = babelParser.parse( + fileString, + { + allowReturnOutsideFunction: true, + allowAwaitOutsideFunction: true + } + ); let body = AST.program.body; @@ -80,15 +105,11 @@ class ExportedFunctionParser { ) }); - if (!statements.length) { - throw new Error(`Nothing exported from file via "module.exports"`); - } - if (statements.length > 1) { throw new Error(`Too many exports from file via "module.exports"`); } - return statements[0]; + return statements[0] || null; } @@ -103,84 +124,94 @@ class ExportedFunctionParser { return expression.right; } - parseParamsFromFunctionExpression(functionExpression) { + parseParamsFromFunctionExpression(functionExpression, fileString) { - let params = functionExpression.params; + if (!functionExpression) { + return { + async: true, + inline: true, + context: {}, + params: [] + }; + } else { + let params = functionExpression.params; - if (!functionExpression.async) { - let lastParam = params.pop(); - if (!lastParam || lastParam.type !== 'Identifier' || lastParam.name !== 'callback') { - throw new Error(`Non-async functions must have parameter named "callback" as the last argument`); + if (!functionExpression.async) { + let lastParam = params.pop(); + if (!lastParam || lastParam.type !== 'Identifier' || lastParam.name !== 'callback') { + throw new Error(`Non-async functions must have parameter named "callback" as the last argument`); + } } - } - let paramsObject = {}; + let paramsObject = {}; - if (params.length) { - let lastParam = params.pop(); - if (lastParam.type === 'Identifier' && lastParam.name === 'context') { - paramsObject.context = {}; - } else { - params.push(lastParam); + if (params.length) { + let lastParam = params.pop(); + if (lastParam.type === 'Identifier' && lastParam.name === 'context') { + paramsObject.context = {}; + } else { + params.push(lastParam); + } } - } - return { - async: functionExpression.async, - context: paramsObject.context || null, - params: params.slice() - .reverse() - .map((param, i) => { - let formattedParam; - if (param.type === 'Identifier') { - if (param.name === 'context') { - throw new Error(`When specified, "context" must be the last provided (non-callback) argument`); + return { + async: functionExpression.async, + inline: false, + context: paramsObject.context || null, + params: params.slice() + .reverse() + .map((param, i) => { + let formattedParam; + if (param.type === 'Identifier') { + if (param.name === 'context') { + throw new Error(`When specified, "context" must be the last provided (non-callback) argument`); + } + if (functionExpression.async && param.name === 'callback') { + throw new Error(`Async functions can not have a parameter named "callback"`); + } + if (!this.validateFunctionParamName(param.name)) { + throw new Error(`Invalid parameter name "${param.name}"`); + } + formattedParam = {name: param.name}; + } else if (param.type === 'AssignmentPattern') { + if (param.left.type !== 'Identifier') { + throw new Error('Expected Identifier in Left-Hand of AssignmentPattern'); + } + if (param.left.name === 'context') { + throw new Error(`When specified, "context" can not be assigned a default value`); + } + if (functionExpression.async && param.left.name === 'callback') { + throw new Error(`Async functions can not have a parameter named "callback"`); + } + if (!this.validateFunctionParamName(param.left.name)) { + throw new Error(`Invalid parameter name "${param.left.name}"`); + } + let defaultValue; + try { + defaultValue = this.validateDefaultParameterExpression(param.left.name, param.right); + } catch (e) { + throw new Error([ + `Invalid default parameter: ${param.left.name} = ${fileString.slice(param.right.start, param.right.end)}`, + `(${e.message})` + ].join(' ')); + } + formattedParam = { + name: param.left.name, + type: this.literals[param.right.type], + defaultValue: defaultValue + }; } - if (functionExpression.async && param.name === 'callback') { - throw new Error(`Async functions can not have a parameter named "callback"`); - } - if (!this.validateFunctionParamName(param.name)) { - throw new Error(`Invalid parameter name "${param.name}"`); - } - formattedParam = {name: param.name}; - } else if (param.type === 'AssignmentPattern') { - if (param.left.type !== 'Identifier') { - throw new Error('Expected Identifier in Left-Hand of AssignmentPattern'); - } - if (param.left.name === 'context') { - throw new Error(`When specified, "context" can not be assigned a default value`); - } - if (functionExpression.async && param.left.name === 'callback') { - throw new Error(`Async functions can not have a parameter named "callback"`); - } - if (!this.validateFunctionParamName(param.left.name)) { - throw new Error(`Invalid parameter name "${param.left.name}"`); - } - let defaultValue = this.validateDefaultParameterExpression(param.left.name, param.right); - formattedParam = { - name: param.left.name, - type: this.literals[param.right.type], - defaultValue: defaultValue - }; - } - paramsObject[formattedParam.name] = formattedParam; - return formattedParam; - }) - .reverse() - }; + paramsObject[formattedParam.name] = formattedParam; + return formattedParam; + }) + .reverse() + }; + } } validateFunctionParamName(param) { - try { - let token = babylon.parseExpression(param); - if (!token || token.type !== 'Identifier') { - return false; - } - return true; - } catch (e) { - return false; - } + return validateParameterName(param); } parseCommentFromModuleExportsStatement(moduleStatement) { @@ -232,7 +263,10 @@ class ExportedFunctionParser { defParam.type, param.defaultValue, param.defaultValue === null, - defParam.members || defParam.schema, + ( + defParam.members || + (defParam.alternateSchemas || []).concat(defParam.schema ? [defParam.schema] : []) + ), defParam.options && defParam.options.values ) ) { @@ -245,48 +279,70 @@ class ExportedFunctionParser { } else { throw new Error(`Parameter "${defParam.name}" type "${defParam.type}" does not match the default value ${JSON.stringify(param.defaultValue)} ("${param.type}")`); } + } else { + try { + types.sanitize(defParam.type, param.defaultValue, defParam.range); + } catch (e) { + throw new Error(`Parameter "${defParam.name}" defaultValue invalid: ${e.message}`); + } } } param.type = defParam.type || types.defaultType; param.description = defParam.description; defParam.defaultMetafield && (param.defaultMetafield = defParam.defaultMetafield); defParam.options && (param.options = defParam.options); + defParam.range && (param.range = defParam.range); defParam.schema && (param.schema = defParam.schema); + defParam.alternateSchemas && (param.alternateSchemas = defParam.alternateSchemas); defParam.members && (param.members = defParam.members); } return param; }); } - parse(name, fileString) { + parse (name, fileString, pathname, buffer) { let moduleStatement = this.parseModuleExportsStatement(fileString); - let functionExpression = this.parseFunctionExpressionFromModuleExportsStatement(moduleStatement); - let comment = this.parseCommentFromModuleExportsStatement(moduleStatement); + let comment = null; + let functionExpression = null; + + if (moduleStatement) { + comment = this.parseCommentFromModuleExportsStatement(moduleStatement); + functionExpression = this.parseFunctionExpressionFromModuleExportsStatement(moduleStatement); + } + let commentDefinition = this.commentDefinitionParser.parse(name, comment); - let functionDefinition = this.parseParamsFromFunctionExpression(functionExpression); + let functionDefinition = this.parseParamsFromFunctionExpression(functionExpression, fileString); let description = commentDefinition.description || ''; - let bg = commentDefinition.bg || background.generateDefaultValue(); + let origins = commentDefinition.origins || null; + let background = commentDefinition.background || null; let charge = isNaN(commentDefinition.charge) ? 1 : commentDefinition.charge; + let streams = commentDefinition.streams || null; let context = commentDefinition.context || functionDefinition.context; let acl = commentDefinition.acl; let keys = commentDefinition.keys; let isAsync = functionDefinition.async; + let isInline = functionDefinition.inline; let params = this.compareParameters(functionDefinition.params, commentDefinition.params); let returns = commentDefinition.returns; return { name: name, + pathname: pathname, format: { language: this.language, - async: isAsync + async: isAsync, + inline: isInline }, description: description, - bg: bg, + metadata: {}, + origins: origins, + background: background, acl: acl, keys: keys, charge: charge, + streams: streams, context: context, params: params, returns: returns diff --git a/lib/parser/nodejs/validate_parameter_name.js b/lib/parser/nodejs/validate_parameter_name.js new file mode 100644 index 0000000..30f99a7 --- /dev/null +++ b/lib/parser/nodejs/validate_parameter_name.js @@ -0,0 +1,25 @@ +const babelParser = require('@babel/parser'); + +const RESERVED_NAMES = [ + '_stream', + '_debug', + '_bg', + 'context' +]; + +function validateParameterName(param) { + try { + let token = babelParser.parseExpression(param); + if ( + !token || token.type !== 'Identifier' || token.name === 'let' || + RESERVED_NAMES.indexOf(token.name) !== -1 + ) { + return false; + } + } catch (e) { + return false; + } + return true; +} + +module.exports = validateParameterName; diff --git a/lib/parser/static/static_parser.js b/lib/parser/static/static_parser.js new file mode 100644 index 0000000..4f2a958 --- /dev/null +++ b/lib/parser/static/static_parser.js @@ -0,0 +1,52 @@ +const mime = require('mime'); + +class StaticParser { + + constructor() { + this.language = 'static'; + } + + parse (name, fileString, pathname, buffer) { + + let filename = pathname.split('/').pop(); + + return { + name: name, + pathname: pathname, + format: { + language: this.language, + async: false, + inline: false + }, + description: filename, + metadata: { + filename: filename, + contentType: mime.getType(filename), + contentLength: buffer.byteLength, + }, + origins: null, + background: null, + keys: [], + charge: 0, + streams: null, + context: {}, + params: [ + { + name: 'raw', + description: 'turn off auto-formatting and return the raw file contents where applicable', + type: 'boolean', + defaultValue: false + } + ], + returns: { + name: 'file', + description: 'Static File', + type: 'object.http' + } + }; + + } + +} + +module.exports = StaticParser; diff --git a/lib/types.js b/lib/types.js index 7111711..3f32bed 100644 --- a/lib/types.js +++ b/lib/types.js @@ -6,67 +6,109 @@ const OPTIONS_ALLOWED = [ 'float', 'integer', 'object.keyql.query', + 'object.keyql.order', ]; -const validate = (type, v, nullable, schema, options) => { +const RANGE_ALLOWED = [ + 'number', + 'float', + 'integer', + 'object.keyql.limit' +]; + +const validate = (type, v, nullable, schemas, options, range, stack = [], mismatch = {}) => { + if (schemas && !Array.isArray(schemas)) { + throw new Error(`Argument schemas must be an Array`) + } return (nullable && v === null) || - _validations[type](v, schema, options); + _validations[type](v, schemas, options, range, stack, mismatch); }; const _validations = { - string: (v, schema, options = []) => typeof v === 'string' && (options.length ? options.indexOf(v) > -1 : true), - number: (v, schema, options = []) => typeof v === 'number' && v === v && (options.length ? options.indexOf(v) > -1 : true), - float: (v, schema, options = []) => parseFloat(v) === v && (options.length ? options.indexOf(v) > -1 : true), - integer: (v, schema, options = []) => parseInt(v) === v && v >= Number.MIN_SAFE_INTEGER && v <= Number.MAX_SAFE_INTEGER && (options.length ? options.indexOf(v) > -1 : true), + string: (v, schemas = [], options = []) => typeof v === 'string' && (options.length ? options.indexOf(v) > -1 : true), + number: (v, schemas = [], options = []) => typeof v === 'number' && v === v && (options.length ? options.indexOf(v) > -1 : true), + float: (v, schemas = [], options = []) => parseFloat(v) === v && (options.length ? options.indexOf(v) > -1 : true), + integer: (v, schemas = [], options = []) => parseInt(v) === v && v >= Number.MIN_SAFE_INTEGER && v <= Number.MAX_SAFE_INTEGER && (options.length ? options.indexOf(v) > -1 : true), boolean: (v) => typeof v === 'boolean', - object: (v, schema = []) => { + object: (v, schemas = [], options = [], range = {}, stack = [], mismatch = {}) => { if (!!v && typeof v === 'object' && !Array.isArray(v) && !Buffer.isBuffer(v)) { - for (let i = 0; i < schema.length; i++) { - let param = schema[i]; - if (!v.hasOwnProperty(param.name) && !param.hasOwnProperty('defaultValue')) { - return false; - } else if ( - !validate( - param.type, - v.hasOwnProperty(param.name) - ? v[param.name] - : param.defaultValue, - param.defaultValue === null, - param.type === 'enum' - ? param.members - : param.schema, - param.options && param.options.values - ) - ) { - return false; + let matchedSchemas = schemas.filter(schema => { + for (let i = 0; i < schema.length; i++) { + let param = schema[i]; + if (!v.hasOwnProperty(param.name) && !param.hasOwnProperty('defaultValue')) { + mismatch.stack = mismatch.stack || stack.concat(param.name); + return false; + } else if ( + !validate( + param.type, + v.hasOwnProperty(param.name) + ? v[param.name] + : param.defaultValue, + param.defaultValue === null, + ( + param.type === 'enum' + ? param.members + : (param.alternateSchemas || []).concat(param.schema ? [param.schema] : []) + ), + param.options && param.options.values, + param.range, + stack.concat(param.name), + mismatch + ) + ) { + mismatch.stack = mismatch.stack || stack.concat(param.name); + return false; + } } - } - return true; + return true; + }); + return schemas.length > 0 + ? matchedSchemas.length > 0 + : true; } else { + mismatch.stack = mismatch.stack || stack; return false; } }, - array: (v, schema = []) => { + array: (v, schemas = [], options = [], range = {}, stack = [], mismatch = {}) => { + let schema = schemas[0]; if (Array.isArray(v)) { - let param = schema[0]; + let param = schema && schema[0]; if (param) { for (let i = 0; i < v.length; i++) { let invalidParam = !validate( param.type, v[i], param.defaultValue === null, - param.type === 'enum' - ? param.members - : param.schema, - param.options && param.options.values + ( + param.type === 'enum' + ? param.members + : (param.alternateSchemas || []).concat(param.schema ? [param.schema] : []) + ), + param.options && param.options.values, + param.range, + stack.length + ? stack.slice(0, stack.length - 1).concat(`${stack[stack.length - 1]}[${i}]`) + : stack, + mismatch ); if (invalidParam) { + mismatch.stack = mismatch.stack || ( + stack.length + ? stack.slice(0, stack.length - 1).concat(`${stack[stack.length - 1]}[${i}]`) + : stack + ); return false; } } } return true; } else { + mismatch.stack = mismatch.stack || ( + stack.length + ? stack.slice(0, stack.length - 1).concat(`${stack[stack.length - 1]}[]`) + : stack + ); return false; } }, @@ -75,8 +117,31 @@ const _validations = { ? members.some(member => member[0] === v) : false; }, - 'object.http': v => _validations.object(v), - 'object.keyql.query': (v, schema, options = []) => { + 'object.http': v => { + let isObject = _validations.object(v); + if (!isObject) { + return false; + } else { + return ( + ( + Object.keys(v).length === 1 && + v.hasOwnProperty('body') + ) || + ( + Object.keys(v).length === 2 && + v.hasOwnProperty('body') && + v.hasOwnProperty('headers') + ) || + ( + Object.keys(v).length === 3 && + v.hasOwnProperty('body') && + v.hasOwnProperty('headers') && + v.hasOwnProperty('statusCode') + ) + ); + } + }, + 'object.keyql.query': (v, schemas = [], options = []) => { try { KeyQL.validateQueryObject(v, options); } catch (e) { @@ -84,13 +149,58 @@ const _validations = { } return true; }, - 'object.keyql.limit': v => _validations.object(v), + 'object.keyql.limit': (v, schemas = [], options = []) => { + try { + KeyQL.validateLimit(v); + } catch (e) { + return false; + } + return true; + }, + 'object.keyql.order': (v, schemas = [], options = []) => { + try { + KeyQL.validateOrderObject(v, options); + } catch (e) { + return false; + } + return true; + }, buffer: v => Buffer.isBuffer(_format.buffer(v)), any: v => true }; // Unlike validations, sanitizations will throw errors if failed const _sanitizations = { + number: (v, range) => { + if (range) { + if (v < range.min) { + throw new Error(`must be greater than or equal to ${range.min}`); + } else if (v > range.max) { + throw new Error(`must less than or equal to ${range.max}`); + } + } + return v; + }, + float: (v, range) => { + if (range) { + if (v < range.min) { + throw new Error(`must be greater than or equal to ${range.min}`); + } else if (v > range.max) { + throw new Error(`must less than or equal to ${range.max}`); + } + } + return v; + }, + integer: (v, range) => { + if (range) { + if (v < range.min) { + throw new Error(`must be greater than or equal to ${range.mind}`); + } else if (v > range.max) { + throw new Error(`must less than or equal to ${range.max}`); + } + } + return v; + }, 'object.http': v => { if (!v || typeof v !== 'object') { return v; @@ -121,8 +231,19 @@ const _sanitizations = { KeyQL.validateQueryObject(v); return v; }, - 'object.keyql.limit': v => { - KeyQL.validateLimit(v) + 'object.keyql.limit': (v, range) => { + v = KeyQL.validateLimit(v); + if (range) { + if (v.count < range.min) { + throw new Error(`count must be greater than or equal to ${range[0]}`); + } else if (v.count > range.max) { + throw new Error(`count must less than or equal to ${range[1]}`); + } + } + return v; + }, + 'object.keyql.order': v => { + KeyQL.validateOrderObject(v); return v; } }; @@ -144,11 +265,12 @@ const _format = { float: v => v, integer: v => v, boolean: v => v, - object: v => v, + object: v => _convertBuffers(v, true, true), 'object.http': v => v, 'object.keyql.query': v => v, 'object.keyql.limit': v => v, - array: v => v, + 'object.keyql.order': v => v, + array: v => _convertBuffers(v, true, true), buffer: v => { if (!v || typeof v !== 'object' || Buffer.isBuffer(v)) { return v; @@ -283,11 +405,42 @@ const _convert = { s = s.trim().toLowerCase(); return s in convert ? !!convert[s] : s; }, - object: s => JSON.parse(s), + object: (s, schema) => { + let obj = s; + if (typeof obj !== 'object') { + obj = JSON.parse(s); + } + if (schema && schema.length && obj && typeof obj === 'object') { + schema + .filter(param => param.name in obj) + .forEach(param => { + obj[param.name] = _convert[param.type](obj[param.name]); + }); + } + return obj; + }, 'object.http': s => JSON.parse(s), 'object.keyql.query': s => JSON.parse(s), - 'object.keyql.limit': s => JSON.parse(s), - array: s => JSON.parse(s), + 'object.keyql.limit': s => { + return _convert['object']( + s, + [ + {name: 'count', type: 'integer'}, + {name: 'offset', type: 'integer'} + ] + ); + }, + 'object.keyql.order': s => JSON.parse(s), + array: (s, schema) => { + let arr = s; + if (!Array.isArray(arr)) { + arr = JSON.parse(s); + } + if (schema && schema.length && Array.isArray(arr)) { + arr = arr.map(_convert[schema[0].type]); + } + return arr; + }, buffer: s => { let o = JSON.parse(s); return _format.buffer(o); @@ -295,15 +448,15 @@ const _convert = { any: s => s }; -const _convertBuffers = function (v, base) { - v = base ? _format.buffer(v) : v; +const _convertBuffers = function (v, applyFormatting, formatRecursively) { + v = applyFormatting ? _format.buffer(v) : v; if (Buffer.isBuffer(v)) { - return base ? v : {_base64: v.toString('base64')}; + return applyFormatting ? v : {_base64: v.toString('base64')}; } else if (Array.isArray(v)) { - return v.map(v => _convertBuffers(v)); + return v.map(v => _convertBuffers(v, formatRecursively ? applyFormatting : false, formatRecursively)); } else if (v && typeof v === 'object') { return Object.keys(v).reduce(function (n, key) { - n[key] = _convertBuffers(v[key]); + n[key] = _convertBuffers(v[key], formatRecursively ? applyFormatting : false, formatRecursively); return n; }, {}); } else { @@ -322,6 +475,20 @@ const _httpResponse = { body: _convertBuffers(v, true) }; }, + 'any': (v, headers) => { + if (_validations['object.http'](v)) { + return _sanitizations['object.http'](v); + } else { + headers = headers || {}; + let statusCode = headers.status || 200; // legacy support + delete headers.status; + return { + statusCode: statusCode, + headers: headers, + body: _convertBuffers(v, true) + }; + } + }, 'object.http': (v, headers) => { v.headers = Object.keys(v.headers || {}).reduce((headers, key) => { headers[key] = key.startsWith('x-') @@ -333,20 +500,50 @@ const _httpResponse = { } }; +const _generateSchema = { + javascript: function (payload, defaultDepth = 0) { + let definition = _introspect(payload); + if (definition.type !== 'object') { + throw new Error('Can only generateSchema from an object'); + } + let parseSchema = (schema, depth = 0) => { + return schema.map((field) => { + let prefix = !depth + ? ` * @param ` + : ` * @ ${' '.repeat((depth - 1) * 2)}`; + let fieldContent = field.name ? ` ${field.name}` : ''; + if (field.schema) { + return [ + `${prefix}{${'defaultValue' in field ? '?' : ''}${field.type}}${fieldContent}`, + parseSchema(field.schema, depth + 1) + ].join('\n'); + } else { + return `${prefix}{${'defaultValue' in field ? '?' : ''}${field.type}}${fieldContent}`; + } + }).join('\n'); + }; + let commentDefinition = parseSchema(definition.schema, defaultDepth); + return commentDefinition; + } +}; + module.exports = { optionsAllowed: OPTIONS_ALLOWED, + rangeAllowed: RANGE_ALLOWED, defaultType: 'any', list: Object.keys(_validations), validate: validate, + serialize: (type, v) => _convertBuffers(v), convert: (type, s) => _convert[type](s), format: (type, v) => _format[type](v), - parse: (type, v, convert) => { + parse: (type, schema, v, convert) => { return convert - ? _format[type in _format ? type : ''](_convert[type](v)) + ? _format[type in _format ? type : ''](_convert[type](v, schema)) : _format[type](v); }, check: (v) => _check(v), introspect: (v) => _introspect(v), - sanitize: (type, v) => type in _sanitizations ? _sanitizations[type](v) : v, - httpResponse: (type, v, headers) => type in _httpResponse ? _httpResponse[type](v, headers) : _httpResponse[''](v, headers) + generateSchema: _generateSchema, + sanitize: (type, v, range) => type in _sanitizations ? _sanitizations[type](v, range) : v, + httpResponse: (type, v, headers = {}) => type in _httpResponse ? _httpResponse[type](v, headers) : _httpResponse[''](v, headers) }; diff --git a/lib/well-knowns.js b/lib/well-knowns.js new file mode 100644 index 0000000..c5a21b1 --- /dev/null +++ b/lib/well-knowns.js @@ -0,0 +1,453 @@ +const YAML = require('json-to-pretty-yaml'); + +const FunctionScriptToJSONSchemaMapping = { + 'boolean': (param) => { + let json = {}; + json.type = 'boolean'; + return json; + }, + 'string': (param) => { + let json = {}; + json.type = 'string'; + if (param.options && param.options.values) { + json.enum = param.options.values; + } + return json; + }, + 'number': (param) => { + let json = {}; + json.type = 'number'; + if (param.range) { + json.minimum = param.range[0]; + json.maximum = param.range[1]; + } + if (param.options && param.options.values) { + json.enum = param.options.values; + } + return json; + }, + 'float': (param) => { + let json = {}; + json.type = 'number'; + if (param.range) { + json.minimum = param.range[0]; + json.maximum = param.range[1]; + } + if (param.options && param.options.values) { + json.enum = param.options.values; + } + return json; + }, + 'integer': (param) => { + let json = {}; + json.type = 'integer'; + if (param.range) { + json.minimum = param.range[0]; + json.maximum = param.range[1]; + } + if (param.options) { + json.enum = param.options; + } + return json; + }, + 'array': (param) => { + let json = {}; + json.type = 'array'; + if (param.schema && param.schema.length) { + json.items = convertParamToJSONSchema(param.schema[0]); + } + return json; + }, + 'object': (param) => { + let schemas = [param.schema || []].concat(param.alternateSchemas || []); + schemas = schemas.filter(schema => schema.length > 0); + if (schemas.length > 1) { + return { + oneOf: schemas.map(schema => convertParametersArrayToJSONSchema(schema)) + }; + } else { + return convertParametersArrayToJSONSchema(schemas[0] || []); + } + }, + 'enum': (param) => { + let json = {}; + json.enum = param.members.map(member => member[0]); + return json; + }, + 'buffer': (param) => { + let json = {}; + json.type = 'object'; + json.properties = { + '_base64': {type: 'string', contentEncoding: 'base64'}, + }; + return json; + }, + 'object.http': (param) => { + let json = {}; + json.type = 'object'; + json.properties = { + 'statusCode': {type: 'integer'}, + 'headers': {type: 'object'}, + 'body': {type: 'string'} + }; + return json; + }, + 'object.keyql.query': (param) => { + let json = {}; + json.type = 'object'; + return json; + }, + 'object.keyql.limit': (param) => { + let json = {}; + json.type = 'object'; + json.properties = { + 'count': {type: 'integer', minimum: 0}, + 'offset': {type: 'integer', minimum: 0} + }; + return json; + }, + 'object.keyql.order': (param) => { + let json = {}; + json.type = 'array'; + json.items = { + type: 'object', + properties: { + 'field': {type: 'string'}, + 'sort': {enum: ['ASC', 'DESC']} + } + }; + return json; + }, + 'any': (param) => { + let json = {}; + return json; + } +}; + +const convertParamToJSONSchema = (param) => { + let convert = FunctionScriptToJSONSchemaMapping[param.type]; + if (typeof convert === 'function') { + let json = convert(param); + if (param.description) { + json.description = param.description; + } + if (param.defaultValue !== null) { + json.default = param.defaultValue; + } + return json; + } else { + throw new Error(`Invalid param type for JSON Schema: "${param.type}"`); + } +}; + +const convertParametersArrayToJSONSchema = (params) => { + return params.reduce((obj, param) => { + obj.properties[param.name] = convertParamToJSONSchema(param); + if (!param.hasOwnProperty('defaultValue')) { + obj.required = obj.required || []; + obj.required.push(param.name); + } + return obj; + }, { + type: 'object', + properties: {} + }); +}; + +const generateOpenAPISpec = (definitions, plugin, server, origin, identifier) => { + + const paths = Object.keys(definitions) + .filter(key => { + let def = definitions[key]; + return def.format.language !== 'static'; + }) + .reduce((paths, key) => { + let def = definitions[key]; + let parts = key.split('/'); + if (!parts[parts.length - 1]) { + parts.pop(); + } + let name = parts.join('_').replace(/[^a-z0-9\-\_]+/gi, '-') || '_'; + let route = `/${key}`; + if (route.match(/:notfound$/)) { + route = route.replace(/:notfound$/, '*/'); + } else if (!route.endsWith('/')) { + route = route + '/'; + } + let opIdentifier = identifier.replace(/\[(.*)\]/gi, ''); + let operationId = `${opIdentifier}${parts.length ? ('.' + parts.join('.')) : ''}`; + operationId = operationId.replace(/[^A-Z0-9_]+/gi, '_'); + operationId = operationId.replace(/^_+/gi, ''); + operationId = operationId.replace(/_+$/gi, ''); + let pathData = { + description: def.description, + operationId: operationId + }; + // If we have at least one required parameter... + if (def.params.filter(param => !param.hasOwnProperty('defaultValue').length > 0)) { + pathData.requestBody = { + 'content': { + 'application/json': { + 'schema': { + ...convertParametersArrayToJSONSchema(def.params) + } + } + } + } + }; + if (def.returns && def.returns.type !== 'object.http') { + pathData.responses = { + '200': { + 'content': { + 'application/json': { + 'schema': { + ...convertParamToJSONSchema(def.returns) + } + } + } + } + }; + } + paths[route] = { + post: pathData + }; + return paths; + }, {}); + + const spec = { + openapi: '3.1.0', + info: { + version: plugin.version, + title: plugin.name, + description: plugin.description + }, + servers: [ + { + url: origin, + description: server + } + ], + paths: paths + }; + + if (plugin.termsOfService) { + spec.info.termsOfService = plugin.termsOfService; + } + + if (plugin.contact) { + const {name, url, email} = plugin.contact; + if (name || url || email) { + spec.info.contact = {}; + } + name && (spec.info.contact.name = name); + url && (spec.info.contact.url = url); + email && (spec.info.contact.email = email); + } + + return spec; + +}; + +const wellKnowns = { + + validatePlugin: (rawPlugin, origin) => { + + const plugin = {}; + + if (rawPlugin && typeof rawPlugin !== 'object' || Array.isArray(rawPlugin)) { + throw new Error(`"plugin" must be an object`); + } + + plugin.name = rawPlugin.name || '(No name provided)'; + plugin.description = rawPlugin.description || '(No description provided)'; + plugin.version = rawPlugin.version || 'local'; + plugin.image_url = rawPlugin.image_url || null; + if ( + typeof plugin.image_url === 'string' && + plugin.image_url.startsWith('/') + ) { + plugin.image_url = `${origin}${plugin.image_url}`; + } + + plugin.forModel = rawPlugin.forModel || {}; + plugin.forModel.name = plugin.forModel.name || plugin.name; + plugin.forModel.description = plugin.forModel.description || plugin.description; + + plugin.termsOfService = rawPlugin.termsOfService || null; + if ( + typeof plugin.termsOfService === 'string' && + plugin.termsOfService.startsWith('/') + ) { + plugin.termsOfService = `${origin}${plugin.termsOfService}`; + } + + plugin.contact = rawPlugin.contact || {}; + + const checkPluginValue = (name, isRequired = false) => { + let names = name.split('.'); + let check = plugin; + for (let i = 0; i < names.length; i++) { + let n = names[i]; + check = check[n]; + } + if (typeof check !== 'string') { + if (check === null || check === void 0) { + if (isRequired) { + throw new Error(`plugin.${name} is required`); + } + } else { + throw new Error(`plugin.${name} must be a string`); + } + } + }; + + [ + 'name', + 'description', + 'version', + 'forModel.name', + 'forModel.description' + ].forEach(name => checkPluginValue(name, true)); + + [ + 'image_url', + 'termsOfService', + 'contact.name', + 'contact.url', + 'contact.email' + ].forEach(name => checkPluginValue(name)); + + return plugin; + + }, + + handlers: { + + '.well-known/plugin.json': (definitions, plugin, server, origin, identifier) => { + + const body = Buffer.from(JSON.stringify(plugin, null, 2)); + return { + headers: { + 'Content-Type': 'application/json' + }, + body + }; + + }, + + '.well-known/ai-plugin.json': (definitions, plugin, server, origin, identifier) => { + + const AIPlugin = {}; + AIPlugin.schema_version = 'v1'; + AIPlugin.name_for_human = plugin.name.slice(0, 20); + AIPlugin.name_for_model = plugin.forModel.name.slice(0, 50); + AIPlugin.name_for_model = AIPlugin.name_for_model.replace(/[^A-Z0-9_]+/gi, '_'); + AIPlugin.name_for_model = AIPlugin.name_for_model.replace(/^_+/gi, ''); + AIPlugin.name_for_model = AIPlugin.name_for_model.replace(/_+$/gi, ''); + AIPlugin.description_for_human = plugin.description.slice(0, 100); + AIPlugin.description_for_model = plugin.forModel.description.slice(0, 8000); + AIPlugin.auth = { + type: 'none' + }; + AIPlugin.api = { + type: 'openapi', + url: `${origin}/.well-known/openapi.yaml` + }; + AIPlugin.logo_url = `${origin}/logo.png`; + AIPlugin.contact_email = `noreply@${origin.replace(/^https?\:\/\/(.*)(:\d+)/gi, '$1')}`; + AIPlugin.legal_info_url = `${origin}/tos.txt`; + if (plugin.image_url) { + AIPlugin.logo_url = plugin.image_url; + } + if (plugin.contact && plugin.contact.email) { + AIPlugin.contact_email = plugin.contact.email; + } + if (plugin.termsOfService) { + AIPlugin.legal_info_url = plugin.termsOfService; + } + + return { + headers: { + 'Content-Type': 'application/json' + }, + body: Buffer.from(JSON.stringify(AIPlugin, null, 2)) + }; + + }, + + '.well-known/openapi.json': (definitions, plugin, server, origin, identifier) => { + + const spec = generateOpenAPISpec(definitions, plugin, server, origin, identifier); + const body = Buffer.from(JSON.stringify(spec, null, 2)); + return { + headers: { + 'Content-Type': 'application/json' + }, + body + }; + + }, + + '.well-known/openapi.yaml': (definitions, plugin, server, origin, identifier) => { + + const spec = generateOpenAPISpec(definitions, plugin, server, origin, identifier); + const body = Buffer.from(YAML.stringify(spec)); + return { + headers: { + 'Content-Type': 'application/yaml' + }, + body + }; + + }, + + '.well-known/schema.json': (definitions, plugin, server, origin, identifier) => { + + const schema = Object.keys(definitions) + .filter(key => { + let def = definitions[key]; + return def.format.language !== 'static'; + }) + .map(key => { + let def = definitions[key]; + let parts = key.split('/'); + if (!parts[parts.length - 1]) { + parts.pop(); + } + let name = parts.join('_').replace(/[^a-z0-9\-\_]+/gi, '-') || '_'; + let route = `/${key}`; + if (route.match(/:notfound$/)) { + route = route.replace(/:notfound$/, '*/'); + } else if (!route.endsWith('/')) { + route = route + '/'; + } + return { + name: name, + description: def.description, + route: route, + url: `${origin}${route}`, + method: `POST`, + lib: `${identifier}${parts.length ? ('.' + parts.join('.')) : ''}`, + parameters: convertParametersArrayToJSONSchema(def.params) + }; + }); + + const response = { + functions: schema + }; + + const body = Buffer.from(JSON.stringify(response, null, 2)); + + return { + headers: { + 'Content-Type': 'application/json' + }, + body + }; + + } + } + +}; + +module.exports = wellKnowns; diff --git a/package.json b/package.json index 34e547c..74e2a23 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "functionscript", - "version": "1.4.0", - "description": "A language and specification for turning JavaScript functions into typed HTTP APIs", + "version": "2.10.6", + "description": "An API gateway and framework for turning functions into web services", "author": "Keith Horwood ", "main": "index.js", "scripts": { @@ -9,14 +9,17 @@ "output": "node ./tests/output.js" }, "dependencies": { - "babylon": "^6.16.1", - "keyql": "^0.1.4", + "@babel/parser": "^7.12.11", + "fast-xml-parser": "^3.19.0", + "json-to-pretty-yaml": "^1.2.2", + "keyql": "^0.2.2", + "mime": "^2.5.2", "minimatch": "^3.0.4", "uuid": "^3.0.1" }, "devDependencies": { "chai": "^3.5.0", "form-data": "^2.3.2", - "mocha": "^6.1.4" + "mocha": "^10.1.0" } } diff --git a/tests/files/cases/function_invalid_background.js b/tests/files/cases/function_invalid_background.js new file mode 100644 index 0000000..de01cd9 --- /dev/null +++ b/tests/files/cases/function_invalid_background.js @@ -0,0 +1,10 @@ +/** +* Function with an invalid background +* @background hello +* @returns {string} +*/ +module.exports = (callback) => { + + return callback(null, 'invalid acl entry'); + +}; diff --git a/tests/files/cases/function_invalid_definition_order_charge.js b/tests/files/cases/function_invalid_definition_order_charge.js index 11091a9..95d876a 100644 --- a/tests/files/cases/function_invalid_definition_order_charge.js +++ b/tests/files/cases/function_invalid_definition_order_charge.js @@ -1,6 +1,6 @@ /** * Function with invalid function definition order due to charge -* @bg empty +* @background empty * @acl * * user_username faas_tester deny * user_username faas_tester2 deny diff --git a/tests/files/cases/function_invalid_keyql_limit_default.js b/tests/files/cases/function_invalid_keyql_limit_default.js new file mode 100644 index 0000000..a65c4c0 --- /dev/null +++ b/tests/files/cases/function_invalid_keyql_limit_default.js @@ -0,0 +1,8 @@ +/** +* Invalid KeyQL Limit +* @param {object.keyql.limit} limit Some limit +* @returns {string} +*/ +module.exports = async (limit = {count: 0, fhg: 'wat'}) => { + return 'hello'; +}; diff --git a/tests/files/cases/function_invalid_keyql_limit_float.js b/tests/files/cases/function_invalid_keyql_limit_float.js new file mode 100644 index 0000000..b8e1c37 --- /dev/null +++ b/tests/files/cases/function_invalid_keyql_limit_float.js @@ -0,0 +1,8 @@ +/** +* Invalid KeyQL Limit +* @param {object.keyql.limit} limit Some limit +* @returns {string} +*/ +module.exports = async (limit = {count: 0.354, offset: 0}) => { + return 'hello'; +}; diff --git a/tests/files/cases/function_invalid_keyql_limit_mismatch.js b/tests/files/cases/function_invalid_keyql_limit_mismatch.js new file mode 100644 index 0000000..e27aced --- /dev/null +++ b/tests/files/cases/function_invalid_keyql_limit_mismatch.js @@ -0,0 +1,8 @@ +/** +* Invalid KeyQL Limit +* @param {object.keyql.limit} limit Some limit {:} [5, 4] +* @returns {string} +*/ +module.exports = async (limit) => { + return 'hello'; +}; diff --git a/tests/files/cases/function_invalid_keyql_limit_oob.js b/tests/files/cases/function_invalid_keyql_limit_oob.js new file mode 100644 index 0000000..9c21ac6 --- /dev/null +++ b/tests/files/cases/function_invalid_keyql_limit_oob.js @@ -0,0 +1,8 @@ +/** +* Invalid KeyQL Limit +* @param {object.keyql.limit} limit Some limit +* @returns {string} +*/ +module.exports = async (limit = {count: -1, offset: 0}) => { + return 'hello'; +}; diff --git a/tests/files/cases/function_invalid_keyql_limit_oob_offset.js b/tests/files/cases/function_invalid_keyql_limit_oob_offset.js new file mode 100644 index 0000000..f45bcf0 --- /dev/null +++ b/tests/files/cases/function_invalid_keyql_limit_oob_offset.js @@ -0,0 +1,8 @@ +/** +* Invalid KeyQL Limit +* @param {object.keyql.limit} limit Some limit +* @returns {string} +*/ +module.exports = async (limit = {count: 0, offset: -1}) => { + return 'hello'; +}; diff --git a/tests/files/cases/function_invalid_keyql_limit_oob_user_lb.js b/tests/files/cases/function_invalid_keyql_limit_oob_user_lb.js new file mode 100644 index 0000000..6173526 --- /dev/null +++ b/tests/files/cases/function_invalid_keyql_limit_oob_user_lb.js @@ -0,0 +1,8 @@ +/** +* Invalid KeyQL Limit +* @param {object.keyql.limit} limit Some limit {:} [2, 20] +* @returns {string} +*/ +module.exports = async (limit = {count: 1, offset: 0}) => { + return 'hello'; +}; diff --git a/tests/files/cases/function_invalid_keyql_limit_oob_user_ub.js b/tests/files/cases/function_invalid_keyql_limit_oob_user_ub.js new file mode 100644 index 0000000..c43bd49 --- /dev/null +++ b/tests/files/cases/function_invalid_keyql_limit_oob_user_ub.js @@ -0,0 +1,8 @@ +/** +* Invalid KeyQL Limit +* @param {object.keyql.limit} limit Some limit {:} [2, 20] +* @returns {string} +*/ +module.exports = async (limit = {count: 21, offset: 0}) => { + return 'hello'; +}; diff --git a/tests/files/cases/function_invalid_keyql_options.js b/tests/files/cases/function_invalid_keyql_options.js index 35aab61..4515559 100644 --- a/tests/files/cases/function_invalid_keyql_options.js +++ b/tests/files/cases/function_invalid_keyql_options.js @@ -1,5 +1,5 @@ /** -* Valid KeyQL Query with Options +* Invalid KeyQL Query with Options * @param {object.keyql.query} query Query API based on these parameters {?} ["status", null, "goodbye"] * @returns {string} */ diff --git a/tests/files/cases/function_invalid_keyql_order_options.js b/tests/files/cases/function_invalid_keyql_order_options.js new file mode 100644 index 0000000..77b265b --- /dev/null +++ b/tests/files/cases/function_invalid_keyql_order_options.js @@ -0,0 +1,8 @@ +/** +* Invalid KeyQL Query with Options +* @param {object.keyql.order} order Order for keyql {?} ["status", null, "goodbye"] +* @returns {string} +*/ +module.exports = async (order) => { + return 'hello'; +}; diff --git a/tests/files/cases/function_invalid_multiple_schema.js b/tests/files/cases/function_invalid_multiple_schema.js new file mode 100644 index 0000000..263a4cc --- /dev/null +++ b/tests/files/cases/function_invalid_multiple_schema.js @@ -0,0 +1,13 @@ +/** +* Accepts multiple schemas for an object +* @param {object} fileOrFolder +* @ {string} name +* @ {number} size +* @ OR +* @ {string} name2 +* @ {number} size2 +* @returns {object} +*/ +module.exports = async (fileOrFolder) => { + return true; +}; diff --git a/tests/files/cases/function_invalid_multiple_schema_empty.js b/tests/files/cases/function_invalid_multiple_schema_empty.js new file mode 100644 index 0000000..2b99003 --- /dev/null +++ b/tests/files/cases/function_invalid_multiple_schema_empty.js @@ -0,0 +1,11 @@ +/** +* Accepts multiple schemas for an object +* @param {object} fileOrFolder +* @ OR +* @ {string} name +* @ {number} size +* @returns {object} +*/ +module.exports = async (fileOrFolder) => { + return true; +}; diff --git a/tests/files/cases/function_invalid_multiple_schema_empty_late.js b/tests/files/cases/function_invalid_multiple_schema_empty_late.js new file mode 100644 index 0000000..53c97aa --- /dev/null +++ b/tests/files/cases/function_invalid_multiple_schema_empty_late.js @@ -0,0 +1,11 @@ +/** +* Accepts multiple schemas for an object +* @param {object} fileOrFolder +* @ {string} name +* @ {number} size +* @ OR +* @returns {object} +*/ +module.exports = async (fileOrFolder) => { + return true; +}; diff --git a/tests/files/cases/function_invalid_multiple_schema_nested.js b/tests/files/cases/function_invalid_multiple_schema_nested.js new file mode 100644 index 0000000..d14f0e6 --- /dev/null +++ b/tests/files/cases/function_invalid_multiple_schema_nested.js @@ -0,0 +1,17 @@ +/** +* Accepts multiple schemas for an object +* @param {object} fileOrFolder +* @ {string} name +* @ {number} size +* @ OR +* @ {string} name2 +* @ {number} size2 +* @ {object} props +* @ {string} label +* @ OR +* @ {number} size +* @returns {object} +*/ +module.exports = async (fileOrFolder) => { + return true; +}; diff --git a/tests/files/cases/function_invalid_origin.js b/tests/files/cases/function_invalid_origin.js new file mode 100644 index 0000000..bc8812d --- /dev/null +++ b/tests/files/cases/function_invalid_origin.js @@ -0,0 +1,10 @@ +/** +* Function with an invalid origin +* @origin *.autocode.com +* @returns {string} +*/ +module.exports = (callback) => { + + return callback(null, 'origin'); + +}; diff --git a/tests/files/cases/function_invalid_origin_asterisk.js b/tests/files/cases/function_invalid_origin_asterisk.js new file mode 100644 index 0000000..ed024b8 --- /dev/null +++ b/tests/files/cases/function_invalid_origin_asterisk.js @@ -0,0 +1,10 @@ +/** +* Function with an invalid origin +* @origin * +* @returns {string} +*/ +module.exports = (callback) => { + + return callback(null, 'origin'); + +}; diff --git a/tests/files/cases/function_invalid_origin_bad_protocol.js b/tests/files/cases/function_invalid_origin_bad_protocol.js new file mode 100644 index 0000000..acfc712 --- /dev/null +++ b/tests/files/cases/function_invalid_origin_bad_protocol.js @@ -0,0 +1,10 @@ +/** +* Function with an invalid origin +* @origin file://www.autocode.com +* @returns {string} +*/ +module.exports = (callback) => { + + return callback(null, 'origin'); + +}; diff --git a/tests/files/cases/function_invalid_parameter_not.js b/tests/files/cases/function_invalid_parameter_not.js new file mode 100644 index 0000000..9cfab89 --- /dev/null +++ b/tests/files/cases/function_invalid_parameter_not.js @@ -0,0 +1,9 @@ +/** +* My function +* @param {string} a alpha +* @param {number} b beta +* @returns {string} +*/ +module.exports = async (a = 'hi', b = !7, context) => { + // valid, negative number +}; diff --git a/tests/files/cases/function_invalid_parameter_not_not.js b/tests/files/cases/function_invalid_parameter_not_not.js new file mode 100644 index 0000000..2f3c645 --- /dev/null +++ b/tests/files/cases/function_invalid_parameter_not_not.js @@ -0,0 +1,9 @@ +/** +* My function +* @param {string} a alpha +* @param {number} b beta +* @returns {string} +*/ +module.exports = async (a = 'hi', b = !!7, context) => { + // valid, negative number +}; diff --git a/tests/files/cases/function_invalid_parameter_reserved_name.js b/tests/files/cases/function_invalid_parameter_reserved_name.js new file mode 100644 index 0000000..e3e0016 --- /dev/null +++ b/tests/files/cases/function_invalid_parameter_reserved_name.js @@ -0,0 +1,8 @@ +/** +* Invalid param - reserved name +* @param {string} _stream +* @returns {boolean} +*/ +module.exports = async (_stream, context) => { + return true; +}; diff --git a/tests/files/cases/function_invalid_stream_no_name.js b/tests/files/cases/function_invalid_stream_no_name.js new file mode 100644 index 0000000..9306012 --- /dev/null +++ b/tests/files/cases/function_invalid_stream_no_name.js @@ -0,0 +1,8 @@ +/** +* Invalid stream - no name +* @stream {string} +* @returns {boolean} +*/ +module.exports = async (context) => { + return true; +}; diff --git a/tests/files/cases/function_invalid_stream_nonfriendly_name.js b/tests/files/cases/function_invalid_stream_nonfriendly_name.js new file mode 100644 index 0000000..b112fe8 --- /dev/null +++ b/tests/files/cases/function_invalid_stream_nonfriendly_name.js @@ -0,0 +1,8 @@ +/** +* Invalid stream - nonfriendly name +* @stream {string} ?stream +* @returns {boolean} +*/ +module.exports = async (context) => { + return true; +}; diff --git a/tests/files/cases/function_valid_array_keyql_order_options.js b/tests/files/cases/function_valid_array_keyql_order_options.js new file mode 100644 index 0000000..e0ccfd5 --- /dev/null +++ b/tests/files/cases/function_valid_array_keyql_order_options.js @@ -0,0 +1,9 @@ +/** +* Valid KeyQL Query with Options +* @param {array} order +* @ {object.keyql.order} orderEntry Order for keyql {?} ["status", "color", "goodbye"] +* @returns {string} +*/ +module.exports = async (order) => { + return 'hello'; +}; diff --git a/tests/files/cases/function_valid_background.js b/tests/files/cases/function_valid_background.js new file mode 100644 index 0000000..2b5b53b --- /dev/null +++ b/tests/files/cases/function_valid_background.js @@ -0,0 +1,10 @@ +/** +* Function with an valid background +* @background +* @returns {string} +*/ +module.exports = (callback) => { + + return callback(null, 'invalid acl entry'); + +}; diff --git a/tests/files/cases/function_valid_background_mode.js b/tests/files/cases/function_valid_background_mode.js new file mode 100644 index 0000000..1347667 --- /dev/null +++ b/tests/files/cases/function_valid_background_mode.js @@ -0,0 +1,10 @@ +/** +* Function with an valid set background +* @background params +* @returns {string} +*/ +module.exports = (callback) => { + + return callback(null, 'invalid acl entry'); + +}; diff --git a/tests/files/cases/function_valid_definition_order.js b/tests/files/cases/function_valid_definition_order.js index 340fb4e..b9a07da 100644 --- a/tests/files/cases/function_valid_definition_order.js +++ b/tests/files/cases/function_valid_definition_order.js @@ -1,6 +1,6 @@ /** * Function with valid function definition order -* @bg empty +* @background empty * @charge 10 * @acl * * user_username faas_tester deny diff --git a/tests/files/cases/function_valid_inline.js b/tests/files/cases/function_valid_inline.js new file mode 100644 index 0000000..8bfb529 --- /dev/null +++ b/tests/files/cases/function_valid_inline.js @@ -0,0 +1 @@ +return 'lol'; // valid diff --git a/tests/files/cases/function_valid_inline_addition.js b/tests/files/cases/function_valid_inline_addition.js new file mode 100644 index 0000000..cc20675 --- /dev/null +++ b/tests/files/cases/function_valid_inline_addition.js @@ -0,0 +1,5 @@ +// Valid inline function +let a = 1; +let b = 2; + +return a + b; diff --git a/tests/files/cases/function_valid_inline_await.js b/tests/files/cases/function_valid_inline_await.js new file mode 100644 index 0000000..f59a515 --- /dev/null +++ b/tests/files/cases/function_valid_inline_await.js @@ -0,0 +1,3 @@ +const sleep = t => new Promise(res => setTimeout(() => res(null), t)) +await sleep(100); +return `hello world`; diff --git a/tests/files/cases/function_valid_inline_context.js b/tests/files/cases/function_valid_inline_context.js new file mode 100644 index 0000000..887f0bf --- /dev/null +++ b/tests/files/cases/function_valid_inline_context.js @@ -0,0 +1,4 @@ +// Valid inline function +console.log(context); + +return context.params; diff --git a/tests/files/cases/function_valid_inline_empty.js b/tests/files/cases/function_valid_inline_empty.js new file mode 100644 index 0000000..fce0b49 --- /dev/null +++ b/tests/files/cases/function_valid_inline_empty.js @@ -0,0 +1 @@ +console.log('wat'); // valid diff --git a/tests/files/cases/function_valid_keyql_limit.js b/tests/files/cases/function_valid_keyql_limit.js new file mode 100644 index 0000000..cf646c8 --- /dev/null +++ b/tests/files/cases/function_valid_keyql_limit.js @@ -0,0 +1,8 @@ +/** +* Invalid KeyQL Limit +* @param {object.keyql.limit} limit Some limit {:} [2, 20] +* @returns {string} +*/ +module.exports = async (limit = {count: 15, offset: 0}) => { + return 'hello'; +}; diff --git a/tests/files/cases/function_valid_keyql_order_options.js b/tests/files/cases/function_valid_keyql_order_options.js new file mode 100644 index 0000000..4d03a4a --- /dev/null +++ b/tests/files/cases/function_valid_keyql_order_options.js @@ -0,0 +1,8 @@ +/** +* Valid KeyQL Query with Options +* @param {object.keyql.order} order Order for keyql {?} ["status", "color", "goodbye"] +* @returns {string} +*/ +module.exports = async (order) => { + return 'hello'; +}; diff --git a/tests/files/cases/function_valid_multiline_param.js b/tests/files/cases/function_valid_multiline_param.js new file mode 100644 index 0000000..9155fa7 --- /dev/null +++ b/tests/files/cases/function_valid_multiline_param.js @@ -0,0 +1,11 @@ +/** +* Valid function with a multiline param definition +* @param {string} alpha Not the omega, +* but the ____... +* @returns {boolean} +*/ +module.exports = async (alpha) => { + + return true; + +}; diff --git a/tests/files/cases/function_valid_multiple_schema.js b/tests/files/cases/function_valid_multiple_schema.js new file mode 100644 index 0000000..0e2cb06 --- /dev/null +++ b/tests/files/cases/function_valid_multiple_schema.js @@ -0,0 +1,13 @@ +/** +* Accepts multiple schemas for an object +* @param {object} fileOrFolder +* @ {string} name +* @ {number} size +* @ OR +* @ {string} name2 +* @ {number} size2 +* @returns {object} +*/ +module.exports = async (fileOrFolder) => { + return true; +}; diff --git a/tests/files/cases/function_valid_multiple_schema_nested.js b/tests/files/cases/function_valid_multiple_schema_nested.js new file mode 100644 index 0000000..5ec8501 --- /dev/null +++ b/tests/files/cases/function_valid_multiple_schema_nested.js @@ -0,0 +1,17 @@ +/** +* Accepts multiple schemas for an object +* @param {object} fileOrFolder +* @ {string} name +* @ {number} size +* @ OR +* @ {string} name2 +* @ {number} size2 +* @ {object} props +* @ {string} label +* @ OR +* @ {number} size +* @returns {object} +*/ +module.exports = async (fileOrFolder) => { + return true; +}; diff --git a/tests/files/cases/function_valid_multiple_schema_nested_returns.js b/tests/files/cases/function_valid_multiple_schema_nested_returns.js new file mode 100644 index 0000000..77881fc --- /dev/null +++ b/tests/files/cases/function_valid_multiple_schema_nested_returns.js @@ -0,0 +1,26 @@ +/** +* Accepts multiple schemas for an object +* @param {object} fileOrFolder +* @ {string} name +* @ {number} size +* @ OR +* @ {string} name2 +* @ {number} size2 +* @ {object} props +* @ {string} label +* @ OR +* @ {number} size +* @returns {object} fileOrFolder +* @ {string} name +* @ {number} size +* @ OR +* @ {string} name2 +* @ {number} size2 +* @ {object} props +* @ {string} label +* @ OR +* @ {number} size +*/ +module.exports = async (fileOrFolder) => { + return true; +}; diff --git a/tests/files/cases/function_valid_nested_enum_being_first_in_object_nested_in_arrays.js b/tests/files/cases/function_valid_nested_enum_being_first_in_object_nested_in_arrays.js new file mode 100644 index 0000000..8415e85 --- /dev/null +++ b/tests/files/cases/function_valid_nested_enum_being_first_in_object_nested_in_arrays.js @@ -0,0 +1,50 @@ +/** +* Valid function with nested enums being first fields inside objects nested in arrays +* @param {?array} ruleSet The rules used to assign products to the collection. +* @ {object} obj desc stuff +* @ {enum} relation Specifies the relationship between the `column` and the condition. +* ["CONTAINS", "CONTAINS"] +* ["ENDS_WITH", "ENDS_WITH"] +* ["EQUALS", "EQUALS"] +* ["GREATER_THAN", "GREATER_THAN"] +* ["IS_NOT_SET", "IS_NOT_SET"] +* ["IS_SET", "IS_SET"] +* ["LESS_THAN", "LESS_THAN"] +* ["NOT_CONTAINS", "NOT_CONTAINS"] +* ["NOT_EQUALS", "NOT_EQUALS"] +* ["STARTS_WITH", "STARTS_WITH"] +* @param {?array} ruleSet2 The rules used to assign products to the collection. +* @ {object} obj desc stuff +* @ {enum} relation Specifies the relationship between the `column` and the condition. +* ["CONTAINS", "CONTAINS"] +* ["ENDS_WITH", "ENDS_WITH"] +* ["EQUALS", "EQUALS"] +* ["GREATER_THAN", "GREATER_THAN"] +* ["IS_NOT_SET", "IS_NOT_SET"] +* ["IS_SET", "IS_SET"] +* ["LESS_THAN", "LESS_THAN"] +* ["NOT_CONTAINS", "NOT_CONTAINS"] +* ["NOT_EQUALS", "NOT_EQUALS"] +* ["STARTS_WITH", "STARTS_WITH"] +* @ {boolean} appliedDisjunctively Whether products must match any +* @param {?array} anotherArray The rules used to assign products to the collection. +* @ {object} obj object desc +* @ {array} arr arr desc +* @ {object} obj2 obj2 desc +* @ {enum} opts options +* ["OPTION_ONE", "OPTION_ONE"] +* ["OPTION_TWO", "OPTION_TWO"] +* @ {object} obj3 obj3 desc +* @ {any} id id desc +* @ {array} arr2 Array description +* @ {object} obj3 object desc +* @ {enum} opt2 options2 +* ["OPTION_ONE", "OPTION_ONE"] +* ["OPTION_TWO", "OPTION_TWO"] +* @returns {any} result +*/ +module.exports = async (ruleSet = null, ruleSet2 = null, anotherArray = null, context) => { + + return {}; + +}; diff --git a/tests/files/cases/function_valid_origin.js b/tests/files/cases/function_valid_origin.js new file mode 100644 index 0000000..c7907b7 --- /dev/null +++ b/tests/files/cases/function_valid_origin.js @@ -0,0 +1,10 @@ +/** +* Function with an valid origin +* @origin autocode.com +* @returns {string} +*/ +module.exports = (callback) => { + + return callback(null, 'origin'); + +}; diff --git a/tests/files/cases/function_valid_origin_http_port_subdomain.js b/tests/files/cases/function_valid_origin_http_port_subdomain.js new file mode 100644 index 0000000..5083f4e --- /dev/null +++ b/tests/files/cases/function_valid_origin_http_port_subdomain.js @@ -0,0 +1,10 @@ +/** +* Function with an valid origin +* @origin http://x.y.z.localhost:8000 +* @returns {string} +*/ +module.exports = (callback) => { + + return callback(null, 'origin'); + +}; diff --git a/tests/files/cases/function_valid_origin_https_port_subdomain.js b/tests/files/cases/function_valid_origin_https_port_subdomain.js new file mode 100644 index 0000000..4f277f1 --- /dev/null +++ b/tests/files/cases/function_valid_origin_https_port_subdomain.js @@ -0,0 +1,10 @@ +/** +* Function with an valid origin +* @origin https://x.y.z.localhost:8000 +* @returns {string} +*/ +module.exports = (callback) => { + + return callback(null, 'origin'); + +}; diff --git a/tests/files/cases/function_valid_origin_localhost.js b/tests/files/cases/function_valid_origin_localhost.js new file mode 100644 index 0000000..9de08ab --- /dev/null +++ b/tests/files/cases/function_valid_origin_localhost.js @@ -0,0 +1,10 @@ +/** +* Function with an valid origin +* @origin localhost +* @returns {string} +*/ +module.exports = (callback) => { + + return callback(null, 'origin'); + +}; diff --git a/tests/files/cases/function_valid_origin_port.js b/tests/files/cases/function_valid_origin_port.js new file mode 100644 index 0000000..da2625c --- /dev/null +++ b/tests/files/cases/function_valid_origin_port.js @@ -0,0 +1,10 @@ +/** +* Function with an valid origin +* @origin localhost:8000 +* @returns {string} +*/ +module.exports = (callback) => { + + return callback(null, 'origin'); + +}; diff --git a/tests/files/cases/function_valid_origin_port_subdomain.js b/tests/files/cases/function_valid_origin_port_subdomain.js new file mode 100644 index 0000000..b5e7da4 --- /dev/null +++ b/tests/files/cases/function_valid_origin_port_subdomain.js @@ -0,0 +1,10 @@ +/** +* Function with an valid origin +* @origin x.y.z.localhost:8000 +* @returns {string} +*/ +module.exports = (callback) => { + + return callback(null, 'origin'); + +}; diff --git a/tests/files/cases/function_valid_parameter_negative.js b/tests/files/cases/function_valid_parameter_negative.js new file mode 100644 index 0000000..f87f645 --- /dev/null +++ b/tests/files/cases/function_valid_parameter_negative.js @@ -0,0 +1,9 @@ +/** +* My function +* @param {string} a alpha +* @param {number} b beta +* @returns {string} +*/ +module.exports = async (a = 'hi', b = -7, context) => { + // valid, negative number +}; diff --git a/tests/files/cases/function_valid_parameter_positive.js b/tests/files/cases/function_valid_parameter_positive.js new file mode 100644 index 0000000..c864594 --- /dev/null +++ b/tests/files/cases/function_valid_parameter_positive.js @@ -0,0 +1,9 @@ +/** +* My function +* @param {string} a alpha +* @param {number} b beta +* @returns {string} +*/ +module.exports = async (a = 'hi', b = +7, context) => { + // valid, negative number +}; diff --git a/tests/files/cases/function_valid_parameter_reserved_name_nested.js b/tests/files/cases/function_valid_parameter_reserved_name_nested.js new file mode 100644 index 0000000..b4183d5 --- /dev/null +++ b/tests/files/cases/function_valid_parameter_reserved_name_nested.js @@ -0,0 +1,9 @@ +/** +* Valid param - reserved name in subset +* @param {object} myObject +* @ {string} _stream +* @returns {boolean} +*/ +module.exports = async (myObject, context) => { + return true; +}; diff --git a/tests/files/cases/function_valid_stream.js b/tests/files/cases/function_valid_stream.js new file mode 100644 index 0000000..2fd3163 --- /dev/null +++ b/tests/files/cases/function_valid_stream.js @@ -0,0 +1,8 @@ +/** +* Valid stream - has name +* @stream {string} message +* @returns {boolean} +*/ +module.exports = async (context) => { + return true; +}; diff --git a/tests/files/cases/function_valid_stream_with_name.js b/tests/files/cases/function_valid_stream_with_name.js new file mode 100644 index 0000000..93679d0 --- /dev/null +++ b/tests/files/cases/function_valid_stream_with_name.js @@ -0,0 +1,8 @@ +/** +* Valid Streaming +* @stream {string} hello Some description +* @returns {boolean} +*/ +module.exports = async (context) => { + return true; +}; diff --git a/tests/files/cases/inline_valid_addition.js b/tests/files/cases/inline_valid_addition.js new file mode 100644 index 0000000..cc20675 --- /dev/null +++ b/tests/files/cases/inline_valid_addition.js @@ -0,0 +1,5 @@ +// Valid inline function +let a = 1; +let b = 2; + +return a + b; diff --git a/tests/files/cases/inline_valid_context.js b/tests/files/cases/inline_valid_context.js new file mode 100644 index 0000000..887f0bf --- /dev/null +++ b/tests/files/cases/inline_valid_context.js @@ -0,0 +1,4 @@ +// Valid inline function +console.log(context); + +return context.params; diff --git a/tests/files/comprehensive/alternate_schemas.js b/tests/files/comprehensive/alternate_schemas.js new file mode 100644 index 0000000..b7e3005 --- /dev/null +++ b/tests/files/comprehensive/alternate_schemas.js @@ -0,0 +1,28 @@ +/** +* Provides alternateSchemas +* @param {object} fileOrFolder +* @ {string} name +* @ {integer} size +* @ OR +* @ {string} name +* @ {array} files +* @ {object} options +* @ {string} type +* @ OR +* @ {number} type +* @returns {object} fileOrFolder +* @ {string} name +* @ {integer} size +* @ OR +* @ {string} name +* @ {array} files +* @ {object} options +* @ {string} type +* @ OR +* @ {number} type +*/ +module.exports = async (fileOrFolder) => { + + return {name: 'hello', size: 100}; + +}; diff --git a/tests/files/comprehensive/enum_nested.js b/tests/files/comprehensive/enum_nested.js index 0c04aff..207a32d 100644 --- a/tests/files/comprehensive/enum_nested.js +++ b/tests/files/comprehensive/enum_nested.js @@ -15,8 +15,23 @@ * ["html", "html"] * ["attr", "attr"] * @ {?string} attr If method is "attr", which attribute to retrieve +* @param {object} obj2 +* @ {enum} operator Which data to retrieve: can be "text", "html" or "attr" +* ["text", "text"] +* ["html", "html"] +* ["attr", "attr"] +* @ {string} selector The selector to query +* @ {?string} attr If method is "attr", which attribute to retrieve +* @param {array} arr2 +* @ {object} obj +* @ {enum} operator Which data to retrieve: can be "text", "html" or "attr" +* ["text", "text"] +* ["html", "html"] +* ["attr", "attr"] +* @ {string} selector The selector to query +* @ {?string} attr If method is "attr", which attribute to retrieve * @returns {boolean} myBool A boolean value */ -module.exports = async (obj, arr, context) => { +module.exports = async (obj, arr, obj2, arr2, context) => { return obj.operator; }; diff --git a/tests/files/comprehensive/enum_nested_optional.js b/tests/files/comprehensive/enum_nested_optional.js new file mode 100644 index 0000000..4d3e5f7 --- /dev/null +++ b/tests/files/comprehensive/enum_nested_optional.js @@ -0,0 +1,60 @@ +/** +* Test Optional Nested Enum +* @param {?string} descriptionHtml The description of the product, complete with HTML formatting. +* @param {?array} metafields The metafields to associate with this product. +* @ {object} MetafieldInput Specifies the input fields for a metafield. +* @ {?string} value The value of a metafield. +* @ {?enum} valueType Metafield value types. +* ["STRING", "STRING"] +* ["INTEGER", "INTEGER"] +* ["JSON_STRING", "JSON_STRING"] +* @param {?array} privateMetafields The private metafields to associated with this product. +* @ {object} PrivateMetafieldInput Specifies the input fields for a PrivateMetafield. +* @ {?any} owner The owning resource. +* @ {object} valueInput The value and value type of the metafield, wrapped in a ValueInput object. +* @ {string} value The value of a private metafield. +* @ {enum} valueType Private Metafield value types. +* ["STRING", "STRING"] +* ["INTEGER", "INTEGER"] +* ["JSON_STRING", "JSON_STRING"] +* @param {?array} variants A list of variants associated with the product. +* @ {object} ProductVariantInput Specifies a product variant to create or update. +* @ {?string} barcode The value of the barcode associated with the product. +* @ {?enum} inventoryPolicy The inventory policy for a product variant controls whether customers can continue to buy the variant when it is out of stock. When the value is `continue`, customers are able to buy the variant when it's out of stock. When the value is `deny`, customers can't buy the variant when it's out of stock. +* ["DENY", "DENY"] +* ["CONTINUE", "CONTINUE"] +* @ {?array} metafields Additional customizable information about the product variant. +* @ {object} MetafieldInput Specifies the input fields for a metafield. +* @ {?string} description The description of the metafield . +* @ {?enum} valueType Metafield value types. +* ["STRING", "STRING"] +* ["INTEGER", "INTEGER"] +* ["JSON_STRING", "JSON_STRING"] +* @ {?array} privateMetafields The private metafields to associated with this product. +* @ {object} PrivateMetafieldInput Specifies the input fields for a PrivateMetafield. +* @ {?any} owner The owning resource. +* @ {object} valueInput The value and value type of the metafield, wrapped in a ValueInput object. +* @ {string} value The value of a private metafield. +* @ {enum} valueType Private Metafield value types. +* ["STRING", "STRING"] +* ["INTEGER", "INTEGER"] +* ["JSON_STRING", "JSON_STRING"] +* @ {?string} taxCode The tax code associated with the variant. +* @ {?enum} weightUnit Units of measurement for weight. +* ["KILOGRAMS", "KILOGRAMS"] +* ["GRAMS", "GRAMS"] +* ["POUNDS", "POUNDS"] +* ["OUNCES", "OUNCES"] +* @param {?array} media List of new media to be added to the product. +* @ {object} CreateMediaInput Specifies the input fields required to create a media object. +* @ {string} originalSource The original source of the media object. May be an external URL or signed upload URL. +* @ {enum} mediaContentType The possible content types for a media object. +* ["VIDEO", "VIDEO"] +* ["EXTERNAL_VIDEO", "EXTERNAL_VIDEO"] +* ["MODEL_3D", "MODEL_3D"] +* ["IMAGE", "IMAGE"] +* @returns {boolean} myBool A boolean value +*/ +module.exports = async (descriptionHtml = null, metafields = null, privateMetafields = null, variants = null, media = null, context) => { + return obj.operator; +}; diff --git a/tests/files/comprehensive/inline.js b/tests/files/comprehensive/inline.js new file mode 100644 index 0000000..eb9f5f8 --- /dev/null +++ b/tests/files/comprehensive/inline.js @@ -0,0 +1 @@ +return context.params; diff --git a/tests/gateway/functions/a_standard_function.js b/tests/gateway/functions/a_standard_function.js new file mode 100644 index 0000000..b95b146 --- /dev/null +++ b/tests/gateway/functions/a_standard_function.js @@ -0,0 +1,6 @@ +/** +* Nothing special about this function +*/ +module.exports = async () => { + return true; +}; diff --git a/tests/gateway/functions/bg/__main__.js b/tests/gateway/functions/bg/__main__.js index bfb069e..6f0397c 100644 --- a/tests/gateway/functions/bg/__main__.js +++ b/tests/gateway/functions/bg/__main__.js @@ -1,4 +1,5 @@ /** +* @background * @returns {string} */ module.exports = (callback) => { diff --git a/tests/gateway/functions/bg/empty.js b/tests/gateway/functions/bg/empty.js index af3fdb8..f863d1d 100644 --- a/tests/gateway/functions/bg/empty.js +++ b/tests/gateway/functions/bg/empty.js @@ -1,5 +1,5 @@ /** -* @bg empty +* @background empty * @returns {string} */ module.exports = (callback) => { diff --git a/tests/gateway/functions/bg/info.js b/tests/gateway/functions/bg/info.js index fd21d58..ec68fe2 100644 --- a/tests/gateway/functions/bg/info.js +++ b/tests/gateway/functions/bg/info.js @@ -1,5 +1,5 @@ /** -* @bg info +* @background info * @returns {string} */ module.exports = (callback) => { diff --git a/tests/gateway/functions/bg/params.js b/tests/gateway/functions/bg/params.js index 21ff70e..d2820a9 100644 --- a/tests/gateway/functions/bg/params.js +++ b/tests/gateway/functions/bg/params.js @@ -1,5 +1,5 @@ /** -* @bg params +* @background params * @returns {string} */ module.exports = (callback) => { diff --git a/tests/gateway/functions/bg/paramsSpecific1.js b/tests/gateway/functions/bg/paramsSpecific1.js index 15a3b65..046839b 100644 --- a/tests/gateway/functions/bg/paramsSpecific1.js +++ b/tests/gateway/functions/bg/paramsSpecific1.js @@ -1,6 +1,6 @@ /** * Comment for the function is here -* @bg params +* @background params * data * @returns {string} */ diff --git a/tests/gateway/functions/bg/paramsSpecific2.js b/tests/gateway/functions/bg/paramsSpecific2.js index 5c10849..1133f05 100644 --- a/tests/gateway/functions/bg/paramsSpecific2.js +++ b/tests/gateway/functions/bg/paramsSpecific2.js @@ -1,5 +1,5 @@ /** -* @bg params data otherdata +* @background params data otherdata * @returns {string} */ module.exports = (callback) => { diff --git a/tests/gateway/functions/bg/paramsSpecific3.js b/tests/gateway/functions/bg/paramsSpecific3.js index 6c6b786..e5ef6d6 100644 --- a/tests/gateway/functions/bg/paramsSpecific3.js +++ b/tests/gateway/functions/bg/paramsSpecific3.js @@ -1,5 +1,5 @@ /** -* @bg params data +* @background params data * @returns {string} */ module.exports = (callback) => { diff --git a/tests/gateway/functions/buffer_reflect.js b/tests/gateway/functions/buffer_reflect.js new file mode 100644 index 0000000..4cad328 --- /dev/null +++ b/tests/gateway/functions/buffer_reflect.js @@ -0,0 +1,7 @@ +/** +* @param {buffer} bufferParam +* @returns {buffer} mybuf +*/ +module.exports = async (bufferParam) => { + return bufferParam; +}; diff --git a/tests/gateway/functions/buffer_return_content_type.js b/tests/gateway/functions/buffer_return_content_type.js new file mode 100644 index 0000000..baf136b --- /dev/null +++ b/tests/gateway/functions/buffer_return_content_type.js @@ -0,0 +1,8 @@ +/** +* @returns {buffer} mybuf +*/ +module.exports = async () => { + let buffer = Buffer.from('lol'); + buffer.contentType = 'image/png'; + return buffer; +}; diff --git a/tests/gateway/functions/buffer_within_array_param.js b/tests/gateway/functions/buffer_within_array_param.js new file mode 100644 index 0000000..93916d8 --- /dev/null +++ b/tests/gateway/functions/buffer_within_array_param.js @@ -0,0 +1,13 @@ +/** +* @param {array} arrayParam +* @ {buffer} bufferItem +*/ +module.exports = async (arrayParam) => { + if (!arrayParam.length) { + throw new Error('No array items provided'); + } + if (!Buffer.isBuffer(arrayParam[0])) { + throw new Error('The parsed value of the array parameter\'s item is not a buffer'); + } + return 'ok'; +}; diff --git a/tests/gateway/functions/buffer_within_object_param.js b/tests/gateway/functions/buffer_within_object_param.js new file mode 100644 index 0000000..0c9fc9a --- /dev/null +++ b/tests/gateway/functions/buffer_within_object_param.js @@ -0,0 +1,10 @@ +/** +* @param {object} objectParam +* @ {buffer} bufferVal +*/ +module.exports = async (objectParam) => { + if (!Buffer.isBuffer(objectParam.bufferVal)) { + throw new Error('The parsed value of the object parameter\'s "bufferVal" key is not a buffer'); + } + return 'ok'; +}; diff --git a/tests/gateway/functions/empty/__main__.js b/tests/gateway/functions/empty/__main__.js new file mode 100644 index 0000000..82db7cb --- /dev/null +++ b/tests/gateway/functions/empty/__main__.js @@ -0,0 +1,9 @@ +/** +* @param {integer} intValue +* @returns {string} +*/ +module.exports = (intValue, callback) => { + + return callback(null, 'hello world'); + +}; diff --git a/tests/gateway/functions/inline/await.js b/tests/gateway/functions/inline/await.js new file mode 100644 index 0000000..e7a27ce --- /dev/null +++ b/tests/gateway/functions/inline/await.js @@ -0,0 +1,3 @@ +const sleep = t => new Promise(res => setTimeout(() => res(null), t)) +await sleep(10); +return `hello world`; diff --git a/tests/gateway/functions/inline/buffer.js b/tests/gateway/functions/inline/buffer.js new file mode 100644 index 0000000..71038f2 --- /dev/null +++ b/tests/gateway/functions/inline/buffer.js @@ -0,0 +1,3 @@ +let buffer = Buffer.from('lol'); +buffer.contentType = 'text/html'; +return buffer; diff --git a/tests/gateway/functions/inline/buffer_mock.js b/tests/gateway/functions/inline/buffer_mock.js new file mode 100644 index 0000000..b09395d --- /dev/null +++ b/tests/gateway/functions/inline/buffer_mock.js @@ -0,0 +1 @@ +return {_base64: Buffer.from('lol').toString('base64')}; diff --git a/tests/gateway/functions/inline/context.js b/tests/gateway/functions/inline/context.js new file mode 100644 index 0000000..9e1e721 --- /dev/null +++ b/tests/gateway/functions/inline/context.js @@ -0,0 +1 @@ +return context; diff --git a/tests/gateway/functions/inline/extended_http_is_object.js b/tests/gateway/functions/inline/extended_http_is_object.js new file mode 100644 index 0000000..cbdf15e --- /dev/null +++ b/tests/gateway/functions/inline/extended_http_is_object.js @@ -0,0 +1,6 @@ +return { + statusCode: 429, + headers: {'Content-Type': 'text/html'}, + body: 'lol', + extend: true +}; diff --git a/tests/gateway/functions/inline/http.js b/tests/gateway/functions/inline/http.js new file mode 100644 index 0000000..9383865 --- /dev/null +++ b/tests/gateway/functions/inline/http.js @@ -0,0 +1,5 @@ +return { + statusCode: 429, + headers: {'Content-Type': 'text/html'}, + body: Buffer.from('lol') +}; diff --git a/tests/gateway/functions/inline/http_no_status.js b/tests/gateway/functions/inline/http_no_status.js new file mode 100644 index 0000000..f7f69b6 --- /dev/null +++ b/tests/gateway/functions/inline/http_no_status.js @@ -0,0 +1,4 @@ +return { + headers: {'Content-Type': 'text/html'}, + body: Buffer.from('lol') +}; diff --git a/tests/gateway/functions/inline/number.js b/tests/gateway/functions/inline/number.js new file mode 100644 index 0000000..b3074e4 --- /dev/null +++ b/tests/gateway/functions/inline/number.js @@ -0,0 +1 @@ +return 1988; diff --git a/tests/gateway/functions/inline/require.js b/tests/gateway/functions/inline/require.js new file mode 100644 index 0000000..48d0999 --- /dev/null +++ b/tests/gateway/functions/inline/require.js @@ -0,0 +1,3 @@ +const fs = require('fs'); + +return 'hello'; diff --git a/tests/gateway/functions/keyql.js b/tests/gateway/functions/keyql.js index 99197ea..1582dad 100644 --- a/tests/gateway/functions/keyql.js +++ b/tests/gateway/functions/keyql.js @@ -2,8 +2,9 @@ * Test Optional Params * @param {object.keyql.query} query * @param {object.keyql.limit} limit +* @param {object.keyql.order} order * @returns {string} */ -module.exports = async (query, limit) => { +module.exports = async (query, limit, order) => { return 'hello'; }; diff --git a/tests/gateway/functions/keyql_limit.js b/tests/gateway/functions/keyql_limit.js new file mode 100644 index 0000000..b8c028b --- /dev/null +++ b/tests/gateway/functions/keyql_limit.js @@ -0,0 +1,8 @@ +/** +* Test Optional Params +* @param {object.keyql.limit} limit +* @returns {string} +*/ +module.exports = async (limit) => { + return 'hello'; +}; diff --git a/tests/gateway/functions/keyql_limit_range.js b/tests/gateway/functions/keyql_limit_range.js new file mode 100644 index 0000000..7f22fa3 --- /dev/null +++ b/tests/gateway/functions/keyql_limit_range.js @@ -0,0 +1,8 @@ +/** +* Test Optional Params +* @param {object.keyql.limit} limit {:} [2, 20] +* @returns {string} +*/ +module.exports = async (limit) => { + return 'hello'; +}; diff --git a/tests/gateway/functions/keyql_order_options.js b/tests/gateway/functions/keyql_order_options.js new file mode 100644 index 0000000..c09e4c9 --- /dev/null +++ b/tests/gateway/functions/keyql_order_options.js @@ -0,0 +1,8 @@ +/** +* KeyQL with only a set of keys +* @param {object.keyql.order} order {?} ["alpha", "beta"] +* @returns {string} +*/ +module.exports = async (order) => { + return 'hello'; +}; diff --git a/tests/gateway/functions/keyql_order_options_array.js b/tests/gateway/functions/keyql_order_options_array.js new file mode 100644 index 0000000..febb738 --- /dev/null +++ b/tests/gateway/functions/keyql_order_options_array.js @@ -0,0 +1,9 @@ +/** +* KeyQL with only a set of keys +* @param {array} order +* @ {object.keyql.order} orderObj {?} ["alpha", "beta"] +* @returns {string} +*/ +module.exports = async (order) => { + return 'hello'; +}; diff --git a/tests/gateway/functions/mismatch_params_deep.js b/tests/gateway/functions/mismatch_params_deep.js new file mode 100644 index 0000000..6d5647d --- /dev/null +++ b/tests/gateway/functions/mismatch_params_deep.js @@ -0,0 +1,13 @@ +/** + * Test param mismatch + * @param {object} userData + * @ {object} user + * @ {array} posts + * @ {object} post + * @ {string} title + * @ {array} messages + * @ {string} + */ +module.exports = async (userData) => { + return userData; +}; diff --git a/tests/gateway/functions/mismatch_returns_anon.js b/tests/gateway/functions/mismatch_returns_anon.js new file mode 100644 index 0000000..61deb2d --- /dev/null +++ b/tests/gateway/functions/mismatch_returns_anon.js @@ -0,0 +1,13 @@ +/** + * Test mismatch + * @returns {object} + * @ {object} user + * @ {string} name + */ +module.exports = async () => { + return { + user: { + name: 5 + } + }; +}; diff --git a/tests/gateway/functions/mismatch_returns_anon_array.js b/tests/gateway/functions/mismatch_returns_anon_array.js new file mode 100644 index 0000000..2ee80a9 --- /dev/null +++ b/tests/gateway/functions/mismatch_returns_anon_array.js @@ -0,0 +1,14 @@ +/** + * Test mismatch + * @returns {object} + * @ {object} user + * @ {array} names + * @ {string} name + */ +module.exports = async () => { + return { + user: { + names: ['keith', 2] + } + }; +}; diff --git a/tests/gateway/functions/mismatch_returns_deep.js b/tests/gateway/functions/mismatch_returns_deep.js new file mode 100644 index 0000000..2640541 --- /dev/null +++ b/tests/gateway/functions/mismatch_returns_deep.js @@ -0,0 +1,22 @@ +/** + * Test mismatch + * @returns {object} + * @ {object} user + * @ {array} posts + * @ {object} post + * @ {string} title + * @ {array} messages + * @ {string} + */ +module.exports = async () => { + return { + user: { + posts: [ + { + title: 'sup', + messages: ['hey', 'there', 7] + } + ] + } + }; +}; diff --git a/tests/gateway/functions/mismatch_returns_named.js b/tests/gateway/functions/mismatch_returns_named.js new file mode 100644 index 0000000..6b255c6 --- /dev/null +++ b/tests/gateway/functions/mismatch_returns_named.js @@ -0,0 +1,13 @@ +/** + * Test mismatch + * @returns {object} myObject + * @ {object} user + * @ {string} name + */ +module.exports = async () => { + return { + user: { + name: 5 + } + }; +}; diff --git a/tests/gateway/functions/mismatch_returns_named_array.js b/tests/gateway/functions/mismatch_returns_named_array.js new file mode 100644 index 0000000..e232fc3 --- /dev/null +++ b/tests/gateway/functions/mismatch_returns_named_array.js @@ -0,0 +1,14 @@ +/** + * Test mismatch + * @returns {object} myObject + * @ {object} user + * @ {array} names + * @ {string} name + */ +module.exports = async () => { + return { + user: { + names: ['keith', 2] + } + }; +}; diff --git a/tests/gateway/functions/my_function.js b/tests/gateway/functions/my_function.js index 635ea42..88c432e 100644 --- a/tests/gateway/functions/my_function.js +++ b/tests/gateway/functions/my_function.js @@ -1,4 +1,5 @@ /** +* My function * @returns {number} */ module.exports = (a = 1, b = 2, c = 3, callback) => { diff --git a/tests/gateway/functions/my_function_test_parsing.js b/tests/gateway/functions/my_function_test_parsing.js new file mode 100644 index 0000000..6e387e0 --- /dev/null +++ b/tests/gateway/functions/my_function_test_parsing.js @@ -0,0 +1,11 @@ +/** +* @returns {object} +*/ +module.exports = async (a = [], b = {}) => { + + return { + a: a, + b: b + }; + +}; diff --git a/tests/gateway/functions/my_function_test_parsing_convert.js b/tests/gateway/functions/my_function_test_parsing_convert.js new file mode 100644 index 0000000..61ac3c4 --- /dev/null +++ b/tests/gateway/functions/my_function_test_parsing_convert.js @@ -0,0 +1,16 @@ +/** +* @param {array} a +* @ {?number} someNumber +* @param {object} b +* @ {?number} lol +* @ {?string} wat +* @returns {object} +*/ +module.exports = async (a = [], b = {}) => { + + return { + a: a, + b: b + }; + +}; diff --git a/tests/gateway/functions/nonstandard/json.js b/tests/gateway/functions/nonstandard/json.js new file mode 100644 index 0000000..e018b6d --- /dev/null +++ b/tests/gateway/functions/nonstandard/json.js @@ -0,0 +1,9 @@ +/** +* Test nonstandard JSON submissions +* @returns {object} +*/ +module.exports = async (a = 1, b = 2, c = 3, context) => { + + return context; + +}; diff --git a/tests/gateway/functions/object_alternate_schema.js b/tests/gateway/functions/object_alternate_schema.js new file mode 100644 index 0000000..4cdaed9 --- /dev/null +++ b/tests/gateway/functions/object_alternate_schema.js @@ -0,0 +1,16 @@ +/** +* Provides alternateSchemas +* @param {object} fileOrFolder +* @ {string} name +* @ {integer} size +* @ OR +* @ {string} name +* @ {array} files +* @ {object} options +* @ {string} type +* @ OR +* @ {number} type +*/ +module.exports = async (fileOrFolder) => { + return {}; +}; diff --git a/tests/gateway/functions/object_tojson.js b/tests/gateway/functions/object_tojson.js new file mode 100644 index 0000000..291a116 --- /dev/null +++ b/tests/gateway/functions/object_tojson.js @@ -0,0 +1,25 @@ +class MyClass { + + constructor (name = '?') { + this.name = name; + } + + toJSON () { + return { + name: this.name, + description: this.constructor.name + }; + } + +} + +/** +* @returns {any} +*/ +module.exports = async () => { + + let obj = new MyClass('hello world'); + + return obj; + +}; diff --git a/tests/gateway/functions/origin/allow.js b/tests/gateway/functions/origin/allow.js new file mode 100644 index 0000000..a2f3321 --- /dev/null +++ b/tests/gateway/functions/origin/allow.js @@ -0,0 +1,15 @@ +/** +* Valid function for origins +* @origin autocode.com +* @origin localhost:8000 +* @origin test.some-url.com:9999 +* @origin https://hello.com +* @background +* @param {string} alpha Some value +* @stream {boolean} hello Some value +*/ +module.exports = async (alpha, context) => { + + return true; + +}; diff --git a/tests/gateway/functions/range_integer.js b/tests/gateway/functions/range_integer.js new file mode 100644 index 0000000..7c5d715 --- /dev/null +++ b/tests/gateway/functions/range_integer.js @@ -0,0 +1,8 @@ +/** + * Test range + * @param {integer} ranged {:} [1, 200] + * @returns {any} + */ +module.exports = async (ranged) => { + return ranged; +}; diff --git a/tests/gateway/functions/range_number.js b/tests/gateway/functions/range_number.js new file mode 100644 index 0000000..345f5a9 --- /dev/null +++ b/tests/gateway/functions/range_number.js @@ -0,0 +1,8 @@ +/** + * Test range + * @param {number} ranged {:} [1.01, 199.9] + * @returns {any} + */ +module.exports = async (ranged) => { + return ranged; +}; diff --git a/tests/gateway/functions/runtime/details.js b/tests/gateway/functions/runtime/details.js new file mode 100644 index 0000000..a82c393 --- /dev/null +++ b/tests/gateway/functions/runtime/details.js @@ -0,0 +1,10 @@ +/** +* @returns {any} +*/ +module.exports = (callback) => { + + let error = new Error('error'); + error.details = {objects: 'supported'}; + callback(error); + +}; diff --git a/tests/gateway/functions/runtime/timeout.js b/tests/gateway/functions/runtime/timeout.js new file mode 100644 index 0000000..fd1880a --- /dev/null +++ b/tests/gateway/functions/runtime/timeout.js @@ -0,0 +1,8 @@ +/** +* @returns {any} +*/ +module.exports = async () => { + return new Promise((resolve, reject) => { + // Empty promise that never resolves + }); +}; diff --git a/tests/gateway/functions/sanitize/http_object_invalid_header_names.js b/tests/gateway/functions/sanitize/http_object_invalid_header_names.js new file mode 100644 index 0000000..6da6533 --- /dev/null +++ b/tests/gateway/functions/sanitize/http_object_invalid_header_names.js @@ -0,0 +1,20 @@ +/** +* Test rejection of invalid header names +* @param {string} contentType A content type +* @returns {object.http} +*/ +module.exports = (contentType = 'text/html', callback) => { + + return callback(null, { + body: 'hello', + headers: { + 'Content-Type ': contentType, + 'X Authorization Key': 'somevalue', + ' AnotherHeader': 'somevalue', + 'WeirdName!@#$%^&*()œ∑´®†¥¨ˆøπåß∂ƒ©˙∆˚¬≈ç√∫˜µ≤:|\{}🔥🔥🔥': 'test', + 'MultilineName\n': 'test', + 'Good-Header-Name': 'good value' + } + }); + +}; diff --git a/tests/gateway/functions/sanitize/http_object_invalid_header_values.js b/tests/gateway/functions/sanitize/http_object_invalid_header_values.js new file mode 100644 index 0000000..9f4fcf6 --- /dev/null +++ b/tests/gateway/functions/sanitize/http_object_invalid_header_values.js @@ -0,0 +1,22 @@ +/** +* Test rejection of invalid header values +* @param {string} contentType A content type +* @returns {object.http} +*/ +module.exports = (contentType = 'text/html', callback) => { + + return callback(null, { + body: 'hello', + headers: { + 'Null-Value': null, + 'Undefined-Value': undefined, + 'Object-Value': { + 'a': 'b' + }, + 'Number-Value': 0xdeadbeef, + 'Boolean-Value': false, + 'Empty-String-Value': '' + } + }); + +}; diff --git a/tests/gateway/functions/stream/basic.js b/tests/gateway/functions/stream/basic.js new file mode 100644 index 0000000..458a3b6 --- /dev/null +++ b/tests/gateway/functions/stream/basic.js @@ -0,0 +1,12 @@ +/** +* Valid function for streaming +* @param {string} alpha Some value +* @stream {boolean} hello Some value +*/ +module.exports = async (alpha, context) => { + + context.stream('hello', true); + + return true; + +}; diff --git a/tests/gateway/functions/stream/basic_buffer.js b/tests/gateway/functions/stream/basic_buffer.js new file mode 100644 index 0000000..b71dd32 --- /dev/null +++ b/tests/gateway/functions/stream/basic_buffer.js @@ -0,0 +1,12 @@ +/** +* Valid function for streaming +* @param {string} alpha Some value +* @stream {buffer} hello Some value +*/ +module.exports = async (alpha, context) => { + + context.stream('hello', Buffer.from('123')); + + return true; + +}; diff --git a/tests/gateway/functions/stream/basic_buffer_mocked.js b/tests/gateway/functions/stream/basic_buffer_mocked.js new file mode 100644 index 0000000..fd980db --- /dev/null +++ b/tests/gateway/functions/stream/basic_buffer_mocked.js @@ -0,0 +1,12 @@ +/** +* Valid function for streaming +* @param {string} alpha Some value +* @stream {buffer} hello Some value +*/ +module.exports = async (alpha, context) => { + + context.stream('hello', {_base64: Buffer.from('123').toString('base64')}); + + return true; + +}; diff --git a/tests/gateway/functions/stream/basic_buffer_nested.js b/tests/gateway/functions/stream/basic_buffer_nested.js new file mode 100644 index 0000000..bad76ff --- /dev/null +++ b/tests/gateway/functions/stream/basic_buffer_nested.js @@ -0,0 +1,12 @@ +/** +* Valid function for streaming +* @param {string} alpha Some value +* @stream {any} hello +*/ +module.exports = async (alpha, context) => { + + context.stream('hello', {mybuff: Buffer.from('123')}); + + return true; + +}; diff --git a/tests/gateway/functions/stream/basic_buffer_nested_mocked.js b/tests/gateway/functions/stream/basic_buffer_nested_mocked.js new file mode 100644 index 0000000..b980ece --- /dev/null +++ b/tests/gateway/functions/stream/basic_buffer_nested_mocked.js @@ -0,0 +1,13 @@ +/** +* Valid function for streaming +* @param {string} alpha Some value +* @stream {object} hello +* @ {buffer} mybuff +*/ +module.exports = async (alpha, context) => { + + context.stream('hello', {mybuff: {_base64: Buffer.from('123').toString('base64')}}); + + return true; + +}; diff --git a/tests/gateway/functions/stream/debug.js b/tests/gateway/functions/stream/debug.js new file mode 100644 index 0000000..b13102b --- /dev/null +++ b/tests/gateway/functions/stream/debug.js @@ -0,0 +1,25 @@ +/** +* Valid function for streaming +* @param {string} alpha Some value +* @stream {string} hello Hello message +* @stream {string} goodbye Goodbye message +*/ +module.exports = async (alpha, context) => { + + context.stream('hello', 'Hello?'); + context.stream('hello', 'How are you?'); + console.log('what?', 'who?'); + + await new Promise(resolve => setTimeout(() => resolve(), 20)); + + context.stream('hello', 'Is it me you\'re looking for?'); + console.error('oh no'); + + await new Promise(resolve => setTimeout(() => resolve(), 20)); + + context.stream('goodbye', 'Nice to see ya'); + console.log('finally'); + + return true; + +}; diff --git a/tests/gateway/functions/stream/debug_no_stream.js b/tests/gateway/functions/stream/debug_no_stream.js new file mode 100644 index 0000000..e606ec3 --- /dev/null +++ b/tests/gateway/functions/stream/debug_no_stream.js @@ -0,0 +1,19 @@ +/** +* Not a streaming function, can debug with streaming +* @param {string} alpha Some value +*/ +module.exports = async (alpha, context) => { + + console.log('what?', 'who?'); + + await new Promise(resolve => setTimeout(() => resolve(), 20)); + + console.error('oh no'); + + await new Promise(resolve => setTimeout(() => resolve(), 20)); + + console.log('finally'); + + return true; + +}; diff --git a/tests/gateway/functions/stream/invalid_stream_name.js b/tests/gateway/functions/stream/invalid_stream_name.js new file mode 100644 index 0000000..c8dd30f --- /dev/null +++ b/tests/gateway/functions/stream/invalid_stream_name.js @@ -0,0 +1,12 @@ +/** +* Valid function for streaming +* @param {string} alpha Some value +* @stream {boolean} hello Some value +*/ +module.exports = async (alpha, context) => { + + context.stream('hello2', 'what'); + + return true; + +}; diff --git a/tests/gateway/functions/stream/invalid_stream_param.js b/tests/gateway/functions/stream/invalid_stream_param.js new file mode 100644 index 0000000..ae5a79a --- /dev/null +++ b/tests/gateway/functions/stream/invalid_stream_param.js @@ -0,0 +1,12 @@ +/** +* Valid function for streaming +* @param {string} alpha Some value +* @stream {boolean} hello Some value +*/ +module.exports = async (alpha, context) => { + + context.stream('hello', 'what'); + + return true; + +}; diff --git a/tests/gateway/functions/stream/sleep.js b/tests/gateway/functions/stream/sleep.js new file mode 100644 index 0000000..18eb3d7 --- /dev/null +++ b/tests/gateway/functions/stream/sleep.js @@ -0,0 +1,22 @@ +/** +* Valid function for streaming +* @param {string} alpha Some value +* @stream {string} hello Hello message +* @stream {string} goodbye Goodbye message +*/ +module.exports = async (alpha, context) => { + + context.stream('hello', 'Hello?'); + context.stream('hello', 'How are you?'); + + await new Promise(resolve => setTimeout(() => resolve(), 20)); + + context.stream('hello', 'Is it me you\'re looking for?'); + + await new Promise(resolve => setTimeout(() => resolve(), 20)); + + context.stream('goodbye', 'Nice to see ya'); + + return true; + +}; diff --git a/tests/gateway/functions/string_options.js b/tests/gateway/functions/string_options.js new file mode 100644 index 0000000..3ee9213 --- /dev/null +++ b/tests/gateway/functions/string_options.js @@ -0,0 +1,8 @@ +/** + * Test options with string + * @param {string} value {?} ["one", "two", "three"] + * @returns {any} + */ +module.exports = async (value) => { + return value; +}; diff --git a/tests/gateway/functions/value_error/object_alternate_schema_invalid.js b/tests/gateway/functions/value_error/object_alternate_schema_invalid.js new file mode 100644 index 0000000..bf953ed --- /dev/null +++ b/tests/gateway/functions/value_error/object_alternate_schema_invalid.js @@ -0,0 +1,18 @@ +/** +* Provides alternateSchemas +* @returns {object} fileOrFolder +* @ {string} name +* @ {integer} size +* @ OR +* @ {string} name +* @ {array} files +* @ {object} options +* @ {string} type +* @ OR +* @ {number} type +*/ +module.exports = async () => { + + return {name: 'keith', files: []}; + +}; diff --git a/tests/gateway/tests.js b/tests/gateway/tests.js index 7221948..7571227 100644 --- a/tests/gateway/tests.js +++ b/tests/gateway/tests.js @@ -1,4 +1,6 @@ const http = require('http'); +const zlib = require('zlib'); +const fs = require('fs') const FormData = require('form-data'); const {Gateway, FunctionParser} = require('../../index.js'); @@ -6,10 +8,41 @@ const PORT = 7357; const HOST = 'localhost' const ROOT = './tests/gateway'; -const FaaSGateway = new Gateway({debug: false, root: ROOT}); +const FaaSGateway = new Gateway({debug: false, root: ROOT, defaultTimeout: 1000}); const parser = new FunctionParser(); -function request(method, headers, path, data, callback) { +function parseServerSentEvents (buffer) { + let events = {}; + let entries = buffer.toString().split('\n\n'); + entries + .filter(entry => !!entry) + .forEach(entry => { + let event = ''; + let data = null; + let lines = entry.split('\n').map((line, i) => { + let lineData = line.split(':'); + let type = lineData[0]; + let contents = lineData.slice(1).join(':'); + if (contents.startsWith(' ')) { + contents = contents.slice(1); + } + if (type === 'event' && data === null) { + event = contents; + } else if (type === 'data') { + data = data || ''; + data = data + contents + '\n'; + } + }); + if (data && data.endsWith('\n')) { + data = data.slice(0, -1); + } + events[event] = events[event] || []; + events[event].push(data || ''); + }); + return events; +} + +function request (method, headers, path, data, callback) { headers = headers || {}; method = method || 'GET'; path = path || ''; @@ -18,7 +51,10 @@ function request(method, headers, path, data, callback) { data = JSON.stringify(data); headers['Content-Type'] = 'application/json'; } else if (typeof data === 'string') { - headers['Content-Type'] = 'application/x-www-form-urlencoded'; + let contentType = Object.keys(headers).find(k => k.toLowerCase() === 'content-type'); + if (!contentType) { + headers['Content-Type'] = 'application/x-www-form-urlencoded'; + } } data = data || ''; let req = http.request({ @@ -32,6 +68,11 @@ function request(method, headers, path, data, callback) { res.on('data', chunk => buffers.push(chunk)); res.on('end', () => { let result = Buffer.concat(buffers); + if (res.headers['content-encoding'] === 'gzip') { + result = zlib.gunzipSync(result); + } else if (res.headers['content-encoding'] === 'deflate') { + result = zlib.inflateSync(result); + } if ((res.headers['content-type'] || '').split(';')[0] === 'application/json') { result = JSON.parse(result.toString()); } @@ -45,8 +86,14 @@ function request(method, headers, path, data, callback) { module.exports = (expect) => { before(() => { + const preloadFiles = { + 'functions/sample_preload.js': Buffer.from(`module.exports = async () => { return true; };`) + }; FaaSGateway.listen(PORT); - FaaSGateway.define(parser.load(ROOT, 'functions')); + FaaSGateway.define( + parser.load(ROOT, 'functions', 'www', null, preloadFiles), + preloadFiles + ); }); it('Should setup correctly', () => { @@ -54,6 +101,52 @@ module.exports = (expect) => { expect(FaaSGateway.server).to.exist; expect(FaaSGateway.definitions).to.exist; expect(FaaSGateway.definitions).to.haveOwnProperty('my_function'); + expect(FaaSGateway.definitions).to.haveOwnProperty('sample_preload'); + expect(FaaSGateway.preloadFiles).to.haveOwnProperty('functions/sample_preload.js'); + + }); + + it('Should have the parser preload correctly', () => { + + const preloadFiles = { + 'functions/preload.js': Buffer.from(` + module.exports = async (a, context) => { + return true; + }; + `), + 'functions/preload2.js': Buffer.from(` + module.exports = async (b, c) => { + return true; + }; + `) + }; + + let definitions = parser.load(ROOT, 'functions', 'www', null, preloadFiles); + expect(definitions).to.haveOwnProperty('preload'); + expect(definitions['preload'].params.length).to.equal(1); + expect(definitions['preload'].context).to.deep.equal({}); + expect(definitions['preload2'].params.length).to.equal(2); + expect(definitions['preload2'].context).to.equal(null); + + }); + + it('Should parser error if trying to load a preloaded file name', () => { + + const preloadFiles = { + 'functions/my_function.js': Buffer.from(` + module.exports = async (a, context) => { + return true; + }; + `) + }; + + let definitions; + try { + definitions = parser.load(ROOT, 'functions', 'www', null, preloadFiles); + } catch (e) { + expect(e).to.exist; + expect(e.message).to.contain('preload'); + } }); @@ -73,7 +166,7 @@ module.exports = (expect) => { }); }); - it('Should return 302 redirect when missing trailing / with user agent', done => { + it('Should return 302 redirect on GET request when missing trailing / with user agent', done => { request('GET', {'user-agent': 'testing'}, '/my_function', '', (err, res, result) => { expect(err).to.not.exist; @@ -88,9 +181,23 @@ module.exports = (expect) => { }); }); - it('Should not return 302 redirect when missing trailing / without user agent', done => { + it('Should not return 302 redirect on a GET request when missing trailing / without user agent', done => { request('GET', {}, '/my_function', '', (err, res, result) => { + expect(err).to.not.exist; + expect(res.statusCode).to.not.equal(302); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + + done(); + + }); + }); + + it('Should not return 302 redirect for POST request with trailing slash with user agent', done => { + request('POST', {'user-agent': 'testing'}, '/my_function', '', (err, res, result) => { + expect(err).to.not.exist; expect(res.statusCode).to.not.equal(302); expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); @@ -141,7 +248,7 @@ module.exports = (expect) => { }); }); - it('Should return 400 Bad Request + ClientError when no Content-Type specified on POST', done => { + it('Should return 400 Bad Request + ParameterParseError when no Content-Type specified on POST', done => { request('POST', {}, '/my_function/', undefined, (err, res, result) => { expect(err).to.not.exist; @@ -151,2260 +258,5442 @@ module.exports = (expect) => { expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); expect(result).to.exist; expect(result.error).to.exist; - expect(result.error.type).to.equal('ClientError'); + expect(result.error.type).to.equal('ParameterParseError'); done(); }); }); - it('Should return 200 OK + result when executed', done => { - request('GET', {}, '/my_function/', '', (err, res, result) => { + it('Should return 400 Bad Request + ParameterParseError when Content-Type: text/plain specified on POST with invalid JSON', done => { + request('POST', {'Content-Type': 'text/plain'}, '/my_function/', 'lol', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(200); + expect(res.statusCode).to.equal(400); expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(result).to.equal(6); + expect(result).to.exist; + expect(result.error).to.exist; + expect(result.error.type).to.equal('ParameterParseError'); done(); }); }); - it('Should parse arguments from URL', done => { - request('GET', {}, '/my_function/?a=10&b=20&c=30', '', (err, res, result) => { + it('Should return 400 Bad Request + ParameterParseError when Content-Type: text/plain specified on POST with non-object JSON', done => { + request('POST', {'Content-Type': 'text/plain'}, '/my_function/', '[]', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(200); + expect(res.statusCode).to.equal(400); expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(result).to.equal(60); + expect(result).to.exist; + expect(result.error).to.exist; + expect(result.error.type).to.equal('ParameterParseError'); done(); }); }); - it('Should parse arguments from POST (URL encoded)', done => { - request('POST', {}, '/my_function/', 'a=10&b=20&c=30', (err, res, result) => { + it('Should return 200 OK when Content-Type: text/plain specified on POST with valid JSON object', done => { + request('POST', {'Content-Type': 'text/plain'}, '/my_function/', '{}', (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(result).to.equal(60); + expect(result).to.equal(6); done(); }); }); - it('Should not overwrite POST (URL encoded) data with query parameters', done => { - request('POST', {}, '/my_function/?c=300', 'a=10&b=20&c=30', (err, res, result) => { + it('Should return 200 OK when Content-Type: text/plain specified on POST with valid doubly-stringified JSON object', done => { + request('POST', {'Content-Type': 'text/plain;charset=UTF-8'}, '/my_function/', '"{}"', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(400); + expect(res.statusCode).to.equal(200); expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(result.error).to.exist; - expect(result.error.type).to.equal('ClientError'); + expect(result).to.equal(6); done(); }); }); - it('Should parse arguments from POST (JSON)', done => { - request('POST', {}, '/my_function/', {a: 10, b: 20, c: 30}, (err, res, result) => { + it('Should return 200 OK + result when executed', done => { + request('GET', {}, '/my_function/', '', (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(result).to.equal(60); + expect(result).to.equal(6); done(); }); }); - it('Should not overwrite POST (JSON) data with query parameters', done => { - request('POST', {}, '/my_function/?c=300', {a: 10, b: 20, c: 30}, (err, res, result) => { + it('Should return 200 OK + result when preloadFile executed', done => { + request('GET', {}, '/sample_preload/', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(400); + expect(res.statusCode).to.equal(200); expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(result.error).to.exist; - expect(result.error.type).to.equal('ClientError'); + expect(result).to.equal(true); done(); }); }); - it('Should not parse arguments from POST (JSON Array)', done => { - request('POST', {}, '/my_function/', [10, 20, 30], (err, res, result) => { + it('Should return 200 OK + gzip result when executed with Accept-Encoding: gzip', done => { + request('GET', {'accept-encoding': 'gzip'}, '/my_function/', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(400); - expect(result.error).to.exist; - expect(result.error.type).to.equal('ClientError'); - expect(result.error.message).to.equal('Bad Request: Invalid JSON: Must be Object'); + expect(res.statusCode).to.equal(200); + expect(res.headers['content-encoding']).to.equal('gzip'); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result).to.equal(6); done(); }); }); - it('Should give ParameterError if parameter doesn\'t match (converted)', done => { - request('POST', {}, '/my_function/', 'a=10&b=20&c=hello%20world', (err, res, result) => { + it('Should return 200 OK + deflate result when executed with Accept-Encoding: deflate', done => { + request('GET', {'accept-encoding': 'deflate'}, '/my_function/', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(400); + expect(res.statusCode).to.equal(200); + expect(res.headers['content-encoding']).to.equal('deflate'); expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(result.error).to.exist; - expect(result.error.type).to.equal('ParameterError'); - expect(result.error.details).to.exist; - expect(result.error.details.c).to.exist; - expect(result.error.details.c.expected).to.exist; - expect(result.error.details.c.expected.type).to.equal('number'); - expect(result.error.details.c.actual).to.exist; - expect(result.error.details.c.actual.type).to.equal('string'); - expect(result.error.details.c.actual.value).to.equal('hello world'); + expect(result).to.equal(6); done(); }); }); - it('Should give ParameterError if parameter doesn\'t match (not converted)', done => { - request('POST', {}, '/my_function/', {a: 10, b: 20, c: '30'}, (err, res, result) => { + it('Should return 200 OK + gzip result when executed with Accept-Encoding: gzip, deflate', done => { + request('GET', {'accept-encoding': 'gzip, deflate'}, '/my_function/', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(400); + expect(res.statusCode).to.equal(200); + expect(res.headers['content-encoding']).to.equal('gzip'); expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(result.error).to.exist; - expect(result.error.type).to.equal('ParameterError'); - expect(result.error.details).to.exist; - expect(result.error.details.c).to.exist; - expect(result.error.details.c.expected).to.exist; - expect(result.error.details.c.expected.type).to.equal('number'); - expect(result.error.details.c.actual).to.exist; - expect(result.error.details.c.actual.type).to.equal('string'); - expect(result.error.details.c.actual.value).to.equal('30'); + expect(result).to.equal(6); done(); }); }); - it('Should give 502 + ValueError if unexpected value', done => { - request('POST', {}, '/my_function/', {c: 100}, (err, res, result) => { + it('Should parse arguments from URL', done => { + request('GET', {}, '/my_function/?a=10&b=20&c=30', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(502); + expect(res.statusCode).to.equal(200); expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(result.error).to.exist; - expect(result.error.type).to.equal('ValueError'); - expect(result.error.details).to.exist; - expect(result.error.details.returns).to.exist; - expect(result.error.details.returns.message).to.exist; - expect(result.error.details.returns.expected).to.exist; - expect(result.error.details.returns.expected.type).to.equal('number'); - expect(result.error.details.returns.actual).to.exist; - expect(result.error.details.returns.actual.type).to.equal('string'); - expect(result.error.details.returns.actual.value).to.equal('hello value'); + expect(result).to.equal(60); done(); }); }); - it('Should give 200 OK for not found function', done => { - request('POST', {}, '/test/', {}, (err, res, result) => { + it('Should parse arguments from URL into array, var[] format', done => { + request('GET', {}, '/my_function_test_parsing/?a[]=1&a[]=2&a[]=3', '', (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(result).to.equal('not found?'); + expect(result).to.deep.equal({a: ['1', '2', '3'], b: {}}); done(); }); }); - it('Should allow status setting from third callback parameter', done => { - request('POST', {}, '/test/status/', {}, (err, res, result) => { + it('Should parse arguments from URL into array, var[0] format, with push', done => { + request('GET', {}, '/my_function_test_parsing/?a[1]=1&a[0]=2&a[]=100&a[5]=3&a[]=7', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(404); + expect(res.statusCode).to.equal(200); expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(result).to.be.instanceof(Buffer); - expect(result.toString()).to.equal('not found'); + expect(result).to.deep.equal({a: ['2', '1', null, null, null, '3', '100', '7'], b: {}}); done(); }); }); - it('Should pass headers properly', done => { - request('POST', {}, '/headers/', {}, (err, res, result) => { + it('Should return bad request if array populated by pushing and set', done => { + request('GET', {}, '/my_function_test_parsing/?a[]=1&a[]=2&a=[1,2,3]', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(200); + expect(res.statusCode).to.equal(400); expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(res.headers['content-type']).to.equal('text/html'); - expect(result).to.be.instanceof(Buffer); - expect(result.toString()).to.equal('abcdef'); done(); }); }); - it('Should parse object properly', done => { - request('POST', {}, '/object_parsing/', {}, (err, res, result) => { + it('Should return bad request if non-integer value used as index in array', done => { + request('GET', {}, '/my_function_test_parsing/?a[1.5]=1', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(200); - expect(result).to.equal(null); + expect(res.statusCode).to.equal(400); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); done(); }); }); - it('Should populate HTTP body', done => { - request('POST', {}, '/http_body/', {abc: 123}, (err, res, result) => { + it('Should parse arguments from URL into array, var[0] format, with push and covert to type', done => { + request('GET', {}, '/my_function_test_parsing_convert/?a[1]=1&a[0]=2&a[]=100&a[5]=3&a[]=7', '', (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(result).to.be.a.string; - expect(result).to.equal('{"abc":123}'); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result).to.deep.equal({a: [2, 1, null, null, null, 3, 100, 7], b: {}}); done(); }); }); - it('Should null number properly (POST)', done => { - request('POST', {}, '/number_nullable/', {}, (err, res, result) => { + it('Should parse arguments from URL into array, obj.field format', done => { + request('GET', {}, '/my_function_test_parsing/?b.lol=1&b.wat=23', '', (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(result).to.be.an.array; - expect(result[0]).to.equal(null); - expect(result[1]).to.equal(null); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result).to.deep.equal({a: [], b: {lol: '1', wat: '23'}}); done(); }); }); - it('Should null number properly (GET)', done => { - request('GET', {}, '/number_nullable/', '', (err, res, result) => { + it('Should parse arguments from URL into array, obj.field format, setting multiple field levels', done => { + request('GET', {}, '/my_function_test_parsing/?b.lol=1&b.wat=23&b.cool.beans=hi', '', (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(result).to.be.an.array; - expect(result[0]).to.equal(null); - expect(result[1]).to.equal(null); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result).to.deep.equal({a: [], b: {lol: '1', wat: '23', cool: {beans: 'hi'}}}); done(); }); }); - it('Should error object on string provided', done => { - request('POST', {}, '/object_parsing/', {obj: 'xxx'}, (err, res, result) => { + it('Should parse arguments from URL into array, obj.field format, setting multiple field levels with array', done => { + request('GET', {}, '/my_function_test_parsing/?b.lol=1&b.wat=23&b.cool.beans[]=hi', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(400); - expect(result.error).to.exist; - expect(result.error.type).to.equal('ParameterError'); - expect(result.error.details).to.exist; - expect(result.error.details.obj).to.exist; - expect(result.error.details.obj.message).to.exist; - expect(result.error.details.obj.expected).to.exist; - expect(result.error.details.obj.expected.type).to.equal('object'); - expect(result.error.details.obj.actual).to.exist; - expect(result.error.details.obj.actual.type).to.equal('string'); - expect(result.error.details.obj.actual.value).to.equal('xxx'); + expect(res.statusCode).to.equal(200); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result).to.deep.equal({a: [], b: {lol: '1', wat: '23', cool: {beans: ['hi']}}}); done(); }); }); - it('Should reject integer type when provided float (GET)', done => { - request('GET', {}, '/type_rejection/?alpha=47.2', '', (err, res, result) => { + it('Should parse arguments from URL into array, obj.field format, setting multiple field levels with array and sub object', done => { + request('GET', {}, '/my_function_test_parsing/?b.lol=1&b.wat=23&b.cool[].beans=hi', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(400); - expect(result.error).to.exist; - expect(result.error.type).to.equal('ParameterError'); - expect(result.error.details).to.exist; - expect(result.error.details.alpha).to.exist; - expect(result.error.details.alpha.message).to.exist; - expect(result.error.details.alpha.expected).to.exist; - expect(result.error.details.alpha.expected.type).to.equal('integer'); - expect(result.error.details.alpha.actual).to.exist; - expect(result.error.details.alpha.actual.type).to.equal('number'); - expect(result.error.details.alpha.actual.value).to.equal(47.2); + expect(res.statusCode).to.equal(200); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result).to.deep.equal({a: [], b: {lol: '1', wat: '23', cool: [{beans: 'hi'}]}}); done(); }); }); - it('Should reject integer type when provided float (POST)', done => { - request('POST', {}, '/type_rejection/', {alpha: 47.2}, (err, res, result) => { + it('Should parse arguments from URL into array, obj.field format, setting multiple field levels with 2d array and sub object within an array', done => { + request('GET', {}, '/my_function_test_parsing/?b.lol=1&b.wat=23&b.cool[][].beans=hi', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(400); - expect(result.error).to.exist; - expect(result.error.type).to.equal('ParameterError'); - expect(result.error.details).to.exist; - expect(result.error.details.alpha).to.exist; - expect(result.error.details.alpha.message).to.exist; - expect(result.error.details.alpha.expected).to.exist; - expect(result.error.details.alpha.expected.type).to.equal('integer'); - expect(result.error.details.alpha.actual).to.exist; - expect(result.error.details.alpha.actual.type).to.equal('number'); - expect(result.error.details.alpha.actual.value).to.equal(47.2); + expect(res.statusCode).to.equal(200); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result).to.deep.equal({a: [], b: {lol: '1', wat: '23', cool: [[{beans: 'hi'}]]}}); done(); }); }); - it('Should accept integer type when provided integer (GET)', done => { - request('GET', {}, '/type_rejection/?alpha=47', '', (err, res, result) => { + it('Should parse arguments from URL into array, obj.field format, setting multiple items in an array and sub object within an array', done => { + request('GET', {}, '/my_function_test_parsing/?b.lol=1&b.wat=23&b.cool[]=hi&b.cool[]=anotheritem', '', (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(result).to.equal(47); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result).to.deep.equal({a: [], b: {lol: '1', wat: '23', cool: ['hi', 'anotheritem']}}); done(); }); }); - it('Should accept integer type when provided integer (POST)', done => { - request('POST', {}, '/type_rejection/', {alpha: 47}, (err, res, result) => { + it('Should reject obj.field format for an argument in the URL that is already typed as a non-object with a ParameterParseError', done => { + request('GET', {}, '/my_function_test_parsing/?c=1&c.field=1', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(200); - expect(result).to.equal(47); + expect(res.statusCode).to.equal(400); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result.error).to.exist; + expect(result.error.type).to.equal('ParameterParseError'); done(); }); }); - it('Should not accept empty object.http', done => { - request('GET', {}, '/sanitize/http_object_empty/', '', (err, res, result) => { + it('Should reject obj.field format for a nested argument in the URL that is already typed as a non object with a ParameterParseError', done => { + request('GET', {}, '/my_function_test_parsing/?b.field=1&b.field.test=2', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(502); + expect(res.statusCode).to.equal(400); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); expect(result.error).to.exist; - expect(result.error.details).to.exist; - expect(result.error.details.returns).to.exist - expect(result.error.details.returns.invalid).to.equal(true); + expect(result.error.type).to.equal('ParameterParseError'); done(); }); }); - it('Should sanitize a {_base64: ...} buffer input', done => { - request('GET', {}, '/sanitize/http_object_base64/', '', (err, res, result) => { + it('Should reject obj.field format, setting multiple field levels with array', done => { + request('GET', {}, '/my_function_test_parsing/?b.lol=1&b.wat=23&b.cool[]=hi&b.cool=hi', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(200); - expect(result.error).to.not.exist; - expect(result.toString()).to.equal('fix for steven'); - done(); - + expect(res.statusCode).to.equal(400); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + done(); + }); }); - it('Should accept uppercase Content-Type', done => { - request('GET', {}, '/sanitize/http_object_header_case/?contentType=image/png', '', (err, res, result) => { + it('Should reject obj.field format, overwriting child object', done => { + request('GET', {}, '/my_function_test_parsing/?b.lol=1&b.wat=23&b.cool.beans=hi&b.cool=beans', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(200); - expect(res.headers).to.exist; - expect(res.headers).to.haveOwnProperty('content-type'); - expect(res.headers['content-type']).to.equal('image/png'); + expect(res.statusCode).to.equal(400); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); done(); }); }); - it('Should not accept object.http with null body', done => { - request('GET', {}, '/sanitize/http_object/', '', (err, res, result) => { + it('Should return bad request if object populated by value and set', done => { + request('GET', {}, '/my_function_test_parsing/?b.lol=1&b={}', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(502); - expect(result.error).to.exist; - expect(result.error.details).to.exist; - expect(result.error.details.returns).to.exist - expect(result.error.details.returns.invalid).to.equal(true); + expect(res.statusCode).to.equal(400); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); done(); - }); }); - it('Should accept object.http with string body', done => { - request('GET', {}, '/sanitize/http_object/?body=hello', '', (err, res, result) => { + it('Should parse arguments from URL into object, obj.field format, and convert to type', done => { + request('GET', {}, '/my_function_test_parsing_convert/?b.lol=1&b.wat=23', '', (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(res.headers['content-type']).to.equal('text/plain'); - expect(result.toString()).to.equal('hello'); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result).to.deep.equal({a: [], b: {lol: 1, wat: '23'}}); done(); }); }); - it('Should not accept object.http with statusCode out of range', done => { - request('GET', {}, '/sanitize/http_object/?statusCode=600', '', (err, res, result) => { + it('Should parse arguments from POST (URL encoded)', done => { + request('POST', {}, '/my_function/', 'a=10&b=20&c=30', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(502); - expect(result.error).to.exist; - expect(result.error.details).to.exist; - expect(result.error.details.returns).to.exist - expect(result.error.details.returns.invalid).to.equal(true); + expect(res.statusCode).to.equal(200); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result).to.equal(60); done(); - }); }); - it('Should not accept object.http with invalid headers object', done => { - request('POST', {}, '/sanitize/http_object/', {headers: true}, (err, res, result) => { + it('Should not overwrite POST (URL encoded) data with query parameters', done => { + request('POST', {}, '/my_function/?c=300', 'a=10&b=20&c=30', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(502); + expect(res.statusCode).to.equal(400); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); expect(result.error).to.exist; - expect(result.error.details).to.exist; - expect(result.error.details.returns).to.exist - expect(result.error.details.returns.invalid).to.equal(true); + expect(result.error.type).to.equal('ParameterParseError'); done(); - }); }); - it('Should allow header setting', done => { - request('POST', {}, '/sanitize/http_object/', {body: 'hello', headers: {'content-type': 'text/html'}}, (err, res, result) => { + it('Should parse arguments from POST (JSON)', done => { + request('POST', {}, '/my_function/', {a: 10, b: 20, c: 30}, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(res.headers['content-type']).to.equal('text/html'); - expect(result.toString()).to.equal('hello'); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result).to.equal(60); done(); }); }); - it('Should overwrite access-control-allow-origin', done => { - request('POST', {}, '/sanitize/http_object/', {body: 'hello', headers: {'access-control-allow-origin': '$'}}, (err, res, result) => { + it('Should not overwrite POST (JSON) data with query parameters', done => { + request('POST', {}, '/my_function/?c=300', {a: 10, b: 20, c: 30}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(200); - expect(res.headers['access-control-allow-origin']).to.equal('$'); - expect(result.toString()).to.equal('hello'); + expect(res.statusCode).to.equal(400); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result.error).to.exist; + expect(result.error.type).to.equal('ParameterParseError'); done(); }); }); - it('Should NOT overwrite x-functionscript', done => { - request('POST', {}, '/sanitize/http_object/', {body: 'hello', headers: {'x-functionscript': '$'}}, (err, res, result) => { + it('Should successfully parse arguments from POST (JSON Array)', done => { + request('POST', {}, '/my_function/', [10, 20, 30], (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(res.headers['x-functionscript']).to.not.equal('$'); - expect(result.toString()).to.equal('hello'); + expect(result.error).to.not.exist; done(); }); }); - it('Should run a background function', done => { - request('POST', {}, '/bg/:bg', {data: 'xxx'}, (err, res, result) => { + it('Should give ParameterError if parameter doesn\'t match (converted)', done => { + request('POST', {}, '/my_function/', 'a=10&b=20&c=hello%20world', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(202); - expect(result).to.exist; - expect(result).to.be.instanceof(Buffer); - expect(result.length).to.be.greaterThan(0); + expect(res.statusCode).to.equal(400); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result.error).to.exist; + expect(result.error.type).to.equal('ParameterError'); + expect(result.error.details).to.exist; + expect(result.error.details.c).to.exist; + expect(result.error.details.c.expected).to.exist; + expect(result.error.details.c.expected.type).to.equal('number'); + expect(result.error.details.c.actual).to.exist; + expect(result.error.details.c.actual.type).to.equal('string'); + expect(result.error.details.c.actual.value).to.equal('hello world'); done(); }); }); - it('Should return 302 redirect with correct url when running a background function missing a slash before :bg and at end of url', done => { - request('POST', {'user-agent': 'testing'}, '/bg:bg', '', (err, res, result) => { + it('Should give ParameterError if parameter doesn\'t match (not converted)', done => { + request('POST', {}, '/my_function/', {a: 10, b: 20, c: '30'}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(302); + expect(res.statusCode).to.equal(400); expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(res.headers).to.haveOwnProperty('location'); - expect(res.headers.location).to.equal('/bg/:bg'); + expect(result.error).to.exist; + expect(result.error.type).to.equal('ParameterError'); + expect(result.error.details).to.exist; + expect(result.error.details.c).to.exist; + expect(result.error.details.c.expected).to.exist; + expect(result.error.details.c.expected.type).to.equal('number'); + expect(result.error.details.c.actual).to.exist; + expect(result.error.details.c.actual.type).to.equal('string'); + expect(result.error.details.c.actual.value).to.equal('30'); done(); }); }); - it('Should return 302 redirect with correct url when running a background function missing a slash before :bg but with slash at end of url', done => { - request('POST', {'user-agent': 'testing'}, '/bg:bg/', '', (err, res, result) => { + it('Should give 502 + ValueError if unexpected value', done => { + request('POST', {}, '/my_function/', {c: 100}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(302); + expect(res.statusCode).to.equal(502); expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(res.headers).to.haveOwnProperty('location'); - expect(res.headers.location).to.equal('/bg/:bg'); + expect(res.headers['x-execution-uuid'].length).to.be.greaterThan(1); + expect(result.error).to.exist; + expect(result.error.type).to.equal('ValueError'); + expect(result.error.details).to.exist; + expect(result.error.details.returns).to.exist; + expect(result.error.details.returns.message).to.exist; + expect(result.error.details.returns.expected).to.exist; + expect(result.error.details.returns.expected.type).to.equal('number'); + expect(result.error.details.returns.actual).to.exist; + expect(result.error.details.returns.actual.type).to.equal('string'); + expect(result.error.details.returns.actual.value).to.equal('hello value'); done(); }); }); - it('Should return 302 redirect with correct url when running a background function missing a slash before :bg and at end of url with a query', done => { - request('POST', {'user-agent': 'testing'}, '/bg:bg?test=param', '', (err, res, result) => { + it('Should give 200 OK for not found function', done => { + request('POST', {}, '/test/', {}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(302); + expect(res.statusCode).to.equal(200); expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(res.headers).to.haveOwnProperty('location'); - expect(res.headers.location).to.equal('/bg/:bg?test=param'); + expect(result).to.equal('not found?'); done(); }); }); - it('Should return 302 redirect with correct url when running a background function missing a slash before :bg but with slash at end of url with a query', done => { - request('POST', {'user-agent': 'testing'}, '/bg:bg/?test=param', '', (err, res, result) => { + it('Should allow status setting from third callback parameter', done => { + request('POST', {}, '/test/status/', {}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(302); + expect(res.statusCode).to.equal(404); expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(res.headers).to.haveOwnProperty('location'); - expect(res.headers.location).to.equal('/bg/:bg?test=param'); + expect(result).to.be.instanceof(Buffer); + expect(result.toString()).to.equal('not found'); done(); }); }); - it('Should run a background function with bg mode "info"', done => { - request('POST', {}, '/bg/info/:bg', {data: 'xxx'}, (err, res, result) => { + it('Should pass headers properly', done => { + request('POST', {}, '/headers/', {}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(202); - expect(result).to.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(res.headers['content-type']).to.equal('text/html'); expect(result).to.be.instanceof(Buffer); - expect(result.length).to.be.greaterThan(0); + expect(result.toString()).to.equal('abcdef'); done(); }); }); - it('Should run a background function with bg mode "empty"', done => { - request('POST', {}, '/bg/empty/:bg', {data: 'xxx'}, (err, res, result) => { + it('Should parse object properly', done => { + request('POST', {}, '/object_parsing/', {}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(202); - expect(result).to.exist; - expect(result).to.be.instanceof(Buffer); - expect(result.length).to.equal(0); + expect(res.statusCode).to.equal(200); + expect(result).to.equal(null); done(); }); }); - it('Should run a background function with bg mode "params"', done => { - request('POST', {}, '/bg/params/:bg', {data: 'xxx'}, (err, res, result) => { + it('Should toJSON object properly', done => { + request('POST', {}, '/object_tojson/', {}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(202); - expect(result).to.exist; - expect(result).to.be.an('object'); - expect(result).to.haveOwnProperty('data'); - expect(result.data).to.equal('xxx'); + expect(res.statusCode).to.equal(200); + expect(result).to.deep.equal({name: 'hello world', description: 'MyClass'}); done(); }); }); - it('Should run a background function with bg mode "params" looking for a specific parameter', done => { - request('POST', {}, '/bg/paramsSpecific1/:bg', {data: 'xxx', discarded: 'xxx'}, (err, res, result) => { + it('Should populate HTTP body', done => { + request('POST', {}, '/http_body/', {abc: 123}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(202); - expect(result).to.exist; - expect(result).to.be.an('object'); - expect(result).to.haveOwnProperty('data'); - expect(result).to.not.haveOwnProperty('discarded'); - expect(result.data).to.equal('xxx'); + expect(res.statusCode).to.equal(200); + expect(result).to.be.a.string; + expect(result).to.equal('{"abc":123}'); done(); }); }); - it('Should run a background function with bg mode "params" looking for two specific parameters', done => { - request('POST', {}, '/bg/paramsSpecific2/:bg', {data: 'xxx', otherdata: 'xxx', discarded: 'xxx'}, (err, res, result) => { + it('Should null number properly (POST)', done => { + request('POST', {}, '/number_nullable/', {}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(202); - expect(result).to.exist; - expect(result).to.be.an('object'); - expect(result).to.haveOwnProperty('data'); - expect(result).to.haveOwnProperty('otherdata'); - expect(result.data).to.equal('xxx'); - expect(result.otherdata).to.equal('xxx'); + expect(res.statusCode).to.equal(200); + expect(result).to.be.an.array; + expect(result[0]).to.equal(null); + expect(result[1]).to.equal(null); done(); }); }); - it('Should run a background function with bg mode "params" looking for specific param that is not there', done => { - request('POST', {}, '/bg/paramsSpecific3/:bg', {otherdata: 'xxx'}, (err, res, result) => { + it('Should null number properly (GET)', done => { + request('GET', {}, '/number_nullable/', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(202); - expect(result).to.exist; - expect(result).to.be.an('object'); - expect(result).to.not.haveOwnProperty('data'); + expect(res.statusCode).to.equal(200); + expect(result).to.be.an.array; + expect(result[0]).to.equal(null); + expect(result[1]).to.equal(null); done(); }); }); - it('Should register an error in the resolve step with type AccessPermissionError', done => { - - let originalResolveFn = FaaSGateway.resolve; - FaaSGateway.resolve = (req, res, buffer, callback) => { - let error = new Error('You are not allowed to access this API.'); - error.accessPermissionError = true; - return callback(error); - }; - - request('POST', {}, '/my_function/', {}, (err, res, result) => { - - FaaSGateway.resolve = originalResolveFn; + it('Should error object on string provided', done => { + request('POST', {}, '/object_parsing/', {obj: 'xxx'}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(401); + expect(res.statusCode).to.equal(400); expect(result.error).to.exist; - expect(result.error.type).to.equal('AccessPermissionError'); + expect(result.error.type).to.equal('ParameterError'); + expect(result.error.details).to.exist; + expect(result.error.details.obj).to.exist; + expect(result.error.details.obj.message).to.exist; + expect(result.error.details.obj.expected).to.exist; + expect(result.error.details.obj.expected.type).to.equal('object'); + expect(result.error.details.obj.actual).to.exist; + expect(result.error.details.obj.actual.type).to.equal('string'); + expect(result.error.details.obj.actual.value).to.equal('xxx'); done(); }); - }); - it('Should register an error in the resolve step with type AccessSourceError', done => { - - let originalResolveFn = FaaSGateway.resolve; - FaaSGateway.resolve = (req, res, buffer, callback) => { - let error = new Error('You are not allowed to access this API.'); - error.accessSourceError = true; - return callback(error); - }; - - request('POST', {}, '/my_function/', {}, (err, res, result) => { - - FaaSGateway.resolve = originalResolveFn; + it('Should reject integer type when provided float (GET)', done => { + request('GET', {}, '/type_rejection/?alpha=47.2', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(401); + expect(res.statusCode).to.equal(400); expect(result.error).to.exist; - expect(result.error.type).to.equal('AccessSourceError'); + expect(result.error.type).to.equal('ParameterError'); + expect(result.error.details).to.exist; + expect(result.error.details.alpha).to.exist; + expect(result.error.details.alpha.message).to.exist; + expect(result.error.details.alpha.expected).to.exist; + expect(result.error.details.alpha.expected.type).to.equal('integer'); + expect(result.error.details.alpha.actual).to.exist; + expect(result.error.details.alpha.actual.type).to.equal('number'); + expect(result.error.details.alpha.actual.value).to.equal(47.2); done(); }); - }); - it('Should register an error in the resolve step with type AccessAuthError', done => { - - let originalResolveFn = FaaSGateway.resolve; - FaaSGateway.resolve = (req, res, buffer, callback) => { - let error = new Error('You are not allowed to access this API.'); - error.accessAuthError = true; - return callback(error); - }; - - request('POST', {}, '/my_function/', {}, (err, res, result) => { - - FaaSGateway.resolve = originalResolveFn; + it('Should reject integer type when provided float (POST)', done => { + request('POST', {}, '/type_rejection/', {alpha: 47.2}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(401); + expect(res.statusCode).to.equal(400); expect(result.error).to.exist; - expect(result.error.type).to.equal('AccessAuthError'); + expect(result.error.type).to.equal('ParameterError'); + expect(result.error.details).to.exist; + expect(result.error.details.alpha).to.exist; + expect(result.error.details.alpha.message).to.exist; + expect(result.error.details.alpha.expected).to.exist; + expect(result.error.details.alpha.expected.type).to.equal('integer'); + expect(result.error.details.alpha.actual).to.exist; + expect(result.error.details.alpha.actual.type).to.equal('number'); + expect(result.error.details.alpha.actual.value).to.equal(47.2); done(); }); - }); - it('Should register an error in the resolve step with type AccessSuspendedError', done => { + it('Should accept integer type when provided integer (GET)', done => { + request('GET', {}, '/type_rejection/?alpha=47', '', (err, res, result) => { - let originalResolveFn = FaaSGateway.resolve; - FaaSGateway.resolve = (req, res, buffer, callback) => { - let error = new Error('You are not allowed to access this API.'); - error.accessSuspendedError = true; - return callback(error); - }; + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.equal(47); + done(); - request('POST', {}, '/my_function/', {}, (err, res, result) => { + }); + }); - FaaSGateway.resolve = originalResolveFn; + it('Should accept integer type when provided integer (POST)', done => { + request('POST', {}, '/type_rejection/', {alpha: 47}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(401); - expect(result.error).to.exist; - expect(result.error.type).to.equal('AccessSuspendedError'); + expect(res.statusCode).to.equal(200); + expect(result).to.equal(47); done(); }); - }); - it('Should register an error in the resolve step with type PaymentRequiredError', done => { + it('Should not accept empty object.http', done => { + request('GET', {}, '/sanitize/http_object_empty/', '', (err, res, result) => { - let originalResolveFn = FaaSGateway.resolve; - FaaSGateway.resolve = (req, res, buffer, callback) => { - let error = new Error('You are not allowed to access this API.'); - error.paymentRequiredError = true; - return callback(error); - }; + expect(err).to.not.exist; + expect(res.statusCode).to.equal(502); + expect(result.error).to.exist; + expect(result.error.details).to.exist; + expect(result.error.details.returns).to.exist + expect(result.error.details.returns.invalid).to.equal(true); + done(); - request('POST', {}, '/my_function/', {}, (err, res, result) => { + }); + }); - FaaSGateway.resolve = originalResolveFn; + it('Should sanitize a {_base64: ...} buffer input', done => { + request('GET', {}, '/sanitize/http_object_base64/', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(402); - expect(result.error).to.exist; - expect(result.error.type).to.equal('PaymentRequiredError'); + expect(res.statusCode).to.equal(200); + expect(result.error).to.not.exist; + expect(result.toString()).to.equal('fix for steven'); done(); }); - }); - it('Should register an error in the resolve step with type RateLimitError', done => { + it('Should accept uppercase Content-Type', done => { + request('GET', {}, '/sanitize/http_object_header_case/?contentType=image/png', '', (err, res, result) => { - let originalResolveFn = FaaSGateway.resolve; - FaaSGateway.resolve = (req, res, buffer, callback) => { - let error = new Error('You have called this API too many times.'); - error.rateLimitError = true; - error.rate = { - count: 1, - period: 3600 - }; - return callback(error); - }; + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers).to.exist; + expect(res.headers).to.haveOwnProperty('content-type'); + expect(res.headers['content-type']).to.equal('image/png'); + done(); - request('POST', {}, '/my_function/', {}, (err, res, result) => { + }); + }); - FaaSGateway.resolve = originalResolveFn; + it('Should return a proper error for invalid header names', done => { + request('GET', {}, '/sanitize/http_object_invalid_header_names/', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(429); + expect(res.statusCode).to.equal(502); expect(result.error).to.exist; - expect(result.error.type).to.equal('RateLimitError'); - expect(result.error.details).to.haveOwnProperty('rate'); - expect(result.error.details.rate).to.haveOwnProperty('count'); - expect(result.error.details.rate).to.haveOwnProperty('period'); - expect(result.error.details.rate.count).to.equal(1); - expect(result.error.details.rate.period).to.equal(3600); + expect(result.error.details).to.exist; + expect(Object.keys(result.error.details).length).to.equal(5); + expect(result.error.details['content-type ']).to.exist; + expect(result.error.details['x authorization key']).to.exist; + expect(result.error.details[' anotherheader']).to.exist; + expect(result.error.details['multilinename\n']).to.exist; + expect(result.error.details['weirdname!@#$%^&*()œ∑´®†¥¨ˆøπåß∂ƒ©˙∆˚¬≈ç√∫˜µ≤:|{}🔥🔥🔥']).to.exist; done(); }); - }); - it('Should register a runtime error properly', done => { - request('POST', {}, '/runtime/', {}, (err, res, result) => { + it('Should return a proper error for invalid header values', done => { + request('GET', {}, '/sanitize/http_object_invalid_header_values/', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(403); - expect(result).to.exist; - expect(result).to.be.an('object'); + expect(res.statusCode).to.equal(502); expect(result.error).to.exist; - expect(result.error).to.be.an('object'); - expect(result.error.type).to.equal('RuntimeError'); + expect(result.error.details).to.exist; + expect(Object.keys(result.error.details).length).to.equal(3); + expect(result.error.details['object-value']).to.exist; + expect(result.error.details['undefined-value']).to.exist; + expect(result.error.details['null-value']).to.exist; done(); }); }); - it('Should register a fatal error properly', done => { - request('POST', {}, '/runtime/fatal/', {}, (err, res, result) => { + it('Should not accept object.http with null body', done => { + request('GET', {}, '/sanitize/http_object/', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(500); - expect(result).to.exist; - expect(result).to.be.an('object'); + expect(res.statusCode).to.equal(502); expect(result.error).to.exist; - expect(result.error).to.be.an('object'); - expect(result.error.type).to.equal('FatalError'); - expect(result.error.stack).to.exist; + expect(result.error.details).to.exist; + expect(result.error.details.returns).to.exist + expect(result.error.details.returns.invalid).to.equal(true); done(); + }); }); - it('Should register a fatal error with no stack properly', done => { - request('POST', {}, '/runtime/fatal_no_stack/', {}, (err, res, result) => { + it('Should accept object.http with string body', done => { + request('GET', {}, '/sanitize/http_object/?body=hello', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(500); - expect(result).to.exist; - expect(result).to.be.an('object'); - expect(result.error).to.exist; - expect(result.error).to.be.an('object'); - expect(result.error.type).to.equal('FatalError'); - expect(result.error.stack).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('text/plain'); + expect(result.toString()).to.equal('hello'); done(); }); }); - it('Should register a thrown error properly', done => { - request('POST', {}, '/runtime/thrown/', {}, (err, res, result) => { + it('Should not accept object.http with statusCode out of range', done => { + request('GET', {}, '/sanitize/http_object/?statusCode=600', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(403); - expect(result).to.exist; - expect(result).to.be.an('object'); + expect(res.statusCode).to.equal(502); expect(result.error).to.exist; - expect(result.error).to.be.an('object'); - expect(result.error.type).to.equal('RuntimeError'); + expect(result.error.details).to.exist; + expect(result.error.details.returns).to.exist + expect(result.error.details.returns.invalid).to.equal(true); done(); + }); }); - it('Should register an uncaught promise', done => { - request('POST', {}, '/runtime/promise_uncaught/', {}, (err, res, result) => { + it('Should not accept object.http with invalid headers object', done => { + request('POST', {}, '/sanitize/http_object/', {headers: true}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(403); - expect(result).to.exist; - expect(result).to.be.an('object'); + expect(res.statusCode).to.equal(502); expect(result.error).to.exist; - expect(result.error).to.be.an('object'); - expect(result.error.type).to.equal('RuntimeError'); + expect(result.error.details).to.exist; + expect(result.error.details.returns).to.exist + expect(result.error.details.returns.invalid).to.equal(true); done(); + }); }); - it('Should respond to an array as an implementation error', done => { - request('POST', {}, '/runtime/array/', {}, (err, res, result) => { + it('Should allow header setting', done => { + request('POST', {}, '/sanitize/http_object/', {body: 'hello', headers: {'content-type': 'text/html'}}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(403); - expect(result).to.exist; - expect(result).to.be.an('object'); - expect(result.error).to.exist; - expect(result.error).to.be.an('object'); - expect(result.error.type).to.equal('RuntimeError'); + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('text/html'); + expect(result.toString()).to.equal('hello'); done(); }); }); - it('Should respond to a boolean as an implementation error', done => { - request('POST', {}, '/runtime/boolean/', {}, (err, res, result) => { + it('Should overwrite access-control-allow-origin', done => { + request('POST', {}, '/sanitize/http_object/', {body: 'hello', headers: {'access-control-allow-origin': '$'}}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(403); - expect(result).to.exist; - expect(result).to.be.an('object'); - expect(result.error).to.exist; - expect(result.error).to.be.an('object'); - expect(result.error.type).to.equal('RuntimeError'); + expect(res.statusCode).to.equal(200); + expect(res.headers['access-control-allow-origin']).to.equal('$'); + expect(result.toString()).to.equal('hello'); done(); }); }); - it('Should respond to a number as an implementation error', done => { - request('POST', {}, '/runtime/number/', {}, (err, res, result) => { + it('Should NOT overwrite x-functionscript', done => { + request('POST', {}, '/sanitize/http_object/', {body: 'hello', headers: {'x-functionscript': '$'}}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(403); - expect(result).to.exist; - expect(result).to.be.an('object'); - expect(result.error).to.exist; - expect(result.error).to.be.an('object'); - expect(result.error.type).to.equal('RuntimeError'); + expect(res.statusCode).to.equal(200); + expect(res.headers['x-functionscript']).to.not.equal('$'); + expect(result.toString()).to.equal('hello'); done(); }); }); - it('Should respond to an object as an implementation error', done => { - request('POST', {}, '/runtime/object/', {}, (err, res, result) => { + it('Should run a function with an empty response', done => { + request('POST', {}, '/empty/:bg', {intValue: 1}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(403); + expect(res.statusCode).to.equal(202); expect(result).to.exist; - expect(result).to.be.an('object'); - expect(result.error).to.exist; - expect(result.error).to.be.an('object'); - expect(result.error.type).to.equal('RuntimeError'); + expect(result).to.be.instanceof(Buffer); + expect(result.length).to.be.greaterThan(0); + expect(result.toString()).to.equal('202 accepted'); done(); }); }); - it('Should respond to a string as an implementation error', done => { - request('POST', {}, '/runtime/string/', {}, (err, res, result) => { + it('Should run a function with an empty response even if it has an invalid parameter', done => { + request('POST', {}, '/empty/:bg', {intValue: 'what'}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(403); + expect(res.statusCode).to.equal(202); expect(result).to.exist; - expect(result).to.be.an('object'); - expect(result.error).to.exist; - expect(result.error).to.be.an('object'); - expect(result.error.type).to.equal('RuntimeError'); + expect(result).to.be.instanceof(Buffer); + expect(result.length).to.be.greaterThan(0); + expect(result.toString()).to.equal('202 accepted'); done(); }); }); - it('Should handle multipart/form-data', done => { - - let form = new FormData(); - form.append('my_field', 'my value'); - form.append('my_other_field', 'my other value'); - - form.submit(`http://${HOST}:${PORT}/reflect`, (err, response) => { + it('Should return 302 redirect on a GET request with correct url when running a background function missing a slash before :bg and at end of url', done => { + request('GET', {'user-agent': 'testing'}, '/empty:bg', '', (err, res, result) => { expect(err).to.not.exist; - expect(response.statusCode).to.equal(200); - - let body = []; - response.on('readable', function() { - body.push(response.read()); - }); - - response.on('end', function() { - let results = JSON.parse(body); - expect(results.my_field).to.equal('my value'); - expect(results.my_other_field).to.equal('my other value'); - done(); - }); - - response.on('err', function(err) { - expect(err).to.not.exist; - done(); - }) + expect(res.statusCode).to.equal(302); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(res.headers).to.haveOwnProperty('location'); + expect(res.headers.location).to.equal('/empty/:bg'); + done(); - }) + }); }); - it('Should handle multipart/form-data with buffer', done => { - const fs = require('fs') - let pkgJson = fs.readFileSync(process.cwd() + '/package.json') - - let form = new FormData(); - form.append('my_field', 'my value'); - form.append('my_string_buffer', Buffer.from('123')); - form.append('my_file_buffer', pkgJson); - - form.submit(`http://${HOST}:${PORT}/reflect`, (err, response) => { + it('Should return 302 redirect on a GET request with correct url when running a background function missing a slash before :bg but with slash at end of url', done => { + request('GET', {'user-agent': 'testing'}, '/empty:bg/', '', (err, res, result) => { expect(err).to.not.exist; - expect(response.statusCode).to.equal(200); - - let body = []; - response.on('readable', function() { body.push(response.read()); }); - - response.on('end', function() { - let results = JSON.parse(body); - let stringBuffer = Buffer.from(results.my_string_buffer._base64, 'base64'); - let fileBuffer = Buffer.from(results.my_file_buffer._base64, 'base64'); - expect(results.my_field).to.equal('my value'); - expect(stringBuffer).to.be.deep.equal(Buffer.from('123')) - expect(fileBuffer).to.be.deep.equal(pkgJson) - done(); - }); - - response.on('err', function(err) { - expect(err).to.not.exist; - done(); - }) + expect(res.statusCode).to.equal(302); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(res.headers).to.haveOwnProperty('location'); + expect(res.headers.location).to.equal('/empty/:bg'); + done(); - }) + }); }); - it('Should handle multipart/form-data with json', done => { - - let form = new FormData(); - form.append('my_field', 'my value'); - form.append('my_json', JSON.stringify({ - someJsonNums: 123, - someJson: 'hello' - }), 'my.json'); - - form.submit(`http://${HOST}:${PORT}/reflect`, (err, response) => { + it('Should return 302 redirect on a GET request with correct url when running a background function missing a slash before :bg and at end of url with a query', done => { + request('GET', {'user-agent': 'testing'}, '/empty:bg?test=param', '', (err, res, result) => { expect(err).to.not.exist; - expect(response.statusCode).to.equal(200); + expect(res.statusCode).to.equal(302); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(res.headers).to.haveOwnProperty('location'); + expect(res.headers.location).to.equal('/empty/:bg?test=param'); + done(); - let body = []; - response.on('readable', function() { - body.push(response.read()); - }); + }); + }); - response.on('end', function() { - let results = JSON.parse(body); - expect(results.my_field).to.equal('my value'); - expect(results.my_json).to.deep.equal({ - someJsonNums: 123, - someJson: 'hello' - }); - done(); - }); + it('Should return 302 redirect on a GET request with correct url when running a background function missing a slash before :bg but with slash at end of url with a query', done => { + request('GET', {'user-agent': 'testing'}, '/empty:bg/?test=param', '', (err, res, result) => { - response.on('err', function(err) { - expect(err).to.not.exist; - done(); - }) + expect(err).to.not.exist; + expect(res.statusCode).to.equal(302); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(res.headers).to.haveOwnProperty('location'); + expect(res.headers.location).to.equal('/empty/:bg?test=param'); + done(); - }) + }); }); - it('Should handle multipart/form-data with bad json', done => { - - let form = new FormData(); - form.append('my_field', 'my value'); - form.append('my_json', 'totally not json', 'my.json'); - - form.submit(`http://${HOST}:${PORT}/reflect`, (err, response) => { + it('Should fail to run a background function without @background specified', done => { + request('POST', {}, '/a_standard_function/', {_background: true}, (err, res, result) => { expect(err).to.not.exist; - expect(response.statusCode).to.equal(400); + expect(res.statusCode).to.equal(403); + expect(result).to.exist; + expect(result).to.be.an.object; + expect(result.error).to.exist; + expect(result.error.type).to.equal('ExecutionModeError'); + expect(result.error.message).to.contain('"background"'); + done(); - let body = []; - response.on('readable', function() { - body.push(response.read()); - }); + }); + }); - response.on('end', function() { - let results = JSON.parse(body); - expect(results.error).to.exist - expect(results.error.message).to.equal('Bad Request: Invalid multipart form-data with key: my_json') - done(); - }); + it('Should fail to run a background function without @stream specified', done => { + request('POST', {}, '/a_standard_function/', {_stream: true}, (err, res, result) => { - response.on('err', function(err) { - expect(err).to.not.exist; - done(); - }) + expect(err).to.not.exist; + expect(res.statusCode).to.equal(403); + expect(result).to.exist; + expect(result).to.be.an.object; + expect(result.error).to.exist; + expect(result.error.type).to.equal('ExecutionModeError'); + expect(result.error.message).to.contain('"stream"'); + done(); - }) + }); }); - it('Should reject an object that doesn\'t map to Schema', done => { - request('POST', {}, '/schema_rejection/', { - obj: { - name: 'hello', - enabled: true, - data: 'xxx', - timestamp: 1337 - } - }, - (err, res, result) => { + it('Should run a background function', done => { + request('POST', {}, '/bg/', {data: 'xxx', _background: true}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(400); - expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); - expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); - expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(result.error).to.exist; - expect(result.error.type).to.equal('ParameterError'); - expect(result.error.details).to.exist; - expect(result.error.details.obj).to.exist; - expect(result.error.details.obj.expected).to.exist; - expect(result.error.details.obj.expected.type).to.equal('object'); - expect(result.error.details.obj.expected.schema).to.exist; - expect(result.error.details.obj.expected.schema).to.have.length(4); - expect(result.error.details.obj.expected.schema[0].name).to.equal('name'); - expect(result.error.details.obj.expected.schema[0].type).to.equal('string'); - expect(result.error.details.obj.expected.schema[1].name).to.equal('enabled'); - expect(result.error.details.obj.expected.schema[1].type).to.equal('boolean'); - expect(result.error.details.obj.expected.schema[2].name).to.equal('data'); - expect(result.error.details.obj.expected.schema[2].type).to.equal('object'); - expect(result.error.details.obj.expected.schema[2].schema).to.exist; - expect(result.error.details.obj.expected.schema[2].schema).to.have.length(2); - expect(result.error.details.obj.expected.schema[2].schema[0].name).to.equal('a'); - expect(result.error.details.obj.expected.schema[2].schema[0].type).to.equal('string'); - expect(result.error.details.obj.expected.schema[2].schema[1].name).to.equal('b'); - expect(result.error.details.obj.expected.schema[2].schema[1].type).to.equal('string'); - expect(result.error.details.obj.expected.schema[3].name).to.equal('timestamp'); - expect(result.error.details.obj.expected.schema[3].type).to.equal('number'); - expect(result.error.details.obj.actual).to.exist; - expect(result.error.details.obj.actual.type).to.equal('object'); - expect(result.error.details.obj.actual.value).to.deep.equal({ - name: 'hello', - enabled: true, - data: 'xxx', - timestamp: 1337 - }); + expect(res.statusCode).to.equal(202); + expect(result).to.exist; + expect(result).to.be.instanceof(Buffer); + expect(result.length).to.be.greaterThan(0); + expect(result.toString()).to.equal(`initiated "bg"...`); done(); }); }); - it('Should accept an object that correctly maps to Schema', done => { - request('POST', {}, '/schema_rejection/', { - obj: { - name: 'hello', - enabled: true, - data: {a: 'alpha', b: 'beta'}, - timestamp: 1337 - } - }, - (err, res, result) => { + it('Should return 302 redirect on a GET request with correct url when running a background function missing a slash before ?_background and at end of url', done => { + request('GET', {'user-agent': 'testing'}, '/bg?_background', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(200); + expect(res.statusCode).to.equal(302); expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(result).to.equal('hello'); + expect(res.headers).to.haveOwnProperty('location'); + expect(res.headers.location).to.equal('/bg/?_background'); done(); }); }); - it('Should reject an array that doesn\'t map to Schema', done => { - request('POST', {}, '/schema_rejection_array/', { - users: ['alpha', 'beta'] - }, - (err, res, result) => { + it('Should return 302 redirect on a GET request with correct url when running a background function missing a slash before ?_background but with slash at end of url', done => { + request('GET', {'user-agent': 'testing'}, '/bg?_background/', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(400); + expect(res.statusCode).to.equal(302); expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(result.error).to.exist; - expect(result.error.type).to.equal('ParameterError'); - expect(result.error.details).to.exist; - expect(result.error.details.users).to.exist; - expect(result.error.details.users.expected).to.exist; - expect(result.error.details.users.expected.type).to.equal('array'); - expect(result.error.details.users.expected.schema).to.exist; - expect(result.error.details.users.expected.schema).to.have.length(1); - expect(result.error.details.users.expected.schema[0].name).to.equal('user'); - expect(result.error.details.users.expected.schema[0].type).to.equal('object'); - expect(result.error.details.users.expected.schema[0].schema).to.exist; - expect(result.error.details.users.expected.schema[0].schema).to.have.length(2); - expect(result.error.details.users.expected.schema[0].schema[0].name).to.equal('username'); - expect(result.error.details.users.expected.schema[0].schema[0].type).to.equal('string'); - expect(result.error.details.users.expected.schema[0].schema[1].name).to.equal('age'); - expect(result.error.details.users.expected.schema[0].schema[1].type).to.equal('number'); - expect(result.error.details.users.actual).to.exist; - expect(result.error.details.users.actual.type).to.equal('array'); - expect(result.error.details.users.actual.value).to.deep.equal(['alpha', 'beta']); + expect(res.headers).to.haveOwnProperty('location'); + expect(res.headers.location).to.equal('/bg/?_background/'); done(); }); }); - it('Should accept an array that correctly maps to Schema', done => { - request('POST', {}, '/schema_rejection_array/', { - users: [ - { - username: 'alpha', - age: 1 - }, - { - username: 'beta', - age: 2 - } - ] - }, - (err, res, result) => { + it('Should return 302 redirect on a GET request with correct url when running a background function missing a slash before ?_background and at end of url with a query', done => { + request('GET', {'user-agent': 'testing'}, '/bg?_background&test=param', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(200); + expect(res.statusCode).to.equal(302); expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(result).to.equal('hello'); + expect(res.headers).to.haveOwnProperty('location'); + expect(res.headers.location).to.equal('/bg/?_background&test=param'); done(); }); }); - it('Should reject an nested array that doesn\'t map to Schema', done => { - request('POST', {}, '/schema_rejection_nested_array/', { + it('Should run a background function with bg mode "info"', done => { + request('POST', {}, '/bg/info/', {data: 'xxx', _background: true}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(202); + expect(result).to.exist; + expect(result).to.be.instanceof(Buffer); + expect(result.length).to.be.greaterThan(0); + done(); + + }); + }); + + it('Should run a background function with bg mode "empty"', done => { + request('POST', {}, '/bg/empty/', {data: 'xxx', _background: true}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(202); + expect(result).to.exist; + expect(result).to.be.instanceof(Buffer); + expect(result.length).to.equal(0); + done(); + + }); + }); + + it('Should run a background function with bg mode "params"', done => { + request('POST', {}, '/bg/params/', {data: 'xxx', _background: true}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(202); + expect(result).to.exist; + expect(result).to.be.an('object'); + expect(result).to.haveOwnProperty('data'); + expect(result.data).to.equal('xxx'); + done(); + + }); + }); + + it('Should run a background function with bg mode "params" looking for a specific parameter', done => { + request('POST', {}, '/bg/paramsSpecific1/', {data: 'xxx', discarded: 'xxx', _background: true}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(202); + expect(result).to.exist; + expect(result).to.be.an('object'); + expect(result).to.haveOwnProperty('data'); + expect(result).to.not.haveOwnProperty('discarded'); + expect(result.data).to.equal('xxx'); + done(); + + }); + }); + + it('Should run a background function with bg mode "params" looking for two specific parameters', done => { + request('POST', {}, '/bg/paramsSpecific2/', {data: 'xxx', otherdata: 'xxx', discarded: 'xxx', _background: true}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(202); + expect(result).to.exist; + expect(result).to.be.an('object'); + expect(result).to.haveOwnProperty('data'); + expect(result).to.haveOwnProperty('otherdata'); + expect(result.data).to.equal('xxx'); + expect(result.otherdata).to.equal('xxx'); + done(); + + }); + }); + + it('Should run a background function with bg mode "params" looking for specific param that is not there', done => { + request('POST', {}, '/bg/paramsSpecific3/', {otherdata: 'xxx', _background: true}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(202); + expect(result).to.exist; + expect(result).to.be.an('object'); + expect(result).to.not.haveOwnProperty('data'); + done(); + + }); + }); + + it('Should register an error in the resolve step with type AccessPermissionError', done => { + + let originalResolveFn = FaaSGateway.resolve; + FaaSGateway.resolve = (req, res, buffer, callback) => { + let error = new Error('You are not allowed to access this API.'); + error.accessPermissionError = true; + return callback(error); + }; + + request('POST', {}, '/my_function/', {}, (err, res, result) => { + + FaaSGateway.resolve = originalResolveFn; + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(401); + expect(result.error).to.exist; + expect(result.error.type).to.equal('AccessPermissionError'); + done(); + + }); + + }); + + it('Should register an error in the resolve step with type AccessSourceError', done => { + + let originalResolveFn = FaaSGateway.resolve; + FaaSGateway.resolve = (req, res, buffer, callback) => { + let error = new Error('You are not allowed to access this API.'); + error.accessSourceError = true; + return callback(error); + }; + + request('POST', {}, '/my_function/', {}, (err, res, result) => { + + FaaSGateway.resolve = originalResolveFn; + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(401); + expect(result.error).to.exist; + expect(result.error.type).to.equal('AccessSourceError'); + done(); + + }); + + }); + + it('Should register an error in the resolve step with type AccessAuthError', done => { + + let originalResolveFn = FaaSGateway.resolve; + FaaSGateway.resolve = (req, res, buffer, callback) => { + let error = new Error('You are not allowed to access this API.'); + error.accessAuthError = true; + return callback(error); + }; + + request('POST', {}, '/my_function/', {}, (err, res, result) => { + + FaaSGateway.resolve = originalResolveFn; + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(401); + expect(result.error).to.exist; + expect(result.error.type).to.equal('AccessAuthError'); + done(); + + }); + + }); + + it('Should register an error in the resolve step with type AccessSuspendedError', done => { + + let originalResolveFn = FaaSGateway.resolve; + FaaSGateway.resolve = (req, res, buffer, callback) => { + let error = new Error('You are not allowed to access this API.'); + error.accessSuspendedError = true; + return callback(error); + }; + + request('POST', {}, '/my_function/', {}, (err, res, result) => { + + FaaSGateway.resolve = originalResolveFn; + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(401); + expect(result.error).to.exist; + expect(result.error.type).to.equal('AccessSuspendedError'); + done(); + + }); + + }); + + it('Should register an error in the resolve step with type OwnerSuspendedError', done => { + + let originalResolveFn = FaaSGateway.resolve; + FaaSGateway.resolve = (req, res, buffer, callback) => { + let error = new Error('You are not allowed to access this API.'); + error.ownerSuspendedError = true; + return callback(error); + }; + + request('POST', {}, '/my_function/', {}, (err, res, result) => { + + FaaSGateway.resolve = originalResolveFn; + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(401); + expect(result.error).to.exist; + expect(result.error.type).to.equal('OwnerSuspendedError'); + done(); + + }); + + }); + + it('Should register an error in the resolve step with type OwnerPaymentRequiredError', done => { + + let originalResolveFn = FaaSGateway.resolve; + FaaSGateway.resolve = (req, res, buffer, callback) => { + let error = new Error('You are not allowed to access this API.'); + error.ownerPaymentRequiredError = true; + return callback(error); + }; + + request('POST', {}, '/my_function/', {}, (err, res, result) => { + + FaaSGateway.resolve = originalResolveFn; + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(401); + expect(result.error).to.exist; + expect(result.error.type).to.equal('OwnerPaymentRequiredError'); + done(); + + }); + + }); + + it('Should register an error in the resolve step with type PaymentRequiredError', done => { + + let originalResolveFn = FaaSGateway.resolve; + FaaSGateway.resolve = (req, res, buffer, callback) => { + let error = new Error('You are not allowed to access this API.'); + error.paymentRequiredError = true; + return callback(error); + }; + + request('POST', {}, '/my_function/', {}, (err, res, result) => { + + FaaSGateway.resolve = originalResolveFn; + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(402); + expect(result.error).to.exist; + expect(result.error.type).to.equal('PaymentRequiredError'); + done(); + + }); + + }); + + it('Should register an error in the resolve step with type RateLimitError', done => { + + let errorMessage = 'You have called this API too many times.'; + let originalResolveFn = FaaSGateway.resolve; + FaaSGateway.resolve = (req, res, buffer, callback) => { + let error = new Error(errorMessage); + error.rateLimitError = true; + error.rate = { + count: 1, + period: 3600 + }; + return callback(error); + }; + + request('POST', {}, '/my_function/', {}, (err, res, result) => { + + FaaSGateway.resolve = originalResolveFn; + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(429); + expect(result.error).to.exist; + expect(result.error.message).to.equal(errorMessage); + expect(result.error.type).to.equal('RateLimitError'); + expect(result.error.details).to.haveOwnProperty('rate'); + expect(result.error.details.rate).to.haveOwnProperty('count'); + expect(result.error.details.rate).to.haveOwnProperty('period'); + expect(result.error.details.rate.count).to.equal(1); + expect(result.error.details.rate.period).to.equal(3600); + done(); + + }); + + }); + + it('Should register an error in the resolve step with type AuthRateLimitError', done => { + + let errorMessage = 'You have called this API authenticated too many times.'; + let originalResolveFn = FaaSGateway.resolve; + FaaSGateway.resolve = (req, res, buffer, callback) => { + let error = new Error(errorMessage); + error.authRateLimitError = true; + error.rate = { + count: 1, + period: 3600 + }; + return callback(error); + }; + + request('POST', {}, '/my_function/', {}, (err, res, result) => { + + FaaSGateway.resolve = originalResolveFn; + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(429); + expect(result.error).to.exist; + expect(result.error.message).to.equal(errorMessage); + expect(result.error.type).to.equal('AuthRateLimitError'); + expect(result.error.details).to.haveOwnProperty('rate'); + expect(result.error.details.rate).to.haveOwnProperty('count'); + expect(result.error.details.rate).to.haveOwnProperty('period'); + expect(result.error.details.rate.count).to.equal(1); + expect(result.error.details.rate.period).to.equal(3600); + done(); + + }); + + }); + + it('Should register an error in the resolve step with type UnauthRateLimitError', done => { + + let errorMessage = 'You have called this API unauthenticated too many times.'; + let originalResolveFn = FaaSGateway.resolve; + FaaSGateway.resolve = (req, res, buffer, callback) => { + let error = new Error(errorMessage); + error.unauthRateLimitError = true; + error.rate = { + count: 1, + period: 3600 + }; + return callback(error); + }; + + request('POST', {}, '/my_function/', {}, (err, res, result) => { + + FaaSGateway.resolve = originalResolveFn; + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(429); + expect(result.error).to.exist; + expect(result.error.message).to.equal(errorMessage); + expect(result.error.type).to.equal('UnauthRateLimitError'); + expect(result.error.details).to.haveOwnProperty('rate'); + expect(result.error.details.rate).to.haveOwnProperty('count'); + expect(result.error.details.rate).to.haveOwnProperty('period'); + expect(result.error.details.rate.count).to.equal(1); + expect(result.error.details.rate.period).to.equal(3600); + done(); + + }); + + }); + + it('Should register an error in the resolve step with type SaveError', done => { + + let originalResolveFn = FaaSGateway.resolve; + FaaSGateway.resolve = (req, res, buffer, callback) => { + let error = new Error('There was a problem when saving your API.'); + error.saveError = true; + return callback(error); + }; + + request('POST', {}, '/my_function/', {}, (err, res, result) => { + + FaaSGateway.resolve = originalResolveFn; + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(503); + expect(result.error).to.exist; + expect(result.error.type).to.equal('SaveError'); + done(); + + }); + + }); + + it('Should register an error in the resolve step with type MaintenanceError', done => { + + let originalResolveFn = FaaSGateway.resolve; + FaaSGateway.resolve = (req, res, buffer, callback) => { + let error = new Error('Your API is in maintenance mode.'); + error.maintenanceError = true; + return callback(error); + }; + + request('POST', {}, '/my_function/', {}, (err, res, result) => { + + FaaSGateway.resolve = originalResolveFn; + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(403); + expect(result.error).to.exist; + expect(result.error.type).to.equal('MaintenanceError'); + done(); + + }); + + }); + + it('Should register an error in the resolve step with type UpdateError', done => { + + let originalResolveFn = FaaSGateway.resolve; + FaaSGateway.resolve = (req, res, buffer, callback) => { + let error = new Error('Your API is currently updating.'); + error.updateError = true; + return callback(error); + }; + + request('POST', {}, '/my_function/', {}, (err, res, result) => { + + FaaSGateway.resolve = originalResolveFn; + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(409); + expect(result.error).to.exist; + expect(result.error.type).to.equal('UpdateError'); + done(); + + }); + + }); + + it('Should register a runtime error properly', done => { + request('POST', {}, '/runtime/', {}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(403); + expect(res.headers['x-execution-uuid'].length).to.be.greaterThan(1); + expect(result).to.exist; + expect(result).to.be.an('object'); + expect(result.error).to.exist; + expect(result.error).to.be.an('object'); + expect(result.error.type).to.equal('RuntimeError'); + expect(result.error).to.not.haveOwnProperty('details'); + done(); + + }); + }); + + it('Should register a runtime error properly with details', done => { + request('POST', {}, '/runtime/details/', {}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(403); + expect(res.headers['x-execution-uuid'].length).to.be.greaterThan(1); + expect(result).to.exist; + expect(result).to.be.an('object'); + expect(result.error).to.exist; + expect(result.error).to.be.an('object'); + expect(result.error.type).to.equal('RuntimeError'); + expect(result.error.details).to.deep.equal({objects: 'supported'}); + done(); + + }); + }); + + it('Should register a fatal error properly', done => { + request('POST', {}, '/runtime/fatal/', {}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(500); + expect(res.headers['x-execution-uuid'].length).to.be.greaterThan(1); + expect(result).to.exist; + expect(result).to.be.an('object'); + expect(result.error).to.exist; + expect(result.error).to.be.an('object'); + expect(result.error.type).to.equal('FatalError'); + expect(result.error.stack).to.exist; + done(); + + }); + }); + + it('Should register a fatal error with no stack properly', done => { + request('POST', {}, '/runtime/fatal_no_stack/', {}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(500); + expect(res.headers['x-execution-uuid'].length).to.be.greaterThan(1); + expect(result).to.exist; + expect(result).to.be.an('object'); + expect(result.error).to.exist; + expect(result.error).to.be.an('object'); + expect(result.error.type).to.equal('FatalError'); + expect(result.error.stack).to.not.exist; + done(); + + }); + }); + + it('Should register a timeout error properly', done => { + request('POST', {}, '/runtime/timeout/', {}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(504); + expect(res.headers['x-execution-uuid'].length).to.be.greaterThan(1); + expect(result).to.exist; + expect(result).to.be.an('object'); + expect(result.error).to.exist; + expect(result.error).to.be.an('object'); + expect(result.error.type).to.equal('TimeoutError'); + done(); + + }); + }); + + it('Should register a thrown error properly', done => { + request('POST', {}, '/runtime/thrown/', {}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(403); + expect(res.headers['x-execution-uuid'].length).to.be.greaterThan(1); + expect(result).to.exist; + expect(result).to.be.an('object'); + expect(result.error).to.exist; + expect(result.error).to.be.an('object'); + expect(result.error.type).to.equal('RuntimeError'); + done(); + + }); + }); + + it('Should register an uncaught promise', done => { + request('POST', {}, '/runtime/promise_uncaught/', {}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(403); + expect(res.headers['x-execution-uuid'].length).to.be.greaterThan(1); + expect(result).to.exist; + expect(result).to.be.an('object'); + expect(result.error).to.exist; + expect(result.error).to.be.an('object'); + expect(result.error.type).to.equal('RuntimeError'); + done(); + + }); + }); + + it('Should respond to an array as an implementation error', done => { + request('POST', {}, '/runtime/array/', {}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(403); + expect(res.headers['x-execution-uuid'].length).to.be.greaterThan(1); + expect(result).to.exist; + expect(result).to.be.an('object'); + expect(result.error).to.exist; + expect(result.error).to.be.an('object'); + expect(result.error.type).to.equal('RuntimeError'); + done(); + + }); + }); + + it('Should respond to a boolean as an implementation error', done => { + request('POST', {}, '/runtime/boolean/', {}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(403); + expect(res.headers['x-execution-uuid'].length).to.be.greaterThan(1); + expect(result).to.exist; + expect(result).to.be.an('object'); + expect(result.error).to.exist; + expect(result.error).to.be.an('object'); + expect(result.error.type).to.equal('RuntimeError'); + done(); + + }); + }); + + it('Should respond to a number as an implementation error', done => { + request('POST', {}, '/runtime/number/', {}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(403); + expect(res.headers['x-execution-uuid'].length).to.be.greaterThan(1); + expect(result).to.exist; + expect(result).to.be.an('object'); + expect(result.error).to.exist; + expect(result.error).to.be.an('object'); + expect(result.error.type).to.equal('RuntimeError'); + done(); + + }); + }); + + it('Should respond to an object as an implementation error', done => { + request('POST', {}, '/runtime/object/', {}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(403); + expect(res.headers['x-execution-uuid'].length).to.be.greaterThan(1); + expect(result).to.exist; + expect(result).to.be.an('object'); + expect(result.error).to.exist; + expect(result.error).to.be.an('object'); + expect(result.error.type).to.equal('RuntimeError'); + done(); + + }); + }); + + it('Should respond to a string as an implementation error', done => { + request('POST', {}, '/runtime/string/', {}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(403); + expect(res.headers['x-execution-uuid'].length).to.be.greaterThan(1); + expect(result).to.exist; + expect(result).to.be.an('object'); + expect(result.error).to.exist; + expect(result.error).to.be.an('object'); + expect(result.error.type).to.equal('RuntimeError'); + done(); + + }); + }); + + it('Should handle multipart/form-data', done => { + + let form = new FormData(); + form.append('my_field', 'my value'); + form.append('my_other_field', 'my other value'); + + form.submit(`http://${HOST}:${PORT}/reflect`, (err, response) => { + + expect(err).to.not.exist; + expect(response.statusCode).to.equal(200); + + let body = []; + response.on('readable', function() { + body.push(response.read()); + }); + + response.on('end', function() { + let results = JSON.parse(body); + expect(results.my_field).to.equal('my value'); + expect(results.my_other_field).to.equal('my other value'); + done(); + }); + + response.on('err', function(err) { + expect(err).to.not.exist; + done(); + }) + + }) + }); + + it('Should handle multipart/form-data with buffer', done => { + + let pkgJson = fs.readFileSync(process.cwd() + '/package.json') + + let form = new FormData(); + form.append('my_field', 'my value'); + form.append('my_string_buffer', Buffer.from('123')); + form.append('my_file_buffer', pkgJson); + + form.submit(`http://${HOST}:${PORT}/reflect`, (err, response) => { + + expect(err).to.not.exist; + expect(response.statusCode).to.equal(200); + + let body = []; + response.on('readable', function() { body.push(response.read()); }); + + response.on('end', function() { + let results = JSON.parse(body); + let stringBuffer = Buffer.from(results.my_string_buffer._base64, 'base64'); + let fileBuffer = Buffer.from(results.my_file_buffer._base64, 'base64'); + expect(results.my_field).to.equal('my value'); + expect(stringBuffer).to.be.deep.equal(Buffer.from('123')) + expect(fileBuffer).to.be.deep.equal(pkgJson) + done(); + }); + + response.on('err', function(err) { + expect(err).to.not.exist; + done(); + }) + + }) + }); + + it('Should handle multipart/form-data with json', done => { + + let form = new FormData(); + form.append('my_field', 'my value'); + form.append('my_json', JSON.stringify({ + someJsonNums: 123, + someJson: 'hello' + }), 'my.json'); + + form.submit(`http://${HOST}:${PORT}/reflect`, (err, response) => { + + expect(err).to.not.exist; + expect(response.statusCode).to.equal(200); + + let body = []; + response.on('readable', function() { + body.push(response.read()); + }); + + response.on('end', function() { + let results = JSON.parse(body); + expect(results.my_field).to.equal('my value'); + expect(results.my_json).to.deep.equal({ + someJsonNums: 123, + someJson: 'hello' + }); + done(); + }); + + response.on('err', function(err) { + expect(err).to.not.exist; + done(); + }) + + }) + }); + + it('Should handle multipart/form-data with bad json', done => { + + let form = new FormData(); + form.append('my_field', 'my value'); + form.append('my_json', 'totally not json', 'my.json'); + + form.submit(`http://${HOST}:${PORT}/reflect`, (err, response) => { + + expect(err).to.not.exist; + expect(response.statusCode).to.equal(400); + + let body = []; + response.on('readable', function() { + body.push(response.read()); + }); + + response.on('end', function() { + let results = JSON.parse(body); + expect(results.error).to.exist + expect(results.error.type).to.equal('ParameterParseError'); + expect(results.error.message).to.equal('Invalid multipart form-data with key: my_json'); + done(); + }); + + response.on('err', function(err) { + expect(err).to.not.exist; + done(); + }) + + }) + }); + + it('Should handle multipart/form-data with a png', done => { + + let image = fs.readFileSync(process.cwd() + '/tests/gateway/www/fs-wordmark.png'); + + let form = new FormData(); + form.append('bufferParam', image); + + form.submit(`http://${HOST}:${PORT}/buffer_reflect`, (err, response) => { + + expect(err).to.not.exist; + expect(response.statusCode).to.equal(200); + + let body = []; + response.on('readable', function() { + body.push(response.read()); + }); + + response.on('end', function() { + let result = Buffer.concat(body); + expect(image.equals(result)).to.equal(true); + done(); + }); + + response.on('err', function(err) { + expect(err).to.not.exist; + done(); + }) + + }) + }); + + it('Should reject an object that doesn\'t map to Schema', done => { + request('POST', {}, '/schema_rejection/', { + obj: { + name: 'hello', + enabled: true, + data: 'xxx', + timestamp: 1337 + } + }, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result.error).to.exist; + expect(result.error.type).to.equal('ParameterError'); + expect(result.error.details).to.exist; + expect(result.error.details.obj).to.exist; + expect(result.error.details.obj.expected).to.exist; + expect(result.error.details.obj.expected.type).to.equal('object'); + expect(result.error.details.obj.expected.schema).to.exist; + expect(result.error.details.obj.expected.schema).to.have.length(4); + expect(result.error.details.obj.expected.schema[0].name).to.equal('name'); + expect(result.error.details.obj.expected.schema[0].type).to.equal('string'); + expect(result.error.details.obj.expected.schema[1].name).to.equal('enabled'); + expect(result.error.details.obj.expected.schema[1].type).to.equal('boolean'); + expect(result.error.details.obj.expected.schema[2].name).to.equal('data'); + expect(result.error.details.obj.expected.schema[2].type).to.equal('object'); + expect(result.error.details.obj.expected.schema[2].schema).to.exist; + expect(result.error.details.obj.expected.schema[2].schema).to.have.length(2); + expect(result.error.details.obj.expected.schema[2].schema[0].name).to.equal('a'); + expect(result.error.details.obj.expected.schema[2].schema[0].type).to.equal('string'); + expect(result.error.details.obj.expected.schema[2].schema[1].name).to.equal('b'); + expect(result.error.details.obj.expected.schema[2].schema[1].type).to.equal('string'); + expect(result.error.details.obj.expected.schema[3].name).to.equal('timestamp'); + expect(result.error.details.obj.expected.schema[3].type).to.equal('number'); + expect(result.error.details.obj.actual).to.exist; + expect(result.error.details.obj.actual.type).to.equal('object'); + expect(result.error.details.obj.actual.value).to.deep.equal({ + name: 'hello', + enabled: true, + data: 'xxx', + timestamp: 1337 + }); + done(); + + }); + }); + + it('Should accept an object that correctly maps to Schema', done => { + request('POST', {}, '/schema_rejection/', { + obj: { + name: 'hello', + enabled: true, + data: {a: 'alpha', b: 'beta'}, + timestamp: 1337 + } + }, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result).to.equal('hello'); + done(); + + }); + }); + + it('Should reject an array that doesn\'t map to Schema', done => { + request('POST', {}, '/schema_rejection_array/', { + users: ['alpha', 'beta'] + }, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result.error).to.exist; + expect(result.error.type).to.equal('ParameterError'); + expect(result.error.details).to.exist; + expect(result.error.details.users).to.exist; + expect(result.error.details.users.expected).to.exist; + expect(result.error.details.users.expected.type).to.equal('array'); + expect(result.error.details.users.expected.schema).to.exist; + expect(result.error.details.users.expected.schema).to.have.length(1); + expect(result.error.details.users.expected.schema[0].name).to.equal('user'); + expect(result.error.details.users.expected.schema[0].type).to.equal('object'); + expect(result.error.details.users.expected.schema[0].schema).to.exist; + expect(result.error.details.users.expected.schema[0].schema).to.have.length(2); + expect(result.error.details.users.expected.schema[0].schema[0].name).to.equal('username'); + expect(result.error.details.users.expected.schema[0].schema[0].type).to.equal('string'); + expect(result.error.details.users.expected.schema[0].schema[1].name).to.equal('age'); + expect(result.error.details.users.expected.schema[0].schema[1].type).to.equal('number'); + expect(result.error.details.users.actual).to.exist; + expect(result.error.details.users.actual.type).to.equal('array'); + expect(result.error.details.users.actual.value).to.deep.equal(['alpha', 'beta']); + done(); + + }); + }); + + it('Should accept an array that correctly maps to Schema', done => { + request('POST', {}, '/schema_rejection_array/', { + users: [ + { + username: 'alpha', + age: 1 + }, + { + username: 'beta', + age: 2 + } + ] + }, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result).to.equal('hello'); + done(); + + }); + }); + + it('Should reject an nested array that doesn\'t map to Schema', done => { + request('POST', {}, '/schema_rejection_nested_array/', { users: [ { username: 'steve', posts: [{ title: 't', body: 'b' }] }, { posts: [{ title: 't', body: 'b' }] } ] }, - (err, res, result) => { + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result.error).to.exist; + expect(result.error.type).to.equal('ParameterError'); + expect(result.error.details).to.exist; + expect(result.error.details.users).to.exist; + expect(result.error.details.users.expected).to.exist; + expect(result.error.details.users.expected.type).to.equal('array'); + expect(result.error.details.users.expected.schema).to.deep.equal([ + { + name: 'user', + type: 'object', + description: 'a user', + schema: [ + { + name: 'username', + type: 'string', + description: '' + }, + { + name: 'posts', + type: 'array', + description: '', + schema: [ + { + name: 'post', + type: 'object', + description: '', + schema: [ + { + name: 'title', + type: 'string', + description: '' + }, + { + name: 'body', + type: 'string', + description: '' + } + ] + } + ] + } + ] + } + ]); + expect(result.error.details.users.actual).to.deep.equal({ + type: 'array', + value: [ + { + posts: [ + { + body: 'b', + title: 't' + } + ], + username: 'steve' + }, + { + posts: [ + { + body: 'b', + title: 't' + } + ] + } + ] + }); + done(); + + }); + }); + + it('Should accept a nested array that correctly maps to Schema', done => { + request('POST', {}, '/schema_rejection_nested_array/', { + users: [ + { username: 'steve', posts: [{ title: 't', body: 'b' }] }, + { username: 'steve2', posts: [{ title: 't', body: 'b' }] } + ] + }, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result).to.deep.equal( [ + { username: 'steve', posts: [{ title: 't', body: 'b' }] }, + { username: 'steve2', posts: [{ title: 't', body: 'b' }] } + ]); + done(); + + }); + }); + + it('Should reject an array that doesn\'t map to a Schema for an array of numbers', done => { + request('POST', {}, '/schema_rejection_number_array/', { + userIds: ['alpha', 'beta'] + }, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result.error).to.exist; + expect(result.error.type).to.equal('ParameterError'); + expect(result.error.details).to.exist; + expect(result.error.details.userIds).to.exist; + expect(result.error.details.userIds.expected).to.exist; + expect(result.error.details.userIds.expected.type).to.equal('array'); + expect(result.error.details.userIds.expected.schema).to.exist; + expect(result.error.details.userIds.expected.schema).to.have.length(1); + expect(result.error.details.userIds.expected.schema[0].type).to.equal('number'); + expect(result.error.details.userIds.actual).to.exist; + expect(result.error.details.userIds.actual.type).to.equal('array'); + expect(result.error.details.userIds.actual.value).to.deep.equal(['alpha', 'beta']); + done(); + + }); + }); + + it('Should accept an array that correctly maps to Schema', done => { + request('POST', {}, '/schema_rejection_number_array/', { + userIds: [1, 2, 3] + }, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result).to.equal('hello'); + done(); + + }); + }); + + it('Should handle large buffer parameters', done => { + request('POST', {'x-convert-strings': true}, '/runtime/largebuffer/', { + file: `{"_base64": "${'a'.repeat(50000000)}"}` + }, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result.error).to.not.exist; + done(); + + }); + }).timeout(5000); + + it('Should accept a request with the optional param', done => { + request('POST', {}, '/optional_params/', {name: 'steve'}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.equal('steve'); + done(); + + }); + }); + + it('Should accept a request without the optional param', done => { + request('POST', {}, '/optional_params/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.equal('hello'); + done(); + + }); + }); + + it('Should accept a request without the optional param', done => { + request('POST', {}, '/schema_optional_params/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.equal(null); + done(); + + }); + }); + + it('Should accept a request without the optional param field', done => { + request('POST', {}, '/schema_optional_params/', {obj: {name: 'steve'}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.deep.equal({name: 'steve'}); + done(); + + }); + }); + + it('Should accept a request with the optional param field set to null', done => { + request('POST', {}, '/schema_optional_params/', {obj: {name: 'steve', enabled: null}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.deep.equal({name: 'steve', enabled: null}); + done(); + + }); + }); + + it('Should accept a request with the optional param field', done => { + request('POST', {}, '/schema_optional_params/', {obj: {name: 'steve', enabled: true}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.deep.equal({name: 'steve', enabled: true}); + done(); + + }); + }); + + it('Should accept a request without the optional param (nested schema)', done => { + request('POST', {}, '/optional_nested_schema_params/', {obj: {name: 'steve' }}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.deep.equal({name: 'steve'}); + done(); + + }); + }); + + it('Should reject a request without the required param within the optional object (nested schema)', done => { + request('POST', {}, '/optional_nested_schema_params/', {obj: {name: 'steve', options: {}}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(result).to.exist; + expect(result.error).to.exist; + expect(res.statusCode).to.equal(400); + done(); + + }); + }); + + it('Should accept a request with the optional object (nested schema)', done => { + request('POST', {}, '/optional_nested_schema_params/', {obj: {name: 'steve', options: {istest: true}}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result).to.deep.equal({name: 'steve', options: { istest: true}}); + done(); + + }); + }); + + it('Should accept a request with the optional object and optional field (nested schema)', done => { + request('POST', {}, '/optional_nested_schema_params/', {obj: {name: 'steve', options: {istest: true, threads: 4}}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result).to.deep.equal({name: 'steve', options: { istest: true, threads: 4}}); + done(); + + }); + }); + + it('Should successfully return a request without the optional value', done => { + request('POST', {}, '/optional_nested_schema_params/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.equal(null); + done(); + + }); + }); + + + it('Should successfully return a request without the optional values', done => { + request('POST', {}, '/optional_nested_schema_params/', {obj: {name: 'steve'}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result).to.deep.equal({name: 'steve'}); + done(); + + }); + }); + + it('Should successfully return a request with the optional values', done => { + request('POST', {}, '/optional_nested_schema_params/', {obj: {name: 'steve', options: {istest: true}}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result).to.deep.equal({name: 'steve', options: {istest: true}}); + done(); + + }); + }); + + it('Should successfully return a request with the optional values and fields', done => { + request('POST', {}, '/optional_nested_schema_params/', {obj: {name: 'steve', options: {istest: true, threads: 4}}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result).to.deep.equal({name: 'steve', options: {istest: true, threads: 4}}); + done(); + + }); + }); + + it('Should accept a request that matches first of two schemas', done => { + request('POST', {}, '/object_alternate_schema/', {fileOrFolder: {name: 'test', size: 100}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(result).to.exist; + expect(result.error).to.not.exist; + expect(res.statusCode).to.equal(200); + done(); + + }); + }); + + it('Should accept a request that matches second of two schemas', done => { + request('POST', {}, '/object_alternate_schema/', {fileOrFolder: {name: 'test', files: [], options: {type: 'test'}}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(result).to.exist; + expect(result.error).to.not.exist; + expect(res.statusCode).to.equal(200); + done(); + + }); + }); + + it('Should accept a request that matches second subsection of two schemas', done => { + request('POST', {}, '/object_alternate_schema/', {fileOrFolder: {name: 'test', files: [], options: {type: 100}}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(result).to.exist; + expect(result.error).to.not.exist; + expect(res.statusCode).to.equal(200); + done(); + + }); + }); + + it('Should reject a request that matches no schema', done => { + request('POST', {}, '/object_alternate_schema/', {fileOrFolder: {name: 'test'}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(result).to.exist; + expect(result.error).to.exist; + expect(res.statusCode).to.equal(400); + done(); + + }); + }); + + it('Should reject a request that matches no schema based on subsection', done => { + request('POST', {}, '/object_alternate_schema/', {fileOrFolder: {name: 'test', files: [], options: {}}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(result).to.exist; + expect(result.error).to.exist; + expect(res.statusCode).to.equal(400); + done(); + + }); + }); + + it('Should reject a request that matches no schema based on subsection type mismatch', done => { + request('POST', {}, '/object_alternate_schema/', {fileOrFolder: {name: 'test', files: [], options: {type: false}}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(result).to.exist; + expect(result.error).to.exist; + expect(res.statusCode).to.equal(400); + done(); + + }); + }); + + it('Should successfully return a default value with an optional field', done => { + request('POST', {}, '/optional_param_not_null/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result).to.equal('default'); + done(); + + }); + }); + + it('Should successfully return a schema with a default set to 0', done => { + request('POST', {}, '/stripe/', {id: '0'}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + done(); + + }); + }); + + it('Should successfully return a schema with an array', done => { + request('POST', {}, '/giphy/', {query: 'q'}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + done(); + + }); + }); + + it('Should reject a request without an proper enum member', done => { + request('POST', {}, '/enum/', { day: 'funday' }, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + expect(result).to.exist; + expect(result.error).to.exist; + expect(result.error.type).to.exist; + expect(result.error.type).to.equal('ParameterError'); + expect(result.error.details).to.exist; + expect(result.error.details).to.deep.equal({ + day: { + message: 'invalid value: "funday" (string), expected (enum)', + invalid: true, + expected: { + type: 'enum', + members: [ + ['sunday', 0], + ['monday', '0'], + ['tuesday', { a: 1, b: 2 }], + ['wednesday', 3], + ['thursday', [1, 2, 3]], + ['friday', 5.4321], + ['saturday', 6] + ] + }, + actual: { + value: 'funday', + type: 'string' + } + } + }); + done(); + + }); + }); + + it('Should successfully return an enum variant (number)', done => { + request('POST', {}, '/enum/', { day: 'sunday' }, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result).to.equal(0); + done(); + + }); + }); + + it('Should successfully return an enum variant (string)', done => { + request('POST', {}, '/enum/', { day: 'monday' }, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result).to.equal("0"); + done(); + + }); + }); + + it('Should successfully return an enum variant (object)', done => { + request('POST', {}, '/enum/', { day: 'tuesday' }, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result).to.deep.equal({a: 1, b: 2}); + done(); + + }); + }); + + + it('Should successfully return an enum variant (array)', done => { + request('POST', {}, '/enum/', { day: 'thursday' }, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result).to.deep.equal([1, 2, 3]); + done(); + + }); + }); + + it('Should successfully return an enum variant (float)', done => { + request('POST', {}, '/enum/', { day: 'friday' }, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result).to.equal(5.4321); + done(); + + }); + }); + + it('Should return a default enum variant', done => { + request('POST', {}, '/enum_default/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result).to.equal(0); + done(); + + }); + }); + + it('Should return an enum using the context param', done => { + request('POST', {}, '/enum_context/', { thingA: 'a', thingB: 'c' }, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result).to.deep.equal({ + a: 0, + b: { + c: 1, + d: [1, 2, 3] + }, + c: '4', + d: 5.4321 + }); + done(); + + }); + }); + + it('Should return an enum variant when the return type is enum', done => { + request('POST', {}, '/enum_return/', { a: 'a' }, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result).to.equal(0); + done(); + + }); + }); + + it('Should reject returning an invalid enum variant when the return type is enum', done => { + request('POST', {}, '/enum_return/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(502); + expect(res.headers['x-execution-uuid'].length).to.be.greaterThan(1); + expect(result).to.exist; + expect(result.error).to.deep.equal({ + type: 'ValueError', + message: 'The value returned by the function did not match the specified type', + details: { + returns: { + message: 'invalid return value: "not correct" (string), expected (enum)', + invalid: true, + expected: { + type: 'enum', + members: [['a', 0], ['b', [1, 2, 3]]] + }, + actual: { + value: 'not correct', + type: 'string' + } + } + } + }); + done(); + + }); + }); + + it('Should fail to return null from a function without a nullable return value', done => { + request('POST', {}, '/not_nullable_return/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(502); + expect(result.error).to.exist; + expect(result.error.details).to.exist; + expect(result.error.details.returns).to.exist + expect(result.error.details.returns.invalid).to.equal(true); + done(); + + }); + }); + + it('Should return null from a function with a nullable return value', done => { + request('POST', {}, '/nullable_return/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.equal(null);; + done(); + + }); + }); + + it('Should return a value from a function with a nullable return value', done => { + request('POST', {}, '/nullable_return/', {a: 'hello'}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.equal('hello');; + done(); + + }); + }); + + it('Should successfully return a default parameter after passing in null', done => { + request('POST', {}, '/null_default_param/', {name: null}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.equal('default'); + done(); + + }); + }); + + it('Should successfully return a default parameter after passing in undefined', done => { + request('POST', {}, '/null_default_param/', {name: undefined}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.equal('default'); + done(); + + }); + }); + + it('Should successfully return an object with a schema that has an enum variant', done => { + request( + 'POST', + {}, + '/enum_schema/', + { + before: 'before', + valueRange: { + range: 'a range', + majorDimension: 'ROWS', + values: [] + }, + after: 'after', + }, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result).to.deep.equal({ + range: 'a range', + majorDimension: 'ROWS', + values: [] + }); + done(); + + } + ); + }); + + it('Should return a default enum variant set to null', done => { + request('POST', {}, '/enum_null/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.equal(null); + done(); + + }); + }); + + it('Should accept keyql params', done => { + request('POST', {}, '/keyql/', { query: { name: 'steve' }, limit: { count: 0, offset: 0 }, order: { field: 'name', sort: 'ASC' } }, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.equal('hello'); + done(); + + }); + }); + + it('Should accept keyql params', done => { + let query = JSON.stringify({ name: 'steve' }); + let limit = JSON.stringify({ count: 0, offset: 0 }); + let order = JSON.stringify({field: 'name', sort: 'ASC'}); + + request('GET', {'x-convert-strings': true}, `/keyql/?query=${query}&limit=${limit}&order=${order}`, '', (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.equal('hello'); + done(); + + }); + }); + + it('Should reject invalid keyql limit', done => { + request('POST', {}, '/keyql/', { query: { name: 'steve' }, limit: { count: 0, wrong: 0 }, order: { field: 'name', sort: 'ASC' }}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + expect(result.error).to.exist; + expect(result.error.details).to.exist; + expect(result.error.details.limit).to.exist; + done(); + + }); + }); + + it('Should reject invalid keyql order (no field)', done => { + request('POST', {}, '/keyql/', { query: { name: 'steve' }, limit: { count: 0, offset: 0 }, order: { field: null, sort: 'ASC' }}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + expect(result.error).to.exist; + expect(result.error.details).to.exist; + expect(result.error.details.order).to.exist; + done(); + + }); + }); + + it('Should reject invalid keyql order (invalid sort)', done => { + request('POST', {}, '/keyql/', { query: { name: 'steve' }, limit: { count: 0, offset: 0 }, order: { field: 'name', sort: 'WRONG' }}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + expect(result.error).to.exist; + expect(result.error.details).to.exist; + expect(result.error.details.order).to.exist; + done(); + + }); + }); + + it('Should reject invalid keyql order (overloaded)', done => { + request('POST', {}, '/keyql/', { query: { name: 'steve' }, limit: { count: 0, offset: 0 }, order: { field: 'name', sort: 'ASC', wrong: 'WRONG' }}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + expect(result.error).to.exist; + expect(result.error.details).to.exist; + expect(result.error.details.order).to.exist; + done(); + + }); + }); + + it('Should accept keyql with correct options', done => { + request('POST', {}, '/keyql_options/', {query: {alpha: 'hello'}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.equal('hello'); + done(); + + }); + }); + + it('Should accept keyql with correct options and an operator', done => { + request('POST', {}, '/keyql_options/', {query: {alpha__is: 'hello'}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.equal('hello'); + done(); + + }); + }); + + it('Should reject keyql with correct options with an incorrect operator', done => { + request('POST', {}, '/keyql_options/', {query: {alpha__isnt: 'hello'}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + done(); + + }); + }); + + it('Should reject keyql with incorrect options', done => { + request('POST', {}, '/keyql_options/', {query: {gamma: 'hello'}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + done(); + + }); + }); + + it('Should reject keyql with incorrect options with an operator', done => { + request('POST', {}, '/keyql_options/', {query: {gamma__is: 'hello'}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + done(); + + }); + }); + + it('Should accept keyql array with correct options', done => { + request('POST', {}, '/keyql_options_array/', {query: [{alpha: 'hello'}]}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.equal('hello'); + done(); + + }); + }); + + it('Should accept keyql array with correct options and an operator', done => { + request('POST', {}, '/keyql_options_array/', {query: [{alpha__is: 'hello'}]}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.equal('hello'); + done(); + + }); + }); + + it('Should reject keyql array with correct options with an incorrect operator', done => { + request('POST', {}, '/keyql_options_array/', {query: [{alpha__isnt: 'hello'}]}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + done(); + + }); + }); + + it('Should reject keyql array with incorrect options', done => { + request('POST', {}, '/keyql_options_array/', {query: [{gamma: 'hello'}]}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + done(); + + }); + }); + + it('Should reject keyql array with incorrect options with an operator', done => { + request('POST', {}, '/keyql_options_array/', {query: [{gamma__is: 'hello'}]}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + done(); + + }); + }); + + it('Should accept keyql order with correct options', done => { + request('POST', {}, '/keyql_order_options/', {order: {field: 'alpha'}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.equal('hello'); + done(); + + }); + }); + + it('Should reject keyql order with incorrect options', done => { + request('POST', {}, '/keyql_order_options/', {order: {field: 'gamma'}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + done(); + + }); + }); + + it('Should accept keyql array with correct options', done => { + request('POST', {}, '/keyql_order_options_array/', {order: [{field: 'alpha'}]}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.equal('hello'); + done(); + + }); + }); + + it('Should reject keyql array with incorrect options', done => { + request('POST', {}, '/keyql_order_options_array/', {order: [{field: 'gamma'}]}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + done(); + + }); + }); + + it('Should accept keyql array with all correct options', done => { + request('POST', {}, '/keyql_order_options_array/', {order: [{field: 'alpha'}, {field: 'beta'}]}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + done(); + + }); + }); + + it('Should reject keyql array with an incorrect option', done => { + request('POST', {}, '/keyql_order_options_array/', {order: [{field: 'alpha'}, {field: 'gamma'}]}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + done(); + + }); + }); + + it('Should reject keyql limit count out of range, hard limit', done => { + request('POST', {}, '/keyql_limit/', {limit: {count: -1, offset: 0}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + done(); + + }); + }); + + it('Should reject keyql limit offset out of range, hard limit', done => { + request('POST', {}, '/keyql_limit/', {limit: {count: 0, offset: -1}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + done(); + + }); + }); + + it('Should reject keyql limit count non-integer, hard limit', done => { + request('POST', {}, '/keyql_limit/', {limit: {count: 0.256, offset: 0}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + done(); + + }); + }); + + it('Should reject keyql limit count out of lowerbound range, user limit', done => { + request('POST', {}, '/keyql_limit_range/', {limit: {count: 1, offset: 0}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + done(); + + }); + }); + + it('Should reject keyql limit count out of upperbound range, user limit', done => { + request('POST', {}, '/keyql_limit_range/', {limit: {count: 30, offset: 0}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + done(); + + }); + }); + + it('Should accept keyql limit count in range', done => { + request('POST', {}, '/keyql_limit_range/', {limit: {count: 5}}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + done(); + + }); + }); + + it('Should accept number inside integer range', done => { + request('POST', {}, '/range_integer/', {ranged: 1}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + done(); + + }); + }); + + it('Should reject number outside integer range, lowerbound', done => { + request('POST', {}, '/range_integer/', {ranged: -1}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + done(); + + }); + }); + + it('Should reject number outside integer range, upperbound', done => { + request('POST', {}, '/range_integer/', {ranged: 201}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + done(); + + }); + }); + + it('Should accept number inside number range', done => { + request('POST', {}, '/range_number/', {ranged: 1.5}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + done(); + + }); + }); + + it('Should reject number outside number range, lowerbound', done => { + request('POST', {}, '/range_number/', {ranged: 1}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + expect(result.error).to.exist; + expect(result.error.details.ranged.message).to.equal('must be greater than or equal to 1.01'); + done(); + + }); + }); + + it('Should reject number outside number range, upperbound', done => { + request('POST', {}, '/range_number/', {ranged: 200}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + done(); + + }); + }); + + it('Should accept string within provided options', done => { + request('POST', {}, '/string_options/', {value: 'one'}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.equal('one'); + done(); + + }); + }); + + it('Should reject string outside provided options', done => { + request('POST', {}, '/string_options/', {value: 'four'}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + expect(result.error.details.value.message).to.contain('["one","two","three"]'); + expect(result.error.details.value.expected.values).to.deep.equal(['one', 'two', 'three']); + done(); + + }); + }); + + it('Should identify mismatch in a returns statement (unnamed, non-array)', done => { + request('POST', {}, '/mismatch_returns_anon/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(502); + expect(result.error.details.returns.mismatch).to.exist; + expect(result.error.details.returns.mismatch).to.equal('$.user.name'); + done(); + + }); + }); + + it('Should identify mismatch in a returns statement (unnamed, array)', done => { + request('POST', {}, '/mismatch_returns_anon_array/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(502); + expect(result.error.details.returns.mismatch).to.exist; + expect(result.error.details.returns.mismatch).to.equal('$.user.names[1]'); + done(); + + }); + }); + + it('Should identify mismatch in a returns statement (named, non-array)', done => { + request('POST', {}, '/mismatch_returns_named/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(502); + expect(result.error.details.returns.mismatch).to.exist; + expect(result.error.details.returns.mismatch).to.equal('myObject.user.name'); + done(); + + }); + }); + + it('Should identify mismatch in a returns statement (named, array)', done => { + request('POST', {}, '/mismatch_returns_named_array/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(502); + expect(result.error.details.returns.mismatch).to.exist; + expect(result.error.details.returns.mismatch).to.equal('myObject.user.names[1]'); + done(); + + }); + }); + + it('Should identify mismatch in a returns statement (deep)', done => { + request('POST', {}, '/mismatch_returns_deep/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(502); + expect(result.error.details.returns.mismatch).to.exist; + expect(result.error.details.returns.mismatch).to.equal('$.user.posts[0].messages[2]'); + done(); + + }); + }); + + it('Should identify mismatch in a param statement (deep)', done => { + request('POST', {}, '/mismatch_params_deep/', + { + userData: { + user: { + posts: [ + { + title: 'sup', + messages: ['hey', 'there', 7] + } + ] + } + } + }, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + expect(result.error.details.userData).to.exist; + expect(result.error.details.userData.mismatch).to.equal('userData.user.posts[0].messages[2]'); + done(); + + }); + }); + + it('Should return a buffer properly', done => { + request('POST', {}, '/buffer_return/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/octet-stream'); + expect(result).to.exist; + expect(result).to.be.instanceof(Buffer); + expect(result.toString()).to.equal('lol'); + done(); + + }); + }); + + it('Should return a buffer properly with a .contentType set', done => { + request('POST', {}, '/buffer_return_content_type/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('image/png'); + expect(result).to.exist; + expect(result).to.be.instanceof(Buffer); + expect(result.toString()).to.equal('lol'); + done(); + + }); + }); + + it('Should return a nested buffer properly', done => { + request('POST', {}, '/buffer_nested_return/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result).to.haveOwnProperty('body'); + expect(result.body).to.haveOwnProperty('_base64'); + expect(Buffer.from(result.body._base64, 'base64').toString()).to.equal('lol'); + expect(result.test).to.exist; + expect(result.test.deep).to.exist; + expect(result.test.deep).to.be.an('array'); + expect(result.test.deep.length).to.equal(3); + expect(result.test.deep[1]).to.haveOwnProperty('_base64'); + expect(Buffer.from(result.test.deep[1]._base64, 'base64').toString()).to.equal('wat'); + done(); + + }); + }); + + it('Should parse buffers within object params', done => { + request('POST', {}, '/buffer_within_object_param/', { + objectParam: { + bufferVal: { + _base64: 'abcde' + } + } + }, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/json'); + expect(result).to.exist; + expect(result).to.equal('ok'); + done(); + + }); + }); + + it('Should parse buffers within array params', done => { + request('POST', {}, '/buffer_within_array_param/', { + arrayParam: [{ + _base64: 'abcde' + }] + }, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/json'); + expect(result).to.exist; + expect(result).to.equal('ok'); + done(); + + }); + }); + + it('Should return a mocked buffer as if it were a real one', done => { + request('POST', {}, '/buffer_mocked_return/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result).to.be.instanceof(Buffer); + expect(result.toString()).to.equal('lol'); + done(); + + }); + }); + + it('Should return a nested mocked buffer as if it were a real one', done => { + request('POST', {}, '/buffer_nested_mocked_return/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result).to.haveOwnProperty('body'); + expect(result.body).to.haveOwnProperty('_base64'); + expect(Buffer.from(result.body._base64, 'base64').toString()).to.equal('lol'); + expect(result.test).to.exist; + expect(result.test.deep).to.exist; + expect(result.test.deep).to.be.an('array'); + expect(result.test.deep.length).to.equal(3); + expect(result.test.deep[1]).to.haveOwnProperty('_base64'); + expect(Buffer.from(result.test.deep[1]._base64, 'base64').toString()).to.equal('wat'); + done(); + + }); + }); + + it('Should return a mocked buffer as if it were a real one, if type "any"', done => { + request('POST', {}, '/buffer_any_return/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result).to.be.instanceof(Buffer); + expect(result.toString()).to.equal('lol'); + done(); + + }); + }); + + it('Should return a nested mocked buffer as if it were a real one, if type "any"', done => { + request('POST', {}, '/buffer_nested_any_return/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result).to.haveOwnProperty('body'); + expect(result.body).to.haveOwnProperty('_base64'); + expect(Buffer.from(result.body._base64, 'base64').toString()).to.equal('lol'); + expect(result.test).to.exist; + expect(result.test.deep).to.exist; + expect(result.test.deep).to.be.an('array'); + expect(result.test.deep.length).to.equal(3); + expect(result.test.deep[1]).to.haveOwnProperty('_base64'); + expect(Buffer.from(result.test.deep[1]._base64, 'base64').toString()).to.equal('wat'); + done(); + + }); + }); + + it('Should throw an ValueError on an invalid Buffer type', done => { + request('POST', {}, '/value_error/buffer_invalid/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(502); + expect(res.headers['x-execution-uuid'].length).to.be.greaterThan(1); + expect(result).to.exist; + done(); + + }); + }); + + it('Should throw an ValueError on an invalid Number type', done => { + request('POST', {}, '/value_error/number_invalid/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(502); + expect(res.headers['x-execution-uuid'].length).to.be.greaterThan(1); + expect(result).to.exist; + done(); + + }); + }); + + it('Should throw an ValueError on an invalid Object type with alternate schema', done => { + request('POST', {}, '/value_error/object_alternate_schema_invalid/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(502); + expect(res.headers['x-execution-uuid'].length).to.be.greaterThan(1); + expect(result).to.exist; + done(); + + }); + }); + + it('Should not populate "context.keys" with no authorization keys header provided', done => { + request('POST', {}, '/keys/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result).to.deep.equal({ + TEST_KEY: null, + ANOTHER_KEY: null, + A_THIRD_KEY: null + }); + done(); + + }); + }); + + it('Should not populate "context.keys" if the authorization keys header is not a serialized object', done => { + request('POST', { + 'X-Authorization-Keys': 'stringvalue' + }, '/keys/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result).to.deep.equal({ + TEST_KEY: null, + ANOTHER_KEY: null, + A_THIRD_KEY: null + }); + done(); + + }); + }); + + it('Should populate "context.keys" with only the proper keys', done => { + request('POST', { + 'X-Authorization-Keys': JSON.stringify({ + TEST_KEY: '123', + ANOTHER_KEY: 'abc', + UNSPECIFIED_KEY: '987' + }) + }, '/keys/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result).to.deep.equal({ + TEST_KEY: '123', + ANOTHER_KEY: 'abc', + A_THIRD_KEY: null + }); + done(); + + }); + }); + + it('Should not populate "context.providers" with no authorization providers header provided', done => { + request('POST', {}, '/context/', {}, + (err, res, result) => { - expect(err).to.not.exist; - expect(res.statusCode).to.equal(400); - expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); - expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); - expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(result.error).to.exist; - expect(result.error.type).to.equal('ParameterError'); - expect(result.error.details).to.exist; - expect(result.error.details.users).to.exist; - expect(result.error.details.users.expected).to.exist; - expect(result.error.details.users.expected.type).to.equal('array'); - expect(result.error.details.users.expected.schema).to.deep.equal([ - { - name: 'user', - type: 'object', - description: 'a user', - schema: [ - { - name: 'username', - type: 'string', - description: '' - }, - { - name: 'posts', - type: 'array', - description: '', - schema: [ - { - name: 'post', - type: 'object', - description: '', - schema: [ - { - name: 'title', - type: 'string', - description: '' - }, - { - name: 'body', - type: 'string', - description: '' - } - ] - } - ] - } - ] - } - ]); - expect(result.error.details.users.actual).to.deep.equal({ - type: 'array', - value: [ - { - posts: [ - { - body: 'b', - title: 't' - } - ], - username: 'steve' - }, - { - posts: [ - { - body: 'b', - title: 't' - } - ] - } - ] - }); - done(); + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result.providers).to.deep.equal({}); + done(); + + }); + }); + + it('Should not populate "context.providers" if the authorization providers header is not an serialized object', done => { + request('POST', { + 'X-Authorization-Providers': 'stringvalue' + }, '/context/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result.providers).to.deep.equal({}); + done(); + + }); + }); + + it('Should populate "context.providers" as the value of the authorization providers header if it is a serialized object', done => { + let headerValue = { + test: { + item: 'value' + } + }; + request('POST', { + 'X-Authorization-Providers': JSON.stringify(headerValue) + }, '/context/', {}, + (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(result).to.exist; + expect(result.providers).to.deep.equal(headerValue); + done(); + + }); + }); + + it('Should populate context in "inline/context"', done => { + request('POST', {}, '/inline/context/', {a: 1, b: 2}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/json'); + expect(result).to.exist; + expect(result.http.method).to.equal('POST'); + expect(result.params).to.deep.equal({a: 1, b: 2}); + done(); + + }); + }); + + it('Should output buffer from "inline/buffer"', done => { + request('POST', {}, '/inline/buffer/', {}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('text/html'); + expect(result).to.exist; + expect(result).to.be.instanceof(Buffer); + expect(result.toString()).to.equal('lol'); + done(); + + }); + }); + + it('Should output buffer from "inline/buffer_mock"', done => { + request('POST', {}, '/inline/buffer_mock/', {}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/octet-stream'); + expect(result).to.exist; + expect(result).to.be.instanceof(Buffer); + expect(result.toString()).to.equal('lol'); + done(); + + }); + }); + + it('Should output buffer from "inline/http"', done => { + request('POST', {}, '/inline/http/', {}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(429); + expect(res.headers['content-type']).to.equal('text/html'); + expect(result).to.exist; + expect(result).to.be.instanceof(Buffer); + expect(result.toString()).to.equal('lol'); + done(); + + }); + }); + + it('Should output buffer from "inline/http_no_status"', done => { + request('POST', {}, '/inline/http_no_status/', {}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('text/html'); + expect(result).to.exist; + expect(result).to.be.instanceof(Buffer); + expect(result.toString()).to.equal('lol'); + done(); + + }); + }); + + it('Should output object from "inline/extended_http_is_object"', done => { + request('POST', {}, '/inline/extended_http_is_object/', {}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/json'); + expect(result).to.exist; + expect(result).to.deep.equal({ + statusCode: 429, + headers: {'Content-Type': 'text/html'}, + body: 'lol', + extend: true + }); + done(); + + }); + }); + + it('Should output object from "inline/number"', done => { + request('POST', {}, '/inline/number/', {}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/json'); + expect(result).to.exist; + expect(result).to.equal(1988); + done(); + + }); + }); + + it('Should allow you to use "require()"', done => { + request('POST', {}, '/inline/require/', {}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/json'); + expect(result).to.exist; + expect(result).to.equal('hello'); + done(); + + }); + }); + + it('Should allow you to use "await"', done => { + request('POST', {}, '/inline/await/', {}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/json'); + expect(result).to.exist; + expect(result).to.equal('hello world'); + done(); + + }); + }); + + it('Should support static files in "www" directory properly', done => { + request('GET', {}, '/page.html', '', (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('text/html; charset=utf-8'); + expect(result.toString()).to.equal('this is an html file\n'); + done(); + + }); + }); + + it('Should support POST to static files in "www" directory properly (noop)', done => { + request('POST', {}, '/page.html', {}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('text/html; charset=utf-8'); + expect(result.toString()).to.equal('this is an html file\n'); + done(); + + }); + }); + + it('Should NOT support static files in "www" directory properly, without .html', done => { + request('GET', {}, '/page/', '', (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(404); + done(); + + }); + }); + + it('Should NOT support static files in "www" directory properly, without .htm', done => { + request('GET', {}, '/page2/', '', (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(404); + done(); + + }); + }); + + it('Should support 404 not found in "www" directory properly by direct accession', done => { + request('GET', {}, '/error/404.html', '', (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(404); + expect(res.headers['content-type']).to.equal('text/html; charset=utf-8'); + expect(result.toString()).to.equal('error 404\n'); + done(); + + }); + }); + + it('Should support 404 not found in "www" directory properly by dir accession', done => { + request('GET', {}, '/error/', '', (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(404); + expect(res.headers['content-type']).to.equal('text/html; charset=utf-8'); + expect(result.toString()).to.equal('error 404\n'); + done(); - }); + }); }); - it('Should accept a nested array that correctly maps to Schema', done => { - request('POST', {}, '/schema_rejection_nested_array/', { - users: [ - { username: 'steve', posts: [{ title: 't', body: 'b' }] }, - { username: 'steve2', posts: [{ title: 't', body: 'b' }] } - ] - }, - (err, res, result) => { + it('Should support 404 not found in "www" directory properly by non-existent file accession', done => { + request('GET', {}, '/error/nope.txt', '', (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(404); + expect(res.headers['content-type']).to.equal('text/html; charset=utf-8'); + expect(result.toString()).to.equal('error 404\n'); + done(); + + }); + }); + + it('Should support 404 not found in "www" directory properly by nested non-existent file accession', done => { + request('GET', {}, '/error/path/to/nope.txt', '', (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(404); + expect(res.headers['content-type']).to.equal('text/html; charset=utf-8'); + expect(result.toString()).to.equal('error 404\n'); + done(); + + }); + }); + + it('Should support 404 not found in "www" directory properly by nested non-existent file accession', done => { + request('GET', {}, '/error/path/to/nope.txt', '', (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(404); + expect(res.headers['content-type']).to.equal('text/html; charset=utf-8'); + expect(result.toString()).to.equal('error 404\n'); + done(); + + }); + }); + + it('Should support "index.html" mapping to root directory', done => { + request('GET', {}, '/static-test/', '', (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); - expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); - expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(result).to.deep.equal( [ - { username: 'steve', posts: [{ title: 't', body: 'b' }] }, - { username: 'steve2', posts: [{ title: 't', body: 'b' }] } - ]); + expect(res.headers['content-type']).to.equal('text/html; charset=utf-8'); + expect(result.toString()).to.equal('this is an index.html file\n'); done(); }); }); - it('Should reject an array that doesn\'t map to a Schema for an array of numbers', done => { - request('POST', {}, '/schema_rejection_number_array/', { - userIds: ['alpha', 'beta'] - }, - (err, res, result) => { + it('Should support "index.html" also mapping to itself', done => { + request('GET', {}, '/static-test/index.html', '', (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(400); - expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); - expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); - expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(result.error).to.exist; - expect(result.error.type).to.equal('ParameterError'); - expect(result.error.details).to.exist; - expect(result.error.details.userIds).to.exist; - expect(result.error.details.userIds.expected).to.exist; - expect(result.error.details.userIds.expected.type).to.equal('array'); - expect(result.error.details.userIds.expected.schema).to.exist; - expect(result.error.details.userIds.expected.schema).to.have.length(1); - expect(result.error.details.userIds.expected.schema[0].type).to.equal('number'); - expect(result.error.details.userIds.actual).to.exist; - expect(result.error.details.userIds.actual.type).to.equal('array'); - expect(result.error.details.userIds.actual.value).to.deep.equal(['alpha', 'beta']); + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('text/html; charset=utf-8'); + expect(result.toString()).to.equal('this is an index.html file\n'); done(); }); }); - it('Should accept an array that correctly maps to Schema', done => { - request('POST', {}, '/schema_rejection_number_array/', { - userIds: [1, 2, 3] - }, - (err, res, result) => { + it('Should support "index.htm" mapping to root directory', done => { + request('GET', {}, '/static-test/htm/', '', (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); - expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); - expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); - expect(result).to.equal('hello'); + expect(res.headers['content-type']).to.equal('text/html; charset=utf-8'); + expect(result.toString()).to.equal('this is an index.htm file\n'); done(); }); }); - it('Should handle large buffer parameters', done => { - request('POST', {'x-convert-strings': true}, '/runtime/largebuffer/', { - file: `{"_base64": "${'a'.repeat(50000000)}"}` - }, (err, res, result) => { + it('Should support "index.htm" also mapping to itself', done => { + request('GET', {}, '/static-test/htm/index.htm', '', (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(result).to.exist; - expect(result.error).to.not.exist; + expect(res.headers['content-type']).to.equal('text/html; charset=utf-8'); + expect(result.toString()).to.equal('this is an index.htm file\n'); done(); }); - }).timeout(5000); + }); - it('Should accept a request with the optional param', done => { - request('POST', {}, '/optional_params/', {name: 'steve'}, - (err, res, result) => { + it('Should support static (www) ".png" files properly', done => { + request('GET', {}, '/fs-wordmark.png', '', (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(result).to.equal('steve'); + expect(res.headers['content-type']).to.equal('image/png'); + expect(result.byteLength).to.equal(parseInt(res.headers['content-length'])); done(); }); }); - it('Should accept a request without the optional param', done => { - request('POST', {}, '/optional_params/', {}, - (err, res, result) => { + it('Should support static (www) ".mp4" files properly', done => { + request('GET', {}, '/video.mp4', '', (err, res, result, headers) => { + let size = parseInt(res.headers['content-length']); expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(result).to.equal('hello'); + expect(res.headers['content-type']).to.equal('video/mp4'); + expect(res.headers['content-range']).to.equal('bytes 0-' + (size - 1) + '/' + size); + expect(res.headers['accept-ranges']).to.equal('bytes'); + expect(result.byteLength).to.equal(size); done(); }); }); - it('Should accept a request without the optional param', done => { - request('POST', {}, '/schema_optional_params/', {}, - (err, res, result) => { + it('Should support static (www) ".mp4" files properly with range header', done => { + request('GET', {range: '27-255'}, '/video.mp4', '', (err, res, result) => { + + let size = parseInt(res.headers['content-length']); + expect(err).to.not.exist; + expect(res.statusCode).to.equal(206); + expect(res.headers['content-type']).to.equal('video/mp4'); + expect(res.headers['content-range']).to.equal('bytes 27-255/574823'); + expect(res.headers['accept-ranges']).to.equal('bytes'); + expect(size).to.equal(255 - 27 + 1); + expect(result.byteLength).to.equal(size); + done(); + + }); + }); + it('Should support static (www) ".mp4" files properly with range header (prefix)', done => { + request('GET', {range: '0-'}, '/video.mp4', '', (err, res, result) => { + + let size = parseInt(res.headers['content-length']); expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(result).to.equal(null); + expect(res.headers['content-type']).to.equal('video/mp4'); + expect(res.headers['content-range']).to.equal('bytes 0-' + (size - 1) + '/' + size); + expect(res.headers['accept-ranges']).to.equal('bytes'); + expect(result.byteLength).to.equal(size); done(); }); }); - it('Should accept a request without the optional param field', done => { - request('POST', {}, '/schema_optional_params/', {obj: {name: 'steve'}}, - (err, res, result) => { + it('Should support static (www) ".mp4" files properly with range header (prefix + 1)', done => { + request('GET', {range: '1-'}, '/video.mp4', '', (err, res, result) => { + + let size = parseInt(res.headers['content-length']); + expect(err).to.not.exist; + expect(res.statusCode).to.equal(206); + expect(res.headers['content-type']).to.equal('video/mp4'); + expect(res.headers['content-range']).to.equal('bytes 1-574822/574823'); + expect(res.headers['accept-ranges']).to.equal('bytes'); + expect(result.byteLength).to.equal(size); + done(); + + }); + }); + + it('Should support static (www) ".mp4" files properly with range header (suffix)', done => { + request('GET', {range: '-500'}, '/video.mp4', '', (err, res, result) => { + + let size = parseInt(res.headers['content-length']); + expect(err).to.not.exist; + expect(res.statusCode).to.equal(206); + expect(res.headers['content-type']).to.equal('video/mp4'); + expect(res.headers['content-range']).to.equal('bytes 574323-574822/574823'); + expect(res.headers['accept-ranges']).to.equal('bytes'); + expect(size).to.equal(500); + expect(result.byteLength).to.equal(size); + done(); + + }); + }); + + it('Should support POST with nonstandard JSON (array)', done => { + request('POST', {}, '/nonstandard/json/', [1, 2, 3], (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(result).to.deep.equal({name: 'steve'}); + expect(res.headers['content-type']).to.equal('application/json'); + expect(result.http.json).to.exist; + expect(result.http.json).to.deep.equal([1, 2, 3]); done(); }); }); - it('Should accept a request with the optional param field set to null', done => { - request('POST', {}, '/schema_optional_params/', {obj: {name: 'steve', enabled: null}}, - (err, res, result) => { + it('Should support POST with nonstandard JSON (string)', done => { + request('POST', {}, '/nonstandard/json/', '"hello"', (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(result).to.deep.equal({name: 'steve', enabled: null}); + expect(res.headers['content-type']).to.equal('application/json'); + expect(result.http.json).to.exist; + expect(result.http.json).to.equal('hello'); done(); }); }); - it('Should accept a request with the optional param field', done => { - request('POST', {}, '/schema_optional_params/', {obj: {name: 'steve', enabled: true}}, - (err, res, result) => { + it('Should support POST with nonstandard JSON (boolean)', done => { + request('POST', {}, '/nonstandard/json/', 'true', (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(result).to.deep.equal({name: 'steve', enabled: true}); + expect(res.headers['content-type']).to.equal('application/json'); + expect(result.http.json).to.exist; + expect(result.http.json).to.equal(true); done(); }); }); - it('Should accept a request without the optional param (nested schema)', done => { - request('POST', {}, '/optional_nested_schema_params/', {obj: {name: 'steve' }}, - (err, res, result) => { + it('Should support POST with nonstandard JSON (number)', done => { + request('POST', {}, '/nonstandard/json/', '1.2', (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(result).to.deep.equal({name: 'steve'}); + expect(res.headers['content-type']).to.equal('application/json'); + expect(result.http.json).to.exist; + expect(result.http.json).to.equal(1.2); + done(); + + }); + }); + + it('Should support POST with XML', done => { + + let xmlData = ` + + + John + Doe + 1234567890 + johndoe@example.com +
+ San Francisco + California + 123456 +
+ True +
+ + Jane + Smith + 0987654321 + janesmith@example.com +
+ Los Angeles + California + 654321 +
+ False +
+
`; + + let parsedData = { + Company: { + Employee: [ + { + FirstName: 'John', + LastName: 'Doe', + ContactNo: "1234567890", + Email: 'johndoe@example.com', + Address: { + City: 'San Francisco', + State: 'California', + Zip: '123456' + }, + Fulltime: 'True', + }, + { + FirstName: 'Jane', + LastName: 'Smith', + ContactNo: '0987654321', + Email: 'janesmith@example.com', + Address: { + City: 'Los Angeles', + State: 'California', + Zip: '654321' + }, + Fulltime: 'False', + } + ] + } + } + + request('POST', {'Content-Type': 'application/xml'}, '/reflect/', xmlData, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result).to.deep.equal(parsedData); + done(); + + }); + }); + + it('Should support POST with XML (containing attributes)', done => { + + let xmlData = ` + + + YouTube video feed + 2015-04-01T19:05:24.552394234+00:00 + + yt:video:VIDEO_ID + VIDEO_ID + CHANNEL_ID + Video title + + + Channel title + http://www.youtube.com/channel/CHANNEL_ID + + 2015-03-06T21:40:57+00:00 + 2015-03-09T19:05:24.552394234+00:00 + + `; + + let parsedData = { + "feed": { + "@_xmlns:yt": "http://www.youtube.com/xml/schemas/2015", + "@_xmlns": "http://www.w3.org/2005/Atom", + "link": [ + { + "@_rel": "hub", + "@_href": "https://pubsubhubbub.appspot.com" + }, + { + "@_rel": "self", + "@_href": "https://www.youtube.com/xml/feeds/videos.xml?channel_id=CHANNEL_ID" + } + ], + "title": "YouTube video feed", + "updated": "2015-04-01T19:05:24.552394234+00:00", + "entry": { + "id": "yt:video:VIDEO_ID", + "yt:videoId": "VIDEO_ID", + "yt:channelId": "CHANNEL_ID", + "title": "Video title", + "link": { + "@_rel": "alternate", + "@_href": "http://www.youtube.com/watch?v=VIDEO_ID" + }, + "author": { + "name": "Channel title", + "uri": "http://www.youtube.com/channel/CHANNEL_ID" + }, + "published": "2015-03-06T21:40:57+00:00", + "updated": "2015-03-09T19:05:24.552394234+00:00" + } + } + } + + request('POST', {'Content-Type': 'application/xml'}, '/reflect/', xmlData, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result).to.deep.equal(parsedData); done(); }); + }); - it('Should reject a request without the required param within the optional object (nested schema)', done => { - request('POST', {}, '/optional_nested_schema_params/', {obj: {name: 'steve', options: {}}}, - (err, res, result) => { + it('Should reject invalid XML', done => { + + let xmlData = ` + + + John + Doe + 1234567890 + johndoe@example.com +
+ San Francisco + California + 123456 +
+ True +
+ + Jane + Smith + 0987654321 + janesmith@example.com +
+ Los Angeles + California + 654321 +
+ False +
+ `; + + request('POST', {'Content-Type': 'application/xml'}, '/reflect/', xmlData, (err, res, result) => { expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); expect(result).to.exist; expect(result.error).to.exist; - expect(res.statusCode).to.equal(400); + expect(result.error.type).to.equal('ParameterParseError'); done(); }); }); + it('Should not reject nor parse XML if no Content-Type headers are passed in', done => { - it('Should accept a request with the optional object (nested schema)', done => { - request('POST', {}, '/optional_nested_schema_params/', {obj: {name: 'steve', options: {istest: true}}}, - (err, res, result) => { + let xmlData = ` + + + John + Doe + 1234567890 + johndoe@example.com +
+ San Francisco + California + 123456 +
+ True +
+ + Jane + Smith + 0987654321 + janesmith@example.com +
+ Los Angeles + California + 654321 +
+ False +
+
`; + + request('POST', {}, '/reflect/', xmlData, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(result).to.exist; - expect(result).to.deep.equal({name: 'steve', options: { istest: true}}); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); done(); }); }); - it('Should accept a request with the optional object and optional field (nested schema)', done => { - request('POST', {}, '/optional_nested_schema_params/', {obj: {name: 'steve', options: {istest: true, threads: 4}}}, - (err, res, result) => { + it('Should support POST with XML for content type "application/atom+xml"', done => { + + let xmlData = ` + + + John + Doe + 1234567890 + johndoe@example.com +
+ San Francisco + California + 123456 +
+ True +
+ + Jane + Smith + 0987654321 + janesmith@example.com +
+ Los Angeles + California + 654321 +
+ False +
+
`; + + let parsedData = { + Company: { + Employee: [ + { + FirstName: 'John', + LastName: 'Doe', + ContactNo: "1234567890", + Email: 'johndoe@example.com', + Address: { + City: 'San Francisco', + State: 'California', + Zip: '123456' + }, + Fulltime: 'True', + }, + { + FirstName: 'Jane', + LastName: 'Smith', + ContactNo: '0987654321', + Email: 'janesmith@example.com', + Address: { + City: 'Los Angeles', + State: 'California', + Zip: '654321' + }, + Fulltime: 'False', + } + ] + } + } + + request('POST', {'Content-Type': 'application/atom+xml'}, '/reflect/', xmlData, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(result).to.exist; - expect(result).to.deep.equal({name: 'steve', options: { istest: true, threads: 4}}); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result).to.deep.equal(parsedData); done(); }); }); - it('Should successfully return a request without the optional value', done => { - request('POST', {}, '/optional_nested_schema_params/', {}, - (err, res, result) => { + it('Should support POST with XML for content type "application/atom+xml" (containing attributes)', done => { + + let xmlData = ` + yt:video:abdefghijklmnop + abdefghijklmnop + abcdefghijklmnop + Some Video Title + + + Video Name + https://www.youtube.com/channel/abcdefghijklmnop + + 2021-06-24T23:37:28+00:00 + 2021-06-24T23:37:58.731601431+00:00 + `; + + let parsedData = { + "entry": { + "author": { + "name": "Video Name", + "uri": "https://www.youtube.com/channel/abcdefghijklmnop" + }, + "id": "yt:video:abdefghijklmnop", + "link": { + "@_href": "https://www.youtube.com/watch?v=abcdefghijklmnop", + "@_rel": "alternate", + }, + "published": "2021-06-24T23:37:28+00:00", + "title": "Some Video Title", + "updated": "2021-06-24T23:37:58.731601431+00:00", + "yt:channelId": "abcdefghijklmnop", + "yt:videoId": "abdefghijklmnop" + } + }; + + request('POST', {'Content-Type': 'application/atom+xml'}, '/reflect/', xmlData, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(result).to.equal(null); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result).to.deep.equal(parsedData); done(); }); + }); + it('Should reject invalid XML for content type "application/atom+xml"', done => { - it('Should successfully return a request without the optional values', done => { - request('POST', {}, '/optional_nested_schema_params/', {obj: {name: 'steve'}}, - (err, res, result) => { + let xmlData = ` + + + John + Doe + 1234567890 + johndoe@example.com +
+ San Francisco + California + 123456 +
+ True +
+ + Jane + Smith + 0987654321 + janesmith@example.com +
+ Los Angeles + California + 654321 +
+ False +
+ `; + + request('POST', {'Content-Type': 'application/atom+xml'}, '/reflect/', xmlData, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(result).to.exist; + expect(result.error).to.exist; + expect(result.error.type).to.equal('ParameterParseError'); + done(); + + }); + }); + + it('Streaming endpoints should default to normal request with no _stream sent', done => { + request('POST', {}, '/stream/basic/', {alpha: 'hello'}, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/json'); expect(result).to.exist; - expect(result).to.deep.equal({name: 'steve'}); + expect(result).to.equal(true); + done(); }); }); - it('Should successfully return a request with the optional values', done => { - request('POST', {}, '/optional_nested_schema_params/', {obj: {name: 'steve', options: {istest: true}}}, - (err, res, result) => { + it('Streaming endpoints should default to normal request with _stream falsy', done => { + request('POST', {}, '/stream/basic/', {alpha: 'hello', _stream: false}, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/json'); expect(result).to.exist; - expect(result).to.deep.equal({name: 'steve', options: {istest: true}}); + expect(result).to.equal(true); + + done(); + + }); + }); + + it('Streaming endpoints should default to normal request with _stream falsy in query params', done => { + request('POST', {}, '/stream/basic/?alpha=hello&_stream=false', '', (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/json'); + expect(result).to.exist; + expect(result).to.equal(true); + + done(); + + }); + }); + + it('Streaming endpoints should fail with StreamError if contains an invalid stream', done => { + request('POST', {}, '/stream/basic/', {alpha: 'hello', _stream: {test: true}}, (err, res, result) => { + + expect(err).to.not.exist; + expect(res.statusCode).to.equal(400); + expect(res.headers['content-type']).to.equal('application/json'); + expect(result).to.exist; + expect(result.error).to.exist; + expect(result.error.type).to.equal('StreamListenerError'); + expect(result.error.details).to.haveOwnProperty('test'); + done(); }); }); - it('Should successfully return a request with the optional values and fields', done => { - request('POST', {}, '/optional_nested_schema_params/', {obj: {name: 'steve', options: {istest: true, threads: 4}}}, - (err, res, result) => { + it('Should support POST with streaming with _stream in query params', done => { + request('POST', {}, '/stream/basic/?alpha=hello&_stream', '', (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('text/event-stream'); expect(result).to.exist; - expect(result).to.deep.equal({name: 'steve', options: {istest: true, threads: 4}}); + + let events = parseServerSentEvents(result); + expect(events['hello']).to.exist; + expect(events['hello'][0]).to.equal('true'); + expect(events['@response']).to.exist; + + let response = JSON.parse(events['@response'][0]); + expect(response.headers['Content-Type']).to.equal('application/json'); + expect(response.body).to.equal('true'); + done(); }); }); - it('Should successfully return a default value with an optional field', done => { - request('POST', {}, '/optional_param_not_null/', {}, - (err, res, result) => { + it('Should support POST with streaming with _stream in query params with truthy value', done => { + request('POST', {}, '/stream/basic/?alpha=hello&_stream=lol', '', (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('text/event-stream'); expect(result).to.exist; - expect(result).to.equal('default'); + + let events = parseServerSentEvents(result); + expect(events['hello']).to.exist; + expect(events['hello'][0]).to.equal('true'); + expect(events['@response']).to.exist; + + let response = JSON.parse(events['@response'][0]); + expect(response.headers['Content-Type']).to.equal('application/json'); + expect(response.body).to.equal('true'); + done(); }); }); - it('Should successfully return a schema with a default set to 0', done => { - request('POST', {}, '/stripe/', {id: '0'}, - (err, res, result) => { + it('Should support POST with streaming with _stream set to valid stream', done => { + request('POST', {}, '/stream/basic/', {alpha: 'hello', _stream: {'hello': true}}, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('text/event-stream'); expect(result).to.exist; + + let events = parseServerSentEvents(result); + expect(events['hello']).to.exist; + expect(events['hello'][0]).to.equal('true'); + expect(events['@response']).to.exist; + + let response = JSON.parse(events['@response'][0]); + expect(response.headers['Content-Type']).to.equal('application/json'); + expect(response.body).to.equal('true'); + done(); }); }); - it('Should successfully return a schema with an array', done => { - request('POST', {}, '/giphy/', {query: 'q'}, - (err, res, result) => { + it('Should support POST with streaming with _stream set to *', done => { + request('POST', {}, '/stream/basic/', {alpha: 'hello', _stream: {'*': true}}, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('text/event-stream'); expect(result).to.exist; + + let events = parseServerSentEvents(result); + expect(events['hello']).to.exist; + expect(events['hello'][0]).to.equal('true'); + expect(events['@response']).to.exist; + + let response = JSON.parse(events['@response'][0]); + expect(response.headers['Content-Type']).to.equal('application/json'); + expect(response.body).to.equal('true'); + done(); }); }); - it('Should reject a request without an proper enum member', done => { - request('POST', {}, '/enum/', { day: 'funday' }, (err, res, result) => { + it('Should support POST with streaming with _stream set to valid stream or *', done => { + request('POST', {}, '/stream/basic/', {alpha: 'hello', _stream: {'hello': true, '*': true}}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(400); + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('text/event-stream'); expect(result).to.exist; - expect(result.error).to.exist; - expect(result.error.type).to.exist; - expect(result.error.type).to.equal('ParameterError'); - expect(result.error.details).to.exist; - expect(result.error.details).to.deep.equal({ - day: { - message: 'invalid value: "funday" (string), expected (enum)', - invalid: true, - expected: { - type: 'enum', - members: [ - ['sunday', 0], - ['monday', '0'], - ['tuesday', { a: 1, b: 2 }], - ['wednesday', 3], - ['thursday', [1, 2, 3]], - ['friday', 5.4321], - ['saturday', 6] - ] - }, - actual: { - value: 'funday', - type: 'string' - } - } - }); + + let events = parseServerSentEvents(result); + expect(events['hello']).to.exist; + expect(events['hello'][0]).to.equal('true'); + expect(events['@response']).to.exist; + + let response = JSON.parse(events['@response'][0]); + expect(response.headers['Content-Type']).to.equal('application/json'); + expect(response.body).to.equal('true'); + done(); }); }); - it('Should successfully return an enum varient (number)', done => { - request('POST', {}, '/enum/', { day: 'sunday' }, - (err, res, result) => { + it('Should support POST with streaming with _stream set', done => { + request('POST', {}, '/stream/basic/', {alpha: 'hello', _stream: true}, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('text/event-stream'); expect(result).to.exist; - expect(result).to.equal(0); + + let events = parseServerSentEvents(result); + expect(events['hello']).to.exist; + expect(events['hello'][0]).to.equal('true'); + expect(events['@response']).to.exist; + + let response = JSON.parse(events['@response'][0]); + expect(response.headers['Content-Type']).to.equal('application/json'); + expect(response.body).to.equal('true'); + done(); }); }); - it('Should successfully return an enum varient (string)', done => { - request('POST', {}, '/enum/', { day: 'monday' }, - (err, res, result) => { + it('Should support POST with streaming (buffer) with _stream set', done => { + request('POST', {}, '/stream/basic_buffer/', {alpha: 'hello', _stream: true}, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('text/event-stream'); expect(result).to.exist; - expect(result).to.equal("0"); + + let events = parseServerSentEvents(result); + expect(events['hello']).to.exist; + expect(events['@response']).to.exist; + + let stream = JSON.parse(events['hello'][0]); + expect(stream).to.be.an.object; + expect(stream).to.haveOwnProperty('_base64'); + expect(stream._base64).to.equal(Buffer.from('123').toString('base64')); + + let response = JSON.parse(events['@response'][0]); + expect(response.headers['Content-Type']).to.equal('application/json'); + expect(response.body).to.equal('true'); + done(); }); }); - it('Should successfully return an enum varient (object)', done => { - request('POST', {}, '/enum/', { day: 'tuesday' }, - (err, res, result) => { + it('Should support POST with streaming (mocked buffer) with _stream set', done => { + request('POST', {}, '/stream/basic_buffer_mocked/', {alpha: 'hello', _stream: true}, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('text/event-stream'); expect(result).to.exist; - expect(result).to.deep.equal({a: 1, b: 2}); + + let events = parseServerSentEvents(result); + expect(events['hello']).to.exist; + expect(events['@response']).to.exist; + + let stream = JSON.parse(events['hello'][0]); + expect(stream).to.be.an.object; + expect(stream).to.haveOwnProperty('_base64'); + expect(stream._base64).to.equal(Buffer.from('123').toString('base64')); + + let response = JSON.parse(events['@response'][0]); + expect(response.headers['Content-Type']).to.equal('application/json'); + expect(response.body).to.equal('true'); + done(); }); }); - - it('Should successfully return an enum varient (array)', done => { - request('POST', {}, '/enum/', { day: 'thursday' }, - (err, res, result) => { + it('Should support POST with streaming (nested buffer) with _stream set', done => { + request('POST', {}, '/stream/basic_buffer_nested/', {alpha: 'hello', _stream: true}, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('text/event-stream'); expect(result).to.exist; - expect(result).to.deep.equal([1, 2, 3]); + + let events = parseServerSentEvents(result); + expect(events['hello']).to.exist; + expect(events['@response']).to.exist; + + let stream = JSON.parse(events['hello'][0]); + expect(stream).to.be.an.object; + expect(stream).to.haveOwnProperty('mybuff'); + expect(stream.mybuff).to.haveOwnProperty('_base64'); + expect(stream.mybuff._base64).to.equal(Buffer.from('123').toString('base64')); + + let response = JSON.parse(events['@response'][0]); + expect(response.headers['Content-Type']).to.equal('application/json'); + expect(response.body).to.equal('true'); + done(); }); }); - it('Should successfully return an enum varient (float)', done => { - request('POST', {}, '/enum/', { day: 'friday' }, - (err, res, result) => { + it('Should support POST with streaming (nested mocked buffer) with _stream set', done => { + request('POST', {}, '/stream/basic_buffer_nested_mocked/', {alpha: 'hello', _stream: true}, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('text/event-stream'); expect(result).to.exist; - expect(result).to.equal(5.4321); + + let events = parseServerSentEvents(result); + expect(events['hello']).to.exist; + expect(events['@response']).to.exist; + + let stream = JSON.parse(events['hello'][0]); + expect(stream).to.be.an.object; + expect(stream).to.haveOwnProperty('mybuff'); + expect(stream.mybuff).to.haveOwnProperty('_base64'); + expect(stream.mybuff._base64).to.equal(Buffer.from('123').toString('base64')); + + let response = JSON.parse(events['@response'][0]); + expect(response.headers['Content-Type']).to.equal('application/json'); + expect(response.body).to.equal('true'); + done(); }); }); - it('Should return a default enum varient', done => { - request('POST', {}, '/enum_default/', {}, - (err, res, result) => { + it('Should error POST with invalid stream name in execution with _stream set', done => { + request('POST', {}, '/stream/invalid_stream_name/', {alpha: 'hello', _stream: true}, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('text/event-stream'); expect(result).to.exist; - expect(result).to.equal(0); + + let events = parseServerSentEvents(result); + expect(events['hello']).to.not.exist; + expect(events['@error']).to.exist; + expect(events['@response']).to.exist; + + let error = JSON.parse(events['@error'][0]); + expect(error.type).to.equal('StreamError'); + expect(error.message).to.satisfy(msg => msg.startsWith(`No such stream "hello2" in function definition.`)); + + let response = JSON.parse(events['@response'][0]); + expect(response.headers['Content-Type']).to.equal('application/json'); + expect(response.statusCode).to.equal(200); + expect(response.body).to.equal('true'); + done(); }); }); - it('Should return an enum using the context param', done => { - request('POST', {}, '/enum_context/', { thingA: 'a', thingB: 'c' }, - (err, res, result) => { + it('Should quietly fail if invalid stream name in execution without _stream set', done => { + request('POST', {}, '/stream/invalid_stream_name/', {alpha: 'hello'}, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(result).to.exist; - expect(result).to.deep.equal({ - a: 0, - b: { - c: 1, - d: [1, 2, 3] - }, - c: '4', - d: 5.4321 - }); + expect(res.headers['content-type']).to.equal('application/json'); + expect(result).to.equal(true); + done(); }); }); - it('Should return an enum varient when the return type is enum', done => { - request('POST', {}, '/enum_return/', { a: 'a' }, - (err, res, result) => { + it('Should stream error POST with invalid stream value in execution with _stream set', done => { + request('POST', {}, '/stream/invalid_stream_param/', {alpha: 'hello', _stream: true}, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('text/event-stream'); expect(result).to.exist; - expect(result).to.equal(0); + + let events = parseServerSentEvents(result); + expect(events['hello']).to.not.exist; + expect(events['@error']).to.exist; + expect(events['@response']).to.exist; + + let error = JSON.parse(events['@error'][0]); + expect(error.type).to.equal('StreamError'); + expect(error.message).to.satisfy(msg => msg.startsWith(`Stream Parameter Error: "hello".`)); + expect(error.details).to.haveOwnProperty('hello'); + expect(error.details['hello'].invalid).to.equal(true); + expect(error.details['hello'].expected.type).to.equal('boolean'); + expect(error.details['hello'].actual.type).to.equal('string'); + expect(error.details['hello'].actual.value).to.equal('what'); + + let response = JSON.parse(events['@response'][0]); + expect(response.headers['Content-Type']).to.equal('application/json'); + expect(response.statusCode).to.equal(200); + expect(response.body).to.equal('true'); + done(); }); }); - it('Should reject returning an invalid enum varient when the return type is enum', done => { - request('POST', {}, '/enum_return/', {}, - (err, res, result) => { + it('Should support POST with streaming and stream components between sleep() calls with _stream set', done => { + request('POST', {}, '/stream/sleep/', {alpha: 'hello', _stream: true}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(502); + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('text/event-stream'); expect(result).to.exist; - expect(result.error).to.deep.equal({ - type: 'ValueError', - message: 'The value returned by the function did not match the specified type', - details: { - returns: { - message: 'invalid return value: "not correct" (string), expected (enum)', - invalid: true, - expected: { - type: 'enum', - members: [['a', 0], ['b', [1, 2, 3]]] - }, - actual: { - value: 'not correct', - type: 'string' - } - } - } - }); + + let events = parseServerSentEvents(result); + expect(events['hello']).to.exist; + expect(events['hello'].length).to.equal(3); + expect(events['hello'][0]).to.equal('"Hello?"'); + expect(events['hello'][1]).to.equal('"How are you?"'); + expect(events['hello'][2]).to.equal('"Is it me you\'re looking for?"'); + + expect(events['goodbye']).to.exist; + expect(events['goodbye'].length).to.equal(1); + expect(events['goodbye'][0]).to.equal('"Nice to see ya"'); + + expect(events['@response']).to.exist; + let response = JSON.parse(events['@response'][0]); + expect(response.headers['Content-Type']).to.equal('application/json'); + expect(response.body).to.equal('true'); + done(); }); }); - it('Should fail to return null from a function without a nullable return value', done => { - request('POST', {}, '/not_nullable_return/', {}, - (err, res, result) => { + it('Should POST normally with streaming and stream components between sleep() calls without _stream set', done => { + request('POST', {}, '/stream/sleep/', {alpha: 'hello'}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(502); - expect(result.error).to.exist; - expect(result.error.details).to.exist; - expect(result.error.details.returns).to.exist - expect(result.error.details.returns.invalid).to.equal(true); + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/json'); + expect(result).to.equal(true); + done(); }); }); - it('Should return null from a function with a nullable return value', done => { - request('POST', {}, '/nullable_return/', {}, - (err, res, result) => { + it('Should support POST with streaming without _debug set', done => { + request('POST', {}, '/stream/debug/', {alpha: 'hello', _stream: true}, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(result).to.equal(null);; + expect(res.headers['content-type']).to.equal('text/event-stream'); + expect(result).to.exist; + + let events = parseServerSentEvents(result); + expect(Object.keys(events).length).to.equal(4); + + expect(events['hello']).to.exist; + expect(events['hello'].length).to.equal(3); + expect(events['hello'][0]).to.equal('"Hello?"'); + expect(events['hello'][1]).to.equal('"How are you?"'); + expect(events['hello'][2]).to.equal('"Is it me you\'re looking for?"'); + + expect(events['goodbye']).to.exist; + expect(events['goodbye'].length).to.equal(1); + expect(events['goodbye'][0]).to.equal('"Nice to see ya"'); + + expect(events['@begin']).to.exist; + expect(events['@begin'].length).to.equal(1); + expect(events['@begin'][0]).to.be.a.string; + + expect(events['@response']).to.exist; + let response = JSON.parse(events['@response'][0]); + expect(response.headers['Content-Type']).to.equal('application/json'); + expect(response.body).to.equal('true'); + done(); }); }); - it('Should return a value from a function with a nullable return value', done => { - request('POST', {}, '/nullable_return/', {a: 'hello'}, - (err, res, result) => { + it('Should support POST with streaming with _debug set without _stream set', done => { + request('POST', {}, '/stream/debug/', {alpha: 'hello', _debug: true}, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(result).to.equal('hello');; + expect(res.headers['content-type']).to.equal('text/event-stream'); + expect(res.headers['x-debug']).to.equal('true'); + expect(result).to.exist; + + let events = parseServerSentEvents(result); + expect(Object.keys(events).length).to.equal(6); + + expect(events['hello']).to.exist; + expect(events['hello'].length).to.equal(3); + expect(events['hello'][0]).to.equal('"Hello?"'); + expect(events['hello'][1]).to.equal('"How are you?"'); + expect(events['hello'][2]).to.equal('"Is it me you\'re looking for?"'); + + expect(events['goodbye']).to.exist; + expect(events['goodbye'].length).to.equal(1); + expect(events['goodbye'][0]).to.equal('"Nice to see ya"'); + + expect(events['@begin']).to.exist; + expect(events['@begin'].length).to.equal(1); + expect(events['@begin'][0]).to.be.a.string; + + expect(events['@stdout']).to.exist; + expect(events['@stdout'].length).to.equal(2); + expect(events['@stdout'][0]).to.equal('"what? who?"'); + expect(events['@stdout'][1]).to.equal('"finally"'); + + expect(events['@stderr']).to.exist; + expect(events['@stderr'].length).to.equal(1); + expect(events['@stderr'][0]).to.equal('"oh no"'); + + expect(events['@response']).to.exist; + let response = JSON.parse(events['@response'][0]); + expect(response.headers['Content-Type']).to.equal('application/json'); + expect(response.body).to.equal('true'); + done(); }); }); - it('Should successfully return a default parameter after passing in null', done => { - request('POST', {}, '/null_default_param/', {name: null}, - (err, res, result) => { + it('Should support POST with streaming with _debug set to valid channels without _stream set', done => { + request('POST', {}, '/stream/debug/', {alpha: 'hello', _debug: {'*': true, '@begin': true, '@stdout': true, '@stderr': true, '@error': true}}, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(result).to.equal('default'); + expect(res.headers['content-type']).to.equal('text/event-stream'); + expect(res.headers['x-debug']).to.equal('true'); + expect(result).to.exist; + + let events = parseServerSentEvents(result); + expect(Object.keys(events).length).to.equal(6); + + expect(events['hello']).to.exist; + expect(events['hello'].length).to.equal(3); + expect(events['hello'][0]).to.equal('"Hello?"'); + expect(events['hello'][1]).to.equal('"How are you?"'); + expect(events['hello'][2]).to.equal('"Is it me you\'re looking for?"'); + + expect(events['goodbye']).to.exist; + expect(events['goodbye'].length).to.equal(1); + expect(events['goodbye'][0]).to.equal('"Nice to see ya"'); + + expect(events['@begin']).to.exist; + expect(events['@begin'].length).to.equal(1); + expect(events['@begin'][0]).to.be.a.string; + + expect(events['@stdout']).to.exist; + expect(events['@stdout'].length).to.equal(2); + expect(events['@stdout'][0]).to.equal('"what? who?"'); + expect(events['@stdout'][1]).to.equal('"finally"'); + + expect(events['@stderr']).to.exist; + expect(events['@stderr'].length).to.equal(1); + expect(events['@stderr'][0]).to.equal('"oh no"'); + + expect(events['@response']).to.exist; + let response = JSON.parse(events['@response'][0]); + expect(response.headers['Content-Type']).to.equal('application/json'); + expect(response.body).to.equal('true'); + done(); }); }); - it('Should successfully return a default parameter after passing in undefined', done => { - request('POST', {}, '/null_default_param/', {name: undefined}, - (err, res, result) => { + it('Streaming endpoints should succeed if contains a valid stream in _debug', done => { + request('POST', {}, '/stream/basic/', {alpha: 'hello', _stream: {}, _debug: {hello: true}}, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(result).to.equal('default'); + expect(res.headers['content-type']).to.equal('text/event-stream'); + expect(res.headers['x-debug']).to.equal('true'); + expect(result).to.exist; + + let events = parseServerSentEvents(result); + expect(Object.keys(events).length).to.equal(3); + + expect(events['@begin']).to.exist; + expect(events['@begin'].length).to.equal(1); + expect(events['@begin'][0]).to.be.a.string; + + expect(events['hello']).to.exist; + expect(events['hello'].length).to.equal(1); + expect(events['hello'][0]).to.equal('true'); + + expect(events['@response']).to.exist; + let response = JSON.parse(events['@response'][0]); + expect(response.headers['Content-Type']).to.equal('application/json'); + expect(response.body).to.equal('true'); + done(); }); }); - it('Should successfully return an object with a schema that has an enum varient', done => { - request( - 'POST', - {}, - '/enum_schema/', - { - before: 'before', - valueRange: { - range: 'a range', - majorDimension: 'ROWS', - values: [] - }, - after: 'after', - }, - (err, res, result) => { + it('Streaming endpoints should fail if contains an invalid stream in _debug', done => { + request('POST', {}, '/stream/basic/', {alpha: 'hello', _stream: {}, _debug: {test: true}}, (err, res, result) => { - expect(err).to.not.exist; - expect(res.statusCode).to.equal(200); - expect(result).to.exist; - expect(result).to.deep.equal({ - range: 'a range', - majorDimension: 'ROWS', - values: [] - }); - done(); + expect(err).to.not.exist; + expect(res.statusCode).to.equal(403); + expect(res.headers['content-type']).to.equal('application/json'); + expect(res.headers['x-debug']).to.equal('true'); + expect(result).to.exist; + expect(result.error).to.exist; + expect(result.error.type).to.equal('DebugError'); + expect(result.error.message).to.contain('"test"'); - } - ); + done(); + + }); }); - it('Should return a default enum varient set to null', done => { - request('POST', {}, '/enum_null/', {}, - (err, res, result) => { + it('Streaming endpoints should fail with StreamError if contains an invalid stream when _debug', done => { + request('POST', {}, '/stream/basic/', {alpha: 'hello', _stream: {test: true}, _debug: true}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(200); - expect(result).to.equal(null); + expect(res.statusCode).to.equal(400); + expect(res.headers['content-type']).to.equal('application/json'); + expect(res.headers['x-debug']).to.equal('true'); + expect(result).to.exist; + expect(result.error).to.exist; + expect(result.error.type).to.equal('StreamListenerError'); + expect(result.error.details).to.haveOwnProperty('test'); + done(); }); }); - it('Should accept keyql params', done => { - request('POST', {}, '/keyql/', { query: { name: 'steve' }, limit: { count: 0, offset: 0 } }, - (err, res, result) => { + it('Should support POST with streaming with _debug to a function with no stream', done => { + request('POST', {}, '/stream/debug_no_stream/', {alpha: 'hello', _debug: true}, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(result).to.equal('hello'); + expect(res.headers['content-type']).to.equal('text/event-stream'); + expect(res.headers['x-debug']).to.equal('true'); + expect(result).to.exist; + + let events = parseServerSentEvents(result); + expect(Object.keys(events).length).to.equal(4); + + expect(events['@begin']).to.exist; + expect(events['@begin'].length).to.equal(1); + expect(events['@begin'][0]).to.be.a.string; + + expect(events['@stdout']).to.exist; + expect(events['@stdout'].length).to.equal(2); + expect(events['@stdout'][0]).to.equal('"what? who?"'); + expect(events['@stdout'][1]).to.equal('"finally"'); + + expect(events['@stderr']).to.exist; + expect(events['@stderr'].length).to.equal(1); + expect(events['@stderr'][0]).to.equal('"oh no"'); + + expect(events['@response']).to.exist; + let response = JSON.parse(events['@response'][0]); + expect(response.headers['Content-Type']).to.equal('application/json'); + expect(response.body).to.equal('true'); + done(); }); }); - it('Should accept keyql params', done => { - let query = JSON.stringify({ name: 'steve' }); - let limit = JSON.stringify({ count: 0, offset: 0 }); - - request('GET', {'x-convert-strings': true}, `/keyql/?query=${query}&limit=${limit}`, '', (err, res, result) => { + it('Endpoint without stream should fail with ExecutionModeError if _debug set and _stream set', done => { + request('POST', {}, '/stream/debug_no_stream/', {alpha: 'hello', _stream: {test: true}, _debug: true}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(200); - expect(result).to.equal('hello'); + expect(res.statusCode).to.equal(403); + expect(res.headers['content-type']).to.equal('application/json'); + expect(res.headers['x-debug']).to.equal('true'); + expect(result).to.exist; + expect(result.error).to.exist; + expect(result.error.type).to.equal('ExecutionModeError'); + expect(result.error.message).to.contain('"stream"'); + done(); }); }); - it('Should reject invalid keyql limit', done => { - request('POST', {}, '/keyql/', { query: { name: 'steve' }, limit: { count: 0, wrong: 0 } }, - (err, res, result) => { + it('Endpoint without stream should fail with ExecutionModeError if _debug not set and _stream set', done => { + request('POST', {}, '/stream/debug_no_stream/', {alpha: 'hello', _stream: {test: true}}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(400); + expect(res.statusCode).to.equal(403); + expect(res.headers['content-type']).to.equal('application/json'); + expect(result).to.exist; expect(result.error).to.exist; - expect(result.error.details).to.exist; - expect(result.error.details.limit).to.exist; + expect(result.error.type).to.equal('ExecutionModeError'); + expect(result.error.message).to.contain('"stream"'); + done(); }); }); - it('Should accept keyql with correct options', done => { - request('POST', {}, '/keyql_options/', {query: {alpha: 'hello'}}, - (err, res, result) => { + it('Endpoint without stream should fail with DebugError if _debug set to an invalid listener', done => { + request('POST', {}, '/stream/debug_no_stream/', {alpha: 'hello', _debug: {test: true}}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(200); - expect(result).to.equal('hello'); + expect(res.statusCode).to.equal(403); + expect(res.headers['content-type']).to.equal('application/json'); + expect(res.headers['x-debug']).to.equal('true'); + expect(result).to.exist; + expect(result.error).to.exist; + expect(result.error.type).to.equal('DebugError'); + expect(result.error.message).to.contain('"test"'); + done(); }); }); - it('Should accept keyql with correct options and an operator', done => { - request('POST', {}, '/keyql_options/', {query: {alpha__is: 'hello'}}, - (err, res, result) => { + it('Endpoint without stream should fail with DebugError if _debug set and _background set', done => { + request('POST', {}, '/stream/debug_no_stream/', {alpha: 'hello', _debug: true, _background: true}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(200); - expect(result).to.equal('hello'); + expect(res.statusCode).to.equal(403); + expect(res.headers['content-type']).to.equal('application/json'); + expect(res.headers['x-debug']).to.equal('true'); + expect(result).to.exist; + expect(result.error).to.exist; + expect(result.error.type).to.equal('DebugError'); + expect(result.error.message).to.equal('Can not debug with "background" mode set'); + done(); }); }); - it('Should reject keyql with correct options with an incorrect operator', done => { - request('POST', {}, '/keyql_options/', {query: {alpha__isnt: 'hello'}}, - (err, res, result) => { + it('Endpoint triggered with request origin "autocode.com" should not work', done => { + request('POST', {'origin': 'autocode.com'}, '/origin/allow/', {alpha: 'hello'}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(400); + expect(res.statusCode).to.equal(403); + expect(res.headers['content-type']).to.equal('application/json'); + expect(res.headers['access-control-allow-origin']).to.equal('!'); + expect(result).to.exist; + expect(result.error).to.exist; + expect(result.error.type).to.equal('OriginError'); + expect(result.error.message).to.contain(`"autocode.com"`) + done(); }); }); - it('Should reject keyql with incorrect options', done => { - request('POST', {}, '/keyql_options/', {query: {gamma: 'hello'}}, - (err, res, result) => { + it('Endpoint triggered with request origin "https://sub.autocode.com" should not work', done => { + request('POST', {'origin': 'https://sub.autocode.com'}, '/origin/allow/', {alpha: 'hello'}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(400); + expect(res.statusCode).to.equal(403); + expect(res.headers['content-type']).to.equal('application/json'); + expect(res.headers['access-control-allow-origin']).to.equal('!'); + expect(result).to.exist; + expect(result.error).to.exist; + expect(result.error.type).to.equal('OriginError'); + expect(result.error.message).to.contain(`"https://sub.autocode.com"`) + done(); }); }); - it('Should reject keyql with incorrect options with an operator', done => { - request('POST', {}, '/keyql_options/', {query: {gamma__is: 'hello'}}, - (err, res, result) => { + it('Endpoint triggered with request origin "http://autocode.com" should work', done => { + request('POST', {'origin': 'http://autocode.com'}, '/origin/allow/', {alpha: 'hello'}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(400); + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/json'); + expect(res.headers['access-control-allow-origin']).to.equal('http://autocode.com'); + expect(result).to.exist; + done(); }); }); - it('Should accept keyql array with correct options', done => { - request('POST', {}, '/keyql_options_array/', {query: [{alpha: 'hello'}]}, - (err, res, result) => { + it('Endpoint triggered with request origin "https://autocode.com" should work', done => { + request('POST', {'origin': 'https://autocode.com'}, '/origin/allow/', {alpha: 'hello'}, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(result).to.equal('hello'); + expect(res.headers['content-type']).to.equal('application/json'); + expect(res.headers['access-control-allow-origin']).to.equal('https://autocode.com'); + expect(result).to.exist; + done(); }); }); - it('Should accept keyql array with correct options and an operator', done => { - request('POST', {}, '/keyql_options_array/', {query: [{alpha__is: 'hello'}]}, - (err, res, result) => { + it('Endpoint triggered with request origin "localhost:8000" should not work', done => { + request('POST', {'origin': 'localhost:8000'}, '/origin/allow/', {alpha: 'hello'}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(200); - expect(result).to.equal('hello'); + expect(res.statusCode).to.equal(403); + expect(res.headers['content-type']).to.equal('application/json'); + expect(res.headers['access-control-allow-origin']).to.equal('!'); + expect(result).to.exist; + expect(result.error).to.exist; + expect(result.error.type).to.equal('OriginError'); + expect(result.error.message).to.contain(`"localhost:8000"`) + done(); }); }); - it('Should reject keyql array with correct options with an incorrect operator', done => { - request('POST', {}, '/keyql_options_array/', {query: [{alpha__isnt: 'hello'}]}, - (err, res, result) => { + it('Endpoint triggered with request origin "http://localhost:8000" should work', done => { + request('POST', {'origin': 'http://localhost:8000'}, '/origin/allow/', {alpha: 'hello'}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(400); + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/json'); + expect(res.headers['access-control-allow-origin']).to.equal('http://localhost:8000'); + expect(result).to.exist; + done(); }); }); - it('Should reject keyql array with incorrect options', done => { - request('POST', {}, '/keyql_options_array/', {query: [{gamma: 'hello'}]}, - (err, res, result) => { + it('Endpoint triggered with request origin "https://localhost:8000" should work', done => { + request('POST', {'origin': 'https://localhost:8000'}, '/origin/allow/', {alpha: 'hello'}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(400); + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/json'); + expect(res.headers['access-control-allow-origin']).to.equal('https://localhost:8000'); + expect(result).to.exist; + done(); }); }); - it('Should reject keyql array with incorrect options with an operator', done => { - request('POST', {}, '/keyql_options_array/', {query: [{gamma__is: 'hello'}]}, - (err, res, result) => { + it('Endpoint triggered with request origin "test.some-url.com:9999" should not work', done => { + request('POST', {'origin': 'test.some-url.com:9999'}, '/origin/allow/', {alpha: 'hello'}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(400); + expect(res.statusCode).to.equal(403); + expect(res.headers['content-type']).to.equal('application/json'); + expect(res.headers['access-control-allow-origin']).to.equal('!'); + expect(result).to.exist; + expect(result.error).to.exist; + expect(result.error.type).to.equal('OriginError'); + expect(result.error.message).to.contain(`"test.some-url.com:9999"`) + done(); }); }); - it('Should return a buffer properly', done => { - request('POST', {}, '/buffer_return/', {}, - (err, res, result) => { + it('Endpoint triggered with request origin "http://test.some-url.com:9999" should work', done => { + request('POST', {'origin': 'http://test.some-url.com:9999'}, '/origin/allow/', {alpha: 'hello'}, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/json'); + expect(res.headers['access-control-allow-origin']).to.equal('http://test.some-url.com:9999'); expect(result).to.exist; - expect(result).to.be.instanceof(Buffer); - expect(result.toString()).to.equal('lol'); + done(); }); }); - it('Should return a nested buffer properly', done => { - request('POST', {}, '/buffer_nested_return/', {}, - (err, res, result) => { + it('Endpoint triggered with request origin "https://test.some-url.com:9999" should work', done => { + request('POST', {'origin': 'https://test.some-url.com:9999'}, '/origin/allow/', {alpha: 'hello'}, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/json'); + expect(res.headers['access-control-allow-origin']).to.equal('https://test.some-url.com:9999'); expect(result).to.exist; - expect(result).to.haveOwnProperty('body'); - expect(result.body).to.haveOwnProperty('_base64'); - expect(Buffer.from(result.body._base64, 'base64').toString()).to.equal('lol'); - expect(result.test).to.exist; - expect(result.test.deep).to.exist; - expect(result.test.deep).to.be.an('array'); - expect(result.test.deep.length).to.equal(3); - expect(result.test.deep[1]).to.haveOwnProperty('_base64'); - expect(Buffer.from(result.test.deep[1]._base64, 'base64').toString()).to.equal('wat'); + done(); }); }); - it('Should return a mocked buffer as if it were a real one', done => { - request('POST', {}, '/buffer_mocked_return/', {}, - (err, res, result) => { + it('Endpoint triggered with request origin "http://hello.com" should not work', done => { + request('POST', {'origin': 'http://hello.com'}, '/origin/allow/', {alpha: 'hello'}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(200); + expect(res.statusCode).to.equal(403); + expect(res.headers['content-type']).to.equal('application/json'); + expect(res.headers['access-control-allow-origin']).to.equal('!'); expect(result).to.exist; - expect(result).to.be.instanceof(Buffer); - expect(result.toString()).to.equal('lol'); + expect(result.error).to.exist; + expect(result.error.type).to.equal('OriginError'); + expect(result.error.message).to.contain(`"http://hello.com"`) + done(); }); }); - it('Should return a nested mocked buffer as if it were a real one', done => { - request('POST', {}, '/buffer_nested_mocked_return/', {}, - (err, res, result) => { + it('Endpoint triggered with request origin "http://hello.com" should not work with _stream', done => { + request('POST', {'origin': 'http://hello.com'}, '/origin/allow/', {alpha: 'hello', _stream: true}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(200); + expect(res.statusCode).to.equal(403); + expect(res.headers['content-type']).to.equal('application/json'); + expect(res.headers['access-control-allow-origin']).to.equal('!'); expect(result).to.exist; - expect(result).to.haveOwnProperty('body'); - expect(result.body).to.haveOwnProperty('_base64'); - expect(Buffer.from(result.body._base64, 'base64').toString()).to.equal('lol'); - expect(result.test).to.exist; - expect(result.test.deep).to.exist; - expect(result.test.deep).to.be.an('array'); - expect(result.test.deep.length).to.equal(3); - expect(result.test.deep[1]).to.haveOwnProperty('_base64'); - expect(Buffer.from(result.test.deep[1]._base64, 'base64').toString()).to.equal('wat'); + expect(result.error).to.exist; + expect(result.error.type).to.equal('OriginError'); + expect(result.error.message).to.contain(`"http://hello.com"`) + done(); }); }); - it('Should return a mocked buffer as if it were a real one, if type "any"', done => { - request('POST', {}, '/buffer_any_return/', {}, - (err, res, result) => { + it('Endpoint triggered with request origin "http://hello.com" should not work with _debug', done => { + request('POST', {'origin': 'http://hello.com'}, '/origin/allow/', {alpha: 'hello', _debug: true}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(200); + expect(res.statusCode).to.equal(403); + expect(res.headers['content-type']).to.equal('application/json'); + expect(res.headers['access-control-allow-origin']).to.equal('!'); expect(result).to.exist; - expect(result).to.be.instanceof(Buffer); - expect(result.toString()).to.equal('lol'); + expect(result.error).to.exist; + expect(result.error.type).to.equal('OriginError'); + expect(result.error.message).to.contain(`"http://hello.com"`) + done(); }); }); - it('Should return a nested mocked buffer as if it were a real one, if type "any"', done => { - request('POST', {}, '/buffer_nested_any_return/', {}, - (err, res, result) => { + it('Endpoint triggered with request origin "http://hello.com" should not work with _background', done => { + request('POST', {'origin': 'http://hello.com'}, '/origin/allow/', {alpha: 'hello', _background: true}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(200); + expect(res.statusCode).to.equal(403); + expect(res.headers['content-type']).to.equal('application/json'); + expect(res.headers['access-control-allow-origin']).to.equal('!'); expect(result).to.exist; - expect(result).to.haveOwnProperty('body'); - expect(result.body).to.haveOwnProperty('_base64'); - expect(Buffer.from(result.body._base64, 'base64').toString()).to.equal('lol'); - expect(result.test).to.exist; - expect(result.test.deep).to.exist; - expect(result.test.deep).to.be.an('array'); - expect(result.test.deep.length).to.equal(3); - expect(result.test.deep[1]).to.haveOwnProperty('_base64'); - expect(Buffer.from(result.test.deep[1]._base64, 'base64').toString()).to.equal('wat'); + expect(result.error).to.exist; + expect(result.error.type).to.equal('OriginError'); + expect(result.error.message).to.contain(`"http://hello.com"`) + done(); }); }); - it('Should throw an ValueError on an invalid Buffer type', done => { - request('POST', {}, '/value_error/buffer_invalid/', {}, - (err, res, result) => { + it('Endpoint triggered with request origin "https://hello.com" should work', done => { + request('POST', {'origin': 'https://hello.com'}, '/origin/allow/', {alpha: 'hello'}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(502); + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/json'); + expect(res.headers['access-control-allow-origin']).to.equal('https://hello.com'); expect(result).to.exist; + done(); }); }); - it('Should throw an ValueError on an invalid Number type', done => { - request('POST', {}, '/value_error/number_invalid/', {}, - (err, res, result) => { + it('Endpoint triggered with request origin "https://hello.com" should work with _stream', done => { + request('POST', {'origin': 'https://hello.com'}, '/origin/allow/', {alpha: 'hello', _stream: true}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(502); + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('text/event-stream'); + expect(res.headers['access-control-allow-origin']).to.equal('https://hello.com'); expect(result).to.exist; + done(); }); }); - it('Should not populate "context.keys" with no authorization keys header provided', done => { - request('POST', {}, '/keys/', {}, - (err, res, result) => { + it('Endpoint triggered with request origin "https://hello.com" should work with _debug', done => { + request('POST', {'origin': 'https://hello.com'}, '/origin/allow/', {alpha: 'hello', _debug: true}, (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('text/event-stream'); + expect(res.headers['access-control-allow-origin']).to.equal('https://hello.com'); expect(result).to.exist; - expect(result).to.deep.equal({ - TEST_KEY: null, - ANOTHER_KEY: null, - A_THIRD_KEY: null - }); + done(); }); }); - it('Should not populate "context.keys" if the authorization keys header is not a serialized object', done => { - request('POST', { - 'X-Authorization-Keys': 'stringvalue' - }, '/keys/', {}, - (err, res, result) => { + it('Endpoint triggered with request origin "https://hello.com" should work with _background', done => { + request('POST', {'origin': 'https://hello.com'}, '/origin/allow/', {alpha: 'hello', _background: true}, (err, res, result) => { expect(err).to.not.exist; - expect(res.statusCode).to.equal(200); + expect(res.statusCode).to.equal(202); + expect(res.headers['content-type']).to.equal('text/plain'); + expect(res.headers['access-control-allow-origin']).to.equal('https://hello.com'); expect(result).to.exist; - expect(result).to.deep.equal({ - TEST_KEY: null, - ANOTHER_KEY: null, - A_THIRD_KEY: null - }); + done(); }); }); - it('Should populate "context.keys" with only the proper keys', done => { - request('POST', { - 'X-Authorization-Keys': JSON.stringify({ - TEST_KEY: '123', - ANOTHER_KEY: 'abc', - UNSPECIFIED_KEY: '987' - }) - }, '/keys/', {}, - (err, res, result) => { + it('Should return a valid /.well-known/ai-plugin.json', done => { + request('GET', {}, '/.well-known/ai-plugin.json', '', (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); expect(result).to.exist; - expect(result).to.deep.equal({ - TEST_KEY: '123', - ANOTHER_KEY: 'abc', - A_THIRD_KEY: null - }); + expect(result.schema_version).to.equal('v1'); + expect(result.name_for_human).to.equal('(No name provided)'); + expect(result.name_for_model).to.equal('No_name_provided'); + expect(result.description_for_human).to.equal('(No description provided)'); + expect(result.description_for_model).to.equal('(No description provided)'); + expect(result.api).to.exist; + expect(result.api.type).to.equal('openapi'); + expect(result.api.url).to.equal('localhost/.well-known/openapi.yaml'); done(); }); }); - it('Should not populate "context.providers" with no authorization providers header provided', done => { - request('POST', {}, '/context/', {}, - (err, res, result) => { + it('Should return a valid /.well-known/openapi.json', done => { + request('GET', {}, '/.well-known/openapi.json', '', (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); expect(result).to.exist; - expect(result.providers).to.deep.equal({}); + expect(result.openapi).to.equal('3.1.0'); + expect(result.info).to.exist; + expect(result.info.version).to.equal('local'); + expect(result.info.title).to.equal('(No name provided)'); + expect(result.info.description).to.equal('(No description provided)'); + expect(result.servers).to.be.an('Array'); + expect(result.servers[0]).to.exist; + expect(result.servers[0].url).to.equal('localhost'); + expect(result.servers[0].description).to.equal('FunctionScript Gateway'); + expect(result.paths).to.be.an('Object'); + expect(result.paths['/my_function/']).to.exist; + expect(result.paths['/my_function/'].post).to.exist; + expect(result.paths['/my_function/'].post.description).to.equal('My function'); + expect(result.paths['/my_function/'].post.operationId).to.equal('service_localhost_my_function'); + expect(result.paths['/my_function/'].post.requestBody).to.exist; + expect(result.paths['/my_function/'].post.requestBody.content).to.exist; + expect(result.paths['/my_function/'].post.requestBody.content['application/json']).to.exist; + expect(result.paths['/my_function/'].post.requestBody.content['application/json']).to.deep.equal({ + "schema": { + "type": "object", + "properties": { + "a": { + "type": "number", + "default": 1 + }, + "b": { + "type": "number", + "default": 2 + }, + "c": { + "type": "number", + "default": 3 + } + } + } + }); + expect(result.paths['/my_function/'].post.responses).to.exist; + expect(result.paths['/my_function/'].post.responses['200']).to.exist; + expect(result.paths['/my_function/'].post.responses['200'].content).to.exist; + expect(result.paths['/my_function/'].post.responses['200'].content['application/json']).to.exist; + expect(result.paths['/my_function/'].post.responses['200'].content['application/json']).to.deep.equal({ + "schema": { + "type": "number" + } + }); + expect(result.paths['/a_standard_function/']).to.exist; + expect(result.paths['/reflect/']).to.exist; done(); }); }); - it('Should not populate "context.providers" if the authorization providers header is not an serialized object', done => { - request('POST', { - 'X-Authorization-Providers': 'stringvalue' - }, '/context/', {}, - (err, res, result) => { + it('Should return a valid /.well-known/openapi.yaml', done => { + request('GET', {}, '/.well-known/openapi.yaml', '', (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); - expect(result).to.exist; - expect(result.providers).to.deep.equal({}); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(res.headers['content-type']).to.equal('application/yaml'); + let yaml = result.toString(); + expect(yaml.startsWith('openapi: "3.1.0"')).to.equal(true); done(); }); }); - it('Should populate "context.providers" as the value of the authorization providers header if it is a serialized object', done => { - let headerValue = { - test: { - item: 'value' - } - }; - request('POST', { - 'X-Authorization-Providers': JSON.stringify(headerValue) - }, '/context/', {}, - (err, res, result) => { + it('Should return a valid /.well-known/schema.json', done => { + request('GET', {}, '/.well-known/schema.json', '', (err, res, result) => { expect(err).to.not.exist; expect(res.statusCode).to.equal(200); + expect(res.headers).to.haveOwnProperty('access-control-allow-origin'); + expect(res.headers).to.haveOwnProperty('access-control-allow-headers'); + expect(res.headers).to.haveOwnProperty('access-control-expose-headers'); + expect(res.headers['content-type']).to.equal('application/json'); expect(result).to.exist; - expect(result.providers).to.deep.equal(headerValue); + expect(result.functions).to.exist; + expect(result.functions.length).to.be.greaterThan(1); done(); }); diff --git a/tests/gateway/www/compile/test-sass-error.scss b/tests/gateway/www/compile/test-sass-error.scss new file mode 100644 index 0000000..ac62b3d --- /dev/null +++ b/tests/gateway/www/compile/test-sass-error.scss @@ -0,0 +1,5 @@ +body { + div { + font-family: + } +} diff --git a/tests/gateway/www/compile/test-sass.scss b/tests/gateway/www/compile/test-sass.scss new file mode 100644 index 0000000..dd4144f --- /dev/null +++ b/tests/gateway/www/compile/test-sass.scss @@ -0,0 +1,5 @@ +body { + div { + font-family: Arial; + } +} diff --git a/tests/gateway/www/error/404.html b/tests/gateway/www/error/404.html new file mode 100644 index 0000000..568430b --- /dev/null +++ b/tests/gateway/www/error/404.html @@ -0,0 +1 @@ +error 404 diff --git a/tests/gateway/www/fs-wordmark.png b/tests/gateway/www/fs-wordmark.png new file mode 100644 index 0000000..5d58a44 Binary files /dev/null and b/tests/gateway/www/fs-wordmark.png differ diff --git a/tests/gateway/www/page.html b/tests/gateway/www/page.html new file mode 100644 index 0000000..e2ad6e4 --- /dev/null +++ b/tests/gateway/www/page.html @@ -0,0 +1 @@ +this is an html file diff --git a/tests/gateway/www/page2.htm b/tests/gateway/www/page2.htm new file mode 100644 index 0000000..cafe103 --- /dev/null +++ b/tests/gateway/www/page2.htm @@ -0,0 +1 @@ +this is an htm file diff --git a/tests/gateway/www/static-test/htm/index.htm b/tests/gateway/www/static-test/htm/index.htm new file mode 100644 index 0000000..18ed0fd --- /dev/null +++ b/tests/gateway/www/static-test/htm/index.htm @@ -0,0 +1 @@ +this is an index.htm file diff --git a/tests/gateway/www/static-test/index.html b/tests/gateway/www/static-test/index.html new file mode 100644 index 0000000..669ffa8 --- /dev/null +++ b/tests/gateway/www/static-test/index.html @@ -0,0 +1 @@ +this is an index.html file diff --git a/tests/gateway/www/video.mp4 b/tests/gateway/www/video.mp4 new file mode 100644 index 0000000..5b621bd Binary files /dev/null and b/tests/gateway/www/video.mp4 differ diff --git a/tests/runner.js b/tests/runner.js index a104cbc..d838b91 100644 --- a/tests/runner.js +++ b/tests/runner.js @@ -37,6 +37,7 @@ describe('LibDoc', () => { try { parser.parseDefinition(functionCase.pathname, functionCase.buffer); } catch (e) { + // console.log(e.message); err = e; } @@ -53,11 +54,11 @@ describe('LibDoc', () => { describe('Comprehensive Test', () => { let definitions = parser.load('./tests/files/comprehensive'); - let ignoredDefinitions = parser.load('./tests/files/ignore', null, ['ignoreme.js']); + let ignoredDefinitions = parser.load('./tests/files/ignore', null, null, ['ignoreme.js']); it('Should read all functions correctly', () => { - expect(Object.keys(definitions).length).to.equal(22); + expect(Object.keys(definitions).length).to.equal(25); expect(definitions).to.haveOwnProperty(''); expect(definitions).to.haveOwnProperty('test'); expect(definitions).to.haveOwnProperty('returns'); @@ -73,8 +74,11 @@ describe('LibDoc', () => { expect(definitions).to.haveOwnProperty('enum'); expect(definitions).to.haveOwnProperty('enum_return'); expect(definitions).to.haveOwnProperty('enum_nested'); + expect(definitions).to.haveOwnProperty('enum_nested_optional'); expect(definitions).to.haveOwnProperty('options'); expect(definitions).to.haveOwnProperty('keyql_options'); + expect(definitions).to.haveOwnProperty('alternate_schemas'); + expect(definitions).to.haveOwnProperty('inline'); }); @@ -102,7 +106,9 @@ describe('LibDoc', () => { expect(definitions['enum'].pathname).to.equal('enum.js'); expect(definitions['enum_return'].pathname).to.equal('enum_return.js'); expect(definitions['enum_nested'].pathname).to.equal('enum_nested.js'); + expect(definitions['enum_nested_optional'].pathname).to.equal('enum_nested_optional.js'); expect(definitions['options'].pathname).to.equal('options.js'); + expect(definitions['inline'].pathname).to.equal('inline.js'); }); @@ -123,7 +129,9 @@ describe('LibDoc', () => { expect(definitions['enum'].description).to.equal('Test Enum'); expect(definitions['enum_return'].description).to.equal('Test Enum Returns'); expect(definitions['enum_nested'].description).to.equal('Test Nested Enum'); + expect(definitions['enum_nested_optional'].description).to.equal('Test Optional Nested Enum'); expect(definitions['options'].description).to.equal('Populate options properly'); + expect(definitions['inline'].description).to.equal(''); }); @@ -144,7 +152,10 @@ describe('LibDoc', () => { expect(definitions['enum'].context).to.equal(null); expect(definitions['enum_return'].context).to.exist; expect(definitions['enum_nested'].context).to.exist; + expect(definitions['enum_nested_optional'].context).to.exist; expect(definitions['options'].context).to.equal(null); + expect(definitions['inline'].context).to.exist; + }); @@ -164,7 +175,9 @@ describe('LibDoc', () => { expect(definitions['enum'].returns.description).to.equal(''); expect(definitions['enum_return'].returns.description).to.equal('a or b'); expect(definitions['enum_nested'].returns.description).to.equal('A boolean value'); + expect(definitions['enum_nested_optional'].returns.description).to.equal('A boolean value'); expect(definitions['options'].returns.description).to.equal('a Boolean?'); + expect(definitions['inline'].returns.description).to.equal(''); }); @@ -184,7 +197,9 @@ describe('LibDoc', () => { expect(definitions['enum'].returns.type).to.equal('any'); expect(definitions['enum_return'].returns.type).to.equal('enum'); expect(definitions['enum_nested'].returns.type).to.equal('boolean'); + expect(definitions['enum_nested'].returns.type).to.equal('boolean'); expect(definitions['options'].returns.type).to.equal('boolean'); + expect(definitions['inline'].returns.type).to.equal('any'); }); @@ -205,7 +220,9 @@ describe('LibDoc', () => { expect(definitions['enum'].charge).to.equal(1); expect(definitions['enum_return'].charge).to.equal(1); expect(definitions['enum_nested'].charge).to.equal(1); + expect(definitions['enum_nested_optional'].charge).to.equal(1); expect(definitions['options'].charge).to.equal(1); + expect(definitions['inline'].charge).to.equal(1); }); @@ -243,8 +260,11 @@ describe('LibDoc', () => { expect(definitions['enum_return'].keys).to.have.length(0); expect(definitions['enum_nested'].keys).to.be.an('Array'); expect(definitions['enum_nested'].keys).to.have.length(0); + expect(definitions['enum_nested_optional'].keys).to.have.an('Array'); + expect(definitions['enum_nested_optional'].keys).to.have.length(0); expect(definitions['options'].keys).to.be.an('Array'); expect(definitions['options'].keys).to.have.length(0); + expect(definitions['inline'].keys).to.have.length(0); }); @@ -599,6 +619,13 @@ describe('LibDoc', () => { }); + it('Should read "inline" parameters', () => { + + let params = definitions['inline'].params; + expect(params.length).to.equal(0); + + }); + it('Should have a named return value and description', () => { let definition = definitions['named_return']; @@ -712,6 +739,277 @@ describe('LibDoc', () => { ] } ] + }, + { + name: 'obj2', + type: 'object', + description: '', + schema: [ + { + name: 'operator', + type: 'enum', + description: 'Which data to retrieve: can be "text", "html" or "attr"', + members: [['text', 'text'], ['html', 'html'], ['attr', 'attr']] + }, + { + name: 'selector', + type: 'string', + description: 'The selector to query' + }, + { + name: 'attr', + type: 'string', + defaultValue: null, + description: 'If method is "attr", which attribute to retrieve' + } + ] + }, + { + name: 'arr2', + type: 'array', + description: '', + schema: [ + { + name: 'obj', + type: 'object', + description: '', + schema: [ + { + name: 'operator', + type: 'enum', + description: 'Which data to retrieve: can be "text", "html" or "attr"', + members: [['text', 'text'], ['html', 'html'], ['attr', 'attr']] + }, + { + name: 'selector', + type: 'string', + description: 'The selector to query' + }, + { + name: 'attr', + type: 'string', + defaultValue: null, + description: 'If method is "attr", which attribute to retrieve' + } + ] + } + ] + } + ]); + + }); + + it('Should read "enum_nested_optional" parameters', () => { + + let params = definitions['enum_nested_optional'].params; + + expect(params).to.deep.equal([ + { + name: 'descriptionHtml', + type: 'string', + defaultValue: null, + description: 'The description of the product, complete with HTML formatting.' + }, + { + name: 'metafields', + type: 'array', + defaultValue: null, + description: 'The metafields to associate with this product.', + schema: [ + { + name: 'MetafieldInput', + type: 'object', + description: 'Specifies the input fields for a metafield.', + schema: [ + { + name: 'value', + type: 'string', + description: 'The value of a metafield.', + defaultValue: null + }, + { + name: 'valueType', + type: 'enum', + description: 'Metafield value types.', + members: [['STRING', 'STRING'], ['INTEGER', 'INTEGER'], ['JSON_STRING', 'JSON_STRING']], + defaultValue: null + } + ] + } + ] + }, + { + name: 'privateMetafields', + type: 'array', + description: 'The private metafields to associated with this product.', + defaultValue: null, + schema: [ + { + name: 'PrivateMetafieldInput', + type: 'object', + description: 'Specifies the input fields for a PrivateMetafield.', + schema: [ + { + name: 'owner', + type: 'any', + description: 'The owning resource.', + defaultValue: null + }, + { + name: 'valueInput', + type: 'object', + description: 'The value and value type of the metafield, wrapped in a ValueInput object.', + schema: [ + { + name: 'value', + type: 'string', + description: 'The value of a private metafield.' + }, + { + name: 'valueType', + type: 'enum', + description: 'Private Metafield value types.', + members: [['STRING', 'STRING'], ['INTEGER', 'INTEGER'], ['JSON_STRING', 'JSON_STRING']] + } + ] + } + ] + } + ] + }, + { + name: 'variants', + type: 'array', + description: 'A list of variants associated with the product.', + defaultValue: null, + schema: [ + { + name: 'ProductVariantInput', + type: 'object', + description: 'Specifies a product variant to create or update.', + schema: [ + { + name: 'barcode', + type: 'string', + description: 'The value of the barcode associated with the product.', + defaultValue: null + }, + { + name: 'inventoryPolicy', + type: 'enum', + description: 'The inventory policy for a product variant controls whether customers can continue to buy the variant when it is out of stock. When the value is `continue`, customers are able to buy the variant when it\'s out of stock. When the value is `deny`, customers can\'t buy the variant when it\'s out of stock.', + members: [['DENY', 'DENY'], ['CONTINUE', 'CONTINUE']], + defaultValue: null + }, + { + name: 'metafields', + type: 'array', + description: 'Additional customizable information about the product variant.', + defaultValue: null, + schema: [ + { + name: 'MetafieldInput', + type: 'object', + description: 'Specifies the input fields for a metafield.', + schema: [ + { + name: 'description', + type: 'string', + description: 'The description of the metafield .', + defaultValue: null + }, + { + name: 'valueType', + type: 'enum', + description: 'Metafield value types.', + members: [['STRING', 'STRING'], ['INTEGER', 'INTEGER'], ['JSON_STRING', 'JSON_STRING']], + defaultValue: null + } + ] + } + ] + }, + { + name: 'privateMetafields', + type: 'array', + description: 'The private metafields to associated with this product.', + defaultValue: null, + schema: [ + { + name: 'PrivateMetafieldInput', + type: 'object', + description: 'Specifies the input fields for a PrivateMetafield.', + schema: [ + { + name: 'owner', + type: 'any', + description: 'The owning resource.', + defaultValue: null + }, + { + name: 'valueInput', + type: 'object', + description: 'The value and value type of the metafield, wrapped in a ValueInput object.', + schema: [ + { + name: 'value', + type: 'string', + description: 'The value of a private metafield.' + }, + { + name: 'valueType', + type: 'enum', + description: 'Private Metafield value types.', + members: [['STRING', 'STRING'], ['INTEGER', 'INTEGER'], ['JSON_STRING', 'JSON_STRING']] + } + ] + } + ] + } + ] + }, + { + name: 'taxCode', + type: 'string', + description: 'The tax code associated with the variant.', + defaultValue: null + }, + { + name: 'weightUnit', + type: 'enum', + description: 'Units of measurement for weight.', + members: [['KILOGRAMS', 'KILOGRAMS'], ["GRAMS", "GRAMS"], ["POUNDS", "POUNDS"], ["OUNCES", "OUNCES"]], + defaultValue: null + } + ] + } + ] + }, + { + name: 'media', + type: 'array', + description: 'List of new media to be added to the product.', + defaultValue: null, + schema: [ + { + name: 'CreateMediaInput', + type: 'object', + description: 'Specifies the input fields required to create a media object.', + schema: [ + { + name: 'originalSource', + type: 'string', + description: 'The original source of the media object. May be an external URL or signed upload URL.' + }, + { + name: 'mediaContentType', + type: 'enum', + description: 'The possible content types for a media object.', + members: [['VIDEO', 'VIDEO'], ['EXTERNAL_VIDEO', 'EXTERNAL_VIDEO'], ['MODEL_3D', 'MODEL_3D'], ['IMAGE', 'IMAGE']] + } + ] + } + ] } ]); @@ -791,6 +1089,70 @@ describe('LibDoc', () => { }); + it('Should read "alternate_schemas" parameters', () => { + + let params = definitions['alternate_schemas'].params; + let returns = definitions['alternate_schemas'].returns; + let schemaCheck = [ + { + name: 'fileOrFolder', + description: '', + type: 'object', + schema: [ + { + name: 'name', + description: '', + type: 'string' + }, + { + name: 'size', + description: '', + type: 'integer' + } + ], + alternateSchemas: [ + [ + { + name: 'name', + description: '', + type: 'string' + }, + { + name: 'files', + description: '', + type: 'array' + }, + { + name: 'options', + description: '', + type: 'object', + schema: [ + { + name: 'type', + description: '', + type: 'string' + } + ], + alternateSchemas: [ + [ + { + name: 'type', + description: '', + type: 'number' + } + ] + ] + } + ] + ] + } + ]; + + expect(params).to.deep.equal(schemaCheck); + expect(returns).to.deep.equal(schemaCheck[0]); + + }); + }); describe('Types', () => { @@ -980,33 +1342,37 @@ describe('LibDoc', () => { expect( types.validate('object', { offset: '0 minutes' }, false, [ - { - name: 'offset', - type: 'enum', - description: `How many minutes past the start of each hour you would like your API to execute`, - members: [ - ['0 minutes', 0], - ['15 minutes', 60 * 15], - ['30 minutes', 60 * 30], - ['45 minutes', 60 * 45] - ] - } + [ + { + name: 'offset', + type: 'enum', + description: `How many minutes past the start of each hour you would like your API to execute`, + members: [ + ['0 minutes', 0], + ['15 minutes', 60 * 15], + ['30 minutes', 60 * 30], + ['45 minutes', 60 * 45] + ] + } + ] ]) ).to.equal(true); expect( types.validate('object', { offset: '0 min' }, false, [ - { - name: 'offset', - type: 'enum', - description: `How many minutes past the start of each hour you would like your API to execute`, - members: [ - ['0 minutes', 0], - ['15 minutes', 60 * 15], - ['30 minutes', 60 * 30], - ['45 minutes', 60 * 45] - ] - } + [ + { + name: 'offset', + type: 'enum', + description: `How many minutes past the start of each hour you would like your API to execute`, + members: [ + ['0 minutes', 0], + ['15 minutes', 60 * 15], + ['30 minutes', 60 * 30], + ['45 minutes', 60 * 45] + ] + } + ] ]) ).to.equal(false); @@ -1016,38 +1382,49 @@ describe('LibDoc', () => { expect( types.validate('array', ['0 minutes'], false, [ - { - name: 'offset', - type: 'enum', - description: `How many minutes past the start of each hour you would like your API to execute`, - members: [ - ['0 minutes', 0], - ['15 minutes', 60 * 15], - ['30 minutes', 60 * 30], - ['45 minutes', 60 * 45] - ] - } + [ + { + name: 'offset', + type: 'enum', + description: `How many minutes past the start of each hour you would like your API to execute`, + members: [ + ['0 minutes', 0], + ['15 minutes', 60 * 15], + ['30 minutes', 60 * 30], + ['45 minutes', 60 * 45] + ] + } + ] ]) ).to.equal(true); expect( types.validate('array', ['0 min'], false, [ - { - name: 'offset', - type: 'enum', - description: `How many minutes past the start of each hour you would like your API to execute`, - members: [ - ['0 minutes', 0], - ['15 minutes', 60 * 15], - ['30 minutes', 60 * 30], - ['45 minutes', 60 * 45] - ] - } + [ + { + name: 'offset', + type: 'enum', + description: `How many minutes past the start of each hour you would like your API to execute`, + members: [ + ['0 minutes', 0], + ['15 minutes', 60 * 15], + ['30 minutes', 60 * 30], + ['45 minutes', 60 * 45] + ] + } + ] ]) ).to.equal(false); }); + it('should throw on invalid schema type', () => { + const throws = () => { + types.validate('object', {}, false, {}) + } + expect(throws).to.throw(/Array/) + }) + it('should validate "object" with schema', () => { expect(types.validate('object', {})).to.equal(true); @@ -1057,7 +1434,9 @@ describe('LibDoc', () => { {}, false, [ - {name: 'hello', type: 'string'} + [ + {name: 'hello', type: 'string'} + ] ] ) ).to.equal(false); @@ -1069,7 +1448,9 @@ describe('LibDoc', () => { }, false, [ - {name: 'hello', type: 'string'} + [ + {name: 'hello', type: 'string'} + ] ] ) ).to.equal(true); @@ -1088,7 +1469,7 @@ describe('LibDoc', () => { 'object', {}, false, - testSchema + [testSchema] ) ).to.equal(false); expect( @@ -1098,7 +1479,7 @@ describe('LibDoc', () => { hello: 'hey', }, false, - testSchema + [testSchema] ) ).to.equal(false); expect( @@ -1110,7 +1491,7 @@ describe('LibDoc', () => { tf: true }, false, - testSchema + [testSchema] ) ).to.equal(true); expect( @@ -1122,7 +1503,7 @@ describe('LibDoc', () => { tf: true }, false, - testSchema + [testSchema] ) ).to.equal(false); @@ -1148,7 +1529,7 @@ describe('LibDoc', () => { expect( types.validate('object.keyql.limit', { offset: 0, - limit: 0 + count: 0 }) ).to.equal(true); @@ -1458,6 +1839,113 @@ describe('LibDoc', () => { }); + it('Should generateSchema for javascript', () => { + + try { + types.generateSchema.javascript([1, 2, null, 4]); + } catch (e) { + expect(e).to.exist; + expect(e.message).to.contain('from an object'); + } + + let result; + + result = types.generateSchema.javascript({list: [1, 2, 3, 4]}); + expect(result).to.equal([ + ` * @param {array} list`, + ` * @ {number}` + ].join('\n')); + + result = types.generateSchema.javascript({list: [1, 2, null, 4]}); + expect(result).to.equal([ + ` * @param {array} list`, + ` * @ {?number}` + ].join('\n')); + + result = types.generateSchema.javascript({list: [null, null, 1, null, 2, 3]}); + expect(result).to.equal([ + ` * @param {array} list`, + ` * @ {?number}` + ].join('\n')); + + result = types.generateSchema.javascript({list: [null]}); + expect(result).to.equal([ + ` * @param {array} list`, + ` * @ {?any}` + ].join('\n')); + + result = types.generateSchema.javascript({list: [1, 'two', null, 4]}); + expect(result).to.equal([ + ` * @param {array} list`, + ` * @ {?any}` + ].join('\n')); + + result = types.generateSchema.javascript({ + nested: ['one', 2, 3, null], + nestedObjects: [{ + one: 'one', + two: 2, + three: 3, + four: 4, + five: 'five' + }, { + one: null, + two: null, + three: null, + four: 44, + five: '5ive' + }, { + one: 'uno', + two: 2, + three: 'three', + four: 444 + }] + }); + expect(result).to.deep.equal([ + ` * @param {array} nested`, + ` * @ {?any}`, + ` * @param {array} nestedObjects`, + ` * @ {object}`, + ` * @ {?any} one`, + ` * @ {?any} two`, + ` * @ {?any} three`, + ` * @ {number} four` + ].join('\n')); + + result = types.generateSchema.javascript({ + nested: ['one', 2, 3, null], + nestedObjects: [{ + one: 'one', + two: 2, + three: 3, + four: 4, + five: 'five' + }, { + one: null, + two: null, + three: null, + four: 44, + five: '5ive' + }, { + one: 'uno', + two: 2, + three: 'three', + four: 444 + }] + }, 1); + expect(result).to.deep.equal([ + ` * @ {array} nested`, + ` * @ {?any}`, + ` * @ {array} nestedObjects`, + ` * @ {object}`, + ` * @ {?any} one`, + ` * @ {?any} two`, + ` * @ {?any} three`, + ` * @ {number} four` + ].join('\n')); + + }); + it('Should parse valid Node.js variable names', () => { expect(NodeJsFunctionParser.validateFunctionParamName('test')).to.equal(true);