@@ -297,15 +297,19 @@ pub struct ConfigApplyView {
297297
298298// ── Contract-mirroring request/response types ────────────────────────────────────────────────
299299
300- #[ derive( Deserialize ) ]
301- struct ErrorEnvelope {
302- error : ErrorDetail ,
300+ /// The gateway's error envelope (`Error` in the spec): `{"error":{"code","message"}}`. Public so
301+ /// the conformance tests can assert the decoded shape against the committed schema; the CLI itself
302+ /// only renders `code`/`message` into a message.
303+ #[ derive( Debug , Deserialize , Serialize ) ]
304+ pub struct ErrorEnvelope {
305+ pub error : ErrorDetail ,
303306}
304307
305- #[ derive( Deserialize ) ]
306- struct ErrorDetail {
307- code : String ,
308- message : String ,
308+ /// One error detail: a `code` drawn from the spec's closed vocabulary plus a human `message`.
309+ #[ derive( Debug , Deserialize , Serialize ) ]
310+ pub struct ErrorDetail {
311+ pub code : String ,
312+ pub message : String ,
309313}
310314
311315/// `GET /info` — mirrors `contract::InfoView`.
@@ -610,151 +614,3 @@ pub struct HookPage {
610614 #[ serde( default ) ]
611615 pub next_cursor : Option < String > ,
612616}
613-
614- #[ cfg( test) ]
615- mod spec_shape_tests {
616- // These lock the 1.5.2 wire shapes this crate was realigned to (commit that repaired the
617- // 1.4.x drift). The spec-drift CI job only checks the committed openapi.json's VERSION string;
618- // it does NOT check that these structs still match the spec's schemas. Without these, a
619- // rebase reintroducing a 1.4.x field, or flipping `allowed_pools` back to a non-Option Vec,
620- // compiles clean and ships — exactly the regression class the realignment fixed.
621- use super :: * ;
622-
623- #[ test]
624- fn key_view_allowed_pools_null_is_all_pools ( ) {
625- // null (or omitted) allowed_pools => None => "all pools". A regression to `Vec<String>`
626- // would fail to deserialize null, or silently decode it as an empty (== NO pools) list.
627- let k: KeyView = serde_json:: from_str (
628- r#"{"id":"vk_1","name":"n","allowed_pools":null,"state":"active","enabled":true}"# ,
629- )
630- . expect ( "null allowed_pools must decode" ) ;
631- assert_eq ! (
632- k. allowed_pools, None ,
633- "null must mean all-pools (None), not []"
634- ) ;
635- }
636-
637- #[ test]
638- fn key_view_allowed_pools_empty_is_no_pools ( ) {
639- let k: KeyView =
640- serde_json:: from_str ( r#"{"id":"vk_1","name":"n","allowed_pools":[]}"# ) . unwrap ( ) ;
641- assert_eq ! (
642- k. allowed_pools,
643- Some ( Vec :: new( ) ) ,
644- "explicit [] must stay a distinct empty list (NO pools), never collapse to None"
645- ) ;
646- }
647-
648- #[ test]
649- fn created_key_view_carries_signed_token_not_secret ( ) {
650- // 1.5.0 credential is `token` (+ expires_at), NOT the 1.4.x `secret`. A revert to a
651- // required `secret` field would fail to decode this real-shaped response.
652- let c: CreatedKeyView = serde_json:: from_str (
653- r#"{"id":"vk_1","name":"n","token":"bbk_abc","expires_at":1785772871,"state":"active"}"# ,
654- )
655- . expect ( "token-shaped CreatedKeyView must decode" ) ;
656- assert_eq ! ( c. token, "bbk_abc" ) ;
657- assert_eq ! ( c. expires_at, 1785772871_u64 ) ;
658- }
659-
660- #[ test]
661- fn create_key_req_omits_none_fields_and_never_sends_legacy_budget ( ) {
662- // deny_unknown_fields on the server rejects any stray field. This pins the exact minimal
663- // body and fails if a 1.4.x budget/rpm/tpm field is ever reintroduced onto the struct.
664- let req = CreateKeyReq {
665- name : "svc" . into ( ) ,
666- allowed_pools : None ,
667- group : None ,
668- parent : None ,
669- expires_in : None ,
670- expires_at : None ,
671- labels : Default :: default ( ) ,
672- issue_aws_credential : false ,
673- } ;
674- let v: serde_json:: Value = serde_json:: to_value ( & req) . unwrap ( ) ;
675- let obj = v. as_object ( ) . unwrap ( ) ;
676- assert_eq ! (
677- obj. keys( ) . collect:: <Vec <_>>( ) ,
678- vec![ "name" ] ,
679- "an all-defaults CreateKeyReq must serialize to exactly {{name}} — any extra key is \
680- either a leaked None or a reintroduced legacy field the server will 400 on"
681- ) ;
682- for banned in [
683- "budget" ,
684- "budget_period" ,
685- "rpm_limit" ,
686- "tpm_limit" ,
687- "max_budget_cents" ,
688- ] {
689- assert ! (
690- !obj. contains_key( banned) ,
691- "legacy field {banned} must never serialize"
692- ) ;
693- }
694- }
695-
696- #[ test]
697- fn create_key_req_empty_allowed_pools_serializes_as_empty_array ( ) {
698- // The --no-pools path sets Some(vec![]); it must reach the wire as `[]`, not be dropped.
699- let req = CreateKeyReq {
700- name : "svc" . into ( ) ,
701- allowed_pools : Some ( Vec :: new ( ) ) ,
702- group : None ,
703- parent : None ,
704- expires_in : None ,
705- expires_at : None ,
706- labels : Default :: default ( ) ,
707- issue_aws_credential : false ,
708- } ;
709- let v: serde_json:: Value = serde_json:: to_value ( & req) . unwrap ( ) ;
710- assert_eq ! ( v[ "allowed_pools" ] , serde_json:: json!( [ ] ) ) ;
711- }
712-
713- #[ test]
714- fn rotated_key_view_token_and_secret_are_both_optional ( ) {
715- // The doc invariant is "exactly one of token or secret"; the type models both as optional
716- // so a token-only rotate decodes without a phantom `secret`.
717- let r: RotatedKeyView =
718- serde_json:: from_str ( r#"{"id":"vk_1","name":"n","token":"bbk_new"}"# ) . unwrap ( ) ;
719- assert_eq ! ( r. token. as_deref( ) , Some ( "bbk_new" ) ) ;
720- assert_eq ! ( r. secret, None ) ;
721- }
722-
723- #[ test]
724- fn inspect_view_carries_manifest_preview_shape ( ) {
725- // 1.5.2 added POST /plugins/inspect — a stateless preview whose body is the PluginSchemaView
726- // shape plus version/kind. This pins the fields busbar-admin renders so a future spec resync
727- // that drops/renames one (e.g. `trust`, `schema_error`, or the new `version`) fails loudly.
728- let v: InspectView = serde_json:: from_str (
729- r#"{"name":"acme-store","version":"1.2.3","kind":"store","trust":"unverified",
730- "source":"manifest","restart_required_default":true,
731- "schema":{"type":"object"},"schema_error":null}"# ,
732- )
733- . expect ( "inspect preview must decode" ) ;
734- assert_eq ! ( v. name, "acme-store" ) ;
735- assert_eq ! ( v. version. as_deref( ) , Some ( "1.2.3" ) ) ;
736- assert_eq ! ( v. kind. as_deref( ) , Some ( "store" ) ) ;
737- assert_eq ! ( v. trust, "unverified" ) ;
738- assert_eq ! ( v. restart_required_default, Some ( true ) ) ;
739- assert ! ( v. schema. is_some( ) ) ;
740- assert_eq ! ( v. schema_error, None ) ;
741- }
742-
743- #[ test]
744- fn inspect_view_tolerates_null_kind_and_absent_version ( ) {
745- // An unresolvable candidate reports kind/version/restart_required_default as null; the CLI
746- // must still decode (never refuse) so it can render the `trust`/`schema_error` verdict.
747- let v: InspectView = serde_json:: from_str (
748- r#"{"name":"bad","kind":null,"trust":"rejected","source":"manifest","schema":null,
749- "schema_error":"settings_schema is not valid JSON"}"# ,
750- )
751- . expect ( "a rejected/unresolvable candidate must still decode" ) ;
752- assert_eq ! ( v. kind, None ) ;
753- assert_eq ! ( v. version, None ) ;
754- assert_eq ! ( v. trust, "rejected" ) ;
755- assert_eq ! (
756- v. schema_error. as_deref( ) ,
757- Some ( "settings_schema is not valid JSON" )
758- ) ;
759- }
760- }
0 commit comments