From 7ebf3c18ab891584897b18494473fcc792dd84e3 Mon Sep 17 00:00:00 2001 From: Calvin Montgomery Date: Wed, 28 Jun 2017 22:58:40 -0700 Subject: [PATCH] Add knex AliasesDB --- integration_test/db/aliases.js | 76 +++++++++++++++++++++++ src/db/aliases.js | 59 ++++++++++++++++++ test/db/aliases.js | 108 +++++++++++++++++++++++++++++++++ test/testutil/db.js | 12 +++- 4 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 integration_test/db/aliases.js create mode 100644 src/db/aliases.js create mode 100644 test/db/aliases.js diff --git a/integration_test/db/aliases.js b/integration_test/db/aliases.js new file mode 100644 index 00000000..7b190e11 --- /dev/null +++ b/integration_test/db/aliases.js @@ -0,0 +1,76 @@ +const assert = require('assert'); +const AliasesDB = require('../../lib/db/aliases').AliasesDB; +const testDB = require('../testutil/db').testDB; + +const aliasesDB = new AliasesDB(testDB); +const testIPs = ['111.111.111.111', '111.111.111.222']; +const testNames = ['itest1', 'itest2']; + +function cleanup() { + return testDB.knex.table('aliases') + .where('ip', 'in', testIPs) + .del() + .then(() => { + return testDB.knex.table('aliases') + .where('name', 'in', testNames) + .del(); + }); +} + +function addSomeAliases() { + return cleanup().then(() => { + return testDB.knex.table('aliases') + .insert([ + { ip: testIPs[0], name: testNames[0] }, + { ip: testIPs[0], name: testNames[1] }, + { ip: testIPs[1], name: testNames[1] } + ]); + }); +} + +describe('AliasesDB', () => { + describe('#addAlias', () => { + beforeEach(cleanup); + afterEach(cleanup); + + it('adds a new alias', () => { + return aliasesDB.addAlias(testIPs[0], testNames[0]) + .then(() => { + return testDB.knex.table('aliases') + .where({ ip: testIPs[0], name: testNames[0] }) + .select() + .then(rows => { + assert.strictEqual(rows.length, 1, 'expected 1 row'); + }); + }); + }); + }); + + describe('#getAliasesByIP', () => { + beforeEach(addSomeAliases); + afterEach(cleanup); + + it('retrieves aliases by IP', () => { + return aliasesDB.getAliasesByIP(testIPs[0]) + .then(names => assert.deepStrictEqual( + names.sort(), testNames.sort())); + }); + + it('retrieves aliases by partial IP', () => { + return aliasesDB.getAliasesByIP(testIPs[0].substring(4)) + .then(names => assert.deepStrictEqual( + names.sort(), testNames.sort())); + }); + }); + + describe('#getIPsByName', () => { + beforeEach(addSomeAliases); + afterEach(cleanup); + + it('retrieves IPs by name', () => { + return aliasesDB.getIPsByName(testNames[1]) + .then(ips => assert.deepStrictEqual( + ips.sort(), testIPs.sort())); + }); + }); +}); diff --git a/src/db/aliases.js b/src/db/aliases.js new file mode 100644 index 00000000..03eb2eb2 --- /dev/null +++ b/src/db/aliases.js @@ -0,0 +1,59 @@ +// @flow + +import { Database } from '../database'; +import { LoggerFactory } from '@calzoneman/jsli'; +import net from 'net'; + +const LOGGER = LoggerFactory.getLogger('AliasesDB'); + +class AliasesDB { + db: Database; + + constructor(db: Database) { + this.db = db; + } + + async addAlias(ip: string, name: string) { + return this.db.runTransaction(async tx => { + try { + await tx.table('aliases') + .where({ ip, name }) + .del(); + await tx.table('aliases') + .insert({ ip, name, time: Date.now() }); + } catch (error) { + LOGGER.error('Failed to save alias: %s (ip=%s, name=%s)', + error.message, ip, name); + } + }); + } + + async getAliasesByIP(ip: string): Promise> { + return this.db.runTransaction(async tx => { + const query = tx.table('aliases'); + if (net.isIP(ip)) { + query.where({ ip: ip }) + } else { + const delimiter = /^[0-9]+\./.test(ip) ? '.' : ':'; + query.where('ip', 'LIKE', ip + delimiter + '%'); + } + + const rows = await query.select() + .distinct('name') + .orderBy('time', 'desc') + .limit(5); + return rows.map(row => row.name); + }); + } + + async getIPsByName(name: string): Promise> { + return this.db.runTransaction(async tx => { + const rows = await tx.table('aliases') + .select('ip') + .where({ name }); + return rows.map(row => row.ip); + }); + } +} + +export { AliasesDB }; diff --git a/test/db/aliases.js b/test/db/aliases.js new file mode 100644 index 00000000..0f10ae59 --- /dev/null +++ b/test/db/aliases.js @@ -0,0 +1,108 @@ +const assert = require('assert'); +const sinon = require('sinon'); +const TestUtilDB = require('../testutil/db'); +const AliasesDB = require('../../lib/db/aliases').AliasesDB; + +describe('AliasesDB', () => { + let mockTx, mockDB, aliasesDB; + + beforeEach(() => { + mockTx = new TestUtilDB.MockTx(); + mockDB = new TestUtilDB.MockDB(mockTx); + aliasesDB = new AliasesDB(mockDB); + }); + + describe('#addAlias', () => { + it('adds a new alias', () => { + const ip = '1.2.3.4'; + const name = 'foo'; + sinon.stub(mockTx, 'table').withArgs('aliases').returns(mockTx); + sinon.stub(mockTx, 'where').withArgs({ ip, name }).returns(mockTx); + const del = sinon.stub(mockTx, 'del').resolves(); + const insert = sinon.stub(mockTx, 'insert').resolves(); + return aliasesDB.addAlias(ip, name).then(() => { + assert(del.called, 'Expected old alias to be purged'); + assert(insert.called, 'Expected new alias to be inserted'); + const record = insert.getCall(0).args[0]; + assert.strictEqual(record.ip, ip); + assert.strictEqual(record.name, name); + assert(typeof record.time === 'number', 'Expected time field to be a number'); + }); + }); + }); + + describe('#getAliasesByIP', () => { + it('retrieves aliases by full IP', () => { + const ip = '1.2.3.4'; + const rows = [ + { ip, name: 'foo' }, + { ip, name: 'bar' } + ]; + + sinon.stub(mockTx, 'table').withArgs('aliases').returns(mockTx); + sinon.stub(mockTx, 'where').withArgs({ ip }).returns(mockTx); + sinon.stub(mockTx, 'select').returns(mockTx); + sinon.stub(mockTx, 'distinct').withArgs('name').returns(mockTx); + sinon.stub(mockTx, 'orderBy').withArgs('time', 'desc').returns(mockTx); + sinon.stub(mockTx, 'limit').withArgs(5).resolves(rows); + return aliasesDB.getAliasesByIP(ip).then(names => { + assert.deepStrictEqual(names.sort(), ['bar', 'foo']); + }); + }); + + it('retrieves aliases by IPv4 range', () => { + const ip = '1.2.3'; + const rows = [ + { ip: ip + '.4', name: 'foo' }, + { ip: ip + '.5', name: 'bar' } + ]; + + sinon.stub(mockTx, 'table').withArgs('aliases').returns(mockTx); + sinon.stub(mockTx, 'where').withArgs('ip', 'LIKE', `${ip}.%`).returns(mockTx); + sinon.stub(mockTx, 'select').returns(mockTx); + sinon.stub(mockTx, 'distinct').withArgs('name').returns(mockTx); + sinon.stub(mockTx, 'orderBy').withArgs('time', 'desc').returns(mockTx); + sinon.stub(mockTx, 'limit').withArgs(5).resolves(rows); + return aliasesDB.getAliasesByIP(ip).then(names => { + assert.deepStrictEqual(names.sort(), ['bar', 'foo']); + }); + }); + + it('retrieves aliases by IPv6 range', () => { + const ip = '1:2:3'; + const rows = [ + { ip: ip + '::4', name: 'foo' }, + { ip: ip + '::5', name: 'bar' } + ]; + + sinon.stub(mockTx, 'table').withArgs('aliases').returns(mockTx); + const where = sinon.stub(mockTx, 'where') + .withArgs('ip', 'LIKE', `${ip}:%`).returns(mockTx); + sinon.stub(mockTx, 'select').returns(mockTx); + sinon.stub(mockTx, 'distinct').withArgs('name').returns(mockTx); + sinon.stub(mockTx, 'orderBy').withArgs('time', 'desc').returns(mockTx); + sinon.stub(mockTx, 'limit').withArgs(5).resolves(rows); + return aliasesDB.getAliasesByIP(ip).then(names => { + assert(where.called, 'Expected WHERE LIKE clause'); + assert.deepStrictEqual(names.sort(), ['bar', 'foo']); + }); + }); + }); + + describe('#getIPsByName', () => { + it('retrieves IPs by name', () => { + const name = 'foo'; + const rows = [ + { name, ip: '1.2.3.4' }, + { name, ip: '5.6.7.8' } + ]; + + sinon.stub(mockTx, 'table').withArgs('aliases').returns(mockTx); + sinon.stub(mockTx, 'select').withArgs('ip').returns(mockTx); + sinon.stub(mockTx, 'where').withArgs({ name }).resolves(rows); + return aliasesDB.getIPsByName(name).then(ips => { + assert.deepStrictEqual(ips.sort(), ['1.2.3.4', '5.6.7.8']); + }); + }); + }); +}); diff --git a/test/testutil/db.js b/test/testutil/db.js index da04648f..1ccf9345 100644 --- a/test/testutil/db.js +++ b/test/testutil/db.js @@ -14,7 +14,17 @@ function MockTx() { } -['insert', 'update', 'select', 'del', 'where', 'table'].forEach(method => { +[ + 'del', + 'distinct', + 'insert', + 'limit', + 'orderBy', + 'select', + 'table', + 'update', + 'where', +].forEach(method => { MockTx.prototype[method] = function () { return Promise.reject(new Error(`No stub defined for method "${method}"`)); };