Compare commits
1 Commits
fix/99-ser
...
remove-dns
| Author | SHA1 | Date | |
|---|---|---|---|
| a535ae864b |
21
README.md
21
README.md
@@ -182,27 +182,6 @@ dnswatcher exposes a lightweight HTTP API for operational visibility:
|
|||||||
| `GET /api/v1/status` | Current monitoring state |
|
| `GET /api/v1/status` | Current monitoring state |
|
||||||
| `GET /metrics` | Prometheus metrics (optional) |
|
| `GET /metrics` | Prometheus metrics (optional) |
|
||||||
|
|
||||||
#### Server timeouts
|
|
||||||
|
|
||||||
The HTTP server sets all four socket-level timeouts. These are compile-time
|
|
||||||
constants in `internal/server/server.go`, not configurable via environment
|
|
||||||
variables.
|
|
||||||
|
|
||||||
| Timeout | Value | Purpose |
|
|
||||||
|---------------------|-------|-----------------------------------------------|
|
|
||||||
| `ReadHeaderTimeout` | 10s | Bounds the request header read (slowloris) |
|
|
||||||
| `ReadTimeout` | 15s | Bounds the whole request read, headers + body |
|
|
||||||
| `WriteTimeout` | 75s | Bounds handler execution plus response flush |
|
|
||||||
| `IdleTimeout` | 120s | Reaps idle keep-alive connections |
|
|
||||||
|
|
||||||
These are distinct from the 60s per-request handler budget applied by
|
|
||||||
`chimw.Timeout` in `internal/server/routes.go`, which cancels the request
|
|
||||||
context but does not touch the socket. `WriteTimeout` is deliberately
|
|
||||||
larger than that budget: the write deadline is armed once request headers
|
|
||||||
are read, so a smaller value would sever the connection before a handler
|
|
||||||
using its full budget could respond. `IdleTimeout` exceeds common
|
|
||||||
Prometheus scrape intervals so the scraper reuses its connection.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|||||||
15
TESTING.md
15
TESTING.md
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
## DNS Resolution Tests
|
## DNS Resolution Tests
|
||||||
|
|
||||||
All resolver tests **MUST** use live queries against real DNS servers.
|
All tests that involve DNS resolution — in every package, including
|
||||||
No mocking of the DNS client layer is permitted.
|
consumers of the resolver such as the watcher — **MUST** use live
|
||||||
|
queries against real DNS servers. No mocking, faking, or stubbing of
|
||||||
|
DNS at any layer is permitted.
|
||||||
|
|
||||||
### Rationale
|
### Rationale
|
||||||
|
|
||||||
@@ -12,6 +14,8 @@ the full delegation chain. Mocked responses cannot faithfully represent
|
|||||||
the variety of real-world DNS behavior (truncation, referrals, glue
|
the variety of real-world DNS behavior (truncation, referrals, glue
|
||||||
records, DNSSEC, varied response times, EDNS, etc.). Testing against
|
records, DNSSEC, varied response times, EDNS, etc.). Testing against
|
||||||
real servers ensures the resolver works correctly in production.
|
real servers ensures the resolver works correctly in production.
|
||||||
|
Robustness comes from handling real-world DNS behavior with tolerant
|
||||||
|
assertions and sensible timeouts, not from mocks.
|
||||||
|
|
||||||
### Constraints
|
### Constraints
|
||||||
|
|
||||||
@@ -24,11 +28,14 @@ real servers ensures the resolver works correctly in production.
|
|||||||
- Flaky failures from transient network issues are acceptable and
|
- Flaky failures from transient network issues are acceptable and
|
||||||
should be investigated as potential resolver bugs, not papered over
|
should be investigated as potential resolver bugs, not papered over
|
||||||
with mocks or skip flags
|
with mocks or skip flags
|
||||||
|
- Watcher change-detection tests seed a synthetic *previous state*
|
||||||
|
and compare it against fresh live lookups; the DNS side is never
|
||||||
|
faked
|
||||||
|
|
||||||
### What NOT to do
|
### What NOT to do
|
||||||
|
|
||||||
- **Do not mock `DNSClient`** for resolver tests (the mock constructor
|
- **Do not mock `DNSClient`**, the watcher's `DNSResolver` interface,
|
||||||
exists for unit-testing other packages that consume the resolver)
|
or any other DNS abstraction — in any package, for any reason
|
||||||
- **Do not add `-short` flags** to skip slow tests
|
- **Do not add `-short` flags** to skip slow tests
|
||||||
- **Do not increase `-timeout`** to hide hanging queries
|
- **Do not increase `-timeout`** to hide hanging queries
|
||||||
- **Do not modify linter configuration** to suppress findings
|
- **Do not modify linter configuration** to suppress findings
|
||||||
|
|||||||
32
TODO.md
32
TODO.md
@@ -14,7 +14,10 @@ pre-1.0. No git tags. Core resolver work in flight on feature/resolver
|
|||||||
(dirty: internal/resolver/resolver_test.go). Local checkout has diverged
|
(dirty: internal/resolver/resolver_test.go). Local checkout has diverged
|
||||||
from origin: origin/main is 8 commits ahead (watcher orchestrator,
|
from origin: origin/main is 8 commits ahead (watcher orchestrator,
|
||||||
unified TARGETS) and origin/feature/resolver already contains the full
|
unified TARGETS) and origin/feature/resolver already contains the full
|
||||||
iterative resolver implementation with hermetic mocked tests.
|
iterative resolver implementation. DNS mocking is banned in this repo
|
||||||
|
(see `TESTING.md`): all tests use live DNS only. The hermetic mocked
|
||||||
|
tests previously noted on `feature/resolver` are gone from its current
|
||||||
|
tip, which carries a live-DNS suite against `*.dns.sneak.cloud`.
|
||||||
|
|
||||||
# Next Step
|
# Next Step
|
||||||
|
|
||||||
@@ -25,14 +28,10 @@ confirm make check still passes.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
- 2026-08-09: `http.Server` now sets all four socket-level timeouts
|
- 2026-08-07: DNS mocking removed from the entire test suite; watcher
|
||||||
(`ReadTimeout` 15s, `ReadHeaderTimeout` 10s, `WriteTimeout` 75s,
|
tests now drive the real iterative resolver against live DNS and
|
||||||
`IdleTimeout` 120s) as named constants in `internal/server/server.go`,
|
`TESTING.md` bans DNS mocks in every package (`remove-dns-mocking`
|
||||||
closing the slowloris / unreaped-keep-alive exposure required by
|
branch)
|
||||||
`REPO_POLICIES.md` before 1.0; `WriteTimeout` is deliberately greater
|
|
||||||
than the 60s `chimw.Timeout` handler budget so that budget stays
|
|
||||||
reachable, and tests in `internal/server` pin both the non-zero
|
|
||||||
values and that relationship (#99)
|
|
||||||
- 2026-08-07: golangci-lint bumped to v2.12.2 (commit-pinned installs
|
- 2026-08-07: golangci-lint bumped to v2.12.2 (commit-pinned installs
|
||||||
in `Dockerfile` and `script/bootstrap`); `.golangci.yml` set to the
|
in `Dockerfile` and `script/bootstrap`); `.golangci.yml` set to the
|
||||||
org-standard v2-schema config used across the org's repos
|
org-standard v2-schema config used across the org's repos
|
||||||
@@ -44,7 +43,8 @@ confirm make check still passes.
|
|||||||
- 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints,
|
- 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints,
|
||||||
Makefile shims, README Entrypoints section
|
Makefile shims, README Entrypoints section
|
||||||
- 2026-02-20: iterative DNS resolver implemented; tests made hermetic
|
- 2026-02-20: iterative DNS resolver implemented; tests made hermetic
|
||||||
with mocked DNS (origin/feature/resolver, unmerged)
|
with mocked DNS (origin/feature/resolver, unmerged; superseded — DNS
|
||||||
|
mocking is banned, see `TESTING.md`)
|
||||||
- 2026-02-20: CI actions and go install refs pinned to commit SHAs;
|
- 2026-02-20: CI actions and go install refs pinned to commit SHAs;
|
||||||
Gitea Actions workflow for make check (origin/ci/make-check, unmerged)
|
Gitea Actions workflow for make check (origin/ci/make-check, unmerged)
|
||||||
- 2026-02-20: watcher monitoring orchestrator merged to main (#8)
|
- 2026-02-20: watcher monitoring orchestrator merged to main (#8)
|
||||||
@@ -71,8 +71,9 @@ Branch reconciliation:
|
|||||||
- Sync local checkout with origin: local main is 8 commits behind
|
- Sync local checkout with origin: local main is 8 commits behind
|
||||||
origin/main; local feature/resolver has diverged from
|
origin/main; local feature/resolver has diverged from
|
||||||
origin/feature/resolver, which already implements the resolver
|
origin/feature/resolver, which already implements the resolver
|
||||||
- Merge in-flight branches to main once green: feature/resolver,
|
- Merge in-flight branches to main once green: feature/resolver
|
||||||
ci/make-check, feature/portcheck-implementation,
|
(confirm its tests remain live-DNS — DNS mocking is banned, see
|
||||||
|
`TESTING.md`), ci/make-check, feature/portcheck-implementation,
|
||||||
feature/tlscheck-implementation
|
feature/tlscheck-implementation
|
||||||
|
|
||||||
Resolver (plan from untracked TODO.md; largely implemented on
|
Resolver (plan from untracked TODO.md; largely implemented on
|
||||||
@@ -156,6 +157,7 @@ Infrastructure notes (from untracked TODO.md):
|
|||||||
- Module path sneak.berlin/go/dnswatcher differs from the git.eeqj.de
|
- Module path sneak.berlin/go/dnswatcher differs from the git.eeqj.de
|
||||||
remote intentionally; do not "fix" it
|
remote intentionally; do not "fix" it
|
||||||
- Dependencies: github.com/miekg/dns, golang.org/x/net/publicsuffix
|
- Dependencies: github.com/miekg/dns, golang.org/x/net/publicsuffix
|
||||||
- Resolver tests originally used live DNS against *.dns.sneak.cloud
|
- Resolver tests originally used live DNS against `*.dns.sneak.cloud`
|
||||||
(required records documented in the test file header); origin now has
|
(required records documented in the test file header); `main` now
|
||||||
mocked hermetic tests, keep them hermetic
|
tests against live public DNS. DNS mocking is banned (see
|
||||||
|
`TESTING.md`); never reintroduce hermetic mocked DNS tests
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DNSClient abstracts DNS wire-protocol exchanges so the resolver
|
// DNSClient abstracts DNS wire-protocol exchanges over a single
|
||||||
// can be tested without hitting real nameservers.
|
// transport, letting the resolver switch between UDP and TCP.
|
||||||
type DNSClient interface {
|
type DNSClient interface {
|
||||||
ExchangeContext(
|
ExchangeContext(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
|||||||
@@ -67,17 +67,4 @@ func NewFromLogger(log *slog.Logger) *Resolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFromLoggerWithClient creates a Resolver with a custom DNS
|
|
||||||
// client, useful for testing with mock DNS responses.
|
|
||||||
func NewFromLoggerWithClient(
|
|
||||||
log *slog.Logger,
|
|
||||||
client DNSClient,
|
|
||||||
) *Resolver {
|
|
||||||
return &Resolver{
|
|
||||||
log: log,
|
|
||||||
client: client,
|
|
||||||
tcp: client,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method implementations are in iterative.go.
|
// Method implementations are in iterative.go.
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
@@ -624,58 +623,41 @@ func TestQueryAllNameservers_ContextCanceled(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
// Timeout tests
|
// Unreachable nameserver tests
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
func TestQueryNameserverIP_Timeout(t *testing.T) {
|
func TestQueryNameserverIP_UnreachableServer(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
log := slog.New(slog.NewTextHandler(
|
r := newTestResolver(t)
|
||||||
os.Stderr,
|
|
||||||
&slog.HandlerOptions{Level: slog.LevelDebug},
|
|
||||||
))
|
|
||||||
|
|
||||||
r := resolver.NewFromLoggerWithClient(
|
|
||||||
log, &timeoutClient{},
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(
|
ctx, cancel := context.WithTimeout(
|
||||||
context.Background(), 10*time.Second,
|
context.Background(), 10*time.Second,
|
||||||
)
|
)
|
||||||
t.Cleanup(cancel)
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
// Query any IP — the client always returns a timeout error.
|
// 192.0.2.1 is an RFC 5737 documentation address: no
|
||||||
|
// nameserver can exist there. Depending on the network
|
||||||
|
// path the queries either time out (silent drop) or fail
|
||||||
|
// fast (ICMP unreachable), so accept any non-OK status;
|
||||||
|
// the resolver must return a classified response with no
|
||||||
|
// records rather than an error or a hang.
|
||||||
resp, err := r.QueryNameserverIP(
|
resp, err := r.QueryNameserverIP(
|
||||||
ctx, "unreachable.test.", "192.0.2.1",
|
ctx, "unreachable.test.", "192.0.2.1",
|
||||||
"example.com",
|
"example.com",
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, resolver.StatusTimeout, resp.Status)
|
assert.NotEqual(t, resolver.StatusOK, resp.Status)
|
||||||
assert.NotEmpty(t, resp.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// timeoutClient simulates DNS timeout errors for testing.
|
totalRecords := 0
|
||||||
type timeoutClient struct{}
|
for _, values := range resp.Records {
|
||||||
|
totalRecords += len(values)
|
||||||
func (c *timeoutClient) ExchangeContext(
|
|
||||||
_ context.Context,
|
|
||||||
_ *dns.Msg,
|
|
||||||
_ string,
|
|
||||||
) (*dns.Msg, time.Duration, error) {
|
|
||||||
return nil, 0, &net.OpError{
|
|
||||||
Op: "read",
|
|
||||||
Net: "udp",
|
|
||||||
Err: &timeoutError{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert.Zero(t, totalRecords)
|
||||||
}
|
}
|
||||||
|
|
||||||
type timeoutError struct{}
|
|
||||||
|
|
||||||
func (e *timeoutError) Error() string { return "i/o timeout" }
|
|
||||||
func (e *timeoutError) Timeout() bool { return true }
|
|
||||||
func (e *timeoutError) Temporary() bool { return true }
|
|
||||||
|
|
||||||
func TestResolveIPAddresses_ContextCanceled(t *testing.T) {
|
func TestResolveIPAddresses_ContextCanceled(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewHTTPServer exports newHTTPServer for testing.
|
|
||||||
func NewHTTPServer(
|
|
||||||
listenAddr string,
|
|
||||||
handler http.Handler,
|
|
||||||
) *http.Server {
|
|
||||||
return newHTTPServer(listenAddr, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequestTimeout exports the handler execution budget applied by
|
|
||||||
// chimw.Timeout in SetupRoutes, so tests can assert the relationship
|
|
||||||
// between it and the server's WriteTimeout.
|
|
||||||
const RequestTimeout time.Duration = requestTimeout
|
|
||||||
@@ -33,52 +33,8 @@ type Params struct {
|
|||||||
// shutdownTimeout is how long to wait for graceful shutdown.
|
// shutdownTimeout is how long to wait for graceful shutdown.
|
||||||
const shutdownTimeout = 30 * time.Second
|
const shutdownTimeout = 30 * time.Second
|
||||||
|
|
||||||
// Socket-level timeouts for the HTTP server.
|
// readHeaderTimeout is the max duration for reading request headers.
|
||||||
//
|
const readHeaderTimeout = 10 * time.Second
|
||||||
// These bound time spent on the connection itself and are a distinct
|
|
||||||
// control from the per-request handler budget enforced by
|
|
||||||
// chimw.Timeout(requestTimeout) in routes.go: that one cancels the
|
|
||||||
// request context after requestTimeout but never touches the socket,
|
|
||||||
// so without the values below a peer can hold a connection open
|
|
||||||
// forever (slowloris, unreaped keep-alives).
|
|
||||||
//
|
|
||||||
// The one hard constraint between the two controls is
|
|
||||||
// writeTimeout > requestTimeout. net/http arms the write deadline
|
|
||||||
// once the request headers have been read, so on a plaintext
|
|
||||||
// connection it covers handler execution AND the response flush. If
|
|
||||||
// writeTimeout were <= requestTimeout the server would sever the
|
|
||||||
// connection before a handler that legitimately consumed its full
|
|
||||||
// budget could emit anything, making the 60s budget unreachable in
|
|
||||||
// practice. The margin between them is the response-flush allowance.
|
|
||||||
//
|
|
||||||
// The only clients of this service are browsers loading the dashboard
|
|
||||||
// and a Prometheus scraper; the values are sized for those.
|
|
||||||
const (
|
|
||||||
// readHeaderTimeout is the max duration for reading request
|
|
||||||
// headers.
|
|
||||||
readHeaderTimeout = 10 * time.Second
|
|
||||||
|
|
||||||
// readTimeout bounds reading the entire request, headers plus
|
|
||||||
// body. Every route here is a GET with no body, so this only
|
|
||||||
// ever needs to cover headers; the extra 5s over
|
|
||||||
// readHeaderTimeout is slack, not a real allowance, and keeps a
|
|
||||||
// body dribbled one byte at a time from holding the read side
|
|
||||||
// open indefinitely.
|
|
||||||
readTimeout = 15 * time.Second
|
|
||||||
|
|
||||||
// writeTimeout must exceed the requestTimeout handler budget
|
|
||||||
// (60s) per the note above. The 15s difference is the allowance
|
|
||||||
// for flushing a completed response to a slow client.
|
|
||||||
writeTimeout = 75 * time.Second
|
|
||||||
|
|
||||||
// idleTimeout reaps keep-alive connections between requests. It
|
|
||||||
// is deliberately longer than the common Prometheus scrape
|
|
||||||
// intervals (15s/30s/60s) so the scraper reuses its connection
|
|
||||||
// rather than reconnecting every cycle, while a browser tab
|
|
||||||
// left open on the dashboard stops occupying a connection
|
|
||||||
// within two minutes of going quiet.
|
|
||||||
idleTimeout = 120 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
// Server is the HTTP server.
|
// Server is the HTTP server.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
@@ -120,29 +76,16 @@ func New(
|
|||||||
return srv, nil
|
return srv, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// newHTTPServer builds the listening http.Server with every
|
|
||||||
// socket-level timeout set. All four are set deliberately: a zero
|
|
||||||
// value in net/http means "no limit", not "some default".
|
|
||||||
func newHTTPServer(
|
|
||||||
listenAddr string,
|
|
||||||
handler http.Handler,
|
|
||||||
) *http.Server {
|
|
||||||
return &http.Server{
|
|
||||||
Addr: listenAddr,
|
|
||||||
Handler: handler,
|
|
||||||
ReadTimeout: readTimeout,
|
|
||||||
ReadHeaderTimeout: readHeaderTimeout,
|
|
||||||
WriteTimeout: writeTimeout,
|
|
||||||
IdleTimeout: idleTimeout,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run starts the HTTP server.
|
// Run starts the HTTP server.
|
||||||
func (s *Server) Run() {
|
func (s *Server) Run() {
|
||||||
s.SetupRoutes()
|
s.SetupRoutes()
|
||||||
|
|
||||||
listenAddr := fmt.Sprintf(":%d", s.port)
|
listenAddr := fmt.Sprintf(":%d", s.port)
|
||||||
s.httpServer = newHTTPServer(listenAddr, s)
|
s.httpServer = &http.Server{
|
||||||
|
Addr: listenAddr,
|
||||||
|
Handler: s,
|
||||||
|
ReadHeaderTimeout: readHeaderTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
s.log.Info("http server starting", "addr", listenAddr)
|
s.log.Info("http server starting", "addr", listenAddr)
|
||||||
|
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
package server_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"sneak.berlin/go/dnswatcher/internal/server"
|
|
||||||
)
|
|
||||||
|
|
||||||
// noopHandler stands in for the router; newHTTPServer only stores it.
|
|
||||||
func noopHandler() http.Handler {
|
|
||||||
return http.HandlerFunc(
|
|
||||||
func(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestHTTPServerTimeoutsAreSet asserts that every socket-level
|
|
||||||
// timeout is configured. A zero value in net/http means "no limit",
|
|
||||||
// so a refactor that silently drops one of these reintroduces the
|
|
||||||
// slowloris / unreaped-keep-alive exposure this guards against.
|
|
||||||
//
|
|
||||||
// The assertions are on the configured field values only; nothing
|
|
||||||
// here measures elapsed time, so the test cannot flake on timing.
|
|
||||||
func TestHTTPServerTimeoutsAreSet(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
srv := server.NewHTTPServer(":8080", noopHandler())
|
|
||||||
|
|
||||||
if srv.ReadTimeout <= 0 {
|
|
||||||
t.Errorf(
|
|
||||||
"ReadTimeout must be non-zero, got %v",
|
|
||||||
srv.ReadTimeout,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if srv.ReadHeaderTimeout <= 0 {
|
|
||||||
t.Errorf(
|
|
||||||
"ReadHeaderTimeout must be non-zero, got %v",
|
|
||||||
srv.ReadHeaderTimeout,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if srv.WriteTimeout <= 0 {
|
|
||||||
t.Errorf(
|
|
||||||
"WriteTimeout must be non-zero, got %v",
|
|
||||||
srv.WriteTimeout,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if srv.IdleTimeout <= 0 {
|
|
||||||
t.Errorf(
|
|
||||||
"IdleTimeout must be non-zero, got %v",
|
|
||||||
srv.IdleTimeout,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestWriteTimeoutExceedsHandlerBudget pins the one relationship the
|
|
||||||
// values must satisfy. net/http arms the write deadline once request
|
|
||||||
// headers are read, so it covers handler execution plus the response
|
|
||||||
// flush. If WriteTimeout were not greater than the chimw.Timeout
|
|
||||||
// handler budget, the connection would be severed before a handler
|
|
||||||
// that used its full budget could respond, making that budget
|
|
||||||
// unreachable.
|
|
||||||
func TestWriteTimeoutExceedsHandlerBudget(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
srv := server.NewHTTPServer(":8080", noopHandler())
|
|
||||||
|
|
||||||
if srv.WriteTimeout <= server.RequestTimeout {
|
|
||||||
t.Errorf(
|
|
||||||
"WriteTimeout (%v) must exceed handler budget (%v)",
|
|
||||||
srv.WriteTimeout,
|
|
||||||
server.RequestTimeout,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestReadTimeoutCoversHeaderTimeout asserts the read deadline for
|
|
||||||
// the whole request is at least as long as the header-only deadline;
|
|
||||||
// a smaller ReadTimeout would make ReadHeaderTimeout unreachable.
|
|
||||||
func TestReadTimeoutCoversHeaderTimeout(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
srv := server.NewHTTPServer(":8080", noopHandler())
|
|
||||||
|
|
||||||
if srv.ReadTimeout < srv.ReadHeaderTimeout {
|
|
||||||
t.Errorf(
|
|
||||||
"ReadTimeout (%v) must be >= ReadHeaderTimeout (%v)",
|
|
||||||
srv.ReadTimeout,
|
|
||||||
srv.ReadHeaderTimeout,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestHTTPServerAddrAndHandler covers the rest of the constructor so
|
|
||||||
// a future edit cannot drop the listen address or the handler.
|
|
||||||
func TestHTTPServerAddrAndHandler(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
srv := server.NewHTTPServer(":9999", noopHandler())
|
|
||||||
|
|
||||||
if srv.Addr != ":9999" {
|
|
||||||
t.Errorf("Addr = %q, want %q", srv.Addr, ":9999")
|
|
||||||
}
|
|
||||||
|
|
||||||
if srv.Handler == nil {
|
|
||||||
t.Error("Handler must not be nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user