1use std::sync::Arc;
16
17use crate::client::{ApiError, Pre};
18use crate::keys::KeyError;
19use crate::proto::api::ErrorCode;
20use crate::proto::audit::ChainError;
21use crate::proto::integrity::IntegrityError;
22use crate::seal::SealError;
23
24use crate::store::StoreError;
25
26pub mod code {
29 pub const INVALID_TOKEN: &str = "invalid_token";
31 pub const INVALID_TOKEN_FILE: &str = "invalid_token_file";
34 pub const INVALID_SERVER: &str = "invalid_server";
36 pub const INVALID_PROXY: &str = "invalid_proxy";
38 pub const MISSING_SERVER: &str = "missing_server";
40 pub const INVALID_ENVIRONMENT: &str = "invalid_environment";
42 pub const INVALID_NAME: &str = "invalid_name";
44 pub const INVALID_CONFIG: &str = "invalid_config";
46 pub const INVALID_AUDIT_HEAD: &str = "invalid_audit_head";
48 pub const CREDENTIAL_LITERAL: &str = "credential_literal";
50 pub const UNSUPPORTED_FORMAT: &str = "unsupported_format";
52 pub const NOT_TEXT: &str = "not_text";
54 pub const NOT_FOUND: &str = "not_found";
56 pub const FORBIDDEN: &str = "forbidden";
58 pub const CONFLICT: &str = "conflict";
60 pub const STALE_GENERATION: &str = "stale_generation";
62 pub const UNREACHABLE: &str = "unreachable";
64 pub const ERROR: &str = "error";
66 pub const BAD_SIGNATURE: &str = "bad_signature";
68 pub const KEY_MISMATCH: &str = "key_mismatch";
70 pub const BINDING_MISMATCH: &str = "binding_mismatch";
72 pub const VERSION_ROLLBACK: &str = "version_rollback";
74 pub const GENERATION_ROLLBACK: &str = "generation_rollback";
76 pub const AUDIT_MISMATCH: &str = "audit_mismatch";
78 pub const STORE: &str = "store";
80 pub const UNKNOWN_PATH: &str = "unknown_path";
82 pub const INVALID_PATH: &str = "invalid_path";
84 pub const INVALID_KIT: &str = "invalid_kit";
86 pub const UNKNOWN_PROJECT: &str = "unknown_project";
88 pub const PROJECT_EXISTS: &str = "project_exists";
90 pub const ENV_EXISTS: &str = "env_exists";
92 pub const IS_PROJECT: &str = "is_project";
94 pub const HAS_CHILDREN: &str = "has_children";
96 pub const VAULT_EXISTS: &str = "vault_exists";
98 pub const NO_KEY: &str = "no_key";
100 pub const SERVER_MISMATCH: &str = "server_mismatch";
102 pub const REKEY_IN_PROGRESS: &str = "rekey_in_progress";
104 pub const NO_REKEY: &str = "no_rekey";
106 pub const REKEY_FORWARD_ONLY: &str = "rekey_forward_only";
108 pub const DATA_DIR_IN_USE: &str = "data_dir_in_use";
111 pub const INVALID_DATA_DIR: &str = "invalid_data_dir";
114 pub const UNSUPPORTED_PROTOCOL: &str = "unsupported_protocol";
117 pub const UNSUPPORTED_BY_CLIENT: &str = "unsupported_by_client";
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126#[non_exhaustive]
127pub enum ErrorKind {
128 Auth,
131 Forbidden,
133 NotFound,
135 Conflict,
137 Transport,
139 Invalid,
142 Integrity,
145 Other,
147}
148
149impl ErrorKind {
150 pub fn as_str(self) -> &'static str {
152 match self {
153 ErrorKind::Auth => "auth",
154 ErrorKind::Forbidden => "forbidden",
155 ErrorKind::NotFound => "not_found",
156 ErrorKind::Conflict => "conflict",
157 ErrorKind::Transport => "transport",
158 ErrorKind::Invalid => "invalid",
159 ErrorKind::Integrity => "integrity",
160 ErrorKind::Other => "error",
161 }
162 }
163}
164
165pub fn kind_of(code: &str) -> ErrorKind {
174 if let Some(kind) = ErrorCode::parse(code).and_then(kind_of_protocol) {
175 return kind;
176 }
177 match code {
178 code::INVALID_TOKEN => ErrorKind::Auth,
179 code::UNREACHABLE => ErrorKind::Transport,
180 code::INVALID_TOKEN_FILE
181 | code::INVALID_SERVER
182 | code::INVALID_PROXY
183 | code::MISSING_SERVER
184 | code::INVALID_ENVIRONMENT
185 | code::INVALID_NAME
186 | code::INVALID_CONFIG
187 | code::INVALID_AUDIT_HEAD
188 | code::CREDENTIAL_LITERAL
189 | code::UNSUPPORTED_FORMAT
190 | code::NOT_TEXT
191 | code::UNKNOWN_PATH
192 | code::INVALID_PATH
193 | code::INVALID_KIT
194 | code::UNKNOWN_PROJECT
195 | code::PROJECT_EXISTS
196 | code::ENV_EXISTS
197 | code::IS_PROJECT
198 | code::HAS_CHILDREN
199 | code::VAULT_EXISTS
200 | code::NO_KEY
201 | code::SERVER_MISMATCH
202 | code::REKEY_IN_PROGRESS
203 | code::NO_REKEY
204 | code::REKEY_FORWARD_ONLY
205 | code::DATA_DIR_IN_USE
206 | code::INVALID_DATA_DIR
207 | code::UNSUPPORTED_PROTOCOL
208 | code::UNSUPPORTED_BY_CLIENT => ErrorKind::Invalid,
209 code::BINDING_MISMATCH | code::AUDIT_MISMATCH => ErrorKind::Integrity,
210 _ => ErrorKind::Other,
211 }
212}
213
214fn kind_of_protocol(code: ErrorCode) -> Option<ErrorKind> {
222 Some(match code {
223 ErrorCode::Unauthorized | ErrorCode::TokenExpired => ErrorKind::Auth,
224 ErrorCode::Forbidden => ErrorKind::Forbidden,
225 ErrorCode::NotFound => ErrorKind::NotFound,
226 ErrorCode::PreconditionFailed
227 | ErrorCode::Conflict
228 | ErrorCode::StaleGeneration
229 | ErrorCode::VersionMismatch => ErrorKind::Conflict,
230 ErrorCode::BadSignature
231 | ErrorCode::KeyMismatch
232 | ErrorCode::VersionRollback
233 | ErrorCode::GenerationRollback => ErrorKind::Integrity,
234 _ => return None,
235 })
236}
237
238pub fn kind_of_status(status: u16) -> ErrorKind {
241 match status {
242 401 => ErrorKind::Auth,
243 403 => ErrorKind::Forbidden,
244 404 => ErrorKind::NotFound,
245 409 | 412 => ErrorKind::Conflict,
246 _ => ErrorKind::Other,
247 }
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252#[non_exhaustive]
253pub enum Expected {
254 Absent,
256 Version(u64),
258}
259
260impl From<Pre> for Expected {
261 fn from(pre: Pre) -> Expected {
262 match pre {
263 Pre::Create | Pre::Revive(_) => Expected::Absent,
264 Pre::Update(v) => Expected::Version(v),
265 }
266 }
267}
268
269impl std::fmt::Display for Expected {
270 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271 match self {
272 Expected::Absent => f.write_str("no existing record"),
273 Expected::Version(v) => write!(f, "version {v}"),
274 }
275 }
276}
277
278#[derive(Debug, Clone, Copy, PartialEq, Eq)]
281#[non_exhaustive]
282pub enum IntegrityFailure {
283 BadSignature(&'static str),
285 KeyMismatch(&'static str),
287 BindingMismatch(&'static str),
289 VersionRollback {
291 known: u64,
293 got: u64,
295 },
296 GenerationRollback {
298 known: u32,
300 got: u32,
302 },
303 AuditMismatch,
305}
306
307impl IntegrityFailure {
308 pub fn code(&self) -> &'static str {
310 match self {
311 IntegrityFailure::BadSignature(_) => code::BAD_SIGNATURE,
312 IntegrityFailure::KeyMismatch(_) => code::KEY_MISMATCH,
313 IntegrityFailure::BindingMismatch(_) => code::BINDING_MISMATCH,
314 IntegrityFailure::VersionRollback { .. } => code::VERSION_ROLLBACK,
315 IntegrityFailure::GenerationRollback { .. } => code::GENERATION_ROLLBACK,
316 IntegrityFailure::AuditMismatch => code::AUDIT_MISMATCH,
317 }
318 }
319}
320
321impl From<&IntegrityError> for IntegrityFailure {
322 fn from(e: &IntegrityError) -> IntegrityFailure {
323 match *e {
324 IntegrityError::BadSignature(w) => IntegrityFailure::BadSignature(w),
325 IntegrityError::KeyMismatch(w) => IntegrityFailure::KeyMismatch(w),
326 IntegrityError::BindingMismatch(w) | IntegrityError::Malformed(w) => {
327 IntegrityFailure::BindingMismatch(w)
328 }
329 IntegrityError::VersionRollback { known, got } => {
330 IntegrityFailure::VersionRollback { known, got }
331 }
332 IntegrityError::GenerationRollback { known, got } => {
333 IntegrityFailure::GenerationRollback { known, got }
334 } }
337 }
338}
339
340#[derive(Debug, Clone)]
342#[non_exhaustive]
343pub enum Error {
344 Invalid {
347 code: &'static str,
349 message: String,
351 },
352 Auth {
355 code: String,
357 message: String,
359 },
360 Forbidden {
362 message: String,
364 },
365 NotFound {
367 message: String,
369 },
370 VaultNotFound {
373 label: String,
375 message: String,
377 },
378 UnknownPath {
380 path: String,
382 suggestion: Option<String>,
384 project_known: bool,
386 message: String,
388 },
389 Conflict {
391 name: String,
393 expected: Expected,
395 current: Option<u64>,
397 message: String,
399 },
400 StaleGeneration {
403 label: String,
405 message: String,
407 },
408 Integrity {
410 failure: IntegrityFailure,
412 message: String,
414 },
415 Server {
418 code: String,
420 status: u16,
422 message: String,
424 },
425 Transport {
427 server: String,
429 message: String,
431 source: Arc<dyn std::error::Error + Send + Sync>,
433 },
434 Store {
436 message: String,
438 source: Arc<StoreError>,
440 },
441 Other {
443 message: String,
445 },
446}
447
448impl Error {
449 pub fn code(&self) -> &str {
451 match self {
452 Error::Invalid { code, .. } => code,
453 Error::Auth { code, .. } | Error::Server { code, .. } => code,
454 Error::Forbidden { .. } => code::FORBIDDEN,
455 Error::NotFound { .. } | Error::VaultNotFound { .. } => code::NOT_FOUND,
456 Error::UnknownPath { .. } => code::UNKNOWN_PATH,
457 Error::Conflict { .. } => code::CONFLICT,
458 Error::StaleGeneration { .. } => code::STALE_GENERATION,
459 Error::Integrity { failure, .. } => failure.code(),
460 Error::Transport { .. } => code::UNREACHABLE,
461 Error::Store { .. } => code::STORE,
462 Error::Other { .. } => code::ERROR,
463 }
464 }
465
466 pub fn kind(&self) -> ErrorKind {
469 match self {
470 Error::Server { code, status, .. } if ErrorCode::parse(code).is_none() => {
471 kind_of_status(*status)
472 }
473 _ => kind_of(self.code()),
474 }
475 }
476
477 pub fn message(&self) -> &str {
479 match self {
480 Error::Invalid { message, .. }
481 | Error::Auth { message, .. }
482 | Error::Forbidden { message }
483 | Error::NotFound { message }
484 | Error::VaultNotFound { message, .. }
485 | Error::UnknownPath { message, .. }
486 | Error::Conflict { message, .. }
487 | Error::StaleGeneration { message, .. }
488 | Error::Integrity { message, .. }
489 | Error::Server { message, .. }
490 | Error::Transport { message, .. }
491 | Error::Store { message, .. }
492 | Error::Other { message } => message,
493 }
494 }
495
496 fn message_mut(&mut self) -> &mut String {
497 match self {
498 Error::Invalid { message, .. }
499 | Error::Auth { message, .. }
500 | Error::Forbidden { message }
501 | Error::NotFound { message }
502 | Error::VaultNotFound { message, .. }
503 | Error::UnknownPath { message, .. }
504 | Error::Conflict { message, .. }
505 | Error::StaleGeneration { message, .. }
506 | Error::Integrity { message, .. }
507 | Error::Server { message, .. }
508 | Error::Transport { message, .. }
509 | Error::Store { message, .. }
510 | Error::Other { message } => message,
511 }
512 }
513
514 pub(crate) fn context(mut self, context: impl std::fmt::Display) -> Error {
516 let m = self.message_mut();
517 *m = format!("{context}: {m}");
518 self
519 }
520
521 pub(crate) fn local(code: &'static str, message: impl Into<String>) -> Error {
522 Error::Invalid {
523 code,
524 message: message.into(),
525 }
526 }
527
528 pub(crate) fn forbidden(message: impl Into<String>) -> Error {
529 Error::Forbidden {
530 message: message.into(),
531 }
532 }
533
534 pub(crate) fn not_found(message: impl Into<String>) -> Error {
535 Error::NotFound {
536 message: message.into(),
537 }
538 }
539
540 pub(crate) fn other(message: impl Into<String>) -> Error {
541 Error::Other {
542 message: message.into(),
543 }
544 }
545
546 pub(crate) fn stale(label: &str, message: impl Into<String>) -> Error {
547 Error::StaleGeneration {
548 label: label.to_owned(),
549 message: message.into(),
550 }
551 }
552
553 pub(crate) fn unsupported(message: impl Into<String>) -> Error {
556 Error::Invalid {
557 code: code::UNSUPPORTED_BY_CLIENT,
558 message: message.into(),
559 }
560 }
561
562 pub(crate) fn integrity(label: &str, e: IntegrityError) -> Error {
564 Error::Integrity {
565 failure: IntegrityFailure::from(&e),
566 message: format!("{label}: integrity check failed: {e}"),
567 }
568 }
569
570 pub(crate) fn conflict(name: &str, pre: Pre, current: Option<u64>, deleting: bool) -> Error {
571 let expected = Expected::from(pre);
572 let current_text = current.map_or("none".to_owned(), |v| v.to_string());
573 let outcome = if deleting {
574 "nothing was deleted"
575 } else {
576 "nothing was overwritten"
577 };
578 Error::Conflict {
579 name: name.to_owned(),
580 expected,
581 current,
582 message: format!(
583 "{name} changed on the server: expected {expected}, the server has version {current_text}; {outcome}"
584 ),
585 }
586 }
587
588 pub(crate) fn seal(e: SealError, context: impl std::fmt::Display) -> Error {
590 let message = format!("{context}: {}", chain(&e));
591 match seal_class(&e) {
592 Class::Integrity(failure) => Error::Integrity { failure, message },
593 Class::Stale => Error::StaleGeneration {
594 label: context.to_string(),
595 message,
596 },
597 Class::Unsupported => Error::unsupported(message),
598 Class::Other => Error::Other { message },
599 }
600 }
601
602 pub(crate) fn key(e: KeyError, context: impl std::fmt::Display) -> Error {
604 let message = format!("{context}: {}", chain(&e));
605 match key_class(&e) {
606 Class::Integrity(failure) => Error::Integrity { failure, message },
607 Class::Unsupported => Error::unsupported(message),
608 _ => Error::Other { message },
609 }
610 }
611
612 pub(crate) fn audit_chain(e: ChainError, context: impl std::fmt::Display) -> Error {
616 let message = format!("{context}: {}", chain(&e));
617 match e {
618 ChainError::UnknownValue { .. } => Error::unsupported(message),
619 ChainError::HeadMismatch => Error::Integrity {
620 failure: IntegrityFailure::BindingMismatch("audit head"),
621 message,
622 },
623 _ => Error::Integrity {
624 failure: IntegrityFailure::AuditMismatch,
625 message,
626 },
627 }
628 }
629
630 pub(crate) fn api(e: ApiError, label: Option<&str>) -> Error {
633 let labelled = |m: String| match label {
634 Some(l) => format!("{l}: {m}"),
635 None => m,
636 };
637 match &e {
638 ApiError::Transport(t) => Error::Transport {
639 server: t.server().to_owned(),
640 message: labelled(t.to_string()),
641 source: t.cause().unwrap_or_else(|| Arc::new(t.clone())),
642 },
643 ApiError::UnsupportedProtocol { .. } => Error::Invalid {
644 code: code::UNSUPPORTED_PROTOCOL,
645 message: labelled(e.to_string()),
646 },
647 ApiError::Refused { status, .. } => {
648 let code = e.stable_code();
649 let message = labelled(e.to_string());
650 match kind_of(&code) {
651 ErrorKind::Auth => Error::Auth { code, message },
652 ErrorKind::Forbidden => Error::Forbidden { message },
653 ErrorKind::NotFound => Error::NotFound { message },
654 _ => Error::Server {
655 code,
656 status: *status,
657 message,
658 },
659 }
660 }
661 _ => Error::Other {
662 message: e.to_string(),
663 },
664 }
665 }
666
667 pub(crate) fn store(e: StoreError) -> Error {
669 Error::Store {
670 message: e.message().to_owned(),
671 source: Arc::new(e),
672 }
673 }
674
675 #[cfg(feature = "http")]
676 pub(crate) fn build(e: crate::client::BuildError) -> Error {
677 match e {
678 crate::client::BuildError::InvalidServer(m) => Error::local(code::INVALID_SERVER, m),
679 other => Error::local(code::INVALID_PROXY, other.to_string()),
680 }
681 }
682}
683
684impl std::fmt::Display for Error {
685 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
686 f.write_str(self.message())
687 }
688}
689
690impl std::error::Error for Error {
691 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
692 match self {
693 Error::Transport { source, .. } => Some(&**source),
694 Error::Store { source, .. } => Some(&**source),
695 _ => None,
696 }
697 }
698}
699
700pub(crate) fn chain(e: &dyn std::error::Error) -> String {
702 let mut out = e.to_string();
703 let mut cur = e.source();
704 while let Some(s) = cur {
705 out.push_str(": ");
706 out.push_str(&s.to_string());
707 cur = s.source();
708 }
709 out
710}
711
712enum Class {
713 Integrity(IntegrityFailure),
714 Stale,
715 Unsupported,
716 Other,
717}
718
719fn key_class(e: &KeyError) -> Class {
720 match e {
721 KeyError::Integrity(i) => Class::Integrity(IntegrityFailure::from(i)),
722 KeyError::Binding(w) => Class::Integrity(IntegrityFailure::BindingMismatch(w)),
723 KeyError::NameMismatch => {
724 Class::Integrity(IntegrityFailure::BindingMismatch("record name"))
725 }
726 KeyError::Kind { .. } => Class::Integrity(IntegrityFailure::BindingMismatch("bundle kind")),
727 KeyError::UnsupportedScope(_) => Class::Unsupported,
728 _ => Class::Other,
729 }
730}
731
732fn seal_class(e: &SealError) -> Class {
733 match e {
734 SealError::Integrity(i) => Class::Integrity(IntegrityFailure::from(i)),
735 SealError::Key(k) => key_class(k),
736 SealError::Binding(w) => Class::Integrity(IntegrityFailure::BindingMismatch(w)),
737 SealError::NameMismatch => {
738 Class::Integrity(IntegrityFailure::BindingMismatch("record name"))
739 }
740 SealError::KindMismatch { .. } => {
741 Class::Integrity(IntegrityFailure::BindingMismatch("record kind"))
742 }
743 SealError::StaleGeneration { .. } => Class::Stale,
744 SealError::UnsupportedByClient(_) => Class::Unsupported,
745 _ => Class::Other,
746 }
747}
748
749#[cfg(test)]
750mod tests {
751 use super::*;
752
753 #[test]
754 fn codes_map_to_kinds() {
755 assert_eq!(kind_of("token_expired"), ErrorKind::Auth);
756 assert_eq!(kind_of(code::INVALID_TOKEN), ErrorKind::Auth);
757 assert_eq!(kind_of("precondition_failed"), ErrorKind::Conflict);
758 assert_eq!(kind_of("version_mismatch"), ErrorKind::Conflict);
759 assert_eq!(kind_of(code::STALE_GENERATION), ErrorKind::Conflict);
760 assert_eq!(kind_of(code::CREDENTIAL_LITERAL), ErrorKind::Invalid);
761 assert_eq!(kind_of(code::UNKNOWN_PATH), ErrorKind::Invalid);
762 for c in [
763 code::BAD_SIGNATURE,
764 code::KEY_MISMATCH,
765 code::VERSION_ROLLBACK,
766 code::GENERATION_ROLLBACK,
767 code::AUDIT_MISMATCH,
768 ] {
769 assert_eq!(kind_of(c), ErrorKind::Integrity, "{c}");
770 }
771 assert_eq!(Error::forbidden("x").code(), code::FORBIDDEN);
772 }
773
774 #[test]
779 fn protocol_codes_classify_through_their_type() {
780 for (c, kind) in [
781 (ErrorCode::Unauthorized, ErrorKind::Auth),
782 (ErrorCode::TokenExpired, ErrorKind::Auth),
783 (ErrorCode::Forbidden, ErrorKind::Forbidden),
784 (ErrorCode::NotFound, ErrorKind::NotFound),
785 (ErrorCode::PreconditionFailed, ErrorKind::Conflict),
786 (ErrorCode::Conflict, ErrorKind::Conflict),
787 (ErrorCode::StaleGeneration, ErrorKind::Conflict),
788 (ErrorCode::VersionMismatch, ErrorKind::Conflict),
789 (ErrorCode::BadSignature, ErrorKind::Integrity),
790 (ErrorCode::KeyMismatch, ErrorKind::Integrity),
791 (ErrorCode::VersionRollback, ErrorKind::Integrity),
792 (ErrorCode::GenerationRollback, ErrorKind::Integrity),
793 ] {
794 assert_eq!(kind_of(c.as_str()), kind, "{c}");
795 }
796 assert_eq!(kind_of(ErrorCode::RateLimited.as_str()), ErrorKind::Other);
799 assert_eq!(kind_of(ErrorCode::Unavailable.as_str()), ErrorKind::Other);
800 }
801
802 #[test]
803 fn integrity_failures_keep_their_code_through_context() {
804 let e = Error::seal(
805 SealError::Integrity(IntegrityError::BadSignature("record")),
806 "acme/dev: version 2 of DB",
807 );
808 assert_eq!(
809 (e.code(), e.kind()),
810 (code::BAD_SIGNATURE, ErrorKind::Integrity)
811 );
812 assert!(e.message().starts_with("acme/dev: version 2 of DB"), "{e}");
813 assert!(matches!(
814 e,
815 Error::Integrity {
816 failure: IntegrityFailure::BadSignature("record"),
817 ..
818 }
819 ));
820 let stale = Error::seal(
821 SealError::StaleGeneration {
822 bundle: 1,
823 vault: 2,
824 },
825 "a",
826 );
827 assert_eq!(stale.code(), code::STALE_GENERATION);
828 }
829
830 #[test]
831 fn a_conflict_carries_both_versions() {
832 let e = Error::conflict("app", Pre::Update(7), Some(8), false);
833 assert!(matches!(
834 e,
835 Error::Conflict {
836 expected: Expected::Version(7),
837 current: Some(8),
838 ..
839 }
840 ));
841 assert_eq!(e.code(), code::CONFLICT);
842 assert_eq!(
843 e.message(),
844 "app changed on the server: expected version 7, the server has version 8; nothing was overwritten"
845 );
846 assert_eq!(e.clone().context("acme/dev").kind(), ErrorKind::Conflict);
847 }
848
849 #[test]
852 fn an_unknown_server_code_keeps_its_code_message_and_status_kind() {
853 let body = br#"{"error":"future_conflict","message":"try again later"}"#;
854 let transport = crate::client::RecordingTransport::responding(move |_| {
855 Ok(crate::client::Response::new(409, body.to_vec()))
856 });
857 let e = crate::client::Api::new(transport)
858 .status(crate::client::Auth::None)
859 .unwrap_err();
860 let e = Error::api(e, Some("acme/dev"));
861 assert_eq!(e.code(), "future_conflict");
862 assert_eq!(e.kind(), ErrorKind::Conflict);
863 assert!(e.message().contains("try again later"), "{e}");
864 assert_eq!(kind_of_status(401), ErrorKind::Auth);
865 assert_eq!(kind_of_status(418), ErrorKind::Other);
866 assert_eq!(kind_of(code::UNSUPPORTED_BY_CLIENT), ErrorKind::Invalid);
867 assert_eq!(kind_of(code::UNSUPPORTED_PROTOCOL), ErrorKind::Invalid);
868 }
869
870 #[test]
871 fn an_error_is_shareable() {
872 fn shared<T: Send + Sync + Clone + std::error::Error>() {}
873 shared::<Error>();
874 }
875
876 #[test]
877 fn a_transport_failure_keeps_its_cause() {
878 let io = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
879 let t = crate::client::TransportError::with_source("https://vault.example", io);
880 let e = Error::api(ApiError::Transport(t), Some("acme/dev"));
881 assert_eq!(e.code(), code::UNREACHABLE);
882 assert!(e.message().starts_with("acme/dev: could not reach"), "{e}");
883 let source = std::error::Error::source(&e).expect("a cause");
884 assert!(source.downcast_ref::<std::io::Error>().is_some());
885 }
886}