package objstore import ( "bytes" "context" "crypto/ed25519" "crypto/rand" "encoding/json" "errors" "os" "testing" "time" "github.com/carlosframework/platform/internal/release" ) // The scanning block round-trips, and its three sub-blocks are null-or-present // so a client can tell "requested, waiting for an operator" from "enabled" // without inspecting fields — the same tri-state the store's own blocks give. func TestScanBlockRoundTrip(t *testing.T) { now := time.Now().UTC().Truncate(time.Second) rec := &Record{ App: "titogo", Scanning: &ScanBlock{ Requested: &ScanRequested{At: now, By: "m@example.com"}, Enabled: &ScanEnabled{Engine: "clamav", At: now, By: "op@example.com"}, Enforce: &DenyTag{Key: TagVerdict, Value: string(VerdictInfected)}, }, } body, err := json.Marshal(rec) if err != nil { t.Fatal(err) } var back Record if err := json.Unmarshal(body, &back); err != nil { t.Fatal(err) } if back.Scanning == nil || back.Scanning.Enabled == nil { t.Fatalf("scanning.enabled did not round-trip: %s", body) } if back.Scanning.Enabled.Engine != "clamav" { t.Fatalf("engine = %q, want clamav", back.Scanning.Enabled.Engine) } if back.Scanning.Enforce == nil || back.Scanning.Enforce.Value != string(VerdictInfected) { t.Fatalf("enforce did not round-trip: %s", body) } } // An absent scanning block stays absent. Every deployment that never touches // scanning must not grow a null key in every store record it owns. func TestScanBlockOmittedWhenAbsent(t *testing.T) { body, err := json.Marshal(&Record{App: "titogo"}) if err != nil { t.Fatal(err) } if bytes.Contains(body, []byte("scanning")) { t.Fatalf("an absent scanning block leaked into the JSON: %s", body) } } // The attestation is a real field, not an inference: a store whose objects // arrive already encrypted cannot be scanned, and the record has to say so // because nothing can tell by looking (spec §2.2). func TestScanRequestedCarriesEncryptedAttestation(t *testing.T) { body, err := json.Marshal(&ScanRequested{ At: time.Now().UTC(), By: "m@example.com", Encrypted: true, }) if err != nil { t.Fatal(err) } var back ScanRequested if err := json.Unmarshal(body, &back); err != nil { t.Fatal(err) } if !back.Encrypted { t.Fatalf("the encrypted attestation did not round-trip: %s", body) } } // The scanning block is ADDITIVE. release.PurgeApp parses this shape with its // own local struct — it cannot import objstore, because objstore imports // release — and must keep refusing on grant/provisioned while ignoring keys it // has never heard of. objstore.go's PARSER LIST comment claims exactly this, // and TestPurgeGuardSeesOurRecord pins the two names without ever writing a // scanning key, so without this test the additive half of the claim is // unasserted. // // Driven through release.PurgeApp itself rather than an extracted predicate: // the guard is inline in PurgeApp, and pulling it out to make it callable // would refactor the fleet-wide purge path for the sake of a test. func TestPurgeGuardIgnoresScanningBlock(t *testing.T) { ctx := context.Background() _, pub := testSealKeypair(t) relPub, _, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } backdated := time.Now().Add(-2 * release.TrashCooloff) // A record naming backend estate still blocks the purge with a scanning // block sitting beside it — the new key must not shadow the guard. t.Run("provisioned-plus-scanning-still-blocks", func(t *testing.T) { s := release.NewDirStore(t.TempDir()) p := NewMemProvisioner("eu-west-1") rotateFixture(t, s, pub, p) stored, etag, err := Get(ctx, s, "bab", "titogo") if err != nil || stored == nil { t.Fatalf("re-read: %+v, %v", stored, err) } stored.Scanning = &ScanBlock{ Enabled: &ScanEnabled{Engine: "clamav", At: time.Now().UTC(), By: "op@example.com"}, } if _, err := Put(ctx, s, "bab", "titogo", stored, etag); err != nil { t.Fatal(err) } if err := release.TrashApp(ctx, s, "bab", "titogo", "paul@keymail.dev", backdated); err != nil { t.Fatal(err) } err = release.PurgeApp(ctx, s, relPub, "bab", "titogo", time.Now()) if !errors.Is(err, release.ErrStoreProvisioned) { t.Fatalf("PurgeApp over provisioned+scanning = %v, want release.ErrStoreProvisioned", err) } }) // The other direction, and the one that proves the key is genuinely // ignored rather than accidentally load-bearing: scanning alone names no // backend estate, so it must not block a purge. t.Run("scanning-alone-names-no-estate", func(t *testing.T) { s := release.NewDirStore(t.TempDir()) declare(t, s, "bab", "titogo", "TITO_S3") stored, etag, err := Get(ctx, s, "bab", "titogo") if err != nil || stored == nil { t.Fatalf("re-read: %+v, %v", stored, err) } stored.Scanning = &ScanBlock{ Requested: &ScanRequested{At: time.Now().UTC(), By: "m@example.com"}, } if _, err := Put(ctx, s, "bab", "titogo", stored, etag); err != nil { t.Fatal(err) } if err := release.TrashApp(ctx, s, "bab", "titogo", "paul@keymail.dev", backdated); err != nil { t.Fatal(err) } if err := release.PurgeApp(ctx, s, relPub, "bab", "titogo", time.Now()); err != nil { t.Fatalf("PurgeApp over a declared+scanning record: %v", err) } if _, _, err := s.Get(ctx, Key("bab", "titogo")); !errors.Is(err, os.ErrNotExist) { t.Fatalf("store.json survived the purge: %v", err) } }) }