Full Code of sideshowcoder/canned for AI

master 86983a2d53d3 cached
61 files
75.5 KB
21.6k tokens
8 symbols
1 requests
Download .txt
Repository: sideshowcoder/canned
Branch: master
Commit: 86983a2d53d3
Files: 61
Total size: 75.5 KB

Directory structure:
gitextract_myo37vrg/

├── .gitignore
├── .jshintrc
├── .travis.yml
├── Makefile
├── README.md
├── bin/
│   └── canned
├── example/
│   ├── _search.get.json
│   └── comment/
│       ├── _myfunc.get.json
│       ├── any.get.json
│       ├── any.put.json
│       ├── index.get.json
│       └── index.post.json
├── index.js
├── lib/
│   ├── canned.js
│   ├── lookup.js
│   ├── response.js
│   └── utils.js
├── package.json
├── spec/
│   ├── canned.spec.js
│   ├── lookup.spec.js
│   └── test_responses/
│       ├── _201.get.json
│       ├── _201_multiple_responses.get.json
│       ├── _a.get.html
│       ├── _b.get.txt
│       ├── _broken_sanitize.get.json
│       ├── _chartest.get.json
│       ├── _empty.get.json
│       ├── _empty_with_headers.get.json
│       ├── _invalid_syntax.get.json
│       ├── _multimatch.get.json.1
│       ├── _multimatch.get.json.2
│       ├── _multimatch_query_param.get.html
│       ├── _multiple_custom_header.get.json
│       ├── _multiple_get_responses.get.json
│       ├── _multiple_responses.get.json
│       ├── _multiple_responses.post.json
│       ├── _multiple_responses_text.get.html
│       ├── _multiple_responses_text.post.html
│       ├── _multiple_responses_xml.post.xml
│       ├── _multiple_responses_xml_request_body.post.xml
│       ├── _multiple_type.get.csv
│       ├── _multiple_type.get.html
│       ├── _multiple_type.get.js
│       ├── _multiple_type.get.json
│       ├── _multiple_type.get.jsonld
│       ├── _multiple_type.get.nt
│       ├── _multiple_type.get.txt
│       ├── _response_with_url_param.post.json
│       ├── _single_custom_header.get.json
│       ├── _vendor_type.get.xml
│       ├── d/
│       │   ├── 2/
│       │   │   └── index.get.json
│       │   ├── _commented.get.json
│       │   ├── any/
│       │   │   ├── _bar.get.json
│       │   │   └── index.get.json
│       │   ├── any.get.html
│       │   ├── e/
│       │   │   └── index.get.html
│       │   ├── index.get.html
│       │   └── multiple-accept-types/
│       │       └── index.get.json
│       └── index.get.html
└── test/
    ├── assert.sh
    └── bin_test.sh

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
/node_modules
npm-debug.log
.idea/
/TAGS


================================================
FILE: .jshintrc
================================================
{
    "bitwise"       : true,     // Prohibit bitwise operators (&, |, ^, etc.).
    "forin"         : true,     // Tolerate `for in` loops without `hasOwnPrototype`.
    "immed"         : true,     // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );`
    "latedef"       : true,     // Prohibit variable use before definition.
    "eqeqeq"        : true,     // Require ===
    "newcap"        : true,     // Require capitalization of all constructor functions e.g. `new F()`.
    "noempty"       : true,     // Prohibit use of empty blocks.
    "nonew"         : true,     // Prohibit use of constructors for side-effects.
    "regexp"        : true,     // Prohibit `.` and `[^...]` in regular expressions.
    "undef"         : true,     // Require all non-global variables be declared before they are used.
    "strict"        : true,     // Require `use strict` pragma in every file.
    "trailing"      : true,     // Prohibit trailing whitespaces.
    "asi"           : true,     // Tolerate Automatic Semicolon Insertion (no semicolons).
    "boss"          : false,    // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments.
    "debug"         : false,    // Allow debugger statements e.g. browser breakpoints.
    "eqnull"        : true,    // Tolerate use of `== null`.
    "es5"           : false,    // Allow EcmaScript 5 syntax.
    "esnext"        : false,    // Allow ES.next specific features such as `const` and `let`.
    "evil"          : false,    // Tolerate use of `eval`.
    "expr"          : false,    // Tolerate `ExpressionStatement` as Programs.
    "funcscope"     : false,    // Tolerate declarations of variables inside of control structures while accessing them later from the outside.
    "globalstrict"  : false,    // Allow global "use strict" (also enables 'strict').
    "iterator"      : false,    // Allow usage of __iterator__ property.
    "lastsemic"     : false,    // Tolerat missing semicolons when the it is omitted for the last statement in a one-line block.
    "laxbreak"      : false,    // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons.
    "laxcomma"      : false,    // Suppress warnings about comma-first coding style.
    "loopfunc"      : false,    // Allow functions to be defined within loops.
    "multistr"      : false,    // Tolerate multi-line strings.
    "onecase"       : false,    // Tolerate switches with just one case.
    "proto"         : false,    // Tolerate __proto__ property. This property is deprecated.
    "regexdash"     : false,    // Tolerate unescaped last dash i.e. `[-...]`.
    "scripturl"     : false,    // Tolerate script-targeted URLs.
    "smarttabs"     : false,    // Tolerate mixed tabs and spaces when the latter are used for alignmnent only.
    "shadow"        : false,    // Allows re-define variables later in code e.g. `var x=1; x=2;`.
    "sub"           : false,    // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`.
    "supernew"      : false,    // Tolerate `new function () { ... };` and `new Object;`.
    "validthis"     : false,    // Tolerate strict violations when the code is running in strict mode and you use this in a non-constructor function.
    "devel"         : false,    // Allow development statements e.g. `console.log();`.
    "node"          : true,     // Enable globals available when code is running inside of the NodeJS runtime environment.
    "nonstandard"   : false,    // Define non-standard but widely adopted globals such as escape and unescape.

    "maxerr"        : 100,      // Maximum errors before stopping.
    "indent"        : 2,        // Specify indentation spacing
    "laxcomma"      : true,     // tolerate comma first coding style

    "globals"       : {         // Jasmine globals for spec files
         "afterEach": false,
         "beforeEach": false,
         "confirm": false,
         "context": false,
         "describe": false,
         "expect": false,
         "it": false,
         "xit": false,
         "jasmine": false,
         "JSHINT": false,
         "mostRecentAjaxRequest": false,
         "qq": false,
         "runs": false,
         "spyOn": false,
         "spyOnEvent": false,
         "waitsFor": false,
         "xdescribe": false
      }
}


================================================
FILE: .travis.yml
================================================
language: node_js
sudo: false
node_js:
  - node
  - lts/*
script: make
notifications:
  webhooks:
    urls:
      - https://webhooks.gitter.im/e/30d8496b867bc082157a
    on_success: change  # options: [always|never|change] default: always
    on_failure: always  # options: [always|never|change] default: always
    on_start: never     # options: [always|never|change] default: always



================================================
FILE: Makefile
================================================
.PHONY: test hint default
CURRENT_BRANCH=$(shell git rev-parse --abbrev-ref HEAD)

default: bootstrap hint test

bootstrap:
	npm install

test:
	./node_modules/.bin/jasmine-node --captureExceptions spec
	cd test && ./bin_test.sh

hint:
	./node_modules/.bin/jshint bin/canned index.js lib/ spec/

release: docs
	@read -p "Version to release: " version; \
	git tag -a $$version -m "version $$version release"
	git push --tags
	npm publish

docs:
	@git stash
	@git checkout gh-pages
	@git checkout master -- README.md
	@echo "---\nlayout: index\n---" | cat - README.md > index.md
	@git reset README.md
	@rm README.md
	@git add index.md
	-git commit -m "updated docs"
	-git push origin gh-pages
	@git checkout ${CURRENT_BRANCH}
	-@git stash apply


================================================
FILE: README.md
================================================
Canned fake API server
======================

[![Build Status](https://travis-ci.org/sideshowcoder/canned.png?branch=master)](https://travis-ci.org/sideshowcoder/canned)
[![Code Climate](https://codeclimate.com/github/sideshowcoder/canned.png)](https://codeclimate.com/github/sideshowcoder/canned)
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/sideshowcoder/canned?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)

View the docs on [Docs](http://sideshowcoder.github.io/canned), and join the
chat at [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/sideshowcoder/canned?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)

Working with APIs, more often than not, during development you want to work
with a fixed version of the responses provided. This is especially true if the
API is still under development, and maybe even still needs input on how to
output something. This is what Canned is for!

What does it do?
----------------

Canned maps a folder structure to API responses. Given the following directory
structure:

    /content/index.get.html
    /comment/any.get.json
    /comment/1/votes/index.get.json
    /comment/any/votes/index.get.json

requests like

    Accept: application/json
    GET /comment/:id

are served from the file `/comment/any.get.json` as

    Content-Type: application/json
    { "content": "I am a comment", "author": "sideshowcoder" }

requests like

    Accept: text/html
    GET /content/

are served from the file `/content/index.get.html` as

    Content-Type: text/html
    <html>
      <body>Some html in here</body>
    </html>


requests like

    Accept: application/json
    GET /comment/1/votes

are served from the file `/comment/1/index.get.json` as

    Content-Type: application/json
    { "content": "I am comment 1", "author": "sideshowcoder" }

requests like

    Accept: application/json
    GET /comment/123456789/votes

are served from the file `/comment/any/index.get.json`

    Content-Type: application/json
    { "content": "I am a wildcard comment for any id", "author": "sideshowcoder" }

The matching works on the filename by treating it as `PATH.VERB.CONTENT_TYPE` so
`index.get.json` has the path `index` the verb is `get` and the content-type
`json`. Supported content types are

```
json   => application/json
html   => text/html
txt    => text/plain
js     => application/javascript
csv    => text/csv
// linked-data formats:
nt     => application/n-triples
jsonld => application/ld+json
```

So an example is for querying (with canned running on localhost:3000)

```
$ curl -H "Accept: text/javascript" http://localhost:3000/comment/1
> { "content": "I am a comment", "author": "sideshowcoder" }
```

Awesome! so what is supported?
------------------------------
Currently Canned supports the basic REST-API mapping, as well as custom method
mapping with nested endpoints.

    file                            | resquest
    /index.get.json                 | GET /
    /any.get.json                   | GET /:id
    /_search.get.json               | GET /search
    /comments/index.get.json        | GET /comments/
    /comments/any.get.json          | GET /comments/:id
    /comments/_search.get.json      | GET /comments/search
    /comments/any/index.get.json    | GET /comments/:id/

You can even add query parameters to your filenames to return different
responses on the same route. If the all query params in a filename match the
incoming request, this file will be returned. It will fall back to returning the
file with no query params if it exists.

*Warning this will be deprecated in the future since canned now supports
multiple response based on the request body or GET URL parameters in one file.
This is the preferred way since files with ? in the name do not work on Windows*

    file                            | resquest
    /index?name=Superman.get.json   | GET /?name=Superman&NotAllParams=NeedToMatch
    /_search?q=hello.get.json       | GET /comments/search?q=hello
    /_search.get.json               | GET /comments/search?iam=soignored

Same support is available for PUT, POST, etc.

    /index.post.json            | POST serves /... + CORS Headers
    /index.put.json             | PUT serves /... + CORS Headers

If CORS support is enabled additionally options will be available as a http verb
and all requests will serve the CORS Headers as well

    /                           | OPTIONS serve all the options needed for CORS
    /index.get.json             | GET serves /... + CORS Headers

If you need some custom return codes, just add them to the file via adding a
file header like so

    //! statusCode: 201
    <html>
      <body>Created something successfully! Happy!</body>
    </html>

The header will be stripped before sending and the statusCode will be set.

You can also override the default content types by adding a custom content type to the file header:

    //! contentType: "application/vnd.custom+xml"
    <xml>
        <created>1</created>
    </xml>

This will be returned with a `Content-type: application/vnd.custom+xml` header.

Multiple headers need to be written on one single line and comma-separated, like so:

    //! statusCode: 201, contentType: "application/vnd.custom+xml"

If you need to send bind custom HTTP headers to the response you can add them as headers to the response file using
 the keyword `customHeader`:

    //! customHeader: {"MyCustomHeaderName": "MyCustomHeaderValue"}

In case you need more then one custom header in the response, you can just use the same keyword multiple times:

    //! customHeader: {"MyCustomHeaderName": "MyCustomHeaderValue"}
    //! customHeader: {"SecondHeaderName": "SecondHeaderValue"}

Variable responses
------------------
You can get a different response by specifying request data in variant
comments. If the request data matches the comment data the matching response is
returned. If there is no match the first response is returned

*Note: comments must be on a single line*

Custom headers:

    //! header: {"authorization": "abc"}
    {
        "response": "response for abc"
    }

    //! header: {"authorization": "123"}
    {
        "response": "response for 123"
    }

If you need different responses based on request body then you can specify the
request you want matched via body comments:

    //! body: {"email": "one@example.com"}
    {
        "response": "response for one@example.com"
    }

    //! body: {"email": "two@example.com"}
    {
        "response": "response for two@example.com"
    }

If you need different responses based on request parameters then you can specify
them via parameters comments:

    //! params: {"foo": "bar"}
    {
        "response": "response for bar"
    }

    //! params: {"foo": "baz"}
    {
        "response": "response for baz"
    }

this would match `http://my.local.server/my_get_request_path?foo=bar` or
`http://my.local.server/my_get_request_path?foo=baz` respectively.

To use in conjunction with response headers and status codes, just add them on
the line above.

	//! statusCode: 201
	//! header: {"authorization": "abc"}
	{
	    "response": "response for abc"
	}

  	//! statusCode: 201, contentType: "application/my-personal-json"
	//! header: {"authorization": "123"}
	{
	    "response": "response for 123"
	}

Wildcard responses are also supported, very useful to have 'wildcard'
directories, so that if for given a request like:

  	GET /api/users/1/profile/

you don't have a file in `./canned/api/users/1/profile/index.get.json` then
it would look for a file in `./canned/api/users/any/index.get.json` or
similar. Wildcards can be specified on the command line via

  	canned --wildcard myany

This would change the lookup to `./canned/api/users/myany/index.get.json`

How about some docs inside for the responses?
---------------------------------------------
Most content types support comments natively, like html or javascript. Sadly the
probably most used type (JSON) does not :(. So canned actually extends the JSON
syntax a little so it can include comments with _//_ or _/**/_. In case you use
the JSON files directly on the backend side as test cases make sure you strip
those out as well!


