Add some basic tests for implemented /account/data handlers
This commit is contained in:
parent
8b1b501bbd
commit
b76869e2d2
|
|
@ -1,6 +1,6 @@
|
||||||
import { GET, POST, PATCH, DELETE } from '@calzoneman/express-babel-decorators';
|
import { GET, POST, PATCH, DELETE } from '@calzoneman/express-babel-decorators';
|
||||||
import { CSRFError, InvalidRequestError } from '../../../errors';
|
import { CSRFError, InvalidRequestError } from '../../../errors';
|
||||||
import { verify as csrfVerify } from '../../csrf';
|
import Promise from 'bluebird';
|
||||||
|
|
||||||
const LOGGER = require('@calzoneman/jsli')('AccountDataRoute');
|
const LOGGER = require('@calzoneman/jsli')('AccountDataRoute');
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@ function checkAcceptsJSON(req, res) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function authorize(req, res) {
|
async function authorize(req, res, csrfVerify, verifySessionAsync) {
|
||||||
if (!req.signedCookies || !req.signedCookies.auth) {
|
if (!req.signedCookies || !req.signedCookies.auth) {
|
||||||
res.status(401).json({
|
res.status(401).json({
|
||||||
error: 'Authorization required'
|
error: 'Authorization required'
|
||||||
|
|
@ -38,7 +38,23 @@ async function authorize(req, res) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: verify session
|
try {
|
||||||
|
const user = await verifySessionAsync(req.signedCookies.auth);
|
||||||
|
|
||||||
|
if (user.name !== req.params.user) {
|
||||||
|
res.status(403).json({
|
||||||
|
error: 'Session username does not match'
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(403).json({
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -59,15 +75,17 @@ function reportError(req, res, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountDataRoute {
|
class AccountDataRoute {
|
||||||
constructor(accountDB, channelDB) {
|
constructor(accountDB, channelDB, csrfVerify, verifySessionAsync) {
|
||||||
this.accountDB = accountDB;
|
this.accountDB = accountDB;
|
||||||
this.channelDB = channelDB;
|
this.channelDB = channelDB;
|
||||||
|
this.csrfVerify = csrfVerify;
|
||||||
|
this.verifySessionAsync = verifySessionAsync;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET('/account/data/:user')
|
@GET('/account/data/:user')
|
||||||
async getAccount(req, res) {
|
async getAccount(req, res) {
|
||||||
if (!checkAcceptsJSON(req, res)) return;
|
if (!checkAcceptsJSON(req, res)) return;
|
||||||
if (!await authorize(req, res)) return;
|
if (!await authorize(req, res, this.csrfVerify, this.verifySessionAsync)) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await this.accountDB.getByName(req.params.user);
|
const user = await this.accountDB.getByName(req.params.user);
|
||||||
|
|
@ -94,7 +112,7 @@ class AccountDataRoute {
|
||||||
@PATCH('/account/data/:user')
|
@PATCH('/account/data/:user')
|
||||||
async updateAccount(req, res) {
|
async updateAccount(req, res) {
|
||||||
if (!checkAcceptsJSON(req, res)) return;
|
if (!checkAcceptsJSON(req, res)) return;
|
||||||
if (!await authorize(req, res)) return;
|
if (!await authorize(req, res, this.csrfVerify, this.verifySessionAsync)) return;
|
||||||
|
|
||||||
res.status(501).json({ error: 'Not implemented' });
|
res.status(501).json({ error: 'Not implemented' });
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +120,7 @@ class AccountDataRoute {
|
||||||
@GET('/account/data/:user/channels')
|
@GET('/account/data/:user/channels')
|
||||||
async listChannels(req, res) {
|
async listChannels(req, res) {
|
||||||
if (!checkAcceptsJSON(req, res)) return;
|
if (!checkAcceptsJSON(req, res)) return;
|
||||||
if (!await authorize(req, res)) return;
|
if (!await authorize(req, res, this.csrfVerify, this.verifySessionAsync)) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const channels = await this.channelDB.listByOwner(req.params.user).map(
|
const channels = await this.channelDB.listByOwner(req.params.user).map(
|
||||||
|
|
@ -124,7 +142,7 @@ class AccountDataRoute {
|
||||||
@POST('/account/data/:user/channels/:name')
|
@POST('/account/data/:user/channels/:name')
|
||||||
async createChannel(req, res) {
|
async createChannel(req, res) {
|
||||||
if (!checkAcceptsJSON(req, res)) return;
|
if (!checkAcceptsJSON(req, res)) return;
|
||||||
if (!await authorize(req, res)) return;
|
if (!await authorize(req, res, this.csrfVerify, this.verifySessionAsync)) return;
|
||||||
|
|
||||||
res.status(501).json({ error: 'Not implemented' });
|
res.status(501).json({ error: 'Not implemented' });
|
||||||
}
|
}
|
||||||
|
|
@ -132,7 +150,7 @@ class AccountDataRoute {
|
||||||
@DELETE('/account/data/:user/channels/:name')
|
@DELETE('/account/data/:user/channels/:name')
|
||||||
async deleteChannel(req, res) {
|
async deleteChannel(req, res) {
|
||||||
if (!checkAcceptsJSON(req, res)) return;
|
if (!checkAcceptsJSON(req, res)) return;
|
||||||
if (!await authorize(req, res)) return;
|
if (!await authorize(req, res, this.csrfVerify, this.verifySessionAsync)) return;
|
||||||
|
|
||||||
res.status(501).json({ error: 'Not implemented' });
|
res.status(501).json({ error: 'Not implemented' });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { CSRFError, HTTPError } from '../errors';
|
||||||
import counters from '../counters';
|
import counters from '../counters';
|
||||||
import { Summary, Counter } from 'prom-client';
|
import { Summary, Counter } from 'prom-client';
|
||||||
import session from '../session';
|
import session from '../session';
|
||||||
|
import { verify as csrfVerify } from './csrf';
|
||||||
const verifySessionAsync = require('bluebird').promisify(session.verifySession);
|
const verifySessionAsync = require('bluebird').promisify(session.verifySession);
|
||||||
|
|
||||||
const LOGGER = require('@calzoneman/jsli')('webserver');
|
const LOGGER = require('@calzoneman/jsli')('webserver');
|
||||||
|
|
@ -256,13 +257,11 @@ module.exports = {
|
||||||
require('./routes/google_drive_userscript')(app);
|
require('./routes/google_drive_userscript')(app);
|
||||||
require('./routes/ustream_bypass')(app);
|
require('./routes/ustream_bypass')(app);
|
||||||
|
|
||||||
/*
|
|
||||||
const { AccountDataRoute } = require('./routes/account/data');
|
const { AccountDataRoute } = require('./routes/account/data');
|
||||||
require('@calzoneman/express-babel-decorators').bind(
|
require('@calzoneman/express-babel-decorators').bind(
|
||||||
app,
|
app,
|
||||||
new AccountDataRoute(accountDB, channelDB)
|
new AccountDataRoute(accountDB, channelDB, csrfVerify, verifySessionAsync)
|
||||||
);
|
);
|
||||||
*/
|
|
||||||
|
|
||||||
app.use(serveStatic(path.join(__dirname, '..', '..', 'www'), {
|
app.use(serveStatic(path.join(__dirname, '..', '..', 'www'), {
|
||||||
maxAge: webConfig.getCacheTTL()
|
maxAge: webConfig.getCacheTTL()
|
||||||
|
|
|
||||||
287
test/web/routes/account/data.js
Normal file
287
test/web/routes/account/data.js
Normal file
|
|
@ -0,0 +1,287 @@
|
||||||
|
const assert = require('assert');
|
||||||
|
const sinon = require('sinon');
|
||||||
|
const express = require('express');
|
||||||
|
const { AccountDB } = require('../../../../lib/db/account');
|
||||||
|
const { ChannelDB } = require('../../../../lib/db/channel');
|
||||||
|
const { AccountDataRoute } = require('../../../../lib/web/routes/account/data');
|
||||||
|
const http = require('http');
|
||||||
|
const expressBabelDecorators = require('@calzoneman/express-babel-decorators');
|
||||||
|
const nodeurl = require('url');
|
||||||
|
const Promise = require('bluebird');
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
|
const { CSRFError } = require('../../../../lib/errors');
|
||||||
|
|
||||||
|
const TEST_PORT = 10111;
|
||||||
|
const URL_BASE = `http://localhost:${TEST_PORT}`;
|
||||||
|
|
||||||
|
function request(method, url, additionalOptions) {
|
||||||
|
if (!additionalOptions) additionalOptions = {};
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
method
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(options, nodeurl.parse(url), additionalOptions);
|
||||||
|
|
||||||
|
const req = http.request(options);
|
||||||
|
|
||||||
|
req.on('error', error => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('response', res => {
|
||||||
|
let buffer = '';
|
||||||
|
res.setEncoding('utf8');
|
||||||
|
|
||||||
|
res.on('data', data => {
|
||||||
|
buffer += data;
|
||||||
|
|
||||||
|
if (buffer.length > 100 * 1024) {
|
||||||
|
req.abort();
|
||||||
|
reject(new Error('Response size exceeds 100KB'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
res.body = buffer;
|
||||||
|
resolve(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('AccountDataRoute', () => {
|
||||||
|
let accountDB;
|
||||||
|
let channelDB;
|
||||||
|
let csrfVerify;
|
||||||
|
let verifySessionAsync;
|
||||||
|
let server;
|
||||||
|
let app;
|
||||||
|
let signedCookies;
|
||||||
|
let accountDataRoute;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
let realAccountDB = new AccountDB();
|
||||||
|
let realChannelDB = new ChannelDB();
|
||||||
|
accountDB = sinon.mock(realAccountDB);
|
||||||
|
channelDB = sinon.mock(realChannelDB);
|
||||||
|
csrfVerify = sinon.stub();
|
||||||
|
verifySessionAsync = sinon.stub();
|
||||||
|
verifySessionAsync.withArgs('test_auth_cookie').resolves({ name: 'test' });
|
||||||
|
|
||||||
|
signedCookies = {
|
||||||
|
auth: 'test_auth_cookie'
|
||||||
|
};
|
||||||
|
app = express();
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
req.signedCookies = signedCookies;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
app.use(bodyParser.urlencoded({
|
||||||
|
extended: false,
|
||||||
|
limit: '1kb'
|
||||||
|
}));
|
||||||
|
|
||||||
|
accountDataRoute = new AccountDataRoute(
|
||||||
|
realAccountDB,
|
||||||
|
realChannelDB,
|
||||||
|
csrfVerify,
|
||||||
|
verifySessionAsync
|
||||||
|
);
|
||||||
|
|
||||||
|
expressBabelDecorators.bind(app, accountDataRoute);
|
||||||
|
|
||||||
|
server = http.createServer(app);
|
||||||
|
server.listen(TEST_PORT);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
server.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
function checkDefaults(route, method) {
|
||||||
|
it('rejects requests that don\'t accept JSON', () => {
|
||||||
|
return request(method, `${URL_BASE}${route}`, {
|
||||||
|
headers: { 'Accept': 'text/plain' }
|
||||||
|
}).then(res => {
|
||||||
|
assert.strictEqual(res.statusCode, 406);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
res.body,
|
||||||
|
'Not Acceptable'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects requests with no auth cookie', () => {
|
||||||
|
signedCookies.auth = null;
|
||||||
|
|
||||||
|
return request(method, `${URL_BASE}${route}`).then(res => {
|
||||||
|
assert.strictEqual(res.statusCode, 401);
|
||||||
|
|
||||||
|
const response = JSON.parse(res.body);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
response,
|
||||||
|
{ error: 'Authorization required' }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects requests with invalid auth cookie', () => {
|
||||||
|
signedCookies.auth = 'invalid';
|
||||||
|
verifySessionAsync.withArgs('invalid').rejects(new Error('Invalid'));
|
||||||
|
|
||||||
|
return request(method, `${URL_BASE}${route}`).then(res => {
|
||||||
|
assert.strictEqual(res.statusCode, 403);
|
||||||
|
|
||||||
|
const response = JSON.parse(res.body);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
response,
|
||||||
|
{ error: 'Invalid' }
|
||||||
|
);
|
||||||
|
assert(verifySessionAsync.calledWith('invalid'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects requests with mismatched auth cookie', () => {
|
||||||
|
signedCookies.auth = 'mismatch';
|
||||||
|
verifySessionAsync.withArgs('mismatch').resolves({ name: 'not_test' });
|
||||||
|
|
||||||
|
return request(method, `${URL_BASE}${route}`).then(res => {
|
||||||
|
assert.strictEqual(res.statusCode, 403);
|
||||||
|
|
||||||
|
const response = JSON.parse(res.body);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
response,
|
||||||
|
{ error: 'Session username does not match' }
|
||||||
|
);
|
||||||
|
assert(verifySessionAsync.calledWith('mismatch'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects requests with invalid CSRF tokens', () => {
|
||||||
|
csrfVerify.throws(new CSRFError('CSRF'));
|
||||||
|
|
||||||
|
return request(method, `${URL_BASE}${route}`).then(res => {
|
||||||
|
assert.strictEqual(res.statusCode, 403);
|
||||||
|
|
||||||
|
const response = JSON.parse(res.body);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
response,
|
||||||
|
{ error: 'Invalid CSRF token' }
|
||||||
|
);
|
||||||
|
assert(csrfVerify.called);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects requests with an internal CSRF handling error', () => {
|
||||||
|
csrfVerify.throws(new Error('broken'));
|
||||||
|
|
||||||
|
return request(method, `${URL_BASE}${route}`).then(res => {
|
||||||
|
assert.strictEqual(res.statusCode, 503);
|
||||||
|
|
||||||
|
const response = JSON.parse(res.body);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
response,
|
||||||
|
{ error: 'Internal error' }
|
||||||
|
);
|
||||||
|
assert(csrfVerify.called);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('#getAccount', () => {
|
||||||
|
it('serves a valid request', () => {
|
||||||
|
accountDB.expects('getByName').withArgs('test').returns({
|
||||||
|
name: 'test',
|
||||||
|
email: 'test@example.com',
|
||||||
|
profile: { text: 'blah', image: 'image.jpeg' },
|
||||||
|
time: new Date('2017-09-01T00:00:00.000Z'),
|
||||||
|
extraData: 'foo'
|
||||||
|
});
|
||||||
|
|
||||||
|
return request('GET', `${URL_BASE}/account/data/test`)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.statusCode, 200);
|
||||||
|
|
||||||
|
const response = JSON.parse(res.body);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
response,
|
||||||
|
{
|
||||||
|
result: {
|
||||||
|
name: 'test',
|
||||||
|
email: 'test@example.com',
|
||||||
|
profile: { text: 'blah', image: 'image.jpeg' },
|
||||||
|
time: '2017-09-01T00:00:00.000Z'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert(verifySessionAsync.calledWith(signedCookies.auth));
|
||||||
|
assert(csrfVerify.called);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
checkDefaults('/account/data/test', 'GET');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#updateAccount', () => {
|
||||||
|
checkDefaults('/account/data/test', 'PATCH');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#createChannel', () => {
|
||||||
|
checkDefaults('/account/data/test/channels/test_channel', 'POST');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#deleteChannel', () => {
|
||||||
|
checkDefaults('/account/data/test/channels/test_channel', 'DELETE');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#listChannels', () => {
|
||||||
|
it('serves a valid request', () => {
|
||||||
|
channelDB.expects('listByOwner').withArgs('test').returns([{
|
||||||
|
name: 'test_channel',
|
||||||
|
owner: 'test',
|
||||||
|
time: new Date('2017-09-01T00:00:00.000Z'),
|
||||||
|
last_loaded: new Date('2017-09-01T01:00:00.000Z'),
|
||||||
|
owner_last_seen: new Date('2017-09-01T02:00:00.000Z'),
|
||||||
|
extraData: 'foo'
|
||||||
|
}]);
|
||||||
|
|
||||||
|
return request('GET', `${URL_BASE}/account/data/test/channels`)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.statusCode, 200);
|
||||||
|
|
||||||
|
const response = JSON.parse(res.body);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
response,
|
||||||
|
{
|
||||||
|
result: [{
|
||||||
|
name: 'test_channel',
|
||||||
|
owner: 'test',
|
||||||
|
time: '2017-09-01T00:00:00.000Z',
|
||||||
|
last_loaded: '2017-09-01T01:00:00.000Z',
|
||||||
|
owner_last_seen: '2017-09-01T02:00:00.000Z',
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert(verifySessionAsync.calledWith(signedCookies.auth));
|
||||||
|
assert(csrfVerify.called);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
checkDefaults('/account/data/test/channels', 'GET');
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue