Add test runner for RippleAPI, begin to break up large test file

This commit is contained in:
Fred K. Schott
2019-11-10 13:23:52 -08:00
parent a98526b398
commit b77a12fd0d
11 changed files with 704 additions and 488 deletions

View File

@@ -0,0 +1,91 @@
import assert from 'assert-diff'
import { assertResultMatch, TestSuite } from '../utils'
import responses from '../../fixtures/responses'
const { getLedger: RESPONSE_FIXTURES } = responses
/**
* Every test suite exports their tests in the default object.
* - Check out the "TestSuite" type for documentation on the interface.
* - Check out "test/api/index.ts" for more information about the test runner.
*/
export default <TestSuite>{
'simple test': async api => {
const response = await api.getLedger()
assertResultMatch(response, RESPONSE_FIXTURES.header, 'getLedger')
},
'by hash': async api => {
const response = await api.getLedger({
ledgerHash:
'15F20E5FA6EA9770BBFFDBD62787400960B04BE32803B20C41F117F41C13830D'
})
assertResultMatch(response, RESPONSE_FIXTURES.headerByHash, 'getLedger')
},
'future ledger version': async api => {
const response = await api.getLedger({ ledgerVersion: 14661789 })
assert(!!response)
},
'with state as hashes': async api => {
const request = {
includeTransactions: true,
includeAllData: false,
includeState: true,
ledgerVersion: 6
}
const response = await api.getLedger(request)
assertResultMatch(
response,
RESPONSE_FIXTURES.withStateAsHashes,
'getLedger'
)
},
'with settings transaction': async api => {
const request = {
includeTransactions: true,
includeAllData: true,
ledgerVersion: 4181996
}
const response = await api.getLedger(request)
assertResultMatch(response, RESPONSE_FIXTURES.withSettingsTx, 'getLedger')
},
'with partial payment': async api => {
const request = {
includeTransactions: true,
includeAllData: true,
ledgerVersion: 22420574
}
const response = await api.getLedger(request)
assertResultMatch(response, RESPONSE_FIXTURES.withPartial, 'getLedger')
},
'pre 2014 with partial payment': async api => {
const request = {
includeTransactions: true,
includeAllData: true,
ledgerVersion: 100001
}
const response = await api.getLedger(request)
assertResultMatch(
response,
RESPONSE_FIXTURES.pre2014withPartial,
'getLedger'
)
},
'full, then computeLedgerHash': async api => {
const request = {
includeTransactions: true,
includeState: true,
includeAllData: true,
ledgerVersion: 38129
}
const response = await api.getLedger(request)
assertResultMatch(response, RESPONSE_FIXTURES.full, 'getLedger')
const ledger = {
...response,
parentCloseTime: response.closeTime
}
const hash = api.computeLedgerHash(ledger, { computeTreeHashes: true })
assert.strictEqual(
hash,
'E6DB7365949BF9814D76BCC730B01818EB9136A89DB224F3F9F5AAE4569D758E'
)
}
}

View File