Ok I need this!
---------------
Just install via npm

    $ npm install canned

which will install it locally in node\_modules, if you want to have it
available from anywhere just install globally

    $ npm install -g canned

How do I use it
---------------
There are 2 ways here, either you embed it somewhere programmatically

    var canned = require('canned')
    ,   http = require('http')
    ,   opts = { logger: process.stdout }

    can = canned('/path/to/canned/response/folder', opts)

    http.createServer(can).listen(3000)

Or just run the provided canned server script

    $ canned

Which serves the current folder with canned responses on port 3000

    $ canned -p 5000 ./my/responses/

will serve the relative folder via port 5000

If you need canned to respond with some delay, pass delay in ms to `response_delay` arg

    $ canned --response_delay=1000 ./my/reponses/

If you want canned to iterate through all accepted content types in the `Accept` header, use

    $ canned --relaxed_accept=true ./my/reponses/

If for whatever reason you want to turn of CORS support do so via

    $ canned --cors=false ./my/responses/

Also if you need additional headers to be served alongside the CORS headers
these can be added like this (thanks to runemadsen)

    $ canned --headers "Authorization, Another-Header"

To enable CORS programmatically, you can use the following options:

    var canned = require('canned')
    ,   http = require('http')
    ,   opts = {
            cors: true,
            cors_headers: ["Content-Type", "Location"]
        }

Optionally, the cors_headers value can be a comma-separated string, as per the CLI option.

Other optional options include:
    
    var opts = {
            sanitize: false, // get responses as is without any sanitization
            response_delay: 2000, // delay the response for 2 seconds
            relaxed_accept: true // iterate through all accepted content types in the `Accept` header
            wildcard: 'myany', // specify 'wildcard' directory, e.g. ./canned/api/users/myany/index.get.json
        }

