dnscontrol/providers/bind/fnames_test.go
Tom Limoncelli c11a523982
FEATURE: Fixing IDN support for domains (#3879)
# Issue

The previous fix had backwards compatibility issues and treated
uppercase Unicode incorrectly.

# Resolution

* Don't call strings.ToUpper() on Unicode strings. Only call it on the
output of ToASCII.
* Fix BIND's "filenameformat" to be more compatible (only breaks if you
had uppercase unicode in a domain name... which you probably didn't)
* Change IDN to ASCII in most places (Thanks for the suggestion,
@KaiSchwarz-cnic!)
* Update BIND documentation
2025-12-03 20:31:59 -05:00

210 lines
8.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package bind
import (
"reflect"
"testing"
"github.com/StackExchange/dnscontrol/v4/pkg/domaintags"
)
func Test_makeFileName(t *testing.T) {
fmtDefault := "%U.zone"
fmtBasic := "%U - %T - %D"
fmtFancy := "%T%?_%D.zone" // Include the tag_ only if there is a tag
fmtErrorPct := "literal%"
fmtErrorOpt := "literal%?"
fmtErrorUnk := "literal%o" // Unknown % verb
ff := domaintags.DomainFixedForms{
NameRaw: "domy",
NameASCII: "idn",
NameUnicode: "uni",
UniqueName: "unique!taga",
Tag: "tagy",
}
tagless := domaintags.DomainFixedForms{
NameRaw: "domy",
NameASCII: "idn",
NameUnicode: "uni",
UniqueName: "unique",
Tag: "",
}
type args struct {
format string
ff domaintags.DomainFixedForms
}
tests := []struct {
name string
args args
want string
}{
// Test corner cases and common cases.
{"literal", args{"literal", ff}, "literal"},
{"basic", args{fmtBasic, ff}, "domy!tagy - tagy - domy"},
{"solo", args{"%D", ff}, "domy"},
{"front", args{"%Daaa", ff}, "domyaaa"},
{"tail", args{"bbb%D", ff}, "bbbdomy"},
{"def", args{fmtDefault, ff}, "domy!tagy.zone"},
{"fanWI", args{fmtFancy, ff}, "tagy_domy.zone"},
{"fanWO", args{fmtFancy, tagless}, "domy.zone"},
{"errP", args{fmtErrorPct, ff}, "literal%(format may not end in %)"},
{"errQ", args{fmtErrorOpt, ff}, "literal%(format may not end in %?)"},
{"errU", args{fmtErrorUnk, ff}, "literal%(unknown %verb %o)"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := makeFileName(tt.args.format, tt.args.ff); got != tt.want {
t.Errorf("makeFileName(%q) = %q, want %q", tt.args.format, got, tt.want)
}
})
}
}
func Test_makeFileName_2(t *testing.T) {
ff1 := domaintags.MakeDomainFixForms(`EXAMple.com`)
ff2 := domaintags.MakeDomainFixForms(`EXAMple.com!myTag`)
ff3 := domaintags.MakeDomainFixForms(`рф.com!myTag`)
tests := []struct {
name string
format string
want1 string
want2 string
want3 string
descr string // Not used in test, just for documentation generation
}{
// NOTE: "Domain" in these descriptions means the domain name without any split horizon tag. Technically the "Zone".
{`T`, `%T`, ``, `myTag`, `myTag`, `the tag`},
{`c`, `%c`, `example.com`, `example.com!myTag`, `xn--p1ai.com!myTag`, `canonical name, globally unique and comparable`},
{`a`, `%a`, `example.com`, `example.com`, `xn--p1ai.com`, `ASCII domain (Punycode, downcased)`},
{`u`, `%u`, `example.com`, `example.com`, `рф.com`, `Unicode domain (non-Unicode parts downcased)`},
{`r`, `%r`, `EXAMple.com`, `EXAMple.com`, `рф.com`, "Raw (unmodified) Domain from `D()` (risky!)"},
{`f`, `%f`, `example.com`, `example.com_myTag`, `xn--p1ai.com_myTag`, "like `%c` but better for filenames (`%a%?_%T`)"},
{`F`, `%F`, `example.com`, `myTag_example.com`, `myTag_xn--p1ai.com`, "like `%f` but reversed order (`%T%?_%a`)"},
{`%?x`, `%?x`, ``, `x`, `x`, "returns `x` if tag exists, otherwise \"\""},
{`%`, `%%`, `%`, `%`, `%`, `a literal percent sign`},
// Pre-v4.28 names kept for compatibility (note: pre v4.28 did not permit mixed case domain names, we downcased them here for the tests)
{`U`, `%U`, `example.com`, `example.com!myTag`, `рф.com!myTag`, "(deprecated, use `%c`) Same as `%D%?!%T` (risky!)"},
{`D`, `%D`, `example.com`, `example.com`, `рф.com`, "(deprecated, use `%r`) mangles Unicode (risky!)"},
{`%T%?_%D.zone`, `%T%?_%D.zone`, `example.com.zone`, `myTag_example.com.zone`, `myTag_рф.com.zone`, `mentioned in the docs`},
{`db_%T%?_%D`, `db_%T%?_%D`, `db_example.com`, `db_myTag_example.com`, `db_myTag_рф.com`, `mentioned in the docs`},
{`db_%D`, `db_%D`, `db_example.com`, `db_example.com`, `db_рф.com`, `mentioned in the docs`},
// Examples used in the documentation for the BIND provider
{`%c.zone`, `%c.zone`, "example.com.zone", "example.com!myTag.zone", "xn--p1ai.com!myTag.zone", "Default format (v4.28 and later)"},
{`%U.zone`, `%U.zone`, `example.com.zone`, `example.com!myTag.zone`, `рф.com!myTag.zone`, "Default format (pre-v4.28) (risky!)"},
{`db_%f`, `db_%f`, `db_example.com`, `db_example.com_myTag`, `db_xn--p1ai.com_myTag`, "Recommended in a popular DNS book"},
{`db_a%?_%T`, `db_%a%?_%T`, `db_example.com`, `db_example.com_myTag`, `db_xn--p1ai.com_myTag`, "same as above but using `%?_`"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got1 := makeFileName(tt.format, ff1)
if got1 != tt.want1 {
t.Errorf("makeFileName(%q) = ff1 %q, want %q", tt.format, got1, tt.want1)
}
got2 := makeFileName(tt.format, ff2)
if got2 != tt.want2 {
t.Errorf("makeFileName(%q) = ff2 %q, want %q", tt.format, got2, tt.want2)
}
got3 := makeFileName(tt.format, ff3)
if got3 != tt.want3 {
t.Errorf("makeFileName(%q) = ff3 %q, want %q", tt.format, got3, tt.want3)
}
//Uncomment to regenerate lines used in documentation/provider/bind.md 's table:
// fmt.Print(strings.ReplaceAll(fmt.Sprintf("| `%s` | %s | `%s` | `%s` | `%s` |\n", tt.format, tt.descr, got1, got2, got3), "``", "`\"\"` (null)"))
//Uncomment to regenerate the above test cases:
// fmt.Printf("{`%s`, `%s`, `%s`, `%s`, `%s`, %q},\n", tt.name, tt.format, got1, got2, got3, tt.descr)
})
}
}
func Test_makeExtractor(t *testing.T) {
type args struct {
format string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{"c", args{"%c.zone"}, `(.*)!.+\.zone|(.*)\.zone`, false},
{"a", args{"%a.zone"}, `(.*)\.zone`, false},
{"u", args{"%u.zone"}, `(.*)\.zone`, false},
{"r", args{"%r.zone"}, `(.*)\.zone`, false},
// TODO: Add test cases.
{"u", args{"%U.zone"}, `(.*)!.+\.zone|(.*)\.zone`, false},
{"d", args{"%D.zone"}, `(.*)\.zone`, false},
{"basic", args{"%U - %T - %D"}, `(.*)!.+ - .* - (.*)|(.*) - - (.*)`, false},
{"bk1", args{"db_%U"}, `db_(.*)!.+|db_(.*)`, false},
{"bk2", args{"db_%T_%D"}, `db_.*_(.*)`, false},
{"fan", args{"%T%?_%D.zone"}, `.*_(.*)\.zone|(.*)\.zone`, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := makeExtractor(tt.args.format)
if (err != nil) != tt.wantErr {
t.Errorf("makeExtractor() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("makeExtractor() = %v, want %v", got, tt.want)
}
})
}
}
func Test_extractZonesFromFilenames(t *testing.T) {
type args struct {
format string
names []string
}
// A list of filenames one might find in a directory.
filelist := []string{
"foo!one.zone", // u
"dom.tld!two.zone", // u
"foo.zone", // d
"dom.tld.zone", // d
"foo!one - one - foo", // basic
"dom.tld!two - two - dom.tld", // basic
"db_foo", // bk1
"db_dom.tld", // bk1
"db_dom.tld!tag", // bk1
"db_inside_foo", // bk2
"db_outside_dom.tld", // bk2
"db__example.com", // bk2
"dom.zone", // fan
"example.com.zone", // fan (no tag)
"mytag_example.com.zone", // fan (w/ tag)
"dom.zone", // fan (no tag)
"mytag_dom.zone", // fan (w/ tag)
}
tests := []struct {
name string
args args
want []string
}{
{"0", args{"%D.zone", []string{"foo.zone", "dom.tld.zone"}}, []string{"foo", "dom.tld"}},
{"1", args{"%U.zone", []string{"foo.zone", "dom.tld.zone"}}, []string{"foo", "dom.tld"}},
{"2", args{"%T%?_%D.zone", []string{"inside_ex.tld.zone", "foo.zone", "dom.tld.zone"}}, []string{"ex.tld", "foo", "dom.tld"}},
{"d", args{"%D.zone", filelist}, []string{"foo!one", "dom.tld!two", "foo", "dom.tld", "dom", "example.com", "mytag_example.com", "dom", "mytag_dom"}},
{"u", args{"%U.zone", filelist}, []string{"foo", "dom.tld", "foo", "dom.tld", "dom", "example.com", "mytag_example.com", "dom", "mytag_dom"}},
{"bk1", args{"db_%U", filelist}, []string{"foo", "dom.tld", "dom.tld", "inside_foo", "outside_dom.tld", "_example.com"}},
{"bk2", args{"db_%T_%D", filelist}, []string{"foo", "dom.tld", "example.com"}},
{"fan", args{"%T%?_%D.zone", filelist}, []string{"foo!one", "dom.tld!two", "foo", "dom.tld", "dom", "example.com", "example.com", "dom", "dom"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := extractZonesFromFilenames(tt.args.format, tt.args.names); !reflect.DeepEqual(got, tt.want) {
ext, _ := makeExtractor(tt.args.format)
t.Errorf("extractZonesFromFilenames() = %v, want %v Fm=%s Ex=%s", got, tt.want, tt.args.format, ext)
}
})
}
}