@@ -147,6 +147,37 @@ func TestV1ReadSurfaceExcludesUnitConfig(t *testing.T) {
147147 assert .Equal (t , 1 , resp .JSON200 .TotalCount , "TotalCount must reflect the filtered set, not the raw count" )
148148 })
149149
150+ // and:
151+ // - v1 mutations on the unit_config plan are rejected BEFORE any side effect. The guard runs
152+ // pre-commit in the service (not in the response mapper), so a 400 means nothing committed —
153+ // the plan is left exactly as it was. This is the core OM-409 follow-up: no commit-then-400.
154+ runRequired (t , "v1 plan mutations reject the unit_config plan with no side effect" , func (t * testing.T ) {
155+ // archive: the guard runs before the active-status check, so an active plan is still rejected.
156+ archiveResp , err := v1 .ArchivePlanWithResponse (t .Context (), ucPlan .ID )
157+ require .NoError (t , err )
158+ require .Equal (t , http .StatusBadRequest , archiveResp .StatusCode (), "body: %s" , string (archiveResp .Body ))
159+ assert .Contains (t , string (archiveResp .Body ), unitConfigNotRepresentableCode , "archive 400 must carry the typed unit_config code" )
160+
161+ // publish: the guard runs before the publishable-status check.
162+ publishResp , err := v1 .PublishPlanWithResponse (t .Context (), ucPlan .ID )
163+ require .NoError (t , err )
164+ require .Equal (t , http .StatusBadRequest , publishResp .StatusCode (), "body: %s" , string (publishResp .Body ))
165+ assert .Contains (t , string (publishResp .Body ), unitConfigNotRepresentableCode , "publish 400 must carry the typed unit_config code" )
166+
167+ // next: the guard runs before the next draft version is created.
168+ nextResp , err := v1 .NextPlanWithResponse (t .Context (), ucPlan .ID )
169+ require .NoError (t , err )
170+ require .Equal (t , http .StatusBadRequest , nextResp .StatusCode (), "body: %s" , string (nextResp .Body ))
171+ assert .Contains (t , string (nextResp .Body ), unitConfigNotRepresentableCode , "next 400 must carry the typed unit_config code" )
172+
173+ // no side effect: after three rejected mutations the plan is unchanged — still the single
174+ // active version it was published as (a commit-then-400 mapper would have left it archived).
175+ after , err := c .Plans .Get (t .Context (), ucPlan .ID )
176+ c .requireStatus (http .StatusOK , err )
177+ require .NotNil (t , after )
178+ assert .Equal (t , v3sdk .PlanStatusActive , after .Status , "the plan must remain active — no mutation may have committed" )
179+ })
180+
150181 // and:
151182 // - a v1 subscription created from the unit_config plan BY KEY still succeeds (read ≠ subscribe; the
152183 // server rates it correctly per OM-399, only the read surfaces are restricted).
@@ -185,4 +216,16 @@ func TestV1ReadSurfaceExcludesUnitConfig(t *testing.T) {
185216 require .NotNil (t , resp .ApplicationproblemJSON400 )
186217 assert .Contains (t , string (resp .Body ), unitConfigNotRepresentableCode , "the 400 must carry the typed unit_config code" )
187218 })
219+
220+ // and:
221+ // - v1 add-on read surfaces of that subscription are rejected too (the sub-addon list serializes
222+ // the subscription's items, so it must not silently strip). Guarded on the subscription's spec,
223+ // so it rejects even with no add-ons attached.
224+ runRequired (t , "v1 subscription add-on LIST rejects the unit_config subscription" , func (t * testing.T ) {
225+ require .NotEmpty (t , subscriptionID )
226+ resp , err := v1 .ListSubscriptionAddonsWithResponse (t .Context (), subscriptionID )
227+ require .NoError (t , err )
228+ require .Equal (t , http .StatusBadRequest , resp .StatusCode (), "body: %s" , string (resp .Body ))
229+ assert .Contains (t , string (resp .Body ), unitConfigNotRepresentableCode , "the 400 must carry the typed unit_config code" )
230+ })
188231}
0 commit comments