Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions src/api/issue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,30 @@ pub struct IssueComment {
pub extra: BTreeMap<String, serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IssueParticipant {
pub id: u64,
pub user_id: Option<String>,
pub name: String,
pub role_type: Option<u8>,
pub lang: Option<String>,
pub mail_address: Option<String>,
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IssueSharedFile {
pub id: u64,
pub dir: String,
pub name: String,
pub size: u64,
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IssueCommentCount {
pub count: u64,
Expand Down Expand Up @@ -197,6 +221,47 @@ impl BacklogClient {
deserialize(value)
}

pub fn delete_issue_attachment(
&self,
key: &str,
attachment_id: u64,
) -> Result<IssueAttachment> {
let value = self.delete_req(&format!("/issues/{}/attachments/{}", key, attachment_id))?;
deserialize(value)
}

pub fn get_issue_participants(&self, key: &str) -> Result<Vec<IssueParticipant>> {
let value = self.get(&format!("/issues/{}/participants", key))?;
deserialize(value)
}

pub fn get_issue_shared_files(&self, key: &str) -> Result<Vec<IssueSharedFile>> {
let value = self.get(&format!("/issues/{}/sharedFiles", key))?;
deserialize(value)
}

pub fn link_issue_shared_files(
&self,
key: &str,
shared_file_ids: &[u64],
) -> Result<Vec<IssueSharedFile>> {
let params: Vec<(String, String)> = shared_file_ids
.iter()
.map(|id| ("fileId[]".to_string(), id.to_string()))
.collect();
let value = self.post_form(&format!("/issues/{}/sharedFiles", key), &params)?;
deserialize(value)
}

pub fn unlink_issue_shared_file(
&self,
key: &str,
shared_file_id: u64,
) -> Result<IssueSharedFile> {
let value = self.delete_req(&format!("/issues/{}/sharedFiles/{}", key, shared_file_id))?;
deserialize(value)
}

pub fn count_issue_comments(&self, key: &str) -> Result<IssueCommentCount> {
let value = self.get(&format!("/issues/{}/comments/count", key))?;
deserialize(value)
Expand Down Expand Up @@ -418,6 +483,98 @@ mod tests {
assert_eq!(attachments[0].name, "file.txt");
}

fn participant_json() -> serde_json::Value {
json!({
"id": 1,
"userId": "alice",
"name": "Alice",
"roleType": 1,
"lang": null,
"mailAddress": null
})
}

fn shared_file_json() -> serde_json::Value {
json!({
"id": 1,
"dir": "/docs",
"name": "spec.pdf",
"size": 2048
})
}

#[test]
fn delete_issue_attachment_returns_attachment() {
let server = MockServer::start();
server.mock(|when, then| {
when.method(DELETE)
.path("/issues/TEST-1/attachments/1")
.query_param("apiKey", TEST_KEY);
then.status(200).json_body(attachment_json());
});
let client = super::super::BacklogClient::new_with(&server.base_url(), TEST_KEY).unwrap();
let a = client.delete_issue_attachment("TEST-1", 1).unwrap();
assert_eq!(a.name, "file.txt");
}

#[test]
fn get_issue_participants_returns_list() {
let server = MockServer::start();
server.mock(|when, then| {
when.method(GET)
.path("/issues/TEST-1/participants")
.query_param("apiKey", TEST_KEY);
then.status(200).json_body(json!([participant_json()]));
});
let client = super::super::BacklogClient::new_with(&server.base_url(), TEST_KEY).unwrap();
let ps = client.get_issue_participants("TEST-1").unwrap();
assert_eq!(ps.len(), 1);
assert_eq!(ps[0].name, "Alice");
}

#[test]
fn get_issue_shared_files_returns_list() {
let server = MockServer::start();
server.mock(|when, then| {
when.method(GET)
.path("/issues/TEST-1/sharedFiles")
.query_param("apiKey", TEST_KEY);
then.status(200).json_body(json!([shared_file_json()]));
});
let client = super::super::BacklogClient::new_with(&server.base_url(), TEST_KEY).unwrap();
let fs = client.get_issue_shared_files("TEST-1").unwrap();
assert_eq!(fs.len(), 1);
assert_eq!(fs[0].name, "spec.pdf");
}

#[test]
fn link_issue_shared_files_returns_list() {
let server = MockServer::start();
server.mock(|when, then| {
when.method(POST)
.path("/issues/TEST-1/sharedFiles")
.query_param("apiKey", TEST_KEY);
then.status(200).json_body(json!([shared_file_json()]));
});
let client = super::super::BacklogClient::new_with(&server.base_url(), TEST_KEY).unwrap();
let fs = client.link_issue_shared_files("TEST-1", &[1]).unwrap();
assert_eq!(fs.len(), 1);
}

#[test]
fn unlink_issue_shared_file_returns_file() {
let server = MockServer::start();
server.mock(|when, then| {
when.method(DELETE)
.path("/issues/TEST-1/sharedFiles/1")
.query_param("apiKey", TEST_KEY);
then.status(200).json_body(shared_file_json());
});
let client = super::super::BacklogClient::new_with(&server.base_url(), TEST_KEY).unwrap();
let f = client.unlink_issue_shared_file("TEST-1", 1).unwrap();
assert_eq!(f.name, "spec.pdf");
}

#[test]
fn count_issue_comments_returns_count() {
let server = MockServer::start();
Expand Down
48 changes: 48 additions & 0 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use activity::Activity;
use disk_usage::DiskUsage;
use issue::{
Issue, IssueAttachment, IssueComment, IssueCommentCount, IssueCommentNotification, IssueCount,
IssueParticipant, IssueSharedFile,
};
use licence::Licence;
use notification::{Notification, NotificationCount};
Expand Down Expand Up @@ -141,6 +142,29 @@ pub trait BacklogApi {
fn get_issue_attachments(&self, _key: &str) -> Result<Vec<IssueAttachment>> {
unimplemented!()
}
fn delete_issue_attachment(&self, _key: &str, _attachment_id: u64) -> Result<IssueAttachment> {
unimplemented!()
}
fn get_issue_participants(&self, _key: &str) -> Result<Vec<IssueParticipant>> {
unimplemented!()
}
fn get_issue_shared_files(&self, _key: &str) -> Result<Vec<IssueSharedFile>> {
unimplemented!()
}
fn link_issue_shared_files(
&self,
_key: &str,
_shared_file_ids: &[u64],
) -> Result<Vec<IssueSharedFile>> {
unimplemented!()
}
fn unlink_issue_shared_file(
&self,
_key: &str,
_shared_file_id: u64,
) -> Result<IssueSharedFile> {
unimplemented!()
}
fn count_issue_comments(&self, _key: &str) -> Result<IssueCommentCount> {
unimplemented!()
}
Expand Down Expand Up @@ -340,6 +364,30 @@ impl BacklogApi for BacklogClient {
self.get_issue_attachments(key)
}

fn delete_issue_attachment(&self, key: &str, attachment_id: u64) -> Result<IssueAttachment> {
self.delete_issue_attachment(key, attachment_id)
}

fn get_issue_participants(&self, key: &str) -> Result<Vec<IssueParticipant>> {
self.get_issue_participants(key)
}

fn get_issue_shared_files(&self, key: &str) -> Result<Vec<IssueSharedFile>> {
self.get_issue_shared_files(key)
}

fn link_issue_shared_files(
&self,
key: &str,
shared_file_ids: &[u64],
) -> Result<Vec<IssueSharedFile>> {
self.link_issue_shared_files(key, shared_file_ids)
}

fn unlink_issue_shared_file(&self, key: &str, shared_file_id: u64) -> Result<IssueSharedFile> {
self.unlink_issue_shared_file(key, shared_file_id)
}

fn count_issue_comments(&self, key: &str) -> Result<IssueCommentCount> {
self.count_issue_comments(key)
}
Expand Down
107 changes: 107 additions & 0 deletions src/cmd/issue/attachment/delete.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use anstream::println;
use anyhow::{Context, Result};

use crate::api::{BacklogApi, BacklogClient};

pub struct IssueAttachmentDeleteArgs {
key: String,
attachment_id: u64,
json: bool,
}

impl IssueAttachmentDeleteArgs {
pub fn new(key: String, attachment_id: u64, json: bool) -> Self {
Self {
key,
attachment_id,
json,
}
}
}

pub fn delete(args: &IssueAttachmentDeleteArgs) -> Result<()> {
let client = BacklogClient::from_config()?;
delete_with(args, &client)
}

pub fn delete_with(args: &IssueAttachmentDeleteArgs, api: &dyn BacklogApi) -> Result<()> {
let attachment = api.delete_issue_attachment(&args.key, args.attachment_id)?;
if args.json {
println!(
"{}",
serde_json::to_string_pretty(&attachment).context("Failed to serialize JSON")?
);
} else {
println!("Deleted: {} ({} bytes)", attachment.name, attachment.size);
}
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use crate::api::issue::{IssueAttachment, IssueUser};
use anyhow::anyhow;
use std::collections::BTreeMap;

fn sample_attachment() -> IssueAttachment {
IssueAttachment {
id: 1,
name: "file.txt".to_string(),
size: 1024,
created_user: IssueUser {
id: 1,
user_id: Some("john".to_string()),
name: "John Doe".to_string(),
role_type: 1,
lang: None,
mail_address: None,
extra: BTreeMap::new(),
},
created: "2024-01-01T00:00:00Z".to_string(),
}
}

struct MockApi {
attachment: Option<IssueAttachment>,
}

impl crate::api::BacklogApi for MockApi {
fn delete_issue_attachment(
&self,
_key: &str,
_attachment_id: u64,
) -> anyhow::Result<IssueAttachment> {
self.attachment
.clone()
.ok_or_else(|| anyhow!("no attachment"))
}
}

fn args(json: bool) -> IssueAttachmentDeleteArgs {
IssueAttachmentDeleteArgs::new("TEST-1".to_string(), 1, json)
}

#[test]
fn delete_with_text_output_succeeds() {
let api = MockApi {
attachment: Some(sample_attachment()),
};
assert!(delete_with(&args(false), &api).is_ok());
}

#[test]
fn delete_with_json_output_succeeds() {
let api = MockApi {
attachment: Some(sample_attachment()),
};
assert!(delete_with(&args(true), &api).is_ok());
}

#[test]
fn delete_with_propagates_api_error() {
let api = MockApi { attachment: None };
let err = delete_with(&args(false), &api).unwrap_err();
assert!(err.to_string().contains("no attachment"));
}
}
2 changes: 2 additions & 0 deletions src/cmd/issue/attachment/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod delete;
mod list;

pub use delete::{IssueAttachmentDeleteArgs, delete};
pub use list::{IssueAttachmentListArgs, list};
2 changes: 2 additions & 0 deletions src/cmd/issue/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ mod count;
mod create;
mod delete;
pub mod list;
pub mod participant;
pub mod shared_file;
mod show;
mod update;

Expand Down
Loading
Loading