-- SPDX-FileCopyrightText: © 2022—2026 Vladimir Zorin -- SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later -- Licensed under OWL v1.0+. See LICENSE. local testimony = require("testimony") local url = require("http.url") local testify = testimony.new("== http.url ==") -- ========================================================================= -- escape -- ========================================================================= testify:that("escape: unreserved characters pass through", function() testimony.assert_equal("hello-world_1.0~test", url.escape("hello-world_1.0~test")) end) testify:that("escape: spaces become %20", function() testimony.assert_equal("hello%20world", url.escape("hello world")) end) testify:that("escape: special characters are encoded", function() testimony.assert_equal("a%3Db%26c", url.escape("a=b&c")) end) testify:that("escape: slash is encoded", function() testimony.assert_equal("%2Fpath%2Fto", url.escape("/path/to")) end) testify:that("escape: empty string returns empty", function() testimony.assert_equal("", url.escape("")) end) -- ========================================================================= -- unescape -- ========================================================================= testify:that("unescape: decodes %20 to space", function() testimony.assert_equal("hello world", url.unescape("hello%20world")) end) testify:that("unescape: handles uppercase hex", function() testimony.assert_equal("/", url.unescape("%2F")) end) testify:that("unescape: handles lowercase hex", function() testimony.assert_equal("/", url.unescape("%2f")) end) testify:that("unescape: passes through plain strings", function() testimony.assert_equal("hello", url.unescape("hello")) end) testify:that("unescape: empty string returns empty", function() testimony.assert_equal("", url.unescape("")) end) -- ========================================================================= -- parse -- ========================================================================= testify:that("parse: full URL with all components", function() local p = url.parse("https://user:pass@example.com:8080/path?q=1#frag") testimony.assert_equal("https", p.scheme) testimony.assert_equal("user:pass@example.com:8080", p.authority) testimony.assert_equal("user:pass", p.userinfo) testimony.assert_equal("user", p.user) testimony.assert_equal("pass", p.password) testimony.assert_equal("example.com", p.host) testimony.assert_equal("8080", p.port) testimony.assert_equal("/path", p.path) testimony.assert_equal("q=1", p.query) testimony.assert_equal("frag", p.fragment) end) testify:that("parse: scheme and host only", function() local p = url.parse("http://example.com") testimony.assert_equal("http", p.scheme) testimony.assert_equal("example.com", p.host) testimony.assert_nil(p.port) testimony.assert_nil(p.path) testimony.assert_nil(p.query) end) testify:that("parse: path-only URL", function() local p = url.parse("/just/a/path") testimony.assert_equal("/just/a/path", p.path) testimony.assert_nil(p.scheme) testimony.assert_nil(p.host) end) testify:that("parse: with query string", function() local p = url.parse("http://host/path?key=val&foo=bar") testimony.assert_equal("/path", p.path) testimony.assert_equal("key=val&foo=bar", p.query) end) testify:that("parse: with fragment", function() local p = url.parse("http://host/path#section") testimony.assert_equal("/path", p.path) testimony.assert_equal("section", p.fragment) end) testify:that("parse: IPv6 host", function() local p = url.parse("http://[::1]:8080/path") testimony.assert_equal("::1", p.host) testimony.assert_equal("8080", p.port) end) testify:that("parse: nil input returns nil and error", function() local p, err = url.parse(nil) testimony.assert_nil(p) testimony.assert_equal("invalid url", err) end) testify:that("parse: empty string returns nil and error", function() local p, err = url.parse("") testimony.assert_nil(p) testimony.assert_equal("invalid url", err) end) testify:that("parse: with semicolon params", function() local p = url.parse("http://host/path;type=a?q=1") testimony.assert_equal("/path", p.path) testimony.assert_equal("type=a", p.params) testimony.assert_equal("q=1", p.query) end) -- ========================================================================= -- build -- ========================================================================= testify:that("build: round-trip preserves full URL", function() local original = "https://user:pass@example.com:8080/path?q=1#frag" testimony.assert_equal(original, url.build(url.parse(original))) end) testify:that("build: from parts", function() local result = url.build({ scheme = "https", host = "example.com", path = "/foo" }) testimony.assert_equal("https://example.com/foo", result) end) testify:that("build: IPv6 host gets brackets", function() local result = url.build({ scheme = "http", host = "::1", port = "80", path = "/" }) testimony.assert_equal("http://[::1]:80/", result) end) testify:that("build: path only", function() testimony.assert_equal("/hello", url.build({ path = "/hello" })) end) testify:that("build: round-trip preserves scheme and host", function() local original = "http://example.com" testimony.assert_equal(original, url.build(url.parse(original))) end) -- ========================================================================= -- absolute -- ========================================================================= testify:that("absolute: relative URL with scheme returned as-is", function() testimony.assert_equal("http://c/d", url.absolute("http://a/b", "http://c/d")) end) testify:that("absolute: relative path merges with base", function() testimony.assert_equal("http://a/b/d", url.absolute("http://a/b/c", "d")) end) testify:that("absolute: parent traversal with ..", function() testimony.assert_equal("http://a/d", url.absolute("http://a/b/c", "../d")) end) testify:that("absolute: absolute path replaces base path", function() testimony.assert_equal("http://a/d", url.absolute("http://a/b/c", "/d")) end) testify:that("absolute: query-only inherits path", function() testimony.assert_equal("http://a/b?new", url.absolute("http://a/b?old", "?new")) end) testify:that("absolute: fragment-only inherits everything", function() testimony.assert_equal("http://a/b#new", url.absolute("http://a/b#old", "#new")) end) testify:that("absolute: real-world redirect with absolute path", function() testimony.assert_equal("https://example.com/login", url.absolute("https://example.com/api/v1", "/login")) end) testify:that("absolute: dot segment normalization", function() testimony.assert_equal("http://a/b/d", url.absolute("http://a/b/c", "./d")) end) testify:that("absolute: multiple parent traversals", function() testimony.assert_equal("http://a/e", url.absolute("http://a/b/c/d", "../../e")) end) testify:conclude()