@@ -0,0 +1,95 @@
import assert from 'assert-diff'
import { assertResultMatch, assertRejects, TestSuite } from '../utils'
import responses from '../../fixtures/responses'
import requests from '../../fixtures/requests'
import addresses from '../../fixtures/addresses.json'
const { getPaths: REQUEST_FIXTURES } = requests
const { getPaths: RESPONSE_FIXTURES } = responses
/**
* Every test suite exports their tests in the default object.
* - Check out the "TestSuite" type for documentation on the interface.
* - Check out "test/api/index.ts" for more information about the test runner.
*/
export default <TestSuite>{
'simple test': async api => {
const response = await api.getPaths(REQUEST_FIXTURES.normal)
assertResultMatch(response, RESPONSE_FIXTURES.XrpToUsd, 'getPaths')
},
'queuing': async api => {
const [normalResult, usdOnlyResult, xrpOnlyResult] = await Promise.all([
api.getPaths(REQUEST_FIXTURES.normal),
api.getPaths(REQUEST_FIXTURES.UsdToUsd),
api.getPaths(REQUEST_FIXTURES.XrpToXrp)
])
assertResultMatch(normalResult, RESPONSE_FIXTURES.XrpToUsd, 'getPaths')
assertResultMatch(usdOnlyResult, RESPONSE_FIXTURES.UsdToUsd, 'getPaths')
assertResultMatch(xrpOnlyResult, RESPONSE_FIXTURES.XrpToXrp, 'getPaths')
},
// @TODO
// need decide what to do with currencies/XRP:
// if add 'XRP' in currencies, then there will be exception in
// xrpToDrops function (called from toRippledAmount)
'getPaths USD 2 USD': async api => {
const response = await api.getPaths(REQUEST_FIXTURES.UsdToUsd)
assertResultMatch(response, RESPONSE_FIXTURES.UsdToUsd, 'getPaths')
},
'getPaths XRP 2 XRP': async api => {
const response = await api.getPaths(REQUEST_FIXTURES.XrpToXrp)
assertResultMatch(response, RESPONSE_FIXTURES.XrpToXrp, 'getPaths')
},
'source with issuer': async api => {
return assertRejects(
api.getPaths(REQUEST_FIXTURES.issuer),
api.errors.NotFoundError
)
},
'XRP 2 XRP - not enough': async api => {
return assertRejects(
api.getPaths(REQUEST_FIXTURES.XrpToXrpNotEnough),
api.errors.NotFoundError
)
},
'invalid PathFind': async api => {
assert.throws(() => {
api.getPaths(REQUEST_FIXTURES.invalid)
}, /Cannot specify both source.amount/)
},
'does not accept currency': async api => {
return assertRejects(
api.getPaths(REQUEST_FIXTURES.NotAcceptCurrency),
api.errors.NotFoundError
)
},
'no paths': async api => {
return assertRejects(
api.getPaths(REQUEST_FIXTURES.NoPaths),
api.errors.NotFoundError
)
},
'no paths source amount': async api => {
return assertRejects(
api.getPaths(REQUEST_FIXTURES.NoPathsSource),
api.errors.NotFoundError
)
},
'no paths with source currencies': async api => {
return assertRejects(
api.getPaths(REQUEST_FIXTURES.NoPathsWithCurrencies),
api.errors.NotFoundError
)
},
'error: srcActNotFound': async api => {
return assertRejects(
api.getPaths({
...REQUEST_FIXTURES.normal,
source: { address: addresses.NOTFOUND }
}),
api.errors.RippleError
)
},
'send all': async api => {
const response = await api.getPaths(REQUEST_FIXTURES.sendAll)
assertResultMatch(response, RESPONSE_FIXTURES.sendAll, 'getPaths')
}
}

67
test/api/index.ts Normal file
View File

@@ -0,0 +1,67 @@
import setupAPI from '../setup-api'
import { RippleAPI } from 'ripple-api'
import addresses from '../fixtures/addresses.json'
import { getAllPublicMethods, loadTestSuite } from './utils'
/**
* RippleAPI Test Runner
*
* Background: "test/api-test.ts" had hit 4000+ lines of test code and 300+
* individual tests. Additionally, a new address format was added which
* forced us to copy-paste duplicate the test file to test both the old forms
* of address. This added a significant maintenance burden.
*
* This test runner allows us to split our tests by RippleAPI method, and
* automatically load, validate, and run them. Each tests accepts arguments to
* test with, which allows us to re-run tests across different data
* (ex: different address styles).
*
* Additional benefits:
* - Throw errors when we detect the absence of tests.
* - Type the API object under test and catch typing issues (currently untyped).
* - Sets the stage for more cleanup, like moving test-specific fixtures closer to their tests.
*/
describe('RippleAPI [Test Runner]', function() {
beforeEach(setupAPI.setup)
afterEach(setupAPI.teardown)
// Collect all the tests:
const allPublicMethods = getAllPublicMethods(new RippleAPI())
const allTestSuites = allPublicMethods.map(loadTestSuite)
// TODO: Once migration is complete, remove this filter so that missing tests are reported.
const filteredTestSuites = allTestSuites.filter(({ isMissing }) => !isMissing)
// Run all the tests:
for (const { name: suiteName, tests, isMissing } of filteredTestSuites) {
describe(suiteName, () => {
// Check that tests exist as expected, and report any errors if they don't.
it('has valid test suite', () => {
if (isMissing) {
throw new Error(
`Test file not found! Create file "test/api/${suiteName}/index.ts".`
)
}
if (tests.length === 0) {
throw new Error(`No tests found! Is your test file set up properly?`)
}
})
// Run each test with the original-style address.
describe(`1. Original Address Style`, () => {
for (const [testName, fn] of tests) {
it(testName, function() {
return fn(this.api, addresses.ACCOUNT)
})
}
})
// Run each test with the newer, x-address style.
describe(`2. X-Address Style`, () => {
for (const [testName, fn] of tests) {
it(testName, function() {
return fn(this.api, addresses.ACCOUNT_X)
})
}
})
})
}
})

