mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-07 11:54:59 -04:00
567 lines
14 KiB
Go
567 lines
14 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func openTestStore(t *testing.T) *Store {
|
|
t.Helper()
|
|
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "test.db")
|
|
|
|
s, err := Open(path)
|
|
if err != nil {
|
|
t.Fatalf("Open() error = %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = s.Close() })
|
|
return s
|
|
}
|
|
|
|
func TestOpenAppliesMigrations(t *testing.T) {
|
|
s := openTestStore(t)
|
|
|
|
version, err := s.SchemaVersion()
|
|
if err != nil {
|
|
t.Fatalf("SchemaVersion() error = %v", err)
|
|
}
|
|
if version != 2 {
|
|
t.Fatalf("SchemaVersion() = %d, want 2", version)
|
|
}
|
|
|
|
tables := []string{
|
|
"schema_migrations",
|
|
"raw_payloads",
|
|
"ingestion_runs",
|
|
"meetings",
|
|
"sessions",
|
|
"drivers",
|
|
"session_drivers",
|
|
"session_results",
|
|
"starting_grid",
|
|
"stints",
|
|
"pit_stops",
|
|
"positions",
|
|
"race_control",
|
|
"weather",
|
|
"laps",
|
|
}
|
|
for _, table := range tables {
|
|
var name string
|
|
err := s.db.QueryRow(
|
|
`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`,
|
|
table,
|
|
).Scan(&name)
|
|
if err != nil {
|
|
t.Fatalf("table %q missing: %v", table, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMigrationsAreIdempotent(t *testing.T) {
|
|
s := openTestStore(t)
|
|
|
|
if err := s.applyMigrations(); err != nil {
|
|
t.Fatalf("second applyMigrations() error = %v", err)
|
|
}
|
|
if err := s.applyMigrations(); err != nil {
|
|
t.Fatalf("third applyMigrations() error = %v", err)
|
|
}
|
|
|
|
var count int
|
|
if err := s.db.QueryRow(`SELECT COUNT(*) FROM schema_migrations WHERE version = 1`).Scan(&count); err != nil {
|
|
t.Fatalf("count schema_migrations: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("schema_migrations count = %d, want 1", count)
|
|
}
|
|
if err := s.db.QueryRow(`SELECT COUNT(*) FROM schema_migrations WHERE version = 2`).Scan(&count); err != nil {
|
|
t.Fatalf("count schema_migrations v2: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("schema_migrations v2 count = %d, want 1", count)
|
|
}
|
|
}
|
|
|
|
func TestRawPayloadInsertAndRead(t *testing.T) {
|
|
s := openTestStore(t)
|
|
|
|
meetingKey := 1229
|
|
sessionKey := 9472
|
|
fetchedAt := time.Unix(1710000000, 0).UTC()
|
|
|
|
payload := RawPayload{
|
|
Source: "openf1",
|
|
Endpoint: "session_result",
|
|
RequestKey: "session_key=9472",
|
|
MeetingKey: &meetingKey,
|
|
SessionKey: &sessionKey,
|
|
Payload: `[{"position":1,"driver_number":1}]`,
|
|
PayloadHash: "abc123",
|
|
FetchedAt: fetchedAt,
|
|
ProvenanceJSON: `{"status":"ok","http_status":200}`,
|
|
}
|
|
|
|
id, inserted, err := s.InsertRawPayload(payload)
|
|
if err != nil {
|
|
t.Fatalf("InsertRawPayload() error = %v", err)
|
|
}
|
|
if !inserted {
|
|
t.Fatal("InsertRawPayload() inserted = false, want true")
|
|
}
|
|
if id <= 0 {
|
|
t.Fatalf("InsertRawPayload() id = %d, want > 0", id)
|
|
}
|
|
|
|
got, err := s.GetRawPayload(id)
|
|
if err != nil {
|
|
t.Fatalf("GetRawPayload() error = %v", err)
|
|
}
|
|
|
|
if got.Source != payload.Source ||
|
|
got.Endpoint != payload.Endpoint ||
|
|
got.RequestKey != payload.RequestKey ||
|
|
got.Payload != payload.Payload ||
|
|
got.PayloadHash != payload.PayloadHash ||
|
|
got.ProvenanceJSON != payload.ProvenanceJSON {
|
|
t.Fatalf("GetRawPayload() = %+v, want provenance preserved", got)
|
|
}
|
|
if got.MeetingKey == nil || *got.MeetingKey != meetingKey {
|
|
t.Fatalf("MeetingKey = %v, want %d", got.MeetingKey, meetingKey)
|
|
}
|
|
if got.SessionKey == nil || *got.SessionKey != sessionKey {
|
|
t.Fatalf("SessionKey = %v, want %d", got.SessionKey, sessionKey)
|
|
}
|
|
if !got.FetchedAt.Equal(fetchedAt) {
|
|
t.Fatalf("FetchedAt = %v, want %v", got.FetchedAt, fetchedAt)
|
|
}
|
|
|
|
rows, err := s.ListRawPayloadsBySession(sessionKey)
|
|
if err != nil {
|
|
t.Fatalf("ListRawPayloadsBySession() error = %v", err)
|
|
}
|
|
if len(rows) != 1 || rows[0].ID != id {
|
|
t.Fatalf("ListRawPayloadsBySession() = %+v, want one row id=%d", rows, id)
|
|
}
|
|
}
|
|
|
|
func TestRawPayloadDuplicateIsIdempotent(t *testing.T) {
|
|
s := openTestStore(t)
|
|
|
|
payload := RawPayload{
|
|
Source: "openf1",
|
|
Endpoint: "meetings",
|
|
RequestKey: "year=2025",
|
|
Payload: `[{"meeting_key":1229}]`,
|
|
PayloadHash: "dup-hash",
|
|
}
|
|
|
|
firstID, inserted, err := s.InsertRawPayload(payload)
|
|
if err != nil {
|
|
t.Fatalf("first InsertRawPayload() error = %v", err)
|
|
}
|
|
if !inserted {
|
|
t.Fatal("first insert should succeed")
|
|
}
|
|
|
|
secondID, inserted, err := s.InsertRawPayload(payload)
|
|
if err != nil {
|
|
t.Fatalf("second InsertRawPayload() error = %v", err)
|
|
}
|
|
if inserted {
|
|
t.Fatal("duplicate insert should not create a new row")
|
|
}
|
|
if secondID != firstID {
|
|
t.Fatalf("duplicate id = %d, want %d", secondID, firstID)
|
|
}
|
|
|
|
var count int
|
|
if err := s.db.QueryRow(`SELECT COUNT(*) FROM raw_payloads`).Scan(&count); err != nil {
|
|
t.Fatalf("count raw_payloads: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("raw_payloads count = %d, want 1", count)
|
|
}
|
|
}
|
|
|
|
func TestMeetingSessionDriverUpsertsAreIdempotent(t *testing.T) {
|
|
s := openTestStore(t)
|
|
|
|
meeting := Meeting{
|
|
MeetingKey: 1229,
|
|
MeetingName: "Monaco",
|
|
MeetingOfficialName: "FORMULA 1 GRAND PRIX DE MONACO 2025",
|
|
Location: "Monaco",
|
|
CountryCode: "MON",
|
|
CountryName: "Monaco",
|
|
CircuitKey: 10,
|
|
CircuitShortName: "Monaco",
|
|
Year: 2025,
|
|
DateStart: "2025-05-23T00:00:00+00:00",
|
|
DateEnd: "2025-05-25T00:00:00+00:00",
|
|
}
|
|
updatedMeeting := meeting
|
|
updatedMeeting.MeetingName = "Monaco GP"
|
|
|
|
for i := 0; i < 2; i++ {
|
|
m := meeting
|
|
if i == 1 {
|
|
m = updatedMeeting
|
|
}
|
|
if err := s.UpsertMeeting(m); err != nil {
|
|
t.Fatalf("UpsertMeeting(%d) error = %v", i, err)
|
|
}
|
|
}
|
|
|
|
gotMeeting, err := s.GetMeeting(meeting.MeetingKey)
|
|
if err != nil {
|
|
t.Fatalf("GetMeeting() error = %v", err)
|
|
}
|
|
if gotMeeting.MeetingName != updatedMeeting.MeetingName {
|
|
t.Fatalf("MeetingName = %q, want %q", gotMeeting.MeetingName, updatedMeeting.MeetingName)
|
|
}
|
|
|
|
var meetingCount int
|
|
if err := s.db.QueryRow(`SELECT COUNT(*) FROM meetings`).Scan(&meetingCount); err != nil {
|
|
t.Fatalf("count meetings: %v", err)
|
|
}
|
|
if meetingCount != 1 {
|
|
t.Fatalf("meetings count = %d, want 1", meetingCount)
|
|
}
|
|
|
|
session := Session{
|
|
SessionKey: 9472,
|
|
MeetingKey: meeting.MeetingKey,
|
|
SessionName: "Race",
|
|
SessionType: "Race",
|
|
CircuitKey: 10,
|
|
DateStart: "2025-05-25T13:00:00+00:00",
|
|
}
|
|
updatedSession := session
|
|
updatedSession.DateEnd = "2025-05-25T15:00:00+00:00"
|
|
|
|
for i := 0; i < 2; i++ {
|
|
sess := session
|
|
if i == 1 {
|
|
sess = updatedSession
|
|
}
|
|
if err := s.UpsertSession(sess); err != nil {
|
|
t.Fatalf("UpsertSession(%d) error = %v", i, err)
|
|
}
|
|
}
|
|
|
|
gotSession, err := s.GetSession(session.SessionKey)
|
|
if err != nil {
|
|
t.Fatalf("GetSession() error = %v", err)
|
|
}
|
|
if gotSession.DateEnd != updatedSession.DateEnd {
|
|
t.Fatalf("DateEnd = %q, want %q", gotSession.DateEnd, updatedSession.DateEnd)
|
|
}
|
|
|
|
driver := Driver{
|
|
DriverNumber: 1,
|
|
FullName: "Max Verstappen",
|
|
NameAcronym: "VER",
|
|
TeamName: "Red Bull Racing",
|
|
TeamColour: "3671C6",
|
|
}
|
|
updatedDriver := driver
|
|
updatedDriver.TeamName = "Oracle Red Bull Racing"
|
|
|
|
for i := 0; i < 2; i++ {
|
|
d := driver
|
|
if i == 1 {
|
|
d = updatedDriver
|
|
}
|
|
if err := s.UpsertDriver(d); err != nil {
|
|
t.Fatalf("UpsertDriver(%d) error = %v", i, err)
|
|
}
|
|
}
|
|
|
|
gotDriver, err := s.GetDriver(driver.DriverNumber)
|
|
if err != nil {
|
|
t.Fatalf("GetDriver() error = %v", err)
|
|
}
|
|
if gotDriver.TeamName != updatedDriver.TeamName {
|
|
t.Fatalf("TeamName = %q, want %q", gotDriver.TeamName, updatedDriver.TeamName)
|
|
}
|
|
|
|
sessionDriver := SessionDriver{
|
|
SessionKey: session.SessionKey,
|
|
DriverNumber: driver.DriverNumber,
|
|
MeetingKey: meeting.MeetingKey,
|
|
TeamName: "Red Bull Racing",
|
|
TeamColour: "3671C6",
|
|
}
|
|
if err := s.UpsertSessionDriver(sessionDriver); err != nil {
|
|
t.Fatalf("UpsertSessionDriver() error = %v", err)
|
|
}
|
|
if err := s.UpsertSessionDriver(sessionDriver); err != nil {
|
|
t.Fatalf("second UpsertSessionDriver() error = %v", err)
|
|
}
|
|
|
|
meetings, err := s.ListMeetingsByYear(2025)
|
|
if err != nil {
|
|
t.Fatalf("ListMeetingsByYear() error = %v", err)
|
|
}
|
|
if len(meetings) != 1 {
|
|
t.Fatalf("ListMeetingsByYear() len = %d, want 1", len(meetings))
|
|
}
|
|
|
|
sessions, err := s.ListSessionsByMeeting(meeting.MeetingKey)
|
|
if err != nil {
|
|
t.Fatalf("ListSessionsByMeeting() error = %v", err)
|
|
}
|
|
if len(sessions) != 1 {
|
|
t.Fatalf("ListSessionsByMeeting() len = %d, want 1", len(sessions))
|
|
}
|
|
|
|
sessionDrivers, err := s.ListSessionDrivers(session.SessionKey)
|
|
if err != nil {
|
|
t.Fatalf("ListSessionDrivers() error = %v", err)
|
|
}
|
|
if len(sessionDrivers) != 1 {
|
|
t.Fatalf("ListSessionDrivers() len = %d, want 1", len(sessionDrivers))
|
|
}
|
|
}
|
|
|
|
func TestSessionResultAndStartingGridUpsertRead(t *testing.T) {
|
|
s := openTestStore(t)
|
|
|
|
meetingKey := 1229
|
|
sessionKey := 9472
|
|
|
|
if err := s.UpsertMeeting(Meeting{
|
|
MeetingKey: meetingKey,
|
|
MeetingName: "Monaco",
|
|
Year: 2025,
|
|
}); err != nil {
|
|
t.Fatalf("UpsertMeeting() error = %v", err)
|
|
}
|
|
if err := s.UpsertSession(Session{
|
|
SessionKey: sessionKey,
|
|
MeetingKey: meetingKey,
|
|
SessionName: "Race",
|
|
SessionType: "Race",
|
|
}); err != nil {
|
|
t.Fatalf("UpsertSession() error = %v", err)
|
|
}
|
|
|
|
result := SessionResult{
|
|
SessionKey: sessionKey,
|
|
DriverNumber: 1,
|
|
MeetingKey: meetingKey,
|
|
Position: 1,
|
|
Points: 25,
|
|
NumberOfLaps: 78,
|
|
DurationJSON: "5234.567",
|
|
GapToLeaderJSON: "0",
|
|
}
|
|
updatedResult := result
|
|
updatedResult.Points = 26
|
|
|
|
for i := 0; i < 2; i++ {
|
|
r := result
|
|
if i == 1 {
|
|
r = updatedResult
|
|
}
|
|
if err := s.UpsertSessionResult(r); err != nil {
|
|
t.Fatalf("UpsertSessionResult(%d) error = %v", i, err)
|
|
}
|
|
}
|
|
|
|
results, err := s.ListSessionResults(sessionKey)
|
|
if err != nil {
|
|
t.Fatalf("ListSessionResults() error = %v", err)
|
|
}
|
|
if len(results) != 1 {
|
|
t.Fatalf("ListSessionResults() len = %d, want 1", len(results))
|
|
}
|
|
if results[0].Points != updatedResult.Points {
|
|
t.Fatalf("Points = %v, want %v", results[0].Points, updatedResult.Points)
|
|
}
|
|
|
|
grid := StartingGridEntry{
|
|
SessionKey: sessionKey,
|
|
DriverNumber: 1,
|
|
MeetingKey: meetingKey,
|
|
Position: 1,
|
|
LapDuration: 71.234,
|
|
}
|
|
updatedGrid := grid
|
|
updatedGrid.LapDuration = 71.111
|
|
|
|
for i := 0; i < 2; i++ {
|
|
g := grid
|
|
if i == 1 {
|
|
g = updatedGrid
|
|
}
|
|
if err := s.UpsertStartingGridEntry(g); err != nil {
|
|
t.Fatalf("UpsertStartingGridEntry(%d) error = %v", i, err)
|
|
}
|
|
}
|
|
|
|
grids, err := s.ListStartingGrid(sessionKey)
|
|
if err != nil {
|
|
t.Fatalf("ListStartingGrid() error = %v", err)
|
|
}
|
|
if len(grids) != 1 {
|
|
t.Fatalf("ListStartingGrid() len = %d, want 1", len(grids))
|
|
}
|
|
if grids[0].LapDuration != updatedGrid.LapDuration {
|
|
t.Fatalf("LapDuration = %v, want %v", grids[0].LapDuration, updatedGrid.LapDuration)
|
|
}
|
|
|
|
var resultCount int
|
|
if err := s.db.QueryRow(`SELECT COUNT(*) FROM session_results`).Scan(&resultCount); err != nil {
|
|
t.Fatalf("count session_results: %v", err)
|
|
}
|
|
if resultCount != 1 {
|
|
t.Fatalf("session_results count = %d, want 1", resultCount)
|
|
}
|
|
}
|
|
|
|
func TestAnalyticsUpsertRead(t *testing.T) {
|
|
s := openTestStore(t)
|
|
|
|
meetingKey := 1229
|
|
sessionKey := 9472
|
|
|
|
stint := Stint{
|
|
SessionKey: sessionKey,
|
|
DriverNumber: 1,
|
|
MeetingKey: meetingKey,
|
|
StintNumber: 1,
|
|
Compound: "SOFT",
|
|
LapStart: 1,
|
|
LapEnd: 20,
|
|
TyreAgeAtStart: 0,
|
|
}
|
|
if err := s.UpsertStint(stint); err != nil {
|
|
t.Fatalf("UpsertStint() error = %v", err)
|
|
}
|
|
stints, err := s.ListStints(sessionKey)
|
|
if err != nil || len(stints) != 1 || stints[0].Compound != "SOFT" {
|
|
t.Fatalf("ListStints() = %+v, err = %v", stints, err)
|
|
}
|
|
|
|
pit := PitStop{
|
|
SessionKey: sessionKey,
|
|
DriverNumber: 1,
|
|
MeetingKey: meetingKey,
|
|
LapNumber: 21,
|
|
Date: "2025-05-25T14:00:00+00:00",
|
|
StopDuration: 2.5,
|
|
}
|
|
if err := s.UpsertPitStop(pit); err != nil {
|
|
t.Fatalf("UpsertPitStop() error = %v", err)
|
|
}
|
|
pits, err := s.ListPitStops(sessionKey)
|
|
if err != nil || len(pits) != 1 {
|
|
t.Fatalf("ListPitStops() = %+v, err = %v", pits, err)
|
|
}
|
|
|
|
pos := PositionSample{
|
|
SessionKey: sessionKey,
|
|
DriverNumber: 1,
|
|
MeetingKey: meetingKey,
|
|
Date: "2025-05-25T14:05:00+00:00",
|
|
Position: 1,
|
|
}
|
|
if err := s.UpsertPositionSample(pos); err != nil {
|
|
t.Fatalf("UpsertPositionSample() error = %v", err)
|
|
}
|
|
positions, err := s.ListPositionSamples(sessionKey)
|
|
if err != nil || len(positions) != 1 {
|
|
t.Fatalf("ListPositionSamples() = %+v, err = %v", positions, err)
|
|
}
|
|
|
|
rc := RaceControlMessage{
|
|
SessionKey: sessionKey,
|
|
MeetingKey: meetingKey,
|
|
Date: "2025-05-25T14:10:00+00:00",
|
|
Category: "Flag",
|
|
Flag: "YELLOW",
|
|
Message: "Yellow flag sector 1",
|
|
Scope: "Track",
|
|
}
|
|
if err := s.UpsertRaceControlMessage(rc); err != nil {
|
|
t.Fatalf("UpsertRaceControlMessage() error = %v", err)
|
|
}
|
|
messages, err := s.ListRaceControlMessages(sessionKey)
|
|
if err != nil || len(messages) != 1 {
|
|
t.Fatalf("ListRaceControlMessages() = %+v, err = %v", messages, err)
|
|
}
|
|
|
|
weather := WeatherSample{
|
|
SessionKey: sessionKey,
|
|
MeetingKey: meetingKey,
|
|
Date: "2025-05-25T14:00:00+00:00",
|
|
AirTemperature: 22.5,
|
|
TrackTemperature: 35.0,
|
|
Humidity: 45.0,
|
|
}
|
|
if err := s.UpsertWeatherSample(weather); err != nil {
|
|
t.Fatalf("UpsertWeatherSample() error = %v", err)
|
|
}
|
|
samples, err := s.ListWeatherSamples(sessionKey)
|
|
if err != nil || len(samples) != 1 {
|
|
t.Fatalf("ListWeatherSamples() = %+v, err = %v", samples, err)
|
|
}
|
|
|
|
lap := Lap{
|
|
SessionKey: sessionKey,
|
|
DriverNumber: 1,
|
|
MeetingKey: meetingKey,
|
|
LapNumber: 1,
|
|
LapDuration: 75.123,
|
|
}
|
|
if err := s.UpsertLap(lap); err != nil {
|
|
t.Fatalf("UpsertLap() error = %v", err)
|
|
}
|
|
laps, err := s.ListLaps(sessionKey)
|
|
if err != nil || len(laps) != 1 {
|
|
t.Fatalf("ListLaps() = %+v, err = %v", laps, err)
|
|
}
|
|
}
|
|
|
|
func TestWithTxRollback(t *testing.T) {
|
|
s := openTestStore(t)
|
|
|
|
err := s.WithTx(func(tx *sql.Tx) error {
|
|
if _, err := tx.Exec(`
|
|
INSERT INTO meetings (meeting_key, meeting_name, year, updated_at)
|
|
VALUES (999, 'Rollback Test', 2025, ?)
|
|
`, time.Now().Unix()); err != nil {
|
|
return err
|
|
}
|
|
return assertAnError("rollback")
|
|
})
|
|
if err == nil {
|
|
t.Fatal("WithTx() error = nil, want rollback error")
|
|
}
|
|
|
|
var count int
|
|
if err := s.db.QueryRow(`SELECT COUNT(*) FROM meetings WHERE meeting_key = 999`).Scan(&count); err != nil {
|
|
t.Fatalf("count meetings: %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Fatalf("meetings count after rollback = %d, want 0", count)
|
|
}
|
|
}
|
|
|
|
func assertAnError(msg string) error {
|
|
return &testError{msg: msg}
|
|
}
|
|
|
|
type testError struct {
|
|
msg string
|
|
}
|
|
|
|
func (e *testError) Error() string {
|
|
return e.msg
|
|
}
|