Files
xahau.js/packages/xahau/test/testUtils.ts
Denis Angell 9544e1794e xahau-patch
2025-03-14 15:08:35 +01:00

223 lines
7.8 KiB
TypeScript

/* eslint-disable @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- required for
assertions. */
import net from 'net'
import { assert } from 'chai'
import omit from 'lodash/omit'
import { rippleTimeToUnixTime, unixTimeToRippleTime } from '../src'
import addresses from './fixtures/addresses.json'
/**
* Setup to run tests on both classic addresses and X-addresses.
*/
export const addressTests = [
{ type: 'Classic Address', address: addresses.ACCOUNT },
{ type: 'X-Address', address: addresses.ACCOUNT_X },
]
/**
* Check the response against the expected result. Optionally validate
* that response against a given schema as well.
*
* @param response - Response received from the method.
* @param expected - Expected response from the method.
* @param _schemaName - Name of the schema used to validate the shape of the response.
*/
export function assertResultMatch(
response: any,
expected: any,
_schemaName?: string,
): void {
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']),
)
}
/**
* Check that the promise rejects with an expected error.
*
* @param promise - The promise returned by the method.
* @param instanceOf - Expected error type that the method will throw.
* @param message - Expected error message/substring of the error message.
*/
export async function assertRejects(
promise: PromiseLike<unknown>,
instanceOf: any,
message?: string | RegExp,
): Promise<void> {
try {
await promise
assert(false, 'Expected an error to be thrown')
} catch (error) {
if (!(error instanceof Error)) {
throw error
}
assert(error instanceof instanceOf, error.message)
if (typeof message === 'string') {
assert.strictEqual(error.message, message, 'Messages do not match')
} else if (message instanceof RegExp) {
assert(message.test(error.message))
}
}
}
const lastSocketKeyMap: { [port: string]: number } = {}
const socketMap: {
[port: string]:
| {
server: net.Server
sockets: { [socketKey: string]: net.Socket }
}
| undefined
} = {}
export async function destroyServer(port: number): Promise<void> {
// loop through all sockets and destroy them
if (socketMap[port]) {
Object.keys(socketMap[port]!.sockets).forEach(function (socketKey) {
socketMap[port]!.sockets[socketKey].destroy()
})
}
return new Promise((resolve, reject) => {
if (socketMap[port]) {
// after all the sockets are destroyed, we may close the server!
socketMap[port]!.server.close((error) => {
if (error) {
reject(error)
return
}
resolve()
})
} else {
resolve()
}
})
}
// using a free port instead of a constant port enables parallelization
export async function getFreePort(): Promise<number> {
return new Promise((resolve, reject) => {
const server = net.createServer()
let port: number
server.on('listening', function () {
port = (server.address() as net.AddressInfo).port
server.close()
})
server.on('close', function () {
resolve(port)
})
server.on('error', function (error) {
reject(error)
})
const listener = server.listen(0)
// Keep track of all connections so we can destroy them at the end of the test
// This will prevent Jest from having open handles when all tests are done
listener.on('connection', (socket) => {
// generate a new, unique socket-key
lastSocketKeyMap[port] += 1
const lastSocketKey = lastSocketKeyMap[port]
// add socket when it is connected
if (socketMap[port]) {
socketMap[port]!.sockets[lastSocketKey] = socket
} else {
socketMap[port] = { sockets: { [lastSocketKey]: socket }, server }
}
socket.on('close', () => {
// remove socket when it is closed
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- Necessary to delete key
delete socketMap[port]!.sockets[lastSocketKey]
})
})
})
}
/**
* Ignore WebSocket DisconnectErrors. Useful for making requests where we don't
* care about the response and plan to teardown the test before the response
* has come back.
*
* @param error - Thrown error.
* @throws If error is not websocket disconnect error.
*/
export function ignoreWebSocketDisconnect(error: Error): void {
if (error.message === 'websocket was closed') {
return
}
throw error
}
/**
* Attempts to log information about how far off the current time is from the last ledger close time.
* This is useful for debugging ledger close time issues when sending multiple ledgerAccept requests too quickly.
* If you send multiple requests in the span of a single second, the ledger can end up with a close time well into the future.
* See https://xrpl.org/ledgers.html#ledger-close-times for more information.
* The time that a ledger version closed is recorded at the close_time field of the ledger header. To make it easier for
* the network to reach a consensus on an exact close time, this value is rounded to a number of seconds based on the
* close time resolution, currently 10 seconds. If rounding would cause a ledger's close time to be the same as (or earlier than)
* its parent ledger's, the child ledger has its close time set to the parent's close time plus 1. This guarantees that the
* close times of validated ledgers are strictly increasing.
*
* Since new ledger versions usually close about every 3 to 5 seconds,
* these rules result in a loose pattern where ledgers' close times end in :00, :01, :02, :10, :11, :20, :21, and so on.
* Times ending in 2 are less common and times ending in 3 are very rare, but both occur randomly when more ledgers randomly
* happen to close within a 10-second window.
*
* Generally speaking, the ledger cannot make any time-based measurements that are more precise than the close time resolution.
* For example, to check if an object has passed an expiration date, the rule is to compare it to the close time of the parent
* ledger. (The close time of a ledger is not yet known when executing transactions to go into that ledger.) This means that,
* for example, an Escrow could successfully finish at a real-world time that is up to about 10 seconds later than the time-based
* expiration specified in the Escrow object.
*
*
* @param closeTime - ledger close time in xahau time
* @returns The difference between last ledger close time and current time in seconds
*/
export function debugPrintLedgerTime(closeTime: number): number {
const closeTimeUnix = rippleTimeToUnixTime(closeTime)
const closeTimeDate = new Date()
closeTimeDate.setTime(closeTimeUnix * 1000)
const currentTimeUnix = Math.floor(new Date().getTime())
const currentTimeRipple = unixTimeToRippleTime(currentTimeUnix)
const currentTimeDate = new Date()
currentTimeDate.setTime(currentTimeUnix * 1000)
// eslint-disable-next-line no-console -- Intentional debugging function
console.log(
`closeTime (xahau): ${closeTime}\n`,
`closeTime (unix): ${closeTimeUnix}\n`,
`closeTime (date): \n`,
closeTimeDate,
`\n`,
`currentTime (xahau): ${currentTimeRipple}\n`,
`currentTime (unix): ${currentTimeUnix}\n`,
`currentTime (date): \n`,
currentTimeDate,
`\n`,
`diff (current - close) (unix): ${currentTimeUnix - closeTimeUnix}`,
`diff (current - close) (xahau): ${currentTimeRipple - closeTime}`,
)
return currentTimeRipple - closeTime
}