Artifact Content
Not logged in

Artifact da8378602ae9b3c270123fac6c1930f680448d62:


'use strict'

const IlpPacket = require('ilp-packet')
const chai = require('chai')
const agent = require('superagent')
const chaiAsPromised = require('chai-as-promised')
chai.use(chaiAsPromised)
const expect = chai.expect
const assert = chai.assert
const MockPlugin = require('./mocks/mockPlugin')
const nock = require('nock')
const SPSP = require('../src/lib/spsp')
const base64url = require('../src/utils/base64url')
const cryptoHelper = require('../src/utils/crypto')
const webfinger = {
  links: [{
    rel: 'https://interledger.org/rel/spsp/v2',
    href: 'https://example.com/spsp'
  }]
}
const spspResponse = {
  // make sure it accepts a shared secret generated by cryptoHelper
  shared_secret: base64url(
    cryptoHelper.getPskSharedSecret(
      Buffer.from('secret'),
      Buffer.from('token')
    )
  ),
  destination_account: 'test.other.alice',
  maximum_destination_amount: '20',
  minimum_destination_amount: '10',
  receiver_info: {},
  ledger_info: {
    currency_code: 'USD',
    currency_scale: 2
  }
}

describe('SPSP', function () {
  beforeEach(function () {
    this.quoteRequestCalled = false
    this.quoteSourceAmountCalled = false
    this.plugin = new MockPlugin()
  })

  afterEach(function () {
    assert.isTrue(nock.isDone())
  })

  describe('query', function () {
    it('should query the right endpoints', async function () {
      nock('https://example.com')
        .get('/.well-known/webfinger?resource=acct:alice@example.com')
        .reply(200, webfinger)

      nock('https://example.com')
        .get('/spsp')
        .reply(200, spspResponse)

      expect(await SPSP.query('alice@example.com'))
        .to.deep.equal(spspResponse)
    })
  })

  describe('quote', function () {
    beforeEach(function () {
      this.plugin.dataHandler = (msg) => {
        return IlpPacket.serializeIlqpByDestinationResponse({
          sourceAmount: '1',
          sourceHoldDuration: 10000
        })
      }

      this.id = '622d0846-2063-45c3-9dc0-ddf5182f833c'
      this.result = {
        destinationAccount: 'test.other.alice',
        sourceExpiryDuration: '10',
        // amounts are converted according to src and dest scales of 2
        sourceScale: 2,
        sourceAmount: '0.01',
        destinationAmount: '0.12',
        id: this.id,
        spsp: spspResponse
      }

      this.params = {
        receiver: 'alice@example.com',
        // amounts are converted according to src and dest scales of 2
        destinationAmount: '0.12',
        sourceScale: 2,
        timeout: 200,
        id: this.id
      }
    })

    it('should return a valid SPSP payment', async function () {
      nock('https://example.com')
        .get('/.well-known/webfinger?resource=acct:alice@example.com')
        .reply(200, webfinger)

      nock('https://example.com')
        .get('/spsp')
        .reply(200, spspResponse)

      const payment = await SPSP.quote(this.plugin, this.params)
      assert.deepEqual(payment, this.result)
    })

    it('should accept an SPSP response as a parameter', async function () {
      // an error will be occurred if this nock is called
      nock('https://example.com')
        .get('/spsp')
        .reply(200, { garbage: 'trash' })

      this.params.spspResponse = spspResponse
      const payment = await SPSP.quote(this.plugin, this.params)
      assert.deepEqual(payment, this.result)

      // get the nock to clean it up
      await agent.get('https://example.com/spsp')
    })

    it('should return an error if spspResponse is invalid', async function () {
      nock('https://example.com')
        .get('/.well-known/webfinger?resource=acct:alice@example.com')
        .reply(200, webfinger)

      const badResponse = Object.assign({},
        spspResponse,
        { shared_secret: 'garbage' })

      nock('https://example.com')
        .get('/spsp')
        .reply(200, badResponse)

      await expect(SPSP.quote(this.plugin, this.params))
        .to.eventually.be.rejectedWith(/shared_secret must be 16 bytes/)
    })

    it('should return an error if webfinger can\'t be reached', async function () {
      nock('https://example.com')
        .get('/.well-known/webfinger?resource=acct:alice@example.com')
        .reply(404)

      await expect(SPSP.quote(this.plugin, this.params))
        .to.eventually.be.rejectedWith(/Not Found/)
    })

    it('should return an error if webfinger is missing fields', async function () {
      nock('https://example.com')
        .get('/.well-known/webfinger?resource=acct:alice@example.com')
        .reply(200, {links: []})

      await expect(SPSP.quote(this.plugin, this.params))
        .to.eventually.be.rejectedWith(/spsp\/v2 not found/)
    })

    it('should fail without an amount', async function () {
      delete this.params.destinationAmount
      await expect(SPSP.quote(this.plugin, this.params))
        .to.eventually.be
        .rejectedWith(/destinationAmount or sourceAmount must be specified/)
    })

    describe('sendPayment', function () {
      beforeEach(async function () {
        nock('https://example.com')
          .get('/.well-known/webfinger?resource=acct:alice@example.com')
          .reply(200, webfinger)

        nock('https://example.com')
          .get('/spsp')
          .reply(200, spspResponse)

        this.payment = await SPSP.quote(this.plugin, this.params)
      })

      it('should successfully send a payment', async function () {
        const nextDataHandler = this.plugin.dataHandler
        this.plugin.dataHandler = (packet) => {
          if (packet[0] === IlpPacket.Type.TYPE_ILP_PREPARE) {
            return IlpPacket.serializeIlpFulfill({
              fulfillment: Buffer.alloc(32),
              data: Buffer.alloc(0)
            })
          } else {
            return nextDataHandler(packet)
          }
        }

        const result = await SPSP.sendPayment(this.plugin, this.payment)
        expect(result).to.deep.equal({ fulfillment: Buffer.alloc(32), data: Buffer.alloc(0) })
      })

      it('should reject if payment is rejected', async function () {
        const nextDataHandler = this.plugin.dataHandler
        this.plugin.dataHandler = (packet) => {
          if (packet[0] === IlpPacket.Type.TYPE_ILP_PREPARE) {
            return IlpPacket.serializeIlpReject({
              code: '123',
              message: 'bad payment.',
              triggeredBy: 'test.grumpy-connector',
              data: Buffer.alloc(0)
            })
          } else {
            return nextDataHandler(packet)
          }
        }

        await expect(SPSP.sendPayment(this.plugin, this.payment))
          .to.eventually.be.rejectedWith(/transfer .* failed: payment rejected: bad payment/)
      })

      it('should reject if sendData throws', async function () {
        const nextDataHandler = this.plugin.dataHandler
        this.plugin.dataHandler = (packet) => {
          if (packet[0] === IlpPacket.Type.TYPE_ILP_PREPARE) {
            throw new Error('not feeling like it.')
          } else {
            return nextDataHandler(packet)
          }
        }

        await expect(SPSP.sendPayment(this.plugin, this.payment))
          .to.eventually.be.rejectedWith(/transfer .* failed: not feeling like it./)
      })
    })
  })
})