storage: add speedtests table + Save/List/Delete + retention
Adds a Store-method API for the RecordSpeedTest RPC (Task 8) and the
retention sweep (Task 14):
- SaveSpeedTest / ListRecentSpeedTests / DeleteSpeedTestsBefore
- SpeedTest struct (no json tags; that decision deferred to Task 10)
Schema lives in migrations/002_speedtests.sql to match the existing
embed.FS pattern (rather than inline in Go). The migrate() loop now
iterates every *.sql in lexical order, so future migrations drop in
without code changes. Both files use CREATE ... IF NOT EXISTS so the
existing 001 migration remains idempotent on already-initialised DBs.
CHECK constraints enforce node IN ('fl','pl1') and non-negative metric
values at the DB level. country has no DB CHECK — validation belongs
to the gRPC handler in Task 8.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// newSpeedTestDB mirrors the open pattern from sqlite_test.go since the
|
||||
// storage package has no dedicated test-DB helper — every test calls
|
||||
// Open(t.TempDir()/...) directly.
|
||||
func newSpeedTestDB(t *testing.T) *Store {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
st, err := Open(filepath.Join(dir, "test.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("open: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { st.Close() })
|
||||
return st
|
||||
}
|
||||
|
||||
func TestSpeedTest_SaveAndList(t *testing.T) {
|
||||
db := newSpeedTestDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
now := time.Now().Unix()
|
||||
id, err := db.SaveSpeedTest(ctx, SpeedTest{
|
||||
Timestamp: now, Node: "fl", Country: "RU",
|
||||
DlMbps: 100.5, UlMbps: 20.1, PingMs: 35.2, JitterMs: 2.4,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SaveSpeedTest: %v", err)
|
||||
}
|
||||
if id <= 0 {
|
||||
t.Fatalf("expected positive id, got %d", id)
|
||||
}
|
||||
|
||||
rows, err := db.ListRecentSpeedTests(ctx, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("ListRecentSpeedTests: %v", err)
|
||||
}
|
||||
if len(rows) != 1 {
|
||||
t.Fatalf("expected 1 row, got %d", len(rows))
|
||||
}
|
||||
if rows[0].Country != "RU" || rows[0].Node != "fl" {
|
||||
t.Errorf("row mismatch: %+v", rows[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpeedTest_RetentionDelete(t *testing.T) {
|
||||
db := newSpeedTestDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
old := time.Now().Add(-31 * 24 * time.Hour).Unix()
|
||||
fresh := time.Now().Unix()
|
||||
|
||||
for _, ts := range []int64{old, fresh} {
|
||||
if _, err := db.SaveSpeedTest(ctx, SpeedTest{
|
||||
Timestamp: ts, Node: "fl", Country: "RU",
|
||||
DlMbps: 1, UlMbps: 1, PingMs: 1, JitterMs: 1,
|
||||
}); err != nil {
|
||||
t.Fatalf("save: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
deleted, err := db.DeleteSpeedTestsBefore(ctx, time.Now().Add(-30*24*time.Hour).Unix())
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteSpeedTestsBefore: %v", err)
|
||||
}
|
||||
if deleted != 1 {
|
||||
t.Errorf("expected 1 deleted row, got %d", deleted)
|
||||
}
|
||||
|
||||
rows, _ := db.ListRecentSpeedTests(ctx, 10)
|
||||
if len(rows) != 1 {
|
||||
t.Errorf("expected 1 remaining row, got %d", len(rows))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user