check / check (push) Successful in 1m18s
The watcher tests used a stand-in resolver and the resolver timeout test a stand-in DNS client, against the rule that DNS is never mocked. The watcher tests now run the real resolver against live DNS. A change is tested by saving values live DNS never returns (names under .invalid, 192.0.2.1) in the state a check starts from, or by marking a real nameserver failed. The timeout test queries 192.0.2.1, where nothing answers. The live-DNS retry and concurrency limit moved from the resolver tests to internal/livedns, so both packages share them. NewFromLoggerWithClient had no other use and is gone. TESTING.md and the DNSClient comment now state the README's rule. Model: opus-5-5
193 lines
4.6 KiB
Go
193 lines
4.6 KiB
Go
package resolver_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"sneak.berlin/go/dnswatcher/internal/resolver"
|
|
)
|
|
|
|
// Tests for the live-DNS harness in livedns_test.go itself. These
|
|
// exercise pure logic; they perform no DNS resolution of any kind, so
|
|
// they neither mock DNS nor depend on it.
|
|
|
|
// Names for the synthetic status maps below. Nothing is ever queried
|
|
// at them: they are map keys handed to the package's pure counting
|
|
// helpers, not a stand-in for a nameserver.
|
|
const (
|
|
nsExample1 = "ns1.example."
|
|
nsExample2 = "ns2.example."
|
|
nsExample3 = "ns3.example."
|
|
nsExample4 = "ns4.example."
|
|
)
|
|
|
|
func TestLiveQuorumIsStrictMajority(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := map[int]int{
|
|
0: 1,
|
|
1: 1,
|
|
2: 2,
|
|
3: 2,
|
|
4: 3,
|
|
5: 3,
|
|
13: 7,
|
|
}
|
|
|
|
for total, want := range cases {
|
|
assert.Equal(
|
|
t, want, liveQuorum(total),
|
|
"liveQuorum(%d)", total,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestStatusCountingIgnoresSilentNameservers(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
results := map[string]*resolver.NameserverResponse{
|
|
nsExample1: {
|
|
Nameserver: nsExample1,
|
|
Status: resolver.StatusOK,
|
|
},
|
|
nsExample2: {
|
|
Nameserver: nsExample2,
|
|
Status: resolver.StatusOK,
|
|
},
|
|
nsExample3: {
|
|
Nameserver: nsExample3,
|
|
Status: resolver.StatusTimeout,
|
|
},
|
|
nsExample4: {
|
|
Nameserver: nsExample4,
|
|
Status: resolver.StatusError,
|
|
},
|
|
}
|
|
|
|
assert.Equal(
|
|
t, 2, countStatus(results, resolver.StatusOK),
|
|
)
|
|
assert.Equal(
|
|
t, 0, countStatus(results, resolver.StatusNXDomain),
|
|
)
|
|
|
|
// Two of four answered, which is short of the quorum of
|
|
// three: this is the state that triggers a retry rather
|
|
// than an assertion failure.
|
|
assert.Equal(t, 2, answeredCount(results))
|
|
assert.Less(t, answeredCount(results), liveQuorum(len(results)))
|
|
|
|
assert.Equal(
|
|
t,
|
|
"ns1.example.=ok ns2.example.=ok "+
|
|
"ns3.example.=timeout ns4.example.=error",
|
|
describeStatuses(results),
|
|
)
|
|
}
|
|
|
|
// TestUnsanctionedStatusesRejectsWrongAnswers is the regression test
|
|
// for the defect this allowlist exists to prevent: a minority of
|
|
// nameservers answering WRONGLY while quorum keeps the suite green.
|
|
// nodata is the case that motivated it — it is a wrong answer, not
|
|
// silence, and it was previously banned by neither test.
|
|
func TestUnsanctionedStatusesRejectsWrongAnswers(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Four nameservers, three OK and one answering nodata: a
|
|
// quorum of three is satisfied and no NXDOMAIN is present, so
|
|
// the old blocklist assertions both passed on this input.
|
|
results := map[string]*resolver.NameserverResponse{
|
|
nsExample1: {
|
|
Nameserver: nsExample1,
|
|
Status: resolver.StatusOK,
|
|
},
|
|
nsExample2: {
|
|
Nameserver: nsExample2,
|
|
Status: resolver.StatusOK,
|
|
},
|
|
nsExample3: {
|
|
Nameserver: nsExample3,
|
|
Status: resolver.StatusOK,
|
|
},
|
|
nsExample4: {
|
|
Nameserver: nsExample4,
|
|
Status: resolver.StatusNoData,
|
|
},
|
|
}
|
|
|
|
assert.GreaterOrEqual(
|
|
t,
|
|
countStatus(results, resolver.StatusOK),
|
|
liveQuorum(len(results)),
|
|
)
|
|
assert.Zero(t, countStatus(results, resolver.StatusNXDomain))
|
|
|
|
// nodata is an ANSWER, so it never triggers a retry: nothing
|
|
// but the allowlist stands between it and a false green.
|
|
assert.Equal(t, len(results), answeredCount(results))
|
|
|
|
assert.Equal(
|
|
t,
|
|
[]string{nsExample4 + "=nodata"},
|
|
unsanctionedStatuses(
|
|
results,
|
|
resolver.StatusOK,
|
|
resolver.StatusTimeout,
|
|
resolver.StatusError,
|
|
),
|
|
"nodata must be reported as an unsanctioned status",
|
|
)
|
|
}
|
|
|
|
func TestUnsanctionedStatusesToleratesSilenceOnly(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
results := map[string]*resolver.NameserverResponse{
|
|
nsExample1: {
|
|
Nameserver: nsExample1,
|
|
Status: resolver.StatusNXDomain,
|
|
},
|
|
nsExample2: {
|
|
Nameserver: nsExample2,
|
|
Status: resolver.StatusTimeout,
|
|
},
|
|
nsExample3: {
|
|
Nameserver: nsExample3,
|
|
Status: resolver.StatusError,
|
|
},
|
|
}
|
|
|
|
allowed := []string{
|
|
resolver.StatusNXDomain,
|
|
resolver.StatusTimeout,
|
|
resolver.StatusError,
|
|
}
|
|
|
|
assert.Empty(
|
|
t,
|
|
unsanctionedStatuses(results, allowed...),
|
|
"timeout and error are non-answers and are tolerated",
|
|
)
|
|
|
|
// The same silent nameservers do not count towards a quorum.
|
|
assert.Equal(t, 1, answeredCount(results))
|
|
|
|
// An unknown status is treated as silence by answeredCount —
|
|
// so it retries and fails loudly — and is unsanctioned by the
|
|
// allowlist rather than quietly permitted.
|
|
const laterStatus = "some-status-added-later"
|
|
|
|
results[nsExample4] = &resolver.NameserverResponse{
|
|
Nameserver: nsExample4,
|
|
Status: laterStatus,
|
|
}
|
|
|
|
assert.Equal(t, 1, answeredCount(results))
|
|
assert.Equal(
|
|
t,
|
|
[]string{nsExample4 + "=" + laterStatus},
|
|
unsanctionedStatuses(results, allowed...),
|
|
)
|
|
}
|