Skip to main content

tail_fin_common/
error.rs

1#[cfg(feature = "browser")]
2use night_fury_core::NightFuryError;
3#[cfg(feature = "core-api")]
4use tail_fin_core::SiteError;
5
6#[derive(Debug, thiserror::Error)]
7pub enum TailFinError {
8    #[error("not logged in: auth cookie not found")]
9    AuthRequired,
10
11    #[cfg(feature = "browser")]
12    #[error("browser error: {0}")]
13    Browser(#[from] NightFuryError),
14
15    #[error("API error: {0}")]
16    Api(String),
17
18    #[error("HTTP {status} {status_text} — body: {body}")]
19    Http {
20        status: u16,
21        status_text: String,
22        body: String,
23    },
24
25    #[error("IO error: {0}")]
26    Io(String),
27
28    #[error("parse error: {0}")]
29    Parse(String),
30
31    #[cfg(feature = "core-api")]
32    #[error("site error: {0}")]
33    Site(#[from] SiteError),
34}
35
36impl From<serde_json::Error> for TailFinError {
37    fn from(e: serde_json::Error) -> Self {
38        TailFinError::Parse(e.to_string())
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45
46    #[test]
47    fn serde_json_error_converts_to_parse_variant() {
48        let bad_json = "not json at all";
49        let serde_err: serde_json::Error =
50            serde_json::from_str::<serde_json::Value>(bad_json).unwrap_err();
51        let tail_err: TailFinError = serde_err.into();
52        match &tail_err {
53            TailFinError::Parse(msg) => assert!(!msg.is_empty()),
54            other => panic!("expected Parse variant, got {:?}", other),
55        }
56    }
57
58    #[test]
59    fn display_auth_required() {
60        let err = TailFinError::AuthRequired;
61        assert_eq!(err.to_string(), "not logged in: auth cookie not found");
62    }
63
64    #[test]
65    fn display_api_error() {
66        let err = TailFinError::Api("rate limited".into());
67        assert_eq!(err.to_string(), "API error: rate limited");
68    }
69
70    #[test]
71    fn display_http_error_includes_status_and_body() {
72        let err = TailFinError::Http {
73            status: 403,
74            status_text: "Forbidden".into(),
75            body: "invalid token".into(),
76        };
77        let s = err.to_string();
78        assert!(s.contains("403"), "expected status in: {s}");
79        assert!(s.contains("Forbidden"), "expected status text in: {s}");
80        assert!(s.contains("invalid token"), "expected body in: {s}");
81    }
82
83    #[test]
84    fn display_io_error() {
85        let err = TailFinError::Io("file not found".into());
86        assert_eq!(err.to_string(), "IO error: file not found");
87    }
88
89    #[test]
90    fn display_parse_error() {
91        let err = TailFinError::Parse("unexpected token".into());
92        assert_eq!(err.to_string(), "parse error: unexpected token");
93    }
94
95    #[test]
96    fn debug_output_is_not_empty() {
97        let err = TailFinError::AuthRequired;
98        let debug = format!("{:?}", err);
99        assert!(!debug.is_empty());
100    }
101
102    #[cfg(feature = "core-api")]
103    #[test]
104    fn tail_fin_error_from_site_error() {
105        // SiteError now lives in tail-fin-core; TailFinError::Site has
106        // `#[from]` so conversion still works transparently.
107        let site_err = SiteError::ManualLoginRequired { site: "shopee" };
108        let tail_err: TailFinError = site_err.into();
109        match tail_err {
110            TailFinError::Site(_) => {}
111            other => panic!("expected Site variant, got {other:?}"),
112        }
113    }
114}