package home import ( "testing" "time" "github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/timeutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TODO(a.garipov): Cover all migrations, use a testdata/ dir. func TestUpgradeSchema1to2(t *testing.T) { diskConf := testDiskConf(1) err := upgradeSchema1to2(diskConf) require.NoError(t, err) require.Equal(t, diskConf["schema_version"], 2) _, ok := diskConf["coredns"] require.False(t, ok) newDNSConf, ok := diskConf["dns"] require.True(t, ok) oldDNSConf := testDNSConf(1) assert.Equal(t, oldDNSConf, newDNSConf) oldExcludedEntries := []string{"coredns", "schema_version"} newExcludedEntries := []string{"dns", "schema_version"} oldDiskConf := testDiskConf(1) assertEqualExcept(t, oldDiskConf, diskConf, oldExcludedEntries, newExcludedEntries) } func TestUpgradeSchema2to3(t *testing.T) { diskConf := testDiskConf(2) err := upgradeSchema2to3(diskConf) require.NoError(t, err) require.Equal(t, diskConf["schema_version"], 3) dnsMap, ok := diskConf["dns"] require.True(t, ok) newDNSConf, ok := dnsMap.(yobj) require.True(t, ok) bootstrapDNS := newDNSConf["bootstrap_dns"] switch v := bootstrapDNS.(type) { case []string: require.Len(t, v, 1) require.Equal(t, "8.8.8.8:53", v[0]) default: t.Fatalf("wrong type for bootstrap dns: %T", v) } excludedEntries := []string{"bootstrap_dns"} oldDNSConf := testDNSConf(2) assertEqualExcept(t, oldDNSConf, newDNSConf, excludedEntries, excludedEntries) excludedEntries = []string{"dns", "schema_version"} oldDiskConf := testDiskConf(2) assertEqualExcept(t, oldDiskConf, diskConf, excludedEntries, excludedEntries) } func TestUpgradeSchema7to8(t *testing.T) { const host = "1.2.3.4" oldConf := yobj{ "dns": yobj{ "bind_host": host, }, "schema_version": 7, } err := upgradeSchema7to8(oldConf) require.NoError(t, err) require.Equal(t, oldConf["schema_version"], 8) dnsVal, ok := oldConf["dns"] require.True(t, ok) newDNSConf, ok := dnsVal.(yobj) require.True(t, ok) newBindHosts, ok := newDNSConf["bind_hosts"].(yarr) require.True(t, ok) require.Len(t, newBindHosts, 1) assert.Equal(t, host, newBindHosts[0]) } func TestUpgradeSchema8to9(t *testing.T) { const tld = "foo" t.Run("with_autohost_tld", func(t *testing.T) { oldConf := yobj{ "dns": yobj{ "autohost_tld": tld, }, "schema_version": 8, } err := upgradeSchema8to9(oldConf) require.NoError(t, err) require.Equal(t, oldConf["schema_version"], 9) dnsVal, ok := oldConf["dns"] require.True(t, ok) newDNSConf, ok := dnsVal.(yobj) require.True(t, ok) localDomainName, ok := newDNSConf["local_domain_name"].(string) require.True(t, ok) assert.Equal(t, tld, localDomainName) }) t.Run("without_autohost_tld", func(t *testing.T) { oldConf := yobj{ "dns": yobj{}, "schema_version": 8, } err := upgradeSchema8to9(oldConf) require.NoError(t, err) require.Equal(t, oldConf["schema_version"], 9) dnsVal, ok := oldConf["dns"] require.True(t, ok) newDNSConf, ok := dnsVal.(yobj) require.True(t, ok) // Should be nil in order to be set to the default value by the // following config rewrite. _, ok = newDNSConf["local_domain_name"] require.False(t, ok) }) } // assertEqualExcept removes entries from configs and compares them. func assertEqualExcept(t *testing.T, oldConf, newConf yobj, oldKeys, newKeys []string) { t.Helper() for _, k := range oldKeys { delete(oldConf, k) } for _, k := range newKeys { delete(newConf, k) } assert.Equal(t, oldConf, newConf) } func testDiskConf(schemaVersion int) (diskConf yobj) { filters := []filtering.FilterYAML{{ URL: "https://filters.adtidy.org/android/filters/111_optimized.txt", Name: "Latvian filter", RulesCount: 100, }, { URL: "https://easylist.to/easylistgermany/easylistgermany.txt", Name: "Germany filter", RulesCount: 200, }} diskConf = yobj{ "language": "en", "filters": filters, "user_rules": []string{}, "schema_version": schemaVersion, "bind_host": "0.0.0.0", "bind_port": 80, "auth_name": "name", "auth_pass": "pass", } dnsConf := testDNSConf(schemaVersion) if schemaVersion > 1 { diskConf["dns"] = dnsConf } else { diskConf["coredns"] = dnsConf } return diskConf } // testDNSConf creates a DNS config for test the way gopkg.in/yaml.v3 would // unmarshal it. In YAML, keys aren't guaranteed to always only be strings. func testDNSConf(schemaVersion int) (dnsConf yobj) { dnsConf = yobj{ "port": 53, "blocked_response_ttl": 10, "querylog_enabled": true, "ratelimit": 20, "bootstrap_dns": "8.8.8.8:53", "parental_sensitivity": 13, "ratelimit_whitelist": []string{}, "upstream_dns": []string{"tls://1.1.1.1", "tls://1.0.0.1", "8.8.8.8"}, "filtering_enabled": true, "refuse_any": true, "parental_enabled": true, "bind_host": "0.0.0.0", "protection_enabled": true, "safesearch_enabled": true, "safebrowsing_enabled": true, } if schemaVersion > 2 { dnsConf["bootstrap_dns"] = []string{"8.8.8.8:53"} } return dnsConf } func TestAddQUICPort(t *testing.T) { testCases := []struct { name string ups string want string }{{ name: "simple_ip", ups: "8.8.8.8", want: "8.8.8.8", }, { name: "url_ipv4", ups: "quic://8.8.8.8", want: "quic://8.8.8.8:784", }, { name: "url_ipv4_with_port", ups: "quic://8.8.8.8:25565", want: "quic://8.8.8.8:25565", }, { name: "url_ipv6", ups: "quic://[::1]", want: "quic://[::1]:784", }, { name: "url_ipv6_invalid", ups: "quic://::1", want: "quic://::1", }, { name: "url_ipv6_with_port", ups: "quic://[::1]:25565", want: "quic://[::1]:25565", }, { name: "url_hostname", ups: "quic://example.com", want: "quic://example.com:784", }, { name: "url_hostname_with_port", ups: "quic://example.com:25565", want: "quic://example.com:25565", }, { name: "url_hostname_with_endpoint", ups: "quic://example.com/some-endpoint", want: "quic://example.com:784/some-endpoint", }, { name: "url_hostname_with_port_endpoint", ups: "quic://example.com:25565/some-endpoint", want: "quic://example.com:25565/some-endpoint", }, { name: "non-quic_proto", ups: "tls://example.com", want: "tls://example.com", }, { name: "comment", ups: "# comment", want: "# comment", }, { name: "blank", ups: "", want: "", }, { name: "with_domain_ip", ups: "[/example.domain/]8.8.8.8", want: "[/example.domain/]8.8.8.8", }, { name: "with_domain_url", ups: "[/example.domain/]quic://example.com", want: "[/example.domain/]quic://example.com:784", }, { name: "invalid_domain", ups: "[/exmaple.domain]quic://example.com", want: "[/exmaple.domain]quic://example.com", }} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { withPort := addQUICPort(tc.ups, 784) assert.Equal(t, tc.want, withPort) }) } } func TestUpgradeSchema9to10(t *testing.T) { const ultimateAns = 42 testCases := []struct { ups any want any wantErr string name string }{{ ups: yarr{"quic://8.8.8.8"}, want: yarr{"quic://8.8.8.8:784"}, wantErr: "", name: "success", }, { ups: ultimateAns, want: nil, wantErr: "unexpected type of dns.upstream_dns: int", name: "bad_yarr_type", }, { ups: yarr{ultimateAns}, want: nil, wantErr: "unexpected type of upstream field: int", name: "bad_upstream_type", }} for _, tc := range testCases { conf := yobj{ "dns": yobj{ "upstream_dns": tc.ups, }, "schema_version": 9, } t.Run(tc.name, func(t *testing.T) { err := upgradeSchema9to10(conf) if tc.wantErr != "" { testutil.AssertErrorMsg(t, tc.wantErr, err) return } require.NoError(t, err) require.Equal(t, conf["schema_version"], 10) dnsVal, ok := conf["dns"] require.True(t, ok) newDNSConf, ok := dnsVal.(yobj) require.True(t, ok) fixedUps, ok := newDNSConf["upstream_dns"].(yarr) require.True(t, ok) assert.Equal(t, tc.want, fixedUps) }) } t.Run("no_dns", func(t *testing.T) { err := upgradeSchema9to10(yobj{}) assert.NoError(t, err) }) t.Run("bad_dns", func(t *testing.T) { err := upgradeSchema9to10(yobj{ "dns": ultimateAns, }) testutil.AssertErrorMsg(t, "unexpected type of dns: int", err) }) } func TestUpgradeSchema10to11(t *testing.T) { check := func(t *testing.T, conf yobj) { rlimit, _ := conf["rlimit_nofile"].(int) err := upgradeSchema10to11(conf) require.NoError(t, err) require.Equal(t, conf["schema_version"], 11) _, ok := conf["rlimit_nofile"] assert.False(t, ok) osVal, ok := conf["os"] require.True(t, ok) newOSConf, ok := osVal.(yobj) require.True(t, ok) _, ok = newOSConf["group"] assert.True(t, ok) _, ok = newOSConf["user"] assert.True(t, ok) rlimitVal, ok := newOSConf["rlimit_nofile"].(int) require.True(t, ok) assert.Equal(t, rlimit, rlimitVal) } const rlimit = 42 t.Run("with_rlimit", func(t *testing.T) { conf := yobj{ "rlimit_nofile": rlimit, "schema_version": 10, } check(t, conf) }) t.Run("without_rlimit", func(t *testing.T) { conf := yobj{ "schema_version": 10, } check(t, conf) }) } func TestUpgradeSchema11to12(t *testing.T) { testCases := []struct { ivl any want any wantErr string name string }{{ ivl: 1, want: timeutil.Duration{Duration: timeutil.Day}, wantErr: "", name: "success", }, { ivl: 0.25, want: 0, wantErr: "unexpected type of querylog_interval: float64", name: "fail", }} for _, tc := range testCases { conf := yobj{ "dns": yobj{ "querylog_interval": tc.ivl, }, "schema_version": 11, } t.Run(tc.name, func(t *testing.T) { err := upgradeSchema11to12(conf) if tc.wantErr != "" { require.Error(t, err) assert.Equal(t, tc.wantErr, err.Error()) return } require.NoError(t, err) require.Equal(t, conf["schema_version"], 12) dnsVal, ok := conf["dns"] require.True(t, ok) var newDNSConf yobj newDNSConf, ok = dnsVal.(yobj) require.True(t, ok) var newIvl timeutil.Duration newIvl, ok = newDNSConf["querylog_interval"].(timeutil.Duration) require.True(t, ok) assert.Equal(t, tc.want, newIvl) }) } t.Run("no_dns", func(t *testing.T) { err := upgradeSchema11to12(yobj{}) assert.NoError(t, err) }) t.Run("bad_dns", func(t *testing.T) { err := upgradeSchema11to12(yobj{ "dns": 0, }) testutil.AssertErrorMsg(t, "unexpected type of dns: int", err) }) t.Run("no_field", func(t *testing.T) { conf := yobj{ "dns": yobj{}, } err := upgradeSchema11to12(conf) require.NoError(t, err) dns, ok := conf["dns"] require.True(t, ok) var dnsVal yobj dnsVal, ok = dns.(yobj) require.True(t, ok) var ivl any ivl, ok = dnsVal["querylog_interval"] require.True(t, ok) var ivlVal timeutil.Duration ivlVal, ok = ivl.(timeutil.Duration) require.True(t, ok) assert.Equal(t, 90*24*time.Hour, ivlVal.Duration) }) } func TestUpgradeSchema12to13(t *testing.T) { const newSchemaVer = 13 testCases := []struct { in yobj want yobj name string }{{ in: yobj{}, want: yobj{"schema_version": newSchemaVer}, name: "no_dns", }, { in: yobj{"dns": yobj{}}, want: yobj{ "dns": yobj{}, "schema_version": newSchemaVer, }, name: "no_dhcp", }, { in: yobj{ "dns": yobj{ "local_domain_name": "lan", }, "dhcp": yobj{}, "schema_version": newSchemaVer - 1, }, want: yobj{ "dns": yobj{}, "dhcp": yobj{ "local_domain_name": "lan", }, "schema_version": newSchemaVer, }, name: "good", }} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { err := upgradeSchema12to13(tc.in) require.NoError(t, err) assert.Equal(t, tc.want, tc.in) }) } } func TestUpgradeSchema13to14(t *testing.T) { const newSchemaVer = 14 testClient := &clientObject{ Name: "agh-client", IDs: []string{"id1"}, UseGlobalSettings: true, } testCases := []struct { in yobj want yobj name string }{{ in: yobj{}, want: yobj{ "schema_version": newSchemaVer, // The clients field will be added anyway. "clients": yobj{ "persistent": yarr{}, "runtime_sources": &clientSourcesConf{ WHOIS: true, ARP: true, RDNS: false, DHCP: true, HostsFile: true, }, }, }, name: "no_clients", }, { in: yobj{ "clients": []*clientObject{testClient}, }, want: yobj{ "schema_version": newSchemaVer, "clients": yobj{ "persistent": []*clientObject{testClient}, "runtime_sources": &clientSourcesConf{ WHOIS: true, ARP: true, RDNS: false, DHCP: true, HostsFile: true, }, }, }, name: "no_dns", }, { in: yobj{ "clients": []*clientObject{testClient}, "dns": yobj{ "resolve_clients": true, }, }, want: yobj{ "schema_version": newSchemaVer, "clients": yobj{ "persistent": []*clientObject{testClient}, "runtime_sources": &clientSourcesConf{ WHOIS: true, ARP: true, RDNS: true, DHCP: true, HostsFile: true, }, }, "dns": yobj{}, }, name: "good", }} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { err := upgradeSchema13to14(tc.in) require.NoError(t, err) assert.Equal(t, tc.want, tc.in) }) } } func TestUpgradeSchema14to15(t *testing.T) { const newSchemaVer = 15 defaultWantObj := yobj{ "querylog": map[string]any{ "enabled": true, "file_enabled": true, "interval": "2160h", "size_memory": 1000, "ignored": []any{}, }, "dns": map[string]any{}, "schema_version": newSchemaVer, } testCases := []struct { in yobj want yobj name string }{{ in: yobj{ "dns": map[string]any{ "querylog_enabled": true, "querylog_file_enabled": true, "querylog_interval": "2160h", "querylog_size_memory": 1000, }, }, want: defaultWantObj, name: "basic", }, { in: yobj{ "dns": map[string]any{}, }, want: defaultWantObj, name: "default_values", }} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { err := upgradeSchema14to15(tc.in) require.NoError(t, err) assert.Equal(t, tc.want, tc.in) }) } } func TestUpgradeSchema15to16(t *testing.T) { const newSchemaVer = 16 defaultWantObj := yobj{ "statistics": map[string]any{ "enabled": true, "interval": 1, "ignored": []any{}, }, "dns": map[string]any{}, "schema_version": newSchemaVer, } testCases := []struct { in yobj want yobj name string }{{ in: yobj{ "dns": map[string]any{ "statistics_interval": 1, }, }, want: defaultWantObj, name: "basic", }, { in: yobj{ "dns": map[string]any{}, }, want: defaultWantObj, name: "default_values", }, { in: yobj{ "dns": map[string]any{ "statistics_interval": 0, }, }, want: yobj{ "statistics": map[string]any{ "enabled": false, "interval": 0, "ignored": []any{}, }, "dns": map[string]any{}, "schema_version": newSchemaVer, }, name: "stats_disabled", }} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { err := upgradeSchema15to16(tc.in) require.NoError(t, err) assert.Equal(t, tc.want, tc.in) }) } }