<|start_filename|>package.json<|end_filename|>
{
  "name": "chimneypot",
  "version": "1.0.0",
  "description": "Lightweight git webhook handler",
  "main": "index.js",
  "scripts": {
    "test": "node_modules/istanbul/lib/cli.js cover node_modules/nodeunit/bin/nodeunit"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/Jameskmonger/chimneypot.git"
  },
  "author": "<NAME> <<EMAIL>> (http://jamesmonger.com)",
  "license": "Unlicense",
  "bugs": {
    "url": "https://github.com/Jameskmonger/chimneypot/issues"
  },
  "homepage": "https://github.com/Jameskmonger/chimneypot",
  "dependencies": {
    "github-webhook-handler": "^0.5.0"
  },
  "devDependencies": {
    "istanbul": "^0.4.2",
    "nodeunit": "^0.9.1",
    "proxyquire": "^1.7.4"
  }
}


<|start_filename|>test/listening.js<|end_filename|>
// Stub the http library to prevent servers from actually being setup
var proxyquire =  require('proxyquire');
var httpStub = {
    createServer: function (cb) {
        return {
            listen: function () {},
            close: function () {}
        };
    }
};
var chimneypot = proxyquire('../index', {
    'http': httpStub
});


var c;

exports.setUp = function (cb) {
    c = new chimneypot({
        port: 8888,
        path: '/',
        secret: 'spooky'
    });
    cb();
}

exports.tearDown = function (cb) {
    c.kill();
    cb();
}

exports.testListenCalls = function (test) {
    test.throws(function () {
        c.listen();
    }, Error, 'Listen called before route throws an error.');

    c.route('push', function(){});

    c.listen();

    test.ok(c.server, 'Should have setup a server object');

    test.throws(function () {
        c.route('push', function(){});
    }, Error, 'Trying to add routes after listening throws an error.');

    test.done();
};





<|start_filename|>test/callbacks.js<|end_filename|>
var proxyquire =  require('proxyquire');
var listenerMethods = {};
var hookHandlerStub = function() {
    function mock (req, res, cb) {}

    mock.on = function (eventName, eventHandler) {
        listenerMethods[eventName] = eventHandler;
    };

    return mock;
};
var httpStub = {
    createServer: function (cb) {
        return {
            listen: function (port, cb) {cb();},
            close: function () {}
        };
    }
};
var chimneypot = proxyquire('../index', {
    'http': httpStub,
    'github-webhook-handler': hookHandlerStub
});


var c;

exports.setUp = function (cb) {
    c = new chimneypot({
        port: 8888,
        path: '/',
        secret: 'spooky'
    });
    cb();
}

exports.tearDown = function (cb) {
    c.kill();
    cb();
}

exports.testMultipleHandlers = function (test) {

    var pushCount = 0;
    var incrementPush = function () {
        pushCount++;
    };

    c.route('push', incrementPush);
    c.route('push', incrementPush);
    c.route('push', incrementPush);

    c.listen();

    listenerMethods['push']();

    test.ok(pushCount === 3, 'Expected 3 push events. Received ' + pushCount);


    test.done();
};

exports.testMultipleCallbacks = function (test) {

    var pushCount = 0;
    var incrementPush = function () {
        pushCount++;
    };

    c.route('push', incrementPush);
    c.route('push', incrementPush);
    c.route('push', incrementPush);

    c.listen();

    listenerMethods['push']();
    listenerMethods['push']();
    listenerMethods['push']();

    test.ok(pushCount === 9, 'Expected 9 push events. Received ' + pushCount);


    test.done();
};

exports.testMultipleEventsAndHandlers = function (test) {
    var pushCount = 0;
    var deleteCount = 0;
    var incrementPush = function () {
        pushCount++;
    };
    var incrementDelete = function () {
        deleteCount++;
    };

    c.route('push', incrementPush);
    c.route('delete', incrementDelete);
    c.route('push', incrementPush);
    c.route('delete', incrementDelete);

    c.listen();

    listenerMethods['push']();
    listenerMethods['delete']();

    test.ok(pushCount === 2, 'Expected 2 push events. Received ' + pushCount);
    test.ok(deleteCount === 2, 'Expected 2 delete events. Received ' + deleteCount);


    test.done();
};


<|start_filename|>index.js<|end_filename|>
var http = require('http');
var webhookHandler = require('github-webhook-handler');

