omni_dev/atlassian/
jira_api.rs1use 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
12pub struct JiraApi {
14 client: AtlassianClient,
15}
16
17impl JiraApi {
18 pub fn new(client: AtlassianClient) -> Self {
20 Self { client }
21 }
22
23 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 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}