package storage import ( "context" "path/filepath" "testing" "time" "github.com/bergabruh/stats-gateway/internal/probes" ) func TestInsertAndQuery(t *testing.T) { dir := t.TempDir() st, err := Open(filepath.Join(dir, "test.db")) if err != nil { t.Fatal(err) } defer st.Close() ctx := context.Background() r := &probes.Result{ HTTPLatencyMs: 42.5, TCPRttMs: 12.0, DNSResolveMs: 3.0, TLSHandshakeMs: 25.0, Timestamp: time.Now().UTC(), } if err := st.InsertSample(ctx, "fl", r); err != nil { t.Fatalf("insert: %v", err) } samples, err := st.QueryRecent(ctx, "fl", 10, time.Time{}) if err != nil { t.Fatalf("query: %v", err) } if len(samples) != 1 { t.Fatalf("expected 1 sample, got %d", len(samples)) } if samples[0].HTTPLatencyMs != 42.5 { t.Errorf("expected latency 42.5, got %f", samples[0].HTTPLatencyMs) } } func TestQueryRange(t *testing.T) { dir := t.TempDir() st, err := Open(filepath.Join(dir, "test.db")) if err != nil { t.Fatal(err) } defer st.Close() ctx := context.Background() now := time.Now().UTC().Truncate(time.Second) for i, dt := range []time.Duration{-2 * time.Hour, -1 * time.Hour, 0, 1 * time.Hour} { r := &probes.Result{HTTPLatencyMs: float64(10 + i), Timestamp: now.Add(dt)} if err := st.InsertSample(ctx, "fl", r); err != nil { t.Fatal(err) } } got, err := st.QueryRange(ctx, "fl", now.Add(-90*time.Minute), now.Add(30*time.Minute)) if err != nil { t.Fatal(err) } // expect 2 samples: -1h and 0 if len(got) != 2 { t.Fatalf("expected 2, got %d", len(got)) } } // TestQueryRange_SubsecondOrdering verifies fixed-width nanosecond format — // regression guard for RFC3339Nano trailing-zero stripping bug. func TestQueryRange_SubsecondOrdering(t *testing.T) { dir := t.TempDir() st, err := Open(filepath.Join(dir, "test.db")) if err != nil { t.Fatal(err) } defer st.Close() ctx := context.Background() base := time.Date(2026, 5, 5, 12, 0, 0, 0, time.UTC) // Two timestamps where lexical ordering of RFC3339Nano disagrees with chronology: // t1 = base + 100ms → RFC3339Nano formats as "...:00.1Z" // t2 = base + 100ms + 1ns → RFC3339Nano formats as "...:00.100000001Z" // Lexically ".1Z" > ".100000001Z" because 'Z' > '0', but t1 < t2. t1 := base.Add(100 * time.Millisecond) t2 := base.Add(100*time.Millisecond + time.Nanosecond) for _, ts := range []time.Time{t1, t2} { if err := st.InsertSample(ctx, "fl", &probes.Result{HTTPLatencyMs: 1.0, Timestamp: ts}); err != nil { t.Fatal(err) } } // Range starting just before t1 must include both samples. got, err := st.QueryRange(ctx, "fl", base.Add(50*time.Millisecond), base.Add(200*time.Millisecond)) if err != nil { t.Fatal(err) } if len(got) != 2 { t.Fatalf("expected 2 samples in [t-50ms, t+200ms), got %d — lexical ordering bug?", len(got)) } // Verify chronological ordering preserved on read. if !got[0].Timestamp.Before(got[1].Timestamp) { t.Errorf("expected got[0] < got[1] chronologically; got[0]=%v got[1]=%v", got[0].Timestamp, got[1].Timestamp) } }