Files
nullcart/backend/src/validation/decorators/isMoneroStandardAddress.spec.ts
T
2026-08-28 17:31:02 +02:00

185 lines
5.5 KiB
TypeScript

import { validateSync } from 'class-validator';
import { MoneroNetwork } from '../../types/MoneroNetwork';
import { IsMoneroStandardAddress } from './isMoneroStandardAddress';
class TestDto {
@IsMoneroStandardAddress()
destinationAddress: string;
}
const pad = (prefix: string) => `${prefix}${'A'.repeat(95 - prefix.length)}`;
const MAINNET_STANDARD_ADDRESS =
'4AdUndXHHZ6cfufTMvppY6JwXNouMBzSkbLYfpAV5Usx3skxNgYeYTRj5UzqtReoS44qo9mtmXCqY45DJ852K5Jv2684Rge';
const validateAddress = (value: string) => {
const dto = Object.assign(new TestDto(), { destinationAddress: value });
return validateSync(dto);
};
const accepts = (value: string) => {
expect(validateAddress(value)).toHaveLength(0);
};
const rejects = (value: string) => {
expect(validateAddress(value).length).toBeGreaterThan(0);
};
describe('IsMoneroStandardAddress', () => {
const originalNetwork = process.env.MONERO_NETWORK;
afterEach(() => {
if (originalNetwork === undefined) {
delete process.env.MONERO_NETWORK;
} else {
process.env.MONERO_NETWORK = originalNetwork;
}
});
describe('mainnet', () => {
beforeEach(() => {
process.env.MONERO_NETWORK = MoneroNetwork.Mainnet;
});
it('accepts a real mainnet standard address', () => {
accepts(MAINNET_STANDARD_ADDRESS);
});
it('accepts mainnet standard addresses', () => {
accepts(pad('41'));
accepts(pad('49'));
accepts(pad('4A'));
accepts(pad('4B'));
});
it('accepts mainnet subaddresses', () => {
accepts(pad('82'));
accepts(pad('89'));
accepts(pad('8A'));
accepts(pad('8B'));
accepts(pad('8C'));
});
it('rejects stagenet addresses', () => {
rejects(pad('51'));
rejects(pad('72'));
});
it('rejects testnet addresses', () => {
rejects(pad('91'));
rejects(pad('BY'));
});
});
describe('stagenet', () => {
beforeEach(() => {
process.env.MONERO_NETWORK = MoneroNetwork.Stagenet;
});
it('accepts stagenet standard addresses', () => {
accepts(pad('51'));
accepts(pad('59'));
accepts(pad('5A'));
accepts(pad('5B'));
});
it('accepts stagenet subaddresses', () => {
accepts(pad('72'));
accepts(pad('79'));
accepts(pad('7A'));
accepts(pad('7B'));
});
it('rejects mainnet addresses', () => {
rejects(MAINNET_STANDARD_ADDRESS);
rejects(pad('4A'));
rejects(pad('82'));
});
it('rejects testnet addresses', () => {
rejects(pad('91'));
rejects(pad('BY'));
});
});
describe('shared validation rules', () => {
beforeEach(() => {
process.env.MONERO_NETWORK = MoneroNetwork.Mainnet;
});
it('accepts lowercase base58 characters in the body', () => {
accepts('4AdUndXHHZ6cfufTMvppY6JwXNouMBzSkbLYfpAV5Usx3skxNgYeYTRj5UzqtReoS44qo9mtmXCqY45DJ852K5Jv2684Rge');
});
it('trims surrounding whitespace on mainnet', () => {
accepts(` ${pad('4A')} `);
});
it('trims surrounding whitespace on stagenet', () => {
process.env.MONERO_NETWORK = MoneroNetwork.Stagenet;
accepts(`\n${pad('5B')}\t`);
});
it.each(['0', '1', '2', '3', '6', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G'])(
'rejects addresses starting with %s',
prefix => {
rejects(pad(`${prefix}A`));
}
);
it.each(['0', 'C'])('rejects mainnet standard addresses with second character %s', second => {
rejects(pad(`4${second}`));
});
it.each(['0', '1'])('rejects mainnet subaddresses with second character %s', second => {
rejects(pad(`8${second}`));
});
it.each(['0', 'C'])('rejects stagenet standard addresses with second character %s', second => {
process.env.MONERO_NETWORK = MoneroNetwork.Stagenet;
rejects(pad(`5${second}`));
});
it.each(['0', '1', 'C'])('rejects stagenet subaddresses with second character %s', second => {
process.env.MONERO_NETWORK = MoneroNetwork.Stagenet;
rejects(pad(`7${second}`));
});
it('rejects empty input', () => {
rejects('');
});
it('rejects whitespace-only input', () => {
rejects(' ');
});
it('rejects addresses that are too short', () => {
rejects(pad('4A').slice(0, 94));
});
it('rejects addresses that are too long', () => {
rejects(`${pad('4A')}A`);
});
it('rejects integrated addresses (106 chars, also start with 4 on mainnet)', () => {
rejects(`${MAINNET_STANDARD_ADDRESS}${'A'.repeat(11)}`);
});
it.each(['0', 'O', 'I', 'l'])('rejects addresses containing %s', invalidChar => {
const address = `${pad('4A').slice(0, 10)}${invalidChar}${pad('4A').slice(11)}`;
rejects(address);
});
it('returns a network-specific error message', () => {
const [error] = validateAddress('invalid');
expect(error.constraints?.isMoneroStandardAddress).toBe('Enter a valid mainnet Monero address.');
});
});
});