| 1 | package hostops |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "slices" |
| 7 | "testing" |
| 8 | |
| 9 | "github.com/carlosframework/platform/internal/release" |
| 10 | ) |
| 11 | |
| 12 | func relayPut(before, after string) MutationTarget { |
| 13 | return MutationTarget{Surface: "bucket", Method: "Put", Key: RelayAuthorityKey, Before: []byte(before), After: []byte(after)} |
| 14 | } |
| 15 | |
| 16 | // control/relay.json is per-app authority: only the entries a rewrite adds, |
| 17 | // removes or changes need permits, and a written_at-only rewrite is the one |
| 18 | // non-app write of that key. |
| 19 | func TestRelayAuthorityClassifiesChangedEntries(t *testing.T) { |
| 20 | ctx := t.Context() |
| 21 | const ( |
| 22 | a = `{"account":"bab","app":"demo","incarnation":"i1","regions":["eu-west-1"]}` |
| 23 | b = `{"account":"bab","app":"kass","incarnation":"i2","regions":["eu-west-1"]}` |
| 24 | b2 = `{"account":"bab","app":"kass","incarnation":"i3","regions":["eu-west-1"]}` |
| 25 | bR = `{"account":"bab","app":"kass","incarnation":"i2","regions":["us-east-1"]}` |
| 26 | c = `{"account":"zed","app":"meet","incarnation":"i4","regions":["eu-west-1"]}` |
| 27 | at1 = `"written_at":"2026-10-07T12:00:00Z"` |
| 28 | at2 = `"written_at":"2026-10-07T12:05:00Z"` |
| 29 | ) |
| 30 | doc := func(at string, apps ...string) string { |
| 31 | out := `{"apps":[` |
| 32 | for i, e := range apps { |
| 33 | if i > 0 { |
| 34 | out += "," |
| 35 | } |
| 36 | out += e |
| 37 | } |
| 38 | return out + `],` + at + `}` |
| 39 | } |
| 40 | for _, tc := range []struct { |
| 41 | name string |
| 42 | before, after string |
| 43 | want []AppScope |
| 44 | }{ |
| 45 | {"written-at-only", doc(at1, a, b), doc(at2, a, b), nil}, |
| 46 | {"order-only", doc(at1, a, b), doc(at2, b, a), nil}, |
| 47 | {"first-publish", "", doc(at1, a), []AppScope{{"bab", "demo"}}}, |
| 48 | {"added", doc(at1, a), doc(at2, c, a), []AppScope{{"zed", "meet"}}}, |
| 49 | {"removed", doc(at1, a, b), doc(at2, a), []AppScope{{"bab", "kass"}}}, |
| 50 | {"incarnation", doc(at1, a, b), doc(at2, a, b2), []AppScope{{"bab", "kass"}}}, |
| 51 | {"regions", doc(at1, a, b), doc(at2, a, bR), []AppScope{{"bab", "kass"}}}, |
| 52 | {"many", doc(at1, a, b), doc(at2, c, b2), []AppScope{{"bab", "demo"}, {"bab", "kass"}, {"zed", "meet"}}}, |
| 53 | } { |
| 54 | t.Run(tc.name, func(t *testing.T) { |
| 55 | c, err := ClassifyMutation(ctx, nil, nil, relayPut(tc.before, tc.after)) |
| 56 | if err != nil { |
| 57 | t.Fatal(err) |
| 58 | } |
| 59 | if !slices.Equal(c.Scopes, tc.want) { |
| 60 | t.Fatalf("scopes %v, want %v", c.Scopes, tc.want) |
| 61 | } |
| 62 | if (len(tc.want) == 0) != (c.NonApp != "") { |
| 63 | t.Fatalf("non-app reason %q with scopes %v", c.NonApp, c.Scopes) |
| 64 | } |
| 65 | got, err := RelayAuthorityScopes([]byte(tc.before), []byte(tc.after)) |
| 66 | if err != nil || !slices.Equal(got, tc.want) { |
| 67 | t.Fatalf("publisher scopes %v %v disagree with the boundary", got, err) |
| 68 | } |
| 69 | }) |
| 70 | } |
| 71 | for name, tgt := range map[string]MutationTarget{ |
| 72 | "malformed-after": relayPut(doc(at1, a), `{"apps":[`), |
| 73 | "malformed-before": relayPut(`not json`, doc(at1, a)), |
| 74 | "duplicate-after": relayPut(doc(at1, a), doc(at2, a, a)), |
| 75 | "duplicate-before": relayPut(doc(at1, a, a), doc(at2, a)), |
| 76 | "unknown-field": relayPut(doc(at1, a), `{"apps":[],"extra":1,`+at2+`}`), |
| 77 | "unknown-entry-key": relayPut(doc(at1, a), `{"apps":[{"account":"bab","app":"demo","incarnation":"i1","regions":[],"x":1}],`+at2+`}`), |
| 78 | "invalid-identity": relayPut(doc(at1, a), doc(at2, `{"account":"","app":"demo","incarnation":"i1","regions":[]}`)), |
| 79 | "trailing-value": relayPut(doc(at1, a), doc(at2, a)+`{}`), |
| 80 | } { |
| 81 | if _, err := ClassifyMutation(ctx, nil, nil, tgt); !errors.Is(err, ErrIdentity) { |
| 82 | t.Fatalf("%s: %v", name, err) |
| 83 | } |
| 84 | } |
| 85 | del := MutationTarget{Surface: "bucket", Method: "Delete", Key: RelayAuthorityKey, Before: []byte(doc(at1, a))} |
| 86 | if _, err := ClassifyMutation(ctx, nil, nil, del); !errors.Is(err, ErrUnclassified) { |
| 87 | t.Fatalf("aggregate delete: %v", err) |
| 88 | } |
| 89 | if _, err := ClassifyMutation(ctx, nil, nil, MutationTarget{Method: "Put", Key: "control/relay.json.tmp", After: []byte(`{}`)}); !errors.Is(err, ErrUnclassified) { |
| 90 | t.Fatalf("no prefix admission: %v", err) |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | // control/relay-issuer.json is the operator's regional issuer pointer: an |
| 95 | // exact non-app family when its body is a valid {region: kid} map. |
| 96 | func TestRelayIssuerIsExactNonApp(t *testing.T) { |
| 97 | ctx := t.Context() |
| 98 | put := func(body string) MutationTarget { |
| 99 | return MutationTarget{Surface: "bucket", Method: "Put", Key: RelayIssuerKey, After: []byte(body)} |
| 100 | } |
| 101 | c, err := ClassifyMutation(ctx, nil, nil, put(`{"eu-west-1":"k2","us-east-1":"k1"}`)) |
| 102 | if err != nil || c.NonApp == "" || len(c.Scopes) != 0 { |
| 103 | t.Fatalf("issuer pointer: %+v %v", c, err) |
| 104 | } |
| 105 | for _, body := range []string{`{"eu-west-1":"TOOLONGKID"}`, `{"EU":"k1"}`, `[]`, `null`, `{"eu-west-1":"k1"} {}`} { |
| 106 | if _, err := ClassifyMutation(ctx, nil, nil, put(body)); !errors.Is(err, ErrIdentity) { |
| 107 | t.Fatalf("%s: %v", body, err) |
| 108 | } |
| 109 | } |
| 110 | if _, err := ClassifyMutation(ctx, nil, nil, MutationTarget{Method: "Delete", Key: RelayIssuerKey}); !errors.Is(err, ErrUnclassified) { |
| 111 | t.Fatalf("issuer delete: %v", err) |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | func boundRunner(t *testing.T, apps ...string) (context.Context, release.Store, *release.DirStore) { |
| 116 | t.Helper() |
| 117 | ctx := t.Context() |
| 118 | w, raw, _ := testWriter(t) |
| 119 | for _, app := range apps { |
| 120 | seedAdmission(t, raw, "bab", app, openAdmission(app+"-id")) |
| 121 | } |
| 122 | check, err := NewProductionMutationCheck(raw, w) |
| 123 | if err != nil { |
| 124 | t.Fatal(err) |
| 125 | } |
| 126 | data, err := NewMutationBoundary(true, raw, check) |
| 127 | if err != nil { |
| 128 | t.Fatal(err) |
| 129 | } |
| 130 | if err = BindMutationRunner(data, w, testOwner, func(_ context.Context, _, app string) (string, error) { return app + "-id", nil }); err != nil { |
| 131 | t.Fatal(err) |
| 132 | } |
| 133 | return ctx, data, raw |
| 134 | } |
| 135 | |
| 136 | // A raw write made under a nested scope is a possible late effect of every |
| 137 | // enclosing operation, so a failing multi-scope call holds both apps. |
| 138 | func TestNestedScopeErrorHoldsEveryEnclosingApp(t *testing.T) { |
| 139 | ctx, data, raw := boundRunner(t, "demo", "kass") |
| 140 | failure := errors.New("ambiguous nested failure") |
| 141 | err := release.WithAppMutation(ctx, data, "bab", "demo", "outer", func(outer context.Context) error { |
| 142 | return release.WithAppMutation(outer, data, "bab", "kass", "inner", func(inner context.Context) error { |
| 143 | if _, err := data.Put(inner, "apps/bab/kass/test.json", []byte(`{}`), ""); err != nil { |
| 144 | return err |
| 145 | } |
| 146 | return failure |
| 147 | }) |
| 148 | }) |
| 149 | if !errors.Is(err, failure) { |
| 150 | t.Fatalf("nested failure: %v", err) |
| 151 | } |
| 152 | for _, app := range []string{"demo", "kass"} { |
| 153 | a, _ := ReadAdmission(ctx, raw, "bab", app, app+"-id") |
| 154 | if a.Value.State != "closed" || a.Value.Op == nil { |
| 155 | t.Fatalf("%s released after an ambiguous nested effect: %+v", app, a.Value) |
| 156 | } |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | // A nested acquisition that fails issues nothing, so the outer operation |
| 161 | // still releases as unapplied. |
| 162 | func TestFailedNestedAcquisitionReleasesOuter(t *testing.T) { |
| 163 | ctx, data, raw := boundRunner(t, "demo") |
| 164 | err := release.WithAppMutation(ctx, data, "bab", "demo", "outer", func(outer context.Context) error { |
| 165 | // kass has no admission: the nested acquisition refuses before any effect. |
| 166 | return release.WithAppMutation(outer, data, "bab", "kass", "inner", func(context.Context) error { |
| 167 | t.Fatal("unadmitted nested scope ran") |
| 168 | return nil |
| 169 | }) |
| 170 | }) |
| 171 | if err == nil { |
| 172 | t.Fatal("nested acquisition of an unadmitted app succeeded") |
| 173 | } |
| 174 | a, _ := ReadAdmission(ctx, raw, "bab", "demo", "demo-id") |
| 175 | if a.Value.State != "open" || a.Value.Op != nil { |
| 176 | t.Fatalf("unissued outer operation retained authority: %+v", a.Value) |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | // A context whose permit for an app is no longer valid cannot open a fresh |
| 181 | // operation for that app by nesting. |
| 182 | func TestInvalidNestedPermitRefuses(t *testing.T) { |
| 183 | ctx, data, raw := boundRunner(t, "demo") |
| 184 | var saved context.Context |
| 185 | if err := release.WithAppMutation(ctx, data, "bab", "demo", "outer", func(pctx context.Context) error { |
| 186 | saved = pctx |
| 187 | return nil |
| 188 | }); err != nil { |
| 189 | t.Fatal(err) |
| 190 | } |
| 191 | before, _ := ReadAdmission(ctx, raw, "bab", "demo", "demo-id") |
| 192 | called := false |
| 193 | err := release.WithAppMutation(saved, data, "bab", "demo", "nested", func(context.Context) error { |
| 194 | called = true |
| 195 | return nil |
| 196 | }) |
| 197 | after, _ := ReadAdmission(ctx, raw, "bab", "demo", "demo-id") |
| 198 | if called || !errors.Is(err, ErrPending) || before.ETag != after.ETag { |
| 199 | t.Fatalf("retired permit acquired a new operation: called=%v err=%v", called, err) |
| 200 | } |
| 201 | } |
| 202 | |