module.exports = (function() {
  function chimneypot(opts) {
    if (!opts || !isOptionsValid(opts)) {
      throw new Error("Required options: port, path, secret");
    }

    this.options = {
      port: opts.port,
      path: opts.path,
      secret: opts.secret
    };

    this.routeCount = 0;
    this.routes = {};
  }

  function isOptionsValid(opts) {
    if (opts.port === undefined || opts.path === undefined || opts.secret === undefined) {
      return false;
    }

    // Non numbers and non-integers
    if (Number(opts.port) !== opts.port || opts.port % 1 !== 0) {
      return false;
    }

    return true;
  }

  function route(path, callback) {
    if (this.server !== undefined) {
      throw new Error("Route cannot be applied while the server is listening");
    }

    if (typeof callback !== 'function') {
      throw new Error("Route listeners must be functions.");
    }

    this.routeCount++;

    if (this.routes[path] === undefined) {
      this.routes[path] = [];
    }

    this.routes[path][this.routes[path].length + 1] = callback;
  }

  function listen() {
    if (this.routeCount === 0) {
      throw new Error("Routes must be applied before listening.");
    }

    var handler = webhookHandler({ path: this.options.path, secret: this.options.secret });

    this.server = http.createServer(function (req, res) {
      handler(req, res, function (err) {
        res.statusCode = 404;
        res.end('no such location');
      });
    });

    this.server.listen(this.options.port, function() {
      console.log("listening on *:" + this.options.port);
    }.bind(this));

    for (var path in this.routes) {
      (function (path) {
        handler.on(path, function(data) {
          for (var c in this.routes[path]) {
            this.routes[path][c](data);
          }
        }.bind(this));
      }).bind(this)(path);
    }
  }

  function kill() {
    if (this.server !== undefined) {
      this.server.close();
    }

    this.server = undefined;
    console.log("Server killed");
  }

  chimneypot.prototype = {
    constructor: chimneypot,

    route: route,

    listen: listen,

    kill: kill
  };

  return chimneypot;
})();


<|start_filename|>test/constructors.js<|end_filename|>
var chimneypot = require('../index.js');

exports.testEmptyConstructor = function (test) {
    test.throws(function () {
        new chimneypot();
    }, Error, 'Empty constructor throws an error.');
    test.done();
};

exports.testMissingPort = function (test) {
    test.throws(function () {
        new chimneypot({
            path: '/',
            secret: 'spooky'
        });
    }, Error, 'Missing port option throws an error.');
    test.done();
};

exports.testMissingPath = function (test) {
    test.throws(function () {
        new chimneypot({
            port: 1996,
            secret: 'spooky'
        });
    }, Error, 'Missing path option throws an error.');
    test.done();
};

exports.testMissingSecret = function (test) {
    test.throws(function () {
        new chimneypot({
            port: 1996,
            path: '/'
        });
    }, Error, 'Missing secret option throws an error.');
    test.done();
};

exports.testStringPort = function (test) {
    test.throws(function () {
        new chimneypot({
            port: 'wrong',
            path: '/',
            secret: 'spooky'
        });
    }, Error, 'Port as a string throws an error.');
    test.done();
};

exports.testPortAsArray = function (test) {
    test.throws(function () {
        new chimneypot({
            port: [],
            path: '/',
            secret: 'spooky'
        });
    }, Error, 'Port as an array throws an error.');
    test.done();
};

exports.testPortAsObject = function (test) {
    test.throws(function () {
        new chimneypot({
            port: {},
            path: '/',
            secret: 'spooky'
        });
    }, Error, 'Port as an object throws an error.');
    test.done();
};

exports.testPortAsFloat = function (test) {
    test.throws(function () {
        new chimneypot({
            port: 50.5,
            path: '/',
            secret: 'spooky'
        });
    }, Error, 'Port as a float throws an error.');
    test.done();
};

exports.testInjection = function (test) {
    chimneypot.injectHookHandler
    var c = new chimneypot({
        port: 50,
        path: '/',
        secret: 'spooky'
    });



    test.done();
};

exports.testPortAsBoolean = function (test) {
    test.throws(function () {
        new chimneypot({
            port: false,
            path: '/',
            secret: 'spooky'
        });
    }, Error, 'Port as a false boolean throws an error.');

    test.throws(function () {
        new chimneypot({
            port: true,
            path: '/',
            secret: 'spooky'
        });
    }, Error, 'Port as a true boolean throws an error.');

    test.done();
};



<|start_filename|>test/routing.js<|end_filename|>
var chimneypot = require('../index.js');

var c;

exports.setUp = function (cb) {
    c = new chimneypot({
        port: 8888,
        path: '/',
        secret: 'spooky'
    });
    cb();
}

exports.tearDown = function (cb) {
    c.kill();
    cb();
}

exports.testMultipleRoutesToSameEvent = function (test) {
    c.route('push', function(){});
    c.route('push', function(){});

    test.ok(c.routeCount === 2, 'Both routes should be setup.');

    c.route('delete', function(){});
    c.route('delete', function(){});

    test.ok(c.routeCount === 4, 'All routes should be setup.');

    test.done();
};

exports.testNonFunctionRoute = function (test) {
    test.throws(function () {
        c.route('push', 'monkey');
    }, Error, 'Trying to route to a string throws an error.');

    test.throws(function () {
        c.route('push', 123);
    }, Error, 'Trying to route to a number throws an error.');

    test.throws(function () {
        c.route('push', {name: 'monkey'});
    }, Error, 'Trying to route to an object throws an error.');

    test.done();
};


