mirror of https://github.com/calzoneman/sync.git
Remove code that was never finished and likely won't be used
This commit is contained in:
parent
553052f901
commit
7b0427afa2
|
@ -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'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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 };
|
|
|
@ -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 };
|
|
|
@ -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 };
|
|
|
@ -49,9 +49,6 @@ import session from './session';
|
||||||
import { LegacyModule } from './legacymodule';
|
import { LegacyModule } from './legacymodule';
|
||||||
import { PartitionModule } from './partition/partitionmodule';
|
import { PartitionModule } from './partition/partitionmodule';
|
||||||
import { Gauge } from 'prom-client';
|
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';
|
import { EmailController } from './controller/email';
|
||||||
|
|
||||||
var Server = function () {
|
var Server = function () {
|
||||||
|
@ -84,12 +81,6 @@ var Server = function () {
|
||||||
self.db.init();
|
self.db.init();
|
||||||
ChannelStore.init();
|
ChannelStore.init();
|
||||||
|
|
||||||
const accountDB = new AccountDB(db.getDB());
|
|
||||||
const channelDB = new ChannelDB(db.getDB());
|
|
||||||
|
|
||||||
// controllers
|
|
||||||
const accountController = new AccountController(accountDB, globalMessageBus);
|
|
||||||
|
|
||||||
let emailTransport;
|
let emailTransport;
|
||||||
if (Config.getEmailConfig().getPasswordReset().isEnabled()) {
|
if (Config.getEmailConfig().getPasswordReset().isEnabled()) {
|
||||||
const smtpConfig = Config.getEmailConfig().getSmtp();
|
const smtpConfig = Config.getEmailConfig().getSmtp();
|
||||||
|
@ -138,8 +129,6 @@ var Server = function () {
|
||||||
channelIndex,
|
channelIndex,
|
||||||
session,
|
session,
|
||||||
globalMessageBus,
|
globalMessageBus,
|
||||||
accountController,
|
|
||||||
channelDB,
|
|
||||||
Config.getEmailConfig(),
|
Config.getEmailConfig(),
|
||||||
emailController
|
emailController
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 };
|
|
|
@ -12,7 +12,6 @@ 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');
|
||||||
|
@ -142,8 +141,6 @@ module.exports = {
|
||||||
channelIndex,
|
channelIndex,
|
||||||
session,
|
session,
|
||||||
globalMessageBus,
|
globalMessageBus,
|
||||||
accountController,
|
|
||||||
channelDB,
|
|
||||||
emailConfig,
|
emailConfig,
|
||||||
emailController
|
emailController
|
||||||
) {
|
) {
|
||||||
|
@ -210,19 +207,6 @@ module.exports = {
|
||||||
require('../google2vtt').attach(app);
|
require('../google2vtt').attach(app);
|
||||||
require('./routes/google_drive_userscript')(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'), {
|
app.use(serveStatic(path.join(__dirname, '..', '..', 'www'), {
|
||||||
maxAge: webConfig.getCacheTTL()
|
maxAge: webConfig.getCacheTTL()
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Reference in New Issue