| index d41463f7d..24f91218d 100644 |
| --- a/internal/objstore/walk_test.go |
| +++ b/internal/objstore/walk_test.go |
| @@ -4,6 +4,7 @@ import ( |
| "context" |
| "crypto/ecdh" |
| "errors" |
| + "os" |
| "strings" |
| "testing" |
| "time" |
| @@ -992,3 +993,158 @@ func TestRotateRefusesRetryAgainstAnotherRotation(t *testing.T) { |
| t.Fatalf("the competing rotation was clobbered: %+v, %v", stored.Provisioned, err) |
| } |
| } |
| + |
| +// teardownFixture grants a fresh store and returns its record — the estate a |
| +// teardown has to dismantle. |
| +func teardownFixture(t *testing.T, s release.Store, pub *ecdh.PublicKey, p *MemProvisioner) *Record { |
| + t.Helper() |
| + declare(t, s, "bab", "titogo", "TITO_S3") |
| + rec, err := Grant(context.Background(), s, p, testDeployment(), pub, "bab", "titogo", "op", "", time.Now().UTC()) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return rec |
| +} |
| + |
| +func TestTeardownFullSweep(t *testing.T) { |
| + ctx := context.Background() |
| + s := release.NewDirStore(t.TempDir()) |
| + _, pub := testSealKeypair(t) |
| + p := NewMemProvisioner("eu-west-1") |
| + rec := teardownFixture(t, s, pub, p) |
| + // A bucket with data in it is the case that matters: DeleteBucket alone |
| + // would fail on a real backend, so the walk must empty it first. |
| + p.Buckets[rec.Provisioned.Bucket]["uploads/one.txt"] = []byte("payload") |
| + |
| + if err := Teardown(ctx, s, p, "bab", "titogo"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if len(p.Buckets) != 0 { |
| + t.Fatalf("buckets after teardown = %v", p.Buckets) |
| + } |
| + if len(p.Users) != 0 { |
| + t.Fatalf("users after teardown = %v", p.Users) |
| + } |
| + for name := range mustVars(t, s, "bab", "titogo") { |
| + if strings.HasPrefix(name, "TITO_S3_") { |
| + t.Fatalf("delivered var %q survived teardown", name) |
| + } |
| + } |
| + stored, _, err := Get(ctx, s, "bab", "titogo") |
| + if err != nil || stored != nil { |
| + t.Fatalf("record after teardown = %+v, %v", stored, err) |
| + } |
| +} |
| + |
| +func TestTeardownResumable(t *testing.T) { |
| + for _, method := range []string{"EmptyBucket", "DeleteBucket", "DeleteUser"} { |
| + t.Run(method, func(t *testing.T) { |
| + ctx := context.Background() |
| + s := release.NewDirStore(t.TempDir()) |
| + _, pub := testSealKeypair(t) |
| + p := NewMemProvisioner("eu-west-1") |
| + rec := teardownFixture(t, s, pub, p) |
| + p.Buckets[rec.Provisioned.Bucket]["uploads/one.txt"] = []byte("payload") |
| + |
| + p.FailNext[method] = errors.New("injected: " + method + " failed") |
| + if err := Teardown(ctx, s, p, "bab", "titogo"); err == nil { |
| + t.Fatalf("%s failure: Teardown returned no error", method) |
| + } |
| + // The record is the ONLY name for what is left standing (§9), so a |
| + // half-failed teardown must leave it intact and still provisioned. |
| + half, _, err := Get(ctx, s, "bab", "titogo") |
| + if err != nil || half == nil || half.Provisioned == nil { |
| + t.Fatalf("record after %s failure = %+v, %v", method, half, err) |
| + } |
| + if half.Provisioned.Bucket != rec.Provisioned.Bucket || half.Provisioned.IAMUser != rec.Provisioned.IAMUser { |
| + t.Fatalf("record coordinates changed after %s failure: %+v", method, half.Provisioned) |
| + } |
| + |
| + if err := Teardown(ctx, s, p, "bab", "titogo"); err != nil { |
| + t.Fatalf("resume after %s failure: %v", method, err) |
| + } |
| + if len(p.Buckets) != 0 || len(p.Users) != 0 { |
| + t.Fatalf("estate after resume: buckets %v users %v", p.Buckets, p.Users) |
| + } |
| + for name := range mustVars(t, s, "bab", "titogo") { |
| + if strings.HasPrefix(name, "TITO_S3_") { |
| + t.Fatalf("delivered var %q survived teardown", name) |
| + } |
| + } |
| + stored, _, err := Get(ctx, s, "bab", "titogo") |
| + if err != nil || stored != nil { |
| + t.Fatalf("record after resume = %+v, %v", stored, err) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestTeardownDeclaredOnly(t *testing.T) { |
| + ctx := context.Background() |
| + s := release.NewDirStore(t.TempDir()) |
| + p := NewMemProvisioner("eu-west-1") |
| + declare(t, s, "bab", "titogo", "TITO_S3") |
| + |
| + if err := Teardown(ctx, s, p, "bab", "titogo"); err != nil { |
| + t.Fatal(err) |
| + } |
| + stored, _, err := Get(ctx, s, "bab", "titogo") |
| + if err != nil || stored != nil { |
| + t.Fatalf("declared-only record after teardown = %+v, %v", stored, err) |
| + } |
| + if len(p.Buckets) != 0 || len(p.Users) != 0 { |
| + t.Fatalf("declared-only teardown touched the backend: %v %v", p.Buckets, p.Users) |
| + } |
| +} |
| + |
| +func TestTeardownIdempotentOnAbsent(t *testing.T) { |
| + ctx := context.Background() |
| + s := release.NewDirStore(t.TempDir()) |
| + p := NewMemProvisioner("eu-west-1") |
| + if err := Teardown(ctx, s, p, "bab", "titogo"); err != nil { |
| + t.Fatalf("teardown with no record: %v", err) |
| + } |
| + if len(p.Buckets) != 0 || len(p.Users) != 0 { |
| + t.Fatalf("teardown with no record touched the backend: %v %v", p.Buckets, p.Users) |
| + } |
| +} |
| + |
| +func TestTeardownConfigObjectAbsent(t *testing.T) { |
| + ctx := context.Background() |
| + s := release.NewDirStore(t.TempDir()) |
| + p := NewMemProvisioner("eu-west-1") |
| + // A provisioned record whose app was never configured — hand-crafted, so |
| + // no config.json exists at all. appconfig REFUSES an unset-only apply on a |
| + // nonexistent object, and that refusal must not strand the estate. |
| + if _, err := p.EnsureBucket(ctx, "carlos-store-tito-bab-titogo"); err != nil { |
| + t.Fatal(err) |
| + } |
| + arn, err := p.EnsureUser(ctx, "carlos-store-bab-titogo", UserPath, "arn:aws:s3:::carlos-store-tito-bab-titogo") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + rec := &Record{ |
| + Declared: &Declared{EnvPrefix: "TITO_S3", At: time.Now().UTC(), By: "member@example.com"}, |
| + Grant: &GrantBlock{Backend: "aws", Bucket: "carlos-store-tito-bab-titogo", At: time.Now().UTC(), By: "op"}, |
| + Provisioned: &Provisioned{Bucket: "carlos-store-tito-bab-titogo", Region: "eu-west-1", |
| + IAMUser: "carlos-store-bab-titogo", UserARN: arn, KeyID: "MEMKEY1", At: time.Now().UTC()}, |
| + } |
| + if _, err := Put(ctx, s, "bab", "titogo", rec, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + |
| + if err := Teardown(ctx, s, p, "bab", "titogo"); err != nil { |
| + t.Fatalf("teardown with no config object: %v", err) |
| + } |
| + if len(p.Buckets) != 0 || len(p.Users) != 0 { |
| + t.Fatalf("estate after teardown: buckets %v users %v", p.Buckets, p.Users) |
| + } |
| + stored, _, err := Get(ctx, s, "bab", "titogo") |
| + if err != nil || stored != nil { |
| + t.Fatalf("record after teardown = %+v, %v", stored, err) |
| + } |
| + // And no phantom config object was minted on the way through. |
| + if _, _, err := s.Get(ctx, appconfig.Key("bab", "titogo")); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatalf("teardown minted a config object: %v", err) |
| + } |
| +} |