Comparing version 0.8.2 to 0.8.3
@@ -30,20 +30,28 @@ // Load modules | ||
var implementation = null; | ||
if (this.settings.engine === 'redis') { | ||
implementation = Redis; | ||
var engine = self.settings.engine; | ||
if (typeof engine === 'object') { | ||
this.connection = engine; | ||
engine = 'extension'; | ||
} | ||
else if (this.settings.engine === 'mongodb') { | ||
implementation = Mongo; | ||
else { | ||
var factory = null; | ||
if (engine === 'redis') { | ||
factory = Redis; | ||
} | ||
else if (engine === 'mongodb') { | ||
factory = Mongo; | ||
} | ||
else if (engine === 'memory') { | ||
factory = Memory; | ||
} | ||
Utils.assert(factory, 'Unknown cache engine type'); | ||
this.connection = new factory.Connection(this.settings); | ||
} | ||
else if (this.settings.engine === 'memory') { | ||
implementation = Memory; | ||
} | ||
Utils.assert(implementation, 'Unknown cache engine type'); | ||
this.connection = new implementation.Connection(this.settings); | ||
this.connection.start(function (err) { | ||
if (err) { | ||
Log.event(['cache', 'error', self.settings.engine], 'Failed initializing cache engine'); | ||
Log.event(['cache', 'error', engine], 'Failed initializing cache engine'); | ||
} | ||
@@ -58,5 +66,3 @@ }); | ||
if (this.connection) { | ||
this.connection.stop(); | ||
} | ||
this.connection.stop(); | ||
}; | ||
@@ -67,14 +73,14 @@ | ||
if (this.connection) { | ||
this.connection.start(callback); | ||
} | ||
this.connection.start(callback); | ||
}; | ||
internals.Client.prototype.isReady = function () { | ||
return this.connection.isReady(); | ||
}; | ||
internals.Client.prototype.validateSegmentName = function (name) { | ||
if (!this.connection) { | ||
return new Error('Disconnected'); | ||
} | ||
return this.connection.validateSegmentName(name); | ||
@@ -88,2 +94,6 @@ }; | ||
if (!this.connection.isReady()) { | ||
// Disconnected | ||
return callback(new Error('Disconnected')); | ||
} | ||
@@ -95,7 +105,2 @@ if (key === null) { | ||
if (!this.connection) { | ||
// Disconnected | ||
return callback(new Error('Disconnected')); | ||
} | ||
this.connection.get(key, function (err, result) { | ||
@@ -138,2 +143,7 @@ | ||
if (!this.connection.isReady()) { | ||
// Disconnected | ||
return callback(new Error('Disconnected')); | ||
} | ||
if (key === null) { | ||
@@ -144,7 +154,2 @@ // null key not allowed | ||
if (!this.connection) { | ||
// Disconnected | ||
return callback(new Error('Disconnected')); | ||
} | ||
if (ttl <= 0) { | ||
@@ -161,2 +166,7 @@ // Not cachable (or bad rules) | ||
if (!this.connection.isReady()) { | ||
// Disconnected | ||
return callback(new Error('Disconnected')); | ||
} | ||
if (key === null) { | ||
@@ -167,7 +177,2 @@ // null key not allowed | ||
if (!this.connection) { | ||
// Disconnected | ||
return callback(new Error('Disconnected')); | ||
} | ||
this.connection.drop(key, callback); // Always drop, regardless of caching rules | ||
@@ -174,0 +179,0 @@ }; |
// Load modules | ||
var Utils = require('../utils'); | ||
var Err = require('../error'); | ||
var NodeUtils = require('util'); | ||
@@ -15,3 +17,3 @@ | ||
this.settings = options; | ||
this.settings = options || {}; | ||
this.cache = null; | ||
@@ -24,7 +26,7 @@ return this; | ||
if (this.cache) { | ||
return callback(new Error('Connection already established')); | ||
if (!this.cache) { | ||
this.cache = {}; | ||
this.byteSize = 0; | ||
} | ||
this.cache = {}; | ||
return callback(); | ||
@@ -37,2 +39,3 @@ }; | ||
this.cache = null; | ||
this.byteSize = 0; | ||
return; | ||
@@ -42,2 +45,8 @@ }; | ||
internals.Connection.prototype.isReady = function () { | ||
return (!!this.cache); | ||
}; | ||
internals.Connection.prototype.validateSegmentName = function (name) { | ||
@@ -79,2 +88,3 @@ | ||
var self = this; | ||
if (!this.cache) { | ||
@@ -84,2 +94,3 @@ return callback(new Error('Connection not started')); | ||
var maxByteSize = self.settings.maxByteSize; | ||
var envelope = { | ||
@@ -94,2 +105,26 @@ item: value, | ||
var cachedItem = segment[key.id]; | ||
if (cachedItem && cachedItem.timeoutId) { | ||
clearTimeout(cachedItem.timeoutId); | ||
if (cachedItem.byteSize) { | ||
self.byteSize -= cachedItem.byteSize; // If the item existed, decrement the byteSize as the value could be different | ||
} | ||
} | ||
if (maxByteSize && maxByteSize > 0) { | ||
envelope.byteSize = internals.itemByteSize(value); | ||
if (self.byteSize + envelope.byteSize > maxByteSize) { | ||
return callback(Err.internal('Cache size limit reached')); | ||
} | ||
} | ||
var timeoutId = setTimeout(function() { | ||
self.drop(key, function() { }); | ||
}, ttl); | ||
envelope.timeoutId = timeoutId; | ||
segment[key.id] = envelope; | ||
@@ -108,2 +143,8 @@ return callback(null); | ||
if (segment) { | ||
var item = segment[key.id]; | ||
if (item && item.byteSize) { | ||
this.byteSize -= item.byteSize; | ||
} | ||
delete segment[key.id]; | ||
@@ -116,1 +157,58 @@ } | ||
internals.itemByteSize = function(item) { | ||
var type = typeof item; | ||
if (type === 'object') { | ||
return internals.objectByteSize(item); | ||
} | ||
else if (type === 'string') { | ||
return internals.stringByteSize(item); | ||
} | ||
else if (type === 'boolean') { | ||
return 4; | ||
} | ||
else if (type === 'number') { | ||
return 8; | ||
} | ||
else if (NodeUtils.isDate(item)) { | ||
return 8; | ||
} | ||
else if (NodeUtils.isArray(item)) { | ||
return internals.arrayByteSize(item); | ||
} | ||
else { | ||
return 0; | ||
} | ||
}; | ||
internals.objectByteSize = function(object) { | ||
var keys = Object.keys(object); | ||
var size = 8; // Initial object overhead | ||
size += keys.length * 2; | ||
for (var i = 0, il = keys.length; i < il; ++i) { | ||
size += internals.itemByteSize(object[keys[i]]); | ||
} | ||
return size; | ||
}; | ||
internals.stringByteSize = function(string) { | ||
return Buffer.byteLength(string); | ||
}; | ||
internals.arrayByteSize = function(array) { | ||
var size = 0; | ||
for (var i = 0, il = array.length; i < il; ++i) { | ||
size += internals.itemByteSize(array[i]); | ||
}; | ||
return size; | ||
}; |
@@ -32,4 +32,5 @@ // Load modules | ||
this.client = null; | ||
this.isReady = false; | ||
this.isConnected = false; | ||
this.collections = {}; | ||
this.startPending = null; // Set to an array of callbacks if start pending | ||
return this; | ||
@@ -43,15 +44,46 @@ }; | ||
if (this.client) { | ||
return callback(new Error('Connection already established')); | ||
// Check if already connected | ||
if (this.isConnected) { | ||
return callback(); | ||
} | ||
// Check if start already pending | ||
if (this.startPending) { | ||
this.startPending.push(callback); | ||
return; | ||
} | ||
// Set start pending state | ||
this.startPending = [callback]; | ||
var connected = function (err) { | ||
self.isConnected = !err; | ||
for (var i = 0, il = self.startPending.length; i < il; ++i) { | ||
self.startPending[i](err); | ||
} | ||
self.startPending = null; | ||
}; | ||
// Create client | ||
var server = new MongoDB.Server(this.settings.host, this.settings.port, { auto_reconnect: true, poolSize: this.settings.poolSize }); | ||
this.client = new MongoDB.Db(this.settings.partition, server, { safe: true }); | ||
// Open connection | ||
this.client.open(function (err, client) { | ||
if (err) { | ||
return callback(new Error('Failed opening connection')); | ||
return connected(new Error('Failed opening connection')); | ||
} | ||
// Authenticate | ||
if (self.settings.username) { | ||
@@ -64,12 +96,10 @@ self.client.authenticate(self.settings.username, self.settings.password, function (err, result) { | ||
self.stop(); | ||
return callback(new Error('Database authentication error: ' + (err ? JSON.stringify(err) : 'failed'))); | ||
return connected(new Error('Database authentication error: ' + (err ? JSON.stringify(err) : 'failed'))); | ||
} | ||
self.isReady = true; | ||
return callback(); | ||
return connected(); | ||
}); | ||
} | ||
else { | ||
self.isReady = true; | ||
return callback(); | ||
return connected(); | ||
} | ||
@@ -80,2 +110,19 @@ }); | ||
internals.Connection.prototype.stop = function () { | ||
if (this.client) { | ||
this.client.close(); | ||
this.client = null; | ||
this.collections = {}; | ||
this.isConnected = false; | ||
} | ||
}; | ||
internals.Connection.prototype.isReady = function () { | ||
return this.isConnected; | ||
}; | ||
internals.Connection.prototype.validateSegmentName = function (name) { | ||
@@ -121,3 +168,3 @@ | ||
if (!this.isReady) { | ||
if (!this.isConnected) { | ||
return callback(new Error('Connection not ready')); | ||
@@ -150,13 +197,2 @@ } | ||
internals.Connection.prototype.stop = function () { | ||
if (this.client) { | ||
this.client.close(); | ||
this.client = null; | ||
this.collections = {}; | ||
this.isReady = false; | ||
} | ||
}; | ||
internals.Connection.prototype.get = function (key, callback) { | ||
@@ -163,0 +199,0 @@ |
@@ -25,3 +25,3 @@ // Load modules | ||
if (this.client) { | ||
return callback(new Error('Connection already established')); | ||
return callback(); | ||
} | ||
@@ -64,2 +64,8 @@ | ||
internals.Connection.prototype.isReady = function () { | ||
return (!!this.client); | ||
}; | ||
internals.Connection.prototype.validateSegmentName = function (name) { | ||
@@ -66,0 +72,0 @@ |
@@ -17,3 +17,3 @@ // Load modules | ||
Utils.assert(!options || !options.toResponse || typeof options.toResponse === 'function', 'options.toReponse must be a function'); | ||
Utils.assert(code >= 400 && code < 600, 'Error code must be 4xx or 5xx'); | ||
Utils.assert(code >= 400, 'Error code must be 4xx or 5xx'); | ||
@@ -162,8 +162,2 @@ Error.call(this); | ||
return response; | ||
}; | ||
}; |
@@ -25,7 +25,2 @@ // Load modules | ||
if (process.env.NODE_ENV === 'test') { | ||
return; // Silence log output during test execution | ||
} | ||
tags = (tags instanceof Array ? tags : [tags]); | ||
@@ -47,4 +42,10 @@ var now = (timestamp ? (timestamp instanceof Date ? timestamp : new Date(timestamp)) : new Date()); | ||
internals.Logger.prototype.print = function (event, _isBypass) { | ||
internals.Logger.prototype.print = function (event, _isBypass, _isTest) { | ||
if (process.env.NODE_ENV === 'test' && | ||
!_isTest) { | ||
return; // Silence log output during test execution | ||
} | ||
var pad = function (value) { | ||
@@ -51,0 +52,0 @@ |
@@ -17,6 +17,4 @@ // Load modules | ||
var self = this; | ||
Utils.assert(options, 'Missing options'); | ||
Utils.assert(options.host || options.mapUri, 'Missing options.host and no options.mapUri'); | ||
Utils.assert(!!options.host ^ !!options.mapUri, 'Must have either options.host or options.mapUri'); | ||
Utils.assert(!options.passThrough || !route.cache.isMode('server'), 'Cannot use pass-through proxy mode with caching'); | ||
@@ -27,10 +25,8 @@ Utils.assert(!options.mapUri || typeof options.mapUri === 'function', 'options.mapUri must be a function'); | ||
this.settings = Utils.clone(options); // Options can be reused | ||
this.settings.protocol = this.settings.protocol || 'http'; | ||
this.settings.port = this.settings.port || (this.settings.protocol === 'http' ? 80 : 443); | ||
this.settings.xforward = this.settings.xforward || false; | ||
this.settings.passHeaders = this.settings.passThrough || false; | ||
this.settings.mapUri = this.settings.mapUri || internals.mapUri; // function (request, settings, function (err, uri, query)) | ||
this.settings.isCustomPostResponse = !!this.settings.postResponse; | ||
this.settings.postResponse = this.settings.postResponse || internals.postResponse; // function (request, settings, response, payload) | ||
this.settings = {}; | ||
this.settings.mapUri = options.mapUri || internals.mapUri(options.protocol, options.host, options.port); | ||
this.settings.xforward = options.xforward || false; | ||
this.settings.passHeaders = options.passThrough || false; | ||
this.settings.isCustomPostResponse = !!options.postResponse; | ||
this.settings.postResponse = options.postResponse || internals.postResponse; // function (request, settings, response, payload) | ||
@@ -47,3 +43,3 @@ return this; | ||
self.settings.mapUri(request, self.settings, function (err, uri, query) { | ||
self.settings.mapUri(request, function (err, uri, query) { | ||
@@ -63,3 +59,3 @@ if (err) { | ||
if (self.settings.passThrough) { // Never set with cache | ||
if (self.settings.passHeaders) { // Never set with cache | ||
options.headers = Utils.clone(req.headers); | ||
@@ -123,5 +119,12 @@ delete options.headers.host; | ||
internals.mapUri = function (request, settings, callback) { | ||
internals.mapUri = function (protocol, host, port) { | ||
return callback(null, settings.protocol + '://' + settings.host + ':' + settings.port + request.path, request.query); | ||
protocol = protocol || 'http'; | ||
port = port || (protocol === 'http' ? 80 : 443); | ||
var baseUrl = protocol + '://' + host + ':' + port; | ||
return function(request, callback) { | ||
return callback(null, baseUrl + request.path, request.query); | ||
}; | ||
}; | ||
@@ -132,12 +135,14 @@ | ||
if (response.statusCode >= 400) { | ||
return request.reply(Err.internal('Error proxy response', { code: response.statusCode, payload: payload })); | ||
var contentType = response.headers['content-type']; | ||
var statusCode = response.statusCode; | ||
if (statusCode >= 400) { | ||
return request.reply(Err.passThrough(statusCode, payload, contentType)); | ||
} | ||
if (response.headers['content-type']) { | ||
request.reply.type(response.headers['content-type']); | ||
if (contentType) { | ||
request.reply.type(contentType); | ||
} | ||
return request.reply(payload); | ||
}; | ||
}; |
@@ -571,3 +571,2 @@ // Load modules | ||
var headers = this.response.options.headers || {}; | ||
var contentType = ''; | ||
var payload = null; | ||
@@ -577,5 +576,24 @@ | ||
// Set CORS and Cache headers | ||
self._setCors(headers); | ||
self._setCache(headers); | ||
// Set options | ||
if (self.response.options.created) { | ||
code = 201; | ||
headers.Location = self.response.options.created; | ||
} | ||
if (self.response.options.contentType) { | ||
headers['Content-Type'] = self.response.options.contentType; | ||
} | ||
if (self.response.options.contentLength) { | ||
headers['Content-Length'] = self.response.options.contentLength; | ||
} | ||
// Empty response | ||
var result = self.response.result; | ||
@@ -600,3 +618,3 @@ if (!result) { | ||
result = errPayload; | ||
contentType = errContentType; | ||
headers['Content-Type'] = errContentType || headers['Content-Type']; // Override | ||
}); | ||
@@ -608,5 +626,3 @@ } | ||
result = errResponse.payload || ''; | ||
if (errResponse.contentType) { | ||
contentType = errResponse.contentType; | ||
} | ||
headers['Content-Type'] = errResponse.contentType || headers['Content-Type']; // Override | ||
} | ||
@@ -618,17 +634,2 @@ } | ||
// Set options | ||
if (self.response.options.created) { | ||
code = 201; | ||
headers.Location = self.response.options.created; | ||
} | ||
if (self.response.options.contentType) { | ||
contentType = self.response.options.contentType; | ||
} | ||
if (self.response.options.contentLength) { | ||
headers['Content-Length'] = self.response.options.contentLength; | ||
} | ||
// Payload | ||
@@ -652,3 +653,3 @@ | ||
payload = JSON.stringify(result); | ||
contentType = contentType || 'application/json'; | ||
headers['Content-Type'] = headers['Content-Type'] || 'application/json'; // If not defined | ||
} | ||
@@ -660,3 +661,3 @@ else { | ||
payload = (typeof result === 'string' ? result : JSON.stringify(result)); | ||
contentType = contentType || 'text/html'; | ||
headers['Content-Type'] = headers['Content-Type'] || 'text/html'; // If not defined | ||
} | ||
@@ -667,8 +668,5 @@ | ||
inject(result); | ||
if (!headers['Content-Type']) { | ||
headers['Content-Type'] = contentType; | ||
} | ||
if (payload !== null && | ||
!headers['Content-Length']) { // payload can be empty string | ||
if (payload !== null && // payload can be empty string | ||
!headers['Content-Length']) { | ||
@@ -675,0 +673,0 @@ headers['Content-Length'] = Buffer.byteLength(payload); |
@@ -5,3 +5,3 @@ { | ||
"homepage": "http://hapijs.com", | ||
"version": "0.8.2", | ||
"version": "0.8.3", | ||
"author": "Eran Hammer <eran@hueniverse.com> (http://hueniverse.com)", | ||
@@ -8,0 +8,0 @@ "contributors":[ |
@@ -11,3 +11,3 @@ ![hapi Logo](https://raw.github.com/walmartlabs/hapi/master/images/hapi.png) | ||
Current version: **0.8.2** | ||
Current version: **0.8.3** | ||
@@ -51,2 +51,3 @@ [![Build Status](https://secure.travis-ci.org/walmartlabs/hapi.png)](http://travis-ci.org/walmartlabs/hapi) | ||
- [Caching](#caching) | ||
- [Proxy](#proxy) | ||
- [Route Prerequisites](#route-prerequisites) | ||
@@ -308,3 +309,3 @@ <p></p> | ||
an object with the following options: | ||
- `engine` - the cache server implementation. Options are _redis_ and _mongodb_. | ||
- `engine` - the cache server implementation. Options are _redis_, _mongodb_, and _memory_. | ||
- `host` - the cache server hostname. | ||
@@ -315,5 +316,7 @@ - `port` - the cache server port. | ||
For convenience, two pre-configured options are provided for Redis and MongoDB. To use them, simply set the server's `cache` option to: | ||
For convenience, pre-configured options are provided for Redis, MongoDB, and an experimental memory store. To use them, simply set the server's `cache` option to: | ||
* _'redis'_ - Connects to _127.0.0.1:6379_ using partition name 'hapi-cache'. | ||
* _'mongodb'_ - Connects to _127.0.0.1:27017_ using partition name 'hapi-cache', no authentication, and pool size 5. | ||
* _'memory'_ - This is an experimental engine and should be avoided in production environments. The memory engine will run within the node process and supports the following option: | ||
- `maxByteSize` - Sets an upper limit on the number of bytes that can be consumed by the total of everything cached in the memory engine. Once this limit is reached no more items will be added to the cache. | ||
@@ -623,4 +626,25 @@ For example: | ||
### Requisites | ||
### Proxy | ||
It is possible with hapi to setup a reverse proxy for routes. This is especially useful if you plan to stand-up hapi in front of an existing API or you need to augment the functionality of an existing API. Additionally, this feature is powerful in that it can be combined with caching to cache the responses from external APIs. The proxy route configuration has the following options: | ||
* `passThrough` - determines if the headers sent from the clients user-agent will be forwarded on to the external service being proxied to (default: false) | ||
* `xforward` - determines if the x-forward headers will be set when making a request to the proxied endpoint (default: false) | ||
* `host` - The host to proxy requests to. The same path on the client request will be used as the path to the host. | ||
* `port` - The port to use when making a request to the host. | ||
* `protocol` - The protocol to use when making a request to the proxied host (http or https) | ||
* `mapUri` - A function that receives the clients request and a passes the URI to a callback to make the proxied request to. If host is set mapUri cannot be used, set either host or mapUri. | ||
* `postResponse` - A function that will be executed before sending the response to the client for requests that can be cached. Use this for any custom error handling of responses from the proxied endpoint. | ||
For example, to proxy a request to the homepage to google: | ||
```javascript | ||
// Create Hapi servers | ||
var http = new Hapi.Server('0.0.0.0', 8080); | ||
// Proxy request to / to google.com | ||
http.addRoute({ method: 'GET', path: '/', config: { proxy: { protocol: 'http', host: 'google.com', port: 80 } } }); | ||
http.start(); | ||
### Prequisites | ||
Before the handler is called, it is often necessary to perform other actions such as loading required reference data from a database. The `pre` option | ||
@@ -627,0 +651,0 @@ allows defining such pre-handler methods. The methods are called in order, unless a `mode` is specified with value 'parallel' in which case, all the parallel methods |
@@ -17,6 +17,4 @@ // Load modules | ||
var listening = false; | ||
var config = null; | ||
var routeCache = { | ||
mode: 'server', | ||
expiresIn: 500 | ||
@@ -36,3 +34,6 @@ }; | ||
{ method: 'GET', path: '/item', config: { handler: activeItem } }, | ||
{ method: 'POST', path: '/item', config: { handler: item } }]); | ||
{ method: 'POST', path: '/item', config: { handler: item } }, | ||
{ method: 'GET', path: '/unauthorized', config: { handler: unauthorized }}, | ||
{ method: 'POST', path: '/echo', config: { handler: echoPostBody } } | ||
]); | ||
@@ -43,3 +44,8 @@ _server = new Hapi.Server('0.0.0.0', 18092, config); | ||
{ method: 'GET', path: '/item', config: { proxy: { host: '127.0.0.1', port: 18093 }, cache: routeCache } }, | ||
{ method: 'POST', path: '/item', config: { proxy: { host: '127.0.0.1', port: 18093 } } } | ||
{ method: 'GET', path: '/unauthorized', config: { proxy: { host: '127.0.0.1', port: 18093 }, cache: routeCache } }, | ||
{ method: 'POST', path: '/item', config: { proxy: { host: '127.0.0.1', port: 18093 } } }, | ||
{ method: 'POST', path: '/notfound', config: { proxy: { host: '127.0.0.1', port: 18093 } } }, | ||
{ method: 'GET', path: '/postResponseError', config: { proxy: { host: '127.0.0.1', port: 18093, postResponse: postResponseWithError }, cache: routeCache } }, | ||
{ method: 'POST', path: '/echo', config: { proxy: { mapUri: mapUri } } }, | ||
{ method: 'GET', path: '/maperror', config: { proxy: { mapUri: mapUriWithError } } } | ||
]); | ||
@@ -68,3 +74,14 @@ | ||
function mapUri(request, callback) { | ||
return callback(null, 'http://127.0.0.1:18093' + request.path, request.query); | ||
} | ||
function mapUriWithError(request, callback) { | ||
return callback(new Error('myerror')); | ||
} | ||
function profile(request) { | ||
request.reply({ | ||
@@ -77,2 +94,3 @@ 'id': 'fa0dbda9b1b', | ||
function activeItem(request) { | ||
request.reply({ | ||
@@ -85,2 +103,3 @@ 'id': '55cf687663', | ||
function item(request) { | ||
request.reply.created('http://google.com')({ | ||
@@ -92,3 +111,25 @@ 'id': '55cf687663', | ||
function echoPostBody(request) { | ||
request.reply(request.payload); | ||
} | ||
function unauthorized(request) { | ||
request.reply(Hapi.Error.unauthorized('Not authorized')); | ||
} | ||
function postResponseWithError(request) { | ||
request.reply(Hapi.Error.forbidden('Forbidden')); | ||
} | ||
function postResponse(request, settings, response, payload) { | ||
request.reply.type(response.headers['content-type']); | ||
request.reply(payload); | ||
} | ||
function makeRequest(options, callback) { | ||
var next = function (err, res) { | ||
@@ -104,3 +145,4 @@ return callback(res); | ||
method: options.method, | ||
url: _serverUrl + options.path | ||
url: _serverUrl + options.path, | ||
form: options.form | ||
}, next); | ||
@@ -110,3 +152,5 @@ } | ||
it('forwards on the response when making a GET request', function (done) { | ||
makeRequest({ path: '/profile' }, function (rawRes) { | ||
expect(rawRes.statusCode).to.equal(200); | ||
@@ -119,3 +163,5 @@ expect(rawRes.body).to.contain('John Doe'); | ||
it('forwards on the response when making a GET request to a route that also accepts a POST', function (done) { | ||
makeRequest({ path: '/item' }, function (rawRes) { | ||
expect(rawRes.statusCode).to.equal(200); | ||
@@ -128,3 +174,5 @@ expect(rawRes.body).to.contain('Active Item'); | ||
it('forwards on the status code when making a POST request', function (done) { | ||
makeRequest({ path: '/item', method: 'post' }, function (rawRes) { | ||
expect(rawRes.statusCode).to.equal(201); | ||
@@ -135,2 +183,57 @@ expect(rawRes.body).to.contain('Item'); | ||
}); | ||
it('sends the correct status code with a request is unauthorized', function(done) { | ||
makeRequest({ path: '/unauthorized', method: 'get' }, function (rawRes) { | ||
expect(rawRes.statusCode).to.equal(401); | ||
done(); | ||
}); | ||
}); | ||
it('sends a 404 status code with a proxied route doesn\'t exist', function(done) { | ||
makeRequest({ path: '/notfound', method: 'get' }, function (rawRes) { | ||
expect(rawRes.statusCode).to.equal(404); | ||
done(); | ||
}); | ||
}); | ||
it('forwards on the status code when a custom postResponse returns an error', function(done) { | ||
makeRequest({ path: '/postResponseError', method: 'get' }, function (rawRes) { | ||
expect(rawRes.statusCode).to.equal(403); | ||
done(); | ||
}); | ||
}); | ||
it('forwards the error message with a custom postResponse and a route error', function(done) { | ||
makeRequest({ path: '/postResponseNotFound', method: 'get' }, function (rawRes) { | ||
expect(rawRes.body).to.contain('error'); | ||
done(); | ||
}); | ||
}); | ||
it('forwards on a POST body', function(done) { | ||
makeRequest({ path: '/echo', method: 'post', form: { echo: true } }, function (rawRes) { | ||
expect(rawRes.statusCode).to.equal(200); | ||
expect(rawRes.body).to.contain('echo'); | ||
done(); | ||
}); | ||
}); | ||
it('replies with an error when it occurs in mapUri', function(done) { | ||
makeRequest({ path: '/maperror', method: 'get' }, function (rawRes) { | ||
expect(rawRes.body).to.contain('myerror'); | ||
done(); | ||
}); | ||
}); | ||
}); |
@@ -8,2 +8,3 @@ // Load modules | ||
var Defaults = require(libPath + 'defaults'); | ||
var Log = require(libPath + 'log'); | ||
var Sinon = require('sinon'); | ||
@@ -17,2 +18,3 @@ | ||
it('throws an error if using an unknown engine type', function (done) { | ||
var fn = function () { | ||
@@ -30,26 +32,22 @@ var options = { | ||
if (useRedis) { | ||
it('creates a new connection when using redis', function (done) { | ||
var client = new Cache.Client(Defaults.cache('redis')); | ||
expect(client).to.exist; | ||
done(); | ||
}); | ||
} | ||
var testEngine = function (engine) { | ||
if (useMongo) { | ||
it('creates a new connection when using mongodb', function (done) { | ||
var client = new Cache.Client(Defaults.cache('mongodb')); | ||
it('creates a new connection using ' + engine, function (done) { | ||
expect(client.connection.client).to.exist; | ||
done(); | ||
var client = new Cache.Client(Defaults.cache(engine)); | ||
client.start(function (err) { | ||
expect(client.isReady()).to.equal(true); | ||
done(); | ||
}); | ||
}); | ||
} | ||
if (useRedis) { | ||
it('returns not found on get when using null key', function (done) { | ||
var client = new Cache.Client(Defaults.cache('redis')); | ||
client.get(null, function (err, result) { | ||
it('closes the connection using ' + engine, function (done) { | ||
expect(err).to.equal(null); | ||
expect(result).to.equal(null); | ||
var client = new Cache.Client(Defaults.cache(engine)); | ||
client.start(function (err) { | ||
expect(client.isReady()).to.equal(true); | ||
client.stop(); | ||
expect(client.isReady()).to.equal(false); | ||
done(); | ||
@@ -59,4 +57,39 @@ }); | ||
it('returns error on set when using null key', function (done) { | ||
var client = new Cache.Client(Defaults.cache('redis')); | ||
it('ignored starting a connection twice using ' + engine, function (done) { | ||
var client = new Cache.Client(Defaults.cache(engine)); | ||
var x = 2; | ||
var start = function () { | ||
client.start(function (err) { | ||
expect(client.isReady()).to.equal(true); | ||
--x; | ||
if (!x) { | ||
done(); | ||
} | ||
}); | ||
}; | ||
start(); | ||
start(); | ||
}); | ||
it('returns not found on get when using null key using ' + engine, function (done) { | ||
var client = new Cache.Client(Defaults.cache(engine)); | ||
client.start(function (err) { | ||
client.get(null, function (err, result) { | ||
expect(err).to.equal(null); | ||
expect(result).to.equal(null); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
it('returns error on set when using null key using ' + engine, function (done) { | ||
var client = new Cache.Client(Defaults.cache(engine)); | ||
client.set(null, {}, 1000, function (err) { | ||
@@ -69,4 +102,18 @@ | ||
it('returns error on drop when using null key', function (done) { | ||
var client = new Cache.Client(Defaults.cache('redis')); | ||
it('ignores set when using non-positive ttl value using ' + engine, function (done) { | ||
var client = new Cache.Client(Defaults.cache(engine)); | ||
client.start(function (err) { | ||
client.set('x', 'y', 0, function (err) { | ||
expect(err).to.not.exist; | ||
done(); | ||
}); | ||
}); | ||
}); | ||
it('returns error on drop when using null key using ' + engine, function (done) { | ||
var client = new Cache.Client(Defaults.cache(engine)); | ||
client.drop(null, function (err) { | ||
@@ -78,66 +125,193 @@ | ||
}); | ||
} | ||
if (useMongo) { | ||
it('creates a new connection when using mongodb', function (done) { | ||
var client = new Cache.Client(Defaults.cache('mongodb')); | ||
it('returns error on get when stopped using ' + engine, function (done) { | ||
expect(client).to.exist; | ||
done(); | ||
var client = new Cache.Client(Defaults.cache(engine)); | ||
client.stop(); | ||
client.connection.get('x', function (err, result) { | ||
expect(err).to.exist; | ||
expect(result).to.not.exist; | ||
done(); | ||
}); | ||
}); | ||
} | ||
describe('#stop', function() { | ||
it('returns error on set when stopped using ' + engine, function (done) { | ||
if (useMongo) { | ||
it('closes the connection when using mongodb', function (done) { | ||
var client = new Cache.Client(Defaults.cache('mongodb')); | ||
var client = new Cache.Client(Defaults.cache(engine)); | ||
client.stop(); | ||
client.connection.set('x', 'y', 1, function (err) { | ||
expect(client.connection.client).to.exist; | ||
client.stop(); | ||
expect(client.connection.client).to.not.exist; | ||
expect(err).to.exist; | ||
done(); | ||
}); | ||
} | ||
}); | ||
if (useRedis) { | ||
it('closes the connection when using redis', function (done) { | ||
var client = new Cache.Client(Defaults.cache('redis')); | ||
it('returns error on drop when stopped using ' + engine, function (done) { | ||
expect(client.connection.client).to.exist; | ||
var client = new Cache.Client(Defaults.cache(engine)); | ||
client.stop(); | ||
client.connection.drop('x', function (err) { | ||
client.stop(); | ||
expect(client.connection.client).to.not.exist; | ||
expect(err).to.exist; | ||
done(); | ||
}); | ||
} | ||
}); | ||
}); | ||
describe('#stop', function() { | ||
it('returns error on missing segment name using ' + engine, function (done) { | ||
if (useMongo) { | ||
it('closes the connection when using mongodb', function (done) { | ||
var client = new Cache.Client(Defaults.cache('mongodb')); | ||
var config = { | ||
expiresIn: 50000, | ||
segment: '' | ||
}; | ||
var fn = function () { | ||
var client = new Cache.Client(Defaults.cache(engine)); | ||
var cache = new Cache.Policy(config, client); | ||
}; | ||
expect(fn).to.throw(Error); | ||
done(); | ||
}); | ||
expect(client.connection.client).to.exist; | ||
it('returns error on bad segment name using ' + engine, function (done) { | ||
client.stop(); | ||
expect(client.connection.client).to.not.exist; | ||
var config = { | ||
expiresIn: 50000, | ||
segment: 'a\0b' | ||
}; | ||
var fn = function () { | ||
var client = new Cache.Client(Defaults.cache(engine)); | ||
var cache = new Cache.Policy(config, client); | ||
}; | ||
expect(fn).to.throw(Error); | ||
done(); | ||
}); | ||
it('returns error when cache item dropped while stopped using ' + engine, function (done) { | ||
var client = new Cache.Client(Defaults.cache(engine)); | ||
client.stop(); | ||
client.drop('a', function (err) { | ||
expect(err).to.exist; | ||
done(); | ||
}); | ||
} | ||
}); | ||
}; | ||
it('closes the connection when using redis', function (done) { | ||
var client = new Cache.Client(Defaults.cache('redis')); | ||
testEngine('memory'); | ||
expect(client.connection.client).to.exist; | ||
if (useMongo) { | ||
testEngine('mongodb'); | ||
} | ||
client.stop(); | ||
expect(client.connection.client).to.not.exist; | ||
if (useRedis) { | ||
testEngine('redis'); | ||
} | ||
// Error engine | ||
var failOn = function (method) { | ||
var err = new Error('FAIL'); | ||
var errorEngineImp = { | ||
start: function (callback) { callback(method === 'start' ? err : null); }, | ||
stop: function () { }, | ||
isReady: function () { return method !== 'isReady'; }, | ||
validateSegmentName: function () { return method === 'validateSegmentName' ? err : null; }, | ||
get: function (key, callback) { return callback(method === 'get' ? err : null); }, | ||
set: function (key, value, ttl, callback) { return callback(method === 'set' ? err : null); }, | ||
drop: function (key, callback) { return callback(method === 'drop' ? err : null); } | ||
}; | ||
var options = { | ||
engine: errorEngineImp, | ||
partition: 'hapi-cache' | ||
}; | ||
return new Cache.Client(options); | ||
}; | ||
it('returns error when calling get on a bad connection', function (done) { | ||
var client = failOn('get'); | ||
client.get('x', function (err, result) { | ||
expect(err).to.exist; | ||
expect(err.message).to.equal('FAIL'); | ||
done(); | ||
}); | ||
}); | ||
it('logs an error when fails to start on bad connection', function (done) { | ||
Log.once('log', function (event) { | ||
expect(event).to.exist; | ||
expect(event.tags).to.exist; | ||
expect(event.tags[0]).to.equal('cache'); | ||
done(); | ||
}); | ||
var client = failOn('start'); | ||
}); | ||
}); | ||
describe('Policy', function () { | ||
var getCache = function () { | ||
var config = { | ||
mode: 'client', | ||
expiresIn: 1 | ||
}; | ||
var client = new Cache.Client(Defaults.cache('memory')); | ||
var cache = new Cache.Policy(config, client); | ||
return cache; | ||
}; | ||
it('returns null on get when cache mode is not server', function (done) { | ||
getCache().get('x', function (err, result) { | ||
expect(err).to.not.exist; | ||
expect(result).to.not.exist; | ||
done(); | ||
}); | ||
}); | ||
it('returns null on set when cache mode is not server', function (done) { | ||
getCache().set('x', 'y', 100, function (err) { | ||
expect(err).to.not.exist; | ||
done(); | ||
}); | ||
}); | ||
it('returns null on drop when cache mode is not server', function (done) { | ||
getCache().drop('x', function (err) { | ||
expect(err).to.not.exist; | ||
done(); | ||
}); | ||
}); | ||
it('returns null on get when item expired', function (done) { | ||
var client = new Cache.Client(Defaults.cache('memory')); | ||
client.set('x', 'y', 1, function (err) { | ||
setTimeout(function () { | ||
client.get('x', function (err, result) { | ||
expect(err).to.not.exist; | ||
expect(result).to.not.exist; | ||
done(); | ||
}); | ||
}, 2); | ||
}); | ||
}); | ||
}); | ||
describe('Cache Rules', function () { | ||
@@ -148,2 +322,3 @@ | ||
it('compiles a single rule', function (done) { | ||
var config = { | ||
@@ -160,2 +335,3 @@ expiresIn: 50000 | ||
it('is enabled for both client and server by defaults', function (done) { | ||
var config = { | ||
@@ -165,3 +341,3 @@ expiresIn: 50000, | ||
}; | ||
var client = new Cache.Client(Defaults.cache('redis')); | ||
var client = new Cache.Client(Defaults.cache('memory')); | ||
var cache = new Cache.Policy(config, client); | ||
@@ -177,6 +353,7 @@ | ||
it('is disabled when mode is none', function (done) { | ||
var config = { | ||
mode: 'none' | ||
}; | ||
var client = new Cache.Client(Defaults.cache('redis')); | ||
var client = new Cache.Client(Defaults.cache('memory')); | ||
var cache = new Cache.Policy(config, client); | ||
@@ -191,2 +368,3 @@ | ||
it('throws an error when mode is none and config has other options set', function (done) { | ||
var config = { | ||
@@ -197,2 +375,3 @@ mode: 'none', | ||
var fn = function () { | ||
var cache = new Cache.Policy(config, {}); | ||
@@ -207,2 +386,3 @@ }; | ||
it('throws an error when segment is missing', function (done) { | ||
var config = { | ||
@@ -212,3 +392,4 @@ expiresIn: 50000 | ||
var fn = function () { | ||
var client = new Cache.Client(Defaults.cache('redis')); | ||
var client = new Cache.Client(Defaults.cache('memory')); | ||
var cache = new Cache.Policy(config, client); | ||
@@ -235,2 +416,3 @@ }; | ||
it('throws an error when parsing a rule with both expiresAt and expiresIn', function (done) { | ||
var config = { | ||
@@ -241,2 +423,3 @@ expiresAt: 50, | ||
var fn = function () { | ||
Cache.compile(config); | ||
@@ -251,5 +434,7 @@ }; | ||
it('throws an error when parsing a rule with niether expiresAt or expiresIn', function (done) { | ||
var config = { | ||
}; | ||
var fn = function () { | ||
Cache.compile(config); | ||
@@ -264,2 +449,3 @@ }; | ||
it('throws an error when parsing a bad expiresAt value', function (done) { | ||
var config = { | ||
@@ -269,2 +455,3 @@ expiresAt: function () { } | ||
var fn = function () { | ||
Cache.compile(config); | ||
@@ -279,2 +466,3 @@ }; | ||
it('throws an error when staleIn is used without staleTimeout', function (done) { | ||
var config = { | ||
@@ -285,2 +473,3 @@ expiresAt: '03:00', | ||
var fn = function () { | ||
Cache.compile(config); | ||
@@ -295,2 +484,3 @@ }; | ||
it('throws an error when staleTimeout is used without staleIn', function (done) { | ||
var config = { | ||
@@ -301,2 +491,3 @@ expiresAt: '03:00', | ||
var fn = function () { | ||
Cache.compile(config); | ||
@@ -311,2 +502,3 @@ }; | ||
it('throws an error when staleIn is greater than a day and using expiresAt', function (done) { | ||
var config = { | ||
@@ -318,2 +510,3 @@ expiresAt: '03:00', | ||
var fn = function () { | ||
Cache.compile(config); | ||
@@ -328,2 +521,3 @@ }; | ||
it('throws an error when staleIn is greater than expiresIn', function (done) { | ||
var config = { | ||
@@ -335,2 +529,3 @@ expiresIn: 500000, | ||
var fn = function () { | ||
Cache.compile(config); | ||
@@ -345,2 +540,3 @@ }; | ||
it('throws an error when staleTimeout is greater than expiresIn', function (done) { | ||
var config = { | ||
@@ -352,2 +548,3 @@ expiresIn: 500000, | ||
var fn = function () { | ||
Cache.compile(config); | ||
@@ -362,2 +559,3 @@ }; | ||
it('throws an error when staleTimeout is greater than expiresIn - staleIn', function (done) { | ||
var config = { | ||
@@ -369,2 +567,3 @@ expiresIn: 30000, | ||
var fn = function () { | ||
Cache.compile(config); | ||
@@ -379,2 +578,3 @@ }; | ||
it('throws an error when staleTimeout is used without server mode', function (done) { | ||
var config = { | ||
@@ -387,2 +587,3 @@ mode: 'client', | ||
var fn = function () { | ||
var cache = new Cache.Policy(config, {}); | ||
@@ -397,2 +598,3 @@ }; | ||
it('returns rule when staleIn is less than expiresIn', function (done) { | ||
var config = { | ||
@@ -412,2 +614,3 @@ expiresIn: 1000000, | ||
it('returns rule when staleIn is less than 24 hours and using expiresAt', function (done) { | ||
var config = { | ||
@@ -426,2 +629,3 @@ expiresAt: '03:00', | ||
it('throws an error if has only staleTimeout or staleIn', function (done) { | ||
var config = { | ||
@@ -434,2 +638,3 @@ mode: 'server', | ||
var fn = function () { | ||
Cache.compile(config); | ||
@@ -443,2 +648,3 @@ }; | ||
it('doesn\'t throw an error if has both staleTimeout and staleIn', function (done) { | ||
var config = { | ||
@@ -452,2 +658,3 @@ mode: 'server', | ||
var fn = function () { | ||
Cache.compile(config); | ||
@@ -460,2 +667,3 @@ }; | ||
it('throws an error if trying to use stale caching on the client', function (done) { | ||
var config = { | ||
@@ -469,2 +677,3 @@ mode: 'client', | ||
var fn = function () { | ||
Cache.compile(config); | ||
@@ -478,2 +687,3 @@ }; | ||
it('converts the stale time to ms', function (done) { | ||
var config = { | ||
@@ -493,2 +703,3 @@ mode: 'server+client', | ||
it('throws an error if staleTimeout is greater than expiresIn', function (done) { | ||
var config = { | ||
@@ -502,2 +713,3 @@ mode: 'client', | ||
var fn = function () { | ||
Cache.compile(config); | ||
@@ -511,2 +723,3 @@ }; | ||
it('throws an error if staleIn is greater than expiresIn', function (done) { | ||
var config = { | ||
@@ -520,2 +733,3 @@ mode: 'client', | ||
var fn = function () { | ||
Cache.compile(config); | ||
@@ -532,2 +746,3 @@ }; | ||
it('returns zero when a rule is expired', function (done) { | ||
var config = { | ||
@@ -546,2 +761,3 @@ expiresIn: 50000 | ||
it('returns a positive number when a rule is not expired', function (done) { | ||
var config = { | ||
@@ -559,2 +775,3 @@ expiresIn: 50000 | ||
it('returns the correct expires time when no created time is provided', function (done) { | ||
var config = { | ||
@@ -571,2 +788,3 @@ expiresIn: 50000 | ||
it('returns 0 when created several days ago and expiresAt is used', function (done) { | ||
var config = { | ||
@@ -583,3 +801,25 @@ expiresAt: '13:00' | ||
it('returns 0 when created in the future', function (done) { | ||
var config = { | ||
expiresIn: '100' | ||
}; | ||
var created = Date.now() + 1000; | ||
var rule = Cache.compile(config); | ||
var ttl = Cache.ttl(rule, created); | ||
expect(ttl).to.equal(0); | ||
done(); | ||
}); | ||
it('returns 0 for bad rule', function (done) { | ||
var created = Date.now() - 1000; | ||
var ttl = Cache.ttl({}, created); | ||
expect(ttl).to.equal(0); | ||
done(); | ||
}); | ||
it('returns 0 when created 60 hours ago and expiresAt is used with an hour before the created hour', function (done) { | ||
var config = { | ||
@@ -597,3 +837,5 @@ expiresAt: '12:00' | ||
it('returns a positive number when using a future expiresAt', function (done) { | ||
var hour = new Date(Date.now() + 60 * 60 * 1000).getHours(); | ||
hour = hour === 0 ? 1 : hour; | ||
@@ -612,2 +854,3 @@ var config = { | ||
it('returns the correct number when using a future expiresAt', function (done) { | ||
var twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000); | ||
@@ -631,2 +874,3 @@ var hours = twoHoursAgo.getHours(); | ||
it('returns correct number when using an expiresAt time tomorrow', function (done) { | ||
var hour = new Date(Date.now() - 60 * 60 * 1000).getHours(); | ||
@@ -646,2 +890,3 @@ | ||
it('returns correct number when using a created time from yesterday and expires in 2 hours', function (done) { | ||
var hour = new Date(Date.now() + 2 * 60 * 60 * 1000).getHours(); | ||
@@ -664,62 +909,2 @@ | ||
describe('Memory', function () { | ||
it('returns error when cache started twice', function (done) { | ||
var config = { | ||
expiresIn: 50000, | ||
segment: 'test' | ||
}; | ||
var client = new Cache.Client(Defaults.cache('memory')); | ||
client.start(function (err) { | ||
expect(err).to.exist; | ||
done(); | ||
}); | ||
}); | ||
it('returns error on missing segment name', function (done) { | ||
var config = { | ||
expiresIn: 50000, | ||
segment: '' | ||
}; | ||
var fn = function () { | ||
var client = new Cache.Client(Defaults.cache('memory')); | ||
var cache = new Cache.Policy(config, client); | ||
}; | ||
expect(fn).to.throw(Error); | ||
done(); | ||
}); | ||
it('returns error on bad segment name', function (done) { | ||
var config = { | ||
expiresIn: 50000, | ||
segment: 'a\0b' | ||
}; | ||
var fn = function () { | ||
var client = new Cache.Client(Defaults.cache('memory')); | ||
var cache = new Cache.Policy(config, client); | ||
}; | ||
expect(fn).to.throw(Error); | ||
done(); | ||
}); | ||
it('returns error when cache item dropped while stopped', function (done) { | ||
var config = { | ||
expiresIn: 50000, | ||
segment: 'test' | ||
}; | ||
var client = new Cache.Client(Defaults.cache('memory')); | ||
client.stop(); | ||
client.drop('a', function (err) { | ||
expect(err).to.exist; | ||
done(); | ||
}); | ||
}); | ||
}); | ||
describe('Stale', function () { | ||
@@ -934,1 +1119,2 @@ | ||
}); | ||
@@ -23,7 +23,5 @@ // Load modules | ||
it('fires an event with the passed in tags', function(done) { | ||
var env = process.env.NODE_ENV; | ||
it('fires an event with the passed in tags', function (done) { | ||
var tags = ['hello']; | ||
process.env.NODE_ENV = 'nottatest'; | ||
Log.once('log', function(event) { | ||
@@ -36,4 +34,2 @@ expect(event).to.exist; | ||
Log.event(tags, null, Date.now()); | ||
process.env.NODE_ENV = env; | ||
}); | ||
@@ -43,3 +39,2 @@ | ||
var env = process.env.NODE_ENV; | ||
var tags = ['hello']; | ||
process.env.NODE_ENV = 'nottatest'; | ||
@@ -51,2 +46,3 @@ | ||
var tags = ['hello']; | ||
Log.event(tags, null, Date.now()); | ||
@@ -72,3 +68,3 @@ | ||
Log.print(event, false); | ||
Log.print(event, false, true); | ||
unhookStdout(); | ||
@@ -89,3 +85,3 @@ done(); | ||
Log.print(event, false); | ||
Log.print(event, false, true); | ||
unhookStdout(); | ||
@@ -92,0 +88,0 @@ done(); |
@@ -66,3 +66,3 @@ // Load modules | ||
expect(result).to.not.exist; | ||
expect(mongo.isReady).to.be.true; | ||
expect(mongo.isReady()).to.be.true; | ||
done(); | ||
@@ -69,0 +69,0 @@ }); |
@@ -39,19 +39,2 @@ // Load modules | ||
}); | ||
it('returns an error when the connection is already started', function(done) { | ||
var options = { | ||
host: '127.0.0.1', | ||
port: 6379 | ||
}; | ||
var redis = new Redis.Connection(options); | ||
redis.start(function() { | ||
redis.start(function(err) { | ||
expect(err).to.exist; | ||
expect(err).to.be.instanceOf(Error); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
@@ -58,0 +41,0 @@ |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
491668
87
8011
949