tail-fin-common 0.6.2

Shared infrastructure for tail-fin: error types, page_fetch, cookies, CDP helpers
Documentation
use night_fury_core::NightFuryError;
use tail_fin_core::SiteError;

#[derive(Debug, thiserror::Error)]
pub enum TailFinError {
    #[error("not logged in: auth cookie not found")]
    AuthRequired,

    #[error("browser error: {0}")]
    Browser(#[from] NightFuryError),

    #[error("API error: {0}")]
    Api(String),

    #[error("HTTP {status} {status_text} — body: {body}")]
    Http {
        status: u16,
        status_text: String,
        body: String,
    },

    #[error("IO error: {0}")]
    Io(String),

    #[error("parse error: {0}")]
    Parse(String),

    #[error("site error: {0}")]
    Site(#[from] SiteError),
}

impl From<serde_json::Error> for TailFinError {
    fn from(e: serde_json::Error) -> Self {
        TailFinError::Parse(e.to_string())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn serde_json_error_converts_to_parse_variant() {
        let bad_json = "not json at all";
        let serde_err: serde_json::Error =
            serde_json::from_str::<serde_json::Value>(bad_json).unwrap_err();
        let tail_err: TailFinError = serde_err.into();
        match &tail_err {
            TailFinError::Parse(msg) => assert!(!msg.is_empty()),
            other => panic!("expected Parse variant, got {:?}", other),
        }
    }

    #[test]
    fn display_auth_required() {
        let err = TailFinError::AuthRequired;
        assert_eq!(err.to_string(), "not logged in: auth cookie not found");
    }

    #[test]
    fn display_api_error() {
        let err = TailFinError::Api("rate limited".into());
        assert_eq!(err.to_string(), "API error: rate limited");
    }

    #[test]
    fn display_http_error_includes_status_and_body() {
        let err = TailFinError::Http {
            status: 403,
            status_text: "Forbidden".into(),
            body: "invalid token".into(),
        };
        let s = err.to_string();
        assert!(s.contains("403"), "expected status in: {s}");
        assert!(s.contains("Forbidden"), "expected status text in: {s}");
        assert!(s.contains("invalid token"), "expected body in: {s}");
    }

    #[test]
    fn display_io_error() {
        let err = TailFinError::Io("file not found".into());
        assert_eq!(err.to_string(), "IO error: file not found");
    }

    #[test]
    fn display_parse_error() {
        let err = TailFinError::Parse("unexpected token".into());
        assert_eq!(err.to_string(), "parse error: unexpected token");
    }

    #[test]
    fn debug_output_is_not_empty() {
        let err = TailFinError::AuthRequired;
        let debug = format!("{:?}", err);
        assert!(!debug.is_empty());
    }

    #[test]
    fn tail_fin_error_from_site_error() {
        // SiteError now lives in tail-fin-core; TailFinError::Site has
        // `#[from]` so conversion still works transparently.
        let site_err = SiteError::ManualLoginRequired { site: "shopee" };
        let tail_err: TailFinError = site_err.into();
        match tail_err {
            TailFinError::Site(_) => {}
            other => panic!("expected Site variant, got {other:?}"),
        }
    }
}