Skip to content
quarry Artifact [5c0fd41b73]
Login

Artifact [5c0fd41b73]

Artifact 5c0fd41b73ed7b7803230192acd72dbd9d8f1bf9ade74e9bf2970a96b2b66170:


//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)
		}
	}
}