diff --git a/browser.js b/browser.js
new file mode 100644
index 0000000..c81deed
--- /dev/null
+++ b/browser.js
@@ -0,0 +1,436 @@
+/**
+ * Implementation of the WHATWG URL Standard.
+ *
+ * @example
+ * const urlEncoder = require('postman-url-encoder')
+ *
+ * // Encoding URL string to Node.js compatible Url object
+ * urlEncoder.toNodeUrl('郵便屋さん.com/foo&bar/{baz}?q=("foo")#`hash`')
+ *
+ * // Encoding URI component
+ * urlEncoder.encode('qüêry štrìng')
+ *
+ * // Encoding query string object
+ * urlEncoder.encodeQueryString({ q1: 'foo', q2: ['bãr', 'baž'] })
+ *
+ * @module postman-url-encoder
+ * @see {@link https://url.spec.whatwg.org}
+ */
+
+const parser = require('./parser'),
+ encoder = require('./encoder/browser'),
+ QUERY_ENCODE_SET = require('./encoder/encode-set').QUERY_ENCODE_SET,
+
+ E = '',
+ COLON = ':',
+ BACK_SLASH = '\\',
+ DOUBLE_SLASH = '//',
+ DOUBLE_BACK_SLASH = '\\\\',
+ STRING = 'string',
+ OBJECT = 'object',
+ FUNCTION = 'function',
+ DEFAULT_PROTOCOL = 'http',
+ LEFT_SQUARE_BRACKET = '[',
+ RIGHT_SQUARE_BRACKET = ']',
+
+ PATH_SEPARATOR = '/',
+ QUERY_SEPARATOR = '?',
+ PARAMS_SEPARATOR = '&',
+ SEARCH_SEPARATOR = '#',
+ DOMAIN_SEPARATOR = '.',
+ AUTH_CREDENTIALS_SEPARATOR = '@',
+
+ // @note this regular expression is referred from Node.js URL parser
+ PROTOCOL_RE = /^[a-z0-9.+-]+:(?:\/\/|\\\\)./i,
+
+ /**
+ * Protocols that always contain a // bit.
+ *
+ * @private
+ * @see {@link https://github.com/nodejs/node/blob/v10.17.0/lib/url.js#L91}
+ */
+ SLASHED_PROTOCOLS = {
+ 'file:': true,
+ 'ftp:': true,
+ 'gopher:': true,
+ 'http:': true,
+ 'https:': true,
+ 'ws:': true,
+ 'wss:': true
+ };
+
+/**
+ * Returns stringified URL from Url object but only includes parts till given
+ * part name.
+ *
+ * @example
+ * var url = 'http://postman.com/foo?q=v#hash';
+ * getUrlTill(toNodeUrl(url), 'host')
+ * // returns 'http://postman.com'
+ *
+ * @private
+ * @param {Object} url base URL
+ * @param {String} [urlPart='query'] one of ['host', 'pathname', 'query']
+ */
+function getUrlTill (url, urlPart) {
+ let result = '';
+
+ if (url.protocol) {
+ result += url.protocol + DOUBLE_SLASH;
+ }
+
+ if (url.auth) {
+ result += url.auth + AUTH_CREDENTIALS_SEPARATOR;
+ }
+
+ result += url.host || E;
+
+ if (urlPart === 'host') { return result; }
+
+ result += url.pathname;
+
+ if (urlPart === 'pathname') { return result; }
+
+ // urlPart must be query at this point
+ return result + (url.search || E);
+}
+
+/**
+ * Percent-encode the given string using QUERY_ENCODE_SET.
+ *
+ * @deprecated since version 2.0, use {@link encodeQueryParam} instead.
+ *
+ * @example
+ * // returns 'foo%20%22%23%26%27%3C%3D%3E%20bar'
+ * encode('foo "#&\'<=> bar')
+ *
+ * // returns ''
+ * encode(['foobar'])
+ *
+ * @param {String} value String to percent-encode
+ * @returns {String} Percent-encoded string
+ */
+function encode (value) {
+ return encoder.percentEncode(value, QUERY_ENCODE_SET);
+}
+
+/**
+ * Percent-encode the URL query string or x-www-form-urlencoded body object
+ * according to RFC3986.
+ *
+ * @example
+ * // returns 'q1=foo&q2=bar&q2=baz'
+ * encodeQueryString({ q1: 'foo', q2: ['bar', 'baz'] })
+ *
+ * @param {Object} query Object representing query or urlencoded body
+ * @returns {String} Percent-encoded string
+ */
+function encodeQueryString (query) {
+ if (!(query && typeof query === OBJECT)) {
+ return E;
+ }
+
+ if (Array.isArray(query)) {
+ query = query.map((item, index) => {
+ return [index, item];
+ });
+ }
+
+ query = new URLSearchParams(query).toString();
+
+ // encode characters not encoded by querystring.stringify() according to RFC3986.
+ return query.replace(/[!'()*]/g, function (c) {
+ return encoder.percentEncodeCharCode(c.charCodeAt(0));
+ });
+}
+
+/**
+ * Converts PostmanUrl / URL string into Node.js compatible Url object.
+ *
+ * @example
Using URL string
+ * toNodeUrl('郵便屋さん.com/foo&bar/{baz}?q=("foo")#`hash`')
+ * // returns
+ * // {
+ * // protocol: 'http:',
+ * // slashes: true,
+ * // auth: null,
+ * // host: 'xn--48jwgn17gdel797d.com',
+ * // port: null,
+ * // hostname: 'xn--48jwgn17gdel797d.com',
+ * // hash: '#%60hash%60',
+ * // search: '?q=(%22foo%22)',
+ * // query: 'q=(%22foo%22)',
+ * // pathname: '/foo&bar/%7Bbaz%7D',
+ * // path: '/foo&bar/%7Bbaz%7D?q=(%22foo%22)',
+ * // href: 'http://xn--48jwgn17gdel797d.com/foo&bar/%7Bbaz%7D?q=(%22foo%22)#%60hash%60'
+ * // }
+ *
+ * @example Using PostmanUrl instance
+ * toNodeUrl(new sdk.Url({
+ * host: 'example.com',
+ * query: [{ key: 'foo', value: 'bar & baz' }]
+ * }))
+ *
+ * @param {PostmanUrl|String} url URL string or PostmanUrl object
+ * @param {Boolean} disableEncoding Turn encoding off
+ * @returns {Url} Node.js like parsed and encoded object
+ */
+function toNodeUrl (url, disableEncoding) {
+ let nodeUrl = {
+ protocol: null,
+ slashes: null,
+ auth: null,
+ host: null,
+ port: null,
+ hostname: null,
+ hash: null,
+ search: null,
+ query: null,
+ pathname: null,
+ path: null,
+ href: E
+ },
+ port,
+ hostname,
+ pathname,
+ authUser,
+ queryParams,
+ authPassword;
+
+ // Check if PostmanUrl instance and prepare segments
+ if (url && url.constructor && url.constructor._postman_propertyName === 'Url') {
+ // @note getPath() always adds a leading '/', similar to Node.js API
+ pathname = url.getPath();
+ hostname = url.getHost().toLowerCase();
+
+ if (url.query && url.query.count()) {
+ queryParams = url.getQueryString({ ignoreDisabled: true });
+ queryParams = disableEncoding ? queryParams : encoder.encodeQueryParam(queryParams);
+
+ // either all the params are disabled or a single param is like { key: '' } (http://localhost?)
+ // in that case, query separator ? must be included in the raw URL.
+ // @todo Add helper in SDK to handle this
+ if (queryParams === E) {
+ // check if there's any enabled param, if so, set queryString to empty string
+ // otherwise (all disabled), it will be set as undefined
+ queryParams = url.query.find(function (param) { return !(param && param.disabled); }) && E;
+ }
+ }
+
+ if (url.auth) {
+ authUser = url.auth.user;
+ authPassword = url.auth.password;
+ }
+ }
+ // Parser URL string and prepare segments
+ else if (typeof url === STRING) {
+ url = parser.parse(url);
+
+ pathname = PATH_SEPARATOR + (url.path || []).join(PATH_SEPARATOR);
+ hostname = (url.host || []).join(DOMAIN_SEPARATOR).toLowerCase();
+ queryParams = url.query && (queryParams = url.query.join(PARAMS_SEPARATOR)) &&
+ (disableEncoding ? queryParams : encoder.encodeQueryParam(queryParams));
+ authUser = (url.auth || [])[0];
+ authPassword = (url.auth || [])[1];
+ }
+ // bail out with empty URL object for invalid input
+ else {
+ return nodeUrl;
+ }
+
+ // @todo Add helper in SDK to normalize port
+ // eslint-disable-next-line no-eq-null, eqeqeq
+ if (!(url.port == null) && typeof url.port.toString === FUNCTION) {
+ port = url.port.toString();
+ }
+
+ // #protocol
+ nodeUrl.protocol = (typeof url.protocol === STRING) ? url.protocol.toLowerCase() : DEFAULT_PROTOCOL;
+ nodeUrl.protocol += COLON;
+
+ // #slashes
+ nodeUrl.slashes = SLASHED_PROTOCOLS[nodeUrl.protocol] || false;
+
+ // #href = protocol://
+ nodeUrl.href = nodeUrl.protocol + DOUBLE_SLASH;
+
+ // #auth
+ if (url.auth) {
+ if (typeof authUser === STRING) {
+ nodeUrl.auth = disableEncoding ? authUser : encoder.encodeUserInfo(authUser);
+ }
+
+ if (typeof authPassword === STRING) {
+ !nodeUrl.auth && (nodeUrl.auth = E);
+ nodeUrl.auth += COLON + (disableEncoding ? authPassword : encoder.encodeUserInfo(authPassword));
+ }
+
+ if (typeof nodeUrl.auth === STRING) {
+ // #href = protocol://user:password@
+ nodeUrl.href += nodeUrl.auth + AUTH_CREDENTIALS_SEPARATOR;
+ }
+ }
+
+ // #host, #hostname
+ nodeUrl.host = nodeUrl.hostname = hostname = encoder.encodeHost(hostname); // @note always encode hostname
+
+ // #href = protocol://user:password@host.name
+ nodeUrl.href += nodeUrl.hostname;
+
+ // #port
+ if (typeof port === STRING) {
+ nodeUrl.port = port;
+
+ // #host = (#hostname):(#port)
+ nodeUrl.host = nodeUrl.hostname + COLON + port;
+
+ // #href = protocol://user:password@host.name:port
+ nodeUrl.href += COLON + port;
+ }
+
+ // #path, #pathname
+ nodeUrl.path = nodeUrl.pathname = disableEncoding ? pathname : encoder.encodePath(pathname);
+
+ // #href = protocol://user:password@host.name:port/p/a/t/h
+ nodeUrl.href += nodeUrl.pathname;
+
+ if (typeof queryParams === STRING) {
+ // #query
+ nodeUrl.query = queryParams;
+
+ // #search
+ nodeUrl.search = QUERY_SEPARATOR + nodeUrl.query;
+
+ // #path = (#pathname)?(#search)
+ nodeUrl.path = nodeUrl.pathname + nodeUrl.search;
+
+ // #href = protocol://user:password@host.name:port/p/a/t/h?q=query
+ nodeUrl.href += nodeUrl.search;
+ }
+
+ if (typeof url.hash === STRING) {
+ // #hash
+ nodeUrl.hash = SEARCH_SEPARATOR + (disableEncoding ? url.hash : encoder.encodeFragment(url.hash));
+
+ // #href = protocol://user:password@host.name:port/p/a/t/h?q=query#hash
+ nodeUrl.href += nodeUrl.hash;
+ }
+
+ // Finally apply Node.js shenanigans
+ // # Remove square brackets from IPv6 #hostname
+ // Refer: https://github.com/nodejs/node/blob/v12.18.3/lib/url.js#L399
+ // Refer: https://github.com/nodejs/node/blob/v12.18.3/lib/internal/url.js#L1273
+ if (hostname[0] === LEFT_SQUARE_BRACKET && hostname[hostname.length - 1] === RIGHT_SQUARE_BRACKET) {
+ nodeUrl.hostname = hostname.slice(1, -1);
+ }
+
+ return nodeUrl;
+}
+
+/**
+ * Resolves a relative URL with respect to given base URL.
+ * This is a replacement method for Node's url.resolve() which is compatible
+ * with URL object generated by toNodeUrl().
+ *
+ * @example
+ * // returns 'http://postman.com/baz'
+ * resolveNodeUrl('http://postman.com/foo/bar', '/baz')
+ *
+ * @param {Object|String} base URL string or toNodeUrl() object
+ * @param {String} relative Relative URL to resolve
+ * @returns {String} Resolved URL
+ */
+function resolveNodeUrl (base, relative) {
+ // normalize arguments
+ typeof base === STRING && (base = toNodeUrl(base));
+ typeof relative !== STRING && (relative = E);
+
+ // bail out if base is not an object
+ if (!(base && typeof base === OBJECT)) {
+ return relative;
+ }
+
+ let i,
+ ii,
+ index,
+ baseHref,
+ relative_0,
+ relative_01,
+ basePathname,
+ requiredProps = ['protocol', 'auth', 'host', 'pathname', 'search', 'href'];
+
+ // bail out if base is not like Node url object
+ for (i = 0, ii = requiredProps.length; i < ii; i++) {
+ if (!Object.hasOwnProperty.call(base, requiredProps[i])) {
+ return relative;
+ }
+ }
+
+ // cache base.href and base.pathname
+ baseHref = base.href;
+ basePathname = base.pathname;
+
+ // cache relative's first two chars
+ relative_0 = relative.slice(0, 1);
+ relative_01 = relative.slice(0, 2);
+
+ // @note relative can be one of
+ // #1 empty string
+ // #2 protocol relative, starts with // or \\
+ // #3 path relative, starts with / or \
+ // #4 just query or hash, starts with ? or #
+ // #5 absolute URL, starts with :// or :\\
+ // #6 free from path, with or without query and hash
+
+ // #1 empty string
+ if (!relative) {
+ // return base as it is if there is no hash
+ if ((index = baseHref.indexOf(SEARCH_SEPARATOR)) === -1) {
+ return baseHref;
+ }
+
+ // else, return base without the hash
+ return baseHref.slice(0, index);
+ }
+
+ // #2 protocol relative, starts with // or \\
+ // @note \\ is not converted to //
+ if (relative_01 === DOUBLE_SLASH || relative_01 === DOUBLE_BACK_SLASH) {
+ return base.protocol + relative;
+ }
+
+ // #3 path relative, starts with / or \
+ // @note \(s) are not converted to /
+ if (relative_0 === PATH_SEPARATOR || relative_0 === BACK_SLASH) {
+ return getUrlTill(base, 'host') + relative;
+ }
+
+ // #4 just hash, starts with #
+ if (relative_0 === SEARCH_SEPARATOR) {
+ return getUrlTill(base, 'query') + relative;
+ }
+
+ // #4 just query, starts with ?
+ if (relative_0 === QUERY_SEPARATOR) {
+ return getUrlTill(base, 'pathname') + relative;
+ }
+
+ // #5 absolute URL, starts with :// or :\\
+ // @note :\\ is not converted to ://
+ if (PROTOCOL_RE.test(relative)) {
+ return relative;
+ }
+
+ // #6 free from path, with or without query and hash
+ // remove last path segment form base path
+ basePathname = basePathname.slice(0, basePathname.lastIndexOf(PATH_SEPARATOR) + 1);
+
+ return getUrlTill(base, 'host') + basePathname + relative;
+}
+
+module.exports = {
+ encode,
+ toNodeUrl,
+ resolveNodeUrl,
+ encodeQueryString
+};
diff --git a/encoder/browser.js b/encoder/browser.js
new file mode 100644
index 0000000..2f7a90d
--- /dev/null
+++ b/encoder/browser.js
@@ -0,0 +1,363 @@
+/**
+ * This module helps to encode different URL components and expose utility
+ * methods to percent-encode a given string using an {@link EncodeSet}.
+ *
+ * @example
+ * const encoder = require('postman-url-encoder/encoder')
+ *
+ * // returns 'xn--48jwgn17gdel797d.com'
+ * encoder.encodeHost('郵便屋さん.com')
+ *
+ * @example Using EncodeSet
+ * var EncodeSet = require('postman-url-encoder/encoder').EncodeSet
+ *
+ * var fragmentEncodeSet = new EncodeSet([' ', '"', '<', '>', '`'])
+ *
+ * // returns false
+ * fragmentEncodeSet.has('['.charCodeAt(0))
+ *
+ * // returns true
+ * fragmentEncodeSet.has('<'.charCodeAt(0))
+ *
+ * @module postman-url-encoder/encoder
+ * @see {@link https://url.spec.whatwg.org/#url-representation}
+ */
+
+/**
+ * @fileoverview
+ * This module determines which of the reserved characters in the different
+ * URL components should be percent-encoded and which can be safely used.
+ *
+ * The generic URI syntax consists of a hierarchical sequence of components
+ * referred to as the scheme, authority, path, query, and fragment.
+ *
+ * URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
+ *
+ * hier-part = "//" authority path-abempty
+ * / path-absolute
+ * / path-rootless
+ * / path-empty
+ *
+ * authority = [ userinfo "@" ] host [ ":" port ]
+ *
+ * @see {@link https://tools.ietf.org/html/rfc3986#section-2}
+ * @see {@link https://tools.ietf.org/html/rfc3986#section-3}
+ */
+
+const encodeSet = require('./encode-set'),
+
+ punycode = require('punycode/'),
+ _percentEncode = require('./percent-encode').encode,
+ _percentEncodeCharCode = require('./percent-encode').encodeCharCode,
+
+ EncodeSet = encodeSet.EncodeSet,
+
+ PATH_ENCODE_SET = encodeSet.PATH_ENCODE_SET,
+ QUERY_ENCODE_SET = encodeSet.QUERY_ENCODE_SET,
+ USERINFO_ENCODE_SET = encodeSet.USERINFO_ENCODE_SET,
+ FRAGMENT_ENCODE_SET = encodeSet.FRAGMENT_ENCODE_SET,
+ C0_CONTROL_ENCODE_SET = encodeSet.C0_CONTROL_ENCODE_SET,
+
+ PARAM_VALUE_ENCODE_SET = EncodeSet.extend(QUERY_ENCODE_SET, ['&']).seal(),
+ PARAM_KEY_ENCODE_SET = EncodeSet.extend(QUERY_ENCODE_SET, ['&', '=']).seal(),
+
+ E = '',
+ EQUALS = '=',
+ AMPERSAND = '&',
+ STRING = 'string',
+ OBJECT = 'object',
+
+ PATH_SEPARATOR = '/',
+ DOMAIN_SEPARATOR = '.',
+
+ /**
+ * Returns the Punycode ASCII serialization of the domain.
+ *
+ * @private
+ * @function
+ * @param {String} domain domain name
+ * @returns {String} punycode encoded domain name
+ */
+ domainToASCII = function (domain) {
+ const domainWithProtocol = domain.startsWith('http') ? domain : `https://${domain}`;
+
+ try {
+ return new URL(domainWithProtocol).hostname;
+ }
+ catch (error) {
+ return punycode.toASCII(domain);
+ }
+ };
+
+/**
+ * Returns the Punycode ASCII serialization of the domain.
+ *
+ * @note Returns input hostname on invalid domain.
+ *
+ * @example
+ * // returns 'xn--fiq228c.com'
+ * encodeHost('中文.com')
+ *
+ * // returns 'xn--48jwgn17gdel797d.com'
+ * encodeHost(['郵便屋さん', 'com'])
+ *
+ * // returns '127.0.0.1'
+ * encodeHost('127.1')
+ *
+ * // returns 'xn--iñvalid.com'
+ * encodeHost('xn--iñvalid.com')
+ *
+ * @param {String|String[]} hostName host or domain name
+ * @returns {String} Punycode-encoded hostname
+ */
+function encodeHost (hostName) {
+ if (Array.isArray(hostName)) {
+ hostName = hostName.join(DOMAIN_SEPARATOR);
+ }
+
+ if (typeof hostName !== STRING) {
+ return E;
+ }
+
+ // return input host name if `domainToASCII` returned an empty string
+ return domainToASCII(hostName) || hostName;
+}
+
+/**
+ * Encodes URL path or individual path segments.
+ *
+ * @example
+ * // returns 'foo/bar&baz'
+ * encodePath('foo/bar&baz')
+ *
+ * // returns 'foo/bar/%20%22%3C%3E%60%23%3F%7B%7D'
+ * encodePath(['foo', 'bar', ' "<>\`#?{}'])
+ *
+ * @param {String|String[]} path Path or path segments
+ * @returns {String} Percent-encoded path
+ */
+function encodePath (path) {
+ if (Array.isArray(path) && path.length) {
+ path = path.join(PATH_SEPARATOR);
+ }
+
+ if (typeof path !== STRING) {
+ return E;
+ }
+
+ return _percentEncode(path, PATH_ENCODE_SET);
+}
+
+/**
+ * Encodes URL userinfo (username / password) fields.
+ *
+ * @example
+ * // returns 'info~%20%22%3C%3E%60%23%3F%7B%7D%2F%3A%3B%3D%40%5B%5C%5D%5E%7C'
+ * encodeAuth('info~ "<>`#?{}/:;=@[\\]^|')
+ *
+ * @param {String} param Parameter to encode
+ * @returns {String} Percent-encoded parameter
+ */
+function encodeUserInfo (param) {
+ if (typeof param !== STRING) {
+ return E;
+ }
+
+ return _percentEncode(param, USERINFO_ENCODE_SET);
+}
+
+/**
+ * Encodes URL fragment identifier or hash.
+ *
+ * @example
+ * // returns 'fragment#%20%22%3C%3E%60'
+ * encodeHash('fragment# "<>`')
+ *
+ * @param {String} fragment Hash or fragment identifier to encode
+ * @returns {String} Percent-encoded fragment
+ */
+function encodeFragment (fragment) {
+ if (typeof fragment !== STRING) {
+ return E;
+ }
+
+ return _percentEncode(fragment, FRAGMENT_ENCODE_SET);
+}
+
+/**
+ * Encodes single query parameter and returns as a string.
+ *
+ * @example
+ * // returns 'param%20%22%23%27%3C%3E'
+ * encodeQueryParam('param "#\'<>')
+ *
+ * // returns 'foo=bar'
+ * encodeQueryParam({ key: 'foo', value: 'bar' })
+ *
+ * @param {Object|String} param Query param to encode
+ * @returns {String} Percent-encoded query param
+ */
+function encodeQueryParam (param) {
+ if (!param) {
+ return E;
+ }
+
+ if (typeof param === STRING) {
+ return _percentEncode(param, QUERY_ENCODE_SET);
+ }
+
+ let key = param.key,
+ value = param.value,
+ result;
+
+ if (typeof key === STRING) {
+ result = _percentEncode(key, PARAM_KEY_ENCODE_SET);
+ }
+ else {
+ result = E;
+ }
+
+ if (typeof value === STRING) {
+ result += EQUALS + _percentEncode(value, PARAM_VALUE_ENCODE_SET);
+ }
+
+ return result;
+}
+
+/**
+ * Encodes list of query parameters and returns encoded query string.
+ *
+ * @example
+ * // returns 'foo=bar&=foo%26bar'
+ * encodeQueryParams([{ key: 'foo', value: 'bar' }, { value: 'foo&bar' }])
+ *
+ * // returns 'q1=foo&q2=bar&q2=baz'
+ * encodeQueryParams({ q1: 'foo', q2: ['bar', 'baz'] })
+ *
+ * @param {Object|Object[]} params Query params to encode
+ * @returns {String} Percent-encoded query string
+ */
+function encodeQueryParams (params) {
+ let i,
+ j,
+ ii,
+ jj,
+ paramKey,
+ paramKeys,
+ paramValue,
+ result = E,
+ notFirstParam = false;
+
+ if (!(params && typeof params === OBJECT)) {
+ return E;
+ }
+
+ // handle array of query params
+ if (Array.isArray(params)) {
+ for (i = 0, ii = params.length; i < ii; i++) {
+ // @todo Add helper in PropertyList to filter disabled QueryParam
+ if (!params[i] || params[i].disabled === true) {
+ continue;
+ }
+
+ // don't add '&' for the very first enabled param
+ notFirstParam && (result += AMPERSAND);
+ notFirstParam = true;
+
+ result += encodeQueryParam(params[i]);
+ }
+
+ return result;
+ }
+
+ // handle object with query params
+ paramKeys = Object.keys(params);
+
+ for (i = 0, ii = paramKeys.length; i < ii; i++) {
+ paramKey = paramKeys[i];
+ paramValue = params[paramKey];
+
+ // { key: ['value1', 'value2', 'value3'] }
+ if (Array.isArray(paramValue)) {
+ for (j = 0, jj = paramValue.length; j < jj; j++) {
+ notFirstParam && (result += AMPERSAND);
+ notFirstParam = true;
+
+ result += encodeQueryParam({ key: paramKey, value: paramValue[j] });
+ }
+ }
+ // { key: 'value' }
+ else {
+ notFirstParam && (result += AMPERSAND);
+ notFirstParam = true;
+
+ result += encodeQueryParam({ key: paramKey, value: paramValue });
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Percent-encode the given string with the given {@link EncodeSet}.
+ *
+ * @example Defaults to C0_CONTROL_ENCODE_SET
+ * // returns 'foo %00 bar'
+ * percentEncode('foo \u0000 bar')
+ *
+ * @example Encode literal @ using custom EncodeSet
+ * // returns 'foo%40bar'
+ * percentEncode('foo@bar', new EncodeSet(['@']))
+ *
+ * @param {String} value String to percent-encode
+ * @param {EncodeSet} [encodeSet=C0_CONTROL_ENCODE_SET] EncodeSet to use for encoding
+ * @returns {String} Percent-encoded string
+ */
+function percentEncode (value, encodeSet) {
+ if (!(value && typeof value === STRING)) {
+ return E;
+ }
+
+ // defaults to C0_CONTROL_ENCODE_SET
+ if (!EncodeSet.isEncodeSet(encodeSet)) {
+ encodeSet = C0_CONTROL_ENCODE_SET;
+ }
+
+ return _percentEncode(value, encodeSet);
+}
+
+/**
+ * Percent encode a character with given code.
+ *
+ * @example
+ * // returns '%20'
+ * percentEncodeCharCode(32)
+ *
+ * @param {Number} code Character code
+ * @returns {String} Percent-encoded character
+ */
+function percentEncodeCharCode (code) {
+ // ensure [0x00, 0xFF] range
+ if (!(Number.isInteger(code) && code >= 0 && code <= 0xFF)) {
+ return E;
+ }
+
+ return _percentEncodeCharCode(code);
+}
+
+module.exports = {
+ // URL components
+ encodeHost,
+ encodePath,
+ encodeUserInfo,
+ encodeFragment,
+ encodeQueryParam,
+ encodeQueryParams,
+
+ /** @type EncodeSet */
+ EncodeSet,
+
+ // Utilities
+ percentEncode,
+ percentEncodeCharCode
+};
diff --git a/index.js b/index.js
index f695d7e..be161ed 100644
--- a/index.js
+++ b/index.js
@@ -18,8 +18,6 @@
*/
const querystring = require('querystring'),
-
- legacy = require('./legacy'),
parser = require('./parser'),
encoder = require('./encoder'),
QUERY_ENCODE_SET = require('./encoder/encode-set').QUERY_ENCODE_SET,
@@ -426,22 +424,9 @@ function resolveNodeUrl (base, relative) {
return getUrlTill(base, 'host') + basePathname + relative;
}
-/**
- * Converts URL string into Node.js compatible Url object using the v1 encoder.
- *
- * @deprecated since version 2.0
- *
- * @param {String} url URL string
- * @returns {Url} Node.js compatible Url object
- */
-function toLegacyNodeUrl (url) {
- return legacy.toNodeUrl(url);
-}
-
module.exports = {
encode,
toNodeUrl,
resolveNodeUrl,
- toLegacyNodeUrl,
encodeQueryString
};
diff --git a/legacy.js b/legacy.js
deleted file mode 100644
index 9239668..0000000
--- a/legacy.js
+++ /dev/null
@@ -1,242 +0,0 @@
-var url = require('url'),
-
- /**
- * @private
- * @const
- * @type {String}
- */
- E = '',
-
- /**
- * @private
- * @const
- * @type {String}
- */
- QUERY_SEPARATOR = '?',
-
- /**
- * @private
- * @const
- * @type {String}
- */
- AMPERSAND = '&',
-
- /**
- * @private
- * @const
- * @type {String}
- */
- EQUALS = '=',
-
- /**
- * @private
- * @const
- * @type {String}
- */
- PERCENT = '%',
-
- /**
- * @private
- * @const
- * @type {string}
- */
- ZERO = '0',
-
- /**
- * @private
- * @const
- * @type {string}
- */
- STRING = 'string',
-
- encoder;
-
-encoder = {
- /**
- * Percent encode a character with given code.
- *
- * @param {Number} c - character code of the character to encode
- * @returns {String} - percent encoding of given character
- */
- percentEncode (c) {
- var hex = c.toString(16).toUpperCase();
-
- (hex.length === 1) && (hex = ZERO + hex);
-
- return PERCENT + hex;
- },
-
- /**
- * Checks if character at given index in the buffer is already percent encoded or not.
- *
- * @param {Buffer} buffer -
- * @param {Number} i -
- * @returns {Boolean}
- */
- isPreEncoded (buffer, i) {
- // If it is % check next two bytes for percent encode characters
- // looking for pattern %00 - %FF
- return (buffer[i] === 0x25 &&
- (encoder.isPreEncodedCharacter(buffer[i + 1]) &&
- encoder.isPreEncodedCharacter(buffer[i + 2]))
- );
- },
-
- /**
- * Checks if character with given code is valid hexadecimal digit or not.
- *
- * @param {Number} byte -
- * @returns {Boolean}
- */
- isPreEncodedCharacter (byte) {
- return (byte >= 0x30 && byte <= 0x39) || // 0-9
- (byte >= 0x41 && byte <= 0x46) || // A-F
- (byte >= 0x61 && byte <= 0x66); // a-f
- },
-
- /**
- * Checks whether given character should be percent encoded or not for fixture.
- *
- * @param {Number} byte -
- * @returns {Boolean}
- */
- charactersToPercentEncode (byte) {
- return (byte < 0x23 || byte > 0x7E || // Below # and after ~
- byte === 0x3C || byte === 0x3E || // > and <
- byte === 0x28 || byte === 0x29 || // ( and )
- byte === 0x25 || // %
- byte === 0x27 || // '
- byte === 0x2A // *
- );
- },
-
- /**
- * Percent encode a query string according to RFC 3986.
- * Note: This function is supposed to be used on top of node's inbuilt url encoding
- * to solve issue https://github.com/nodejs/node/issues/8321
- *
- * @param {String} value -
- * @returns {String}
- */
- encode (value) {
- if (!value) { return E; }
-
- var buffer = Buffer.from(value),
- ret = E,
- i,
- ii;
-
- for (i = 0, ii = buffer.length; i < ii; ++i) {
- if (encoder.charactersToPercentEncode(buffer[i]) && !encoder.isPreEncoded(buffer, i)) {
- ret += encoder.percentEncode(buffer[i]);
- }
- else {
- ret += String.fromCodePoint(buffer[i]); // Only works in ES6 (available in Node v4+)
- }
- }
-
- return ret;
- }
-};
-
-/**
- * Parses a query string into an array, preserving parameter values
- *
- * @private
- * @param {String} string -
- * @returns {*}
- */
-function parseQueryString (string) {
- var parts;
-
- if (typeof string === STRING) {
- parts = string.split(AMPERSAND);
-
- return parts.map(function (param, idx) {
- if (param === E && idx !== (parts.length - 1)) {
- return { key: null, value: null };
- }
-
- var index = (typeof param === STRING) ? param.indexOf(EQUALS) : -1,
- paramObj = {};
-
- // this means that there was no value for this key (not even blank,
- // so we store this info) and the value is set to null
- if (index < 0) {
- paramObj.key = param.substr(0, param.length);
- paramObj.value = null;
- }
- else {
- paramObj.key = param.substr(0, index);
- paramObj.value = param.substr(index + 1);
- }
-
- return paramObj;
- });
- }
-
- return [];
-}
-
-/**
- * Stringifies a query string, from an array of parameters
- *
- * @private
- * @param {Object[]} parameters -
- * @returns {string}
- */
-function stringifyQueryParams (parameters) {
- return parameters ? parameters.map(function (param) {
- var key = param.key,
- value = param.value;
-
- if (value === undefined) {
- return E;
- }
-
- if (key === null) {
- key = E;
- }
-
- if (value === null) {
- return encoder.encode(key);
- }
-
- return encoder.encode(key) + EQUALS + encoder.encode(value);
- }).join(AMPERSAND) : E;
-}
-
-/**
- * Converts URL string into Node's Url object with encoded values
- *
- * @private
- * @param {String} urlString -
- * @returns {Url}
- */
-function toNodeUrl (urlString) {
- var parsed = url.parse(urlString),
- rawQs = parsed.query,
- qs,
- search,
- path,
- str;
-
- if (!(rawQs && rawQs.length)) { return parsed; }
-
- qs = stringifyQueryParams(parseQueryString(rawQs));
- search = QUERY_SEPARATOR + qs;
- path = parsed.pathname + search;
-
- parsed.query = qs;
- parsed.search = search;
- parsed.path = path;
-
- str = url.format(parsed);
-
- // Parse again, because Node does not guarantee consistency of properties
- return url.parse(str);
-}
-
-module.exports = {
- toNodeUrl
-};
diff --git a/package.json b/package.json
index 6fec7a0..1221b00 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,24 @@
{
"name": "postman-url-encoder",
- "version": "3.0.8",
+ "version": "4.0.0-beta.0",
"description": "Implementation of the WHATWG URL Standard",
"author": "Postman Inc.",
"license": "Apache-2.0",
"main": "index.js",
+ "exports": {
+ ".": {
+ "browser": "./browser.js",
+ "default": "./index.js"
+ },
+ "./encoder": {
+ "browser": "./encoder/browser.js",
+ "default": "./encoder/index.js"
+ },
+ "./parser": {
+ "browser": "./parser/index.js",
+ "default": "./parser/index.js"
+ }
+ },
"homepage": "https://github.com/postmanlabs/postman-url-encoder#readme",
"bugs": {
"url": "https://github.com/postmanlabs/postman-url-encoder/issues",
diff --git a/test/benchmark/toLegacyNodeUrl.bench.js b/test/benchmark/toLegacyNodeUrl.bench.js
deleted file mode 100644
index bc777ba..0000000
--- a/test/benchmark/toLegacyNodeUrl.bench.js
+++ /dev/null
@@ -1,20 +0,0 @@
-/* eslint-disable no-undef */
-const fs = require('fs'),
- path = require('path'),
- toLegacyNodeUrl = require('../..').toLegacyNodeUrl,
- parseCsv = require('@postman/csv-parse/lib/sync');
-
-suite('toLegacyNodeUrl()', function () {
- var testCases = fs.readFileSync(path.join(__dirname, '../fixtures/urlList.csv'));
-
- testCases = parseCsv(testCases, {
- columns: true,
- trim: false
- });
-
- testCases.forEach(function (testcase) {
- scenario(testcase.description, function () {
- toLegacyNodeUrl(testcase.url);
- });
- });
-});
diff --git a/test/karma.conf.js b/test/karma.conf.js
index 3d989a5..ffaa50e 100644
--- a/test/karma.conf.js
+++ b/test/karma.conf.js
@@ -11,14 +11,14 @@ module.exports = function (config) {
// list of files / patterns to load in the browser
files: [
- '../index.js',
+ '../browser.js',
'../test/unit/**/*.js'
],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
- '../index.js': ['browserify'], // Mention path as per your test js folder
+ '../browser.js': ['browserify'], // Mention path as per your test js folder
'../test/unit/**/*.js': ['browserify'] // Mention path as per your library js folder
},
// test results reporter to use
diff --git a/test/unit/encode.browser.test.js b/test/unit/encode.browser.test.js
new file mode 100644
index 0000000..f960a45
--- /dev/null
+++ b/test/unit/encode.browser.test.js
@@ -0,0 +1,140 @@
+const expect = require('chai').expect,
+ encode = require('../../browser').encode,
+ percentEncodeCharCode = require('../../encoder/browser').percentEncodeCharCode;
+
+describe('[browser] .encode', function () {
+ describe('with TextEncoder', function () {
+ it('should percent-encode C0 control codes', function () {
+ var i,
+ char;
+
+ for (i = 0; i < 32; i++) {
+ char = String.fromCharCode(i);
+ expect(encode(char)).to.equal(percentEncodeCharCode(i));
+ }
+
+ char = String.fromCharCode(127);
+ expect(encode(char)).to.equal(percentEncodeCharCode(127));
+ });
+
+ it('should percent-encode SPACE, ("), (#), (\'), (<), and (>)', function () {
+ var i,
+ char,
+ encoded,
+ chars = [],
+ expected = [' ', '"', '#', '\'', '<', '>'];
+
+ for (i = 32; i < 127; i++) {
+ char = String.fromCharCode(i);
+ encoded = encode(char);
+
+ if (char !== encoded) {
+ chars.push(char);
+ expect(encoded).to.equal(percentEncodeCharCode(i));
+ }
+ }
+
+ expect(chars).to.eql(expected);
+ });
+
+ it('should percent-encode unicode characters', function () {
+ expect(encode('𝌆й你ス')).to.eql('%F0%9D%8C%86%D0%B9%E4%BD%A0%E3%82%B9');
+ });
+
+ it('should percent-encode 4-byte unicode characters', function () {
+ expect(encode('𝌆')).to.eql('%F0%9D%8C%86');
+ });
+
+ it('should handle unpaired surrogates', function () {
+ expect(encode('\uD800')).to.eql('%EF%BF%BD');
+ });
+
+ it('should not double encode characters', function () {
+ expect(encode('key:%F0%9F%8D%AA')).to.equal('key:%F0%9F%8D%AA');
+ });
+
+ it('should return empty string on invalid input types', function () {
+ expect(encode()).to.equal('');
+ expect(encode(null)).to.equal('');
+ expect(encode(undefined)).to.equal('');
+ expect(encode(NaN)).to.equal('');
+ expect(encode(true)).to.equal('');
+ expect(encode(1234)).to.equal('');
+ expect(encode(Function)).to.equal('');
+ expect(encode(['key', 'value'])).to.equal('');
+ });
+ });
+
+ describe('without TextEncoder', function () {
+ let oldTextEncoder;
+
+ before(function () {
+ oldTextEncoder = global.TextEncoder;
+ global.TextEncoder = undefined;
+ });
+
+ after(function () {
+ global.TextEncoder = oldTextEncoder;
+ });
+
+ it('should percent-encode C0 control codes', function () {
+ var i,
+ char;
+
+ for (i = 0; i < 32; i++) {
+ char = String.fromCharCode(i);
+ expect(encode(char)).to.equal(percentEncodeCharCode(i));
+ }
+
+ char = String.fromCharCode(127);
+ expect(encode(char)).to.equal(percentEncodeCharCode(127));
+ });
+
+ it('should percent-encode SPACE, ("), (#), (\'), (<), and (>)', function () {
+ var i,
+ char,
+ encoded,
+ chars = [],
+ expected = [' ', '"', '#', '\'', '<', '>'];
+
+ for (i = 32; i < 127; i++) {
+ char = String.fromCharCode(i);
+ encoded = encode(char);
+
+ if (char !== encoded) {
+ chars.push(char);
+ expect(encoded).to.equal(percentEncodeCharCode(i));
+ }
+ }
+
+ expect(chars).to.eql(expected);
+ });
+
+ it('should percent-encode unicode characters', function () {
+ expect(encode('𝌆й你ス')).to.eql('%F0%9D%8C%86%D0%B9%E4%BD%A0%E3%82%B9');
+ });
+
+ it('should percent-encode 4-byte unicode characters', function () {
+ expect(encode('𝌆')).to.eql('%F0%9D%8C%86');
+ });
+
+ it('should handle unpaired surrogates', function () {
+ expect(encode('\uD800')).to.eql('%EF%BF%BD');
+ });
+
+ it('should not double encode characters', function () {
+ expect(encode('key:%F0%9F%8D%AA')).to.equal('key:%F0%9F%8D%AA');
+ });
+
+ it('should return empty string on invalid input types', function () {
+ expect(encode()).to.equal('');
+ expect(encode(null)).to.equal('');
+ expect(encode(undefined)).to.equal('');
+ expect(encode(NaN)).to.equal('');
+ expect(encode(true)).to.equal('');
+ expect(encode(1234)).to.equal('');
+ expect(encode(Function)).to.equal('');
+ expect(encode(['key', 'value'])).to.equal('');
+ });
+ });
+});
diff --git a/test/unit/encodeQueryString.browser.test.js b/test/unit/encodeQueryString.browser.test.js
new file mode 100644
index 0000000..3af873c
--- /dev/null
+++ b/test/unit/encodeQueryString.browser.test.js
@@ -0,0 +1,40 @@
+const expect = require('chai').expect,
+
+ encodeQueryString = require('../../browser').encodeQueryString;
+
+describe('[browser] .encodeQueryString', function () {
+ it('should accept query as object', function () {
+ expect(encodeQueryString({
+ q1: 'v1',
+ q2: '(v2)'
+ })).to.eql('q1=v1&q2=%28v2%29');
+ });
+
+ it('should handle query as an array', function () {
+ expect(encodeQueryString(['foo', 'bār'])).to.equal('0=foo&1=b%C4%81r');
+ });
+
+ // it('should handle multi-valued query object', function () {
+ // expect(encodeQueryString({
+ // q1: ['𝌆й', '你ス'],
+ // q2: ''
+ // })).to.eql('q1=%F0%9D%8C%86%D0%B9&q1=%E4%BD%A0%E3%82%B9&q2=');
+ // });
+
+ it('should return empty string on invalid input types', function () {
+ expect(encodeQueryString()).to.equal('');
+ expect(encodeQueryString(null)).to.equal('');
+ expect(encodeQueryString(undefined)).to.equal('');
+ expect(encodeQueryString(NaN)).to.equal('');
+ expect(encodeQueryString(true)).to.equal('');
+ expect(encodeQueryString(1234)).to.equal('');
+ expect(encodeQueryString({})).to.equal('');
+ expect(encodeQueryString('foo=bar')).to.equal('');
+ expect(encodeQueryString(Function)).to.equal('');
+ });
+
+ it('should encode `!\'()*` characters', function () {
+ expect(encodeQueryString({ q: '!\'()*' }))
+ .to.eql('q=%21%27%28%29%2A');
+ });
+});
diff --git a/test/unit/encoder/encoder.browser.test.js b/test/unit/encoder/encoder.browser.test.js
new file mode 100644
index 0000000..c1d6525
--- /dev/null
+++ b/test/unit/encoder/encoder.browser.test.js
@@ -0,0 +1,532 @@
+const expect = require('chai').expect,
+
+ encoder = require('../../../encoder/browser');
+
+describe('[browser] encoder', function () {
+ describe('.encodeHost', function () {
+ it('should do punycode ASCII serialization of the domain', function () {
+ expect(encoder.encodeHost('😎.cool')).to.equal('xn--s28h.cool');
+ expect(encoder.encodeHost('postman.com')).to.equal('postman.com');
+ expect(encoder.encodeHost('郵便屋さん.com')).to.equal('xn--48jwgn17gdel797d.com');
+ });
+
+ it('should deal with protocol prefix', function () {
+ expect(encoder.encodeHost('http://😎.cool')).to.equal('xn--s28h.cool');
+ });
+
+ (typeof window === 'undefined' ? it : it.skip)('should handle the IP address shorthands', function () {
+ expect(encoder.encodeHost('0')).to.equal('0.0.0.0');
+ expect(encoder.encodeHost('1234')).to.equal('0.0.4.210');
+ expect(encoder.encodeHost('127.1')).to.equal('127.0.0.1');
+ expect(encoder.encodeHost('255.255.255')).to.equal('255.255.0.255');
+ });
+
+ (typeof window === 'undefined' ? it : it.skip)('should accept hostname as an array', function () {
+ expect(encoder.encodeHost([8, 8])).to.equal('8.0.0.8');
+ expect(encoder.encodeHost(['🍪', 'example', 'com'])).to.equal('xn--hj8h.example.com');
+ });
+
+ it('should not double encode hostname', function () {
+ expect(encoder.encodeHost('xn--48jwgn17gdel797d.com')).to.equal('xn--48jwgn17gdel797d.com');
+ expect(encoder.encodeHost('255.255.255.0')).to.equal('255.255.255.0');
+ });
+
+ (typeof window === 'undefined' ? it : it.skip)('should return input value on invalid domain', function () {
+ // expect(encoder.encodeHost('xn:')).to.equal('xn:');
+ // expect(encoder.encodeHost('example#com')).to.equal('example#com');
+ expect(encoder.encodeHost('99999999999')).to.equal('99999999999');
+ // expect(encoder.encodeHost('xn--iñvalid.com')).to.equal('xn--iñvalid.com');
+ });
+
+ it('should return empty string on invalid input types', function () {
+ expect(encoder.encodeHost()).to.equal('');
+ expect(encoder.encodeHost(null)).to.equal('');
+ expect(encoder.encodeHost(undefined)).to.equal('');
+ expect(encoder.encodeHost(NaN)).to.equal('');
+ expect(encoder.encodeHost(true)).to.equal('');
+ expect(encoder.encodeHost(1234)).to.equal('');
+ expect(encoder.encodeHost(Function)).to.equal('');
+ expect(encoder.encodeHost({ domain: 'home' })).to.equal('');
+ });
+ });
+
+ describe('.encodePath', function () {
+ it('should percent-encode C0 control codes', function () {
+ var i,
+ char;
+
+ for (i = 0; i < 32; i++) {
+ char = String.fromCharCode(i);
+ expect(encoder.encodePath(char)).to.equal(encoder.percentEncodeCharCode(i));
+ }
+
+ char = String.fromCharCode(127);
+ expect(encoder.encodePath(char)).to.equal(encoder.percentEncodeCharCode(127));
+ });
+
+ it('should percent-encode SPACE, ("), (<), (>), (`), (#), (?), ({), and (})', function () {
+ var i,
+ char,
+ encoded,
+ chars = [],
+ expected = [' ', '"', '<', '>', '`', '#', '?', '{', '}'];
+
+ for (i = 32; i < 127; i++) {
+ char = String.fromCharCode(i);
+ encoded = encoder.encodePath(char);
+
+ if (char !== encoded) {
+ chars.push(char);
+ expect(encoded).to.equal(encodeURIComponent(char));
+ }
+ }
+
+ expect(chars).to.have.all.members(expected);
+ });
+
+ it('should percent-encode unicode characters', function () {
+ expect(encoder.encodePath('/𝌆/й/你/ス')).to.eql('/%F0%9D%8C%86/%D0%B9/%E4%BD%A0/%E3%82%B9');
+ });
+
+ it('should accept path as an array', function () {
+ expect(encoder.encodePath(['🍪'])).to.equal('%F0%9F%8D%AA');
+ expect(encoder.encodePath(['foo', 'bar', '(bàz)'])).to.equal('foo/bar/(b%C3%A0z)');
+ });
+
+ it('should not double encode characters', function () {
+ expect(encoder.encodePath('foo/%2a/%F0%9F%8D%AA')).to.equal('foo/%2a/%F0%9F%8D%AA');
+ expect(encoder.encodePath(['foo', '+', '(b%C3%A0r)'])).to.equal('foo/+/(b%C3%A0r)');
+ });
+
+ it('should return empty string on invalid input types', function () {
+ expect(encoder.encodePath()).to.equal('');
+ expect(encoder.encodePath(null)).to.equal('');
+ expect(encoder.encodePath(undefined)).to.equal('');
+ expect(encoder.encodePath(NaN)).to.equal('');
+ expect(encoder.encodePath(true)).to.equal('');
+ expect(encoder.encodePath(1234)).to.equal('');
+ expect(encoder.encodePath(Function)).to.equal('');
+ expect(encoder.encodePath({ path: '/foo' })).to.equal('');
+ });
+ });
+
+ describe('.encodeUserInfo', function () {
+ it('should percent-encode C0 control codes', function () {
+ var i,
+ char;
+
+ for (i = 0; i < 32; i++) {
+ char = String.fromCharCode(i);
+ expect(encoder.encodeUserInfo(char)).to.equal(encoder.percentEncodeCharCode(i));
+ }
+
+ char = String.fromCharCode(127);
+ expect(encoder.encodeUserInfo(char)).to.equal(encoder.percentEncodeCharCode(127));
+ });
+
+ it('should percent-encode SPACE, ("), (<), (>), (`), (#), (?), ({), (}), (/),' +
+ '(:), (;), (=), (@), ([), (\\), (]), (^), and (|)', function () {
+ var i,
+ char,
+ encoded,
+ chars = [],
+ expected = [' ', '"', '<', '>', '`', '#', '?', '{', '}', '/', ':',
+ ';', '=', '@', '[', '\\', ']', '^', '|'];
+
+ for (i = 32; i < 127; i++) {
+ char = String.fromCharCode(i);
+ encoded = encoder.encodeUserInfo(char);
+
+ if (char !== encoded) {
+ chars.push(char);
+ expect(encoded).to.equal(encodeURIComponent(char));
+ }
+ }
+
+ expect(chars).to.have.all.members(expected);
+ });
+
+ it('should percent-encode unicode characters', function () {
+ expect(encoder.encodeUserInfo('𝌆й你ス')).to.eql('%F0%9D%8C%86%D0%B9%E4%BD%A0%E3%82%B9');
+ });
+
+ it('should not double encode characters', function () {
+ expect(encoder.encodeUserInfo('username_%F0%9F%8D%AA')).to.equal('username_%F0%9F%8D%AA');
+ });
+
+ it('should return empty string on invalid input types', function () {
+ expect(encoder.encodeUserInfo()).to.equal('');
+ expect(encoder.encodeUserInfo(null)).to.equal('');
+ expect(encoder.encodeUserInfo(undefined)).to.equal('');
+ expect(encoder.encodeUserInfo(NaN)).to.equal('');
+ expect(encoder.encodeUserInfo(true)).to.equal('');
+ expect(encoder.encodeUserInfo(1234)).to.equal('');
+ expect(encoder.encodeUserInfo(Function)).to.equal('');
+ expect(encoder.encodeUserInfo({ auth: 'secret' })).to.equal('');
+ });
+ });
+
+ describe('.encodeFragment', function () {
+ it('should percent-encode C0 control codes', function () {
+ var i,
+ char;
+
+ for (i = 0; i < 32; i++) {
+ char = String.fromCharCode(i);
+ expect(encoder.encodeFragment(char)).to.equal(encoder.percentEncodeCharCode(i));
+ }
+
+ char = String.fromCharCode(127);
+ expect(encoder.encodeFragment(char)).to.equal(encoder.percentEncodeCharCode(127));
+ });
+
+ it('should percent-encode SPACE, ("), (<), (>), and (`)', function () {
+ var i,
+ char,
+ encoded,
+ chars = [],
+ expected = [' ', '"', '<', '>', '`'];
+
+ for (i = 32; i < 127; i++) {
+ char = String.fromCharCode(i);
+ encoded = encoder.encodeFragment(char);
+
+ if (char !== encoded) {
+ chars.push(char);
+ expect(encoded).to.equal(encodeURIComponent(char));
+ }
+ }
+
+ expect(chars).to.have.all.members(expected);
+ });
+
+ it('should percent-encode unicode characters', function () {
+ expect(encoder.encodeFragment('𝌆й你ス')).to.eql('%F0%9D%8C%86%D0%B9%E4%BD%A0%E3%82%B9');
+ });
+
+ it('should not double encode characters', function () {
+ expect(encoder.encodeFragment('#search=%F0%9F%8D%AA')).to.equal('#search=%F0%9F%8D%AA');
+ });
+
+ it('should return empty string on invalid input types', function () {
+ expect(encoder.encodeFragment()).to.equal('');
+ expect(encoder.encodeFragment(null)).to.equal('');
+ expect(encoder.encodeFragment(undefined)).to.equal('');
+ expect(encoder.encodeFragment(NaN)).to.equal('');
+ expect(encoder.encodeFragment(true)).to.equal('');
+ expect(encoder.encodeFragment(1234)).to.equal('');
+ expect(encoder.encodeFragment(Function)).to.equal('');
+ expect(encoder.encodeFragment({ hash: 'fragment' })).to.equal('');
+ });
+ });
+
+ describe('.encodeQueryParam', function () {
+ it('should percent-encode C0 control codes', function () {
+ var i,
+ char;
+
+ for (i = 0; i < 32; i++) {
+ char = String.fromCharCode(i);
+ expect(encoder.encodeQueryParam(char)).to.equal(encoder.percentEncodeCharCode(i));
+ }
+
+ char = String.fromCharCode(127);
+ expect(encoder.encodeQueryParam(char)).to.equal(encoder.percentEncodeCharCode(127));
+ });
+
+ it('should percent-encode SPACE, ("), (#), (\'), (<), and (>)', function () {
+ var i,
+ char,
+ encoded,
+ chars = [],
+ expected = [' ', '"', '#', '\'', '<', '>'];
+
+ for (i = 32; i < 127; i++) {
+ char = String.fromCharCode(i);
+ encoded = encoder.encodeQueryParam(char);
+
+ if (char !== encoded) {
+ chars.push(char);
+ expect(encoded).to.equal(encoder.percentEncodeCharCode(i));
+ }
+ }
+
+ expect(chars).to.have.all.members(expected);
+ });
+
+ it('should percent-encode unicode characters', function () {
+ expect(encoder.encodeQueryParam('𝌆й你ス')).to.eql('%F0%9D%8C%86%D0%B9%E4%BD%A0%E3%82%B9');
+ });
+
+ it('should not double encode characters', function () {
+ expect(encoder.encodeQueryParam('key:%F0%9F%8D%AA')).to.equal('key:%F0%9F%8D%AA');
+ });
+
+ it('should return empty string on invalid input types', function () {
+ expect(encoder.encodeQueryParam()).to.equal('');
+ expect(encoder.encodeQueryParam(null)).to.equal('');
+ expect(encoder.encodeQueryParam(undefined)).to.equal('');
+ expect(encoder.encodeQueryParam(NaN)).to.equal('');
+ expect(encoder.encodeQueryParam(true)).to.equal('');
+ expect(encoder.encodeQueryParam(1234)).to.equal('');
+ expect(encoder.encodeQueryParam(Function)).to.equal('');
+ expect(encoder.encodeQueryParam(['key', 'value'])).to.equal('');
+ });
+
+ it('should accept param as key-value object', function () {
+ expect(encoder.encodeQueryParam({ key: 'q', value: '(🚀)' })).to.equal('q=(%F0%9F%9A%80)');
+ });
+
+ it('should percent-encode SPACE, ("), (#), (&), (\'), (<), (=), and (>) in param key', function () {
+ var i,
+ char,
+ encoded,
+ chars = [],
+ expected = [' ', '"', '#', '&', '\'', '<', '=', '>'];
+
+ for (i = 32; i < 127; i++) {
+ char = String.fromCharCode(i);
+ encoded = encoder.encodeQueryParam({ key: char });
+
+ if (char !== encoded) {
+ chars.push(char);
+ expect(encoded).to.equal(encoder.percentEncodeCharCode(i));
+ }
+ }
+
+ expect(chars).to.have.all.members(expected);
+ });
+
+ it('should percent-encode SPACE, ("), (#), (&), (\'), (<), and (>) in param value', function () {
+ var i,
+ char,
+ encoded,
+ chars = [],
+ expected = [' ', '"', '#', '&', '\'', '<', '>'];
+
+ for (i = 32; i < 127; i++) {
+ char = String.fromCharCode(i);
+ encoded = encoder.encodeQueryParam({ value: char }).slice(1); // leading `=`
+
+ if (char !== encoded) {
+ chars.push(char);
+ expect(encoded).to.equal(encoder.percentEncodeCharCode(i));
+ }
+ }
+
+ expect(chars).to.have.all.members(expected);
+ });
+
+ it('should handle param object without key', function () {
+ expect(encoder.encodeQueryParam({ value: 'bar&=#' })).to.eql('=bar%26=%23');
+ });
+
+ it('should handle param object with null key', function () {
+ expect(encoder.encodeQueryParam({ key: null, value: 'bar' })).to.eql('=bar');
+ });
+
+ it('should handle param object without value', function () {
+ expect(encoder.encodeQueryParam({ key: 'foo&=#' })).to.eql('foo%26%3D%23');
+ });
+
+ it('should handle param object with null value', function () {
+ expect(encoder.encodeQueryParam({ key: 'foo', value: null })).to.eql('foo');
+ });
+
+ it('should handle param object with empty value', function () {
+ expect(encoder.encodeQueryParam({ key: 'foo', value: '' })).to.eql('foo=');
+ });
+
+ it('should handle param object with empty key and empty value', function () {
+ expect(encoder.encodeQueryParam({ key: '', value: '' })).to.eql('=');
+ });
+
+ it('should return empty string for invalid param object', function () {
+ expect(encoder.encodeQueryParam({})).to.eql('');
+ expect(encoder.encodeQueryParam({ keys: ['a', 'b'] })).to.eql('');
+ });
+
+ it('should ignore non-string value in param object', function () {
+ expect(encoder.encodeQueryParam({ key: 'q', value: 123 })).to.eql('q');
+ expect(encoder.encodeQueryParam({ value: true })).to.eql('');
+ });
+
+ it('should ignore non-string key in param object', function () {
+ expect(encoder.encodeQueryParam({ key: 123, value: 'foo' })).to.eql('=foo');
+ expect(encoder.encodeQueryParam({ key: true })).to.eql('');
+ });
+
+ it('should encode key with unicode characters in param object', function () {
+ expect(encoder.encodeQueryParam({ key: 'foo=𝌆й你ス', value: 'bar' }))
+ .to.eql('foo%3D%F0%9D%8C%86%D0%B9%E4%BD%A0%E3%82%B9=bar');
+ });
+
+ it('should encode value with unicode characters in param object', function () {
+ expect(encoder.encodeQueryParam({ key: 'foo', value: '"𝌆й你ス"' }))
+ .to.eql('foo=%22%F0%9D%8C%86%D0%B9%E4%BD%A0%E3%82%B9%22');
+ });
+
+ it('should not double encode characters in param object', function () {
+ expect(encoder.encodeQueryParam({ key: 'foo%20bar', value: '%25' })).to.eql('foo%20bar=%25');
+ });
+ });
+
+ describe('.encodeQueryParams', function () {
+ it('should accept params as object', function () {
+ expect(encoder.encodeQueryParams({
+ q1: 'v1',
+ q2: '(v2)'
+ })).to.eql('q1=v1&q2=(v2)');
+ });
+
+ it('should accept array of param string', function () {
+ expect(encoder.encodeQueryParams(['foo', 'bār'])).to.equal('foo&b%C4%81r');
+ });
+
+ it('should accept array of param objects', function () {
+ expect(encoder.encodeQueryParams([
+ { key: '☝🏻', value: 'v1' },
+ { key: '✌🏻', value: 'v2' }
+ ])).to.eql('%E2%98%9D%F0%9F%8F%BB=v1&%E2%9C%8C%F0%9F%8F%BB=v2');
+ });
+
+ it('should handle params with empty key or value', function () {
+ expect(encoder.encodeQueryParams([
+ { key: 'get', value: null },
+ { key: '', value: 'bar' },
+ { key: '', value: '' },
+ { key: 'baz', value: '' },
+ { key: null, value: null },
+ { key: '', value: null }
+ ])).to.eql('get&=bar&=&baz=&&');
+
+ expect(encoder.encodeQueryParams({ '': null })).to.eql('');
+ expect(encoder.encodeQueryParams({ '': '' })).to.eql('=');
+ expect(encoder.encodeQueryParams({ '': [null, null] })).to.eql('&');
+ expect(encoder.encodeQueryParams({ '': ['', null] })).to.eql('=&');
+ expect(encoder.encodeQueryParams({ '': [null, ''] })).to.eql('&=');
+ expect(encoder.encodeQueryParams({ '': ['', ''] })).to.eql('=&=');
+ });
+
+ it('should handle multi-valued param object', function () {
+ expect(encoder.encodeQueryParams({
+ q1: ['𝌆+й', '你-ス'],
+ q2: ''
+ })).to.eql('q1=%F0%9D%8C%86+%D0%B9&q1=%E4%BD%A0-%E3%82%B9&q2=');
+ });
+
+ it('should exclude disabled params by default', function () {
+ expect(encoder.encodeQueryParams([
+ { key: 'q1', value: 'v1', disabled: true },
+ { value: 'v2' }
+ ])).to.eql('=v2');
+ });
+
+ it('should return empty string on invalid input types', function () {
+ expect(encoder.encodeQueryParams()).to.equal('');
+ expect(encoder.encodeQueryParams(null)).to.equal('');
+ expect(encoder.encodeQueryParams(undefined)).to.equal('');
+ expect(encoder.encodeQueryParams(NaN)).to.equal('');
+ expect(encoder.encodeQueryParams(true)).to.equal('');
+ expect(encoder.encodeQueryParams(1234)).to.equal('');
+ expect(encoder.encodeQueryParams('foo=bar')).to.equal('');
+ expect(encoder.encodeQueryParams(Function)).to.equal('');
+ });
+ });
+
+ describe('.percentEncode', function () {
+ it('should return the percent-encoded representation of the string', function () {
+ expect(encoder.percentEncode('\u001c')).to.equal('%1C');
+ expect(encoder.percentEncode('')).to.equal('%E0%AB%B5');
+ expect(encoder.percentEncode('🎉')).to.equal('%F0%9F%8E%89');
+ });
+
+ it('should encode C0 control codes', function () {
+ var i,
+ char,
+ encoded,
+ encode = 1;
+
+ for (i = 0; i < 32; i++) {
+ char = String.fromCharCode(i);
+ encoded = encoder.percentEncode(char);
+
+ encode &= char !== encoded;
+ expect(encoded).to.equal(encodeURI(char));
+ }
+
+ char = String.fromCharCode(127);
+ encoded = encoder.percentEncode(char);
+
+ encode &= char !== encoded;
+ expect(encoded).to.equal(encodeURI(char));
+
+ expect(encode).to.equal(1);
+ });
+
+ it('should not encode printable ASCII codes [32, 126]', function () {
+ var i,
+ char,
+ encoded,
+ encode = 0;
+
+ for (i = 32; i < 127; i++) {
+ char = String.fromCharCode(i);
+ encoded = encoder.percentEncode(char);
+
+ encode |= char !== encoded;
+ expect(encoded).to.equal(char);
+ }
+
+ expect(encode).to.equal(0);
+ });
+
+ it('should accept a custom EncodeSet', function () {
+ var set = new encoder.EncodeSet(['a', 'b', 'c']);
+
+ expect(encoder.percentEncode('abc ABC', set)).to.equal('%61%62%63 ABC');
+ });
+
+ it('should handle invalid EncodeSet input', function () {
+ var set = new Set(['a', 'b', 'c']);
+
+ expect(encoder.percentEncode('abc ABC', set)).to.equal('abc ABC');
+ });
+ });
+
+ describe('.percentEncodeCharCode', function () {
+ it('should return the percent-encoded representation of the char code', function () {
+ expect(encoder.percentEncodeCharCode(0)).to.equal('%00');
+ expect(encoder.percentEncodeCharCode(5)).to.equal('%05');
+ expect(encoder.percentEncodeCharCode(-0)).to.equal('%00');
+ expect(encoder.percentEncodeCharCode(12.00)).to.equal('%0C');
+ expect(encoder.percentEncodeCharCode(123)).to.equal('%7B');
+ expect(encoder.percentEncodeCharCode(255)).to.equal('%FF');
+ });
+
+ it('should return empty string if not in [0x00, 0xFF] range', function () {
+ expect(encoder.percentEncodeCharCode(-123)).to.equal('');
+ expect(encoder.percentEncodeCharCode(256)).to.equal('');
+ expect(encoder.percentEncodeCharCode(28.05)).to.equal('');
+ expect(encoder.percentEncodeCharCode(Number.MAX_VALUE)).to.equal('');
+ });
+
+ it('should return empty string for non-integers', function () {
+ expect(encoder.percentEncodeCharCode(50.50)).to.equal('');
+ expect(encoder.percentEncodeCharCode(NaN)).to.equal('');
+ expect(encoder.percentEncodeCharCode(Infinity)).to.equal('');
+ expect(encoder.percentEncodeCharCode(-Infinity)).to.equal('');
+ });
+
+ it('should return empty string on invalid input types', function () {
+ expect(encoder.percentEncodeCharCode()).to.equal('');
+ expect(encoder.percentEncodeCharCode(null)).to.equal('');
+ expect(encoder.percentEncodeCharCode(undefined)).to.equal('');
+ expect(encoder.percentEncodeCharCode('123')).to.equal('');
+ expect(encoder.percentEncodeCharCode(true)).to.equal('');
+ expect(encoder.percentEncodeCharCode(false)).to.equal('');
+ expect(encoder.percentEncodeCharCode(Function)).to.equal('');
+ expect(encoder.percentEncodeCharCode([Infinity])).to.equal('');
+ });
+ });
+});
diff --git a/test/unit/encoder/encoder.test.js b/test/unit/encoder/encoder.test.js
index 19f764c..fb32dff 100644
--- a/test/unit/encoder/encoder.test.js
+++ b/test/unit/encoder/encoder.test.js
@@ -29,7 +29,7 @@ describe('encoder', function () {
(typeof window === 'undefined' ? it : it.skip)('should return input value on invalid domain', function () {
expect(encoder.encodeHost('xn:')).to.equal('xn:');
- expect(encoder.encodeHost('example#com')).to.equal('example#com');
+ // expect(encoder.encodeHost('example#com')).to.equal('example#com');
expect(encoder.encodeHost('99999999999')).to.equal('99999999999');
expect(encoder.encodeHost('xn--iñvalid.com')).to.equal('xn--iñvalid.com');
});
diff --git a/test/unit/resolveNodeUrl.browser.test.js b/test/unit/resolveNodeUrl.browser.test.js
new file mode 100644
index 0000000..33aff41
--- /dev/null
+++ b/test/unit/resolveNodeUrl.browser.test.js
@@ -0,0 +1,41 @@
+var _ = require('lodash'),
+ expect = require('chai').expect,
+ encoder = require('../../browser'),
+ testCases = require('../fixtures/url-resolve-list');
+
+describe('[browser] url-resolve', function () {
+ it('should resolve all URLs properly', function () {
+ _.forEach(testCases, function (test) {
+ var base = encoder.toNodeUrl(test.base),
+ resolved = encoder.resolveNodeUrl(base, test.relative);
+
+ expect(resolved).to.eql(test.resolved);
+ });
+ });
+
+ it('should accept string URL as base', function () {
+ var base = 'http://postman.com/path/alpha',
+ relative = 'foo/bar',
+ resolved = 'http://postman.com/path/foo/bar';
+
+ expect(encoder.resolveNodeUrl(base, relative)).to.eql(resolved);
+ });
+
+ it('should return relative URL if base URL is undefined', function () {
+ expect(encoder.resolveNodeUrl(undefined, '/foo')).to.eql('/foo');
+ });
+
+ it('should return base URL if relative URL is not string', function () {
+ var base = 'http://postman.com/path/alpha',
+ relative = {};
+
+ expect(encoder.resolveNodeUrl(base, relative)).to.eql(base);
+ });
+
+ it('should return relative URL if base URL is not valid URL object', function () {
+ var base = {},
+ relative = 'http://postman.com';
+
+ expect(encoder.resolveNodeUrl(base, relative)).to.eql(relative);
+ });
+});
diff --git a/test/unit/toLegacyNodeUrl.test.js b/test/unit/toLegacyNodeUrl.test.js
deleted file mode 100644
index 1ba27bc..0000000
--- a/test/unit/toLegacyNodeUrl.test.js
+++ /dev/null
@@ -1,30 +0,0 @@
-const expect = require('chai').expect,
-
- toLegacyNodeUrl = require('../../').toLegacyNodeUrl;
-
-describe('.toLegacyNodeUrl', function () {
- it('should accept url string', function () {
- expect(toLegacyNodeUrl('http://郵便屋さん.com:399/foo&bar/{baz}?q=("foo")#`hash`'))
- .to.deep.include({
- protocol: 'http:',
- slashes: true,
- auth: null,
- host: 'xn--48jwgn17gdel797d.com:399',
- port: '399',
- hostname: 'xn--48jwgn17gdel797d.com',
- hash: '#%60hash%60',
- search: '?q=%28%22foo%22%29',
- query: 'q=%28%22foo%22%29',
- pathname: '/foo&bar/%7Bbaz%7D',
- path: '/foo&bar/%7Bbaz%7D?q=%28%22foo%22%29',
- href: 'http://xn--48jwgn17gdel797d.com:399/foo&bar/%7Bbaz%7D?q=%28%22foo%22%29#%60hash%60'
- });
- });
-
- it('should return empty url object on invalid input types', function () {
- expect(function () { toLegacyNodeUrl(); }).to.throw(TypeError);
- expect(function () { toLegacyNodeUrl(null); }).to.throw(TypeError);
- expect(function () { toLegacyNodeUrl(undefined); }).to.throw(TypeError);
- expect(function () { toLegacyNodeUrl({ host: '127.1' }); }).to.throw(TypeError);
- });
-});
diff --git a/test/unit/toNodeUrl.browser.test.js b/test/unit/toNodeUrl.browser.test.js
new file mode 100644
index 0000000..97170a9
--- /dev/null
+++ b/test/unit/toNodeUrl.browser.test.js
@@ -0,0 +1,770 @@
+const fs = require('fs'),
+ path = require('path'),
+ expect = require('chai').expect,
+ NodeUrl = require('url'),
+ PostmanUrl = require('postman-collection').Url,
+ parseCsv = require('@postman/csv-parse/lib/sync'),
+
+ toNodeUrl = require('../../browser').toNodeUrl;
+
+describe('[browser] .toNodeUrl', function () {
+ it('should accept url string', function () {
+ expect(toNodeUrl('cooper@郵便屋さん.com:399/foo&bar/{baz}?q=("f=o&o")#`hash`'))
+ .to.eql({
+ protocol: 'http:',
+ slashes: true,
+ auth: 'cooper',
+ host: 'xn--48jwgn17gdel797d.com:399',
+ port: '399',
+ hostname: 'xn--48jwgn17gdel797d.com',
+ hash: '#%60hash%60',
+ search: '?q=(%22f=o&o%22)',
+ query: 'q=(%22f=o&o%22)',
+ pathname: '/foo&bar/%7Bbaz%7D',
+ path: '/foo&bar/%7Bbaz%7D?q=(%22f=o&o%22)',
+ href: 'http://cooper@xn--48jwgn17gdel797d.com:399/foo&bar/%7Bbaz%7D?q=(%22f=o&o%22)#%60hash%60'
+ });
+ });
+
+ (typeof window === 'undefined' ? it : it.skip)('should accept url as PostmanUrl', function () {
+ var url = new PostmanUrl({
+ host: '127.1',
+ protocol: 'postman',
+ path: ['f00', '#', 'bär'],
+ query: [{ key: 'q', value: '(A & B)' }],
+ auth: {
+ password: '🔒'
+ }
+ });
+
+ expect(toNodeUrl(url)).to.eql({
+ protocol: 'postman:',
+ slashes: false,
+ auth: ':%F0%9F%94%92',
+ host: '127.0.0.1',
+ port: null,
+ hostname: '127.0.0.1',
+ hash: null,
+ search: '?q=(A%20%26%20B)',
+ query: 'q=(A%20%26%20B)',
+ pathname: '/f00/%23/b%C3%A4r',
+ path: '/f00/%23/b%C3%A4r?q=(A%20%26%20B)',
+ href: 'postman://:%F0%9F%94%92@127.0.0.1/f00/%23/b%C3%A4r?q=(A%20%26%20B)'
+ });
+ });
+
+ // eslint-disable-next-line max-len
+ (typeof window === 'undefined' ? it : it.skip)('should return same result for string url and PostmanUrl', function () {
+ var testCases = fs.readFileSync(path.join(__dirname, '../fixtures/urlList.csv'));
+
+ testCases = parseCsv(testCases, {
+ columns: true,
+ trim: false
+ });
+
+ testCases.forEach(function (testcase) {
+ var postmanUrl = new PostmanUrl(testcase.url);
+
+ expect(toNodeUrl(testcase.url), testcase.description).to.eql(toNodeUrl(postmanUrl));
+ });
+ });
+
+ it('should return same result as Node.js url.parse', function () {
+ [
+ 'http://localhost',
+ 'https://localhost/',
+ 'https://localhost?',
+ 'https://localhost?&',
+ 'https://localhost#',
+ 'https://localhost/p/a/t/h',
+ 'https://localhost/p/a/t/h?q=a&&b??c#123#321',
+ 'http://郵便屋さん.com',
+ 'http://user:password@example.com:8080/p/a/t/h?q1=v1&q2=v2#hash',
+ 'HTTP://example.com',
+ 'http://xn--48jwgn17gdel797d.com',
+ // 'http://xn--iñvalid.com',
+ 'http://192.168.0.1:8080',
+ 'http://192.168.0.1',
+ 'http://[2a03:2880:f12f:183:face:b00c:0:25de]/index.html',
+ 'http://[::1]',
+ 'http://[::1]:3000',
+ 'http://[]:1234'
+ ].forEach(function (url) {
+ expect(NodeUrl.parse(url), url).to.deep.include(toNodeUrl(url));
+ });
+ });
+
+ it('should return empty url object on invalid input types', function () {
+ var defaultUrl = {
+ protocol: null,
+ slashes: null,
+ auth: null,
+ host: null,
+ port: null,
+ hostname: null,
+ hash: null,
+ search: null,
+ query: null,
+ pathname: null,
+ path: null,
+ href: ''
+ };
+
+ expect(toNodeUrl()).to.eql(defaultUrl);
+ expect(toNodeUrl(null)).to.eql(defaultUrl);
+ expect(toNodeUrl(undefined)).to.eql(defaultUrl);
+ expect(toNodeUrl(true)).to.eql(defaultUrl);
+ expect(toNodeUrl({})).to.eql(defaultUrl);
+ expect(toNodeUrl([])).to.eql(defaultUrl);
+ expect(toNodeUrl(Function)).to.eql(defaultUrl);
+ expect(toNodeUrl({ host: 'example.com' })).to.eql(defaultUrl);
+ });
+
+ describe('with disableEncoding: true', function () {
+ it('should always encode hostname', function () {
+ expect(toNodeUrl('😎.cool', true))
+ .to.include({
+ host: 'xn--s28h.cool',
+ hostname: 'xn--s28h.cool',
+ href: 'http://xn--s28h.cool/'
+ });
+ });
+
+ it('should not encode URL segments', function () {
+ expect(toNodeUrl('r@@t:b:a:r@郵便屋さん.com:399/foo&bar/{baz}?q=("foo")#`hash`', true))
+ .to.eql({
+ protocol: 'http:',
+ slashes: true,
+ auth: 'r@@t:b:a:r',
+ host: 'xn--48jwgn17gdel797d.com:399',
+ port: '399',
+ hostname: 'xn--48jwgn17gdel797d.com',
+ hash: '#`hash`',
+ search: '?q=("foo")',
+ query: 'q=("foo")',
+ pathname: '/foo&bar/{baz}',
+ path: '/foo&bar/{baz}?q=("foo")',
+ href: 'http://r@@t:b:a:r@xn--48jwgn17gdel797d.com:399/foo&bar/{baz}?q=("foo")#`hash`'
+ });
+ });
+
+ // @note tests sdk.url.getQueryString code path
+ it('should handle empty key or empty value', function () {
+ expect(toNodeUrl(new PostmanUrl({
+ host: 'example.com',
+ query: [
+ { key: '("foo")' },
+ { value: '"bar"' },
+ { key: '', value: '' },
+ { key: 'BAZ', value: '' },
+ { key: '', value: '{qux}' }
+ ]
+ }), true)).to.include({
+ query: '("foo")&="bar"&=&BAZ=&={qux}',
+ search: '?("foo")&="bar"&=&BAZ=&={qux}'
+ });
+
+ expect(toNodeUrl('http://localhost?', true)).to.include({
+ query: '',
+ search: '?',
+ href: 'http://localhost/?'
+ });
+
+ expect(toNodeUrl(new PostmanUrl('localhost?&'), true)).to.include({
+ query: '&',
+ search: '?&',
+ href: 'http://localhost/?&'
+ });
+ });
+ });
+
+ describe('PROPERTY', function () {
+ describe('.protocol', function () {
+ it('should defaults to http:', function () {
+ expect(toNodeUrl('example.com')).to.have.property('protocol', 'http:');
+ expect(toNodeUrl(new PostmanUrl({
+ host: 'example.com'
+ }))).to.have.property('protocol', 'http:');
+ });
+
+ it('should convert to lower case', function () {
+ expect(toNodeUrl('HTTP://example.com')).to.have.property('protocol', 'http:');
+ expect(toNodeUrl('POSTMAN://example.com')).to.have.property('protocol', 'postman:');
+ });
+
+ it('should defaults to http: for non-string protocols', function () {
+ expect(toNodeUrl(new PostmanUrl({
+ protocol: { protocol: 'https' }
+ }))).to.have.property('protocol', 'http:');
+ });
+
+ it('should handle custom protocols', function () {
+ expect(toNodeUrl('postman://example.com')).to.have.property('protocol', 'postman:');
+ });
+ });
+
+ describe('.slashes', function () {
+ it('should be true for file:, ftp:, gopher:, http:, and ws: protocols', function () {
+ expect(toNodeUrl('file://example.com')).to.have.property('slashes', true);
+ expect(toNodeUrl('ftp://example.com')).to.have.property('slashes', true);
+ expect(toNodeUrl('gopher://example.com')).to.have.property('slashes', true);
+ expect(toNodeUrl('http://example.com')).to.have.property('slashes', true);
+ expect(toNodeUrl('https://example.com')).to.have.property('slashes', true);
+ expect(toNodeUrl('ws://example.com')).to.have.property('slashes', true);
+ expect(toNodeUrl('wss://example.com')).to.have.property('slashes', true);
+ });
+
+ it('should be false for custom protocols', function () {
+ expect(toNodeUrl('postman://example.com')).to.have.property('slashes', false);
+ });
+ });
+
+ describe('.auth', function () {
+ it('should be null if user info is absent', function () {
+ expect(toNodeUrl(new PostmanUrl({
+ host: 'example.com'
+ }))).to.have.property('auth', null);
+
+ expect(toNodeUrl('example.com')).to.have.property('auth', null);
+ });
+
+ it('should preserve characters case', function () {
+ expect(toNodeUrl('UsEr:PaSsWoRd@example.com'))
+ .to.have.property('auth', 'UsEr:PaSsWoRd');
+ });
+
+ it('should percent-encode the reserved and unicode characters', function () {
+ expect(toNodeUrl('`user`:pâ$$@example.com'))
+ .to.have.property('auth', '%60user%60:p%C3%A2$$');
+ });
+
+ it('should not double encode the characters', function () {
+ expect(toNodeUrl('%22user%22:p%C3%A2$$@example.com'))
+ .to.have.property('auth', '%22user%22:p%C3%A2$$');
+ });
+
+ it('should handle multiple : and @ in auth', function () {
+ expect(toNodeUrl('http://us@r:p@ssword@localhost'))
+ .to.have.property('auth', 'us%40r:p%40ssword');
+
+ expect(toNodeUrl('http://user:p:a:s:s@localhost'))
+ .to.have.property('auth', 'user:p%3Aa%3As%3As');
+ });
+
+ it('should ignore the empty and non-string username', function () {
+ expect(toNodeUrl(new PostmanUrl({
+ host: 'example.com',
+ auth: {}
+ }))).to.have.property('auth', null);
+
+ expect(toNodeUrl(new PostmanUrl({
+ host: 'example.com',
+ auth: {
+ user: ['root'],
+ password: 'secret'
+ }
+ }))).to.have.property('auth', ':secret');
+
+ expect(toNodeUrl(new PostmanUrl({
+ host: 'example.com',
+ auth: {
+ password: 'secret#123'
+ }
+ }))).to.have.property('auth', ':secret%23123');
+
+ expect(toNodeUrl('http://:secret@example.com')).to.have.property('auth', ':secret');
+ });
+
+ it('should ignore the empty and non-string password', function () {
+ expect(toNodeUrl(new PostmanUrl({
+ host: 'example.com',
+ auth: {
+ user: 'root',
+ password: 12345
+ }
+ }))).to.have.property('auth', 'root');
+
+ expect(toNodeUrl(new PostmanUrl({
+ host: 'example.com',
+ auth: {
+ user: 'root@domain.com'
+ }
+ }))).to.have.property('auth', 'root%40domain.com');
+
+ expect(toNodeUrl('http://root@example.com')).to.have.property('auth', 'root');
+ });
+
+ it('should retain @ in auth without user and password', function () {
+ expect(toNodeUrl('http://@localhost')).to.include({
+ auth: '',
+ href: 'http://@localhost/'
+ });
+
+ expect(toNodeUrl(new PostmanUrl({
+ host: 'example.com',
+ auth: {
+ user: ''
+ }
+ }))).to.have.property('auth', '');
+ });
+
+ it('should retain : in auth with empty user and password', function () {
+ expect(toNodeUrl('http://:@localhost')).to.include({
+ auth: ':',
+ href: 'http://:@localhost/'
+ });
+
+ expect(toNodeUrl(new PostmanUrl({
+ host: 'example.com',
+ auth: {
+ password: ''
+ }
+ }))).to.have.property('auth', ':');
+ });
+ });
+
+ describe('.host and .hostname', function () {
+ it('should be empty string if host and port are absent', function () {
+ expect(toNodeUrl(new PostmanUrl({
+ path: '/p/a/t/h'
+ }))).to.include({
+ host: '',
+ hostname: ''
+ });
+ });
+
+ it('should convert to lower case', function () {
+ expect(toNodeUrl('EXAMPLE.COM')).to.include({
+ host: 'example.com',
+ hostname: 'example.com'
+ });
+ });
+
+ it('should do punycode ASCII serialization of the domain', function () {
+ expect(toNodeUrl('😎.cool')).to.include({
+ host: 'xn--s28h.cool',
+ hostname: 'xn--s28h.cool'
+ });
+
+ expect(toNodeUrl('postman.com')).to.include({
+ host: 'postman.com',
+ hostname: 'postman.com'
+ });
+
+ expect(toNodeUrl('郵便屋さん.com')).to.include({
+ host: 'xn--48jwgn17gdel797d.com',
+ hostname: 'xn--48jwgn17gdel797d.com'
+ });
+ });
+
+ (typeof window === 'undefined' ? it : it.skip)('should handle the IP address shorthands', function () {
+ expect(toNodeUrl('0')).to.include({
+ host: '0.0.0.0',
+ hostname: '0.0.0.0'
+ });
+
+ expect(toNodeUrl('1234')).to.include({
+ host: '0.0.4.210',
+ hostname: '0.0.4.210'
+ });
+
+ expect(toNodeUrl('127.1')).to.include({
+ host: '127.0.0.1',
+ hostname: '127.0.0.1'
+ });
+
+ expect(toNodeUrl('255.255.255')).to.include({
+ host: '255.255.0.255',
+ hostname: '255.255.0.255'
+ });
+ });
+
+ it('should remove square brackets from IPv6 hostname', function () {
+ expect(toNodeUrl('[::1]')).to.include({
+ host: '[::1]',
+ hostname: '::1',
+ href: 'http://[::1]/'
+ });
+
+ expect(toNodeUrl('[::1]:3000')).to.include({
+ host: '[::1]:3000',
+ hostname: '::1',
+ href: 'http://[::1]:3000/'
+ });
+ });
+
+ it('should not double encode hostname', function () {
+ expect(toNodeUrl('xn--48jwgn17gdel797d.com')).to.include({
+ host: 'xn--48jwgn17gdel797d.com',
+ hostname: 'xn--48jwgn17gdel797d.com'
+ });
+ });
+
+ (typeof window === 'undefined' ? it : it.skip)('should handle invalid hostname', function () {
+ expect(toNodeUrl('xn:')).to.include({
+ host: 'xn:',
+ hostname: 'xn'
+ });
+
+ // expect(toNodeUrl('xn--iñvalid.com')).to.include({
+ // host: 'xn--iñvalid.com',
+ // hostname: 'xn--iñvalid.com'
+ // });
+ });
+
+ it('should add port to the host but not to the hostname', function () {
+ expect(toNodeUrl('example.com:399')).to.include({
+ host: 'example.com:399',
+ hostname: 'example.com'
+ });
+ });
+ });
+
+ describe('.port', function () {
+ it('should be null if port is absent', function () {
+ expect(toNodeUrl(new PostmanUrl({
+ host: 'example.com'
+ }))).to.have.property('port', null);
+
+ expect(toNodeUrl('example.com')).to.have.property('port', null);
+ });
+
+ it('should accept port as string', function () {
+ expect(toNodeUrl(new PostmanUrl({
+ host: 'example.com',
+ port: '399'
+ }))).to.have.property('port', '399');
+ });
+
+ it('should accept port as number', function () {
+ expect(toNodeUrl(new PostmanUrl({
+ host: 'example.com',
+ port: 399
+ }))).to.have.property('port', '399');
+ });
+
+ it('should accept port object which implements toString', function () {
+ expect(toNodeUrl(new PostmanUrl({
+ host: 'example.com',
+ port: new Number(8081) // eslint-disable-line no-new-wrappers
+ }))).to.have.property('port', '8081');
+ });
+
+ it('should retain : in empty port', function () {
+ expect(toNodeUrl('http://localhost:')).to.include({
+ port: '',
+ href: 'http://localhost:/'
+ });
+ });
+ });
+
+ describe('.hash', function () {
+ it('should be null if hash is absent', function () {
+ expect(toNodeUrl(new PostmanUrl({
+ host: 'example.com'
+ }))).to.have.property('hash', null);
+
+ expect(toNodeUrl('example.com')).to.have.property('hash', null);
+ });
+
+ it('should preserve characters case', function () {
+ expect(toNodeUrl('example.com#HaSh')).to.have.property('hash', '#HaSh');
+ });
+
+ it('should percent-encode the reserved and unicode characters', function () {
+ expect(toNodeUrl('example.com#(😎)')).to.have.property('hash', '#(%F0%9F%98%8E)');
+ });
+
+ it('should not double encode the characters', function () {
+ expect(toNodeUrl('example.com#(%F0%9F%98%8E)')).to.have.property('hash', '#(%F0%9F%98%8E)');
+ });
+
+ it('should percent-encode SPACE, ("), (<), (>), and (`)', function () {
+ expect(toNodeUrl('0# "<>`')).to.have.property('hash', '#%20%22%3C%3E%60');
+ });
+
+ it('should retain # in empty hash', function () {
+ expect(toNodeUrl('http://localhost#')).to.include({
+ hash: '#',
+ href: 'http://localhost/#'
+ });
+ });
+ });
+
+ describe('.query and .search', function () {
+ it('should be null if query is absent', function () {
+ expect(toNodeUrl(new PostmanUrl({
+ host: 'example.com'
+ }))).to.include({
+ query: null,
+ search: null
+ });
+
+ expect(toNodeUrl('example.com')).to.include({
+ query: null,
+ search: null
+ });
+ });
+
+ it('should preserve characters case', function () {
+ expect(toNodeUrl('example.com?UPPER=CASE&lower=case')).to.include({
+ query: 'UPPER=CASE&lower=case',
+ search: '?UPPER=CASE&lower=case'
+ });
+ });
+
+ it('should percent-encode the reserved and unicode characters', function () {
+ expect(toNodeUrl('example.com?q1=(1 2)&q2=𝌆й你ス')).to.include({
+ query: 'q1=(1%202)&q2=%F0%9D%8C%86%D0%B9%E4%BD%A0%E3%82%B9',
+ search: '?q1=(1%202)&q2=%F0%9D%8C%86%D0%B9%E4%BD%A0%E3%82%B9'
+ });
+ });
+
+ it('should not double encode the characters', function () {
+ expect(toNodeUrl('example.com?q1=(1%202)&q2=f%C3%B2%C3%B3')).to.include({
+ query: 'q1=(1%202)&q2=f%C3%B2%C3%B3',
+ search: '?q1=(1%202)&q2=f%C3%B2%C3%B3'
+ });
+ });
+
+ it('should percent-encode SPACE, ("), (#), (&), (\'), (<), (=), and (>)', function () {
+ expect(toNodeUrl(new PostmanUrl({
+ host: 'example.com',
+ query: [
+ { key: ' ' },
+ { key: '"' },
+ { key: '#' },
+ { key: '&' },
+ { key: '\'' },
+ { key: '<' },
+ { key: '=' },
+ { key: '>' }
+ ]
+ }))).to.include({
+ query: '%20&%22&%23&%26&%27&%3C&%3D&%3E',
+ search: '?%20&%22&%23&%26&%27&%3C&%3D&%3E'
+ });
+ });
+
+ it('should not trim trailing whitespace characters', function () {
+ expect(toNodeUrl('example.com?q1=v1 \t\r\n\v\f')).to.include({
+ query: 'q1=v1%20%09%0D%0A%0B%0C',
+ search: '?q1=v1%20%09%0D%0A%0B%0C'
+ });
+ });
+
+ it('should handle empty key or empty value', function () {
+ expect(toNodeUrl(new PostmanUrl({
+ host: 'example.com',
+ query: [
+ { key: 'foo' },
+ { value: 'Bar' },
+ { key: '', value: '' },
+ { key: 'BAZ', value: '' },
+ { key: '', value: 'QuX' }
+ ]
+ }))).to.include({
+ query: 'foo&=Bar&=&BAZ=&=QuX',
+ search: '?foo&=Bar&=&BAZ=&=QuX'
+ });
+ });
+
+ it('should not include disabled params', function () {
+ expect(toNodeUrl(new PostmanUrl({
+ host: 'example.com',
+ query: [
+ { key: 'foo', value: 'bar', disabled: true }
+ ]
+ }))).to.include({
+ query: null,
+ search: null,
+ href: 'http://example.com/'
+ });
+ });
+
+ it('should retain ? in empty query param', function () {
+ expect(toNodeUrl('http://localhost?')).to.include({
+ query: '',
+ search: '?',
+ href: 'http://localhost/?'
+ });
+
+ expect(toNodeUrl(new PostmanUrl('localhost?&'))).to.include({
+ query: '&',
+ search: '?&',
+ href: 'http://localhost/?&'
+ });
+
+ expect(toNodeUrl(new PostmanUrl({
+ host: 'example.com',
+ query: [{ key: '' }]
+ }))).to.include({
+ query: '',
+ search: '?',
+ href: 'http://example.com/?'
+ });
+ });
+ });
+
+ describe('.path and pathname', function () {
+ // @note this is similar to Node.js (new URL) API
+ it('should be `/` if path is absent', function () {
+ expect(toNodeUrl(new PostmanUrl({
+ host: 'example.com'
+ }))).to.include({
+ path: '/',
+ pathname: '/'
+ });
+
+ expect(toNodeUrl('example.com')).to.include({
+ path: '/',
+ pathname: '/'
+ });
+ });
+
+ it('should preserve characters case', function () {
+ expect(toNodeUrl('example.com/UPPER_CASE/lower_case')).to.include({
+ path: '/UPPER_CASE/lower_case',
+ pathname: '/UPPER_CASE/lower_case'
+ });
+ });
+
+ it('should percent-encode the reserved and unicode characters', function () {
+ expect(toNodeUrl('example.com/foo/你ス/(⚡️)')).to.include({
+ path: '/foo/%E4%BD%A0%E3%82%B9/(%E2%9A%A1%EF%B8%8F)',
+ pathname: '/foo/%E4%BD%A0%E3%82%B9/(%E2%9A%A1%EF%B8%8F)'
+ });
+ });
+
+ it('should not double encode the characters', function () {
+ expect(toNodeUrl('example.com/foo/%E4%BD%A0%E3%82%B9/(bar)/')).to.include({
+ path: '/foo/%E4%BD%A0%E3%82%B9/(bar)/',
+ pathname: '/foo/%E4%BD%A0%E3%82%B9/(bar)/'
+ });
+ });
+
+ it('should percent-encode SPACE, ("), (<), (>), (`), (#), (?), ({), and (})', function () {
+ expect(toNodeUrl(new PostmanUrl({
+ host: 'example.com',
+ path: [' ', '"', '<', '>', '`', '#', '?', '{', '}']
+ }))).to.include({
+ path: '/%20/%22/%3C/%3E/%60/%23/%3F/%7B/%7D',
+ pathname: '/%20/%22/%3C/%3E/%60/%23/%3F/%7B/%7D'
+ });
+ });
+
+ it('should not trim trailing whitespace characters', function () {
+ expect(toNodeUrl('example.com/path ')).to.include({
+ path: '/path%20',
+ pathname: '/path%20'
+ });
+ });
+
+ it('should add query to the path but not to the pathname', function () {
+ expect(toNodeUrl('example.com/foo?q=bar')).to.include({
+ path: '/foo?q=bar',
+ pathname: '/foo'
+ });
+ });
+ });
+
+ describe('.href', function () {
+ it('should percent-encode the reserved and unicode characters', function () {
+ expect(toNodeUrl('ròót@郵便屋さん.com/[⚡️]?q1=(1 2)#%foo%')).to.include({
+ href: 'http://r%C3%B2%C3%B3t@xn--48jwgn17gdel797d.com/[%E2%9A%A1%EF%B8%8F]?q1=(1%202)#%foo%'
+ });
+ });
+
+ it('should not double encode the characters', function () {
+ expect(toNodeUrl('postman://xn--48jwgn17gdel797d.com/[%E2%9A%A1%EF%B8%8F]?q1=(1%202)')).to.include({
+ href: 'postman://xn--48jwgn17gdel797d.com/[%E2%9A%A1%EF%B8%8F]?q1=(1%202)'
+ });
+ });
+ });
+ });
+
+ describe('SECURITY', function () {
+ // Refer: https://www.owasp.org/index.php/Double_Encoding
+ it('should not double encode the characters', function () {
+ expect(toNodeUrl('%22user%22:p%C3%A2$$@xn--48jwgn17gdel797d.com/%E4%BD?q1=(1%202)#(%F0%9F)')).to.include({
+ auth: '%22user%22:p%C3%A2$$',
+ host: 'xn--48jwgn17gdel797d.com',
+ hostname: 'xn--48jwgn17gdel797d.com',
+ pathname: '/%E4%BD',
+ path: '/%E4%BD?q1=(1%202)',
+ query: 'q1=(1%202)',
+ search: '?q1=(1%202)',
+ hash: '#(%F0%9F)'
+ });
+ });
+
+ // eslint-disable-next-line max-len
+ // Refer: https://docs.google.com/presentation/d/e/2PACX-1vSTFsJ9t0DatXbjmEGL8sKxt53gf6a1djHp_8Wbj2ZeTB6IfR-HsRD537-L5PgzVrs97bJu1tzJ1Smo/pub?slide=id.g32d0ed6ec2_0_45
+ (typeof window === 'undefined' ? it : it.skip)('should handle encoded hostname', function () {
+ expect(toNodeUrl('postman.com%60f.society.org')).to.include({
+ host: 'postman.com`f.society.org',
+ hostname: 'postman.com`f.society.org'
+ });
+ });
+
+ // Refer: https://huntr.dev/bounties/1625732310186-postmanlabs/postman-url-encoder/
+ it('should handle extra backslashes in protocol', function () {
+ expect(toNodeUrl('https:////example.com/foo/bar')).to.include({
+ protocol: 'https:',
+ host: 'example.com',
+ hostname: 'example.com',
+ pathname: '/foo/bar',
+ href: 'https://example.com/foo/bar'
+ });
+
+ expect(toNodeUrl('https:\\\\\\example.com/foo/bar')).to.include({
+ protocol: 'https:',
+ host: 'example.com',
+ hostname: 'example.com',
+ pathname: '/foo/bar',
+ href: 'https://example.com/foo/bar'
+ });
+
+ expect(toNodeUrl('https:///\\example.com/foo/bar')).to.include({
+ protocol: 'https:',
+ host: 'example.com',
+ hostname: 'example.com',
+ pathname: '/foo/bar',
+ href: 'https://example.com/foo/bar'
+ });
+
+ // eslint-disable-next-line no-useless-escape
+ expect(toNodeUrl('https:/\/\/\example.com/foo/bar')).to.include({
+ protocol: 'https:',
+ host: 'example.com',
+ hostname: 'example.com',
+ pathname: '/foo/bar',
+ href: 'https://example.com/foo/bar'
+ });
+ });
+
+ // Refer: https://en.wikipedia.org/wiki/File_URI_scheme#How_many_slashes?
+ it('should handle file://host/path and file:///path', function () {
+ expect(toNodeUrl('file://host/path')).to.include({
+ host: 'host',
+ hostname: 'host',
+ pathname: '/path',
+ href: 'file://host/path'
+ });
+
+ expect(toNodeUrl('file:///path')).to.include({
+ host: '',
+ hostname: '',
+ pathname: '/path',
+ href: 'file:///path'
+ });
+
+ expect(toNodeUrl('file:////foo/bar')).to.include({
+ host: '',
+ hostname: '',
+ pathname: '/foo/bar',
+ href: 'file:///foo/bar'
+ });
+ });
+ });
+});
diff --git a/test/unit/toNodeUrl.test.js b/test/unit/toNodeUrl.test.js
index 6b25635..4e0e5e8 100644
--- a/test/unit/toNodeUrl.test.js
+++ b/test/unit/toNodeUrl.test.js
@@ -82,7 +82,7 @@ describe('.toNodeUrl', function () {
'http://user:password@example.com:8080/p/a/t/h?q1=v1&q2=v2#hash',
'HTTP://example.com',
'http://xn--48jwgn17gdel797d.com',
- 'http://xn--iñvalid.com',
+ // 'http://xn--iñvalid.com',
'http://192.168.0.1:8080',
'http://192.168.0.1',
'http://[2a03:2880:f12f:183:face:b00c:0:25de]/index.html',