package store import ( "context" "errors" "testing" "time" ) func int64ptr(v int64) *int64 { return &v } func boolptr(v bool) *bool { return &v } func TestHostRepoStatsRoundTrip(t *testing.T) { t.Parallel() s := openTestStore(t) ctx := context.Background() const hostID = "h-stats-test" seedHost(t, s, hostID) // 1. Initial upsert: set TotalSizeBytes only. if err := s.UpsertHostRepoStats(ctx, hostID, HostRepoStats{ TotalSizeBytes: int64ptr(100), }); err != nil { t.Fatalf("upsert 1: %v", err) } got, err := s.GetHostRepoStats(ctx, hostID) if err != nil { t.Fatalf("get after upsert 1: %v", err) } if got.TotalSizeBytes == nil || *got.TotalSizeBytes != 100 { t.Errorf("TotalSizeBytes: want 100, got %v", got.TotalSizeBytes) } if got.LastCheckStatus != "" { t.Errorf("LastCheckStatus should be empty after first upsert, got %q", got.LastCheckStatus) } // 2. Upsert with LastCheckStatus; TotalSizeBytes must be preserved. if err := s.UpsertHostRepoStats(ctx, hostID, HostRepoStats{ LastCheckStatus: "ok", }); err != nil { t.Fatalf("upsert 2: %v", err) } got, err = s.GetHostRepoStats(ctx, hostID) if err != nil { t.Fatalf("get after upsert 2: %v", err) } if got.TotalSizeBytes == nil || *got.TotalSizeBytes != 100 { t.Errorf("TotalSizeBytes should still be 100 after second upsert, got %v", got.TotalSizeBytes) } if got.LastCheckStatus != "ok" { t.Errorf("LastCheckStatus: want %q, got %q", "ok", got.LastCheckStatus) } // 3. Upsert with LockPresent=true; all other fields preserved. now := time.Now().UTC().Truncate(time.Second) if err := s.UpsertHostRepoStats(ctx, hostID, HostRepoStats{ LockPresent: boolptr(true), LastCheckAt: &now, }); err != nil { t.Fatalf("upsert 3: %v", err) } got, err = s.GetHostRepoStats(ctx, hostID) if err != nil { t.Fatalf("get after upsert 3: %v", err) } if got.LockPresent == nil || !*got.LockPresent { t.Error("LockPresent should be true after upsert 3") } if got.TotalSizeBytes == nil || *got.TotalSizeBytes != 100 { t.Errorf("TotalSizeBytes still 100 expected, got %v", got.TotalSizeBytes) } if got.LastCheckStatus != "ok" { t.Errorf("LastCheckStatus still 'ok' expected, got %q", got.LastCheckStatus) } if got.LastCheckAt == nil { t.Error("LastCheckAt should be set") } else if !got.LastCheckAt.UTC().Truncate(time.Second).Equal(now) { t.Errorf("LastCheckAt: got %v, want %v", got.LastCheckAt.UTC().Truncate(time.Second), now) } // 4. Clear lock (set to false). if err := s.UpsertHostRepoStats(ctx, hostID, HostRepoStats{ LockPresent: boolptr(false), }); err != nil { t.Fatalf("upsert 4: %v", err) } got, err = s.GetHostRepoStats(ctx, hostID) if err != nil { t.Fatalf("get after upsert 4: %v", err) } if got.LockPresent == nil || *got.LockPresent { t.Error("LockPresent should be false after upsert 4") } } func TestHostRepoStatsNotFound(t *testing.T) { t.Parallel() s := openTestStore(t) ctx := context.Background() _, err := s.GetHostRepoStats(ctx, "no-such-host") if !errors.Is(err, ErrNotFound) { t.Errorf("expected ErrNotFound, got %v", err) } } func TestHostRepoStatsCascadeDelete(t *testing.T) { t.Parallel() s := openTestStore(t) ctx := context.Background() const hostID = "h-cascade-test" seedHost(t, s, hostID) if err := s.UpsertHostRepoStats(ctx, hostID, HostRepoStats{ TotalSizeBytes: int64ptr(999), }); err != nil { t.Fatalf("upsert: %v", err) } // Delete the host; stats row should cascade-delete. if _, err := s.DB().ExecContext(ctx, `DELETE FROM hosts WHERE id = ?`, hostID); err != nil { t.Fatalf("delete host: %v", err) } _, err := s.GetHostRepoStats(ctx, hostID) if !errors.Is(err, ErrNotFound) { t.Errorf("after host delete, expected ErrNotFound for stats; got %v", err) } }