package main import ( "encoding/json" "encoding/xml" "io" "math" "net/http" "strings" "testing" "time" ) func TestNiceScale(t *testing.T) { cases := []struct{ max, step, top float64 }{ {0, 0.5, 1}, {1, 0.5, 1}, {7, 2, 8}, {123, 50, 150}, {0.37, 0.1, 0.4}, {1000, 500, 1000}, } for _, c := range cases { step, top := niceScale(c.max) if math.Abs(step-c.step) > 1e-9 || math.Abs(top-c.top) > 1e-9 { t.Errorf("niceScale(%v) = %v, %v; erwartet %v, %v", c.max, step, top, c.step, c.top) } } } func TestDateTicks(t *testing.T) { day := func(s string) time.Time { d, _ := time.Parse(time.DateOnly, s); return d } labels := func(from, to string) []string { days := dayRange(day(from), day(to)) var l []string for _, i := range dateTicks(days) { l = append(l, dateTickLabel(days[i])) } return strings.Fields(strings.Join(l, " ")) } if got := strings.Join(labels("2026-06-18", "2026-10-06"), " "); got != "Jul Aug Sep Okt" { t.Errorf("Monate: %q", got) } if got := strings.Join(labels("2025-12-13", "2027-12-01"), " "); got != "2026 Apr Jul Okt 2027 Apr Jul Okt" { t.Errorf("Quartale: %q", got) } if got := strings.Join(labels("2026-09-20", "2026-10-06"), " "); got != "21.09. 28.09. 05.10." { t.Errorf("Wochen: %q", got) } } // Ende-zu-Ende: Beiträge und Aufrufe direkt in die DB, Aggregate prüfen, // dann über die API ausliefern und jedes SVG als XML parsen. func TestStatsCharts(t *testing.T) { srv := newTestServer(t) at := func(s string) int64 { tm, err := time.ParseInLocation("2006-01-02 15:04", s, berlin) if err != nil { t.Fatal(err) } return tm.Unix() } // Mo 2026-09-28 bis Mi 2026-09-30; "heute" ist Do 2026-10-01. posts := []struct { created int64 reply int64 deleted int64 }{ {at("2026-09-28 09:30"), 0, 0}, {at("2026-09-28 09:45"), 1, 0}, {at("2026-09-28 23:30"), 1, 1}, // gelöscht, zählt trotzdem; UTC wäre es 21 Uhr {at("2026-09-30 00:10"), 0, 0}, {at("2026-10-01 08:00"), 0, 0}, // heute -> fehlt } for i, p := range posts { if _, err := db.Exec(`INSERT INTO entry (pid, uid, created_at, reply_to, deleted) VALUES ($1, 1, $2, $3, $4)`, 100+i, p.created, p.reply, p.deleted); err != nil { t.Fatal(err) } } for _, imp := range []struct { day, ip string hits int }{ {"2026-09-27", "10.1.0.0", 3}, {"2026-09-27", "10.2.0.0", 2}, {"2026-09-29", "10.1.0.0", 1}, {"2026-10-01", "10.1.0.0", 9}, // heute -> fehlt } { db.Exec(`INSERT INTO impression (day, anon_ip, path, hits) VALUES ($1, $2, '/', $3)`, imp.day, imp.ip, imp.hits) } now := time.Date(2026, 10, 1, 12, 0, 0, 0, berlin) ent, err := collectEntryStats(now) if err != nil { t.Fatal(err) } if len(ent.days) != 3 || ent.perDay[0] != 3 || ent.perDay[1] != 0 || ent.perDay[2] != 1 { t.Errorf("perDay %v über %d Tage", ent.perDay, len(ent.days)) } // Je ein Mo/Di/Mi im Zeitraum -> Ø = Summe. if ent.weekday[0] != [2]float64{1, 2} || ent.weekday[2] != [2]float64{1, 0} || ent.weekday[3] != [2]float64{} { t.Errorf("weekday %v", ent.weekday) } // Stunden: Summe / 3 Tage. if ent.hour[9] != [2]float64{1.0 / 3, 1.0 / 3} || ent.hour[23] != [2]float64{0, 1.0 / 3} || ent.hour[0] != [2]float64{1.0 / 3, 0} { t.Errorf("hour 0/9/23: %v %v %v", ent.hour[0], ent.hour[9], ent.hour[23]) } impr, err := collectImpressionStats(now) if err != nil { t.Fatal(err) } wantHits, wantNets := []float64{5, 0, 1, 0}, []float64{2, 0, 1, 0} if len(impr.days) != 4 { t.Fatalf("impression-Tage: %v", impr.days) } for i := range wantHits { if impr.hits[i] != wantHits[i] || impr.networks[i] != wantNets[i] { t.Errorf("Tag %d: %v/%v", i, impr.hits[i], impr.networks[i]) } } if err := renderCharts(now); err != nil { t.Fatal(err) } resp, err := http.Get(srv.URL + "/api/stats/charts") if err != nil { t.Fatal(err) } defer resp.Body.Close() var set StatsCharts if err := json.NewDecoder(resp.Body).Decode(&set); err != nil { t.Fatal(err) } for _, name := range []string{"impressions", "networks", "entries", "weekday", "hour"} { pair := set.Charts[name] if !strings.Contains(pair.Narrow, `viewBox="0 0 600 `) || !strings.Contains(pair.Wide, `viewBox="0 0 1200 `) { t.Errorf("%s: falsche Größen", name) } for _, s := range []string{pair.Narrow, pair.Wide} { if !strings.HasPrefix(s, " 1e-9 { t.Errorf("entries_per_day_30 = %v, erwartet 0,1", body.Avg) } }