/*
 *
 * Copyright 2018 gRPC authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package dns_test

import (
	"context"
	"errors"
	"fmt"
	"net"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"google.golang.org/grpc/balancer"
	grpclbstate "google.golang.org/grpc/balancer/grpclb/state"
	"google.golang.org/grpc/internal"
	"google.golang.org/grpc/internal/envconfig"
	"google.golang.org/grpc/internal/grpctest"
	"google.golang.org/grpc/internal/resolver/dns"
	dnsinternal "google.golang.org/grpc/internal/resolver/dns/internal"
	"google.golang.org/grpc/internal/testutils"
	"google.golang.org/grpc/resolver"
	dnspublic "google.golang.org/grpc/resolver/dns"
	"google.golang.org/grpc/serviceconfig"

	_ "google.golang.org/grpc" // To initialize internal.ParseServiceConfig
)

const (
	txtBytesLimit           = 255
	defaultTestTimeout      = 10 * time.Second
	defaultTestShortTimeout = 10 * time.Millisecond

	colonDefaultPort = ":443"
)

type s struct {
	grpctest.Tester
}

func Test(t *testing.T) {
	grpctest.RunSubTests(t, s{})
}

// Override the default net.Resolver with a test resolver.
func overrideNetResolver(t *testing.T, r *testNetResolver) {
	origNetResolver := dnsinternal.NewNetResolver
	dnsinternal.NewNetResolver = func(string) (dnsinternal.NetResolver, error) { return r, nil }
	t.Cleanup(func() { dnsinternal.NewNetResolver = origNetResolver })
}

// Override the DNS minimum resolution interval used by the resolver.
func overrideResolutionInterval(t *testing.T, d time.Duration) {
	origMinResInterval := dns.MinResolutionInterval
	dnspublic.SetMinResolutionInterval(d)
	t.Cleanup(func() { dnspublic.SetMinResolutionInterval(origMinResInterval) })
}

// Override the timer used by the DNS resolver to fire after a duration of d.
func overrideTimeAfterFunc(t *testing.T, d time.Duration) {
	origTimeAfter := dnsinternal.TimeAfterFunc
	dnsinternal.TimeAfterFunc = func(time.Duration) <-chan time.Time {
		return time.After(d)
	}
	t.Cleanup(func() { dnsinternal.TimeAfterFunc = origTimeAfter })
}

// Override the timer used by the DNS resolver as follows:
// - use the durChan to read the duration that the resolver wants to wait for
// - use the timerChan to unblock the wait on the timer
func overrideTimeAfterFuncWithChannel(t *testing.T) (durChan chan time.Duration, timeChan chan time.Time) {
	origTimeAfter := dnsinternal.TimeAfterFunc
	durChan = make(chan time.Duration, 1)
	timeChan = make(chan time.Time)
	dnsinternal.TimeAfterFunc = func(d time.Duration) <-chan time.Time {
		select {
		case durChan <- d:
		default:
		}
		return timeChan
	}
	t.Cleanup(func() { dnsinternal.TimeAfterFunc = origTimeAfter })
	return durChan, timeChan
}

// Override the current time used by the DNS resolver.
func overrideTimeNowFunc(t *testing.T, now time.Time) {
	origTimeNowFunc := dnsinternal.TimeNowFunc
	dnsinternal.TimeNowFunc = func() time.Time { return now }
	t.Cleanup(func() { dnsinternal.TimeNowFunc = origTimeNowFunc })
}

// Override the remaining wait time to allow re-resolution by DNS resolver.
// Use the timeChan to read the time until resolver needs to wait for
// and return 0 wait time.
func overrideTimeUntilFuncWithChannel(t *testing.T) (timeChan chan time.Time) {
	timeCh := make(chan time.Time, 1)
	origTimeUntil := dnsinternal.TimeUntilFunc
	dnsinternal.TimeUntilFunc = func(t time.Time) time.Duration {
		timeCh <- t
		return 0
	}
	t.Cleanup(func() { dnsinternal.TimeUntilFunc = origTimeUntil })
	return timeCh
}

func enableSRVLookups(t *testing.T) {
	origEnableSRVLookups := dns.EnableSRVLookups
	dns.EnableSRVLookups = true
	t.Cleanup(func() { dns.EnableSRVLookups = origEnableSRVLookups })
}

// Builds a DNS resolver for target and returns a couple of channels to read the
// state and error pushed by the resolver respectively.
func buildResolverWithTestClientConn(t *testing.T, target string) (resolver.Resolver, chan resolver.State, chan error) {
	t.Helper()

	b := resolver.Get("dns")
	if b == nil {
		t.Fatalf("Resolver for dns:/// scheme not registered")
	}

	stateCh := make(chan resolver.State, 1)
	updateStateF := func(s resolver.State) error {
		select {
		case stateCh <- s:
		default:
		}
		return nil
	}

	errCh := make(chan error, 1)
	reportErrorF := func(err error) {
		select {
		case errCh <- err:
		default:
		}
	}

	tcc := &testutils.ResolverClientConn{Logger: t, UpdateStateF: updateStateF, ReportErrorF: reportErrorF}
	r, err := b.Build(resolver.Target{URL: *testutils.MustParseURL(fmt.Sprintf("dns:///%s", target))}, tcc, resolver.BuildOptions{})
	if err != nil {
		t.Fatalf("Failed to build DNS resolver for target %q: %v\n", target, err)
	}
	t.Cleanup(func() { r.Close() })

	return r, stateCh, errCh
}

// Waits for a state update from the DNS resolver and verifies the following:
// - wantAddrs matches the list of addresses in the update
// - wantBalancerAddrs matches the list of grpclb addresses in the update
// - wantSC matches the service config in the update
func verifyUpdateFromResolver(ctx context.Context, t *testing.T, stateCh chan resolver.State, wantAddrs, wantBalancerAddrs []resolver.Address, wantSC string) {
	t.Helper()

	var state resolver.State
	select {
	case <-ctx.Done():
		t.Fatal("Timeout when waiting for a state update from the resolver")
	case state = <-stateCh:
	}

	if !cmp.Equal(state.Addresses, wantAddrs, cmpopts.EquateEmpty()) {
		t.Fatalf("Got addresses: %+v, want: %+v", state.Addresses, wantAddrs)
	}
	if gs := grpclbstate.Get(state); gs == nil {
		if len(wantBalancerAddrs) > 0 {
			t.Fatalf("Got no grpclb addresses. Want %d", len(wantBalancerAddrs))
		}
	} else {
		if !cmp.Equal(gs.BalancerAddresses, wantBalancerAddrs) {
			t.Fatalf("Got grpclb addresses %+v, want %+v", gs.BalancerAddresses, wantBalancerAddrs)
		}
	}
	if wantSC == "{}" {
		if state.ServiceConfig != nil && state.ServiceConfig.Config != nil {
			t.Fatalf("Got service config:\n%s \nWant service config: {}", cmp.Diff(nil, state.ServiceConfig.Config))
		}

	} else if wantSC != "" {
		wantSCParsed := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)(wantSC)
		if !internal.EqualServiceConfigForTesting(state.ServiceConfig.Config, wantSCParsed.Config) {
			t.Fatalf("Got service config:\n%s \nWant service config:\n%s", cmp.Diff(nil, state.ServiceConfig.Config), cmp.Diff(nil, wantSCParsed.Config))
		}
	}
}

// This is the service config used by the fake net.Resolver in its TXT record.
//   - it contains an array of 5 entries
//   - the first three will be dropped by the DNS resolver as part of its
//     canarying rule matching functionality:
//   - the client language does not match in the first entry
//   - the percentage is set to 0 in the second entry
//   - the client host name does not match in the third entry
//   - the fourth and fifth entries will match the canarying rules, and therefore
//     the fourth entry will be used as it will be  the first matching entry.
const txtRecordGood = `
[
	{
		"clientLanguage": [
			"CPP",
			"JAVA"
		],
		"serviceConfig": {
			"loadBalancingPolicy": "grpclb",
			"methodConfig": [
				{
					"name": [
						{
							"service": "all"
						}
					],
					"timeout": "1s"
				}
			]
		}
	},
	{
		"percentage": 0,
		"serviceConfig": {
			"loadBalancingPolicy": "grpclb",
			"methodConfig": [
				{
					"name": [
						{
							"service": "all"
						}
					],
					"timeout": "1s"
				}
			]
		}
	},
	{
		"clientHostName": [
			"localhost"
		],
		"serviceConfig": {
			"loadBalancingPolicy": "grpclb",
			"methodConfig": [
				{
					"name": [
						{
							"service": "all"
						}
					],
					"timeout": "1s"
				}
			]
		}
	},
	{
		"clientLanguage": [
			"GO"
		],
		"percentage": 100,
		"serviceConfig": {
			"loadBalancingPolicy": "round_robin",
			"methodConfig": [
				{
					"name": [
						{
							"service": "foo"
						}
					],
					"waitForReady": true,
					"timeout": "1s"
				},
				{
					"name": [
						{
							"service": "bar"
						}
					],
					"waitForReady": false
				}
			]
		}
	},
	{
		"serviceConfig": {
			"loadBalancingPolicy": "round_robin",
			"methodConfig": [
				{
					"name": [
						{
							"service": "foo",
							"method": "bar"
						}
					],
					"waitForReady": true
				}
			]
		}
	}
]`

// This is the matched portion of the above TXT record entry.
const scJSON = `
{
	"loadBalancingPolicy": "round_robin",
	"methodConfig": [
		{
			"name": [
				{
					"service": "foo"
				}
			],
			"waitForReady": true,
			"timeout": "1s"
		},
		{
			"name": [
				{
					"service": "bar"
				}
			],
			"waitForReady": false
		}
	]
}`

// This service config contains three entries, but none of the match the DNS
// resolver's canarying rules and hence the resulting service config pushed by
// the DNS resolver will be an empty one.
const txtRecordNonMatching = `
[
	{
		"clientLanguage": [
			"CPP",
			"JAVA"
		],
		"serviceConfig": {
			"loadBalancingPolicy": "grpclb",
			"methodConfig": [
				{
					"name": [
						{
							"service": "all"
						}
					],
					"timeout": "1s"
				}
			]
		}
	},
	{
		"percentage": 0,
		"serviceConfig": {
			"loadBalancingPolicy": "grpclb",
			"methodConfig": [
				{
					"name": [
						{
							"service": "all"
						}
					],
					"timeout": "1s"
				}
			]
		}
	},
	{
		"clientHostName": [
			"localhost"
		],
		"serviceConfig": {
			"loadBalancingPolicy": "grpclb",
			"methodConfig": [
				{
					"name": [
						{
							"service": "all"
						}
					],
					"timeout": "1s"
				}
			]
		}
	}
]`

// Tests the scenario where a name resolves to a list of addresses, possibly
// some grpclb addresses as well, and a service config. The test verifies that
// the expected update is pushed to the channel.
func (s) TestDNSResolver_Basic(t *testing.T) {
	tests := []struct {
		name              string
		target            string
		hostLookupTable   map[string][]string
		srvLookupTable    map[string][]*net.SRV
		txtLookupTable    map[string][]string
		wantAddrs         []resolver.Address
		wantBalancerAddrs []resolver.Address
		wantSC            string
	}{
		{
			name:   "default_port",
			target: "foo.bar.com",
			hostLookupTable: map[string][]string{
				"foo.bar.com": {"1.2.3.4", "5.6.7.8"},
			},
			txtLookupTable: map[string][]string{
				"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
			},
			wantAddrs:         []resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}, {Addr: "5.6.7.8" + colonDefaultPort}},
			wantBalancerAddrs: nil,
			wantSC:            scJSON,
		},
		{
			name:   "specified_port",
			target: "foo.bar.com:1234",
			hostLookupTable: map[string][]string{
				"foo.bar.com": {"1.2.3.4", "5.6.7.8"},
			},
			txtLookupTable: map[string][]string{
				"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
			},
			wantAddrs:         []resolver.Address{{Addr: "1.2.3.4:1234"}, {Addr: "5.6.7.8:1234"}},
			wantBalancerAddrs: nil,
			wantSC:            scJSON,
		},
		{
			name:   "ipv4_with_SRV_and_single_grpclb_address",
			target: "srv.ipv4.single.fake",
			hostLookupTable: map[string][]string{
				"srv.ipv4.single.fake": {"2.4.6.8"},
				"ipv4.single.fake":     {"1.2.3.4"},
			},
			srvLookupTable: map[string][]*net.SRV{
				"_grpclb._tcp.srv.ipv4.single.fake": {&net.SRV{Target: "ipv4.single.fake", Port: 1234}},
			},
			txtLookupTable: map[string][]string{
				"_grpc_config.srv.ipv4.single.fake": txtRecordServiceConfig(txtRecordGood),
			},
			wantAddrs:         []resolver.Address{{Addr: "2.4.6.8" + colonDefaultPort}},
			wantBalancerAddrs: []resolver.Address{{Addr: "1.2.3.4:1234", ServerName: "ipv4.single.fake"}},
			wantSC:            scJSON,
		},
		{
			name:   "ipv4_with_SRV_and_multiple_grpclb_address",
			target: "srv.ipv4.multi.fake",
			hostLookupTable: map[string][]string{
				"ipv4.multi.fake": {"1.2.3.4", "5.6.7.8", "9.10.11.12"},
			},
			srvLookupTable: map[string][]*net.SRV{
				"_grpclb._tcp.srv.ipv4.multi.fake": {&net.SRV{Target: "ipv4.multi.fake", Port: 1234}},
			},
			txtLookupTable: map[string][]string{
				"_grpc_config.srv.ipv4.multi.fake": txtRecordServiceConfig(txtRecordGood),
			},
			wantAddrs: nil,
			wantBalancerAddrs: []resolver.Address{
				{Addr: "1.2.3.4:1234", ServerName: "ipv4.multi.fake"},
				{Addr: "5.6.7.8:1234", ServerName: "ipv4.multi.fake"},
				{Addr: "9.10.11.12:1234", ServerName: "ipv4.multi.fake"},
			},
			wantSC: scJSON,
		},
		{
			name:   "ipv6_with_SRV_and_single_grpclb_address",
			target: "srv.ipv6.single.fake",
			hostLookupTable: map[string][]string{
				"srv.ipv6.single.fake": nil,
				"ipv6.single.fake":     {"2607:f8b0:400a:801::1001"},
			},
			srvLookupTable: map[string][]*net.SRV{
				"_grpclb._tcp.srv.ipv6.single.fake": {&net.SRV{Target: "ipv6.single.fake", Port: 1234}},
			},
			txtLookupTable: map[string][]string{
				"_grpc_config.srv.ipv6.single.fake": txtRecordServiceConfig(txtRecordNonMatching),
			},
			wantAddrs:         nil,
			wantBalancerAddrs: []resolver.Address{{Addr: "[2607:f8b0:400a:801::1001]:1234", ServerName: "ipv6.single.fake"}},
			wantSC:            "{}",
		},
		{
			name:   "ipv6_with_SRV_and_multiple_grpclb_address",
			target: "srv.ipv6.multi.fake",
			hostLookupTable: map[string][]string{
				"srv.ipv6.multi.fake": nil,
				"ipv6.multi.fake":     {"2607:f8b0:400a:801::1001", "2607:f8b0:400a:801::1002", "2607:f8b0:400a:801::1003"},
			},
			srvLookupTable: map[string][]*net.SRV{
				"_grpclb._tcp.srv.ipv6.multi.fake": {&net.SRV{Target: "ipv6.multi.fake", Port: 1234}},
			},
			txtLookupTable: map[string][]string{
				"_grpc_config.srv.ipv6.multi.fake": txtRecordServiceConfig(txtRecordNonMatching),
			},
			wantAddrs: nil,
			wantBalancerAddrs: []resolver.Address{
				{Addr: "[2607:f8b0:400a:801::1001]:1234", ServerName: "ipv6.multi.fake"},
				{Addr: "[2607:f8b0:400a:801::1002]:1234", ServerName: "ipv6.multi.fake"},
				{Addr: "[2607:f8b0:400a:801::1003]:1234", ServerName: "ipv6.multi.fake"},
			},
			wantSC: "{}",
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			overrideTimeAfterFunc(t, 2*defaultTestTimeout)
			overrideNetResolver(t, &testNetResolver{
				hostLookupTable: test.hostLookupTable,
				srvLookupTable:  test.srvLookupTable,
				txtLookupTable:  test.txtLookupTable,
			})
			enableSRVLookups(t)
			_, stateCh, _ := buildResolverWithTestClientConn(t, test.target)

			ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
			defer cancel()
			verifyUpdateFromResolver(ctx, t, stateCh, test.wantAddrs, test.wantBalancerAddrs, test.wantSC)
		})
	}
}

// Tests the case where the channel returns an error for the update pushed by
// the DNS resolver. Verifies that the DNS resolver backs off before trying to
// resolve. Once the channel returns a nil error, the test verifies that the DNS
// resolver does not backoff anymore.
func (s) TestDNSResolver_ExponentialBackoff(t *testing.T) {
	tests := []struct {
		name            string
		target          string
		hostLookupTable map[string][]string
		txtLookupTable  map[string][]string
		wantAddrs       []resolver.Address
		wantSC          string
	}{
		{
			name:            "happy case default port",
			target:          "foo.bar.com",
			hostLookupTable: map[string][]string{"foo.bar.com": {"1.2.3.4", "5.6.7.8"}},
			txtLookupTable: map[string][]string{
				"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
			},
			wantAddrs: []resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}, {Addr: "5.6.7.8" + colonDefaultPort}},
			wantSC:    scJSON,
		},
		{
			name:            "happy case specified port",
			target:          "foo.bar.com:1234",
			hostLookupTable: map[string][]string{"foo.bar.com": {"1.2.3.4", "5.6.7.8"}},
			txtLookupTable: map[string][]string{
				"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
			},
			wantAddrs: []resolver.Address{{Addr: "1.2.3.4:1234"}, {Addr: "5.6.7.8:1234"}},
			wantSC:    scJSON,
		},
		{
			name:   "happy case another default port",
			target: "srv.ipv4.single.fake",
			hostLookupTable: map[string][]string{
				"srv.ipv4.single.fake": {"2.4.6.8"},
				"ipv4.single.fake":     {"1.2.3.4"},
			},
			txtLookupTable: map[string][]string{
				"_grpc_config.srv.ipv4.single.fake": txtRecordServiceConfig(txtRecordGood),
			},
			wantAddrs: []resolver.Address{{Addr: "2.4.6.8" + colonDefaultPort}},
			wantSC:    scJSON,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			durChan, timeChan := overrideTimeAfterFuncWithChannel(t)
			overrideNetResolver(t, &testNetResolver{
				hostLookupTable: test.hostLookupTable,
				txtLookupTable:  test.txtLookupTable,
			})

			// Set the test clientconn to return error back to the resolver when
			// it pushes an update on the channel.
			var returnNilErr atomic.Bool
			updateStateF := func(resolver.State) error {
				if returnNilErr.Load() {
					return nil
				}
				return balancer.ErrBadResolverState
			}
			tcc := &testutils.ResolverClientConn{Logger: t, UpdateStateF: updateStateF}

			b := resolver.Get("dns")
			if b == nil {
				t.Fatalf("Resolver for dns:/// scheme not registered")
			}
			r, err := b.Build(resolver.Target{URL: *testutils.MustParseURL(fmt.Sprintf("dns:///%s", test.target))}, tcc, resolver.BuildOptions{})
			if err != nil {
				t.Fatalf("Failed to build DNS resolver for target %q: %v\n", test.target, err)
			}
			defer r.Close()

			// Expect the DNS resolver to backoff and attempt to re-resolve.
			ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
			defer cancel()
			const retries = 10
			var prevDur time.Duration
			for i := 0; i < retries; i++ {
				select {
				case <-ctx.Done():
					t.Fatalf("(Iteration: %d): Timeout when waiting for DNS resolver to backoff", i)
				case dur := <-durChan:
					if dur <= prevDur {
						t.Fatalf("(Iteration: %d): Unexpected decrease in amount of time to backoff", i)
					}
				}

				if i == retries-1 {
					// Update resolver.ClientConn to not return an error
					// anymore before last resolution retry to ensure that
					// last resolution attempt doesn't back off.
					returnNilErr.Store(true)
				}

				// Unblock the DNS resolver's backoff by pushing the current time.
				timeChan <- time.Now()
			}

			// Verify that the DNS resolver does not backoff anymore.
			sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
			defer sCancel()
			select {
			case <-durChan:
				t.Fatal("Unexpected DNS resolver backoff")
			case <-sCtx.Done():
			}
		})
	}
}

// Tests the case where the DNS resolver is asked to re-resolve by invoking the
// ResolveNow method.
func (s) TestDNSResolver_ResolveNow(t *testing.T) {
	const target = "foo.bar.com"

	overrideResolutionInterval(t, 0)
	overrideTimeAfterFunc(t, 0)
	tr := &testNetResolver{
		hostLookupTable: map[string][]string{
			"foo.bar.com": {"1.2.3.4", "5.6.7.8"},
		},
		txtLookupTable: map[string][]string{
			"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
		},
	}
	overrideNetResolver(t, tr)

	r, stateCh, _ := buildResolverWithTestClientConn(t, target)

	// Verify that the first update pushed by the resolver matches expectations.
	wantAddrs := []resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}, {Addr: "5.6.7.8" + colonDefaultPort}}
	wantSC := scJSON
	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
	defer cancel()
	verifyUpdateFromResolver(ctx, t, stateCh, wantAddrs, nil, wantSC)

	// Update state in the fake net.Resolver to return only one address and a
	// new service config.
	tr.UpdateHostLookupTable(map[string][]string{target: {"1.2.3.4"}})
	tr.UpdateTXTLookupTable(map[string][]string{
		"_grpc_config.foo.bar.com": txtRecordServiceConfig(`[{"serviceConfig":{"loadBalancingPolicy": "grpclb"}}]`),
	})

	// Ask the resolver to re-resolve and verify that the new update matches
	// expectations.
	r.ResolveNow(resolver.ResolveNowOptions{})
	wantAddrs = []resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}}
	wantSC = `{"loadBalancingPolicy": "grpclb"}`
	verifyUpdateFromResolver(ctx, t, stateCh, wantAddrs, nil, wantSC)

	// Update state in the fake resolver to return no addresses and the same
	// service config as before.
	tr.UpdateHostLookupTable(map[string][]string{target: nil})

	// Ask the resolver to re-resolve and verify that the new update matches
	// expectations.
	r.ResolveNow(resolver.ResolveNowOptions{})
	verifyUpdateFromResolver(ctx, t, stateCh, nil, nil, wantSC)
}

// Tests the case where the given name is an IP address and verifies that the
// update pushed by the DNS resolver meets expectations.
func (s) TestIPResolver(t *testing.T) {
	tests := []struct {
		name     string
		target   string
		wantAddr []resolver.Address
	}{
		{
			name:     "localhost ipv4 default port",
			target:   "127.0.0.1",
			wantAddr: []resolver.Address{{Addr: "127.0.0.1:443"}},
		},
		{
			name:     "localhost ipv4 non-default port",
			target:   "127.0.0.1:12345",
			wantAddr: []resolver.Address{{Addr: "127.0.0.1:12345"}},
		},
		{
			name:     "localhost ipv6 default port no brackets",
			target:   "::1",
			wantAddr: []resolver.Address{{Addr: "[::1]:443"}},
		},
		{
			name:     "localhost ipv6 default port with brackets",
			target:   "[::1]",
			wantAddr: []resolver.Address{{Addr: "[::1]:443"}},
		},
		{
			name:     "localhost ipv6 non-default port",
			target:   "[::1]:12345",
			wantAddr: []resolver.Address{{Addr: "[::1]:12345"}},
		},
		{
			name:     "ipv6 default port no brackets",
			target:   "2001:db8:85a3::8a2e:370:7334",
			wantAddr: []resolver.Address{{Addr: "[2001:db8:85a3::8a2e:370:7334]:443"}},
		},
		{
			name:     "ipv6 default port with brackets",
			target:   "[2001:db8:85a3::8a2e:370:7334]",
			wantAddr: []resolver.Address{{Addr: "[2001:db8:85a3::8a2e:370:7334]:443"}},
		},
		{
			name:     "ipv6 non-default port with brackets",
			target:   "[2001:db8:85a3::8a2e:370:7334]:12345",
			wantAddr: []resolver.Address{{Addr: "[2001:db8:85a3::8a2e:370:7334]:12345"}},
		},
		{
			name:     "abbreviated ipv6 address",
			target:   "[2001:db8::1]:http",
			wantAddr: []resolver.Address{{Addr: "[2001:db8::1]:http"}},
		},
		{
			name:     "ipv6 with zone and port",
			target:   "[fe80::1%25eth0]:1234",
			wantAddr: []resolver.Address{{Addr: "[fe80::1%eth0]:1234"}},
		},
		{
			name:     "ipv6 with zone and default port",
			target:   "fe80::1%25eth0",
			wantAddr: []resolver.Address{{Addr: "[fe80::1%eth0]:443"}},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			overrideResolutionInterval(t, 0)
			overrideTimeAfterFunc(t, 2*defaultTestTimeout)
			r, stateCh, _ := buildResolverWithTestClientConn(t, test.target)

			ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
			defer cancel()
			verifyUpdateFromResolver(ctx, t, stateCh, test.wantAddr, nil, "")

			// Attempt to re-resolve should not result in a state update.
			r.ResolveNow(resolver.ResolveNowOptions{})
			sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
			defer sCancel()
			select {
			case <-sCtx.Done():
			case s := <-stateCh:
				t.Fatalf("Unexpected state update from the resolver: %+v", s)
			}
		})
	}
}

// Tests the DNS resolver builder with different target names.
func (s) TestResolverBuild(t *testing.T) {
	tests := []struct {
		name    string
		target  string
		wantErr string
	}{
		{
			name:   "valid url",
			target: "www.google.com",
		},
		{
			name:   "host port",
			target: "foo.bar:12345",
		},
		{
			name:   "ipv4 address with default port",
			target: "127.0.0.1",
		},
		{
			name:   "ipv6 address without brackets and default port",
			target: "::",
		},
		{
			name:   "ipv4 address with non-default port",
			target: "127.0.0.1:12345",
		},
		{
			name:   "localhost ipv6 with brackets",
			target: "[::1]:80",
		},
		{
			name:   "ipv6 address with brackets",
			target: "[2001:db8:a0b:12f0::1]:21",
		},
		{
			name:   "empty host with port",
			target: ":80",
		},
		{
			name:   "ipv6 address with zone",
			target: "[fe80::1%25lo0]:80",
		},
		{
			name:   "url with port",
			target: "golang.org:http",
		},
		{
			name:   "ipv6 address with non integer port",
			target: "[2001:db8::1]:http",
		},
		{
			name:    "address ends with colon",
			target:  "[2001:db8::1]:",
			wantErr: dnsinternal.ErrEndsWithColon.Error(),
		},
		{
			name:    "address contains only a colon",
			target:  ":",
			wantErr: dnsinternal.ErrEndsWithColon.Error(),
		},
		{
			name:    "empty address",
			target:  "",
			wantErr: dnsinternal.ErrMissingAddr.Error(),
		},
		{
			name:    "invalid address",
			target:  "[2001:db8:a0b:12f0::1",
			wantErr: "invalid target address",
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			overrideTimeAfterFunc(t, 2*defaultTestTimeout)

			b := resolver.Get("dns")
			if b == nil {
				t.Fatalf("Resolver for dns:/// scheme not registered")
			}

			tcc := &testutils.ResolverClientConn{Logger: t}
			r, err := b.Build(resolver.Target{URL: *testutils.MustParseURL(fmt.Sprintf("dns:///%s", test.target))}, tcc, resolver.BuildOptions{})
			if err != nil {
				if test.wantErr == "" {
					t.Fatalf("DNS resolver build for target %q failed with error: %v", test.target, err)
				}
				if !strings.Contains(err.Error(), test.wantErr) {
					t.Fatalf("DNS resolver build for target %q failed with error: %v, wantErr: %s", test.target, err, test.wantErr)
				}
				return
			}
			if err == nil && test.wantErr != "" {
				t.Fatalf("DNS resolver build for target %q succeeded when expected to fail with error: %s", test.target, test.wantErr)
			}
			r.Close()
		})
	}
}

// Tests scenarios where fetching of service config is enabled or disabled, and
// verifies that the expected update is pushed by the DNS resolver.
func (s) TestDisableServiceConfig(t *testing.T) {
	tests := []struct {
		name                 string
		target               string
		hostLookupTable      map[string][]string
		txtLookupTable       map[string][]string
		txtLookupsEnabled    bool
		disableServiceConfig bool
		wantAddrs            []resolver.Address
		wantSC               string
	}{
		{
			name:            "not disabled in BuildOptions; enabled globally",
			target:          "foo.bar.com",
			hostLookupTable: map[string][]string{"foo.bar.com": {"1.2.3.4", "5.6.7.8"}},
			txtLookupTable: map[string][]string{
				"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
			},
			txtLookupsEnabled:    true,
			disableServiceConfig: false,
			wantAddrs:            []resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}, {Addr: "5.6.7.8" + colonDefaultPort}},
			wantSC:               scJSON,
		},
		{
			name:            "disabled in BuildOptions; enabled globally",
			target:          "foo.bar.com",
			hostLookupTable: map[string][]string{"foo.bar.com": {"1.2.3.4", "5.6.7.8"}},
			txtLookupTable: map[string][]string{
				"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
			},
			txtLookupsEnabled:    true,
			disableServiceConfig: true,
			wantAddrs:            []resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}, {Addr: "5.6.7.8" + colonDefaultPort}},
			wantSC:               "{}",
		},
		{
			name:            "not disabled in BuildOptions; disabled globally",
			target:          "foo.bar.com",
			hostLookupTable: map[string][]string{"foo.bar.com": {"1.2.3.4", "5.6.7.8"}},
			txtLookupTable: map[string][]string{
				"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
			},
			txtLookupsEnabled:    false,
			disableServiceConfig: false,
			wantAddrs:            []resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}, {Addr: "5.6.7.8" + colonDefaultPort}},
			wantSC:               "{}",
		},
		{
			name:            "disabled in BuildOptions; disabled globally",
			target:          "foo.bar.com",
			hostLookupTable: map[string][]string{"foo.bar.com": {"1.2.3.4", "5.6.7.8"}},
			txtLookupTable: map[string][]string{
				"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
			},
			txtLookupsEnabled:    false,
			disableServiceConfig: true,
			wantAddrs:            []resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}, {Addr: "5.6.7.8" + colonDefaultPort}},
			wantSC:               "{}",
		},
	}

	oldEnableTXTServiceConfig := envconfig.EnableTXTServiceConfig
	defer func() {
		envconfig.EnableTXTServiceConfig = oldEnableTXTServiceConfig
	}()
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			envconfig.EnableTXTServiceConfig = test.txtLookupsEnabled
			overrideTimeAfterFunc(t, 2*defaultTestTimeout)
			overrideNetResolver(t, &testNetResolver{
				hostLookupTable: test.hostLookupTable,
				txtLookupTable:  test.txtLookupTable,
			})

			b := resolver.Get("dns")
			if b == nil {
				t.Fatalf("Resolver for dns:/// scheme not registered")
			}

			stateCh := make(chan resolver.State, 1)
			updateStateF := func(s resolver.State) error {
				stateCh <- s
				return nil
			}
			tcc := &testutils.ResolverClientConn{Logger: t, UpdateStateF: updateStateF}
			r, err := b.Build(resolver.Target{URL: *testutils.MustParseURL(fmt.Sprintf("dns:///%s", test.target))}, tcc, resolver.BuildOptions{DisableServiceConfig: test.disableServiceConfig})
			if err != nil {
				t.Fatalf("Failed to build DNS resolver for target %q: %v\n", test.target, err)
			}
			defer r.Close()

			ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
			defer cancel()
			verifyUpdateFromResolver(ctx, t, stateCh, test.wantAddrs, nil, test.wantSC)
		})
	}
}

// Tests the case where a TXT lookup is expected to return an error. Verifies
// that errors are ignored with the corresponding env var is set.
func (s) TestTXTError(t *testing.T) {
	for _, ignore := range []bool{false, true} {
		t.Run(fmt.Sprintf("%v", ignore), func(t *testing.T) {
			overrideTimeAfterFunc(t, 2*defaultTestTimeout)
			overrideNetResolver(t, &testNetResolver{hostLookupTable: map[string][]string{"ipv4.single.fake": {"1.2.3.4"}}})

			origTXTIgnore := envconfig.TXTErrIgnore
			envconfig.TXTErrIgnore = ignore
			defer func() { envconfig.TXTErrIgnore = origTXTIgnore }()

			// There is no entry for "ipv4.single.fake" in the txtLookupTbl
			// maintained by the fake net.Resolver. So, a TXT lookup for this
			// name will return an error.
			_, stateCh, _ := buildResolverWithTestClientConn(t, "ipv4.single.fake")

			ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
			defer cancel()
			var state resolver.State
			select {
			case <-ctx.Done():
				t.Fatal("Timeout when waiting for a state update from the resolver")
			case state = <-stateCh:
			}

			if ignore {
				if state.ServiceConfig != nil {
					t.Fatalf("Received non-nil service config: %+v; want nil", state.ServiceConfig)
				}
			} else {
				if state.ServiceConfig == nil || state.ServiceConfig.Err == nil {
					t.Fatalf("Received service config %+v; want non-nil error", state.ServiceConfig)
				}
			}
		})
	}
}

// Tests different cases for a user's dial target that specifies a non-empty
// authority (or Host field of the URL).
func (s) TestCustomAuthority(t *testing.T) {
	tests := []struct {
		name          string
		authority     string
		wantAuthority string
		wantBuildErr  bool
	}{
		{
			name:          "authority with default DNS port",
			authority:     "4.3.2.1:53",
			wantAuthority: "4.3.2.1:53",
		},
		{
			name:          "authority with non-default DNS port",
			authority:     "4.3.2.1:123",
			wantAuthority: "4.3.2.1:123",
		},
		{
			name:          "authority with no port",
			authority:     "4.3.2.1",
			wantAuthority: "4.3.2.1:53",
		},
		{
			name:          "ipv6 authority with no port",
			authority:     "::1",
			wantAuthority: "[::1]:53",
		},
		{
			name:          "ipv6 authority with brackets and no port",
			authority:     "[::1]",
			wantAuthority: "[::1]:53",
		},
		{
			name:          "ipv6 authority with brackets and non-default DNS port",
			authority:     "[::1]:123",
			wantAuthority: "[::1]:123",
		},
		{
			name:          "host name with no port",
			authority:     "dnsserver.com",
			wantAuthority: "dnsserver.com:53",
		},
		{
			name:          "no host port and non-default port",
			authority:     ":123",
			wantAuthority: "localhost:123",
		},
		{
			name:          "only colon",
			authority:     ":",
			wantAuthority: "",
			wantBuildErr:  true,
		},
		{
			name:          "ipv6 name ending in colon",
			authority:     "[::1]:",
			wantAuthority: "",
			wantBuildErr:  true,
		},
		{
			name:          "host name ending in colon",
			authority:     "dnsserver.com:",
			wantAuthority: "",
			wantBuildErr:  true,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			overrideTimeAfterFunc(t, 2*defaultTestTimeout)

			// Override the address dialer to verify the authority being passed.
			origAddressDialer := dnsinternal.AddressDialer
			errChan := make(chan error, 1)
			dnsinternal.AddressDialer = func(authority string) func(ctx context.Context, network, address string) (net.Conn, error) {
				if authority != test.wantAuthority {
					errChan <- fmt.Errorf("wrong custom authority passed to resolver. target: %s got authority: %s want authority: %s", test.authority, authority, test.wantAuthority)
				} else {
					errChan <- nil
				}
				return func(context.Context, string, string) (net.Conn, error) {
					return nil, errors.New("no need to dial")
				}
			}
			defer func() { dnsinternal.AddressDialer = origAddressDialer }()

			b := resolver.Get("dns")
			if b == nil {
				t.Fatalf("Resolver for dns:/// scheme not registered")
			}

			tcc := &testutils.ResolverClientConn{Logger: t}
			endpoint := "foo.bar.com"
			target := resolver.Target{URL: *testutils.MustParseURL(fmt.Sprintf("dns://%s/%s", test.authority, endpoint))}
			r, err := b.Build(target, tcc, resolver.BuildOptions{})
			if (err != nil) != test.wantBuildErr {
				t.Fatalf("DNS resolver build for target %+v returned error %v: wantErr: %v\n", target, err, test.wantBuildErr)
			}
			if err != nil {
				return
			}
			defer r.Close()

			if err := <-errChan; err != nil {
				t.Fatal(err)
			}
		})
	}
}

// TestRateLimitedResolve exercises the rate limit enforced on re-resolution
// requests. It sets the re-resolution rate to a small value and repeatedly
// calls ResolveNow() and ensures only the expected number of resolution
// requests are made.
func (s) TestRateLimitedResolve(t *testing.T) {
	const target = "foo.bar.com"
	_, timeChan := overrideTimeAfterFuncWithChannel(t)
	tr := &testNetResolver{
		lookupHostCh:    testutils.NewChannel(),
		hostLookupTable: map[string][]string{target: {"1.2.3.4", "5.6.7.8"}},
	}
	overrideNetResolver(t, tr)

	r, stateCh, _ := buildResolverWithTestClientConn(t, target)

	// Wait for the first resolution request to be done. This happens as part
	// of the first iteration of the for loop in watcher().
	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
	defer cancel()
	if _, err := tr.lookupHostCh.Receive(ctx); err != nil {
		t.Fatalf("Timed out waiting for lookup() call.")
	}

	// Call Resolve Now 100 times, shouldn't continue onto next iteration of
	// watcher, thus shouldn't lookup again.
	for i := 0; i <= 100; i++ {
		r.ResolveNow(resolver.ResolveNowOptions{})
	}

	continueCtx, continueCancel := context.WithTimeout(context.Background(), defaultTestShortTimeout)
	defer continueCancel()
	if _, err := tr.lookupHostCh.Receive(continueCtx); err == nil {
		t.Fatalf("Should not have looked up again as DNS Min Res Rate timer has not gone off.")
	}

	// Make the DNSMinResRate timer fire immediately, by sending the current
	// time on it. This will unblock the resolver which is currently blocked on
	// the DNS Min Res Rate timer going off, which will allow it to continue to
	// the next iteration of the watcher loop.
	select {
	case timeChan <- time.Now():
	case <-ctx.Done():
		t.Fatal("Timed out waiting for the DNS resolver to block on DNS Min Res Rate to elapse")
	}

	// Now that DNS Min Res Rate timer has gone off, it should lookup again.
	if _, err := tr.lookupHostCh.Receive(ctx); err != nil {
		t.Fatalf("Timed out waiting for lookup() call.")
	}

	// Resolve Now 1000 more times, shouldn't lookup again as DNS Min Res Rate
	// timer has not gone off.
	for i := 0; i < 1000; i++ {
		r.ResolveNow(resolver.ResolveNowOptions{})
	}
	continueCtx, continueCancel = context.WithTimeout(context.Background(), defaultTestShortTimeout)
	defer continueCancel()
	if _, err := tr.lookupHostCh.Receive(continueCtx); err == nil {
		t.Fatalf("Should not have looked up again as DNS Min Res Rate timer has not gone off.")
	}

	// Make the DNSMinResRate timer fire immediately again.
	select {
	case timeChan <- time.Now():
	case <-ctx.Done():
		t.Fatal("Timed out waiting for the DNS resolver to block on DNS Min Res Rate to elapse")
	}

	// Now that DNS Min Res Rate timer has gone off, it should lookup again.
	if _, err := tr.lookupHostCh.Receive(ctx); err != nil {
		t.Fatalf("Timed out waiting for lookup() call.")
	}

	wantAddrs := []resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}, {Addr: "5.6.7.8" + colonDefaultPort}}
	var state resolver.State
	select {
	case <-ctx.Done():
		t.Fatal("Timeout when waiting for a state update from the resolver")
	case state = <-stateCh:
	}
	if !cmp.Equal(state.Addresses, wantAddrs, cmpopts.EquateEmpty()) {
		t.Fatalf("Got addresses: %+v, want: %+v", state.Addresses, wantAddrs)
	}
}

// Test verifies that when the DNS resolver gets an error from the underlying
// net.Resolver, it reports the error to the channel and backs off and retries.
func (s) TestReportError(t *testing.T) {
	durChan, timeChan := overrideTimeAfterFuncWithChannel(t)
	overrideNetResolver(t, &testNetResolver{})

	const target = "notfoundaddress"
	_, _, errorCh := buildResolverWithTestClientConn(t, target)

	// Should receive first error.
	ctx, ctxCancel := context.WithTimeout(context.Background(), defaultTestTimeout)
	defer ctxCancel()
	select {
	case <-ctx.Done():
		t.Fatal("Timeout when waiting for an error from the resolver")
	case err := <-errorCh:
		if !strings.Contains(err.Error(), "hostLookup error") {
			t.Fatalf(`ReportError(err=%v) called; want err contains "hostLookupError"`, err)
		}
	}

	// Expect the DNS resolver to backoff and attempt to re-resolve. Every time,
	// the DNS resolver will receive the same error from the net.Resolver and is
	// expected to push it to the channel.
	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
	defer cancel()
	const retries = 10
	var prevDur time.Duration
	for i := 0; i < retries; i++ {
		select {
		case <-ctx.Done():
			t.Fatalf("(Iteration: %d): Timeout when waiting for DNS resolver to backoff", i)
		case dur := <-durChan:
			if dur <= prevDur {
				t.Fatalf("(Iteration: %d): Unexpected decrease in amount of time to backoff", i)
			}
		}

		// Unblock the DNS resolver's backoff by pushing the current time.
		timeChan <- time.Now()

		select {
		case <-ctx.Done():
			t.Fatal("Timeout when waiting for an error from the resolver")
		case err := <-errorCh:
			if !strings.Contains(err.Error(), "hostLookup error") {
				t.Fatalf(`ReportError(err=%v) called; want err contains "hostLookupError"`, err)
			}
		}
	}
}

// Override the default dns.ResolvingTimeout with a test duration.
func overrideResolveTimeoutDuration(t *testing.T, dur time.Duration) {
	t.Helper()

	origDur := dns.ResolvingTimeout
	dnspublic.SetResolvingTimeout(dur)

	t.Cleanup(func() { dnspublic.SetResolvingTimeout(origDur) })
}

// Test verifies that the DNS resolver gets timeout error when net.Resolver
// takes too long to resolve a target.
func (s) TestResolveTimeout(t *testing.T) {
	// Set DNS resolving timeout duration to 7ms
	timeoutDur := 7 * time.Millisecond
	overrideResolveTimeoutDuration(t, timeoutDur)

	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
	defer cancel()

	// We are trying to resolve hostname which takes infinity time to resolve.
	const target = "infinity"

	// Define a testNetResolver with lookupHostCh, an unbuffered channel,
	// so we can block the resolver until reaching timeout.
	tr := &testNetResolver{
		lookupHostCh:    testutils.NewChannelWithSize(0),
		hostLookupTable: map[string][]string{target: {"1.2.3.4"}},
	}
	overrideNetResolver(t, tr)

	_, _, errCh := buildResolverWithTestClientConn(t, target)
	select {
	case <-ctx.Done():
		t.Fatal("Timeout when waiting for the DNS resolver to timeout")
	case err := <-errCh:
		if err == nil || !strings.Contains(err.Error(), "context deadline exceeded") {
			t.Fatalf(`Expected to see Timeout error; got: %v`, err)
		}
	}
}

// Test verifies that changing [MinResolutionInterval] variable correctly effects
// the resolution behaviour
func (s) TestMinResolutionInterval(t *testing.T) {
	const target = "foo.bar.com"

	overrideResolutionInterval(t, 1*time.Millisecond)
	tr := &testNetResolver{
		hostLookupTable: map[string][]string{
			"foo.bar.com": {"1.2.3.4", "5.6.7.8"},
		},
		txtLookupTable: map[string][]string{
			"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
		},
	}
	overrideNetResolver(t, tr)

	r, stateCh, _ := buildResolverWithTestClientConn(t, target)

	wantAddrs := []resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}, {Addr: "5.6.7.8" + colonDefaultPort}}
	wantSC := scJSON

	for i := 0; i < 5; i++ {
		// set context timeout slightly higher than the min resolution interval to make sure resolutions
		// happen successfully
		ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
		defer cancel()

		verifyUpdateFromResolver(ctx, t, stateCh, wantAddrs, nil, wantSC)
		r.ResolveNow(resolver.ResolveNowOptions{})
	}
}

// TestMinResolutionInterval_NoExtraDelay verifies that there is no extra delay
// between two resolution requests apart from [MinResolutionInterval].
func (s) TestMinResolutionInterval_NoExtraDelay(t *testing.T) {
	tr := &testNetResolver{
		hostLookupTable: map[string][]string{
			"foo.bar.com": {"1.2.3.4", "5.6.7.8"},
		},
		txtLookupTable: map[string][]string{
			"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
		},
	}
	overrideNetResolver(t, tr)
	// Override time.Now() to return a zero value for time. This will allow us
	// to verify that the call to time.Until is made with the exact
	// [MinResolutionInterval] that we expect.
	overrideTimeNowFunc(t, time.Time{})
	// Override time.Until() to read the time passed to it
	// and return immediately without any delay
	timeCh := overrideTimeUntilFuncWithChannel(t)

	r, stateCh, errorCh := buildResolverWithTestClientConn(t, "foo.bar.com")

	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
	defer cancel()

	// Ensure that the first resolution happens.
	select {
	case <-ctx.Done():
		t.Fatal("Timeout when waiting for DNS resolver")
	case err := <-errorCh:
		t.Fatalf("Unexpected error from resolver, %v", err)
	case <-stateCh:
	}

	// Request re-resolution and verify that the resolver waits for
	// [MinResolutionInterval].
	r.ResolveNow(resolver.ResolveNowOptions{})
	select {
	case <-ctx.Done():
		t.Fatal("Timeout when waiting for DNS resolver")
	case gotTime := <-timeCh:
		wantTime := time.Time{}.Add(dns.MinResolutionInterval)
		if !gotTime.Equal(wantTime) {
			t.Fatalf("DNS resolver waits for %v time before re-resolution, want %v", gotTime, wantTime)
		}
	}

	// Ensure that the re-resolution request actually happens.
	select {
	case <-ctx.Done():
		t.Fatal("Timeout when waiting for an error from the resolver")
	case err := <-errorCh:
		t.Fatalf("Unexpected error from resolver, %v", err)
	case <-stateCh:
	}
}
