diff --git a/main_test.go b/main_test.go index 55e293f7..9c037b62 100644 --- a/main_test.go +++ b/main_test.go @@ -1,13 +1,22 @@ package main import ( + "errors" "os" + "os/exec" "path/filepath" "slices" "strings" "testing" + + "github.com/v2fly/domain-list-community/internal/dlc" + router "github.com/v2fly/v2ray-core/v5/app/router/routercommon" + "google.golang.org/protobuf/proto" ) +// mainExitEnv marks the re-executed test binary that has to run main(). +const mainExitEnv = "DLC_TEST_MAIN_EXIT" + func TestParseEntry(t *testing.T) { testCases := []struct { name string @@ -29,6 +38,10 @@ func TestParseEntry(t *testing.T) { {name: "empty rule", typ: "domain", rule: " ", wantErr: true}, {name: "unknown type", typ: "prefix", rule: "example.com", wantErr: true}, {name: "invalid domain", typ: "domain", rule: "exa_mple.com @ads", wantErr: true}, + {name: "invalid keyword", typ: "keyword", rule: "ad_s", wantErr: true}, + {name: "overlong domain", typ: "domain", rule: strings.Repeat("a.", 127) + "com", wantErr: true}, + {name: "invalid attr", typ: "domain", rule: "example.com @a_ds", wantErr: true}, + {name: "invalid affiliation", typ: "domain", rule: "example.com &other_list", wantErr: true}, {name: "empty label", typ: "domain", rule: "example..com", wantErr: true}, {name: "trailing dot", typ: "full", rule: "example.com.", wantErr: true}, {name: "leading dot", typ: "domain", rule: ".example.com", wantErr: true}, @@ -81,6 +94,7 @@ func TestParseInclusion(t *testing.T) { {name: "empty ban attr", rule: "other @-", wantErr: true}, {name: "empty rule", rule: " ", wantErr: true}, {name: "invalid name", rule: "other@list", wantErr: true}, + {name: "unknown field", rule: "other list", wantErr: true}, {name: "affiliation", rule: "other &another", wantErr: true}, } for _, tc := range testCases { @@ -193,6 +207,566 @@ func TestResolveCircularInclusion(t *testing.T) { } } +func TestValidateChars(t *testing.T) { + testCases := []struct { + name string + validate func(string) bool + input string + want bool + }{ + {name: "domain chars", validate: validateDomainChars, input: "my-example.com", want: true}, + {name: "empty domain chars", validate: validateDomainChars, input: "", want: false}, + {name: "uppercase domain chars", validate: validateDomainChars, input: "Example.com", want: false}, + {name: "domain name", validate: validateDomainName, input: "a.example.com", want: true}, + {name: "empty domain name", validate: validateDomainName, input: "", want: false}, + {name: "attr chars", validate: validateAttrChars, input: "!ads1", want: true}, + {name: "empty attr chars", validate: validateAttrChars, input: "", want: false}, + {name: "uppercase attr chars", validate: validateAttrChars, input: "Ads", want: false}, + {name: "site name", validate: validateSiteName, input: "GEOLOCATION-!CN", want: true}, + {name: "empty site name", validate: validateSiteName, input: "", want: false}, + {name: "lowercase site name", validate: validateSiteName, input: "cn", want: false}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if got := tc.validate(tc.input); got != tc.want { + t.Errorf("validate(%q) = %v, want %v", tc.input, got, tc.want) + } + }) + } +} + +func TestLoadData(t *testing.T) { + dataPath := t.TempDir() + content := strings.Join([]string{ + "# A comment line", + "", + " ", + "example.com # Rule without type falls back to domain", + "full:mail.example.com @cn &affiliated", + "include:other @ads", + "keyword:example", + `regexp:^ads\.example\.com$`, + }, "\n") + writeTestData(t, dataPath, map[string]string{"test": content}) + + processor := &Processor{parsedListByName: make(map[string]*ParsedList)} + if err := processor.loadData("TEST", filepath.Join(dataPath, "test")); err != nil { + t.Fatalf("loadData(\"TEST\") got unexpected error: %v", err) + } + pl := processor.parsedListByName["TEST"] + assertPlains(t, "TEST", pl.Entries, []string{ + "domain:example.com", + "full:mail.example.com:@cn", + "keyword:example", + `regexp:^ads\.example\.com$`, + }) + if len(pl.Inclusions) != 1 || pl.Inclusions[0].Source != "OTHER" { + t.Errorf("TEST inclusions = %+v, want one inclusion of %q", pl.Inclusions, "OTHER") + } + // Affiliated entries are also added to the affiliated list + apl, exist := processor.parsedListByName["AFFILIATED"] + if !exist { + t.Fatal("list \"AFFILIATED\" does not exist") + } + assertPlains(t, "AFFILIATED", apl.Entries, []string{"full:mail.example.com:@cn"}) +} + +func TestLoadDataErrors(t *testing.T) { + dataPath := t.TempDir() + files := map[string]string{ + "invalid-entry": "domain:exa_mple.com\n", + "invalid-inclusion": "include:other@list\n", + } + writeTestData(t, dataPath, files) + for name := range files { + t.Run(name, func(t *testing.T) { + processor := &Processor{parsedListByName: make(map[string]*ParsedList)} + if err := processor.loadData("TEST", filepath.Join(dataPath, name)); err == nil { + t.Errorf("loadData(%q) = nil, want parsing error", name) + } + }) + } + t.Run("missing file", func(t *testing.T) { + processor := &Processor{parsedListByName: make(map[string]*ParsedList)} + if err := processor.loadData("TEST", filepath.Join(dataPath, "missing")); err == nil { + t.Error("loadData(\"missing\") = nil, want file opening error") + } + }) +} + +func TestResolveMissingList(t *testing.T) { + processor := &Processor{parsedListByName: make(map[string]*ParsedList)} + if _, err := processor.resolveList("MISSING"); err == nil { + t.Error("resolveList(\"MISSING\") = nil, want list not found error") + } +} + +func TestResolveEmptyList(t *testing.T) { + dataPath := t.TempDir() + writeTestData(t, dataPath, map[string]string{"empty": "# Only a comment\n"}) + processor := &Processor{parsedListByName: make(map[string]*ParsedList)} + if err := processor.loadData("EMPTY", filepath.Join(dataPath, "empty")); err != nil { + t.Fatalf("loadData(\"EMPTY\") got unexpected error: %v", err) + } + pl, err := processor.resolveList("EMPTY") + if err != nil { + t.Fatalf("resolveList(\"EMPTY\") got unexpected error: %v", err) + } + if len(pl.FinalEntries) != 0 { + t.Errorf("EMPTY final entries = %v, want none", pl.FinalEntries) + } +} + +func TestMakeProtoList(t *testing.T) { + rules := []struct{ typ, rule string }{ + {"domain", "example.com @cn @ads"}, + {"full", "www.example.com"}, + {"keyword", "example"}, + {"regexp", `^example\.com$`}, + } + entries := make([]*Entry, 0, len(rules)) + for _, r := range rules { + entry, _, err := parseEntry(r.typ, r.rule) + if err != nil { + t.Fatalf("parseEntry(%q, %q) got unexpected error: %v", r.typ, r.rule, err) + } + entries = append(entries, entry) + } + + site := makeProtoList("TEST", entries) + if site.CountryCode != "TEST" { + t.Errorf("makeProtoList() country code = %q, want %q", site.CountryCode, "TEST") + } + wantTypes := []router.Domain_Type{ + router.Domain_RootDomain, + router.Domain_Full, + router.Domain_Plain, + router.Domain_Regex, + } + if len(site.Domain) != len(wantTypes) { + t.Fatalf("makeProtoList() domains = %v, want %d domains", site.Domain, len(wantTypes)) + } + for i, pdomain := range site.Domain { + if pdomain.Type != wantTypes[i] { + t.Errorf("makeProtoList() domain[%d] type = %v, want %v", i, pdomain.Type, wantTypes[i]) + } + if pdomain.Value != entries[i].Value { + t.Errorf("makeProtoList() domain[%d] value = %q, want %q", i, pdomain.Value, entries[i].Value) + } + } + attrs := site.Domain[0].Attribute + if len(attrs) != 2 || attrs[0].Key != "ads" || attrs[1].Key != "cn" { + t.Fatalf("makeProtoList() domain[0] attributes = %v, want %v", attrs, []string{"ads", "cn"}) + } + for _, attr := range attrs { + if !attr.GetBoolValue() { + t.Errorf("makeProtoList() attribute %q value = %v, want true", attr.Key, attr.GetBoolValue()) + } + } +} + +func TestLoadTasks(t *testing.T) { + dataPath := t.TempDir() + testCases := []struct { + name string + content string + want []DatTask + wantErr bool + }{ + { + name: "valid", + content: `[{"name":"dlc.dat","mode":"all"},{"name":"cn.dat","mode":"allowlist","lists":["cn"]},{"name":"nocn.dat","mode":"denylist","lists":["cn"]}]`, + want: []DatTask{ + {Name: "dlc.dat", Mode: ModeAll}, + {Name: "cn.dat", Mode: ModeAllowlist, Lists: []string{"cn"}}, + {Name: "nocn.dat", Mode: ModeDenylist, Lists: []string{"cn"}}, + }, + }, + {name: "invalid json", content: "{", wantErr: true}, + {name: "missing name", content: `[{"mode":"all"}]`, wantErr: true}, + {name: "invalid mode", content: `[{"name":"dlc.dat","mode":"unknown"}]`, wantErr: true}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + path := filepath.Join(dataPath, tc.name+".json") + if err := os.WriteFile(path, []byte(tc.content), 0644); err != nil { + t.Fatalf("failed to write test profile %q: %v", path, err) + } + tasks, err := loadTasks(path) + if tc.wantErr { + if err == nil { + t.Fatalf("loadTasks(%q) = %+v, want error", tc.name, tasks) + } + return + } + if err != nil { + t.Fatalf("loadTasks(%q) got unexpected error: %v", tc.name, err) + } + if !slices.EqualFunc(tasks, tc.want, func(a, b DatTask) bool { + return a.Name == b.Name && a.Mode == b.Mode && slices.Equal(a.Lists, b.Lists) + }) { + t.Errorf("loadTasks(%q) = %+v, want %+v", tc.name, tasks, tc.want) + } + }) + } + t.Run("missing file", func(t *testing.T) { + if _, err := loadTasks(filepath.Join(dataPath, "missing.json")); err == nil { + t.Error("loadTasks(\"missing.json\") = nil, want file opening error") + } + }) +} + +func TestAssembleDat(t *testing.T) { + outPath := t.TempDir() + setFlag(t, outputDir, outPath) + gs := newTestGeoSites("APPLE", "CN", "GOOGLE") + testCases := []struct { + name string + task DatTask + want []string + wantErr bool + }{ + {name: "all", task: DatTask{Name: "All.dat", Mode: ModeAll}, want: []string{"APPLE", "CN", "GOOGLE"}}, + {name: "allowlist", task: DatTask{Name: "allow.dat", Mode: ModeAllowlist, Lists: []string{"google", "cn", "CN"}}, want: []string{"CN", "GOOGLE"}}, + {name: "denylist", task: DatTask{Name: "deny.dat", Mode: ModeDenylist, Lists: []string{"cn", "missing"}}, want: []string{"APPLE", "GOOGLE"}}, + {name: "denylist without valid list", task: DatTask{Name: "nodeny.dat", Mode: ModeDenylist, Lists: []string{"missing"}}, want: []string{"APPLE", "CN", "GOOGLE"}}, + {name: "allowlist with missing list", task: DatTask{Name: "missing.dat", Mode: ModeAllowlist, Lists: []string{"missing"}}, wantErr: true}, + {name: "allowlist without list", task: DatTask{Name: "none.dat", Mode: ModeAllowlist}, wantErr: true}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := gs.assembleDat(tc.task) + if tc.wantErr { + if err == nil { + t.Fatalf("assembleDat(%+v) = nil, want error", tc.task) + } + return + } + if err != nil { + t.Fatalf("assembleDat(%+v) got unexpected error: %v", tc.task, err) + } + got := readDatSites(t, filepath.Join(outPath, strings.ToLower(tc.task.Name))) + if !slices.Equal(got, tc.want) { + t.Errorf("assembleDat(%+v) = %v, want %v", tc.task, got, tc.want) + } + }) + } +} + +func TestAssembleDatErrors(t *testing.T) { + outPath := t.TempDir() + setFlag(t, outputDir, outPath) + // A directory occupying the output path makes writing the dat file fail + if err := os.Mkdir(filepath.Join(outPath, "blocked.dat"), 0755); err != nil { + t.Fatalf("failed to create blocking directory: %v", err) + } + gs := newTestGeoSites("CN") + if err := gs.assembleDat(DatTask{Name: "Blocked.dat", Mode: ModeAll}); err == nil { + t.Error("assembleDat() = nil, want file writing error") + } + // Invalid UTF-8 in a proto3 string field makes marshaling fail + invalid := &GeoSites{Sites: []*router.GeoSite{{CountryCode: "\xff"}}} + if err := invalid.assembleDat(DatTask{Name: "invalid.dat", Mode: ModeAll}); err == nil { + t.Error("assembleDat() = nil, want marshaling error") + } +} + +func TestWritePlainList(t *testing.T) { + outPath := t.TempDir() + setFlag(t, outputDir, outPath) + entries := []*Entry{{Plain: "domain:example.com"}, {Plain: "full:www.example.com:@ads"}} + if err := writePlainList("CN", entries); err != nil { + t.Fatalf("writePlainList(\"CN\") got unexpected error: %v", err) + } + got, err := os.ReadFile(filepath.Join(outPath, "cn.txt")) + if err != nil { + t.Fatalf("failed to read generated list: %v", err) + } + want := "domain:example.com\nfull:www.example.com:@ads\n" + if string(got) != want { + t.Errorf("writePlainList(\"CN\") wrote %q, want %q", got, want) + } + // A directory occupying the output path makes creating the list file fail + if err := os.Mkdir(filepath.Join(outPath, "blocked.txt"), 0755); err != nil { + t.Fatalf("failed to create blocking directory: %v", err) + } + if err := writePlainList("Blocked", entries); err == nil { + t.Error("writePlainList(\"Blocked\") = nil, want file creating error") + } +} + +func TestRun(t *testing.T) { + dataPath := t.TempDir() + outPath := filepath.Join(t.TempDir(), "output") // Not existing yet + writeTestData(t, dataPath, map[string]string{ + "cn": "# Chinese sites\ndomain:example.cn\nfull:www.example.cn @ads\nkeyword:examplecn\n", + "apple": "include:cn\ndomain:apple.com &icloud\n", + "icloud": "domain:icloud.com\n", + "empty": "# Nothing here\n", + }) + setRunFlags(t, dataPath, outPath, "test.dat", "", "cn, ,missing,") + + if err := run(); err != nil { + t.Fatalf("run() got unexpected error: %v", err) + } + got := readDatLists(t, filepath.Join(outPath, "test.dat")) + want := map[string][]string{ + "APPLE": {"domain:apple.com", "domain:example.cn", "full:www.example.cn:@ads", "keyword:examplecn"}, + "CN": {"domain:example.cn", "full:www.example.cn:@ads", "keyword:examplecn"}, + "ICLOUD": {"domain:apple.com", "domain:icloud.com"}, + } + if len(got) != len(want) { + t.Fatalf("run() generated lists %v, want %v", got, want) + } + for name, wantRules := range want { + if !slices.Equal(got[name], wantRules) { + t.Errorf("run() list %q = %v, want %v", name, got[name], wantRules) + } + } + plain, err := os.ReadFile(filepath.Join(outPath, "cn.txt")) + if err != nil { + t.Fatalf("failed to read exported list: %v", err) + } + wantPlain := "domain:example.cn\nfull:www.example.cn:@ads\nkeyword:examplecn\n" + if string(plain) != wantPlain { + t.Errorf("run() exported list = %q, want %q", plain, wantPlain) + } +} + +func TestRunWithDatProfile(t *testing.T) { + dataPath := t.TempDir() + outPath := t.TempDir() + writeTestData(t, dataPath, map[string]string{ + "cn": "domain:example.cn\n", + "google": "domain:google.com\n", + }) + profile := filepath.Join(t.TempDir(), "profile.json") + if err := os.WriteFile(profile, []byte(`[{"name":"CN.dat","mode":"allowlist","lists":["cn"]}]`), 0644); err != nil { + t.Fatalf("failed to write test profile: %v", err) + } + setRunFlags(t, dataPath, outPath, "dlc.dat", profile, "") + + if err := run(); err != nil { + t.Fatalf("run() got unexpected error: %v", err) + } + if got, want := readDatSites(t, filepath.Join(outPath, "cn.dat")), []string{"CN"}; !slices.Equal(got, want) { + t.Errorf("run() generated dat = %v, want %v", got, want) + } + // Only the dat files defined by the profile are generated + if _, err := os.Stat(filepath.Join(outPath, "dlc.dat")); !os.IsNotExist(err) { + t.Errorf("run() generated %q, want it not to exist", "dlc.dat") + } +} + +func TestRunErrors(t *testing.T) { + t.Run("missing data directory", func(t *testing.T) { + setRunFlags(t, filepath.Join(t.TempDir(), "missing"), t.TempDir(), "dlc.dat", "", "") + if err := run(); err == nil { + t.Error("run() = nil, want data directory error") + } + }) + t.Run("invalid list name", func(t *testing.T) { + dataPath := t.TempDir() + writeTestData(t, dataPath, map[string]string{"invalid_name": "domain:example.com\n"}) + setRunFlags(t, dataPath, t.TempDir(), "dlc.dat", "", "") + if err := run(); err == nil { + t.Error("run() = nil, want invalid list name error") + } + }) + t.Run("invalid rule", func(t *testing.T) { + dataPath := t.TempDir() + writeTestData(t, dataPath, map[string]string{"cn": "domain:exa_mple.cn\n"}) + setRunFlags(t, dataPath, t.TempDir(), "dlc.dat", "", "") + if err := run(); err == nil { + t.Error("run() = nil, want invalid rule error") + } + }) + t.Run("circular inclusion", func(t *testing.T) { + dataPath := t.TempDir() + writeTestData(t, dataPath, map[string]string{ + "first": "include:second\n", + "second": "include:first\n", + }) + setRunFlags(t, dataPath, t.TempDir(), "dlc.dat", "", "") + if err := run(); err == nil { + t.Error("run() = nil, want circular inclusion error") + } + }) + t.Run("invalid output directory", func(t *testing.T) { + dataPath := t.TempDir() + writeTestData(t, dataPath, map[string]string{"cn": "domain:example.cn\n"}) + outPath := t.TempDir() + blocking := filepath.Join(outPath, "blocking") + if err := os.WriteFile(blocking, nil, 0644); err != nil { + t.Fatalf("failed to create blocking file: %v", err) + } + setRunFlags(t, dataPath, filepath.Join(blocking, "output"), "dlc.dat", "", "") + if err := run(); err == nil { + t.Error("run() = nil, want output directory error") + } + }) + t.Run("missing dat profile", func(t *testing.T) { + dataPath := t.TempDir() + writeTestData(t, dataPath, map[string]string{"cn": "domain:example.cn\n"}) + setRunFlags(t, dataPath, t.TempDir(), "dlc.dat", filepath.Join(t.TempDir(), "missing.json"), "") + if err := run(); err == nil { + t.Error("run() = nil, want dat profile error") + } + }) + t.Run("failed plaintext export", func(t *testing.T) { + dataPath := t.TempDir() + writeTestData(t, dataPath, map[string]string{"cn": "domain:example.cn\n"}) + outPath := t.TempDir() + if err := os.Mkdir(filepath.Join(outPath, "cn.txt"), 0755); err != nil { + t.Fatalf("failed to create blocking directory: %v", err) + } + setRunFlags(t, dataPath, outPath, "dlc.dat", "", "cn") + if err := run(); err == nil { + t.Error("run() = nil, want plaintext export error") + } + }) + t.Run("failed dat task", func(t *testing.T) { + dataPath := t.TempDir() + writeTestData(t, dataPath, map[string]string{"cn": "domain:example.cn\n"}) + profile := filepath.Join(t.TempDir(), "profile.json") + if err := os.WriteFile(profile, []byte(`[{"name":"none.dat","mode":"allowlist","lists":["missing"]}]`), 0644); err != nil { + t.Fatalf("failed to write test profile: %v", err) + } + setRunFlags(t, dataPath, t.TempDir(), "dlc.dat", profile, "") + if err := run(); err == nil { + t.Error("run() = nil, want dat task error") + } + }) +} + +func TestMainGeneratesDat(t *testing.T) { + dataPath := t.TempDir() + outPath := t.TempDir() + writeTestData(t, dataPath, map[string]string{"cn": "domain:example.cn\n"}) + setRunFlags(t, dataPath, outPath, "dlc.dat", "", "") + + oldArgs := os.Args + t.Cleanup(func() { os.Args = oldArgs }) + os.Args = []string{"domain-list-community", "--datapath=" + dataPath, "--outputdir=" + outPath, "--outputname=main.dat"} + main() + + if got, want := readDatSites(t, filepath.Join(outPath, "main.dat")), []string{"CN"}; !slices.Equal(got, want) { + t.Errorf("main() generated dat = %v, want %v", got, want) + } +} + +// TestMainExitsOnError re-executes the test binary, because main() terminates +// the process when the generation fails. +func TestMainExitsOnError(t *testing.T) { + if os.Getenv(mainExitEnv) == "1" { + os.Args = []string{"domain-list-community", "--datapath=" + filepath.Join(t.TempDir(), "missing")} + main() + return + } + cmd := exec.Command(os.Args[0], "-test.run=^TestMainExitsOnError$") + cmd.Env = append(os.Environ(), mainExitEnv+"=1") + out, err := cmd.CombinedOutput() + var exitErr *exec.ExitError + if !errors.As(err, &exitErr) || exitErr.ExitCode() != 1 { + t.Fatalf("main() exited with %v, want exit status 1, output:\n%s", err, out) + } + if !strings.Contains(string(out), "[Fatal]") { + t.Errorf("main() output = %q, want it to report a fatal error", out) + } +} + +// setFlag overrides a command line flag for the duration of the test. +func setFlag(t *testing.T, flagValue *string, value string) { + t.Helper() + old := *flagValue + *flagValue = value + t.Cleanup(func() { *flagValue = old }) +} + +func setRunFlags(t *testing.T, data, output, name, profile, exports string) { + t.Helper() + setFlag(t, dataPath, data) + setFlag(t, outputDir, output) + setFlag(t, outputName, name) + setFlag(t, datProfile, profile) + setFlag(t, exportLists, exports) +} + +func writeTestData(t *testing.T, dataPath string, files map[string]string) { + t.Helper() + for name, content := range files { + if err := os.WriteFile(filepath.Join(dataPath, name), []byte(content), 0644); err != nil { + t.Fatalf("failed to write test data %q: %v", name, err) + } + } +} + +func newTestGeoSites(names ...string) *GeoSites { + gs := &GeoSites{ + Sites: make([]*router.GeoSite, 0, len(names)), + SiteIdx: make(map[string]int, len(names)), + } + for i, name := range names { + gs.Sites = append(gs.Sites, &router.GeoSite{ + CountryCode: name, + Domain: []*router.Domain{{Type: router.Domain_RootDomain, Value: strings.ToLower(name) + ".com"}}, + }) + gs.SiteIdx[name] = i + } + return gs +} + +func readDatSites(t *testing.T, path string) []string { + t.Helper() + lists := readDatLists(t, path) + names := make([]string, 0, len(lists)) + for name := range lists { + names = append(names, name) + } + slices.Sort(names) + return names +} + +func readDatLists(t *testing.T, path string) map[string][]string { + t.Helper() + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("failed to read generated dat: %v", err) + } + geoSiteList := new(router.GeoSiteList) + if err := proto.Unmarshal(data, geoSiteList); err != nil { + t.Fatalf("failed to unmarshal generated dat: %v", err) + } + lists := make(map[string][]string, len(geoSiteList.Entry)) + for _, site := range geoSiteList.Entry { + rules := make([]string, 0, len(site.Domain)) + for _, pdomain := range site.Domain { + rule := pdomain.Value + for i, attr := range pdomain.Attribute { + if i == 0 { + rule += ":" + } else { + rule += "," + } + rule += "@" + attr.Key + } + switch pdomain.Type { + case router.Domain_RootDomain: + rule = dlc.RuleTypeDomain + ":" + rule + case router.Domain_Full: + rule = dlc.RuleTypeFullDomain + ":" + rule + case router.Domain_Plain: + rule = dlc.RuleTypeKeyword + ":" + rule + case router.Domain_Regex: + rule = dlc.RuleTypeRegexp + ":" + rule + } + rules = append(rules, rule) + } + lists[site.CountryCode] = rules + } + return lists +} + func assertList(t *testing.T, p *Processor, name string, want []string) { t.Helper() pl, exist := p.parsedListByName[name]