UNPKG

fast-uri

Version:

Dependency-free RFC 3986 URI toolbox

211 lines (184 loc) • 6 kB
'use strict' const test = require('tape') const fastURI = require('..') const { normalizePathEncoding, escapePreservingEscapes } = require('../lib/utils') // Helper: any %uXXXX in output is a bug. RFC 3986 only allows %XX. const PERCENT_U = /%u[\da-fA-F]{4}/ test('normalizePathEncoding emits UTF-8 %XX for BMP chars', (t) => { const cases = [ ['/日本', '/%E6%97%A5%E6%9C%AC'], ['/ä', '/%C3%A4'], ['/€', '/%E2%82%AC'], ['/中文/path', '/%E4%B8%AD%E6%96%87/path'] ] t.plan(cases.length * 2) cases.forEach(([input, expected]) => { const out = normalizePathEncoding(input) t.equal(out, expected, input) t.notOk(PERCENT_U.test(out), 'no %uXXXX in ' + out) }) }) test('normalizePathEncoding emits 4-byte UTF-8 for supplementary plane (emoji)', (t) => { const cases = [ ['/😀', '/%F0%9F%98%80'], ['/🚀', '/%F0%9F%9A%80'], ['/a🚀b', '/a%F0%9F%9A%80b'] ] t.plan(cases.length * 2) cases.forEach(([input, expected]) => { const out = normalizePathEncoding(input) t.equal(out, expected, input) t.notOk(PERCENT_U.test(out), 'no %uXXXX in ' + out) }) }) test('normalizePathEncoding encodes control chars as %XX', (t) => { const cases = [ ['', '%00'], ['', '%07'], ['', '%1F'], [' ', '%20'], ['', '%7F'] ] t.plan(cases.length) cases.forEach(([input, expected]) => { t.equal(normalizePathEncoding(input), expected, JSON.stringify(input)) }) }) test('normalizePathEncoding preserves valid path characters', (t) => { const passthrough = "abcXYZ0/9-._~!$&'()*+,;=:@" t.plan(1) t.equal(normalizePathEncoding(passthrough), passthrough) }) test('normalizePathEncoding replaces lone surrogates with U+FFFD bytes', (t) => { const fffd = '%EF%BF%BD' const cases = [ ['\uD800', fffd], ['\uDC00', fffd], ['a\uD800b', 'a' + fffd + 'b'] ] t.plan(cases.length * 2) cases.forEach(([input, expected]) => { const out = normalizePathEncoding(input) t.equal(out, expected) t.notOk(PERCENT_U.test(out)) }) }) test('escapePreservingEscapes emits UTF-8 %XX for non-ASCII', (t) => { const cases = [ ['日', '%E6%97%A5'], ['ä', '%C3%A4'], ['😀', '%F0%9F%98%80'], ['/€/', '/%E2%82%AC/'] ] t.plan(cases.length * 2) cases.forEach(([input, expected]) => { const out = escapePreservingEscapes(input) t.equal(out, expected, input) t.notOk(PERCENT_U.test(out), 'no %uXXXX in ' + out) }) }) test('escapePreservingEscapes preserves existing valid %XX sequences', (t) => { const cases = [ ['%2F', '%2F'], ['%2f', '%2F'], ['a%E6%97%A5b', 'a%E6%97%A5b'] ] t.plan(cases.length) cases.forEach(([input, expected]) => { t.equal(escapePreservingEscapes(input), expected, input) }) }) test('escapePreservingEscapes preserves escape() ASCII safe-set', (t) => { const passthrough = 'abcXYZ012*+-./@_' t.plan(1) t.equal(escapePreservingEscapes(passthrough), passthrough) }) test('serialize produces valid UTF-8 percent-encoding for non-ASCII paths', (t) => { const cases = [ [{ path: '/日本' }, '/%E6%97%A5%E6%9C%AC'], [{ scheme: 'http', host: 'example.com', path: '/café' }, 'http://example.com/caf%C3%A9'], [{ scheme: 'https', host: 'example.com', path: '/emoji/🚀' }, 'https://example.com/emoji/%F0%9F%9A%80'] ] t.plan(cases.length * 2) cases.forEach(([component, expected]) => { const out = fastURI.serialize(component) t.equal(out, expected, JSON.stringify(component)) t.notOk(PERCENT_U.test(out), 'no %uXXXX in ' + out) }) }) test('serialize output round-trips via decodeURIComponent', (t) => { const inputs = [ '/日本/path', '/café', '/emoji/🚀/end', '/a b c' ] t.plan(inputs.length) inputs.forEach((path) => { const out = fastURI.serialize({ path }) t.equal(decodeURIComponent(out), path, path) }) }) test('normalize produces valid UTF-8 for non-ASCII URIs', (t) => { const cases = [ ['http://example.com/日本', 'http://example.com/%E6%97%A5%E6%9C%AC'], ['http://example.com/café', 'http://example.com/caf%C3%A9'] ] t.plan(cases.length * 2) cases.forEach(([input, expected]) => { const out = fastURI.normalize(input) t.equal(out, expected, input) t.notOk(PERCENT_U.test(out)) }) }) test('encoded UTF-8 is idempotent under repeated normalize', (t) => { const inputs = [ 'http://example.com/日本', 'http://example.com/café', 'http://example.com/emoji/🚀' ] t.plan(inputs.length) inputs.forEach((input) => { const once = fastURI.normalize(input) const twice = fastURI.normalize(once) t.equal(twice, once, input) }) }) test('mailto serialization emits valid UTF-8 for supplementary characters', (t) => { const component = { scheme: 'mailto', to: ['😀@example.org'], subject: '😀', body: 'go 🚀', headers: { x: '😀' } } const uri = fastURI.serialize(component) t.equal( uri, 'mailto:%F0%9F%98%80@example.org?x=%F0%9F%98%80&subject=%F0%9F%98%80&body=go%20%F0%9F%9A%80' ) t.doesNotThrow(() => decodeURIComponent(uri), 'serialized mailto URI can be decoded') const parsed = fastURI.parse(uri) t.deepEqual(parsed.to, ['😀@example.org'], 'local part round-trips') t.equal(parsed.subject, '😀', 'subject round-trips') t.equal(parsed.body, 'go 🚀', 'body round-trips') // spread: `headers` has a null prototype, which t.deepEqual compares strictly t.deepEqual({ ...parsed.headers }, { x: '😀' }, 'custom header round-trips') t.end() }) test('mailto serialization replaces lone surrogates with U+FFFD', (t) => { t.equal( fastURI.serialize({ scheme: 'mailto', to: ['user@example.org'], subject: '\uD800' }), 'mailto:user@example.org?subject=%EF%BF%BD', 'lone high surrogate' ) t.equal( fastURI.serialize({ scheme: 'mailto', to: ['user@example.org'], body: '\uDC00' }), 'mailto:user@example.org?body=%EF%BF%BD', 'lone low surrogate' ) t.end() })