package app import ( "bytes" "encoding/json" "strings" "sync" "testing" "github.com/miekg/dns" ) type lockedBuf struct { mu sync.Mutex b bytes.Buffer } func (l *lockedBuf) Write(p []byte) (int, error) { l.mu.Lock() defer l.mu.Unlock() return l.b.Write(p) } func (l *lockedBuf) lines() []map[string]any { l.mu.Lock() defer l.mu.Unlock() var out []map[string]any for _, ln := range strings.Split(strings.TrimSpace(l.b.String()), "\n") { if ln == "" { continue } var m map[string]any if err := json.Unmarshal([]byte(ln), &m); err != nil { panic("access log line is not JSON: " + ln) } out = append(out, m) } return out } // TestAccessLogDefaultsToBlockedAndFailed runs the whole resolver with the // shipped defaults: a blocked query leaves a line, an ordinary answer and a // cache hit leave none, and the line carries what a log search needs. func TestAccessLogDefaultsToBlockedAndFailed(t *testing.T) { out := &lockedBuf{} h := newHarness(t, nil, func(o *Options) { o.AccessLogOut = out }) h.query("www.example.test", dns.TypeA) // allowed h.query("www.example.test", dns.TypeA) // cached // A name that does not exist is an answer, not an error worth a line. if m := h.query("no-such-name.example.org", dns.TypeA); m.Rcode != dns.RcodeNameError { t.Fatalf("setup: no-such-name.example.org rcode %d, want NXDOMAIN", m.Rcode) } if m := h.query("ads.example.com", dns.TypeA); m.Rcode != dns.RcodeNameError { t.Fatalf("setup: ads.example.com rcode %d, want a block", m.Rcode) } h.app.logger.Flush() lines := out.lines() if len(lines) != 1 { t.Fatalf("%d access-log lines, want exactly the blocked query (not the allowed, cached or NXDOMAIN ones): %v", len(lines), lines) } l := lines[0] for k, want := range map[string]any{ "log": "access", "msg": "query", "name": "ads.example.com.", "action": "block", "list": "test-hosts", "qtype": "A", "client": "127.0.0.1", "protocol": "udp", } { if l[k] != want { t.Errorf("%s = %v, want %v (line %v)", k, l[k], want, l) } } }