1use thiserror::Error;
2
3#[derive(Debug, Error)]
4pub enum HubError {
5 #[error("iLink upstream HTTP error {status}: {msg}")]
7 UpstreamHttp { status: u16, msg: String },
8
9 #[error("iLink upstream parse error: {0}")]
11 UpstreamParse(String),
12
13 #[error("iLink upstream error: {0}")]
16 Upstream(String),
17
18 #[error("client not found: {0}")]
19 ClientNotFound(String),
20
21 #[error("invalid token")]
22 InvalidToken,
23
24 #[error("session not found: {0}")]
25 SessionNotFound(String),
26
27 #[error("configuration error: {0}")]
28 Config(String),
29
30 #[error("I/O error: {0}")]
31 Io(#[from] std::io::Error),
32
33 #[error("database error: {0}")]
34 Database(#[from] sqlx::Error),
35
36 #[error("operation timed out")]
37 Timeout,
38
39 #[error("queue backend error: {0}")]
41 QueueBackend(String),
42}
43
44impl From<anyhow::Error> for HubError {
45 fn from(e: anyhow::Error) -> Self {
46 match e.downcast::<HubError>() {
56 Ok(hub_err) => hub_err,
57 Err(e) => match e.downcast::<sqlx::Error>() {
58 Ok(db_err) => HubError::Database(db_err),
59 Err(e) => HubError::Upstream(e.to_string()),
60 },
61 }
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
73 fn upstream_http_display_includes_status_and_msg() {
74 let err = HubError::UpstreamHttp {
75 status: 503,
76 msg: "service unavailable".to_string(),
77 };
78 let s = err.to_string();
79 assert!(s.contains("503"), "status missing from Display: {s}");
80 assert!(
81 s.contains("service unavailable"),
82 "msg missing from Display: {s}"
83 );
84 }
85
86 #[test]
89 fn upstream_parse_display_includes_message() {
90 let err = HubError::UpstreamParse("unexpected token at line 3".to_string());
91 let s = err.to_string();
92 assert!(
93 s.contains("unexpected token at line 3"),
94 "msg missing from Display: {s}"
95 );
96 }
97
98 #[test]
104 fn from_anyhow_preserves_upstream_http_via_downcast() {
105 let original = HubError::UpstreamHttp {
106 status: 429,
107 msg: "rate limited".to_string(),
108 };
109 let wrapped: anyhow::Error = anyhow::Error::new(original);
110 let recovered: HubError = wrapped.into();
111 match recovered {
112 HubError::UpstreamHttp { status, msg } => {
113 assert_eq!(status, 429);
114 assert_eq!(msg, "rate limited");
115 }
116 other => panic!("expected UpstreamHttp, got {other:?}"),
117 }
118 }
119
120 #[test]
122 fn from_anyhow_preserves_upstream_parse_via_downcast() {
123 let original = HubError::UpstreamParse("bad json".to_string());
124 let wrapped: anyhow::Error = anyhow::Error::new(original);
125 let recovered: HubError = wrapped.into();
126 match recovered {
127 HubError::UpstreamParse(msg) => assert_eq!(msg, "bad json"),
128 other => panic!("expected UpstreamParse, got {other:?}"),
129 }
130 }
131
132 #[test]
137 fn from_anyhow_collapses_other_errors_to_upstream_string() {
138 let wrapped: anyhow::Error = anyhow::anyhow!("raw anyhow message");
139 let recovered: HubError = wrapped.into();
140 match recovered {
141 HubError::Upstream(s) => assert_eq!(s, "raw anyhow message"),
142 other => panic!("expected Upstream, got {other:?}"),
143 }
144 }
145
146 #[test]
152 fn upstream_http_status_zero_is_legal_for_pre_send_failures() {
153 let err = HubError::UpstreamHttp {
154 status: 0,
155 msg: "connection refused".to_string(),
156 };
157 let s = err.to_string();
158 assert!(s.contains("connection refused"), "msg missing: {s}");
159 }
160}