package objstore import ( "context" "errors" "testing" "github.com/carlosframework/platform/internal/release" ) func TestScanKey(t *testing.T) { if got := ScanKey("bab", "titogo"); got != "apps/bab/titogo/scan.json" { t.Fatalf("ScanKey = %q", got) } } func TestSidecarRoundTrip(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) sc := &ScanSidecar{ LastComplete: &Sweep{ Objects: 12481, Scanned: 12479, Skipped: 2, Engine: "clamav", Signature: "ClamAV 1.4.1/27500/Mon Sep 1 08:00:00 2026", }, } if _, err := PutSidecar(ctx, s, "bab", "titogo", sc, ""); err != nil { t.Fatal(err) } back, etag, err := GetSidecar(ctx, s, "bab", "titogo") if err != nil { t.Fatal(err) } if etag == "" { t.Fatal("GetSidecar returned no etag, so no caller can CAS against it") } if back.LastComplete == nil || back.LastComplete.Objects != 12481 { t.Fatalf("rollup did not round-trip: %+v", back) } if back.LastComplete.Skipped != 2 { t.Fatalf("skipped = %d, want 2 — skipped must never be folded into scanned", back.LastComplete.Skipped) } if back.Schema != SidecarSchema || back.Account != "bab" || back.App != "titogo" { t.Fatalf("PutSidecar did not stamp identity: %+v", back) } } // Absent is (nil, "", nil): a store nobody has swept has no sidecar, and that // is a state rather than an error. func TestSidecarAbsentIsNotAnError(t *testing.T) { sc, etag, err := GetSidecar(context.Background(), release.NewDirStore(t.TempDir()), "bab", "titogo") if err != nil || sc != nil || etag != "" { t.Fatalf("absent sidecar = (%v, %q, %v), want (nil, \"\", nil)", sc, etag, err) } } // accounts migrate copies every key under an app's prefix, so a sidecar can // arrive under an account it was never written for, still carrying rollups for // a bucket named after the old one. Refusing beats reporting one account's // coverage as another's. func TestSidecarRefusesAccountMismatch(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) if _, err := PutSidecar(ctx, s, "bab", "titogo", &ScanSidecar{}, ""); err != nil { t.Fatal(err) } raw, _, err := s.Get(ctx, ScanKey("bab", "titogo")) if err != nil { t.Fatal(err) } // Exactly what a migration does: the same bytes under a new prefix. if _, err := s.Put(ctx, ScanKey("zed", "titogo"), raw, ""); err != nil { t.Fatal(err) } if _, _, err := GetSidecar(ctx, s, "zed", "titogo"); !errors.Is(err, ErrSidecarMismatch) { t.Fatalf("migrated sidecar error = %v, want ErrSidecarMismatch", err) } } // An unknown schema is refused, not guessed at. func TestSidecarRefusesUnknownSchema(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) if _, err := s.Put(ctx, ScanKey("bab", "titogo"), []byte(`{"schema":99,"account":"bab","app":"titogo"}`), ""); err != nil { t.Fatal(err) } if _, _, err := GetSidecar(ctx, s, "bab", "titogo"); err == nil { t.Fatal("an unknown sidecar schema must be refused") } } // Current and LastComplete are separate fields so a finished sweep survives // the start of the next one. With a single field the tick after completion // would overwrite the audit answer with page-one partials. func TestSidecarKeepsLastCompleteWhileCurrentRuns(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) done := &Sweep{Objects: 100, Scanned: 100, Engine: "clamav"} if _, err := PutSidecar(ctx, s, "bab", "titogo", &ScanSidecar{LastComplete: done}, ""); err != nil { t.Fatal(err) } stored, etag, err := GetSidecar(ctx, s, "bab", "titogo") if err != nil { t.Fatal(err) } // A fresh sweep begins: Current starts at page one, LastComplete must not // move. stored.Current = &Sweep{Objects: 7, Scanned: 7, Engine: "clamav", After: "g.txt"} if _, err := PutSidecar(ctx, s, "bab", "titogo", stored, etag); err != nil { t.Fatal(err) } back, _, err := GetSidecar(ctx, s, "bab", "titogo") if err != nil { t.Fatal(err) } if back.LastComplete == nil || back.LastComplete.Objects != 100 { t.Fatalf("a running sweep destroyed the last completed one: %+v", back.LastComplete) } if back.Current == nil || back.Current.After != "g.txt" { t.Fatalf("current sweep did not round-trip: %+v", back.Current) } }