View File

@@ -0,0 +1,238 @@
import assert from 'assert-diff'
import requests from '../../fixtures/requests'
import responses from '../../fixtures/responses'
import { assertResultMatch, TestSuite } from '../utils'
const instructionsWithMaxLedgerVersionOffset = { maxLedgerVersionOffset: 100 }
/**
* Every test suite exports their tests in the default object.
* - Check out the "TestSuite" type for documentation on the interface.
* - Check out "test/api/index.ts" for more information about the test runner.
*/
export default <TestSuite>{
'simple test': async (api, address) => {
const response = await api.prepareSettings(
address,
requests.prepareSettings.domain,
instructionsWithMaxLedgerVersionOffset
)
assertResultMatch(response, responses.prepareSettings.flags, 'prepare')
},
'no maxLedgerVersion': async (api, address) => {
const response = await api.prepareSettings(
address,
requests.prepareSettings.domain,
{
maxLedgerVersion: null
}
)
assertResultMatch(
response,
responses.prepareSettings.noMaxLedgerVersion,
'prepare'
)
},
'no instructions': async (api, address) => {
const response = await api.prepareSettings(
address,
requests.prepareSettings.domain
)
assertResultMatch(
response,
responses.prepareSettings.noInstructions,
'prepare'
)
},
'regularKey': async (api, address) => {
const regularKey = { regularKey: 'rAR8rR8sUkBoCZFawhkWzY4Y5YoyuznwD' }
const response = await api.prepareSettings(
address,
regularKey,
instructionsWithMaxLedgerVersionOffset
)
assertResultMatch(response, responses.prepareSettings.regularKey, 'prepare')
},
'remove regularKey': async (api, address) => {
const regularKey = { regularKey: null }
const response = await api.prepareSettings(
address,
regularKey,
instructionsWithMaxLedgerVersionOffset
)
assertResultMatch(
response,
responses.prepareSettings.removeRegularKey,
'prepare'
)
},
'flag set': async (api, address) => {
const settings = { requireDestinationTag: true }
const response = await api.prepareSettings(
address,
settings,
instructionsWithMaxLedgerVersionOffset
)
assertResultMatch(response, responses.prepareSettings.flagSet, 'prepare')
},
'flag clear': async (api, address) => {
const settings = { requireDestinationTag: false }
const response = await api.prepareSettings(
address,
settings,
instructionsWithMaxLedgerVersionOffset
)
assertResultMatch(response, responses.prepareSettings.flagClear, 'prepare')
},
'set depositAuth flag': async (api, address) => {
const settings = { depositAuth: true }
const response = await api.prepareSettings(
address,
settings,
instructionsWithMaxLedgerVersionOffset
)
assertResultMatch(
response,
responses.prepareSettings.flagSetDepositAuth,
'prepare'
)
},
'clear depositAuth flag': async (api, address) => {
const settings = { depositAuth: false }
const response = await api.prepareSettings(
address,
settings,
instructionsWithMaxLedgerVersionOffset
)
assertResultMatch(
response,
responses.prepareSettings.flagClearDepositAuth,
'prepare'
)
},
'integer field clear': async (api, address) => {
const settings = { transferRate: null }
const response = await api.prepareSettings(
address,
settings,
instructionsWithMaxLedgerVersionOffset
)
assert(response)
assert.strictEqual(JSON.parse(response.txJSON).TransferRate, 0)
},
'set transferRate': async (api, address) => {
const settings = { transferRate: 1 }
const response = await api.prepareSettings(
address,
settings,
instructionsWithMaxLedgerVersionOffset
)
assertResultMatch(
response,
responses.prepareSettings.setTransferRate,
'prepare'
)
},
'set signers': async (api, address) => {
const settings = requests.prepareSettings.signers.normal
const response = await api.prepareSettings(
address,
settings,
instructionsWithMaxLedgerVersionOffset
)
assertResultMatch(response, responses.prepareSettings.signers, 'prepare')
},
'signers no threshold': async (api, address) => {
const settings = requests.prepareSettings.signers.noThreshold
try {
const response = await api.prepareSettings(
address,
settings,
instructionsWithMaxLedgerVersionOffset
)
throw new Error(
'Expected method to reject. Prepared transaction: ' +
JSON.stringify(response)
)
} catch (err) {
assert.strictEqual(
err.message,
'instance.settings.signers requires property "threshold"'
)
assert.strictEqual(err.name, 'ValidationError')
}
},
'signers no weights': async (api, address) => {
const settings = requests.prepareSettings.signers.noWeights
const localInstructions = {
signersCount: 1,
...instructionsWithMaxLedgerVersionOffset
}
const response = await api.prepareSettings(
address,
settings,
localInstructions
)
assertResultMatch(response, responses.prepareSettings.noWeights, 'prepare')
},
'fee for multisign': async (api, address) => {
const localInstructions = {
signersCount: 4,
...instructionsWithMaxLedgerVersionOffset
}
const response = await api.prepareSettings(
address,
requests.prepareSettings.domain,
localInstructions
)
assertResultMatch(
response,
responses.prepareSettings.flagsMultisign,
'prepare'
)
},
'no signer list': async (api, address) => {
const settings = requests.prepareSettings.noSignerEntries
const localInstructions = {
signersCount: 1,
...instructionsWithMaxLedgerVersionOffset
}
const response = await api.prepareSettings(
address,
settings,
localInstructions
)
assertResultMatch(
response,
responses.prepareSettings.noSignerList,
'prepare'
)
},
'invalid': async (api, address) => {
// domain must be a string
const settings = Object.assign({}, requests.prepareSettings.domain, {
domain: 123
})
const localInstructions = {
signersCount: 4,
...instructionsWithMaxLedgerVersionOffset
}
try {
const response = await api.prepareSettings(
address,
settings,
localInstructions
)
throw new Error(
'Expected method to reject. Prepared transaction: ' +
JSON.stringify(response)
)
} catch (err) {
assert.strictEqual(
err.message,
'instance.settings.domain is not of a type(s) string'
)
assert.strictEqual(err.name, 'ValidationError')
}
}
}

