diff --git a/integration_test/db/account.js b/integration_test/db/account.js new file mode 100644 index 00000000..a9e6729c --- /dev/null +++ b/integration_test/db/account.js @@ -0,0 +1,152 @@ +const assert = require('assert'); +const AccountDB = require('../../lib/db/account').AccountDB; +const testDB = require('../testutil/db').testDB; + +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.strictEqual( + error.message, + 'Cannot update: name "test" does not exist' + ); + }); + }); + }); +}); diff --git a/src/db/account.js b/src/db/account.js new file mode 100644 index 00000000..4ed34e18 --- /dev/null +++ b/src/db/account.js @@ -0,0 +1,65 @@ +const LOGGER = require('@calzoneman/jsli')('AccountDB'); + +class AccountDB { + constructor(db) { + this.db = db; + } + + getByName(name) { + return this.db.runTransaction(async tx => { + const rows = await tx.table('users').where({ name }).select(); + + if (rows.length === 0) { + return null; + } + + return this.mapUser(rows[0]); + }); + } + + updateByName(name, changedFields) { + return this.db.runTransaction(tx => { + if (changedFields.profile) { + changedFields.profile = JSON.stringify(changedFields.profile); + } + + return tx.table('users') + .update(changedFields) + .where({ name }) + .then(rowsUpdated => { + if (rowsUpdated === 0) { + throw new Error(`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 };