package hostops import ( "context" "encoding/json" "strings" "testing" "time" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" ) func TestLifecycleRecordProductionWrapper(t *testing.T) { for _, mode := range []string{"moving", "restoring", "flip", "foreign-home", "matched-home", "changed-home", "missing-home", "wrong-op", "wrong-region", "absent", "stale-claim", "metadata", "wrong-kind", "admission-not-op", "unarchived", "restore-region", "restore-stale-source-region", "restore-elsewhere", "restore-no-journal", "restore-committed"} { t.Run(mode, func(t *testing.T) { ctx := context.Background() raw := release.NewDirStore(t.TempDir()) now := time.Now().UTC() host := "app.example" inc, err := hostclaim.Incarnation("bab", "app", host, now) if err != nil { t.Fatal(err) } key := provision.RecordKey("bab", "app", host) claim := hostclaim.Claim{V: 2, Host: host, Gen: 1, Holder: &hostclaim.Holder{Family: "instance", Account: "bab", App: "app", Key: key, Acquisition: "acq", CreatedAt: now, Incarnation: inc, Location: "instances/" + host}, Op: hostclaim.Op{ID: "move", Kind: hostclaim.KindMoving, Phase: "sealed", Intent: map[string]string{"app_id": "canonical", "admission_id": "move", "source": "ie-box", "target": "uk-box", "to": "uk"}}} // BeginAdmittedMove's intent carries no source region: a rollback // returns to the record's original Move.From. regID := "move" switch mode { case "foreign-home", "matched-home", "changed-home": claim.Op.Intent["home_region"] = "ie" case "admission-not-op": // A move registration under another id is drainable. regID = "registration" claim.Op.Intent["admission_id"] = regID case "restore-stale-source-region": // A pre-contract key neither steers nor blocks the rollback. claim.Op.Intent["source_region"] = "fr" } state := "cutting" if strings.HasPrefix(mode, "restore-") { claim.Op.Kind = hostclaim.KindLanding state = "landing" if mode == "restore-committed" { claim.Op.Phase = "committed" } } if mode == "restoring" { claim.Op.Kind = hostclaim.KindRestoring claim.Op.Phase = "" state = "rolling_back" } cb, _ := json.Marshal(claim) ck, _ := hostclaim.Key(host) if _, err = raw.Put(ctx, ck, cb, ""); err != nil { t.Fatal(err) } o, err := hostclaim.Read(ctx, raw, host) if err != nil { t.Fatal(err) } rec := map[string]any{"schema": 1, "host": host, "slug": "app", "account": "bab", "app": "app", "channel": "edge", "region": "ie", "created_at": now, "acq": "acq", "move": map[string]any{"id": "move", "incarnation": inc, "from": "ie", "to": "uk", "state": state, "cut_phase": "archived"}} if strings.HasPrefix(mode, "restore-") { rec["region"] = "uk" } body, _ := json.Marshal(rec) tag, err := raw.Put(ctx, key, body, "") if err != nil { t.Fatal(err) } admission := AppAdmission{V: 1, AppID: "canonical", Revision: 1, Epoch: 1, State: "closed", Registrations: map[string]Registration{regID: {Kind: "move", AppID: "canonical", Epoch: 1, Host: host, Incarnation: inc}}, Effects: map[string]Effect{}} if mode == "wrong-kind" { reg := admission.Registrations[regID] reg.Kind = "create" admission.Registrations[regID] = reg } ab, _ := json.Marshal(admission) if _, err = raw.Put(ctx, "apps/bab/app/admission.json", ab, ""); err != nil { t.Fatal(err) } mv := rec["move"].(map[string]any) mv["updated_at"] = now switch mode { case "flip", "unarchived": rec["region"] = "uk" mv["state"] = "landing" mv["cut_phase"] = "flipped" mv["flipped_at"] = now mv["landing_deadline"] = now.Add(time.Minute) if mode == "unarchived" { mv["cut_phase"] = "requested" } case "restore-region", "restore-stale-source-region", "restore-elsewhere", "restore-no-journal", "restore-committed": rec["region"] = "ie" if mode == "restore-elsewhere" { // Not the original Move.From. rec["region"] = "de" } mv["state"] = "rolling_back" if mode != "restore-no-journal" { mv["rollback"] = map[string]any{"id": "rollback", "requested_at": now, "target_phase": "releasing", "source_phase": "waiting", "release_deadline": now.Add(15 * time.Minute)} } case "wrong-op": mv["id"] = "another" case "wrong-region": rec["region"] = "de" case "absent": if err = raw.Delete(ctx, key); err != nil { t.Fatal(err) } case "stale-claim": if _, err = hostclaim.Update(ctx, raw, o, func(*hostclaim.Claim) error { return nil }); err != nil { t.Fatal(err) } case "metadata": rec["channel"] = "other" } body, _ = json.Marshal(rec) wrapped := conditionalRecordStore{raw: raw} if mode == "matched-home" || mode == "missing-home" { wrapped.homeRegion = "ie" } if mode == "changed-home" { wrapped.homeRegion = "uk" } permitted := context.WithValue(ctx, lifecycleRecordKey{}, lifecycleRecordPermit{o}) _, err = wrapped.Put(permitted, key, body, tag) // missing-home is an operation that started before any designation was // published: it stays alias-free and is not stranded by a later one. allowed := mode == "matched-home" || mode == "missing-home" || mode == "moving" || mode == "restoring" || mode == "flip" || mode == "restore-region" || mode == "restore-stale-source-region" if (err == nil) != allowed { t.Fatalf("allowed=%v err=%v", allowed, err) } if !allowed { return } if _, err = wrapped.Put(ctx, key, body, tag); err == nil { t.Fatal("ordinary writer crossed lifecycle gate") } }) } }