103
test/api/utils.ts Normal file
View File

@@ -0,0 +1,103 @@
import _ from 'lodash'
import { RippleAPI } from 'ripple-api'
import assert from 'assert-diff'
const { schemaValidator } = RippleAPI._PRIVATE
/**
* The test function. It takes a RippleAPI object and then some other data to
* test (currently: an address). May be called multiple times with different
* arguments, to test different types of data.
*/
export type TestFn = (
api: RippleAPI,
address: string
) => void | PromiseLike<void>
/**
* A suite of tests to run. Maps the test name to the test function.
*/
export interface TestSuite {
[testName: string]: TestFn
}
/**
* When the test suite is loaded, we represent it with the following
* data structure containing tests and metadata about the suite.
* If no test suite exists, we return this object with `isMissing: true`
* so that we can report it.
*/
interface LoadedTestSuite {
name: string
tests: [string, TestFn][]
isMissing: boolean
}
/**
* Check the response against the expected result. Optionally validate
* that response against a given schema as well.
*/
export function assertResultMatch(
response: any,
expected: any,
schemaName?: string
) {
if (expected.txJSON) {
assert(response.txJSON)
assert.deepEqual(
JSON.parse(response.txJSON),
JSON.parse(expected.txJSON),
'checkResult: txJSON must match'
)
}
if (expected.tx_json) {
assert(response.tx_json)
assert.deepEqual(
response.tx_json,
expected.tx_json,
'checkResult: tx_json must match'
)
}
assert.deepEqual(
_.omit(response, ['txJSON', 'tx_json']),
_.omit(expected, ['txJSON', 'tx_json'])
)
if (schemaName) {
schemaValidator.schemaValidate(schemaName, response)
}
}
/**
* Check that the promise rejects with an expected error instance.
*/
export async function assertRejects(
promise: PromiseLike<any>,
instanceOf: any
) {
try {
await promise
assert(false, 'Expected an error to be thrown')
} catch (error) {
assert(error instanceof instanceOf)
}
}
export function getAllPublicMethods(api: RippleAPI) {
return Object.keys(api).filter(key => !key.startsWith('_'))
}
export function loadTestSuite(methodName: string): LoadedTestSuite | null {
try {
const testSuite = require(`./${methodName}`)
return {
isMissing: false,
name: methodName,
tests: Object.entries(testSuite.default || {}),
}
} catch (err) {
return {
isMissing: true,
name: methodName,
tests: [],
}
}
}

