diff --git a/integration_test/db/channel.js b/integration_test/db/channel.js new file mode 100644 index 00000000..313658c1 --- /dev/null +++ b/integration_test/db/channel.js @@ -0,0 +1,235 @@ +const assert = require('assert'); +const ChannelDB = require('../../lib/db/channel').ChannelDB; +const testDB = require('../testutil/db').testDB; + +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.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/db/account.js b/src/db/account.js index 4ed34e18..1945acb8 100644 --- a/src/db/account.js +++ b/src/db/account.js @@ -7,30 +7,27 @@ class AccountDB { getByName(name) { return this.db.runTransaction(async tx => { - const rows = await tx.table('users').where({ name }).select(); + const user = await tx.table('users').where({ name }).first(); - if (rows.length === 0) { - return null; - } + if (!user) return null; - return this.mapUser(rows[0]); + return this.mapUser(user); }); } updateByName(name, changedFields) { - return this.db.runTransaction(tx => { + return this.db.runTransaction(async tx => { if (changedFields.profile) { changedFields.profile = JSON.stringify(changedFields.profile); } - return tx.table('users') + const rowsUpdated = await tx.table('users') .update(changedFields) - .where({ name }) - .then(rowsUpdated => { - if (rowsUpdated === 0) { - throw new Error(`Cannot update: name "${name}" does not exist`); - } - }); + .where({ name }); + + if (rowsUpdated === 0) { + throw new Error(`Cannot update: name "${name}" does not exist`); + } }); } diff --git a/src/db/channel.js b/src/db/channel.js new file mode 100644 index 00000000..4662ab32 --- /dev/null +++ b/src/db/channel.js @@ -0,0 +1,99 @@ +import fs from 'fs'; +import path from 'path'; +import Promise from 'bluebird'; + +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 Error(`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 };