Skip to main content

jmap_types/
error.rs

1//! RFC 8620 §3.6 JMAP method-level error type ([`JmapError`]).
2
3use crate::Id;
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7/// JMAP method-level error, serializable for inclusion in `methodResponses`.
8///
9/// See RFC 8620 §3.6.2 for the standard error type strings.
10/// The JSON key is `"type"` (not `"error_type"`) per RFC 8620.
11#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)]
12#[error("{error_type}")]
13#[non_exhaustive]
14pub struct JmapError {
15    /// Error type string per RFC 8620 §3.6.2.
16    #[serde(rename = "type")]
17    pub error_type: String,
18    /// Human-readable description. Omitted from JSON when `None`.
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub description: Option<String>,
21    /// The id of the existing record. Only set for `"alreadyExists"` (RFC 8620 §5.4 MUST).
22    #[serde(rename = "existingId", skip_serializing_if = "Option::is_none")]
23    pub existing_id: Option<Id>,
24    /// Maximum `maxChanges` value the server will accept. Only set for `"tooManyChanges"` (RFC 8620 §9.6.1 MUST).
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub limit: Option<u64>,
27}
28
29impl JmapError {
30    /// RFC 8620 §3.6.2 — "invalidArguments"
31    pub fn invalid_arguments(desc: impl Into<String>) -> Self {
32        Self {
33            error_type: "invalidArguments".into(),
34            description: Some(desc.into()),
35            existing_id: None,
36            limit: None,
37        }
38    }
39
40    /// RFC 8620 §3.6.2 — "forbidden"
41    pub fn forbidden() -> Self {
42        Self {
43            error_type: "forbidden".into(),
44            description: None,
45            existing_id: None,
46            limit: None,
47        }
48    }
49
50    /// RFC 8620 §5.3 — "notFound"
51    pub fn not_found() -> Self {
52        Self {
53            error_type: "notFound".into(),
54            description: None,
55            existing_id: None,
56            limit: None,
57        }
58    }
59
60    /// RFC 8620 §5.1 — "accountNotFound"
61    pub fn account_not_found() -> Self {
62        Self {
63            error_type: "accountNotFound".into(),
64            description: None,
65            existing_id: None,
66            limit: None,
67        }
68    }
69
70    /// RFC 8620 §5.1 — "accountNotSupportedByMethod"
71    pub fn account_not_supported_by_method() -> Self {
72        Self {
73            error_type: "accountNotSupportedByMethod".into(),
74            description: None,
75            existing_id: None,
76            limit: None,
77        }
78    }
79
80    /// RFC 8620 §5.1 — "accountReadOnly"
81    pub fn account_read_only() -> Self {
82        Self {
83            error_type: "accountReadOnly".into(),
84            description: None,
85            existing_id: None,
86            limit: None,
87        }
88    }
89
90    /// RFC 8620 §3.6.2 — "serverUnavailable"
91    pub fn server_unavailable() -> Self {
92        Self {
93            error_type: "serverUnavailable".into(),
94            description: None,
95            existing_id: None,
96            limit: None,
97        }
98    }
99
100    /// RFC 8620 §3.6.2 — "serverFail"
101    pub fn server_fail(desc: impl Into<String>) -> Self {
102        Self {
103            error_type: "serverFail".into(),
104            description: Some(desc.into()),
105            existing_id: None,
106            limit: None,
107        }
108    }
109
110    /// RFC 8620 §3.6.2 — "serverPartialFail"
111    pub fn server_partial_fail() -> Self {
112        Self {
113            error_type: "serverPartialFail".into(),
114            description: None,
115            existing_id: None,
116            limit: None,
117        }
118    }
119
120    /// RFC 8620 §3.6.2 — "unknownMethod"
121    pub fn unknown_method() -> Self {
122        Self {
123            error_type: "unknownMethod".into(),
124            description: None,
125            existing_id: None,
126            limit: None,
127        }
128    }
129
130    /// RFC 8620 §3.6.2 — "invalidResultReference"
131    pub fn invalid_result_reference() -> Self {
132        Self {
133            error_type: "invalidResultReference".into(),
134            description: None,
135            existing_id: None,
136            limit: None,
137        }
138    }
139
140    /// RFC 8620 §5.2 and §5.6 — "cannotCalculateChanges"
141    pub fn cannot_calculate_changes() -> Self {
142        Self {
143            error_type: "cannotCalculateChanges".into(),
144            description: None,
145            existing_id: None,
146            limit: None,
147        }
148    }
149
150    /// RFC 8620 §5.3 — "stateMismatch"
151    pub fn state_mismatch() -> Self {
152        Self {
153            error_type: "stateMismatch".into(),
154            description: None,
155            existing_id: None,
156            limit: None,
157        }
158    }
159
160    /// RFC 8620 §5.3 — "tooLarge"
161    pub fn too_large() -> Self {
162        Self {
163            error_type: "tooLarge".into(),
164            description: None,
165            existing_id: None,
166            limit: None,
167        }
168    }
169
170    /// RFC 8620 §5.1 and §5.3 — "requestTooLarge"
171    pub fn request_too_large() -> Self {
172        Self {
173            error_type: "requestTooLarge".into(),
174            description: None,
175            existing_id: None,
176            limit: None,
177        }
178    }
179
180    /// RFC 8620 §5.3 — "overQuota"
181    pub fn over_quota() -> Self {
182        Self {
183            error_type: "overQuota".into(),
184            description: None,
185            existing_id: None,
186            limit: None,
187        }
188    }
189
190    /// RFC 8620 §5.3 — "rateLimit"
191    pub fn rate_limit() -> Self {
192        Self {
193            error_type: "rateLimit".into(),
194            description: None,
195            existing_id: None,
196            limit: None,
197        }
198    }
199
200    /// RFC 8620 §5.3 — "invalidPatch"
201    pub fn invalid_patch() -> Self {
202        Self {
203            error_type: "invalidPatch".into(),
204            description: None,
205            existing_id: None,
206            limit: None,
207        }
208    }
209
210    /// RFC 8620 §5.3 — "willDestroy"
211    pub fn will_destroy() -> Self {
212        Self {
213            error_type: "willDestroy".into(),
214            description: None,
215            existing_id: None,
216            limit: None,
217        }
218    }
219
220    /// RFC 8620 §5.3 — "invalidProperties"
221    pub fn invalid_properties() -> Self {
222        Self {
223            error_type: "invalidProperties".into(),
224            description: None,
225            existing_id: None,
226            limit: None,
227        }
228    }
229
230    /// RFC 8620 §5.3 — "singleton"
231    pub fn singleton() -> Self {
232        Self {
233            error_type: "singleton".into(),
234            description: None,
235            existing_id: None,
236            limit: None,
237        }
238    }
239
240    /// RFC 8620 §5.5 — "unsupportedFilter"
241    pub fn unsupported_filter() -> Self {
242        Self {
243            error_type: "unsupportedFilter".into(),
244            description: None,
245            existing_id: None,
246            limit: None,
247        }
248    }
249
250    /// RFC 8620 §5.5 — "anchorNotFound"
251    pub fn anchor_not_found() -> Self {
252        Self {
253            error_type: "anchorNotFound".into(),
254            description: None,
255            existing_id: None,
256            limit: None,
257        }
258    }
259
260    /// RFC 8620 §5.4 — "alreadyExists"
261    ///
262    /// `existing_id` is the id of the record that already exists in the target account.
263    /// Per RFC 8620 §5.4, this field MUST be present on the SetError object.
264    pub fn already_exists(existing_id: Id) -> Self {
265        Self {
266            error_type: "alreadyExists".into(),
267            description: None,
268            existing_id: Some(existing_id),
269            limit: None,
270        }
271    }
272
273    /// RFC 8620 §5.4 — "fromAccountNotFound"
274    pub fn from_account_not_found() -> Self {
275        Self {
276            error_type: "fromAccountNotFound".into(),
277            description: None,
278            existing_id: None,
279            limit: None,
280        }
281    }
282
283    /// RFC 8620 §5.4 — "fromAccountNotSupportedByMethod"
284    pub fn from_account_not_supported_by_method() -> Self {
285        Self {
286            error_type: "fromAccountNotSupportedByMethod".into(),
287            description: None,
288            existing_id: None,
289            limit: None,
290        }
291    }
292
293    /// RFC 8620 §5.5 — "unsupportedSort"
294    pub fn unsupported_sort() -> Self {
295        Self {
296            error_type: "unsupportedSort".into(),
297            description: None,
298            existing_id: None,
299            limit: None,
300        }
301    }
302
303    /// RFC 8620 §5.6 — "tooManyChanges"
304    ///
305    /// Always prefer [`too_many_changes_with_limit`][Self::too_many_changes_with_limit],
306    /// which includes the server's limit so the client knows the
307    /// maximum `maxChanges` value to use on retry. RFC 8620 §9.6.1
308    /// requires `limit` to be present on the wire; this bare constructor
309    /// produces a wire form that violates that requirement.
310    #[deprecated(
311        note = "always use too_many_changes_with_limit to include the limit per RFC 8620 §9.6.1"
312    )]
313    pub fn too_many_changes() -> Self {
314        Self {
315            error_type: "tooManyChanges".into(),
316            description: None,
317            existing_id: None,
318            limit: None,
319        }
320    }
321
322    /// Returns a `tooManyChanges` error with the server's limit included.
323    ///
324    /// Per RFC 8620 §9.6.1, the `limit` field MUST be present so the client
325    /// knows the maximum `maxChanges` value to use on retry.
326    pub fn too_many_changes_with_limit(limit: u64) -> Self {
327        Self {
328            error_type: "tooManyChanges".into(),
329            description: None,
330            existing_id: None,
331            limit: Some(limit),
332        }
333    }
334
335    /// RFC 8620 §3.6.1 — "notJSON" (request-level error)
336    ///
337    /// The request body was not valid JSON or did not have `application/json` content type.
338    pub fn not_json() -> Self {
339        Self {
340            error_type: "notJSON".into(),
341            description: None,
342            existing_id: None,
343            limit: None,
344        }
345    }
346
347    /// RFC 8620 §3.6.1 — "notRequest" (request-level error)
348    ///
349    /// The request parsed as JSON but did not match the JMAP Request object shape.
350    pub fn not_request() -> Self {
351        Self {
352            error_type: "notRequest".into(),
353            description: None,
354            existing_id: None,
355            limit: None,
356        }
357    }
358
359    /// RFC 8620 §3.6.1 — "limit" (request-level error)
360    ///
361    /// The request was rejected because it would exceed a capability limit such as
362    /// `maxCallsInRequest` or `maxSizeRequest`.
363    ///
364    /// `limit_name` is the name of the exceeded limit (e.g. `"maxCallsInRequest"`).
365    /// The HTTP layer MUST forward this name as the `"limit"` property in the
366    /// RFC 7807 Problem Details response body.  The name is stored in
367    /// [`description`][JmapError::description] for that purpose.
368    ///
369    /// **Invariant**: always construct limit errors with this function, never by
370    /// setting `error_type = "limit"` and `description` manually.  The HTTP
371    /// response layer (`jmap-server::RequestError`) reads `description` to
372    /// populate the RFC-required `"limit"` field; a missing description produces
373    /// an invalid response.
374    pub fn limit(limit_name: impl Into<String>) -> Self {
375        Self {
376            error_type: "limit".into(),
377            description: Some(limit_name.into()),
378            existing_id: None,
379            limit: None,
380        }
381    }
382
383    /// RFC 8620 §3.6.1 — "unknownCapability" (request-level error)
384    ///
385    /// The request used a capability URI not recognized by this server.
386    ///
387    /// Always prefer [`unknown_capability_with_detail`][Self::unknown_capability_with_detail],
388    /// which includes the failing URI so clients can act on it.
389    #[deprecated(
390        note = "always use unknown_capability_with_detail to include the URI in the error"
391    )]
392    pub fn unknown_capability() -> Self {
393        Self {
394            error_type: "unknownCapability".into(),
395            description: None,
396            existing_id: None,
397            limit: None,
398        }
399    }
400
401    /// "unknownCapability" with the failing URI surfaced to the client.
402    ///
403    /// Always use this instead of [`unknown_capability()`][Self::unknown_capability].
404    ///
405    /// The URI is stored in [`description`][JmapError::description].  The HTTP layer
406    /// (`jmap-server::RequestError`) reads `description` to populate the `"detail"` field
407    /// in the RFC 7807 problem-details response body — the same mechanism used by
408    /// [`limit()`][Self::limit] for its limit-name payload.
409    ///
410    /// **Invariant**: always construct unknownCapability errors that carry a URI with this
411    /// function, never by setting `description` manually.  A missing or incorrect description
412    /// means the client never learns which capability it requested that the server does not
413    /// support.
414    pub fn unknown_capability_with_detail(uri: impl Into<String>) -> Self {
415        Self {
416            error_type: "unknownCapability".into(),
417            description: Some(uri.into()),
418            existing_id: None,
419            limit: None,
420        }
421    }
422
423    /// Create a `JmapError` with a custom or extension error type string.
424    ///
425    /// Use this when propagating a server error whose `type` value is not one of
426    /// the RFC 8620 standard types, or in tests that need to construct an
427    /// arbitrary `JmapError` value.
428    pub fn custom(error_type: impl Into<String>) -> Self {
429        Self {
430            error_type: error_type.into(),
431            description: None,
432            existing_id: None,
433            limit: None,
434        }
435    }
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441
442    // Independent oracle: RFC 8620 §3.6.2 and §5.x specify these exact type strings.
443
444    #[test]
445    fn invalid_arguments_serializes_type_and_description() {
446        let e = JmapError::invalid_arguments("ids field is required");
447        let json = serde_json::to_string(&e).unwrap();
448        assert!(
449            json.contains("\"type\""),
450            "must use 'type' key per RFC 8620"
451        );
452        assert!(json.contains("\"invalidArguments\""));
453        assert!(json.contains("\"description\""));
454        assert!(json.contains("ids field is required"));
455    }
456
457    #[test]
458    fn forbidden_omits_description() {
459        let e = JmapError::forbidden();
460        let json = serde_json::to_string(&e).unwrap();
461        assert!(json.contains("\"forbidden\""));
462        assert!(
463            !json.contains("\"description\""),
464            "None description must be omitted"
465        );
466    }
467
468    #[test]
469    fn not_found_type_string() {
470        let e = JmapError::not_found();
471        let json = serde_json::to_string(&e).unwrap();
472        assert!(json.contains("\"notFound\""));
473        assert!(!json.contains("\"description\""));
474    }
475
476    #[test]
477    fn account_not_found_type_string() {
478        let e = JmapError::account_not_found();
479        let json = serde_json::to_string(&e).unwrap();
480        assert!(json.contains("\"accountNotFound\""));
481        assert!(!json.contains("\"description\""));
482    }
483
484    #[test]
485    fn account_not_supported_by_method_type_string() {
486        let e = JmapError::account_not_supported_by_method();
487        let json = serde_json::to_string(&e).unwrap();
488        assert!(json.contains("\"accountNotSupportedByMethod\""));
489    }
490
491    #[test]
492    fn account_read_only_type_string() {
493        let e = JmapError::account_read_only();
494        let json = serde_json::to_string(&e).unwrap();
495        assert!(json.contains("\"accountReadOnly\""));
496    }
497
498    #[test]
499    fn server_unavailable_type_string() {
500        let e = JmapError::server_unavailable();
501        let json = serde_json::to_string(&e).unwrap();
502        assert!(json.contains("\"serverUnavailable\""));
503    }
504
505    #[test]
506    fn server_fail_includes_description() {
507        let e = JmapError::server_fail("internal error");
508        let json = serde_json::to_string(&e).unwrap();
509        assert!(json.contains("\"serverFail\""));
510        assert!(json.contains("internal error"));
511    }
512
513    #[test]
514    fn server_partial_fail_type_string() {
515        let e = JmapError::server_partial_fail();
516        let json = serde_json::to_string(&e).unwrap();
517        assert!(json.contains("\"serverPartialFail\""));
518    }
519
520    #[test]
521    fn unknown_method_type_string() {
522        let e = JmapError::unknown_method();
523        let json = serde_json::to_string(&e).unwrap();
524        assert!(json.contains("\"unknownMethod\""));
525    }
526
527    #[test]
528    fn invalid_result_reference_type_string() {
529        let e = JmapError::invalid_result_reference();
530        let json = serde_json::to_string(&e).unwrap();
531        assert!(json.contains("\"invalidResultReference\""));
532    }
533
534    #[test]
535    fn cannot_calculate_changes_type_string() {
536        let e = JmapError::cannot_calculate_changes();
537        let json = serde_json::to_string(&e).unwrap();
538        assert!(json.contains("\"cannotCalculateChanges\""));
539    }
540
541    #[test]
542    fn state_mismatch_type_string() {
543        let e = JmapError::state_mismatch();
544        let json = serde_json::to_string(&e).unwrap();
545        assert!(json.contains("\"stateMismatch\""));
546    }
547
548    #[test]
549    fn too_large_type_string() {
550        let e = JmapError::too_large();
551        let json = serde_json::to_string(&e).unwrap();
552        assert!(json.contains("\"tooLarge\""));
553    }
554
555    #[test]
556    fn request_too_large_type_string() {
557        let e = JmapError::request_too_large();
558        let json = serde_json::to_string(&e).unwrap();
559        assert!(json.contains("\"requestTooLarge\""));
560        assert!(!json.contains("\"description\""));
561    }
562
563    #[test]
564    fn over_quota_type_string() {
565        let e = JmapError::over_quota();
566        let json = serde_json::to_string(&e).unwrap();
567        assert!(json.contains("\"overQuota\""));
568    }
569
570    #[test]
571    fn rate_limit_type_string() {
572        let e = JmapError::rate_limit();
573        let json = serde_json::to_string(&e).unwrap();
574        assert!(json.contains("\"rateLimit\""));
575    }
576
577    #[test]
578    fn invalid_patch_type_string() {
579        let e = JmapError::invalid_patch();
580        let json = serde_json::to_string(&e).unwrap();
581        assert!(json.contains("\"invalidPatch\""));
582    }
583
584    #[test]
585    fn will_destroy_type_string() {
586        let e = JmapError::will_destroy();
587        let json = serde_json::to_string(&e).unwrap();
588        assert!(json.contains("\"willDestroy\""));
589    }
590
591    #[test]
592    fn invalid_properties_type_string() {
593        let e = JmapError::invalid_properties();
594        let json = serde_json::to_string(&e).unwrap();
595        assert!(json.contains("\"invalidProperties\""));
596    }
597
598    #[test]
599    fn singleton_type_string() {
600        let e = JmapError::singleton();
601        let json = serde_json::to_string(&e).unwrap();
602        assert!(json.contains("\"singleton\""));
603    }
604
605    #[test]
606    fn unsupported_filter_type_string() {
607        let e = JmapError::unsupported_filter();
608        let json = serde_json::to_string(&e).unwrap();
609        assert!(json.contains("\"unsupportedFilter\""));
610    }
611
612    #[test]
613    fn anchor_not_found_type_string() {
614        let e = JmapError::anchor_not_found();
615        let json = serde_json::to_string(&e).unwrap();
616        assert!(json.contains("\"anchorNotFound\""));
617    }
618
619    // Oracle: RFC 8620 §5.4 — alreadyExists MUST include existingId of type Id.
620    #[test]
621    fn already_exists_includes_existing_id() {
622        let e = JmapError::already_exists(Id::from("abc123"));
623        let json = serde_json::to_string(&e).unwrap();
624        assert!(json.contains("\"alreadyExists\""));
625        assert!(json.contains("\"existingId\""));
626        assert!(json.contains("\"abc123\""));
627        assert!(!json.contains("\"description\""));
628    }
629
630    #[test]
631    fn from_account_not_found_type_string() {
632        let e = JmapError::from_account_not_found();
633        let json = serde_json::to_string(&e).unwrap();
634        assert!(json.contains("\"fromAccountNotFound\""));
635        assert!(!json.contains("\"description\""));
636    }
637
638    #[test]
639    fn from_account_not_supported_by_method_type_string() {
640        let e = JmapError::from_account_not_supported_by_method();
641        let json = serde_json::to_string(&e).unwrap();
642        assert!(json.contains("\"fromAccountNotSupportedByMethod\""));
643        assert!(!json.contains("\"description\""));
644    }
645
646    #[test]
647    fn unsupported_sort_type_string() {
648        let e = JmapError::unsupported_sort();
649        let json = serde_json::to_string(&e).unwrap();
650        assert!(json.contains("\"unsupportedSort\""));
651        assert!(!json.contains("\"description\""));
652    }
653
654    #[allow(deprecated)]
655    #[test]
656    fn too_many_changes_type_string() {
657        let e = JmapError::too_many_changes();
658        let json = serde_json::to_string(&e).unwrap();
659        assert!(json.contains("\"tooManyChanges\""));
660        assert!(!json.contains("\"description\""));
661    }
662
663    #[test]
664    fn too_many_changes_with_limit_serializes_limit_field() {
665        let err = JmapError::too_many_changes_with_limit(100);
666        let v = serde_json::to_value(&err).unwrap();
667        assert_eq!(v["type"], "tooManyChanges");
668        assert_eq!(v["limit"], 100u64);
669        assert!(v.get("description").is_none());
670    }
671
672    /// Pins the deprecated bare-constructor's wire form — the absence of
673    /// the `limit` field is the RFC 8620 §9.6.1 noncompliance the
674    /// deprecation was added to flag. If a future contributor changes
675    /// the bare constructor to populate `limit` with a placeholder, this
676    /// test would fail and force a deliberate review of whether the
677    /// deprecation should be removed at that time.
678    #[allow(deprecated)]
679    #[test]
680    fn too_many_changes_without_limit_has_no_limit_field() {
681        let err = JmapError::too_many_changes();
682        let v = serde_json::to_value(&err).unwrap();
683        assert_eq!(v["type"], "tooManyChanges");
684        assert!(v.get("limit").is_none());
685    }
686
687    #[test]
688    fn not_json_type_string() {
689        let e = JmapError::not_json();
690        let json = serde_json::to_string(&e).unwrap();
691        assert!(json.contains("\"notJSON\""));
692        assert!(!json.contains("\"description\""));
693    }
694
695    #[test]
696    fn not_request_type_string() {
697        let e = JmapError::not_request();
698        let json = serde_json::to_string(&e).unwrap();
699        assert!(json.contains("\"notRequest\""));
700        assert!(!json.contains("\"description\""));
701    }
702
703    #[test]
704    fn limit_includes_limit_name_in_description() {
705        // Oracle: the limit name is stored in description so the HTTP layer
706        // can forward it as the "limit" field in RFC 7807 Problem Details.
707        let e = JmapError::limit("maxCallsInRequest");
708        let json = serde_json::to_string(&e).unwrap();
709        assert!(json.contains("\"limit\""));
710        assert!(json.contains("\"maxCallsInRequest\""));
711    }
712
713    #[allow(deprecated)]
714    #[test]
715    fn unknown_capability_type_string() {
716        let e = JmapError::unknown_capability();
717        let json = serde_json::to_string(&e).unwrap();
718        assert!(json.contains("\"unknownCapability\""));
719        assert!(!json.contains("\"description\""));
720    }
721
722    // Oracle: RFC 8620 §3.6.1 — unknownCapability with detail includes the URI.
723    #[test]
724    fn unknown_capability_with_detail_includes_uri() {
725        let e = JmapError::unknown_capability_with_detail("urn:example:unknown");
726        assert_eq!(e.error_type, "unknownCapability");
727        assert_eq!(e.description.as_deref(), Some("urn:example:unknown"));
728    }
729
730    #[test]
731    fn custom_error_type_round_trips() {
732        let e = JmapError::custom("urn:example:customError");
733        assert_eq!(e.error_type, "urn:example:customError");
734        let json = serde_json::to_string(&e).unwrap();
735        assert!(json.contains("\"urn:example:customError\""));
736        let restored: JmapError = serde_json::from_str(&json).unwrap();
737        assert_eq!(restored.error_type, "urn:example:customError");
738    }
739
740    #[test]
741    fn round_trip_deserialize() {
742        // Verify the "type" rename survives a JSON round-trip.
743        let original = JmapError::invalid_arguments("test");
744        let json = serde_json::to_string(&original).unwrap();
745        let restored: JmapError = serde_json::from_str(&json).unwrap();
746        assert_eq!(restored.error_type, "invalidArguments");
747        assert_eq!(restored.description.as_deref(), Some("test"));
748    }
749
750    // Oracle: RFC 8620 §3.4.1 fixture — methodResponses[3] is ["error", {"type":"unknownMethod"}, "c3"]
751    #[test]
752    fn fixture_response_contains_unknown_method_error() {
753        let raw = include_str!("../tests/fixtures/rfc8620-response.json");
754        let v: serde_json::Value = serde_json::from_str(raw).expect("parse fixture");
755        let inv = &v["methodResponses"][3];
756        assert_eq!(inv[0], "error");
757        assert_eq!(inv[1]["type"], "unknownMethod");
758        assert_eq!(inv[2], "c3");
759    }
760}