+
-### Web and services application framework
+# @hapi/hapi
-
+#### The Simple, Secure Framework Developers Trust
-Lead Maintainer: [Eran Hammer](https://github.com/hueniverse)
+Build powerful, scalable applications, with minimal overhead and full out-of-the-box functionality - your code, your way.
-**hapi** is sponsored by [Sideway](https://sideway.com).
+### Visit the [hapi.dev](https://hapi.dev) Developer Portal for tutorials, documentation, and support
-**hapi** is a simple to use configuration-centric framework with built-in support for input validation, caching,
-authentication, and other essential facilities for building web and services applications. **hapi** enables
-developers to focus on writing reusable application logic in a highly modular and prescriptive approach.
+## Useful resources
-Development version: **10.0.x** ([release notes](https://github.com/hapijs/hapi/issues?labels=release+notes&page=1&state=closed))
-[](http://travis-ci.org/hapijs/hapi)
+- [Documentation and API](https://hapi.dev/)
+- [Version status](https://hapi.dev/resources/status/#hapi) (builds, dependencies, node versions, licenses, eol)
+- [Changelog](https://hapi.dev/resources/changelog/)
+- [Project policies](https://hapi.dev/policies/)
+- [Support](https://hapi.dev/support/)
-For the latest updates, [change log](http://hapijs.com/updates), and release information visit [hapijs.com](http://hapijs.com) and follow [@hapijs](https://twitter.com/hapijs) on twitter. If you have questions, please open an issue in the
-[discussion forum](https://github.com/hapijs/discuss).
+## Technical Steering Committee (TSC) Members
+
+ - Devin Ivy ([@devinivy](https://github.com/devinivy))
+ - Lloyd Benson ([@lloydbenson](https://github.com/lloydbenson))
+ - Nathan LaFreniere ([@nlf](https://github.com/nlf))
+ - Wyatt Lyon Preul ([@geek](https://github.com/geek))
+ - Nicolas Morel ([@marsup](https://github.com/marsup))
+ - Jonathan Samines ([@jonathansamines](https://github.com/jonathansamines))
diff --git a/SPONSORS.md b/SPONSORS.md
new file mode 100755
index 000000000..3a1841fdf
--- /dev/null
+++ b/SPONSORS.md
@@ -0,0 +1,15 @@
+We'd like to thank our sponsors as well as the legacy sponsors who have supported hapi throughout the years. Thanks so much for your support!
+
+> Below are hapi's top recurring sponsors, but there are many more to thank. For the complete list, see [hapi.dev/policies/sponsors](https://hapi.dev/policies/sponsors/) or [hapijs/.github/SPONSORS.md](https://github.com/hapijs/.github/blob/master/SPONSORS.md).
+
+# Staff Sponsors
+
+- [Big Room Studios](https://www.bigroomstudios.com/)
+- [Dixeed](https://dixeed.com/)
+
+# Top Sponsors
+
+- Fabian Gündel / [DataWrapper.de](https://www.datawrapper.de/)
+- Devin Stewart
+- [Raider.IO](https://raider.io/)
+- [Florence Healthcare](https://florencehc.com/)
diff --git a/images/hapi.png b/images/hapi.png
deleted file mode 100755
index 92938ede7..000000000
Binary files a/images/hapi.png and /dev/null differ
diff --git a/lib/auth.js b/lib/auth.js
index 5c8fa84b4..e3de0fc7f 100755
--- a/lib/auth.js
+++ b/lib/auth.js
@@ -1,414 +1,571 @@
-// Load modules
+'use strict';
-var Boom = require('boom');
-var Hoek = require('hoek');
-var Schema = require('./schema');
+const Boom = require('@hapi/boom');
+const Bounce = require('@hapi/bounce');
+const Hoek = require('@hapi/hoek');
+const Config = require('./config');
+const Request = require('./request');
-// Declare internals
-var internals = {};
+const internals = {
+ missing: Symbol('missing')
+};
+
+exports = module.exports = internals.Auth = class {
-exports = module.exports = internals.Auth = function (connection) {
+ #core = null;
+ #schemes = {};
+ #strategies = {};
- this.connection = connection;
- this._schemes = {};
- this._strategies = {};
- this.settings = {
+ api = {}; // Do not reassign api or settings, as they are referenced in public()
+ settings = {
default: null // Strategy used as default if route has no auth settings
};
-};
+ constructor(core) {
-internals.Auth.prototype.scheme = function (name, scheme) {
+ this.#core = core;
+ }
- Hoek.assert(name, 'Authentication scheme must have a name');
- Hoek.assert(!this._schemes[name], 'Authentication scheme name already exists:', name);
- Hoek.assert(typeof scheme === 'function', 'scheme must be a function:', name);
+ public(server) {
+
+ return {
+ api: this.api,
+ settings: this.settings,
+ scheme: this.scheme.bind(this),
+ strategy: this._strategy.bind(this, server),
+ default: this.default.bind(this),
+ test: this.test.bind(this),
+ verify: this.verify.bind(this),
+ lookup: this.lookup.bind(this)
+ };
+ }
- this._schemes[name] = scheme;
-};
+ scheme(name, scheme) {
+ Hoek.assert(name, 'Authentication scheme must have a name');
+ Hoek.assert(!this.#schemes[name], 'Authentication scheme name already exists:', name);
+ Hoek.assert(typeof scheme === 'function', 'scheme must be a function:', name);
-internals.Auth.prototype.strategy = function (name, scheme /*, mode, options */) {
+ this.#schemes[name] = scheme;
+ }
- var hasMode = (typeof arguments[2] === 'string' || typeof arguments[2] === 'boolean');
- var mode = (hasMode ? arguments[2] : false);
- var options = (hasMode ? arguments[3] : arguments[2]) || null;
+ _strategy(server, name, scheme, options = {}) {
- Hoek.assert(name, 'Authentication strategy must have a name');
- Hoek.assert(name !== 'bypass', 'Cannot use reserved strategy name: bypass');
- Hoek.assert(!this._strategies[name], 'Authentication strategy name already exists');
- Hoek.assert(scheme, 'Authentication strategy', name, 'missing scheme');
- Hoek.assert(this._schemes[scheme], 'Authentication strategy', name, 'uses unknown scheme:', scheme);
+ Hoek.assert(name, 'Authentication strategy must have a name');
+ Hoek.assert(typeof options === 'object', 'options must be an object');
+ Hoek.assert(!this.#strategies[name], 'Authentication strategy name already exists');
+ Hoek.assert(scheme, 'Authentication strategy', name, 'missing scheme');
+ Hoek.assert(this.#schemes[scheme], 'Authentication strategy', name, 'uses unknown scheme:', scheme);
- var server = this.connection.server._clone([this.connection], '');
- var strategy = this._schemes[scheme](server, options);
+ server = server._clone();
+ const strategy = this.#schemes[scheme](server, options);
- Hoek.assert(strategy.authenticate, 'Invalid scheme:', name, 'missing authenticate() method');
- Hoek.assert(typeof strategy.authenticate === 'function', 'Invalid scheme:', name, 'invalid authenticate() method');
- Hoek.assert(!strategy.payload || typeof strategy.payload === 'function', 'Invalid scheme:', name, 'invalid payload() method');
- Hoek.assert(!strategy.response || typeof strategy.response === 'function', 'Invalid scheme:', name, 'invalid response() method');
- strategy.options = strategy.options || {};
- Hoek.assert(strategy.payload || !strategy.options.payload, 'Cannot require payload validation without a payload method');
+ Hoek.assert(strategy.authenticate, 'Invalid scheme:', name, 'missing authenticate() method');
+ Hoek.assert(typeof strategy.authenticate === 'function', 'Invalid scheme:', name, 'invalid authenticate() method');
+ Hoek.assert(!strategy.payload || typeof strategy.payload === 'function', 'Invalid scheme:', name, 'invalid payload() method');
+ Hoek.assert(!strategy.response || typeof strategy.response === 'function', 'Invalid scheme:', name, 'invalid response() method');
+ strategy.options = strategy.options ?? {};
+ Hoek.assert(strategy.payload || !strategy.options.payload, 'Cannot require payload validation without a payload method');
- this._strategies[name] = {
- methods: strategy,
- realm: server.realm
- };
+ this.#strategies[name] = {
+ methods: strategy,
+ realm: server.realm
+ };
- if (mode) {
- this.default({ strategies: [name], mode: mode === true ? 'required' : mode });
+ if (strategy.api) {
+ this.api[name] = strategy.api;
+ }
}
-};
-
-internals.Auth.prototype.default = function (options) {
+ default(options) {
- Schema.assert('auth', options, 'default strategy');
- Hoek.assert(!this.settings.default, 'Cannot set default strategy more than once');
+ Hoek.assert(!this.settings.default, 'Cannot set default strategy more than once');
+ options = Config.apply('auth', options, 'default strategy');
- var settings = Hoek.clone(options); // options can be reused
+ this.settings.default = this._setupRoute(Hoek.clone(options)); // Prevent changes to options
- if (typeof settings === 'string') {
- settings = {
- strategies: [settings],
- mode: 'required'
- };
- }
- else if (settings.strategy) {
- settings.strategies = [settings.strategy];
- delete settings.strategy;
+ const routes = this.#core.router.table();
+ for (const route of routes) {
+ route.rebuild();
+ }
}
- Hoek.assert(settings.strategies && settings.strategies.length, 'Default authentication strategy missing strategy name');
+ async test(name, request) {
- this.settings.default = settings;
-};
+ Hoek.assert(name, 'Missing authentication strategy name');
+ const strategy = this.#strategies[name];
+ Hoek.assert(strategy, 'Unknown authentication strategy:', name);
+ const bind = strategy.methods;
+ const realm = strategy.realm;
+ const response = await request._core.toolkit.execute(strategy.methods.authenticate, request, { bind, realm, auth: true });
-internals.Auth.prototype.test = function (name, request, next) {
+ if (!response.isAuth) {
+ throw response;
+ }
- Hoek.assert(name, 'Missing authentication strategy name');
- var strategy = this._strategies[name];
- Hoek.assert(strategy, 'Unknown authentication strategy:', name);
+ if (response.error) {
+ throw response.error;
+ }
- var transfer = function (response, data) {
+ return response.data;
+ }
- return next(response, data && data.credentials);
- };
+ async verify(request) {
- var reply = request.server._replier.interface(request, strategy.realm, transfer);
- strategy.methods.authenticate(request, reply);
-};
+ const auth = request.auth;
+ if (auth.error) {
+ throw auth.error;
+ }
-internals.Auth.prototype._setupRoute = function (options, path) {
+ if (!auth.isAuthenticated) {
+ return;
+ }
- var self = this;
+ const strategy = this.#strategies[auth.strategy];
+ Hoek.assert(strategy, 'Unknown authentication strategy:', auth.strategy);
- if (!options) {
- return options; // Preseve the difference between undefined and false
- }
+ if (!strategy.methods.verify) {
+ return;
+ }
- if (typeof options === 'string') {
- options = { strategies: [options] };
- }
- else if (options.strategy) {
- options.strategies = [options.strategy];
- delete options.strategy;
+ const bind = strategy.methods;
+ await strategy.methods.verify.call(bind, auth);
}
- if (!options.strategies) {
- Hoek.assert(this.settings.default, 'Route missing authentication strategy and no default defined:', path);
- options = Hoek.applyToDefaults(this.settings.default, options);
+ static testAccess(request, route) {
+
+ const auth = request._core.auth;
+
+ try {
+ return auth._access(request, route);
+ }
+ catch (err) {
+ Bounce.rethrow(err, 'system');
+ return false;
+ }
}
- if (options.scope) {
- if (typeof options.scope === 'string') {
- options.scope = [options.scope];
+ _setupRoute(options, path) {
+
+ if (!options) {
+ return options; // Preserve the difference between undefined and false
}
- for (var i = 0, il = options.scope.length; i < il; ++i) {
- if (/{([^}]+)}/.test(options.scope[i])) {
- options.hasScopeParameters = true;
- break;
- }
+ if (typeof options === 'string') {
+ options = { strategies: [options] };
+ }
+ else if (options.strategy) {
+ options.strategies = [options.strategy];
+ delete options.strategy;
}
- }
- Hoek.assert(options.strategies.length, 'Route missing authentication strategy:', path);
+ if (path &&
+ !options.strategies) {
- options.mode = options.mode || 'required';
- if (options.payload === true) {
- options.payload = 'required';
- }
+ Hoek.assert(this.settings.default, 'Route missing authentication strategy and no default defined:', path);
+ options = Hoek.applyToDefaults(this.settings.default, options);
+ }
- var hasAuthenticatePayload = false;
- options.strategies.forEach(function (name) {
+ path = path ?? 'default strategy';
+ Hoek.assert(options.strategies?.length, 'Missing authentication strategy:', path);
- var strategy = self._strategies[name];
- Hoek.assert(strategy, 'Unknown authentication strategy:', name, 'in path:', path);
- Hoek.assert(strategy.methods.payload || options.payload !== 'required', 'Payload validation can only be required when all strategies support it in path:', path);
- hasAuthenticatePayload = hasAuthenticatePayload || strategy.methods.payload;
- Hoek.assert(!strategy.methods.options.payload || options.payload === undefined || options.payload === 'required', 'Cannot set authentication payload to', options.payload, 'when a strategy requires payload validation', path);
- });
+ options.mode = options.mode ?? 'required';
- Hoek.assert(!options.payload || hasAuthenticatePayload, 'Payload authentication requires at least one strategy with payload support in path:', path);
+ if (options.entity !== undefined || // Backwards compatibility with <= 11.x.x
+ options.scope !== undefined) {
- return options;
-};
+ options.access = [{ entity: options.entity, scope: options.scope }];
+ delete options.entity;
+ delete options.scope;
+ }
+ if (options.access) {
+ for (const access of options.access) {
+ access.scope = internals.setupScope(access);
+ }
+ }
-internals.Auth.prototype.lookup = function (route) {
+ if (options.payload === true) {
+ options.payload = 'required';
+ }
- if (route.settings.auth === false) {
- return false;
+ let hasAuthenticatePayload = false;
+ for (const name of options.strategies) {
+ const strategy = this.#strategies[name];
+ Hoek.assert(strategy, 'Unknown authentication strategy', name, 'in', path);
+
+ Hoek.assert(strategy.methods.payload || options.payload !== 'required', 'Payload validation can only be required when all strategies support it in', path);
+ hasAuthenticatePayload = hasAuthenticatePayload || strategy.methods.payload;
+ Hoek.assert(!strategy.methods.options.payload || options.payload === undefined || options.payload === 'required', 'Cannot set authentication payload to', options.payload, 'when a strategy requires payload validation in', path);
+ }
+
+ Hoek.assert(!options.payload || hasAuthenticatePayload, 'Payload authentication requires at least one strategy with payload support in', path);
+
+ return options;
}
- return route.settings.auth || this.settings.default;
-};
+ lookup(route) {
+ if (route.settings.auth === false) {
+ return false;
+ }
-internals.Auth.authenticate = function (request, next) {
+ return route.settings.auth || this.settings.default;
+ }
- var auth = request.connection.auth;
- return auth._authenticate(request, next);
-};
+ _enabled(route, type) {
+ const config = this.lookup(route);
+ if (!config) {
+ return false;
+ }
-internals.Auth.prototype._authenticate = function (request, next) {
+ if (type === 'authenticate') {
+ return true;
+ }
- var self = this;
+ if (type === 'access') {
+ return !!config.access;
+ }
- var config = this.lookup(request.route);
- if (!config) {
- return next();
- }
+ for (const name of config.strategies) {
+ const strategy = this.#strategies[name];
+ if (strategy.methods[type]) {
+ return true;
+ }
+ }
- request.auth.mode = config.mode;
+ return false;
+ }
- var authErrors = [];
- var strategyPos = 0;
+ static authenticate(request) {
- var authenticate = function () {
+ const auth = request._core.auth;
+ return auth._authenticate(request);
+ }
- // Find next strategy
+ async _authenticate(request) {
- if (strategyPos >= config.strategies.length) {
- var err = Boom.unauthorized('Missing authentication', authErrors);
+ const config = this.lookup(request.route);
- if (config.mode === 'optional' ||
- config.mode === 'try') {
+ const errors = [];
+ request.auth.mode = config.mode;
- request.auth.isAuthenticated = false;
- request.auth.credentials = null;
- request.auth.error = err;
- request._log(['auth', 'unauthenticated']);
- return next();
- }
+ // Injection bypass
- return next(err);
+ if (request.auth.credentials) {
+ internals.validate(null, { credentials: request.auth.credentials, artifacts: request.auth.artifacts }, request.auth.strategy, config, request, errors);
+ return;
}
- var name = config.strategies[strategyPos];
- ++strategyPos;
+ // Try each strategy
- request._protect.run('auth:request:' + name, validate, function (exit) {
+ for (const name of config.strategies) {
+ const strategy = this.#strategies[name];
- var transfer = function (response, data) {
+ const bind = strategy.methods;
+ const realm = strategy.realm;
+ const response = await request._core.toolkit.execute(strategy.methods.authenticate, request, { bind, realm, auth: true });
- exit(response, name, data);
- };
+ const message = (response.isAuth ? internals.validate(response.error, response.data, name, config, request, errors) : internals.validate(response, null, name, config, request, errors));
+ if (!message) {
+ return;
+ }
- var strategy = self._strategies[name];
- var reply = request.server._replier.interface(request, strategy.realm, transfer);
- strategy.methods.authenticate(request, reply);
- });
- };
+ if (message !== internals.missing) {
+ return message;
+ }
+ }
- var validate = function (err, name, result) { // err can be Boom, Error, or a valid response object
+ // No more strategies
- if (!name) {
- return next(err);
+ const err = Boom.unauthorized('Missing authentication', errors);
+ if (config.mode === 'required') {
+ throw err;
}
- result = result || {};
+ request.auth.isAuthenticated = false;
+ request.auth.credentials = null;
+ request.auth.error = err;
+ request._log(['auth', 'unauthenticated']);
+ }
- // Unauthenticated
+ static access(request) {
- if (!err &&
- !result.credentials) {
+ const auth = request._core.auth;
+ request.auth.isAuthorized = auth._access(request);
+ }
+
+ _access(request, route) {
- return next(Boom.badImplementation('Authentication response missing both error and credentials'));
+ const config = this.lookup(route || request.route);
+ if (!config?.access) {
+ return true;
}
- if (err) {
- if (err instanceof Error === false) {
- request._log(['auth', 'unauthenticated', 'response', name], err.statusCode);
- return next(err);
+ const credentials = request.auth.credentials;
+ if (!credentials) {
+ if (config.mode !== 'required') {
+ return false;
}
- if (err.isMissing) {
+ throw Boom.forbidden('Request is unauthenticated');
+ }
+
+ const requestEntity = (credentials.user ? 'user' : 'app');
+
+ const scopeErrors = [];
+ for (const access of config.access) {
- // Try next name
+ // Check entity
- request._log(['auth', 'unauthenticated', 'missing', name], err);
- authErrors.push(err.output.headers['WWW-Authenticate']);
- return authenticate();
+ const entity = access.entity;
+ if (entity &&
+ entity !== 'any' &&
+ entity !== requestEntity) {
+
+ continue;
}
- if (config.mode === 'try') {
- request.auth.isAuthenticated = false;
- request.auth.strategy = name;
- request.auth.credentials = result.credentials;
- request.auth.artifacts = result.artifacts;
- request.auth.error = err;
- request._log(['auth', 'unauthenticated', 'try', name], err);
- return next();
+ // Check scope
+
+ let scope = access.scope;
+ if (scope) {
+ if (!credentials.scope) {
+ scopeErrors.push(scope);
+ continue;
+ }
+
+ scope = internals.expandScope(request, scope);
+ if (!internals.validateScope(credentials, scope, 'required') ||
+ !internals.validateScope(credentials, scope, 'selection') ||
+ !internals.validateScope(credentials, scope, 'forbidden')) {
+
+ scopeErrors.push(scope);
+ continue;
+ }
}
- request._log(['auth', 'unauthenticated', 'error', name], err);
- return next(err);
+ return true;
}
- // Authenticated
+ // Scope error
- var credentials = result.credentials;
- request.auth.strategy = name;
- request.auth.credentials = credentials;
- request.auth.artifacts = result.artifacts;
+ if (scopeErrors.length) {
+ request._log(['auth', 'scope', 'error']);
+ throw Boom.forbidden('Insufficient scope', { got: credentials.scope, need: scopeErrors });
+ }
- // Check scope
+ // Entity error
- if (config.scope) {
- var scopes = config.scope;
- if (config.hasScopeParameters) {
- scopes = [];
- var context = { params: request.params, query: request.query };
- for (var i = 0, il = config.scope.length; i < il; ++i) {
- scopes[i] = Hoek.reachTemplate(context, config.scope[i]);
- }
- }
+ if (requestEntity === 'app') {
+ request._log(['auth', 'entity', 'user', 'error']);
+ throw Boom.forbidden('Application credentials cannot be used on a user endpoint');
+ }
- if (!credentials.scope ||
- (typeof credentials.scope === 'string' ? scopes.indexOf(credentials.scope) === -1 : !Hoek.intersect(scopes, credentials.scope).length)) {
+ request._log(['auth', 'entity', 'app', 'error']);
+ throw Boom.forbidden('User credentials cannot be used on an application endpoint');
+ }
- request._log(['auth', 'scope', 'error', name], { got: credentials.scope, need: scopes });
- return next(Boom.forbidden('Insufficient scope, expected any of: ' + scopes));
- }
+ static async payload(request) {
+
+ if (!request.auth.isAuthenticated || !request.auth[Request.symbols.authPayload]) {
+ return;
+ }
+
+ const auth = request._core.auth;
+ const strategy = auth.#strategies[request.auth.strategy];
+ Hoek.assert(strategy, 'Unknown authentication strategy:', request.auth.strategy);
+
+ if (!strategy.methods.payload) {
+ return;
}
- // Check entity
+ const config = auth.lookup(request.route);
+ const setting = config.payload ?? (strategy.methods.options.payload ? 'required' : false);
+ if (!setting) {
+ return;
+ }
- var entity = config.entity || 'any';
+ const bind = strategy.methods;
+ const realm = strategy.realm;
+ const response = await request._core.toolkit.execute(strategy.methods.payload, request, { bind, realm });
- // Entity: 'any'
+ if (response.isBoom &&
+ response.isMissing) {
- if (entity === 'any') {
- request._log(['auth', name]);
- request.auth.isAuthenticated = true;
- return next();
+ return setting === 'optional' ? undefined : Boom.unauthorized('Missing payload authentication');
}
- // Entity: 'user'
+ return response;
+ }
- if (entity === 'user') {
- if (!credentials.user) {
- request._log(['auth', 'entity', 'user', 'error', name]);
- return next(Boom.forbidden('Application credentials cannot be used on a user endpoint'));
- }
+ static async response(response) {
- request._log(['auth', name]);
- request.auth.isAuthenticated = true;
- return next();
+ const request = response.request;
+ const auth = request._core.auth;
+ if (!request.auth.isAuthenticated) {
+ return;
}
- // Entity: 'app'
+ const strategy = auth.#strategies[request.auth.strategy];
+ Hoek.assert(strategy, 'Unknown authentication strategy:', request.auth.strategy);
- if (credentials.user) {
- request._log(['auth', 'entity', 'app', 'error', name]);
- return next(Boom.forbidden('User credentials cannot be used on an application endpoint'));
+ if (!strategy.methods.response) {
+ return;
}
- request._log(['auth', name]);
- request.auth.isAuthenticated = true;
- return next();
- };
+ const bind = strategy.methods;
+ const realm = strategy.realm;
+ const error = await request._core.toolkit.execute(strategy.methods.response, request, { bind, realm, continue: 'undefined' });
+ if (error) {
+ throw error;
+ }
+ }
+};
- // Injection bypass
- if (request.auth.credentials) {
- return validate(null, 'bypass', { credentials: request.auth.credentials, artifacts: request.auth.artifacts });
+internals.setupScope = function (access) {
+
+ // No scopes
+
+ if (!access.scope) {
+ return false;
}
- // Authenticate
+ // Already setup
- authenticate();
+ if (!Array.isArray(access.scope)) {
+ return access.scope;
+ }
+
+ const scope = {};
+ for (const value of access.scope) {
+ const prefix = value[0];
+ const type = prefix === '+' ? 'required' : (prefix === '!' ? 'forbidden' : 'selection');
+ const clean = type === 'selection' ? value : value.slice(1);
+ scope[type] = scope[type] ?? [];
+ scope[type].push(clean);
+
+ if ((!scope._hasParameters?.[type]) &&
+ /{([^}]+)}/.test(clean)) {
+
+ scope._hasParameters = scope._hasParameters ?? {};
+ scope._hasParameters[type] = true;
+ }
+ }
+
+ return scope;
};
-internals.Auth.payload = function (request, next) {
+internals.validate = function (err, result, name, config, request, errors) { // err can be Boom, Error, or a valid response object
+
+ result = result ?? {};
+ request.auth.isAuthenticated = !err;
+
+ if (err) {
+
+ // Non-error response
+
+ if (err instanceof Error === false) {
+ request._log(['auth', 'unauthenticated', 'response', name], { statusCode: err.statusCode });
+ return err;
+ }
+
+ // Missing authenticated
+
+ if (err.isMissing) {
+ request._log(['auth', 'unauthenticated', 'missing', name], err);
+ errors.push(err.output.headers['WWW-Authenticate']);
+ return internals.missing;
+ }
+ }
+
+ request.auth.strategy = name;
+ request.auth.credentials = result.credentials;
+ request.auth.artifacts = result.artifacts;
- if (!request.auth.isAuthenticated ||
- request.auth.strategy === 'bypass') {
+ // Authenticated
- return next();
+ if (!err) {
+ return;
}
- var auth = request.connection.auth;
- var strategy = auth._strategies[request.auth.strategy];
+ // Unauthenticated
- if (!strategy.methods.payload) {
- return next();
+ request.auth.error = err;
+
+ if (config.mode === 'try') {
+ request._log(['auth', 'unauthenticated', 'try', name], err);
+ return;
}
- var config = auth.lookup(request.route);
- var setting = config.payload || (strategy.methods.options.payload ? 'required' : false);
- if (!setting) {
- return next();
+ request._log(['auth', 'unauthenticated', 'error', name], err);
+ throw err;
+};
+
+
+internals.expandScope = function (request, scope) {
+
+ if (!scope._hasParameters) {
+ return scope;
}
- var finalize = function (response) {
+ const expanded = {
+ required: internals.expandScopeType(request, scope, 'required'),
+ selection: internals.expandScopeType(request, scope, 'selection'),
+ forbidden: internals.expandScopeType(request, scope, 'forbidden')
+ };
- if (response &&
- response.isBoom &&
- response.isMissing) {
+ return expanded;
+};
- return next(setting === 'optional' ? null : Boom.unauthorized('Missing payload authentication'));
- }
- return next(response);
+internals.expandScopeType = function (request, scope, type) {
+
+ if (!scope._hasParameters[type]) {
+ return scope[type];
+ }
+
+ const expanded = [];
+ const context = {
+ params: request.params,
+ query: request.query,
+ payload: request.payload,
+ credentials: request.auth.credentials
};
- request._protect.run('auth:payload:' + request.auth.strategy, finalize, function (exit) {
+ for (const template of scope[type]) {
+ expanded.push(Hoek.reachTemplate(context, template));
+ }
- var reply = request.server._replier.interface(request, strategy.realm, exit);
- strategy.methods.payload(request, reply);
- });
+ return expanded;
};
-internals.Auth.response = function (request, next) {
-
- var auth = request.connection.auth;
- var config = auth.lookup(request.route);
- if (!config ||
- !request.auth.isAuthenticated ||
- request.auth.strategy === 'bypass') {
+internals.validateScope = function (credentials, scope, type) {
- return next();
+ if (!scope[type]) {
+ return true;
}
- var strategy = auth._strategies[request.auth.strategy];
- if (!strategy.methods.response) {
- return next();
+ const count = typeof credentials.scope === 'string' ?
+ scope[type].indexOf(credentials.scope) !== -1 ? 1 : 0 :
+ Hoek.intersect(scope[type], credentials.scope).length;
+
+ if (type === 'forbidden') {
+ return count === 0;
}
- request._protect.run('auth:response:' + request.auth.strategy, next, function (exit) {
+ if (type === 'required') {
+ return count === scope.required.length;
+ }
- var reply = request.server._replier.interface(request, strategy.realm, exit);
- strategy.methods.response(request, reply);
- });
+ return !!count;
};
diff --git a/lib/compression.js b/lib/compression.js
new file mode 100755
index 000000000..3e4c692e4
--- /dev/null
+++ b/lib/compression.js
@@ -0,0 +1,119 @@
+'use strict';
+
+const Zlib = require('zlib');
+
+const Accept = require('@hapi/accept');
+const Bounce = require('@hapi/bounce');
+const Hoek = require('@hapi/hoek');
+
+
+const internals = {
+ common: ['gzip, deflate', 'deflate, gzip', 'gzip', 'deflate', 'gzip, deflate, br']
+};
+
+
+exports = module.exports = internals.Compression = class {
+
+ decoders = {
+ gzip: (options) => Zlib.createGunzip(options),
+ deflate: (options) => Zlib.createInflate(options)
+ };
+
+ encodings = ['identity', 'gzip', 'deflate'];
+
+ encoders = {
+ identity: null,
+ gzip: (options) => Zlib.createGzip(options),
+ deflate: (options) => Zlib.createDeflate(options)
+ };
+
+ #common = null;
+
+ constructor() {
+
+ this._updateCommons();
+ }
+
+ _updateCommons() {
+
+ this.#common = new Map();
+
+ for (const header of internals.common) {
+ this.#common.set(header, Accept.encoding(header, this.encodings));
+ }
+ }
+
+ addEncoder(encoding, encoder) {
+
+ Hoek.assert(this.encoders[encoding] === undefined, `Cannot override existing encoder for ${encoding}`);
+ Hoek.assert(typeof encoder === 'function', `Invalid encoder function for ${encoding}`);
+ this.encoders[encoding] = encoder;
+ this.encodings.unshift(encoding);
+ this._updateCommons();
+ }
+
+ addDecoder(encoding, decoder) {
+
+ Hoek.assert(this.decoders[encoding] === undefined, `Cannot override existing decoder for ${encoding}`);
+ Hoek.assert(typeof decoder === 'function', `Invalid decoder function for ${encoding}`);
+ this.decoders[encoding] = decoder;
+ }
+
+ accept(request) {
+
+ const header = request.headers['accept-encoding'];
+ if (!header) {
+ return 'identity';
+ }
+
+ const common = this.#common.get(header);
+ if (common) {
+ return common;
+ }
+
+ try {
+ return Accept.encoding(header, this.encodings);
+ }
+ catch (err) {
+ Bounce.rethrow(err, 'system');
+ err.header = header;
+ request._log(['accept-encoding', 'error'], err);
+ return 'identity';
+ }
+ }
+
+ encoding(response, length) {
+
+ if (response.settings.compressed) {
+ response.headers['content-encoding'] = response.settings.compressed;
+ return null;
+ }
+
+ const request = response.request;
+ if (!request._core.settings.compression ||
+ length !== null && length < request._core.settings.compression.minBytes) {
+
+ return null;
+ }
+
+ const mime = request._core.mime.type(response.headers['content-type'] || 'application/octet-stream');
+ if (!mime.compressible) {
+ return null;
+ }
+
+ response.vary('accept-encoding');
+
+ if (response.headers['content-encoding']) {
+ return null;
+ }
+
+ return request.info.acceptEncoding === 'identity' ? null : request.info.acceptEncoding;
+ }
+
+ encoder(request, encoding) {
+
+ const encoder = this.encoders[encoding];
+ Hoek.assert(encoder !== undefined, `Unknown encoding ${encoding}`);
+ return encoder(request.route.settings.compression[encoding]);
+ }
+};
diff --git a/lib/config.js b/lib/config.js
new file mode 100755
index 000000000..2b668f97d
--- /dev/null
+++ b/lib/config.js
@@ -0,0 +1,446 @@
+'use strict';
+
+const Os = require('os');
+
+const Somever = require('@hapi/somever');
+const Validate = require('@hapi/validate');
+
+
+const internals = {};
+
+
+exports.symbol = Symbol('hapi-response');
+
+
+exports.apply = function (type, options, ...message) {
+
+ const result = internals[type].validate(options);
+
+ if (result.error) {
+ throw new Error(`Invalid ${type} options ${message.length ? '(' + message.join(' ') + ')' : ''} ${result.error.annotate()}`);
+ }
+
+ return result.value;
+};
+
+
+exports.enable = function (options) {
+
+ const settings = options ? Object.assign({}, options) : {}; // Shallow cloned
+
+ if (settings.security === true) {
+ settings.security = {};
+ }
+
+ if (settings.cors === true) {
+ settings.cors = {};
+ }
+
+ return settings;
+};
+
+exports.versionMatch = (version, range) => Somever.match(version, range, { includePrerelease: true });
+
+internals.access = Validate.object({
+ entity: Validate.valid('user', 'app', 'any'),
+ scope: [false, Validate.array().items(Validate.string()).single().min(1)]
+});
+
+
+internals.auth = Validate.alternatives([
+ Validate.string(),
+ internals.access.keys({
+ mode: Validate.valid('required', 'optional', 'try'),
+ strategy: Validate.string(),
+ strategies: Validate.array().items(Validate.string()).min(1),
+ access: Validate.array().items(internals.access.min(1)).single().min(1),
+ payload: [
+ Validate.valid('required', 'optional'),
+ Validate.boolean()
+ ]
+ })
+ .without('strategy', 'strategies')
+ .without('access', ['scope', 'entity'])
+]);
+
+
+internals.event = Validate.object({
+ method: Validate.array().items(Validate.function()).single(),
+ options: Validate.object({
+ before: Validate.array().items(Validate.string()).single(),
+ after: Validate.array().items(Validate.string()).single(),
+ bind: Validate.any(),
+ sandbox: Validate.valid('server', 'plugin'),
+ timeout: Validate.number().integer().min(1)
+ })
+ .default({})
+});
+
+
+internals.exts = Validate.array()
+ .items(internals.event.keys({ type: Validate.string().required() })).single();
+
+
+internals.failAction = Validate.alternatives([
+ Validate.valid('error', 'log', 'ignore'),
+ Validate.function()
+])
+ .default('error');
+
+
+internals.routeBase = Validate.object({
+ app: Validate.object().allow(null),
+ auth: internals.auth.allow(false),
+ bind: Validate.object().allow(null),
+ cache: Validate.object({
+ expiresIn: Validate.number(),
+ expiresAt: Validate.string(),
+ privacy: Validate.valid('default', 'public', 'private'),
+ statuses: Validate.array().items(Validate.number().integer().min(200)).min(1).single().default([200, 204]),
+ otherwise: Validate.string().default('no-cache')
+ })
+ .allow(false)
+ .default(),
+ compression: Validate.object()
+ .pattern(/.+/, Validate.object())
+ .default(),
+ cors: Validate.object({
+ origin: Validate.array().min(1).allow('ignore').default(['*']),
+ maxAge: Validate.number().default(86400),
+ headers: Validate.array().items(Validate.string()).default(['Accept', 'Authorization', 'Content-Type', 'If-None-Match']),
+ additionalHeaders: Validate.array().items(Validate.string()).default([]),
+ exposedHeaders: Validate.array().items(Validate.string()).default(['WWW-Authenticate', 'Server-Authorization']),
+ additionalExposedHeaders: Validate.array().items(Validate.string()).default([]),
+ credentials: Validate.boolean().when('origin', { is: 'ignore', then: false }).default(false),
+ preflightStatusCode: Validate.valid(200, 204).default(200)
+ })
+ .allow(false, true)
+ .default(false),
+ ext: Validate.object({
+ onPreAuth: Validate.array().items(internals.event).single(),
+ onCredentials: Validate.array().items(internals.event).single(),
+ onPostAuth: Validate.array().items(internals.event).single(),
+ onPreHandler: Validate.array().items(internals.event).single(),
+ onPostHandler: Validate.array().items(internals.event).single(),
+ onPreResponse: Validate.array().items(internals.event).single(),
+ onPostResponse: Validate.array().items(internals.event).single()
+ })
+ .default({}),
+ files: Validate.object({
+ relativeTo: Validate.string().pattern(/^([\/\.])|([A-Za-z]:\\)|(\\\\)/).default('.')
+ })
+ .default(),
+ json: Validate.object({
+ replacer: Validate.alternatives(Validate.function(), Validate.array()).allow(null).default(null),
+ space: Validate.number().allow(null).default(null),
+ suffix: Validate.string().allow(null).default(null),
+ escape: Validate.boolean().default(false)
+ })
+ .default(),
+ log: Validate.object({
+ collect: Validate.boolean().default(false)
+ })
+ .default(),
+ payload: Validate.object({
+ output: Validate.valid('data', 'stream', 'file').default('data'),
+ parse: Validate.boolean().allow('gunzip').default(true),
+ multipart: Validate.object({
+ output: Validate.valid('data', 'stream', 'file', 'annotated').required()
+ })
+ .default(false)
+ .allow(true, false),
+ allow: Validate.array().items(Validate.string()).single(),
+ override: Validate.string(),
+ protoAction: Validate.valid('error', 'remove', 'ignore').default('error'),
+ maxBytes: Validate.number().integer().positive().default(1024 * 1024),
+ maxParts: Validate.number().integer().positive().default(1000),
+ uploads: Validate.string().default(Os.tmpdir()),
+ failAction: internals.failAction,
+ timeout: Validate.number().integer().positive().allow(false).default(10 * 1000),
+ defaultContentType: Validate.string().default('application/json'),
+ compression: Validate.object()
+ .pattern(/.+/, Validate.object())
+ .default()
+ })
+ .default(),
+ plugins: Validate.object(),
+ response: Validate.object({
+ disconnectStatusCode: Validate.number().integer().min(400).default(499),
+ emptyStatusCode: Validate.valid(200, 204).default(204),
+ failAction: internals.failAction,
+ modify: Validate.boolean(),
+ options: Validate.object(),
+ ranges: Validate.boolean().default(true),
+ sample: Validate.number().min(0).max(100).when('modify', { then: Validate.forbidden() }),
+ schema: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(true, false),
+ status: Validate.object().pattern(/\d\d\d/, Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(true, false))
+ })
+ .default(),
+ security: Validate.object({
+ hsts: Validate.alternatives([
+ Validate.object({
+ maxAge: Validate.number(),
+ includeSubdomains: Validate.boolean(),
+ includeSubDomains: Validate.boolean(),
+ preload: Validate.boolean()
+ }),
+ Validate.boolean(),
+ Validate.number()
+ ])
+ .default(15768000),
+ xframe: Validate.alternatives([
+ Validate.boolean(),
+ Validate.valid('sameorigin', 'deny'),
+ Validate.object({
+ rule: Validate.valid('sameorigin', 'deny', 'allow-from'),
+ source: Validate.string()
+ })
+ ])
+ .default('deny'),
+ xss: Validate.valid('enabled', 'disabled', false).default('disabled'),
+ noOpen: Validate.boolean().default(true),
+ noSniff: Validate.boolean().default(true),
+ referrer: Validate.alternatives([
+ Validate.boolean().valid(false),
+ Validate.valid('', 'no-referrer', 'no-referrer-when-downgrade',
+ 'unsafe-url', 'same-origin', 'origin', 'strict-origin',
+ 'origin-when-cross-origin', 'strict-origin-when-cross-origin')
+ ])
+ .default(false)
+ })
+ .allow(null, false, true)
+ .default(false),
+ state: Validate.object({
+ parse: Validate.boolean().default(true),
+ failAction: internals.failAction
+ })
+ .default(),
+ timeout: Validate.object({
+ socket: Validate.number().integer().positive().allow(false),
+ server: Validate.number().integer().positive().allow(false).default(false)
+ })
+ .default(),
+ validate: Validate.object({
+ headers: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, true),
+ params: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, true),
+ query: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, false, true),
+ payload: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, false, true),
+ state: Validate.alternatives(Validate.object(), Validate.array(), Validate.function()).allow(null, false, true),
+ failAction: internals.failAction,
+ errorFields: Validate.object(),
+ options: Validate.object().default(),
+ validator: Validate.object()
+ })
+ .default()
+});
+
+
+internals.server = Validate.object({
+ address: Validate.string().hostname(),
+ app: Validate.object().allow(null),
+ autoListen: Validate.boolean(),
+ cache: Validate.allow(null), // Validated elsewhere
+ compression: Validate.object({
+ minBytes: Validate.number().min(1).integer().default(1024)
+ })
+ .allow(false)
+ .default(),
+ debug: Validate.object({
+ request: Validate.array().items(Validate.string()).single().allow(false).default(['implementation']),
+ log: Validate.array().items(Validate.string()).single().allow(false)
+ })
+ .allow(false)
+ .default(),
+ host: Validate.string().hostname().allow(null),
+ info: Validate.object({
+ remote: Validate.boolean().default(false)
+ })
+ .default({}),
+ listener: Validate.any(),
+ load: Validate.object({
+ sampleInterval: Validate.number().integer().min(0).default(0)
+ })
+ .unknown()
+ .default(),
+ mime: Validate.object().empty(null).default(),
+ operations: Validate.object({
+ cleanStop: Validate.boolean().default(true)
+ })
+ .default(),
+ plugins: Validate.object(),
+ port: Validate.alternatives([
+ Validate.number().integer().min(0), // TCP port
+ Validate.string().pattern(/\//), // Unix domain socket
+ Validate.string().pattern(/^\\\\\.\\pipe\\/) // Windows named pipe
+ ])
+ .allow(null),
+ query: Validate.object({
+ parser: Validate.function()
+ })
+ .default(),
+ router: Validate.object({
+ isCaseSensitive: Validate.boolean().default(true),
+ stripTrailingSlash: Validate.boolean().default(false)
+ })
+ .default(),
+ routes: internals.routeBase.default(),
+ state: Validate.object(), // Cookie defaults
+ tls: Validate.alternatives([
+ Validate.object().allow(null),
+ Validate.boolean()
+ ]),
+ uri: Validate.string().pattern(/[^/]$/)
+});
+
+
+internals.vhost = Validate.alternatives([
+ Validate.string().hostname(),
+ Validate.array().items(Validate.string().hostname()).min(1)
+]);
+
+
+internals.handler = Validate.alternatives([
+ Validate.function(),
+ Validate.object().length(1)
+]);
+
+
+internals.route = Validate.object({
+ method: Validate.string().pattern(/^[a-zA-Z0-9!#\$%&'\*\+\-\.^_`\|~]+$/).required(),
+ path: Validate.string().required(),
+ rules: Validate.object(),
+ vhost: internals.vhost,
+
+ // Validated in route construction
+
+ handler: Validate.any(),
+ options: Validate.any(),
+ config: Validate.any() // Backwards compatibility
+})
+ .without('config', 'options');
+
+
+internals.pre = [
+ Validate.function(),
+ Validate.object({
+ method: Validate.alternatives(Validate.string(), Validate.function()).required(),
+ assign: Validate.string(),
+ mode: Validate.valid('serial', 'parallel'),
+ failAction: internals.failAction
+ })
+];
+
+
+internals.routeConfig = internals.routeBase.keys({
+ description: Validate.string(),
+ id: Validate.string(),
+ isInternal: Validate.boolean(),
+ notes: [
+ Validate.string(),
+ Validate.array().items(Validate.string())
+ ],
+ pre: Validate.array().items(...internals.pre.concat(Validate.array().items(...internals.pre).min(1))),
+ tags: [
+ Validate.string(),
+ Validate.array().items(Validate.string())
+ ]
+});
+
+
+internals.cacheConfig = Validate.alternatives([
+ Validate.function(),
+ Validate.object({
+ name: Validate.string().invalid('_default'),
+ shared: Validate.boolean(),
+ provider: [
+ Validate.function(),
+ {
+ constructor: Validate.function().required(),
+ options: Validate.object({
+ partition: Validate.string().default('hapi-cache')
+ })
+ .unknown() // Catbox client validates other keys
+ .default({})
+ }
+ ],
+ engine: Validate.object()
+ })
+ .xor('provider', 'engine')
+]);
+
+
+internals.cache = Validate.array().items(internals.cacheConfig).min(1).single();
+
+
+internals.cachePolicy = Validate.object({
+ cache: Validate.string().allow(null).allow(''),
+ segment: Validate.string(),
+ shared: Validate.boolean()
+})
+ .unknown(); // Catbox policy validates other keys
+
+
+internals.method = Validate.object({
+ bind: Validate.object().allow(null),
+ generateKey: Validate.function(),
+ cache: internals.cachePolicy
+});
+
+
+internals.methodObject = Validate.object({
+ name: Validate.string().required(),
+ method: Validate.function().required(),
+ options: Validate.object()
+});
+
+
+internals.register = Validate.object({
+ once: true,
+ routes: Validate.object({
+ prefix: Validate.string().pattern(/^\/.+/),
+ vhost: internals.vhost
+ })
+ .default({})
+});
+
+
+internals.semver = Validate.string();
+
+
+internals.plugin = internals.register.keys({
+ options: Validate.any(),
+ plugin: Validate.object({
+ register: Validate.function().required(),
+ name: Validate.string().when('pkg.name', { is: Validate.exist(), otherwise: Validate.required() }),
+ version: Validate.string(),
+ multiple: Validate.boolean().default(false),
+ dependencies: [
+ Validate.array().items(Validate.string()).single(),
+ Validate.object().pattern(/.+/, internals.semver)
+ ],
+ once: true,
+ requirements: Validate.object({
+ hapi: Validate.string(),
+ node: Validate.string()
+ })
+ .default(),
+ pkg: Validate.object({
+ name: Validate.string(),
+ version: Validate.string().default('0.0.0')
+ })
+ .unknown()
+ .default({})
+ })
+ .unknown()
+})
+ .without('once', 'options')
+ .unknown();
+
+
+internals.rules = Validate.object({
+ validate: Validate.object({
+ schema: Validate.alternatives(Validate.object(), Validate.array()).required(),
+ options: Validate.object()
+ .default({ allowUnknown: true })
+ })
+});
diff --git a/lib/connection.js b/lib/connection.js
deleted file mode 100755
index d8b0304ab..000000000
--- a/lib/connection.js
+++ /dev/null
@@ -1,413 +0,0 @@
-// Load modules
-
-var Events = require('events');
-var Http = require('http');
-var Https = require('https');
-var Os = require('os');
-var Path = require('path');
-var Boom = require('boom');
-var Call = require('call');
-var Hoek = require('hoek');
-var Shot = require('shot');
-var Statehood = require('statehood');
-var Topo = require('topo');
-var Auth = require('./auth');
-var Route = require('./route');
-
-
-// Declare internals
-
-var internals = {
- counter: {
- min: 10000,
- max: 99999
- }
-};
-
-
-exports = module.exports = internals.Connection = function (server, options) {
-
- var self = this;
-
- var now = Date.now();
-
- Events.EventEmitter.call(this);
-
- this.settings = options; // options cloned in server.connection()
- this.server = server;
-
- // Normalize settings
-
- this.settings.labels = Hoek.unique(this.settings.labels || []); // Remove duplicates
- if (this.settings.port === undefined) {
- this.settings.port = 0;
- }
-
- this.type = (typeof this.settings.port === 'string' ? 'socket' : 'tcp');
- if (this.type === 'socket') {
- this.settings.port = (this.settings.port.indexOf('/') !== -1 ? Path.resolve(this.settings.port) : this.settings.port.toLowerCase());
- }
-
- if (this.settings.autoListen === undefined) {
- this.settings.autoListen = true;
- }
-
- Hoek.assert(this.settings.autoListen || !this.settings.port, 'Cannot specify port when autoListen is false');
- Hoek.assert(this.settings.autoListen || !this.settings.address, 'Cannot specify address when autoListen is false');
-
- this.settings.query = this.settings.query || {};
-
- // Connection facilities
-
- this._started = false;
- this._connections = {};
- this._onConnection = null; // Used to remove event listener on stop
- this._registrations = {}; // Tracks plugin for dependency validation
-
- this._extensions = {
- onRequest: null, // New request, before handing over to the router (allows changes to the request method, url, etc.)
- onPreAuth: null, // After cookie parse and before authentication (skipped if state error)
- onPostAuth: null, // After authentication (and payload processing) and before validation (skipped if auth or payload error)
- onPreHandler: null, // After validation and body parsing, before route handler (skipped if auth or validation error)
- onPostHandler: null, // After route handler returns, before sending response (skipped if onPreHandler not called)
- onPreResponse: null // Before response is sent (always called)
- };
-
- this._requestCounter = { value: internals.counter.min, min: internals.counter.min, max: internals.counter.max };
- this._load = server._heavy.policy(this.settings.load);
- this.states = new Statehood.Definitions(this.settings.state);
- this.auth = new Auth(this);
- this._router = new Call.Router(this.settings.router);
- this._defaultRoutes();
-
- this.plugins = {}; // Registered plugin APIs by plugin name
- this.app = {}; // Place for application-specific state without conflicts with hapi, should not be used by plugins
-
- // Create listener
-
- this.listener = this.settings.listener || (this.settings.tls ? Https.createServer(this.settings.tls) : Http.createServer());
- this.listener.on('request', this._dispatch());
- this._init();
-
- this.listener.on('clientError', function (err, socket) {
-
- self.server._log(['connection', 'client', 'error'], err);
- });
-
- // Connection information
-
- this.info = {
- created: now,
- started: 0,
- host: this.settings.host || Os.hostname() || 'localhost',
- port: this.settings.port,
- protocol: this.type === 'tcp' ? (this.settings.tls ? 'https' : 'http') : this.type,
- id: Os.hostname() + ':' + process.pid + ':' + now.toString(36)
- };
-
- this.info.uri = (this.settings.uri || (this.info.protocol + ':' + (this.type === 'tcp' ? '//' + this.info.host + (this.info.port ? ':' + this.info.port : '') : this.info.port)));
-};
-
-Hoek.inherits(internals.Connection, Events.EventEmitter);
-
-
-internals.Connection.prototype._init = function () {
-
- var self = this;
-
- // Setup listener
-
- this.listener.once('listening', function () {
-
- // Update the address, port, and uri with active values
-
- if (self.type === 'tcp') {
- var address = self.listener.address();
- self.info.address = address.address;
- self.info.port = address.port;
- self.info.uri = (self.settings.uri || (self.info.protocol + '://' + self.info.host + ':' + self.info.port));
- }
-
- self._onConnection = function (connection) {
-
- var key = connection.remoteAddress + ':' + connection.remotePort;
- self._connections[key] = connection;
-
- connection.once('close', function () {
-
- delete self._connections[key];
- });
- };
-
- self.listener.on('connection', self._onConnection);
- });
-};
-
-
-internals.Connection.prototype._start = function (callback) {
-
- var self = this;
-
- if (this._started) {
- return process.nextTick(callback);
- }
-
- this._started = true;
- this.info.started = Date.now();
-
- if (!this.settings.autoListen) {
- return process.nextTick(callback);
- }
-
- var onError = function (err) {
-
- self._started = false;
- return callback(err);
- };
-
- this.listener.once('error', onError);
-
- var finalize = function () {
-
- self.listener.removeListener('error', onError);
- callback();
- };
-
- if (this.type !== 'tcp') {
- this.listener.listen(this.settings.port, finalize);
- }
- else {
- var address = this.settings.address || this.settings.host || '0.0.0.0';
- this.listener.listen(this.settings.port, address, finalize);
- }
-};
-
-
-internals.Connection.prototype._stop = function (options, callback) {
-
- var self = this;
-
- if (!this._started) {
- return process.nextTick(callback);
- }
-
- this._started = false;
- this.info.started = 0;
-
- var timeoutId = setTimeout(function () {
-
- Object.keys(self._connections).forEach(function (key) {
-
- self._connections[key].destroy();
- });
-
-
- self._connections = {};
- }, options.timeout);
-
- this.listener.close(function () {
-
- self.listener.removeListener('connection', self._onConnection);
- clearTimeout(timeoutId);
-
- self._init();
- return callback();
- });
-};
-
-
-internals.Connection.prototype._dispatch = function (options) {
-
- var self = this;
-
- options = options || {};
-
- return function (req, res) {
-
- if (!self._started &&
- !Shot.isInjection(req)) {
-
- return req.connection.end();
- }
-
- // Create request
-
- var request = self.server._requestor.request(self, req, res, options);
-
- // Check load
-
- var overload = self._load.check();
- if (overload) {
- self.server._log(['load'], self.server.load);
- request._reply(overload);
- }
- else {
-
- // Execute request lifecycle
-
- request._protect.domain.run(function () {
-
- request._execute();
- });
- }
- };
-};
-
-
-internals.Connection.prototype.inject = function (options, callback) {
-
- var settings = options;
- if (settings.credentials ||
- settings.allowInternals !== undefined) { // Can be false
-
- settings = Hoek.shallow(options); // options can be reused
- delete settings.credentials;
- delete settings.artifacts; // Cannot appear without credentials
- delete settings.allowInternals;
- }
-
- var needle = this._dispatch({
- credentials: options.credentials,
- artifacts: options.artifacts,
- allowInternals: options.allowInternals
- });
-
- Shot.inject(needle, settings, function (res) {
-
- if (res.raw.res._hapi) {
- res.result = res.raw.res._hapi.result;
- res.request = res.raw.res._hapi.request;
- delete res.raw.res._hapi;
- }
-
- if (res.result === undefined) {
- res.result = res.payload;
- }
-
- return callback(res);
- });
-};
-
-
-internals.Connection.prototype.table = function (host) {
-
- return this._router.table(host);
-};
-
-
-internals.Connection.prototype.lookup = function (id) {
-
- Hoek.assert(id && typeof id === 'string', 'Invalid route id:', id);
-
- var record = this._router.ids[id];
- if (!record) {
- return null;
- }
-
- return record.route.public;
-};
-
-
-internals.Connection.prototype.match = function (method, path, host) {
-
- Hoek.assert(method && typeof method === 'string', 'Invalid method:', method);
- Hoek.assert(path && typeof path === 'string' && path[0] === '/', 'Invalid path:', path);
- Hoek.assert(!host || typeof host === 'string', 'Invalid host:', host);
-
- var match = this._router.route(method.toLowerCase(), path, host);
- if (match.route.method === 'notfound') {
- return null;
- }
-
- Hoek.assert(match.route.method !== 'badrequest', 'Invalid path:', path);
-
- return match.route.public;
-};
-
-
-internals.Connection.prototype._ext = function (event, nodes, options) {
-
- Hoek.assert(this._extensions[event] !== undefined, 'Unknown event type', event);
-
- this._extensions[event] = this._extensions[event] || new Topo();
- this._extensions[event].add(nodes, options);
-};
-
-
-internals.Connection.prototype._route = function (configs, realm) {
-
- configs = [].concat(configs);
- for (var i = 0, il = configs.length; i < il; ++i) {
- var config = configs[i];
-
- if (Array.isArray(config.method)) {
- for (var m = 0, ml = config.method.length; m < ml; ++m) {
- var method = config.method[m];
-
- var settings = Hoek.shallow(config);
- settings.method = method;
- this._addRoute(settings, realm);
- }
- }
- else {
- this._addRoute(config, realm);
- }
- }
-};
-
-
-internals.Connection.prototype._addRoute = function (config, realm) {
-
- var route = new Route(config, this, realm); // Do no use config beyond this point, use route members
- var vhosts = [].concat(route.settings.vhost || '*');
-
- for (var i = 0, il = vhosts.length; i < il; ++i) {
- var vhost = vhosts[i];
- var record = this._router.add({ method: route.method, path: route.path, vhost: vhost, analysis: route._analysis, id: route.settings.id }, route);
- route.fingerprint = record.fingerprint;
- route.params = record.params;
- }
-};
-
-
-internals.Connection.prototype._defaultRoutes = function () {
-
- this._router.special('notFound', new Route({
- method: 'notFound',
- path: '/{p*}',
- config: {
- auth: false, // Override any defaults
- handler: function (request, reply) {
-
- return reply(Boom.notFound());
- }
- }
- }, this, this.server.realm));
-
- this._router.special('badRequest', new Route({
- method: 'badRequest',
- path: '/{p*}',
- config: {
- auth: false, // Override any defaults
- handler: function (request, reply) {
-
- return reply(Boom.badRequest());
- }
- }
- }, this, this.server.realm));
-
- if (this.settings.routes.cors) {
- this._router.special('options', new Route({
- path: '/{p*}',
- method: 'options',
- config: {
- auth: false, // Override any defaults
- cors: this.settings.routes.cors,
- handler: function (request, reply) {
-
- return reply();
- }
- }
- }, this, this.server.realm));
- }
-};
diff --git a/lib/core.js b/lib/core.js
new file mode 100755
index 000000000..202f6dbe0
--- /dev/null
+++ b/lib/core.js
@@ -0,0 +1,718 @@
+'use strict';
+
+const Http = require('http');
+const Https = require('https');
+const Os = require('os');
+const Path = require('path');
+
+const Boom = require('@hapi/boom');
+const Bounce = require('@hapi/bounce');
+const Call = require('@hapi/call');
+const Catbox = require('@hapi/catbox');
+const { Engine: CatboxMemory } = require('@hapi/catbox-memory');
+const { Heavy } = require('@hapi/heavy');
+const Hoek = require('@hapi/hoek');
+const { Mimos } = require('@hapi/mimos');
+const Podium = require('@hapi/podium');
+const Statehood = require('@hapi/statehood');
+
+const Auth = require('./auth');
+const Compression = require('./compression');
+const Config = require('./config');
+const Cors = require('./cors');
+const Ext = require('./ext');
+const Methods = require('./methods');
+const Request = require('./request');
+const Response = require('./response');
+const Route = require('./route');
+const Toolkit = require('./toolkit');
+const Validation = require('./validation');
+
+
+const internals = {
+ counter: {
+ min: 10000,
+ max: 99999
+ },
+ events: [
+ { name: 'cachePolicy', spread: true },
+ { name: 'log', channels: ['app', 'internal'], tags: true },
+ { name: 'request', channels: ['app', 'internal', 'error'], tags: true, spread: true },
+ 'response',
+ 'route',
+ 'start',
+ 'closing',
+ 'stop'
+ ],
+ badRequestResponse: Buffer.from('HTTP/1.1 400 Bad Request\r\n\r\n', 'ascii')
+};
+
+
+exports = module.exports = internals.Core = class {
+
+ actives = new WeakMap(); // Active requests being processed
+ app = {};
+ auth = new Auth(this);
+ caches = new Map(); // Cache clients
+ compression = new Compression();
+ controlled = null; // Other servers linked to the phases of this server
+ dependencies = []; // Plugin dependencies
+ events = new Podium.Podium(internals.events);
+ heavy = null;
+ info = null;
+ instances = new Set();
+ listener = null;
+ methods = new Methods(this); // Server methods
+ mime = null;
+ onConnection = null; // Used to remove event listener on stop
+ phase = 'stopped'; // 'stopped', 'initializing', 'initialized', 'starting', 'started', 'stopping', 'invalid'
+ plugins = {}; // Exposed plugin properties by name
+ registrations = {}; // Tracks plugin for dependency validation { name -> { version } }
+ registring = 0; // > 0 while register() is waiting for plugin callbacks
+ Request = class extends Request { };
+ Response = class extends Response { };
+ requestCounter = { value: internals.counter.min, min: internals.counter.min, max: internals.counter.max };
+ root = null;
+ router = null;
+ settings = null;
+ sockets = null; // Track open sockets for graceful shutdown
+ started = false;
+ states = null;
+ toolkit = new Toolkit.Manager();
+ type = null;
+ validator = null;
+
+ extensionsSeq = 0; // Used to keep absolute order of extensions based on the order added across locations
+ extensions = {
+ server: {
+ onPreStart: new Ext('onPreStart', this),
+ onPostStart: new Ext('onPostStart', this),
+ onPreStop: new Ext('onPreStop', this),
+ onPostStop: new Ext('onPostStop', this)
+ },
+ route: {
+ onRequest: new Ext('onRequest', this),
+ onPreAuth: new Ext('onPreAuth', this),
+ onCredentials: new Ext('onCredentials', this),
+ onPostAuth: new Ext('onPostAuth', this),
+ onPreHandler: new Ext('onPreHandler', this),
+ onPostHandler: new Ext('onPostHandler', this),
+ onPreResponse: new Ext('onPreResponse', this),
+ onPostResponse: new Ext('onPostResponse', this)
+ }
+ };
+
+ decorations = {
+ handler: new Map(),
+ request: new Map(),
+ response: new Map(),
+ server: new Map(),
+ toolkit: new Map(),
+ requestApply: null,
+ public: { handler: [], request: [], response: [], server: [], toolkit: [] }
+ };
+
+ constructor(options) {
+
+ const { settings, type } = internals.setup(options);
+
+ this.settings = settings;
+ this.type = type;
+
+ this.heavy = new Heavy(this.settings.load);
+ this.mime = new Mimos(this.settings.mime);
+ this.router = new Call.Router(this.settings.router);
+ this.states = new Statehood.Definitions(this.settings.state);
+
+ this._debug();
+ this._initializeCache();
+
+ if (this.settings.routes.validate.validator) {
+ this.validator = Validation.validator(this.settings.routes.validate.validator);
+ }
+
+ this.listener = this._createListener();
+ this._initializeListener();
+ this.info = this._info();
+ }
+
+ _debug() {
+
+ const debug = this.settings.debug;
+ if (!debug) {
+ return;
+ }
+
+ // Subscribe to server log events
+
+ const method = (event) => {
+
+ const data = event.error ?? event.data;
+ console.error('Debug:', event.tags.join(', '), data ? '\n ' + (data.stack ?? (typeof data === 'object' ? Hoek.stringify(data) : data)) : '');
+ };
+
+ if (debug.log) {
+ const filter = debug.log.some((tag) => tag === '*') ? undefined : debug.log;
+ this.events.on({ name: 'log', filter }, method);
+ }
+
+ if (debug.request) {
+ const filter = debug.request.some((tag) => tag === '*') ? undefined : debug.request;
+ this.events.on({ name: 'request', filter }, (request, event) => method(event));
+ }
+ }
+
+ _initializeCache() {
+
+ if (this.settings.cache) {
+ this._createCache(this.settings.cache);
+ }
+
+ if (!this.caches.has('_default')) {
+ this._createCache([{ provider: CatboxMemory }]); // Defaults to memory-based
+ }
+ }
+
+ _info() {
+
+ const now = Date.now();
+ const protocol = this.type === 'tcp' ? (this.settings.tls ? 'https' : 'http') : this.type;
+ const host = this.settings.host || Os.hostname() || 'localhost';
+ const port = this.settings.port;
+
+ const info = {
+ created: now,
+ started: 0,
+ host,
+ port,
+ protocol,
+ id: Os.hostname() + ':' + process.pid + ':' + now.toString(36),
+ uri: this.settings.uri ?? (protocol + ':' + (this.type === 'tcp' ? '//' + host + (port ? ':' + port : '') : port))
+ };
+
+ return info;
+ }
+
+ _counter() {
+
+ const next = ++this.requestCounter.value;
+
+ if (this.requestCounter.value > this.requestCounter.max) {
+ this.requestCounter.value = this.requestCounter.min;
+ }
+
+ return next - 1;
+ }
+
+ _createCache(configs) {
+
+ Hoek.assert(this.phase !== 'initializing', 'Cannot provision server cache while server is initializing');
+
+ configs = Config.apply('cache', configs);
+
+ const added = [];
+ for (let config of configs) {
+
+ // (type: 'handler', property: P, method: HandlerDecorationMethod, options?: { apply?: boolean | undefined, extend?: never }): void; + + decorate
(type: 'request', property: ExceptName
, method: (existing: ((...args: any[]) => any)) => (request: Request) => DecorationMethod (type: 'request', property: ExceptName , method: (request: Request) => DecorationMethod (type: 'request', property: ExceptName , method: DecorationMethod (type: 'request', property: ExceptName , value: (existing: ((...args: any[]) => any)) => (request: Request) => any, options: {apply: true, extend: true}): void;
+ decorate (type: 'request', property: ExceptName , value: (request: Request) => any, options: {apply: true, extend?: boolean | undefined}): void;
+ decorate (type: 'request', property: ExceptName , value: DecorationValue, options?: never): void;
+
+ decorate (type: 'toolkit', property: ExceptName , method: (existing: ((...args: any[]) => any)) => DecorationMethod (type: 'toolkit', property: ExceptName , method: DecorationMethod (type: 'toolkit', property: ExceptName , value: (existing: ((...args: any[]) => any)) => any, options: {apply?: boolean | undefined, extend: true}): void;
+ decorate (type: 'toolkit', property: ExceptName , value: DecorationValue, options?: never): void;
+
+ decorate (type: 'server', property: ExceptName , method: (existing: ((...args: any[]) => any)) => DecorationMethod (type: 'server', property: ExceptName , method: DecorationMethod (type: 'server', property: ExceptName , value: (existing: ((...args: any[]) => any)) => any, options: {apply?: boolean | undefined, extend: true}): void;
+ decorate (type: 'server', property: ExceptName , value: DecorationValue, options?: never): void;
+
+ /**
+ * Used within a plugin to declare a required dependency on other plugins where:
+ * @param dependencies - plugins which must be registered in order for this plugin to operate. Plugins listed must be registered before the server is
+ * initialized or started.
+ * @param after - (optional) a function that is called after all the specified dependencies have been registered and before the server starts. The function is only called if the server is
+ * initialized or started. The function signature is async function(server) where: server - the server the dependency() method was called on.
+ * @return Return value: none.
+ * The after method is identical to setting a server extension point on 'onPreStart'.
+ * If a circular dependency is detected, an exception is thrown (e.g. two plugins each has an after function to be called after the other).
+ * The method does not provide version dependency which should be implemented using npm peer dependencies.
+ * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverdependencydependencies-after)
+ */
+ dependency(dependencies: Dependencies, after?: ((server: Server) => Promisesteve
');
- expect(res1.result).to.equal('steve
');
+ const res2 = await server.inject('/');
+ expect(res2.statusCode).to.equal(200);
+ expect(res2.result).to.equal('xyz
');
+ });
- server.inject('/', function (res2) {
+ it('exposes an api', () => {
- expect(res2.statusCode).to.equal(200);
- expect(res2.result).to.equal('xyz
');
- done();
- });
- });
- });
- });
+ const implementation = function (server, options) {
- describe('default()', function () {
+ return {
+ api: {
+ x: 5
+ },
+ authenticate: (request, h) => h.continue(null, {})
+ };
+ };
- it('sets default', function (done) {
+ const server = Hapi.server();
+ server.auth.scheme('custom', implementation);
+ server.auth.strategy('xyz', 'custom');
+ server.auth.default('xyz');
- var server = new Hapi.Server();
- server.connection();
- server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', { users: { steve: {} } });
+ expect(server.auth.api.xyz.x).to.equal(5);
+ });
- server.auth.default('default');
- expect(server.connections[0].auth.settings.default).to.deep.equal({ strategies: ['default'], mode: 'required' });
+ it('has its own realm', async () => {
- var handler = function (request, reply) {
+ const implementation = function (server) {
- return reply(request.auth.credentials.user);
+ return {
+ authenticate: (_, h) => h.authenticated({ credentials: server.realm })
+ };
};
- server.route({ method: 'GET', path: '/', handler: handler });
+ const server = Hapi.server();
+
+ server.auth.scheme('custom', implementation);
+ server.auth.strategy('root', 'custom');
- server.inject('/', function (res1) {
+ let pluginA;
- expect(res1.statusCode).to.equal(401);
+ await server.register({
+ name: 'plugin-a',
+ register(srv) {
- server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res2) {
+ pluginA = srv;
- expect(res2.statusCode).to.equal(200);
- done();
- });
+ srv.auth.strategy('a', 'custom');
+ }
});
+
+ const handler = (request) => request.auth.credentials;
+ server.route({ method: 'GET', path: '/a', handler, options: { auth: 'a' } });
+ server.route({ method: 'GET', path: '/root', handler, options: { auth: 'root' } });
+
+ const { result: realm1 } = await server.inject('/a');
+ expect(realm1.plugin).to.be.undefined();
+ expect(realm1).to.not.shallow.equal(server.realm);
+ expect(realm1.parent).to.shallow.equal(pluginA.realm);
+
+ const { result: realm2 } = await server.inject('/root');
+ expect(realm2.plugin).to.be.undefined();
+ expect(realm2).to.not.shallow.equal(server.realm);
+ expect(realm2.parent).to.shallow.equal(server.realm);
});
+ });
- it('sets default with object', function (done) {
+ describe('default()', () => {
- var handler = function (request, reply) {
+ it('sets default', async () => {
- return reply(request.auth.credentials.user);
- };
+ const server = Hapi.server();
+ server.auth.scheme('custom', internals.implementation);
+ server.auth.strategy('default', 'custom', { users: { steve: {} } });
+
+ server.auth.default('default');
+ expect(server.auth.settings.default).to.equal({ strategies: ['default'], mode: 'required' });
+
+ server.route({ method: 'GET', path: '/', handler: (request) => request.auth.credentials.user });
- var server = new Hapi.Server();
- server.connection();
+ const res1 = await server.inject('/');
+ expect(res1.statusCode).to.equal(401);
+
+ const res2 = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res2.statusCode).to.equal(204);
+ });
+
+ it('sets default with object', async () => {
+
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: {} } });
- server.auth.default({ strategy: 'default' });
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res1) {
+ server.auth.default({ strategy: 'default' });
+ expect(server.auth.settings.default).to.equal({ strategies: ['default'], mode: 'required' });
- expect(res1.statusCode).to.equal(401);
+ server.route({ method: 'GET', path: '/', handler: (request) => request.auth.credentials.user });
- server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res2) {
+ const res1 = await server.inject('/');
+ expect(res1.statusCode).to.equal(401);
- expect(res2.statusCode).to.equal(200);
- done();
- });
- });
+ const res2 = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res2.statusCode).to.equal(204);
});
- it('throws when setting default twice', function (done) {
+ it('throws when setting default twice', () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: {} } });
- expect(function () {
+ expect(() => {
server.auth.default('default');
server.auth.default('default');
}).to.throw('Cannot set default strategy more than once');
- done();
});
- it('throws when setting default without strategy', function (done) {
+ it('throws when setting default without strategy', () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: {} } });
- expect(function () {
+ expect(() => {
server.auth.default({ mode: 'required' });
- }).to.throw('Default authentication strategy missing strategy name');
- done();
+ }).to.throw('Missing authentication strategy: default strategy');
+ });
+
+ it('matches dynamic scope', async () => {
+
+ const server = Hapi.server();
+ server.auth.scheme('custom', internals.implementation);
+ server.auth.strategy('default', 'custom', { users: { steve: { user: 'steve', scope: 'one-test-admin-x-steve' } } });
+ server.auth.default({ strategy: 'default', scope: 'one-{params.id}-{params.role}-{payload.x}-{credentials.user}' });
+ server.route({
+ method: 'POST',
+ path: '/{id}/{role}',
+ handler: (request) => request.auth.credentials.user
+ });
+
+ const res = await server.inject({ method: 'POST', url: '/test/admin', headers: { authorization: 'Custom steve' }, payload: { x: 'x' } });
+ expect(res.statusCode).to.equal(200);
});
});
- describe('_setupRoute()', function () {
+ describe('_setupRoute()', () => {
- it('throws when route refers to nonexistent strategy', function (done) {
+ it('throws when route refers to nonexistent strategy', () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('a', 'custom', { users: { steve: {} } });
server.auth.strategy('b', 'custom', { users: { steve: {} } });
- expect(function () {
+ expect(() => {
server.route({
path: '/',
method: 'GET',
- config: {
+ options: {
auth: {
strategy: 'c'
},
- handler: function (request, reply) {
-
- return reply('ok');
- }
+ handler: () => 'ok'
}
});
- }).to.throw('Unknown authentication strategy: c in path: /');
-
- done();
+ }).to.throw('Unknown authentication strategy c in /');
});
});
- describe('lookup', function () {
-
- it('returns the route auth config', function (done) {
+ describe('lookup', () => {
- var handler = function (request, reply) {
+ it('returns the route auth config', async () => {
- return reply(request.connection.auth.lookup(request.route));
- };
-
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: {} } });
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) {
-
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.deep.equal({
- strategies: ['default'],
- mode: 'required'
- });
+ server.auth.strategy('default', 'custom', { users: { steve: {} } });
+ server.auth.default('default');
+ server.route({ method: 'GET', path: '/', handler: (request) => request.server.auth.lookup(request.route) });
- done();
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal({
+ strategies: ['default'],
+ mode: 'required'
});
});
});
- describe('authenticate()', function () {
+ describe('authenticate()', () => {
- it('setups route with optional authentication', function (done) {
+ it('setups route with optional authentication', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: {} } });
-
- var handler = function (request, reply) {
-
- return reply(!!request.auth.credentials);
- };
- server.route({ method: 'GET', path: '/', config: { handler: handler, auth: { mode: 'optional' } } });
-
- server.inject('/', function (res1) {
-
- expect(res1.statusCode).to.equal(200);
- expect(res1.payload).to.equal('false');
+ server.auth.strategy('default', 'custom', { users: { steve: {} } });
+ server.auth.default('default');
+ server.route({
+ method: 'GET',
+ path: '/',
+ options: {
+ handler: (request) => !!request.auth.credentials,
+ auth: {
+ mode: 'optional'
+ }
+ }
+ });
- server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res2) {
+ const res1 = await server.inject('/');
+ expect(res1.statusCode).to.equal(200);
+ expect(res1.payload).to.equal('false');
- expect(res2.statusCode).to.equal(200);
- expect(res2.payload).to.equal('true');
- done();
- });
- });
+ const res2 = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res2.statusCode).to.equal(200);
+ expect(res2.payload).to.equal('true');
});
- it('exposes mode', function (done) {
+ it('exposes mode', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: {} } });
+ server.auth.strategy('default', 'custom', { users: { steve: {} } });
+ server.auth.default('default');
server.route({
method: 'GET',
path: '/',
- handler: function (request, reply) {
-
- return reply(request.auth.mode);
- }
+ handler: (request) => request.auth.mode
});
- server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) {
-
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.equal('required');
- done();
- });
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('required');
});
- it('authenticates using multiple strategies', function (done) {
+ it('authenticates using multiple strategies', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('first', 'custom', { users: { steve: 'skip' } });
server.auth.strategy('second', 'custom', { users: { steve: {} } });
server.route({
method: 'GET',
path: '/',
- config: {
- handler: function (request, reply) {
-
- return reply(request.auth.strategy);
- },
+ options: {
+ handler: (request) => request.auth.strategy,
auth: {
strategies: ['first', 'second']
}
}
});
- server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) {
-
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.equal('second');
- done();
- });
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('second');
});
- it('authenticates using credentials object', function (done) {
+ it('authenticates using credentials object', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: { user: 'steve' } } });
-
- var doubleHandler = function (request, reply) {
-
- var options = { url: '/2', credentials: request.auth.credentials };
- server.inject(options, function (res) {
-
- return reply(res.result);
- });
- };
+ server.auth.strategy('default', 'custom', { users: { steve: { user: 'steve' } } });
+ server.auth.default('default');
- var handler = function (request, reply) {
+ const doubleHandler = async (request) => {
- return reply(request.auth.credentials.user);
+ const options = { url: '/2', auth: { credentials: request.auth.credentials, strategy: 'default' } };
+ const res = await server.inject(options);
+ return res.result;
};
server.route({ method: 'GET', path: '/1', handler: doubleHandler });
- server.route({ method: 'GET', path: '/2', handler: handler });
-
- server.inject({ url: '/1', headers: { authorization: 'Custom steve' } }, function (res) {
+ server.route({ method: 'GET', path: '/2', handler: (request) => request.auth.credentials.user });
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.equal('steve');
- done();
- });
+ const res = await server.inject({ url: '/1', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('steve');
});
- it('authenticates using credentials object (with artifacts)', function (done) {
+ it('authenticates using credentials object (with artifacts)', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: { user: 'steve' } } });
-
- var doubleHandler = function (request, reply) {
+ server.auth.strategy('default', 'custom', { users: { steve: { user: 'steve' } } });
+ server.auth.default('default');
- var options = { url: '/2', credentials: request.auth.credentials, artifacts: '!' };
- server.inject(options, function (res) {
+ const doubleHandler = async (request) => {
- return reply(res.result);
- });
+ const options = { url: '/2', auth: { credentials: request.auth.credentials, artifacts: '!', strategy: 'default' } };
+ const res = await server.inject(options);
+ return res.result;
};
- var handler = function (request, reply) {
+ const handler = (request) => {
- return reply(request.auth.credentials.user + request.auth.artifacts);
+ return request.auth.credentials.user + request.auth.artifacts;
};
server.route({ method: 'GET', path: '/1', handler: doubleHandler });
- server.route({ method: 'GET', path: '/2', handler: handler });
-
- server.inject({ url: '/1', headers: { authorization: 'Custom steve' } }, function (res) {
+ server.route({ method: 'GET', path: '/2', handler });
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.equal('steve!');
- done();
- });
+ const res = await server.inject({ url: '/1', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('steve!');
});
- it('authenticates a request with custom auth settings', function (done) {
-
- var handler = function (request, reply) {
+ it('authenticates a request with custom auth settings', async () => {
- return reply(request.auth.credentials.user);
- };
-
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: {} } });
+ server.auth.strategy('default', 'custom', { users: { steve: {} } });
+ server.auth.default('default');
server.route({
method: 'GET',
path: '/',
- config: {
- handler: handler,
+ options: {
+ handler: (request) => request.auth.credentials.user,
auth: {
strategy: 'default'
}
}
});
- server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) {
-
- expect(res.statusCode).to.equal(200);
- done();
- });
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(204);
});
- it('authenticates a request with auth strategy name config', function (done) {
-
- var handler = function (request, reply) {
+ it('authenticates a request with auth strategy name config', async () => {
- return reply(request.auth.credentials.user);
- };
-
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: {} } });
server.route({
method: 'GET',
path: '/',
- config: {
- handler: handler,
+ options: {
+ handler: (request) => request.auth.credentials.user,
auth: 'default'
}
});
- server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) {
-
- expect(res.statusCode).to.equal(200);
- done();
- });
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(204);
});
- it('tries to authenticate a request', function (done) {
+ it('tries to authenticate a request', async () => {
- var handler = function (request, reply) {
+ const handler = (request) => {
- return reply({ status: request.auth.isAuthenticated, error: request.auth.error });
+ return { status: request.auth.isAuthenticated, error: request.auth.error };
};
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', 'try', { users: { steve: {} } });
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject('/', function (res1) {
-
- expect(res1.statusCode).to.equal(200);
- expect(res1.result.status).to.equal(false);
- expect(res1.result.error.message).to.equal('Missing authentication');
+ server.auth.strategy('default', 'custom', { users: { steve: {} } });
+ server.auth.default({ strategy: 'default', mode: 'try' });
- server.inject({ url: '/', headers: { authorization: 'Custom john' } }, function (res2) {
+ server.route({ method: 'GET', path: '/', handler });
- expect(res2.statusCode).to.equal(200);
- expect(res2.result.status).to.equal(false);
- expect(res2.result.error.message).to.equal('Missing credentials');
+ const res1 = await server.inject('/');
+ expect(res1.statusCode).to.equal(200);
+ expect(res1.result.status).to.equal(false);
+ expect(res1.result.error.message).to.equal('Missing authentication');
- server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res3) {
+ const res2 = await server.inject({ url: '/', headers: { authorization: 'Custom john' } });
+ expect(res2.statusCode).to.equal(200);
+ expect(res2.result.status).to.equal(false);
+ expect(res2.result.error.message).to.equal('Missing credentials');
- expect(res3.statusCode).to.equal(200);
- expect(res3.result.status).to.equal(true);
- expect(res3.result.error).to.not.exist();
- done();
- });
- });
- });
+ const res3 = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res3.statusCode).to.equal(200);
+ expect(res3.result.status).to.equal(true);
+ expect(res3.result.error).to.not.exist();
});
- it('errors on invalid authenticate callback missing both error and credentials', function (done) {
-
- var handler = function (request, reply) {
-
- return reply(request.auth.credentials.user);
- };
+ it('errors on invalid authenticate callback missing both error and credentials', async () => {
- var server = new Hapi.Server({ debug: false });
- server.connection();
+ const server = Hapi.server({ debug: false });
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: {} } });
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject({ url: '/', headers: { authorization: 'Custom' } }, function (res) {
+ server.auth.strategy('default', 'custom', { users: { steve: {} } });
+ server.auth.default('default');
+ server.route({ method: 'GET', path: '/', handler: (request) => request.auth.credentials.user });
- expect(res.statusCode).to.equal(500);
- done();
- });
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom' } });
+ expect(res.statusCode).to.equal(500);
});
- it('logs error', function (done) {
-
- var handler = function (request, reply) {
-
- return reply(request.auth.credentials.user);
- };
+ it('logs error', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: {} } });
- server.route({ method: 'GET', path: '/', handler: handler });
+ server.auth.strategy('default', 'custom', { users: { steve: {} } });
+ server.auth.default('default');
+ server.route({ method: 'GET', path: '/', handler: (request) => request.auth.credentials.user });
- server.on('request-internal', function (request, event, tags) {
+ let logged = false;
+ server.events.on({ name: 'request', channels: 'internal' }, (request, event, tags) => {
if (tags.auth) {
- done();
+ logged = true;
}
});
- server.inject({ url: '/', headers: { authorization: 'Custom john' } }, function (res) {
-
- expect(res.statusCode).to.equal(401);
- });
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom john' } });
+ expect(res.statusCode).to.equal(401);
+ expect(logged).to.be.true();
});
- it('returns a non Error error response', function (done) {
-
- var handler = function (request, reply) {
+ it('returns a non Error error response', async () => {
- return reply(request.auth.credentials.user);
- };
-
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { message: 'in a bottle' } });
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject({ url: '/', headers: { authorization: 'Custom message' } }, function (res) {
+ server.auth.strategy('default', 'custom', { users: { message: 'in a bottle' } });
+ server.auth.default('default');
+ server.route({ method: 'GET', path: '/', handler: (request) => request.auth.credentials.user });
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.equal('in a bottle');
- done();
- });
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom message' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('in a bottle');
});
- it('handles errors thrown inside authenticate', function (done) {
+ it('passes non Error error response when set to try ', async () => {
- var server = new Hapi.Server({ debug: false });
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: 'throw' } });
-
- server.once('request-error', function (request, err) {
+ server.auth.strategy('default', 'custom', { users: { message: 'in a bottle' } });
+ server.auth.default({ strategy: 'default', mode: 'try' });
+ server.route({ method: 'GET', path: '/', handler: () => 'ok' });
- expect(err.message).to.equal('Uncaught error: Boom');
- });
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom message' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('in a bottle');
+ });
- var handler = function (request, reply) {
+ it('matches scope (array to single)', async () => {
- return reply('ok');
- };
+ const server = Hapi.server();
+ server.auth.scheme('custom', internals.implementation);
+ server.auth.strategy('default', 'custom', { users: { steve: { scope: ['one'] } } });
+ server.auth.default('default');
+ server.route({
+ method: 'GET',
+ path: '/',
+ options: {
+ handler: (request) => request.auth.credentials.user,
+ auth: {
+ scope: 'one'
+ }
+ }
+ });
- server.route({ method: 'GET', path: '/', handler: handler });
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(204);
+ });
- server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) {
+ it('matches scope (array to array)', async () => {
- expect(res.statusCode).to.equal(500);
- done();
+ const server = Hapi.server();
+ server.auth.scheme('custom', internals.implementation);
+ server.auth.strategy('default', 'custom', { users: { steve: { scope: ['one', 'two'] } } });
+ server.auth.default('default');
+ server.route({
+ method: 'GET',
+ path: '/',
+ options: {
+ handler: (request) => request.auth.credentials.user,
+ auth: {
+ scope: ['one', 'three']
+ }
+ }
});
- });
- it('passes non Error error response when set to try ', function (done) {
-
- var handler = function (request, reply) {
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(204);
+ });
- return reply('ok');
- };
+ it('matches scope (single to array)', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', 'try', { users: { message: 'in a bottle' } });
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject({ url: '/', headers: { authorization: 'Custom message' } }, function (res) {
-
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.equal('in a bottle');
- done();
+ server.auth.strategy('default', 'custom', { users: { steve: { scope: 'one' } } });
+ server.auth.default('default');
+ server.route({
+ method: 'GET',
+ path: '/',
+ options: {
+ handler: (request) => request.auth.credentials.user,
+ auth: {
+ scope: ['one', 'three']
+ }
+ }
});
- });
-
- it('matches scope (array to single)', function (done) {
- var handler = function (request, reply) {
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(204);
+ });
- return reply(request.auth.credentials.user);
- };
+ it('matches scope (single to single)', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: { scope: ['one'] } } });
+ server.auth.strategy('default', 'custom', { users: { steve: { scope: 'one' } } });
+ server.auth.default('default');
server.route({
method: 'GET',
path: '/',
- config: {
- handler: handler,
+ options: {
+ handler: (request) => request.auth.credentials.user,
auth: {
scope: 'one'
}
}
});
- server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) {
-
- expect(res.statusCode).to.equal(200);
- done();
- });
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(204);
});
- it('matches scope (array to array)', function (done) {
+ it('matches dynamic scope (single to single)', async () => {
- var handler = function (request, reply) {
+ const server = Hapi.server();
+ server.auth.scheme('custom', internals.implementation);
+ server.auth.strategy('default', 'custom', { users: { steve: { scope: 'one-test' } } });
+ server.auth.default('default');
+ server.route({
+ method: 'GET',
+ path: '/{id}',
+ options: {
+ handler: (request) => request.auth.credentials.user,
+ auth: {
+ scope: 'one-{params.id}'
+ }
+ }
+ });
- return reply(request.auth.credentials.user);
- };
+ const res = await server.inject({ url: '/test', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(204);
+ });
- var server = new Hapi.Server();
- server.connection();
+ it('matches multiple required dynamic scopes', async () => {
+
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: { scope: ['one', 'two'] } } });
+ server.auth.strategy('default', 'custom', { users: { steve: { scope: ['test', 'one-test'] } } });
+ server.auth.default('default');
server.route({
method: 'GET',
- path: '/',
- config: {
- handler: handler,
+ path: '/{id}',
+ options: {
+ handler: (request) => request.auth.credentials.user,
auth: {
- scope: ['one', 'three']
+ scope: ['+one-{params.id}', '+{params.id}']
}
}
});
- server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) {
+ const res = await server.inject({ url: '/test', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(204);
+ });
+
+ it('matches multiple required dynamic scopes (mixed types)', async () => {
- expect(res.statusCode).to.equal(200);
- done();
+ const server = Hapi.server();
+ server.auth.scheme('custom', internals.implementation);
+ server.auth.strategy('default', 'custom', { users: { steve: { scope: ['test', 'one-test'] } } });
+ server.auth.default('default');
+ server.route({
+ method: 'GET',
+ path: '/{id}',
+ options: {
+ handler: (request) => request.auth.credentials.user,
+ auth: {
+ scope: ['+one-{params.id}', '{params.id}']
+ }
+ }
});
+
+ const res = await server.inject({ url: '/test', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(204);
});
- it('matches scope (single to array)', function (done) {
+ it('matches dynamic scope with multiple parts (single to single)', async () => {
+
+ const server = Hapi.server();
+ server.auth.scheme('custom', internals.implementation);
+ server.auth.strategy('default', 'custom', { users: { steve: { scope: 'one-test-admin' } } });
+ server.auth.default('default');
+ server.route({
+ method: 'GET',
+ path: '/{id}/{role}',
+ options: {
+ handler: (request) => request.auth.credentials.user,
+ auth: {
+ scope: 'one-{params.id}-{params.role}'
+ }
+ }
+ });
- var handler = function (request, reply) {
+ const res = await server.inject({ url: '/test/admin', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(204);
+ });
- return reply(request.auth.credentials.user);
- };
+ it('does not match broken dynamic scope (single to single)', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: { scope: 'one' } } });
+ server.auth.strategy('default', 'custom', { users: { steve: { scope: 'one-test' } } });
+ server.auth.default('default');
server.route({
method: 'GET',
- path: '/',
- config: {
- handler: handler,
+ path: '/{id}',
+ options: {
+ handler: (request) => request.auth.credentials.user,
auth: {
- scope: ['one', 'three']
+ scope: 'one-params.id}'
}
}
});
- server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) {
+ server.ext('onPreResponse', (request, h) => {
- expect(res.statusCode).to.equal(200);
- done();
+ expect(request.response.data).to.contain(['got', 'need']);
+ return h.continue;
});
+
+ const res = await server.inject({ url: '/test', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(403);
+ expect(res.result.message).to.equal('Insufficient scope');
});
- it('matches scope (single to single)', function (done) {
+ it('does not match scope (single to single)', async () => {
- var handler = function (request, reply) {
+ const server = Hapi.server();
+ server.auth.scheme('custom', internals.implementation);
+ server.auth.strategy('default', 'custom', { users: { steve: { scope: 'one' } } });
+ server.auth.default('default');
+ server.route({
+ method: 'GET',
+ path: '/',
+ options: {
+ handler: (request) => request.auth.credentials.user,
+ auth: {
+ scope: 'onex'
+ }
+ }
+ });
- return reply(request.auth.credentials.user);
- };
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(403);
+ expect(res.result.message).to.equal('Insufficient scope');
+ });
+
+ it('matches modified scope', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: { scope: 'one' } } });
+ server.auth.strategy('default', 'custom', { users: { steve: { scope: 'two' } } });
+ server.auth.default('default');
server.route({
method: 'GET',
path: '/',
- config: {
- handler: handler,
+ options: {
+ handler: (request) => request.auth.credentials.user,
auth: {
scope: 'one'
}
}
});
- server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) {
+ server.ext('onCredentials', (request, h) => {
- expect(res.statusCode).to.equal(200);
- done();
+ request.auth.credentials.scope = 'one';
+ return h.continue;
});
+
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(204);
});
- it('matches dynamic scope (single to single)', function (done) {
+ it('errors on missing scope', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: { scope: 'one-test' } } });
+ server.auth.strategy('default', 'custom', { users: { steve: { scope: ['a'] } } });
+ server.auth.default('default');
server.route({
method: 'GET',
- path: '/{id}',
- config: {
- handler: function (request, reply) {
-
- return reply(request.auth.credentials.user);
- },
+ path: '/',
+ options: {
+ handler: (request) => request.auth.credentials.user,
auth: {
- scope: 'one-{params.id}'
+ scope: 'b'
}
}
});
- server.inject({ url: '/test', headers: { authorization: 'Custom steve' } }, function (res) {
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(403);
+ expect(res.result.message).to.equal('Insufficient scope');
+ });
+
+ it('errors on missing scope property', async () => {
- expect(res.statusCode).to.equal(200);
- done();
+ const server = Hapi.server();
+ server.auth.scheme('custom', internals.implementation);
+ server.auth.strategy('default', 'custom', { users: { steve: {} } });
+ server.auth.default('default');
+ server.route({
+ method: 'GET',
+ path: '/',
+ options: {
+ handler: (request) => request.auth.credentials.user,
+ auth: {
+ scope: 'b'
+ }
+ }
});
+
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(403);
+ expect(res.result.message).to.equal('Insufficient scope');
});
- it('matches dynamic scope with multiple parts (single to single)', function (done) {
+ it('validates required scope', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: { scope: 'one-test-admin' } } });
+ server.auth.strategy('default', 'custom', {
+ users: {
+ steve: { scope: ['a', 'b'] },
+ john: { scope: ['a', 'b', 'c'] }
+ }
+ });
+
+ server.auth.default('default');
+
server.route({
method: 'GET',
- path: '/{id}/{role}',
- config: {
- handler: function (request, reply) {
-
- return reply(request.auth.credentials.user);
- },
+ path: '/',
+ options: {
+ handler: (request) => request.auth.credentials.user,
auth: {
- scope: 'one-{params.id}-{params.role}'
+ scope: ['+c', 'b']
}
}
});
- server.inject({ url: '/test/admin', headers: { authorization: 'Custom steve' } }, function (res) {
+ const res1 = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res1.statusCode).to.equal(403);
+ expect(res1.result.message).to.equal('Insufficient scope');
- expect(res.statusCode).to.equal(200);
- done();
- });
+ const res2 = await server.inject({ url: '/', headers: { authorization: 'Custom john' } });
+ expect(res2.statusCode).to.equal(204);
});
- it('does not match broken dynamic scope (single to single)', function (done) {
+ it('validates forbidden scope', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: { scope: 'one-test' } } });
+ server.auth.strategy('default', 'custom', {
+ users: {
+ steve: { scope: ['a', 'b'] },
+ john: { scope: ['b', 'c'] }
+ }
+ });
+
+ server.auth.default('default');
+
server.route({
method: 'GET',
- path: '/{id}',
- config: {
- handler: function (request, reply) {
-
- return reply(request.auth.credentials.user);
- },
+ path: '/',
+ options: {
+ handler: (request) => request.auth.credentials.user,
auth: {
- scope: 'one-params.id}'
+ scope: ['!a', 'b']
}
}
});
- server.inject({ url: '/test', headers: { authorization: 'Custom steve' } }, function (res) {
+ const res1 = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res1.statusCode).to.equal(403);
+ expect(res1.result.message).to.equal('Insufficient scope');
- expect(res.statusCode).to.equal(403);
- done();
- });
+ const res2 = await server.inject({ url: '/', headers: { authorization: 'Custom john' } });
+ expect(res2.statusCode).to.equal(204);
});
- it('does not match scope (single to single)', function (done) {
+ it('validates complex scope', async () => {
- var handler = function (request, reply) {
+ const server = Hapi.server();
+ server.auth.scheme('custom', internals.implementation);
+ server.auth.strategy('default', 'custom', {
+ users: {
+ steve: { scope: ['a', 'b', 'c'] },
+ john: { scope: ['b', 'c'] },
+ mary: { scope: ['b', 'd'] },
+ lucy: { scope: 'b' },
+ larry: { scope: ['c', 'd'] }
+ }
+ });
- return reply(request.auth.credentials.user);
- };
+ server.auth.default('default');
- var server = new Hapi.Server();
- server.connection();
- server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: { scope: 'one' } } });
server.route({
method: 'GET',
path: '/',
- config: {
- handler: handler,
+ options: {
+ handler: (request) => request.auth.credentials.user,
auth: {
- scope: 'onex'
+ scope: ['!a', '+b', 'c', 'd']
}
}
});
- server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) {
+ const res1 = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res1.statusCode).to.equal(403);
+ expect(res1.result.message).to.equal('Insufficient scope');
- expect(res.statusCode).to.equal(403);
- done();
- });
- });
+ const res2 = await server.inject({ url: '/', headers: { authorization: 'Custom john' } });
+ expect(res2.statusCode).to.equal(204);
- it('errors on missing scope', function (done) {
+ const res3 = await server.inject({ url: '/', headers: { authorization: 'Custom mary' } });
+ expect(res3.statusCode).to.equal(204);
- var handler = function (request, reply) {
+ const res4 = await server.inject({ url: '/', headers: { authorization: 'Custom lucy' } });
+ expect(res4.statusCode).to.equal(403);
+ expect(res4.result.message).to.equal('Insufficient scope');
- return reply(request.auth.credentials.user);
- };
+ const res5 = await server.inject({ url: '/', headers: { authorization: 'Custom larry' } });
+ expect(res5.statusCode).to.equal(403);
+ expect(res5.result.message).to.equal('Insufficient scope');
+ });
+
+ it('errors on missing scope using arrays', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: { scope: ['a'] } } });
+ server.auth.strategy('default', 'custom', { users: { steve: { scope: ['a', 'b'] } } });
+ server.auth.default('default');
server.route({
method: 'GET',
path: '/',
- config: {
- handler: handler,
+ options: {
+ handler: (request) => request.auth.credentials.user,
auth: {
- scope: 'b'
+ scope: ['c', 'd']
}
}
});
- server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) {
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(403);
+ expect(res.result.message).to.equal('Insufficient scope');
+ });
- expect(res.statusCode).to.equal(403);
- done();
+ it('uses default scope when no scope override is set', async () => {
+
+ const server = Hapi.server();
+ server.auth.scheme('custom', internals.implementation);
+ server.auth.strategy('a', 'custom', { users: { steve: { scope: ['two'] } } });
+ server.auth.default({
+ strategy: 'a',
+ access: {
+ scope: 'one'
+ }
});
- });
- it('errors on missing scope property', function (done) {
+ server.route({
+ path: '/',
+ method: 'GET',
+ options: {
+ auth: {
+ mode: 'required'
+ },
+ handler: () => 'ok'
+ }
+ });
- var handler = function (request, reply) {
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(403);
+ expect(res.result.message).to.equal('Insufficient scope');
+ });
- return reply(request.auth.credentials.user);
- };
+ it('ignores default scope when override set to null', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: {} } });
+ server.auth.strategy('default', 'custom', { users: { steve: {} } });
+ server.auth.default({
+ strategy: 'default',
+ scope: 'one'
+ });
+
server.route({
method: 'GET',
path: '/',
- config: {
- handler: handler,
+ options: {
+ handler: (request) => request.auth.credentials.user,
auth: {
- scope: 'b'
+ scope: false
}
}
});
- server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) {
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(204);
+ });
+
+ it('matches scope (access single)', async () => {
- expect(res.statusCode).to.equal(403);
- done();
+ const server = Hapi.server();
+ server.auth.scheme('custom', internals.implementation);
+ server.auth.strategy('default', 'custom', { users: { steve: { scope: ['one'] } } });
+ server.auth.default('default');
+ server.route({
+ method: 'GET',
+ path: '/',
+ options: {
+ handler: (request) => request.auth,
+ auth: {
+ access: {
+ scope: 'one'
+ }
+ }
+ }
});
+
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal({
+ isAuthenticated: true,
+ isAuthorized: true,
+ isInjected: false,
+ credentials: { scope: ['one'], user: null },
+ artifacts: undefined,
+ strategy: 'default',
+ mode: 'required',
+ error: null
+ }, { symbols: false });
});
- it('errors on missing scope using arrays', function (done) {
+ it('matches scope (access array)', async () => {
- var handler = function (request, reply) {
+ const server = Hapi.server();
+ server.auth.scheme('custom', internals.implementation);
+ server.auth.strategy('default', 'custom', { users: { steve: { scope: ['one'] } } });
+ server.auth.default('default');
+ server.route({
+ method: 'GET',
+ path: '/',
+ options: {
+ handler: (request) => request.auth.credentials.user,
+ auth: {
+ access: [
+ { scope: 'other' },
+ { scope: 'one' }
+ ]
+ }
+ }
+ });
- return reply(request.auth.credentials.user);
- };
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(204);
+ });
- var server = new Hapi.Server();
- server.connection();
+ it('errors on matching scope (access array)', async () => {
+
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: { scope: ['a', 'b'] } } });
+ server.auth.strategy('default', 'custom', { users: { steve: { scope: ['one'] } } });
+ server.auth.default('default');
server.route({
method: 'GET',
path: '/',
- config: {
- handler: handler,
+ options: {
+ handler: (request) => request.auth.credentials.user,
auth: {
- scope: ['c', 'd']
+ access: [
+ { scope: 'two' },
+ { scope: 'three' },
+ { entity: 'user', scope: 'one' },
+ { entity: 'app', scope: 'four' }
+ ]
}
}
});
- server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) {
-
- expect(res.statusCode).to.equal(403);
- done();
- });
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(403);
+ expect(res.result.message).to.equal('Insufficient scope');
});
- it('ignores default scope when override set to null', function (done) {
+ it('matches any entity', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', { users: { steve: {} } });
- server.auth.default({
- strategy: 'default',
- scope: 'one'
- });
-
+ server.auth.strategy('default', 'custom', { users: { steve: { user: 'steve' } } });
+ server.auth.default('default');
server.route({
method: 'GET',
path: '/',
- config: {
- handler: function (request, reply) {
-
- return reply(request.auth.credentials.user);
- },
+ options: {
+ handler: () => null,
auth: {
- scope: false
+ entity: 'any'
}
}
});
- server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) {
-
- expect(res.statusCode).to.equal(200);
- done();
- });
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(204);
});
- it('matches user entity', function (done) {
+ it('matches user entity', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: { user: 'steve' } } });
+ server.auth.strategy('default', 'custom', { users: { steve: { user: 'steve' } } });
+ server.auth.default('default');
server.route({
method: 'GET',
path: '/',
- config: {
- handler: function (request, reply) {
-
- return reply(request.auth.credentials.user);
- },
+ options: {
+ handler: () => null,
auth: {
entity: 'user'
}
}
});
- server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) {
-
- expect(res.statusCode).to.equal(200);
- done();
- });
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(204);
});
- it('errors on missing user entity', function (done) {
+ it('errors on missing user entity', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { client: {} } });
+ server.auth.strategy('default', 'custom', { users: { client: {} } });
+ server.auth.default('default');
server.route({
method: 'GET',
path: '/',
- config: {
- handler: function (request, reply) {
-
- return reply(request.auth.credentials.user);
- },
+ options: {
+ handler: () => null,
auth: {
entity: 'user'
}
}
});
- server.inject({ url: '/', headers: { authorization: 'Custom client' } }, function (res) {
-
- expect(res.statusCode).to.equal(403);
- done();
- });
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom client' } });
+ expect(res.statusCode).to.equal(403);
+ expect(res.result.message).to.equal('Application credentials cannot be used on a user endpoint');
});
- it('matches app entity', function (done) {
+ it('matches app entity', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { client: {} } });
+ server.auth.strategy('default', 'custom', { users: { client: {} } });
+ server.auth.default('default');
server.route({
method: 'GET',
path: '/',
- config: {
- handler: function (request, reply) {
-
- return reply(request.auth.credentials.user);
- },
+ options: {
+ handler: () => null,
auth: {
entity: 'app'
}
}
});
- server.inject({ url: '/', headers: { authorization: 'Custom client' } }, function (res) {
-
- expect(res.statusCode).to.equal(200);
- done();
- });
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom client' } });
+ expect(res.statusCode).to.equal(204);
});
- it('errors on missing app entity', function (done) {
+ it('errors on missing app entity', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: { user: 'steve' } } });
+ server.auth.strategy('default', 'custom', { users: { steve: { user: 'steve' } } });
+ server.auth.default('default');
server.route({
method: 'GET',
path: '/',
- config: {
- handler: function (request, reply) {
-
- return reply(request.auth.credentials.user);
- },
+ options: {
+ handler: () => null,
auth: {
entity: 'app'
}
}
});
- server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) {
-
- expect(res.statusCode).to.equal(403);
- done();
- });
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(403);
+ expect(res.result.message).to.equal('User credentials cannot be used on an application endpoint');
});
- it('logs error code when authenticate returns a non-error error', function (done) {
+ it('logs error code when authenticate returns a non-error error', async () => {
- var server = new Hapi.Server();
- server.connection();
- server.auth.scheme('test', function (srv, options) {
+ const server = Hapi.server();
+ server.auth.scheme('test', (srv, options) => {
return {
- authenticate: function (request, reply) {
-
- return reply('Redirecting ...').redirect('/test');
- }
+ authenticate: (request, h) => h.response('Redirecting ...').redirect('/test').takeover()
};
});
- server.auth.strategy('test', 'test', true, {});
+ server.auth.strategy('test', 'test', {});
+ server.auth.default('test');
server.route({
method: 'GET',
path: '/',
- handler: function (request, reply) {
-
- return reply('test');
- }
+ handler: () => 'test'
});
- var result;
- server.on('request-internal', function (request, event, tags) {
+ let logged = null;
+ server.events.on({ name: 'request', channels: 'internal' }, (request, event, tags) => {
if (tags.unauthenticated) {
- result = event.data;
+ logged = event;
}
});
- server.inject('/', function (res) {
-
- expect(result).to.equal(302);
- done();
- });
+ await server.inject('/');
+ expect(logged.data).to.equal({ statusCode: 302 });
});
- it('passes the options.artifacts object, even with an auth filter', function (done) {
+ it('passes the options.artifacts object, even with an auth filter', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: {} } });
+ server.auth.strategy('default', 'custom', { users: { steve: {} } });
+ server.auth.default('default');
server.route({
method: 'GET',
path: '/',
- config: {
- handler: function (request, reply) {
-
- return reply(request.auth.artifacts);
- },
+ options: {
+ handler: (request) => request.auth.artifacts,
auth: 'default'
}
});
- var options = {
+ const options = {
url: '/',
headers: { authorization: 'Custom steve' },
- credentials: { foo: 'bar' },
- artifacts: { bar: 'baz' }
+ auth: {
+ credentials: { foo: 'bar' },
+ artifacts: { bar: 'baz' },
+ strategy: 'default'
+ }
};
- server.inject(options, function (res) {
+ const res = await server.inject(options);
+ expect(res.statusCode).to.equal(200);
+ expect(res.result.bar).to.equal('baz');
+ });
- expect(res.statusCode).to.equal(200);
- expect(res.result.bar).to.equal('baz');
- done();
- });
+ it('errors on empty authenticate()', async () => {
+ const scheme = () => {
+ return { authenticate: (request, h) => h.authenticated() };
+ };
+
+ const server = Hapi.server({ debug: false });
+ server.auth.scheme('custom', scheme);
+ server.auth.strategy('default', 'custom');
+ server.auth.default('default');
+ server.route({ method: 'GET', path: '/', handler: () => null });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
+ });
+
+ it('passes credentials on unauthenticated() in try mode', async () => {
+
+ const scheme = () => {
+
+ return { authenticate: (request, h) => h.unauthenticated(Boom.unauthorized(), { credentials: { user: 'steve' } }) };
+ };
+
+ const server = Hapi.server();
+ server.ext('onPreResponse', (request, h) => {
+
+ if (request.auth.credentials.user === 'steve') {
+ return h.continue;
+ }
+ });
+
+ server.auth.scheme('custom', scheme);
+ server.auth.strategy('default', 'custom');
+ server.auth.default({ strategy: 'default', mode: 'try' });
+ server.route({ method: 'GET', path: '/', handler: () => null });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(204);
});
+ it('passes strategy, credentials, artifacts, error on unauthenticated() in required mode', async () => {
+
+ const scheme = () => {
+
+ return { authenticate: (request, h) => h.unauthenticated(Boom.unauthorized(), { credentials: { user: 'steve' }, artifacts: '!' }) };
+ };
+
+ const server = Hapi.server();
+ server.ext('onPreResponse', (request, h) => {
+
+ if (request.auth.credentials.user === 'steve') {
+ return h.continue;
+ }
+ });
+
+ server.ext('onPreResponse', (request, h) => {
+
+ expect(request.auth.credentials).to.equal({ user: 'steve' });
+ expect(request.auth.artifacts).to.equal('!');
+ expect(request.auth.strategy).to.equal('default');
+ expect(request.auth.error.message).to.equal('Unauthorized');
+ return h.continue;
+ });
+
+ server.auth.scheme('custom', scheme);
+ server.auth.strategy('default', 'custom');
+ server.auth.default('default', { mode: 'required' });
+
+ server.route({ method: 'GET', path: '/', handler: () => null });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(401);
+ });
});
- describe('payload()', function () {
+ describe('verify()', () => {
- it('authenticates request payload', function (done) {
+ it('verifies an authenticated request', async () => {
+
+ const implementation = (...args) => {
+
+ const imp = internals.implementation(...args);
+ imp.verify = async (auth) => {
+
+ await Hoek.wait(1);
+ if (auth.credentials.user !== 'steve') {
+ throw Boom.unauthorized('Invalid');
+ }
+ };
+
+ return imp;
+ };
+
+ const server = Hapi.server();
+ server.auth.scheme('custom', implementation);
+ server.auth.strategy('default', 'custom', { users: { steve: { user: 'steve' }, john: { user: 'john' } } });
- var server = new Hapi.Server();
- server.connection();
- server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { validPayload: { payload: null } } });
server.route({
- method: 'POST',
+ method: 'GET',
path: '/',
- config: {
- handler: function (request, reply) {
-
- return reply(request.auth.credentials.user);
- },
+ options: {
auth: {
- payload: 'required'
+ mode: 'try',
+ strategy: 'default'
+ },
+ handler: async (request) => {
+
+ if (request.auth.error &&
+ request.auth.error.message === 'Missing authentication') {
+
+ request.auth.error = null;
+ }
+
+ return await server.auth.verify(request) || 'ok';
}
}
});
- server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom validPayload' } }, function (res) {
+ const res1 = await server.inject('/');
+ expect(res1.result).to.equal('ok');
- expect(res.statusCode).to.equal(200);
- done();
- });
+ const res2 = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res2.result).to.equal('ok');
+
+ const res3 = await server.inject({ url: '/', headers: { authorization: 'Custom unknown' } });
+ expect(res3.result.message).to.equal('Missing credentials');
+
+ const res4 = await server.inject({ url: '/', auth: { credentials: {}, strategy: 'default' } });
+ expect(res4.result.message).to.equal('Invalid');
+
+ const res5 = await server.inject({ url: '/', auth: { credentials: { user: 'steve' }, strategy: 'default' } });
+ expect(res5.result).to.equal('ok');
+
+ const res6 = await server.inject({ url: '/', headers: { authorization: 'Custom john' } });
+ expect(res6.result.message).to.equal('Invalid');
});
- it('skips when scheme does not support it', function (done) {
+ it('skips when verify unsupported', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { validPayload: { payload: null } }, payload: false });
+ server.auth.strategy('default', 'custom', { users: { steve: { user: 'steve' } } });
+
server.route({
- method: 'POST',
+ method: 'GET',
path: '/',
- config: {
- handler: function (request, reply) {
+ options: {
+ auth: {
+ mode: 'try',
+ strategy: 'default'
+ },
+ handler: async (request) => {
- return reply(request.auth.credentials.user);
+ return await server.auth.verify(request) || 'ok';
}
}
});
- server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom validPayload' } }, function (res) {
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.result).to.equal('ok');
+ });
+ });
+
+ describe('access()', () => {
+
+ it('skips access when unauthenticated and mode is not required', async () => {
- expect(res.statusCode).to.equal(200);
- done();
+ const server = Hapi.server();
+ server.auth.scheme('custom', internals.implementation);
+ server.auth.strategy('default', 'custom', { users: { steve: { scope: ['one'] } } });
+ server.auth.default('default');
+ server.route({
+ method: 'GET',
+ path: '/',
+ options: {
+ handler: (request) => request.auth,
+ auth: {
+ mode: 'optional',
+ access: {
+ scope: 'one'
+ }
+ }
+ }
});
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.result.isAuthenticated).to.be.false();
+ expect(res.result.isAuthorized).to.be.false();
});
+ });
- it('authenticates request payload (required scheme)', function (done) {
+ describe('payload()', () => {
- var server = new Hapi.Server();
- server.connection();
+ it('authenticates request payload', async () => {
+
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { validPayload: { payload: null } }, options: { payload: true } });
+ server.auth.strategy('default', 'custom', { users: { validPayload: { payload: null } } });
+ server.auth.default('default');
server.route({
method: 'POST',
path: '/',
- config: {
- handler: function (request, reply) {
-
- return reply(request.auth.credentials.user);
- },
- auth: {}
+ options: {
+ handler: (request) => request.auth.credentials.user,
+ auth: {
+ payload: 'required'
+ }
}
});
- server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom validPayload' } }, function (res) {
+ const res = await server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom validPayload' } });
+ expect(res.statusCode).to.equal(204);
+ });
+
+ it('skips when scheme does not support it', async () => {
- expect(res.statusCode).to.equal(200);
- done();
+ const server = Hapi.server();
+ server.auth.scheme('custom', internals.implementation);
+ server.auth.strategy('default', 'custom', { users: { validPayload: { payload: null } }, payload: false });
+ server.auth.default('default');
+ server.route({
+ method: 'POST',
+ path: '/',
+ options: {
+ handler: (request) => request.auth.credentials.user
+ }
});
+
+ const res = await server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom validPayload' } });
+ expect(res.statusCode).to.equal(204);
});
- it('authenticates request payload (required scheme and required route)', function (done) {
+ it('authenticates request payload (required scheme)', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { validPayload: { payload: null } }, options: { payload: true } });
+ server.auth.strategy('default', 'custom', { users: { validPayload: { payload: null } }, options: { payload: true } });
+ server.auth.default('default');
server.route({
method: 'POST',
path: '/',
- config: {
- handler: function (request, reply) {
+ options: {
+ handler: (request) => request.auth.credentials.user,
+ auth: {}
+ }
+ });
- return reply(request.auth.credentials.user);
- },
+ const res = await server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom validPayload' } });
+ expect(res.statusCode).to.equal(204);
+ });
+
+ it('authenticates request payload (required scheme and required route)', async () => {
+
+ const server = Hapi.server();
+ server.auth.scheme('custom', internals.implementation);
+ server.auth.strategy('default', 'custom', { users: { validPayload: { payload: null } }, options: { payload: true } });
+ server.auth.default('default');
+ server.route({
+ method: 'POST',
+ path: '/',
+ options: {
+ handler: (request) => request.auth.credentials.user,
auth: {
payload: true
}
}
});
- server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom validPayload' } }, function (res) {
-
- expect(res.statusCode).to.equal(200);
- done();
- });
+ const res = await server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom validPayload' } });
+ expect(res.statusCode).to.equal(204);
});
- it('throws when scheme requires payload authentication and route conflicts', function (done) {
+ it('throws when scheme requires payload authentication and route conflicts', () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { validPayload: { payload: null } }, options: { payload: true } });
- expect(function () {
+ server.auth.strategy('default', 'custom', { users: { validPayload: { payload: null } }, options: { payload: true } });
+ server.auth.default('default');
+ expect(() => {
server.route({
method: 'POST',
path: '/',
- config: {
- handler: function (request, reply) {
-
- return reply(request.auth.credentials.user);
- },
+ options: {
+ handler: (request) => request.auth.credentials.user,
auth: {
payload: 'optional'
}
}
});
- }).to.throw('Cannot set authentication payload to optional when a strategy requires payload validation /');
- done();
+ }).to.throw('Cannot set authentication payload to optional when a strategy requires payload validation in /');
});
- it('throws when strategy does not support payload authentication', function (done) {
+ it('throws when strategy does not support payload authentication', () => {
- var server = new Hapi.Server();
- server.connection();
- var implementation = function () {
+ const server = Hapi.server();
+ const implementation = function () {
return { authenticate: internals.implementation().authenticate };
};
server.auth.scheme('custom', implementation);
- server.auth.strategy('default', 'custom', true, {});
- expect(function () {
+ server.auth.strategy('default', 'custom', {});
+ server.auth.default('default');
+ expect(() => {
server.route({
method: 'POST',
path: '/',
- config: {
- handler: function (request, reply) {
-
- return reply(request.auth.credentials.user);
- },
+ options: {
+ handler: (request) => request.auth.credentials.user,
auth: {
payload: 'required'
}
}
});
- }).to.throw('Payload validation can only be required when all strategies support it in path: /');
- done();
+ }).to.throw('Payload validation can only be required when all strategies support it in /');
});
- it('throws when no strategy supports optional payload authentication', function (done) {
+ it('throws when no strategy supports optional payload authentication', () => {
- var server = new Hapi.Server();
- server.connection();
- var implementation = function () {
+ const server = Hapi.server();
+ const implementation = function () {
return { authenticate: internals.implementation().authenticate };
};
server.auth.scheme('custom', implementation);
- server.auth.strategy('default', 'custom', true, {});
- expect(function () {
+ server.auth.strategy('default', 'custom', {});
+ server.auth.default('default');
+ expect(() => {
server.route({
method: 'POST',
path: '/',
- config: {
- handler: function (request, reply) {
-
- return reply(request.auth.credentials.user);
- },
+ options: {
+ handler: (request) => request.auth.credentials.user,
auth: {
payload: 'optional'
}
}
});
- }).to.throw('Payload authentication requires at least one strategy with payload support in path: /');
- done();
+ }).to.throw('Payload authentication requires at least one strategy with payload support in /');
});
- it('allows one strategy to supports optional payload authentication while another does not', function (done) {
+ it('allows one strategy to supports optional payload authentication while another does not', async () => {
- var server = new Hapi.Server();
- server.connection();
- var implementation = function () {
+ const server = Hapi.server();
+ const implementation = function (...args) {
- return { authenticate: internals.implementation().authenticate };
+ return { authenticate: internals.implementation(...args).authenticate };
};
server.auth.scheme('custom1', implementation);
- server.auth.scheme('custom2', internals.implementation);
- server.auth.strategy('default1', 'custom1', {});
+ server.auth.scheme('custom2', internals.implementation, { users: {} });
+ server.auth.strategy('default1', 'custom1', { users: { steve: { user: 'steve' } } });
server.auth.strategy('default2', 'custom2', {});
- expect(function () {
- server.route({
- method: 'POST',
- path: '/',
- config: {
- handler: function (request, reply) {
-
- return reply(request.auth.credentials.user);
- },
- auth: {
- strategies: ['default2', 'default1'],
- payload: 'optional'
- }
+ server.route({
+ method: 'POST',
+ path: '/',
+ options: {
+ handler: (request) => request.auth.credentials.user,
+ auth: {
+ strategies: ['default1', 'default2'],
+ payload: 'optional'
}
- });
- }).to.not.throw();
- done();
+ }
+ });
+
+ const res = await server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(200);
});
- it('skips request payload by default', function (done) {
+ it('skips request payload by default', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { skip: {} } });
+ server.auth.strategy('default', 'custom', { users: { skip: {} } });
+ server.auth.default('default');
server.route({
method: 'POST',
path: '/',
- config: {
- handler: function (request, reply) {
-
- return reply(request.auth.credentials.user);
- }
+ options: {
+ handler: (request) => request.auth.credentials.user
}
});
- server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom skip' } }, function (res) {
-
- expect(res.statusCode).to.equal(200);
- done();
- });
+ const res = await server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom skip' } });
+ expect(res.statusCode).to.equal(204);
});
- it('skips request payload when unauthenticated', function (done) {
+ it('skips request payload when unauthenticated', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { skip: {} } });
+ server.auth.strategy('default', 'custom', { users: { skip: {} } });
+ server.auth.default('default');
server.route({
method: 'POST',
path: '/',
- config: {
- handler: function (request, reply) {
-
- return reply();
- },
+ options: {
+ handler: () => null,
auth: {
mode: 'try',
payload: 'required'
@@ -1476,181 +1775,166 @@ describe('authentication', function () {
}
});
- server.inject({ method: 'POST', url: '/' }, function (res) {
-
- expect(res.statusCode).to.equal(200);
- done();
- });
+ const res = await server.inject({ method: 'POST', url: '/' });
+ expect(res.statusCode).to.equal(204);
});
- it('skips optional payload', function (done) {
+ it('skips optional payload', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { optionalPayload: { payload: Boom.unauthorized(null, 'Custom') } } });
+ server.auth.strategy('default', 'custom', { users: { optionalPayload: { payload: Boom.unauthorized(null, 'Custom') } } });
+ server.auth.default('default');
server.route({
method: 'POST',
path: '/',
- config: {
- handler: function (request, reply) {
-
- return reply(request.auth.credentials.user);
- },
+ options: {
+ handler: (request) => request.auth.credentials.user,
auth: {
payload: 'optional'
}
}
});
- server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom optionalPayload' } }, function (res) {
+ const res = await server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom optionalPayload' } });
+ expect(res.statusCode).to.equal(204);
+ });
+
+ it('skips required payload authentication when disabled on injection', async () => {
- expect(res.statusCode).to.equal(200);
- done();
+ const server = Hapi.server();
+ server.auth.scheme('custom', internals.implementation);
+ server.auth.strategy('default', 'custom');
+ server.auth.default('default');
+ server.route({
+ method: 'POST',
+ path: '/',
+ options: {
+ handler: (request) => null,
+ auth: {
+ mode: 'try',
+ payload: true
+ }
+ }
});
+
+ const res = await server.inject({ method: 'POST', url: '/', auth: { credentials: { payload: Boom.internal('payload error') }, payload: false, strategy: 'default' } });
+ expect(res.statusCode).to.equal(204);
});
- it('errors on missing payload when required', function (done) {
+ it('errors on missing payload when required', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { optionalPayload: { payload: Boom.unauthorized(null, 'Custom') } } });
+ server.auth.strategy('default', 'custom', { users: { optionalPayload: { payload: Boom.unauthorized(null, 'Custom') } } });
+ server.auth.default('default');
server.route({
method: 'POST',
path: '/',
- config: {
- handler: function (request, reply) {
-
- return reply(request.auth.credentials.user);
- },
+ options: {
+ handler: (request) => request.auth.credentials.user,
auth: {
payload: 'required'
}
}
});
- server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom optionalPayload' } }, function (res) {
-
- expect(res.statusCode).to.equal(401);
- done();
- });
+ const res = await server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom optionalPayload' } });
+ expect(res.statusCode).to.equal(401);
});
- it('errors on invalid payload auth when required', function (done) {
+ it('errors on invalid payload auth when required', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { optionalPayload: { payload: Boom.unauthorized() } } });
+ server.auth.strategy('default', 'custom', { users: { optionalPayload: { payload: Boom.unauthorized() } } });
+ server.auth.default('default');
server.route({
method: 'POST',
path: '/',
- config: {
- handler: function (request, reply) {
-
- return reply(request.auth.credentials.user);
- },
+ options: {
+ handler: (request) => request.auth.credentials.user,
auth: {
payload: 'required'
}
}
});
- server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom optionalPayload' } }, function (res) {
-
- expect(res.statusCode).to.equal(401);
- done();
- });
+ const res = await server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom optionalPayload' } });
+ expect(res.statusCode).to.equal(401);
});
- it('errors on invalid request payload (non error)', function (done) {
+ it('errors on invalid request payload (non error)', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { invalidPayload: { payload: 'Payload is invalid' } } });
+ server.auth.strategy('default', 'custom', { users: { invalidPayload: { payload: 'Payload is invalid' } } });
+ server.auth.default('default');
server.route({
method: 'POST',
path: '/',
- config: {
- handler: function (request, reply) {
-
- return reply(request.auth.credentials.user);
- },
+ options: {
+ handler: (request) => request.auth.credentials.user,
auth: {
payload: 'required'
}
}
});
- server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom invalidPayload' } }, function (res) {
-
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.equal('Payload is invalid');
- done();
- });
+ const res = await server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom invalidPayload' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('Payload is invalid');
});
});
- describe('response()', function () {
+ describe('response()', () => {
- it('fails on response error', function (done) {
+ it('fails on response error', async () => {
- var handler = function (request, reply) {
-
- return reply(request.auth.credentials.user);
- };
-
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', true, { users: { steve: { response: Boom.internal() } } });
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res) {
+ server.auth.strategy('default', 'custom', { users: { steve: { response: Boom.internal() } } });
+ server.auth.default('default');
+ server.route({ method: 'GET', path: '/', handler: (request) => request.auth.credentials.user });
- expect(res.statusCode).to.equal(500);
- done();
- });
+ const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res.statusCode).to.equal(500);
});
});
- describe('test()', function () {
-
- it('tests a request', function (done) {
-
- var handler = function (request, reply) {
+ describe('test()', () => {
- request.server.auth.test('default', request, function (err, credentials) {
+ it('tests a request', async () => {
- if (err) {
- return reply({ status: false });
- }
+ const handler = async (request) => {
- return reply({ status: true, user: credentials.name });
- });
+ try {
+ const { credentials, artifacts } = await request.server.auth.test('default', request);
+ return { status: true, user: credentials.name, artifacts };
+ }
+ catch (err) {
+ return { status: false };
+ }
};
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
- server.auth.strategy('default', 'custom', { users: { steve: { name: 'steve' } } });
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject('/', function (res1) {
-
- expect(res1.statusCode).to.equal(200);
- expect(res1.result.status).to.equal(false);
-
- server.inject({ url: '/', headers: { authorization: 'Custom steve' } }, function (res2) {
-
- expect(res2.statusCode).to.equal(200);
- expect(res2.result.status).to.equal(true);
- expect(res2.result.user).to.equal('steve');
- done();
- });
- });
+ server.auth.strategy('default', 'custom', { users: { steve: { name: 'steve' }, skip: 'skip' }, artifacts: {} });
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res1 = await server.inject('/');
+ expect(res1.statusCode).to.equal(200);
+ expect(res1.result.status).to.be.false();
+
+ const res2 = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
+ expect(res2.statusCode).to.equal(200);
+ expect(res2.result.status).to.be.true();
+ expect(res2.result.user).to.equal('steve');
+ expect(res2.result.artifacts).to.equal({});
+
+ const res3 = await server.inject({ url: '/', headers: { authorization: 'Custom skip' } });
+ expect(res3.statusCode).to.equal(200);
+ expect(res3.result.status).to.be.false();
});
});
});
@@ -1658,76 +1942,71 @@ describe('authentication', function () {
internals.implementation = function (server, options) {
- var settings = Hoek.clone(options);
+ const settings = Hoek.clone(options);
if (settings &&
settings.route) {
- server.route({
- method: 'GET',
- path: '/',
- handler: function (request, reply) {
-
- return reply(request.auth.credentials.user);
- }
- });
+ server.route({ method: 'GET', path: '/', handler: (request) => (request.auth.credentials.user || null) });
}
- var scheme = {
- authenticate: function (request, reply) {
+ const scheme = {
+ authenticate: (request, h) => {
- var req = request.raw.req;
- var authorization = req.headers.authorization;
+ const req = request.raw.req;
+ const authorization = req.headers.authorization;
if (!authorization) {
- return reply(Boom.unauthorized(null, 'Custom'));
+ return Boom.unauthorized(null, 'Custom');
}
- var parts = authorization.split(/\s+/);
+ const parts = authorization.split(/\s+/);
if (parts.length !== 2) {
- return reply.continue(); // Error without error or credentials
+ return h.continue; // Error without error or credentials
}
- var username = parts[1];
- var credentials = settings.users[username];
+ const username = parts[1];
+ const credentials = settings.users[username];
if (!credentials) {
- return reply(Boom.unauthorized('Missing credentials', 'Custom'));
+ throw Boom.unauthorized('Missing credentials', 'Custom');
}
if (credentials === 'skip') {
- return reply(Boom.unauthorized(null, 'Custom'));
- }
-
- if (credentials === 'throw') {
- throw new Error('Boom');
+ return h.unauthenticated(Boom.unauthorized(null, 'Custom'));
}
if (typeof credentials === 'string') {
- return reply(credentials);
+ return h.response(credentials).takeover();
}
- return reply.continue({ credentials: credentials });
+ credentials.user = credentials.user || null;
+ return h.authenticated({ credentials, artifacts: settings.artifacts });
},
- response: function (request, reply) {
+ response: (request, h) => {
if (request.auth.credentials.response) {
- return reply(request.auth.credentials.response);
+ throw request.auth.credentials.response;
}
- return reply.continue();
+ return h.continue;
}
};
if (!settings ||
settings.payload !== false) {
- scheme.payload = function (request, reply) {
+ scheme.payload = (request, h) => {
+
+ const result = request.auth.credentials.payload;
+ if (!result) {
+ return h.continue;
+ }
- if (request.auth.credentials.payload) {
- return reply(request.auth.credentials.payload);
+ if (result.isBoom) {
+ throw result;
}
- return reply.continue();
+ return h.response(request.auth.credentials.payload).takeover();
};
}
diff --git a/test/common.js b/test/common.js
new file mode 100644
index 000000000..eae48a36b
--- /dev/null
+++ b/test/common.js
@@ -0,0 +1,32 @@
+'use strict';
+
+const ChildProcess = require('child_process');
+const Http = require('http');
+const Net = require('net');
+
+const internals = {};
+
+internals.hasLsof = () => {
+
+ try {
+ ChildProcess.execSync(`lsof -p ${process.pid}`, { stdio: 'ignore' });
+ }
+ catch (err) {
+ return false;
+ }
+
+ return true;
+};
+
+internals.hasIPv6 = () => {
+
+ const server = Http.createServer().listen();
+ const { address } = server.address();
+ server.close();
+
+ return Net.isIPv6(address);
+};
+
+exports.hasLsof = internals.hasLsof();
+
+exports.hasIPv6 = internals.hasIPv6();
diff --git a/test/connection.js b/test/connection.js
deleted file mode 100755
index 13dfee34d..000000000
--- a/test/connection.js
+++ /dev/null
@@ -1,1555 +0,0 @@
-// Load modules
-
-var ChildProcess = require('child_process');
-var Fs = require('fs');
-var Http = require('http');
-var Https = require('https');
-var Net = require('net');
-var Os = require('os');
-var Path = require('path');
-var Boom = require('boom');
-var Code = require('code');
-var Handlebars = require('handlebars');
-var Hapi = require('..');
-var Hoek = require('hoek');
-var Inert = require('inert');
-var Lab = require('lab');
-var Vision = require('vision');
-var Wreck = require('wreck');
-
-
-// Declare internals
-
-var internals = {};
-
-
-// Test shortcuts
-
-var lab = exports.lab = Lab.script();
-var describe = lab.describe;
-var it = lab.it;
-var expect = Code.expect;
-
-
-describe('Connection', function () {
-
- it('allows null port and host', function (done) {
-
- var server = new Hapi.Server();
- expect(function () {
-
- server.connection({ host: null, port: null });
- }).to.not.throw();
- done();
- });
-
- it('removes duplicate labels', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ labels: ['a', 'b', 'a', 'c', 'b'] });
- expect(server.connections[0].settings.labels).to.deep.equal(['a', 'b', 'c']);
- done();
- });
-
- it('throws when disabling autoListen and providing a port', function (done) {
-
- var server = new Hapi.Server();
- expect(function () {
-
- server.connection({ port: 80, autoListen: false });
- }).to.throw('Cannot specify port when autoListen is false');
- done();
- });
-
- it('throws when disabling autoListen and providing special host', function (done) {
-
- var server = new Hapi.Server();
- var port = Path.join(__dirname, 'hapi-server.socket');
- expect(function () {
-
- server.connection({ port: port, autoListen: false });
- }).to.throw('Cannot specify port when autoListen is false');
- done();
- });
-
- it('defaults address to 0.0.0.0 or :: when no host is provided', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- server.start(function (err) {
-
- expect(err).to.not.exist();
-
- var expectedBoundAddress = '0.0.0.0';
- if (Net.isIPv6(server.listener.address().address)) {
- expectedBoundAddress = '::';
- }
-
- expect(server.info.address).to.equal(expectedBoundAddress);
- server.stop(done);
- });
- });
-
- it('uses address when present instead of host', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ host: 'no.such.domain.hapi', address: 'localhost' });
- server.start(function (err) {
-
- expect(err).to.not.exist();
- expect(server.info.host).to.equal('no.such.domain.hapi');
- expect(server.info.address).to.equal('127.0.0.1');
- server.stop(done);
- });
- });
-
- it('uses uri when present instead of host and port', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ host: 'no.such.domain.hapi', address: 'localhost', uri: 'http://uri.example.com:8080' });
- expect(server.info.uri).to.equal('http://uri.example.com:8080');
- server.start(function (err) {
-
- expect(err).to.not.exist();
- expect(server.info.host).to.equal('no.such.domain.hapi');
- expect(server.info.address).to.equal('127.0.0.1');
- expect(server.info.uri).to.equal('http://uri.example.com:8080');
- server.stop(done);
- });
- });
-
- it('throws on uri ending with /', function (done) {
-
- var server = new Hapi.Server();
- expect(function () {
-
- server.connection({ uri: 'http://uri.example.com:8080/' });
- }).to.throw(/Invalid connection options/);
- done();
- });
-
- it('creates a server listening on a unix domain socket', { skip: process.platform === 'win32' }, function (done) {
-
- var port = Path.join(__dirname, 'hapi-server.socket');
- var server = new Hapi.Server();
- server.connection({ port: port });
-
- expect(server.connections[0].type).to.equal('socket');
-
- server.start(function (err) {
-
- expect(err).to.not.exist();
- var absSocketPath = Path.resolve(port);
- expect(server.info.port).to.equal(absSocketPath);
- server.stop(function (err) {
-
- expect(err).to.not.exist();
-
- if (Fs.existsSync(port)) {
- Fs.unlinkSync(port);
- }
- done();
- });
- });
- });
-
- it('creates a server listening on a windows named pipe', function (done) {
-
- var port = '\\\\.\\pipe\\6653e55f-26ec-4268-a4f2-882f4089315c';
- var server = new Hapi.Server();
- server.connection({ port: port });
-
- expect(server.connections[0].type).to.equal('socket');
-
- server.start(function (err) {
-
- expect(server.info.port).to.equal(port);
- server.stop(done);
- });
- });
-
- it('creates an https server when passed tls options', function (done) {
-
- var tlsOptions = {
- key: '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA0UqyXDCqWDKpoNQQK/fdr0OkG4gW6DUafxdufH9GmkX/zoKz\ng/SFLrPipzSGINKWtyMvo7mPjXqqVgE10LDI3VFV8IR6fnART+AF8CW5HMBPGt/s\nfQW4W4puvBHkBxWSW1EvbecgNEIS9hTGvHXkFzm4xJ2e9DHp2xoVAjREC73B7JbF\nhc5ZGGchKw+CFmAiNysU0DmBgQcac0eg2pWoT+YGmTeQj6sRXO67n2xy/hA1DuN6\nA4WBK3wM3O4BnTG0dNbWUEbe7yAbV5gEyq57GhJIeYxRvveVDaX90LoAqM4cUH06\n6rciON0UbDHV2LP/JaH5jzBjUyCnKLLo5snlbwIDAQABAoIBAQDJm7YC3pJJUcxb\nc8x8PlHbUkJUjxzZ5MW4Zb71yLkfRYzsxrTcyQA+g+QzA4KtPY8XrZpnkgm51M8e\n+B16AcIMiBxMC6HgCF503i16LyyJiKrrDYfGy2rTK6AOJQHO3TXWJ3eT3BAGpxuS\n12K2Cq6EvQLCy79iJm7Ks+5G6EggMZPfCVdEhffRm2Epl4T7LpIAqWiUDcDfS05n\nNNfAGxxvALPn+D+kzcSF6hpmCVrFVTf9ouhvnr+0DpIIVPwSK/REAF3Ux5SQvFuL\njPmh3bGwfRtcC5d21QNrHdoBVSN2UBLmbHUpBUcOBI8FyivAWJhRfKnhTvXMFG8L\nwaXB51IZAoGBAP/E3uz6zCyN7l2j09wmbyNOi1AKvr1WSmuBJveITouwblnRSdvc\nsYm4YYE0Vb94AG4n7JIfZLKtTN0xvnCo8tYjrdwMJyGfEfMGCQQ9MpOBXAkVVZvP\ne2k4zHNNsfvSc38UNSt7K0HkVuH5BkRBQeskcsyMeu0qK4wQwdtiCoBDAoGBANF7\nFMppYxSW4ir7Jvkh0P8bP/Z7AtaSmkX7iMmUYT+gMFB5EKqFTQjNQgSJxS/uHVDE\nSC5co8WGHnRk7YH2Pp+Ty1fHfXNWyoOOzNEWvg6CFeMHW2o+/qZd4Z5Fep6qCLaa\nFvzWWC2S5YslEaaP8DQ74aAX4o+/TECrxi0z2lllAoGAdRB6qCSyRsI/k4Rkd6Lv\nw00z3lLMsoRIU6QtXaZ5rN335Awyrfr5F3vYxPZbOOOH7uM/GDJeOJmxUJxv+cia\nPQDflpPJZU4VPRJKFjKcb38JzO6C3Gm+po5kpXGuQQA19LgfDeO2DNaiHZOJFrx3\nm1R3Zr/1k491lwokcHETNVkCgYBPLjrZl6Q/8BhlLrG4kbOx+dbfj/euq5NsyHsX\n1uI7bo1Una5TBjfsD8nYdUr3pwWltcui2pl83Ak+7bdo3G8nWnIOJ/WfVzsNJzj7\n/6CvUzR6sBk5u739nJbfgFutBZBtlSkDQPHrqA7j3Ysibl3ZIJlULjMRKrnj6Ans\npCDwkQKBgQCM7gu3p7veYwCZaxqDMz5/GGFUB1My7sK0hcT7/oH61yw3O8pOekee\nuctI1R3NOudn1cs5TAy/aypgLDYTUGQTiBRILeMiZnOrvQQB9cEf7TFgDoRNCcDs\nV/ZWiegVB/WY7H0BkCekuq5bHwjgtJTpvHGqQ9YD7RhE8RSYOhdQ/Q==\n-----END RSA PRIVATE KEY-----\n',
- cert: '-----BEGIN CERTIFICATE-----\nMIIDBjCCAe4CCQDvLNml6smHlTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0\ncyBQdHkgTHRkMB4XDTE0MDEyNTIxMjIxOFoXDTE1MDEyNTIxMjIxOFowRTELMAkG\nA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0\nIFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nANFKslwwqlgyqaDUECv33a9DpBuIFug1Gn8Xbnx/RppF/86Cs4P0hS6z4qc0hiDS\nlrcjL6O5j416qlYBNdCwyN1RVfCEen5wEU/gBfAluRzATxrf7H0FuFuKbrwR5AcV\nkltRL23nIDRCEvYUxrx15Bc5uMSdnvQx6dsaFQI0RAu9weyWxYXOWRhnISsPghZg\nIjcrFNA5gYEHGnNHoNqVqE/mBpk3kI+rEVzuu59scv4QNQ7jegOFgSt8DNzuAZ0x\ntHTW1lBG3u8gG1eYBMquexoSSHmMUb73lQ2l/dC6AKjOHFB9Ouq3IjjdFGwx1diz\n/yWh+Y8wY1Mgpyiy6ObJ5W8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAoSc6Skb4\ng1e0ZqPKXBV2qbx7hlqIyYpubCl1rDiEdVzqYYZEwmst36fJRRrVaFuAM/1DYAmT\nWMhU+yTfA+vCS4tql9b9zUhPw/IDHpBDWyR01spoZFBF/hE1MGNpCSXXsAbmCiVf\naxrIgR2DNketbDxkQx671KwF1+1JOMo9ffXp+OhuRo5NaGIxhTsZ+f/MA4y084Aj\nDI39av50sTRTWWShlN+J7PtdQVA5SZD97oYbeUeL7gI18kAJww9eUdmT0nEjcwKs\nxsQT1fyKbo7AlZBY4KSlUMuGnn0VnAsB9b+LxtXlDfnjyM8bVQx1uAfRo0DO8p/5\n3J5DTjAU55deBQ==\n-----END CERTIFICATE-----\n'
- };
-
- var server = new Hapi.Server();
- server.connection({ tls: tlsOptions });
- expect(server.listener instanceof Https.Server).to.equal(true);
- done();
- });
-
- it('uses a provided listener', function (done) {
-
- var handler = function (request, reply) {
-
- return reply('ok');
- };
-
- var listener = Http.createServer();
- var server = new Hapi.Server();
- server.connection({ listener: listener });
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.start(function (err) {
-
- expect(err).to.not.exist();
- Wreck.get('http://localhost:' + server.info.port + '/', {}, function (err, res, body) {
-
- expect(err).to.not.exist();
- expect(body.toString()).to.equal('ok');
- server.stop(done);
- });
- });
- });
-
- it('uses a provided listener (TLS)', function (done) {
-
- var handler = function (request, reply) {
-
- return reply('ok');
- };
-
- var listener = Http.createServer();
- var server = new Hapi.Server();
- server.connection({ listener: listener, tls: true });
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.start(function (err) {
-
- expect(err).to.not.exist();
- expect(server.info.protocol).to.equal('https');
- server.stop(done);
- });
- });
-
- it('uses a provided listener with manual listen', function (done) {
-
- var handler = function (request, reply) {
-
- return reply('ok');
- };
-
- var listener = Http.createServer();
- var server = new Hapi.Server();
- server.connection({ listener: listener, autoListen: false });
- server.route({ method: 'GET', path: '/', handler: handler });
-
- listener.listen(0, 'localhost', function () {
-
- server.start(function (err) {
-
- expect(err).to.not.exist();
- Wreck.get('http://localhost:' + server.info.port + '/', {}, function (err, res, body) {
-
- expect(err).to.not.exist();
- expect(body.toString()).to.equal('ok');
- server.stop(done);
- });
- });
- });
- });
-
- it('sets info.uri with default localhost when no hostname', { parallel: false }, function (done) {
-
- var orig = Os.hostname;
- Os.hostname = function () {
-
- Os.hostname = orig;
- return '';
- };
-
- var server = new Hapi.Server();
- server.connection({ port: 80 });
- expect(server.info.uri).to.equal('http://localhost:80');
- done();
- });
-
- it('sets info.uri without port when 0', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ host: 'example.com' });
- expect(server.info.uri).to.equal('http://example.com');
- done();
- });
-
- it('closes connection on socket timeout', { parallel: false }, function (done) {
-
- var server = new Hapi.Server();
- server.connection({ routes: { timeout: { socket: 50 }, payload: { timeout: 45 } } });
- server.route({
- method: 'GET', path: '/', config: {
- handler: function (request, reply) {
-
- setTimeout(function () {
-
- return reply('too late');
- }, 70);
- }
- }
- });
-
- server.start(function (err) {
-
- expect(err).to.not.exist();
- Wreck.request('GET', 'http://localhost:' + server.info.port + '/', {}, function (err, res) {
-
- expect(err).to.exist();
- expect(err.message).to.equal('Client request error: socket hang up');
- server.stop(done);
- });
- });
- });
-
- it('disables node socket timeout', { parallel: false }, function (done) {
-
- var handler = function (request, reply) {
-
- return reply();
- };
-
- var server = new Hapi.Server();
- server.connection({ routes: { timeout: { socket: false } } });
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
-
- server.start(function (err) {
-
- expect(err).to.not.exist();
-
- var timeout;
- var orig = Net.Socket.prototype.setTimeout;
- Net.Socket.prototype.setTimeout = function () {
-
- timeout = 'gotcha';
- Net.Socket.prototype.setTimeout = orig;
- return orig.apply(this, arguments);
- };
-
- Wreck.request('GET', 'http://localhost:' + server.info.port + '/', {}, function (err, res) {
-
- Wreck.read(res, {}, function (err, payload) {
-
- expect(err).to.not.exist();
- expect(timeout).to.equal('gotcha');
- server.stop(done);
- });
- });
- });
- });
-
- describe('_start()', function () {
-
- it('starts connection', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- server.start(function (err) {
-
- expect(err).to.not.exist();
- var expectedBoundAddress = '0.0.0.0';
- if (Net.isIPv6(server.listener.address().address)) {
- expectedBoundAddress = '::';
- }
-
- expect(server.info.host).to.equal(Os.hostname());
- expect(server.info.address).to.equal(expectedBoundAddress);
- expect(server.info.port).to.be.a.number().and.above(1);
- server.stop(done);
- });
- });
-
- it('starts connection (tls)', function (done) {
-
- var tlsOptions = {
- key: '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA0UqyXDCqWDKpoNQQK/fdr0OkG4gW6DUafxdufH9GmkX/zoKz\ng/SFLrPipzSGINKWtyMvo7mPjXqqVgE10LDI3VFV8IR6fnART+AF8CW5HMBPGt/s\nfQW4W4puvBHkBxWSW1EvbecgNEIS9hTGvHXkFzm4xJ2e9DHp2xoVAjREC73B7JbF\nhc5ZGGchKw+CFmAiNysU0DmBgQcac0eg2pWoT+YGmTeQj6sRXO67n2xy/hA1DuN6\nA4WBK3wM3O4BnTG0dNbWUEbe7yAbV5gEyq57GhJIeYxRvveVDaX90LoAqM4cUH06\n6rciON0UbDHV2LP/JaH5jzBjUyCnKLLo5snlbwIDAQABAoIBAQDJm7YC3pJJUcxb\nc8x8PlHbUkJUjxzZ5MW4Zb71yLkfRYzsxrTcyQA+g+QzA4KtPY8XrZpnkgm51M8e\n+B16AcIMiBxMC6HgCF503i16LyyJiKrrDYfGy2rTK6AOJQHO3TXWJ3eT3BAGpxuS\n12K2Cq6EvQLCy79iJm7Ks+5G6EggMZPfCVdEhffRm2Epl4T7LpIAqWiUDcDfS05n\nNNfAGxxvALPn+D+kzcSF6hpmCVrFVTf9ouhvnr+0DpIIVPwSK/REAF3Ux5SQvFuL\njPmh3bGwfRtcC5d21QNrHdoBVSN2UBLmbHUpBUcOBI8FyivAWJhRfKnhTvXMFG8L\nwaXB51IZAoGBAP/E3uz6zCyN7l2j09wmbyNOi1AKvr1WSmuBJveITouwblnRSdvc\nsYm4YYE0Vb94AG4n7JIfZLKtTN0xvnCo8tYjrdwMJyGfEfMGCQQ9MpOBXAkVVZvP\ne2k4zHNNsfvSc38UNSt7K0HkVuH5BkRBQeskcsyMeu0qK4wQwdtiCoBDAoGBANF7\nFMppYxSW4ir7Jvkh0P8bP/Z7AtaSmkX7iMmUYT+gMFB5EKqFTQjNQgSJxS/uHVDE\nSC5co8WGHnRk7YH2Pp+Ty1fHfXNWyoOOzNEWvg6CFeMHW2o+/qZd4Z5Fep6qCLaa\nFvzWWC2S5YslEaaP8DQ74aAX4o+/TECrxi0z2lllAoGAdRB6qCSyRsI/k4Rkd6Lv\nw00z3lLMsoRIU6QtXaZ5rN335Awyrfr5F3vYxPZbOOOH7uM/GDJeOJmxUJxv+cia\nPQDflpPJZU4VPRJKFjKcb38JzO6C3Gm+po5kpXGuQQA19LgfDeO2DNaiHZOJFrx3\nm1R3Zr/1k491lwokcHETNVkCgYBPLjrZl6Q/8BhlLrG4kbOx+dbfj/euq5NsyHsX\n1uI7bo1Una5TBjfsD8nYdUr3pwWltcui2pl83Ak+7bdo3G8nWnIOJ/WfVzsNJzj7\n/6CvUzR6sBk5u739nJbfgFutBZBtlSkDQPHrqA7j3Ysibl3ZIJlULjMRKrnj6Ans\npCDwkQKBgQCM7gu3p7veYwCZaxqDMz5/GGFUB1My7sK0hcT7/oH61yw3O8pOekee\nuctI1R3NOudn1cs5TAy/aypgLDYTUGQTiBRILeMiZnOrvQQB9cEf7TFgDoRNCcDs\nV/ZWiegVB/WY7H0BkCekuq5bHwjgtJTpvHGqQ9YD7RhE8RSYOhdQ/Q==\n-----END RSA PRIVATE KEY-----\n',
- cert: '-----BEGIN CERTIFICATE-----\nMIIDBjCCAe4CCQDvLNml6smHlTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0\ncyBQdHkgTHRkMB4XDTE0MDEyNTIxMjIxOFoXDTE1MDEyNTIxMjIxOFowRTELMAkG\nA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0\nIFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nANFKslwwqlgyqaDUECv33a9DpBuIFug1Gn8Xbnx/RppF/86Cs4P0hS6z4qc0hiDS\nlrcjL6O5j416qlYBNdCwyN1RVfCEen5wEU/gBfAluRzATxrf7H0FuFuKbrwR5AcV\nkltRL23nIDRCEvYUxrx15Bc5uMSdnvQx6dsaFQI0RAu9weyWxYXOWRhnISsPghZg\nIjcrFNA5gYEHGnNHoNqVqE/mBpk3kI+rEVzuu59scv4QNQ7jegOFgSt8DNzuAZ0x\ntHTW1lBG3u8gG1eYBMquexoSSHmMUb73lQ2l/dC6AKjOHFB9Ouq3IjjdFGwx1diz\n/yWh+Y8wY1Mgpyiy6ObJ5W8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAoSc6Skb4\ng1e0ZqPKXBV2qbx7hlqIyYpubCl1rDiEdVzqYYZEwmst36fJRRrVaFuAM/1DYAmT\nWMhU+yTfA+vCS4tql9b9zUhPw/IDHpBDWyR01spoZFBF/hE1MGNpCSXXsAbmCiVf\naxrIgR2DNketbDxkQx671KwF1+1JOMo9ffXp+OhuRo5NaGIxhTsZ+f/MA4y084Aj\nDI39av50sTRTWWShlN+J7PtdQVA5SZD97oYbeUeL7gI18kAJww9eUdmT0nEjcwKs\nxsQT1fyKbo7AlZBY4KSlUMuGnn0VnAsB9b+LxtXlDfnjyM8bVQx1uAfRo0DO8p/5\n3J5DTjAU55deBQ==\n-----END CERTIFICATE-----\n'
- };
-
- var server = new Hapi.Server();
- server.connection({ host: '0.0.0.0', port: 0, tls: tlsOptions });
- server.start(function (err) {
-
- expect(err).to.not.exist();
- expect(server.info.host).to.equal('0.0.0.0');
- expect(server.info.port).to.not.equal(0);
- server.stop(done);
- });
- });
-
- it('sets info with defaults when missing hostname and address', { parallel: false }, function (done) {
-
- var hostname = Os.hostname;
- Os.hostname = function () {
-
- Os.hostname = hostname;
- return '';
- };
-
- var server = new Hapi.Server();
- server.connection({ port: '8000' });
- expect(server.info.host).to.equal('localhost');
- expect(server.info.uri).to.equal('http://localhost:8000');
- done();
- });
-
- it('ignored repeated calls', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- server.start(function (err) {
-
- expect(err).to.not.exist();
- server.start(function (err) {
-
- expect(err).to.not.exist();
- server.stop(function (err) {
-
- expect(err).to.not.exist();
- done();
- });
- });
- });
- });
-
- it('will return an error if the port is already in use', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- server.start(function (err) {
-
- expect(err).to.not.exist();
- server.connection({ port: server.info.port });
- server.start(function (err) {
-
- expect(err).to.exist();
- expect(err.message).to.match(/EADDRINUSE/);
- server.stop(done);
- });
- });
- });
- });
-
- describe('_stop()', function () {
-
- it('waits to stop until all connections are closed', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- server.start(function (err) {
-
- expect(err).to.not.exist();
- var socket1 = new Net.Socket();
- var socket2 = new Net.Socket();
- socket1.on('error', function () { });
- socket2.on('error', function () { });
-
- socket1.connect(server.info.port, '127.0.0.1', function () {
-
- socket2.connect(server.info.port, '127.0.0.1', function () {
-
- server.listener.getConnections(function (err, count1) {
-
- expect(count1).to.be.greaterThan(0);
-
- server.stop(function (err) {
-
- expect(err).to.not.exist();
-
- server.listener.getConnections(function (err, count2) {
-
- expect(count2).to.equal(0);
- done();
- });
- });
-
- socket1.end();
- socket2.end();
- });
- });
- });
- });
- });
-
- it('waits to destroy connections until after the timeout', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- server.start(function (err) {
-
- expect(err).to.not.exist();
-
- var socket1 = new Net.Socket();
- var socket2 = new Net.Socket();
-
- socket1.once('error', function (err) {
-
- expect(err.errno).to.equal('ECONNRESET');
- });
-
- socket2.once('error', function (err) {
-
- expect(err.errno).to.equal('ECONNRESET');
- });
-
- socket1.connect(server.info.port, server.connections[0].settings.host, function () {
-
- socket2.connect(server.info.port, server.connections[0].settings.host, function () {
-
- server.listener.getConnections(function (err, count) {
-
- expect(count).to.be.greaterThan(0);
- var timer = new Hoek.Bench();
-
- server.stop({ timeout: 20 }, function (err) {
-
- expect(err).to.not.exist();
- expect(timer.elapsed()).to.be.at.least(19);
- done();
- });
- });
- });
- });
- });
- });
-
- it('waits to destroy connections if they close by themselves', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- server.start(function (err) {
-
- expect(err).to.not.exist();
-
- var socket1 = new Net.Socket();
- var socket2 = new Net.Socket();
-
- socket1.once('error', function (err) {
-
- expect(err.errno).to.equal('ECONNRESET');
- });
-
- socket2.once('error', function (err) {
-
- expect(err.errno).to.equal('ECONNRESET');
- });
-
- socket1.connect(server.info.port, server.connections[0].settings.host, function () {
-
- socket2.connect(server.info.port, server.connections[0].settings.host, function () {
-
- server.listener.getConnections(function (err, count1) {
-
- expect(count1).to.be.greaterThan(0);
- var timer = new Hoek.Bench();
-
- server.stop(function (err) {
-
- expect(err).to.not.exist();
-
- server.listener.getConnections(function (err, count2) {
-
- expect(count2).to.equal(0);
- expect(timer.elapsed()).to.be.at.least(9);
- done();
- });
- });
-
- setTimeout(function () {
-
- socket1.end();
- socket2.end();
- }, 10);
- });
- });
- });
- });
- });
-
- it('refuses to handle new incoming requests', function (done) {
-
- var handler = function (request, reply) {
-
- return reply('ok');
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
- server.start(function (err) {
-
- expect(err).to.not.exist();
-
- var agent = new Http.Agent({ keepAlive: true, maxSockets: 1 });
- var err2;
-
- Wreck.get('http://localhost:' + server.info.port + '/', { agent: agent }, function (err1, res, body) {
-
- server.stop(function (err3) {
-
- expect(err3).to.not.exist();
- expect(err1).to.not.exist();
- expect(body.toString()).to.equal('ok');
- expect(server.connections[0]._started).to.equal(false);
- expect(err2).to.exist();
- done();
- });
- });
-
- Wreck.get('http://localhost:' + server.info.port + '/', { agent: agent }, function (err, res, body) {
-
- err2 = err;
- });
- });
- });
-
- it('removes connection event listeners after it stops', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- var initial = server.listener.listeners('connection').length;
- server.start(function (err) {
-
- expect(err).to.not.exist();
-
- expect(server.listener.listeners('connection').length).to.be.greaterThan(initial);
-
- server.stop(function (err) {
-
- expect(err).to.not.exist();
-
- server.start(function (err) {
-
- expect(err).to.not.exist();
-
- server.stop(function (err) {
-
- expect(err).to.not.exist();
- expect(server.listener.listeners('connection').length).to.equal(initial);
- done();
- });
- });
- });
- });
- });
-
- it('ignores repeated calls', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- server.stop(function (err) {
-
- server.stop(done);
- });
- });
- });
-
- describe('_dispatch()', function () {
-
- it('rejects request due to high rss load', { parallel: false }, function (done) {
-
- var server = new Hapi.Server({ load: { sampleInterval: 5 } });
- server.connection({ load: { maxRssBytes: 1 } });
-
- var handler = function (request, reply) {
-
- var start = Date.now();
- while (Date.now() - start < 10) { }
- return reply('ok');
- };
-
- var logged = null;
- server.once('log', function (event, tags) {
-
- logged = (event.internal && tags.load && event.data);
- });
-
- server.route({ method: 'GET', path: '/', handler: handler });
- server.start(function (err) {
-
- expect(err).to.not.exist();
-
- server.inject('/', function (res1) {
-
- expect(res1.statusCode).to.equal(200);
-
- setImmediate(function () {
-
- server.inject('/', function (res2) {
-
- expect(res2.statusCode).to.equal(503);
- expect(logged.rss > 10000).to.equal(true);
- server.stop(done);
- });
- });
- });
- });
- });
- });
-
- describe('inject()', function () {
-
- it('keeps the options.credentials object untouched', function (done) {
-
- var handler = function (request, reply) {
-
- return reply();
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
-
- var options = {
- url: '/',
- credentials: { foo: 'bar' }
- };
-
- server.connections[0].inject(options, function (res) {
-
- expect(res.statusCode).to.equal(200);
- expect(options.credentials).to.exist();
- done();
- });
- });
-
- it('passes the options.artifacts object', function (done) {
-
- var handler = function (request, reply) {
-
- return reply(request.auth.artifacts);
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
-
- var options = {
- url: '/',
- credentials: { foo: 'bar' },
- artifacts: { bar: 'baz' }
- };
-
- server.connections[0].inject(options, function (res) {
-
- expect(res.statusCode).to.equal(200);
- expect(res.result.bar).to.equal('baz');
- expect(options.artifacts).to.exist();
- done();
- });
- });
-
- it('returns the request object', function (done) {
-
- var handler = function (request, reply) {
-
- request.app.key = 'value';
- return reply();
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
-
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(200);
- expect(res.request.app.key).to.equal('value');
- done();
- });
- });
-
- it('can set a client remoteAddress', function (done) {
-
- var handler = function (request, reply) {
-
- return reply(request.info.remoteAddress);
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
-
- server.inject({ url: '/', remoteAddress: '1.2.3.4' }, function (res) {
-
- expect(res.statusCode).to.equal(200);
- expect(res.payload).to.equal('1.2.3.4');
- done();
- });
- });
-
- it('sets a default remoteAddress of 127.0.0.1', function (done) {
-
- var handler = function (request, reply) {
-
- return reply(request.info.remoteAddress);
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
-
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(200);
- expect(res.payload).to.equal('127.0.0.1');
- done();
- });
- });
- });
-
- describe('table()', function () {
-
- it('returns an array of the current routes', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- server.route({ path: '/test/', method: 'get', handler: function () { } });
- server.route({ path: '/test/{p}/end', method: 'get', handler: function () { } });
-
- var routes = server.table()[0].table;
-
- expect(routes.length).to.equal(2);
- expect(routes[0].path).to.equal('/test/');
- done();
- });
-
- it('returns the labels for the connections', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ labels: ['test'] });
-
- server.route({ path: '/test/', method: 'get', handler: function () { } });
- server.route({ path: '/test/{p}/end', method: 'get', handler: function () { } });
-
- var connection = server.table()[0];
-
- expect(connection.labels).to.only.include(['test']);
- done();
- });
-
- it('returns an array of the current routes (connection)', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- server.route({ path: '/test/', method: 'get', handler: function () { } });
- server.route({ path: '/test/{p}/end', method: 'get', handler: function () { } });
-
- var routes = server.connections[0].table();
-
- expect(routes.length).to.equal(2);
- expect(routes[0].path).to.equal('/test/');
- done();
- });
-
- it('combines global and vhost routes', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- server.route({ path: '/test/', method: 'get', handler: function () { } });
- server.route({ path: '/test/', vhost: 'one.example.com', method: 'get', handler: function () { } });
- server.route({ path: '/test/', vhost: 'two.example.com', method: 'get', handler: function () { } });
- server.route({ path: '/test/{p}/end', method: 'get', handler: function () { } });
-
- var routes = server.table()[0].table;
-
- expect(routes.length).to.equal(4);
- done();
- });
-
- it('combines global and vhost routes and filters based on host', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- server.route({ path: '/test/', method: 'get', handler: function () { } });
- server.route({ path: '/test/', vhost: 'one.example.com', method: 'get', handler: function () { } });
- server.route({ path: '/test/', vhost: 'two.example.com', method: 'get', handler: function () { } });
- server.route({ path: '/test/{p}/end', method: 'get', handler: function () { } });
-
- var routes = server.table('one.example.com')[0].table;
-
- expect(routes.length).to.equal(3);
- done();
- });
-
- it('accepts a list of hosts', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- server.route({ path: '/test/', method: 'get', handler: function () { } });
- server.route({ path: '/test/', vhost: 'one.example.com', method: 'get', handler: function () { } });
- server.route({ path: '/test/', vhost: 'two.example.com', method: 'get', handler: function () { } });
- server.route({ path: '/test/{p}/end', method: 'get', handler: function () { } });
-
- var routes = server.table(['one.example.com', 'two.example.com'])[0].table;
-
- expect(routes.length).to.equal(4);
- done();
- });
-
- it('ignores unknown host', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- server.route({ path: '/test/', method: 'get', handler: function () { } });
- server.route({ path: '/test/', vhost: 'one.example.com', method: 'get', handler: function () { } });
- server.route({ path: '/test/', vhost: 'two.example.com', method: 'get', handler: function () { } });
- server.route({ path: '/test/{p}/end', method: 'get', handler: function () { } });
-
- var routes = server.table('three.example.com')[0].table;
-
- expect(routes.length).to.equal(2);
- done();
- });
- });
-
- describe('ext()', function () {
-
- it('supports adding an array of methods', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- server.ext('onPreHandler', [
- function (request, reply) {
-
- request.app.x = '1';
- return reply.continue();
- },
- function (request, reply) {
-
- request.app.x += '2';
- return reply.continue();
- }
- ]);
-
- var handler = function (request, reply) {
-
- return reply(request.app.x);
- };
-
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject('/', function (res) {
-
- expect(res.result).to.equal('12');
- done();
- });
- });
-
- it('sets bind via options', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- server.ext('onPreHandler', function (request, reply) {
-
- request.app.x = this.y;
- return reply.continue();
- }, { bind: { y: 42 } });
-
- var handler = function (request, reply) {
-
- return reply(request.app.x);
- };
-
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject('/', function (res) {
-
- expect(res.result).to.equal(42);
- done();
- });
- });
-
- it('uses server views for ext added via server', function (done) {
-
- var server = new Hapi.Server();
- server.register(Vision, Hoek.ignore);
- server.connection();
-
- server.views({
- engines: { html: Handlebars },
- path: __dirname + '/templates'
- });
-
- server.ext('onPreHandler', function (request, reply) {
-
- return reply.view('test');
- });
-
- var test = function (plugin, options, next) {
-
- plugin.views({
- engines: { html: Handlebars },
- path: './no_such_directory_found'
- });
-
- plugin.route({ path: '/view', method: 'GET', handler: function (request, reply) { } });
- return next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- server.register(test, function (err) {
-
- server.inject('/view', function (res) {
-
- expect(res.statusCode).to.equal(200);
- done();
- });
- });
- });
-
- it('supports reply decorators on empty result', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- server.ext('onRequest', function (request, reply) {
-
- return reply().redirect('/elsewhere');
- });
-
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(302);
- expect(res.headers.location).to.equal('/elsewhere');
- done();
- });
- });
-
- it('supports direct reply decorators', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- server.ext('onRequest', function (request, reply) {
-
- return reply.redirect('/elsewhere');
- });
-
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(302);
- expect(res.headers.location).to.equal('/elsewhere');
- done();
- });
- });
-
- describe('onRequest', function (done) {
-
- it('replies with custom response', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- server.ext('onRequest', function (request, reply) {
-
- return reply(Boom.badRequest('boom'));
- });
-
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(400);
- expect(res.result.message).to.equal('boom');
- done();
- });
- });
-
- it('replies with error using reply(null, result)', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- server.ext('onRequest', function (request, reply) {
-
- return reply(null, Boom.badRequest('boom'));
- });
-
-
- var handler = function (request, reply) {
-
- return reply('ok');
- };
-
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject('/', function (res) {
-
- expect(res.result.message).to.equal('boom');
- done();
- });
- });
-
- it('replies with a view', function (done) {
-
- var server = new Hapi.Server();
- server.register(Vision, Hoek.ignore);
- server.connection();
-
- server.views({
- engines: { 'html': Handlebars },
- path: __dirname + '/templates'
- });
-
- server.ext('onRequest', function (request, reply) {
-
- return reply.view('test', { message: 'hola!' });
- });
-
- var handler = function (request, reply) {
-
- return reply('ok');
- };
-
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject('/', function (res) {
-
- expect(res.result).to.equal('hola!
\nhola!<\/h1>\r?\n<\/div>\r?\n/);
+ });
+ });
+
+ describe('onPreResponse', () => {
+
+ it('replies with custom response', async () => {
+
+ const server = Hapi.server();
+
+ const preRequest = (request, h) => {
+
+ if (typeof request.response.source === 'string') {
+ throw Boom.badRequest('boom');
+ }
+
+ return h.continue;
+ };
+
+ server.ext('onPreResponse', preRequest);
+
+ server.route({
+ method: 'GET',
+ path: '/text',
+ handler: () => 'ok'
+ });
+
+ server.route({
+ method: 'GET',
+ path: '/obj',
+ handler: () => ({ status: 'ok' })
+ });
+
+ const res1 = await server.inject({ method: 'GET', url: '/text' });
+ expect(res1.result.message).to.equal('boom');
+
+ const res2 = await server.inject({ method: 'GET', url: '/obj' });
+ expect(res2.result.status).to.equal('ok');
+ });
+
+ it('intercepts 404 responses', async () => {
+
+ const server = Hapi.server();
+
+ const preResponse = (request, h) => {
+
+ return h.response(request.response.output.statusCode).takeover();
+ };
+
+ server.ext('onPreResponse', preResponse);
+
+ const res = await server.inject({ method: 'GET', url: '/missing' });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal(404);
+ });
+
+ it('intercepts 404 when using directory handler and file is missing', async () => {
+
+ const server = Hapi.server();
+ await server.register(Inert);
+
+ const preResponse = (request) => {
+
+ const response = request.response;
+ return { isBoom: response.isBoom };
+ };
+
+ server.ext('onPreResponse', preResponse);
+
+ server.route({ method: 'GET', path: '/{path*}', handler: { directory: { path: './somewhere', listing: false, index: true } } });
+
+ const res = await server.inject('/missing');
+ expect(res.statusCode).to.equal(200);
+ expect(res.result.isBoom).to.equal(true);
+ });
+
+ it('intercepts 404 when using file handler and file is missing', async () => {
+
+ const server = Hapi.server();
+ await server.register(Inert);
+
+ const preResponse = (request) => {
+
+ const response = request.response;
+ return { isBoom: response.isBoom };
+ };
+
+ server.ext('onPreResponse', preResponse);
+
+ server.route({ method: 'GET', path: '/{path*}', handler: { file: './somewhere/something.txt' } });
+
+ const res = await server.inject('/missing');
+ expect(res.statusCode).to.equal(200);
+ expect(res.result.isBoom).to.equal(true);
+ });
+
+ it('cleans unused file stream when response is overridden', { skip: !Common.hasLsof }, async () => {
+
+ const server = Hapi.server();
+ await server.register(Inert);
+
+ const preResponse = (request) => {
+
+ return { something: 'else' };
+ };
+
+ server.ext('onPreResponse', preResponse);
+
+ server.route({ method: 'GET', path: '/{path*}', handler: { directory: { path: './' } } });
+
+ const res = await server.inject('/package.json');
+ expect(res.statusCode).to.equal(200);
+ expect(res.result.something).to.equal('else');
+
+ await new Promise((resolve) => {
+
+ const cmd = ChildProcess.spawn('lsof', ['-p', process.pid]);
+ let lsof = '';
+
+ cmd.stdout.on('data', (buffer) => {
+
+ lsof += buffer.toString();
+ });
+
+ cmd.stdout.on('end', () => {
+
+ let count = 0;
+ const lines = lsof.split('\n');
+ for (let i = 0; i < lines.length; ++i) {
+ count += !!lines[i].match(/package.json/);
+ }
+
+ expect(count).to.equal(0);
+ resolve();
+ });
+
+ cmd.stdin.end();
+ });
+ });
+
+ it('executes multiple extensions', async () => {
+
+ const server = Hapi.server();
+
+ const preResponse1 = (request, h) => {
+
+ request.response.source = request.response.source + '1';
+ return h.continue;
+ };
+
+ server.ext('onPreResponse', preResponse1);
+
+ const preResponse2 = (request, h) => {
+
+ request.response.source = request.response.source + '2';
+ return h.continue;
+ };
+
+ server.ext('onPreResponse', preResponse2);
+ server.route({ method: 'GET', path: '/', handler: () => '0' });
+
+ const res = await server.inject({ method: 'GET', url: '/' });
+ expect(res.result).to.equal('012');
+ });
+ });
+ });
+
+ describe('route()', () => {
+
+ it('emits route event', async () => {
+
+ const server = Hapi.server();
+ const log = server.events.once('route');
+
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: () => null
+ });
+
+ const [route] = await log;
+ expect(route.path).to.equal('/');
+ });
+
+ it('overrides the default notFound handler', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: '*', path: '/{p*}', handler: () => 'found' });
+ const res = await server.inject({ method: 'GET', url: '/page' });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('found');
+ });
+
+ it('responds to HEAD requests for a GET route', async () => {
+
+ const handler = (request, h) => {
+
+ return h.response('ok').etag('test').code(205);
+ };
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+ const res1 = await server.inject({ method: 'GET', url: '/' });
+
+ expect(res1.statusCode).to.equal(205);
+ expect(res1.headers['content-type']).to.equal('text/html; charset=utf-8');
+ expect(res1.headers['content-length']).to.equal(2);
+ expect(res1.headers.etag).to.equal('"test"');
+ expect(res1.result).to.equal('ok');
+
+ const res2 = await server.inject({ method: 'HEAD', url: '/' });
+ expect(res2.statusCode).to.equal(res1.statusCode);
+ expect(res2.headers['content-type']).to.equal(res1.headers['content-type']);
+ expect(res2.headers['content-length']).to.equal(res1.headers['content-length']);
+ expect(res2.headers.etag).to.equal(res1.headers.etag);
+ expect(res2.result).to.not.exist();
+ });
+
+ it('returns 404 on HEAD requests for non-GET routes', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/', handler: () => 'ok' });
+
+ const res1 = await server.inject({ method: 'HEAD', url: '/' });
+ expect(res1.statusCode).to.equal(404);
+ expect(res1.result).to.not.exist();
+
+ const res2 = await server.inject({ method: 'HEAD', url: '/not-there' });
+
+ expect(res2.statusCode).to.equal(404);
+ expect(res2.result).to.not.exist();
+ });
+
+ it('returns 500 on HEAD requests for failed responses', async () => {
+
+ const preResponse = (request, h) => {
+
+ request.response._processors.marshal = function (response, callback) {
+
+ process.nextTick(callback, new Error('boom!'));
+ };
+
+ return h.continue;
+ };
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler: () => 'ok' });
+ server.ext('onPreResponse', preResponse);
+
+ const res1 = await server.inject({ method: 'GET', url: '/' });
+ expect(res1.statusCode).to.equal(500);
+ expect(res1.result).to.exist();
+
+ const res2 = await server.inject({ method: 'HEAD', url: '/' });
+ expect(res2.statusCode).to.equal(res1.statusCode);
+ expect(res2.headers['content-type']).to.equal(res1.headers['content-type']);
+ expect(res2.headers['content-length']).to.equal(res1.headers['content-length']);
+ expect(res2.result).to.not.exist();
+ });
+
+ it('allows methods array', async () => {
+
+ const server = Hapi.server();
+ const config = { method: ['GET', 'PUT', 'POST', 'DELETE'], path: '/', handler: (request) => request.route.method };
+ server.route(config);
+ expect(config.method).to.equal(['GET', 'PUT', 'POST', 'DELETE']); // Ensure config is cloned
+
+ const res1 = await server.inject({ method: 'HEAD', url: '/' });
+ expect(res1.statusCode).to.equal(200);
+
+ const res2 = await server.inject({ method: 'GET', url: '/' });
+ expect(res2.statusCode).to.equal(200);
+ expect(res2.payload).to.equal('get');
+
+ const res3 = await server.inject({ method: 'PUT', url: '/' });
+ expect(res3.statusCode).to.equal(200);
+ expect(res3.payload).to.equal('put');
+
+ const res4 = await server.inject({ method: 'POST', url: '/' });
+ expect(res4.statusCode).to.equal(200);
+ expect(res4.payload).to.equal('post');
+
+ const res5 = await server.inject({ method: 'DELETE', url: '/' });
+ expect(res5.statusCode).to.equal(200);
+ expect(res5.payload).to.equal('delete');
+ });
+
+ it('adds routes using single and array methods', () => {
+
+ const server = Hapi.server();
+ server.route([
+ {
+ method: 'GET',
+ path: '/api/products',
+ handler: () => null
+ },
+ {
+ method: 'GET',
+ path: '/api/products/{id}',
+ handler: () => null
+ },
+ {
+ method: 'POST',
+ path: '/api/products',
+ handler: () => null
+ },
+ {
+ method: ['PUT', 'PATCH'],
+ path: '/api/products/{id}',
+ handler: () => null
+ },
+ {
+ method: 'DELETE',
+ path: '/api/products/{id}',
+ handler: () => null
+ }
+ ]);
+
+ const table = server.table();
+ const paths = table.map((route) => {
+
+ const obj = {
+ method: route.method,
+ path: route.path
+ };
+ return obj;
+ });
+
+ expect(table).to.have.length(6);
+ expect(paths).to.only.include([
+ { method: 'get', path: '/api/products' },
+ { method: 'get', path: '/api/products/{id}' },
+ { method: 'post', path: '/api/products' },
+ { method: 'put', path: '/api/products/{id}' },
+ { method: 'patch', path: '/api/products/{id}' },
+ { method: 'delete', path: '/api/products/{id}' }
+ ]);
+ });
+
+ it('throws on methods array with id', () => {
+
+ const server = Hapi.server();
+
+ expect(() => {
+
+ server.route({
+ method: ['GET', 'PUT', 'POST', 'DELETE'],
+ path: '/',
+ options: {
+ id: 'abc',
+ handler: (request) => request.route.method
+ }
+ });
+ }).to.throw('Route id abc for path / conflicts with existing path /');
+ });
+ });
+
+ describe('_defaultRoutes()', () => {
+
+ it('returns 404 when making a request to a route that does not exist', async () => {
+
+ const server = Hapi.server();
+ const res = await server.inject({ method: 'GET', url: '/nope' });
+ expect(res.statusCode).to.equal(404);
+ });
+
+ it('returns 400 on bad request', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/a/{p}', handler: () => null });
+ const res = await server.inject('/a/%');
+ expect(res.statusCode).to.equal(400);
+ });
+ });
+
+ describe('load', () => {
+
+ it('measures loop delay', async () => {
+
+ const server = Hapi.server({ load: { sampleInterval: 4 } });
+
+ const handler = (request) => {
+
+ const start = Date.now();
+ while (Date.now() - start < 5) { }
+ return 'ok';
+ };
+
+ server.route({ method: 'GET', path: '/', handler });
+ await server.start();
+
+ await server.inject('/');
+ expect(server.load.eventLoopDelay).to.be.below(7);
+
+ await Hoek.wait(0);
+
+ await server.inject('/');
+ expect(server.load.eventLoopDelay).to.be.above(0);
+
+ await Hoek.wait(0);
+
+ await server.inject('/');
+ expect(server.load.eventLoopDelay).to.be.above(0);
+ expect(server.load.eventLoopUtilization).to.be.above(0);
+ expect(server.load.heapUsed).to.be.above(1024 * 1024);
+ expect(server.load.rss).to.be.above(1024 * 1024);
+ await server.stop();
+ });
+ });
+});
+
+
+internals.countConnections = function (server) {
+
+ return new Promise((resolve, reject) => {
+
+ server.listener.getConnections((err, count) => {
+
+ return (err ? reject(err) : resolve(count));
+ });
+ });
+};
+
+
+internals.socket = function (server, mode) {
+
+ const socket = new Net.Socket();
+ socket.on('error', Hoek.ignore);
+
+ if (mode === 'tls') {
+ socket.connect(server.info.port, '127.0.0.1');
+ return new Promise((resolve) => TLS.connect({ socket, rejectUnauthorized: false }, () => resolve(socket)));
+ }
+
+ return new Promise((resolve) => socket.connect(server.info.port, '127.0.0.1', () => resolve(socket)));
+};
diff --git a/test/cors.js b/test/cors.js
new file mode 100755
index 000000000..079562efd
--- /dev/null
+++ b/test/cors.js
@@ -0,0 +1,676 @@
+'use strict';
+
+const Boom = require('@hapi/boom');
+const Code = require('@hapi/code');
+const Hapi = require('..');
+const Lab = require('@hapi/lab');
+
+
+const internals = {};
+
+
+const { describe, it } = exports.lab = Lab.script();
+const expect = Code.expect;
+
+
+describe('CORS', () => {
+
+ it('returns 404 on OPTIONS when cors disabled', async () => {
+
+ const server = Hapi.server({ routes: { cors: false } });
+ server.route({ method: 'GET', path: '/', handler: () => null });
+
+ const res = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
+ expect(res.statusCode).to.equal(404);
+ });
+
+ it('returns OPTIONS response', async () => {
+
+ const handler = function () {
+
+ throw Boom.badRequest();
+ };
+
+ const server = Hapi.server({ routes: { cors: true } });
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
+ expect(res.headers['access-control-allow-origin']).to.equal('http://example.com/');
+ });
+
+ it('returns OPTIONS response (server config)', async () => {
+
+ const handler = function () {
+
+ throw Boom.badRequest();
+ };
+
+ const server = Hapi.server({ routes: { cors: true } });
+ server.route({ method: 'GET', path: '/x', handler });
+
+ const res = await server.inject({ method: 'OPTIONS', url: '/x', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
+ expect(res.headers['access-control-allow-origin']).to.equal('http://example.com/');
+ });
+
+ it('returns headers on single route', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/a', handler: () => 'ok', options: { cors: true } });
+ server.route({ method: 'GET', path: '/b', handler: () => 'ok' });
+
+ const res1 = await server.inject({ method: 'OPTIONS', url: '/a', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
+ expect(res1.statusCode).to.equal(200);
+ expect(res1.result).to.be.null();
+ expect(res1.headers['access-control-allow-origin']).to.equal('http://example.com/');
+
+ const res2 = await server.inject({ method: 'OPTIONS', url: '/b', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
+ expect(res2.statusCode).to.equal(200);
+ expect(res2.result.message).to.equal('CORS is disabled for this route');
+ expect(res2.headers['access-control-allow-origin']).to.not.exist();
+ });
+
+ it('allows headers on multiple routes but not all', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/a', handler: () => 'ok', options: { cors: true } });
+ server.route({ method: 'GET', path: '/b', handler: () => 'ok', options: { cors: true } });
+ server.route({ method: 'GET', path: '/c', handler: () => 'ok' });
+
+ const res1 = await server.inject({ method: 'OPTIONS', url: '/a', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
+ expect(res1.statusCode).to.equal(200);
+ expect(res1.result).to.be.null();
+ expect(res1.headers['access-control-allow-origin']).to.equal('http://example.com/');
+
+ const res2 = await server.inject({ method: 'OPTIONS', url: '/b', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
+ expect(res2.statusCode).to.equal(200);
+ expect(res2.result).to.be.null();
+ expect(res2.headers['access-control-allow-origin']).to.equal('http://example.com/');
+
+ const res3 = await server.inject({ method: 'OPTIONS', url: '/c', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
+ expect(res3.statusCode).to.equal(200);
+ expect(res3.result.message).to.equal('CORS is disabled for this route');
+ expect(res3.headers['access-control-allow-origin']).to.not.exist();
+ });
+
+ it('allows same headers on multiple routes with same path', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/a', handler: () => 'ok', options: { cors: true } });
+ server.route({ method: 'POST', path: '/a', handler: () => 'ok', options: { cors: true } });
+
+ const res = await server.inject({ method: 'OPTIONS', url: '/a', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.be.null();
+ expect(res.headers['access-control-allow-origin']).to.equal('http://example.com/');
+ });
+
+ it('returns headers on single route (overrides defaults)', async () => {
+
+ const server = Hapi.server({ routes: { cors: { origin: ['b'] } } });
+ server.route({ method: 'GET', path: '/a', handler: () => 'ok', options: { cors: { origin: ['a'] } } });
+ server.route({ method: 'GET', path: '/b', handler: () => 'ok' });
+
+ const res1 = await server.inject({ method: 'OPTIONS', url: '/a', headers: { origin: 'a', 'access-control-request-method': 'GET' } });
+ expect(res1.statusCode).to.equal(200);
+ expect(res1.result).to.be.null();
+ expect(res1.headers['access-control-allow-origin']).to.equal('a');
+
+ const res2 = await server.inject({ method: 'OPTIONS', url: '/b', headers: { origin: 'b', 'access-control-request-method': 'GET' } });
+ expect(res2.statusCode).to.equal(200);
+ expect(res2.result).to.be.null();
+ expect(res2.headers['access-control-allow-origin']).to.equal('b');
+ });
+
+ it('sets access-control-allow-credentials header', async () => {
+
+ const server = Hapi.server({ routes: { cors: { credentials: true } } });
+ server.route({ method: 'GET', path: '/', handler: () => null });
+
+ const res = await server.inject({ url: '/', headers: { origin: 'http://example.com/' } });
+ expect(res.statusCode).to.equal(204);
+ expect(res.result).to.equal(null);
+ expect(res.headers['access-control-allow-credentials']).to.equal('true');
+ });
+
+ it('combines server defaults with route config', async () => {
+
+ const server = Hapi.server({ routes: { cors: { origin: ['http://example.com/'] } } });
+ server.route({ method: 'GET', path: '/', handler: () => null, options: { cors: { credentials: true } } });
+
+ const res1 = await server.inject({ url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
+ expect(res1.statusCode).to.equal(204);
+ expect(res1.result).to.equal(null);
+ expect(res1.headers['access-control-allow-credentials']).to.equal('true');
+
+ const res2 = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
+ expect(res2.statusCode).to.equal(200);
+ expect(res2.result).to.equal(null);
+ expect(res2.headers['access-control-allow-credentials']).to.equal('true');
+
+ const res3 = await server.inject({ url: '/', headers: { origin: 'http://example.org/', 'access-control-request-method': 'GET' } });
+ expect(res3.statusCode).to.equal(204);
+ expect(res3.result).to.equal(null);
+ expect(res3.headers['access-control-allow-credentials']).to.not.exist();
+
+ const res4 = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.org/', 'access-control-request-method': 'GET' } });
+ expect(res4.statusCode).to.equal(200);
+ expect(res4.result).to.equal({ message: 'CORS error: Origin not allowed' });
+ expect(res4.headers['access-control-allow-credentials']).to.not.exist();
+ expect(res4.headers['access-control-allow-origin']).to.not.exist();
+ });
+
+ it('handles request without origin header', async () => {
+
+ const server = Hapi.server({ port: 8080, routes: { cors: { origin: ['http://*.domain.com'] } } });
+ server.route({ method: 'GET', path: '/test', handler: () => null });
+
+ const res1 = await server.inject('/');
+ expect(res1.statusCode).to.equal(404);
+ expect(res1.headers['access-control-allow-origin']).to.not.exist();
+
+ const res2 = await server.inject('/test');
+ expect(res2.statusCode).to.equal(204);
+ expect(res2.headers['access-control-allow-origin']).to.not.exist();
+ });
+
+ it('handles missing routes', async () => {
+
+ const server = Hapi.server({ port: 8080, routes: { cors: { origin: ['http://*.domain.com'] } } });
+
+ const res1 = await server.inject('/');
+ expect(res1.statusCode).to.equal(404);
+ expect(res1.headers['access-control-allow-origin']).to.not.exist();
+
+ const res2 = await server.inject({ url: '/', headers: { origin: 'http://example.domain.com' } });
+ expect(res2.statusCode).to.equal(404);
+ expect(res2.headers['access-control-allow-origin']).to.exist();
+ });
+
+ it('uses server defaults in onRequest', async () => {
+
+ const server = Hapi.server({ port: 8080, routes: { cors: { origin: ['http://*.domain.com'] } } });
+
+ server.ext('onRequest', (request, h) => {
+
+ expect(request.info.cors).to.be.null(); // Do not set potentially incorrect information
+ return h.response('skip').takeover();
+ });
+
+ const res1 = await server.inject({ url: '/', headers: { origin: 'http://example.domain.com' } });
+ expect(res1.statusCode).to.equal(200);
+ expect(res1.headers['access-control-allow-origin']).to.exist();
+
+ const res2 = await server.inject({ url: '/', headers: { origin: 'http://example.domain.net' } });
+ expect(res2.statusCode).to.equal(200);
+ expect(res2.headers['access-control-allow-origin']).to.not.exist();
+ });
+
+ describe('headers()', () => {
+
+ it('returns CORS origin (route level)', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler: () => 'ok', options: { cors: true } });
+
+ const res1 = await server.inject({ url: '/', headers: { origin: 'http://example.com/' } });
+ expect(res1.statusCode).to.equal(200);
+ expect(res1.result).to.exist();
+ expect(res1.result).to.equal('ok');
+ expect(res1.headers['access-control-allow-origin']).to.equal('http://example.com/');
+
+ const res2 = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
+ expect(res2.statusCode).to.equal(200);
+ expect(res2.result).to.be.null();
+ expect(res2.headers['access-control-allow-origin']).to.equal('http://example.com/');
+ });
+
+ it('returns CORS origin (GET)', async () => {
+
+ const server = Hapi.server({ routes: { cors: { origin: ['http://x.example.com', 'http://www.example.com'] } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'ok' });
+
+ const res = await server.inject({ url: '/', headers: { origin: 'http://x.example.com' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('ok');
+ expect(res.headers['access-control-allow-origin']).to.equal('http://x.example.com');
+ });
+
+ it('returns CORS origin (OPTIONS)', async () => {
+
+ const server = Hapi.server({ routes: { cors: { origin: ['http://test.example.com', 'http://www.example.com'] } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'ok' });
+
+ const res = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://test.example.com', 'access-control-request-method': 'GET' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.payload.length).to.equal(0);
+ expect(res.headers['access-control-allow-origin']).to.equal('http://test.example.com');
+ });
+
+ it('merges CORS access-control-expose-headers header', async () => {
+
+ const handler = (request, h) => {
+
+ return h.response('ok').header('access-control-expose-headers', 'something');
+ };
+
+ const server = Hapi.server({ routes: { cors: { additionalExposedHeaders: ['xyz'] } } });
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject({ url: '/', headers: { origin: 'http://example.com/' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('ok');
+ expect(res.headers['access-control-expose-headers']).to.equal('something,WWW-Authenticate,Server-Authorization,xyz');
+ });
+
+ it('returns no CORS headers when route CORS disabled', async () => {
+
+ const server = Hapi.server({ routes: { cors: { origin: ['http://test.example.com', 'http://www.example.com'] } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'ok', options: { cors: false } });
+
+ const res = await server.inject({ url: '/', headers: { origin: 'http://x.example.com' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('ok');
+ expect(res.headers['access-control-allow-origin']).to.not.exist();
+ });
+
+ it('returns matching CORS origin', async () => {
+
+ const handler = (request, h) => {
+
+ return h.response('Tada').header('vary', 'x-test');
+ };
+
+ const server = Hapi.server({ compression: { minBytes: 1 }, routes: { cors: { origin: ['http://test.example.com', 'http://www.example.com', 'http://*.a.com'] } } });
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject({ url: '/', headers: { origin: 'http://www.example.com' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Tada');
+ expect(res.headers['access-control-allow-origin']).to.equal('http://www.example.com');
+ expect(res.headers.vary).to.equal('x-test,origin,accept-encoding');
+ });
+
+ it('returns origin header when matching against *', async () => {
+
+ const handler = (request, h) => {
+
+ return h.response('Tada').header('vary', 'x-test');
+ };
+
+ const server = Hapi.server({ compression: { minBytes: 1 }, routes: { cors: { origin: ['*'] } } });
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject({ url: '/', headers: { origin: 'http://www.example.com' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Tada');
+ expect(res.headers['access-control-allow-origin']).to.equal('http://www.example.com');
+ expect(res.headers.vary).to.equal('x-test,origin,accept-encoding');
+ });
+
+ it('returns * origin header when matching against * and origin is ignored', async () => {
+
+ const handler = (request, h) => {
+
+ return h.response('Tada').header('vary', 'x-test');
+ };
+
+ const server = Hapi.server({ compression: { minBytes: 1 }, routes: { cors: { origin: 'ignore' } } });
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject({ url: '/', headers: { origin: 'http://www.example.com' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Tada');
+ expect(res.headers['access-control-allow-origin']).to.equal('*');
+ expect(res.headers.vary).to.equal('x-test,accept-encoding');
+ });
+
+ it('returns matching CORS origin wildcard', async () => {
+
+ const handler = (request, h) => {
+
+ return h.response('Tada').header('vary', 'x-test');
+ };
+
+ const server = Hapi.server({ compression: { minBytes: 1 }, routes: { cors: { origin: ['http://test.example.com', 'http://www.example.com', 'http://*.a.com'] } } });
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject({ url: '/', headers: { origin: 'http://www.a.com' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Tada');
+ expect(res.headers['access-control-allow-origin']).to.equal('http://www.a.com');
+ expect(res.headers.vary).to.equal('x-test,origin,accept-encoding');
+ });
+
+ it('returns matching CORS origin wildcard when more than one wildcard', async () => {
+
+ const handler = (request, h) => {
+
+ return h.response('Tada').header('vary', 'x-test', true);
+ };
+
+ const server = Hapi.server({ compression: { minBytes: 1 }, routes: { cors: { origin: ['http://test.example.com', 'http://www.example.com', 'http://*.b.com', 'http://*.a.com'] } } });
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject({ url: '/', headers: { origin: 'http://www.a.com' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Tada');
+ expect(res.headers['access-control-allow-origin']).to.equal('http://www.a.com');
+ expect(res.headers.vary).to.equal('x-test,origin,accept-encoding');
+ });
+
+ it('does not set empty CORS expose headers', async () => {
+
+ const server = Hapi.server({ routes: { cors: { exposedHeaders: [] } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'ok' });
+
+ const res1 = await server.inject({ url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
+ expect(res1.statusCode).to.equal(200);
+ expect(res1.headers['access-control-allow-origin']).to.equal('http://example.com/');
+ expect(res1.headers['access-control-expose-headers']).to.not.exist();
+
+ const res2 = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
+ expect(res2.statusCode).to.equal(200);
+ expect(res2.headers['access-control-allow-origin']).to.equal('http://example.com/');
+ expect(res2.headers['access-control-expose-headers']).to.not.exist();
+ });
+ });
+
+ describe('options()', () => {
+
+ it('ignores OPTIONS route', () => {
+
+ const server = Hapi.server();
+ server.route({
+ method: 'OPTIONS',
+ path: '/',
+ handler: () => null
+ });
+
+ expect(server._core.router.special.options).to.not.exist();
+ });
+ });
+
+ describe('handler()', () => {
+
+ it('errors on missing origin header', async () => {
+
+ const server = Hapi.server({ routes: { cors: true } });
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: () => null
+ });
+
+ const res = await server.inject({ method: 'OPTIONS', url: '/', headers: { 'access-control-request-method': 'GET' } });
+ expect(res.statusCode).to.equal(404);
+ expect(res.result.message).to.equal('CORS error: Missing Origin header');
+ });
+
+ it('errors on missing access-control-request-method header', async () => {
+
+ const server = Hapi.server({ routes: { cors: true } });
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: () => null
+ });
+
+ const res = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.com/' } });
+ expect(res.statusCode).to.equal(404);
+ expect(res.result.message).to.equal('CORS error: Missing Access-Control-Request-Method header');
+ });
+
+ it('errors on missing route', async () => {
+
+ const server = Hapi.server({ routes: { cors: true } });
+
+ const res = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
+ expect(res.statusCode).to.equal(404);
+ });
+
+ it('errors on mismatching origin header', async () => {
+
+ const server = Hapi.server({ routes: { cors: { origin: ['a'] } } });
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: () => null
+ });
+
+ const res = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result.message).to.equal('CORS error: Origin not allowed');
+ });
+
+ it('matches a wildcard origin if origin is ignored and present', async () => {
+
+ const server = Hapi.server({ routes: { cors: { origin: 'ignore' } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'ok' });
+
+ const res = await server.inject({
+ method: 'OPTIONS',
+ url: '/',
+ headers: {
+ origin: 'http://test.example.com',
+ 'access-control-request-method': 'GET',
+ 'access-control-request-headers': 'Authorization'
+ }
+ });
+
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers['access-control-allow-origin']).to.equal('*');
+
+ });
+
+ it('matches a wildcard origin if origin is ignored and missing', async () => {
+
+ const server = Hapi.server({ routes: { cors: { origin: 'ignore' } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'ok' });
+
+ const res = await server.inject({
+ method: 'OPTIONS',
+ url: '/',
+ headers: {
+ 'access-control-request-method': 'GET',
+ 'access-control-request-headers': 'Authorization'
+ }
+ });
+
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers['access-control-allow-origin']).to.equal('*');
+ });
+
+ it('matches allowed headers', async () => {
+
+ const server = Hapi.server({ routes: { cors: true } });
+ server.route({ method: 'GET', path: '/', handler: () => 'ok' });
+
+ const res = await server.inject({
+ method: 'OPTIONS',
+ url: '/',
+ headers: {
+ origin: 'http://test.example.com',
+ 'access-control-request-method': 'GET',
+ 'access-control-request-headers': 'Authorization'
+ }
+ });
+
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers['access-control-allow-headers']).to.equal('Accept,Authorization,Content-Type,If-None-Match');
+ });
+
+ it('matches allowed headers (case insensitive)', async () => {
+
+ const server = Hapi.server({ routes: { cors: true } });
+ server.route({ method: 'GET', path: '/', handler: () => 'ok' });
+
+ const res = await server.inject({
+ method: 'OPTIONS',
+ url: '/',
+ headers: {
+ origin: 'http://test.example.com',
+ 'access-control-request-method': 'GET',
+ 'access-control-request-headers': 'authorization'
+ }
+ });
+
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers['access-control-allow-headers']).to.equal('Accept,Authorization,Content-Type,If-None-Match');
+ });
+
+ it('matches allowed headers (Origin explicit)', async () => {
+
+ const server = Hapi.server({ routes: { cors: { additionalHeaders: ['Origin'] } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'ok' });
+
+ const res = await server.inject({
+ method: 'OPTIONS',
+ url: '/',
+ headers: {
+ origin: 'http://test.example.com',
+ 'access-control-request-method': 'GET',
+ 'access-control-request-headers': 'Origin'
+ }
+ });
+
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers['access-control-allow-headers']).to.equal('Accept,Authorization,Content-Type,If-None-Match,Origin');
+ expect(res.headers['access-control-expose-headers']).to.equal('WWW-Authenticate,Server-Authorization');
+ });
+
+ it('responds with configured preflight status code', async () => {
+
+ const server = Hapi.server({ routes: { cors: { preflightStatusCode: 204 } } });
+ server.route({ method: 'GET', path: '/204', handler: () => 'ok', options: { cors: true } });
+ server.route({ method: 'GET', path: '/200', handler: () => 'ok', options: { cors: { preflightStatusCode: 200 } } });
+
+ const res1 = await server.inject({
+ method: 'OPTIONS',
+ url: '/204',
+ headers: {
+ origin: 'http://test.example.com',
+ 'access-control-request-method': 'GET'
+ }
+ });
+
+ expect(res1.statusCode).to.equal(204);
+
+ const res2 = await server.inject({
+ method: 'OPTIONS',
+ url: '/200',
+ headers: {
+ origin: 'http://test.example.com',
+ 'access-control-request-method': 'GET'
+ }
+ });
+
+ expect(res2.statusCode).to.equal(200);
+ });
+
+ it('matches allowed headers (Origin implicit)', async () => {
+
+ const server = Hapi.server({ routes: { cors: true } });
+ server.route({ method: 'GET', path: '/', handler: () => 'ok' });
+
+ const res = await server.inject({
+ method: 'OPTIONS',
+ url: '/',
+ headers: {
+ origin: 'http://test.example.com',
+ 'access-control-request-method': 'GET',
+ 'access-control-request-headers': 'Origin'
+ }
+ });
+
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers['access-control-allow-headers']).to.equal('Accept,Authorization,Content-Type,If-None-Match');
+ });
+
+ it('errors on disallowed headers', async () => {
+
+ const server = Hapi.server({ routes: { cors: true } });
+ server.route({ method: 'GET', path: '/', handler: () => 'ok' });
+
+ const res = await server.inject({
+ method: 'OPTIONS',
+ url: '/',
+ headers: {
+ origin: 'http://test.example.com',
+ 'access-control-request-method': 'GET',
+ 'access-control-request-headers': 'X'
+ }
+ });
+
+ expect(res.statusCode).to.equal(200);
+ expect(res.result.message).to.equal('CORS error: Some headers are not allowed');
+ });
+
+ it('allows credentials', async () => {
+
+ const server = Hapi.server({ routes: { cors: { credentials: true } } });
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: () => null
+ });
+
+ const res = await server.inject({ method: 'OPTIONS', url: '/', headers: { origin: 'http://example.com/', 'access-control-request-method': 'GET' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers['access-control-allow-credentials']).to.equal('true');
+ });
+
+ it('correctly finds route when using vhost setting', async () => {
+
+ const server = Hapi.server({ routes: { cors: true } });
+ server.route({
+ method: 'POST',
+ vhost: 'example.com',
+ path: '/',
+ handler: () => null
+ });
+
+ const res = await server.inject({ method: 'OPTIONS', url: 'http://example.com:4000/', headers: { origin: 'http://localhost', 'access-control-request-method': 'POST' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers['access-control-allow-methods']).to.equal('POST');
+ });
+ });
+
+ describe('headers()', () => {
+
+ it('skips CORS when missing origin header and wildcard does not ignore origin', async () => {
+
+ const server = Hapi.server({ routes: { cors: { origin: ['*'] } } });
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: () => 'ok'
+ });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers['access-control-allow-origin']).to.not.exist();
+ });
+
+ it('uses CORS when missing origin header and wildcard ignores origin', async () => {
+
+ const server = Hapi.server({ routes: { cors: { origin: 'ignore' } } });
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: () => 'ok'
+ });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers['access-control-allow-origin']).to.equal('*');
+ });
+ });
+});
diff --git a/test/file/note.txt b/test/file/note.txt
old mode 100644
new mode 100755
diff --git a/test/handler.js b/test/handler.js
index 118ef0ac3..334a1a3d9 100755
--- a/test/handler.js
+++ b/test/handler.js
@@ -1,230 +1,201 @@
-// Load modules
+'use strict';
-var Path = require('path');
-var Boom = require('boom');
-var Code = require('code');
-var Handlebars = require('handlebars');
-var Hapi = require('..');
-var Hoek = require('hoek');
-var Inert = require('inert');
-var Lab = require('lab');
-var Vision = require('vision');
+const Boom = require('@hapi/boom');
+const Code = require('@hapi/code');
+const Hapi = require('..');
+const Hoek = require('@hapi/hoek');
+const Lab = require('@hapi/lab');
-// Declare internals
+const internals = {};
-var internals = {};
+const { describe, it } = exports.lab = Lab.script();
+const expect = Code.expect;
-// Test shortcuts
-var lab = exports.lab = Lab.script();
-var describe = lab.describe;
-var it = lab.it;
-var expect = Code.expect;
+describe('handler', () => {
+ describe('execute()', () => {
-describe('handler', function () {
+ it('bypasses onPostHandler when handler calls takeover()', async () => {
- describe('execute()', function () {
+ const server = Hapi.server();
+ server.ext('onPostHandler', () => 'else');
+ server.route({ method: 'GET', path: '/', handler: (request, h) => 'something' });
+ server.route({ method: 'GET', path: '/takeover', handler: (request, h) => h.response('something').takeover() });
- it('returns 500 on handler exception (same tick)', function (done) {
+ const res1 = await server.inject('/');
+ expect(res1.result).to.equal('else');
- var server = new Hapi.Server({ debug: false });
- server.connection();
+ const res2 = await server.inject('/takeover');
+ expect(res2.result).to.equal('something');
+ });
- var handler = function (request) {
+ it('returns 500 on handler exception (same tick)', async () => {
- var x = a.b.c;
- };
+ const server = Hapi.server({ debug: false });
+
+ const handler = (request) => {
- server.route({ method: 'GET', path: '/domain', handler: handler });
+ const a = null;
+ a.b.c;
+ };
- server.inject('/domain', function (res) {
+ server.route({ method: 'GET', path: '/domain', handler });
- expect(res.statusCode).to.equal(500);
- done();
- });
+ const res = await server.inject('/domain');
+ expect(res.statusCode).to.equal(500);
});
- it('returns 500 on handler exception (next tick)', { parallel: false }, function (done) {
-
- var handler = function (request) {
+ it('returns 500 on handler exception (next tick await)', async () => {
- setImmediate(function () {
+ const handler = async (request) => {
- var x = not.here;
- });
+ await Hoek.wait(0);
+ const not = null;
+ not.here;
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
- server.on('request-error', function (request, err) {
-
- expect(err.message).to.equal('Uncaught error: not is not defined');
- done();
- });
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+ const log = server.events.once({ name: 'request', channels: 'error' });
- var orig = console.error;
- console.error = function () {
+ const orig = console.error;
+ console.error = function (...args) {
console.error = orig;
- expect(arguments[0]).to.equal('Debug:');
- expect(arguments[1]).to.equal('internal, implementation, error');
+ expect(args[0]).to.equal('Debug:');
+ expect(args[1]).to.equal('internal, implementation, error');
};
- server.inject('/', function (res) {
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
- expect(res.statusCode).to.equal(500);
- });
+ const [, event] = await log;
+ expect(event.error.message).to.include(['Cannot read prop', 'null', 'here']);
});
});
- describe('handler()', function () {
+ describe('handler()', () => {
- it('binds handler to route bind object', function (done) {
+ it('binds handler to route bind object', async () => {
- var item = { x: 123 };
+ const item = { x: 123 };
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
- config: {
- handler: function (request, reply) {
+ options: {
+ handler: function (request) {
- return reply(this.x);
+ return this.x;
},
bind: item
}
});
- server.inject('/', function (res) {
-
- expect(res.result).to.equal(item.x);
- done();
- });
+ const res = await server.inject('/');
+ expect(res.result).to.equal(item.x);
});
- it('invokes handler with right arguments', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- var handler = function (request, reply) {
-
- expect(arguments.length).to.equal(2);
- expect(reply.send).to.not.exist();
- return reply('ok');
- };
-
- server.route({ method: 'GET', path: '/', handler: handler });
+ it('binds handler to route bind object (toolkit)', async () => {
- server.inject('/', function (res) {
+ const item = { x: 123 };
- expect(res.result).to.equal('ok');
- done();
+ const server = Hapi.server();
+ server.route({
+ method: 'GET',
+ path: '/',
+ options: {
+ handler: (request, h) => h.context.x,
+ bind: item
+ }
});
+
+ const res = await server.inject('/');
+ expect(res.result).to.equal(item.x);
});
- });
- describe('register()', function () {
+ it('returns 500 on ext method exception (same tick)', async () => {
- it('returns a file', function (done) {
+ const server = Hapi.server({ debug: false });
- var server = new Hapi.Server();
- server.register(Inert, Hoek.ignore);
- server.connection({ routes: { files: { relativeTo: __dirname } } });
- var handler = function (request, reply) {
+ const onRequest = function () {
- return reply.file('../package.json').code(499);
+ const a = null;
+ a.b.c;
};
- server.route({ method: 'GET', path: '/file', handler: handler });
+ server.ext('onRequest', onRequest);
- server.inject('/file', function (res) {
+ server.route({ method: 'GET', path: '/domain', handler: () => 'neven gonna happen' });
- expect(res.statusCode).to.equal(499);
- expect(res.payload).to.contain('hapi');
- expect(res.headers['content-type']).to.equal('application/json; charset=utf-8');
- expect(res.headers['content-length']).to.exist();
- expect(res.headers['content-disposition']).to.not.exist();
- done();
- });
+ const res = await server.inject('/domain');
+ expect(res.statusCode).to.equal(500);
});
- it('returns a view', function (done) {
+ it('returns 500 on custom function error', async () => {
- var server = new Hapi.Server();
- server.register(Vision, Hoek.ignore);
- server.connection();
+ const server = Hapi.server({ debug: false });
- server.views({
- engines: { 'html': Handlebars },
- relativeTo: Path.join(__dirname, '/templates/plugin')
- });
+ const onPreHandler = function (request, h) {
+
+ request.app.custom = () => {
- var handler = function (request, reply) {
+ throw new Error('oops');
+ };
- return reply.view('test', { message: 'steve' });
+ return h.continue;
};
- server.route({ method: 'GET', path: '/', handler: handler });
+ server.ext('onPreHandler', onPreHandler);
- server.inject('/', function (res) {
+ server.route({ method: 'GET', path: '/', handler: (request) => request.app.custom() });
- expect(res.result).to.equal('
steve
');
- done();
- });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
});
});
- describe('prerequisites()', function () {
+ describe('prerequisitesConfig()', () => {
- it('shows the complete prerequisite pipeline in the response', function (done) {
+ it('shows the complete prerequisite pipeline in the response', async () => {
- var pre1 = function (request, reply) {
+ const pre1 = (request, h) => {
- return reply('Hello').code(444);
+ return h.response('Hello').code(444);
};
- var pre2 = function (request, reply) {
-
- return reply(request.pre.m1 + request.pre.m3 + request.pre.m4);
- };
-
- var pre3 = function (request, reply) {
-
- process.nextTick(function () {
+ const pre2 = (request) => {
- return reply(' ');
- });
+ return request.pre.m1 + request.pre.m3 + request.pre.m4;
};
- var pre4 = function (request, reply) {
+ const pre3 = async (request) => {
- return reply('World');
+ await Hoek.wait(0);
+ return ' ';
};
- var pre5 = function (request, reply) {
-
- return reply(request.pre.m2 + '!');
- };
+ const pre4 = () => 'World';
- var handler = function (request, reply) {
+ const pre5 = (request) => {
- return reply(request.pre.m5);
+ return request.pre.m2 + (request.pre.m0 === null ? '!' : 'x');
};
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
- config: {
+ options: {
pre: [
+ {
+ method: (request, h) => h.continue,
+ assign: 'm0'
+ },
[
{ method: pre1, assign: 'm1' },
{ method: pre3, assign: 'm3' },
@@ -233,117 +204,77 @@ describe('handler', function () {
{ method: pre2, assign: 'm2' },
{ method: pre5, assign: 'm5' }
],
- handler: handler
+ handler: (request) => request.pre.m5
}
});
- server.inject('/', function (res) {
-
- expect(res.result).to.equal('Hello World!');
- done();
- });
+ const res = await server.inject('/');
+ expect(res.result).to.equal('Hello World!');
});
- it('allows a single prerequisite', function (done) {
-
- var pre = function (request, reply) {
-
- return reply('Hello');
- };
-
- var handler = function (request, reply) {
-
- return reply(request.pre.p);
- };
+ it('allows a single prerequisite', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
- config: {
+ options: {
pre: [
- { method: pre, assign: 'p' }
+ { method: () => 'Hello', assign: 'p' }
],
- handler: handler
+ handler: (request) => request.pre.p
}
});
- server.inject('/', function (res) {
-
- expect(res.result).to.equal('Hello');
- done();
- });
+ const res = await server.inject('/');
+ expect(res.result).to.equal('Hello');
});
- it('allows an empty prerequisite array', function (done) {
-
- var handler = function (request, reply) {
+ it('allows an empty prerequisite array', async () => {
- return reply('Hello');
- };
-
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
- config: {
+ options: {
pre: [],
- handler: handler
+ handler: () => 'Hello'
}
});
- server.inject('/', function (res) {
-
- expect(res.result).to.equal('Hello');
- done();
- });
+ const res = await server.inject('/');
+ expect(res.result).to.equal('Hello');
});
- it('takes over response', function (done) {
-
- var pre1 = function (request, reply) {
-
- return reply('Hello');
- };
-
- var pre2 = function (request, reply) {
-
- return reply(request.pre.m1 + request.pre.m3 + request.pre.m4);
- };
+ it('takes over response', async () => {
- var pre3 = function (request, reply) {
+ const pre1 = () => 'Hello';
- process.nextTick(function () {
+ const pre2 = (request) => {
- return reply(' ').takeover();
- });
+ return request.pre.m1 + request.pre.m3 + request.pre.m4;
};
- var pre4 = function (request, reply) {
+ const pre3 = async (request, h) => {
- return reply('World');
+ await Hoek.wait(0);
+ return h.response(' ').takeover();
};
- var pre5 = function (request, reply) {
+ const pre4 = () => 'World';
- return reply(request.pre.m2 + '!');
- };
+ const pre5 = (request) => {
- var handler = function (request, reply) {
-
- return reply(request.pre.m5);
+ return request.pre.m2 + '!';
};
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
- config: {
+ options: {
pre: [
[
{ method: pre1, assign: 'm1' },
@@ -353,739 +284,325 @@ describe('handler', function () {
{ method: pre2, assign: 'm2' },
{ method: pre5, assign: 'm5' }
],
- handler: handler
+ handler: (request) => request.pre.m5
}
});
- server.inject('/', function (res) {
-
- expect(res.result).to.equal(' ');
- done();
- });
+ const res = await server.inject('/');
+ expect(res.result).to.equal(' ');
});
- it('returns error if prerequisite returns error', function (done) {
-
- var pre1 = function (request, reply) {
-
- return reply('Hello');
- };
-
- var pre2 = function (request, reply) {
+ it('returns error if prerequisite returns error', async () => {
- return reply(Boom.internal('boom'));
- };
+ const pre1 = () => 'Hello';
- var handler = function (request, reply) {
+ const pre2 = function () {
- return reply(request.pre.m1);
+ throw Boom.internal('boom');
};
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
- config: {
+ options: {
pre: [
[{ method: pre1, assign: 'm1' }],
{ method: pre2, assign: 'm2' }
],
- handler: handler
+ handler: (request) => request.pre.m1
}
});
- server.inject('/', function (res) {
-
- expect(res.result.statusCode).to.equal(500);
- done();
- });
+ const res = await server.inject('/');
+ expect(res.result.statusCode).to.equal(500);
});
- it('passes wrapped object', function (done) {
+ it('passes wrapped object', async () => {
- var pre = function (request, reply) {
+ const pre = (request, h) => {
- return reply('Hello').code(444);
+ return h.response('Hello').code(444);
};
- var handler = function (request, reply) {
-
- return reply(request.preResponses.p);
- };
-
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
- config: {
+ options: {
pre: [
{ method: pre, assign: 'p' }
],
- handler: handler
+ handler: (request) => request.preResponses.p
}
});
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(444);
- done();
- });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(444);
});
- it('returns 500 if prerequisite throws', function (done) {
-
- var pre1 = function (request, reply) {
+ it('returns 500 if prerequisite throws', async () => {
- return reply('Hello');
- };
-
- var pre2 = function (request, reply) {
+ const pre1 = () => 'Hello';
+ const pre2 = function () {
+ const a = null;
a.b.c = 0;
};
- var handler = function (request, reply) {
-
- return reply(request.pre.m1);
- };
-
-
- var server = new Hapi.Server({ debug: false });
- server.connection();
+ const server = Hapi.server({ debug: false });
server.route({
method: 'GET',
path: '/',
- config: {
+ options: {
pre: [
[{ method: pre1, assign: 'm1' }],
{ method: pre2, assign: 'm2' }
],
- handler: handler
- }
- });
-
- server.inject('/', function (res) {
-
- expect(res.result.statusCode).to.equal(500);
- done();
- });
- });
-
- it('returns a user record using server method', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- server.method('user', function (id, next) {
-
- return next(null, { id: id, name: 'Bob' });
- });
-
- server.route({
- method: 'GET',
- path: '/user/{id}',
- config: {
- pre: [
- 'user(params.id)'
- ],
- handler: function (request, reply) {
-
- return reply(request.pre.user);
- }
+ handler: (request) => request.pre.m1
}
});
- server.inject('/user/5', function (res) {
-
- expect(res.result).to.deep.equal({ id: '5', name: 'Bob' });
- done();
- });
+ const res = await server.inject('/');
+ expect(res.result.statusCode).to.equal(500);
});
- it('returns a user record using server method in object', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- server.method('user', function (id, next) {
-
- return next(null, { id: id, name: 'Bob' });
- });
+ it('sets pre failAction to error', async () => {
+ const server = Hapi.server();
server.route({
method: 'GET',
- path: '/user/{id}',
- config: {
+ path: '/',
+ options: {
pre: [
{
- method: 'user(params.id)',
- assign: 'steve'
+ method: () => {
+
+ throw Boom.forbidden();
+ },
+ failAction: 'error'
}
],
- handler: function (request, reply) {
-
- return reply(request.pre.steve);
- }
+ handler: () => 'ok'
}
});
- server.inject('/user/5', function (res) {
-
- expect(res.result).to.deep.equal({ id: '5', name: 'Bob' });
- done();
- });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(403);
});
- it('returns a user name using multiple server methods', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- server.method('user', function (id, next) {
-
- return next(null, { id: id, name: 'Bob' });
- });
-
- server.method('name', function (user, next) {
-
- return next(null, user.name);
- });
+ it('sets pre failAction to ignore', async () => {
+ const server = Hapi.server();
server.route({
method: 'GET',
- path: '/user/{id}/name',
- config: {
+ path: '/',
+ options: {
pre: [
- 'user(params.id)',
- 'name(pre.user)'
- ],
- handler: function (request, reply) {
-
- return reply(request.pre.name);
- }
- }
- });
-
- server.inject('/user/5/name', function (res) {
-
- expect(res.result).to.equal('Bob');
- done();
- });
- });
-
- it('returns a user record using server method with trailing space', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- server.method('user', function (id, next) {
-
- return next(null, { id: id, name: 'Bob' });
- });
+ {
+ method: () => {
- server.route({
- method: 'GET',
- path: '/user/{id}',
- config: {
- pre: [
- 'user(params.id )'
+ throw Boom.forbidden();
+ },
+ failAction: 'ignore'
+ }
],
- handler: function (request, reply) {
-
- return reply(request.pre.user);
- }
+ handler: () => 'ok'
}
});
- server.inject('/user/5', function (res) {
-
- expect(res.result).to.deep.equal({ id: '5', name: 'Bob' });
- done();
- });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
});
- it('returns a user record using server method with leading space', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- server.method('user', function (id, next) {
-
- return next(null, { id: id, name: 'Bob' });
- });
+ it('sets pre failAction to log', async () => {
+ const server = Hapi.server();
server.route({
method: 'GET',
- path: '/user/{id}',
- config: {
+ path: '/',
+ options: {
pre: [
- 'user( params.id)'
- ],
- handler: function (request, reply) {
-
- return reply(request.pre.user);
- }
- }
- });
-
- server.inject('/user/5', function (res) {
-
- expect(res.result).to.deep.equal({ id: '5', name: 'Bob' });
- done();
- });
- });
-
- it('returns a user record using server method with zero args', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- server.method('user', function (next) {
-
- return next(null, { name: 'Bob' });
- });
+ {
+ assign: 'before',
+ method: () => {
- server.route({
- method: 'GET',
- path: '/user',
- config: {
- pre: [
- 'user()'
+ throw Boom.forbidden();
+ },
+ failAction: 'log'
+ }
],
- handler: function (request, reply) {
-
- return reply(request.pre.user);
- }
- }
- });
+ handler: (request) => {
- server.inject('/user', function (res) {
-
- expect(res.result).to.deep.equal({ name: 'Bob' });
- done();
- });
- });
-
- it('returns a user record using server method with no args', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- server.method('user', function (request, next) {
-
- return next(null, { id: request.params.id, name: 'Bob' });
- });
+ if (request.pre.before === request.preResponses.before &&
+ request.pre.before instanceof Error) {
- server.route({
- method: 'GET',
- path: '/user/{id}',
- config: {
- pre: [
- 'user'
- ],
- handler: function (request, reply) {
+ return 'ok';
+ }
- return reply(request.pre.user);
+ throw new Error();
}
}
});
- server.inject('/user/5', function (res) {
-
- expect(res.result).to.deep.equal({ id: '5', name: 'Bob' });
- done();
- });
- });
-
- it('returns a user record using server method with nested name', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- server.method('user.get', function (next) {
+ let logged;
+ server.events.on({ name: 'request', channels: 'internal' }, (request, event, tags) => {
- return next(null, { name: 'Bob' });
- });
-
- server.route({
- method: 'GET',
- path: '/user',
- config: {
- pre: [
- 'user.get()'
- ],
- handler: function (request, reply) {
+ if (tags.pre &&
+ tags.error) {
- return reply(request.pre['user.get']);
- }
+ logged = event;
}
});
- server.inject('/user', function (res) {
-
- expect(res.result).to.deep.equal({ name: 'Bob' });
- done();
- });
- });
-
- it('fails on bad method name', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- var test = function () {
-
- server.route({
- method: 'GET',
- path: '/x/{id}',
- config: {
- pre: [
- 'xuser(params.id)'
- ],
- handler: function (request, reply) {
-
- return reply(request.pre.user);
- }
- }
- });
- };
-
- expect(test).to.throw('Unknown server method in string notation: xuser(params.id)');
- done();
- });
-
- it('fails on bad method syntax name', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- var test = function () {
-
- server.route({
- method: 'GET',
- path: '/x/{id}',
- config: {
- pre: [
- 'userparams.id)'
- ],
- handler: function (request, reply) {
-
- return reply(request.pre.user);
- }
- }
- });
- };
-
- expect(test).to.throw('Invalid server method string notation: userparams.id)');
- done();
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(logged.error.assign).to.equal('before');
});
- it('sets pre failAction to error', function (done) {
+ it('sets pre failAction to method', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
- config: {
+ options: {
pre: [
{
- method: function (request, reply) {
+ assign: 'value',
+ method: () => {
- return reply(Boom.forbidden());
+ throw Boom.forbidden();
},
- failAction: 'error'
- }
- ],
- handler: function (request, reply) {
-
- return reply('ok');
- }
- }
- });
-
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(403);
- done();
- });
- });
-
- it('sets pre failAction to ignore', function (done) {
+ failAction: (request, h, err) => {
- var server = new Hapi.Server();
- server.connection();
- server.route({
- method: 'GET',
- path: '/',
- config: {
- pre: [
- {
- method: function (request, reply) {
-
- return reply(Boom.forbidden());
- },
- failAction: 'ignore'
+ expect(err.output.statusCode).to.equal(403);
+ return 'failed';
+ }
}
],
- handler: function (request, reply) {
-
- return reply('ok');
- }
+ handler: (request) => (request.pre.value + '!')
}
});
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(200);
- done();
- });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('failed!');
});
- it('sets pre failAction to log', function (done) {
+ it('sets pre failAction to method with takeover', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
- config: {
+ options: {
pre: [
{
- assign: 'before',
- method: function (request, reply) {
+ assign: 'value',
+ method: () => {
- return reply(Boom.forbidden());
+ throw Boom.forbidden();
},
- failAction: 'log'
+ failAction: (request, h, err) => {
+
+ expect(err.output.statusCode).to.equal(403);
+ return h.response('failed').takeover();
+ }
}
],
- handler: function (request, reply) {
-
- return reply('ok');
- }
- }
- });
-
- var log = null;
- server.on('request-internal', function (request, event, tags) {
-
- if (event.internal &&
- tags.pre &&
- tags.error) {
-
- log = event.data.assign;
+ handler: (request) => (request.pre.value + '!')
}
});
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(200);
- expect(log).to.equal('before');
- done();
- });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('failed');
});
- it('binds pre to route bind object', function (done) {
+ it('binds pre to route bind object', async () => {
- var item = { x: 123 };
+ const item = { x: 123 };
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
- config: {
+ options: {
pre: [{
- method: function (request, reply) {
+ method: function (request) {
- return reply(this.x);
- }, assign: 'x'
+ return this.x;
+ },
+ assign: 'x'
}],
- handler: function (request, reply) {
-
- return reply(request.pre.x);
- },
+ handler: (request) => request.pre.x,
bind: item
}
});
- server.inject('/', function (res) {
-
- expect(res.result).to.equal(item.x);
- done();
- });
+ const res = await server.inject('/');
+ expect(res.result).to.equal(item.x);
});
- it('logs boom error instance as data if handler returns boom error', function (done) {
+ it('logs boom error instance as data if handler returns boom error', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.route({
method: 'GET',
path: '/',
- config: {
- handler: function (request, reply) {
-
- return reply(Boom.forbidden());
- }
- }
- });
-
- var log = null;
- server.on('request-internal', function (request, event, tags) {
-
- if (event.internal &&
- tags.handler &&
- tags.error) {
-
- log = event.data;
- }
- });
-
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(403);
- expect(log.data.isBoom).to.equal(true);
- expect(log.data.output.statusCode).to.equal(403);
- expect(log.data.message).to.equal('Forbidden');
- done();
- });
- });
-
- it('logs server method using string notation when cache enabled', function (done) {
+ options: {
+ handler: function () {
- var server = new Hapi.Server();
- server.connection();
-
- server.method('user', function (id, next) {
-
- return next(null, { id: id, name: 'Bob' });
- }, { cache: { expiresIn: 1000, generateTimeout: 10 } });
-
- server.route({
- method: 'GET',
- path: '/user/{id}',
- config: {
- pre: [
- 'user(params.id)'
- ],
- handler: function (request, reply) {
-
- return reply(request.getLog('method'));
+ throw Boom.forbidden();
}
}
});
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
-
- server.inject('/user/5', function (res) {
-
- expect(res.result[0].tags).to.deep.equal(['pre', 'method', 'user']);
- expect(res.result[0].internal).to.equal(true);
- expect(res.result[0].data.msec).to.exist();
- done();
- });
- });
- });
-
- it('uses server method with cache via string notation', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
+ const log = new Promise((resolve) => {
- var gen = 0;
- server.method('user', function (id, next) {
+ server.events.on({ name: 'request', channels: 'internal' }, (request, event, tags) => {
- return next(null, { id: id, name: 'Bob', gen: gen++ });
- }, { cache: { expiresIn: 1000, generateTimeout: 10 } });
+ if (tags.handler &&
+ tags.error) {
- server.route({
- method: 'GET',
- path: '/user/{id}',
- config: {
- pre: [
- 'user(params.id)'
- ],
- handler: function (request, reply) {
-
- return reply(request.pre.user.gen);
+ resolve({ event, tags });
}
- }
- });
-
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
-
- server.inject('/user/5', function (res1) {
-
- expect(res1.result).to.equal(0);
-
- server.inject('/user/5', function (res2) {
-
- expect(res2.result).to.equal(0);
- done();
- });
});
});
- });
- });
-
- describe('fromString()', function () {
-
- it('uses string handler', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- server.method('handler.get', function (request, reply) {
-
- return reply(null, request.params.x + request.params.y).code(299);
- });
- server.route({ method: 'GET', path: '/{x}/{y}', handler: 'handler.get' });
- server.inject('/a/b', function (res) {
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(403);
- expect(res.statusCode).to.equal(299);
- expect(res.result).to.equal('ab');
- done();
- });
+ const { event } = await log;
+ expect(event.error.isBoom).to.equal(true);
+ expect(event.error.output.statusCode).to.equal(403);
+ expect(event.error.message).to.equal('Forbidden');
+ expect(event.error.stack).to.exist();
});
});
- describe('defaults()', function () {
-
- it('returns handler without defaults', function (done) {
+ describe('defaults()', () => {
- var handler = function (route, options) {
+ it('returns handler without defaults', async () => {
- return function (request, reply) {
+ const handler = function (route, options) {
- return reply(request.route.settings.app);
- };
+ return (request) => request.route.settings.app;
};
- var server = new Hapi.Server();
- server.connection();
- server.handler('test', handler);
+ const server = Hapi.server();
+ server.decorate('handler', 'test', handler);
server.route({ method: 'get', path: '/', handler: { test: 'value' } });
- server.inject('/', function (res) {
-
- expect(res.result).to.deep.equal({});
- done();
- });
+ const res = await server.inject('/');
+ expect(res.result).to.equal({});
});
- it('returns handler with object defaults', function (done) {
-
- var handler = function (route, options) {
+ it('returns handler with object defaults', async () => {
- return function (request, reply) {
+ const handler = function (route, options) {
- return reply(request.route.settings.app);
- };
+ return (request) => request.route.settings.app;
};
handler.defaults = {
@@ -1094,25 +611,18 @@ describe('handler', function () {
}
};
- var server = new Hapi.Server();
- server.connection();
- server.handler('test', handler);
+ const server = Hapi.server();
+ server.decorate('handler', 'test', handler);
server.route({ method: 'get', path: '/', handler: { test: 'value' } });
- server.inject('/', function (res) {
-
- expect(res.result).to.deep.equal({ x: 1 });
- done();
- });
+ const res = await server.inject('/');
+ expect(res.result).to.equal({ x: 1 });
});
- it('returns handler with function defaults', function (done) {
-
- var handler = function (route, options) {
+ it('returns handler with function defaults', async () => {
- return function (request, reply) {
+ const handler = function (route, options) {
- return reply(request.route.settings.app);
- };
+ return (request) => request.route.settings.app;
};
handler.defaults = function (method) {
@@ -1124,63 +634,27 @@ describe('handler', function () {
};
};
- var server = new Hapi.Server();
- server.connection();
- server.handler('test', handler);
+ const server = Hapi.server();
+ server.decorate('handler', 'test', handler);
server.route({ method: 'get', path: '/', handler: { test: 'value' } });
- server.inject('/', function (res) {
-
- expect(res.result).to.deep.equal({ x: 'get' });
- done();
- });
+ const res = await server.inject('/');
+ expect(res.result).to.equal({ x: 'get' });
});
- it('throws on handler with invalid defaults', function (done) {
-
- var handler = function (route, options) {
+ it('throws on handler with invalid defaults', () => {
- return function (request, reply) {
+ const handler = function (route, options) {
- return reply(request.route.settings.app);
- };
+ return (request) => request.route.settings.app;
};
handler.defaults = 'invalid';
- var server = new Hapi.Server();
- server.connection();
- expect(function () {
+ const server = Hapi.server();
+ expect(() => {
- server.handler('test', handler);
+ server.decorate('handler', 'test', handler);
}).to.throw('Handler defaults property must be an object or function');
-
- done();
- });
- });
-
- describe('invoke()', function () {
-
- it('returns 500 on ext method exception (same tick)', function (done) {
-
- var server = new Hapi.Server({ debug: false });
- server.connection();
- server.ext('onRequest', function (request, next) {
-
- var x = a.b.c;
- });
-
- var handler = function (request, reply) {
-
- return reply('neven gonna happen');
- };
-
- server.route({ method: 'GET', path: '/domain', handler: handler });
-
- server.inject('/domain', function (res) {
-
- expect(res.statusCode).to.equal(500);
- done();
- });
});
});
});
diff --git a/test/headers.js b/test/headers.js
new file mode 100755
index 000000000..b5ed11004
--- /dev/null
+++ b/test/headers.js
@@ -0,0 +1,555 @@
+'use strict';
+
+const Boom = require('@hapi/boom');
+const { Engine: CatboxMemory } = require('@hapi/catbox-memory');
+const Code = require('@hapi/code');
+const Hapi = require('..');
+const Inert = require('@hapi/inert');
+const Lab = require('@hapi/lab');
+
+
+const internals = {};
+
+
+const { describe, it } = exports.lab = Lab.script();
+const expect = Code.expect;
+
+
+describe('Headers', () => {
+
+ describe('cache()', () => {
+
+ it('sets max-age value (method and route)', async () => {
+
+ const server = Hapi.server();
+
+ const method = function (id) {
+
+ return {
+ 'id': 'fa0dbda9b1b',
+ 'name': 'John Doe'
+ };
+ };
+
+ server.method('profile', method, { cache: { expiresIn: 120000, generateTimeout: 10 } });
+
+ const profileHandler = (request) => {
+
+ return server.methods.profile(0);
+ };
+
+ server.route({ method: 'GET', path: '/profile', options: { handler: profileHandler, cache: { expiresIn: 120000, privacy: 'private' } } });
+ await server.start();
+
+ const res = await server.inject('/profile');
+ expect(res.headers['cache-control']).to.equal('max-age=120, must-revalidate, private');
+ await server.stop();
+ });
+
+ it('sets max-age value (expiresAt)', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', options: { handler: () => null, cache: { expiresAt: '10:00' } } });
+ await server.start();
+
+ const res = await server.inject('/');
+ expect(res.headers['cache-control']).to.match(/^max-age=\d+, must-revalidate$/);
+ await server.stop();
+ });
+
+ it('returns no-cache on error', async () => {
+
+ const handler = () => {
+
+ throw Boom.badRequest();
+ };
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', options: { handler, cache: { expiresIn: 120000 } } });
+ const res = await server.inject('/');
+ expect(res.headers['cache-control']).to.equal('no-cache');
+ });
+
+ it('returns custom value on error', async () => {
+
+ const handler = () => {
+
+ throw Boom.badRequest();
+ };
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', options: { handler, cache: { otherwise: 'no-store' } } });
+ const res = await server.inject('/');
+ expect(res.headers['cache-control']).to.equal('no-store');
+ });
+
+ it('sets cache-control on error with status override', async () => {
+
+ const handler = () => {
+
+ throw Boom.badRequest();
+ };
+
+ const server = Hapi.server({ routes: { cache: { statuses: [200, 400] } } });
+ server.route({ method: 'GET', path: '/', options: { handler, cache: { expiresIn: 120000 } } });
+ const res = await server.inject('/');
+ expect(res.headers['cache-control']).to.equal('max-age=120, must-revalidate');
+ });
+
+ it('does not return max-age value when route is not cached', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/item2', options: { handler: () => ({ 'id': '55cf687663', 'name': 'Active Items' }) } });
+ const res = await server.inject('/item2');
+ expect(res.headers['cache-control']).to.not.equal('max-age=120, must-revalidate');
+ });
+
+ it('caches using non default cache', async () => {
+
+ const server = Hapi.server({ cache: { name: 'primary', provider: CatboxMemory } });
+ const defaults = server.cache({ segment: 'a', expiresIn: 2000, getDecoratedValue: true });
+ const primary = server.cache({ segment: 'a', expiresIn: 2000, getDecoratedValue: true, cache: 'primary' });
+
+ await server.start();
+
+ await defaults.set('b', 1);
+ await primary.set('b', 2);
+ const { value: value1 } = await defaults.get('b');
+ expect(value1).to.equal(1);
+
+ const { cached: cached2 } = await primary.get('b');
+ expect(cached2.item).to.equal(2);
+
+ await server.stop();
+ });
+
+ it('leaves existing cache-control header', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler: (request, h) => h.response('text').code(400).header('cache-control', 'some value') });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(400);
+ expect(res.headers['cache-control']).to.equal('some value');
+ });
+
+ it('sets cache-control header from ttl without policy', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler: (request, h) => h.response('text').ttl(10000) });
+
+ const res = await server.inject('/');
+ expect(res.headers['cache-control']).to.equal('max-age=10, must-revalidate');
+ });
+
+ it('sets cache-control header from ttl with disabled policy', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', options: { cache: false, handler: (request, h) => h.response('text').ttl(10000) } });
+
+ const res = await server.inject('/');
+ expect(res.headers['cache-control']).to.equal('max-age=10, must-revalidate');
+ });
+
+ it('leaves existing cache-control header (ttl)', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler: (request, h) => h.response('text').ttl(1000).header('cache-control', 'none') });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers['cache-control']).to.equal('none');
+ });
+
+ it('includes caching header with 304', async () => {
+
+ const server = Hapi.server();
+ await server.register(Inert);
+ server.route({ method: 'GET', path: '/file', handler: { file: __dirname + '/../package.json' }, options: { cache: { expiresIn: 60000 } } });
+
+ const res1 = await server.inject('/file');
+ const res2 = await server.inject({ url: '/file', headers: { 'if-modified-since': res1.headers['last-modified'] } });
+ expect(res2.statusCode).to.equal(304);
+ expect(res2.headers['cache-control']).to.equal('max-age=60, must-revalidate');
+ });
+
+ it('forbids caching on 304 if 200 is not included', async () => {
+
+ const server = Hapi.server({ routes: { cache: { statuses: [400] } } });
+ await server.register(Inert);
+ server.route({ method: 'GET', path: '/file', handler: { file: __dirname + '/../package.json' }, options: { cache: { expiresIn: 60000 } } });
+
+ const res1 = await server.inject('/file');
+ const res2 = await server.inject({ url: '/file', headers: { 'if-modified-since': res1.headers['last-modified'] } });
+ expect(res2.statusCode).to.equal(304);
+ expect(res2.headers['cache-control']).to.equal('no-cache');
+ });
+ });
+
+ describe('security()', () => {
+
+ it('does not set security headers by default', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['strict-transport-security']).to.not.exist();
+ expect(res.headers['x-frame-options']).to.not.exist();
+ expect(res.headers['x-xss-protection']).to.not.exist();
+ expect(res.headers['x-download-options']).to.not.exist();
+ expect(res.headers['x-content-type-options']).to.not.exist();
+ });
+
+ it('returns default security headers when security is true', async () => {
+
+ const server = Hapi.server({ routes: { security: true } });
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['strict-transport-security']).to.equal('max-age=15768000');
+ expect(res.headers['x-frame-options']).to.equal('DENY');
+ expect(res.headers['x-xss-protection']).to.equal('0');
+ expect(res.headers['x-download-options']).to.equal('noopen');
+ expect(res.headers['x-content-type-options']).to.equal('nosniff');
+ });
+
+ it('does not set default security headers when the route sets security false', async () => {
+
+ const server = Hapi.server({ routes: { security: true } });
+ server.route({ method: 'GET', path: '/', handler: () => 'Test', options: { security: false } });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['strict-transport-security']).to.not.exist();
+ expect(res.headers['x-frame-options']).to.not.exist();
+ expect(res.headers['x-xss-protection']).to.not.exist();
+ expect(res.headers['x-download-options']).to.not.exist();
+ expect(res.headers['x-content-type-options']).to.not.exist();
+ });
+
+ it('does not return hsts header when secuirty.hsts is false', async () => {
+
+ const server = Hapi.server({ routes: { security: { hsts: false } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['strict-transport-security']).to.not.exist();
+ expect(res.headers['x-frame-options']).to.equal('DENY');
+ expect(res.headers['x-xss-protection']).to.equal('0');
+ expect(res.headers['x-download-options']).to.equal('noopen');
+ expect(res.headers['x-content-type-options']).to.equal('nosniff');
+ });
+
+ it('returns only default hsts header when security.hsts is true', async () => {
+
+ const server = Hapi.server({ routes: { security: { hsts: true } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['strict-transport-security']).to.equal('max-age=15768000');
+ });
+
+ it('returns correct hsts header when security.hsts is a number', async () => {
+
+ const server = Hapi.server({ routes: { security: { hsts: 123456789 } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['strict-transport-security']).to.equal('max-age=123456789');
+ });
+
+ it('returns correct hsts header when security.hsts is an object', async () => {
+
+ const server = Hapi.server({ routes: { security: { hsts: { maxAge: 123456789, includeSubDomains: true } } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['strict-transport-security']).to.equal('max-age=123456789; includeSubDomains');
+ });
+
+ it('returns the correct hsts header when security.hsts is an object only sepcifying maxAge', async () => {
+
+ const server = Hapi.server({ routes: { security: { hsts: { maxAge: 123456789 } } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['strict-transport-security']).to.equal('max-age=123456789');
+ });
+
+ it('returns correct hsts header when security.hsts is an object only specifying includeSubdomains', async () => {
+
+ const server = Hapi.server({ routes: { security: { hsts: { includeSubdomains: true } } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['strict-transport-security']).to.equal('max-age=15768000; includeSubDomains');
+ });
+
+ it('returns correct hsts header when security.hsts is an object only specifying includeSubDomains', async () => {
+
+ const server = Hapi.server({ routes: { security: { hsts: { includeSubDomains: true } } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['strict-transport-security']).to.equal('max-age=15768000; includeSubDomains');
+ });
+
+ it('returns correct hsts header when security.hsts is an object only specifying includeSubDomains and preload', async () => {
+
+ const server = Hapi.server({ routes: { security: { hsts: { includeSubDomains: true, preload: true } } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['strict-transport-security']).to.equal('max-age=15768000; includeSubDomains; preload');
+ });
+
+ it('does not return the xframe header whe security.xframe is false', async () => {
+
+ const server = Hapi.server({ routes: { security: { xframe: false } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['x-frame-options']).to.not.exist();
+ expect(res.headers['strict-transport-security']).to.equal('max-age=15768000');
+ expect(res.headers['x-xss-protection']).to.equal('0');
+ expect(res.headers['x-download-options']).to.equal('noopen');
+ expect(res.headers['x-content-type-options']).to.equal('nosniff');
+ });
+
+ it('returns only default xframe header when security.xframe is true', async () => {
+
+ const server = Hapi.server({ routes: { security: { xframe: true } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['x-frame-options']).to.equal('DENY');
+ });
+
+ it('returns correct xframe header when security.xframe is a string', async () => {
+
+ const server = Hapi.server({ routes: { security: { xframe: 'sameorigin' } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['x-frame-options']).to.equal('SAMEORIGIN');
+ });
+
+ it('returns correct xframe header when security.xframe is an object', async () => {
+
+ const server = Hapi.server({ routes: { security: { xframe: { rule: 'allow-from', source: 'http://example.com' } } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['x-frame-options']).to.equal('ALLOW-FROM http://example.com');
+ });
+
+ it('returns correct xframe header when security.xframe is an object', async () => {
+
+ const server = Hapi.server({ routes: { security: { xframe: { rule: 'deny' } } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['x-frame-options']).to.equal('DENY');
+ });
+
+ it('returns sameorigin xframe header when rule is allow-from but source is unspecified', async () => {
+
+ const server = Hapi.server({ routes: { security: { xframe: { rule: 'allow-from' } } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['x-frame-options']).to.equal('SAMEORIGIN');
+ });
+
+ it('does not set x-download-options if noOpen is false', async () => {
+
+ const server = Hapi.server({ routes: { security: { noOpen: false } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['x-download-options']).to.not.exist();
+ });
+
+ it('does not set x-content-type-options if noSniff is false', async () => {
+
+ const server = Hapi.server({ routes: { security: { noSniff: false } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['x-content-type-options']).to.not.exist();
+ });
+
+ it('sets the x-xss-protection header when security.xss is enabled', async () => {
+
+ const server = Hapi.server({ routes: { security: { xss: 'enabled' } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['x-xss-protection']).to.equal('1; mode=block');
+ expect(res.headers['strict-transport-security']).to.equal('max-age=15768000');
+ expect(res.headers['x-frame-options']).to.equal('DENY');
+ expect(res.headers['x-download-options']).to.equal('noopen');
+ expect(res.headers['x-content-type-options']).to.equal('nosniff');
+ });
+
+ it('sets the x-xss-protection header when security.xss is disabled', async () => {
+
+ const server = Hapi.server({ routes: { security: { xss: 'disabled' } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['x-xss-protection']).to.equal('0');
+ expect(res.headers['strict-transport-security']).to.equal('max-age=15768000');
+ expect(res.headers['x-frame-options']).to.equal('DENY');
+ expect(res.headers['x-download-options']).to.equal('noopen');
+ expect(res.headers['x-content-type-options']).to.equal('nosniff');
+ });
+
+ it('does not set the x-xss-protection header when security.xss is false', async () => {
+
+ const server = Hapi.server({ routes: { security: { xss: false } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['x-xss-protection']).to.not.exist();
+ expect(res.headers['strict-transport-security']).to.equal('max-age=15768000');
+ expect(res.headers['x-frame-options']).to.equal('DENY');
+ expect(res.headers['x-download-options']).to.equal('noopen');
+ expect(res.headers['x-content-type-options']).to.equal('nosniff');
+ });
+
+ it('does not return the referrer-policy header by default', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['referrer-policy']).to.not.exist();
+ });
+
+ it('does not return the referrer-policy header when security.referrer is false', async () => {
+
+ const server = Hapi.server({ routes: { security: { referrer: false } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['referrer-policy']).to.not.exist();
+ });
+
+ it('does not allow security.referrer to be true', () => {
+
+ let err;
+ try {
+ Hapi.server({ routes: { security: { referrer: true } } });
+ }
+ catch (ex) {
+ err = ex;
+ }
+
+ expect(err).to.exist();
+ });
+
+ it('returns correct referrer-policy header when security.referrer is a string with a valid value', async () => {
+
+ const server = Hapi.server({ routes: { security: { referrer: 'strict-origin-when-cross-origin' } } });
+ server.route({ method: 'GET', path: '/', handler: () => 'Test' });
+
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('Test');
+ expect(res.headers['referrer-policy']).to.equal('strict-origin-when-cross-origin');
+ });
+ });
+
+ describe('content()', () => {
+
+ it('does not modify content-type header when charset manually set', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler: (request, h) => h.response('text').type('text/plain; charset=ISO-8859-1') });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers['content-type']).to.equal('text/plain; charset=ISO-8859-1');
+ });
+
+ it('does not modify content-type header when charset is unset', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler: (request, h) => h.response('text').type('text/plain').charset() });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers['content-type']).to.equal('text/plain');
+ });
+
+ it('does not modify content-type header when charset is unset (default type)', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler: (request, h) => h.response('text').charset() });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers['content-type']).to.equal('text/html');
+ });
+
+ it('does not set content-type by default on 204 response', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler: (request, h) => h.response().code(204) });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(204);
+ expect(res.headers['content-type']).to.equal(undefined);
+ });
+ });
+});
diff --git a/test/index.js b/test/index.js
new file mode 100755
index 000000000..c416a85f9
--- /dev/null
+++ b/test/index.js
@@ -0,0 +1,25 @@
+'use strict';
+
+const Code = require('@hapi/code');
+const Hapi = require('..');
+const Lab = require('@hapi/lab');
+
+
+const internals = {};
+
+
+const { describe, it } = exports.lab = Lab.script();
+const expect = Code.expect;
+
+
+describe('Server', () => {
+
+ it('supports new Server()', async () => {
+
+ const server = new Hapi.Server();
+ server.route({ method: 'GET', path: '/', handler: () => 'old school' });
+
+ const res = await server.inject('/');
+ expect(res.result).to.equal('old school');
+ });
+});
diff --git a/test/methods.js b/test/methods.js
index 435ae48a0..70a3dcb38 100755
--- a/test/methods.js
+++ b/test/methods.js
@@ -1,1181 +1,795 @@
-// Load modules
+'use strict';
-var Bluebird = require('bluebird');
-var CatboxMemory = require('catbox-memory');
-var Code = require('code');
-var Hapi = require('..');
-var Lab = require('lab');
+const Catbox = require('@hapi/catbox');
+const { Engine: CatboxMemory } = require('@hapi/catbox-memory');
+const Code = require('@hapi/code');
+const Hapi = require('..');
+const Hoek = require('@hapi/hoek');
+const Lab = require('@hapi/lab');
-// Declare internals
+const internals = {};
-var internals = {};
+const { describe, it } = exports.lab = Lab.script();
+const expect = Code.expect;
-// Test shortcuts
-var lab = exports.lab = Lab.script();
-var describe = lab.describe;
-var it = lab.it;
-var expect = Code.expect;
+describe('Methods', () => {
+ it('registers a method', () => {
-describe('Methods', function () {
+ const add = function (a, b) {
- it('registers a method', function (done) {
-
- var add = function (a, b, next) {
-
- return next(null, a + b);
+ return a + b;
};
- var server = new Hapi.Server();
+ const server = Hapi.server();
server.method('add', add);
- server.methods.add(1, 5, function (err, result) {
-
- expect(result).to.equal(6);
- done();
- });
+ const result = server.methods.add(1, 5);
+ expect(result).to.equal(6);
});
- it('registers a method with leading _', function (done) {
+ it('registers a method (object)', () => {
- var _add = function (a, b, next) {
+ const add = function (a, b) {
- return next(null, a + b);
+ return a + b;
};
- var server = new Hapi.Server();
- server.method('_add', _add);
-
- server.methods._add(1, 5, function (err, result) {
+ const server = Hapi.server();
+ server.method({ name: 'add', method: add });
- expect(result).to.equal(6);
- done();
- });
+ const result = server.methods.add(1, 5);
+ expect(result).to.equal(6);
});
- it('registers a method with leading $', function (done) {
+ it('registers a method with leading _', () => {
- var $add = function (a, b, next) {
+ const _add = function (a, b) {
- return next(null, a + b);
+ return a + b;
};
- var server = new Hapi.Server();
- server.method('$add', $add);
-
- server.methods.$add(1, 5, function (err, result) {
+ const server = Hapi.server();
+ server.method('_add', _add);
- expect(result).to.equal(6);
- done();
- });
+ const result = server.methods._add(1, 5);
+ expect(result).to.equal(6);
});
- it('registers a method with _', function (done) {
+ it('registers a method with leading $', () => {
- var _add = function (a, b, next) {
+ const $add = function (a, b) {
- return next(null, a + b);
+ return a + b;
};
- var server = new Hapi.Server();
- server.method('add_._that', _add);
-
- server.methods.add_._that(1, 5, function (err, result) {
+ const server = Hapi.server();
+ server.method('$add', $add);
- expect(result).to.equal(6);
- done();
- });
+ const result = server.methods.$add(1, 5);
+ expect(result).to.equal(6);
});
- it('registers a method with $', function (done) {
+ it('registers a method with _', () => {
- var $add = function (a, b, next) {
+ const _add = function (a, b) {
- return next(null, a + b);
+ return a + b;
};
- var server = new Hapi.Server();
- server.method('add$.$that', $add);
-
- server.methods.add$.$that(1, 5, function (err, result) {
+ const server = Hapi.server();
+ server.method('add_._that', _add);
- expect(result).to.equal(6);
- done();
- });
+ const result = server.methods.add_._that(1, 5);
+ expect(result).to.equal(6);
});
- it('registers a method (no callback)', function (done) {
+ it('registers a method with $', () => {
- var add = function (a, b) {
+ const $add = function (a, b) {
return a + b;
};
- var server = new Hapi.Server();
- server.method('add', add, { callback: false });
+ const server = Hapi.server();
+ server.method('add$.$that', $add);
- expect(server.methods.add(1, 5)).to.equal(6);
- done();
+ const result = server.methods.add$.$that(1, 5);
+ expect(result).to.equal(6);
});
- it('registers a method (promise)', function (done) {
+ it('registers a method (promise)', async () => {
- var addAsync = function (a, b, next) {
+ const add = function (a, b) {
- return next(null, a + b);
+ return new Promise((resolve) => resolve(a + b));
};
- var add = Bluebird.promisify(addAsync);
-
- var server = new Hapi.Server();
- server.method('add', add, { callback: false });
-
- server.methods.add(1, 5).then(function (result) {
+ const server = Hapi.server();
+ server.method('add', add);
- expect(result).to.equal(6);
- done();
- });
+ const value = await server.methods.add(1, 5);
+ expect(value).to.equal(6);
});
- it('registers a method with nested name', function (done) {
+ it('registers a method with nested name', () => {
- var add = function (a, b, next) {
+ const add = function (a, b) {
- return next(null, a + b);
+ return a + b;
};
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.method('tools.add', add);
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
-
- server.methods.tools.add(1, 5, function (err, result) {
-
- expect(result).to.equal(6);
- done();
- });
- });
- });
-
- it('registers a method with bind and callback', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- var context = { name: 'Bob' };
- server.method('user', function (id, next) {
-
- return next(null, { id: id, name: this.name });
- }, { bind: context });
-
- server.route({
- method: 'GET',
- path: '/user/{id}',
- config: {
- pre: [
- 'user(params.id)'
- ],
- handler: function (request, reply) {
-
- return reply(request.pre.user);
- }
- }
- });
-
- server.inject('/user/5', function (res) {
-
- expect(res.result).to.deep.equal({ id: '5', name: 'Bob' });
- done();
- });
+ const result = server.methods.tools.add(1, 5);
+ expect(result).to.equal(6);
});
- it('registers two methods with shared nested name', function (done) {
+ it('registers two methods with shared nested name', () => {
- var add = function (a, b, next) {
+ const add = function (a, b) {
- return next(null, a + b);
+ return a + b;
};
- var sub = function (a, b, next) {
+ const sub = function (a, b) {
- return next(null, a - b);
+ return a - b;
};
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.method('tools.add', add);
server.method('tools.sub', sub);
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
-
- server.methods.tools.add(1, 5, function (err, result1) {
-
- expect(result1).to.equal(6);
- server.methods.tools.sub(1, 5, function (err, result2) {
-
- expect(result2).to.equal(-4);
- done();
- });
- });
- });
+ const result1 = server.methods.tools.add(1, 5);
+ expect(result1).to.equal(6);
+ const result2 = server.methods.tools.sub(1, 5);
+ expect(result2).to.equal(-4);
});
- it('throws when registering a method with nested name twice', function (done) {
+ it('throws when registering a method with nested name twice', () => {
- var add = function (a, b, next) {
+ const server = Hapi.server();
+ server.method('tools.add', Hoek.ignore);
+ expect(() => {
- return next(null, a + b);
- };
+ server.method('tools.add', Hoek.ignore);
+ }).to.throw('Server method function name already exists: tools.add');
+ });
- var server = new Hapi.Server();
- server.method('tools.add', add);
- expect(function () {
+ it('throws when registering a method with name nested through a function', () => {
- server.method('tools.add', add);
- }).to.throw('Server method function name already exists: tools.add');
+ const server = Hapi.server();
+ server.method('add', Hoek.ignore);
+ expect(() => {
- done();
+ server.method('add.another', Hoek.ignore);
+ }).to.throw('Invalid segment another in reach path add.another');
});
- it('throws when registering a method with name nested through a function', function (done) {
+ it('calls non cached method multiple times', () => {
- var add = function (a, b, next) {
+ let gen = 0;
+ const method = function (id) {
- return next(null, a + b);
+ return { id, gen: gen++ };
};
- var server = new Hapi.Server();
- server.method('add', add);
- expect(function () {
+ const server = Hapi.server();
+ server.method('test', method);
- server.method('add.another', add);
- }).to.throw('Invalid segment another in reach path add.another');
+ const result1 = server.methods.test(1);
+ expect(result1.gen).to.equal(0);
- done();
+ const result2 = server.methods.test(1);
+ expect(result2.gen).to.equal(1);
});
- it('calls non cached method multiple times', function (done) {
+ it('caches method value', async () => {
- var gen = 0;
- var method = function (id, next) {
+ let gen = 0;
+ const method = function (id) {
- return next(null, { id: id, gen: gen++ });
+ return { id, gen: gen++ };
};
- var server = new Hapi.Server();
- server.connection();
- server.method('test', method);
-
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
-
- server.methods.test(1, function (err, result1) {
+ const server = Hapi.server();
+ server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 } });
- expect(result1.gen).to.equal(0);
+ await server.initialize();
- server.methods.test(1, function (err, result2) {
+ const result1 = await server.methods.test(1);
+ expect(result1.gen).to.equal(0);
- expect(result2.gen).to.equal(1);
- done();
- });
- });
- });
+ const result2 = await server.methods.test(1);
+ expect(result2.gen).to.equal(0);
});
- it('caches method value', function (done) {
+ it('emits a cache policy event on cached methods with default cache provision', async () => {
- var gen = 0;
- var method = function (id, next) {
+ const method = function (id) {
- return next(null, { id: id, gen: gen++ });
+ return { id };
};
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
+ const cachePolicyEvent = server.events.once('cachePolicy');
+
server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 } });
- server.initialize(function (err) {
+ const [policy, cacheName, segment] = await cachePolicyEvent;
+ expect(policy).to.be.instanceOf(Catbox.Policy);
+ expect(cacheName).to.equal(undefined);
+ expect(segment).to.equal('#test');
+ });
+
+ it('emits a cache policy event on cached methods with named cache provision', async () => {
- expect(err).to.not.exist();
+ const method = function (id) {
- server.methods.test(1, function (err, result1) {
+ return { id };
+ };
- expect(err).to.not.exist();
- expect(result1.gen).to.equal(0);
+ const server = Hapi.server();
+ await server.cache.provision({ provider: CatboxMemory, name: 'named' });
+ const cachePolicyEvent = server.events.once('cachePolicy');
- server.methods.test(1, function (err, result2) {
+ server.method('test', method, { cache: { cache: 'named', expiresIn: 1000, generateTimeout: 10 } });
- expect(err).to.not.exist();
- expect(result2.gen).to.equal(0);
- done();
- });
- });
- });
+ const [policy, cacheName, segment] = await cachePolicyEvent;
+ expect(policy).to.be.instanceOf(Catbox.Policy);
+ expect(cacheName).to.equal('named');
+ expect(segment).to.equal('#test');
});
- it('caches method value (no callback)', function (done) {
+ it('caches method value (async)', async () => {
- var gen = 0;
- var method = function (id) {
+ let gen = 0;
+ const method = async function (id) {
- return { id: id, gen: gen++ };
+ await Hoek.wait(1);
+ return { id, gen: gen++ };
};
- var server = new Hapi.Server();
- server.connection();
- server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 }, callback: false });
+ const server = Hapi.server();
+ server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 } });
- server.initialize(function (err) {
+ await server.initialize();
- expect(err).to.not.exist();
+ const result1 = await server.methods.test(1);
+ expect(result1.gen).to.equal(0);
- server.methods.test(1, function (err, result1) {
+ const result2 = await server.methods.test(1);
+ expect(result2.gen).to.equal(0);
+ });
- expect(err).to.not.exist();
- expect(result1.gen).to.equal(0);
+ it('caches method value (promise)', async () => {
- server.methods.test(1, function (err, result2) {
+ let gen = 0;
+ const method = function (id) {
- expect(err).to.not.exist();
- expect(result2.gen).to.equal(0);
- done();
- });
- });
- });
- });
+ return new Promise((resolve, reject) => {
- it('caches method value (promise)', function (done) {
+ if (id === 2) {
+ return reject(new Error('boom'));
+ }
- var gen = 0;
- var methodAsync = function (id, next) {
+ return resolve({ id, gen: gen++ });
+ });
+ };
- if (id === 2) {
- return next(new Error('boom'));
- }
+ const server = Hapi.server();
+ server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 } });
- return next(null, { id: id, gen: gen++ });
- };
+ await server.initialize();
- var method = Bluebird.promisify(methodAsync);
+ const result1 = await server.methods.test(1);
+ expect(result1.gen).to.equal(0);
- var server = new Hapi.Server();
- server.connection();
- server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 }, callback: false });
+ const result2 = await server.methods.test(1);
+ expect(result2.gen).to.equal(0);
- server.initialize(function (err) {
+ await expect(server.methods.test(2)).to.reject('boom');
+ });
- expect(err).to.not.exist();
+ it('caches method value (decorated)', async () => {
- server.methods.test(1, function (err, result1) {
+ let gen = 0;
+ const method = function (id) {
- expect(err).to.not.exist();
- expect(result1.gen).to.equal(0);
+ return { id, gen: gen++ };
+ };
- server.methods.test(1, function (err, result2) {
+ const server = Hapi.server();
+ server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10, getDecoratedValue: true } });
- expect(err).to.not.exist();
- expect(result2.gen).to.equal(0);
+ await server.initialize();
- server.methods.test(2, function (err, result3) {
+ const { value: result1 } = await server.methods.test(1);
+ expect(result1.gen).to.equal(0);
- expect(err).to.exist();
- expect(err.message).to.equal('boom');
- done();
- });
- });
- });
- });
+ const { value: result2 } = await server.methods.test(1);
+ expect(result2.gen).to.equal(0);
});
- it('reuses cached method value with custom key function', function (done) {
+ it('reuses cached method value with custom key function', async () => {
- var gen = 0;
- var method = function (id, next) {
+ let gen = 0;
+ const method = function (id) {
- return next(null, { id: id, gen: gen++ });
+ return { id, gen: gen++ };
};
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
- var generateKey = function (id) {
+ const generateKey = function (id) {
return '' + (id + 1);
};
- server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 }, generateKey: generateKey });
-
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
+ server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 }, generateKey });
- server.methods.test(1, function (err, result1) {
+ await server.initialize();
- expect(result1.gen).to.equal(0);
+ const result1 = await server.methods.test(1);
+ expect(result1.gen).to.equal(0);
- server.methods.test(1, function (err, result2) {
-
- expect(result2.gen).to.equal(0);
- done();
- });
- });
- });
+ const result2 = await server.methods.test(1);
+ expect(result2.gen).to.equal(0);
});
- it('errors when custom key function return null', function (done) {
+ it('errors when custom key function return null', async () => {
- var method = function (id, next) {
+ const method = function (id) {
- return next(null, { id: id });
+ return { id };
};
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
- var generateKey = function (id) {
+ const generateKey = function (id) {
return null;
};
- server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 }, generateKey: generateKey });
+ server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 }, generateKey });
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
-
- server.methods.test(1, function (err, result) {
-
- expect(err).to.exist();
- expect(err.message).to.equal('Invalid method key when invoking: test');
- done();
- });
- });
+ await server.initialize();
+ await expect(server.methods.test(1)).to.reject('Invalid method key when invoking: test');
});
- it('does not cache when custom key function returns a non-string', function (done) {
+ it('does not cache when custom key function returns a non-string', async () => {
- var method = function (id, next) {
+ const method = function (id) {
- return next(null, { id: id });
+ return { id };
};
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
- var generateKey = function (id) {
+ const generateKey = function (id) {
return 123;
};
- server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 }, generateKey: generateKey });
+ server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 }, generateKey });
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
-
- server.methods.test(1, function (err, result) {
-
- expect(err).to.exist();
- expect(err.message).to.equal('Invalid method key when invoking: test');
- done();
- });
- });
+ await server.initialize();
+ await expect(server.methods.test(1)).to.reject('Invalid method key when invoking: test');
});
- it('does not cache value when ttl is 0', function (done) {
+ it('does not cache value when ttl is 0', async () => {
- var gen = 0;
- var method = function (id, next) {
+ let gen = 0;
+ const method = function (id, flags) {
- return next(null, { id: id, gen: gen++ }, 0);
+ flags.ttl = 0;
+ return { id, gen: gen++ };
};
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 } });
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
-
- server.methods.test(1, function (err, result1) {
+ await server.initialize();
- expect(result1.gen).to.equal(0);
+ const result1 = await server.methods.test(1);
+ expect(result1.gen).to.equal(0);
- server.methods.test(1, function (err, result2) {
-
- expect(result2.gen).to.equal(1);
- done();
- });
- });
- });
+ const result2 = await server.methods.test(1);
+ expect(result2.gen).to.equal(1);
});
- it('generates new value after cache drop', function (done) {
+ it('generates new value after cache drop', async () => {
- var gen = 0;
- var method = function (id, next) {
+ let gen = 0;
+ const method = function (id) {
- return next(null, { id: id, gen: gen++ });
+ return { id, gen: gen++ };
};
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.method('dropTest', method, { cache: { expiresIn: 1000, generateTimeout: 10 } });
- server.initialize(function (err) {
+ await server.initialize();
- expect(err).to.not.exist();
+ const result1 = await server.methods.dropTest(2);
+ expect(result1.gen).to.equal(0);
+ await server.methods.dropTest.cache.drop(2);
+ const result2 = await server.methods.dropTest(2);
+ expect(result2.gen).to.equal(1);
+ });
- server.methods.dropTest(2, function (err, result1) {
+ it('errors on invalid drop key', async () => {
- expect(result1.gen).to.equal(0);
- server.methods.dropTest.cache.drop(2, function (err) {
+ let gen = 0;
+ const method = function (id) {
- expect(err).to.not.exist();
+ return { id, gen: gen++ };
+ };
- server.methods.dropTest(2, function (err, result2) {
+ const server = Hapi.server();
+ server.method('dropErrTest', method, { cache: { expiresIn: 1000, generateTimeout: 10 } });
- expect(result2.gen).to.equal(1);
- done();
- });
- });
- });
- });
+ await server.initialize();
+
+ const invalid = () => { };
+ await expect(server.methods.dropErrTest.cache.drop(invalid)).to.reject();
});
- it('errors on invalid drop key', function (done) {
+ it('reports cache stats for each method', async () => {
- var gen = 0;
- var method = function (id, next) {
+ const method = function (id) {
- return next(null, { id: id, gen: gen++ });
+ return { id };
};
- var server = new Hapi.Server();
- server.connection();
- server.method('dropErrTest', method, { cache: { expiresIn: 1000, generateTimeout: 10 } });
-
- server.initialize(function (err) {
+ const server = Hapi.server();
+ server.method('test', method, { cache: { generateTimeout: 10 } });
+ server.method('test2', method, { cache: { generateTimeout: 10 } });
- expect(err).to.not.exist();
+ await server.initialize();
- server.methods.dropErrTest.cache.drop(function () { }, function (err) {
-
- expect(err).to.exist();
- done();
- });
- });
+ server.methods.test(1);
+ expect(server.methods.test.cache.stats.gets).to.equal(1);
+ expect(server.methods.test2.cache.stats.gets).to.equal(0);
});
- it('throws an error when name is not a string', function (done) {
+ it('throws an error when name is not a string', () => {
- expect(function () {
+ expect(() => {
- var server = new Hapi.Server();
- server.method(0, function () { });
+ const server = Hapi.server();
+ server.method(0, () => { });
}).to.throw('name must be a string');
- done();
});
- it('throws an error when name is invalid', function (done) {
+ it('throws an error when name is invalid', () => {
- expect(function () {
+ expect(() => {
- var server = new Hapi.Server();
- server.method('0', function () { });
+ const server = Hapi.server();
+ server.method('0', () => { });
}).to.throw('Invalid name: 0');
- expect(function () {
+ expect(() => {
- var server = new Hapi.Server();
- server.method('a..', function () { });
+ const server = Hapi.server();
+ server.method('a..', () => { });
}).to.throw('Invalid name: a..');
- expect(function () {
+ expect(() => {
- var server = new Hapi.Server();
- server.method('a.0', function () { });
+ const server = Hapi.server();
+ server.method('a.0', () => { });
}).to.throw('Invalid name: a.0');
- expect(function () {
+ expect(() => {
- var server = new Hapi.Server();
- server.method('.a', function () { });
+ const server = Hapi.server();
+ server.method('.a', () => { });
}).to.throw('Invalid name: .a');
-
- done();
});
- it('throws an error when method is not a function', function (done) {
+ it('throws an error when method is not a function', () => {
- expect(function () {
+ expect(() => {
- var server = new Hapi.Server();
+ const server = Hapi.server();
server.method('user', 'function');
}).to.throw('method must be a function');
- done();
});
- it('throws an error when options is not an object', function (done) {
+ it('throws an error when options is not an object', () => {
- expect(function () {
+ expect(() => {
- var server = new Hapi.Server();
- server.method('user', function () { }, 'options');
+ const server = Hapi.server();
+ server.method('user', () => { }, 'options');
}).to.throw(/Invalid method options \(user\)/);
- done();
});
- it('throws an error when options.generateKey is not a function', function (done) {
+ it('throws an error when options.generateKey is not a function', () => {
- expect(function () {
+ expect(() => {
- var server = new Hapi.Server();
- server.method('user', function () { }, { generateKey: 'function' });
+ const server = Hapi.server();
+ server.method('user', () => { }, { generateKey: 'function' });
}).to.throw(/Invalid method options \(user\)/);
- done();
});
- it('throws an error when options.cache is not valid', function (done) {
+ it('throws an error when options.cache is not valid', () => {
- expect(function () {
+ expect(() => {
- var server = new Hapi.Server({ cache: CatboxMemory });
- server.method('user', function () { }, { cache: { x: 'y', generateTimeout: 10 } });
+ const server = Hapi.server({ cache: CatboxMemory });
+ server.method('user', () => { }, { cache: { x: 'y', generateTimeout: 10 } });
}).to.throw(/Invalid cache policy configuration/);
- done();
});
- it('throws an error when generateTimeout is not present', function (done) {
+ it('throws an error when generateTimeout is not present', () => {
- var server = new Hapi.Server();
- expect(function () {
+ const server = Hapi.server();
+ expect(() => {
- server.method('test', function () { }, { cache: {} });
+ server.method('test', () => { }, { cache: {} });
}).to.throw('Method caching requires a timeout value in generateTimeout: test');
-
- done();
});
- it('allows generateTimeout to be false', function (done) {
+ it('allows generateTimeout to be false', () => {
- var server = new Hapi.Server();
- expect(function () {
+ const server = Hapi.server();
+ expect(() => {
- server.method('test', function () { }, { cache: { generateTimeout: false } });
+ server.method('test', () => { }, { cache: { generateTimeout: false } });
}).to.not.throw();
-
- done();
});
- it('returns a valid result when calling a method without using the cache', function (done) {
+ it('returns timeout when method taking too long using the cache', async () => {
- var server = new Hapi.Server();
+ const server = Hapi.server({ cache: CatboxMemory });
- var method = function (id, next) {
+ let gen = 0;
+ const method = async function (id) {
- return next(null, { id: id });
+ await Hoek.wait(50);
+ return { id, gen: ++gen };
};
- server.method('user', method);
- server.methods.user(4, function (err, result) {
-
- expect(result.id).to.equal(4);
- done();
- });
- });
-
- it('returns a valid result when calling a method when using the cache', function (done) {
+ server.method('user', method, { cache: { expiresIn: 2000, generateTimeout: 30 } });
- var server = new Hapi.Server();
- server.connection();
- server.initialize(function (err) {
+ await server.initialize();
- expect(err).to.not.exist();
+ const id = Math.random();
+ const err = await expect(server.methods.user(id)).to.reject();
+ expect(err.output.statusCode).to.equal(503);
- var method = function (id, str, next) {
+ await Hoek.wait(30);
- return next(null, { id: id, str: str });
- };
-
- server.method('user', method, { cache: { expiresIn: 1000, generateTimeout: 10 } });
- server.methods.user(4, 'something', function (err, result) {
-
- expect(result.id).to.equal(4);
- expect(result.str).to.equal('something');
- done();
- });
- });
- });
-
- it('returns an error result when calling a method that returns an error', function (done) {
-
- var server = new Hapi.Server();
-
- var method = function (id, next) {
-
- return next(new Error());
- };
-
- server.method('user', method);
- server.methods.user(4, function (err, result) {
-
- expect(err).to.exist();
- done();
- });
- });
-
- it('returns a different result when calling a method without using the cache', function (done) {
-
- var server = new Hapi.Server();
-
- var gen = 0;
- var method = function (id, next) {
-
- return next(null, { id: id, gen: ++gen });
- };
-
- server.method('user', method);
- server.methods.user(4, function (err, result1) {
-
- expect(result1.id).to.equal(4);
- expect(result1.gen).to.equal(1);
- server.methods.user(4, function (err, result2) {
-
- expect(result2.id).to.equal(4);
- expect(result2.gen).to.equal(2);
- done();
- });
- });
+ const result2 = await server.methods.user(id);
+ expect(result2.id).to.equal(id);
+ expect(result2.gen).to.equal(1);
});
- it('returns a valid result when calling a method using the cache', function (done) {
+ it('supports empty key method', async () => {
- var server = new Hapi.Server({ cache: CatboxMemory });
- server.connection();
+ const server = Hapi.server({ cache: CatboxMemory });
- var gen = 0;
- var method = function (id, next) {
+ let gen = 0;
+ const terms = 'I agree to give my house';
+ const method = function () {
- return next(null, { id: id, gen: ++gen });
- };
-
- server.method('user', method, { cache: { expiresIn: 2000, generateTimeout: 10 } });
-
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
-
- var id = Math.random();
- server.methods.user(id, function (err, result1) {
-
- expect(result1.id).to.equal(id);
- expect(result1.gen).to.equal(1);
- server.methods.user(id, function (err, result2) {
-
- expect(result2.id).to.equal(id);
- expect(result2.gen).to.equal(1);
- done();
- });
- });
- });
- });
-
- it('returns timeout when method taking too long using the cache', function (done) {
-
- var server = new Hapi.Server({ cache: CatboxMemory });
- server.connection();
-
- var gen = 0;
- var method = function (id, next) {
-
- setTimeout(function () {
-
- return next(null, { id: id, gen: ++gen });
- }, 5);
- };
-
- server.method('user', method, { cache: { expiresIn: 2000, generateTimeout: 3 } });
-
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
-
- var id = Math.random();
- server.methods.user(id, function (err, result1) {
-
- expect(err.output.statusCode).to.equal(503);
-
- setTimeout(function () {
-
- server.methods.user(id, function (err, result2) {
-
- expect(result2.id).to.equal(id);
- expect(result2.gen).to.equal(1);
- done();
- });
- }, 3);
- });
- });
- });
-
- it('supports empty key method', function (done) {
-
- var server = new Hapi.Server({ cache: CatboxMemory });
- server.connection();
-
- var gen = 0;
- var terms = 'I agree to give my house';
- var method = function (next) {
-
- return next(null, { gen: gen++, terms: terms });
+ return { gen: gen++, terms };
};
server.method('tos', method, { cache: { expiresIn: 2000, generateTimeout: 10 } });
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
+ await server.initialize();
- server.methods.tos(function (err, result1) {
+ const result1 = await server.methods.tos();
+ expect(result1.terms).to.equal(terms);
+ expect(result1.gen).to.equal(0);
- expect(result1.terms).to.equal(terms);
- expect(result1.gen).to.equal(0);
- server.methods.tos(function (err, result2) {
-
- expect(result2.terms).to.equal(terms);
- expect(result2.gen).to.equal(0);
- done();
- });
- });
- });
+ const result2 = await server.methods.tos();
+ expect(result2.terms).to.equal(terms);
+ expect(result2.gen).to.equal(0);
});
- it('returns valid results when calling a method (with different keys) using the cache', function (done) {
+ it('returns valid results when calling a method (with different keys) using the cache', async () => {
- var server = new Hapi.Server({ cache: CatboxMemory });
- server.connection();
- var gen = 0;
- var method = function (id, next) {
+ const server = Hapi.server({ cache: CatboxMemory });
+ let gen = 0;
+ const method = function (id) {
- return next(null, { id: id, gen: ++gen });
+ return { id, gen: ++gen };
};
server.method('user', method, { cache: { expiresIn: 2000, generateTimeout: 10 } });
- server.initialize(function (err) {
+ await server.initialize();
- expect(err).to.not.exist();
+ const id1 = Math.random();
+ const result1 = await server.methods.user(id1);
+ expect(result1.id).to.equal(id1);
+ expect(result1.gen).to.equal(1);
- var id1 = Math.random();
- server.methods.user(id1, function (err, result1) {
-
- expect(result1.id).to.equal(id1);
- expect(result1.gen).to.equal(1);
- var id2 = Math.random();
- server.methods.user(id2, function (err, result2) {
-
- expect(result2.id).to.equal(id2);
- expect(result2.gen).to.equal(2);
- done();
- });
- });
- });
+ const id2 = Math.random();
+ const result2 = await server.methods.user(id2);
+ expect(result2.id).to.equal(id2);
+ expect(result2.gen).to.equal(2);
});
- it('errors when key generation fails', function (done) {
+ it('errors when key generation fails', async () => {
- var server = new Hapi.Server({ cache: CatboxMemory });
- server.connection();
+ const server = Hapi.server({ cache: CatboxMemory });
- var method = function (id, next) {
+ const method = function (id) {
- return next(null, { id: id });
+ return { id };
};
- server.method([{ name: 'user', method: method, options: { cache: { expiresIn: 2000, generateTimeout: 10 } } }]);
-
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
-
- server.methods.user(1, function (err, result1) {
+ server.method([{ name: 'user', method, options: { cache: { expiresIn: 2000, generateTimeout: 10 } } }]);
- expect(result1.id).to.equal(1);
+ await server.initialize();
- server.methods.user(function () { }, function (err, result2) {
+ const result1 = await server.methods.user(1);
+ expect(result1.id).to.equal(1);
- expect(err).to.exist();
- expect(err.message).to.equal('Invalid method key when invoking: user');
- done();
- });
- });
- });
+ const invalid = function () { };
+ await expect(server.methods.user(invalid)).to.reject('Invalid method key when invoking: user');
});
- it('sets method bind without cache', function (done) {
+ it('sets method bind without cache', () => {
- var method = function (id, next) {
+ const method = function (id) {
- return next(null, { id: id, gen: this.gen++ });
+ return { id, gen: this.gen++ };
};
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.method('test', method, { bind: { gen: 7 } });
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
-
- server.methods.test(1, function (err, result1) {
+ const result1 = server.methods.test(1);
+ expect(result1.gen).to.equal(7);
- expect(result1.gen).to.equal(7);
-
- server.methods.test(1, function (err, result2) {
-
- expect(result2.gen).to.equal(8);
- done();
- });
- });
- });
+ const result2 = server.methods.test(1);
+ expect(result2.gen).to.equal(8);
});
- it('sets method bind with cache', function (done) {
+ it('sets method bind with cache', async () => {
- var method = function (id, next) {
+ const method = function (id) {
- return next(null, { id: id, gen: this.gen++ });
+ return { id, gen: this.gen++ };
};
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.method('test', method, { bind: { gen: 7 }, cache: { expiresIn: 1000, generateTimeout: 10 } });
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
-
- server.methods.test(1, function (err, result1) {
+ await server.initialize();
- expect(result1.gen).to.equal(7);
+ const result1 = await server.methods.test(1);
+ expect(result1.gen).to.equal(7);
- server.methods.test(1, function (err, result2) {
-
- expect(result2.gen).to.equal(7);
- done();
- });
- });
- });
+ const result2 = await server.methods.test(1);
+ expect(result2.gen).to.equal(7);
});
- it('shallow copies bind config', function (done) {
+ it('shallow copies bind config', async () => {
- var bind = { gen: 7 };
- var method = function (id, next) {
+ const bind = { gen: 7 };
+ const method = function (id) {
- return next(null, { id: id, gen: this.gen++, bound: (this === bind) });
+ return { id, gen: this.gen++, bound: this === bind };
};
- var server = new Hapi.Server();
- server.connection();
- server.method('test', method, { bind: bind, cache: { expiresIn: 1000, generateTimeout: 10 } });
-
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
+ const server = Hapi.server();
+ server.method('test', method, { bind, cache: { expiresIn: 1000, generateTimeout: 10 } });
- server.methods.test(1, function (err, result1) {
+ await server.initialize();
- expect(result1.gen).to.equal(7);
- expect(result1.bound).to.equal(true);
+ const result1 = await server.methods.test(1);
+ expect(result1.gen).to.equal(7);
+ expect(result1.bound).to.equal(true);
- server.methods.test(1, function (err, result2) {
-
- expect(result2.gen).to.equal(7);
- done();
- });
- });
- });
+ const result2 = await server.methods.test(1);
+ expect(result2.gen).to.equal(7);
});
- describe('_add()', function () {
+ describe('_add()', () => {
- it('normalizes no callback into callback (direct)', function (done) {
+ it('handles sync method', () => {
- var add = function (a, b) {
+ const add = function (a, b) {
return a + b;
};
- var server = new Hapi.Server();
- server.method('add', add, { callback: false });
- var result = server.methods.add(1, 5);
+ const server = Hapi.server();
+ server.method('add', add);
+ const result = server.methods.add(1, 5);
expect(result).to.equal(6);
- done();
});
- it('normalizes no callback into callback (direct error)', function (done) {
+ it('handles sync method (direct error)', () => {
- var add = function (a, b) {
+ const add = function (a, b) {
return new Error('boom');
};
- var server = new Hapi.Server();
- server.method('add', add, { callback: false });
- var result = server.methods.add(1, 5);
+ const server = Hapi.server();
+ server.method('add', add);
+ const result = server.methods.add(1, 5);
expect(result).to.be.instanceof(Error);
expect(result.message).to.equal('boom');
- done();
});
- it('normalizes no callback into callback (direct throw)', function (done) {
+ it('handles sync method (direct throw)', () => {
- var add = function (a, b) {
+ const add = function (a, b) {
throw new Error('boom');
};
- var server = new Hapi.Server();
- server.method('add', add, { callback: false });
- expect(function () {
+ const server = Hapi.server();
+ server.method('add', add);
+ expect(() => {
server.methods.add(1, 5);
}).to.throw('boom');
- done();
- });
-
- it('normalizes no callback into callback (normalized)', function (done) {
-
- var add = function (a, b) {
-
- return a + b;
- };
-
- var server = new Hapi.Server();
- server.method('add', add, { callback: false });
-
- server._methods._normalized.add(1, 5, function (err, result) {
-
- expect(result).to.equal(6);
- done();
- });
- });
-
- it('normalizes no callback into callback (normalized error)', function (done) {
-
- var add = function (a, b) {
-
- return new Error('boom');
- };
-
- var server = new Hapi.Server();
- server.method('add', add, { callback: false });
-
- server._methods._normalized.add(1, 5, function (err, result) {
-
- expect(err).to.exist();
- expect(err.message).to.equal('boom');
- done();
- });
});
- it('normalizes no callback into callback (normalized throw)', function (done) {
-
- var add = function (a, b) {
-
- throw new Error('boom');
- };
+ it('throws an error if unknown keys are present when making a server method using an object', () => {
- var server = new Hapi.Server();
- server.method('add', add, { callback: false });
+ const fn = function () { };
+ const server = Hapi.server();
- server._methods._normalized.add(1, 5, function (err, result) {
+ expect(() => {
- expect(err).to.exist();
- expect(err.message).to.equal('boom');
- done();
- });
+ server.method({
+ name: 'fn',
+ method: fn,
+ cache: {}
+ });
+ }).to.throw(/^Invalid methodObject options/);
});
});
- it('normalizes no callback into callback (cached)', function (done) {
+ describe('generateKey()', () => {
- var add = function (a, b) {
+ it('handles string argument type', async () => {
- return a + b;
- };
-
- var server = new Hapi.Server();
- server.method('add', add, { cache: { expiresIn: 10, generateTimeout: 10 }, callback: false });
+ const method = (id) => id;
+ const server = Hapi.server();
+ server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 } });
- server._methods._normalized.add(1, 5, function (err, result) {
-
- expect(result).to.equal(6);
- done();
+ await server.initialize();
+ const value = await server.methods.test('x');
+ expect(value).to.equal('x');
});
- });
-
- it('normalizes no callback into callback (cached error)', function (done) {
-
- var add = function (a, b) {
-
- return new Error('boom');
- };
- var server = new Hapi.Server();
- server.method('add', add, { cache: { expiresIn: 10, generateTimeout: 10 }, callback: false });
+ it('handles multiple arguments', async () => {
- server._methods._normalized.add(1, 5, function (err, result) {
+ const method = (a, b, c) => a + b + c;
+ const server = Hapi.server();
+ server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 } });
- expect(err).to.exist();
- expect(err.message).to.equal('boom');
- done();
+ await server.initialize();
+ const value = await server.methods.test('a', 'b', 'c');
+ expect(value).to.equal('abc');
});
- });
-
- it('normalizes no callback into callback (cached throw)', function (done) {
- var add = function (a, b) {
-
- throw new Error('boom');
- };
+ it('errors on invalid argument type', async () => {
- var server = new Hapi.Server();
- server.method('add', add, { cache: { expiresIn: 10, generateTimeout: 10 }, callback: false });
+ const method = (id) => id;
+ const server = Hapi.server();
+ server.method('test', method, { cache: { expiresIn: 1000, generateTimeout: 10 } });
- server._methods._normalized.add(1, 5, function (err, result) {
-
- expect(err).to.exist();
- expect(err.message).to.equal('boom');
- done();
+ await server.initialize();
+ await expect(server.methods.test({})).to.reject('Invalid method key when invoking: test');
});
});
-
- it('throws an error if unknown keys are present when making a server method using an object', function (done) {
-
- var fn = function () { };
- var server = new Hapi.Server();
-
- expect(function () {
-
- server.method({
- name: 'fn',
- method: fn,
- cache: {}
- });
- }).to.throw();
-
- done();
- });
});
diff --git a/test/payload.js b/test/payload.js
index d9db4f9d9..93b045cdf 100755
--- a/test/payload.js
+++ b/test/payload.js
@@ -1,703 +1,998 @@
-// Load modules
+'use strict';
-var Fs = require('fs');
-var Http = require('http');
-var Path = require('path');
-var Zlib = require('zlib');
-var Code = require('code');
-var Hapi = require('..');
-var Hoek = require('hoek');
-var Lab = require('lab');
-var Wreck = require('wreck');
+const Events = require('events');
+const Fs = require('fs');
+const Http = require('http');
+const Net = require('net');
+const Path = require('path');
+const Zlib = require('zlib');
+const Boom = require('@hapi/boom');
+const Code = require('@hapi/code');
+const Hapi = require('..');
+const Hoek = require('@hapi/hoek');
+const Lab = require('@hapi/lab');
+const Wreck = require('@hapi/wreck');
-// Declare internals
+const internals = {};
-var internals = {};
+const { describe, it } = exports.lab = Lab.script();
+const expect = Code.expect;
-// Test shortcuts
-var lab = exports.lab = Lab.script();
-var describe = lab.describe;
-var it = lab.it;
-var expect = Code.expect;
+describe('Payload', () => {
+ it('sets payload', async () => {
-describe('payload', function () {
+ const payload = '{"x":"1","y":"2","z":"3"}';
- it('sets payload', function (done) {
-
- var payload = '{"x":"1","y":"2","z":"3"}';
-
- var handler = function (request, reply) {
+ const handler = (request) => {
expect(request.payload).to.exist();
expect(request.payload.z).to.equal('3');
expect(request.mime).to.equal('application/json');
- return reply(request.payload);
+ return request.payload;
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'POST', path: '/', config: { handler: handler } });
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/', options: { handler } });
- server.inject({ method: 'POST', url: '/', payload: payload }, function (res) {
+ const res = await server.inject({ method: 'POST', url: '/', payload });
+ expect(res.result).to.exist();
+ expect(res.result.x).to.equal('1');
+ });
- expect(res.result).to.exist();
- expect(res.result.x).to.equal('1');
- done();
- });
+ it('handles request socket error', async () => {
+
+ let called = false;
+ const handler = function () {
+
+ called = true;
+ return null;
+ };
+
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/', options: { handler } });
+
+ const res = await server.inject({ method: 'POST', url: '/', payload: 'test', simulate: { error: true, end: false } });
+ expect(res.result).to.exist();
+ expect(res.result.statusCode).to.equal(500);
+ expect(called).to.be.false();
});
- it('handles request socket error', function (done) {
+ it('handles request socket close', async () => {
- var handler = function () {
+ const handler = function () {
throw new Error('never called');
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'POST', path: '/', config: { handler: handler } });
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/', options: { handler } });
- server.inject({ method: 'POST', url: '/', payload: 'test', simulate: { error: true, end: false } }, function (res) {
+ const responded = server.ext('onPostResponse');
- expect(res.result).to.exist();
- expect(res.result.statusCode).to.equal(500);
- done();
- });
+ server.inject({ method: 'POST', url: '/', payload: 'test', simulate: { close: true, end: false } });
+ const request = await responded;
+ expect(request._isReplied).to.equal(true);
+ expect(request.response.output.statusCode).to.equal(500);
});
- it('handles request socket close', function (done) {
+ it('handles aborted request mid-lifecycle step', async (flags) => {
- var handler = function () {
+ let req = null;
+ const server = Hapi.server();
- throw new Error('never called');
- };
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: async (request) => {
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'POST', path: '/', config: { handler: handler } });
+ req.destroy();
- server.once('response', function (request) {
+ await request.events.once('disconnect');
- expect(request._isBailed).to.equal(true);
- done();
+ return 'ok';
+ }
});
- server.inject({ method: 'POST', url: '/', payload: 'test', simulate: { close: true, end: false } }, function (res) { });
+ // Register post handler that should not be called
+
+ let post = 0;
+ server.ext('onPostHandler', () => {
+
+ ++post;
+ });
+
+ flags.onCleanup = () => server.stop();
+ await server.start();
+
+ req = Http.request({
+ hostname: 'localhost',
+ port: server.info.port,
+ method: 'get'
+ });
+
+ req.on('error', Hoek.ignore);
+ req.end();
+
+ const [request] = await server.events.once('response');
+
+ expect(request.response.isBoom).to.be.true();
+ expect(request.response.output.statusCode).to.equal(499);
+ expect(request.info.completed).to.be.above(0);
+ expect(request.info.responded).to.equal(0);
+
+ expect(post).to.equal(0);
});
- it('handles aborted request', function (done) {
+ it('handles aborted request', { retry: true }, async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/', options: { handler: () => 'Success', payload: { parse: false } } });
- var handler = function (request, reply) {
+ const log = server.events.once('log');
- return reply('Success');
+ await server.start();
+
+ const options = {
+ hostname: 'localhost',
+ port: server.info.port,
+ path: '/',
+ method: 'POST',
+ headers: {
+ 'Content-Length': '10'
+ }
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'POST', path: '/', config: { handler: handler, payload: { parse: false } } });
+ const req = Http.request(options, (res) => { });
+ req.on('error', Hoek.ignore);
+ req.write('Hello\n');
+ setTimeout(() => req.destroy(), 50);
- var message = null;
- server.on('log', function (event, tags) {
+ const [event] = await log;
+ expect(event.error.message).to.equal('Parse Error');
+ await server.stop({ timeout: 10 });
+ });
- message = event.data.message;
- });
+ it('errors when payload too big', async () => {
- server.start(function (err) {
+ const payload = '{"x":"1","y":"2","z":"3"}';
- expect(err).to.not.exist();
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/', options: { handler: () => null, payload: { maxBytes: 10 } } });
- var options = {
- hostname: 'localhost',
- port: server.info.port,
- path: '/',
- method: 'POST',
- headers: {
- 'Content-Length': '10'
- }
- };
+ const res = await server.inject({ method: 'POST', url: '/', payload, headers: { 'content-length': payload.length } });
+ expect(res.statusCode).to.equal(413);
+ expect(res.result).to.exist();
+ expect(res.result.message).to.equal('Payload content length greater than maximum allowed: 10');
+ });
- var req = Http.request(options, function (res) {
+ it('errors when payload too big (implicit length)', async () => {
- });
+ const payload = '{"x":"1","y":"2","z":"3"}';
- req.write('Hello\n');
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/', options: { handler: () => null, payload: { maxBytes: 10 } } });
- req.on('error', function (err) {
+ const res = await server.inject({ method: 'POST', url: '/', payload });
+ expect(res.statusCode).to.equal(413);
+ expect(res.result).to.exist();
+ expect(res.result.message).to.equal('Payload content length greater than maximum allowed: 10');
+ });
- expect(message).to.equal('Parse Error');
- expect(err.code).to.equal('ECONNRESET');
- server.stop(done);
- });
+ it('errors when payload too big (file)', async () => {
- setTimeout(function () {
+ const payload = '{"x":"1","y":"2","z":"3"}';
- req.abort();
- }, 15);
- });
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/', options: { handler: () => null, payload: { output: 'file', maxBytes: 10 } } });
+
+ const res = await server.inject({ method: 'POST', url: '/', payload, headers: { 'content-length': payload.length } });
+ expect(res.statusCode).to.equal(413);
+ expect(res.result).to.exist();
+ expect(res.result.message).to.equal('Payload content length greater than maximum allowed: 10');
});
- it('errors when payload too big', function (done) {
+ it('errors when payload too big (file implicit length)', async () => {
- var payload = '{"x":"1","y":"2","z":"3"}';
+ const payload = '{"x":"1","y":"2","z":"3"}';
- var handler = function (request, reply) {
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/', options: { handler: () => null, payload: { output: 'file', maxBytes: 10 } } });
- expect(request.payload.toString()).to.equal(payload);
- return reply(request.payload);
- };
+ const res = await server.inject({ method: 'POST', url: '/', payload });
+ expect(res.statusCode).to.equal(413);
+ expect(res.result).to.exist();
+ expect(res.result.message).to.equal('Payload content length greater than maximum allowed: 10');
+ });
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'POST', path: '/', config: { handler: handler, payload: { maxBytes: 10 } } });
+ it('errors when payload contains prototype poisoning', async () => {
- server.inject({ method: 'POST', url: '/', payload: payload, headers: { 'content-length': payload.length } }, function (res) {
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/', handler: (request) => request.payload.x });
- expect(res.statusCode).to.equal(400);
- expect(res.result).to.exist();
- expect(res.result.message).to.equal('Payload content length greater than maximum allowed: 10');
- done();
- });
+ const payload = '{"x":"1","y":"2","z":"3","__proto__":{"x":"4"}}';
+ const res = await server.inject({ method: 'POST', url: '/', payload });
+ expect(res.statusCode).to.equal(400);
});
- it('returns 400 with response when payload is not consumed', function (done) {
+ it('ignores when payload contains prototype poisoning', async () => {
+
+ const server = Hapi.server();
+ server.route({
+ method: 'POST',
+ path: '/',
+ options: {
+ payload: {
+ protoAction: 'ignore'
+ },
+ handler: (request) => request.payload.__proto__
+ }
+ });
- var payload = new Buffer(10 * 1024 * 1024).toString();
+ const payload = '{"x":"1","y":"2","z":"3","__proto__":{"x":"4"}}';
+ const res = await server.inject({ method: 'POST', url: '/', payload });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal({ x: '4' });
+ });
- var handler = function (request, reply) {
+ it('sanitizes when payload contains prototype poisoning', async () => {
- return reply();
- };
+ const server = Hapi.server();
+ server.route({
+ method: 'POST',
+ path: '/',
+ options: {
+ payload: {
+ protoAction: 'remove'
+ },
+ handler: (request) => request.payload.__proto__
+ }
+ });
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'POST', path: '/', config: { handler: handler, payload: { maxBytes: 1024 * 1024 } } });
+ const payload = '{"x":"1","y":"2","z":"3","__proto__":{"x":"4"}}';
+ const res = await server.inject({ method: 'POST', url: '/', payload });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal({});
+ });
- server.start(function (err) {
+ it('returns 413 with response when payload is not consumed', async () => {
- expect(err).to.not.exist();
+ const payload = Buffer.alloc(10 * 1024 * 1024).toString();
- var uri = 'http://localhost:' + server.info.port;
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/', options: { handler: () => null, payload: { maxBytes: 1024 * 1024 } } });
- Wreck.post(uri, { payload: payload }, function (err, res, body) {
+ await server.start();
- expect(err).to.not.exist();
- expect(res.statusCode).to.equal(400);
- expect(body.toString()).to.equal('{"statusCode":400,"error":"Bad Request","message":"Payload content length greater than maximum allowed: 1048576"}');
+ const uri = 'http://localhost:' + server.info.port;
+ const err = await expect(Wreck.post(uri, { payload })).to.reject();
+ expect(err.data.res.statusCode).to.equal(413);
+ expect(err.data.payload.toString()).to.equal('{"statusCode":413,"error":"Request Entity Too Large","message":"Payload content length greater than maximum allowed: 1048576"}');
- server.stop(done);
- });
- });
+ await server.stop();
});
- it('peeks at unparsed data', function (done) {
+ it('handles expect 100-continue', async () => {
- var data = null;
- var ext = function (request, reply) {
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
- var chunks = [];
- request.on('peek', function (chunk) {
+ await server.start();
- chunks.push(chunk);
- });
+ const client = Net.connect(server.info.port);
- request.once('finish', function () {
+ await Events.once(client, 'connect');
- data = Buffer.concat(chunks);
- });
+ client.write('POST / HTTP/1.1\r\nexpect: 100-continue\r\nhost: host\r\naccept-encoding: gzip\r\n' +
+ 'content-type: application/json\r\ncontent-length: 14\r\nConnection: close\r\n\r\n');
- return reply.continue();
- };
+ const lines = [];
+ client.setEncoding('ascii');
+ for await (const chunk of client) {
- var handler = function (request, reply) {
+ if (chunk.startsWith('HTTP/1.1 100 Continue')) {
+ client.write('{"hello":true}');
+ }
+ else {
+ lines.push(...chunk.split('\r\n'));
+ }
+ }
- return reply(data);
- };
+ const res = lines.shift();
+ const payload = lines.pop();
- var server = new Hapi.Server();
- server.connection();
- server.ext('onRequest', ext);
- server.route({ method: 'POST', path: '/', config: { handler: handler, payload: { parse: false } } });
+ expect(res).to.equal('HTTP/1.1 200 OK');
+ expect(payload).to.equal('{"hello":true}');
- var payload = '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789';
- server.inject({ method: 'POST', url: '/', payload: payload }, function (res) {
+ await server.stop();
+ });
+
+ it('does not continue on errors before payload processing', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
+ server.ext('onPreAuth', (request, h) => {
- expect(res.result).to.equal(payload);
- done();
+ throw new Boom.forbidden();
});
- });
- it('handles gzipped payload', function (done) {
+ await server.start();
- var handler = function (request, reply) {
+ const client = Net.connect(server.info.port);
- return reply(request.payload);
- };
+ await Events.once(client, 'connect');
- var message = { 'msg': 'This message is going to be gzipped.' };
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'POST', path: '/', handler: handler });
+ client.write('POST / HTTP/1.1\r\nexpect: 100-continue\r\nhost: host\r\naccept-encoding: gzip\r\n' +
+ 'content-type: application/json\r\ncontent-length: 14\r\nConnection: close\r\n\r\n');
- Zlib.gzip(JSON.stringify(message), function (err, buf) {
+ let continued = false;
+ const lines = [];
+ client.setEncoding('ascii');
+ for await (const chunk of client) {
- var request = {
- method: 'POST',
- url: '/',
- headers: {
- 'content-type': 'application/json',
- 'content-encoding': 'gzip',
- 'content-length': buf.length
- },
- payload: buf
- };
+ if (chunk.startsWith('HTTP/1.1 100 Continue')) {
+ client.write('{"hello":true}');
+ continued = true;
+ }
+ else {
+ lines.push(...chunk.split('\r\n'));
+ }
+ }
- server.inject(request, function (res) {
+ const res = lines.shift();
- expect(res.result).to.exist();
- expect(res.result).to.deep.equal(message);
- done();
- });
- });
+ expect(res).to.equal('HTTP/1.1 403 Forbidden');
+ expect(continued).to.be.false();
+
+ await server.stop();
});
- it('saves a file after content decoding', function (done) {
+ it('handles expect 100-continue on undefined routes', async () => {
- var path = Path.join(__dirname, './file/image.jpg');
- var sourceContents = Fs.readFileSync(path);
- var stats = Fs.statSync(path);
+ const server = Hapi.server();
+ await server.start();
- Zlib.gzip(sourceContents, function (err, compressed) {
+ const client = Net.connect(server.info.port);
- var handler = function (request, reply) {
+ await Events.once(client, 'connect');
- var receivedContents = Fs.readFileSync(request.payload.path);
- Fs.unlinkSync(request.payload.path);
- expect(receivedContents).to.deep.equal(sourceContents);
- return reply(request.payload.bytes);
- };
+ client.write('POST / HTTP/1.1\r\nexpect: 100-continue\r\nhost: host\r\naccept-encoding: gzip\r\n' +
+ 'content-type: application/json\r\ncontent-length: 14\r\nConnection: close\r\n\r\n');
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'POST', path: '/file', config: { handler: handler, payload: { output: 'file' } } });
- server.inject({ method: 'POST', url: '/file', payload: compressed, headers: { 'content-encoding': 'gzip' } }, function (res) {
+ let continued = false;
+ const lines = [];
+ client.setEncoding('ascii');
+ for await (const chunk of client) {
- expect(res.result).to.equal(stats.size);
- done();
- });
- });
- });
+ if (chunk.startsWith('HTTP/1.1 100 Continue')) {
+ client.write('{"hello":true}');
+ continued = true;
+ }
+ else {
+ lines.push(...chunk.split('\r\n'));
+ }
+ }
- it('errors saving a file without parse', function (done) {
+ const res = lines.shift();
- var handler = function (request, reply) { };
+ expect(res).to.equal('HTTP/1.1 404 Not Found');
+ expect(continued).to.be.false();
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'POST', path: '/file', config: { handler: handler, payload: { output: 'file', parse: false, uploads: '/a/b/c/d/not' } } });
- server.inject({ method: 'POST', url: '/file', payload: 'abcde' }, function (res) {
+ await server.stop();
+ });
- expect(res.statusCode).to.equal(500);
- done();
+ it('does not continue on custom request.payload', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
+ server.ext('onRequest', (request, h) => {
+
+ request.payload = { custom: true };
+ return h.continue;
});
- });
- it('sets parse mode when route methos is * and request is POST', function (done) {
+ await server.start();
- var handler = function (request, reply) {
+ const client = Net.connect(server.info.port);
- return reply(request.payload.key);
- };
+ await Events.once(client, 'connect');
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: '*', path: '/any', handler: handler });
+ client.write('POST / HTTP/1.1\r\nexpect: 100-continue\r\nhost: host\r\naccept-encoding: gzip\r\n' +
+ 'content-type: application/json\r\ncontent-length: 14\r\nConnection: close\r\n\r\n');
- server.inject({ url: '/any', method: 'POST', payload: { key: '09876' } }, function (res) {
+ let continued = false;
+ const lines = [];
+ client.setEncoding('ascii');
+ for await (const chunk of client) {
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.equal('09876');
- done();
- });
+ if (chunk.startsWith('HTTP/1.1 100 Continue')) {
+ client.write('{"hello":true}');
+ continued = true;
+ }
+ else {
+ lines.push(...chunk.split('\r\n'));
+ }
+ }
+
+ const res = lines.shift();
+ const payload = lines.pop();
+
+ expect(res).to.equal('HTTP/1.1 200 OK');
+ expect(payload).to.equal('{"custom":true}');
+ expect(continued).to.be.false();
+
+ await server.stop();
});
- it('returns an error on unsupported mime type', function (done) {
+ it('peeks at unparsed data', async () => {
+
+ let data = null;
+ const ext = (request, h) => {
+
+ const chunks = [];
+ request.events.on('peek', (chunk, encoding) => {
- var handler = function (request, reply) {
+ chunks.push(chunk);
+ });
- return reply(request.payload.key);
+ request.events.once('finish', () => {
+
+ data = Buffer.concat(chunks);
+ });
+
+ return h.continue;
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'POST', path: '/', config: { handler: handler } });
+ const server = Hapi.server();
+ server.ext('onRequest', ext);
+ server.route({ method: 'POST', path: '/', options: { handler: () => data, payload: { parse: false } } });
- server.start(function (err) {
+ const payload = '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789';
+ const res = await server.inject({ method: 'POST', url: '/', payload });
+ expect(res.result).to.equal(payload);
+ });
- expect(err).to.not.exist();
+ it('peeks at unparsed data (finish only)', async () => {
- var options = {
- hostname: 'localhost',
- port: server.info.port,
- path: '/?x=4',
- method: 'POST',
- headers: {
- 'Content-Type': 'application/unknown',
- 'Content-Length': '18'
- }
- };
+ let peeked = false;
+ const ext = (request, h) => {
- var req = Http.request(options, function (res) {
+ request.events.once('finish', () => {
- expect(res.statusCode).to.equal(415);
- server.stop({ timeout: 1 }, done);
+ peeked = true;
});
- req.end('{ "key": "value" }');
- });
+ return h.continue;
+ };
+
+ const server = Hapi.server();
+ server.ext('onRequest', ext);
+ server.route({ method: 'POST', path: '/', options: { handler: () => null, payload: { parse: false } } });
+
+ const payload = '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789';
+ await server.inject({ method: 'POST', url: '/', payload });
+ expect(peeked).to.be.true();
});
- it('ignores unsupported mime type', function (done) {
+ it('handles gzipped payload', async () => {
- var handler = function (request, reply) {
+ const message = { 'msg': 'This message is going to be gzipped.' };
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
- return reply(request.payload);
+ const compressed = await new Promise((resolve) => Zlib.gzip(JSON.stringify(message), (ignore, result) => resolve(result)));
+
+ const request = {
+ method: 'POST',
+ url: '/',
+ headers: {
+ 'content-type': 'application/json',
+ 'content-encoding': 'gzip',
+ 'content-length': compressed.length
+ },
+ payload: compressed
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'POST', path: '/', config: { handler: handler, payload: { failAction: 'ignore' } } });
+ const res = await server.inject(request);
+ expect(res.result).to.exist();
+ expect(res.result).to.equal(message);
+ });
- server.inject({ method: 'POST', url: '/', payload: 'testing123', headers: { 'content-type': 'application/unknown' } }, function (res) {
+ it('handles deflated payload', async () => {
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.deep.equal(null);
- done();
- });
+ const message = { 'msg': 'This message is going to be gzipped.' };
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
+
+ const compressed = await new Promise((resolve) => Zlib.deflate(JSON.stringify(message), (ignore, result) => resolve(result)));
+
+ const request = {
+ method: 'POST',
+ url: '/',
+ headers: {
+ 'content-type': 'application/json',
+ 'content-encoding': 'deflate',
+ 'content-length': compressed.length
+ },
+ payload: compressed
+ };
+
+ const res = await server.inject(request);
+ expect(res.result).to.exist();
+ expect(res.result).to.equal(message);
});
- it('returns 200 on octet mime type', function (done) {
+ it('handles custom compression', async () => {
+
+ const message = { 'msg': 'This message is going to be gzipped.' };
+ const server = Hapi.server({ routes: { payload: { compression: { test: { some: 'options' } } } } });
- var handler = function (request, reply) {
+ const decoder = (options) => {
- return reply('ok');
+ expect(options).to.equal({ some: 'options' });
+ return Zlib.createGunzip();
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'POST', path: '/', handler: handler });
+ server.decoder('test', decoder);
+ server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
- server.inject({ method: 'POST', url: '/', payload: 'testing123', headers: { 'content-type': 'application/octet-stream' } }, function (res) {
+ const compressed = await new Promise((resolve) => Zlib.gzip(JSON.stringify(message), (ignore, result) => resolve(result)));
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.equal('ok');
- done();
- });
+ const request = {
+ method: 'POST',
+ url: '/',
+ headers: {
+ 'content-type': 'application/json',
+ 'content-encoding': 'test',
+ 'content-length': compressed.length
+ },
+ payload: compressed
+ };
+
+ const res = await server.inject(request);
+ expect(res.result).to.exist();
+ expect(res.result).to.equal(message);
});
- it('returns 200 on text mime type', function (done) {
+ it('saves a file after content decoding', async () => {
+
+ const path = Path.join(__dirname, './file/image.jpg');
+ const sourceContents = Fs.readFileSync(path);
+ const stats = Fs.statSync(path);
- var textHandler = function (request, reply) {
+ const handler = (request) => {
- return reply(request.payload + '+456');
+ const receivedContents = Fs.readFileSync(request.payload.path);
+ Fs.unlinkSync(request.payload.path);
+ expect(receivedContents).to.equal(sourceContents);
+ return request.payload.bytes;
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'POST', path: '/text', config: { handler: textHandler } });
+ const compressed = await new Promise((resolve) => Zlib.gzip(sourceContents, (ignore, result) => resolve(result)));
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/file', options: { handler, payload: { output: 'file' } } });
+ const res = await server.inject({ method: 'POST', url: '/file', payload: compressed, headers: { 'content-encoding': 'gzip' } });
+ expect(res.result).to.equal(stats.size);
+ });
- server.inject({ method: 'POST', url: '/text', payload: 'testing123', headers: { 'content-type': 'text/plain' } }, function (res) {
+ it('errors saving a file without parse', async () => {
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.equal('testing123+456');
- done();
- });
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/file', options: { handler: Hoek.block, payload: { output: 'file', parse: false, uploads: '/a/b/c/d/not' } } });
+ const res = await server.inject({ method: 'POST', url: '/file', payload: 'abcde' });
+ expect(res.statusCode).to.equal(500);
});
- it('returns 200 on override mime type', function (done) {
+ it('sets parse mode when route method is * and request is POST', async () => {
- var handler = function (request, reply) {
+ const server = Hapi.server();
+ server.route({ method: '*', path: '/any', handler: (request) => request.payload.key });
- return reply(request.payload.key);
- };
+ const res = await server.inject({ url: '/any', method: 'POST', payload: { key: '09876' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('09876');
+ });
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'POST', path: '/override', config: { handler: handler, payload: { override: 'application/json' } } });
+ it('returns an error on unsupported mime type', async () => {
- server.inject({ method: 'POST', url: '/override', payload: '{"key":"cool"}', headers: { 'content-type': 'text/plain' } }, function (res) {
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/', handler: (request) => request.payload.key });
+ await server.start();
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.equal('cool');
- done();
- });
+ const options = {
+ headers: {
+ 'Content-Type': 'application/unknown',
+ 'Content-Length': '18'
+ },
+ payload: '{ "key": "value" }'
+ };
+
+ const err = await expect(Wreck.post(`http://localhost:${server.info.port}/?x=4`, options)).to.reject();
+ expect(err.output.statusCode).to.equal(415);
+ await server.stop({ timeout: 1 });
});
- it('returns 200 on text mime type when allowed', function (done) {
+ it('ignores unsupported mime type', async () => {
- var textHandler = function (request, reply) {
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/', options: { handler: (request) => request.payload, payload: { failAction: 'ignore' } } });
- return reply(request.payload + '+456');
- };
+ const res = await server.inject({ method: 'POST', url: '/', payload: 'testing123', headers: { 'content-type': 'application/unknown' } });
+ expect(res.statusCode).to.equal(204);
+ expect(res.result).to.equal(null);
+ });
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'POST', path: '/textOnly', config: { handler: textHandler, payload: { allow: 'text/plain' } } });
+ it('returns 200 on octet mime type', async () => {
- server.inject({ method: 'POST', url: '/textOnly', payload: 'testing123', headers: { 'content-type': 'text/plain' } }, function (res) {
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/', handler: () => 'ok' });
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.equal('testing123+456');
- done();
- });
+ const res = await server.inject({ method: 'POST', url: '/', payload: 'testing123', headers: { 'content-type': 'application/octet-stream' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('ok');
});
- it('returns 415 on non text mime type when disallowed', function (done) {
+ it('returns 200 on text mime type', async () => {
- var textHandler = function (request, reply) {
+ const handler = (request) => {
- return reply(request.payload + '+456');
+ return request.payload + '+456';
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'POST', path: '/textOnly', config: { handler: textHandler, payload: { allow: 'text/plain' } } });
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/text', handler });
- server.inject({ method: 'POST', url: '/textOnly', payload: 'testing123', headers: { 'content-type': 'application/octet-stream' } }, function (res) {
+ const res = await server.inject({ method: 'POST', url: '/text', payload: 'testing123', headers: { 'content-type': 'text/plain' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('testing123+456');
+ });
- expect(res.statusCode).to.equal(415);
- done();
- });
+ it('returns 200 on override mime type', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/override', options: { handler: (request) => request.payload.key, payload: { override: 'application/json' } } });
+
+ const res = await server.inject({ method: 'POST', url: '/override', payload: '{"key":"cool"}', headers: { 'content-type': 'text/plain' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('cool');
});
- it('returns 200 on text mime type when allowed (array)', function (done) {
+ it('returns 200 on text mime type when allowed', async () => {
- var textHandler = function (request, reply) {
+ const handler = (request) => {
- return reply(request.payload + '+456');
+ return request.payload + '+456';
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'POST', path: '/textOnlyArray', config: { handler: textHandler, payload: { allow: ['text/plain'] } } });
-
- server.inject({ method: 'POST', url: '/textOnlyArray', payload: 'testing123', headers: { 'content-type': 'text/plain' } }, function (res) {
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/textOnly', options: { handler, payload: { allow: 'text/plain' } } });
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.equal('testing123+456');
- done();
- });
+ const res = await server.inject({ method: 'POST', url: '/textOnly', payload: 'testing123', headers: { 'content-type': 'text/plain' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('testing123+456');
});
- it('returns 415 on non text mime type when disallowed (array)', function (done) {
+ it('returns 415 on non text mime type when disallowed', async () => {
- var textHandler = function (request, reply) {
+ const handler = (request) => {
- return reply(request.payload + '+456');
+ return request.payload + '+456';
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'POST', path: '/textOnlyArray', config: { handler: textHandler, payload: { allow: ['text/plain'] } } });
-
- server.inject({ method: 'POST', url: '/textOnlyArray', payload: 'testing123', headers: { 'content-type': 'application/octet-stream' } }, function (res) {
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/textOnly', options: { handler, payload: { allow: 'text/plain' } } });
- expect(res.statusCode).to.equal(415);
- done();
- });
+ const res = await server.inject({ method: 'POST', url: '/textOnly', payload: 'testing123', headers: { 'content-type': 'application/octet-stream' } });
+ expect(res.statusCode).to.equal(415);
});
- it('parses application/x-www-form-urlencoded with arrays', function (done) {
+ it('returns 200 on text mime type when allowed (array)', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const handler = (request) => {
- server.route({
- method: 'POST',
- path: '/',
- handler: function (request, reply) {
+ return request.payload + '+456';
+ };
- return reply(request.payload.x.y + request.payload.x.z);
- }
- });
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/textOnlyArray', options: { handler, payload: { allow: ['text/plain'] } } });
- server.inject({ method: 'POST', url: '/', payload: 'x[y]=1&x[z]=2', headers: { 'content-type': 'application/x-www-form-urlencoded' } }, function (res) {
+ const res = await server.inject({ method: 'POST', url: '/textOnlyArray', payload: 'testing123', headers: { 'content-type': 'text/plain' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('testing123+456');
+ });
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.equal('12');
- done();
- });
+ it('returns 415 on non text mime type when disallowed (array)', async () => {
+
+ const handler = (request) => {
+
+ return request.payload + '+456';
+ };
+
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/textOnlyArray', options: { handler, payload: { allow: ['text/plain'] } } });
+
+ const res = await server.inject({ method: 'POST', url: '/textOnlyArray', payload: 'testing123', headers: { 'content-type': 'application/octet-stream' } });
+ expect(res.statusCode).to.equal(415);
});
- it('returns parsed multipart data', function (done) {
-
- var multipartPayload =
- '--AaB03x\r\n' +
- 'content-disposition: form-data; name="x"\r\n' +
- '\r\n' +
- 'First\r\n' +
- '--AaB03x\r\n' +
- 'content-disposition: form-data; name="x"\r\n' +
- '\r\n' +
- 'Second\r\n' +
- '--AaB03x\r\n' +
- 'content-disposition: form-data; name="x"\r\n' +
- '\r\n' +
- 'Third\r\n' +
- '--AaB03x\r\n' +
- 'content-disposition: form-data; name="field1"\r\n' +
- '\r\n' +
- 'Joe Blow\r\nalmost tricked you!\r\n' +
- '--AaB03x\r\n' +
- 'content-disposition: form-data; name="field1"\r\n' +
- '\r\n' +
- 'Repeated name segment\r\n' +
- '--AaB03x\r\n' +
- 'content-disposition: form-data; name="pics"; filename="file1.txt"\r\n' +
- 'Content-Type: text/plain\r\n' +
- '\r\n' +
- '... contents of file1.txt ...\r\r\n' +
- '--AaB03x--\r\n';
-
- var handler = function (request, reply) {
-
- var result = {};
- var keys = Object.keys(request.payload);
- for (var i = 0, il = keys.length; i < il; ++i) {
- var key = keys[i];
- var value = request.payload[key];
+ it('returns parsed multipart data (route)', async () => {
+
+ const multipartPayload =
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="x"\r\n' +
+ '\r\n' +
+ 'First\r\n' +
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="x"\r\n' +
+ '\r\n' +
+ 'Second\r\n' +
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="x"\r\n' +
+ '\r\n' +
+ 'Third\r\n' +
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="field1"\r\n' +
+ '\r\n' +
+ 'Joe Blow\r\nalmost tricked you!\r\n' +
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="field1"\r\n' +
+ '\r\n' +
+ 'Repeated name segment\r\n' +
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="pics"; filename="file1.txt"\r\n' +
+ 'Content-Type: text/plain\r\n' +
+ '\r\n' +
+ '... contents of file1.txt ...\r\r\n' +
+ '--AaB03x--\r\n';
+
+ const handler = (request) => {
+
+ const result = {};
+ const keys = Object.keys(request.payload);
+ for (let i = 0; i < keys.length; ++i) {
+ const key = keys[i];
+ const value = request.payload[key];
result[key] = value._readableState ? true : value;
}
- return reply(result);
+ return result;
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'POST', path: '/echo', config: { handler: handler } });
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/echo', handler, options: { payload: { multipart: true } } });
- server.inject({ method: 'POST', url: '/echo', payload: multipartPayload, headers: { 'content-type': 'multipart/form-data; boundary=AaB03x' } }, function (res) {
-
- expect(Object.keys(res.result).length).to.equal(3);
- expect(res.result.field1).to.exist();
- expect(res.result.field1.length).to.equal(2);
- expect(res.result.field1[1]).to.equal('Repeated name segment');
- expect(res.result.pics).to.exist();
- done();
- });
+ const res = await server.inject({ method: 'POST', url: '/echo', payload: multipartPayload, headers: { 'content-type': 'multipart/form-data; boundary=AaB03x' } });
+ expect(Object.keys(res.result).length).to.equal(3);
+ expect(res.result.field1).to.exist();
+ expect(res.result.field1.length).to.equal(2);
+ expect(res.result.field1[1]).to.equal('Repeated name segment');
+ expect(res.result.pics).to.exist();
});
- it('times out when client request taking too long', function (done) {
-
- var handler = function (request, reply) {
+ it('returns parsed multipart data (server)', async () => {
+
+ const multipartPayload =
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="x"\r\n' +
+ '\r\n' +
+ 'First\r\n' +
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="x"\r\n' +
+ '\r\n' +
+ 'Second\r\n' +
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="x"\r\n' +
+ '\r\n' +
+ 'Third\r\n' +
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="field1"\r\n' +
+ '\r\n' +
+ 'Joe Blow\r\nalmost tricked you!\r\n' +
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="field1"\r\n' +
+ '\r\n' +
+ 'Repeated name segment\r\n' +
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="pics"; filename="file1.txt"\r\n' +
+ 'Content-Type: text/plain\r\n' +
+ '\r\n' +
+ '... contents of file1.txt ...\r\r\n' +
+ '--AaB03x--\r\n';
+
+ const handler = (request) => {
+
+ const result = {};
+ const keys = Object.keys(request.payload);
+ for (let i = 0; i < keys.length; ++i) {
+ const key = keys[i];
+ const value = request.payload[key];
+ result[key] = value._readableState ? true : value;
+ }
- return reply('fast');
+ return result;
};
- var server = new Hapi.Server();
- server.connection({ routes: { payload: { timeout: 50 } } });
- server.route({ method: 'POST', path: '/fast', config: { handler: handler } });
- server.start(function (err) {
+ const server = Hapi.server({ routes: { payload: { multipart: true } } });
+ server.route({ method: 'POST', path: '/echo', handler });
- expect(err).to.not.exist();
-
- var timer = new Hoek.Bench();
- var options = {
- hostname: '127.0.0.1',
- port: server.info.port,
- path: '/fast',
- method: 'POST'
- };
-
- var req = Http.request(options, function (res) {
+ const res = await server.inject({ method: 'POST', url: '/echo', payload: multipartPayload, headers: { 'content-type': 'multipart/form-data; boundary=AaB03x' } });
+ expect(Object.keys(res.result).length).to.equal(3);
+ expect(res.result.field1).to.exist();
+ expect(res.result.field1.length).to.equal(2);
+ expect(res.result.field1[1]).to.equal('Repeated name segment');
+ expect(res.result.pics).to.exist();
+ });
- expect(res.statusCode).to.equal(408);
- expect(timer.elapsed()).to.be.at.least(45);
- server.stop({ timeout: 1 }, done);
- });
+ it('places default limit on max parts in multipart payloads', async () => {
- req.on('error', function (err) { }); // Will error out, so don't allow error to escape test
+ const part = '--AaB03x\r\n' + 'content-disposition: form-data; name="x"\r\n\r\n' + 'x\r\n';
+ const multipartPayload = part.repeat(1001) + '--AaB03x--\r\n';
- req.write('{}\n');
- var now = Date.now();
- setTimeout(function () {
+ const server = Hapi.server({ routes: { payload: { multipart: true } } });
+ server.route({ method: 'POST', path: '/', handler: () => null });
- req.end();
- }, 100);
- });
+ const res = await server.inject({ method: 'POST', url: '/', payload: multipartPayload, headers: { 'content-type': 'multipart/form-data; boundary=AaB03x' } });
+ expect(res.statusCode).to.equal(400);
+ expect(res.result.message).to.equal('Invalid multipart payload format');
});
- it('times out when client request taking too long (route override)', function (done) {
+ it('signals connection close when payload is unconsumed', async () => {
- var handler = function (request, reply) {
+ const payload = Buffer.alloc(1024);
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/', options: { handler: () => 'ok', payload: { maxBytes: 1024, output: 'stream', parse: false } } });
- return reply('fast');
- };
+ const res = await server.inject({ method: 'POST', url: '/', payload, headers: { 'content-type': 'application/octet-stream' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers).to.include({ connection: 'close' });
+ expect(res.result).to.equal('ok');
+ });
+
+ it('times out when client request taking too long', async () => {
- var server = new Hapi.Server();
- server.connection({ routes: { payload: { timeout: false } } });
- server.route({ method: 'POST', path: '/fast', config: { payload: { timeout: 50 }, handler: handler } });
- server.start(function (err) {
+ const server = Hapi.server({ routes: { payload: { timeout: 50 } } });
+ server.route({ method: 'POST', path: '/', handler: () => null });
+ await server.start();
- expect(err).to.not.exist();
+ const request = () => {
- var timer = new Hoek.Bench();
- var options = {
+ const options = {
hostname: '127.0.0.1',
port: server.info.port,
- path: '/fast',
+ path: '/',
method: 'POST'
};
- var req = Http.request(options, function (res) {
-
- expect(res.statusCode).to.equal(408);
- expect(timer.elapsed()).to.be.at.least(45);
- server.stop({ timeout: 1 }, done);
- });
-
- req.on('error', function (err) { }); // Will error out, so don't allow error to escape test
-
+ const req = Http.request(options);
+ req.on('error', Hoek.ignore);
req.write('{}\n');
- var now = Date.now();
- setTimeout(function () {
-
- req.end();
- }, 100);
- });
- });
+ setTimeout(() => req.end(), 100);
+ return new Promise((resolve) => req.once('response', resolve));
+ };
- it('returns payload when timeout is not triggered', function (done) {
+ const timer = new Hoek.Bench();
+ const res = await request();
+ expect(res.statusCode).to.equal(408);
+ expect(timer.elapsed()).to.be.at.least(50);
- var handler = function (request, reply) {
+ await server.stop({ timeout: 1 });
+ });
- return reply('fast');
- };
+ it('times out when client request taking too long (route override)', async () => {
- var server = new Hapi.Server();
- server.connection({ routes: { payload: { timeout: 50 } } });
- server.route({ method: 'POST', path: '/fast', config: { handler: handler } });
- server.start(function (err) {
+ const server = Hapi.server({ routes: { payload: { timeout: false } } });
+ server.route({ method: 'POST', path: '/', options: { payload: { timeout: 50 }, handler: () => null } });
+ await server.start();
- expect(err).to.not.exist();
+ const request = () => {
- var options = {
+ const options = {
hostname: '127.0.0.1',
port: server.info.port,
- path: '/fast',
+ path: '/',
method: 'POST'
};
- var req = Http.request(options, function (res) {
+ const req = Http.request(options);
+ req.on('error', Hoek.ignore);
+ req.write('{}\n');
+ setTimeout(() => req.end(), 100);
+ return new Promise((resolve) => req.once('response', resolve));
+ };
- expect(res.statusCode).to.equal(200);
- server.stop({ timeout: 1 }, done);
- });
+ const timer = new Hoek.Bench();
+ const res = await request();
+ expect(res.statusCode).to.equal(408);
+ expect(timer.elapsed()).to.be.at.least(50);
- req.end();
- });
+ await server.stop({ timeout: 1 });
+ });
+
+ it('returns payload when timeout is not triggered', async () => {
+
+ const server = Hapi.server({ routes: { payload: { timeout: 50 } } });
+ server.route({ method: 'POST', path: '/', handler: () => 'fast' });
+ await server.start();
+ const { res } = await Wreck.post(`http://localhost:${server.info.port}/`);
+ expect(res.statusCode).to.equal(200);
+ await server.stop({ timeout: 1 });
+ });
+
+ it('errors if multipart payload exceeds byte limit', async () => {
+
+ const multipartPayload =
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="x"\r\n' +
+ '\r\n' +
+ 'First\r\n' +
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="x"\r\n' +
+ '\r\n' +
+ 'Second\r\n' +
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="x"\r\n' +
+ '\r\n' +
+ 'Third\r\n' +
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="field1"\r\n' +
+ '\r\n' +
+ 'Joe Blow\r\nalmost tricked you!\r\n' +
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="field1"\r\n' +
+ '\r\n' +
+ 'Repeated name segment\r\n' +
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="pics"; filename="file1.txt"\r\n' +
+ 'Content-Type: text/plain\r\n' +
+ '\r\n' +
+ '... contents of file1.txt ...\r\r\n' +
+ '--AaB03x--\r\n';
+
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/echo', options: { handler: () => 'result', payload: { output: 'data', parse: true, maxBytes: 5, multipart: true } } });
+
+ const res = await server.inject({ method: 'POST', url: '/echo', payload: multipartPayload, simulate: { split: true }, headers: { 'content-length': null, 'content-type': 'multipart/form-data; boundary=AaB03x' } });
+ expect(res.statusCode).to.equal(400);
+ expect(res.payload.toString()).to.equal('{"statusCode":400,"error":"Bad Request","message":"Invalid multipart payload format"}');
+ });
+
+ it('errors if multipart disabled (default)', async () => {
+
+ const multipartPayload =
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="x"\r\n' +
+ '\r\n' +
+ 'First\r\n' +
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="x"\r\n' +
+ '\r\n' +
+ 'Second\r\n' +
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="x"\r\n' +
+ '\r\n' +
+ 'Third\r\n' +
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="field1"\r\n' +
+ '\r\n' +
+ 'Joe Blow\r\nalmost tricked you!\r\n' +
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="field1"\r\n' +
+ '\r\n' +
+ 'Repeated name segment\r\n' +
+ '--AaB03x\r\n' +
+ 'content-disposition: form-data; name="pics"; filename="file1.txt"\r\n' +
+ 'Content-Type: text/plain\r\n' +
+ '\r\n' +
+ '... contents of file1.txt ...\r\r\n' +
+ '--AaB03x--\r\n';
+
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/echo', options: { handler: () => 'result', payload: { output: 'data', parse: true, maxBytes: 5 } } });
+
+ const res = await server.inject({ method: 'POST', url: '/echo', payload: multipartPayload, simulate: { split: true }, headers: { 'content-length': null, 'content-type': 'multipart/form-data; boundary=AaB03x' } });
+ expect(res.statusCode).to.equal(415);
});
});
diff --git a/test/plugin.js b/test/plugin.js
deleted file mode 100755
index be1697930..000000000
--- a/test/plugin.js
+++ /dev/null
@@ -1,3192 +0,0 @@
-// Load modules
-
-var Os = require('os');
-var Path = require('path');
-var Boom = require('boom');
-var CatboxMemory = require('catbox-memory');
-var Code = require('code');
-var Handlebars = require('handlebars');
-var Hapi = require('..');
-var Hoek = require('hoek');
-var Inert = require('inert');
-var Lab = require('lab');
-var Vision = require('vision');
-
-
-// Declare internals
-
-var internals = {};
-
-
-// Test shortcuts
-
-var lab = exports.lab = Lab.script();
-var describe = lab.describe;
-var it = lab.it;
-var expect = Code.expect;
-
-
-describe('Plugin', function () {
-
- describe('select()', function () {
-
- it('creates a subset of connections for manipulation', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ labels: ['s1', 'a', 'b'] });
- server.connection({ labels: ['s2', 'a', 'c'] });
- server.connection({ labels: ['s3', 'a', 'b', 'd'] });
- server.connection({ labels: ['s4', 'b', 'x'] });
-
- var register = function (srv, options, next) {
-
- var a = srv.select('a');
- var ab = a.select('b');
- var memoryx = srv.select('x', 's4');
- var sodd = srv.select(['s2', 's4']);
-
- expect(srv.connections.length).to.equal(4);
- expect(a.connections.length).to.equal(3);
- expect(ab.connections.length).to.equal(2);
- expect(memoryx.connections.length).to.equal(1);
- expect(sodd.connections.length).to.equal(2);
-
- srv.route({
- method: 'GET',
- path: '/all',
- handler: function (request, reply) {
-
- return reply('all');
- }
- });
-
- a.route({
- method: 'GET',
- path: '/a',
- handler: function (request, reply) {
-
- return reply('a');
- }
- });
-
- ab.route({
- method: 'GET',
- path: '/ab',
- handler: function (request, reply) {
-
- return reply('ab');
- }
- });
-
- memoryx.route({
-
- method: 'GET',
- path: '/memoryx',
- handler: function (request, reply) {
-
- return reply('memoryx');
- }
- });
-
- sodd.route({
- method: 'GET',
- path: '/sodd',
- handler: function (request, reply) {
-
- return reply('sodd');
- }
- });
-
- memoryx.state('sid', { encoding: 'base64' });
- srv.method({
- name: 'testMethod', method: function (nxt) {
-
- return nxt(null, '123');
- }, options: { cache: { expiresIn: 1000, generateTimeout: 10 } }
- });
-
- srv.methods.testMethod(function (err, result1) {
-
- expect(result1).to.equal('123');
-
- srv.methods.testMethod(function (err, result2) {
-
- expect(result2).to.equal('123');
- return next();
- });
- });
- };
-
- register.attributes = {
- name: 'plugin'
- };
-
- server.register(register, function (err) {
-
- expect(err).to.not.exist();
-
- expect(internals.routesList(server, 's1')).to.deep.equal(['/a', '/ab', '/all']);
- expect(internals.routesList(server, 's2')).to.deep.equal(['/a', '/all', '/sodd']);
- expect(internals.routesList(server, 's3')).to.deep.equal(['/a', '/ab', '/all']);
- expect(internals.routesList(server, 's4')).to.deep.equal(['/all', '/memoryx', '/sodd']);
- done();
- });
- });
-
- it('registers a plugin on selection inside a plugin', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ labels: ['a'] });
- server.connection({ labels: ['b'] });
- server.connection({ labels: ['c'] });
-
- var server1 = server.connections[0];
- var server2 = server.connections[1];
- var server3 = server.connections[2];
-
- var child = function (srv, options, next) {
-
- srv.expose('key2', srv.connections.length);
- return next();
- };
-
- child.attributes = {
- name: 'child'
- };
-
- var test = function (srv, options, next) {
-
- srv.expose('key1', srv.connections.length);
- srv.select('a').register(child, next);
- };
-
- test.attributes = {
- name: 'test'
- };
-
- server.register(test, { select: ['a', 'b'] }, function (err) {
-
- expect(err).to.not.exist();
- expect(server.plugins.test.key1).to.equal(2);
- expect(server.plugins.child.key2).to.equal(1);
- done();
- });
- });
- });
-
- describe('register()', function () {
-
- it('registers plugin with options', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ labels: ['a', 'b'] });
-
- var test = function (srv, options, next) {
-
- expect(options.something).to.be.true();
- expect(srv.realm.pluginOptions).to.equal(options);
- return next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- server.register({ register: test, options: { something: true } }, function (err) {
-
- expect(err).to.not.exist();
- done();
- });
- });
-
- it('registers a required plugin', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ labels: ['a', 'b'] });
-
- var test = {
- register: function (srv, options, next) {
-
- expect(options.something).to.be.true();
- return next();
- }
- };
-
- test.register.attributes = {
- name: 'test'
- };
-
- server.register({ register: test, options: { something: true } }, function (err) {
-
- expect(err).to.not.exist();
- done();
- });
- });
-
- it('throws on bad plugin (missing attributes)', function (done) {
-
- var server = new Hapi.Server();
- expect(function () {
-
- server.register({
- register: function (srv, options, next) {
-
- return next();
- }
- }, function (err) { });
-
- }).to.throw('Invalid plugin object - invalid or missing register function attributes property');
-
- done();
- });
-
- it('throws on bad plugin (missing name)', function (done) {
-
- var register = function (srv, options, next) {
-
- return next();
- };
-
- register.attributes = {};
-
- var server = new Hapi.Server();
- expect(function () {
-
- server.register(register, function (err) { });
- }).to.throw('Missing plugin name');
-
- done();
- });
-
- it('throws on bad plugin (empty pkg)', function (done) {
-
- var register = function (srv, options, next) {
-
- return next();
- };
-
- register.attributes = {
- pkg: {}
- };
-
- var server = new Hapi.Server();
- expect(function () {
-
- server.register(register, function (err) { });
- }).to.throw('Missing plugin name');
-
- done();
- });
-
- it('throws when register is missing a callback function', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ labels: ['a', 'b'] });
-
- var test = function (srv, options, next) {
-
- expect(options.something).to.be.true();
- return next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- expect(function () {
-
- server.register(test);
- }).to.throw('A callback function is required to register a plugin');
- done();
- });
-
- it('returns plugin error', function (done) {
-
- var test = function (srv, options, next) {
-
- return next(new Error('from plugin'));
- };
-
- test.attributes = {
- name: 'test'
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.register(test, function (err) {
-
- expect(err).to.exist();
- expect(err.message).to.equal('from plugin');
- done();
- });
- });
-
- it('sets version to 0.0.0 if missing', function (done) {
-
- var test = function (srv, options, next) {
-
- srv.route({
- method: 'GET',
- path: '/',
- handler: function (request, reply) {
-
- return reply(srv.version);
- }
- });
- return next();
- };
-
- test.attributes = {
- pkg: {
- name: 'steve'
- }
- };
-
- var server = new Hapi.Server();
- server.connection();
-
- server.register(test, function (err) {
-
- expect(err).to.not.exist();
- expect(server.connections[0]._registrations.steve.version).to.equal('0.0.0');
- server.inject('/', function (res) {
-
- expect(res.result).to.equal(require('../package.json').version);
- done();
- });
- });
- });
-
- it('prevents plugin from multiple registrations', function (done) {
-
- var test = function (srv, options, next) {
-
- srv.route({
- method: 'GET',
- path: '/a',
- handler: function (request, reply) {
-
- return reply('a');
- }
- });
-
- return next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- var server = new Hapi.Server();
- server.connection({ host: 'example.com' });
- server.register(test, function (err) {
-
- expect(err).to.not.exist();
- expect(function () {
-
- server.register(test, function (err) { });
- }).to.throw('Plugin test already registered in: http://example.com');
-
- done();
- });
- });
-
- it('allows plugin multiple registrations (attributes)', function (done) {
-
- var test = function (srv, options, next) {
-
- srv.app.x = srv.app.x ? srv.app.x + 1 : 1;
- return next();
- };
-
- test.attributes = {
- name: 'test',
- multiple: true
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.register(test, function (err) {
-
- expect(err).to.not.exist();
- server.register(test, function (err) {
-
- expect(err).to.not.exist();
- expect(server.app.x).to.equal(2);
- done();
- });
- });
- });
-
- it('registers multiple plugins', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ labels: 'test' });
- var log = null;
- server.once('log', function (event, tags) {
-
- log = [event, tags];
- });
-
- server.register([internals.plugins.test1, internals.plugins.test2], function (err) {
-
- expect(err).to.not.exist();
- expect(internals.routesList(server)).to.deep.equal(['/test1', '/test2']);
- expect(log[1].test).to.equal(true);
- expect(log[0].data).to.equal('abc');
- done();
- });
- });
-
- it('registers multiple plugins (verbose)', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ labels: 'test' });
- var log = null;
- server.once('log', function (event, tags) {
-
- log = [event, tags];
- });
-
- server.register([{ register: internals.plugins.test1 }, { register: internals.plugins.test2 }], function (err) {
-
- expect(err).to.not.exist();
- expect(internals.routesList(server)).to.deep.equal(['/test1', '/test2']);
- expect(log[1].test).to.equal(true);
- expect(log[0].data).to.equal('abc');
- done();
- });
- });
-
- it('registers a child plugin', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ labels: 'test' });
- server.register(internals.plugins.child, function (err) {
-
- expect(err).to.not.exist();
- server.inject('/test1', function (res) {
-
- expect(res.result).to.equal('testing123');
- done();
- });
- });
- });
-
- it('registers a plugin with routes path prefix', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ labels: 'test' });
- server.register(internals.plugins.test1, { routes: { prefix: '/xyz' } }, function (err) {
-
- expect(server.plugins.test1.prefix).to.equal('/xyz');
- expect(err).to.not.exist();
- server.inject('/xyz/test1', function (res) {
-
- expect(res.result).to.equal('testing123');
- done();
- });
- });
- });
-
- it('registers a plugin with routes path prefix and plugin root route', function (done) {
-
- var test = function (srv, options, next) {
-
- srv.route({
- method: 'GET',
- path: '/',
- handler: function (request, reply) {
-
- return reply('ok');
- }
- });
- return next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- var server = new Hapi.Server();
- server.connection({ labels: 'test' });
- server.register(test, { routes: { prefix: '/xyz' } }, function (err) {
-
- expect(err).to.not.exist();
- server.inject('/xyz', function (res) {
-
- expect(res.result).to.equal('ok');
- done();
- });
- });
- });
-
- it('ignores the type of the plugin value', function (done) {
-
- var a = function () { };
- a.register = function (srv, options, next) {
-
- srv.route({
- method: 'GET',
- path: '/',
- handler: function (request, reply) {
-
- return reply('ok');
- }
- });
- return next();
- };
-
- a.register.attributes = { name: 'a' };
-
- var server = new Hapi.Server();
- server.connection({ labels: 'test' });
- server.register(a, { routes: { prefix: '/xyz' } }, function (err) {
-
- expect(err).to.not.exist();
- server.inject('/xyz', function (res) {
-
- expect(res.result).to.equal('ok');
- done();
- });
- });
- });
-
- it('registers a child plugin with parent routes path prefix', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ labels: 'test' });
- server.register(internals.plugins.child, { routes: { prefix: '/xyz' } }, function (err) {
-
- expect(err).to.not.exist();
- server.inject('/xyz/test1', function (res) {
-
- expect(res.result).to.equal('testing123');
- done();
- });
- });
- });
-
- it('registers a child plugin with parent routes vhost prefix', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ labels: 'test' });
- server.register(internals.plugins.child, { routes: { vhost: 'example.com' } }, function (err) {
-
- expect(err).to.not.exist();
- server.inject({ url: '/test1', headers: { host: 'example.com' } }, function (res) {
-
- expect(res.result).to.equal('testing123');
- done();
- });
- });
- });
-
- it('registers a child plugin with parent routes path prefix and inner register prefix', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ labels: 'test' });
- server.register({ register: internals.plugins.child, options: { routes: { prefix: '/inner' } } }, { routes: { prefix: '/xyz' } }, function (err) {
-
- expect(err).to.not.exist();
- server.inject('/xyz/inner/test1', function (res) {
-
- expect(res.result).to.equal('testing123');
- done();
- });
- });
- });
-
- it('registers a child plugin with parent routes vhost prefix and inner register vhost', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ labels: 'test' });
- server.register({ register: internals.plugins.child, options: { routes: { vhost: 'example.net' } } }, { routes: { vhost: 'example.com' } }, function (err) {
-
- expect(err).to.not.exist();
- server.inject({ url: '/test1', headers: { host: 'example.com' } }, function (res) {
-
- expect(res.result).to.equal('testing123');
- done();
- });
- });
- });
-
- it('registers a plugin with routes vhost', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ labels: 'test' });
- server.register(internals.plugins.test1, { routes: { vhost: 'example.com' } }, function (err) {
-
- expect(err).to.not.exist();
- server.inject('/test1', function (res1) {
-
- expect(res1.statusCode).to.equal(404);
-
- server.inject({ url: '/test1', headers: { host: 'example.com' } }, function (res2) {
-
- expect(res2.result).to.equal('testing123');
- done();
- });
- });
- });
- });
-
- it('registers plugins with pre-selected label', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ labels: ['a'] });
- server.connection({ labels: ['b'] });
-
- var server1 = server.connections[0];
- var server2 = server.connections[1];
-
- var test = function (srv, options, next) {
-
- srv.route({
- method: 'GET',
- path: '/',
- handler: function (request, reply) {
-
- return reply('ok');
- }
- });
- return next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- server.register(test, { select: 'a' }, function (err) {
-
- expect(err).to.not.exist();
- server1.inject('/', function (res1) {
-
- expect(res1.statusCode).to.equal(200);
- server2.inject('/', function (res2) {
-
- expect(res2.statusCode).to.equal(404);
- done();
- });
- });
- });
- });
-
- it('registers plugins with pre-selected labels', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ labels: ['a'] });
- server.connection({ labels: ['b'] });
- server.connection({ labels: ['c'] });
-
- var server1 = server.connections[0];
- var server2 = server.connections[1];
- var server3 = server.connections[2];
-
- var test = function (srv, options, next) {
-
- srv.route({
- method: 'GET',
- path: '/',
- handler: function (request, reply) {
-
- return reply('ok');
- }
- });
- srv.expose('super', 'trooper');
- return next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- server.register(test, { select: ['a', 'c'] }, function (err) {
-
- expect(err).to.not.exist();
- expect(server.plugins.test.super).to.equal('trooper');
-
- server1.inject('/', function (res1) {
-
- expect(res1.statusCode).to.equal(200);
- server2.inject('/', function (res2) {
-
- expect(res2.statusCode).to.equal(404);
- server3.inject('/', function (res3) {
-
- expect(res3.statusCode).to.equal(200);
- done();
- });
- });
- });
- });
- });
-
- it('sets multiple dependencies in one statement', function (done) {
-
- var a = function (srv, options, next) {
-
- srv.dependency(['b', 'c']);
- return next();
- };
-
- a.attributes = {
- name: 'a'
- };
-
- var b = function (srv, options, next) {
-
- return next();
- };
-
- b.attributes = {
- name: 'b'
- };
-
- var c = function (srv, options, next) {
-
- return next();
- };
-
- c.attributes = {
- name: 'c'
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.register(b, function (err) {
-
- server.register(c, function (err) {
-
- server.register(a, function (err) {
-
- done();
- });
- });
- });
- });
-
- it('sets multiple dependencies in attributes', function (done) {
-
- var a = function (srv, options, next) {
-
- return next();
- };
-
- a.attributes = {
- name: 'a',
- dependencies: ['b', 'c']
- };
-
- var b = function (srv, options, next) {
-
- return next();
- };
-
- b.attributes = {
- name: 'b'
- };
-
- var c = function (srv, options, next) {
-
- return next();
- };
-
- c.attributes = {
- name: 'c'
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.register(b, function (err) {
-
- server.register(c, function (err) {
-
- server.register(a, function (err) {
-
- done();
- });
- });
- });
- });
-
- it('sets multiple dependencies in multiple statements', function (done) {
-
- var a = function (srv, options, next) {
-
- srv.dependency('b');
- srv.dependency('c');
- return next();
- };
-
- a.attributes = {
- name: 'a'
- };
-
- var b = function (srv, options, next) {
-
- return next();
- };
-
- b.attributes = {
- name: 'b'
- };
-
- var c = function (srv, options, next) {
-
- return next();
- };
-
- c.attributes = {
- name: 'c'
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.register(b, function (err) {
-
- server.register(c, function (err) {
-
- server.register(a, function (err) {
-
- done();
- });
- });
- });
- });
-
- it('sets multiple dependencies in multiple locations', function (done) {
-
- var a = function (srv, options, next) {
-
- srv.dependency('b');
- return next();
- };
-
- a.attributes = {
- name: 'a',
- dependecies: 'c'
- };
-
- var b = function (srv, options, next) {
-
- return next();
- };
-
- b.attributes = {
- name: 'b'
- };
-
- var c = function (srv, options, next) {
-
- return next();
- };
-
- c.attributes = {
- name: 'c'
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.register(b, function (err) {
-
- server.register(c, function (err) {
-
- server.register(a, function (err) {
-
- done();
- });
- });
- });
- });
-
- it('throws when dependencies is an object', function (done) {
-
- var a = function (srv, options, next) {
-
- next();
- };
- a.attributes = {
- name: 'a',
- dependencies: { b: true }
- };
-
- var server = new Hapi.Server();
- server.connection();
-
- expect(function () {
-
- server.register(a, function () {});
- }).to.throw('Invalid dependencies options (must be a string or an array of strings) {\n \"b\": true,\n \u001b[41m\"0\"\u001b[0m\u001b[31m [1]: -- missing --\u001b[0m\n}\n\u001b[31m\n[1] "0" must be a string\u001b[0m');
- done();
- });
-
- it('throws when dependencies contain something else than a string', function (done) {
-
- var a = function (srv, options, next) {
-
- next();
- };
- a.attributes = {
- name: 'a',
- dependencies: [true]
- };
-
- var server = new Hapi.Server();
- server.connection();
-
- expect(function () {
-
- server.register(a, function () {});
- }).to.throw('Invalid dependencies options (must be a string or an array of strings) [\n null\n]\n\u001b[31m\n[1] "0" must be a string\u001b[0m');
- done();
- });
-
- it('exposes server decorations to next register', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- var a = function (srv, options, next) {
-
- srv.decorate('server', 'a', function () {
-
- return 'a';
- });
-
- return next();
- };
-
- a.attributes = {
- name: 'a'
- };
-
- var b = function (srv, options, next) {
-
- return next(typeof srv.a === 'function' ? null : new Error('Missing decoration'));
- };
-
- b.attributes = {
- name: 'b'
- };
-
- server.register([a, b], function (err) {
-
- expect(err).to.not.exist();
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
- done();
- });
- });
- });
-
- it('exposes server decorations to dependency (dependency first)', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- var a = function (srv, options, next) {
-
- srv.decorate('server', 'a', function () {
-
- return 'a';
- });
-
- return next();
- };
-
- a.attributes = {
- name: 'a'
- };
-
- var b = function (srv, options, next) {
-
- srv.dependency('a', function (srv2, next2) {
-
- return next2(typeof srv2.a === 'function' ? null : new Error('Missing decoration'));
- });
-
- return next();
- };
-
- b.attributes = {
- name: 'b'
- };
-
- server.register([a, b], function (err) {
-
- expect(err).to.not.exist();
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
- done();
- });
- });
- });
-
- it('exposes server decorations to dependency (dependency second)', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- var a = function (srv, options, next) {
-
- srv.decorate('server', 'a', function () {
-
- return 'a';
- });
-
- return next();
- };
-
- a.attributes = {
- name: 'a'
- };
-
- var b = function (srv, options, next) {
-
- srv.realm.x = 1;
- srv.dependency('a', function (srv2, next2) {
-
- expect(srv2.realm.x).to.equal(1);
- return next2(typeof srv2.a === 'function' ? null : new Error('Missing decoration'));
- });
-
- return next();
- };
-
- b.attributes = {
- name: 'b'
- };
-
- server.register([b, a], function (err) {
-
- expect(err).to.not.exist();
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
- done();
- });
- });
- });
-
- it('exposes server decorations to next register when nested', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- var a = function (srv, options, next) {
-
- srv.decorate('server', 'a', function () {
-
- return 'a';
- });
-
- return next();
- };
-
- a.attributes = {
- name: 'a'
- };
-
- var b = function (srv, options, next) {
-
- srv.register(a, function (err) {
-
- expect(err).to.not.exist();
- return next(typeof srv.a === 'function' ? null : new Error('Missing decoration'));
- });
- };
-
- b.attributes = {
- name: 'b'
- };
-
- server.register([b], function (err) {
-
- expect(err).to.not.exist();
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
- done();
- });
- });
- });
- });
-
- describe('after()', function () {
-
- it('calls method after plugin', function (done) {
-
- var x = function (srv, options, next) {
-
- srv.expose('a', 'b');
- return next();
- };
-
- x.attributes = {
- name: 'x'
- };
-
- var server = new Hapi.Server();
- server.connection();
-
- expect(server.plugins.x).to.not.exist();
-
- var called = false;
- server.after(function (srv, next) {
-
- expect(srv.plugins.x.a).to.equal('b');
- called = true;
- return next();
- }, 'x');
-
- server.register(x, function (err) {
-
- expect(err).to.not.exist();
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
- expect(called).to.be.true();
- done();
- });
- });
- });
-
- it('calls method before start', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- var called = false;
- server.after(function (srv, next) {
-
- called = true;
- return next();
- });
-
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
- expect(called).to.be.true();
- done();
- });
- });
-
- it('calls method before start even if plugin not registered', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- var called = false;
- server.after(function (srv, next) {
-
- called = true;
- return next();
- }, 'x');
-
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
- expect(called).to.be.true();
- done();
- });
- });
-
- it('fails to start server when after method fails', function (done) {
-
- var test = function (srv, options, next) {
-
- srv.after(function (inner, finish) {
-
- return finish();
- });
-
- srv.after(function (inner, finish) {
-
- return finish(new Error('Not in the mood'));
- });
-
- return next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.register(test, function (err) {
-
- expect(err).to.not.exist();
- server.initialize(function (err) {
-
- expect(err).to.exist();
- done();
- });
- });
- });
- });
-
- describe('auth', function () {
-
- it('adds auth strategy via plugin', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ labels: 'a' });
- server.connection({ labels: 'b' });
- server.route({
- method: 'GET',
- path: '/',
- handler: function (request, reply) {
-
- return reply('authenticated!');
- }
- });
-
- server.register(internals.plugins.auth, function (err) {
-
- expect(err).to.not.exist();
-
- server.inject('/', function (res1) {
-
- expect(res1.statusCode).to.equal(401);
- server.inject({ method: 'GET', url: '/', headers: { authorization: 'Basic ' + (new Buffer('john:12345', 'utf8')).toString('base64') } }, function (res2) {
-
- expect(res2.statusCode).to.equal(200);
- expect(res2.result).to.equal('authenticated!');
- done();
- });
- });
- });
- });
- });
-
- describe('bind()', function () {
-
- it('sets plugin context', function (done) {
-
- var test = function (srv, options, next) {
-
- var bind = {
- value: 'in context',
- suffix: ' throughout'
- };
-
- srv.bind(bind);
-
- srv.route({
- method: 'GET',
- path: '/',
- handler: function (request, reply) {
-
- return reply(this.value);
- }
- });
-
- srv.ext('onPreResponse', function (request, reply) {
-
- return reply(request.response.source + this.suffix);
- });
-
- return next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.register(test, function (err) {
-
- expect(err).to.not.exist();
- server.inject('/', function (res) {
-
- expect(res.result).to.equal('in context throughout');
- done();
- });
- });
- });
- });
-
- describe('cache()', function () {
-
- it('provisions a server cache', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- var cache = server.cache({ segment: 'test', expiresIn: 1000 });
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
-
- cache.set('a', 'going in', 0, function (err) {
-
- cache.get('a', function (err, value, cached, report) {
-
- expect(value).to.equal('going in');
- done();
- });
- });
- });
- });
-
- it('throws when missing segment', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- expect(function () {
-
- server.cache({ expiresIn: 1000 });
- }).to.throw('Missing cache segment name');
- done();
- });
-
- it('provisions a server cache with custom partition', function (done) {
-
- var server = new Hapi.Server({ cache: { engine: CatboxMemory, partition: 'hapi-test-other' } });
- server.connection();
- var cache = server.cache({ segment: 'test', expiresIn: 1000 });
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
-
- cache.set('a', 'going in', 0, function (err) {
-
- cache.get('a', function (err, value, cached, report) {
-
- expect(value).to.equal('going in');
- expect(cache._cache.connection.settings.partition).to.equal('hapi-test-other');
- done();
- });
- });
- });
- });
-
- it('throws when allocating an invalid cache segment', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- expect(function () {
-
- server.cache({ segment: 'a', expiresAt: '12:00', expiresIn: 1000 });
- }).throws();
-
- done();
- });
-
- it('allows allocating a cache segment with empty options', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- expect(function () {
-
- server.cache({ segment: 'a' });
- }).to.not.throw();
-
- done();
- });
-
- it('allows reusing the same cache segment (server)', function (done) {
-
- var server = new Hapi.Server({ cache: { engine: CatboxMemory, shared: true } });
- server.connection();
- expect(function () {
-
- var a1 = server.cache({ segment: 'a', expiresIn: 1000 });
- var a2 = server.cache({ segment: 'a', expiresIn: 1000 });
- }).to.not.throw();
- done();
- });
-
- it('allows reusing the same cache segment (cache)', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- expect(function () {
-
- var a1 = server.cache({ segment: 'a', expiresIn: 1000 });
- var a2 = server.cache({ segment: 'a', expiresIn: 1000, shared: true });
- }).to.not.throw();
- done();
- });
-
- it('uses plugin cache interface', function (done) {
-
- var test = function (srv, options, next) {
-
- var cache = srv.cache({ expiresIn: 10 });
- srv.expose({
- get: function (key, callback) {
-
- cache.get(key, function (err, value, cached, report) {
-
- callback(err, value);
- });
- },
- set: function (key, value, callback) {
-
- cache.set(key, value, 0, callback);
- }
- });
-
- return next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.register(test, function (err) {
-
- expect(err).to.not.exist();
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
-
- server.plugins.test.set('a', '1', function (err) {
-
- expect(err).to.not.exist();
- server.plugins.test.get('a', function (err, value1) {
-
- expect(err).to.not.exist();
- expect(value1).to.equal('1');
- setTimeout(function () {
-
- server.plugins.test.get('a', function (err, value2) {
-
- expect(err).to.not.exist();
- expect(value2).to.equal(null);
- done();
- });
- }, 11);
- });
- });
- });
- });
- });
- });
-
- describe('decorate()', function () {
-
- it('decorates request', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- server.decorate('request', 'getId', function () {
-
- return this.id;
- });
-
- server.route({
- method: 'GET',
- path: '/',
- handler: function (request, reply) {
-
- return reply(request.getId());
- }
- });
-
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.match(/^.*\:.*\:.*\:.*\:.*$/);
- done();
- });
- });
-
- it('decorates reply', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- server.decorate('reply', 'success', function () {
-
- return this.response({ status: 'ok' });
- });
-
- server.route({
- method: 'GET',
- path: '/',
- handler: function (request, reply) {
-
- return reply.success();
- }
- });
-
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(200);
- expect(res.result.status).to.equal('ok');
- done();
- });
- });
-
- it('throws on double reply decoration', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- server.decorate('reply', 'success', function () {
-
- return this.response({ status: 'ok' });
- });
-
- expect(function () {
-
- server.decorate('reply', 'success', function () { });
- }).to.throw('Reply interface decoration already defined: success');
- done();
- });
-
- it('throws on internal conflict', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- expect(function () {
-
- server.decorate('reply', 'redirect', function () { });
- }).to.throw('Cannot override built-in reply interface decoration: redirect');
- done();
- });
-
- it('decorates server', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- server.decorate('server', 'ok', function (path) {
-
- server.route({
- method: 'GET',
- path: path,
- handler: function (request, reply) {
-
- return reply('ok');
- }
- });
- });
-
- server.ok('/');
-
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.equal('ok');
- done();
- });
- });
-
- it('throws on double server decoration', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- server.decorate('server', 'ok', function (path) {
-
- server.route({
- method: 'GET',
- path: path,
- handler: function (request, reply) {
-
- return reply('ok');
- }
- });
- });
-
- expect(function () {
-
- server.decorate('server', 'ok', function () { });
- }).to.throw('Server decoration already defined: ok');
- done();
- });
-
- it('throws on server decoration root conflict', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- expect(function () {
-
- server.decorate('server', 'start', function () { });
- }).to.throw('Cannot override the built-in server interface method: start');
- done();
- });
-
- it('throws on server decoration plugin conflict', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- expect(function () {
-
- server.decorate('server', 'select', function () { });
- }).to.throw('Cannot override the built-in server interface method: select');
- done();
- });
-
- it('throws on invalid decoration name', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- expect(function () {
-
- server.decorate('server', '_special', function () { });
- }).to.throw('Property name cannot begin with an underscore: _special');
- done();
- });
- });
-
- describe('dependency()', function () {
-
- it('fails to register single plugin with dependencies', function (done) {
-
- var test = function (srv, options, next) {
-
- srv.dependency('none');
- return next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.register(test, function (err) {
-
- expect(function () {
-
- server.initialize(Hoek.ignore);
- }).to.throw('Plugin test missing dependency none in connection: ' + server.info.uri);
- done();
- });
- });
-
- it('fails to register single plugin with dependencies (attributes)', function (done) {
-
- var test = function (srv, options, next) {
-
- return next();
- };
-
- test.attributes = {
- name: 'test',
- dependencies: 'none'
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.register(test, function (err) {
-
- expect(function () {
-
- server.initialize(Hoek.ignore);
- }).to.throw('Plugin test missing dependency none in connection: ' + server.info.uri);
- done();
- });
- });
-
- it('fails to register multiple plugins with dependencies', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ port: 80, host: 'localhost' });
- server.register([internals.plugins.deps1, internals.plugins.deps3], function (err) {
-
- expect(function () {
-
- server.initialize(Hoek.ignore);
- }).to.throw('Plugin deps1 missing dependency deps2 in connection: http://localhost:80');
- done();
- });
- });
-
- it('recognizes dependencies from peer plugins', function (done) {
-
- var a = function (srv, options, next) {
-
- srv.register(b, next);
- };
-
- a.attributes = {
- name: 'a'
- };
-
- var b = function (srv, options, next) {
-
- return next();
- };
-
- b.attributes = {
- name: 'b'
- };
-
- var c = function (srv, options, next) {
-
- srv.dependency('b');
- return next();
- };
-
- c.attributes = {
- name: 'c'
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.register([a, c], function (err) {
-
- expect(err).to.not.exist();
- done();
- });
- });
-
- it('errors when missing inner dependencies', function (done) {
-
- var a = function (srv, options, next) {
-
- srv.register(b, next);
- };
-
- a.attributes = {
- name: 'a'
- };
-
- var b = function (srv, options, next) {
-
- srv.dependency('c');
- return next();
- };
-
- b.attributes = {
- name: 'b'
- };
-
- var server = new Hapi.Server();
- server.connection({ port: 80, host: 'localhost' });
- server.register(a, function (err) {
-
- expect(function () {
-
- server.initialize(Hoek.ignore);
- }).to.throw('Plugin b missing dependency c in connection: http://localhost:80');
- done();
- });
- });
-
- it('errors when missing inner dependencies (attributes)', function (done) {
-
- var a = function (srv, options, next) {
-
- srv.register(b, next);
- };
-
- a.attributes = {
- name: 'a'
- };
-
- var b = function (srv, options, next) {
-
- return next();
- };
-
- b.attributes = {
- name: 'b',
- dependencies: 'c'
- };
-
- var server = new Hapi.Server();
- server.connection({ port: 80, host: 'localhost' });
- server.register(a, function (err) {
-
- expect(function () {
-
- server.initialize(Hoek.ignore);
- }).to.throw('Plugin b missing dependency c in connection: http://localhost:80');
- done();
- });
- });
- });
-
- describe('events', function () {
-
- it('plugin event handlers receive more than 2 arguments when they exist', function (done) {
-
- var test = function (srv, options, next) {
-
- srv.once('request-internal', function () {
-
- expect(arguments).to.have.length(3);
- done();
- });
-
- return next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.register(test, function (err) {
-
- expect(err).to.not.exist();
- server.inject({ url: '/' }, function () { });
- });
- });
-
- it('listens to events on selected connections', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ labels: ['a'] });
- server.connection({ labels: ['b'] });
- server.connection({ labels: ['c'] });
-
- var server1 = server.connections[0];
- var server2 = server.connections[1];
- var server3 = server.connections[2];
-
- var counter = 0;
- var test = function (srv, options, next) {
-
- srv.select(['a', 'b']).on('test', function () {
-
- ++counter;
- });
-
- srv.select(['a']).on('start', function () {
-
- ++counter;
- });
-
- return next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- server.register(test, function (err) {
-
- expect(err).to.not.exist();
- server1.emit('test');
- server2.emit('test');
- server3.emit('test');
-
- server.start(function (err) {
-
- expect(err).to.not.exist();
-
- server.stop(function (err) {
-
- expect(err).to.not.exist();
- expect(counter).to.equal(3);
- done();
- });
- });
- });
- });
- });
-
- describe('expose()', function () {
-
- it('exposes an api', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ labels: ['s1', 'a', 'b'] });
- server.connection({ labels: ['s2', 'a', 'test'] });
- server.connection({ labels: ['s3', 'a', 'b', 'd', 'cache'] });
- server.connection({ labels: ['s4', 'b', 'test', 'cache'] });
-
- server.register(internals.plugins.test1, function (err) {
-
- expect(err).to.not.exist();
-
- expect(server.connections[0]._router.routes.get).to.not.exist();
- expect(internals.routesList(server, 's2')).to.deep.equal(['/test1']);
- expect(server.connections[2]._router.routes.get).to.not.exist();
- expect(internals.routesList(server, 's4')).to.deep.equal(['/test1']);
-
- expect(server.plugins.test1.add(1, 3)).to.equal(4);
- expect(server.plugins.test1.glue('1', '3')).to.equal('13');
-
- done();
- });
- });
- });
-
- describe('ext()', function () {
-
- it('extends onRequest point', function (done) {
-
- var test = function (srv, options, next) {
-
- srv.route({
- method: 'GET',
- path: '/b',
- handler: function (request, reply) {
-
- return reply('b');
- }
- });
-
- srv.ext('onRequest', function (request, reply) {
-
- request.setUrl('/b');
- return reply.continue();
- });
-
- return next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.register(test, function (err) {
-
- expect(err).to.not.exist();
- expect(internals.routesList(server)).to.deep.equal(['/b']);
-
- server.inject('/a', function (res) {
-
- expect(res.result).to.equal('b');
- done();
- });
- });
- });
-
- it('adds multiple ext functions with simple dependencies', function (done) {
-
- var server = new Hapi.Server();
- server.connection({ labels: ['a', 'b', '0'] });
- server.connection({ labels: ['a', 'c', '1'] });
- server.connection({ labels: ['c', 'b', '2'] });
-
- var handler = function (request, reply) {
-
- return reply(request.app.deps);
- };
-
- server.select('0').route({ method: 'GET', path: '/', handler: handler });
- server.select('1').route({ method: 'GET', path: '/', handler: handler });
- server.select('2').route({ method: 'GET', path: '/', handler: handler });
-
- server.register([internals.plugins.deps1, internals.plugins.deps2, internals.plugins.deps3], function (err) {
-
- expect(err).to.not.exist();
-
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
- expect(server.plugins.deps1.breaking).to.equal('bad');
-
- server.connections[0].inject('/', function (res1) {
-
- expect(res1.result).to.equal('|2|1|');
-
- server.connections[1].inject('/', function (res2) {
-
- expect(res2.result).to.equal('|3|1|');
-
- server.connections[2].inject('/', function (res3) {
-
- expect(res3.result).to.equal('|3|2|');
- done();
- });
- });
- });
- });
- });
- });
-
- it('adds multiple ext functions with complex dependencies', function (done) {
-
- // Generate a plugin with a specific index and ext dependencies.
-
- var pluginCurrier = function (num, deps) {
-
- var plugin = function (server, options, next) {
-
- server.ext('onRequest', function (request, reply) {
-
- request.app.complexDeps = request.app.complexDeps || '|';
- request.app.complexDeps += num + '|';
- return reply.continue();
- }, deps);
-
- next();
- };
-
- plugin.attributes = {
- name: 'deps' + num
- };
-
- return plugin;
- };
-
- var handler = function (request, reply) {
-
- return reply(request.app.complexDeps);
- };
-
- var server = new Hapi.Server();
- server.connection();
-
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.register([
- pluginCurrier(1, { after: 'deps2' }),
- pluginCurrier(2),
- pluginCurrier(3, { before: ['deps1', 'deps2'] })
- ], function (err) {
-
- expect(err).to.not.exist();
-
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
-
- server.inject('/', function (res) {
-
- expect(res.result).to.equal('|3|2|1|');
- done();
- });
- });
- });
- });
-
- it('throws when adding ext without connections', function (done) {
-
- var server = new Hapi.Server();
- expect(function () {
-
- server.ext('onRequest', function () { });
- }).to.throw('Cannot add ext without a connection');
-
- done();
- });
-
- it('binds server ext to context (options)', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- var bind = {
- state: false
- };
-
- server.ext('onPreStart', function (srv, next) {
-
- this.state = true;
- return next();
- }, { bind: bind });
-
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
- expect(bind.state).to.be.true();
- done();
- });
- });
-
- it('binds server ext to context (realm)', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- var bind = {
- state: false
- };
-
- server.bind(bind);
- server.ext('onPreStart', function (srv, next) {
-
- this.state = true;
- return next();
- });
-
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
- expect(bind.state).to.be.true();
- done();
- });
- });
-
- it('extends server actions', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- var result = '';
- server.ext('onPreStart', function (srv, next) {
-
- result += '1';
- return next();
- });
-
- server.ext('onPostStart', function (srv, next) {
-
- result += '2';
- return next();
- });
-
- server.ext('onPreStop', function (srv, next) {
-
- result += '3';
- return next();
- });
-
- server.ext('onPreStop', function (srv, next) {
-
- result += '4';
- return next();
- });
-
- server.start(function (err) {
-
- expect(err).to.not.exist();
- expect(result).to.equal('12');
-
- server.stop(function (err) {
-
- expect(err).to.not.exist();
- expect(result).to.equal('1234');
- done();
- });
- });
- });
- });
-
- describe('handler()', function () {
-
- it('add new handler', function (done) {
-
- var test = function (srv, options1, next) {
-
- srv.handler('bar', function (route, options2) {
-
- return function (request, reply) {
-
- return reply('success');
- };
- });
-
- return next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.register(test, function (err) {
-
- expect(err).to.not.exist();
- server.route({
- method: 'GET',
- path: '/',
- handler: {
- bar: {}
- }
- });
-
- server.inject('/', function (res) {
-
- expect(res.payload).to.equal('success');
- done();
- });
- });
- });
-
- it('errors on duplicate handler', function (done) {
-
- var server = new Hapi.Server();
- server.register(Inert, Hoek.ignore);
- server.connection();
-
- expect(function () {
-
- server.handler('file', function () { });
- }).to.throw('Handler name already exists: file');
- done();
- });
-
- it('errors on unknown handler', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- expect(function () {
-
- server.route({ method: 'GET', path: '/', handler: { test: {} } });
- }).to.throw('Unknown handler: test');
- done();
- });
-
- it('errors on non-string name', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- expect(function () {
-
- server.handler();
- }).to.throw('Invalid handler name');
- done();
- });
-
- it('errors on non-function handler', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- expect(function () {
-
- server.handler('foo', 'bar');
- }).to.throw('Handler must be a function: foo');
- done();
- });
- });
-
- describe('log()', { parallel: false }, function () {
-
- it('emits a log event', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- var count = 0;
- server.once('log', function (event) {
-
- ++count;
- expect(event.data).to.equal('log event 1');
- });
-
- server.once('log', function (event) {
-
- ++count;
- expect(event.data).to.equal('log event 1');
- });
-
- server.log('1', 'log event 1', Date.now());
-
- server.once('log', function (event) {
-
- ++count;
- expect(event.data).to.equal('log event 2');
- });
-
- server.log(['2'], 'log event 2', new Date(Date.now()));
-
- expect(count).to.equal(3);
- done();
- });
-
- it('emits a log event and print to console', { parallel: false }, function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- server.once('log', function (event) {
-
- expect(event.data).to.equal('log event 1');
- });
-
- var orig = console.error;
- console.error = function () {
-
- console.error = orig;
- expect(arguments[0]).to.equal('Debug:');
- expect(arguments[1]).to.equal('internal, implementation, error');
-
- done();
- };
-
- server.log(['internal', 'implementation', 'error'], 'log event 1');
- });
-
- it('outputs log data to debug console', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- var orig = console.error;
- console.error = function () {
-
- console.error = orig;
- expect(arguments[0]).to.equal('Debug:');
- expect(arguments[1]).to.equal('implementation');
- expect(arguments[2]).to.equal('\n {"data":1}');
- done();
- };
-
- server.log(['implementation'], { data: 1 });
- });
-
- it('outputs log error data to debug console', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- var orig = console.error;
- console.error = function () {
-
- console.error = orig;
- expect(arguments[0]).to.equal('Debug:');
- expect(arguments[1]).to.equal('implementation');
- expect(arguments[2]).to.contain('\n Error: test\n at');
- done();
- };
-
- server.log(['implementation'], new Error('test'));
- });
-
- it('outputs log data to debug console without data', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- var orig = console.error;
- console.error = function () {
-
- console.error = orig;
- expect(arguments[0]).to.equal('Debug:');
- expect(arguments[1]).to.equal('implementation');
- expect(arguments[2]).to.equal('');
- done();
- };
-
- server.log(['implementation']);
- });
-
- it('does not output events when debug disabled', function (done) {
-
- var server = new Hapi.Server({ debug: false });
- server.connection();
-
- var i = 0;
- var orig = console.error;
- console.error = function () {
-
- ++i;
- };
-
- server.log(['implementation']);
- console.error('nothing');
- expect(i).to.equal(1);
- console.error = orig;
- done();
- });
-
- it('does not output events when debug.log disabled', function (done) {
-
- var server = new Hapi.Server({ debug: { log: false } });
- server.connection();
-
- var i = 0;
- var orig = console.error;
- console.error = function () {
-
- ++i;
- };
-
- server.log(['implementation']);
- console.error('nothing');
- expect(i).to.equal(1);
- console.error = orig;
- done();
- });
-
- it('does not output non-implementation events by default', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- var i = 0;
- var orig = console.error;
- console.error = function () {
-
- ++i;
- };
-
- server.log(['xyz']);
- console.error('nothing');
- expect(i).to.equal(1);
- console.error = orig;
- done();
- });
-
- it('emits server log events once', function (done) {
-
- var pc = 0;
- var test = function (srv, options, next) {
-
- srv.on('log', function (event, tags) {
-
- ++pc;
- });
-
- next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- var server = new Hapi.Server();
- server.connection();
-
- var sc = 0;
- server.on('log', function (event, tags) {
-
- ++sc;
- });
-
- server.register(test, function (err) {
-
- expect(err).to.not.exist();
- server.log('test');
- expect(sc).to.equal(1);
- expect(pc).to.equal(1);
- done();
- });
- });
- });
-
- describe('lookup()', function () {
-
- it('returns route based on id', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- server.route({
- method: 'GET',
- path: '/',
- config: {
- handler: function (request, reply) {
-
- return reply();
- },
- id: 'root',
- app: { test: 123 }
- }
- });
-
- var root = server.lookup('root');
- expect(root.path).to.equal('/');
- expect(root.settings.app.test).to.equal(123);
- done();
- });
-
- it('returns null on unknown route', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- var root = server.lookup('root');
- expect(root).to.be.null();
- done();
- });
-
- it('throws on missing id', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- expect(function () {
-
- server.lookup();
- }).to.throw('Invalid route id: ');
- done();
- });
- });
-
- describe('match()', function () {
-
- it('returns route based on path', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- server.route({
- method: 'GET',
- path: '/',
- config: {
- handler: function (request, reply) {
-
- return reply();
- },
- id: 'root'
- }
- });
-
- server.route({
- method: 'GET',
- path: '/abc',
- config: {
- handler: function (request, reply) {
-
- return reply();
- },
- id: 'abc'
- }
- });
-
- server.route({
- method: 'POST',
- path: '/abc',
- config: {
- handler: function (request, reply) {
-
- return reply();
- },
- id: 'post'
- }
- });
-
- server.route({
- method: 'GET',
- path: '/{p}/{x}',
- config: {
- handler: function (request, reply) {
-
- return reply();
- },
- id: 'params'
- }
- });
-
- server.route({
- method: 'GET',
- path: '/abc',
- vhost: 'example.com',
- config: {
- handler: function (request, reply) {
-
- return reply();
- },
- id: 'vhost'
- }
- });
-
- expect(server.match('GET', '/').settings.id).to.equal('root');
- expect(server.match('GET', '/none')).to.equal(null);
- expect(server.match('GET', '/abc').settings.id).to.equal('abc');
- expect(server.match('get', '/').settings.id).to.equal('root');
- expect(server.match('post', '/abc').settings.id).to.equal('post');
- expect(server.match('get', '/a/b').settings.id).to.equal('params');
- expect(server.match('GET', '/abc', 'example.com').settings.id).to.equal('vhost');
- done();
- });
-
- it('throws on missing method', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- expect(function () {
-
- server.match();
- }).to.throw('Invalid method: ');
- done();
- });
-
- it('throws on invalid method', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- expect(function () {
-
- server.match(5);
- }).to.throw('Invalid method: 5');
- done();
- });
-
- it('throws on missing path', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- expect(function () {
-
- server.match('get');
- }).to.throw('Invalid path: ');
- done();
- });
-
- it('throws on invalid path type', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- expect(function () {
-
- server.match('get', 5);
- }).to.throw('Invalid path: 5');
- done();
- });
-
- it('throws on invalid path prefix', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- expect(function () {
-
- server.match('get', '5');
- }).to.throw('Invalid path: 5');
- done();
- });
-
- it('throws on invalid path', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- server.route({
- method: 'GET',
- path: '/{p}',
- config: {
- handler: function (request, reply) {
-
- return reply();
- }
- }
- });
-
- expect(function () {
-
- server.match('GET', '/%p');
- }).to.throw('Invalid path: /%p');
- done();
- });
-
- it('throws on invalid host type', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
- expect(function () {
-
- server.match('get', '/a', 5);
- }).to.throw('Invalid host: 5');
- done();
- });
- });
-
- describe('method()', function () {
-
- it('adds server method using arguments', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- var test = function (srv, options, next) {
-
- srv.method('log', function (methodNext) {
-
- return methodNext(null);
- });
- return next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- server.register(test, function (err) {
-
- expect(err).to.not.exist();
- done();
- });
- });
-
- it('adds server method with plugin bind', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- var test = function (srv, options, next) {
-
- srv.bind({ x: 1 });
- srv.method('log', function (methodNext) {
-
- return methodNext(null, this.x);
- });
- return next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- server.register(test, function (err) {
-
- expect(err).to.not.exist();
- server.methods.log(function (err, result) {
-
- expect(result).to.equal(1);
- done();
- });
- });
- });
-
- it('adds server method with method bind', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- var test = function (srv, options, next) {
-
- srv.method('log', function (methodNext) {
-
- return methodNext(null, this.x);
- }, { bind: { x: 2 } });
- return next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- server.register(test, function (err) {
-
- expect(err).to.not.exist();
- server.methods.log(function (err, result) {
-
- expect(result).to.equal(2);
- done();
- });
- });
- });
-
- it('adds server method with method and ext bind', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- var test = function (srv, options, next) {
-
- srv.bind({ x: 1 });
- srv.method('log', function (methodNext) {
-
- return methodNext(null, this.x);
- }, { bind: { x: 2 } });
- return next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- server.register(test, function (err) {
-
- expect(err).to.not.exist();
- server.methods.log(function (err, result) {
-
- expect(result).to.equal(2);
- done();
- });
- });
- });
- });
-
- describe('path()', function () {
-
- it('sets local path for directory route handler', function (done) {
-
- var test = function (srv, options, next) {
-
- srv.path(Path.join(__dirname, '..'));
-
- srv.route({
- method: 'GET',
- path: '/handler/{file*}',
- handler: {
- directory: {
- path: './'
- }
- }
- });
-
- return next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- var server = new Hapi.Server();
- server.register(Inert, Hoek.ignore);
- server.connection({ routes: { files: { relativeTo: __dirname } } });
- server.register(test, function (err) {
-
- expect(err).to.not.exist();
- server.inject('/handler/package.json', function (res) {
-
- expect(res.statusCode).to.equal(200);
- done();
- });
- });
- });
-
- it('throws when plugin sets undefined path', function (done) {
-
- var test = function (srv, options, next) {
-
- srv.path();
- return next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- var server = new Hapi.Server();
- server.connection();
- expect(function () {
-
- server.register(test, function (err) { });
- }).to.throw('relativeTo must be a non-empty string');
- done();
- });
- });
-
- describe('render()', function () {
-
- it('renders view', function (done) {
-
- var server = new Hapi.Server();
- server.register(Vision, Hoek.ignore);
- server.connection();
- server.views({
- engines: { html: Handlebars },
- path: __dirname + '/templates'
- });
-
- server.render('test', { title: 'test', message: 'Hapi' }, function (err, rendered, config) {
-
- expect(rendered).to.exist();
- expect(rendered).to.contain('Hapi');
- done();
- });
- });
- });
-
- describe('state()', function () {
-
- it('throws when adding state without connections', function (done) {
-
- var server = new Hapi.Server();
- expect(function () {
-
- server.state('sid', { encoding: 'base64' });
- }).to.throw('Cannot add state without a connection');
-
- done();
- });
- });
-
- describe('views()', function () {
-
- it('requires plugin with views', function (done) {
-
- var test = function (srv, options, next) {
-
- srv.path(__dirname);
-
- var views = {
- engines: { 'html': Handlebars },
- path: './templates/plugin'
- };
-
- srv.views(views);
- if (Object.keys(views).length !== 2) {
- return next(new Error('plugin.view() modified options'));
- }
-
- srv.route([
- {
- path: '/view', method: 'GET', handler: function (request, reply) {
-
- return reply.view('test', { message: options.message });
- }
- },
- {
- path: '/file', method: 'GET', handler: { file: './templates/plugin/test.html' }
- }
- ]);
-
- srv.ext('onRequest', function (request, reply) {
-
- if (request.path === '/ext') {
- return reply.view('test', { message: 'grabbed' });
- }
-
- return reply.continue();
- });
-
- return next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- var server = new Hapi.Server();
- server.register([Inert, Vision], Hoek.ignore);
- server.connection();
- server.register({ register: test, options: { message: 'viewing it' } }, function (err) {
-
- expect(err).to.not.exist();
- server.inject('/view', function (res1) {
-
- expect(res1.result).to.equal('viewing it
');
-
- server.inject('/file', function (res2) {
-
- expect(res2.result).to.equal('{{message}}
');
-
- server.inject('/ext', function (res3) {
-
- expect(res3.result).to.equal('grabbed
');
- done();
- });
- });
- });
- });
- });
- });
-});
-
-
-internals.routesList = function (server, label) {
-
- var tables = server.select(label || []).table();
-
- var list = [];
- for (var c = 0, cl = tables.length; c < cl; ++c) {
- var routes = tables[c].table;
- for (var i = 0, il = routes.length; i < il; ++i) {
- var route = routes[i];
- if (route.method === 'get') {
- list.push(route.path);
- }
- }
- }
-
- return list;
-};
-
-
-internals.plugins = {
- auth: function (server, options, next) {
-
- server.auth.scheme('basic', function (srv, authOptions) {
-
- var settings = Hoek.clone(authOptions);
-
- var scheme = {
- authenticate: function (request, reply) {
-
- var req = request.raw.req;
- var authorization = req.headers.authorization;
- if (!authorization) {
- return reply(Boom.unauthorized(null, 'Basic'));
- }
-
- var parts = authorization.split(/\s+/);
-
- if (parts[0] &&
- parts[0].toLowerCase() !== 'basic') {
-
- return reply(Boom.unauthorized(null, 'Basic'));
- }
-
- if (parts.length !== 2) {
- return reply(Boom.badRequest('Bad HTTP authentication header format', 'Basic'));
- }
-
- var credentialsParts = new Buffer(parts[1], 'base64').toString().split(':');
- if (credentialsParts.length !== 2) {
- return reply(Boom.badRequest('Bad header internal syntax', 'Basic'));
- }
-
- var username = credentialsParts[0];
- var password = credentialsParts[1];
-
- settings.validateFunc(username, password, function (err, isValid, credentials) {
-
- if (!isValid) {
- return reply(Boom.unauthorized('Bad username or password', 'Basic'), { credentials: credentials });
- }
-
- return reply.continue({ credentials: credentials });
- });
- }
- };
-
- return scheme;
- });
-
- var loadUser = function (username, password, callback) {
-
- if (username === 'john') {
- return callback(null, password === '12345', { user: 'john' });
- }
-
- return callback(null, false);
- };
-
- server.auth.strategy('basic', 'basic', 'required', { validateFunc: loadUser });
-
- server.auth.scheme('special', function () {
-
- return { authenticate: function () { } };
- });
-
- server.auth.strategy('special', 'special', {});
-
- return next();
- },
- child: function (server, options, next) {
-
- if (options.routes) {
- return server.register(internals.plugins.test1, options, next);
- }
-
- return server.register(internals.plugins.test1, next);
- },
- deps1: function (server, options, next) {
-
- server.dependency('deps2', function (srv, nxt) {
-
- srv.expose('breaking', srv.plugins.deps2.breaking);
- return nxt();
- });
-
- var selection = server.select('a');
- if (selection.connections.length) {
- selection.ext('onRequest', function (request, reply) {
-
- request.app.deps = request.app.deps || '|';
- request.app.deps += '1|';
- return reply.continue();
- }, { after: 'deps3' });
- }
-
- return next();
- },
- deps2: function (server, options, next) {
-
- var selection = server.select('b');
- if (selection.connections.length) {
- selection.ext('onRequest', function (request, reply) {
-
- request.app.deps = request.app.deps || '|';
- request.app.deps += '2|';
- return reply.continue();
- }, { after: 'deps3', before: 'deps1' });
- }
-
- server.expose('breaking', 'bad');
-
- return next();
- },
- deps3: function (server, options, next) {
-
- var selection = server.select('c');
- if (selection.connections.length) {
- selection.ext('onRequest', function (request, reply) {
-
- request.app.deps = request.app.deps || '|';
- request.app.deps += '3|';
- return reply.continue();
- });
- }
-
- return next();
- },
- test1: function (server, options, next) {
-
- var handler = function (request, reply) {
-
- return reply('testing123' + ((server.settings.app && server.settings.app.my) || ''));
- };
-
- server.select('test').route({ path: '/test1', method: 'GET', handler: handler });
-
- server.expose({
- add: function (a, b) {
-
- return a + b;
- }
- });
-
- server.expose('glue', function (a, b) {
-
- return a + b;
- });
-
- server.expose('prefix', server.realm.modifiers.route.prefix);
-
- return next();
- },
- test2: function (server, options, next) {
-
- server.route({
- path: '/test2',
- method: 'GET',
- handler: function (request, reply) {
-
- return reply('testing123');
- }
- });
- server.log('test', 'abc');
- return next();
- }
-};
-
-
-internals.plugins.auth.attributes = {
- name: 'auth'
-};
-
-
-internals.plugins.child.attributes = {
- name: 'child'
-};
-
-
-internals.plugins.deps1.attributes = {
- name: 'deps1'
-};
-
-
-internals.plugins.deps2.attributes = {
- name: 'deps2'
-};
-
-
-internals.plugins.deps3.attributes = {
- name: 'deps3'
-};
-
-
-internals.plugins.test1.attributes = {
- name: 'test1',
- version: '1.0.0'
-};
-
-
-internals.plugins.test2.attributes = {
- pkg: {
- name: 'test2',
- version: '1.0.0'
- }
-};
diff --git a/test/protect.js b/test/protect.js
deleted file mode 100755
index 6c7f507ec..000000000
--- a/test/protect.js
+++ /dev/null
@@ -1,157 +0,0 @@
-// Load modules
-
-var Events = require('events');
-var Code = require('code');
-var Hapi = require('..');
-var Hoek = require('hoek');
-var Lab = require('lab');
-
-
-// Declare internals
-
-var internals = {};
-
-
-// Test shortcuts
-
-var lab = exports.lab = Lab.script();
-var describe = lab.describe;
-var it = lab.it;
-var expect = Code.expect;
-
-
-describe('Protect', function () {
-
- it('catches error when handler throws after reply() is called', function (done) {
-
- var server = new Hapi.Server({ debug: false });
- server.connection();
-
- var handler = function (request, reply) {
-
- reply('ok');
- process.nextTick(function () {
-
- throw new Error('should not leave domain');
- });
- };
-
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(200);
- done();
- });
- });
-
- it('catches error when handler throws twice after reply() is called', function (done) {
-
- var server = new Hapi.Server({ debug: false });
- server.connection();
-
- var handler = function (request, reply) {
-
- reply('ok');
-
- process.nextTick(function () {
-
- throw new Error('should not leave domain 1');
- });
-
- process.nextTick(function () {
-
- throw new Error('should not leave domain 2');
- });
- };
-
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(200);
- done();
- });
- });
-
- it('catches errors thrown during request handling in non-request domain', function (done) {
-
- var Client = function () {
-
- Events.EventEmitter.call(this);
- };
-
- Hoek.inherits(Client, Events.EventEmitter);
-
- var test = function (srv, options, next) {
-
- srv.after(function (plugin, afterNext) {
-
- var client = new Client(); // Created in the global domain
- plugin.bind({ client: client });
- afterNext();
- });
-
- srv.route({
- method: 'GET',
- path: '/',
- handler: function (request, reply) {
-
- this.client.on('event', request.domain.bind(function () {
-
- throw new Error('boom'); // Caught by the global domain by default, not request domain
- }));
-
- this.client.emit('event');
- }
- });
-
- return next();
- };
-
- test.attributes = {
- name: 'test'
- };
-
- var server = new Hapi.Server({ debug: false });
- server.connection();
- server.register(test, function (err) {
-
- expect(err).to.not.exist();
-
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
- server.inject('/', function (res) {
-
- done();
- });
- });
- });
- });
-
- it('logs to console after request completed', function (done) {
-
- var handler = function (request, reply) {
-
- reply('ok');
- setTimeout(function () {
-
- throw new Error('After done');
- }, 10);
- };
-
- var server = new Hapi.Server({ debug: false });
- server.connection();
-
- server.on('log', function (event, tags) {
-
- expect(tags.implementation).to.exist();
- done();
- });
-
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(200);
- });
- });
-});
diff --git a/test/reply.js b/test/reply.js
deleted file mode 100755
index 92188e3c1..000000000
--- a/test/reply.js
+++ /dev/null
@@ -1,626 +0,0 @@
-// Load modules
-
-var Http = require('http');
-var Stream = require('stream');
-var Bluebird = require('bluebird');
-var Boom = require('boom');
-var Code = require('code');
-var Hapi = require('..');
-var Hoek = require('hoek');
-var Lab = require('lab');
-
-
-// Declare internals
-
-var internals = {};
-
-
-// Test shortcuts
-
-var lab = exports.lab = Lab.script();
-var describe = lab.describe;
-var it = lab.it;
-var expect = Code.expect;
-
-
-describe('Reply', function () {
-
- it('throws when reply called twice', function (done) {
-
- var handler = function (request, reply) {
-
- reply('ok'); return reply('not ok');
- };
-
- var server = new Hapi.Server({ debug: false });
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(500);
- done();
- });
- });
-
- it('redirects from handler', function (done) {
-
- var handler = function (request, reply) {
-
- return reply.redirect('/elsewhere');
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(302);
- expect(res.headers.location).to.equal('/elsewhere');
- done();
- });
- });
-
- describe('interface()', function () {
-
- it('uses reply(null, result) for result', function (done) {
-
- var handler = function (request, reply) {
-
- return reply(null, 'steve');
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.equal('steve');
- done();
- });
- });
-
- it('uses reply(null, err) for err', function (done) {
-
- var handler = function (request, reply) {
-
- return reply(null, Boom.badRequest());
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(400);
- done();
- });
- });
-
- it('ignores result when err provided in reply(err, result)', function (done) {
-
- var handler = function (request, reply) {
-
- return reply(Boom.badRequest(), 'steve');
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(400);
- done();
- });
- });
- });
-
- describe('response()', function () {
-
- it('returns null', function (done) {
-
- var handler = function (request, reply) {
-
- return reply(null, null);
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.equal(null);
- expect(res.payload).to.equal('');
- expect(res.headers['content-type']).to.not.exist();
- done();
- });
- });
-
- it('returns a buffer reply', function (done) {
-
- var handler = function (request, reply) {
-
- return reply(new Buffer('Tada1')).code(299);
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
-
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(299);
- expect(res.result).to.equal('Tada1');
- expect(res.headers['content-type']).to.equal('application/octet-stream');
- done();
- });
- });
-
- it('returns an object response', function (done) {
-
- var handler = function (request, reply) {
-
- return reply({ a: 1, b: 2 });
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject('/', function (res) {
-
- expect(res.payload).to.equal('{\"a\":1,\"b\":2}');
- expect(res.headers['content-length']).to.equal(13);
- done();
- });
- });
-
- it('returns false', function (done) {
-
- var handler = function (request, reply) {
-
- return reply(false);
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject('/', function (res) {
-
- expect(res.payload).to.equal('false');
- done();
- });
- });
-
- it('returns an error reply', function (done) {
-
- var handler = function (request, reply) {
-
- return reply(new Error('boom'));
- };
-
- var server = new Hapi.Server({ debug: false });
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(500);
- expect(res.result).to.exist();
- done();
- });
- });
-
- it('returns an empty reply', function (done) {
-
- var handler = function (request, reply) {
-
- return reply().code(299);
- };
-
- var server = new Hapi.Server();
- server.connection({ routes: { cors: { credentials: true } } });
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(299);
- expect(res.result).to.equal(null);
- expect(res.headers['access-control-allow-credentials']).to.equal('true');
- done();
- });
- });
-
- it('returns a stream reply', function (done) {
-
- var TestStream = function () {
-
- Stream.Readable.call(this);
- };
-
- Hoek.inherits(TestStream, Stream.Readable);
-
- TestStream.prototype._read = function (size) {
-
- if (this.isDone) {
- return;
- }
- this.isDone = true;
-
- this.push('x');
- this.push('y');
- this.push(null);
- };
-
- var handler = function (request, reply) {
-
- return reply(new TestStream()).ttl(2000);
- };
-
- var server = new Hapi.Server();
- server.connection({ routes: { cors: { origin: ['test.example.com'] } } });
- server.route({ method: 'GET', path: '/stream', config: { handler: handler, cache: { expiresIn: 9999 } } });
-
- server.inject('/stream', function (res1) {
-
- expect(res1.result).to.equal('xy');
- expect(res1.statusCode).to.equal(200);
- expect(res1.headers['cache-control']).to.equal('max-age=2, must-revalidate');
- expect(res1.headers['access-control-allow-origin']).to.equal('test.example.com');
-
- server.inject({ method: 'HEAD', url: '/stream' }, function (res2) {
-
- expect(res2.result).to.equal('');
- expect(res2.statusCode).to.equal(200);
- expect(res2.headers['cache-control']).to.equal('max-age=2, must-revalidate');
- expect(res2.headers['access-control-allow-origin']).to.equal('test.example.com');
- done();
- });
- });
- });
-
- it('errors on non-readable stream reply', function (done) {
-
- var streamHandler = function (request, reply) {
-
- var stream = new Stream();
- stream.writable = true;
-
- reply(stream);
- };
-
- var writableHandler = function (request, reply) {
-
- var writable = new Stream.Writable();
- writable._write = function () {};
-
- reply(writable);
- };
-
- var server = new Hapi.Server({ debug: false });
- server.connection();
- server.route({ method: 'GET', path: '/stream', handler: streamHandler });
- server.route({ method: 'GET', path: '/writable', handler: writableHandler });
-
- var requestError;
- server.on('request-error', function (request, err) {
-
- requestError = err;
- });
-
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
-
- server.inject('/stream', function (res1) {
-
- expect(res1.statusCode).to.equal(500);
- expect(requestError).to.exist();
- expect(requestError.message).to.equal('Stream must have a streams2 readable interface');
-
- requestError = undefined;
- server.inject('/writable', function (res2) {
-
- expect(res2.statusCode).to.equal(500);
- expect(requestError).to.exist();
- expect(requestError.message).to.equal('Stream must have a streams2 readable interface');
- done();
- });
- });
- });
- });
-
- it('errors on an http client stream reply', function (done) {
-
- var handler = function (request, reply) {
-
- reply('just a string');
- };
-
- var streamHandler = function (request, reply) {
-
- reply(Http.get(request.server.info + '/'));
- };
-
- var server = new Hapi.Server({ debug: false });
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
- server.route({ method: 'GET', path: '/stream', handler: streamHandler });
-
- server.initialize(function (err) {
-
- expect(err).to.not.exist();
-
- server.inject('/stream', function (res) {
-
- expect(res.statusCode).to.equal(500);
- done();
- });
- });
- });
-
- it('errors on objectMode stream reply', function (done) {
-
- var TestStream = function () {
-
- Stream.Readable.call(this, { objectMode: true });
- };
-
- Hoek.inherits(TestStream, Stream.Readable);
-
- TestStream.prototype._read = function (size) {
-
- if (this.isDone) {
- return;
- }
- this.isDone = true;
-
- this.push({ x: 1 });
- this.push({ y: 1 });
- this.push(null);
- };
-
- var handler = function (request, reply) {
-
- return reply(new TestStream());
- };
-
- var server = new Hapi.Server({ debug: false });
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(500);
- done();
- });
- });
-
- describe('promises', function () {
-
- it('returns a stream', function (done) {
-
- var TestStream = function () {
-
- Stream.Readable.call(this);
-
- this.statusCode = 200;
- };
-
- Hoek.inherits(TestStream, Stream.Readable);
-
- TestStream.prototype._read = function (size) {
-
- if (this.isDone) {
- return;
- }
- this.isDone = true;
-
- this.push('x');
- this.push('y');
- this.push(null);
- };
-
- var handler = function (request, reply) {
-
- return reply(Bluebird.resolve(new TestStream())).ttl(2000).code(299);
- };
-
- var server = new Hapi.Server({ debug: false });
- server.connection({ routes: { cors: { origin: ['test.example.com'] } } });
- server.route({ method: 'GET', path: '/stream', config: { handler: handler, cache: { expiresIn: 9999 } } });
-
- server.inject('/stream', function (res) {
-
- expect(res.result).to.equal('xy');
- expect(res.statusCode).to.equal(299);
- expect(res.headers['cache-control']).to.equal('max-age=2, must-revalidate');
- expect(res.headers['access-control-allow-origin']).to.equal('test.example.com');
- done();
- });
- });
-
- it('returns a buffer', function (done) {
-
- var handler = function (request, reply) {
-
- return reply(Bluebird.resolve(new Buffer('buffer content'))).code(299).type('something/special');
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(299);
- expect(res.result.toString()).to.equal('buffer content');
- expect(res.headers['content-type']).to.equal('something/special');
- done();
- });
- });
- });
- });
-
- describe('hold()', function () {
-
- it('undo scheduled next tick in reply interface', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- var handler = function (request, reply) {
-
- return reply('123').hold().send();
- };
-
- server.route({ method: 'GET', path: '/domain', handler: handler });
-
- server.inject('/domain', function (res) {
-
- expect(res.result).to.equal('123');
- done();
- });
- });
-
- it('sends reply after timed handler', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- var handler = function (request, reply) {
-
- var response = reply('123').hold();
- setTimeout(function () {
-
- response.send();
- }, 10);
- };
-
- server.route({ method: 'GET', path: '/domain', handler: handler });
-
- server.inject('/domain', function (res) {
-
- expect(res.result).to.equal('123');
- done();
- });
- });
- });
-
- describe('close()', function () {
-
- it('returns a reply with manual end', function (done) {
-
- var handler = function (request, reply) {
-
- request.raw.res.end();
- return reply.close({ end: false });
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
-
- server.inject('/', function (res) {
-
- expect(res.result).to.equal('');
- done();
- });
- });
-
- it('returns a reply with auto end', function (done) {
-
- var handler = function (request, reply) {
-
- return reply.close();
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
-
- server.inject('/', function (res) {
-
- expect(res.result).to.equal('');
- done();
- });
- });
- });
-
- describe('continue()', function () {
-
- it('sets empty reply on continue in handler', function (done) {
-
- var handler = function (request, reply) {
-
- return reply.continue();
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
-
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.equal(null);
- expect(res.payload).to.equal('');
- done();
- });
- });
-
- it('sets empty reply on continue in prerequisite', function (done) {
-
- var pre1 = function (request, reply) {
-
- return reply.continue();
- };
-
- var pre2 = function (request, reply) {
-
- return reply.continue();
- };
-
- var pre3 = function (request, reply) {
-
- return reply({
- m1: request.pre.m1,
- m2: request.pre.m2
- });
- };
-
- var handler = function (request, reply) {
-
- return reply(request.pre.m3);
- };
-
- var server = new Hapi.Server();
- server.connection();
- server.route({
- method: 'GET',
- path: '/',
- config: {
- pre: [
- { method: pre1, assign: 'm1' },
- { method: pre2, assign: 'm2' },
- { method: pre3, assign: 'm3' }
- ],
- handler: handler
- }
- });
-
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.deep.equal({
- m1: null,
- m2: null
- });
- expect(res.payload).to.equal('{"m1":null,"m2":null}');
- done();
- });
- });
- });
-});
diff --git a/test/request.js b/test/request.js
index 9dc4e7acb..d2f1760e8 100755
--- a/test/request.js
+++ b/test/request.js
@@ -1,1580 +1,2664 @@
-// Load modules
+'use strict';
-var Http = require('http');
-var Net = require('net');
-var Stream = require('stream');
-var Boom = require('boom');
-var Code = require('code');
-var Hapi = require('..');
-var Hoek = require('hoek');
-var Lab = require('lab');
-var Wreck = require('wreck');
+const Http = require('http');
+const Net = require('net');
+const Stream = require('stream');
+const Url = require('url');
+const Events = require('events');
+const Boom = require('@hapi/boom');
+const Code = require('@hapi/code');
+const Hapi = require('..');
+const Hoek = require('@hapi/hoek');
+const Joi = require('joi');
+const Lab = require('@hapi/lab');
+const Teamwork = require('@hapi/teamwork');
+const Wreck = require('@hapi/wreck');
-// Declare internals
+const Common = require('./common');
-var internals = {};
+const internals = {};
-// Test shortcuts
+const { describe, it } = exports.lab = Lab.script();
+const expect = Code.expect;
-var lab = exports.lab = Lab.script();
-var describe = lab.describe;
-var it = lab.it;
-var expect = Code.expect;
+describe('Request.Generator', () => {
-describe('Request.Generator', function () {
+ it('decorates request multiple times', async () => {
- it('decorates request multiple times', function (done) {
+ const server = Hapi.server();
- var server = new Hapi.Server();
- server.connection();
+ server.decorate('request', 'x2', () => 2);
+ server.decorate('request', 'abc', () => 1);
- server.decorate('request', 'x2', function () {
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: (request) => {
- return 2;
+ return request.x2() + request.abc();
+ }
});
- server.decorate('request', 'abc', 1);
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal(3);
+ });
+
+ it('decorates request with non function method', async () => {
+
+ const server = Hapi.server();
+ const symbol = Symbol('abc');
+
+ server.decorate('request', 'x2', 2);
+ server.decorate('request', symbol, 1);
server.route({
method: 'GET',
path: '/',
- handler: function (request, reply) {
+ handler: (request) => {
- return reply(request.x2() + request.abc);
+ return request.x2 + request[symbol];
}
});
- server.inject('/', function (res) {
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal(3);
+ });
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.equal(3);
- done();
+ it('does not share decorations between servers via prototypes', async () => {
+
+ const server1 = Hapi.server();
+ const server2 = Hapi.server();
+ const route = {
+ method: 'GET',
+ path: '/',
+ handler: (request) => {
+
+ return Object.keys(Object.getPrototypeOf(request));
+ }
+ };
+ let res;
+
+ server1.decorate('request', 'x1', 1);
+ server2.decorate('request', 'x2', 2);
+
+ server1.route(route);
+ server2.route(route);
+
+ res = await server1.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal(['x1']);
+
+ res = await server2.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal(['x2']);
+ });
+
+ it('decorates symbols when apply=true', async () => {
+
+ const server = Hapi.server();
+ const symbol = Symbol('abc');
+
+ server.decorate('request', symbol, () => 'foo', { apply: true });
+
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: (request) => {
+
+ return request[symbol];
+ }
});
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('foo');
+
});
});
-describe('Request', function () {
+describe('Request', () => {
+
+ it('sets host and hostname', async () => {
+
+ const server = Hapi.server();
+
+ const handler = (request) => {
+
+ return [request.info.host, request.info.hostname].join('|');
+ };
+
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res1 = await server.inject({ url: '/', headers: { host: 'host' } });
+ expect(res1.payload).to.equal('host|host');
- it('sets client address', function (done) {
+ const res2 = await server.inject({ url: '/', headers: { host: 'host:123' } });
+ expect(res2.payload).to.equal('host:123|host');
- var server = new Hapi.Server();
- server.connection();
+ const res3 = await server.inject({ url: '/', headers: { host: '127.0.0.1' } });
+ expect(res3.payload).to.equal('127.0.0.1|127.0.0.1');
- var handler = function (request, reply) {
+ const res4 = await server.inject({ url: '/', headers: { host: '127.0.0.1:123' } });
+ expect(res4.payload).to.equal('127.0.0.1:123|127.0.0.1');
- var expectedClientAddress = '127.0.0.1';
- if (Net.isIPv6(server.listener.address().address)) {
- expectedClientAddress = '::ffff:127.0.0.1';
+ const res5 = await server.inject({ url: '/', headers: { host: '[::1]' } });
+ expect(res5.payload).to.equal('[::1]|[::1]');
+
+ const res6 = await server.inject({ url: '/', headers: { host: '[::1]:123' } });
+ expect(res6.payload).to.equal('[::1]:123|[::1]');
+ });
+
+ it('sets client address (default)', async (flags) => {
+
+ const server = Hapi.server();
+
+ const handler = (request) => {
+
+ // Call twice to reuse cached values
+
+ if (Common.hasIPv6) {
+ // 127.0.0.1 on node v14 and v16, ::1 on node v18 since DNS resolved to IPv6.
+ expect(request.info.remoteAddress).to.match(/^127\.0\.0\.1|::1$/);
+ expect(request.info.remoteAddress).to.match(/^127\.0\.0\.1|::1$/);
+ }
+ else {
+ expect(request.info.remoteAddress).to.equal('127.0.0.1');
+ expect(request.info.remoteAddress).to.equal('127.0.0.1');
}
- expect(request.info.remoteAddress).to.equal(expectedClientAddress);
- expect(request.info.remoteAddress).to.equal(request.info.remoteAddress);
- return reply('ok');
+ expect(request.info.remotePort).to.be.above(0);
+ expect(request.info.remotePort).to.be.above(0);
+
+ return 'ok';
};
- server.route({ method: 'GET', path: '/', handler: handler });
+ server.route({ method: 'get', path: '/', handler });
+
+ await server.start();
+ flags.onCleanup = () => server.stop();
+
+ const { payload } = await Wreck.get('http://localhost:' + server.info.port);
+ expect(payload.toString()).to.equal('ok');
+ });
- server.start(function (err) {
+ it('sets client address (ipv4)', async (flags) => {
- expect(err).to.not.exist();
+ const server = Hapi.server();
- Wreck.get('http://localhost:' + server.info.port, function (err, res, body) {
+ const handler = (request) => {
- expect(body.toString()).to.equal('ok');
- server.stop(done);
+ Object.defineProperty(request.raw.req.socket, 'remoteAddress', {
+ value: '100.100.100.100'
});
- });
+
+ return request.info.remoteAddress;
+ };
+
+ server.route({ method: 'get', path: '/', handler });
+
+ await server.start();
+ flags.onCleanup = () => server.stop();
+
+ const { payload } = await Wreck.get('http://localhost:' + server.info.port);
+ expect(payload.toString()).to.equal('100.100.100.100');
});
- it('sets referrer', function (done) {
+ it('sets client address (ipv6)', async (flags) => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
- var handler = function (request, reply) {
+ const handler = (request) => {
- expect(request.info.referrer).to.equal('http://site.com');
- return reply('ok');
+ Object.defineProperty(request.raw.req.socket, 'remoteAddress', {
+ value: '::ffff:0:0:0:0:1'
+ });
+
+ return request.info.remoteAddress;
};
- server.route({ method: 'GET', path: '/', handler: handler });
+ server.route({ method: 'get', path: '/', handler });
- server.inject({ url: '/', headers: { referrer: 'http://site.com' } }, function (res) {
+ await server.start();
+ flags.onCleanup = () => server.stop();
- expect(res.result).to.equal('ok');
- done();
+ const { payload } = await Wreck.get('http://localhost:' + server.info.port);
+ expect(payload.toString()).to.equal('::ffff:0:0:0:0:1');
+ });
+
+ it('sets client address (ipv4-mapped ipv6)', async (flags) => {
+
+ const server = Hapi.server();
+
+ const handler = (request) => {
+
+ Object.defineProperty(request.raw.req.socket, 'remoteAddress', {
+ value: '::ffff:100.100.100.100'
+ });
+
+ return request.info.remoteAddress;
+ };
+
+ server.route({ method: 'get', path: '/', handler });
+
+ await server.start();
+ flags.onCleanup = () => server.stop();
+
+ const { payload } = await Wreck.get('http://localhost:' + server.info.port);
+ expect(payload.toString()).to.equal('100.100.100.100');
+ });
+
+ it('sets client address to nothing when not available', async (flags) => {
+
+ const server = Hapi.server();
+ const abortedReqTeam = new Teamwork.Team();
+ let remoteAddr = 'not executed';
+
+ server.route({
+ method: 'GET',
+ path: '/',
+ options: {
+ handler: async (request, h) => {
+
+ req.destroy();
+
+ while (request.active()) {
+ await Hoek.wait(5);
+ }
+
+ abortedReqTeam.attend();
+
+ remoteAddr = request.info.remoteAddress;
+ return null;
+ }
+ }
});
+
+ await server.start();
+ flags.onCleanup = () => server.stop();
+
+ const req = Http.get(server.info.uri, Hoek.ignore);
+ req.on('error', Hoek.ignore);
+
+ await abortedReqTeam.work;
+
+ expect(remoteAddr).to.equal(undefined);
+ });
+
+ it('sets port to nothing when not available', async () => {
+
+ const server = Hapi.server({ debug: false });
+ server.route({ method: 'GET', path: '/', handler: (request) => request.info.remotePort === '' });
+ const res = await server.inject('/');
+ expect(res.result).to.equal(true);
});
- it('sets referer', function (done) {
+ it('sets referrer', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
- var handler = function (request, reply) {
+ const handler = (request) => {
expect(request.info.referrer).to.equal('http://site.com');
- return reply('ok');
+ return 'ok';
};
- server.route({ method: 'GET', path: '/', handler: handler });
+ server.route({ method: 'GET', path: '/', handler });
- server.inject({ url: '/', headers: { referer: 'http://site.com' } }, function (res) {
+ const res = await server.inject({ url: '/', headers: { referrer: 'http://site.com' } });
+ expect(res.result).to.equal('ok');
+ });
- expect(res.result).to.equal('ok');
- done();
- });
+ it('sets referer', async () => {
+
+ const server = Hapi.server();
+
+ const handler = (request) => {
+
+ expect(request.info.referrer).to.equal('http://site.com');
+ return 'ok';
+ };
+
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject({ url: '/', headers: { referer: 'http://site.com' } });
+ expect(res.result).to.equal('ok');
});
- it('sets headers', function (done) {
+ it('sets acceptEncoding', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler: (request) => request.info.acceptEncoding });
+
+ const res = await server.inject({ url: '/', headers: { 'accept-encoding': 'gzip' } });
+ expect(res.result).to.equal('gzip');
+ });
+
+ it('handles invalid accept encoding header', async () => {
+
+ const server = Hapi.server({ routes: { log: { collect: true } } });
- var handler = function (request, reply) {
+ const handler = (request) => {
- return reply(request.headers['user-agent']);
+ expect(request.logs[0].error.header).to.equal('a;b');
+ return request.info.acceptEncoding;
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
+ server.route({ method: 'GET', path: '/', handler });
- server.inject('/', function (res) {
+ const res = await server.inject({ url: '/', headers: { 'accept-encoding': 'a;b' } });
+ expect(res.result).to.equal('identity');
+ });
- expect(res.payload).to.equal('shot');
- done();
- });
+ it('sets headers', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler: (request) => request.headers['user-agent'] });
+
+ const res = await server.inject('/');
+ expect(res.payload).to.equal('shot');
+ });
+
+ it('sets host info from :authority header when host header is absent', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler: (request) => `${request.info.host}|${request.info.hostname}` });
+
+ const res = await server.inject({ url: '/', headers: { host: '', ':authority': 'example.com:8080' } });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('example.com:8080|example.com');
});
- it('generates unique request id', function (done) {
+ it('generates unique request id', async () => {
+
+ const server = Hapi.server();
+ server._core.requestCounter = { value: 10, min: 10, max: 11 };
+ server.route({ method: 'GET', path: '/', handler: (request) => request.info.id });
+
+ const res1 = await server.inject('/');
+ expect(res1.result).to.match(/10$/);
+
+ const res2 = await server.inject('/');
+ expect(res2.result).to.match(/11$/);
+
+ const res3 = await server.inject('/');
+ expect(res3.result).to.match(/10$/);
+ });
+
+ it('can serialize request.info with JSON.stringify()', async () => {
+
+ const server = Hapi.server();
- var handler = function (request, reply) {
+ const handler = (request) => {
- return reply(request.id);
+ const actual = JSON.stringify(request.info);
+ const expected = JSON.stringify({
+ acceptEncoding: request.info.acceptEncoding,
+ completed: request.info.completed,
+ cors: request.info.cors,
+ host: request.info.host,
+ hostname: request.info.hostname,
+ id: request.info.id,
+ received: request.info.received,
+ referrer: request.info.referrer,
+ remoteAddress: request.info.remoteAddress,
+ remotePort: request.info.remotePort,
+ responded: request.info.responded
+ });
+
+ expect(actual).to.equal(expected);
+ return 'ok';
};
- var server = new Hapi.Server();
- server.connection();
- server.connections[0]._requestCounter = { value: 10, min: 10, max: 11 };
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res1) {
+ server.route({ method: 'GET', path: '/', handler });
- server.inject('/', function (res2) {
+ const res = await server.inject({ url: '/' });
+ expect(res.result).to.equal('ok');
+ });
- server.inject('/', function (res3) {
+ describe('active()', () => {
- expect(res1.result).to.match(/10$/);
- expect(res2.result).to.match(/11$/);
- expect(res3.result).to.match(/10$/);
- done();
- });
+ it('exits handler early when request is no longer active', { retry: true }, async (flags) => {
+
+ let testComplete = false;
+
+ const onCleanup = [];
+ flags.onCleanup = async () => {
+
+ testComplete = true;
+
+ for (const cleanup of onCleanup) {
+ await cleanup();
+ }
+ };
+
+ const server = Hapi.server();
+ const leaveHandlerTeam = new Teamwork.Team();
+
+ server.route({
+ method: 'GET',
+ path: '/',
+ options: {
+ handler: async (request, h) => {
+
+ req.destroy();
+
+ while (request.active() && !testComplete) {
+ await Hoek.wait(10);
+ }
+
+ leaveHandlerTeam.attend({
+ active: request.active(),
+ testComplete
+ });
+
+ return null;
+ }
+ }
+ });
+
+ await server.start();
+ onCleanup.unshift(() => server.stop());
+
+ const req = Http.get(server.info.uri, Hoek.ignore);
+ req.on('error', Hoek.ignore);
+
+ const note = await leaveHandlerTeam.work;
+
+ expect(note).to.equal({
+ active: false,
+ testComplete: false
});
});
});
- describe('_execute()', function () {
+ describe('_execute()', () => {
- it('returns 400 on invalid path', function (done) {
+ it('returns 400 on invalid path', async () => {
- var server = new Hapi.Server();
- server.connection();
- server.inject('invalid', function (res) {
+ const server = Hapi.server();
- expect(res.statusCode).to.equal(400);
- done();
+ server.ext('onRequest', (request, h) => {
+
+ expect(request.url).to.be.null();
+ expect(request.query).to.equal({});
+ expect(request.path).to.equal('invalid');
+ return h.continue;
});
+
+ const res = await server.inject('invalid');
+ expect(res.statusCode).to.equal(400);
+ expect(res.result.message).to.startWith('Invalid URL');
});
- it('returns error response on ext error', function (done) {
+ it('returns boom response on ext error', async () => {
- var handler = function (request, reply) {
+ const server = Hapi.server();
- return reply('OK');
+ const ext = (request) => {
+
+ throw Boom.badRequest();
};
- var server = new Hapi.Server();
- server.connection();
+ server.ext('onPostHandler', ext);
+ server.route({ method: 'GET', path: '/', handler: () => 'OK' });
- var ext = function (request, reply) {
+ const res = await server.inject('/');
+ expect(res.result.statusCode).to.equal(400);
+ });
- return reply(Boom.badRequest());
+ it('returns error response on ext error', async () => {
+
+ const server = Hapi.server();
+
+ const ext = (request) => {
+
+ throw new Error('oops');
};
server.ext('onPostHandler', ext);
- server.route({ method: 'GET', path: '/', handler: handler });
+ server.route({ method: 'GET', path: '/', handler: () => 'OK' });
+
+ const res = await server.inject('/');
+ expect(res.result.statusCode).to.equal(500);
+ });
- server.inject('/', function (res) {
+ it('returns error response on ext timeout', async () => {
- expect(res.result.statusCode).to.equal(400);
- done();
- });
+ const server = Hapi.server();
+
+ const responded = server.ext('onPostResponse');
+ const ext = (request) => {
+
+ return Hoek.block();
+ };
+
+ server.ext('onPostHandler', ext, { timeout: 100 });
+ server.route({ method: 'GET', path: '/', handler: () => 'OK' });
+
+ const res = await server.inject('/');
+ expect(res.result.statusCode).to.equal(500);
+
+ const request = await responded;
+ expect(request.response._error).to.be.an.error('onPostHandler timed out');
});
- it('handles aborted requests', { parallel: false }, function (done) {
+ it('logs error responses on onPostResponse ext error', async () => {
- var handler = function (request, reply) {
+ const server = Hapi.server();
- var TestStream = function () {
+ const ext1 = () => {
- Stream.Readable.call(this);
- };
+ throw new Error('oops1');
+ };
- Hoek.inherits(TestStream, Stream.Readable);
+ server.ext('onPostResponse', ext1);
- TestStream.prototype._read = function (size) {
+ const ext2 = () => {
- if (this.isDone) {
- return;
- }
- this.isDone = true;
+ throw new Error('oops2');
+ };
+
+ server.ext('onPostResponse', ext2);
+
+ server.route({ method: 'GET', path: '/', handler: () => 'OK' });
+
+ const log = server.events.few({ name: 'request', channels: 'internal', filter: 'ext', count: 2 });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+
+ const [[, event1], [, event2]] = await log;
+ expect(event1.error).to.be.an.error('oops1');
+ expect(event2.error).to.be.an.error('oops2');
+ });
+
+ it('handles aborted requests (during response)', async () => {
+
+ const handler = (request) => {
+
+ const TestStream = class extends Stream.Readable {
+
+ _read(size) {
- this.push('success');
- this.emit('data', 'success');
+ if (this.isDone) {
+ return;
+ }
+
+ this.isDone = true;
+
+ this.push('success');
+ this.emit('data', 'success');
+ }
};
- var stream = new TestStream();
- return reply(stream);
+ const stream = new TestStream();
+ return stream;
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
+ const server = Hapi.server({ info: { remote: true } });
+ server.route({ method: 'GET', path: '/', handler });
+
+ let disconnected = 0;
+ let info;
+ const onRequest = (request, h) => {
- server.start(function (err) {
+ request.events.once('disconnect', () => {
- expect(err).to.not.exist();
+ info = request.info;
+ ++disconnected;
+ });
+
+ return h.continue;
+ };
- var total = 2;
- var createConnection = function () {
+ server.ext('onRequest', onRequest);
- var client = Net.connect(server.info.port, function () {
+ await server.start();
- client.write('GET / HTTP/1.1\r\n\r\n');
- client.write('GET / HTTP/1.1\r\n\r\n');
- });
+ let total = 2;
+ const createConnection = function () {
- client.on('data', function () {
+ const client = Net.connect(server.info.port, () => {
- total--;
- client.destroy();
- });
- };
+ client.write('GET / HTTP/1.1\r\nHost: host\r\n\r\n');
+ client.write('GET / HTTP/1.1\r\nHost: host\r\n\r\n');
+ });
+
+ client.on('data', () => {
+
+ --total;
+ client.destroy();
+ });
+ };
+
+ await new Promise((resolve) => {
- var check = function () {
+ const check = function () {
if (total) {
createConnection();
- setTimeout(check, 10);
+ setTimeout(check, 100);
}
else {
- server.stop(done);
+ expect(disconnected).to.equal(4); // Each connection sends two HTTP requests
+ resolve();
}
};
check();
});
+
+ await server.stop();
+ expect(info.remotePort).to.exist();
+ expect(info.remoteAddress).to.exist();
});
- it('returns empty params array when none present', function (done) {
+ it('handles aborted requests (before response)', { retry: true }, async (flags) => {
- var handler = function (request, reply) {
+ const server = Hapi.server();
+ server.route({
+ method: 'GET',
+ path: '/test',
+ handler: () => null
+ });
+
+ const codes = [];
+ server.ext('onPostResponse', (request) => codes.push(Boom.isBoom(request.response) ? request.response.output.statusCode : request.response.statusCode));
+
+ const team = new Teamwork.Team();
+ const onRequest = (request, h) => {
- return reply(request.params);
+ request.events.once('disconnect', () => team.attend());
+ return h.continue;
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
+ server.ext('onRequest', onRequest);
- server.inject('/', function (res) {
+ let firstRequest = true;
+ const onPreHandler = async (request, h) => {
- expect(res.result).to.deep.equal({});
- done();
+ if (firstRequest) {
+ client.destroy();
+ firstRequest = false;
+ }
+ else {
+ // To avoid timing differences between node versions, ensure that
+ // the second and third requests always experience the disconnect
+ await team.work;
+ }
+
+ return h.continue;
+ };
+
+ server.ext('onPreHandler', onPreHandler);
+
+ await server.start();
+ flags.onCleanup = () => server.stop();
+
+ const client = Net.connect(server.info.port, () => {
+
+ client.write('GET /test HTTP/1.1\r\nHost: host\r\n\r\n');
+ client.write('GET /test HTTP/1.1\r\nHost: host\r\n\r\n');
+ client.write('GET /test HTTP/1.1\r\nHost: host\r\n\r\n');
});
- });
- it('does not fail on abort', function (done) {
+ await team.work;
+ await server.stop();
- var clientRequest;
+ expect(codes).to.equal([204, 499, 499]);
+ });
- var handler = function (request, reply) {
+ it('returns empty params array when none present', async () => {
- clientRequest.abort();
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler: (request) => request.params });
- setTimeout(function () {
+ const res = await server.inject('/');
+ expect(res.result).to.equal({});
+ });
- reply(new Error('fail'));
- setTimeout(function () {
+ it('returns empty params array when none present (not found)', async () => {
- server.stop(done);
- }, 10);
- }, 10);
+ const server = Hapi.server();
+ const preResponse = (request) => {
+
+ return request.params;
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
+ server.ext('onPreResponse', preResponse);
+
+ const res = await server.inject('/');
+ expect(res.result).to.equal({});
+ });
- server.start(function (err) {
+ it('does not fail on abort', async () => {
- expect(err).to.not.exist();
+ const server = Hapi.server();
+ const team = new Teamwork.Team();
- clientRequest = Http.request({
- hostname: 'localhost',
- port: server.info.port,
- method: 'GET'
- });
+ const handler = async (request) => {
+
+ clientRequest.destroy();
+ await Hoek.wait(10);
+ team.attend();
+ throw new Error('fail');
+ };
+
+ server.route({ method: 'GET', path: '/', handler });
+
+ await server.start();
- clientRequest.on('error', function () { /* NOP */ });
- clientRequest.end();
+ const clientRequest = Http.request({
+ hostname: 'localhost',
+ port: server.info.port,
+ method: 'GET'
});
+
+ clientRequest.on('error', Hoek.ignore);
+ clientRequest.end();
+
+ await team.work;
+ await server.stop();
});
- it('does not fail on abort with ext', function (done) {
+ it('does not fail on abort (onPreHandler)', async () => {
- var clientRequest;
+ const server = Hapi.server();
+ const team = new Teamwork.Team();
- var handler = function (request, reply) {
+ server.route({ method: 'GET', path: '/', handler: () => null });
- clientRequest.abort();
- setTimeout(function () {
+ const preHandler = async (request, h) => {
- return reply(new Error('boom'));
- }, 10);
+ clientRequest.destroy();
+ await Hoek.wait(10);
+ team.attend();
+ return h.continue;
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
+ server.ext('onPreHandler', preHandler);
- server.ext('onPreResponse', function (request, reply) {
+ await server.start();
- return reply.continue();
+ const clientRequest = Http.request({
+ hostname: 'localhost',
+ port: server.info.port,
+ method: 'GET'
});
- server.on('tail', function () {
+ clientRequest.on('error', Hoek.ignore);
+ clientRequest.end();
- server.stop(done);
- });
+ await team.work;
+ await server.stop();
+ });
- server.start(function (err) {
+ it('does not fail on abort with ext', async () => {
- expect(err).to.not.exist();
+ const handler = async (request) => {
- clientRequest = Http.request({
- hostname: 'localhost',
- port: server.info.port,
- method: 'GET'
- });
+ clientRequest.destroy();
+ await Hoek.wait(10);
+ throw new Error('boom');
+ };
- clientRequest.on('error', function () { /* NOP */ });
- clientRequest.end();
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+
+ const preResponse = (request, h) => {
+
+ return h.continue;
+ };
+
+ server.ext('onPreResponse', preResponse);
+
+ const log = server.events.once('response');
+
+ await server.start();
+
+ const clientRequest = Http.request({
+ hostname: 'localhost',
+ port: server.info.port,
+ method: 'GET'
});
+
+ clientRequest.on('error', Hoek.ignore);
+ clientRequest.end();
+
+ await log;
+ await server.stop();
});
- it('returns not found on internal only route (external)', function (done) {
+ it('returns not found on internal only route (external)', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.route({
method: 'GET',
path: '/some/route',
- config: {
+ options: {
isInternal: true,
- handler: function (request, reply) {
+ handler: () => 'ok'
+ }
+ });
- return reply('ok');
- }
+ await server.start();
+ const err = await expect(Wreck.get('http://localhost:' + server.info.port)).to.reject();
+ expect(err.data.res.statusCode).to.equal(404);
+ expect(err.data.payload.toString()).to.equal('{"statusCode":404,"error":"Not Found","message":"Not Found"}');
+ await server.stop();
+ });
+
+ it('returns not found on internal only route (inject)', async () => {
+
+ const server = Hapi.server();
+ server.route({
+ method: 'GET',
+ path: '/some/route',
+ options: {
+ isInternal: true,
+ handler: () => 'ok'
}
});
- server.start(function (err) {
+ const res = await server.inject('/some/route');
+ expect(res.statusCode).to.equal(404);
+ });
- expect(err).to.not.exist();
- Wreck.get('http://localhost:' + server.info.port, function (err, res, body) {
+ it('allows internal only route (inject with allowInternals)', async () => {
- expect(res.statusCode).to.equal(404);
- expect(body.toString()).to.equal('{"statusCode":404,"error":"Not Found"}');
- server.stop(done);
- });
+ const server = Hapi.server();
+ server.route({
+ method: 'GET',
+ path: '/some/route',
+ options: {
+ isInternal: true,
+ handler: () => 'ok'
+ }
});
+
+ const res = await server.inject({ url: '/some/route', allowInternals: true });
+ expect(res.statusCode).to.equal(200);
});
- it('returns not found on internal only route (inject)', function (done) {
+ it('allows internal only route (inject with allowInternals and authority)', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.route({
method: 'GET',
path: '/some/route',
- config: {
+ options: {
isInternal: true,
- handler: function (request, reply) {
+ handler: () => 'ok'
+ }
+ });
- return reply('ok');
- }
+ const res = await server.inject({ url: '/some/route', allowInternals: true, authority: 'server:8000' });
+ expect(res.statusCode).to.equal(200);
+ });
+
+ it('creates arrays from multiple entries', async () => {
+
+ const server = Hapi.server();
+
+ const handler = (request) => {
+
+ return { a: request.query.a, array: Array.isArray(request.query.a), instance: request.query.a instanceof Array };
+ };
+
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject('/?a=1&a=2');
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal({ a: ['1', '2'], array: true, instance: true });
+ });
+
+ it('supports custom query parser (new object)', async () => {
+
+ const parser = (query) => {
+
+ return { hello: query.hi };
+ };
+
+ const server = Hapi.server({ query: { parser } });
+
+ server.route({
+ method: 'GET',
+ path: '/',
+ options: {
+ handler: (request) => request.query.hello
}
});
- server.inject('/some/route', function (res) {
+ const res = await server.inject('/?hi=hola');
+ expect(res.statusCode).to.equal(200);
+ expect(res.payload).to.equal('hola');
+ });
+
+ it('supports custom query parser (same object)', async () => {
- expect(res.statusCode).to.equal(404);
- done();
+ const parser = (query) => {
+
+ query.hello = query.hi;
+ return query;
+ };
+
+ const server = Hapi.server({ query: { parser } });
+
+ server.route({
+ method: 'GET', path: '/', options: {
+ handler: (request) => request.query.hello
+ }
});
+
+ const res = await server.inject('/?hi=hola');
+ expect(res.statusCode).to.equal(200);
+ expect(res.payload).to.equal('hola');
});
- it('allows internal only route (inject with allowInternals)', function (done) {
+ it('returns 500 when custom query parser returns non-object', async () => {
+
+ const server = Hapi.server({ debug: false, query: { parser: () => 'something' } });
- var server = new Hapi.Server();
- server.connection();
+ server.route({
+ method: 'GET', path: '/', options: {
+ handler: (request) => request.query.hello
+ }
+ });
+
+ const res = await server.inject('/?hi=hola');
+ expect(res.statusCode).to.equal(500);
+ expect(res.request.response._error).to.be.an.error('Parsed query must be an object');
+ });
+
+ it('returns 500 when custom query parser returns null', async () => {
+
+ const server = Hapi.server({ debug: false, query: { parser: () => null } });
+
+ server.route({
+ method: 'GET', path: '/', options: {
+ handler: (request) => request.query.hello
+ }
+ });
+
+ const res = await server.inject('/?hi=hola');
+ expect(res.statusCode).to.equal(500);
+ expect(res.request.response._error).to.be.an.error('Parsed query must be an object');
+ });
+ });
+
+ describe('_onRequest()', () => {
+
+ it('errors on non-takeover response', async () => {
+
+ const server = Hapi.server({ debug: false });
+ server.ext('onRequest', () => 'something');
+ server.route({ method: 'GET', path: '/', handler: () => null });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
+ });
+ });
+
+ describe('_lifecycle()', () => {
+
+ it('errors on non-takeover response in pre handler ext', async () => {
+
+ const server = Hapi.server({ debug: false });
+ server.ext('onPreHandler', () => 'something');
+ server.route({ method: 'GET', path: '/', handler: () => null });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
+ });
+
+ it('logs thrown errors as boom errors', async () => {
+
+ const server = Hapi.server({ debug: false });
server.route({
method: 'GET',
- path: '/some/route',
- config: {
- isInternal: true,
- handler: function (request, reply) {
+ path: '/',
+ options: {
+ handler: function () {
- return reply('ok');
+ // eslint-disable-next-line no-undef
+ NOT_DEFINED_VAR;
}
}
});
- server.inject({ url: '/some/route', allowInternals: true }, function (res) {
+ const log = new Promise((resolve) => {
+
+ server.events.on({ name: 'request', channels: 'internal' }, (request, event, tags) => {
+
+ if (tags.handler &&
+ tags.error) {
- expect(res.statusCode).to.equal(200);
- done();
+ resolve({ event, tags });
+ }
+ });
});
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
+
+ const { event } = await log;
+ expect(event.error.isBoom).to.equal(true);
+ expect(event.error.output.statusCode).to.equal(500);
+ expect(event.error.stack).to.exist();
});
});
- describe('_finalize()', function (done) {
+ describe('_postCycle()', () => {
- it('generate response event', function (done) {
+ it('skips onPreResponse when validation terminates request', { retry: true }, async (flags) => {
- var handler = function (request, reply) {
+ const server = Hapi.server();
+ const abortedReqTeam = new Teamwork.Team();
- return reply('ok');
- };
+ let called = false;
+ server.ext('onPreResponse', (request, h) => {
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
+ called = true;
+ return h.continue;
+ });
- server.once('response', function (request) {
+ server.route({
+ method: 'GET',
+ path: '/',
+ options: {
+ handler: (request) => {
- expect(request.info.responded).to.be.min(request.info.received);
- done();
- });
+ // Stash raw so that we can access it on response validation
+ Object.assign(request.app, request.raw);
- server.inject('/', function (res) { });
- });
+ return null;
+ },
+ response: {
+ status: {
+ 200: async (_, { context }) => {
- it('closes response after server timeout', function (done) {
+ req.destroy();
- var handler = function (request, reply) {
+ const raw = context.app.request;
+ await Events.once(raw.req, 'aborted');
- setTimeout(function () {
+ abortedReqTeam.attend();
+ }
+ }
+ }
+ }
+ });
- var stream = new Stream.Readable();
- stream._read = function (size) {
+ await server.start();
+ flags.onCleanup = () => server.stop();
- this.push('value');
- this.push(null);
- };
+ const req = Http.get(server.info.uri, Hoek.ignore);
+ req.on('error', Hoek.ignore);
- stream.close = function () {
+ await abortedReqTeam.work;
- done();
- };
+ await server.events.once('response');
- return reply(stream);
- }, 10);
- };
+ expect(called).to.be.false();
+ });
- var server = new Hapi.Server();
- server.connection({ routes: { timeout: { server: 5 } } });
+ it('handles continue signal', async () => {
+
+ const server = Hapi.server({ debug: false });
server.route({
method: 'GET',
path: '/',
- handler: handler
+ options: {
+ handler: () => ({ a: '1' }),
+ validate: {
+ validator: Joi
+ },
+ response: {
+ failAction: (request, h) => h.continue,
+ schema: {
+ b: Joi.string()
+ }
+ }
+ }
});
- server.inject('/', function (res) {
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ });
+ });
+
+ describe('_reply()', () => {
+
+ it('returns a reply with auto end in onPreResponse', async () => {
+
+ const server = Hapi.server();
+ server.ext('onPreResponse', (request, h) => h.close);
+ server.route({ method: 'GET', path: '/', handler: () => null });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('');
+ });
+ });
+
+ describe('_finalize()', () => {
+
+ it('generate response event', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler: () => 'ok' });
+
+ const log = server.events.once('response');
+ await server.inject('/');
+ const [request] = await log;
+ expect(request.info.responded).to.be.min(request.info.received);
+ expect(request.info.completed).to.be.min(request.info.responded);
+ expect(request.response.source).to.equal('ok');
+ expect(request.response.statusCode).to.equal(200);
+ });
+
+ it('skips logging error when not the result of a thrown error', async () => {
- expect(res.statusCode).to.equal(503);
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler: (request, h) => h.response().code(500) });
+
+ let called = false;
+ server.events.once('request', () => {
+
+ called = true;
});
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
+ expect(res.request.response._error).to.not.exist();
+ expect(called).to.be.false();
});
- it('does not attempt to close error response after server timeout', function (done) {
+ it('destroys response after server timeout', async () => {
- var handler = function (request, reply) {
+ const team = new Teamwork.Team();
+ const handler = async (request) => {
- setTimeout(function () {
+ await Hoek.wait(100);
- return reply(new Error('after'));
- }, 10);
+ const stream = new Stream.Readable();
+ stream._read = function (size) {
+
+ this.push('value');
+ this.push(null);
+ };
+
+ stream._destroy = () => team.attend();
+ return stream;
};
- var server = new Hapi.Server();
- server.connection({ routes: { timeout: { server: 5 } } });
+ const server = Hapi.server({ routes: { timeout: { server: 50 } } });
server.route({
method: 'GET',
path: '/',
- handler: handler
+ handler
});
- server.inject('/', function (res) {
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(503);
+ await team.work;
+ });
- expect(res.statusCode).to.equal(503);
- done();
- });
+ it('does not attempt to close error response after server timeout', async () => {
+
+ const handler = async (request) => {
+
+ await Hoek.wait(40);
+ throw new Error('after');
+ };
+
+ const server = Hapi.server({ routes: { timeout: { server: 20 } } });
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(503);
});
- it('emits request-error once', function (done) {
+ it('emits request-error once', async () => {
- var server = new Hapi.Server({ debug: false });
- server.connection();
+ const server = Hapi.server({ debug: false, routes: { log: { collect: true } } });
- var errs = 0;
- var req = null;
- server.on('request-error', function (request, err) {
+ let errs = 0;
+ let req = null;
+ server.events.on({ name: 'request', channels: 'error' }, (request, { error }) => {
errs++;
- expect(err).to.exist();
- expect(err.message).to.equal('boom2');
+ expect(error).to.exist();
+ expect(error.message).to.equal('boom2');
req = request;
});
- server.ext('onPreResponse', function (request, reply) {
+ const preResponse = (request) => {
- return reply(new Error('boom2'));
- });
+ throw new Error('boom2');
+ };
- var handler = function (request, reply) {
+ server.ext('onPreResponse', preResponse);
- return reply(new Error('boom1'));
- };
+ const handler = (request) => {
- server.route({ method: 'GET', path: '/', handler: handler });
+ throw new Error('boom1');
+ };
- server.inject('/', function (res) {
+ server.route({ method: 'GET', path: '/', handler });
- expect(res.statusCode).to.equal(500);
- expect(res.result).to.exist();
- expect(res.result.message).to.equal('An internal server error occurred');
- });
+ const log = server.events.once('response');
- server.once('response', function () {
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
+ expect(res.result).to.exist();
+ expect(res.result.message).to.equal('An internal server error occurred');
- expect(errs).to.equal(1);
- expect(req.getLog('error')[1].tags).to.deep.equal(['internal', 'error']);
- done();
- });
+ await log;
+ expect(errs).to.equal(1);
+ expect(req.logs[1].tags).to.equal(['internal', 'error']);
});
- it('emits request-error on implementation error', function (done) {
+ it('does not emit request-error when error is replaced with valid response', async () => {
- var server = new Hapi.Server({ debug: false });
- server.connection();
+ const server = Hapi.server({ debug: false });
- var errs = 0;
- var req = null;
- server.on('request-error', function (request, err) {
+ let errs = 0;
+ server.events.on({ name: 'request', channels: 'error' }, (request, event) => {
errs++;
- expect(err).to.exist();
- expect(err.message).to.equal('Uncaught error: boom');
- req = request;
});
- var handler = function (request, reply) {
+ server.ext('onPreResponse', () => 'ok');
- throw new Error('boom');
+ const handler = (request) => {
+
+ throw new Error('boom1');
};
- server.route({ method: 'GET', path: '/', handler: handler });
+ server.route({ method: 'GET', path: '/', handler });
- server.inject('/', function (res) {
+ const log = server.events.once('response');
- expect(res.statusCode).to.equal(500);
- expect(res.result).to.exist();
- expect(res.result.message).to.equal('An internal server error occurred');
- });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('ok');
+
+ await log;
+ expect(errs).to.equal(0);
+ });
+ });
+
+ describe('setMethod()', () => {
+
+ it('changes method with a lowercase version of the value passed in', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler: () => null });
+
+ const onRequest = (request, h) => {
+
+ request.setMethod('POST');
+ return h.response(request.method).takeover();
+ };
+
+ server.ext('onRequest', onRequest);
+
+ const res = await server.inject('/');
+ expect(res.payload).to.equal('post');
+ });
+
+ it('errors on missing method', async () => {
+
+ const server = Hapi.server({ debug: false });
+ server.route({ method: 'GET', path: '/', handler: () => null });
+ server.ext('onRequest', (request) => request.setMethod());
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
+ });
+
+ it('errors on invalid method type', async () => {
+
+ const server = Hapi.server({ debug: false });
+ server.route({ method: 'GET', path: '/', handler: () => null });
+ server.ext('onRequest', (request) => request.setMethod(42));
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
+ });
+ });
+
+ describe('setUrl()', () => {
+
+ it('sets url, path, and query', async () => {
+
+ const url = 'http://localhost/page?param1=something';
+ const server = Hapi.server();
+
+ const handler = (request) => {
+
+ return [request.url.href, request.path, request.query.param1].join('|');
+ };
+
+ server.route({ method: 'GET', path: '/page', handler });
+
+ const onRequest = (request, h) => {
+
+ request.setUrl(url);
+ return h.continue;
+ };
+
+ server.ext('onRequest', onRequest);
+
+ const res = await server.inject('/');
+ expect(res.payload).to.equal(url + '|/page|something');
+ });
+
+ it('sets root url', async () => {
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler: (request) => request.url.pathname });
+
+ const onRequest = (request, h) => {
+
+ request.setUrl('/');
+ return h.continue;
+ };
+
+ server.ext('onRequest', onRequest);
+
+ const res = await server.inject('/a/b/c');
+ expect(res.result).to.equal('/');
+ });
+
+ it('updates host info', async () => {
+
+ const url = 'http://redirected:321/';
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler: () => null });
+
+ const onRequest = (request, h) => {
+
+ const initialHost = request.info.host;
+
+ request.setUrl(url);
+ return h.response([request.url.href, request.path, initialHost, request.info.host, request.info.hostname].join('|')).takeover();
+ };
+
+ server.ext('onRequest', onRequest);
+
+ const res = await server.inject({ url: '/', headers: { host: 'initial:123' } });
+ expect(res.payload).to.equal(url + '|/|initial:123|redirected:321|redirected');
+ });
+
+ it('updates host info when set without port number', async () => {
+
+ const url = 'http://redirected/';
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler: () => null });
+
+ const onRequest = (request, h) => {
+
+ const initialHost = request.info.host;
+
+ request.setUrl(url);
+ return h.response([request.url.href, request.path, initialHost, request.info.host, request.info.hostname].join('|')).takeover();
+ };
+
+ server.ext('onRequest', onRequest);
+
+ const res1 = await server.inject({ url: '/', headers: { host: 'initial:123' } });
+ const res2 = await server.inject({ url: '/', headers: { host: 'initial' } });
+ expect(res1.payload).to.equal(url + '|/|initial:123|redirected|redirected');
+ expect(res2.payload).to.equal(url + '|/|initial|redirected|redirected');
+ });
+
+ it('overrides query string content', async () => {
+
+ const server = Hapi.server();
+
+ const handler = (request) => {
+
+ return [request.url.href, request.path, request.query.a].join('|');
+ };
+
+ server.route({ method: 'GET', path: '/', handler });
+
+ const onRequest = (request, h) => {
+
+ const uri = request.raw.req.url;
+ const parsed = new Url.URL(uri, 'http://test/');
+ parsed.searchParams.set('a', 2);
+ request.setUrl(parsed);
+ return h.continue;
+ };
+
+ server.ext('onRequest', onRequest);
+
+ const res = await server.inject('/?a=1');
+ expect(res.payload).to.equal('http://test/?a=2|/|2');
+ });
+
+ it('normalizes a path', async () => {
+
+ const rawPath = '/%0%1%2%3%4%5%6%7%8%9%a%b%c%d%e%f%10%11%12%13%14%15%16%17%18%19%1a%1b%1c%1d%1e%1f%20%21%22%23%24%25%26%27%28%29%2a%2b%2c%2d%2e%2f%30%31%32%33%34%35%36%37%38%39%3a%3b%3c%3d%3e%3f%40%41%42%43%44%45%46%47%48%49%4a%4b%4c%4d%4e%4f%50%51%52%53%54%55%56%57%58%59%5a%5b%5c%5d%5e%5f%60%61%62%63%64%65%66%67%68%69%6a%6b%6c%6d%6e%6f%70%71%72%73%74%75%76%77%78%79%7a%7b%7c%7d%7e%7f%80%81%82%83%84%85%86%87%88%89%8a%8b%8c%8d%8e%8f%90%91%92%93%94%95%96%97%98%99%9a%9b%9c%9d%9e%9f%a0%a1%a2%a3%a4%a5%a6%a7%a8%a9%aa%ab%ac%ad%ae%af%b0%b1%b2%b3%b4%b5%b6%b7%b8%b9%ba%bb%bc%bd%be%bf%c0%c1%c2%c3%c4%c5%c6%c7%c8%c9%ca%cb%cc%cd%ce%cf%d0%d1%d2%d3%d4%d5%d6%d7%d8%d9%da%db%dc%dd%de%df%e0%e1%e2%e3%e4%e5%e6%e7%e8%e9%ea%eb%ec%ed%ee%ef%f0%f1%f2%f3%f4%f5%f6%f7%f8%f9%fa%fb%fc%fd%fe%ff%0%1%2%3%4%5%6%7%8%9%A%B%C%D%E%F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20%21%22%23%24%25%26%27%28%29%2A%2B%2C%2D%2E%2F%30%31%32%33%34%35%36%37%38%39%3A%3B%3C%3D%3E%3F%40%41%42%43%44%45%46%47%48%49%4A%4B%4C%4D%4E%4F%50%51%52%53%54%55%56%57%58%59%5A%5B%5C%5D%5E%5F%60%61%62%63%64%65%66%67%68%69%6A%6B%6C%6D%6E%6F%70%71%72%73%74%75%76%77%78%79%7A%7B%7C%7D%7E%7F%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF';
+ const normPath = '/%0%1%2%3%4%5%6%7%8%9%a%b%c%d%e%f%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20!%22%23$%25&\'()*+,-.%2F0123456789:;%3C=%3E%3F@ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5C%5D%5E_%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D~%7F%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF%0%1%2%3%4%5%6%7%8%9%A%B%C%D%E%F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20!%22%23$%25&\'()*+,-.%2F0123456789:;%3C=%3E%3F@ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5C%5D%5E_%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D~%7F%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF';
+
+ const url = 'http://localhost' + rawPath + '?param1=something';
+ const normUrl = 'http://localhost' + normPath + '?param1=something';
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler: () => null });
+
+ const onRequest = (request, h) => {
+
+ request.setUrl(url);
+ return h.response([request.url.href, request.path, request.url.searchParams.get('param1')].join('|')).takeover();
+ };
+
+ server.ext('onRequest', onRequest);
+
+ const res = await server.inject('/');
+ expect(res.payload).to.equal(normUrl + '|' + normPath + '|something');
+ });
+
+ it('errors on empty path', async () => {
+
+ const server = Hapi.server({ debug: false });
+ const onRequest = (request, h) => {
+
+ request.setUrl('');
+ return h.continue;
+ };
+
+ server.ext('onRequest', onRequest);
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
+ });
+
+ it('throws when path is missing', async () => {
+
+ const server = Hapi.server();
+ const onRequest = (request, h) => {
+
+ try {
+ request.setUrl();
+ }
+ catch (err) {
+ return h.response(err.message).takeover();
+ }
+
+ return h.continue;
+ };
- server.once('response', function () {
+ server.ext('onRequest', onRequest);
- expect(errs).to.equal(1);
- expect(req.getLog('error')[0].tags).to.deep.equal(['internal', 'implementation', 'error']);
- done();
- });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.payload).to.equal('Url must be a string or URL object');
});
- it('does not emit request-error when error is replaced with valid response', function (done) {
+ it('strips trailing slash', async () => {
- var server = new Hapi.Server({ debug: false });
- server.connection();
+ const server = Hapi.server({ router: { stripTrailingSlash: true } });
+ server.route({ method: 'GET', path: '/test', handler: () => null });
- var errs = 0;
- server.on('request-error', function (request, err) {
+ const res1 = await server.inject('/test/');
+ expect(res1.statusCode).to.equal(204);
- errs++;
- });
+ const res2 = await server.inject('/test');
+ expect(res2.statusCode).to.equal(204);
+ });
- server.ext('onPreResponse', function (request, reply) {
+ it('does not strip trailing slash on /', async () => {
- return reply('ok');
- });
+ const server = Hapi.server({ router: { stripTrailingSlash: true } });
+ server.route({ method: 'GET', path: '/', handler: () => null });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(204);
+ });
- var handler = function (request, reply) {
+ it('strips trailing slash with query', async () => {
- return reply(new Error('boom1'));
- };
+ const server = Hapi.server({ router: { stripTrailingSlash: true } });
+ server.route({ method: 'GET', path: '/test', handler: () => null });
+ const res = await server.inject('/test/?a=b');
+ expect(res.statusCode).to.equal(204);
+ });
- server.route({ method: 'GET', path: '/', handler: handler });
+ it('clones passed url', async () => {
- server.inject('/', function (res) {
+ const urlObject = new Url.URL('http:/%41');
+ let requestUrl;
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.equal('ok');
- });
+ const server = Hapi.server();
+ const onRequest = (request, h) => {
- server.once('response', function () {
+ request.setUrl(urlObject);
+ requestUrl = request.url;
- expect(errs).to.equal(0);
- done();
- });
- });
- });
+ return h.continue;
+ };
- describe('tail()', function () {
+ server.ext('onRequest', onRequest);
- it('generates tail event', function (done) {
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(404);
+ expect(requestUrl).to.equal(urlObject);
+ expect(requestUrl).to.not.shallow.equal(urlObject);
+ });
- var handler = function (request, reply) {
+ it('handles vhost redirection', async () => {
- var t1 = request.addTail('t1');
- var t2 = request.addTail('t2');
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', vhost: 'one', handler: () => 'success' });
- reply('Done');
+ const onRequest = (request, h) => {
- t1();
- t1(); // Ignored
- setTimeout(t2, 10);
+ request.setUrl('http://one/');
+ return h.continue;
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
+ server.ext('onRequest', onRequest);
- var result = null;
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.payload).to.equal('success');
+ });
- server.once('tail', function () {
+ it('handles hostname in HTTP request resource', async () => {
- expect(result).to.equal('Done');
- done();
- });
+ const server = Hapi.server({ debug: false });
+ const team = new Teamwork.Team();
- server.inject('/', function (res) {
+ let hostname;
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: (request) => {
- result = res.result;
+ hostname = request.info.hostname;
+ team.attend();
+ return null;
+ }
});
+
+ await server.start();
+ const socket = Net.createConnection(server.info.port, '127.0.0.1', () => socket.write('GET http://host.com\r\n\r\n'));
+ await team.work;
+ socket.destroy();
+ await server.stop();
+ expect(hostname).to.equal('host.com');
});
- it('generates tail event without name', function (done) {
+ it('handles url starting with multiple /', async () => {
- var handler = function (request, reply) {
+ const server = Hapi.server();
+ server.route({
+ method: 'GET',
+ path: '/{p*}',
+ handler: (request) => {
- var tail = request.tail();
- reply('Done');
- tail();
- };
+ return {
+ p: request.params.p,
+ path: request.path,
+ hostname: request.info.hostname.toLowerCase() // Lowercase for OSX tests
+ };
+ }
+ });
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
+ const res = await server.inject('//path');
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal({ p: '/path', path: '//path', hostname: server.info.host.toLowerCase() });
+ });
- var result = null;
+ it('handles escaped path segments', async () => {
- server.once('tail', function () {
+ const server = Hapi.server();
+ server.route({ path: '/%2F/%2F', method: 'GET', handler: (request) => request.path });
- done();
- });
+ const tests = [
+ ['/', 404],
+ ['////', 404],
+ ['/%2F/%2F', 200, '/%2F/%2F'],
+ ['/%2F/%2F#x', 200, '/%2F/%2F'],
+ ['/%2F/%2F?a=1#x', 200, '/%2F/%2F']
+ ];
- server.inject('/', function (res) {
+ for (const [uri, code, result] of tests) {
+ const res = await server.inject(uri);
+ expect(res.statusCode).to.equal(code);
- });
+ if (code < 400) {
+ expect(res.result).to.equal(result);
+ }
+ }
});
- });
- describe('setMethod()', function () {
+ it('handles fragments (no query)', async () => {
- it('changes method with a lowercase version of the value passed in', function (done) {
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/{p*}', handler: (request) => request.path });
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: function (request, reply) { } });
+ await server.start();
- server.ext('onRequest', function (request, reply) {
+ const options = {
+ hostname: 'localhost',
+ port: server.info.port,
+ path: '/path#ignore',
+ method: 'GET'
+ };
- request.setMethod('POST');
- return reply(request.method);
- });
+ const team = new Teamwork.Team();
+ const req = Http.request(options, (res) => team.attend(res));
+ req.end();
- server.inject('/', function (res) {
+ const res = await team.work;
+ const payload = await Wreck.read(res);
+ expect(payload.toString()).to.equal('/path');
- expect(res.payload).to.equal('post');
- done();
- });
+ await server.stop();
});
- it('errors on missing method', function (done) {
+ it('handles fragments (with query)', async () => {
- var server = new Hapi.Server({ debug: false });
- server.connection();
- server.route({ method: 'GET', path: '/', handler: function (request, reply) { } });
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/{p*}', handler: (request) => request.query.a });
- server.ext('onRequest', function (request, reply) {
+ await server.start();
- request.setMethod();
- });
+ const options = {
+ hostname: 'localhost',
+ port: server.info.port,
+ path: '/path?a=1#ignore',
+ method: 'GET'
+ };
- server.inject('/', function (res) {
+ const team = new Teamwork.Team();
+ const req = Http.request(options, (res) => team.attend(res));
+ req.end();
- expect(res.statusCode).to.equal(500);
- done();
- });
+ const res = await team.work;
+ const payload = await Wreck.read(res);
+ expect(payload.toString()).to.equal('1');
+
+ await server.stop();
});
- it('errors on invalid method type', function (done) {
+ it('handles fragments with ? (no query)', async () => {
- var server = new Hapi.Server({ debug: false });
- server.connection();
- server.route({ method: 'GET', path: '/', handler: function (request, reply) { } });
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/{p*}', handler: (request) => request.path });
- server.ext('onRequest', function (request, reply) {
+ await server.start();
- request.setMethod(42);
- });
+ const options = {
+ hostname: 'localhost',
+ port: server.info.port,
+ path: '/path#ignore?x',
+ method: 'GET'
+ };
- server.inject('/', function (res) {
+ const team = new Teamwork.Team();
+ const req = Http.request(options, (res) => team.attend(res));
+ req.end();
- expect(res.statusCode).to.equal(500);
- done();
- });
+ const res = await team.work;
+ const payload = await Wreck.read(res);
+ expect(payload.toString()).to.equal('/path');
+
+ await server.stop();
});
- });
- describe('setUrl()', function () {
+ it('handles absolute URL (proxy)', async () => {
- it('parses nested query string', function (done) {
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/{p*}', handler: (request) => request.query.a.join() });
- var handler = function (request, reply) {
+ await server.start();
- return reply(request.query);
+ const options = {
+ hostname: 'localhost',
+ port: server.info.port,
+ path: 'http://example.com/path?a=1&a=2#ignore',
+ method: 'GET'
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
+ const team = new Teamwork.Team();
+ const req = Http.request(options, (res) => team.attend(res));
+ req.end();
- server.inject('/?a[b]=5&d[ff]=ok', function (res) {
+ const res = await team.work;
+ const payload = await Wreck.read(res);
+ expect(payload.toString()).to.equal('1,2');
- expect(res.result).to.deep.equal({ a: { b: '5' }, d: { ff: 'ok' } });
- done();
- });
+ await server.stop();
});
+ });
- it('sets url, path, and query', function (done) {
+ describe('url', () => {
- var url = 'http://localhost/page?param1=something';
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: function (request, reply) { } });
+ it('generates URL object lazily', async () => {
- server.ext('onRequest', function (request, reply) {
+ const server = Hapi.server();
- request.setUrl(url);
- return reply([request.url.href, request.path, request.query.param1].join('|'));
- });
+ const handler = (request) => {
- server.inject('/', function (res) {
+ expect(request._url).to.not.exist();
+ return request.url.pathname;
+ };
- expect(res.payload).to.equal(url + '|/page|something');
- done();
- });
+ server.route({ path: '/test', method: 'GET', handler });
+ const res = await server.inject('/test?a=1');
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('/test');
});
- it('normalizes a path', function (done) {
-
- var rawPath = '/%0%1%2%3%4%5%6%7%8%9%a%b%c%d%e%f%10%11%12%13%14%15%16%17%18%19%1a%1b%1c%1d%1e%1f%20%21%22%23%24%25%26%27%28%29%2a%2b%2c%2d%2e%2f%30%31%32%33%34%35%36%37%38%39%3a%3b%3c%3d%3e%3f%40%41%42%43%44%45%46%47%48%49%4a%4b%4c%4d%4e%4f%50%51%52%53%54%55%56%57%58%59%5a%5b%5c%5d%5e%5f%60%61%62%63%64%65%66%67%68%69%6a%6b%6c%6d%6e%6f%70%71%72%73%74%75%76%77%78%79%7a%7b%7c%7d%7e%7f%80%81%82%83%84%85%86%87%88%89%8a%8b%8c%8d%8e%8f%90%91%92%93%94%95%96%97%98%99%9a%9b%9c%9d%9e%9f%a0%a1%a2%a3%a4%a5%a6%a7%a8%a9%aa%ab%ac%ad%ae%af%b0%b1%b2%b3%b4%b5%b6%b7%b8%b9%ba%bb%bc%bd%be%bf%c0%c1%c2%c3%c4%c5%c6%c7%c8%c9%ca%cb%cc%cd%ce%cf%d0%d1%d2%d3%d4%d5%d6%d7%d8%d9%da%db%dc%dd%de%df%e0%e1%e2%e3%e4%e5%e6%e7%e8%e9%ea%eb%ec%ed%ee%ef%f0%f1%f2%f3%f4%f5%f6%f7%f8%f9%fa%fb%fc%fd%fe%ff%0%1%2%3%4%5%6%7%8%9%A%B%C%D%E%F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20%21%22%23%24%25%26%27%28%29%2A%2B%2C%2D%2E%2F%30%31%32%33%34%35%36%37%38%39%3A%3B%3C%3D%3E%3F%40%41%42%43%44%45%46%47%48%49%4A%4B%4C%4D%4E%4F%50%51%52%53%54%55%56%57%58%59%5A%5B%5C%5D%5E%5F%60%61%62%63%64%65%66%67%68%69%6A%6B%6C%6D%6E%6F%70%71%72%73%74%75%76%77%78%79%7A%7B%7C%7D%7E%7F%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF';
- var normPath = '/%0%1%2%3%4%5%6%7%8%9%a%b%c%d%e%f%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20!%22%23$%25&\'()*+,-.%2F0123456789:;%3C=%3E%3F@ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5C%5D%5E_%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D~%7F%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF%0%1%2%3%4%5%6%7%8%9%A%B%C%D%E%F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20!%22%23$%25&\'()*+,-.%2F0123456789:;%3C=%3E%3F@ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5C%5D%5E_%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D~%7F%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF';
-
- var url = 'http://localhost' + rawPath + '?param1=something';
-
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: function (request, reply) { } });
+ it('generates URL object lazily (no host header)', async () => {
- server.ext('onRequest', function (request, reply) {
+ const server = Hapi.server();
- request.setUrl(url);
- return reply([request.url.href, request.path, request.query.param1].join('|'));
- });
+ const handler = (request) => {
- server.inject('/', function (res) {
+ delete request.info.host;
+ expect(request._url).to.not.exist();
+ return request.url.pathname;
+ };
- expect(res.payload).to.equal(url + '|' + normPath + '|something');
- done();
- });
+ server.route({ path: '/test', method: 'GET', handler });
+ const res = await server.inject('/test?a=1');
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('/test');
});
- it('allows missing path', function (done) {
+ it('generates valid URL when server host is IPv6 and host header is absent', async () => {
- var server = new Hapi.Server();
- server.connection();
- server.ext('onRequest', function (request, reply) {
+ const server = Hapi.server({ host: '::1' });
- request.setUrl('');
- return reply.continue();
- });
+ const handler = (request) => {
- server.inject('/', function (res) {
+ delete request.info.host;
+ expect(request._url).to.not.exist();
+ return request.url.host;
+ };
- expect(res.statusCode).to.equal(400);
- done();
- });
+ server.route({ path: '/test', method: 'GET', handler });
+ const res = await server.inject('/test');
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.match(/^\[::1\]:\d+$/);
});
+ });
+
+ describe('_tap()', () => {
- it('strips trailing slash', function (done) {
+ it('listens to request payload read finish', async () => {
- var handler = function (request, reply) {
+ let finish;
+ const ext = (request, h) => {
- return reply();
+ finish = request.events.once('finish');
+ return h.continue;
};
- var server = new Hapi.Server();
- server.connection({ router: { stripTrailingSlash: true } });
- server.route({ method: 'GET', path: '/test', handler: handler });
- server.inject('/test/', function (res) {
+ const server = Hapi.server();
+ server.ext('onRequest', ext);
+ server.route({ method: 'POST', path: '/', options: { handler: () => null, payload: { parse: false } } });
- expect(res.statusCode).to.equal(200);
- done();
- });
+ const payload = '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789';
+ await server.inject({ method: 'POST', url: '/', payload });
+ await finish;
});
- it('does not strip trailing slash on /', function (done) {
+ it('ignores emitter when created for other events', async () => {
- var handler = function (request, reply) {
+ const ext = (request, h) => {
- return reply();
+ request.events;
+ return h.continue;
};
- var server = new Hapi.Server();
- server.connection({ router: { stripTrailingSlash: true } });
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res) {
+ const server = Hapi.server();
+ server.ext('onRequest', ext);
+ server.route({ method: 'POST', path: '/', options: { handler: () => null, payload: { parse: false } } });
- expect(res.statusCode).to.equal(200);
- done();
- });
+ const payload = '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789';
+ await server.inject({ method: 'POST', url: '/', payload });
});
+ });
- it('strips trailing slash with query', function (done) {
+ describe('log()', () => {
- var handler = function (request, reply) {
+ it('outputs log data to debug console', async () => {
- return reply();
- };
+ const handler = (request) => {
- var server = new Hapi.Server();
- server.connection({ router: { stripTrailingSlash: true } });
- server.route({ method: 'GET', path: '/test', handler: handler });
- server.inject('/test/?a=b', function (res) {
+ request.log(['implementation'], 'data');
+ return null;
+ };
- expect(res.statusCode).to.equal(200);
- done();
- });
- });
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
- it('accepts querystring parser options', function (done) {
+ const log = new Promise((resolve) => {
- var url = 'http://localhost/page?a=1&b=1&c=1&d=1&e=1&f=1&g=1&h=1&i=1&j=1&k=1&l=1&m=1&n=1&o=1&p=1&q=1&r=1&s=1&t=1&u=1&v=1&w=1&x=1&y=1&z=1';
- var qsParserOptions = {
- parameterLimit: 26
- };
- var server = new Hapi.Server();
- server.connection();
- server.ext('onRequest', function (request, reply) {
+ const orig = console.error;
+ console.error = function (...args) {
- request.setUrl(url, null, qsParserOptions);
- return reply(request.query);
+ expect(args[0]).to.equal('Debug:');
+ expect(args[1]).to.equal('implementation');
+ expect(args[2]).to.equal('\n data');
+ console.error = orig;
+ resolve();
+ };
});
- server.inject('/', function (res) {
-
- expect(res.result).to.deep.equal({
- a: '1', b: '1', c: '1', d: '1', e: '1', f: '1', g: '1', h: '1', i: '1',
- j: '1', k: '1', l: '1', m: '1', n: '1', o: '1', p: '1', q: '1', r: '1',
- s: '1', t: '1', u: '1', v: '1', w: '1', x: '1', y: '1', z: '1'
- });
- done();
- });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(204);
+ await log;
});
- it('overrides qs settings', function (done) {
+ it('emits a request event', async () => {
- var server = new Hapi.Server();
- server.connection({
- query: {
- qs: {
- parseArrays: false
- }
- }
- });
+ const server = Hapi.server();
- server.route({
- method: 'GET',
- path: '/',
- config: {
- handler: function (request, reply) {
+ const handler = async (request) => {
- return reply(request.query);
- }
- }
- });
+ const log = server.events.once({ name: 'request', channels: 'app' });
+ request.log(['test'], 'data');
+ const [, event, tags] = await log;
+ expect(event).to.contain(['request', 'timestamp', 'tags', 'data', 'channel']);
+ expect(event.data).to.equal('data');
+ expect(event.channel).to.equal('app');
+ expect(tags).to.equal({ test: true });
+ return null;
+ };
- server.inject('/?a[0]=b&a[1]=c', function (res) {
+ server.route({ method: 'GET', path: '/', handler });
- expect(res.result).to.deep.equal({ a: { 0: 'b', 1: 'c' } });
- done();
- });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(204);
});
- });
-
- describe('log()', { parallel: false }, function () {
-
- it('outputs log data to debug console', function (done) {
- var handler = function (request, reply) {
+ it('emits a request event (function data + collect)', async () => {
- request.log(['implementation'], 'data');
- return reply();
- };
+ const server = Hapi.server({ routes: { log: { collect: true } } });
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
+ const handler = async (request) => {
- var orig = console.error;
- console.error = function () {
+ const log = server.events.once('request');
+ request.log(['test'], () => 'data');
- expect(arguments[0]).to.equal('Debug:');
- expect(arguments[1]).to.equal('implementation');
- expect(arguments[2]).to.equal('\n data');
- console.error = orig;
- done();
+ const [, event, tags] = await log;
+ expect(event).to.contain(['request', 'timestamp', 'tags', 'data', 'channel']);
+ expect(event.data).to.equal('data');
+ expect(event.channel).to.equal('app');
+ expect(tags).to.equal({ test: true });
+ expect(request.logs[0].data).to.equal('data');
+ return null;
};
- server.inject('/', function (res) {
+ server.route({ method: 'GET', path: '/', handler });
- expect(res.statusCode).to.equal(200);
- });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(204);
});
- it('emits a request event', function (done) {
+ it('emits a request event (function data)', async () => {
- var handler = function (request, reply) {
+ const server = Hapi.server();
- server.on('request', function (req, event, tags) {
+ const handler = async (request) => {
- expect(event).to.contain(['request', 'timestamp', 'tags', 'data', 'internal']);
- expect(event.data).to.equal('data');
- expect(event.internal).to.be.false();
- expect(tags).to.deep.equal({ test: true });
- return reply();
- });
+ const log = server.events.once('request');
+ request.log(['test'], () => 'data');
- request.log(['test'], 'data');
+ const [, event, tags] = await log;
+ expect(event).to.contain(['request', 'timestamp', 'tags', 'data', 'channel']);
+ expect(event.data).to.equal('data');
+ expect(event.channel).to.equal('app');
+ expect(tags).to.equal({ test: true });
+ return null;
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
+ server.route({ method: 'GET', path: '/', handler });
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(200);
- done();
- });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(204);
});
- it('outputs log to debug console without data', function (done) {
+ it('outputs log to debug console without data', async () => {
- var handler = function (request, reply) {
+ const handler = (request) => {
request.log(['implementation']);
- return reply();
+ return null;
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
- var orig = console.error;
- console.error = function () {
-
- expect(arguments[0]).to.equal('Debug:');
- expect(arguments[1]).to.equal('implementation');
- expect(arguments[2]).to.equal('');
- console.error = orig;
- done();
- };
+ const log = new Promise((resolve) => {
- server.inject('/', function (res) {
+ const orig = console.error;
+ console.error = function (...args) {
- expect(res.statusCode).to.equal(200);
+ expect(args[0]).to.equal('Debug:');
+ expect(args[1]).to.equal('implementation');
+ expect(args[2]).to.equal('');
+ console.error = orig;
+ resolve();
+ };
});
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(204);
+ await log;
});
- it('outputs log to debug console with error data', function (done) {
+ it('outputs log to debug console with error data', async () => {
- var handler = function (request, reply) {
+ const handler = (request) => {
request.log(['implementation'], new Error('boom'));
- return reply();
+ return null;
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
-
- var orig = console.error;
- console.error = function () {
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
- expect(arguments[0]).to.equal('Debug:');
- expect(arguments[1]).to.equal('implementation');
- expect(arguments[2]).to.contain('Error: boom');
- console.error = orig;
- done();
- };
+ const log = new Promise((resolve) => {
- server.inject('/', function (res) {
+ const orig = console.error;
+ console.error = function (...args) {
- expect(res.statusCode).to.equal(200);
+ expect(args[0]).to.equal('Debug:');
+ expect(args[1]).to.equal('implementation');
+ expect(args[2]).to.contain('Error: boom');
+ console.error = orig;
+ resolve();
+ };
});
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(204);
+ await log;
});
- it('handles invalid log data object stringify', function (done) {
+ it('handles invalid log data object stringify', async () => {
- var handler = function (request, reply) {
+ const handler = (request) => {
- var obj = {};
+ const obj = {};
obj.a = obj;
request.log(['implementation'], obj);
- return reply();
+ return null;
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
-
- var orig = console.error;
- console.error = function () {
+ const server = Hapi.server({ routes: { log: { collect: true } } });
+ server.route({ method: 'GET', path: '/', handler });
- console.error = orig;
- expect(arguments[0]).to.equal('Debug:');
- expect(arguments[1]).to.equal('implementation');
- expect(arguments[2]).to.equal('\n [Cannot display object: Converting circular structure to JSON]');
- done();
- };
+ const log = new Promise((resolve) => {
- server.inject('/', function (res) {
+ const orig = console.error;
+ console.error = function (...args) {
- expect(res.statusCode).to.equal(200);
+ expect(args[0]).to.equal('Debug:');
+ expect(args[1]).to.equal('implementation');
+ expect(args[2]).to.match(/Cannot display object: Converting circular structure to JSON/);
+ console.error = orig;
+ resolve();
+ };
});
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(204);
+ await log;
});
- it('adds a log event to the request', function (done) {
+ it('adds a log event to the request', async () => {
- var handler = function (request, reply) {
+ const handler = (request) => {
- request.log('1', 'log event 1', Date.now());
- request.log(['2'], 'log event 2', new Date(Date.now()));
+ request.log('1', 'log event 1');
+ request.log(['2'], 'log event 2');
request.log(['3', '4']);
request.log(['1', '4']);
request.log(['2', '3']);
request.log(['4']);
request.log('4');
- return reply([request.getLog('1').length, request.getLog('4').length, request.getLog(['4']).length, request.getLog('0').length, request.getLog(['1', '2', '3', '4']).length, request.getLog().length >= 7].join('|'));
+ return request.logs.map((event) => event.tags).join('|');
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject('/', function (res) {
+ const server = Hapi.server({ routes: { log: { collect: true } } });
+ server.route({ method: 'GET', path: '/', handler });
- expect(res.payload).to.equal('2|4|4|0|7|true');
- done();
- });
+ const res = await server.inject('/');
+ expect(res.payload).to.equal('1|2|3,4|1,4|2,3|4|4');
});
- it('does not output events when debug disabled', function (done) {
+ it('does not output events when debug disabled', async () => {
- var server = new Hapi.Server({ debug: false });
- server.connection();
+ const server = Hapi.server({ debug: false });
- var i = 0;
- var orig = console.error;
+ let i = 0;
+ const orig = console.error;
console.error = function () {
++i;
};
- var handler = function (request, reply) {
+ const handler = (request) => {
request.log(['implementation']);
- return reply();
+ return null;
};
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject('/', function (res) {
+ server.route({ method: 'GET', path: '/', handler });
- console.error('nothing');
- expect(i).to.equal(1);
- console.error = orig;
- done();
- });
+ await server.inject('/');
+ console.error('nothing');
+ expect(i).to.equal(1);
+ console.error = orig;
});
- it('does not output events when debug.request disabled', function (done) {
+ it('does not output events when debug.request disabled', async () => {
- var server = new Hapi.Server({ debug: { request: false } });
- server.connection();
+ const server = Hapi.server({ debug: { request: false } });
- var i = 0;
- var orig = console.error;
+ let i = 0;
+ const orig = console.error;
console.error = function () {
++i;
};
- var handler = function (request, reply) {
+ const handler = (request) => {
request.log(['implementation']);
- return reply();
+ return null;
};
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject('/', function (res) {
+ server.route({ method: 'GET', path: '/', handler });
- console.error('nothing');
- expect(i).to.equal(1);
- console.error = orig;
- done();
- });
+ await server.inject('/');
+ console.error('nothing');
+ expect(i).to.equal(1);
+ console.error = orig;
});
- it('does not output non-implementation events by default', function (done) {
+ it('does not output non-implementation events by default', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
- var i = 0;
- var orig = console.error;
+ let i = 0;
+ const orig = console.error;
console.error = function () {
++i;
};
- var handler = function (request, reply) {
+ const handler = (request) => {
request.log(['xyz']);
- return reply();
+ return null;
};
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject('/', function (res) {
+ server.route({ method: 'GET', path: '/', handler });
- console.error('nothing');
- expect(i).to.equal(1);
- console.error = orig;
- done();
- });
+ await server.inject('/');
+ console.error('nothing');
+ expect(i).to.equal(1);
+ console.error = orig;
});
- });
- describe('_log()', { parallel: false }, function () {
+ it('logs nothing', async () => {
- it('emits a request-internal event', function (done) {
+ const server = Hapi.server({ debug: false, routes: { log: { collect: false } } });
- var server = new Hapi.Server();
- server.connection();
- server.once('request-internal', function (request, event, tags) {
+ const handler = (request) => {
- expect(tags.received).to.be.true();
- done();
- });
+ expect(request.logs).to.have.length(0);
+ return request.info.acceptEncoding;
+ };
- server.inject('/', function (res) { });
- });
- });
+ server.route({ method: 'GET', path: '/', handler });
- describe('getLog()', function () {
+ const res = await server.inject({ url: '/', headers: { 'accept-encoding': 'a;b' } });
+ expect(res.result).to.equal('identity');
+ });
- it('returns the selected logs', function (done) {
+ it('logs when only collect is true', async () => {
- var handler = function (request, reply) {
+ const server = Hapi.server({ debug: false, routes: { log: { collect: true } } });
- request._log('1');
- request.log('1');
+ const handler = (request) => {
- return reply([request.getLog('1').length, request.getLog('1', true).length, request.getLog('1', false).length, request.getLog(true).length, request.getLog(false).length, request.getLog().length].join('|'));
+ expect(request.logs).to.have.length(1);
+ return request.info.acceptEncoding;
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject('/', function (res) {
+ server.route({ method: 'GET', path: '/', handler });
- expect(res.payload).to.equal('2|1|1|2|1|3');
- done();
- });
+ const res = await server.inject({ url: '/', headers: { 'accept-encoding': 'a;b' } });
+ expect(res.result).to.equal('identity');
});
});
- describe('_setResponse()', function () {
+ describe('_setResponse()', () => {
- it('leaves the response open when the same response is set again', function (done) {
+ it('leaves the response open when the same response is set again', async () => {
- var server = new Hapi.Server();
- server.connection();
- server.ext('onPostHandler', function (request, reply) {
+ const server = Hapi.server();
+ const postHandler = (request) => {
- return reply(request.response);
- });
+ return request.response;
+ };
+
+ server.ext('onPostHandler', postHandler);
- var handler = function (request, reply) {
+ const handler = (request) => {
- var stream = new Stream.Readable();
+ const stream = new Stream.Readable();
stream._read = function (size) {
this.push('value');
this.push(null);
};
- return reply(stream);
+ return stream;
};
- server.route({ method: 'GET', path: '/', handler: handler });
+ server.route({ method: 'GET', path: '/', handler });
- server.inject('/', function (res) {
-
- expect(res.result).to.equal('value');
- done();
- });
+ const res = await server.inject('/');
+ expect(res.result).to.equal('value');
});
- it('leaves the response open when the same response source is set again', function (done) {
+ it('leaves the response open when the same response source is set again', async () => {
- var server = new Hapi.Server();
- server.connection();
- server.ext('onPostHandler', function (request, reply) {
+ const server = Hapi.server();
+ server.ext('onPostHandler', (request) => request.response.source);
- return reply(request.response.source);
- });
-
- var handler = function (request, reply) {
+ const handler = (request) => {
- var stream = new Stream.Readable();
+ const stream = new Stream.Readable();
stream._read = function (size) {
this.push('value');
this.push(null);
};
- return reply(stream);
+ return stream;
};
- server.route({ method: 'GET', path: '/', handler: handler });
+ server.route({ method: 'GET', path: '/', handler });
- server.inject('/', function (res) {
-
- expect(res.result).to.equal('value');
- done();
- });
+ const res = await server.inject('/');
+ expect(res.result).to.equal('value');
});
});
- describe('timeout', { parallel: false }, function () {
+ describe('timeout', () => {
- it('returns server error message when server taking too long', function (done) {
+ it('returns server error message when server taking too long', async () => {
- var timeoutHandler = function (request, reply) { };
+ const handler = async (request) => {
- var server = new Hapi.Server();
- server.connection({ routes: { timeout: { server: 50 } } });
- server.route({ method: 'GET', path: '/timeout', config: { handler: timeoutHandler } });
+ await Hoek.wait(100);
+ return 'too slow';
+ };
- var timer = new Hoek.Bench();
+ const server = Hapi.server({ routes: { timeout: { server: 50 } } });
+ server.route({ method: 'GET', path: '/timeout', handler });
- server.inject('/timeout', function (res) {
+ const timer = new Hoek.Bench();
- expect(res.statusCode).to.equal(503);
- expect(timer.elapsed()).to.be.at.least(45);
- done();
- });
+ const res = await server.inject('/timeout');
+ expect(res.statusCode).to.equal(503);
+ expect(timer.elapsed()).to.be.at.least(49);
});
- it('returns server error message when server timeout happens during request execution (and handler yields)', function (done) {
+ it('returns server error message when server timeout happens during request execution (and handler yields)', async () => {
+
+ const handler = async (request) => {
+
+ await Hoek.wait(20);
+ return null;
+ };
- var slowHandler = function (request, reply) {
+ const server = Hapi.server({ routes: { timeout: { server: 10 } } });
+ server.route({ method: 'GET', path: '/', options: { handler } });
- setTimeout(function () {
+ const postHandler = (request, h) => {
- return reply('Slow');
- }, 30);
+ return h.continue;
};
- var server = new Hapi.Server();
- server.connection({ routes: { timeout: { server: 2 } } });
- server.route({ method: 'GET', path: '/', config: { handler: slowHandler } });
+ server.ext('onPostHandler', postHandler);
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(503);
+ });
- server.inject('/', function (res) {
+ it('returns server error message when server timeout is short and already occurs when request executes', async () => {
- expect(res.statusCode).to.equal(503);
- done();
- });
+ const server = Hapi.server({ routes: { timeout: { server: 2 } } });
+ server.route({ method: 'GET', path: '/', options: { handler: function () { } } });
+ const onRequest = async (request, h) => {
+
+ await Hoek.wait(10);
+ return h.continue;
+ };
+
+ server.ext('onRequest', onRequest);
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(503);
});
- it('returns server error message when server timeout is short and already occurs when request executes', function (done) {
+ it('handles server handler timeout with onPreResponse ext', async () => {
- var server = new Hapi.Server();
- server.connection({ routes: { timeout: { server: 2 } } });
- server.route({ method: 'GET', path: '/', config: { handler: function () { } } });
- server.ext('onRequest', function (request, reply) {
+ const handler = async (request) => {
- setTimeout(function () {
+ await Hoek.wait(20);
+ return null;
+ };
- return reply.continue();
- }, 10);
- });
+ const server = Hapi.server({ routes: { timeout: { server: 10 } } });
+ server.route({ method: 'GET', path: '/', options: { handler } });
+ const preResponse = (request, h) => {
- server.inject('/', function (res) {
+ return h.continue;
+ };
- expect(res.statusCode).to.equal(503);
- done();
- });
+ server.ext('onPreResponse', preResponse);
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(503);
});
- it('handles server handler timeout with onPreResponse ext', function (done) {
+ it('does not return an error response when server is slow but faster than timeout', async () => {
- var handler = function (request, reply) {
+ const slowHandler = async (request) => {
- setTimeout(reply, 20);
+ await Hoek.wait(30);
+ return 'slow';
};
- var server = new Hapi.Server();
- server.connection({ routes: { timeout: { server: 10 } } });
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
- server.ext('onPreResponse', function (request, reply) {
+ const server = Hapi.server({ routes: { timeout: { server: 50 } } });
+ server.route({ method: 'GET', path: '/slow', options: { handler: slowHandler } });
- return reply.continue();
- });
+ const timer = new Hoek.Bench();
+ const res = await server.inject('/slow');
+ expect(timer.elapsed()).to.be.at.least(20);
+ expect(res.statusCode).to.equal(200);
+ });
- server.inject('/', function (res) {
+ it('creates error response when request is aborted while draining payload', async () => {
- expect(res.statusCode).to.equal(503);
- done();
+ const server = Hapi.server({ routes: { timeout: { server: false } } });
+ await server.start();
+
+ const log = server.events.once('response');
+ const ready = new Promise((resolve) => {
+
+ server.ext('onRequest', (request, h) => {
+
+ resolve();
+ return h.continue;
+ });
});
+
+ const req = Http.request({
+ hostname: 'localhost',
+ port: server.info.port,
+ method: 'GET',
+ headers: { 'content-length': 42 }
+ });
+
+ req.on('error', Hoek.ignore);
+ req.flushHeaders();
+
+ await ready;
+ req.destroy();
+ const [request] = await log;
+
+ expect(request.response.output.statusCode).to.equal(499);
+
+ await server.stop({ timeout: 1 });
});
- it('does not return an error response when server is slow but faster than timeout', function (done) {
+ it('returns an unlogged bad request error when parser fails before request is setup', async () => {
- var slowHandler = function (request, reply) {
+ const server = Hapi.server({ routes: { timeout: { server: false } } });
+ await server.start();
- setTimeout(function () {
+ let responseCount = 0;
+ server.events.on('response', () => {
- return reply('Slow');
- }, 30);
- };
+ responseCount += 1;
+ });
+
+ const client = Net.connect(server.info.port);
+ const clientEnded = new Promise((resolve, reject) => {
- var server = new Hapi.Server();
- server.connection({ routes: { timeout: { server: 50 } } });
- server.route({ method: 'GET', path: '/slow', config: { handler: slowHandler } });
+ let response = '';
+ client.on('data', (chunk) => {
- var timer = new Hoek.Bench();
- server.inject('/slow', function (res) {
+ response = response + chunk.toString();
+ });
- expect(timer.elapsed()).to.be.at.least(20);
- expect(res.statusCode).to.equal(200);
- done();
+ client.on('end', () => resolve(response));
+ client.on('error', reject);
});
+
+ await new Promise((resolve) => client.on('connect', resolve));
+ client.write('hello\n\r');
+
+ const clientResponse = await clientEnded;
+ expect(clientResponse).to.contain('400 Bad Request');
+ expect(responseCount).to.equal(0);
+
+ await server.stop({ timeout: 1 });
});
- it('does not return an error when server is responding when the timeout occurs', function (done) {
+ it('returns normal response when parser fails with bad method after request is setup', async () => {
- var respondingHandler = function (request, reply) {
+ const server = Hapi.server({ routes: { timeout: { server: false } } });
+ server.route({ path: '/', method: 'GET', handler: () => 'PAYLOAD' });
+ await server.start();
- var s = new Stream.PassThrough();
- reply(s);
+ const log = server.events.once('response');
+ const client = Net.connect(server.info.port);
+ const clientEnded = Wreck.read(client);
- for (var i = 10000; i > 0; --i) {
- s.write(i.toString());
- }
+ await new Promise((resolve) => client.on('connect', resolve));
+ client.write('GET / HTTP/1.1\r\nHost: test\r\nContent-Length: 0\r\n\r\ninvalid data');
- setTimeout(function () {
+ const [request] = await log;
+ expect(request.response.statusCode).to.equal(200);
+ expect(request.response.source).to.equal('PAYLOAD');
+ const clientResponse = (await clientEnded).toString();
+ expect(clientResponse).to.contain('HTTP/1.1 200 OK');
- s.emit('end');
- }, 40);
- };
+ const nextResponse = clientResponse.slice(clientResponse.indexOf('PAYLOAD') + 7);
+ expect(nextResponse).to.startWith('HTTP/1.1 400 Bad Request');
- var timer = new Hoek.Bench();
+ await server.stop({ timeout: 1 });
+ });
- var server = new Hapi.Server();
- server.connection({ routes: { timeout: { server: 50 } } });
- server.route({ method: 'GET', path: '/responding', config: { handler: respondingHandler } });
- server.start(function (err) {
+ it('returns nothing when parser fails with bad method after request is setup and the connection is closed', async () => {
- expect(err).to.not.exist();
+ const server = Hapi.server({ routes: { timeout: { server: false } } });
+ server.route({ path: '/', method: 'GET', handler: (request, h) => {
- var options = {
- hostname: '127.0.0.1',
- port: server.info.port,
- path: '/responding',
- method: 'GET'
- };
+ request.raw.res.destroy();
+ return h.abandon;
+ } });
+
+ await server.start();
+
+ const log = server.events.once('response');
+ const client = Net.connect(server.info.port);
+ const clientEnded = Wreck.read(client);
- var req = Http.request(options, function (res) {
+ await new Promise((resolve) => client.on('connect', resolve));
+ client.write('GET / HTTP/1.1\r\nHost: test\r\nContent-Length: 0\r\n\r\n\r\ninvalid data');
- expect(timer.elapsed()).to.be.at.least(60);
- expect(res.statusCode).to.equal(200);
- server.stop({ timeout: 1 }, done);
+ const [request] = await log;
+ expect(request.response.statusCode).to.be.undefined();
+ const clientResponse = (await clientEnded).toString();
+ expect(clientResponse).to.equal('');
+
+ await server.stop({ timeout: 1 });
+ });
+
+ it('returns a bad request when parser fails after request is setup (cleanStop false)', async () => {
+
+ const server = Hapi.server({ routes: { timeout: { server: false } }, operations: { cleanStop: false } });
+ server.route({ path: '/', method: 'GET', handler: Hoek.block });
+ await server.start();
+
+ const client = Net.connect(server.info.port);
+ const clientEnded = new Promise((resolve, reject) => {
+
+ let response = '';
+ client.on('data', (chunk) => {
+
+ response = response + chunk.toString();
});
- req.write('\n');
+ client.on('end', () => resolve(response));
+ client.on('error', reject);
});
+
+ await new Promise((resolve) => client.on('connect', resolve));
+ client.write('GET / HTTP/1.1\r\nHost: test\nContent-Length: 0\r\n\r\ninvalid data');
+
+ const clientResponse = await clientEnded;
+ expect(clientResponse).to.contain('400 Bad Request');
+
+ await server.stop({ timeout: 1 });
});
- it('does not return an error response when server is slower than timeout but response has started', function (done) {
+ it('returns a bad request for POST request when chunked parsing fails', async () => {
- var streamHandler = function (request, reply) {
+ const server = Hapi.server({ routes: { timeout: { server: false } } });
+ server.route({ path: '/', method: 'POST', handler: () => 'ok', options: { payload: { parse: true } } });
+ await server.start();
- var TestStream = function () {
+ const log = server.events.once('response');
+ const client = Net.connect(server.info.port);
+ const clientEnded = Wreck.read(client);
- Stream.Readable.call(this);
- };
+ await new Promise((resolve) => client.on('connect', resolve));
+ client.write('POST / HTTP/1.1\r\nHost: test\r\nTransfer-Encoding: chunked\r\n\r\n');
+ await Hoek.wait(10);
+ client.write('not chunked\r\n');
+
+ const [request] = await log;
+ expect(request.response.statusCode).to.equal(400);
+ expect(request.response.source).to.contain({ error: 'Bad Request' });
+ const clientResponse = (await clientEnded).toString();
+ expect(clientResponse).to.contain('400 Bad Request');
+
+ await server.stop({ timeout: 1 });
+ });
+
+ it('returns a bad request for POST request when chunked parsing fails (cleanStop false)', async () => {
+
+ const server = Hapi.server({ routes: { timeout: { server: false } }, operations: { cleanStop: false } });
+ server.route({ path: '/', method: 'POST', handler: () => 'ok', options: { payload: { parse: true } } });
+ await server.start();
- Hoek.inherits(TestStream, Stream.Readable);
+ const client = Net.connect(server.info.port);
+ const clientEnded = Wreck.read(client);
- TestStream.prototype._read = function (size) {
+ await new Promise((resolve) => client.on('connect', resolve));
+ client.write('POST / HTTP/1.1\r\nHost: test\r\nTransfer-Encoding: chunked\r\n\r\n');
+ await Hoek.wait(10);
+ client.write('not chunked\r\n');
- var self = this;
+ const clientResponse = (await clientEnded).toString();
+ expect(clientResponse).to.contain('400 Bad Request');
+
+ await server.stop({ timeout: 1 });
+ });
+
+ it('returns a bad request for POST request when chunked parsing fails', async () => {
+
+ const server = Hapi.server({ routes: { timeout: { server: false } } });
+ server.route({ path: '/', method: 'POST', handler: () => 'ok', options: { payload: { parse: true } } });
+ await server.start();
+
+ const log = server.events.once('response');
+ const client = Net.connect(server.info.port);
+ const clientEnded = Wreck.read(client);
+
+ await new Promise((resolve) => client.on('connect', resolve));
+ client.write('POST / HTTP/1.1\r\nHost: test\r\nContent-Length: 5\r\n\r\n');
+ await Hoek.wait(10);
+ client.write('111A1'); // Doesn't work if 'A' is replaced with '1' !?!
+ client.write('\Q\r\n'); // Extra bytes considered to be start of next request
+ client.end();
+
+ const [request] = await log;
+ expect(request.response.statusCode).to.equal(400);
+ expect(request.response.source).to.contain({ error: 'Bad Request' });
+ const clientResponse = (await clientEnded).toString();
+ expect(clientResponse).to.contain('400 Bad Request');
+
+ await server.stop({ timeout: 1 });
+ });
+
+ it('does not return an error when server is responding when the timeout occurs', async () => {
+
+ let ended = false;
+ const TestStream = class extends Stream.Readable {
+
+ _read(size) {
if (this.isDone) {
return;
}
+
this.isDone = true;
+ this.push('Hello');
+
+ setTimeout(() => {
+
+ this.push(null);
+ ended = true;
+ }, 150);
+ }
+ };
+
+ const handler = (request) => {
+
+ return new TestStream();
+ };
+
+ const timer = new Hoek.Bench();
+
+ const server = Hapi.server({ routes: { timeout: { server: 100 } } });
+ server.route({ method: 'GET', path: '/', handler });
+ await server.start();
+ const { res } = await Wreck.get('http://localhost:' + server.info.port);
+ expect(ended).to.be.true();
+ expect(timer.elapsed()).to.be.at.least(150);
+ expect(res.statusCode).to.equal(200);
+ await server.stop({ timeout: 1 });
+ });
+
+ it('does not return an error response when server is slower than timeout but response has started', async () => {
+
+ const streamHandler = (request) => {
+
+ const TestStream = class extends Stream.Readable {
- setTimeout(function () {
+ _read(size) {
- self.push('Hello');
- }, 30);
+ if (this.isDone) {
+ return;
+ }
- setTimeout(function () {
+ this.isDone = true;
- self.push(null);
- }, 60);
+ setTimeout(() => {
+
+ this.push('Hello');
+ }, 30);
+
+ setTimeout(() => {
+
+ this.push(null);
+ }, 60);
+ }
};
- return reply(new TestStream());
+ return new TestStream();
};
- var server = new Hapi.Server();
- server.connection({ routes: { timeout: { server: 50 } } });
- server.route({ method: 'GET', path: '/stream', config: { handler: streamHandler } });
- server.start(function (err) {
+ const server = Hapi.server({ routes: { timeout: { server: 50 } } });
+ server.route({ method: 'GET', path: '/stream', options: { handler: streamHandler } });
- expect(err).to.not.exist();
+ await server.start();
+ const { res } = await Wreck.get(`http://localhost:${server.info.port}/stream`);
+ expect(res.statusCode).to.equal(200);
+ await server.stop({ timeout: 1 });
+ });
- var options = {
- hostname: '127.0.0.1',
- port: server.info.port,
- path: '/stream',
- method: 'GET'
- };
+ it('does not return an error response when server takes less than timeout to respond', async () => {
- var req = Http.request(options, function (res) {
+ const server = Hapi.server({ routes: { timeout: { server: 50 } } });
+ server.route({ method: 'GET', path: '/fast', handler: () => 'Fast' });
- expect(res.statusCode).to.equal(200);
- server.stop({ timeout: 1 }, done);
- });
- req.end();
- });
+ const res = await server.inject('/fast');
+ expect(res.statusCode).to.equal(200);
});
- it('does not return an error response when server takes less than timeout to respond', function (done) {
+ it('handles race condition between equal client and server timeouts', async (flags) => {
- var fastHandler = function (request, reply) {
+ const onCleanup = [];
+ flags.onCleanup = async () => {
- return reply('Fast');
+ for (const cleanup of onCleanup) {
+ await cleanup();
+ }
};
- var server = new Hapi.Server();
- server.connection({ routes: { timeout: { server: 50 } } });
- server.route({ method: 'GET', path: '/fast', config: { handler: fastHandler } });
+ const server = Hapi.server({ routes: { timeout: { server: 100 }, payload: { timeout: 100 } } });
+ server.route({ method: 'POST', path: '/timeout', options: { handler: Hoek.block } });
- server.inject('/fast', function (res) {
+ await server.start();
+ onCleanup.unshift(() => server.stop());
- expect(res.statusCode).to.equal(200);
- done();
- });
- });
+ const timer = new Hoek.Bench();
+ const options = {
+ hostname: 'localhost',
+ port: server.info.port,
+ path: '/timeout',
+ method: 'POST'
+ };
- it('handles race condition between equal client and server timeouts', function (done) {
+ const req = Http.request(options);
+ onCleanup.unshift(() => req.destroy());
- var timeoutHandler = function (request, reply) { };
+ req.write('\n');
- var server = new Hapi.Server();
- server.connection({ routes: { timeout: { server: 50 }, payload: { timeout: 50 } } });
- server.route({ method: 'POST', path: '/timeout', config: { handler: timeoutHandler } });
+ const [res] = await Events.once(req, 'response');
- server.start(function (err) {
+ expect([503, 408]).to.contain(res.statusCode);
+ expect(timer.elapsed()).to.be.at.least(80);
- expect(err).to.not.exist();
+ await Events.once(req, 'close'); // Ensures that req closes without error
+ });
+ });
- var timer = new Hoek.Bench();
- var options = {
- hostname: '127.0.0.1',
- port: server.info.port,
- path: '/timeout',
- method: 'POST'
- };
+ describe('event()', () => {
- var req = Http.request(options, function (res) {
+ it('does not emit request error on normal close', async () => {
- expect([503, 408]).to.contain(res.statusCode);
- expect(timer.elapsed()).to.be.at.least(45);
- server.stop({ timeout: 1 }, done);
- });
+ const server = Hapi.server();
+ const events = [];
+ server.events.on('request', (request, event, tags) => events.push(tags));
- req.on('error', function (err) {
+ server.route({ method: 'GET', path: '/', handler: () => 'ok' });
- });
+ await server.start();
- req.write('\n');
- setTimeout(function () {
+ const { payload } = await Wreck.get('http://localhost:' + server.info.port);
+ expect(payload.toString()).to.equal('ok');
+ await server.stop();
- req.end();
- }, 100);
- });
+ expect(events).to.have.length(0);
});
});
});
diff --git a/test/response.js b/test/response.js
index 3d1a92a8a..61056a1ba 100755
--- a/test/response.js
+++ b/test/response.js
@@ -1,37 +1,35 @@
-// Load modules
+'use strict';
-var Stream = require('stream');
-var Bluebird = require('bluebird');
-var Boom = require('boom');
-var Code = require('code');
-var Handlebars = require('handlebars');
-var Hapi = require('..');
-var Hoek = require('hoek');
-var Inert = require('inert');
-var Lab = require('lab');
-var Vision = require('vision');
+const Events = require('events');
+const Http = require('http');
+const Path = require('path');
+const Stream = require('stream');
+const Code = require('@hapi/code');
+const Handlebars = require('handlebars');
+const LegacyReadableStream = require('legacy-readable-stream');
+const Hapi = require('..');
+const Inert = require('@hapi/inert');
+const Lab = require('@hapi/lab');
+const Vision = require('@hapi/vision');
-// Declare internals
+const Response = require('../lib/response');
-var internals = {};
+const internals = {};
-// Test shortcuts
-var lab = exports.lab = Lab.script();
-var describe = lab.describe;
-var it = lab.it;
-var expect = Code.expect;
+const { describe, it } = exports.lab = Lab.script();
+const expect = Code.expect;
-describe('Response', function () {
+describe('Response', () => {
- it('returns a reply', function (done) {
+ it('returns a response', async () => {
- var handler = function (request, reply) {
+ const handler = (request, h) => {
- return reply('text')
+ return h.response('text')
.type('text/plain')
.charset('ISO-8859-1')
.ttl(1000)
@@ -44,1018 +42,1547 @@ describe('Response', function () {
.header('combo', 'o')
.header('combo', 'k', { append: true, separator: '-' })
.header('combo', 'bad', { override: false })
- .code(200);
+ .code(200)
+ .message('Super');
};
- var server = new Hapi.Server();
- server.connection({ routes: { cors: true } });
- server.route({ method: 'GET', path: '/', config: { handler: handler, cache: { expiresIn: 9999 } } });
+ const server = Hapi.server({ compression: { minBytes: 1 } });
+ server.route({ method: 'GET', path: '/', options: { handler, cache: { expiresIn: 9999 } } });
server.state('sid', { encoding: 'base64' });
server.state('always', { autoValue: 'present' });
- server.ext('onPostHandler', function (request, reply) {
- reply.state('test', '123');
- reply.unstate('empty');
- return reply.continue();
- });
+ const postHandler = (request, h) => {
- server.inject('/', function (res) {
+ h.state('test', '123');
+ h.unstate('empty', { path: '/path' });
+ return h.continue;
+ };
- expect(res.statusCode).to.equal(200);
- expect(res.result).to.exist();
- expect(res.result).to.equal('text');
- expect(res.headers['cache-control']).to.equal('max-age=1, must-revalidate, private');
- expect(res.headers['content-type']).to.equal('text/plain; something=something, charset=ISO-8859-1');
- expect(res.headers['access-control-allow-origin']).to.equal('*');
- expect(res.headers['access-control-allow-credentials']).to.not.exist();
- expect(res.headers['access-control-allow-methods']).to.equal('GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS');
- expect(res.headers['set-cookie']).to.deep.equal(['abc=123', 'sid=YWJjZGVmZzEyMzQ1Ng==', 'other=something; Secure', 'x=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT', 'test=123', 'empty=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT', 'always=present']);
- expect(res.headers.vary).to.equal('x-control,origin');
- expect(res.headers.combo).to.equal('o-k');
- done();
- });
+ server.ext('onPostHandler', postHandler);
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('text');
+ expect(res.statusMessage).to.equal('Super');
+ expect(res.headers['cache-control']).to.equal('max-age=1, must-revalidate, private');
+ expect(res.headers['content-type']).to.equal('text/plain; something=something; charset=ISO-8859-1');
+ expect(res.headers['set-cookie']).to.equal(['abc=123', 'sid=YWJjZGVmZzEyMzQ1Ng==; Secure; HttpOnly; SameSite=Strict', 'other=something; Secure; HttpOnly; SameSite=Strict', 'x=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; HttpOnly; SameSite=Strict', 'test=123; Secure; HttpOnly; SameSite=Strict', 'empty=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; HttpOnly; SameSite=Strict; Path=/path', 'always=present; Secure; HttpOnly; SameSite=Strict']);
+ expect(res.headers.vary).to.equal('x-control,accept-encoding');
+ expect(res.headers.combo).to.equal('o-k');
});
- describe('header()', function () {
-
- it('appends to set-cookie header', function (done) {
+ it('sets content-type charset (trailing semi column)', async () => {
- var handler = function (request, reply) {
+ const handler = (request, h) => {
- return reply('ok').header('set-cookie', 'A').header('set-cookie', 'B', { append: true });
- };
+ return h.response('text').header('Content-Type', 'text/plain; something=something;');
+ };
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res) {
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
- expect(res.statusCode).to.equal(200);
- expect(res.headers['set-cookie']).to.deep.equal(['A', 'B']);
- done();
- });
- });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers['content-type']).to.equal('text/plain; something=something; charset=utf-8');
});
- describe('created()', function () {
-
- it('returns a stream reply (created)', function (done) {
+ describe('_setSource()', () => {
- var handler = function (request, reply) {
+ it('returns an empty string response', async () => {
- return reply({ a: 1 }).created('/special');
- };
+ const server = Hapi.server();
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: () => ''
+ });
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'POST', path: '/', handler: handler });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(204);
+ expect(res.headers['content-length']).to.not.exist();
+ expect(res.headers['content-type']).to.equal('text/html; charset=utf-8');
+ expect(res.result).to.equal(null);
+ expect(res.payload).to.equal('');
+ });
- server.inject({ method: 'POST', url: '/' }, function (res) {
+ it('returns a null response', async () => {
- expect(res.result).to.deep.equal({ a: 1 });
- expect(res.statusCode).to.equal(201);
- expect(res.headers.location).to.equal('/special');
- expect(res.headers['cache-control']).to.equal('no-cache');
- done();
+ const server = Hapi.server();
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: () => null
});
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(204);
+ expect(res.headers['content-length']).to.not.exist();
+ expect(res.headers['content-type']).to.not.exist();
+ expect(res.result).to.equal(null);
+ expect(res.payload).to.equal('');
});
- it('returns error on created with GET', function (done) {
+ it('returns a stream', async () => {
- var handler = function (request, reply) {
+ const handler = (request) => {
+
+ const stream = new Stream.Readable({
+ read() {
+
+ this.push('x');
+ this.push(null);
+ }
+ });
- return reply().created('/something');
+ return stream;
};
- var server = new Hapi.Server({ debug: false });
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
- server.inject('/', function (res) {
+ const res = await server.inject('/');
+ expect(res.result).to.equal('x');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers['content-type']).to.equal('application/octet-stream');
+ });
+ });
- expect(res.statusCode).to.equal(500);
- done();
- });
+ describe('code()', () => {
+
+ it('sets manual code regardless of emptyStatusCode override', async () => {
+
+ const server = Hapi.server({ routes: { response: { emptyStatusCode: 200 } } });
+ server.route({ method: 'GET', path: '/', handler: (request, h) => h.response().code(204) });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(204);
});
});
- describe('state()', function () {
+ describe('header()', () => {
- it('returns an error on bad cookie', function (done) {
+ it('appends to set-cookie header', async () => {
- var handler = function (request, reply) {
+ const handler = (request, h) => {
- return reply('text').state(';sid', 'abcdefg123456');
+ return h.response('ok').header('set-cookie', 'A').header('set-cookie', 'B', { append: true });
};
- var server = new Hapi.Server({ debug: false });
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers['set-cookie']).to.equal(['A', 'B']);
+ });
- server.inject('/', function (res) {
+ it('sets null header', async () => {
- expect(res.result).to.exist();
- expect(res.statusCode).to.equal(500);
- expect(res.result.message).to.equal('An internal server error occurred');
- expect(res.headers['set-cookie']).to.not.exist();
- done();
- });
+ const handler = (request, h) => {
+
+ return h.response('ok').header('set-cookie', null);
+ };
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers['set-cookie']).to.not.exist();
});
- });
- describe('unstate()', function () {
+ it('throws error on non-ascii value', async () => {
+
+ const handler = (request, h) => {
- it('allows options', function (done) {
+ return h.response('ok').header('set-cookie', decodeURIComponent('%E0%B4%8Aset-cookie:%20foo=bar'));
+ };
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
+ });
- var handler = function (request, reply) {
+ it('throws error on non-ascii value (header name)', async () => {
- return reply().unstate('session', { path: '/unset', isSecure: true });
+ const handler = (request, h) => {
+
+ const badName = decodeURIComponent('%E0%B4%8Aset-cookie:%20foo=bar');
+ return h.response('ok').header(badName, 'value');
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
+ });
- server.inject('/', function (unset) {
+ it('throws error on non-ascii value (buffer)', async () => {
- expect(unset.statusCode).to.equal(200);
- expect(unset.headers['set-cookie']).to.deep.equal(['session=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; Path=/unset']);
- done();
- });
+ const handler = (request, h) => {
+
+ return h.response('ok').header('set-cookie', Buffer.from(decodeURIComponent('%E0%B4%8Aset-cookie:%20foo=bar')));
+ };
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
});
});
- describe('vary()', function () {
+ describe('created()', () => {
- it('sets Vary header with single value', function (done) {
+ it('returns a response (created)', async () => {
- var handler = function (request, reply) {
+ const handler = (request, h) => {
- return reply('ok').vary('x');
+ return h.response({ a: 1 }).created('/special');
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
+ const server = Hapi.server();
+ server.route({ method: 'POST', path: '/', handler });
- server.inject('/', function (res) {
-
- expect(res.result).to.equal('ok');
- expect(res.statusCode).to.equal(200);
- expect(res.headers.vary).to.equal('x');
- done();
- });
+ const res = await server.inject({ method: 'POST', url: '/' });
+ expect(res.result).to.equal({ a: 1 });
+ expect(res.statusCode).to.equal(201);
+ expect(res.headers.location).to.equal('/special');
+ expect(res.headers['cache-control']).to.equal('no-cache');
});
- it('sets Vary header with multiple values', function (done) {
+ it('returns error on created with GET', async () => {
- var handler = function (request, reply) {
+ const handler = (request, h) => {
- return reply('ok').vary('x').vary('y');
+ return h.response().created('/something');
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject('/', function (res) {
+ const server = Hapi.server({ debug: false });
+ server.route({ method: 'GET', path: '/', handler });
- expect(res.result).to.equal('ok');
- expect(res.statusCode).to.equal(200);
- expect(res.headers.vary).to.equal('x,y');
- done();
- });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
});
- it('sets Vary header with *', function (done) {
+ it('does not return an error on created with PUT', async () => {
- var handler = function (request, reply) {
+ const handler = (request, h) => {
- return reply('ok').vary('*');
+ return h.response({ a: 1 }).created();
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject('/', function (res) {
+ const server = Hapi.server();
+ server.route({ method: 'PUT', path: '/', handler });
- expect(res.result).to.equal('ok');
- expect(res.statusCode).to.equal(200);
- expect(res.headers.vary).to.equal('*');
- done();
- });
+ const res = await server.inject({ method: 'PUT', url: '/' });
+ expect(res.result).to.equal({ a: 1 });
+ expect(res.statusCode).to.equal(201);
});
- it('leaves Vary header with * on additional values', function (done) {
+ it('does not return an error on created with PATCH', async () => {
- var handler = function (request, reply) {
+ const handler = (request, h) => {
- return reply('ok').vary('*').vary('x');
+ return h.response({ a: 1 }).created();
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
-
- server.inject('/', function (res) {
+ const server = Hapi.server();
+ server.route({ method: 'PATCH', path: '/', handler });
- expect(res.result).to.equal('ok');
- expect(res.statusCode).to.equal(200);
- expect(res.headers.vary).to.equal('*');
- done();
- });
+ const res = await server.inject({ method: 'PATCH', url: '/' });
+ expect(res.result).to.equal({ a: 1 });
+ expect(res.statusCode).to.equal(201);
});
+ });
- it('drops other Vary header values when set to *', function (done) {
+ describe('state()', () => {
- var handler = function (request, reply) {
+ it('returns an error on bad cookie', async () => {
- return reply('ok').vary('x').vary('*');
- };
+ const handler = (request, h) => {
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
+ return h.response('text').state(';sid', 'abcdefg123456');
+ };
- server.inject('/', function (res) {
+ const server = Hapi.server({ debug: false });
+ server.route({ method: 'GET', path: '/', handler });
- expect(res.result).to.equal('ok');
- expect(res.statusCode).to.equal(200);
- expect(res.headers.vary).to.equal('*');
- done();
- });
+ const res = await server.inject('/');
+ expect(res.result).to.exist();
+ expect(res.statusCode).to.equal(500);
+ expect(res.result.message).to.equal('An internal server error occurred');
+ expect(res.headers['set-cookie']).to.not.exist();
});
});
- describe('etag()', function () {
+ describe('unstate()', () => {
- it('sets etag', function (done) {
+ it('allows options', async () => {
- var handler = function (request, reply) {
+ const handler = (request, h) => {
- return reply('ok').etag('abc');
+ return h.response().unstate('session', { path: '/unset', isSecure: true });
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res) {
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
- expect(res.statusCode).to.equal(200);
- expect(res.headers.etag).to.equal('"abc"');
- done();
- });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(204);
+ expect(res.headers['set-cookie']).to.equal(['session=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; HttpOnly; SameSite=Strict; Path=/unset']);
});
+ });
+
+ describe('vary()', () => {
- it('sets weak etag', function (done) {
+ it('sets Vary header with single value', async () => {
- var handler = function (request, reply) {
+ const handler = (request, h) => {
- return reply('ok').etag('abc', { weak: true });
+ return h.response('ok').vary('x');
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res) {
+ const server = Hapi.server({ compression: { minBytes: 1 } });
+ server.route({ method: 'GET', path: '/', handler });
- expect(res.statusCode).to.equal(200);
- expect(res.headers.etag).to.equal('W/"abc"');
- done();
- });
+ const res = await server.inject('/');
+ expect(res.result).to.equal('ok');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers.vary).to.equal('x,accept-encoding');
});
- it('ignores varyEtag when etag header is removed', function (done) {
+ it('sets Vary header with multiple values', async () => {
- var handler = function (request, reply) {
+ const handler = (request, h) => {
- var response = reply('ok').etag('abc').vary('x');
- delete response.headers.etag;
+ return h.response('ok').vary('x').vary('y');
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res) {
+ const server = Hapi.server({ compression: { minBytes: 1 } });
+ server.route({ method: 'GET', path: '/', handler });
- expect(res.statusCode).to.equal(200);
- expect(res.headers.etag).to.not.exist();
- done();
- });
+ const res = await server.inject('/');
+ expect(res.result).to.equal('ok');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers.vary).to.equal('x,y,accept-encoding');
});
- it('leaves etag header when varyEtag is false', function (done) {
+ it('sets Vary header with *', async () => {
- var handler = function (request, reply) {
+ const handler = (request, h) => {
- return reply('ok').etag('abc', { vary: false }).vary('x');
+ return h.response('ok').vary('*');
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res1) {
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject('/');
+ expect(res.result).to.equal('ok');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers.vary).to.equal('*');
+ });
+
+ it('leaves Vary header with * on additional values', async () => {
+
+ const handler = (request, h) => {
- expect(res1.statusCode).to.equal(200);
- expect(res1.headers.etag).to.equal('"abc"');
+ return h.response('ok').vary('*').vary('x');
+ };
- server.inject({ url: '/', headers: { 'if-none-match': '"abc-gzip"', 'accept-encoding': 'gzip' } }, function (res2) {
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
- expect(res2.statusCode).to.equal(200);
- expect(res2.headers.etag).to.equal('"abc"');
- done();
- });
- });
+ const res = await server.inject('/');
+ expect(res.result).to.equal('ok');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers.vary).to.equal('*');
});
- it('applies varyEtag when returning 304 due to if-modified-since match', function (done) {
+ it('drops other Vary header values when set to *', async () => {
+
+ const handler = (request, h) => {
+
+ return h.response('ok').vary('x').vary('*');
+ };
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject('/');
+ expect(res.result).to.equal('ok');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers.vary).to.equal('*');
+ });
- var mdate = new Date().toUTCString();
+ it('sets Vary header with multiple similar and identical values', async () => {
- var handler = function (request, reply) {
+ const handler = (request, h) => {
- return reply('ok').etag('abc').header('last-modified', mdate);
+ return h.response('ok').vary('x').vary('xyz').vary('xy').vary('x');
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject({ url: '/', headers: { 'if-modified-since': mdate, 'accept-encoding': 'gzip' } }, function (res) {
+ const server = Hapi.server({ compression: { minBytes: 1 } });
+ server.route({ method: 'GET', path: '/', handler });
- expect(res.statusCode).to.equal(304);
- expect(res.headers.etag).to.equal('"abc-gzip"');
- done();
- });
+ const res = await server.inject('/');
+ expect(res.result).to.equal('ok');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers.vary).to.equal('x,xyz,xy,accept-encoding');
});
});
- describe('passThrough()', function () {
+ describe('etag()', () => {
- it('passes stream headers and code through', function (done) {
+ it('sets etag', async () => {
- var TestStream = function () {
+ const handler = (request, h) => {
- Stream.Readable.call(this);
- this.statusCode = 299;
- this.headers = { xcustom: 'some value' };
+ return h.response('ok').etag('abc');
};
- Hoek.inherits(TestStream, Stream.Readable);
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers.etag).to.equal('"abc"');
+ });
- TestStream.prototype._read = function (size) {
+ it('sets weak etag', async () => {
- if (this.isDone) {
- return;
- }
- this.isDone = true;
+ const handler = (request, h) => {
- this.push('x');
- this.push(null);
+ return h.response('ok').etag('abc', { weak: true });
};
- var handler = function (request, reply) {
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers.etag).to.equal('W/"abc"');
+ });
+
+ it('ignores varyEtag when etag header is removed', async () => {
+
+ const handler = (request, h) => {
- return reply(new TestStream());
+ const response = h.response('ok').etag('abc').vary('x');
+ delete response.headers.etag;
+ return response;
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers.etag).to.not.exist();
+ });
- server.inject('/', function (res) {
+ it('leaves etag header when varyEtag is false', async () => {
- expect(res.result).to.equal('x');
- expect(res.statusCode).to.equal(299);
- expect(res.headers.xcustom).to.equal('some value');
- done();
- });
+ const handler = (request, h) => {
+
+ return h.response('ok').etag('abc', { vary: false }).vary('x');
+ };
+
+ const server = Hapi.server({ compression: { minBytes: 1 } });
+ server.route({ method: 'GET', path: '/', handler });
+ const res1 = await server.inject('/');
+ expect(res1.statusCode).to.equal(200);
+ expect(res1.headers.etag).to.equal('"abc"');
+
+ const res2 = await server.inject({ url: '/', headers: { 'if-none-match': '"abc-gzip"', 'accept-encoding': 'gzip' } });
+ expect(res2.statusCode).to.equal(200);
+ expect(res2.headers.etag).to.equal('"abc"');
});
- it('excludes stream headers and code when passThrough is false', function (done) {
+ it('applies varyEtag when returning 304 due to if-modified-since match', async () => {
+
+ const mdate = new Date().toUTCString();
- var TestStream = function () {
+ const handler = (request, h) => {
- Stream.Readable.call(this);
- this.statusCode = 299;
- this.headers = { xcustom: 'some value' };
+ return h.response('ok').etag('abc').header('last-modified', mdate);
};
- Hoek.inherits(TestStream, Stream.Readable);
+ const server = Hapi.server({ compression: { minBytes: 1 } });
+ server.route({ method: 'GET', path: '/', handler });
+ const res = await server.inject({ url: '/', headers: { 'if-modified-since': mdate, 'accept-encoding': 'gzip' } });
+ expect(res.statusCode).to.equal(304);
+ expect(res.headers.etag).to.equal('"abc-gzip"');
+ });
+ });
+
+ describe('passThrough()', () => {
- TestStream.prototype._read = function (size) {
+ it('passes stream headers and code through', async () => {
- if (this.isDone) {
- return;
+ const TestStream = class extends Stream.Readable {
+
+ constructor() {
+
+ super();
+ this.statusCode = 299;
+ this.headers = { xcustom: 'some value', 'content-type': 'something/special' };
}
- this.isDone = true;
- this.push('x');
- this.push(null);
- };
+ _read(size) {
- var handler = function (request, reply) {
+ if (this.isDone) {
+ return;
+ }
- return reply(new TestStream()).passThrough(false);
+ this.isDone = true;
+
+ this.push('x');
+ this.push(null);
+ }
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
+ const handler = (request) => {
+
+ return new TestStream();
+ };
- server.inject('/', function (res) {
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
- expect(res.result).to.equal('x');
- expect(res.statusCode).to.equal(200);
- expect(res.headers.xcustom).to.not.exist();
- done();
- });
+ const res = await server.inject('/');
+ expect(res.result).to.equal('x');
+ expect(res.statusCode).to.equal(299);
+ expect(res.headers.xcustom).to.equal('some value');
+ expect(res.headers['content-type']).to.equal('something/special');
});
- it('ignores stream headers when empty', function (done) {
+ it('excludes connection header and connection options', async () => {
- var TestStream = function () {
+ const upstreamConnectionHeader = 'x-test, x-test-also';
- Stream.Readable.call(this);
- this.statusCode = 299;
- this.headers = {};
- };
+ const TestStream = class extends Stream.Readable {
+
+ constructor() {
+
+ super();
+ this.statusCode = 200;
+ this.headers = {
+ connection: upstreamConnectionHeader,
+ 'x-test': 'something',
+ 'x-test-also': 'also'
+ };
+ }
- Hoek.inherits(TestStream, Stream.Readable);
+ _read(size) {
- TestStream.prototype._read = function (size) {
+ if (this.isDone) {
+ return;
+ }
- if (this.isDone) {
- return;
+ this.isDone = true;
+
+ this.push('x');
+ this.push(null);
}
- this.isDone = true;
+ };
+
+ const handler = (request) => {
- this.push('x');
- this.push(null);
+ return new TestStream();
};
- var handler = function (request, reply) {
+ const server = new Hapi.Server();
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject('/');
+ expect(res.result).to.equal('x');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers.connection).to.not.equal(upstreamConnectionHeader);
+ expect(res.headers['x-test']).to.not.exist();
+ expect(res.headers['x-test-also']).to.not.exist();
+ });
- return reply(new TestStream());
+ it('excludes stream headers and code when passThrough is false', async () => {
+
+ const TestStream = class extends Stream.Readable {
+
+ constructor() {
+
+ super();
+ this.statusCode = 299;
+ this.headers = { xcustom: 'some value' };
+ }
+
+ _read(size) {
+
+ if (this.isDone) {
+ return;
+ }
+
+ this.isDone = true;
+
+ this.push('x');
+ this.push(null);
+ }
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
+ const handler = (request, h) => {
- server.inject('/', function (res) {
+ return h.response(new TestStream()).passThrough(false);
+ };
- expect(res.result).to.equal('x');
- expect(res.statusCode).to.equal(299);
- expect(res.headers.xcustom).to.not.exist();
- done();
- });
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject('/');
+ expect(res.result).to.equal('x');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers.xcustom).to.not.exist();
});
- it('retains local headers with stream headers pass-through', function (done) {
+ it('ignores stream headers when empty', async () => {
- var TestStream = function () {
+ const TestStream = class extends Stream.Readable {
- Stream.Readable.call(this);
- this.headers = { xcustom: 'some value', 'set-cookie': 'a=1' };
- };
+ constructor() {
+
+ super();
+ this.statusCode = 299;
+ this.headers = {};
+ }
+
+ _read(size) {
- Hoek.inherits(TestStream, Stream.Readable);
+ if (this.isDone) {
+ return;
+ }
- TestStream.prototype._read = function (size) {
+ this.isDone = true;
- if (this.isDone) {
- return;
+ this.push('x');
+ this.push(null);
}
- this.isDone = true;
+ };
- this.push('x');
- this.push(null);
+ const handler = (request) => {
+
+ return new TestStream();
};
- var handler = function (request, reply) {
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject('/');
+ expect(res.result).to.equal('x');
+ expect(res.statusCode).to.equal(299);
+ expect(res.headers.xcustom).to.not.exist();
+ });
+
+ it('retains local headers with stream headers pass-through', async () => {
+
+ const TestStream = class extends Stream.Readable {
- return reply(new TestStream()).header('xcustom', 'other value').state('b', '2');
+ constructor() {
+
+ super();
+ this.headers = { xcustom: 'some value', 'set-cookie': 'a=1' };
+ }
+
+ _read(size) {
+
+ if (this.isDone) {
+ return;
+ }
+
+ this.isDone = true;
+
+ this.push('x');
+ this.push(null);
+ }
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
+ const handler = (request, h) => {
- server.inject('/', function (res) {
+ return h.response(new TestStream()).header('xcustom', 'other value').state('b', '2');
+ };
- expect(res.result).to.equal('x');
- expect(res.headers.xcustom).to.equal('other value');
- expect(res.headers['set-cookie']).to.deep.equal(['a=1', 'b=2']);
- done();
- });
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject('/');
+ expect(res.result).to.equal('x');
+ expect(res.headers.xcustom).to.equal('other value');
+ expect(res.headers['set-cookie']).to.equal(['a=1', 'b=2; Secure; HttpOnly; SameSite=Strict']);
});
});
- describe('replacer()', function () {
+ describe('replacer()', () => {
- it('errors when called on wrong type', function (done) {
+ it('errors when called on wrong type', async () => {
- var handler = function (request, reply) {
+ const handler = (request, h) => {
- return reply('x').replacer(['x']);
+ return h.response('x').replacer(['x']);
};
- var server = new Hapi.Server({ debug: false });
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res) {
-
- expect(res.statusCode).to.equal(500);
- done();
- });
+ const server = Hapi.server({ debug: false });
+ server.route({ method: 'GET', path: '/', handler });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
});
});
- describe('spaces()', function () {
+ describe('compressed()', () => {
- it('errors when called on wrong type', function (done) {
+ it('errors on missing encoding', async () => {
- var handler = function (request, reply) {
+ const handler = (request, h) => {
- return reply('x').spaces(2);
+ return h.response('x').compressed();
};
- var server = new Hapi.Server({ debug: false });
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res) {
+ const server = Hapi.server({ debug: false });
+ server.route({ method: 'GET', path: '/', handler });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
+ });
- expect(res.statusCode).to.equal(500);
- done();
- });
+ it('errors on invalid encoding', async () => {
+
+ const handler = (request, h) => {
+
+ return h.response('x').compressed(123);
+ };
+
+ const server = Hapi.server({ debug: false });
+ server.route({ method: 'GET', path: '/', handler });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
});
});
- describe('suffix()', function () {
+ describe('spaces()', () => {
- it('errors when called on wrong type', function (done) {
+ it('errors when called on wrong type', async () => {
- var handler = function (request, reply) {
+ const handler = (request, h) => {
- return reply('x').suffix('x');
+ return h.response('x').spaces(2);
};
- var server = new Hapi.Server({ debug: false });
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res) {
+ const server = Hapi.server({ debug: false });
+ server.route({ method: 'GET', path: '/', handler });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
+ });
+ });
- expect(res.statusCode).to.equal(500);
- done();
- });
+ describe('suffix()', () => {
+
+ it('errors when called on wrong type', async () => {
+
+ const handler = (request, h) => {
+
+ return h.response('x').suffix('x');
+ };
+
+ const server = Hapi.server({ debug: false });
+ server.route({ method: 'GET', path: '/', handler });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
});
});
- describe('type()', function () {
+ describe('escape()', () => {
- it('returns a file in the response with the correct headers using custom mime type', function (done) {
+ it('returns 200 when called with true', async () => {
- var server = new Hapi.Server();
- server.register(Inert, Hoek.ignore);
- server.connection({ routes: { files: { relativeTo: __dirname } } });
- var handler = function (request, reply) {
+ const handler = (request, h) => {
- return reply.file('../LICENSE').type('application/example');
+ return h.response({ x: 'x' }).escape(true);
};
- server.route({ method: 'GET', path: '/file', handler: handler });
+ const server = Hapi.server({ debug: false });
+ server.route({ method: 'GET', path: '/', handler });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ });
- server.inject('/file', function (res) {
+ it('errors when called on wrong type', async () => {
- expect(res.headers['content-type']).to.equal('application/example');
- done();
- });
+ const handler = (request, h) => {
+
+ return h.response('x').escape('x');
+ };
+
+ const server = Hapi.server({ debug: false });
+ server.route({ method: 'GET', path: '/', handler });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
});
});
- describe('redirect()', function () {
+ describe('type()', () => {
- it('returns a redirection reply', function (done) {
+ it('returns a file in the response with the correct headers using custom mime type', async () => {
- var handler = function (request, reply) {
+ const server = Hapi.server({ routes: { files: { relativeTo: Path.join(__dirname, '../') } } });
+ await server.register(Inert);
+ const handler = (request, h) => {
- return reply('Please wait while we send your elsewhere').redirect('/example');
+ return h.file('./LICENSE.md').type('application/example');
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
+ server.route({ method: 'GET', path: '/file', handler });
- server.inject('http://example.org/', function (res) {
+ const res = await server.inject('/file');
+ expect(res.headers['content-type']).to.equal('application/example');
+ });
+ });
- expect(res.result).to.exist();
- expect(res.headers.location).to.equal('/example');
- expect(res.statusCode).to.equal(302);
- done();
- });
+ describe('charset()', () => {
+
+ it('sets charset with default type', async () => {
+
+ const handler = (request, h) => {
+
+ return h.response('text').charset('abc');
+ };
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers['content-type']).to.equal('text/html; charset=abc');
});
- it('returns a redirection reply using verbose call', function (done) {
+ it('sets charset with default type in onPreResponse', async () => {
- var handler = function (request, reply) {
+ const onPreResponse = (request, h) => {
- return reply('We moved!').redirect().location('/examplex');
+ request.response.charset('abc');
+ return h.continue;
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
+ const server = Hapi.server();
+ server.ext('onPreResponse', onPreResponse);
- server.inject('/', function (res) {
+ server.route({ method: 'GET', path: '/', handler: () => 'text' });
- expect(res.result).to.exist();
- expect(res.result).to.equal('We moved!');
- expect(res.headers.location).to.equal('/examplex');
- expect(res.statusCode).to.equal(302);
- done();
- });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers['content-type']).to.equal('text/html; charset=abc');
});
- it('returns a 301 redirection reply', function (done) {
+ it('sets type inside marshal', async () => {
+
+ const handler = (request) => {
+
+ const marshal = (response) => {
- var handler = function (request, reply) {
+ if (!response.headers['content-type']) {
+ response.type('text/html');
+ }
- return reply().redirect('example').permanent().rewritable();
+ return response.source.value;
+ };
+
+ return request.generateResponse({ value: 'text' }, { variety: 'test', marshal });
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
+ const onPreResponse = (request, h) => {
- server.inject('/', function (res) {
+ request.response.charset('abc');
+ return h.continue;
+ };
- expect(res.statusCode).to.equal(301);
- done();
- });
+ const server = Hapi.server();
+ server.ext('onPreResponse', onPreResponse);
+
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.headers['content-type']).to.equal('text/html; charset=abc');
});
+ });
+
+ describe('redirect()', () => {
- it('returns a 302 redirection reply', function (done) {
+ it('returns a redirection response', async () => {
- var handler = function (request, reply) {
+ const handler = (request, h) => {
- return reply().redirect('example').temporary().rewritable();
+ return h.response('Please wait while we send your elsewhere').redirect('/example');
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject('http://example.org/');
+ expect(res.result).to.exist();
+ expect(res.headers.location).to.equal('/example');
+ expect(res.statusCode).to.equal(302);
+ });
- server.inject('/', function (res) {
+ it('returns a redirection response using verbose call', async () => {
- expect(res.statusCode).to.equal(302);
- done();
- });
+ const handler = (request, h) => {
+
+ return h.response('We moved!').redirect().location('/examplex');
+ };
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject('/');
+ expect(res.result).to.exist();
+ expect(res.result).to.equal('We moved!');
+ expect(res.headers.location).to.equal('/examplex');
+ expect(res.statusCode).to.equal(302);
});
- it('returns a 307 redirection reply', function (done) {
+ it('returns a 301 redirection response', async () => {
- var handler = function (request, reply) {
+ const handler = (request, h) => {
- return reply().redirect('example').temporary().rewritable(false);
+ return h.response().redirect('example').permanent().rewritable();
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(301);
+ });
- server.inject('/', function (res) {
+ it('returns a 302 redirection response', async () => {
- expect(res.statusCode).to.equal(307);
- done();
- });
+ const handler = (request, h) => {
+
+ return h.response().redirect('example').temporary().rewritable();
+ };
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(302);
});
- it('returns a 308 redirection reply', function (done) {
+ it('returns a 307 redirection response', async () => {
- var handler = function (request, reply) {
+ const handler = (request, h) => {
- return reply().redirect('example').permanent().rewritable(false);
+ return h.response().redirect('example').temporary().rewritable(false);
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
- server.inject('/', function (res) {
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(307);
+ });
- expect(res.statusCode).to.equal(308);
- done();
- });
+ it('returns a 308 redirection response', async () => {
+
+ const handler = (request, h) => {
+
+ return h.response().redirect('example').permanent().rewritable(false);
+ };
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(308);
});
- it('returns a 301 redirection reply (reveresed methods)', function (done) {
+ it('returns a 301 redirection response (reversed methods)', async () => {
- var handler = function (request, reply) {
+ const handler = (request, h) => {
- return reply().redirect('example').rewritable().permanent();
+ return h.response().redirect('example').rewritable().permanent();
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
- server.inject('/', function (res) {
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(301);
+ });
- expect(res.statusCode).to.equal(301);
- done();
- });
+ it('returns a 302 redirection response (reversed methods)', async () => {
+
+ const handler = (request, h) => {
+
+ return h.response().redirect('example').rewritable().temporary();
+ };
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(302);
});
- it('returns a 302 redirection reply (reveresed methods)', function (done) {
+ it('returns a 307 redirection response (reversed methods)', async () => {
- var handler = function (request, reply) {
+ const handler = (request, h) => {
- return reply().redirect('example').rewritable().temporary();
+ return h.response().redirect('example').rewritable(false).temporary();
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(307);
+ });
- server.inject('/', function (res) {
+ it('returns a 308 redirection response (reversed methods)', async () => {
- expect(res.statusCode).to.equal(302);
- done();
- });
+ const handler = (request, h) => {
+
+ return h.response().redirect('example').rewritable(false).permanent();
+ };
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(308);
});
- it('returns a 307 redirection reply (reveresed methods)', function (done) {
+ it('returns a 302 redirection response (flip flop)', async () => {
- var handler = function (request, reply) {
+ const handler = (request, h) => {
- return reply().redirect('example').rewritable(false).temporary();
+ return h.response().redirect('example').permanent().temporary();
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
- server.inject('/', function (res) {
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(302);
+ });
+ });
- expect(res.statusCode).to.equal(307);
- done();
+ describe('_marshal()', () => {
+
+ it('emits request-error when view file for handler not found', async () => {
+
+ const server = Hapi.server({ debug: false });
+ await server.register(Vision);
+
+ server.views({
+ engines: { 'html': Handlebars },
+ path: __dirname
});
+
+ const log = server.events.once({ name: 'request', channels: 'error' });
+
+ server.route({ method: 'GET', path: '/{param}', handler: { view: 'templates/invalid' } });
+
+ const res = await server.inject('/hello');
+ expect(res.statusCode).to.equal(500);
+ expect(res.result).to.exist();
+ expect(res.result.message).to.equal('An internal server error occurred');
+
+ const [, event] = await log;
+ expect(event.error.message).to.contain('The partial x could not be found: The partial x could not be found');
});
- it('returns a 308 redirection reply (reveresed methods)', function (done) {
+ it('returns a formatted response (spaces)', async () => {
- var handler = function (request, reply) {
+ const handler = (request) => {
- return reply().redirect('example').rewritable(false).permanent();
+ return { a: 1, b: 2, '<': '&' };
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
+ const server = Hapi.server({ routes: { json: { space: 4, suffix: '\n', escape: true } } });
+ server.route({ method: 'GET', path: '/', handler });
- server.inject('/', function (res) {
+ const res = await server.inject('/');
+ expect(res.payload).to.equal('{\n \"a\": 1,\n \"b\": 2,\n \"\\u003c\": \"\\u0026\"\n}\n');
+ });
- expect(res.statusCode).to.equal(308);
- done();
- });
+ it('returns a formatted response (replacer and spaces', async () => {
+
+ const handler = (request) => {
+
+ return { a: 1, b: 2, '<': '&' };
+ };
+
+ const server = Hapi.server({ routes: { json: { replacer: ['a', '<'], space: 4, suffix: '\n', escape: true } } });
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject('/');
+ expect(res.payload).to.equal('{\n \"a\": 1,\n \"\\u003c\": \"\\u0026\"\n}\n');
});
- it('returns a 302 redirection reply (flip flop)', function (done) {
+ it('returns a response with options', async () => {
- var handler = function (request, reply) {
+ const handler = (request, h) => {
- return reply().redirect('example').permanent().temporary();
+ return h.response({ a: 1, b: 2, '<': '&' }).type('application/x-test').spaces(2).replacer(['a']).suffix('\n').escape(false);
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
- server.inject('/', function (res) {
+ const res = await server.inject('/');
+ expect(res.payload).to.equal('{\n \"a\": 1\n}\n');
+ expect(res.headers['content-type']).to.equal('application/x-test');
+ });
- expect(res.statusCode).to.equal(302);
- done();
- });
+ it('returns a response with options (different order)', async () => {
+
+ const handler = (request, h) => {
+
+ return h.response({ a: 1, b: 2, '<': '&' }).type('application/x-test').escape(false).replacer(['a']).suffix('\n').spaces(2);
+ };
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject('/');
+ expect(res.payload).to.equal('{\n \"a\": 1\n}\n');
+ expect(res.headers['content-type']).to.equal('application/x-test');
});
- });
- describe('_prepare()', function () {
+ it('captures object which cannot be stringify', async () => {
+
+ const handler = (request) => {
+
+ const obj = {};
+ obj.a = obj;
+ return obj;
+ };
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
+ });
+
+ it('errors on non-readable stream response', async () => {
- it('handles promises that resolve', function (done) {
+ const streamHandler = (request, h) => {
- var handler = function (request, reply) {
+ const stream = new Stream();
+ stream.writable = true;
- return reply(Bluebird.resolve('promised response')).code(201);
+ return h.response(stream);
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
+ const writableHandler = (request, h) => {
- server.inject('/', function (res) {
+ const writable = new Stream.Writable();
+ writable._write = function () { };
- expect(res.result).to.equal('promised response');
- expect(res.statusCode).to.equal(201);
- done();
- });
+ return h.response(writable);
+ };
+
+ const server = Hapi.server({ debug: false });
+ server.route({ method: 'GET', path: '/stream', handler: streamHandler });
+ server.route({ method: 'GET', path: '/writable', handler: writableHandler });
+
+ await server.initialize();
+
+ const log1 = server.events.once({ name: 'request', channels: 'error' });
+ const res1 = await server.inject('/stream');
+ expect(res1.statusCode).to.equal(500);
+
+ const [, event1] = await log1;
+ expect(event1.error).to.be.an.error('Cannot reply with a stream-like object that is not an instance of Stream.Readable');
+
+ const log2 = server.events.once({ name: 'request', channels: 'error' });
+ const res2 = await server.inject('/writable');
+ expect(res2.statusCode).to.equal(500);
+
+ const [, event2] = await log2;
+ expect(event2.error).to.be.an.error('Cannot reply with a stream-like object that is not an instance of Stream.Readable');
});
- it('handles promises that resolve (object)', function (done) {
+ it('errors on an http client stream response', async () => {
- var handler = function (request, reply) {
+ const streamHandler = (request, h) => {
- return reply(Bluebird.resolve({ status: 'ok' })).code(201);
+ const req = Http.get(request.server.info.uri);
+ req.abort();
+ return h.response(req);
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
+ const server = Hapi.server({ debug: false });
+ server.route({ method: 'GET', path: '/stream', handler: streamHandler });
- server.inject('/', function (res) {
+ const log = server.events.once({ name: 'request', channels: 'error' });
- expect(res.result.status).to.equal('ok');
- expect(res.statusCode).to.equal(201);
- done();
- });
+ await server.initialize();
+ const res = await server.inject('/stream');
+ expect(res.statusCode).to.equal(500);
+
+ const [, event] = await log;
+ expect(event.error).to.be.an.error('Cannot reply with a stream-like object that is not an instance of Stream.Readable');
});
- it('handles promises that resolve (response object)', function (done) {
+ it('errors on a legacy readable stream response', async () => {
+
+ const streamHandler = () => {
+
+ const stream = new LegacyReadableStream.Readable();
+ stream._read = function (size) {
- var handler = function (request, reply) {
+ const chunk = new Array(size).join('x');
- return reply(Bluebird.resolve(request.generateResponse({ status: 'ok' }).code(201)));
+ setTimeout(() => {
+
+ this.push(chunk);
+ }, 10);
+ };
+
+ return stream;
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
+ const server = Hapi.server({ debug: false });
+ server.route({ method: 'GET', path: '/stream', handler: streamHandler });
- server.inject('/', function (res) {
+ const log = server.events.once({ name: 'request', channels: 'error' });
- expect(res.result.status).to.equal('ok');
- expect(res.statusCode).to.equal(201);
- done();
- });
+ await server.initialize();
+ const res = await server.inject('/stream');
+ expect(res.statusCode).to.equal(500);
+
+ const [, event] = await log;
+ expect(event.error).to.be.an.error('Cannot reply with a stream-like object that is not an instance of Stream.Readable');
});
- it('handles promises that reject', function (done) {
+ it('errors on objectMode stream response', async () => {
+
+ const TestStream = class extends Stream.Readable {
+
+ constructor() {
- var handler = function (request, reply) {
+ super({ objectMode: true });
+ }
+
+ _read(size) {
+
+ if (this.isDone) {
+ return;
+ }
- var promise = Bluebird.reject(Boom.forbidden('this is not allowed!'));
- promise.catch(Hoek.ignore);
+ this.isDone = true;
- return reply(promise).code(299); // Code ignored
+ this.push({ x: 1 });
+ this.push({ y: 1 });
+ this.push(null);
+ }
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
+ const handler = (request, h) => {
- server.inject('/', function (res) {
+ return h.response(new TestStream());
+ };
- expect(res.result.message).to.equal('this is not allowed!');
- expect(res.statusCode).to.equal(403);
- done();
- });
+ const server = Hapi.server({ debug: false });
+ server.route({ method: 'GET', path: '/', handler });
+
+ const log = server.events.once({ name: 'request', channels: 'error' });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
+
+ const [, event] = await log;
+ expect(event.error).to.be.an.error('Cannot reply with stream in object mode');
});
});
- describe('_marshal()', function () {
+ describe('_prepare()', () => {
- it('emits request-error when view file for handler not found', function (done) {
+ it('boomifies response prepare error', async () => {
- var server = new Hapi.Server({ debug: false });
- server.register(Vision, Hoek.ignore);
- server.connection();
+ const server = Hapi.server();
- server.views({
- engines: { 'html': Handlebars },
- path: __dirname
- });
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: (request) => {
+
+ const prepare = () => {
- server.once('request-error', function (request, err) {
+ throw new Error('boom');
+ };
- expect(err).to.exist();
- expect(err.message).to.contain('View file not found');
- done();
+ return request.generateResponse('nothing', { variety: 'special', marshal: null, prepare, close: null });
+ }
});
- server.route({ method: 'GET', path: '/{param}', handler: { view: 'noview' } });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(500);
+ });
+
+ it('is only called once for returned responses', async () => {
+
+ let calls = 0;
+ const pre = (request, h) => {
- server.inject('/hello', function (res) {
+ const prepare = (response) => {
- expect(res.statusCode).to.equal(500);
- expect(res.result).to.exist();
- expect(res.result.message).to.equal('An internal server error occurred');
+ ++calls;
+ return response;
+ };
+
+ return request.generateResponse(null, { prepare });
+ };
+
+ const server = Hapi.server();
+ server.route({
+ method: 'GET',
+ path: '/',
+ options: {
+ pre: [
+ { method: pre, assign: 'p' }
+ ],
+ handler: (request) => request.preResponses.p
+ }
});
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(204);
+ expect(calls).to.equal(1);
});
});
- describe('_streamify()', function () {
+ describe('_tap()', () => {
- it('returns a formatted response', function (done) {
+ it('peeks into the response stream', async () => {
- var handler = function (request, reply) {
+ const server = Hapi.server();
- return reply({ a: 1, b: 2 });
- };
+ let output = '';
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: (request, h) => {
- var server = new Hapi.Server();
- server.connection({ routes: { json: { replacer: ['a'], space: 4, suffix: '\n' } } });
- server.route({ method: 'GET', path: '/', handler: handler });
+ const response = h.response('1234567890');
- server.inject('/', function (res) {
+ response.events.on('peek', (chunk, encoding) => {
- expect(res.payload).to.equal('{\n \"a\": 1\n}\n');
- done();
+ output += chunk.toString();
+ });
+
+ response.events.once('finish', () => {
+
+ output += '!';
+ });
+
+ return response;
+ }
});
+
+ await server.inject('/');
+ expect(output).to.equal('1234567890!');
});
- it('returns a response with options', function (done) {
+ it('peeks into the response stream (finish only)', async () => {
- var handler = function (request, reply) {
+ const server = Hapi.server();
- return reply({ a: 1, b: 2 }).type('application/x-test').spaces(2).replacer(['a']).suffix('\n');
- };
+ let output = false;
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: (request, h) => {
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
+ const response = h.response('1234567890');
- server.inject('/', function (res) {
+ response.events.once('finish', () => {
- expect(res.payload).to.equal('{\n \"a\": 1\n}\n');
- expect(res.headers['content-type']).to.equal('application/x-test');
- done();
+ output = true;
+ });
+
+ return response;
+ }
});
+
+ await server.inject('/');
+ expect(output).to.be.true();
});
- it('returns a response with options (different order)', function (done) {
+ it('peeks into the response stream (empty)', async () => {
- var handler = function (request, reply) {
+ const server = Hapi.server();
- return reply({ a: 1, b: 2 }).type('application/x-test').replacer(['a']).suffix('\n').spaces(2);
- };
+ let output = '';
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: (request, h) => {
+
+ const response = h.response(null);
+
+ response.events.on('peek', (chunk, encoding) => { });
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
+ response.events.once('finish', () => {
- server.inject('/', function (res) {
+ output += '!';
+ });
- expect(res.payload).to.equal('{\n \"a\": 1\n}\n');
- expect(res.headers['content-type']).to.equal('application/x-test');
- done();
+ return response;
+ }
});
+
+ await server.inject('/');
+ expect(output).to.equal('!');
});
- it('captures object which cannot be stringify', function (done) {
+ it('peeks into the response stream (empty 304)', async () => {
- var handler = function (request, reply) {
+ const server = Hapi.server();
- var obj = {};
- obj.a = obj;
- return reply(obj);
- };
+ let output = '';
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: (request, h) => {
+
+ const response = h.response(null).code(304);
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
+ response.events.on('peek', (chunk, encoding) => { });
- server.inject('/', function (res) {
+ response.events.once('finish', () => {
- expect(res.statusCode).to.equal(500);
- done();
+ output += '!';
+ });
+
+ return response;
+ }
});
+
+ await server.inject('/');
+ expect(output).to.equal('!');
});
});
- describe('_close()', function () {
+ describe('_close()', () => {
- it('calls custom close processor', function (done) {
+ it('calls custom close processor', async () => {
- var closed = false;
- var close = function (response) {
+ let closed = false;
+ const close = function (response) {
closed = true;
};
- var handler = function (request, reply) {
+ const handler = (request) => {
+
+ return request.generateResponse(null, { close });
+ };
+
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', handler });
+
+ await server.inject('/');
+ expect(closed).to.be.true();
+ });
+
+ it('logs custom close processor error', async () => {
+
+ const close = function (response) {
+
+ throw new Error('oops');
+ };
+
+ const handler = (request) => {
+
+ return request.generateResponse(null, { close });
+ };
+
+ const server = Hapi.server();
+ const log = server.events.once('request');
+ server.route({ method: 'GET', path: '/', handler });
+
+ await server.inject('/');
+ const [, event] = await log;
+ expect(event.tags).to.equal(['response', 'cleanup', 'error']);
+ expect(event.error).to.be.an.error('oops');
+ });
+ });
+
+ describe('Peek', () => {
+
+ it('taps into pass-through stream', async () => {
+
+ // Source
+
+ const Source = class extends Stream.Readable {
+
+ constructor(values) {
+
+ super();
+ this.data = values;
+ this.pos = 0;
+ }
+
+ _read(/* size */) {
+
+ if (this.pos === this.data.length) {
+ this.push(null);
+ return;
+ }
+
+ this.push(this.data[this.pos++]);
+ }
+ };
+
+ // Target
- return reply(request.generateResponse(null, { close: close }));
+ const Target = class extends Stream.Writable {
+
+ constructor() {
+
+ super();
+ this.data = [];
+ }
+
+ _write(chunk, encoding, callback) {
+
+ this.data.push(chunk.toString());
+ return callback();
+ }
};
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler } });
+ // Peek
- server.inject('/', function (res) {
+ const emitter = new Events.EventEmitter();
+ const peek = new Response.Peek(emitter);
- expect(closed).to.be.true();
- done();
+ const chunks = ['abcd', 'efgh', 'ijkl', 'mnop', 'qrst', 'uvwx'];
+ const source = new Source(chunks);
+ const target = new Target();
+
+ const seen = [];
+ emitter.on('peek', (update) => {
+
+ const chunk = update[0];
+ seen.push(chunk.toString());
});
+
+ const finish = new Promise((resolve) => {
+
+ emitter.once('finish', () => {
+
+ expect(seen).to.equal(chunks);
+ expect(target.data).to.equal(chunks);
+ resolve();
+ });
+ });
+
+ source.pipe(peek).pipe(target);
+ await finish;
});
});
});
diff --git a/test/route.js b/test/route.js
index 9b03a5fe3..e8dae012b 100755
--- a/test/route.js
+++ b/test/route.js
@@ -1,217 +1,361 @@
-// Load modules
+'use strict';
-var Code = require('code');
-var Hapi = require('..');
-var Hoek = require('hoek');
-var Inert = require('inert');
-var Joi = require('joi');
-var Lab = require('lab');
+const Path = require('path');
+const Code = require('@hapi/code');
+const Hapi = require('..');
+const Inert = require('@hapi/inert');
+const Joi = require('joi');
+const Lab = require('@hapi/lab');
+const Subtext = require('@hapi/subtext');
-// Declare internals
-var internals = {};
+const internals = {};
-// Test shortcuts
+const { describe, it } = exports.lab = Lab.script();
+const expect = Code.expect;
-var lab = exports.lab = Lab.script();
-var describe = lab.describe;
-var it = lab.it;
-var expect = Code.expect;
+describe('Route', () => {
-describe('Route', function () {
+ it('registers with options function', async () => {
- it('throws an error when a route is missing a path', function (done) {
+ const server = Hapi.server();
+ server.bind({ a: 1 });
+ server.app.b = 2;
+ server.route({
+ method: 'GET',
+ path: '/',
+ options: function (srv) {
+
+ const a = this.a;
+
+ return {
+ handler: () => a + srv.app.b
+ };
+ }
+ });
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal(3);
+ });
+
+ it('registers with config', async () => {
- expect(function () {
+ const server = Hapi.server();
+ server.route({
+ method: 'GET',
+ path: '/',
+ config: {
+ handler: () => 'ok'
+ }
+ });
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', handler: function () { } });
- }).to.throw('Route missing path');
- done();
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('ok');
});
- it('throws an error when a route is made without a connection', function (done) {
+ it('throws an error when a route is missing a path', () => {
- expect(function () {
+ expect(() => {
- var server = new Hapi.Server();
- server.route({ method: 'GET', path: '/dork', handler: function () { } });
- }).to.throw('Cannot add a route without any connections');
- done();
+ const server = Hapi.server();
+ server.route({ method: 'GET', handler: () => null });
+ }).to.throw(/"path" is required/);
});
- it('throws an error when a route is missing a method', function (done) {
+ it('throws an error when a route is missing a method', () => {
- expect(function () {
+ expect(() => {
- var server = new Hapi.Server();
- server.connection();
- server.route({ path: '/', handler: function () { } });
+ const server = Hapi.server();
+ server.route({ path: '/', handler: () => null });
}).to.throw(/"method" is required/);
- done();
});
- it('throws an error when a route has a malformed method name', function (done) {
+ it('throws an error when a route has a malformed method name', () => {
- expect(function () {
+ expect(() => {
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: '"GET"', path: '/', handler: function () { } });
- }).to.throw(/Invalid method name/);
- done();
+ const server = Hapi.server();
+ server.route({ method: '"GET"', path: '/', handler: () => null });
+ }).to.throw(/Invalid route options/);
});
- it('throws an error when a route uses the HEAD method', function (done) {
+ it('throws an error when a route uses the HEAD method', () => {
- expect(function () {
+ expect(() => {
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'HEAD', path: '/', handler: function () { } });
- }).to.throw(/Method name not allowed/);
- done();
+ const server = Hapi.server();
+ server.route({ method: 'HEAD', path: '/', handler: () => null });
+ }).to.throw('Cannot set HEAD route: /');
});
- it('throws an error when a route is missing a handler', function (done) {
+ it('throws an error when a route is missing a handler', () => {
- expect(function () {
+ expect(() => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.route({ path: '/test', method: 'put' });
- }).to.throw('Missing or undefined handler: put /test');
- done();
+ }).to.throw('Missing or undefined handler: PUT /test');
});
- it('throws when handler is missing in config', function (done) {
+ it('throws when handler is missing in config', () => {
- var server = new Hapi.Server();
- server.connection();
- expect(function () {
+ const server = Hapi.server();
+ expect(() => {
- server.route({ method: 'GET', path: '/', config: {} });
+ server.route({ method: 'GET', path: '/', options: {} });
}).to.throw('Missing or undefined handler: GET /');
- done();
});
- it('throws when path has trailing slash and server set to strip', function (done) {
+ it('throws when path has trailing slash and server set to strip', () => {
- var server = new Hapi.Server();
- server.connection({ router: { stripTrailingSlash: true } });
- expect(function () {
+ const server = Hapi.server({ router: { stripTrailingSlash: true } });
+ expect(() => {
- server.route({ method: 'GET', path: '/test/', handler: function () { } });
- }).to.throw('Path cannot end with a trailing slash when connection configured to strip: GET /test/');
- done();
+ server.route({ method: 'GET', path: '/test/', handler: () => null });
+ }).to.throw('Path cannot end with a trailing slash when configured to strip: GET /test/');
});
- it('allows / when path has trailing slash and server set to strip', function (done) {
+ it('allows / when path has trailing slash and server set to strip', () => {
- var server = new Hapi.Server();
- server.connection({ router: { stripTrailingSlash: true } });
- expect(function () {
+ const server = Hapi.server({ router: { stripTrailingSlash: true } });
+ expect(() => {
- server.route({ method: 'GET', path: '/', handler: function () { } });
+ server.route({ method: 'GET', path: '/', handler: () => null });
}).to.not.throw();
- done();
});
- it('sets route plugins and app settings', function (done) {
+ it('sets route plugins and app settings', async () => {
- var handler = function (request, reply) {
+ const handler = (request) => (request.route.settings.app.x + request.route.settings.plugins.x.y);
+ const server = Hapi.server();
+ server.route({ method: 'GET', path: '/', options: { handler, app: { x: 'o' }, plugins: { x: { y: 'k' } } } });
+ const res = await server.inject('/');
+ expect(res.result).to.equal('ok');
+ });
- return reply(request.route.settings.app.x + request.route.settings.plugins.x.y);
- };
+ it('throws when validation is set without payload parsing', () => {
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: 'GET', path: '/', config: { handler: handler, app: { x: 'o' }, plugins: { x: { y: 'k' } } } });
- server.inject('/', function (res) {
+ const server = Hapi.server();
+ expect(() => {
- expect(res.result).to.equal('ok');
- done();
- });
+ server.route({ method: 'POST', path: '/', handler: () => null, options: { validate: { payload: {}, validator: Joi }, payload: { parse: false } } });
+ }).to.throw('Route payload must be set to \'parse\' when payload validation enabled: POST /');
});
- it('throws when validation is set without payload parsing', function (done) {
+ it('throws when validation is set without path parameters', () => {
- var server = new Hapi.Server();
- server.connection();
- expect(function () {
+ const server = Hapi.server();
+ expect(() => {
- server.route({ method: 'POST', path: '/', handler: function () { }, config: { validate: { payload: {} }, payload: { parse: false } } });
- }).to.throw('Route payload must be set to \'parse\' when payload validation enabled: POST /');
- done();
+ server.route({ method: 'POST', path: '/', handler: () => null, options: { validate: { params: {} } } });
+ }).to.throw('Cannot set path parameters validations without path parameters: POST /');
});
- it('throws when validation is set on GET', function (done) {
+ it('ignores payload when overridden', async () => {
+
+ const server = Hapi.server();
+ server.route({
+ method: 'POST',
+ path: '/',
+ handler: (request) => request.payload
+ });
+
+ server.ext('onRequest', (request, h) => {
+
+ request.payload = 'x';
+ return h.continue;
+ });
- var server = new Hapi.Server();
- server.connection();
- expect(function () {
+ const res = await server.inject({ method: 'POST', url: '/', payload: 'y' });
+ expect(res.statusCode).to.equal(200);
+ expect(res.result).to.equal('x');
+ });
+
+ it('ignores payload parsing errors', async () => {
+
+ const server = Hapi.server();
+ server.route({
+ method: 'POST',
+ path: '/',
+ handler: () => 'ok',
+ options: {
+ payload: {
+ parse: true,
+ failAction: 'ignore'
+ }
+ }
+ });
- server.route({ method: 'GET', path: '/', handler: function () { }, config: { validate: { payload: {} } } });
- }).to.throw('Cannot validate HEAD or GET requests: /');
- done();
+ const res = await server.inject({ method: 'POST', url: '/', payload: '{a:"abc"}' });
+ expect(res.statusCode).to.equal(200);
});
- it('throws when payload parsing is set on GET', function (done) {
+ it('logs payload parsing errors', async () => {
+
+ const server = Hapi.server();
+ server.route({
+ method: 'POST',
+ path: '/',
+ handler: () => 'ok',
+ options: {
+ payload: {
+ parse: true,
+ failAction: 'log'
+ }
+ }
+ });
+
+ let logged;
+ server.events.on({ name: 'request', channels: 'internal' }, (request, event, tags) => {
- var server = new Hapi.Server();
- server.connection();
- expect(function () {
+ if (tags.payload && tags.error) {
+ logged = event;
+ }
+ });
- server.route({ method: 'GET', path: '/', handler: function () { }, config: { payload: { parse: true } } });
- }).to.throw('Cannot set payload settings on HEAD or GET request: /');
- done();
+ const res = await server.inject({ method: 'POST', url: '/', payload: '{a:"abc"}' });
+ expect(res.statusCode).to.equal(200);
+ expect(logged).to.be.an.object();
+ expect(logged.error).to.be.an.error('Invalid request payload JSON format');
+ expect(logged.error.data).to.be.an.error(SyntaxError, /at position 1/);
});
- it('ignores validation on * route when request is GET', function (done) {
+ it('returns payload parsing errors', async () => {
- var handler = function (request, reply) {
+ const server = Hapi.server();
+ server.route({
+ method: 'POST',
+ path: '/',
+ handler: () => 'ok',
+ options: {
+ payload: {
+ parse: true,
+ failAction: 'error'
+ }
+ }
+ });
- return reply();
- };
+ const res = await server.inject({ method: 'POST', url: '/', payload: '{a:"abc"}' });
+ expect(res.statusCode).to.equal(400);
+ expect(res.result.message).to.equal('Invalid request payload JSON format');
+ });
+
+ it('replaces payload parsing errors with custom handler', async () => {
- var server = new Hapi.Server();
- server.connection();
- server.route({ method: '*', path: '/', handler: handler, config: { validate: { payload: { a: Joi.required() } } } });
- server.inject('/', function (res) {
+ const server = Hapi.server();
+ server.route({
+ method: 'POST',
+ path: '/',
+ handler: () => 'ok',
+ options: {
+ payload: {
+ parse: true,
+ failAction: function (request, h, error) {
- expect(res.statusCode).to.equal(200);
- done();
+ return h.response('This is a custom error').code(418).takeover();
+ }
+ }
+ }
});
+
+ const res = await server.inject({ method: 'POST', url: '/', payload: '{a:"abc"}' });
+ expect(res.statusCode).to.equal(418);
+ expect(res.result).to.equal('This is a custom error');
+ });
+
+ it('throws when validation is set on GET', () => {
+
+ const server = Hapi.server();
+ expect(() => {
+
+ server.route({ method: 'GET', path: '/', handler: () => null, options: { validate: { payload: {} } } });
+ }).to.throw('Cannot validate HEAD or GET request payload: GET /');
});
- it('ignores default validation on GET', function (done) {
+ it('throws when payload parsing is set on GET', () => {
- var handler = function (request, reply) {
+ const server = Hapi.server();
+ expect(() => {
- return reply();
+ server.route({ method: 'GET', path: '/', handler: () => null, options: { payload: { parse: true } } });
+ }).to.throw('Cannot set payload settings on HEAD or GET request: GET /');
+ });
+
+ it('ignores validation on * route when request is GET', async () => {
+
+ const server = Hapi.server();
+ server.validator(Joi);
+ server.route({ method: '*', path: '/', handler: () => null, options: { validate: { payload: { a: Joi.required() } } } });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(204);
+ });
+
+ it('ignores validation on * route when request is HEAD', async () => {
+
+ const server = Hapi.server();
+ server.validator(Joi);
+ server.route({ method: '*', path: '/', handler: () => null, options: { validate: { payload: { a: Joi.required() } } } });
+ const res = await server.inject({ url: '/', method: 'HEAD' });
+ expect(res.statusCode).to.equal(204);
+ });
+
+ it('skips payload on * route when request is HEAD', async (flags) => {
+
+ const orig = Subtext.parse;
+ let called = false;
+ Subtext.parse = () => {
+
+ called = true;
};
- var server = new Hapi.Server();
- server.connection({ routes: { validate: { payload: { a: Joi.required() } } } });
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res) {
+ flags.onCleanup = () => {
- expect(res.statusCode).to.equal(200);
- done();
- });
+ Subtext.parse = orig;
+ };
+
+ const server = Hapi.server();
+ server.route({ method: '*', path: '/', handler: () => null });
+ const res = await server.inject({ url: '/', method: 'HEAD' });
+ expect(res.statusCode).to.equal(204);
+ expect(called).to.be.false();
});
- it('shallow copies route config bind', function (done) {
+ it('throws error when the default routes payload validation is set without payload parsing', () => {
- var server = new Hapi.Server();
- server.connection();
- var context = { key: 'is ' };
+ expect(() => {
- var count = 0;
+ Hapi.server({ routes: { validate: { payload: {}, validator: Joi }, payload: { parse: false } } });
+ }).to.throw('Route payload must be set to \'parse\' when payload validation enabled');
+ });
+
+ it('throws error when the default routes state validation is set without state parsing', () => {
+
+ expect(() => {
+
+ Hapi.server({ routes: { validate: { state: {}, validator: Joi }, state: { parse: false } } });
+ }).to.throw('Route state must be set to \'parse\' when state validation enabled');
+ });
+
+ it('ignores default validation on GET', async () => {
+
+ const server = Hapi.server({ routes: { validate: { payload: { a: Joi.required() }, validator: Joi } } });
+ server.route({ method: 'GET', path: '/', handler: () => null });
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(204);
+ });
+
+ it('shallow copies route config bind', async () => {
+
+ const server = Hapi.server();
+ const context = { key: 'is ' };
+
+ let count = 0;
Object.defineProperty(context, 'test', {
enumerable: true,
configurable: true,
@@ -221,27 +365,23 @@ describe('Route', function () {
}
});
- var handler = function (request, reply) {
+ const handler = function (request) {
- return reply(this.key + (this === context));
+ return this.key + (this === context);
};
- server.route({ method: 'GET', path: '/', handler: handler, config: { bind: context } });
- server.inject('/', function (res) {
-
- expect(res.result).to.equal('is true');
- expect(count).to.equal(0);
- done();
- });
+ server.route({ method: 'GET', path: '/', handler, options: { bind: context } });
+ const res = await server.inject('/');
+ expect(res.result).to.equal('is true');
+ expect(count).to.equal(0);
});
- it('shallow copies route config bind (server.bind())', function (done) {
+ it('shallow copies route config bind (server.bind())', async () => {
- var server = new Hapi.Server();
- server.connection();
- var context = { key: 'is ' };
+ const server = Hapi.server();
+ const context = { key: 'is ' };
- var count = 0;
+ let count = 0;
Object.defineProperty(context, 'test', {
enumerable: true,
configurable: true,
@@ -251,27 +391,24 @@ describe('Route', function () {
}
});
- var handler = function (request, reply) {
+ const handler = function (request) {
- return reply(this.key + (this === context));
+ return this.key + (this === context);
};
server.bind(context);
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res) {
-
- expect(res.result).to.equal('is true');
- expect(count).to.equal(0);
- done();
- });
+ server.route({ method: 'GET', path: '/', handler });
+ const res = await server.inject('/');
+ expect(res.result).to.equal('is true');
+ expect(count).to.equal(0);
});
- it('shallow copies route config bind (connection defaults)', function (done) {
+ it('shallow copies route config bind (connection defaults)', async () => {
- var server = new Hapi.Server();
- var context = { key: 'is ' };
+ const context = { key: 'is ' };
+ const server = Hapi.server({ routes: { bind: context } });
- var count = 0;
+ let count = 0;
Object.defineProperty(context, 'test', {
enumerable: true,
configurable: true,
@@ -281,26 +418,22 @@ describe('Route', function () {
}
});
- var handler = function (request, reply) {
+ const handler = function (request) {
- return reply(this.key + (this === context));
+ return this.key + (this === context);
};
- server.connection({ routes: { bind: context } });
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res) {
-
- expect(res.result).to.equal('is true');
- expect(count).to.equal(0);
- done();
- });
+ server.route({ method: 'GET', path: '/', handler });
+ const res = await server.inject('/');
+ expect(res.result).to.equal('is true');
+ expect(count).to.equal(0);
});
- it('shallow copies route config bind (server defaults)', function (done) {
+ it('shallow copies route config bind (server defaults)', async () => {
- var context = { key: 'is ' };
+ const context = { key: 'is ' };
- var count = 0;
+ let count = 0;
Object.defineProperty(context, 'test', {
enumerable: true,
configurable: true,
@@ -310,95 +443,525 @@ describe('Route', function () {
}
});
- var handler = function (request, reply) {
+ const handler = function (request) {
- return reply(this.key + (this === context));
+ return this.key + (this === context);
};
- var server = new Hapi.Server({ connections: { routes: { bind: context } } });
- server.connection();
- server.route({ method: 'GET', path: '/', handler: handler });
- server.inject('/', function (res) {
+ const server = Hapi.server({ routes: { bind: context } });
+ server.route({ method: 'GET', path: '/', handler });
+ const res = await server.inject('/');
+ expect(res.result).to.equal('is true');
+ expect(count).to.equal(0);
+ });
+
+ it('overrides server relativeTo', async () => {
- expect(res.result).to.equal('is true');
- expect(count).to.equal(0);
- done();
- });
+ const server = Hapi.server();
+ await server.register(Inert);
+ const handler = (request, h) => h.file('./package.json');
+ server.route({ method: 'GET', path: '/file', handler, options: { files: { relativeTo: Path.join(__dirname, '../') } } });
+
+ const res = await server.inject('/file');
+ expect(res.payload).to.contain('hapi');
});
- it('overrides server relativeTo', function (done) {
+ it('allows payload timeout more then socket timeout', () => {
- var server = new Hapi.Server();
- server.register(Inert, Hoek.ignore);
- server.connection();
- var handler = function (request, reply) {
+ expect(() => {
- return reply.file('../package.json');
- };
+ Hapi.server({ routes: { payload: { timeout: 60000 }, timeout: { socket: 12000 } } });
+ }).to.not.throw();
+ });
- server.route({ method: 'GET', path: '/file', handler: handler, config: { files: { relativeTo: __dirname } } });
+ it('allows payload timeout more then socket timeout (node default)', () => {
- server.inject('/file', function (res) {
+ expect(() => {
- expect(res.payload).to.contain('hapi');
- done();
- });
+ Hapi.server({ routes: { payload: { timeout: 6000000 } } });
+ }).to.not.throw();
});
- it('throws when server timeout is more then socket timeout', function (done) {
+ it('allows server timeout more then socket timeout', () => {
- var server = new Hapi.Server();
- expect(function () {
+ expect(() => {
- server.connection({ routes: { timeout: { server: 60000, socket: 12000 } } });
- }).to.throw('Server timeout must be shorter than socket timeout: /{p*}');
- done();
+ Hapi.server({ routes: { timeout: { server: 60000, socket: 12000 } } });
+ }).to.not.throw();
});
- it('throws when server timeout is more then socket timeout (node default)', function (done) {
+ it('allows server timeout more then socket timeout (node default)', () => {
- var server = new Hapi.Server();
- expect(function () {
+ expect(() => {
- server.connection({ routes: { timeout: { server: 6000000 } } });
- }).to.throw('Server timeout must be shorter than socket timeout: /{p*}');
- done();
+ Hapi.server({ routes: { timeout: { server: 6000000 } } });
+ }).to.not.throw();
});
- it('ignores large server timeout when socket timeout disabled', function (done) {
+ it('ignores large server timeout when socket timeout disabled', () => {
- var server = new Hapi.Server();
- expect(function () {
+ expect(() => {
- server.connection({ routes: { timeout: { server: 6000000, socket: false } } });
+ Hapi.server({ routes: { timeout: { server: 6000000, socket: false } } });
}).to.not.throw();
- done();
});
- it('overrides qs settings', function (done) {
+ describe('extensions', () => {
- var server = new Hapi.Server();
- server.connection();
- server.route({
- method: 'POST',
- path: '/',
- config: {
- payload: {
- qs: {
- parseArrays: false
- }
- },
- handler: function (request, reply) {
+ it('combine connection extensions (route last)', async () => {
+
+ const server = Hapi.server();
+ const onRequest = (request, h) => {
+
+ request.app.x = '1';
+ return h.continue;
+ };
+
+ server.ext('onRequest', onRequest);
+
+ const preAuth = (request, h) => {
+
+ request.app.x += '2';
+ return h.continue;
+ };
+
+ server.ext('onPreAuth', preAuth);
+
+ const postAuth = (request, h) => {
+
+ request.app.x += '3';
+ return h.continue;
+ };
+
+ server.ext('onPostAuth', postAuth);
+
+ const preHandler = (request, h) => {
+
+ request.app.x += '4';
+ return h.continue;
+ };
+
+ server.ext('onPreHandler', preHandler);
+
+ const postHandler = (request, h) => {
+
+ request.response.source += '5';
+ return h.continue;
+ };
+
+ server.ext('onPostHandler', postHandler);
+
+ const preResponse = (request, h) => {
+
+ request.response.source += '6';
+ return h.continue;
+ };
+
+ server.ext('onPreResponse', preResponse);
+
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: (request) => request.app.x
+ });
+
+ const res = await server.inject('/');
+ expect(res.result).to.equal('123456');
+ });
+
+ it('combine connection extensions (route first)', async () => {
+
+ const server = Hapi.server();
- return reply(request.payload);
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: (request) => request.app.x
+ });
+
+ const onRequest = (request, h) => {
+
+ request.app.x = '1';
+ return h.continue;
+ };
+
+ server.ext('onRequest', onRequest);
+
+ const preAuth = (request, h) => {
+
+ request.app.x += '2';
+ return h.continue;
+ };
+
+ server.ext('onPreAuth', preAuth);
+
+ const postAuth = (request, h) => {
+
+ request.app.x += '3';
+ return h.continue;
+ };
+
+ server.ext('onPostAuth', postAuth);
+
+ const preHandler = (request, h) => {
+
+ request.app.x += '4';
+ return h.continue;
+ };
+
+ server.ext('onPreHandler', preHandler);
+
+ const postHandler = (request, h) => {
+
+ request.response.source += '5';
+ return h.continue;
+ };
+
+ server.ext('onPostHandler', postHandler);
+
+ const preResponse = (request, h) => {
+
+ request.response.source += '6';
+ return h.continue;
+ };
+
+ server.ext('onPreResponse', preResponse);
+
+ const res = await server.inject('/');
+ expect(res.result).to.equal('123456');
+ });
+
+ it('combine connection extensions (route middle)', async () => {
+
+ const server = Hapi.server();
+
+ const onRequest = (request, h) => {
+
+ request.app.x = '1';
+ return h.continue;
+ };
+
+ server.ext('onRequest', onRequest);
+
+ const preAuth = (request, h) => {
+
+ request.app.x += '2';
+ return h.continue;
+ };
+
+ server.ext('onPreAuth', preAuth);
+
+ const postAuth = (request, h) => {
+
+ request.app.x += '3';
+ return h.continue;
+ };
+
+ server.ext('onPostAuth', postAuth);
+
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: (request) => request.app.x
+ });
+
+ const preHandler = (request, h) => {
+
+ request.app.x += '4';
+ return h.continue;
+ };
+
+ server.ext('onPreHandler', preHandler);
+
+ const postHandler = (request, h) => {
+
+ request.response.source += '5';
+ return h.continue;
+ };
+
+ server.ext('onPostHandler', postHandler);
+
+ const preResponse = (request, h) => {
+
+ request.response.source += '6';
+ return h.continue;
+ };
+
+ server.ext('onPreResponse', preResponse);
+
+ const res = await server.inject('/');
+ expect(res.result).to.equal('123456');
+ });
+
+ it('combine connection extensions (mixed sources)', async () => {
+
+ const server = Hapi.server();
+
+ const preAuth1 = (request, h) => {
+
+ request.app.x = '1';
+ return h.continue;
+ };
+
+ server.ext('onPreAuth', preAuth1);
+
+ server.route({
+ method: 'GET',
+ path: '/',
+ options: {
+ ext: {
+ onPreAuth: {
+ method: (request, h) => {
+
+ request.app.x += '2';
+ return h.continue;
+ }
+ }
+ },
+ handler: (request) => request.app.x
}
- }
+ });
+
+ const preAuth3 = (request, h) => {
+
+ request.app.x += '3';
+ return h.continue;
+ };
+
+ server.ext('onPreAuth', preAuth3);
+
+ server.route({
+ method: 'GET',
+ path: '/a',
+ handler: (request) => request.app.x
+ });
+
+ const res1 = await server.inject('/');
+ expect(res1.result).to.equal('123');
+
+ const res2 = await server.inject('/a');
+ expect(res2.result).to.equal('13');
+ });
+
+ it('skips inner extensions when not found', async () => {
+
+ const server = Hapi.server();
+
+ let state = '';
+
+ const onRequest = (request, h) => {
+
+ state += 1;
+ return h.continue;
+ };
+
+ server.ext('onRequest', onRequest);
+
+ const preAuth = (request) => {
+
+ state += 2;
+ return 'ok';
+ };
+
+ server.ext('onPreAuth', preAuth);
+
+ const preResponse = (request, h) => {
+
+ state += 3;
+ return h.continue;
+ };
+
+ server.ext('onPreResponse', preResponse);
+
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(404);
+ expect(state).to.equal('13');
+ });
+ });
+
+ describe('rules', () => {
+
+ it('compiles rules into config', async () => {
+
+ const server = Hapi.server();
+ server.validator(Joi);
+
+ const processor = (rules) => {
+
+ if (!rules) {
+ return null;
+ }
+
+ return { validate: { query: { x: rules.x } } };
+ };
+
+ server.rules(processor);
+
+ server.route({ path: '/1', method: 'GET', handler: () => null, rules: { x: Joi.number().valid(1) } });
+ server.route({ path: '/2', method: 'GET', handler: () => null, rules: { x: Joi.number().valid(2) } });
+ server.route({ path: '/3', method: 'GET', handler: () => null });
+
+ expect((await server.inject('/1?x=1')).statusCode).to.equal(204);
+ expect((await server.inject('/1?x=2')).statusCode).to.equal(400);
+ expect((await server.inject('/2?x=1')).statusCode).to.equal(400);
+ expect((await server.inject('/2?x=2')).statusCode).to.equal(204);
+ expect((await server.inject('/3?x=1')).statusCode).to.equal(204);
+ expect((await server.inject('/3?x=2')).statusCode).to.equal(204);
+ });
+
+ it('compiles rules into config (route info)', async () => {
+
+ const server = Hapi.server();
+
+ const processor = (rules, { method, path }) => {
+
+ return { app: { method, path, x: rules.x } };
+ };
+
+ server.rules(processor);
+
+ server.route({ path: '/1', method: 'GET', handler: (request) => request.route.settings.app, rules: { x: 1 } });
+
+ expect((await server.inject('/1')).result).to.equal({ x: 1, path: '/1', method: 'get' });
+ });
+
+ it('compiles rules into config (validate)', () => {
+
+ const server = Hapi.server();
+ server.validator(Joi);
+
+ const processor = (rules) => {
+
+ return { validate: { query: { x: rules.x } } };
+ };
+
+ server.rules(processor, { validate: { schema: { x: Joi.number().required() } } });
+
+ server.route({ path: '/1', method: 'GET', handler: () => null, rules: { x: 1 } });
+ expect(() => server.route({ path: '/2', method: 'GET', handler: () => null, rules: { x: 'y' } })).to.throw(/must be a number/);
+ });
+
+ it('compiles rules into config (validate + options)', () => {
+
+ const server = Hapi.server();
+ server.validator(Joi);
+
+ const processor = (rules) => {
+
+ return { validate: { query: { x: rules.x } } };
+ };
+
+ server.rules(processor, { validate: { schema: { x: Joi.number().required() }, options: { allowUnknown: false } } });
+
+ server.route({ path: '/1', method: 'GET', handler: () => null, rules: { x: 1 } });
+ expect(() => server.route({ path: '/2', method: 'GET', handler: () => null, rules: { x: 1, y: 2 } })).to.throw(/is not allowed/);
});
- server.inject({ method: 'POST', url: '/', payload: 'a[0]=b&a[1]=c', headers: { 'content-type': 'application/x-www-form-urlencoded' } }, function (res) {
+ it('cascades rules into configs', async () => {
+
+ const handler = (request) => {
+
+ return request.route.settings.app.x + ':' + Object.keys(request.route.settings.app).join('').slice(0, -1);
+ };
+
+ const p1 = {
+ name: 'p1',
+ register: async (srv) => {
+
+ const processor = (rules) => {
+
+ return { app: { x: '1+' + rules.x, 1: true } };
+ };
+
+ srv.rules(processor);
+ await srv.register(p3);
+ srv.route({ path: '/1', method: 'GET', handler, rules: { x: 1 } });
+ }
+ };
+
+ const p2 = {
+ name: 'p2',
+ register: (srv) => {
+
+ const processor = (rules) => {
+
+ return { app: { x: '2+' + rules.x, 2: true } };
+ };
+
+ srv.rules(processor);
+ srv.route({ path: '/2', method: 'GET', handler, rules: { x: 2 } });
+ }
+ };
+
+ const p3 = {
+ name: 'p3',
+ register: async (srv) => {
+
+ const processor = (rules) => {
+
+ return { app: { x: '3+' + rules.x, 3: true } };
+ };
+
+ srv.rules(processor);
+ await srv.register(p4);
+ srv.route({ path: '/3', method: 'GET', handler, rules: { x: 3 } });
+ }
+ };
+
+ const p4 = {
+ name: 'p4',
+ register: async (srv) => {
+
+ await srv.register(p5);
+ srv.route({ path: '/4', method: 'GET', handler, rules: { x: 4 } });
+ }
+ };
+
+ const p5 = {
+ name: 'p5',
+ register: (srv) => {
+
+ const processor = (rules) => {
+
+ return { app: { x: '5+' + rules.x, 5: true } };
+ };
+
+ srv.rules(processor);
+ srv.route({ path: '/5', method: 'GET', handler, rules: { x: 5 } });
+ srv.route({ path: '/6', method: 'GET', handler, rules: { x: 6 }, config: { app: { x: '7' } } });
+ }
+ };
+
+ const server = Hapi.server();
+
+ const processor0 = (rules) => {
+
+ return { app: { x: '0+' + rules.x, 0: true } };
+ };
+
+ server.rules(processor0);
+ await server.register([p1, p2]);
+
+ server.route({ path: '/0', method: 'GET', handler, rules: { x: 0 } });
+
+ expect((await server.inject('/0')).result).to.equal('0+0:0');
+ expect((await server.inject('/1')).result).to.equal('1+1:01');
+ expect((await server.inject('/2')).result).to.equal('2+2:02');
+ expect((await server.inject('/3')).result).to.equal('3+3:013');
+ expect((await server.inject('/4')).result).to.equal('3+4:013');
+ expect((await server.inject('/5')).result).to.equal('5+5:0135');
+ expect((await server.inject('/6')).result).to.equal('7:0135');
+ });
+ });
+
+ describe('drain()', () => {
+
+ it('drains the request payload on 404', async () => {
- expect(res.result).to.deep.equal({ a: { 0: 'b', 1: 'c' } });
- done();
+ const server = Hapi.server();
+ const res = await server.inject({ method: 'POST', url: '/nope', payload: 'something' });
+ expect(res.statusCode).to.equal(404);
+ expect(res.raw.req._readableState.ended).to.be.true();
});
});
});
diff --git a/test/security.js b/test/security.js
index b910b38ad..afc533105 100755
--- a/test/security.js
+++ b/test/security.js
@@ -1,203 +1,97 @@
-// Load modules
+'use strict';
-var Code = require('code');
-var Hapi = require('..');
-var Joi = require('joi');
-var Lab = require('lab');
+const Code = require('@hapi/code');
+const Hapi = require('..');
+const Lab = require('@hapi/lab');
-// Declare internals
+const internals = {};
-var internals = {};
+const { describe, it } = exports.lab = Lab.script();
+const expect = Code.expect;
-// Test shortcuts
-var lab = exports.lab = Lab.script();
-var describe = lab.describe;
-var it = lab.it;
-var expect = Code.expect;
+describe('security', () => {
+ it('handles missing routes', async () => {
-describe('security', function () {
+ const server = Hapi.server({ port: 8080, routes: { security: { xframe: true } } });
- it('blocks response splitting through the request.create method', function (done) {
-
- var server = new Hapi.Server();
- server.connection();
-
- var createItemHandler = function (request, reply) {
+ const res = await server.inject('/');
+ expect(res.statusCode).to.equal(404);
+ expect(res.headers['x-frame-options']).to.exist();
+ });
- return reply('Moved').created('/item/' + request.payload.name);
- };
+ it('blocks response splitting through the request.create method', async () => {
- server.route({ method: 'POST', path: '/item', handler: createItemHandler });
+ const server = Hapi.server();
+ const handler = (request, h) => h.response('Moved').created('/item/' + request.payload.name);
+ server.route({ method: 'POST', path: '/item', handler });
- server.inject({
+ const res = await server.inject({
method: 'POST', url: '/item',
payload: '{"name": "foobar\r\nContent-Length: \r\n\r\nHTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 19\r\n\r\nShazam"}',
headers: { 'Content-Type': 'application/json' }
- }, function (res) {
-
- expect(res.statusCode).to.equal(400);
- done();
});
- });
-
- it('prevents xss with invalid content types', function (done) {
- var handler = function (request, reply) {
+ expect(res.statusCode).to.equal(400);
+ });
- return reply('Success');
- };
+ it('prevents xss with invalid content types', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.state('encoded', { encoding: 'iron' });
- server.route({ method: 'POST', path: '/', handler: handler });
+ server.route({
+ method: 'POST', path: '/',
+ handler: () => 'Success'
+ });
- server.inject({
+ const res = await server.inject({
method: 'POST',
url: '/',
payload: '{"something":"something"}',
headers: { 'content-type': ';' }
- },
- function (res) {
-
- expect(res.result.message).to.not.contain('script');
- done();
});
- });
-
- it('prevents xss with invalid cookie values in the request', function (done) {
- var handler = function (request, reply) {
+ expect(res.result.message).to.not.contain('script');
+ });
- return reply('Success');
- };
+ it('prevents xss with invalid cookie values in the request', async () => {
- var server = new Hapi.Server();
- server.connection();
+ const server = Hapi.server();
server.state('encoded', { encoding: 'iron' });
- server.route({ method: 'POST', path: '/', handler: handler });
+ server.route({
+ method: 'POST', path: '/',
+ handler: () => 'Success'
+ });
- server.inject({
+ const res = await server.inject({
method: 'POST',
url: '/',
payload: '{"something":"something"}',
headers: { cookie: 'encoded="";' }
- },
- function (res) {
-
- expect(res.result.message).to.not.contain('=value;' }
- },
- function (res) {
- expect(res.result.message).to.not.contain('":"other"}',
- headers: { 'content-type': 'application/json' }
- },
- function (res) {
-
- expect(res.result.message).to.not.contain('=value;' }
});
- server.inject({
- method: 'GET',
- url: '/fail/query?=value'
- },
- function (res) {
-
- expect(res.result.message).to.not.contain('