1use thiserror::Error;
4
5#[must_use]
11pub fn is_auth_failure(err: &anyhow::Error) -> bool {
12 matches!(
13 err.downcast_ref::<JevError>(),
14 Some(JevError::ApiRequestFailed {
15 status: 401 | 403,
16 ..
17 })
18 )
19}
20
21#[derive(Error, Debug)]
23pub enum JevError {
24 #[error(
26 "Jev credentials not configured. Set TYPESAFE_API_KEY (or OMNI_DEV_JEV_API_KEY, or \
27 either's _FILE companion), or add one to ~/.omni-dev/settings.json"
28 )]
29 CredentialsNotFound,
30
31 #[error("Jev API request failed: HTTP {status}: {body}")]
33 ApiRequestFailed {
34 status: u16,
36 body: String,
38 },
39
40 #[error("Invalid question specification: {0}")]
42 InvalidQuestionSpec(String),
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48
49 #[test]
50 fn is_auth_failure_matches_401_and_403() {
51 for status in [401, 403] {
52 let err = anyhow::Error::new(JevError::ApiRequestFailed {
53 status,
54 body: "nope".to_string(),
55 });
56 assert!(is_auth_failure(&err), "status {status}");
57 }
58 }
59
60 #[test]
61 fn is_auth_failure_rejects_other_statuses_and_errors() {
62 let err = anyhow::Error::new(JevError::ApiRequestFailed {
63 status: 500,
64 body: "boom".to_string(),
65 });
66 assert!(!is_auth_failure(&err));
67 assert!(!is_auth_failure(&anyhow::anyhow!("unrelated")));
68 }
69
70 #[test]
71 fn credentials_not_found_display() {
72 let err = JevError::CredentialsNotFound;
73 let msg = err.to_string();
74 assert!(msg.contains("TYPESAFE_API_KEY"));
75 assert!(msg.contains("OMNI_DEV_JEV_API_KEY"));
76 assert!(msg.contains("settings.json"));
77 }
78
79 #[test]
80 fn api_request_failed_display() {
81 let err = JevError::ApiRequestFailed {
82 status: 401,
83 body: "Unauthorized".to_string(),
84 };
85 let msg = err.to_string();
86 assert!(msg.contains("401"));
87 assert!(msg.contains("Unauthorized"));
88 }
89
90 #[test]
91 fn invalid_question_spec_display() {
92 let err = JevError::InvalidQuestionSpec("missing name".to_string());
93 assert!(err.to_string().contains("missing name"));
94 }
95}