UNPKG

ilp-plugin-mini-accounts

Version:
323 lines (286 loc) 11.4 kB
'use strict' /* eslint-env mocha */ const BtpPacket = require('btp-packet') const crypto = require('crypto') const IlpPacket = require('ilp-packet') const getPort = require('get-port') const chai = require('chai') chai.use(require('chai-as-promised')) const assert = chai.assert const sinon = require('sinon') const PluginMiniAccounts = require('..') const Store = require('ilp-store-memory') const sendAuthPacket = require('./helper/btp-util') const Token = require('../src/token').default const http = require('http') const fetch = require('node-fetch') function sha256 (token) { return BtpPacket.base64url(crypto.createHash('sha256').update(token).digest('sha256')) } describe('Mini Accounts Plugin', () => { beforeEach(async function () { this.port = await getPort() this.plugin = new PluginMiniAccounts({ port: this.port, debugHostIldcpInfo: { clientAddress: 'test.example' }, _store: new Store() }) await this.plugin.connect() this.from = 'test.example.35YywQ-3GYiO3MM4tvfaSGhty9NZELIBO3kmilL0Wak' this.fulfillment = crypto.randomBytes(32) this.condition = crypto.createHash('sha256') .update(this.fulfillment) .digest() }) afterEach(async function () { await this.plugin.disconnect() assert.equal(this.plugin._connections.size, 0) }) describe('Authentication', function () { beforeEach(async function () { this.serverUrl = 'ws://localhost:' + this.port }) describe('new account', function () { it('stores hashed token if account does not exist', async function () { const spy = sinon.spy(this.plugin._store, 'set') await sendAuthPacket(this.serverUrl, 'acc', 'secret_token') // assert that a new account was written to the store with a hashed token const expectedToken = sha256('secret_token') assert.isTrue(spy.calledWith('acc:hashed-token', expectedToken), `expected new account written to store with value ${expectedToken}, but wasn't`) }) it('does not race when storing the token', async function () { const realStoreLoad = this.plugin._store.load.bind(this.plugin._store) sinon.stub(this.plugin._store, 'load').onFirstCall().callsFake(async (...args) => { // forces a race condition await sendAuthPacket(this.serverUrl, 'acc', '2nd_secret_token') return realStoreLoad(...args) }) const msg = await sendAuthPacket(this.serverUrl, 'acc', '1st_secret_token') assert.strictEqual(msg.type, BtpPacket.TYPE_ERROR, 'expected an BTP error') assert.strictEqual(msg.data.code, 'F00') assert.strictEqual(msg.data.name, 'NotAcceptedError') assert.match(msg.data.data, /incorrect token for account/) assert.strictEqual(this.plugin._store.get('acc:hashed-token'), sha256('2nd_secret_token')) }) }) describe('existing account', function () { beforeEach(function () { new Token({ account: 'acc', token: 'secret_token', store: this.plugin._store }).save() }) it('fails if received token does not match stored token', async function () { const msg = await sendAuthPacket(this.serverUrl, 'acc', 'wrong_token') assert.strictEqual(msg.type, BtpPacket.TYPE_ERROR, 'expected an BTP error') assert.strictEqual(msg.data.code, 'F00') assert.strictEqual(msg.data.name, 'NotAcceptedError') assert.match(msg.data.data, /incorrect token for account/) }) it('succeeds if received token matches stored token', async function () { const msg = await sendAuthPacket(this.serverUrl, 'acc', 'secret_token') assert.strictEqual(msg.type, BtpPacket.TYPE_RESPONSE) }) it('migrates an unhashed token', async function () { this.plugin._store.set('other_acc:token', 'unhashed') const token = await Token.load({account: 'other_acc', store: this.plugin._store}) assert.isUndefined(this.plugin._store.get('other_acc:token')) assert.strictEqual(token._account, 'other_acc') assert.strictEqual(token._hashedToken, sha256('unhashed')) }) }) describe('generateAccount = true', function () { it('does not allow a random username', async function () { const port = await getPort() const serverUrl = 'ws://localhost:' + port const plugin = new PluginMiniAccounts({ port: port, debugHostIldcpInfo: { clientAddress: 'test.example' }, generateAccount: true, _store: new Store() }) await plugin.connect() const msg = await sendAuthPacket(serverUrl, 'foobar', 'secret_token') assert.strictEqual(msg.type, BtpPacket.TYPE_ERROR, 'expected a BTP error') assert.strictEqual(msg.data.code, 'F00') assert.strictEqual(msg.data.name, 'NotAcceptedError') assert.strictEqual(msg.data.data, 'auth_username subprotocol is not available') await plugin.disconnect() }) }) describe('generateAccount = false', function () { it('requires a username', async function () { const port = await getPort() const serverUrl = 'ws://localhost:' + port const plugin = new PluginMiniAccounts({ port: port, debugHostIldcpInfo: { clientAddress: 'test.example' }, generateAccount: false, _store: new Store() }) await plugin.connect() const msg = await sendAuthPacket(serverUrl, '', 'secret_token') assert.strictEqual(msg.type, BtpPacket.TYPE_ERROR, 'expected a BTP error') assert.strictEqual(msg.data.code, 'F00') assert.strictEqual(msg.data.name, 'NotAcceptedError') assert.strictEqual(msg.data.data, 'auth_username subprotocol is required') await plugin.disconnect() }) }) }) describe('sendData', function () { beforeEach(function () { this.fulfillment = crypto.randomBytes(32) this.condition = crypto.createHash('sha256') .update(this.fulfillment) .digest() this.plugin._call = async (dest, packet) => { return { protocolData: [ { protocolName: 'ilp', contentType: BtpPacket.MIME_APPLICATION_OCTET_STREAM, data: IlpPacket.serializeIlpFulfill({ fulfillment: this.fulfillment, data: Buffer.alloc(0) }) } ] } } }) it('should return ilp reject when incorrect fulfill is returned', async function () { this.fulfillment = Buffer.alloc(32) const result = await this.plugin.sendData(IlpPacket.serializeIlpPrepare({ destination: this.from, amount: '123', executionCondition: this.condition, expiresAt: new Date(Date.now() + 10000), data: Buffer.alloc(0) })) const parsed = IlpPacket.deserializeIlpPacket(result) assert.equal(parsed.typeString, 'ilp_reject') assert.deepEqual(parsed.data, { code: 'F05', triggeredBy: 'test.example', message: `condition and fulfillment don't match. condition=${this.condition.toString('hex')} fulfillment=0000000000000000000000000000000000000000000000000000000000000000`, data: Buffer.alloc(0) }) }) it('should return ilp reject when _handlePrepareResponse throws', async function () { this.plugin._handlePrepareResponse = () => { throw new IlpPacket.Errors.UnreachableError('cannot be reached') } const result = await this.plugin.sendData(IlpPacket.serializeIlpPrepare({ destination: this.from, amount: '123', executionCondition: this.condition, expiresAt: new Date(Date.now() + 10000), data: Buffer.alloc(0) })) const parsed = IlpPacket.deserializeIlpPacket(result) assert.equal(parsed.typeString, 'ilp_reject') assert.deepEqual(parsed.data, { code: 'F02', triggeredBy: 'test.example', message: 'cannot be reached', data: Buffer.alloc(0) }) }) it('should return ilp reject when the prepare expires', async function () { this.plugin._call = () => new Promise(() => {}) const result = await this.plugin.sendData(IlpPacket.serializeIlpPrepare({ destination: this.from, amount: '123', executionCondition: this.condition, expiresAt: new Date(Date.now() + 50), data: Buffer.alloc(0) })) const parsed = IlpPacket.deserializeIlpPacket(result) assert.equal(parsed.typeString, 'ilp_reject') assert.deepEqual(parsed.data, { code: 'R00', triggeredBy: 'test.example', message: 'Packet expired', data: Buffer.alloc(0) }) }) }) }) describe('Mini Accounts http requests', function() { beforeEach(function() { this.port = 4000 this.pluginConfig = { debugHostIldcpInfo: { clientAddress: 'test.example' }, _store: new Store() } }) describe('falls back', function() { it('falls back to port 3000 if port unspecified', async function() { this.plugin = new PluginMiniAccounts({ ...this.pluginConfig, }) await this.plugin.connect() const res = await fetch('http://localhost:3000') assert.equal(res.status, 426) assert.equal(await res.text(), 'Upgrade Required') await this.plugin.disconnect() }) }) describe('health checks', function() { afterEach(async function() { await this.plugin.disconnect() }) it('fails health check when port specified, wsOpt.port unspecified', async function() { this.plugin = new PluginMiniAccounts({ ...this.pluginConfig, port: this.port, }) await this.plugin.connect() const res = await fetch(`http://localhost:${this.port}`) assert.equal(res.status, 426) assert.equal(await res.text(), 'Upgrade Required') }) it('doesnt let user specify both opts.wsOpt.port and opts.port', async function() { const constructPlugin = () => new PluginMiniAccounts({ ...this.pluginConfig, port: this.port, wsOpts: { port: this.port } }) assert.throws(constructPlugin, 'Specify at most one of: `ops.wsOpts.port`, `opts.port`.') }) it('fails health check when port unspecified, wsOpt.port specified', async function() { this.plugin = new PluginMiniAccounts({ ...this.pluginConfig, wsOpts: { port: this.port }, }) await this.plugin.connect() const res = await fetch(`http://localhost:${this.port}`) assert.equal(res.status, 426) assert.equal(await res.text(), 'Upgrade Required') }) it('passes health check when port specified, wsOpts.server specified', async function() { const httpServer = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/html' }) res.write('ok') res.end() }) this.plugin = new PluginMiniAccounts({ port: this.port, wsOpts: { server: httpServer }, ...this.pluginConfig }) await this.plugin.connect() const res = await fetch(`http://localhost:${this.port}`) assert.equal(res.status, 200) assert.equal(await res.text(), 'ok') }) }) })