182 lines
6.8 KiB
Go
182 lines
6.8 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
"vctp/db"
|
|
"vctp/db/queries"
|
|
"vctp/server/models"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
type snapshotRepairTestDatabase struct {
|
|
dbConn *sqlx.DB
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func (d *snapshotRepairTestDatabase) DB() *sqlx.DB { return d.dbConn }
|
|
func (d *snapshotRepairTestDatabase) Queries() db.Querier { return queries.New(d.dbConn.DB) }
|
|
func (d *snapshotRepairTestDatabase) Logger() *slog.Logger {
|
|
if d.logger != nil {
|
|
return d.logger
|
|
}
|
|
return slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
}
|
|
func (d *snapshotRepairTestDatabase) Close() error { return d.dbConn.Close() }
|
|
|
|
func newSnapshotRepairTestDB(t *testing.T) *sqlx.DB {
|
|
t.Helper()
|
|
dbConn, err := sqlx.Open("sqlite", ":memory:")
|
|
if err != nil {
|
|
t.Fatalf("failed to open sqlite test db: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = dbConn.Close()
|
|
})
|
|
return dbConn
|
|
}
|
|
|
|
func TestSnapshotRepairSuite_RebuildsRegistryTotalsAndLifecycle(t *testing.T) {
|
|
ctx := context.Background()
|
|
dbConn := newSnapshotRepairTestDB(t)
|
|
logger := newTestLogger()
|
|
h := &Handler{
|
|
Logger: logger,
|
|
Database: &snapshotRepairTestDatabase{dbConn: dbConn, logger: logger},
|
|
}
|
|
|
|
dayStart := time.Date(2026, time.March, 16, 0, 0, 0, 0, time.UTC)
|
|
hourlyTs := dayStart.Add(2 * time.Hour).Unix()
|
|
hourlyTable := fmt.Sprintf("inventory_hourly_%d", hourlyTs)
|
|
dailyTable := fmt.Sprintf("inventory_daily_summary_%s", dayStart.Format("20060102"))
|
|
monthlyTable := fmt.Sprintf("inventory_monthly_summary_%s", dayStart.Format("200601"))
|
|
|
|
if err := db.EnsureSnapshotTable(ctx, dbConn, hourlyTable); err != nil {
|
|
t.Fatalf("failed to ensure hourly table: %v", err)
|
|
}
|
|
if err := db.EnsureSummaryTable(ctx, dbConn, dailyTable); err != nil {
|
|
t.Fatalf("failed to ensure daily summary table: %v", err)
|
|
}
|
|
if err := db.EnsureSummaryTable(ctx, dbConn, monthlyTable); err != nil {
|
|
t.Fatalf("failed to ensure monthly summary table: %v", err)
|
|
}
|
|
|
|
if _, err := dbConn.ExecContext(ctx, fmt.Sprintf(`
|
|
INSERT INTO %s (
|
|
"Name","Vcenter","VmId","VmUuid","CreationTime","DeletionTime","ResourcePool","Datacenter","Cluster","Folder",
|
|
"ProvisionedDisk","VcpuCount","RamGB","IsTemplate","PoweredOn","SrmPlaceholder","SnapshotTime"
|
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
`, hourlyTable),
|
|
"vm-a", "vc-a", "vm-a", "uuid-a", dayStart.Add(-24*time.Hour).Unix(), int64(0), "Tin", "dc-a", "cluster-a", "/prod",
|
|
100.0, int64(2), int64(8), "FALSE", "TRUE", "FALSE", hourlyTs,
|
|
); err != nil {
|
|
t.Fatalf("failed to seed hourly table: %v", err)
|
|
}
|
|
|
|
if _, err := dbConn.ExecContext(ctx, fmt.Sprintf(`
|
|
INSERT INTO %s (
|
|
"Name","Vcenter","VmId","VmUuid","CreationTime","DeletionTime","ResourcePool","Datacenter","Cluster","Folder",
|
|
"ProvisionedDisk","VcpuCount","RamGB","IsTemplate","PoweredOn","SrmPlaceholder","SnapshotTime","SamplesPresent",
|
|
"AvgVcpuCount","AvgRamGB","AvgProvisionedDisk","AvgIsPresent","PoolTinPct","PoolBronzePct","PoolSilverPct","PoolGoldPct","Tin","Bronze","Silver","Gold"
|
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
`, dailyTable),
|
|
"vm-a", "vc-a", "vm-a", "uuid-a", int64(0), int64(0), "Tin", "dc-a", "cluster-a", "/prod",
|
|
100.0, int64(2), int64(8), "FALSE", "TRUE", "FALSE", int64(0), int64(1),
|
|
2.0, 8.0, 100.0, 1.0, 100.0, 0.0, 0.0, 0.0, 100.0, 0.0, 0.0, 0.0,
|
|
); err != nil {
|
|
t.Fatalf("failed to seed daily summary table: %v", err)
|
|
}
|
|
|
|
if _, err := dbConn.ExecContext(ctx, fmt.Sprintf(`
|
|
INSERT INTO %s (
|
|
"Name","Vcenter","VmId","VmUuid","CreationTime","DeletionTime","ResourcePool","Datacenter","Cluster","Folder",
|
|
"ProvisionedDisk","VcpuCount","RamGB","IsTemplate","PoweredOn","SrmPlaceholder","SnapshotTime","SamplesPresent",
|
|
"AvgVcpuCount","AvgRamGB","AvgProvisionedDisk","AvgIsPresent","PoolTinPct","PoolBronzePct","PoolSilverPct","PoolGoldPct","Tin","Bronze","Silver","Gold"
|
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
`, monthlyTable),
|
|
"vm-a", "vc-a", "vm-a", "uuid-a", int64(0), int64(0), "Tin", "dc-a", "cluster-a", "/prod",
|
|
100.0, int64(2), int64(8), "FALSE", "TRUE", "FALSE", dayStart.Unix(), int64(1),
|
|
2.0, 8.0, 100.0, 1.0, 100.0, 0.0, 0.0, 0.0, 100.0, 0.0, 0.0, 0.0,
|
|
); err != nil {
|
|
t.Fatalf("failed to seed monthly summary table: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/snapshots/repair/all", nil)
|
|
rr := httptest.NewRecorder()
|
|
h.SnapshotRepairSuite(rr, req)
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d body=%s", http.StatusOK, rr.Code, rr.Body.String())
|
|
}
|
|
|
|
var payload models.SnapshotRepairSuiteResponse
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &payload); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
if payload.Status != "OK" {
|
|
t.Fatalf("unexpected repair suite status: %q", payload.Status)
|
|
}
|
|
|
|
dailyRepaired, err := strconv.Atoi(payload.DailyRepaired)
|
|
if err != nil {
|
|
t.Fatalf("failed to parse daily_repaired: %v", err)
|
|
}
|
|
if dailyRepaired < 1 {
|
|
t.Fatalf("expected at least one daily table repaired, got %d", dailyRepaired)
|
|
}
|
|
monthlyRefined, err := strconv.Atoi(payload.MonthlyRefined)
|
|
if err != nil {
|
|
t.Fatalf("failed to parse monthly_refined: %v", err)
|
|
}
|
|
if monthlyRefined < 1 {
|
|
t.Fatalf("expected at least one monthly table refined, got %d", monthlyRefined)
|
|
}
|
|
monthlyFailed, err := strconv.Atoi(payload.MonthlyFailed)
|
|
if err != nil {
|
|
t.Fatalf("failed to parse monthly_failed: %v", err)
|
|
}
|
|
if monthlyFailed != 0 {
|
|
t.Fatalf("expected monthly_failed=0, got %d", monthlyFailed)
|
|
}
|
|
|
|
assertSnapshotRegistryTypeCount(t, ctx, dbConn, "hourly", 1)
|
|
assertSnapshotRegistryTypeCount(t, ctx, dbConn, "daily", 1)
|
|
assertSnapshotRegistryTypeCount(t, ctx, dbConn, "monthly", 1)
|
|
|
|
var totalsRows int
|
|
if err := dbConn.GetContext(ctx, &totalsRows, `SELECT COUNT(1) FROM vcenter_totals WHERE "Vcenter" = ?`, "vc-a"); err != nil {
|
|
t.Fatalf("failed to query vcenter_totals: %v", err)
|
|
}
|
|
if totalsRows < 1 {
|
|
t.Fatalf("expected vcenter_totals to be backfilled, got %d rows", totalsRows)
|
|
}
|
|
|
|
var dailySnapshotTime int64
|
|
if err := dbConn.GetContext(ctx, &dailySnapshotTime, fmt.Sprintf(`SELECT COALESCE("SnapshotTime",0) FROM %s WHERE "Vcenter" = ? AND "VmId" = ?`, dailyTable), "vc-a", "vm-a"); err != nil {
|
|
t.Fatalf("failed to query repaired daily snapshot time: %v", err)
|
|
}
|
|
if dailySnapshotTime == 0 {
|
|
t.Fatal("expected repaired daily summary SnapshotTime to be backfilled")
|
|
}
|
|
}
|
|
|
|
func assertSnapshotRegistryTypeCount(t *testing.T, ctx context.Context, dbConn *sqlx.DB, snapshotType string, want int) {
|
|
t.Helper()
|
|
var got int
|
|
if err := dbConn.GetContext(ctx, &got, `SELECT COUNT(1) FROM snapshot_registry WHERE snapshot_type = ?`, snapshotType); err != nil {
|
|
t.Fatalf("failed to query snapshot_registry for type %s: %v", snapshotType, err)
|
|
}
|
|
if got != want {
|
|
t.Fatalf("unexpected snapshot_registry count for %s: got %d want %d", snapshotType, got, want)
|
|
}
|
|
}
|