//go:build !js && !wasip1
package obsduck
import (
"testing"
"time"
"craftdesign.group/quarry/internal/obs"
)
func hourlyHit(at time.Time, pattern, ua, account string) obs.Hit {
return obs.Hit{At: at, Pattern: pattern, UA: ua, Account: account, Addr: "203.0.113.7",
Network: "Hetzner", Country: "DE", Elapsed: 5 * time.Millisecond}
}
func TestClosedHoursLandAsOneRowPerKey(t *testing.T) {
s := openSink(t)
h := time.Now().UTC().Truncate(time.Hour).Add(-3 * time.Hour)
for i := 0; i < 4; i++ {
s.Hit(hourlyHit(h.Add(time.Duration(i)*time.Minute), "GET /r/{repo}", "Mozilla/5.0 Firefox/131.0", ""))
}
s.Hit(hourlyHit(h, "GET /r/{repo}", "Mozilla/5.0 Firefox/131.0", "dmestas"))
// The open hour is not written until it closes.
s.Hit(hourlyHit(time.Now(), "GET /r/{repo}", "Mozilla/5.0 Firefox/131.0", ""))
s.Flush()
if n := rows(t, s, "hit_hourly"); n != 2 {
t.Fatalf("FAIL: %d rows, want 2 (anonymous, signed-in)", n)
}
got, err := s.Hourly(obs.HourlyQuery{Pattern: "GET /r/{repo}"})
if err != nil {
t.Fatal(err)
}
var anon, signed int64
for _, c := range got {
if c.SignedIn {
signed += c.Requests
} else {
anon += c.Requests
}
if c.Agent != obs.AgentBrowser || c.Network != "Hetzner" || c.Country != "DE" || !c.Hour.Equal(h) {
t.Errorf("FAIL: row %+v", c)
}
}
if anon != 4 || signed != 1 {
t.Errorf("FAIL: anon %d signed %d, want 4 and 1", anon, signed)
}
if got[0].Elapsed <= 0 {
t.Errorf("FAIL: no handler time kept: %+v", got[0])
}
}
func TestCloseLandsTheOpenHourAndReadersSumRestartedHours(t *testing.T) {
s := openSink(t)
now := time.Now()
s.Hit(hourlyHit(now, "GET /", "curl/8", ""))
s.Flush()
s.flushRollup(true) // a restart mid-hour: the same key written twice
s.Hit(hourlyHit(now, "GET /", "curl/8", ""))
s.flushRollup(true)
if n := rows(t, s, "hit_hourly"); n != 2 {
t.Fatalf("FAIL: %d rows, want 2", n)
}
got, err := s.Hourly(obs.HourlyQuery{})
if err != nil || len(got) != 1 || got[0].Requests != 2 || got[0].Agent != obs.AgentAutomated {
t.Errorf("FAIL: %+v err %v", got, err)
}
}
func TestHourlyIsScopedByRouteAndTime(t *testing.T) {
s := openSink(t)
h := time.Now().UTC().Truncate(time.Hour).Add(-48 * time.Hour)
s.Hit(hourlyHit(h, "GET /a", "curl/8", ""))
s.Hit(hourlyHit(h.Add(24*time.Hour), "GET /a", "curl/8", ""))
s.Hit(hourlyHit(h, "GET /b", "curl/8", ""))
got, err := s.Hourly(obs.HourlyQuery{Pattern: "GET /a", Since: h.Add(time.Hour)})
if err != nil || len(got) != 1 || got[0].Pattern != "GET /a" || !got[0].Hour.Equal(h.Add(24*time.Hour)) {
t.Errorf("FAIL: %+v err %v", got, err)
}
}
func TestPruneKeepsHourlyRowsNinetyDays(t *testing.T) {
s := openSink(t)
now := time.Now().UTC()
s.Hit(hourlyHit(now.Add(-100*24*time.Hour), "GET /old", "curl/8", ""))
s.Hit(hourlyHit(now.Add(-60*24*time.Hour), "GET /mid", "curl/8", ""))
// A short window for the rest does not shorten this one.
if _, err := s.Prune(7 * 24 * time.Hour); err != nil {
t.Fatal(err)
}
got, _ := s.Hourly(obs.HourlyQuery{})
if len(got) != 1 || got[0].Pattern != "GET /mid" {
t.Errorf("FAIL: %+v", got)
}
}
func TestHourlyRowsHoldNoAddress(t *testing.T) {
s := openSink(t)
s.Hit(hourlyHit(time.Now().Add(-3*time.Hour), "GET /", "curl/8", ""))
s.Flush()
rs, err := s.db.Query(`SELECT column_name FROM information_schema.columns WHERE table_name = 'hit_hourly'`)
if err != nil {
t.Fatal(err)
}
defer rs.Close()
for rs.Next() {
var c string
rs.Scan(&c)
if c == "addr" || c == "ua" || c == "visitor" {
t.Errorf("FAIL: hit_hourly has a %q column", c)
}
}
}