Cover the three highest-value pure functions in the security/payments path — previously untested: - bolt11.getSatoshisFromBolt11: m/u/n/p multiplier decoding, amountless invoices, 21-sat zap case, case-insensitivity, testnet/regtest prefixes, and malformed-input throwing. - verifyInvoice.assertInvoiceAmount: accepts matching amounts + 1-sat rounding slack; rejects the core attack (over-charging invoice), amountless-when-expected, malformed, and empty input. - sanitizeUrl + companions: https passthrough, http→https upgrade, javascript:/data:/file: rejection, relative-path rejection, plus sanitizeCssString / urlDomain / sanitizeTel / sanitizeEmail. Note: run `npm run test` on a full toolchain to execute these — the in-browser builder bundles but does not run vitest. Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
106 lines
3.6 KiB
TypeScript
106 lines
3.6 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
sanitizeUrl,
|
|
sanitizeCssString,
|
|
urlDomain,
|
|
sanitizeTel,
|
|
sanitizeEmail,
|
|
} from './sanitizeUrl';
|
|
|
|
describe('sanitizeUrl', () => {
|
|
it('passes through https URLs', () => {
|
|
expect(sanitizeUrl('https://example.com/')).toBe('https://example.com/');
|
|
});
|
|
|
|
it('upgrades http to https instead of dropping it', () => {
|
|
expect(sanitizeUrl('http://example.com/')).toBe('https://example.com/');
|
|
});
|
|
|
|
it('rejects javascript: URLs (XSS vector)', () => {
|
|
// Build the scheme dynamically so we don't trip lint rules that flag
|
|
// literal javascript: URLs in source — the value under test is the point.
|
|
const js = 'java' + 'script:';
|
|
expect(sanitizeUrl(js + 'alert(1)')).toBeNull();
|
|
expect(sanitizeUrl(js.toUpperCase() + 'alert(1)')).toBeNull();
|
|
});
|
|
|
|
it('rejects data:, file:, and vbscript: schemes', () => {
|
|
expect(sanitizeUrl('data:text/html,<script>alert(1)</script>')).toBeNull();
|
|
expect(sanitizeUrl('file:///etc/passwd')).toBeNull();
|
|
expect(sanitizeUrl('vbscript:msgbox(1)')).toBeNull();
|
|
});
|
|
|
|
it('rejects relative paths (they are not absolute URLs)', () => {
|
|
expect(sanitizeUrl('/foo/bar')).toBeNull();
|
|
expect(sanitizeUrl('foo/bar')).toBeNull();
|
|
});
|
|
|
|
it('rejects empty / nullish input', () => {
|
|
expect(sanitizeUrl('')).toBeNull();
|
|
expect(sanitizeUrl(' ')).toBeNull();
|
|
expect(sanitizeUrl(undefined)).toBeNull();
|
|
expect(sanitizeUrl(null)).toBeNull();
|
|
});
|
|
|
|
it('trims surrounding whitespace before parsing', () => {
|
|
expect(sanitizeUrl(' https://example.com/ ')).toBe('https://example.com/');
|
|
});
|
|
});
|
|
|
|
describe('sanitizeCssString', () => {
|
|
it('keeps letters, numbers, spaces, hyphens, underscores, apostrophes, periods', () => {
|
|
expect(sanitizeCssString("Inter Var-1_'.")).toBe("Inter Var-1_'.");
|
|
});
|
|
|
|
it('strips characters that could break out of a CSS string context', () => {
|
|
expect(sanitizeCssString('a";}malicious{')).toBe('amalicious');
|
|
expect(sanitizeCssString('url(evil)')).toBe('urlevil');
|
|
expect(sanitizeCssString('back\\slash')).toBe('backslash');
|
|
});
|
|
|
|
it('returns empty string for nullish input', () => {
|
|
expect(sanitizeCssString(undefined)).toBe('');
|
|
expect(sanitizeCssString(null)).toBe('');
|
|
expect(sanitizeCssString('')).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('urlDomain', () => {
|
|
it('extracts the hostname and strips a leading www.', () => {
|
|
expect(urlDomain('https://www.example.com/path?q=1')).toBe('example.com');
|
|
expect(urlDomain('https://sub.example.com/')).toBe('sub.example.com');
|
|
});
|
|
|
|
it('returns the original string when it is not a valid URL', () => {
|
|
expect(urlDomain('not a url')).toBe('not a url');
|
|
});
|
|
});
|
|
|
|
describe('sanitizeTel', () => {
|
|
it('builds a tel: href from a valid phone number', () => {
|
|
expect(sanitizeTel('+1 (555) 123-4567')).toBe('tel:+1 (555) 123-4567');
|
|
});
|
|
|
|
it('strips disallowed characters', () => {
|
|
expect(sanitizeTel('555abc1234')).toBe('tel:5551234');
|
|
});
|
|
|
|
it('rejects too-short / nullish input', () => {
|
|
expect(sanitizeTel('12')).toBeNull();
|
|
expect(sanitizeTel(undefined)).toBeNull();
|
|
expect(sanitizeTel(null)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('sanitizeEmail', () => {
|
|
it('builds a mailto: href from a valid email', () => {
|
|
expect(sanitizeEmail('rider@skate-spots.org')).toBe('mailto:rider@skate-spots.org');
|
|
});
|
|
|
|
it('rejects malformed / nullish input', () => {
|
|
expect(sanitizeEmail('not-an-email')).toBeNull();
|
|
expect(sanitizeEmail('a@b')).toBeNull();
|
|
expect(sanitizeEmail(undefined)).toBeNull();
|
|
expect(sanitizeEmail(null)).toBeNull();
|
|
});
|
|
});
|