From 7b0427afa2f6ee22ccacdddc318ba99cef19e85d Mon Sep 17 00:00:00 2001 From: Calvin Montgomery Date: Mon, 27 Aug 2018 22:07:42 -0700 Subject: [PATCH] Remove code that was never finished and likely won't be used --- integration_test/db/account.js | 157 --------- integration_test/db/channel.js | 240 ------------- src/controller/account.js | 103 ------ src/db/account.js | 66 ---- src/db/channel.js | 102 ------ src/server.js | 11 - src/web/routes/account/data.js | 157 --------- src/web/webserver.js | 16 - test/web/routes/account/data.js | 582 -------------------------------- 9 files changed, 1434 deletions(-) delete mode 100644 integration_test/db/account.js delete mode 100644 integration_test/db/channel.js delete mode 100644 src/controller/account.js delete mode 100644 src/db/account.js delete mode 100644 src/db/channel.js delete mode 100644 src/web/routes/account/data.js delete mode 100644 test/web/routes/account/data.js diff --git a/integration_test/db/account.js b/integration_test/db/account.js deleted file mode 100644 index df0627aa..00000000 --- a/integration_test/db/account.js +++ /dev/null @@ -1,157 +0,0 @@ -const assert = require('assert'); -const AccountDB = require('../../lib/db/account').AccountDB; -const testDB = require('../testutil/db').testDB; -const { InvalidRequestError } = require('../../lib/errors'); - -const accountDB = new AccountDB(testDB); - -function cleanup() { - return testDB.knex.table('users').del(); -} - -function insert(user) { - return testDB.knex.table('users').insert(user); -} - -function fetch(params) { - return testDB.knex.table('users').where(params).first(); -} - -describe('AccountDB', () => { - let account, expected; - - beforeEach(() => { - account = { - name: 'test', - password: '', - global_rank: 1, - email: 'test@example.com', - profile: '{"image":"image.jpeg","text":"blah"}', - ip: '1.2.3.4', - time: 1500000000000, - name_dedupe: 'test' - }; - - expected = { - name: 'test', - password: '', - global_rank: 1, - email: 'test@example.com', - profile: { - image: 'image.jpeg', - text: 'blah' - }, - ip: '1.2.3.4', - time: new Date(1500000000000), - name_dedupe: 'test' - }; - }); - - beforeEach(cleanup); - - describe('#getByName', () => { - it('retrieves an account by name', () => { - return insert(account).then(() => { - return accountDB.getByName('test'); - }).then(retrieved => { - delete retrieved.id; - - assert.deepStrictEqual(retrieved, expected); - }); - }); - - it('defaults a blank profile', () => { - account.profile = ''; - expected.profile = { image: '', text: '' }; - - return insert(account).then(() => { - return accountDB.getByName('test'); - }).then(retrieved => { - delete retrieved.id; - - assert.deepStrictEqual(retrieved, expected); - }); - }); - - it('defaults an erroneous profile', () => { - account.profile = '{not real json'; - expected.profile = { image: '', text: '' }; - - return insert(account).then(() => { - return accountDB.getByName('test'); - }).then(retrieved => { - delete retrieved.id; - - assert.deepStrictEqual(retrieved, expected); - }); - }); - - it('returns null when no account is found', () => { - return accountDB.getByName('test').then(retrieved => { - assert.deepStrictEqual(retrieved, null); - }); - }); - }); - - describe('#updateByName', () => { - it('updates the password hash', () => { - return insert(account).then(() => { - return accountDB.updateByName( - account.name, - { password: 'secret hash' } - ); - }).then(() => { - return fetch({ name: account.name }); - }).then(retrieved => { - assert.strictEqual(retrieved.password, 'secret hash'); - }); - }); - - it('updates the email', () => { - return insert(account).then(() => { - return accountDB.updateByName( - account.name, - { email: 'bar@example.com' } - ); - }).then(() => { - return fetch({ name: account.name }); - }).then(retrieved => { - assert.strictEqual(retrieved.email, 'bar@example.com'); - }); - }); - - it('updates the profile', () => { - return insert(account).then(() => { - return accountDB.updateByName( - account.name, - { profile: { image: 'shiggy.jpg', text: 'Costanza' } } - ); - }).then(() => { - return fetch({ name: account.name }); - }).then(retrieved => { - assert.deepStrictEqual( - retrieved.profile, - '{"image":"shiggy.jpg","text":"Costanza"}' - ); - }); - }); - - it('raises an error if the username does not exist', () => { - return accountDB.updateByName( - account.name, - { password: 'secret hash' } - ).then(() => { - throw new Error('Expected failure due to missing user'); - }).catch(error => { - assert( - error instanceof InvalidRequestError, - 'Expected InvalidRequestError' - ); - assert.strictEqual( - error.message, - 'Cannot update: name "test" does not exist' - ); - }); - }); - }); -}); diff --git a/integration_test/db/channel.js b/integration_test/db/channel.js deleted file mode 100644 index fa9ead97..00000000 --- a/integration_test/db/channel.js +++ /dev/null @@ -1,240 +0,0 @@ -const assert = require('assert'); -const ChannelDB = require('../../lib/db/channel').ChannelDB; -const testDB = require('../testutil/db').testDB; -const { InvalidRequestError } = require('../../lib/errors'); - -const channelDB = new ChannelDB(testDB); - -function cleanup() { - return testDB.knex.table('channels').del().then(() => { - return testDB.knex.table('channel_ranks').del(); - }).then(() => { - return testDB.knex.table('channel_bans').del(); - }).then(() => { - return testDB.knex.table('channel_libraries').del(); - }).then(() => { - return testDB.knex.table('channel_data').del(); - }); -} - -function insert(channel) { - return testDB.knex.table('channels').insert(channel); -} - -function fetch(params) { - return testDB.knex.table('channels').where(params).first(); -} - -describe('ChannelDB', () => { - let channel, expected; - - beforeEach(() => { - channel = { - name: 'i_test', - owner: 'test_user', - time: 1500000000000, - last_loaded: new Date('2017-08-29T00:00:00Z'), - owner_last_seen: new Date('2017-08-29T01:00:00Z') - }; - - expected = { - name: 'i_test', - owner: 'test_user', - time: new Date(1500000000000), - last_loaded: new Date('2017-08-29T00:00:00Z'), - owner_last_seen: new Date('2017-08-29T01:00:00Z') - }; - }); - - beforeEach(cleanup); - - describe('#getByName', () => { - it('retrieves a channel by name', () => { - return insert(channel).then(() => { - return channelDB.getByName('i_test'); - }).then(retrieved => { - delete retrieved.id; - - assert.deepStrictEqual(retrieved, expected); - }); - }); - - it('returns null if the channel is not found', () => { - return channelDB.getByName('i_test').then(channel => { - assert.strictEqual(channel, null); - }); - }); - }); - - describe('#listByUser', () => { - it('retrieves channels by owner', () => { - return insert(channel).then(() => { - return channelDB.listByOwner('test_user'); - }).then(rows => { - assert.strictEqual(rows.length, 1); - - delete rows[0].id; - - assert.deepStrictEqual(rows[0], expected); - }); - }); - - it('returns empty results if the owner has no channels', () => { - return channelDB.listByOwner('test_user').then(rows => { - assert.strictEqual(rows.length, 0); - }); - }); - }); - - describe('#insert', () => { - it('creates a channel', () => { - return channelDB.insert({ name: 'i_test', owner: 'test_user' }).then(() => { - return fetch({ name: 'i_test' }); - }).then(inserted => { - assert.strictEqual(inserted.name, 'i_test'); - assert.strictEqual(inserted.owner, 'test_user'); - - const now = Date.now(); - - assert( - Math.abs(inserted.time - now) < 1000, - 'Wrong time' - ); - assert( - Math.abs(inserted.last_loaded.getTime() - now) < 1000, - 'Wrong last_loaded' - ); - assert( - Math.abs(inserted.owner_last_seen.getTime() - now) < 1000, - 'Wrong owner_last_seen' - ); - }); - }); - - it('inserts a rank 5 for the owner', () => { - return channelDB.insert({ name: 'i_test', owner: 'test_user' }).then(() => { - return testDB.knex.table('channel_ranks') - .where({ channel: 'i_test', name: 'test_user' }) - .first(); - }).then(inserted => { - assert.deepStrictEqual(inserted, { - name: 'test_user', - channel: 'i_test', - rank: 5 - }); - }); - }); - - it('throws when the channel already exists', () => { - return insert(channel).then(() => { - return channelDB.insert({ name: 'i_test', owner: 'test_user' }); - }).then(() => { - throw new Error('Expected error due to already existing channel'); - }).catch(error => { - assert( - error instanceof InvalidRequestError, - 'Expected InvalidRequestError' - ); - assert.strictEqual( - error.message, - 'Channel "i_test" is already registered.' - ); - }); - }); - - it('propagates other constraint errors', () => { - return testDB.knex.table('channel_ranks') - .insert({ name: 'test_user', channel: 'i_test', rank: 5 }) - .then(() => { - return channelDB.insert({ name: 'i_test', owner: 'test_user' }); - }).then(() => { - throw new Error('Expected error due to already existing channel'); - }).catch(error => { - assert.strictEqual( - error.code, - 'ER_DUP_ENTRY' - ); - }); - }); - }); - - describe('#deleteByName', () => { - it('deletes a channel', () => { - return insert(channel).then(() => { - return channelDB.deleteByName('i_test'); - }).then(() => { - return fetch({ name: 'i_test' }); - }).then(deleted => { - assert.strictEqual(deleted, undefined); - }); - }); - - it('deletes other crap associated with a channel', () => { - let channelId; - - return insert(channel).then(() => { - return fetch({ name: 'i_test' }); - }).then(retrieved => { - channelId = retrieved.id; - }).then(() => { - return testDB.knex.table('channel_ranks') - .insert({ - channel: 'i_test', - name: 'test', - rank: 5 - }); - }).then(() => { - return testDB.knex.table('channel_bans') - .insert({ - channel: 'i_test', - ip: '', - name: 'banned_dude', - reason: '' - }); - }).then(() => { - return testDB.knex.table('channel_libraries') - .insert({ - channel: 'i_test', - id: Math.random().toString(32), - title: 'testing', - seconds: 1, - type: 'tt', - meta: '' - }); - }).then(() => { - return testDB.knex.table('channel_data') - .insert({ - channel_id: channelId, - key: 'test', - value: 'test' - }); - }).then(() => { - return channelDB.deleteByName('i_test'); - }).then(() => { - return testDB.knex.table('channel_ranks') - .where({ channel: 'i_test' }) - .select(); - }).then(rows => { - assert.strictEqual(rows.length, 0); - - return testDB.knex.table('channel_bans') - .where({ channel: 'i_test' }) - .select(); - }).then(rows => { - assert.strictEqual(rows.length, 0); - - return testDB.knex.table('channel_libraries') - .where({ channel: 'i_test' }) - .select(); - }).then(rows => { - assert.strictEqual(rows.length, 0); - - return testDB.knex.table('channel_data') - .where({ channel_id: channelId }) - .select(); - }).then(rows => { - assert.strictEqual(rows.length, 0); - }); - }); - }); -}); diff --git a/src/controller/account.js b/src/controller/account.js deleted file mode 100644 index f0399537..00000000 --- a/src/controller/account.js +++ /dev/null @@ -1,103 +0,0 @@ -import { InvalidRequestError } from '../errors'; -import { isValidEmail } from '../utilities'; -import { parse as parseURL } from 'url'; -import bcrypt from 'bcrypt'; -import Promise from 'bluebird'; - -Promise.promisifyAll(bcrypt); - -class AccountController { - constructor(accountDB, globalMessageBus) { - this.accountDB = accountDB; - this.globalMessageBus = globalMessageBus; - } - - async getAccount(name) { - const user = await this.accountDB.getByName(name); - - if (user) { - return { - name: user.name, - email: user.email, - profile: user.profile, - time: user.time - }; - } else { - return null; - } - } - - async updateAccount(name, updates, password = null) { - let requirePassword = false; - const fields = {}; - - if (!updates || updates.toString() !== '[object Object]') { - throw new InvalidRequestError('Malformed input'); - } - - if (updates.email) { - if (!isValidEmail(updates.email)) { - throw new InvalidRequestError('Invalid email address'); - } - - fields.email = updates.email; - requirePassword = true; - } - - if (updates.profile) { - validateProfile(updates.profile); - - fields.profile = { - image: updates.profile.image.trim(), - text: updates.profile.text - }; - } - - if (requirePassword) { - if (!password) { - throw new InvalidRequestError('Password required'); - } - - const user = await this.accountDB.getByName(name); - - if (!user) { - throw new InvalidRequestError('User does not exist'); - } - - // For legacy reasons, the password was truncated to 100 chars. - password = password.substring(0, 100); - - if (!await bcrypt.compareAsync(password, user.password)) { - throw new InvalidRequestError('Invalid password'); - } - } - - await this.accountDB.updateByName(name, fields); - } -} - -function validateProfile(profile) { - // TODO: replace all of these errors with a standard errorcode + field checker - if (profile.toString() !== '[object Object]') - throw new InvalidRequestError('Invalid profile'); - if (typeof profile.text !== 'string') - throw new InvalidRequestError('Invalid profile'); - if (typeof profile.image !== 'string') - throw new InvalidRequestError('Invalid profile'); - if (profile.text.length > 255) - throw new InvalidRequestError('Profile text must not exceed 255 characters'); - if (profile.image.length > 255) - throw new InvalidRequestError('Profile image URL must not exceed 255 characters'); - - if (profile.image.trim() === '') return true; - - const url = parseURL(profile.image); - if (!url.host) - throw new InvalidRequestError('Invalid profile image URL'); - if (url.protocol !== 'https:') - throw new InvalidRequestError('Profile image URL must start with "https:"'); - - return true; -} - -export { AccountController }; diff --git a/src/db/account.js b/src/db/account.js deleted file mode 100644 index 3040f0e1..00000000 --- a/src/db/account.js +++ /dev/null @@ -1,66 +0,0 @@ -import { InvalidRequestError } from '../errors'; - -const LOGGER = require('@calzoneman/jsli')('AccountDB'); - -class AccountDB { - constructor(db) { - this.db = db; - } - - getByName(name) { - return this.db.runTransaction(async tx => { - const user = await tx.table('users').where({ name }).first(); - - if (!user) return null; - - return this.mapUser(user); - }); - } - - updateByName(name, changedFields) { - return this.db.runTransaction(async tx => { - if (changedFields.profile) { - changedFields.profile = JSON.stringify(changedFields.profile); - } - - const rowsUpdated = await tx.table('users') - .update(changedFields) - .where({ name }); - - if (rowsUpdated === 0) { - throw new InvalidRequestError( - `Cannot update: name "${name}" does not exist` - ); - } - }); - } - - mapUser(user) { - // Backwards compatibility - // Maybe worth backfilling one day to be done with it? - try { - let profile; - - if (!user.profile) { - profile = { image: '', text: '' }; - } else { - profile = JSON.parse(user.profile); - } - - if (!profile.image) profile.image = ''; - if (!profile.text) profile.text = ''; - - user.profile = profile; - } catch (error) { - // TODO: backfill erroneous records and remove this check - LOGGER.warn('Invalid profile "%s": %s', user.profile, error); - user.profile = { image: '', text: '' }; - } - - user.time = new Date(user.time); - - return user; - } -} - -export { AccountDB }; diff --git a/src/db/channel.js b/src/db/channel.js deleted file mode 100644 index e7ba028b..00000000 --- a/src/db/channel.js +++ /dev/null @@ -1,102 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import Promise from 'bluebird'; -import { InvalidRequestError } from '../errors'; - -const unlinkAsync = Promise.promisify(fs.unlink); - -class ChannelDB { - constructor(db) { - this.db = db; - } - - getByName(name) { - return this.db.runTransaction(async tx => { - const channel = await tx.table('channels') - .where({ name }) - .first(); - - if (!channel) return null; - - return this.mapChannel(channel); - }); - } - - listByOwner(owner) { - return this.db.runTransaction(async tx => { - const rows = await tx.table('channels') - .where({ owner }) - .select(); - - return rows.map(row => this.mapChannel(row)); - }); - } - - insert(params) { - const { name, owner } = params; - - return this.db.runTransaction(async tx => { - const existing = await tx.table('channels') - .where({ name }) - .forUpdate() - .first(); - - if (existing) { - throw new InvalidRequestError( - `Channel "${name}" is already registered.` - ); - } - - await tx.table('channels') - .insert({ - name, - owner, - time: Date.now(), // Old column, does not use datetime type - last_loaded: new Date(), - owner_last_seen: new Date() - }); - - await tx.table('channel_ranks') - .insert({ - name: owner, - rank: 5, - channel: name - }); - }); - } - - // TODO: should this be a soft-delete? - deleteByName(name) { - return this.db.runTransaction(async tx => { - const channel = await tx.table('channels') - .where({ name }) - .forUpdate() - .first(); - - if (!channel) return; - - await tx.table('channel_ranks').where({ channel: name }).del(); - await tx.table('channel_bans').where({ channel: name }).del(); - await tx.table('channel_libraries').where({ channel: name }).del(); - await tx.table('channel_data').where({ channel_id: channel.id }).del(); - await tx.table('channels').where({ name }).del(); - - // TODO: deprecate and remove flatfile chandumps - const chandump = path.resolve(__dirname, '..', '..', 'chandump', name); - - try { - await unlinkAsync(chandump); - } catch (error) { - if (error.code !== 'ENOENT') throw error; - } - }); - } - - mapChannel(channel) { - // TODO: fix to datetime column? - channel.time = new Date(channel.time); - return channel; - } -} - -export { ChannelDB }; diff --git a/src/server.js b/src/server.js index 74aa0abe..7e2486ab 100644 --- a/src/server.js +++ b/src/server.js @@ -49,9 +49,6 @@ import session from './session'; import { LegacyModule } from './legacymodule'; import { PartitionModule } from './partition/partitionmodule'; import { Gauge } from 'prom-client'; -import { AccountDB } from './db/account'; -import { ChannelDB } from './db/channel'; -import { AccountController } from './controller/account'; import { EmailController } from './controller/email'; var Server = function () { @@ -84,12 +81,6 @@ var Server = function () { self.db.init(); ChannelStore.init(); - const accountDB = new AccountDB(db.getDB()); - const channelDB = new ChannelDB(db.getDB()); - - // controllers - const accountController = new AccountController(accountDB, globalMessageBus); - let emailTransport; if (Config.getEmailConfig().getPasswordReset().isEnabled()) { const smtpConfig = Config.getEmailConfig().getSmtp(); @@ -138,8 +129,6 @@ var Server = function () { channelIndex, session, globalMessageBus, - accountController, - channelDB, Config.getEmailConfig(), emailController ); diff --git a/src/web/routes/account/data.js b/src/web/routes/account/data.js deleted file mode 100644 index 79f139ed..00000000 --- a/src/web/routes/account/data.js +++ /dev/null @@ -1,157 +0,0 @@ -// TODO: either finish this refactoring or just delete it -import { GET, POST, PATCH, DELETE } from '@calzoneman/express-babel-decorators'; -import { CSRFError, InvalidRequestError } from '../../../errors'; - -const LOGGER = require('@calzoneman/jsli')('AccountDataRoute'); - -function checkAcceptsJSON(req, res) { - if (!req.accepts('application/json')) { - res.status(406).send('Not Acceptable'); - - return false; - } - - return true; -} - -async function authorize(req, res, csrfVerify, verifySessionAsync) { - if (!req.signedCookies || !req.signedCookies.auth) { - res.status(401).json({ - error: 'Authorization required' - }); - - return false; - } - - try { - csrfVerify(req); - } catch (error) { - if (error instanceof CSRFError) { - res.status(403).json({ - error: 'Invalid CSRF token' - }); - } else { - LOGGER.error('CSRF check failed: %s', error.stack); - res.status(503).json({ error: 'Internal error' }); - } - - return false; - } - - 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; -} - -function reportError(req, res, error) { - if (error instanceof InvalidRequestError) { - res.status(400).json({ error: error.message }); - } else { - LOGGER.error( - '%s %s: %s', - req.method, - req.originalUrl, - error.stack - ); - res.status(503).json({ error: 'Internal error' }); - } - -} - -class AccountDataRoute { - constructor(accountController, channelDB, csrfVerify, verifySessionAsync) { - this.accountController = accountController; - this.channelDB = channelDB; - this.csrfVerify = csrfVerify; - this.verifySessionAsync = verifySessionAsync; - } - - @GET('/account/data/:user') - async getAccount(req, res) { - if (!checkAcceptsJSON(req, res)) return; - if (!await authorize(req, res, this.csrfVerify, this.verifySessionAsync)) return; - - try { - const user = await this.accountController.getAccount(req.params.user); - - res.status(user === null ? 404 : 200).json({ result: user }); - } catch (error) { - reportError(req, res, error); - } - } - - @PATCH('/account/data/:user') - async updateAccount(req, res) { - if (!checkAcceptsJSON(req, res)) return; - if (!await authorize(req, res, this.csrfVerify, this.verifySessionAsync)) return; - - const { password, updates } = req.body; - - try { - await this.accountController.updateAccount( - req.params.user, - updates, - password - ); - res.status(204).send(); - } catch (error) { - reportError(req, res, error); - } - } - - @GET('/account/data/:user/channels') - async listChannels(req, res) { - if (!checkAcceptsJSON(req, res)) return; - if (!await authorize(req, res, this.csrfVerify, this.verifySessionAsync)) return; - - try { - const channels = await this.channelDB.listByOwner(req.params.user).map( - channel => ({ - name: channel.name, - owner: channel.owner, - time: channel.time, - last_loaded: channel.last_loaded, - owner_last_seen: channel.owner_last_seen - }) - ); - - res.status(200).json({ result: channels }); - } catch (error) { - reportError(req, res, error); - } - } - - @POST('/account/data/:user/channels/:name') - async createChannel(req, res) { - if (!checkAcceptsJSON(req, res)) return; - if (!await authorize(req, res, this.csrfVerify, this.verifySessionAsync)) return; - - res.status(501).json({ error: 'Not implemented' }); - } - - @DELETE('/account/data/:user/channels/:name') - async deleteChannel(req, res) { - if (!checkAcceptsJSON(req, res)) return; - if (!await authorize(req, res, this.csrfVerify, this.verifySessionAsync)) return; - - res.status(501).json({ error: 'Not implemented' }); - } -} - -export { AccountDataRoute }; diff --git a/src/web/webserver.js b/src/web/webserver.js index 767c53ae..0698e677 100644 --- a/src/web/webserver.js +++ b/src/web/webserver.js @@ -12,7 +12,6 @@ import { CSRFError, HTTPError } from '../errors'; import counters from '../counters'; import { Summary, Counter } from 'prom-client'; import session from '../session'; -import { verify as csrfVerify } from './csrf'; const verifySessionAsync = require('bluebird').promisify(session.verifySession); const LOGGER = require('@calzoneman/jsli')('webserver'); @@ -142,8 +141,6 @@ module.exports = { channelIndex, session, globalMessageBus, - accountController, - channelDB, emailConfig, emailController ) { @@ -210,19 +207,6 @@ module.exports = { require('../google2vtt').attach(app); require('./routes/google_drive_userscript')(app); - if (process.env.UNFINISHED_FEATURE) { - const { AccountDataRoute } = require('./routes/account/data'); - require('@calzoneman/express-babel-decorators').bind( - app, - new AccountDataRoute( - accountController, - channelDB, - csrfVerify, - verifySessionAsync - ) - ); - } - app.use(serveStatic(path.join(__dirname, '..', '..', 'www'), { maxAge: webConfig.getCacheTTL() })); diff --git a/test/web/routes/account/data.js b/test/web/routes/account/data.js deleted file mode 100644 index ca77e5e3..00000000 --- a/test/web/routes/account/data.js +++ /dev/null @@ -1,582 +0,0 @@ -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 { AccountController } = require('../../../../lib/controller/account'); -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 { EventEmitter } = require('events'); - -const TEST_PORT = 10111; -const URL_BASE = `http://localhost:${TEST_PORT}`; - -function request(method, url, additionalOptions) { - if (!additionalOptions) additionalOptions = {}; - - const { body } = additionalOptions; - if (body) { - delete additionalOptions.body; - - if (!additionalOptions.headers) { - additionalOptions.headers = { - 'Accept': 'application/json' - }; - } - - additionalOptions.headers['Content-Type'] = 'application/json'; - } - - 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; - }); - - res.on('end', () => { - res.body = buffer; - resolve(res); - }); - }); - - if (body) { - req.write(JSON.stringify(body)); - } - - 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.json({ - limit: '1kb' - })); - - accountDataRoute = new AccountDataRoute( - new AccountController(realAccountDB, new EventEmitter()), - 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); - accountDB.verify(); - }); - }); - - checkDefaults('/account/data/test', 'GET'); - }); - - describe('#updateAccount', () => { - it('updates email', () => { - accountDB.expects('getByName').withArgs('test').returns({ - name: 'test', - password: '$2a$10$c26sbtkVlYlFUBdSxzQGhenZvdPBI2fvTPOmVRyrBuaD.8j7iyoNm', - email: 'test@example.com', - profile: { text: 'blah', image: 'image.jpeg' }, - time: new Date('2017-09-01T00:00:00.000Z') - }); - accountDB.expects('updateByName').withArgs( - 'test', - { email: 'test_new@example.com' } - ); - - return request('PATCH', `${URL_BASE}/account/data/test`, { - body: { - password: 'test', - updates: { - email: 'test_new@example.com' - } - } - }).then(res => { - assert.strictEqual(res.statusCode, 204); - - accountDB.verify(); - }); - }); - - it('updates profile', () => { - accountDB.expects('updateByName').withArgs( - 'test', - { - profile: { - text: 'testing', - image: 'https://example.com/image.jpg' - } - } - ); - - return request('PATCH', `${URL_BASE}/account/data/test`, { - body: { - updates: { - profile: { - text: 'testing', - image: 'https://example.com/image.jpg' - } - } - } - }).then(res => { - assert.strictEqual(res.statusCode, 204); - - accountDB.verify(); - }); - }); - - it('rejects invalid email address', () => { - return request('PATCH', `${URL_BASE}/account/data/test`, { - body: { - password: 'test', - updates: { - email: 'not!!valid' - } - } - }).then(res => { - assert.strictEqual(res.statusCode, 400); - assert.strictEqual( - JSON.parse(res.body).error, - 'Invalid email address' - ); - - accountDB.verify(); - }); - }); - - it('rejects request to change email with no password', () => { - return request('PATCH', `${URL_BASE}/account/data/test`, { - body: { - updates: { - email: 'test_new@example.com' - } - } - }).then(res => { - assert.strictEqual(res.statusCode, 400); - assert.strictEqual( - JSON.parse(res.body).error, - 'Password required' - ); - - accountDB.verify(); - }); - }); - - it('rejects invalid password', () => { - accountDB.expects('getByName').withArgs('test').returns({ - name: 'test', - password: '$2a$10$c26sbtkVlYlFUBdSxzQGhenZvdPBI2fvTPOmVRyrBuaD.8j7iyoNm', - email: 'test@example.com', - profile: { text: 'blah', image: 'image.jpeg' }, - time: new Date('2017-09-01T00:00:00.000Z') - }); - - return request('PATCH', `${URL_BASE}/account/data/test`, { - body: { - password: 'wrong', - updates: { - email: 'test_new@example.com' - } - } - }).then(res => { - assert.strictEqual(res.statusCode, 400); - assert.strictEqual( - JSON.parse(res.body).error, - 'Invalid password' - ); - - accountDB.verify(); - }); - }); - - it('rejects non-existing user', () => { - accountDB.expects('getByName').withArgs('test').returns(null); - - return request('PATCH', `${URL_BASE}/account/data/test`, { - body: { - password: 'test', - updates: { - email: 'test_new@example.com' - } - } - }).then(res => { - assert.strictEqual(res.statusCode, 400); - assert.strictEqual( - JSON.parse(res.body).error, - 'User does not exist' - ); - - accountDB.verify(); - }); - }); - - it('rejects invalid input', () => { - return request('PATCH', `${URL_BASE}/account/data/test`, { - body: ['not correct'] - }).then(res => { - assert.strictEqual(res.statusCode, 400); - assert.strictEqual( - JSON.parse(res.body).error, - 'Malformed input' - ); - - accountDB.verify(); - }); - }); - - it('rejects invalid profile', () => { - return request('PATCH', `${URL_BASE}/account/data/test`, { - body: { - updates: { - profile: 'not valid' - } - } - }).then(res => { - assert.strictEqual(res.statusCode, 400); - assert.strictEqual( - JSON.parse(res.body).error, - 'Invalid profile' - ); - - accountDB.verify(); - }); - }); - - it('rejects wrongly typed profile text', () => { - return request('PATCH', `${URL_BASE}/account/data/test`, { - body: { - updates: { - profile: { - text: ['wrong'], - image: 'https://example.com' - } - } - } - }).then(res => { - assert.strictEqual(res.statusCode, 400); - assert.strictEqual( - JSON.parse(res.body).error, - 'Invalid profile' - ); - - accountDB.verify(); - }); - }); - - it('rejects too long profile text', () => { - let longText = ''; for (let i = 0; i < 256; i++) longText += 'a'; - - return request('PATCH', `${URL_BASE}/account/data/test`, { - body: { - updates: { - profile: { - text: longText, - image: 'https://example.com' - } - } - } - }).then(res => { - assert.strictEqual(res.statusCode, 400); - assert.strictEqual( - JSON.parse(res.body).error, - 'Profile text must not exceed 255 characters' - ); - - accountDB.verify(); - }); - }); - - it('rejects wrongly typed profile image', () => { - return request('PATCH', `${URL_BASE}/account/data/test`, { - body: { - updates: { - profile: { - text: 'test', - image: 42 - } - } - } - }).then(res => { - assert.strictEqual(res.statusCode, 400); - assert.strictEqual( - JSON.parse(res.body).error, - 'Invalid profile' - ); - - accountDB.verify(); - }); - }); - - it('rejects too long profile image', () => { - let longText = 'https://'; for (let i = 0; i < 256; i++) longText += 'a'; - - return request('PATCH', `${URL_BASE}/account/data/test`, { - body: { - updates: { - profile: { - text: 'test', - image: longText - } - } - } - }).then(res => { - assert.strictEqual(res.statusCode, 400); - assert.strictEqual( - JSON.parse(res.body).error, - 'Profile image URL must not exceed 255 characters' - ); - - accountDB.verify(); - }); - }); - - it('rejects non-https profile image', () => { - return request('PATCH', `${URL_BASE}/account/data/test`, { - body: { - updates: { - profile: { - text: 'test', - image: 'http://example.com/image.jpg' - } - } - } - }).then(res => { - assert.strictEqual(res.statusCode, 400); - assert.strictEqual( - JSON.parse(res.body).error, - 'Profile image URL must start with "https:"' - ); - - accountDB.verify(); - }); - }); - - 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); - channelDB.verify(); - }); - }); - - checkDefaults('/account/data/test/channels', 'GET'); - }); -});