Skip to content

Commit

Permalink
Tests (#1)
Browse files Browse the repository at this point in the history
* Start on tests

* Add .travis.yml

* Clean up a bit

* Test parseJson

* Test routes

* Test static

* Switch methods and routes tests to supertest

* Use http-errors instead of boom

* Use path-match instead of path-to-regexp

* Test mount

* Add logo to readme

* Cleanup
  • Loading branch information
flintinatux authored Jan 21, 2017
1 parent 6360ff8 commit 33c7da6
Show file tree
Hide file tree
Showing 25 changed files with 640 additions and 73 deletions.
1 change: 1 addition & 0 deletions .jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"browserify": true,
"devel": true,
"esversion": 6,
"expr": true,
"laxbreak": true,
"mocha": true,
"undef": true,
Expand Down
10 changes: 10 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
language: node_js
node_js:
- '7'
- '6'
before_install:
- npm install -g [email protected]
install:
- yarn install --pure-lockfile
script:
- yarn test
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# paperplane [![npm version](https://img.shields.io/npm/v/paperplane.svg)](https://www.npmjs.com/package/paperplane) [![npm downloads](https://img.shields.io/npm/dt/paperplane.svg)](https://www.npmjs.com/package/paperplane)

Lighter-than-air node.js server framework.
<p align="center">
<a href="#"><img src="https://cloud.githubusercontent.com/assets/888052/22172037/543e7f10-df6b-11e6-8ab8-8a1e679dd27e.png" alt="paperplane" style="max-width:100%;"></a>
</p>
<p align="center">
Lighter-than-air node.js server framework.
</p>
<p align="center">
<a href="https://www.npmjs.com/package/paperplane"><img src="https://img.shields.io/npm/v/paperplane.svg?colorB=007ec6" alt="npm version" style="max-width:100%;"></a> <a href="https://www.npmjs.com/package/paperplane"><img src="https://img.shields.io/npm/dm/paperplane.svg" alt="npm downloads" style="max-width:100%;"></a> <a href="https://travis-ci.org/articulate/paperplane"><img src="https://travis-ci.org/articulate/paperplane.svg?branch=master" alt="Build Status" style="max-width:100%;"></a>
</p>

## Usage

Expand Down
Binary file added assets/logo-480.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion demo/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
require('./lib/seed')()
const { compose } = require('ramda')
const http = require('http')
const util = require('util')

const { logger, methods, mount, parseJson,
redirect, routes, static } = require('..')
Expand Down
54 changes: 27 additions & 27 deletions docs/API.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
# API

- [html](#html) - response helper, type `text/html`
- [json](#json) - response helper, type `application/json`
- [logger](#logger) - json request logger
- [methods](#methods) - maps request methods to handler functions
- [mount](#mount) - top-level server function wrapper
- [parseJson](#parseJson) - json body parser
- [redirect](#redirect) - redirect response helper
- [routes](#routes) - maps express-style route patterns to handler functions
- [send](#send) - basic response helper
- [static](#static) - static file serving handler
- [`html`](#html) - response helper, type `text/html`
- [`json`](#json) - response helper, type `application/json`
- [`logger`](#logger) - json request logger
- [`methods`](#methods) - maps request methods to handler functions
- [`mount`](#mount) - top-level server function wrapper
- [`parseJson`](#parseJson) - json body parser
- [`redirect`](#redirect) - redirect response helper
- [`routes`](#routes) - maps express-style route patterns to handler functions
- [`send`](#send) - basic response helper
- [`static`](#static) - static file serving handler

### html

```haskell
(String | Buffer | Stream) -> Response
```

Returns a [response object](https://github.com/articulate/paperplane/blob/master/docs/getting-started.md#response-object), with the `content-type` header set to `text/html`.
Returns a [`Response`](https://github.com/articulate/paperplane/blob/master/docs/getting-started.md#response-object), with the `content-type` header set to `text/html`.

```js
const { html } = require('paperplane')
Expand All @@ -29,7 +29,7 @@ const usersPage = () =>
.then(html)
```

In the example above, it resolves with a response similar to:
In the example above, it resolves with a [`Response`](https://github.com/articulate/paperplane/blob/master/docs/getting-started.md#response-object) similar to:

```js
{
Expand All @@ -47,7 +47,7 @@ In the example above, it resolves with a response similar to:
Object -> Response
```

Returns a [response object](https://github.com/articulate/paperplane/blob/master/docs/getting-started.md#response-object), with a `body` encoded with `JSON.stringify`, and the `content-type` header set to `application/json`.
Returns a [`Response`](https://github.com/articulate/paperplane/blob/master/docs/getting-started.md#response-object), with a `body` encoded with `JSON.stringify`, and the `content-type` header set to `application/json`.

```js
const { json } = require('paperplane')
Expand All @@ -57,7 +57,7 @@ const users = () =>
.then(json)
```

In the example above, it resolves with a response similar to:
In the example above, it resolves with a [`Response`](https://github.com/articulate/paperplane/blob/master/docs/getting-started.md#response-object) similar to:

```js
{
Expand All @@ -80,7 +80,7 @@ Logs request/response as `json`. Uses the following whitelists:
- req: `['headers', 'method', 'url']`
- res: `['statusCode']`

Provided as an example logger to use with [mount](#mount), as below.
Provided as an example logger to use with [`mount`](#mount), as below.

```js
const http = require('http')
Expand All @@ -101,10 +101,10 @@ Logs will be formatted as `json`, similar to below:
### methods

```haskell
{ k: (Request -> Response) } -> (Request -> Response)
{ k: (Request -> (Response | Promise Response)) } -> (Request -> Promise Response)
```

Maps handler functions to request methods. Returns a handler function. If the method of an incoming request doesn't match, it rejects with a `404 Not Found`. Use in combination with [routes](#routes) to build a routing table of any complexity.
Maps handler functions to request methods. Returns a handler function. If the method of an incoming request doesn't match, it rejects with a `404 Not Found`. Use in combination with [`routes`](#routes) to build a routing table of any complexity.

```js
const http = require('http')
Expand All @@ -123,10 +123,10 @@ http.createServer(mount(app)).listen(3000)
### mount

```haskell
((Request -> Response), Object) -> (IncomingMessage, ServerResponse) -> ()
((Request -> (Response | Promise Response)), Object) -> (IncomingMessage, ServerResponse) -> ()
```

Wraps a top-level handler function to prepare for mounting as a new `http` server. Lifts the handler into a `Promise` chain, so the handler can respond with either a [response object](https://github.com/articulate/paperplane/blob/master/docs/getting-started.md#response-object), or a `Promise` that resolves with one. Also accepts an options object with `errLogger` and `logger` properties, both of which can be set to [logger](#logger).
Wraps a top-level handler function to prepare for mounting as a new `http` server. Lifts the handler into a `Promise` chain, so the handler can respond with either a [`Response`](https://github.com/articulate/paperplane/blob/master/docs/getting-started.md#response-object), or a `Promise` that resolves with one. Also accepts an options object with `errLogger` and `logger` properties, both of which can be set to [`logger`](#logger).

```js
const http = require('http')
Expand All @@ -146,7 +146,7 @@ http.createServer(mount(app, opts)).listen(3000)
Request -> Request
```

Parses the request body as `json` if available, and if the `content-type` is `application/json`. Otherwise, passes the [request object](https://github.com/articulate/paperplane/blob/master/docs/getting-started.md#request-object) through untouched.
Parses the request body as `json` if available, and if the `content-type` is `application/json`. Otherwise, passes the [`Request`](https://github.com/articulate/paperplane/blob/master/docs/getting-started.md#request-object) through untouched.

```js
const { compose } = require('ramda')
Expand All @@ -167,7 +167,7 @@ http.createServer(mount(app)).listen(3000)
(String, Number) -> Response
```

Accept a `Location` and optional `statusCode` (defaults to `302`), and returns a [response object](https://github.com/articulate/paperplane/blob/master/docs/getting-started.md#response-object) denoting a redirect.
Accept a `Location` and optional `statusCode` (defaults to `302`), and returns a [`Response`](https://github.com/articulate/paperplane/blob/master/docs/getting-started.md#response-object) denoting a redirect.

**Pro-tip:** if you want an earlier function in your composed application to respond with a redirect and skip everything else, just wrap it in a `Promise.reject` (see example below). The error-handling code in `paperplane` will ignore it since it's not a real error.

Expand Down Expand Up @@ -200,7 +200,7 @@ const app = routes({
http.createServer(mount(app)).listen(3000)
```

In the example above, `redirect()` returns a [response object](https://github.com/articulate/paperplane/blob/master/docs/getting-started.md#response-object) similar to:
In the example above, `redirect()` returns a [`Response`](https://github.com/articulate/paperplane/blob/master/docs/getting-started.md#response-object) similar to:

```js
{
Expand All @@ -215,10 +215,10 @@ In the example above, `redirect()` returns a [response object](https://github.co
### routes

```haskell
{ k: (Request -> Response) } -> (Request -> Response)
{ k: (Request -> (Response | Promise Response)) } -> (Request -> Response)
```

Maps handler functions to express-style route patterns. Returns a handler function. If the path of an incoming request doesn't match, it rejects with a `404 Not Found`. Use in combination with [methods](#methods) to build a routing table of any complexity.
Maps handler functions to express-style route patterns. Returns a handler function. If the path of an incoming request doesn't match, it rejects with a `404 Not Found`. Use in combination with [`methods`](#methods) to build a routing table of any complexity.

```js
const http = require('http')
Expand Down Expand Up @@ -246,15 +246,15 @@ http.createServer(mount(app)).listen(3000)
(String | Buffer | Stream) -> Response
```

The most basic response helper. Simply accepts a `body`, and returns a properly formatted [response object](https://github.com/articulate/paperplane/blob/master/docs/getting-started.md#response-object), without making any further assumptions.
The most basic response helper. Simply accepts a `body`, and returns a properly formatted [`Response`](https://github.com/articulate/paperplane/blob/master/docs/getting-started.md#response-object), without making any further assumptions.

```js
const { send } = require('paperplane')

send('This is the response body')
```

In the example above, it returns a response similar to:
In the example above, it returns a [`Response`](https://github.com/articulate/paperplane/blob/master/docs/getting-started.md#response-object) similar to:

```js
{
Expand All @@ -270,7 +270,7 @@ In the example above, it returns a response similar to:
Object -> (Request -> Response)
```

Accepts an options object (see [details here](https://www.npmjs.com/package/send#options)), and returns a handler function for serving static files. Expects a `req.params.path` to be present on the [request object](https://github.com/articulate/paperplane/blob/master/docs/getting-started.md#request-object), so you'll need to format your route pattern similar to the example below, making sure to include a `/:path+` route segment.
Accepts an options object (see [details here](https://www.npmjs.com/package/send#options)), and returns a handler function for serving static files. Expects a `req.params.path` to be present on the [`Request`](https://github.com/articulate/paperplane/blob/master/docs/getting-started.md#request-object), so you'll need to format your route pattern similar to the example below, making sure to include a `/:path+` route segment.

```js
const http = require('http')
Expand Down
4 changes: 2 additions & 2 deletions lib/logger.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const { compose, evolve, pick } = require('ramda')
const { compose, evolve, pick, tap } = require('ramda')

module.exports = compose(
console.info,
tap(console.info),
JSON.stringify,
evolve({
req: pick(['headers', 'method', 'url']),
Expand Down
10 changes: 6 additions & 4 deletions lib/methods.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
const Boom = require('boom')
const { NotFound } = require('http-errors')

const methods = handlers => req =>
handlers[req.method]
? handlers[req.method](req)
: Promise.reject(Boom.notFound())
new Promise((resolve, reject) => {
handlers[req.method]
? resolve(handlers[req.method](req))
: reject(new NotFound())
})

module.exports = methods
3 changes: 1 addition & 2 deletions lib/mount.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const etag = require('etag')
const { Stream } = require('stream')
const { tap } = require('ramda')

const error = require('./error')
const getBody = require('./get-body')
Expand All @@ -9,7 +8,7 @@ const { log, rethrow } = require('./util')

const { byteLength, isBuffer } = Buffer

const mount = (app, { errLogger, logger }) =>
const mount = (app, { errLogger, logger }={}) =>
(req, res) =>
Promise.resolve(req)
.then(getBody)
Expand Down
4 changes: 2 additions & 2 deletions lib/redirect.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module.exports = (Location, statusCode=302) => ({
module.exports = (location, statusCode=302) => ({
body: '',
headers: { Location },
headers: { location },
statusCode
})
27 changes: 11 additions & 16 deletions lib/routes.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
const { assoc, pluck, zipObj } = require('ramda')
const Boom = require('boom')
const pathToReg = require('path-to-regexp')

const routes = handlers => req => {
for (var route in handlers) {
const keys = [],
vals = pathToReg(route, keys).exec(req.pathname)

if (vals) {
req = assoc('params', zipObj(pluck('name', keys), vals.slice(1)), req)
return handlers[route](req)
const { merge } = require('ramda')
const { NotFound } = require('http-errors')
const pathMatch = require('path-match')()

const routes = handlers => req =>
new Promise((resolve, reject) => {
for (var route in handlers) {
const params = pathMatch(route)(req.pathname)
if (params) return resolve(handlers[route](merge(req, { params })))
}
}

return Promise.reject(Boom.notFound())
}
reject(new NotFound())
})

module.exports = routes
14 changes: 10 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,31 @@
"scripts": {
"hint": "jshint --reporter=node_modules/jshint-stylish .",
"start": "node demo/index.js",
"start:dev": "nodemon -w . demo/index.js"
"start:dev": "nodemon -w . demo/index.js",
"test": "mocha --reporter dot"
},
"dependencies": {
"boom": "^4.2.0",
"etag": "^1.7.0",
"http-errors": "^1.5.1",
"media-typer": "^0.3.0",
"path-to-regexp": "^1.7.0",
"path-match": "^1.2.4",
"qs": "^6.3.0",
"ramda": "^0.23.0",
"raw-body": "^2.2.0",
"send": "^0.14.1",
"type-is": "^1.6.14"
},
"devDependencies": {
"boom": "^4.2.0",
"chai": "^3.5.0",
"jshint": "^2.9.4",
"jshint-stylish": "^2.2.1",
"levelup": "^1.3.3",
"memdown": "^1.2.4",
"mocha": "^3.2.0",
"nodemon": "^1.11.0",
"pug": "^2.0.0-beta6"
"pug": "^2.0.0-beta6",
"string-to-stream": "^1.1.0",
"supertest": "^2.0.1"
}
}
1 change: 1 addition & 0 deletions test/fixtures/static-file.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
testing testing
21 changes: 21 additions & 0 deletions test/html.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const { expect } = require('chai')

const html = require('../lib/html')

describe('html', function() {
const body = '<html></html>',
res = html(body)

it('includes the body in the response', function() {
expect(res.body).to.equal(body)
})

it('sets the "content-type" header to "text/html"', function() {
expect(res.headers).to.be.an('object')
expect(res.headers['content-type']).to.equal('text/html')
})

it('sets the statusCode to 200', function() {
expect(res.statusCode).to.equal(200)
})
})
21 changes: 21 additions & 0 deletions test/json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const { expect } = require('chai')

const json = require('../lib/json')

describe('json', function() {
const body = { foo: 'bar' },
res = json(body)

it('stringifies the body', function() {
expect(res.body).to.equal(JSON.stringify(body))
})

it('sets the "content-type" header to "application/json"', function() {
expect(res.headers).to.be.an('object')
expect(res.headers['content-type']).to.equal('application/json')
})

it('sets the statusCode to 200', function() {
expect(res.statusCode).to.equal(200)
})
})
6 changes: 6 additions & 0 deletions test/lib/spy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = () => {
const spy = x => spy.calls.push(x)
spy.calls = []
spy.reset = () => spy.calls.length = 0
return spy
}
Loading

0 comments on commit 33c7da6

Please sign in to comment.