Skip to main content

stygian_graph/adapters/graphql_plugins/
jobber.rs

1//! Jobber GraphQL plugin — see T36 for the full implementation.
2//!
3//! Jobber is a field-service management platform whose GraphQL API lives at
4//! `https://api.getjobber.com/api/graphql` and requires the version header
5//! `X-JOBBER-GRAPHQL-VERSION: 2025-04-16` on every request.
6
7use std::collections::HashMap;
8
9use crate::ports::graphql_plugin::GraphQlTargetPlugin;
10use crate::ports::{GraphQlAuth, GraphQlAuthKind};
11
12/// Jobber GraphQL API plugin.
13///
14/// Supplies the endpoint, required version header, and default bearer-token
15/// auth drawn from `JOBBER_ACCESS_TOKEN` for all Jobber pipeline nodes.
16///
17/// # Example
18///
19/// ```rust
20/// use stygian_graph::adapters::graphql_plugins::jobber::JobberPlugin;
21/// use stygian_graph::ports::graphql_plugin::GraphQlTargetPlugin;
22///
23/// let plugin = JobberPlugin;
24/// assert_eq!(plugin.name(), "jobber");
25/// assert_eq!(plugin.endpoint(), "https://api.getjobber.com/api/graphql");
26/// ```
27pub struct JobberPlugin;
28
29impl GraphQlTargetPlugin for JobberPlugin {
30    fn name(&self) -> &'static str {
31        "jobber"
32    }
33
34    fn endpoint(&self) -> &'static str {
35        "https://api.getjobber.com/api/graphql"
36    }
37
38    fn version_headers(&self) -> HashMap<String, String> {
39        [(
40            "X-JOBBER-GRAPHQL-VERSION".to_string(),
41            "2025-04-16".to_string(),
42        )]
43        .into()
44    }
45
46    fn default_auth(&self) -> Option<GraphQlAuth> {
47        std::env::var("JOBBER_ACCESS_TOKEN")
48            .ok()
49            .map(|token| GraphQlAuth {
50                kind: GraphQlAuthKind::Bearer,
51                token,
52                header_name: None,
53            })
54    }
55
56    fn description(&self) -> &'static str {
57        "Jobber field-service management GraphQL API"
58    }
59}
60
61#[cfg(test)]
62#[allow(unsafe_code, clippy::expect_used)] // set_var/remove_var are unsafe in Rust ≥1.93; scoped to tests only
63mod tests {
64    use super::*;
65    use std::sync::Mutex;
66
67    // Serialise env-var mutations so parallel test threads don't race each other.
68    static ENV_LOCK: Mutex<()> = Mutex::new(());
69
70    #[test]
71    fn plugin_name_is_jobber() {
72        assert_eq!(JobberPlugin.name(), "jobber");
73    }
74
75    #[test]
76    fn endpoint_is_correct() {
77        assert_eq!(
78            JobberPlugin.endpoint(),
79            "https://api.getjobber.com/api/graphql"
80        );
81    }
82
83    #[test]
84    fn version_header_is_set() {
85        let headers = JobberPlugin.version_headers();
86        assert_eq!(
87            headers.get("X-JOBBER-GRAPHQL-VERSION").map(String::as_str),
88            Some("2025-04-16")
89        );
90    }
91
92    #[test]
93    fn default_auth_reads_env() {
94        let key = "JOBBER_ACCESS_TOKEN";
95        let _guard = ENV_LOCK
96            .lock()
97            .unwrap_or_else(std::sync::PoisonError::into_inner);
98        let prev = std::env::var(key).ok();
99        // SAFETY: ENV_LOCK serialises all env mutations in this module
100        unsafe { std::env::set_var(key, "test-token-abc") };
101
102        let auth = JobberPlugin.default_auth();
103        assert!(auth.is_some(), "auth should be Some when env var is set");
104        let auth = auth.expect("auth should be Some when env var is set");
105        assert_eq!(auth.kind, GraphQlAuthKind::Bearer);
106        assert_eq!(auth.token, "test-token-abc");
107        assert!(auth.header_name.is_none());
108
109        // Restore previous state
110        match prev {
111            Some(v) => unsafe { std::env::set_var(key, v) },
112            None => unsafe { std::env::remove_var(key) },
113        }
114    }
115
116    #[test]
117    fn default_auth_absent_when_no_env() {
118        let key = "JOBBER_ACCESS_TOKEN";
119        let _guard = ENV_LOCK
120            .lock()
121            .unwrap_or_else(std::sync::PoisonError::into_inner);
122        let prev = std::env::var(key).ok();
123        // SAFETY: ENV_LOCK serialises all env mutations in this module
124        unsafe { std::env::remove_var(key) };
125
126        assert!(JobberPlugin.default_auth().is_none());
127
128        // Restore
129        if let Some(v) = prev {
130            unsafe { std::env::set_var(key, v) };
131        }
132    }
133
134    #[test]
135    fn description_is_non_empty() {
136        assert!(!JobberPlugin.description().is_empty());
137    }
138}