| index 2ae0866fc..e1a0959f6 100644 |
| --- a/internal/release/production_test.go |
| +++ b/internal/release/production_test.go |
| @@ -13,6 +13,8 @@ import ( |
| "crypto/rand" |
| "encoding/base64" |
| "encoding/json" |
| + "errors" |
| + "os" |
| "strings" |
| "testing" |
| "time" |
| @@ -202,6 +204,68 @@ func TestProductionFlagRefusesTamperedPayload(t *testing.T) { |
| } |
| } |
| |
| +// A trashed app cannot be flagged production in EITHER direction: restore |
| +// first. This holds even though the flag would refuse to WRITE anything |
| +// destructive itself — the point is that a production flag appearing on a |
| +// trashed app contradicts the flag's own promise (production = never |
| +// deletable), so the door closes before the CAS write, not after. |
| +func TestSetProductionRefusesTrashedApp(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, _ := testKeys(t) |
| + |
| + if err := TrashApp(ctx, s, "bab", "derp", "paul@keymail.dev", time.Now()); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := SetProduction(ctx, s, priv, "bab", "derp", true, "paul@keymail.dev"); err == nil { |
| + t.Fatal("SetProduction(true) on a trashed app succeeded, want a refusal") |
| + } |
| + // The OFF direction is refused too — there is nothing legitimate to do to |
| + // a trashed app's flag; restore first. |
| + if err := SetProduction(ctx, s, priv, "bab", "derp", false, "paul@keymail.dev"); err == nil { |
| + t.Fatal("SetProduction(false) on a trashed app succeeded, want a refusal") |
| + } |
| + // No flag object should have been written. |
| + if _, _, err := s.Get(ctx, ProductionKey("bab", "derp")); err == nil { |
| + t.Fatal("a refused SetProduction on a trashed app wrote the flag object anyway") |
| + } |
| +} |
| + |
| +// An unreadable trash marker refuses too — the usual direction: a caller |
| +// about to declare production must never act as though a marker it cannot |
| +// verify simply isn't there. Uses a store that fails ONLY the tombstone |
| +// key (everything else, including the eventual production.json write, |
| +// would otherwise succeed) so this test actually pins the new trash-marker |
| +// check rather than any generic "the store is broken" refusal. |
| +func TestSetProductionRefusesUnreadableTrashMarker(t *testing.T) { |
| + priv, _ := testKeys(t) |
| + real := NewDirStore(t.TempDir()) |
| + s := &tombstoneReadFailStore{Store: real, key: AppTombstoneKey("bab", "derp"), err: errors.New("bucket on fire")} |
| + if err := SetProduction(context.Background(), s, priv, "bab", "derp", true, "paul@keymail.dev"); err == nil { |
| + t.Fatal("SetProduction with an unreadable trash marker succeeded, want a refusal") |
| + } |
| + // Confirm nothing else about the store is broken — the refusal really is |
| + // the trash-marker check, not a store that happens to fail everything. |
| + if _, _, err := real.Get(context.Background(), ProductionKey("bab", "derp")); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatalf("production flag written despite an unreadable trash marker: err=%v", err) |
| + } |
| +} |
| + |
| +// tombstoneReadFailStore fails Get for exactly one key and delegates |
| +// everything else to the wrapped Store. |
| +type tombstoneReadFailStore struct { |
| + Store |
| + key string |
| + err error |
| +} |
| + |
| +func (t *tombstoneReadFailStore) Get(ctx context.Context, key string) ([]byte, string, error) { |
| + if key == t.key { |
| + return nil, "", t.err |
| + } |
| + return t.Store.Get(ctx, key) |
| +} |
| + |
| // timeNowTestHelper is a stamped, truncated now — what signProductionFlag |
| // itself writes — for tests that hand-build payloads. |
| func timeNowTestHelper() time.Time { return time.Now().UTC().Truncate(time.Second) } |