Skip to main content

omni_dev/atlassian/
jira_api.rs

1//! JIRA Cloud REST API v3 implementation of [`AtlassianApi`].
2
3use std::future::Future;
4use std::pin::Pin;
5
6use anyhow::{Context, Result};
7
8use crate::atlassian::adf::AdfDocument;
9use crate::atlassian::api::{AtlassianApi, ContentItem, ContentMetadata};
10use crate::atlassian::client::AtlassianClient;
11
12/// JIRA Cloud REST API v3 backend.
13pub struct JiraApi {
14    client: AtlassianClient,
15}
16
17impl JiraApi {
18    /// Creates a new JIRA API backend.
19    pub fn new(client: AtlassianClient) -> Self {
20        Self { client }
21    }
22
23    /// Returns the underlying HTTP client's instance URL.
24    pub fn instance_url(&self) -> &str {
25        self.client.instance_url()
26    }
27}
28
29impl AtlassianApi for JiraApi {
30    fn get_content<'a>(
31        &'a self,
32        id: &'a str,
33    ) -> Pin<Box<dyn Future<Output = Result<ContentItem>> + Send + 'a>> {
34        Box::pin(async move {
35            let issue = self.client.get_issue(id).await?;
36            Ok(ContentItem {
37                id: issue.key,
38                title: issue.summary,
39                body_adf: issue.description_adf,
40                metadata: ContentMetadata::Jira {
41                    status: issue.status,
42                    issue_type: issue.issue_type,
43                    assignee: issue.assignee,
44                    priority: issue.priority,
45                    labels: issue.labels,
46                },
47            })
48        })
49    }
50
51    fn update_content<'a>(
52        &'a self,
53        id: &'a str,
54        body_adf: &'a AdfDocument,
55        title: Option<&'a str>,
56    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
57        Box::pin(async move {
58            self.client
59                .update_issue(id, body_adf, title)
60                .await
61                .context("Failed to update JIRA issue")
62        })
63    }
64
65    fn verify_auth<'a>(&'a self) -> Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>> {
66        Box::pin(async move {
67            let user = self.client.get_myself().await?;
68            Ok(user.display_name)
69        })
70    }
71
72    fn backend_name(&self) -> &'static str {
73        "jira"
74    }
75}
76
77#[cfg(test)]
78#[allow(clippy::unwrap_used, clippy::expect_used)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn jira_api_backend_name() {
84        let client =
85            AtlassianClient::new("https://org.atlassian.net", "user@test.com", "token").unwrap();
86        let api = JiraApi::new(client);
87        assert_eq!(api.backend_name(), "jira");
88    }
89
90    #[test]
91    fn jira_api_instance_url() {
92        let client =
93            AtlassianClient::new("https://org.atlassian.net", "user@test.com", "token").unwrap();
94        let api = JiraApi::new(client);
95        assert_eq!(api.instance_url(), "https://org.atlassian.net");
96    }
97
98    /// Helper: stand up a wiremock server with a JIRA issue endpoint.
99    async fn setup_jira_mock() -> (wiremock::MockServer, JiraApi) {
100        let server = wiremock::MockServer::start().await;
101
102        wiremock::Mock::given(wiremock::matchers::method("GET"))
103            .and(wiremock::matchers::path("/rest/api/3/issue/PROJ-42"))
104            .respond_with(wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
105                "key": "PROJ-42",
106                "fields": {
107                    "summary": "Fix the bug",
108                    "description": {
109                        "version": 1,
110                        "type": "doc",
111                        "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Details"}]}]
112                    },
113                    "status": {"name": "Open"},
114                    "issuetype": {"name": "Bug"},
115                    "assignee": {"displayName": "Alice"},
116                    "priority": {"name": "High"},
117                    "labels": ["backend"]
118                }
119            })))
120            .mount(&server)
121            .await;
122
123        let client = AtlassianClient::new(&server.uri(), "user@test.com", "token").unwrap();
124        let api = JiraApi::new(client);
125
126        (server, api)
127    }
128
129    #[tokio::test]
130    async fn get_content_success() {
131        use crate::atlassian::api::{AtlassianApi, ContentMetadata};
132
133        let (_server, api) = setup_jira_mock().await;
134        let item = api.get_content("PROJ-42").await.unwrap();
135
136        assert_eq!(item.id, "PROJ-42");
137        assert_eq!(item.title, "Fix the bug");
138        assert!(item.body_adf.is_some());
139        match &item.metadata {
140            ContentMetadata::Jira {
141                status,
142                issue_type,
143                assignee,
144                priority,
145                labels,
146            } => {
147                assert_eq!(status.as_deref(), Some("Open"));
148                assert_eq!(issue_type.as_deref(), Some("Bug"));
149                assert_eq!(assignee.as_deref(), Some("Alice"));
150                assert_eq!(priority.as_deref(), Some("High"));
151                assert_eq!(labels, &["backend"]);
152            }
153            ContentMetadata::Confluence { .. } => panic!("Expected Jira metadata"),
154        }
155    }
156
157    #[tokio::test]
158    async fn update_content_success() {
159        use crate::atlassian::api::AtlassianApi;
160
161        let (server, api) = setup_jira_mock().await;
162
163        wiremock::Mock::given(wiremock::matchers::method("PUT"))
164            .and(wiremock::matchers::path("/rest/api/3/issue/PROJ-42"))
165            .respond_with(wiremock::ResponseTemplate::new(204))
166            .mount(&server)
167            .await;
168
169        let adf = crate::atlassian::adf::AdfDocument::new();
170        let result = api.update_content("PROJ-42", &adf, Some("New Title")).await;
171        assert!(result.is_ok());
172    }
173
174    #[tokio::test]
175    async fn verify_auth_success() {
176        use crate::atlassian::api::AtlassianApi;
177
178        let server = wiremock::MockServer::start().await;
179
180        wiremock::Mock::given(wiremock::matchers::method("GET"))
181            .and(wiremock::matchers::path("/rest/api/3/myself"))
182            .respond_with(
183                wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
184                    "displayName": "Bob",
185                    "accountId": "xyz789"
186                })),
187            )
188            .mount(&server)
189            .await;
190
191        let client = AtlassianClient::new(&server.uri(), "user@test.com", "token").unwrap();
192        let api = JiraApi::new(client);
193        let name = api.verify_auth().await.unwrap();
194        assert_eq!(name, "Bob");
195    }
196}