View File

@@ -0,0 +1,104 @@
import assert from 'assert-diff'
import BigNumber from 'bignumber.js'
import { TestSuite } from '../utils'
/**
* Every test suite exports their tests in the default object.
* - Check out the "TestSuite" type for documentation on the interface.
* - Check out "test/api/index.ts" for more information about the test runner.
*/
export default <TestSuite>{
'works with a typical amount': function(api) {
const drops = api.xrpToDrops('2')
assert.strictEqual(drops, '2000000', '2 XRP equals 2 million drops')
},
'works with fractions': function(api) {
let drops = api.xrpToDrops('3.456789')
assert.strictEqual(drops, '3456789', '3.456789 XRP equals 3,456,789 drops')
drops = api.xrpToDrops('3.400000')
assert.strictEqual(drops, '3400000', '3.400000 XRP equals 3,400,000 drops')
drops = api.xrpToDrops('0.000001')
assert.strictEqual(drops, '1', '0.000001 XRP equals 1 drop')
drops = api.xrpToDrops('0.0000010')
assert.strictEqual(drops, '1', '0.0000010 XRP equals 1 drop')
},
'works with zero': function(api) {
let drops = api.xrpToDrops('0')
assert.strictEqual(drops, '0', '0 XRP equals 0 drops')
drops = api.xrpToDrops('-0') // negative zero is equivalent to zero
assert.strictEqual(drops, '0', '-0 XRP equals 0 drops')
drops = api.xrpToDrops('0.000000')
assert.strictEqual(drops, '0', '0.000000 XRP equals 0 drops')
drops = api.xrpToDrops('0.0000000')
assert.strictEqual(drops, '0', '0.0000000 XRP equals 0 drops')
},
'works with a negative value': function(api) {
const drops = api.xrpToDrops('-2')
assert.strictEqual(drops, '-2000000', '-2 XRP equals -2 million drops')
},
'works with a value ending with a decimal point': function(api) {
let drops = api.xrpToDrops('2.')
assert.strictEqual(drops, '2000000', '2. XRP equals 2000000 drops')
drops = api.xrpToDrops('-2.')
assert.strictEqual(drops, '-2000000', '-2. XRP equals -2000000 drops')
},
'works with BigNumber objects': function(api) {
let drops = api.xrpToDrops(new BigNumber(2))
assert.strictEqual(
drops,
'2000000',
'(BigNumber) 2 XRP equals 2 million drops'
)
drops = api.xrpToDrops(new BigNumber(-2))
assert.strictEqual(
drops,
'-2000000',
'(BigNumber) -2 XRP equals -2 million drops'
)
},
'works with a number': function(api) {
// This is not recommended. Use strings or BigNumber objects to avoid precision errors.
let drops = api.xrpToDrops(2)
assert.strictEqual(
drops,
'2000000',
'(number) 2 XRP equals 2 million drops'
)
drops = api.xrpToDrops(-2)
assert.strictEqual(
drops,
'-2000000',
'(number) -2 XRP equals -2 million drops'
)
},
'throws with an amount with too many decimal places': function(api) {
assert.throws(() => {
api.xrpToDrops('1.1234567')
}, /has too many decimal places/)
assert.throws(() => {
api.xrpToDrops('0.0000001')
}, /has too many decimal places/)
},
'throws with an invalid value': function(api) {
assert.throws(() => {
api.xrpToDrops('FOO')
}, /invalid value/)
assert.throws(() => {
api.xrpToDrops('1e-7')
}, /invalid value/)
assert.throws(() => {
api.xrpToDrops('2,0')
}, /invalid value/)
assert.throws(() => {
api.xrpToDrops('.')
}, /xrpToDrops: invalid value '\.', should be a BigNumber or string-encoded number\./)
},
'throws with an amount more than one decimal point': function(api) {
assert.throws(() => {
api.xrpToDrops('1.0.0')
}, /xrpToDrops: invalid value '1\.0\.0'/)
assert.throws(() => {
api.xrpToDrops('...')
}, /xrpToDrops: invalid value '\.\.\.'/)
}
}