зеркало из https://github.com/mozilla/CSOL-site.git
623 строки
18 KiB
JavaScript
623 строки
18 KiB
JavaScript
const tap = require('tap');
|
|
const test = tap.test;
|
|
const path = require('path');
|
|
const _ = require('underscore');
|
|
const express = require('express');
|
|
const sinon = require('sinon');
|
|
|
|
/* Using injectr on a trial basis to inject request module,
|
|
then stubbing/mocking it with sinon to return remote API
|
|
dummy data. */
|
|
const injectr = require('injectr');
|
|
var request = require('request');
|
|
const Api = injectr(
|
|
path.join(__dirname, '../api.js'),
|
|
{
|
|
request: request
|
|
},
|
|
{
|
|
console: console /* api doesn't seem to have a console unless I do this? */
|
|
}
|
|
);
|
|
|
|
function fakeRequest(func, config, callback) {
|
|
if (typeof config === 'function') {
|
|
callback = config;
|
|
config = {};
|
|
}
|
|
|
|
// TODO: translating config object to req data is sloppy
|
|
var req = _.extend(
|
|
{ __proto__: express.request,
|
|
headers: {} },
|
|
config
|
|
);
|
|
req.headers = {'x-requested-with': config.xhr ? 'XmlHttpRequest' : 'Whatever'};
|
|
var res = { __proto__: express.response };
|
|
sinon.stub(res, 'json');
|
|
var next = sinon.stub();
|
|
|
|
func(req, res, next);
|
|
callback(req, res, next);
|
|
}
|
|
|
|
const ORIGIN = 'http://example.org';
|
|
|
|
test('Api()', function(t) {
|
|
|
|
t.test('requires origin', function(t) {
|
|
t.throws(function(){ new Api(); }, 'throws without');
|
|
t.end();
|
|
});
|
|
|
|
t.test('uses method names', function(t) {
|
|
var stubA = sinon.stub();
|
|
var api = new Api(ORIGIN, {
|
|
methodA: stubA,
|
|
methodB: sinon.stub()
|
|
});
|
|
t.isa(api.methodA, 'function');
|
|
t.isa(api.methodB, 'function');
|
|
t.end();
|
|
});
|
|
|
|
t.test('wrapped method context has remote helpers', function(t) {
|
|
var requestMock = sinon.mock(request);
|
|
var get = requestMock.expects('get');
|
|
var api = new Api(ORIGIN, {
|
|
method: function(){ this.get() },
|
|
});
|
|
api.method();
|
|
t.ok(get.calledOnce, 'this.get');
|
|
t.end();
|
|
requestMock.restore();
|
|
});
|
|
|
|
t.test('calls wrapped method with query and callback', function(t) {
|
|
var method = sinon.stub();
|
|
var api = new Api(ORIGIN, {
|
|
method: method
|
|
});
|
|
api.method();
|
|
t.ok(method.calledOnce, 'wraps');
|
|
t.ok(method.calledWith(sinon.match({}), sinon.match.func), 'args');
|
|
t.end();
|
|
});
|
|
|
|
t.test('string arg becomes id', function(t) {
|
|
var method = sinon.stub();
|
|
var api = new Api(ORIGIN, {
|
|
method: method
|
|
});
|
|
api.method('foo');
|
|
t.ok(method.calledWith(sinon.match({ id: 'foo' })), 'query arg');
|
|
t.end();
|
|
});
|
|
|
|
t.test('query object passed through', function(t) {
|
|
var method = sinon.stub();
|
|
var api = new Api(ORIGIN, {
|
|
method: method
|
|
});
|
|
api.method({ some: 'data' });
|
|
t.ok(method.calledWith(sinon.match({ some: 'data' })), 'query arg');
|
|
t.end();
|
|
});
|
|
|
|
t.test('data passed through to wrapped method callback', function(t) {
|
|
var method = sinon.stub().callsArgWith(1, null, 'data');
|
|
var callback = sinon.stub();
|
|
var api = new Api(ORIGIN, {
|
|
method: method
|
|
});
|
|
api.method(callback);
|
|
t.ok(callback.calledWith(null, 'data'), 'data');
|
|
t.end();
|
|
});
|
|
|
|
t.test('err and message passed through to wrapped method callback', function(t) {
|
|
var method = sinon.stub().callsArgWith(1, 'err', 'msg');
|
|
var callback = sinon.stub();
|
|
var api = new Api(ORIGIN, {
|
|
method: method
|
|
});
|
|
api.method(callback);
|
|
var args = callback.getCall(0).args;
|
|
t.same(args[0], 'err', 'err');
|
|
t.same(args[1], 'msg', 'message');
|
|
t.end();
|
|
});
|
|
|
|
t.test('err and data passed through to wrapped method callback', function(t) {
|
|
var method = sinon.stub().callsArgWith(1, 'err', { some: 'data' });
|
|
var callback = sinon.stub();
|
|
var api = new Api(ORIGIN, {
|
|
method: method
|
|
});
|
|
api.method(callback);
|
|
var args = callback.getCall(0).args;
|
|
t.same(args[0], 'err', 'err');
|
|
t.same(args[1], { some: 'data' }, 'data');
|
|
t.end();
|
|
});
|
|
|
|
});
|
|
|
|
test('api.middleware(method)', function(t) {
|
|
|
|
t.test('invokes method', function(t) {
|
|
var method = sinon.stub();
|
|
var api = new Api(ORIGIN, {
|
|
method: method
|
|
});
|
|
fakeRequest(
|
|
api.middleware(api.method),
|
|
function(req, res, next) {
|
|
t.ok(method.calledOnce, 'method called');
|
|
t.ok(method.calledWith(sinon.match.object, sinon.match.func), 'arg types');
|
|
t.end();
|
|
}
|
|
);
|
|
});
|
|
|
|
t.test('string method lookup', function(t) {
|
|
var method = sinon.stub();
|
|
var api = new Api(ORIGIN, {
|
|
method: method
|
|
});
|
|
fakeRequest(
|
|
api.middleware('method'),
|
|
function(req, res, next) {
|
|
t.ok(method.calledOnce, 'method called');
|
|
t.ok(method.calledWith(sinon.match.object, sinon.match.func), 'arg types');
|
|
t.end();
|
|
}
|
|
);
|
|
});
|
|
|
|
t.test('query object contains request params', function(t) {
|
|
var method = sinon.stub();
|
|
var api = new Api(ORIGIN, {
|
|
method: method
|
|
});
|
|
fakeRequest(
|
|
api.middleware('method'),
|
|
{
|
|
params: { a: 1 },
|
|
body: { b: 1 },
|
|
query: { c: 1 }
|
|
},
|
|
function(req, res, next) {
|
|
t.ok(method.calledWith(sinon.match({ a: 1, b: 1, c: 1 })), 'query object ok');
|
|
t.end();
|
|
}
|
|
);
|
|
});
|
|
|
|
t.test('query object contains default params', function(t) {
|
|
var method = sinon.stub();
|
|
var api = new Api(ORIGIN, {
|
|
method: method
|
|
});
|
|
fakeRequest(
|
|
api.middleware('method', { default: 'hooray' }),
|
|
function(req, res, next) {
|
|
t.ok(method.calledWith(sinon.match({ default: 'hooray' })), 'query object ok');
|
|
t.end();
|
|
}
|
|
);
|
|
});
|
|
|
|
t.test('callback(null, data) attaches data to request and calls next()', function(t) {
|
|
var method = sinon.stub().callsArgWith(1, null, { result: 1 });
|
|
var api = new Api(ORIGIN, {
|
|
method: method
|
|
});
|
|
fakeRequest(
|
|
api.middleware('method'),
|
|
function(req, res, next) {
|
|
console.log(req);
|
|
t.same(req.remote, { result: 1 }, 'req.remote');
|
|
t.ok(next.calledOnce, 'next');
|
|
t.end();
|
|
}
|
|
);
|
|
});
|
|
|
|
t.test('callback(null, data) calls response.json with data for xhr', function(t) {
|
|
var method = sinon.stub().callsArgWith(1, null, { result: 1 });
|
|
var api = new Api(ORIGIN, {
|
|
method: method
|
|
});
|
|
fakeRequest(
|
|
api.middleware('method'),
|
|
{
|
|
xhr: true
|
|
},
|
|
function(req, res, next) {
|
|
t.ok(res.json.calledWithMatch({ result: 1 }), 'json called');
|
|
t.notOk(next.callCount, 'next() not');
|
|
t.end();
|
|
}
|
|
);
|
|
});
|
|
|
|
t.test('callback(err) calls next() with err', function(t) {
|
|
var method = sinon.stub().callsArgWith(1, 500);
|
|
var api = new Api(ORIGIN, {
|
|
method: method
|
|
});
|
|
fakeRequest(
|
|
api.middleware('method'),
|
|
function(req, res, next) {
|
|
t.ok(next.calledOnce, 'next called');
|
|
var arg = next.getCall(0).args[0];
|
|
t.same(arg, 500, 'error');
|
|
t.end();
|
|
}
|
|
);
|
|
});
|
|
|
|
t.test('callback(err, msg) calls next() with err', function(t) {
|
|
var method = sinon.stub().callsArgWith(1, 500, 'msg');
|
|
var api = new Api(ORIGIN, {
|
|
method: method
|
|
});
|
|
fakeRequest(
|
|
api.middleware('method'),
|
|
function(req, res, next) {
|
|
t.ok(next.calledOnce, 'next called');
|
|
var args = next.getCall(0).args;
|
|
t.same(args[0], 500, 'error');
|
|
t.notOk(args[1], 'no data');
|
|
t.end();
|
|
}
|
|
);
|
|
});
|
|
|
|
t.test('callback(err, obj) calls next() with err', function(t) {
|
|
var method = sinon.stub().callsArgWith(1, 500, { some: 'data' });
|
|
var api = new Api(ORIGIN, {
|
|
method: method
|
|
});
|
|
fakeRequest(
|
|
api.middleware('method'),
|
|
function(req, res, next) {
|
|
t.ok(next.calledOnce, 'next called');
|
|
var args = next.getCall(0).args;
|
|
t.same(args[0], 500, 'error');
|
|
t.notOk(args[1], 'no data');
|
|
t.end();
|
|
}
|
|
);
|
|
});
|
|
|
|
t.test('callback(err) calls response.json for xhr', function(t) {
|
|
/* There's an implicit requirement that errors coming from
|
|
api methods must be objects with a status attribute. */
|
|
/* TODO: either add error helpers to the api module that
|
|
enforce the requirements, or loosen them. */
|
|
var err = { code: 500, status: 'error' };
|
|
var method = sinon.stub().callsArgWith(1, err);
|
|
var api = new Api('ORIGIN', {
|
|
method: method
|
|
});
|
|
fakeRequest(
|
|
api.middleware('method'),
|
|
{
|
|
xhr: true
|
|
},
|
|
function(req, res, next) {
|
|
t.ok(res.json.calledOnce);
|
|
t.notOk(next.callCount, 'next() not');
|
|
var arg = res.json.getCall(0).args[0];
|
|
t.similar(arg, { status: 'error' }, 'error');
|
|
t.end();
|
|
}
|
|
);
|
|
});
|
|
|
|
});
|
|
|
|
/* Implicitly testing the wrapped request methods just by
|
|
testing get, which is a bit lame but quicker. */
|
|
/* TODO: also test the underlying api.remote()? */
|
|
test('api.get', function(t) {
|
|
var api = new Api(ORIGIN);
|
|
|
|
t.test('calls request.get under the hood', function(t) {
|
|
var requestMock = sinon.mock(request);
|
|
var get = requestMock.expects('get');
|
|
|
|
api.get('/foo', function(){});
|
|
t.ok(get.calledOnce, 'called');
|
|
t.ok(get.calledWith(sinon.match(ORIGIN + '/foo')), 'with origin and endpoint');
|
|
requestMock.restore();
|
|
t.end();
|
|
});
|
|
|
|
t.test('passes optional params through', function(t) {
|
|
var requestMock = sinon.mock(request);
|
|
var get = requestMock.expects('get');
|
|
|
|
api.get('/foo', { some: 'params' }, function(){});
|
|
t.ok(get.calledOnce, 'called');
|
|
console.log(get.args);
|
|
t.ok(get.calledWith(
|
|
sinon.match(ORIGIN + '/foo'),
|
|
sinon.match({ some: 'params' })
|
|
), 'with params too');
|
|
requestMock.restore();
|
|
t.end();
|
|
});
|
|
|
|
t.test('leading slashes don\'t indicate absolute path', function(t) {
|
|
const WITH_PATH = 'http://example.org/base/';
|
|
var api = new Api(WITH_PATH);
|
|
var requestMock = sinon.mock(request);
|
|
var get = requestMock.expects('get').twice();
|
|
|
|
api.get('/foo/bar', function(){});
|
|
api.get('foo/bar', function(){});
|
|
t.ok(get.alwaysCalledWith(sinon.match(WITH_PATH + 'foo/bar')), 'with origin/endpoint');
|
|
requestMock.restore();
|
|
t.end();
|
|
});
|
|
|
|
t.test('calls against different origins don\'t collide', function(t) {
|
|
const ANOTHER = 'http://another.org';
|
|
var api2 = new Api(ANOTHER);
|
|
|
|
var requestMock = sinon.mock(request);
|
|
var get = requestMock.expects('get').twice();
|
|
|
|
api.get('/foo', function(){});
|
|
api2.get('/bar', function(){});
|
|
t.same(get.getCall(0).args[0], ORIGIN + '/foo', 'api');
|
|
t.same(get.getCall(1).args[0], ANOTHER + '/bar', 'api2');
|
|
requestMock.restore();
|
|
t.end();
|
|
});
|
|
|
|
t.test('calls callback with 500 if request.get errors', function(t) {
|
|
var requestMock = sinon.mock(request);
|
|
var get = requestMock.expects('get').callsArgWith(2, 'Error');
|
|
|
|
api.get('/foo', function(err, data){
|
|
t.similar(err, { code: 500, name: 'Internal', message: 'Error' }, 'error');
|
|
t.notOk(data, 'no data');
|
|
requestMock.restore();
|
|
t.end();
|
|
});
|
|
});
|
|
|
|
t.test('calls callback with 500 if request.get response is not 200', function(t) {
|
|
var requestMock = sinon.mock(request);
|
|
var get = requestMock.expects('get').callsArgWith(2, null, { statusCode: 404 });
|
|
|
|
api.get('/foo', function(err, data){
|
|
t.similar(err, { code: 404, name: 'NotFound' }, 'error');
|
|
t.notOk(data, 'no data');
|
|
requestMock.restore();
|
|
t.end();
|
|
});
|
|
});
|
|
|
|
t.test('calls callback with 500 if request.get response is not json', function(t) {
|
|
var requestMock = sinon.mock(request);
|
|
var get = requestMock.expects('get')
|
|
.callsArgWith(2, null, { statusCode: 200 }, "NOPE!");
|
|
|
|
api.get('/foo', function(err, data){
|
|
t.similar(err, { code: 500, name: 'Internal', message: 'Unexpected token N' }, 'error');
|
|
t.notOk(data, 'no data');
|
|
requestMock.restore();
|
|
t.end();
|
|
});
|
|
});
|
|
|
|
t.test('calls callback with 500 if request.get json response status not "ok"', function(t) {
|
|
var response = {
|
|
status: 'NOPE',
|
|
reason: 'It broke.'
|
|
};
|
|
|
|
var requestMock = sinon.mock(request);
|
|
var get = requestMock.expects('get')
|
|
.callsArgWith(2, null, { statusCode: 200 }, JSON.stringify(response));
|
|
|
|
api.get('/foo', function(err, data){
|
|
t.similar(err, { code: 500, name: 'Internal', message: 'It broke.' }, 'error');
|
|
t.similar(data, response, 'data');
|
|
requestMock.restore();
|
|
t.end();
|
|
});
|
|
});
|
|
|
|
t.test('stringified JSON data gets parsed', function(t) {
|
|
var response = {
|
|
status: 'ok',
|
|
data: 'Stuff.'
|
|
};
|
|
|
|
var requestMock = sinon.mock(request);
|
|
var get = requestMock.expects('get')
|
|
.callsArgWith(2, null, { statusCode: 200 }, JSON.stringify(response));
|
|
|
|
api.get('/foo', function(err, data){
|
|
t.notOk(err, 'no error');
|
|
t.same(data, { status: 'ok', data: 'Stuff.' }, 'data');
|
|
requestMock.restore();
|
|
t.end();
|
|
});
|
|
});
|
|
|
|
/* request parses for you if you post with { json: ... } */
|
|
t.test('pre-parsed data is passed through', function(t) {
|
|
var response = {
|
|
status: 'ok',
|
|
data: 'Stuff.'
|
|
};
|
|
|
|
var requestMock = sinon.mock(request);
|
|
var get = requestMock.expects('get')
|
|
.callsArgWith(2, null, { statusCode: 200 }, response);
|
|
|
|
api.get('/foo', function(err, data){
|
|
t.notOk(err, 'no error');
|
|
t.same(data, { status: 'ok', data: 'Stuff.' }, 'data');
|
|
requestMock.restore();
|
|
t.end();
|
|
});
|
|
});
|
|
|
|
});
|
|
|
|
test('paginate', function(t) {
|
|
|
|
t.test('wrapped method context has remote helpers', function(t) {
|
|
var requestMock = sinon.mock(request);
|
|
var get = requestMock.expects('get');
|
|
var api = new Api(ORIGIN, {
|
|
method: {
|
|
func: function(){ this.get() },
|
|
paginate: true
|
|
}
|
|
});
|
|
api.method();
|
|
t.ok(get.calledOnce, 'this.get');
|
|
t.end();
|
|
requestMock.restore();
|
|
});
|
|
|
|
t.test('page and pageSize defaults', function(t) {
|
|
var method = sinon.stub();
|
|
var api = new Api(ORIGIN, {
|
|
method: { func: method, paginate: true }
|
|
});
|
|
var callback = sinon.stub();
|
|
api.method({}, callback);
|
|
t.similar(method.getCall(0).args[0], { page: 1, pageSize: 12 }, 'defaults');
|
|
t.end();
|
|
});
|
|
|
|
t.test('intercepts bad pageSize', function(t) {
|
|
var method = sinon.stub();
|
|
var api = new Api(ORIGIN, {
|
|
method: { func: method, paginate: true }
|
|
});
|
|
var callback = sinon.stub();
|
|
api.method({ pageSize: -1 }, callback);
|
|
var args = callback.getCall(0).args;
|
|
t.same(args[0].name, 'BadRequest', 'err');
|
|
t.notOk(args[1], 'data');
|
|
t.end();
|
|
});
|
|
|
|
t.test('intercepts bad page', function(t) {
|
|
var method = sinon.stub();
|
|
var api = new Api(ORIGIN, {
|
|
method: { func: method, paginate: true }
|
|
});
|
|
var callback = sinon.stub();
|
|
api.method({ page: -1 }, callback);
|
|
var args = callback.getCall(0).args;
|
|
t.same(args[0].name, 'BadRequest', 'err');
|
|
t.notOk(args[1], 'data');
|
|
t.end();
|
|
});
|
|
|
|
t.test('error on non-pageable data', function(t) {
|
|
var method = sinon.stub().callsArgWith(1, null, { data: 1 });;
|
|
var api = new Api(ORIGIN, {
|
|
method: { func: method, paginate: true }
|
|
});
|
|
var callback = sinon.stub();
|
|
api.method(callback);
|
|
var args = callback.getCall(0).args;
|
|
t.same(args[0].name, 'BadGateway', 'error');
|
|
t.same(args[1], { data: 1 }, 'data');
|
|
t.end();
|
|
});
|
|
|
|
t.test('don\'t error out on no data', function(t) {
|
|
var method = sinon.stub().callsArgWith(1, null, { data: [] });;
|
|
var api = new Api(ORIGIN, {
|
|
method: { func: method, paginate: true }
|
|
});
|
|
var callback = sinon.stub();
|
|
api.method(callback);
|
|
var args = callback.getCall(0).args;
|
|
t.notOk(args[0], 'no error');
|
|
t.similar(args[1], { data: [], pages: 0 }, 'data');
|
|
t.end();
|
|
});
|
|
|
|
t.test('calls paginated method with query and callback', function(t) {
|
|
var method = sinon.stub();
|
|
var api = new Api(ORIGIN, {
|
|
method: { func: method, paginate: true }
|
|
});
|
|
api.method({ pageSize: 5, page: 1 });
|
|
t.ok(method.calledOnce, 'wraps');
|
|
t.ok(method.calledWith(sinon.match({}), sinon.match.func), 'args');
|
|
t.end();
|
|
});
|
|
|
|
t.test('paginates data.data from wrapped method', function(t) {
|
|
var method = sinon.stub().callsArgWith(1, null, { data: [1, 2, 3, 4, 5] });;
|
|
var api = new Api(ORIGIN, {
|
|
method: { func: method, paginate: true }
|
|
});
|
|
var callback = sinon.stub();
|
|
api.method({ pageSize: 2, page: 1 }, callback);
|
|
t.ok(callback.calledOnce, 'callback');
|
|
var args = callback.getCall(0).args;
|
|
t.notOk(args[0], 'no error');
|
|
t.similar(args[1], { data: [1, 2] }, 'data paginated');
|
|
t.end();
|
|
});
|
|
|
|
t.test('paginates data.key from wrapped method if key provided', function(t) {
|
|
var method = sinon.stub().callsArgWith(1, null, { foo: [1, 2, 3, 4, 5] });;
|
|
var api = new Api(ORIGIN, {
|
|
method: { func: method, paginate: true, key: 'foo' }
|
|
});
|
|
var callback = sinon.stub();
|
|
api.method({ pageSize: 2, page: 1 }, callback);
|
|
t.ok(callback.calledOnce, 'callback');
|
|
var args = callback.getCall(0).args;
|
|
t.notOk(args[0], 'no error');
|
|
t.similar(args[1], { foo: [1, 2] }, 'foo paginated');
|
|
t.end();
|
|
});
|
|
|
|
t.test('page count and page passed along', function(t) {
|
|
var method = sinon.stub().callsArgWith(1, null, { data: [1, 2, 3, 4, 5] });;
|
|
var api = new Api(ORIGIN, {
|
|
method: { func: method, paginate: true }
|
|
});
|
|
var callback = sinon.stub();
|
|
api.method({ pageSize: 2, page: 1 }, callback);
|
|
t.ok(callback.calledOnce, 'callback');
|
|
var args = callback.getCall(0).args;
|
|
t.notOk(args[0], 'no error');
|
|
t.similar(args[1], { page: 1, pages: 3 }, 'page/pages');
|
|
t.end();
|
|
});
|
|
|
|
t.test('intercepts page out of range', function(t) {
|
|
var method = sinon.stub().callsArgWith(1, null, { data: [1, 2, 3, 4, 5] });;
|
|
var api = new Api(ORIGIN, {
|
|
method: { func: method, paginate: true }
|
|
});
|
|
var callback = sinon.stub();
|
|
api.method({ pageSize: 2, page: 5 }, callback);
|
|
t.ok(callback.calledOnce, 'callback');
|
|
var args = callback.getCall(0).args;
|
|
t.same(args[0].name, 'NotFound', 'error');
|
|
t.same(args[1], { page: 5, pages: 3 }, 'data');
|
|
t.end();
|
|
});
|
|
});
|