package stats import ( "context" "fmt" "io" "log/slog" "path/filepath" "testing" "time" ) func TestExplore(t *testing.T) { ctx := context.Background() loc := time.UTC store, err := OpenStore(filepath.Join(t.TempDir(), "stats.db")) if err != nil { t.Fatal(err) } defer store.Close() clk := &clock{time.Date(2026, 10, 13, 12, 0, 30, 0, loc)} opts := Options{QueryLog: true, LogFile: "unused", StatisticsChannel: "unused", TopN: 10, RetainMinute: time.Hour, RetainHour: time.Hour, RetainDay: time.Hour, RetainQueries: 7 * 24 * time.Hour, Location: loc} c := NewCollector(store, opts, func() map[string][]string { return nil }, slog.New(slog.NewTextHandler(io.Discard, nil))) c.now = clk.now at := clk.t.Add(-10 * time.Minute) // The phone: lookups, an NXDOMAIN, a blocked tracker. c.HandleLine(logLine(at, "10.0.0.5", "internal", "www.adbraun.lol", "A", "NOERROR")) c.HandleLine(logLine(at, "10.0.0.5", "internal", "nas.adbraun.lol", "AAAA", "NOERROR")) c.HandleLine(logLine(at, "10.0.0.5", "internal", "typo.adbraun.lol", "A", "NXDOMAIN")) c.HandleLine(fmt.Sprintf("%s client @0x7f0000000000 10.0.0.5#5353 (t.tracker.example): view internal: rpz QNAME NXDOMAIN rewrite t.tracker.example/A/IN via t.tracker.example.hagezi.blocklist.jarl.invalid\n", at.Format(logTimeLayout))) c.HandleLine(logLine(at, "10.0.0.5", "internal", "t.tracker.example", "A", "NXDOMAIN")) // A scanner in the external view, NXDOMAIN twice. for i := 0; i < 2; i++ { c.HandleLine(logLine(at, "198.51.100.7", "external", "nope.adbraun.lol", "A", "NXDOMAIN")) } c.flush(ctx, true) q := &Querier{Store: store, Opts: opts, Now: clk.now} ex, err := q.Explore(ctx, ExploreFilter{View: "external", RCode: "NXDOMAIN"}, 10, 50) if err != nil { t.Fatal(err) } if ex.Total != 2 || len(ex.By["client"]) != 1 || ex.By["client"][0].Key != "198.51.100.7" || ex.By["view"] != nil { t.Errorf("external NXDOMAIN: %+v", ex) } // What else does the phone do? ex, err = q.Explore(ctx, ExploreFilter{Client: "10.0.0.5"}, 10, 50) if err != nil { t.Fatal(err) } if ex.Total != 4 || ex.Blocked != 1 || len(ex.Recent) != 4 { t.Errorf("phone: total %d blocked %d recent %d", ex.Total, ex.Blocked, len(ex.Recent)) } if b := ex.By["blocked"]; len(b) != 1 || b[0].Key != "hagezi" || b[0].Count != 1 { t.Errorf("blocked by %+v", b) } if r := ex.By["rcode"]; len(r) != 2 || r[0].Key != "NOERROR" || r[0].Count != 2 { t.Errorf("rcode %+v", r) } var sum int64 for _, p := range ex.Series { sum += p.Count } if sum != 4 || ex.Step != 60*int64(24*60/100) { t.Errorf("series sum %d step %d", sum, ex.Step) } // A domain and what's below it. ex, _ = q.Explore(ctx, ExploreFilter{Domain: "adbraun.lol"}, 10, 50) if ex.Total != 5 { t.Errorf("domain adbraun.lol: %d", ex.Total) } ex, _ = q.Explore(ctx, ExploreFilter{Name: "nas.adbraun.lol", Type: "aaaa"}, 10, 50) if ex.Total != 1 || ex.Recent[0].Client != "10.0.0.5" { t.Errorf("nas AAAA: %+v", ex) } ex, _ = q.Explore(ctx, ExploreFilter{Blocked: "no", Client: "10.0.0.5"}, 10, 50) if ex.Total != 3 { t.Errorf("not blocked: %d", ex.Total) } // Limited to a view (the Statistics page's view switches). ex, _ = q.ForViews([]string{"internal"}).Explore(ctx, ExploreFilter{RCode: "NXDOMAIN"}, 10, 50) if ex.Total != 2 { t.Errorf("internal NXDOMAIN: %d", ex.Total) } // Pruning: past the retention they go. clk.t = clk.t.Add(8 * 24 * time.Hour) c.prune(ctx) if n, _ := store.QueryLogSize(ctx); n != 0 { t.Errorf("after pruning: %d rows", n) } // Off: an error that says how to turn it on. q.Opts.RetainQueries = 0 if _, err := q.Explore(ctx, ExploreFilter{}, 10, 50); err != ErrNoQueryLog { t.Errorf("off: %v", err) } }