From 0e68c5918b445ba64dee8203a58f8304d92ec7f5 Mon Sep 17 00:00:00 2001 From: Stephen Reaves Date: Tue, 10 Dec 2024 12:41:57 -0500 Subject: [PATCH 1/4] Add support for whamcloud/lustre Jira Signed-off-by: Stephen Reaves --- src/issue_model.rs | 4 ++-- tests/integration.rs | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/issue_model.rs b/src/issue_model.rs index ea46746..6a7c7e0 100644 --- a/src/issue_model.rs +++ b/src/issue_model.rs @@ -72,8 +72,8 @@ pub struct Fields { pub timespent: Option, pub aggregatetimespent: Option, pub aggregatetimeoriginalestimate: Option, - pub progress: Progress, - pub aggregateprogress: Progress, + pub progress: Option, + pub aggregateprogress: Option, pub workratio: i64, pub summary: String, pub creator: User, diff --git a/tests/integration.rs b/tests/integration.rs index 3e1f377..5217d79 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -20,6 +20,12 @@ fn apache_jira() -> JiraInstance { JiraInstance::at("https://issues.apache.org/jira/".to_string()).unwrap() } +/// A common convenience function to get anonymous access +/// to the Whamcloud Jira instance. +fn whamcloud_jira() -> JiraInstance { + JiraInstance::at("https://jira.whamcloud.com".to_string()).unwrap() +} + /// Try accessing several public issues separately /// to test the client and the deserialization. #[tokio::test] @@ -148,3 +154,12 @@ async fn access_apache_issues() { .await .unwrap(); } + +#[tokio::test] +async fn access_whamcloud_issues() { + let instance = whamcloud_jira(); + let _issues = instance + .issues(&["LU-10647", "LU-13009", "LU-8002", "LU-8874"]) + .await + .unwrap(); +} From 934072b396cff0596260b6f41884a3cf2f8a9199 Mon Sep 17 00:00:00 2001 From: Stephen Reaves Date: Fri, 13 Dec 2024 18:26:21 -0500 Subject: [PATCH 2/4] Allow posting comments Signed-off-by: Stephen Reaves --- src/access.rs | 62 +++++++++++++++++++++++++++++++++++++++++++++- src/issue_model.rs | 14 +++++------ 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/src/access.rs b/src/access.rs index 359e6ca..801fdb3 100644 --- a/src/access.rs +++ b/src/access.rs @@ -18,8 +18,13 @@ limitations under the License. // * https://docs.atlassian.com/software/jira/docs/api/REST/latest/ // * https://docs.atlassian.com/jira-software/REST/latest/ +use chrono::Local; +use serde::Serialize; +use serde_json::Value; + use crate::errors::JiraQueryError; use crate::issue_model::{Issue, JqlResults}; +use crate::{Comment, User}; // The prefix of every subsequent REST request. // This string comes directly after the host in the URL. @@ -74,6 +79,8 @@ enum Method<'a> { Key(&'a str), Keys(&'a [&'a str]), Search(&'a str), + User(&'a str), + Myself(), } impl<'a> Method<'a> { @@ -82,6 +89,8 @@ impl<'a> Method<'a> { Self::Key(id) => format!("issue/{id}"), Self::Keys(ids) => format!("search?jql=id%20in%20({})", ids.join(",")), Self::Search(query) => format!("search?jql={query}"), + Self::User(id) => format!("user?accountId={id}"), + Self::Myself() => format!("myself"), } } } @@ -137,7 +146,7 @@ impl JiraInstance { // The `startAt` option is only valid with JQL. With a URL by key, it breaks the REST query. let start_at = match method { - Method::Key(_) => String::new(), + Method::Key(_) | Method::User(_) | Method::Myself() => String::new(), Method::Keys(_) | Method::Search(_) => format!("&startAt={start_at}"), }; @@ -162,6 +171,24 @@ impl JiraInstance { authenticated.send().await } + async fn authenticated_post( + &self, + url: &str, + body: &T, + ) -> Result { + let request_builder = self.client.post(url); + let authenticated = match &self.auth { + Auth::Anonymous => request_builder, + Auth::ApiKey(key) => request_builder.header("Authorization", &format!("Bearer {key}")), + Auth::Basic { user, password } => request_builder.basic_auth(user, Some(password)), + }; + authenticated + .header("Content-Type", "application/json") + .json(body) + .send() + .await + } + // This method uses a separate implementation from `issues` because Jira provides a way // to request a single ticket specifically. That conveniently handles error cases // where no tickets might match, or more than one might. @@ -279,6 +306,39 @@ impl JiraInstance { Ok(issues) } } + + pub async fn post_comment( + &self, + issue_id: &str, + _user_id: &str, + content: &str, + ) -> Result> { + let url = self.path(&Method::Key(issue_id), 0) + "/comment"; + + // let user_url = self.path(&Method::User(user_id), 0); + let user_url = self.path(&Method::Myself(), 0); + + let user = self + .authenticated_get(&user_url) + .await? + .json::() + .await?; + + // TODO: If user_id != "", don't use myself + let comment = Comment { + author: Some(user), + body: content.to_owned(), + ..Default::default() + }; + + let comment = self + .authenticated_post(&url, &comment) + .await? + .json::() + .await?; + + Ok(comment) + } } #[cfg(test)] diff --git a/src/issue_model.rs b/src/issue_model.rs index 6a7c7e0..92ccb7d 100644 --- a/src/issue_model.rs +++ b/src/issue_model.rs @@ -272,18 +272,18 @@ pub struct Progress { } /// A comment below a Jira issue. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Default)] pub struct Comment { - pub author: User, + pub author: Option, pub body: String, - pub created: DateTime, - pub id: String, + pub created: Option>, + pub id: Option, #[serde(rename = "updateAuthor")] - pub update_author: User, - pub updated: DateTime, + pub update_author: Option, + pub updated: Option>, pub visibility: Option, #[serde(rename = "self")] - pub self_link: String, + pub self_link: Option, #[serde(flatten)] pub extra: Value, } From d54f311b467ba251f96dcc76a9f0a76dc19181a8 Mon Sep 17 00:00:00 2001 From: Stephen Reaves Date: Tue, 17 Dec 2024 09:29:08 -0500 Subject: [PATCH 3/4] Don't add optional fields to request Signed-off-by: Stephen Reaves --- src/access.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/access.rs b/src/access.rs index 801fdb3..fca52de 100644 --- a/src/access.rs +++ b/src/access.rs @@ -326,7 +326,7 @@ impl JiraInstance { // TODO: If user_id != "", don't use myself let comment = Comment { - author: Some(user), + // author: Some(user), body: content.to_owned(), ..Default::default() }; From 20f41fe63446c830ee4f848b1ca036a691fd8f79 Mon Sep 17 00:00:00 2001 From: Stephen Reaves Date: Tue, 17 Dec 2024 09:30:51 -0500 Subject: [PATCH 4/4] Trace new function Signed-off-by: Stephen Reaves --- Cargo.toml | 1 + src/access.rs | 23 ++++++++++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bb2e243..595b93d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" # Version with a security patch: chrono = { version = ">=0.4.20", features = ["serde"] } +tracing = "0.1.41" [dev-dependencies] tokio = { version = ">=1.37", features = ["full"] } diff --git a/src/access.rs b/src/access.rs index fca52de..ffb8d2f 100644 --- a/src/access.rs +++ b/src/access.rs @@ -18,9 +18,7 @@ limitations under the License. // * https://docs.atlassian.com/software/jira/docs/api/REST/latest/ // * https://docs.atlassian.com/jira-software/REST/latest/ -use chrono::Local; use serde::Serialize; -use serde_json::Value; use crate::errors::JiraQueryError; use crate::issue_model::{Issue, JqlResults}; @@ -315,15 +313,21 @@ impl JiraInstance { ) -> Result> { let url = self.path(&Method::Key(issue_id), 0) + "/comment"; + tracing::info!("URL: {}", url); + // let user_url = self.path(&Method::User(user_id), 0); let user_url = self.path(&Method::Myself(), 0); + tracing::info!("User URL: {}", user_url); + let user = self .authenticated_get(&user_url) .await? .json::() .await?; + tracing::info!("User: {:#?}", user); + // TODO: If user_id != "", don't use myself let comment = Comment { // author: Some(user), @@ -331,11 +335,16 @@ impl JiraInstance { ..Default::default() }; - let comment = self - .authenticated_post(&url, &comment) - .await? - .json::() - .await?; + tracing::info!("Built comment: {:#?}", comment); + + // let response = self.authenticated_post(&url, &comment).await?; + let response = self.authenticated_post(&url, &comment).await?; + + tracing::info!("Response: {:#?}", response); + + let comment = response.json::().await?; + + tracing::info!("Parsed comment: {:#?}", comment); Ok(comment) }