For more information checkout [the pull request](https://github.com/sideshowcoder/canned/pull/9)

Already using grunt? [Great there is a plugin for that,](https://github.com/jkjustjoshing/grunt-canned)
thanks to jkjustjoshing.


It does not work :(
-------------------

### canned not found
make sure you either install globally or put ./node\_modules/.bin in your PATH

### it is still not found, and I installed globally
make sure /usr/local/share/npm/bin is in your path, this should be true for
every install since you won't be able to run any global module bins if not.
(like express, and such)

### the encoding looks wrong
make sure you run a version of node which is 0.10.3 or higher, because it fixes
a problem for the encoding handling when reading files

### My JSON request body is not matching any responses
Set the "Content-Type" header to contain "application/json".

How to Contribute
-----------------
* Checkout the repository
* Run the tests and jshint
    ```$ make```
* Create a topic branch
    ```$ git checkout -b my-new-feature```
* Code test and make jshint happy!
    ```$ make test```
    ```$ make hint```
* Push the branch and create a Pull-Request

I try to review the pull requests as quickly as possible, should it take to long
feel free to [bug me on twitter](https://twitter.com/ischi)

Release History
---------------
### next
* adding PATCH to default Access-Control-Allow-Method Cors header #113 (@william-mcmillian)
* adding support for delayed responses #114 (@Onatolich)
* adding support to make sanatize optional #115 (@YuliyaMarholina)

### 0.3.10
* Windows line ending support #102 (@antxxxx)
* cleanup and documentation #95 (@wadtech)
* customHeader handling #110 (@mazoni)

### 0.3.9
* relaxed handling for accept headers, meaning select the first result that can be 
  served even if it is not the first accepted content type. option 
`--relaxed-accept` #100 (@CheungJ)

### 0.3.8
* fix improper handling of carriage return in windows #79 (@git-jiby-me)
* fix handling for urls in request body #90 (@wadtech)
* documentation and test cases for cors headers #91 (@wadtech)
* enable matching raw request body rather than property-based #96 (@ftes)
* fix query string param handling #97 (@wadtech @targoo)

### 0.3.7
* The regex for matching request, was not considering arrays in the request JSON
  #82
* For request with a request body, canned was checking content type to exactly
  match application/json, which is not good as browsers may sent charset as well
  with the content type. #82
* For matching request and filters with more accuracy, we were converting the
  values of all keys in request to string before comparing, but this was being
  done wrong as it was creating string of Objects and arrays as well, which it
  shouldn’t #82

### 0.3.6
* support checking the `ACCEPT HEADER` for the response type (thanks git-jiby-me) #81

### 0.3.5
* support for custom HTTP headers in responses
* fix for matching multiple parameters in response #73 thanks
  [xdemocle](https://github.com/xdemocle)
* fix any wildcard in the middle of the path #66 thanks
  [msurdi](https://github.com/msurdi)

### 0.3.4
* update dependencies and dev-dependencies
* wildcard parameters thanks to [msurdi](https://github.com/msurdi) see
  https://github.com/sideshowcoder/canned/pull/64

### 0.3.3
* fix support for special characters in the header / params / body matches
  (@simonprickett, @kevinschumacher, @sideshowcoder)
* support differet statusCodes and content types in multiple response files
  (@sideshowcoder)

### 0.3.2
* support for XML headers to support SOAP (@vikalp)
* fix relative path again... (@sideshowcoder)

### 0.3.1
* fixes for variable responses with JSON body (@bibounde)
* fixes for relative paths on start (@sideshowcoder)
* complex get parameters causing regexp match on file to fail (@sideshowcoder)

### 0.3
* support for multiple responses per file (@hungrydavid)
* support for GET responses without the need for special characters in the
  filename (@sideshowcoder based on the work by @hungrydavid)

### 0.2.3
* added support for empty response with 204 for no content (@jkjustjoshing)

### everything before
* sorry haven't kept a version history, yet. Will now!

Contributors
------------
* [sideshowcoder](https://github.com/sideshowcoder)
* [leifg](https://github.com/leifg)
* [runemadsen](https://github.com/runemadsen)
* [mulderp](https://github.com/mulderp)
* [creynders](https://github.com/creynders)
* [jkjustjoshing](https://github.com/jkjustjoshing)
* [hungrydavid](https://github.com/hungrydavid)
* [bibounde](https://github.com/bibounde)
* [vikalp](https://github.com/vikalp)
* [simonprickett](https://github.com/simonprickett)
* [kevinschumacher](https://github.com/kevinschumacher)
* [msurdi](https://github.com/msurdi)
* [Brendan Rius](https://github.com/brendan-rius)
* [Rocco Russo](https://github.com/xdemocle)
* [git-jiby-me](https://github.com/git-jiby-me)
* [wadtech](https://github.com/wadtech)
* [ftes](https://github.com/ftes)
* [targoo](https://github.com/targoo)
* [CheungJ](https://github.com/CheungJ)
* [antxxxx](https://github.com/antxxxx)
* [mazoni](https://github.com/mazoni)
* [william-mcmillian](https://github.com/william-mcmillian)
* [Onatolich](https://github.com/Onatolich)

License
-------
MIT 2013 Philipp Fehre alias @sideshowcoder, or @ischi on twitter


================================================
FILE: bin/canned
================================================
#!/usr/bin/env node
var canned = require('../index')
,   path = require('path')
,   http = require('http')
,   optimist = require('optimist')
,   argv = optimist
          .default('p', 3000)
          .alias('p', 'port')
          .describe('p', 'server port')
          .default('w', 'any')
          .alias('w', 'wildcard')
          .describe('w', 'wildcard path name for ids')
          .default('response_delay', 0)
          .describe('response_delay', 'response resolve delay')
          .default('relaxed_accept', false)
          .describe('relaxed_accept', 'iterates through Accept header values from beginning to end')
          .default('cors', true)
          .describe('cors', 'disable cors support')
          .default('headers', false)
          .describe('headers', 'add custom headers allowed in cors requests')
          .default('h', false)
          .alias('h', 'help')
          .describe('h', 'show the help')
          .usage('Usage: $0 [dir]')
          .argv

if (argv.h) {
  optimist.showHelp()
  return
}

var dir = ''
,   port = argv.p
,   relaxed_accept = argv.relaxed_accept
,   cors = argv.cors
,   cors_headers = argv.headers
,   logger
,   cannedDir
,   wildcard = argv.wildcard
,   response_delay = argv.response_delay

if (argv._.length === 1) dir = argv._[0] // use the passed directory
if (argv.q) {
  logger = null // be quiet
} else {
  logger = process.stdout
  cannedDir = path.resolve(dir)
  process.stdout.write('starting canned on port ' + port + ' for ' + cannedDir + '\n')
}

var can = canned(dir, {
  logger: logger,
  relaxed_accept: relaxed_accept,
  cors: cors,
  cors_headers: cors_headers,
  wildcard: wildcard,
  response_delay: response_delay
})

http.createServer(can).listen(port)


================================================
FILE: example/_search.get.json
================================================
// this response is the default: 
//! params: {"search": "default"}
{
  "search":"result",
  "key":"value"
}

//! params: {"search": "specific"}
{
  "search" : "specific result",
  "key": "value"
}

//! params: {"search": "apostrophe"}
{
  "search":"Mary",
  "results": [
    {
      "id":1,
      "title":"There's something about Mary"
    }
  ]
}

================================================
FILE: example/comment/_myfunc.get.json
================================================
{
  "myfunc":"result",
  "key":"value"
}



================================================
FILE: example/comment/any.get.json
================================================
{
  "id":"get",
  "key":"value"
}



================================================
FILE: example/comment/any.put.json
================================================
{
  "id":"put",
  "key":"value"
}



================================================
FILE: example/comment/index.get.json
================================================
{
  "index":"get",
  "key":"value"
}



================================================
FILE: example/comment/index.post.json
================================================
{
  "index":"post",
  "key":"value"
}



================================================
FILE: index.js
================================================
"use strict";

var path = require('path')
var Canned = require('./lib/canned')

var canned = function (dir, options) {
  if (!options) options = {}
  dir = path.relative(process.cwd(), dir)
  var c = new Canned(dir, options)
  return c.responseFilter.bind(c)
}

module.exports = canned



================================================
FILE: lib/canned.js
================================================
"use strict";

var url = require('url')
var fs = require('fs')
var util = require('util')
var Response = require('./response')
var querystring = require('querystring')
var cannedUtils = require('./utils')
var lookup = require('./lookup')
var _ = require('lodash')

function Canned(dir, options) {
  this.logger = options.logger
  this.wildcard = options.wildcard || 'any'
  this.relaxed_accept = options.relaxed_accept
  this.sanitize = options.sanitize !== undefined ? options.sanitize : true
  var cors_headers = options.cors_headers
  if (cors_headers && cors_headers.join) {
    cors_headers = cors_headers.join(', ')
  }
  this.response_opts = {
    response_delay: options.response_delay,
    cors_enabled: options.cors,
    cors_headers: cors_headers
  }
  this.dir = process.cwd() + '/' + dir
}

function matchFile(matchString, fname, method, ctype) {
  if(!ctype) {
    ctype = '(.+)';
  }
  return matchString.match(
    new RegExp(fname + '\\.' + method + '\\.' + ctype)
  )
}

function matchFileWithQuery(matchString, ctype) {
  if(!ctype) {
    ctype = '(.+)';
  }
  return matchString.match(
    new RegExp('(.*)\\?(.*)\\.(.*)\\.' + ctype)
    )
}

function matchFileWithExactQuery(matchString, fname, queryString, method, ctype) {
  var escapedQueryString = cannedUtils.escapeRegexSpecialChars(queryString)
  return matchString.match(
    new RegExp(fname +
               "(?=.*" +
               escapedQueryString.split("&").join(")(?=.*") +
               ").+" +
               method + "\\." + ctype)
  )
}

Canned.prototype._getFileFromRequest = function(httpObj, files) {

  if (!files) return false

  var m, i, e, matchString, fileMatch, ctype

  // if query params, match regexp based on fname to request
  if(httpObj.query)
  {
    for (i = 0, e = files[i]; e != null; e = files[++i]) {
      fileMatch = matchFileWithQuery(e, httpObj.ctype)
      if (fileMatch)
      {
        ctype = httpObj.ctype || fileMatch[4];
        matchString = httpObj.fname + "?" + httpObj.query + "." + httpObj.method + "." + ctype
        m = matchFileWithExactQuery(matchString, fileMatch[1], fileMatch[2], fileMatch[3], ctype)
        if (m) return { fname: e, mimetype: ctype }
      }
    }
  }

  // if match regexp based on request to fname
  for (i = 0, e = files[i]; e != null; e = files[++i]) {
    var contentTypes = [httpObj.ctype];
    if (this.relaxed_accept) {
      contentTypes = httpObj.matchingContentTypes;
    }
    for (var j = 0; j < contentTypes.length; j++) {
      m = matchFile(e, httpObj.fname, httpObj.method, contentTypes[j])
      if (m) {
        ctype = contentTypes[j] || m[1];
        return { fname : m[0], mimetype : ctype }
      }
    }
  }
  return false
}

function getContentType(mimetype){
  return Response.content_types[mimetype]
}

function stringifyValues(object) {
  _.each(object, function(value, key) {
    if (typeof value === "object") {
      stringifyValues(value);
    } else {
      object[key] = String(value)
    }
  })
}

function isContentTypeJson(request) {
  return request.headers &&
         request.headers['content-type'] &&
         request.headers['content-type'].indexOf('application/json') !== -1;
}

Canned.prototype.parseMetaData = function(response) {
  var metaData = {}
  // convert CR+LF => LF+LF, CR => LF, fixes line breaks causing issues in windows
  response = response.replace("\r", "\n");
  var lines = response.split("\n")
  var that = this
  var requestMatch = new RegExp(/\/\/! [body|params|header]+: (.*)/g)
  lines.forEach(function(line) {
    var optionsMatch = new RegExp(/\/\/!.*[statusCode|contentType|customHeader]/g)

    if(line.indexOf("//!") === 0) { // special comment line
      var matchedRequest = requestMatch.exec(line)
      if(matchedRequest) {
        try {
          metaData.request = JSON.parse(matchedRequest[1])
          stringifyValues(metaData.request);
        } catch (e) {
          metaData.request = matchedRequest[1];
        }
        return
      }
      var matchedOptions = optionsMatch.exec(line)
      if(matchedOptions) {
        try {
          line = line.replace("//!", '')
          var content = line.split(',').map(function (s) {
            var parts = s.split(':');
            parts[0] = '"' + parts[0].trim() + '"'
            return parts.join(':')
          }).join(',')
          var opts = JSON.parse('{' + content  + '}')
          if(opts.hasOwnProperty('customHeader')) {
              if(metaData.hasOwnProperty('customHeaders')) metaData.customHeaders.push(opts.customHeader)
              else metaData.customHeaders = [opts.customHeader]
          } else {
            cannedUtils.extend(metaData, opts)
          }
        } catch(e) {
          that._log('Invalid file header format try //! statusCode: 201')
        }
        return
      }
    }
  })

  return metaData
}

Canned.prototype.getSelectedResponse = function(responses, content, headers) {
  var that = this
  var inputObj
  var response = responses[0]
  var metaData = that.parseMetaData(response)
  var selectedResponse = {
    data: cannedUtils.removeSpecialComments(response),
    statusCode: metaData.statusCode || 200,
    contentType: metaData.contentType,
    customHeaders: metaData.customHeaders
  }

  stringifyValues(content);
  // put the contents of the body and the headers into a big, indexed object.
  inputObj = cannedUtils.extend({}, content, headers)

  responses.forEach(function(response) {
    var metaData = that.parseMetaData(response)

    for(var contentString in content) {
      if (Object.hasOwnProperty.call(content, contentString)) break
    }

    if (contentString === metaData.request) {
      //exact match of request body with body comment in file
      selectedResponse.data = cannedUtils.removeSpecialComments(response)
      if(metaData.statusCode) selectedResponse.statusCode = metaData.statusCode
      return
    }

    if (typeof metaData.request !== 'object') return;

    if(_.isMatch(inputObj, metaData.request)) {
        selectedResponse.data = cannedUtils.removeSpecialComments(response)
        if(metaData.statusCode) selectedResponse.statusCode = metaData.statusCode
      }
    })

  return selectedResponse
}

// return multiple response bodies as array
Canned.prototype.getEachResponse = function(data) {
  if (this.sanitize) {
    data = cannedUtils.removeJSLikeComments(data)
  }
  var responses = data.split(/\n\n(?=[\/\/!])/).filter(function (e) { return e !== '' })
  return responses
}

Canned.prototype.getVariableResponse = function(data, content, headers) {
  if(!data.length) {
    return { statusCode: 204, data: '' }
  }

  var responses = this.getEachResponse(data)
  var response = this.getSelectedResponse(responses, content, headers)
  return response
}

Canned.prototype.sanatizeContent = function (data, fileObject) {
  var sanatized

  if (data.length === 0 || !this.sanitize) {
    return data
  }

  switch (fileObject.mimetype) {
  case 'json':
    // make sure we return valid JSON even so we support comments
    try {
      sanatized = JSON.stringify(JSON.parse(cannedUtils.removeJSLikeComments(data)))
    } catch (err) {
      this._log("problem sanatizing content for " + fileObject.fname + " " + err)
      return false
    }
    break
  default:
    sanatized = data
  }
  return sanatized
}

Canned.prototype._responseForFile = function (httpObj, files, cb) {
  var that = this
  var fileObject = this._getFileFromRequest(httpObj, files);
  httpObj.filename = fileObject.fname
  if (fileObject) {
    var filePath = httpObj.path + '/' + fileObject.fname
    fs.readFile(filePath, { encoding: 'utf8' }, function (err, data) {
      var response
      if (err) {
        response = new Response(getContentType('html'), '', 404, httpObj.res, that.response_opts)
        cb('Not found', response)
      } else {
        data = data.replace(/\r/g, "");
        var _data = that.getVariableResponse(data, httpObj.content, httpObj.headers)
        data = _data.data
        var statusCode = _data.statusCode
        var content = that.sanatizeContent(data, fileObject)

        if (content !== false) {
          response = new Response(_data.contentType || getContentType(fileObject.mimetype), content, statusCode, httpObj.res, that.response_opts, _data.customHeaders)
          cb(null, response)
        } else {
          content = 'Internal Server error invalid input file'
          response = new Response(getContentType('html'), content, 500, httpObj.res, that.response_opts)
          cb(null, response)
        }
      }
    })
  } else {
    var response = new Response(getContentType('html'), '', 404, httpObj.res, that.response_opts)
    cb('Not found', response)
  }
}

Canned.prototype._log = function (message) {
  if (this.logger) this.logger.write(message)
}

Canned.prototype._logHTTPObject = function (httpObj) {
  this._log(' served via: .' + httpObj.pathname.join('/') + '/' + httpObj.filename + '\n')
}

Canned.prototype.respondWithDir = function (httpObj, cb) {
  var that = this;

  var fpath = httpObj.path + '/' + httpObj.dname
  fs.readdir(fpath, function (err, files) {
    httpObj.fname = 'index'
    httpObj.path  = fpath
    that._responseForFile(httpObj, files, function (err, resp) {
      return cb(err, resp)
    })
  })
}

Canned.prototype.respondWithAny = function (httpObj, files, cb) {
  var that = this;

  httpObj.fname = 'any';
  that._responseForFile(httpObj, files, function (err, resp) {
    return cb(err, resp);
  })
}

Canned.prototype.responder = function(body, req, res) {
  var responseHandler
  var httpObj = {}
  var that = this
  var parsedurl = url.parse(req.url)
  httpObj.headers   = req.headers
  httpObj.accept    = (req.headers && req.headers.accept) ? req.headers.accept.trim().split(',') : []
  httpObj.content   = body
  httpObj.pathname  = parsedurl.pathname.split('/')
  httpObj.dname     = httpObj.pathname.pop()
  httpObj.fname     = '_' + httpObj.dname
  httpObj.path      = this.dir + httpObj.pathname.join('/')
  httpObj.query     = parsedurl.query
  httpObj.method    = req.method.toLowerCase()
  httpObj.res       = res
  httpObj.ctype     = ''
  httpObj.matchingContentTypes  = []

  this._log('request: ' + httpObj.method + ' ' + req.url)

  if (httpObj.method === 'options') {
    that._log('Options request, serving CORS Headers\n')
    var response = new Response(null, '', 200, res, this.response_opts)
    return response.send()
  }

  if (httpObj.accept.length) {
    for(var type in Response.content_types){
      if (Response.content_types.hasOwnProperty(type)) {
        if (Response.content_types[type] === httpObj.accept[0].trim()){
          httpObj.ctype = type;
        }
        for(var i = 0; i < httpObj.accept.length; i++){
          if(Response.content_types[type] === httpObj.accept[i].trim()){
            httpObj.matchingContentTypes.push(type);
          }
        }
      }
    }
  }

  var paths = lookup(httpObj.pathname.join('/'), that.wildcard);
  paths.splice(0,1); // The first path is the default
  responseHandler = function (err, resp) {
    if (err) {
      // Try more paths, if there are any still
      if (paths.length > 0) {
        httpObj.path = that.dir + paths.splice(0, 1)[0];
        httpObj.fname = '_' + httpObj.dname;
        return that.findResponse(httpObj, responseHandler);
      } else {
        that._log(' not found\n');
      }
    } else {
      that._logHTTPObject(httpObj)
    }
    return resp.send();
  }

  // Find a response for the first path
  that.findResponse(httpObj, responseHandler);

}

Canned.prototype.findResponse = function(httpObj, cb) {
  var that = this;
  fs.readdir(httpObj.path, function (err, files) {
    fs.stat(httpObj.path + '/' + httpObj.dname, function (err, stats) {
      if (err) {
        that._responseForFile(httpObj, files, function (err, resp) {
          if (err) {
            that.respondWithAny(httpObj, files, cb)
          } else {
            cb(null, resp)
          }
        })
      } else {
        if (stats.isDirectory()) {
          that.respondWithDir(httpObj, cb)
        } else {
          cb(null, new Response('html', '', 500, httpObj.res))
        }
      }
    })
  })
}

Canned.prototype.responseFilter = function (req, res) {
  var that = this
  var body = ''

  // assemble response body if GET/POST/PUT
  switch(req.method) {
  case 'PUT':
  case 'POST':
    req.on('data', function (data) {
      body += data
    })
    req.on('end', function () {
      var responderBody = querystring.parse(body);
      if (isContentTypeJson(req)) {
        try {
          responderBody = JSON.parse(body)
        } catch (e) {
          that._log('Invalid json content')
        }
      }
      that.responder(responderBody, req, res)
    })
    break
  case 'GET':
    var query = url.parse(req.url).query
    if (query && query.length > 0) {
      body = querystring.parse(query)
    }
    that.responder(body, req, res)
    break
  default:
    that.responder(body, req, res)
    break
  }
}

module.exports = Canned;


================================================
FILE: lib/lookup.js
================================================
"use strict";


/***
 * Given a path and a wildcard string, return a list of paths
 * with every combination possible resulting from t he replacement of
 * any integer id found in the path with the wildcard string.
 *
 * The resulting list of paths is ordered by more "specific" to more
 * "generic" paths, for example using a wildcard of 'any' for the path:
 *
 *     api/2/customer/123/invoice/321/
 *
 * will return:
 *
 * [
 *   '/api/2/customer/123/invoice/321/',
 *   '/api/2/customer/123/invoice/any/',
 *   '/api/2/customer/any/invoice/321/',
 *   '/api/2/customer/any/invoice/any/',
 *   '/api/any/customer/123/invoice/321/',
 *   '/api/any/customer/123/invoice/any/',
 *   '/api/any/customer/any/invoice/321/',
 *   '/api/any/customer/any/invoice/any/'
 * ]
 *
 * @param path - the original path
 * @param wildcard - the replacmeent string
 * @returns {Array} - the resulting combination, ordered by specificity
 */

var lookup = module.exports = function (path, wildcard) {

  // Split the path and calculate how many paths will be generated
  var parts = path.split('/')
  var matches = path.match(/\/\d+(\/|$)/gi)
  var i

  if (!matches){
    return [path]
  }

  var lookPathsParts = [];

  // Locate replaceable parts indexes
  var matchesIndexes = []
  parts.forEach(function (p, i) {
    if (p.match(/^\d+$/)) {
      matchesIndexes.push(i)
    }
  })

  // Copy the original parts as a starting point for the new parts
  for (i = 0; i < Math.pow(2, matches.length); i++) {
    lookPathsParts.push(parts.slice())
  }

  // Generate the new paths parts
  for (i = matches.length; i > 0; --i) {
    var skip = Math.pow(2, i) / 2
    var replacePartIndex = matches.length - i
    for (var j = skip; j < lookPathsParts.length; j += (skip * 2)) {
      for (var k = 0; k < skip; k++) {
        lookPathsParts[j + k][matchesIndexes[replacePartIndex]] = wildcard
      }
    }
  }

  // Build the final path strings
  var lookPaths = []
  lookPathsParts.forEach(function (p) {
    lookPaths.push(p.join('/'))
  })
  return lookPaths
}


================================================
FILE: lib/response.js
================================================
"use strict";
var fs = require('fs')

function Response(content_type, content, statusCode, res, options, custom_headers) {
  this.cors_enabled = !!options.cors_enabled
  this.cors_headers = options.cors_headers
  this.response_delay = options.response_delay
  this.content_type = content_type
  this.content = content
  this.statusCode = statusCode
  this.res = res
  this.custom_headers = custom_headers || []
}

Response.content_types = {
  'json': 'application/json',
  'html': 'text/html',
  'txt': 'text/plain',
  'js': 'application/javascript',
  'nt': 'application/n-triples',
  'csv': 'text/csv',
  'jsonld': 'application/ld+json'
}

Response.cors_headers = [
  ['Access-Control-Allow-Origin', '*'],
  ['Access-Control-Allow-Headers', 'X-Requested-With'],
  ['Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS']
]

Response.prototype.send = function () {
  this.headers().forEach(function (header) {
    this.res.setHeader(header[0], header[1])
  }, this)
  this.res.statusCode = this.statusCode

  setTimeout(this.res.end.bind(this.res, this.content), this.response_delay)
}

Response.prototype.headers = function () {
  var headers = []
  headers = this._addContentTypeHeaders(headers)
  headers = this._addCORSHeaders(headers)
  headers = this._addCustomHeaders(headers)
  return headers
}

Response.prototype._addContentTypeHeaders = function (headers) {
  if (this.content_type) {
    headers.push(['Content-Type', this.content_type])
  }
  return headers
}

Response.prototype._addCORSHeaders = function (headers) {
  var that = this;
  if (this.cors_enabled) {
    Response.cors_headers.forEach(function (h) {
      if (!!that.cors_headers && h[0] === 'Access-Control-Allow-Headers')
        headers.push([h[0], h[1] + ", " + that.cors_headers])
      else
        headers.push(h)
    })
  }
  return headers
}

Response.prototype._addCustomHeaders = function (headers) {
  this.custom_headers.forEach(function(header) {
    var key = Object.keys(header)[0]
    headers.push([key, header[key]])
  })
  return headers
}

module.exports = Response


================================================
FILE: lib/utils.js
================================================
"use strict";

var utils = module.exports = {}

utils.escapeRegexSpecialChars = function (text) {
  return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&")
}

utils.extend = function (target) {
  var sources = [].slice.call(arguments, 1);
  sources.forEach(function (source) {
    for (var prop in source) {
      if(Object.hasOwnProperty.call(source, prop)){
        target[prop] = source[prop]
      }
    }
  })
  return target
}

utils.removeJSLikeComments = function (text) {
  return text.replace(/\/\*.+?\*\/|\/\/\s.*(?=[\n\r])/g, '')
}

utils.removeSpecialComments = function (data) {
  return data.split("\n").filter(function(line) {
    return line.indexOf("//!") !== 0
  }).join("\n").trim()
}


================================================
FILE: package.json
================================================
{
  "name": "canned",
  "version": "0.3.13",
  "description": "serve canned responses to mock an api, based on files in a folder",
  "main": "index.js",
  "scripts": {
    "test": "make test",
    "start": "./bin/canned"
  },
  "bin": "./bin/canned",
  "repository": {
    "type": "git",
    "url": "https://github.com/sideshowcoder/canned"
  },
  "dependencies": {
    "lodash": "^4.17.15",
    "optimist": "^0.6.0"
  },
  "devDependencies": {
    "jasmine-node": "^3.0.0",
    "jshint": "^2.10.2",
    "node-dev": "^4.0.0"
  },
  "engines": {
    "node": ">=0.10.3"
  },
  "keywords": [
    "mock",
    "api",
    "server"
  ],
  "author": "Philipp Fehre @sideshowcoder",
  "license": "MIT"
}


================================================
FILE: spec/canned.spec.js
================================================
"use strict";
var querystring = require("querystring")
var canned = require('../index')
var path = require('path')

describe('canned', function () {

  var can, req, res
  beforeEach(function () {
    can = canned('./spec/test_responses')
    req = { method: 'GET' }
    res = { setHeader: function () {}, end: function () {} }
    spyOn(res, 'setHeader')

  })

  describe('paths', function () {
    beforeEach(function () {
      var fullpath = path.resolve('./spec/test_responses')
      can = canned(fullpath)
    })

    it('resolve requests when passed an absolute path', function (done) {
      req.url = '/a'
      res.end = function () {
        expect(res.statusCode).toBe(200)
        done()
      }
      can(req, res)
    })
  })

  describe('error messages', function () {
    var writeLog, logCan
    beforeEach(function () {
      var logger = {
        write: function (msg) {
          if (writeLog) writeLog(msg)
        }
      }
      logCan = canned('./spec/test_responses', { logger: logger })
    })

    it('displays an error for unparsable json files', function (done) {
      var regex = new RegExp('.*Syntax.*')
      writeLog = function (message) {
        if (regex.test(message)) {
          expect(message).toContain("problem sanatizing content for _invalid_syntax.get.json SyntaxError: Unexpected token I")
          done()
        }
      }
      req.url = '/invalid_syntax'
      logCan(req, res)
    })
  })

  describe('sanitization', function () {
    var writeLog, logCan
    var logger = {
      write: function (msg) {
        if (writeLog) writeLog(msg)
      }
    }
    describe('with sanitization enabled', function() {
      beforeEach(function () {
        logCan = canned('./spec/test_responses', { logger: logger })
      })

      it('displays an error for json containing unexpected markup', function (done) {
        var regex = new RegExp('.*Syntax.*')
        writeLog = function (message) {
          if (regex.test(message)) {
            expect(message).toContain("problem sanatizing content for _broken_sanitize.get.json SyntaxError: Unexpected token")
            done()
          }
        }
        req.url = '/broken_sanitize'
        logCan(req, res)
      })
    })

    describe('with sanitization disabled', function() {
      beforeEach(function () {
        logCan = canned('./spec/test_responses', { logger: logger, sanitize: false })
      })

      it('loads content from _broken_sanitize.get.json', function (done) {
        req.url = '/broken_sanitize'
        res.end = function (content) {
          expect(content).toContain('"whatAmI": "I have been copy/pasted into a WYSIWYG editor by your grandma"')
          done()
        }
        logCan(req, res)
      })
    })
  })

  describe('status codes', function () {
    it('sets 404 for non resolveable request', function (done) {
      req.url = '/i_do_not_exist'
      res.end = function () {
        expect(res.statusCode).toBe(404)
        done()
      }
      can(req, res)
    })

    it('sets 200 for resolveable requests', function (done) {
      req.url = '/a'
      res.end = function () {
        expect(res.statusCode).toBe(200)
        done()
      }
      can(req, res)
    })

    it('sets 201 if specified in file', function (done) {
      req.url = '/201'
      res.end = function () {
        expect(res.statusCode).toBe(201)
        done()
      }
      can(req, res)
    })

    it('sets 204 for empty file', function (done) {
      req.url = '/empty'
      res.end = function () {
        expect(res.statusCode).toBe(204)
        done()
      }
      can(req, res)
    })

    it('sets specified status for empty file with headers set', function (done) {
      req.url = '/empty_with_headers'
      res.end = function () {
        expect(res.statusCode).toBe(420)
        done()
      }
      can(req, res)
    })
  })

  describe('content type', function () {
    it('sets text/plain for txt', function (done) {
      req.url = '/b'
      res.setHeader = function (name, value) {
        expect(value).toBe('text/plain')
        expect(name).toBe('Content-Type')
        done()
      }
      can(req, res)
    })

    it('sets text/html for errors', function (done) {
      req.url = '/i_do_not_exist'
      res.setHeader = function (name, value) {
        expect(value).toBe('text/html')
        expect(name).toBe('Content-Type')
        done()
      }
      can(req, res)
    })
    it('sets Content-type header if specified in file', function(done){
      req.url = '/vendor_type'
      res.setHeader = function(name, value){
        expect(value).toBe('application/vnd.custom+xml')
        expect(name).toBe('Content-Type')
        done()
      }
      can(req, res)
    })
  })

    describe('custom response header', function(){
      it('populates custom header with single header', function(done){
          req.url = '/single_custom_header'
          res.end = function() {
            expect(res.setHeader).toHaveBeenCalledWith('Header-Key', 'Header-Content')
            done()
        }
          can(req, res)
      })

      it('populates custom headers with multiple headers', function(done){
          req.url = '/multiple_custom_header'
          res.end = function() {
            expect(res.setHeader).toHaveBeenCalledWith('Header-Key', 'Header-Content')
            expect(res.setHeader).toHaveBeenCalledWith('Header-Key2', 'Header-Content2')
            done()
        }
          can(req, res)
      })
    })

  describe('resolve file paths', function () {

    it('loads index for /', function (done) {
      req.url = '/'
      res.end = function (content) {
        expect(content).toContain('index.get.json')
        done()
      }
      can(req, res)
    });

    it('loads index for /d with d being a directory', function (done) {
      req.url = '/d'
      res.end = function (content) {
        expect(content).toContain('d/index.get.json')
        done()
      }
      can(req, res)
    });

    it('loads index for /d/e with both being directories', function (done) {
      req.url = '/d/e'
      res.end = function (content) {
        expect(content).toContain('d/e/index.get.html')
        done()
      }
      can(req, res);
    });

    it('loads any for /d/something', function (done) {
      req.url = '/d/i_am_an_id'
      res.end = function (content) {
        expect(content).toContain('d/any.get.json')
        done()
      }
      can(req, res)
    })

    it('loads index from wildcard path for /d/1/', function (done) {
      req.url = '/d/1/'
      res.end = function (content) {
        expect(content).toContain('{"wildcard":1}')
        done()
      }
      can(req, res)
    })

    it('loads named response from wildcard path for /d/1/bar', function (done) {
      req.url = '/d/1/bar'
      res.end = function (content) {
        expect(content).toContain('{"wildcard":"named_response"}')
        done()
      }
      can(req, res)
    })

    it('loads index from real path for /d/2/', function (done) {
      req.url = '/d/2/'
      res.end = function (content) {
        expect(content).toContain('{"not-wildcard":1}')
        done()
      }
      can(req, res)
    })

    it('looks for _file with query params', function (done) {
      req.url = '/multimatch_query_param?name=Superman&age=30&idontneed=everyparaminfilename'
      res.end = function (content) {
        expect(content).toContain('Superman!')
        done()
      }
      can(req, res)
    })

    it('can tell different query param files a part', function (done) {
      req.url = '/multimatch_query_param?name=Batman&age=30&idontneed=everyparaminfilename'
      res.end = function (content) {
        expect(content).toContain('Batman!')
        done()
      }
      can(req, res)
    })

    it('falls back to file without query params if one or more params dont match', function (done) {
      req.url = '/a?foo=bar'
      res.end = function (content) {
        expect(content).toContain('_a.get.json')
        done()
      }
      can(req, res)
    })

    it('works for nested folder being not present', function (done) {
      req.url = '/foo/bar/baz'
      res.end = function () {
        expect(res.statusCode).toBe(404)
        done()
      }
      can(req, res)
    })

    it('allows for multiple files to match via the .NUMBER extension and pick 1 by default', function (done) {
      req.url = '/multimatch'
      res.end = function (content) {
        var multimatch = JSON.parse(content).multimatch
        expect(multimatch).toBe(1)
        done()
      }
      can(req, res)
    })

    it('selects json file for request with application/json accept header', function (done) {
      req.url = '/multiple_type';
      req.headers = {
        accept: 'application/json'
      }
      res.end = function (content) {
        var jsonResponse = JSON.parse(content)
        expect(jsonResponse.type).toBe('json')
        done()
      }
      can(req, res)
    })

    it('selects jsonld file for request with application/ld+json accept header', function (done) {
      req.url = '/multiple_type';
      req.headers = {
        accept: 'application/ld+json'
      }
      res.end = function (content) {
        var jsonResponse = JSON.parse(content)
        expect(jsonResponse['@context']).toBe('http://schema.org/')
        done()
      }
      can(req, res)
    })

    it('selects nt file for request with application/n-triples accept header', function (done) {
      req.url = '/multiple_type';
      req.headers = {
        accept: 'application/n-triples'
      }
      res.end = function (content) {
        expect(content).toBe('_:b0 <http://schema.org/name> \"Jane Doe\" .')
        done()
      }
      can(req, res)
    })

    it('selects csv file for request with text/csv accept header', function (done) {
      req.url = '/multiple_type';
      req.headers = {
        accept: 'text/csv'
      }
      res.end = function (content) {
        expect(content).toBe('Jane Doe,Professor,(425) 123-4567,http://www.janedoe.com')
        done()
      }
      can(req, res)
    })

    it('selects js file for request with application/javascript accept header', function (done) {
      req.url = '/multiple_type';
      req.headers = {
        accept: 'application/javascript'
      }
      res.end = function (content) {
        expect(content).toBe('var type = \'js\';')
        done()
      }
      can(req, res)
    })

    it('selects txt file for request with text/plain accept header', function (done) {
      req.url = '/multiple_type';
      req.headers = {
        accept: 'text/plain'
      }
      res.end = function (content) {
        expect(content).toBe('text type')
        done()
      }
      can(req, res)
    })

    it('selects txt file for request with text/html accept header', function (done) {
      req.url = '/multiple_type';
      req.headers = {
        accept: 'text/html'
      }
      res.end = function (content) {
        expect(content).toBe('<type>html</type>')
        done()
      }
      can(req, res)
    })

    it('selects json file when application/json is not first in the Accept header', function (done) {
      can = canned('./spec/test_responses', {"relaxed_accept": true})
      req.url = '/d/multiple-accept-types'
      req.headers = {
        accept: 'text/html, application/json'
      }
      res.end = function (content) {
        expect(content).toBe('{"type":"json"}');
        done()
      }
      can(req, res)
    })
  })

  describe('content modifier', function () {
    it('removes comments from json', function (done) {
      req.url = '/d/commented'
      res.end = function (content) {
        expect(content).toBe('{"no":"comments"}');
        done()
      }
      can(req, res)
    })

    it('works with http:// in json strings', function (done) {
      req.url = '/chartest'
      res.end = function (content) {
        expect(content).toBe('{"my_url":"http://www.mywebsite.com"}');
        done()
      }
      can(req, res)
    })
  })

  describe('CORS', function () {
    var can = canned('./spec/test_responses', { cors: true })
    it('accepts the options verb', function (done) {
      req.method = 'OPTIONS'
      req.url = '/'
      res.end = function (content) {
        // serves no content
        expect(content).toBe('')
        done()
      }
      can(req, res)
    })

    it('sets the headers', function (done) {
      req.url = '/'
      var expectedHeaders = {
        'Access-Control-Allow-Origin': "*",
        'Access-Control-Allow-Headers': "X-Requested-With",
        'Access-Control-Allow-Methods': "GET, POST, PUT, PATCH, DELETE, OPTIONS"
      }
      res.setHeader = function (name, value) {
        if (expectedHeaders[name]) {
          expect(expectedHeaders[name]).toBe(value)
          delete expectedHeaders[name]
        }
        // all expected headers have been set!
        if (Object.keys(expectedHeaders).length === 0) done()
      }
      can(req, res)
    })

    it('adds custom headers from a string', function (done) {
      var can2 = canned('./spec/test_responses', { cors: true, cors_headers: "Authorization, Content-Type" })
      req.url = '/'
      var expectedHeaders = {
        'Access-Control-Allow-Headers': "X-Requested-With, Authorization, Content-Type"
      }
      res.setHeader = function (name, value) {
        if (expectedHeaders[name]) {
          expect(expectedHeaders[name]).toBe(value)
          delete expectedHeaders[name]
        }
        // all expected headers have been set!
        if (Object.keys(expectedHeaders).length === 0) done()
      }
      can2(req, res)
    })

    it('adds custom headers from an array', function (done) {
      var can2 = canned('./spec/test_responses', { cors: true, cors_headers: ["Authorization", "Content-Type"] })
      req.url = '/'
      var expectedHeaders = {
        'Access-Control-Allow-Headers': "X-Requested-With, Authorization, Content-Type"
      }
      res.setHeader = function (name, value) {
        if (expectedHeaders[name]) {
          expect(expectedHeaders[name]).toBe(value)
          delete expectedHeaders[name]
        }
        // all expected headers have been set!
        if (Object.keys(expectedHeaders).length === 0) done()
      }
      can2(req, res)
    })
  })

  describe('variable GET responses', function () {
    it('should return the first JSON response body if no header match', function (done) {
      req.headers = {}
      req.url = '/multiple_responses'
      res.end = function (content) {
        expect(content).toEqual(JSON.stringify({"response":"response for abc"}))
        done()
      }
      can(req, res)
    })

    it('should return the first text response body if no header match', function (done) {
      req.headers = {}
      req.url = '/multiple_responses_text'
      res.end = function (content) {
        expect(content).toEqual('response for abc')
        done()
      }
      can(req, res)
    })

    it('should return the first JSON response body on header match', function (done) {
      req.headers = {
        "authorization": 'abc'
      }
      req.url = '/multiple_responses'
      res.end = function (content) {
        expect(content).toEqual(JSON.stringify({"response":"response for abc"}))
        done()
      }
      can(req, res)
    })

    it('should return the first text response body on header match', function (done) {
      req.headers = {
        "authorization": 'abc'
      }
      req.url = '/multiple_responses_text'
      res.end = function (content) {
        expect(content).toEqual('response for abc')
        done()
      }
      can(req, res)
    })

    it('should return the second response body on header match', function (done) {
      req.headers = {
        "authorization": '123'
      }
      req.url = '/multiple_responses'
      res.end = function (content) {
        expect(content).toEqual(JSON.stringify({"response":"response for 123"}))
        done()
      }
      can(req, res)
    })

    it('should return the second response body on header match', function (done) {
      req.headers = {
        "authorization": '123'
      }
      req.url = '/multiple_responses_text'
      res.end = function (content) {
        expect(content).toEqual('response for 123')
        done()
      }
      can(req, res)
    })

    it('should be able to return html', function (done) {
      req.headers = {
        "authorization": 'html'
      }
      req.url = '/multiple_responses_text'
      res.end = function (content) {
        expect(content).toEqual('<h1>response for html</h1>')
        done()
      }
      can(req, res)
    })

    it('should return correct status code and the first JSON response body on header match', function (done) {
      req.headers = {
        "authorization": 'abc'
      }
      req.url = '/201_multiple_responses'
      res.end = function (content) {
        expect(res.statusCode).toBe(201)
        expect(content).toEqual(JSON.stringify({"response":"response for abc"}))
        done()
      }
      can(req, res)
    })
    it('should return correct status code and the second response body on header match', function (done) {
      req.headers = {
        "authorization": '123'
      }
      req.url = '/201_multiple_responses'
      res.end = function (content) {
        expect(res.statusCode).toBe(201)
        expect(content).toEqual(JSON.stringify({"response":"response for 123"}))
        done()
      }
      can(req, res)
    })
  })

  describe("variable GET responses based on params", function() {
    var req, data
    beforeEach(function() {
      req = { method: 'GET' }
    })

    it("should select the right response based on the GET request data", function (done) {
      req.url = "/multiple_get_responses?" + querystring.stringify({ foo: "bar" })
      res.end = function (content) {
        expect(content).toEqual(JSON.stringify({"response": "response for bar"}))
        done()
      }
      can(req, res)
    })

    it("should select the right response based on the GET request data", function (done) {
      req.url = "/multiple_get_responses?" + querystring.stringify({ foo: "apostrophe" })
      res.end = function (content) {
        expect(content).toEqual(JSON.stringify({"response": "response with 'apostrophes'"}))
        done()
      }
      can(req, res)
    })

    it("should select the right response based on the GET request data", function (done) {
      req.url = "/multiple_get_responses?" + querystring.stringify({ foo: "bar", index: 1 })
      res.end = function (content) {
        expect(content).toEqual(JSON.stringify({"response": "response with index 1"}))
        done()
      }
      can(req, res)
    })

    it("should select the first response with no query string", function (done) {
      req.url = "/multiple_get_responses"
      res.end = function (content) {
        expect(content).toEqual(JSON.stringify({"response": "response for baz"}))
        done()
      }
      can(req, res)
    })
  })

  describe("Issues", function () {
    it("#58", function(done) {
      req.url = "/multiple_get_responses?" + querystring.stringify({foo: "apostrophe"})
      res.end = function(content) {
        expect(content).toEqual(JSON.stringify({"response": "response with 'apostrophes'"}))
        done()
      }
      can(req, res)
    })

    it("#73", function (done) {
      req.url = "/multiple_get_responses?" + querystring.stringify({"foo": "bar", "index": 1})
      res.end = function (content) {
        expect(content).toEqual(JSON.stringify({"response": "response with index 1"}))
        done()
      }
      can(req, res)
    })

    it("#79", function (done) {
        var Canned = require('../lib/canned')
        var can = new Canned('./spec/test_responses', {});
        var mock_text = '//! params: {"serialkey": "abc"}\r\n{\r\n"errorCode": "ERROR1"\r\n}\r\n' +
                        '//! params: {"serialkey": "12121"}\r\n{\r\n"errorCode": "ERROR2"\r\n}';
        var parsedMeta = can.parseMetaData(mock_text);
        expect(parsedMeta).toEqual({
          request: {
            serialkey: 'abc'
          },
          params: {
            serialkey: '12121'
          }
        });
        done();
    })
  })

  describe("variable POST responses", function() {
    var req, data
    beforeEach(function() {
      req = {
        method: 'POST',
        headers: {},
        on: function(event, fn) {
          fn(data)
        }
      }
    })

    it('should return the first response body if no payload match', function (done) {
      data = 'email=nobody@example.com'
      req.url = '/multiple_responses'
      res.end = function (content) {
        expect(content).toEqual(JSON.stringify({"response": "response for one@example.com"}))
        done()
      }
      can(req, res)
    })

    it('should return the first response body if no payload match', function (done) {
      data = 'email=nobody@example.com'
      req.url = '/multiple_responses_text'
      res.end = function (content) {
        expect(content).toEqual('response for one@example.com')
        done()
      }
      can(req, res)
    })

    it('should return the first response body on payload match', function (done) {
      data = 'email=one@example.com'
      req.url = '/multiple_responses'
      res.end = function (content) {
        expect(content).toEqual(JSON.stringify({"response": "response for one@example.com"}))
        done()
      }
      can(req, res)
    })

    it('should return the first response JSON body on payload match', function (done) {
      data = '{"email":"one@example.com"}'
      req.url = '/multiple_responses'
      req.headers['content-type'] = 'application/json'
      res.end = function (content) {
        expect(content).toEqual(JSON.stringify({"response": "response for one@example.com"}))
        done()
      }
      can(req, res)
    })

    it('should return the first response JSON body on payload match even if content type has charset', function (done) {
      data = '{"email":"one@example.com"}'
      req.url = '/multiple_responses'
      req.headers['content-type'] = 'application/json; charset=UTF-8'
      res.end = function (content) {
        expect(content).toEqual(JSON.stringify({"response": "response for one@example.com"}))
        done()
      }
      can(req, res)
    })

    it('should handle request bodies containing arrays', function (done) {
      data = '{"email": "two@example.com","topics": [1,2]}'
      req.url = '/multiple_responses'
      req.headers['content-type'] = 'application/json; charset=UTF-8'
      res.end = function (content) {
        expect(content).toEqual(JSON.stringify({"response": "response for two@example.com topics 1,2"}))
        done()
      }
      can(req, res)
    })

    it('should handle request bodies containing urls', function (done) {
      data = '{"url": "http://example.com"}'
      req.url = '/response_with_url_param'
      req.headers['content-type'] = 'application/json'
      res.end = function (content) {
        expect(content).toEqual(JSON.stringify({"response": "response for url in param"}))
        done()
      }
      can(req, res)
    })

    it('should return the first response JSON body on payload match (because JSON body is invalid)', function (done) {
      data = 'bad json data'
      req.url = '/multiple_responses'
      req.headers['content-type'] = 'application/json'
      res.end = function (content) {
        expect(content).toEqual(JSON.stringify({"response": "response for one@example.com"}))
        done()
      }
      can(req, res)
    })

    it('should return the first response body on payload match', function (done) {
      data = 'email=one@example.com'
      req.url = '/multiple_responses_text'
      res.end = function (content) {
        expect(content).toEqual('response for one@example.com')
        done()
      }
      can(req, res)
    })

    it('should return the second response body on payload match', function (done) {
      data = 'email=two@example.com'
      req.url = '/multiple_responses'
      res.end = function (content) {
        expect(content).toEqual(JSON.stringify({"response": "response for two@example.com"}))
        done()
      }
      can(req, res)
    })

    it('should return the second response JSON body on payload match', function (done) {
      data = '{"email":"two@example.com"}'
      req.url = '/multiple_responses'
      req.headers['content-type'] = 'application/json'
      res.end = function (content) {
        expect(content).toEqual(JSON.stringify({"response": "response for two@example.com"}))
        done()
      }
      can(req, res)
    })

    it('should return the second response body on payload match', function (done) {
      data = 'email=two@example.com'
      req.url = '/multiple_responses_text'
      res.end = function (content) {
        expect(content).toEqual('response for two@example.com')
        done()
      }
      can(req, res)
    })

    it('should return the second response body on xml (or really any string) payload match', function (done) {
      data = '<xml>b</xml>'
      req.url = '/multiple_responses_xml_request_body'
      res.end = function (content) {
        expect(content).toEqual('<xml>B</xml>')
        done()
      }
      can(req, res)
    })

    it('should return the first response xml on header match', function (done) {
      data = ''
      req.url = '/multiple_responses_xml'
      req.headers = {
        "action": 'foo'
      }
      res.end = function (content) {
        expect(content).toEqual('<SOAP:Envelope><SOAP:Body><Foo Time="2015-01-22T08:30:00.000+05:30"/></SOAP:Body></SOAP:Envelope>')
        done()
      }
      can(req, res)
    })

    it('should return the second response xml on header match', function (done) {
      data = ''
      req.url = '/multiple_responses_xml'
      req.headers = {
        "action": 'bar'
      }
      res.end = function (content) {
        expect(content).toEqual('<SOAP:Envelope><SOAP:Body><Bar Time="2015-01-22T08:30:00.000+05:30"/></SOAP:Body></SOAP:Envelope>')
        done()
      }
      can(req, res)
    })
  })

})


================================================
FILE: spec/lookup.spec.js
================================================
"use strict";

var lookup = require('../lib/lookup')

describe('lookup', function () {
  it('should generate a list of paths in the correct order', function (done) {
    var testPath = '/api/2/customer/123/invoice/321/'
    var expectedPaths = [
      '/api/2/customer/123/invoice/321/',
      '/api/2/customer/123/invoice/any/',
      '/api/2/customer/any/invoice/321/',
      '/api/2/customer/any/invoice/any/',
      '/api/any/customer/123/invoice/321/',
      '/api/any/customer/123/invoice/any/',
      '/api/any/customer/any/invoice/321/',
      '/api/any/customer/any/invoice/any/'
    ]
    expect(lookup(testPath, 'any')).toEqual(expectedPaths)
    done()
  });
});


================================================
FILE: spec/test_responses/_201.get.json
================================================
//! statusCode: 201
{
  "created": 1
}


================================================
FILE: spec/test_responses/_201_multiple_responses.get.json
================================================
//! statusCode: 201
//! header: {"authorization": "123"}
{
    "response": "response for 123"
}

//! statusCode: 201
//! header: {"authorization": "abc"}
{
    "response": "response for abc"
}


================================================
FILE: spec/test_responses/_a.get.html
================================================
_a.get.json


================================================
FILE: spec/test_responses/_b.get.txt
================================================
_b.get.txt


================================================
FILE: spec/test_responses/_broken_sanitize.get.json
================================================
{
  "whatAmI": "I have been copy/pasted into a WYSIWYG editor by your grandma",
  "embedCode": "<div class=\"nasty-3rd-party-code\">\n\n\n<script type=\"text/javascript\">\nvar someValue = 'I will make you cry';\n// --></script>\n</script>  </p>\n\nHuh?<p>\n\n</div>\n\nYes, true story!",
  "honestly": "Can we get an Internet User Licence introduced already?"
}


================================================
FILE: spec/test_responses/_chartest.get.json
================================================
{
  "my_url":"http://www.mywebsite.com"
}

================================================
FILE: spec/test_responses/_empty.get.json
================================================


================================================
FILE: spec/test_responses/_empty_with_headers.get.json
================================================
//! statusCode: 420

================================================
FILE: spec/test_responses/_invalid_syntax.get.json
================================================
I am not valid JSON!


================================================
FILE: spec/test_responses/_multimatch.get.json.1
================================================
{
  "multimatch": 1
}


================================================
FILE: spec/test_responses/_multimatch.get.json.2
================================================
{
  "multimatch": 2
}


================================================
FILE: spec/test_responses/_multimatch_query_param.get.html
================================================
//! params: {"name": "Batman", "age": "30"}
Batman!

//! params: {"name": "Superman", "age": "30"}
Superman!


================================================
FILE: spec/test_responses/_multiple_custom_header.get.json
================================================
//! customHeader: {"Header-Key": "Header-Content"}
//! customHeader: {"Header-Key2": "Header-Content2"}


================================================
FILE: spec/test_responses/_multiple_get_responses.get.json
================================================
//! params: {"foo": "baz"}
{
    "response": "response for baz"
}

//! params: {"foo": "bar"}
{
    "response": "response for bar"
}

//! params: {"foo": "apostrophe"}
{
    "response": "response with 'apostrophes'"
}

//! params: {"foo": "bar", "index": 1}
{
    "response": "response with index 1"
}


================================================
FILE: spec/test_responses/_multiple_responses.get.json
================================================
//! header: {"authorization": "abc"}
{
    "response": "response for abc"
}

//! header: {"authorization": "123"}
{
    "response": "response for 123"
}

================================================
FILE: spec/test_responses/_multiple_responses.post.json
================================================
//! body: {"email": "one@example.com"}
{
    "response": "response for one@example.com"
}

//! body: {"email": "two@example.com"}
{
    "response": "response for two@example.com"
}

//! body: {"email": "two@example.com", "topics": ["1","2"]}
{
    "response": "response for two@example.com topics 1,2"
}

================================================
FILE: spec/test_responses/_multiple_responses_text.get.html
================================================
//! header: {"authorization": "abc"}
response for abc

//! header: {"authorization": "123"}
response for 123

//! header: {"authorization": "html"}
<h1>response for html</h1>

================================================
FILE: spec/test_responses/_multiple_responses_text.post.html
================================================
//! body: {"email": "one@example.com"}
response for one@example.com

//! body: {"email": "two@example.com"}
response for two@example.com

================================================
FILE: spec/test_responses/_multiple_responses_xml.post.xml
================================================
//! header: {"action": "foo"}
<SOAP:Envelope><SOAP:Body><Foo Time="2015-01-22T08:30:00.000+05:30"/></SOAP:Body></SOAP:Envelope>

//! header: {"action": "bar"}
<SOAP:Envelope><SOAP:Body><Bar Time="2015-01-22T08:30:00.000+05:30"/></SOAP:Body></SOAP:Envelope>


================================================
FILE: spec/test_responses/_multiple_responses_xml_request_body.post.xml
================================================
//! body: <xml>a</xml>
<xml>A</xml>

//! body: <xml>b</xml>
<xml>B</xml>


================================================
FILE: spec/test_responses/_multiple_type.get.csv
================================================
Jane Doe,Professor,(425) 123-4567,http://www.janedoe.com

================================================
FILE: spec/test_responses/_multiple_type.get.html
================================================
<type>html</type>

================================================
FILE: spec/test_responses/_multiple_type.get.js
================================================
var type = 'js';

================================================
FILE: spec/test_responses/_multiple_type.get.json
================================================
{
	"type": "json"
}

================================================
FILE: spec/test_responses/_multiple_type.get.jsonld
================================================
{
  "@context": "http://schema.org/",
  "@type": "Person",
  "name": "Jane Doe",
  "jobTitle": "Professor",
  "telephone": "(425) 123-4567",
  "url": "http://www.janedoe.com"
}

================================================
FILE: spec/test_responses/_multiple_type.get.nt
================================================
_:b0 <http://schema.org/name> "Jane Doe" .

================================================
FILE: spec/test_responses/_multiple_type.get.txt
================================================
text type

================================================
FILE: spec/test_responses/_response_with_url_param.post.json
================================================
//! body: {"url":"http://example.com"}
{
    "response": "response for url in param"
}



================================================
FILE: spec/test_responses/_single_custom_header.get.json
================================================
//! customHeader: {"Header-Key": "Header-Content"}


================================================
FILE: spec/test_responses/_vendor_type.get.xml
================================================
//! statusCode  :  200   ,contentType  :     "application/vnd.custom+xml"   
<xml>
	<created>1</created>
</xml>

================================================
FILE: spec/test_responses/d/2/index.get.json
================================================
{"not-wildcard":1}

================================================
FILE: spec/test_responses/d/_commented.get.json
================================================
// to level comment
{
  // nested comment
  "no":"comments"
}


================================================
FILE: spec/test_responses/d/any/_bar.get.json
================================================
{
  "wildcard": "named_response"
}


================================================
FILE: spec/test_responses/d/any/index.get.json
================================================
{
  "wildcard": 1
}


================================================
FILE: spec/test_responses/d/any.get.html
================================================
d/any.get.json


================================================
FILE: spec/test_responses/d/e/index.get.html
================================================
d/e/index.get.html

================================================
FILE: spec/test_responses/d/index.get.html
================================================
d/index.get.json


================================================
FILE: spec/test_responses/d/multiple-accept-types/index.get.json
================================================
{
  "type":"json"
}


================================================
FILE: spec/test_responses/index.get.html
================================================
index.get.json


================================================
FILE: test/assert.sh
================================================
#!/bin/bash
# assert.sh 1.0 - bash unit testing framework
# Copyright (C) 2009, 2010, 2011, 2012 Robert Lehmann
#
# http://github.com/lehmannro/assert.sh
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

export DISCOVERONLY=${DISCOVERONLY:-}
export DEBUG=${DEBUG:-}
export STOP=${STOP:-}
export INVARIANT=${INVARIANT:-}

args="$(getopt -n "$0" -l verbose,help,stop,discover,invariant vhxdi $*)" \
|| exit -1
for arg in $args; do
    case "$arg" in
        -h)
            echo "$0 [-vxid] [--verbose] [--stop] [--invariant] [--discover]"
            echo "`sed 's/./ /g' <<< "$0"` [-h] [--help]"
            exit 0;;
        --help)
            cat <<EOF
Usage: $0 [options]
Language-agnostic unit tests for subprocesses.

Options:
  -v, --verbose    generate output for every individual test case
  -x, --stop       stop running tests after the first failure
  -i, --invariant  do not measure timings to remain invariant between runs
  -d, --discover   collect test suites only, do not run any tests
  -h               show brief usage information and exit
  --help           show this help message and exit
EOF
            exit 0;;
        -v|--verbose)
            DEBUG=1;;
        -x|--stop)
            STOP=1;;
        -i|--invariant)
            INVARIANT=1;;
        -d|--discover)
            DISCOVERONLY=1;;
    esac
done

printf -v _indent "\n\t" # local format helper

_assert_reset() {
    tests_ran=0
    tests_failed=0
    tests_errors=()
    tests_starttime="$(date +%s.%N)" # seconds_since_epoch.nanoseconds
}

assert_end() {
    # assert_end [suite ..]
    tests_endtime="$(date +%s.%N)"
    tests="$tests_ran ${*:+$* }tests"
    [[ -n "$DISCOVERONLY" ]] && echo "collected $tests." && _assert_reset && return
    [[ -n "$DEBUG" ]] && echo
    [[ -z "$INVARIANT" ]] && report_time=" in $(bc \
        <<< "${tests_endtime%.N} - ${tests_starttime%.N}" \
        | sed -e 's/\.\([0-9]\{0,3\}\)[0-9]*/.\1/' -e 's/^\./0./')s" \
        || report_time=

    if [[ "$tests_failed" -eq 0 ]]; then
        echo "all $tests passed$report_time."
    else
        for error in "${tests_errors[@]}"; do echo "$error"; done
        echo "$tests_failed of $tests failed$report_time."
    fi
    tests_failed_previous=$tests_failed
    _assert_reset
    return $tests_failed_previous
}

assert() {
    # assert <command> <expected stdout> [stdin]
    (( tests_ran++ ))
    [[ -n "$DISCOVERONLY" ]] && return
    # printf required for formatting
    printf -v expected "x${2:-}" # x required to overwrite older results
    result="$(eval 2>/dev/null $1 <<< ${3:-})"
    # Note: $expected is already decorated
    if [[ "x$result" == "$expected" ]]; then
        [[ -n "$DEBUG" ]] && echo -n .
        return
    fi
    [[ -n "$DEBUG" ]] && echo -n X
    result="$(sed -e :a -e '$!N;s/\n/\\n/;ta' <<< "$result")"
    [[ -z "$result" ]] && result="nothing" || result="\"$result\""
    [[ -z "$2" ]] && expected="nothing" || expected="\"$2\""
    failure="expected $expected${_indent}got $result"
    report="test #$tests_ran \"$1${3:+ <<< $3}\" failed:${_indent}$failure"
    tests_errors[$tests_failed]="$report"
    (( tests_failed++ ))
    if [[ -n "$STOP" ]]; then
        [[ -n "$DEBUG" ]] && echo
        echo "$report"
        exit 1
    fi
}

assert_raises() {
    # assert_raises <command> <expected code> [stdin]
    (( tests_ran++ ))
    [[ -n "$DISCOVERONLY" ]] && return
    (eval $1 <<< ${3:-}) > /dev/null 2>&1
    status=$?
    expected=${2:-0}
    if [[ "$status" -eq "$expected" ]]; then
        [[ -n "$DEBUG" ]] && echo -n .
        return
    fi
    [[ -n "$DEBUG" ]] && echo -n X
    failure="program terminated with code $status instead of $expected"
    report="test #$tests_ran \"$1${3:+ <<< $3}\" failed:${_indent}$failure"
    tests_errors[$tests_failed]="$report"
    (( tests_failed++ ))
    if [[ -n "$STOP" ]]; then
        [[ -n "$DEBUG" ]] && echo
        echo "$report"
        exit 1
    fi
}

_assert_reset


================================================
FILE: test/bin_test.sh
================================================
#!/bin/bash
. assert.sh

# start canned on port
../bin/canned -p 8765 ../example &
CPID=$!
sleep 1
assert "lsof -i:8765 | grep node | awk '{print \$1}'" "node"

curl -sL -w " %{http_code}" http://127.0.0.1:8765/search | grep 200
assert  "echo $?" "0"

curl -sL http://127.0.0.1:8765/search?search=specific | grep "specific result"
assert "echo $?" "0"

curl -sL http://127.0.0.1:8765/search?search=apostrophe | grep "There's something about Mary"
assert "echo $?" "0"

kill $CPID

assert_end examples


Download .txt
gitextract_myo37vrg/

├── .gitignore
├── .jshintrc
├── .travis.yml
├── Makefile
├── README.md
├── bin/
│   └── canned
├── example/
│   ├── _search.get.json
│   └── comment/
│       ├── _myfunc.get.json
│       ├── any.get.json
│       ├── any.put.json
│       ├── index.get.json
│       └── index.post.json
├── index.js
├── lib/
│   ├── canned.js
│   ├── lookup.js
│   ├── response.js
│   └── utils.js
├── package.json
├── spec/
│   ├── canned.spec.js
│   ├── lookup.spec.js
│   └── test_responses/
│       ├── _201.get.json
│       ├── _201_multiple_responses.get.json
│       ├── _a.get.html
│       ├── _b.get.txt
│       ├── _broken_sanitize.get.json
│       ├── _chartest.get.json
│       ├── _empty.get.json
│       ├── _empty_with_headers.get.json
│       ├── _invalid_syntax.get.json
│       ├── _multimatch.get.json.1
│       ├── _multimatch.get.json.2
│       ├── _multimatch_query_param.get.html
│       ├── _multiple_custom_header.get.json
│       ├── _multiple_get_responses.get.json
│       ├── _multiple_responses.get.json
│       ├── _multiple_responses.post.json
│       ├── _multiple_responses_text.get.html
│       ├── _multiple_responses_text.post.html
│       ├── _multiple_responses_xml.post.xml
│       ├── _multiple_responses_xml_request_body.post.xml
│       ├── _multiple_type.get.csv
│       ├── _multiple_type.get.html
│       ├── _multiple_type.get.js
│       ├── _multiple_type.get.json
│       ├── _multiple_type.get.jsonld
│       ├── _multiple_type.get.nt
│       ├── _multiple_type.get.txt
│       ├── _response_with_url_param.post.json
│       ├── _single_custom_header.get.json
│       ├── _vendor_type.get.xml
│       ├── d/
│       │   ├── 2/
│       │   │   └── index.get.json
│       │   ├── _commented.get.json
│       │   ├── any/
│       │   │   ├── _bar.get.json
│       │   │   └── index.get.json
│       │   ├── any.get.html
│       │   ├── e/
│       │   │   └── index.get.html
│       │   ├── index.get.html
│       │   └── multiple-accept-types/
│       │       └── index.get.json
│       └── index.get.html
└── test/
    ├── assert.sh
    └── bin_test.sh
Download .txt
SYMBOL INDEX (8 symbols across 2 files)

FILE: lib/canned.js
  function Canned (line 12) | function Canned(dir, options) {
  function matchFile (line 29) | function matchFile(matchString, fname, method, ctype) {
  function matchFileWithQuery (line 38) | function matchFileWithQuery(matchString, ctype) {
  function matchFileWithExactQuery (line 47) | function matchFileWithExactQuery(matchString, fname, queryString, method...
  function getContentType (line 96) | function getContentType(mimetype){
  function stringifyValues (line 100) | function stringifyValues(object) {
  function isContentTypeJson (line 110) | function isContentTypeJson(request) {

FILE: lib/response.js
  function Response (line 4) | function Response(content_type, content, statusCode, res, options, custo...
Condensed preview — 61 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (85K chars).
[
  {
    "path": ".gitignore",
    "chars": 41,
    "preview": "/node_modules\nnpm-debug.log\n.idea/\n/TAGS\n"
  },
  {
    "path": ".jshintrc",
    "chars": 4389,
    "preview": "{\n    \"bitwise\"       : true,     // Prohibit bitwise operators (&, |, ^, etc.).\n    \"forin\"         : true,     // Tole"
  },
  {
    "path": ".travis.yml",
    "chars": 386,
    "preview": "language: node_js\nsudo: false\nnode_js:\n  - node\n  - lts/*\nscript: make\nnotifications:\n  webhooks:\n    urls:\n      - http"
  },
  {
    "path": "Makefile",
    "chars": 743,
    "preview": ".PHONY: test hint default\nCURRENT_BRANCH=$(shell git rev-parse --abbrev-ref HEAD)\n\ndefault: bootstrap hint test\n\nbootstr"
  },
  {
    "path": "README.md",
    "chars": 15682,
    "preview": "Canned fake API server\n======================\n\n[![Build Status](https://travis-ci.org/sideshowcoder/canned.png?branch=ma"
  },
  {
    "path": "bin/canned",
    "chars": 1739,
    "preview": "#!/usr/bin/env node\nvar canned = require('../index')\n,   path = require('path')\n,   http = require('http')\n,   optimist "
  },
  {
    "path": "example/_search.get.json",
    "chars": 348,
    "preview": "// this response is the default: \n//! params: {\"search\": \"default\"}\n{\n  \"search\":\"result\",\n  \"key\":\"value\"\n}\n\n//! params"
  },
  {
    "path": "example/comment/_myfunc.get.json",
    "chars": 42,
    "preview": "{\n  \"myfunc\":\"result\",\n  \"key\":\"value\"\n}\n\n"
  },
  {
    "path": "example/comment/any.get.json",
    "chars": 35,
    "preview": "{\n  \"id\":\"get\",\n  \"key\":\"value\"\n}\n\n"
  },
  {
    "path": "example/comment/any.put.json",
    "chars": 35,
    "preview": "{\n  \"id\":\"put\",\n  \"key\":\"value\"\n}\n\n"
  },
  {
    "path": "example/comment/index.get.json",
    "chars": 38,
    "preview": "{\n  \"index\":\"get\",\n  \"key\":\"value\"\n}\n\n"
  },
  {
    "path": "example/comment/index.post.json",
    "chars": 39,
    "preview": "{\n  \"index\":\"post\",\n  \"key\":\"value\"\n}\n\n"
  },
  {
    "path": "index.js",
    "chars": 287,
    "preview": "\"use strict\";\n\nvar path = require('path')\nvar Canned = require('./lib/canned')\n\nvar canned = function (dir, options) {\n "
  },
  {
    "path": "lib/canned.js",
    "chars": 13008,
    "preview": "\"use strict\";\n\nvar url = require('url')\nvar fs = require('fs')\nvar util = require('util')\nvar Response = require('./resp"
  },
  {
    "path": "lib/lookup.js",
    "chars": 2046,
    "preview": "\"use strict\";\n\n\n/***\n * Given a path and a wildcard string, return a list of paths\n * with every combination possible re"
  },
  {
    "path": "lib/response.js",
    "chars": 2082,
    "preview": "\"use strict\";\nvar fs = require('fs')\n\nfunction Response(content_type, content, statusCode, res, options, custom_headers)"
  },
  {
    "path": "lib/utils.js",
    "chars": 705,
    "preview": "\"use strict\";\n\nvar utils = module.exports = {}\n\nutils.escapeRegexSpecialChars = function (text) {\n  return text.replace("
  },
  {
    "path": "package.json",
    "chars": 695,
    "preview": "{\n  \"name\": \"canned\",\n  \"version\": \"0.3.13\",\n  \"description\": \"serve canned responses to mock an api, based on files in "
  },
  {
    "path": "spec/canned.spec.js",
    "chars": 26107,
    "preview": "\"use strict\";\nvar querystring = require(\"querystring\")\nvar canned = require('../index')\nvar path = require('path')\n\ndesc"
  },
  {
    "path": "spec/lookup.spec.js",
    "chars": 675,
    "preview": "\"use strict\";\n\nvar lookup = require('../lib/lookup')\n\ndescribe('lookup', function () {\n  it('should generate a list of p"
  },
  {
    "path": "spec/test_responses/_201.get.json",
    "chars": 39,
    "preview": "//! statusCode: 201\n{\n  \"created\": 1\n}\n"
  },
  {
    "path": "spec/test_responses/_201_multiple_responses.get.json",
    "chars": 193,
    "preview": "//! statusCode: 201\n//! header: {\"authorization\": \"123\"}\n{\n    \"response\": \"response for 123\"\n}\n\n//! statusCode: 201\n//!"
  },
  {
    "path": "spec/test_responses/_a.get.html",
    "chars": 12,
    "preview": "_a.get.json\n"
  },
  {
    "path": "spec/test_responses/_b.get.txt",
    "chars": 11,
    "preview": "_b.get.txt\n"
  },
  {
    "path": "spec/test_responses/_broken_sanitize.get.json",
    "chars": 363,
    "preview": "{\n  \"whatAmI\": \"I have been copy/pasted into a WYSIWYG editor by your grandma\",\n  \"embedCode\": \"<div class=\\\"nasty-3rd-p"
  },
  {
    "path": "spec/test_responses/_chartest.get.json",
    "chars": 41,
    "preview": "{\n  \"my_url\":\"http://www.mywebsite.com\"\n}"
  },
  {
    "path": "spec/test_responses/_empty.get.json",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "spec/test_responses/_empty_with_headers.get.json",
    "chars": 19,
    "preview": "//! statusCode: 420"
  },
  {
    "path": "spec/test_responses/_invalid_syntax.get.json",
    "chars": 21,
    "preview": "I am not valid JSON!\n"
  },
  {
    "path": "spec/test_responses/_multimatch.get.json.1",
    "chars": 22,
    "preview": "{\n  \"multimatch\": 1\n}\n"
  },
  {
    "path": "spec/test_responses/_multimatch.get.json.2",
    "chars": 22,
    "preview": "{\n  \"multimatch\": 2\n}\n"
  },
  {
    "path": "spec/test_responses/_multimatch_query_param.get.html",
    "chars": 109,
    "preview": "//! params: {\"name\": \"Batman\", \"age\": \"30\"}\nBatman!\n\n//! params: {\"name\": \"Superman\", \"age\": \"30\"}\nSuperman!\n"
  },
  {
    "path": "spec/test_responses/_multiple_custom_header.get.json",
    "chars": 104,
    "preview": "//! customHeader: {\"Header-Key\": \"Header-Content\"}\n//! customHeader: {\"Header-Key2\": \"Header-Content2\"}\n"
  },
  {
    "path": "spec/test_responses/_multiple_get_responses.get.json",
    "chars": 302,
    "preview": "//! params: {\"foo\": \"baz\"}\n{\n    \"response\": \"response for baz\"\n}\n\n//! params: {\"foo\": \"bar\"}\n{\n    \"response\": \"respons"
  },
  {
    "path": "spec/test_responses/_multiple_responses.get.json",
    "chars": 152,
    "preview": "//! header: {\"authorization\": \"abc\"}\n{\n    \"response\": \"response for abc\"\n}\n\n//! header: {\"authorization\": \"123\"}\n{\n    "
  },
  {
    "path": "spec/test_responses/_multiple_responses.post.json",
    "chars": 303,
    "preview": "//! body: {\"email\": \"one@example.com\"}\n{\n    \"response\": \"response for one@example.com\"\n}\n\n//! body: {\"email\": \"two@exam"
  },
  {
    "path": "spec/test_responses/_multiple_responses_text.get.html",
    "chars": 174,
    "preview": "//! header: {\"authorization\": \"abc\"}\nresponse for abc\n\n//! header: {\"authorization\": \"123\"}\nresponse for 123\n\n//! header"
  },
  {
    "path": "spec/test_responses/_multiple_responses_text.post.html",
    "chars": 136,
    "preview": "//! body: {\"email\": \"one@example.com\"}\nresponse for one@example.com\n\n//! body: {\"email\": \"two@example.com\"}\nresponse for"
  },
  {
    "path": "spec/test_responses/_multiple_responses_xml.post.xml",
    "chars": 257,
    "preview": "//! header: {\"action\": \"foo\"}\n<SOAP:Envelope><SOAP:Body><Foo Time=\"2015-01-22T08:30:00.000+05:30\"/></SOAP:Body></SOAP:En"
  },
  {
    "path": "spec/test_responses/_multiple_responses_xml_request_body.post.xml",
    "chars": 73,
    "preview": "//! body: <xml>a</xml>\n<xml>A</xml>\n\n//! body: <xml>b</xml>\n<xml>B</xml>\n"
  },
  {
    "path": "spec/test_responses/_multiple_type.get.csv",
    "chars": 56,
    "preview": "Jane Doe,Professor,(425) 123-4567,http://www.janedoe.com"
  },
  {
    "path": "spec/test_responses/_multiple_type.get.html",
    "chars": 17,
    "preview": "<type>html</type>"
  },
  {
    "path": "spec/test_responses/_multiple_type.get.js",
    "chars": 16,
    "preview": "var type = 'js';"
  },
  {
    "path": "spec/test_responses/_multiple_type.get.json",
    "chars": 19,
    "preview": "{\n\t\"type\": \"json\"\n}"
  },
  {
    "path": "spec/test_responses/_multiple_type.get.jsonld",
    "chars": 176,
    "preview": "{\n  \"@context\": \"http://schema.org/\",\n  \"@type\": \"Person\",\n  \"name\": \"Jane Doe\",\n  \"jobTitle\": \"Professor\",\n  \"telephone"
  },
  {
    "path": "spec/test_responses/_multiple_type.get.nt",
    "chars": 42,
    "preview": "_:b0 <http://schema.org/name> \"Jane Doe\" ."
  },
  {
    "path": "spec/test_responses/_multiple_type.get.txt",
    "chars": 9,
    "preview": "text type"
  },
  {
    "path": "spec/test_responses/_response_with_url_param.post.json",
    "chars": 88,
    "preview": "//! body: {\"url\":\"http://example.com\"}\n{\n    \"response\": \"response for url in param\"\n}\n\n"
  },
  {
    "path": "spec/test_responses/_single_custom_header.get.json",
    "chars": 51,
    "preview": "//! customHeader: {\"Header-Key\": \"Header-Content\"}\n"
  },
  {
    "path": "spec/test_responses/_vendor_type.get.xml",
    "chars": 111,
    "preview": "//! statusCode  :  200   ,contentType  :     \"application/vnd.custom+xml\"   \n<xml>\n\t<created>1</created>\n</xml>"
  },
  {
    "path": "spec/test_responses/d/2/index.get.json",
    "chars": 18,
    "preview": "{\"not-wildcard\":1}"
  },
  {
    "path": "spec/test_responses/d/_commented.get.json",
    "chars": 62,
    "preview": "// to level comment\n{\n  // nested comment\n  \"no\":\"comments\"\n}\n"
  },
  {
    "path": "spec/test_responses/d/any/_bar.get.json",
    "chars": 35,
    "preview": "{\n  \"wildcard\": \"named_response\"\n}\n"
  },
  {
    "path": "spec/test_responses/d/any/index.get.json",
    "chars": 20,
    "preview": "{\n  \"wildcard\": 1\n}\n"
  },
  {
    "path": "spec/test_responses/d/any.get.html",
    "chars": 15,
    "preview": "d/any.get.json\n"
  },
  {
    "path": "spec/test_responses/d/e/index.get.html",
    "chars": 18,
    "preview": "d/e/index.get.html"
  },
  {
    "path": "spec/test_responses/d/index.get.html",
    "chars": 17,
    "preview": "d/index.get.json\n"
  },
  {
    "path": "spec/test_responses/d/multiple-accept-types/index.get.json",
    "chars": 20,
    "preview": "{\n  \"type\":\"json\"\n}\n"
  },
  {
    "path": "spec/test_responses/index.get.html",
    "chars": 15,
    "preview": "index.get.json\n"
  },
  {
    "path": "test/assert.sh",
    "chars": 4553,
    "preview": "#!/bin/bash\n# assert.sh 1.0 - bash unit testing framework\n# Copyright (C) 2009, 2010, 2011, 2012 Robert Lehmann\n#\n# http"
  },
  {
    "path": "test/bin_test.sh",
    "chars": 503,
    "preview": "#!/bin/bash\n. assert.sh\n\n# start canned on port\n../bin/canned -p 8765 ../example &\nCPID=$!\nsleep 1\nassert \"lsof -i:8765 "
  }
]

About this extraction

This page contains the full source code of the sideshowcoder/canned GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 61 files (75.5 KB), approximately 21.6k tokens, and a symbol index with 8 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!