package db import ( "database/sql" "errors" "strings" "testing" "time" "crypto-miner-server/internal/models" ) func insertBuild(t *testing.T, d *Database, b *models.BuildRecord) { t.Helper() if err := d.InsertBuild(b); err != nil { t.Fatal(err) } } func TestBuildCRUDAndList(t *testing.T) { d := openTestDB(t) created := time.Now().UTC().Truncate(time.Second) build := &models.BuildRecord{ ID: "build-001", WorkerName: "rig1", ServerURL: "http://localhost:8080", Wallet: "48xyz", Threads: 4, FileSize: 1024, BundleSize: 2048, FilePath: "/data/builds/build-001.zip", FileName: "agent.zip", DownloadURL: "/api/v1/builds/build-001/download", Platform: "windows", CreatedAt: created, PoolHost: "pool.example.com", PoolPort: 443, PoolTLS: true, PoolPass: "x", } insertBuild(t, d, build) got, err := d.GetBuild("build-001") if err != nil { t.Fatal(err) } if got.WorkerName != "rig1" || got.Platform != "windows" || got.BundleSize != 2048 { t.Fatalf("build mismatch: %+v", got) } if got.Pinned { t.Fatal("new build should not be pinned") } list, err := d.ListBuilds(10) if err != nil { t.Fatal(err) } if len(list) != 1 || list[0].ID != "build-001" { t.Fatalf("list builds: %+v", list) } } func TestBuildExtraFilesRoundTrip(t *testing.T) { d := openTestDB(t) build := &models.BuildRecord{ ID: "build-extra", WorkerName: "w", ServerURL: "u", Wallet: "w", CreatedAt: time.Now(), DownloadURL: "/api/v1/builds/build-extra/download", ExtraFiles: []models.BuildExtraFile{ {FileName: "payload.enc", FilePath: "/data/payload.enc"}, {FileName: "README.txt"}, }, } insertBuild(t, d, build) got, err := d.GetBuild("build-extra") if err != nil { t.Fatal(err) } if len(got.ExtraFiles) != 2 || got.ExtraFiles[0].FileName != "payload.enc" { t.Fatalf("extra_files mismatch: %+v", got.ExtraFiles) } } func TestGetBuildNotFound(t *testing.T) { d := openTestDB(t) _, err := d.GetBuild("missing") if !errors.Is(err, sql.ErrNoRows) { t.Fatalf("expected sql.ErrNoRows, got %v", err) } } func TestSetPinnedBuild(t *testing.T) { d := openTestDB(t) now := time.Now() insertBuild(t, d, &models.BuildRecord{ID: "b1", WorkerName: "w", ServerURL: "u", Wallet: "w", CreatedAt: now}) insertBuild(t, d, &models.BuildRecord{ID: "b2", WorkerName: "w", ServerURL: "u", Wallet: "w", CreatedAt: now.Add(time.Second)}) if err := d.SetPinnedBuild("b1"); err != nil { t.Fatal(err) } b1, _ := d.GetBuild("b1") b2, _ := d.GetBuild("b2") if !b1.Pinned || b2.Pinned { t.Fatalf("pin state wrong: b1=%v b2=%v", b1.Pinned, b2.Pinned) } if err := d.SetPinnedBuild(""); err != nil { t.Fatal(err) } b1, _ = d.GetBuild("b1") if b1.Pinned { t.Fatal("empty id should unpin all builds") } } func TestSetPinnedBuildUnknownID(t *testing.T) { d := openTestDB(t) now := time.Now() insertBuild(t, d, &models.BuildRecord{ID: "b1", WorkerName: "w", ServerURL: "u", Wallet: "w", CreatedAt: now, Pinned: true}) err := d.SetPinnedBuild("missing") if err == nil { t.Fatal("expected error for unknown build id") } if !strings.Contains(err.Error(), "not found") { t.Fatalf("unexpected error: %v", err) } b1, _ := d.GetBuild("b1") if b1.Pinned { t.Fatal("unknown id should leave builds unpinned, not keep prior pin") } } func TestGetLatestBuildForPlatform(t *testing.T) { d := openTestDB(t) base := time.Now().UTC().Truncate(time.Second) insertBuild(t, d, &models.BuildRecord{ ID: "old-linux", WorkerName: "w", ServerURL: "u", Wallet: "w", Platform: "linux", CreatedAt: base, }) insertBuild(t, d, &models.BuildRecord{ ID: "new-linux", WorkerName: "w", ServerURL: "u", Wallet: "w", Platform: "linux", CreatedAt: base.Add(time.Minute), }) insertBuild(t, d, &models.BuildRecord{ ID: "new-windows", WorkerName: "w", ServerURL: "u", Wallet: "w", Platform: "windows", CreatedAt: base.Add(2 * time.Minute), }) latestLinux, err := d.GetLatestBuildForPlatform("linux") if err != nil { t.Fatal(err) } if latestLinux.ID != "new-linux" { t.Fatalf("latest linux: got %q", latestLinux.ID) } if err := d.SetPinnedBuild("old-linux"); err != nil { t.Fatal(err) } pinnedLinux, err := d.GetLatestBuildForPlatform("linux") if err != nil { t.Fatal(err) } if pinnedLinux.ID != "old-linux" { t.Fatalf("pinned linux: got %q", pinnedLinux.ID) } anyLatest, err := d.GetLatestBuildForPlatform("any") if err != nil { t.Fatal(err) } if anyLatest.ID != "old-linux" { t.Fatalf("any with pinned: got %q want old-linux", anyLatest.ID) } if err := d.SetPinnedBuild(""); err != nil { t.Fatal(err) } emptyPlatform, err := d.GetLatestBuildForPlatform("") if err != nil { t.Fatal(err) } if emptyPlatform.ID != "new-windows" { t.Fatalf("empty platform latest: got %q", emptyPlatform.ID) } } func TestGetLatestBuildForPlatformEmpty(t *testing.T) { d := openTestDB(t) _, err := d.GetLatestBuildForPlatform("linux") if !errors.Is(err, sql.ErrNoRows) { t.Fatalf("expected sql.ErrNoRows on empty db, got %v", err) } } func TestListBuildsOlderThanAndDeleteBuild(t *testing.T) { d := openTestDB(t) cutoff := time.Now().UTC().Truncate(time.Second) oldTime := cutoff.Add(-time.Hour) newTime := cutoff.Add(time.Hour) insertBuild(t, d, &models.BuildRecord{ ID: "old-build", WorkerName: "w", ServerURL: "u", Wallet: "w", CreatedAt: oldTime, }) insertBuild(t, d, &models.BuildRecord{ ID: "new-build", WorkerName: "w", ServerURL: "u", Wallet: "w", CreatedAt: newTime, }) older, err := d.ListBuildsOlderThan(cutoff) if err != nil { t.Fatal(err) } if len(older) != 1 || older[0].ID != "old-build" { t.Fatalf("ListBuildsOlderThan: %+v", older) } if err := d.DeleteBuild("old-build"); err != nil { t.Fatal(err) } _, err = d.GetBuild("old-build") if !errors.Is(err, sql.ErrNoRows) { t.Fatalf("expected deleted build missing, got %v", err) } if err := d.DeleteBuild("never-existed"); err != nil { t.Fatal(err) } }