package console import ( "context" "net/http" "sync/atomic" "testing" "github.com/carlosframework/platform/internal/appconfig" "github.com/carlosframework/platform/internal/objstore" "github.com/carlosframework/platform/internal/release" ) // racingRunner is a claims-on AppMutationRunner whose first permit for action // lets a concurrent writer land before the callback runs. type racingRunner struct { release.Store action string race func(context.Context) raced atomic.Bool puts atomic.Int64 } func (r *racingRunner) RunAppMutation(ctx context.Context, account, app, action string, f func(context.Context) error) error { if action == r.action && r.raced.CompareAndSwap(false, true) { r.race(ctx) } return f(ctx) } func (r *racingRunner) Put(ctx context.Context, key string, body []byte, etag string) (string, error) { if r.raced.Load() { r.puts.Add(1) } return r.Store.Put(ctx, key, body, etag) } func rotationRaceFixture(t *testing.T, code string) (*consoleFixture, string, string) { t.Helper() fx, _, token, priv := storeArmed(t, code) grantStore(t, fx, token) fx.srv.cfg.StoreScanner = objstore.NewMemScanner() enableDeploymentScanning(t, fx) grantScanning(t, fx, token, "titogo") return fx, token, priv } func editRecord(t *testing.T, fx *consoleFixture, edit func(*objstore.Record)) { t.Helper() ctx := context.Background() rec, etag, err := objstore.Get(ctx, fx.rel, fx.sqid, "titogo") if err != nil || rec == nil { t.Fatalf("read record: %v %v", rec, err) } edit(rec) if _, err := objstore.Put(ctx, fx.rel, fx.sqid, "titogo", rec, etag); err != nil { t.Fatal(err) } } func revokeBehindTheDoor(t *testing.T, fx *consoleFixture) { t.Helper() editRecord(t, fx, func(rec *objstore.Record) { rec.Scanning.Enabled = nil rec.Scanning.Enforce = nil }) if _, err := appconfig.Apply(context.Background(), fx.rel, fx.sqid, "titogo", nil, []string{objstore.DefaultEnvPrefix + "_SCAN_CALLBACK_SECRET"}); err != nil { t.Fatal(err) } } func TestDeploymentRotateDeliversTheGenerationHeldUnderThePermit(t *testing.T) { fx, _, priv := rotationRaceFixture(t, "code-rotate-race-gen") fx.srv.cfg.CallbackSigner = callbackTestSigner(t, "race-master", testWebhookMaster) staleKeyID := storedRecord(t, fx, "titogo").Scanning.WebhookKeyID runner := &racingRunner{Store: fx.rel, action: "scan-webhook-rotate", race: func(context.Context) { editRecord(t, fx, func(rec *objstore.Record) { rec.Scanning.WebhookGen++ }) }} fx.srv.cfg.Releases = runner pub, _ := appconfig.ParsePublicKey(fx.srv.cfg.SecretsPub, "test") remaining, changed, err := fx.srv.rotateWebhookBudget(context.Background(), pub) if err != nil { t.Fatal(err) } if remaining != 0 || changed != 1 { t.Fatalf("rotation = remaining %d changed %d, want 0/1", remaining, changed) } rec := storedRecord(t, fx, "titogo") if rec.Scanning.WebhookKeyID == staleKeyID || rec.Scanning.WebhookKeyID != fx.srv.cfg.CallbackSigner.CurrentKeyID() { t.Fatalf("webhook_key_id = %q, want current", rec.Scanning.WebhookKeyID) } want := string(fx.srv.cfg.CallbackSigner.DerivedKey(fx.sqid, "titogo", rec.Scanning.WebhookGen)) if got := callbackSecret(t, fx, priv, "titogo"); got != want { t.Fatal("delivered callback secret does not verify against the recorded generation") } } func TestDeploymentRotateSkipsWorkFinishedBeforeThePermit(t *testing.T) { fx, _, priv := rotationRaceFixture(t, "code-rotate-race-done") fx.srv.cfg.CallbackSigner = callbackTestSigner(t, "race-master", testWebhookMaster) pub, _ := appconfig.ParsePublicKey(fx.srv.cfg.SecretsPub, "test") var runner *racingRunner runner = &racingRunner{Store: fx.rel, action: "scan-webhook-rotate", race: func(ctx context.Context) { cur := storedRecord(t, fx, "titogo") gen := cur.Scanning.WebhookGen + 1 if err := fx.srv.deliverCallbackSecret(ctx, pub, fx.sqid, "titogo", cur, gen); err != nil { t.Fatal(err) } manifest, err := appconfig.Get(ctx, fx.rel, fx.sqid, "titogo") if err != nil { t.Fatal(err) } editRecord(t, fx, func(rec *objstore.Record) { rec.Scanning.WebhookGen = gen rec.Scanning.WebhookKeyID = fx.srv.cfg.CallbackSigner.CurrentKeyID() rec.Scanning.WebhookConfigHash = scanSecretHash(manifest.Vars[rec.Declared.EnvPrefix+"_SCAN_CALLBACK_SECRET"]) }) runner.puts.Store(0) }} fx.srv.cfg.Releases = runner remaining, changed, err := fx.srv.rotateWebhookBudget(context.Background(), pub) if err != nil { t.Fatal(err) } if remaining != 0 || changed != 0 || runner.puts.Load() != 0 { t.Fatalf("rotation = remaining %d changed %d puts %d, want 0/0/0", remaining, changed, runner.puts.Load()) } rec := storedRecord(t, fx, "titogo") want := string(fx.srv.cfg.CallbackSigner.DerivedKey(fx.sqid, "titogo", rec.Scanning.WebhookGen)) if got := callbackSecret(t, fx, priv, "titogo"); got != want { t.Fatal("rotation overwrote the concurrent rotation's callback secret") } } func TestDeploymentRotateLeavesARevokedAppAlone(t *testing.T) { fx, _, _ := rotationRaceFixture(t, "code-rotate-race-revoke") fx.srv.cfg.CallbackSigner = callbackTestSigner(t, "race-master", testWebhookMaster) before := storedRecord(t, fx, "titogo").Scanning.WebhookKeyID runner := &racingRunner{Store: fx.rel, action: "scan-webhook-rotate", race: func(context.Context) { revokeBehindTheDoor(t, fx) }} fx.srv.cfg.Releases = runner pub, _ := appconfig.ParsePublicKey(fx.srv.cfg.SecretsPub, "test") remaining, changed, err := fx.srv.rotateWebhookBudget(context.Background(), pub) if err != nil { t.Fatal(err) } if remaining != 0 || changed != 0 || runner.puts.Load() != 0 { t.Fatalf("rotation = remaining %d changed %d puts %d, want 0/0/0", remaining, changed, runner.puts.Load()) } if _, ok := appVars(t, fx, "titogo")[objstore.DefaultEnvPrefix+"_SCAN_CALLBACK_SECRET"]; ok { t.Fatal("rotation delivered a callback secret to a revoked app") } rec := storedRecord(t, fx, "titogo") if rec.Scanning.Enabled != nil || rec.Scanning.WebhookKeyID != before { t.Fatalf("rotation rewrote a revoked scanning block: %+v", rec.Scanning) } } func TestPerAppRotateRefusesARevokeLandedBeforeThePermit(t *testing.T) { fx, token, _ := rotationRaceFixture(t, "code-rotate-race-app-revoke") before := storedRecord(t, fx, "titogo").Scanning.WebhookGen runner := &racingRunner{Store: fx.rel, action: "store-scan-callback-rotate", race: func(context.Context) { revokeBehindTheDoor(t, fx) }} fx.srv.cfg.Releases = runner s, o := postJSON(t, scanURL(fx, "titogo")+"/callback-key/rotate", map[string]any{}, token) if s != http.StatusConflict || o["error"] != "scanning is not enabled on this store — run `carlos store scan grant` first" { t.Fatalf("per-app rotate after revoke = %d %v, want 409", s, o) } if runner.puts.Load() != 0 { t.Fatalf("per-app rotate after revoke wrote %d objects, want none", runner.puts.Load()) } if _, ok := appVars(t, fx, "titogo")[objstore.DefaultEnvPrefix+"_SCAN_CALLBACK_SECRET"]; ok { t.Fatal("per-app rotate delivered a callback secret to a revoked app") } if got := storedRecord(t, fx, "titogo").Scanning.WebhookGen; got != before { t.Fatalf("webhook_gen = %d, want unchanged %d", got, before) } }