e751b6569a
- Add tests for wallet-store.js (40 tests: MemoryStorage, WalletOutput, WalletTransaction) - Add tests for wallet-sync.js (25 tests: sync engine, events, progress tracking) - Add tests for query.js (45 tests: OutputQuery, TxQuery, TransferQuery, presets) - Add tests for connection-manager.js (29 tests: ConnectionInfo, failover, events) - Add tests for offline.js (29 tests: unsigned/signed tx, key images, output export) - Add tests for multisig.js (39 tests: KEX, signers, accounts, tx sets) - Add tests for persistent-wallet.js (27 tests: lifecycle, balance, outputs, sync) - Update test/all.js to include new test suites - Rename SYNC_UNLOCK_BLOCKS to avoid export conflict with wallet.js
489 lines
15 KiB
JavaScript
489 lines
15 KiB
JavaScript
#!/usr/bin/env bun
|
|
/**
|
|
* Query System Tests
|
|
*
|
|
* Tests for query.js:
|
|
* - OutputQuery class
|
|
* - TxQuery class
|
|
* - TransferQuery class
|
|
* - Query factory functions
|
|
* - Query preset functions
|
|
*/
|
|
|
|
import {
|
|
OutputQuery,
|
|
TxQuery,
|
|
TransferQuery,
|
|
createOutputQuery,
|
|
createTxQuery,
|
|
createTransferQuery,
|
|
unspentOutputs,
|
|
spentOutputs,
|
|
lockedOutputs,
|
|
unlockedOutputs,
|
|
stakingOutputs,
|
|
yieldOutputs,
|
|
incomingTxs,
|
|
outgoingTxs,
|
|
pendingTxs,
|
|
confirmedTxs,
|
|
stakingTxs,
|
|
yieldTxs
|
|
} from '../src/query.js';
|
|
|
|
import { TX_TYPE } from '../src/wallet.js';
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(` ✓ ${name}`);
|
|
passed++;
|
|
} catch (e) {
|
|
console.log(` ✗ ${name}`);
|
|
console.log(` Error: ${e.message}`);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
function assert(condition, message) {
|
|
if (!condition) throw new Error(message || 'Assertion failed');
|
|
}
|
|
|
|
function assertEqual(actual, expected, message) {
|
|
if (actual !== expected) {
|
|
throw new Error(message || `Expected ${expected}, got ${actual}`);
|
|
}
|
|
}
|
|
|
|
function assertDeepEqual(actual, expected, message) {
|
|
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
throw new Error(message || `Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
}
|
|
}
|
|
|
|
console.log('=== Query System Tests ===\n');
|
|
|
|
// ============================================================================
|
|
// OutputQuery Tests
|
|
// ============================================================================
|
|
|
|
console.log('--- OutputQuery ---');
|
|
|
|
test('creates with default values', () => {
|
|
const query = new OutputQuery();
|
|
assertEqual(query.isSpent, null);
|
|
assertEqual(query.isFrozen, null);
|
|
assertEqual(query.isLocked, null);
|
|
assertEqual(query.assetType, null);
|
|
assertEqual(query.minAmount, null);
|
|
assertEqual(query.maxAmount, null);
|
|
});
|
|
|
|
test('creates with provided config', () => {
|
|
const query = new OutputQuery({
|
|
isSpent: false,
|
|
isFrozen: false,
|
|
assetType: 'SAL',
|
|
minAmount: 1000n,
|
|
maxAmount: 10000n,
|
|
accountIndex: 0,
|
|
subaddressIndices: [0, 1, 2]
|
|
});
|
|
|
|
assertEqual(query.isSpent, false);
|
|
assertEqual(query.isFrozen, false);
|
|
assertEqual(query.assetType, 'SAL');
|
|
assertEqual(query.minAmount, 1000n);
|
|
assertEqual(query.maxAmount, 10000n);
|
|
assertEqual(query.accountIndex, 0);
|
|
assertDeepEqual(query.subaddressIndices, [0, 1, 2]);
|
|
});
|
|
|
|
test('matches unspent output', () => {
|
|
const query = new OutputQuery({ isSpent: false });
|
|
const output = { isSpent: false, amount: 1000n };
|
|
|
|
assert(query.matches(output), 'Should match unspent output');
|
|
});
|
|
|
|
test('rejects spent output when querying unspent', () => {
|
|
const query = new OutputQuery({ isSpent: false });
|
|
const output = { isSpent: true, amount: 1000n };
|
|
|
|
assert(!query.matches(output), 'Should not match spent output');
|
|
});
|
|
|
|
test('matches by asset type', () => {
|
|
const query = new OutputQuery({ assetType: 'SAL' });
|
|
|
|
assert(query.matches({ assetType: 'SAL' }), 'Should match SAL');
|
|
assert(!query.matches({ assetType: 'USD' }), 'Should not match USD');
|
|
});
|
|
|
|
test('matches by amount range', () => {
|
|
const query = new OutputQuery({ minAmount: 100n, maxAmount: 1000n });
|
|
|
|
assert(!query.matches({ amount: 50n }), 'Should not match below min');
|
|
assert(query.matches({ amount: 100n }), 'Should match at min');
|
|
assert(query.matches({ amount: 500n }), 'Should match in range');
|
|
assert(query.matches({ amount: 1000n }), 'Should match at max');
|
|
assert(!query.matches({ amount: 1001n }), 'Should not match above max');
|
|
});
|
|
|
|
test('matches by account index', () => {
|
|
const query = new OutputQuery({ accountIndex: 1 });
|
|
|
|
assert(query.matches({ subaddressIndex: { major: 1, minor: 0 } }), 'Should match account 1');
|
|
assert(!query.matches({ subaddressIndex: { major: 0, minor: 0 } }), 'Should not match account 0');
|
|
});
|
|
|
|
test('matches by subaddress indices', () => {
|
|
const query = new OutputQuery({
|
|
subaddressIndices: [
|
|
{ major: 0, minor: 1 },
|
|
{ major: 0, minor: 2 },
|
|
{ major: 0, minor: 3 }
|
|
]
|
|
});
|
|
|
|
assert(query.matches({ subaddressIndex: { major: 0, minor: 1 } }), 'Should match minor 1');
|
|
assert(query.matches({ subaddressIndex: { major: 0, minor: 2 } }), 'Should match minor 2');
|
|
assert(!query.matches({ subaddressIndex: { major: 0, minor: 0 } }), 'Should not match minor 0');
|
|
assert(!query.matches({ subaddressIndex: { major: 0, minor: 5 } }), 'Should not match minor 5');
|
|
});
|
|
|
|
test('matches by tx type', () => {
|
|
const query = new OutputQuery({ txType: TX_TYPE.STAKE });
|
|
|
|
assert(query.matches({ txType: TX_TYPE.STAKE }), 'Should match stake');
|
|
assert(!query.matches({ txType: TX_TYPE.TRANSFER }), 'Should not match transfer');
|
|
});
|
|
|
|
test('matches by key images list', () => {
|
|
const query = new OutputQuery({ keyImages: ['ki1', 'ki2', 'ki3'] });
|
|
|
|
assert(query.matches({ keyImage: 'ki1' }), 'Should match ki1');
|
|
assert(query.matches({ keyImage: 'ki2' }), 'Should match ki2');
|
|
assert(!query.matches({ keyImage: 'ki4' }), 'Should not match ki4');
|
|
});
|
|
|
|
test('matches by block height range', () => {
|
|
const query = new OutputQuery({ minHeight: 100, maxHeight: 200 });
|
|
|
|
assert(!query.matches({ blockHeight: 50 }), 'Should not match below min');
|
|
assert(query.matches({ blockHeight: 100 }), 'Should match at min');
|
|
assert(query.matches({ blockHeight: 150 }), 'Should match in range');
|
|
assert(query.matches({ blockHeight: 200 }), 'Should match at max');
|
|
assert(!query.matches({ blockHeight: 250 }), 'Should not match above max');
|
|
});
|
|
|
|
test('combines multiple criteria (AND logic)', () => {
|
|
const query = new OutputQuery({
|
|
isSpent: false,
|
|
assetType: 'SAL',
|
|
minAmount: 100n
|
|
});
|
|
|
|
assert(query.matches({ isSpent: false, assetType: 'SAL', amount: 200n }), 'Should match all criteria');
|
|
assert(!query.matches({ isSpent: true, assetType: 'SAL', amount: 200n }), 'Should fail on isSpent');
|
|
assert(!query.matches({ isSpent: false, assetType: 'USD', amount: 200n }), 'Should fail on assetType');
|
|
assert(!query.matches({ isSpent: false, assetType: 'SAL', amount: 50n }), 'Should fail on amount');
|
|
});
|
|
|
|
test('config values are accessible', () => {
|
|
const query = new OutputQuery({ isSpent: false, assetType: 'SAL' });
|
|
|
|
assertEqual(query.isSpent, false);
|
|
assertEqual(query.assetType, 'SAL');
|
|
});
|
|
|
|
// ============================================================================
|
|
// TxQuery Tests
|
|
// ============================================================================
|
|
|
|
console.log('\n--- TxQuery ---');
|
|
|
|
test('creates with default values', () => {
|
|
const query = new TxQuery();
|
|
assertEqual(query.isIncoming, null);
|
|
assertEqual(query.isOutgoing, null);
|
|
assertEqual(query.isConfirmed, null);
|
|
assertEqual(query.inTxPool, null);
|
|
});
|
|
|
|
test('matches by direction', () => {
|
|
const incomingQuery = new TxQuery({ isIncoming: true });
|
|
const outgoingQuery = new TxQuery({ isOutgoing: true });
|
|
|
|
assert(incomingQuery.matches({ isIncoming: true, isOutgoing: false }));
|
|
assert(!incomingQuery.matches({ isIncoming: false, isOutgoing: true }));
|
|
assert(outgoingQuery.matches({ isIncoming: false, isOutgoing: true }));
|
|
});
|
|
|
|
test('matches by confirmation status', () => {
|
|
const confirmedQuery = new TxQuery({ isConfirmed: true });
|
|
const pendingQuery = new TxQuery({ inTxPool: true });
|
|
|
|
assert(confirmedQuery.matches({ isConfirmed: true, blockHeight: 1000 }));
|
|
assert(!confirmedQuery.matches({ isConfirmed: false, blockHeight: null }));
|
|
assert(pendingQuery.matches({ inTxPool: true }));
|
|
assert(!pendingQuery.matches({ inTxPool: false }));
|
|
});
|
|
|
|
test('matches by tx hash', () => {
|
|
const query = new TxQuery({ hash: 'abc123' });
|
|
|
|
assert(query.matches({ txHash: 'abc123' }));
|
|
assert(!query.matches({ txHash: 'xyz789' }));
|
|
});
|
|
|
|
test('matches by tx hashes list', () => {
|
|
const query = new TxQuery({ hashes: ['tx1', 'tx2', 'tx3'] });
|
|
|
|
assert(query.matches({ txHash: 'tx1' }));
|
|
assert(query.matches({ txHash: 'tx2' }));
|
|
assert(!query.matches({ txHash: 'tx4' }));
|
|
});
|
|
|
|
test('matches by height range', () => {
|
|
const query = new TxQuery({ minHeight: 100, maxHeight: 200 });
|
|
|
|
assert(!query.matches({ blockHeight: 50 }));
|
|
assert(query.matches({ blockHeight: 150 }));
|
|
assert(!query.matches({ blockHeight: 250 }));
|
|
});
|
|
|
|
test('matches by height', () => {
|
|
const query = new TxQuery({ height: 1500 });
|
|
|
|
assert(!query.matches({ blockHeight: 500 }));
|
|
assert(query.matches({ blockHeight: 1500 }));
|
|
assert(!query.matches({ blockHeight: 2500 }));
|
|
});
|
|
|
|
test('matches by payment ID', () => {
|
|
const query = new TxQuery({ paymentId: 'pay123' });
|
|
|
|
assert(query.matches({ paymentId: 'pay123' }));
|
|
assert(!query.matches({ paymentId: 'pay456' }));
|
|
assert(!query.matches({ paymentId: null }));
|
|
});
|
|
|
|
test('matches by tx type', () => {
|
|
const query = new TxQuery({ txType: TX_TYPE.STAKE });
|
|
|
|
assert(query.matches({ txType: TX_TYPE.STAKE }));
|
|
assert(!query.matches({ txType: TX_TYPE.TRANSFER }));
|
|
});
|
|
|
|
test('matches by amount range', () => {
|
|
const query = new TxQuery({ minAmount: 100n, maxAmount: 1000n });
|
|
|
|
assert(query.matches({ amount: 500n }));
|
|
assert(!query.matches({ amount: 50n }));
|
|
assert(!query.matches({ amount: 2000n }));
|
|
});
|
|
|
|
// ============================================================================
|
|
// TransferQuery Tests
|
|
// ============================================================================
|
|
|
|
console.log('\n--- TransferQuery ---');
|
|
|
|
test('creates transfer query', () => {
|
|
const query = new TransferQuery({
|
|
isIncoming: true,
|
|
address: 'Salv1...'
|
|
});
|
|
|
|
assertEqual(query.isIncoming, true);
|
|
assertEqual(query.address, 'Salv1...');
|
|
});
|
|
|
|
test('matches by address', () => {
|
|
const query = new TransferQuery({ address: 'addr1' });
|
|
|
|
assert(query.matches({ address: 'addr1' }));
|
|
assert(!query.matches({ address: 'addr2' }));
|
|
});
|
|
|
|
test('matches by subaddress index', () => {
|
|
const query = new TransferQuery({ accountIndex: 0, subaddressIndex: 5 });
|
|
|
|
assert(query.matches({ accountIndex: 0, subaddressIndex: 5 }));
|
|
assert(!query.matches({ accountIndex: 0, subaddressIndex: 3 }));
|
|
assert(!query.matches({ accountIndex: 1, subaddressIndex: 5 }));
|
|
});
|
|
|
|
// ============================================================================
|
|
// Factory Functions Tests
|
|
// ============================================================================
|
|
|
|
console.log('\n--- Factory Functions ---');
|
|
|
|
test('createOutputQuery creates OutputQuery', () => {
|
|
const query = createOutputQuery({ isSpent: false });
|
|
assert(query instanceof OutputQuery);
|
|
assertEqual(query.isSpent, false);
|
|
});
|
|
|
|
test('createTxQuery creates TxQuery', () => {
|
|
const query = createTxQuery({ isConfirmed: true });
|
|
assert(query instanceof TxQuery);
|
|
assertEqual(query.isConfirmed, true);
|
|
});
|
|
|
|
test('createTransferQuery creates TransferQuery', () => {
|
|
const query = createTransferQuery({ isIncoming: true });
|
|
assert(query instanceof TransferQuery);
|
|
assertEqual(query.isIncoming, true);
|
|
});
|
|
|
|
// ============================================================================
|
|
// Query Preset Functions Tests
|
|
// ============================================================================
|
|
|
|
console.log('\n--- Query Presets ---');
|
|
|
|
test('unspentOutputs creates correct query', () => {
|
|
const query = unspentOutputs();
|
|
assertEqual(query.isSpent, false);
|
|
});
|
|
|
|
test('unspentOutputs merges additional config', () => {
|
|
const query = unspentOutputs({ assetType: 'USD' });
|
|
assertEqual(query.isSpent, false);
|
|
assertEqual(query.assetType, 'USD');
|
|
});
|
|
|
|
test('spentOutputs creates correct query', () => {
|
|
const query = spentOutputs();
|
|
assertEqual(query.isSpent, true);
|
|
});
|
|
|
|
test('lockedOutputs creates correct query', () => {
|
|
const query = lockedOutputs();
|
|
assertEqual(query.isLocked, true);
|
|
assertEqual(query.isSpent, false);
|
|
});
|
|
|
|
test('unlockedOutputs creates correct query', () => {
|
|
const query = unlockedOutputs();
|
|
assertEqual(query.isLocked, false);
|
|
assertEqual(query.isSpent, false);
|
|
});
|
|
|
|
test('stakingOutputs creates correct query', () => {
|
|
const query = stakingOutputs();
|
|
assertEqual(query.txType, TX_TYPE.STAKE);
|
|
assertEqual(query.isSpent, false);
|
|
});
|
|
|
|
test('yieldOutputs creates correct query', () => {
|
|
const query = yieldOutputs();
|
|
assertEqual(query.txType, TX_TYPE.PROTOCOL);
|
|
assertEqual(query.isSpent, false);
|
|
});
|
|
|
|
test('incomingTxs creates correct query', () => {
|
|
const query = incomingTxs();
|
|
assertEqual(query.isIncoming, true);
|
|
});
|
|
|
|
test('outgoingTxs creates correct query', () => {
|
|
const query = outgoingTxs();
|
|
assertEqual(query.isOutgoing, true);
|
|
});
|
|
|
|
test('pendingTxs creates correct query', () => {
|
|
const query = pendingTxs();
|
|
assertEqual(query.inTxPool, true);
|
|
assertEqual(query.isConfirmed, false);
|
|
});
|
|
|
|
test('confirmedTxs creates correct query', () => {
|
|
const query = confirmedTxs();
|
|
assertEqual(query.isConfirmed, true);
|
|
});
|
|
|
|
test('stakingTxs creates correct query', () => {
|
|
const query = stakingTxs();
|
|
assertEqual(query.txType, TX_TYPE.STAKE);
|
|
});
|
|
|
|
test('yieldTxs creates correct query', () => {
|
|
const query = yieldTxs();
|
|
assertEqual(query.txType, TX_TYPE.PROTOCOL);
|
|
});
|
|
|
|
// ============================================================================
|
|
// Complex Query Tests
|
|
// ============================================================================
|
|
|
|
console.log('\n--- Complex Queries ---');
|
|
|
|
test('filter array of outputs', () => {
|
|
const outputs = [
|
|
{ keyImage: 'ki1', isSpent: false, amount: 100n, assetType: 'SAL' },
|
|
{ keyImage: 'ki2', isSpent: true, amount: 200n, assetType: 'SAL' },
|
|
{ keyImage: 'ki3', isSpent: false, amount: 300n, assetType: 'USD' },
|
|
{ keyImage: 'ki4', isSpent: false, amount: 400n, assetType: 'SAL' },
|
|
];
|
|
|
|
const query = new OutputQuery({ isSpent: false, assetType: 'SAL' });
|
|
const filtered = outputs.filter(o => query.matches(o));
|
|
|
|
assertEqual(filtered.length, 2);
|
|
assertEqual(filtered[0].keyImage, 'ki1');
|
|
assertEqual(filtered[1].keyImage, 'ki4');
|
|
});
|
|
|
|
test('filter array of transactions', () => {
|
|
const txs = [
|
|
{ txHash: 'tx1', isIncoming: true, isConfirmed: true, blockHeight: 100 },
|
|
{ txHash: 'tx2', isIncoming: false, isConfirmed: true, blockHeight: 200 },
|
|
{ txHash: 'tx3', isIncoming: true, isConfirmed: false, inPool: true },
|
|
{ txHash: 'tx4', isIncoming: true, isConfirmed: true, blockHeight: 300 },
|
|
];
|
|
|
|
const query = new TxQuery({ isIncoming: true, isConfirmed: true });
|
|
const filtered = txs.filter(tx => query.matches(tx));
|
|
|
|
assertEqual(filtered.length, 2);
|
|
assertEqual(filtered[0].txHash, 'tx1');
|
|
assertEqual(filtered[1].txHash, 'tx4');
|
|
});
|
|
|
|
test('chain query modifications', () => {
|
|
// Start with unspent, add more criteria
|
|
const baseQuery = unspentOutputs();
|
|
const refinedQuery = new OutputQuery({
|
|
isSpent: baseQuery.isSpent,
|
|
assetType: 'SAL',
|
|
minAmount: 1000n
|
|
});
|
|
|
|
assertEqual(refinedQuery.isSpent, false);
|
|
assertEqual(refinedQuery.assetType, 'SAL');
|
|
assertEqual(refinedQuery.minAmount, 1000n);
|
|
});
|
|
|
|
// ============================================================================
|
|
// Summary
|
|
// ============================================================================
|
|
|
|
console.log('\n--- Summary ---');
|
|
console.log(`Passed: ${passed}`);
|
|
console.log(`Failed: ${failed}`);
|
|
|
|
if (failed > 0) {
|
|
console.log('\n✗ Some tests failed!');
|
|
process.exit(1);
|
|
} else {
|
|
console.log('\n✓ All query system tests passed!');
|
|
process.exit(0);
|
|
}
|