package hostops import ( "context" "encoding/json" "errors" "strings" "testing" "time" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/release" ) type lossScopeCapture struct { release.Store w *lossWorld state, key, id string body []byte etag string ctx context.Context } func (s *lossScopeCapture) Put(ctx context.Context, key string, body []byte, etag string) (string, error) { if key == s.key { loss, err := hostclaim.ReadLossRecord(ctx, s.Store, s.w.pub, s.id) if err == nil && loss.Record.State == s.state { s.ctx = ctx s.body = append([]byte(nil), body...) s.etag = etag return "", errors.New("captured permitted write") } } return s.Store.Put(ctx, key, body, etag) } func scopedLossFixture(t *testing.T, state, kind string) (*lossWorld, *lossScopedStore, *lossScopeCapture) { t.Helper() w := newLossWorld(t, "landing", nil) sub := w.submission(w.inspect(t, "uk-box")) o := w.claim(t) key := w.key if kind == "claim" { key = w.claimK } capture := &lossScopeCapture{Store: w.raw, w: w, state: state, key: key, id: sub.Confirmation} cw, err := NewControlWriter(capture, w.pub, func() time.Time { return w.clock }) if err != nil { t.Fatal(err) } _, err = cw.DeclareMoveLoss(t.Context(), w.signer, lossBucket, "operator", "bab", "app", lossHost, "uk-box", sub) if err == nil || capture.body == nil { t.Fatalf("write not captured: %v", err) } loss, err := hostclaim.ReadLossRecord(t.Context(), w.raw, w.pub, sub.Confirmation) if err != nil || loss.Record.State != state { t.Fatalf("signed declaration: %+v %v", loss, err) } s := &lossScopedStore{raw: w.raw, pub: w.pub, now: func() time.Time { return w.clock }, b: lossBinding{bucket: lossBucket, lossID: sub.Confirmation, host: lossHost, opID: lossOp, kind: hostclaim.KindLanding, box: "uk-box", claimETag: o.ETag, incarnation: w.inc, account: "bab", app: "app"}} return w, s, capture } func TestJ4ScopedWritesComparePreparedAndCommittedTransitions(t *testing.T) { for _, state := range []string{"prepared", "committed"} { for _, kind := range []string{"claim", "record"} { t.Run(state+"/"+kind, func(t *testing.T) { _, s, capture := scopedLossFixture(t, state, kind) if _, err := s.Put(capture.ctx, capture.key, capture.body, capture.etag); err != nil { t.Fatalf("permitted write refused: %v", err) } for _, field := range []string{"identity", "transition", "sequence"} { t.Run(field, func(t *testing.T) { w, s, capture := scopedLossFixture(t, state, kind) var altered map[string]any dec := json.NewDecoder(strings.NewReader(string(capture.body))) dec.UseNumber() if err := dec.Decode(&altered); err != nil { t.Fatal(err) } if kind == "claim" { switch field { case "identity": altered["holder"].(map[string]any)["account"] = "other" case "transition": altered["op"].(map[string]any)["exec"] = "foreign" case "sequence": altered["gen"] = 99999 } } else { switch field { case "identity": altered["account"] = "other" case "transition": altered["move"].(map[string]any)["reason"] = "other" case "sequence": altered["record_seq"] = 99999 } } body, err := json.Marshal(altered) if err != nil { t.Fatal(err) } before := lossSnapshot(t, w) if _, err = s.Put(capture.ctx, capture.key, body, capture.etag); !errors.Is(err, errMoveLossScope) { t.Fatalf("expected scope comparison refusal, got %v", err) } if lossSnapshot(t, w) != before { t.Fatal("altered write landed") } if _, err = s.Put(capture.ctx, capture.key, capture.body, capture.etag); err != nil { t.Fatalf("equivalent valid write refused after negative: %v", err) } }) } }) } } } func TestJ4ScopedLossRecordRejectsRegressionAndEvidenceChanges(t *testing.T) { for _, change := range []string{"regress", "evidence", "registration"} { t.Run(change, func(t *testing.T) { w, s, capture := scopedLossFixture(t, "committed", "claim") loss, err := hostclaim.ReadLossRecord(t.Context(), w.raw, w.pub, capture.id) if err != nil { t.Fatal(err) } next := loss.Record switch change { case "regress": next.State = "prepared" next.ResultClaimGen = 0 case "evidence": next.Actor = "another-operator" case "registration": // Removing the app admission ends the scoped transport's authority. key, _ := AdmissionKey("bab", "app") if err := w.raw.Delete(t.Context(), key); err != nil { t.Fatal(err) } } body, err := release.SignRootEnvelope(w.signer, next) if err != nil { t.Fatal(err) } key, _ := hostclaim.LossRecordKey(capture.id) if _, err = s.Put(t.Context(), key, body, loss.ETag); err == nil { t.Fatal("invalid progression forwarded") } }) } } func TestJ4ScopedAcceptanceRejectsOtherBucket(t *testing.T) { w, s, capture := scopedLossFixture(t, "prepared", "record") key := "control/host-loss-acceptances/" + capture.id + ".json" raw, _, err := w.raw.Get(t.Context(), key) if err != nil { t.Fatal(err) } var a struct { V int `json:"v"` Bucket string `json:"bucket"` RosterETag string `json:"roster_etag"` Loss hostclaim.LossRecord `json:"loss"` } if err := release.VerifyRootEnvelope(w.pub, raw, &a); err != nil { t.Fatal(err) } a.Bucket = "foreign" body, err := release.SignRootEnvelope(w.signer, a) if err != nil { t.Fatal(err) } if err = s.acceptance(t.Context(), body, ""); !errors.Is(err, errMoveLossScope) { t.Fatalf("other bucket: %v", err) } if err = s.acceptance(t.Context(), raw, ""); err != nil { t.Fatalf("correct bucket refused: %v", err) } }