Skip to main content

tail_fin_common/
error.rs

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