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
66    #[test]
67    fn plugin_name_is_jobber() {
68        assert_eq!(JobberPlugin.name(), "jobber");
69    }
70
71    #[test]
72    fn endpoint_is_correct() {
73        assert_eq!(
74            JobberPlugin.endpoint(),
75            "https://api.getjobber.com/api/graphql"
76        );
77    }
78
79    #[test]
80    fn version_header_is_set() {
81        let headers = JobberPlugin.version_headers();
82        assert_eq!(
83            headers.get("X-JOBBER-GRAPHQL-VERSION").map(String::as_str),
84            Some("2025-04-16")
85        );
86    }
87
88    #[test]
89    fn default_auth_reads_env() {
90        let key = "JOBBER_ACCESS_TOKEN";
91        let prev = std::env::var(key).ok();
92        // SAFETY: single-threaded test; no concurrent env access
93        unsafe { std::env::set_var(key, "test-token-abc") };
94
95        let auth = JobberPlugin.default_auth();
96        assert!(auth.is_some(), "auth should be Some when env var is set");
97        let auth = auth.expect("auth should be Some when env var is set");
98        assert_eq!(auth.kind, GraphQlAuthKind::Bearer);
99        assert_eq!(auth.token, "test-token-abc");
100        assert!(auth.header_name.is_none());
101
102        // Restore previous state
103        match prev {
104            Some(v) => unsafe { std::env::set_var(key, v) },
105            None => unsafe { std::env::remove_var(key) },
106        }
107    }
108
109    #[test]
110    fn default_auth_absent_when_no_env() {
111        let key = "JOBBER_ACCESS_TOKEN";
112        let prev = std::env::var(key).ok();
113        // SAFETY: single-threaded test; no concurrent env access
114        unsafe { std::env::remove_var(key) };
115
116        assert!(JobberPlugin.default_auth().is_none());
117
118        // Restore
119        if let Some(v) = prev {
120            unsafe { std::env::set_var(key, v) };
121        }
122    }
123
124    #[test]
125    fn description_is_non_empty() {
126        assert!(!JobberPlugin.description().is_empty());
127    }
128}