package console import ( "context" "crypto/ed25519" "crypto/rand" "encoding/json" "strings" "testing" "time" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostops" "github.com/carlosframework/platform/internal/release" ) func TestClaimHoldRecoveryInvalidatesUnusedStartAndPreservesActiveHold(t *testing.T) { for _, active := range []bool{false, true} { t.Run(map[bool]string{false: "unused", true: "active"}[active], func(t *testing.T) { ctx := context.Background() store := release.NewDirStore(t.TempDir()) pub, key, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } signer := release.RootSigner(key) enrollment := strings.Repeat("a", 32) before, err := hostclaim.Read(ctx, store, "app.example") if err != nil { t.Fatal(err) } roster := hostactors.Roster{V: 1, Revision: 1, DeploymentBucket: "bucket", Boxes: map[string]hostactors.Box{"box": {Enrollment: enrollment, Namespace: "platform", BoxID: "box", Label: "box", Region: "ie", Pool: "default", Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "eu-west-1", InstanceID: "i-box"}, EvidenceSHA256: strings.Repeat("b", 64), State: "active"}}, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{"hold": {ID: "hold", Host: before.Host, OpID: "op", ExpectedClaimETag: before.ETag, FloorSHA256: strings.Repeat("f", 64), Actors: map[string]string{"box:box": enrollment}, Releases: map[string]hostactors.Report{"box:box": {Enrollment: enrollment}}}}, Activations: map[string]hostactors.Reservation{}} wire, err := release.SignRootEnvelope(signer, roster) if err != nil { t.Fatal(err) } if _, err = store.Put(ctx, hostactors.Key, wire, ""); err != nil { t.Fatal(err) } if active { if _, err = hostclaim.Update(ctx, store, before, func(c *hostclaim.Claim) error { now := time.Now().UTC() n := hostclaim.NewHolder{Family: "instance", Key: "apps/bab/app/instances/app.example.json", Account: "bab", App: "app", Host: before.Host, Region: "ie", CreatedAt: now} body, _ := json.Marshal(map[string]any{"host": before.Host, "account": "bab", "app": "app", "created_at": now}) if err := hostclaim.BeginCreate(n, body, "op", "test", now)(c); err != nil { return err } c.Op.Intent["actors_hold"] = "hold" return nil }); err != nil { t.Fatal(err) } } directory := hostactors.Directory{Store: store, PublicKey: pub, Bucket: "bucket"} s := &Server{cfg: Config{HostClaim: true, ClaimAuthority: &hostops.ProductionAuthority{Store: store, Directory: directory, Signer: signer}}} if err = s.SweepClaimHolds(ctx); err != nil { t.Fatal(err) } after, err := directory.Read(ctx) if err != nil { t.Fatal(err) } _, exists := after.Roster.Holds["hold"] if exists != active { t.Fatalf("hold exists=%v active=%v", exists, active) } if !active { if _, err = hostclaim.Update(ctx, store, before, func(*hostclaim.Claim) error { return nil }); err == nil { t.Fatal("unused start observation remained usable") } } }) } }