-- 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 name = require("dns.name") local testify = testimony.new("== dns.name ==") -- ========================================================================= -- normalize -- ========================================================================= testify:that("normalize lowercases and adds trailing dot", function() testimony.assert_equal("example.com.", name.normalize("Example.COM")) end) testify:that("normalize preserves existing trailing dot", function() testimony.assert_equal("example.com.", name.normalize("example.com.")) end) testify:that("normalize handles empty string", function() testimony.assert_equal(".", name.normalize("")) end) testify:that("normalize handles nil", function() testimony.assert_equal(".", name.normalize(nil)) end) testify:that("normalize handles root", function() testimony.assert_equal(".", name.normalize(".")) end) -- ========================================================================= -- is_fqdn -- ========================================================================= testify:that("is_fqdn detects trailing dot", function() testimony.assert_true(name.is_fqdn("example.com.")) end) testify:that("is_fqdn rejects no trailing dot", function() testimony.assert_false(name.is_fqdn("example.com")) end) testify:that("is_fqdn rejects empty", function() testimony.assert_false(name.is_fqdn("")) end) testify:that("is_fqdn rejects nil", function() testimony.assert_false(name.is_fqdn(nil)) end) testify:that("is_fqdn accepts root", function() testimony.assert_true(name.is_fqdn(".")) end) -- ========================================================================= -- labels -- ========================================================================= testify:that("labels splits name into parts", function() local l = name.labels("www.example.com.") testimony.assert_equal(3, #l) testimony.assert_equal("www", l[1]) testimony.assert_equal("example", l[2]) testimony.assert_equal("com", l[3]) end) testify:that("labels handles single label", function() local l = name.labels("com.") testimony.assert_equal(1, #l) testimony.assert_equal("com", l[1]) end) testify:that("labels handles root", function() local l = name.labels(".") testimony.assert_equal(0, #l) end) testify:that("labels handles empty", function() local l = name.labels("") testimony.assert_equal(0, #l) end) testify:that("labels handles nil", function() local l = name.labels(nil) testimony.assert_equal(0, #l) end) testify:that("labels handles name without trailing dot", function() local l = name.labels("example.com") testimony.assert_equal(2, #l) testimony.assert_equal("example", l[1]) testimony.assert_equal("com", l[2]) end) -- ========================================================================= -- label_count -- ========================================================================= testify:that("label_count counts labels", function() testimony.assert_equal(3, name.label_count("www.example.com.")) testimony.assert_equal(2, name.label_count("example.com.")) testimony.assert_equal(1, name.label_count("com.")) testimony.assert_equal(0, name.label_count(".")) end) -- ========================================================================= -- is_valid -- ========================================================================= testify:that("is_valid accepts normal domain", function() testimony.assert_true(name.is_valid("example.com.")) end) testify:that("is_valid accepts root", function() testimony.assert_true(name.is_valid(".")) end) testify:that("is_valid accepts hyphenated labels", function() testimony.assert_true(name.is_valid("my-host.example.com.")) end) testify:that("is_valid accepts underscored labels (SRV)", function() testimony.assert_true(name.is_valid("_sip._tcp.example.com.")) end) testify:that("is_valid rejects leading hyphen", function() testimony.assert_false(name.is_valid("-bad.example.com.")) end) testify:that("is_valid rejects trailing hyphen", function() testimony.assert_false(name.is_valid("bad-.example.com.")) end) testify:that("is_valid rejects label > 63 bytes", function() local long_label = string.rep("a", 64) .. ".com." testimony.assert_false(name.is_valid(long_label)) end) testify:that("is_valid accepts label exactly 63 bytes", function() local max_label = string.rep("a", 63) .. ".com." testimony.assert_true(name.is_valid(max_label)) end) testify:that("is_valid rejects empty", function() testimony.assert_false(name.is_valid("")) end) testify:that("is_valid rejects nil", function() testimony.assert_false(name.is_valid(nil)) end) testify:that("is_valid rejects special chars", function() testimony.assert_false(name.is_valid("bad!name.com.")) end) testify:that("is_valid rejects name with wire length > 253", function() -- 4 labels of 62 bytes + "com" = 4*(63) + 4 + 1 = 257 wire bytes local labels = {} for _ = 1, 4 do labels[#labels + 1] = string.rep("a", 62) end local long_name = table.concat(labels, ".") .. ".com." testimony.assert_false(name.is_valid(long_name)) end) -- ========================================================================= -- equal -- ========================================================================= testify:that("equal compares case-insensitively", function() testimony.assert_true(name.equal("Example.COM.", "example.com.")) end) testify:that("equal handles missing trailing dots", function() testimony.assert_true(name.equal("example.com", "example.com.")) end) testify:that("equal rejects different names", function() testimony.assert_false(name.equal("foo.com.", "bar.com.")) end) -- ========================================================================= -- is_subdomain -- ========================================================================= testify:that("is_subdomain detects subdomain", function() testimony.assert_true(name.is_subdomain("www.example.com.", "example.com.")) end) testify:that("is_subdomain rejects same name", function() testimony.assert_false(name.is_subdomain("example.com.", "example.com.")) end) testify:that("is_subdomain root is parent of all", function() testimony.assert_true(name.is_subdomain("example.com.", ".")) end) testify:that("is_subdomain root is not subdomain of itself", function() testimony.assert_false(name.is_subdomain(".", ".")) end) testify:that("is_subdomain rejects unrelated names", function() testimony.assert_false(name.is_subdomain("www.foo.com.", "bar.com.")) end) testify:that("is_subdomain is case insensitive", function() testimony.assert_true(name.is_subdomain("WWW.Example.COM.", "example.com.")) end) testify:that("is_subdomain deep nesting works", function() testimony.assert_true(name.is_subdomain("a.b.c.example.com.", "example.com.")) end) -- ========================================================================= -- parent -- ========================================================================= testify:that("parent strips leftmost label", function() testimony.assert_equal("example.com.", name.parent("www.example.com.")) end) testify:that("parent of TLD is root", function() testimony.assert_equal(".", name.parent("com.")) end) testify:that("parent of root is root", function() testimony.assert_equal(".", name.parent(".")) end) testify:that("parent of empty is root", function() testimony.assert_equal(".", name.parent("")) end) testify:that("parent of nil is root", function() testimony.assert_equal(".", name.parent(nil)) end) -- ========================================================================= -- apply_search -- ========================================================================= testify:that("apply_search FQDN bypasses search", function() local candidates = name.apply_search("example.com.", { "search.local." }, 1) testimony.assert_equal(1, #candidates) testimony.assert_equal("example.com.", candidates[1]) end) testify:that("apply_search few dots tries search first", function() -- "web" has 0 dots, ndots=1, so 0 < 1 → search first local candidates = name.apply_search("web", { "internal.corp." }, 1) testimony.assert_equal(2, #candidates) testimony.assert_equal("web.internal.corp.", candidates[1]) testimony.assert_equal("web.", candidates[2]) end) testify:that("apply_search enough dots tries as-is first", function() -- "host.sub" has 1 dot, ndots=1, so 1 >= 1 → as-is first local candidates = name.apply_search("host.sub", { "corp.com." }, 1) testimony.assert_equal(2, #candidates) testimony.assert_equal("host.sub.", candidates[1]) testimony.assert_equal("host.sub.corp.com.", candidates[2]) end) testify:that("apply_search empty search list", function() local candidates = name.apply_search("web", {}, 1) testimony.assert_equal(1, #candidates) testimony.assert_equal("web.", candidates[1]) end) testify:that("apply_search multiple search domains", function() local candidates = name.apply_search("web", { "a.com.", "b.com." }, 1) testimony.assert_equal(3, #candidates) testimony.assert_equal("web.a.com.", candidates[1]) testimony.assert_equal("web.b.com.", candidates[2]) testimony.assert_equal("web.", candidates[3]) end) testify:that("apply_search nil search list treated as empty", function() local candidates = name.apply_search("web", nil, 1) testimony.assert_equal(1, #candidates) testimony.assert_equal("web.", candidates[1]) end) testify:that("apply_search with ndots=2", function() -- "host.sub" has 1 dot, ndots=2, so 1 < 2 → search first local candidates = name.apply_search("host.sub", { "corp.com." }, 2) testimony.assert_equal(2, #candidates) testimony.assert_equal("host.sub.corp.com.", candidates[1]) testimony.assert_equal("host.sub.", candidates[2]) end) -- ========================================================================= -- 0x20 Encoding -- ========================================================================= testify:that("encode_0x20 returns string of same length", function() local input = "example.com." local encoded = name.encode_0x20(input) testimony.assert_not_nil(encoded) testimony.assert_equal(#input, #encoded) end) testify:that("encode_0x20 preserves non-letters", function() local input = "host-01.example.com." for _ = 1, 20 do local encoded = name.encode_0x20(input) -- Digits, hyphens, and dots must be unchanged testimony.assert_equal(string.sub(encoded, 5, 5), "-") testimony.assert_equal(string.sub(encoded, 6, 7), "01") testimony.assert_equal(string.sub(encoded, 8, 8), ".") testimony.assert_equal(string.sub(encoded, 20, 20), ".") end end) testify:that("encode_0x20 produces variation", function() -- Run encode_0x20 many times; at least one result should differ from -- the all-lowercase form (statistically near-certain with 10+ letters) local input = "example.com." local normalized = name.normalize(input) local found_different = false for _ = 1, 50 do local encoded = name.encode_0x20(input) if encoded ~= normalized then found_different = true break end end testimony.assert_true(found_different, "encode_0x20 should produce case variation") end) testify:that("encode_0x20 case-insensitive equal to original", function() local input = "Example.COM." for _ = 1, 20 do local encoded = name.encode_0x20(input) testimony.assert_true(name.equal(input, encoded), "encoded name should be case-insensitively equal to original") end end) testify:that("encode_0x20 handles edge cases", function() testimony.assert_nil(name.encode_0x20(nil)) testimony.assert_equal("", name.encode_0x20("")) testimony.assert_equal(".", name.encode_0x20(".")) -- All digits — no randomization possible local digits = "123.456." testimony.assert_equal(digits, name.encode_0x20(digits)) end) testify:that("verify_0x20 exact match passes", function() local sent = "eXaMpLe.CoM." testimony.assert_true(name.verify_0x20(sent, sent)) end) testify:that("verify_0x20 case mismatch fails", function() local sent = "eXaMpLe.CoM." local received = "example.com." testimony.assert_false(name.verify_0x20(sent, received)) end) testify:that("verify_0x20 different name fails", function() local sent = "eXaMpLe.CoM." local received = "oThEr.CoM." testimony.assert_false(name.verify_0x20(sent, received)) end) testify:conclude()