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
12 changes: 11 additions & 1 deletion src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use project::{
use space::Space;
use space_notification::SpaceNotification;
use team::Team;
use user::User;
use user::{RecentlyViewedIssue, User};
use wiki::{Wiki, WikiAttachment, WikiHistory, WikiListItem};

pub trait BacklogApi {
Expand Down Expand Up @@ -71,6 +71,8 @@ pub trait BacklogApi {
fn get_wiki_attachments(&self, wiki_id: u64) -> Result<Vec<WikiAttachment>>;
fn get_teams(&self) -> Result<Vec<Team>>;
fn get_team(&self, team_id: u64) -> Result<Team>;
fn get_user_activities(&self, user_id: u64) -> Result<Vec<Activity>>;
fn get_recently_viewed_issues(&self) -> Result<Vec<RecentlyViewedIssue>>;
}

impl BacklogApi for BacklogClient {
Expand Down Expand Up @@ -222,6 +224,14 @@ impl BacklogApi for BacklogClient {
fn get_team(&self, team_id: u64) -> Result<Team> {
self.get_team(team_id)
}

fn get_user_activities(&self, user_id: u64) -> Result<Vec<Activity>> {
self.get_user_activities(user_id)
}

fn get_recently_viewed_issues(&self) -> Result<Vec<RecentlyViewedIssue>> {
self.get_recently_viewed_issues()
}
}

/// How the client authenticates with Backlog.
Expand Down
123 changes: 117 additions & 6 deletions src/api/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

use super::BacklogClient;
use crate::api::activity::Activity;
use crate::api::issue::Issue;

fn deserialize<T: serde::de::DeserializeOwned>(value: serde_json::Value, ctx: &str) -> Result<T> {
serde_json::from_value(value.clone()).map_err(|e| {
anyhow::anyhow!(
"Failed to deserialize {ctx}: {e}\nRaw JSON:\n{}",
serde_json::to_string_pretty(&value).unwrap_or_else(|_| value.to_string())
)
})
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
Expand All @@ -22,23 +33,39 @@ pub struct User {
pub extra: BTreeMap<String, serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecentlyViewedIssue {
pub issue: Issue,
pub updated: String,
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}

impl BacklogClient {
pub fn get_myself(&self) -> Result<User> {
let value = self.get("/users/myself")?;
serde_json::from_value(value)
.map_err(|e| anyhow::anyhow!("Failed to deserialize user response: {}", e))
deserialize(value, "user response")
}

pub fn get_users(&self) -> Result<Vec<User>> {
let value = self.get("/users")?;
serde_json::from_value(value)
.map_err(|e| anyhow::anyhow!("Failed to deserialize users response: {}", e))
deserialize(value, "users response")
}

pub fn get_user(&self, user_id: u64) -> Result<User> {
let value = self.get(&format!("/users/{user_id}"))?;
serde_json::from_value(value)
.map_err(|e| anyhow::anyhow!("Failed to deserialize user response: {}", e))
deserialize(value, "user response")
}

pub fn get_user_activities(&self, user_id: u64) -> Result<Vec<Activity>> {
let value = self.get(&format!("/users/{user_id}/activities"))?;
deserialize(value, "user activities response")
}

pub fn get_recently_viewed_issues(&self) -> Result<Vec<RecentlyViewedIssue>> {
let value = self.get("/users/myself/recentlyViewedIssues")?;
deserialize(value, "recently viewed issues response")
}
}

Expand Down Expand Up @@ -156,6 +183,90 @@ mod tests {
assert_eq!(user.role_type, 1);
}

#[test]
fn get_user_activities_returns_list() {
let server = MockServer::start();
server.mock(|when, then| {
when.method(GET).path("/users/123/activities");
then.status(200).json_body(json!([{
"id": 1,
"type": 2,
"content": {},
"createdUser": {"id": 123, "userId": "john", "name": "John Doe"},
"created": "2024-01-01T00:00:00Z"
}]));
});

let client = BacklogClient::new_with(&server.base_url(), "test-key").unwrap();
let activities = client.get_user_activities(123).unwrap();
assert_eq!(activities.len(), 1);
assert_eq!(activities[0].id, 1);
}

#[test]
fn get_user_activities_returns_error_on_api_failure() {
let server = MockServer::start();
server.mock(|when, then| {
when.method(GET).path("/users/123/activities");
then.status(404)
.json_body(json!({"errors": [{"message": "No user"}]}));
});

let client = BacklogClient::new_with(&server.base_url(), "test-key").unwrap();
let err = client.get_user_activities(123).unwrap_err();
assert!(err.to_string().contains("No user"));
}

#[test]
fn get_recently_viewed_issues_returns_list() {
let server = MockServer::start();
server.mock(|when, then| {
when.method(GET).path("/users/myself/recentlyViewedIssues");
then.status(200).json_body(json!([{
"issue": {
"id": 1,
"projectId": 1,
"issueKey": "BLG-1",
"keyId": 1,
"summary": "first issue",
"description": "",
"priority": {"id": 3, "name": "Normal"},
"status": {"id": 1, "projectId": 1, "name": "Open", "color": "#ed8077", "displayOrder": 1000},
"issueType": {"id": 2, "projectId": 1, "name": "Task", "color": "#7ea800", "displayOrder": 0},
"assignee": null,
"category": [],
"versions": [],
"milestone": [],
"created": "2024-01-01T00:00:00Z",
"updated": "2024-06-01T00:00:00Z",
"createdUser": {"id": 1, "userId": "admin", "name": "admin", "roleType": 1},
"updatedUser": {"id": 1, "userId": "admin", "name": "admin", "roleType": 1}
},
"updated": "2024-06-01T00:00:00Z"
}]));
});

let client = BacklogClient::new_with(&server.base_url(), "test-key").unwrap();
let items = client.get_recently_viewed_issues().unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0].issue.issue_key, "BLG-1");
assert_eq!(items[0].updated, "2024-06-01T00:00:00Z");
}

#[test]
fn get_recently_viewed_issues_returns_error_on_api_failure() {
let server = MockServer::start();
server.mock(|when, then| {
when.method(GET).path("/users/myself/recentlyViewedIssues");
then.status(401)
.json_body(json!({"errors": [{"message": "Authentication failure"}]}));
});

let client = BacklogClient::new_with(&server.base_url(), "test-key").unwrap();
let err = client.get_recently_viewed_issues().unwrap_err();
assert!(err.to_string().contains("Authentication failure"));
}

#[test]
fn deserialize_user_with_null_user_id() {
let v = json!({
Expand Down
11 changes: 11 additions & 0 deletions src/cmd/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,17 @@ mod tests {
fn get_team(&self, _team_id: u64) -> anyhow::Result<crate::api::team::Team> {
unimplemented!()
}
fn get_user_activities(
&self,
_user_id: u64,
) -> anyhow::Result<Vec<crate::api::activity::Activity>> {
unimplemented!()
}
fn get_recently_viewed_issues(
&self,
) -> anyhow::Result<Vec<crate::api::user::RecentlyViewedIssue>> {
unimplemented!()
}
}

fn sample_user() -> User {
Expand Down
11 changes: 11 additions & 0 deletions src/cmd/issue/attachment/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,17 @@ mod tests {
fn get_team(&self, _team_id: u64) -> anyhow::Result<crate::api::team::Team> {
unimplemented!()
}
fn get_user_activities(
&self,
_user_id: u64,
) -> anyhow::Result<Vec<crate::api::activity::Activity>> {
unimplemented!()
}
fn get_recently_viewed_issues(
&self,
) -> anyhow::Result<Vec<crate::api::user::RecentlyViewedIssue>> {
unimplemented!()
}
}

fn args(json: bool) -> IssueAttachmentListArgs {
Expand Down
11 changes: 11 additions & 0 deletions src/cmd/issue/comment/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,17 @@ mod tests {
fn get_team(&self, _team_id: u64) -> anyhow::Result<crate::api::team::Team> {
unimplemented!()
}
fn get_user_activities(
&self,
_user_id: u64,
) -> anyhow::Result<Vec<crate::api::activity::Activity>> {
unimplemented!()
}
fn get_recently_viewed_issues(
&self,
) -> anyhow::Result<Vec<crate::api::user::RecentlyViewedIssue>> {
unimplemented!()
}
}

fn args(json: bool) -> IssueCommentAddArgs {
Expand Down
11 changes: 11 additions & 0 deletions src/cmd/issue/comment/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,17 @@ mod tests {
fn get_team(&self, _team_id: u64) -> anyhow::Result<crate::api::team::Team> {
unimplemented!()
}
fn get_user_activities(
&self,
_user_id: u64,
) -> anyhow::Result<Vec<crate::api::activity::Activity>> {
unimplemented!()
}
fn get_recently_viewed_issues(
&self,
) -> anyhow::Result<Vec<crate::api::user::RecentlyViewedIssue>> {
unimplemented!()
}
}

fn args(json: bool) -> IssueCommentDeleteArgs {
Expand Down
11 changes: 11 additions & 0 deletions src/cmd/issue/comment/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,17 @@ mod tests {
fn get_team(&self, _team_id: u64) -> anyhow::Result<crate::api::team::Team> {
unimplemented!()
}
fn get_user_activities(
&self,
_user_id: u64,
) -> anyhow::Result<Vec<crate::api::activity::Activity>> {
unimplemented!()
}
fn get_recently_viewed_issues(
&self,
) -> anyhow::Result<Vec<crate::api::user::RecentlyViewedIssue>> {
unimplemented!()
}
}

fn args(json: bool) -> IssueCommentListArgs {
Expand Down
11 changes: 11 additions & 0 deletions src/cmd/issue/comment/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,17 @@ mod tests {
fn get_team(&self, _team_id: u64) -> anyhow::Result<crate::api::team::Team> {
unimplemented!()
}
fn get_user_activities(
&self,
_user_id: u64,
) -> anyhow::Result<Vec<crate::api::activity::Activity>> {
unimplemented!()
}
fn get_recently_viewed_issues(
&self,
) -> anyhow::Result<Vec<crate::api::user::RecentlyViewedIssue>> {
unimplemented!()
}
}

fn args(json: bool) -> IssueCommentUpdateArgs {
Expand Down
11 changes: 11 additions & 0 deletions src/cmd/issue/count.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,17 @@ mod tests {
fn get_team(&self, _team_id: u64) -> anyhow::Result<crate::api::team::Team> {
unimplemented!()
}
fn get_user_activities(
&self,
_user_id: u64,
) -> anyhow::Result<Vec<crate::api::activity::Activity>> {
unimplemented!()
}
fn get_recently_viewed_issues(
&self,
) -> anyhow::Result<Vec<crate::api::user::RecentlyViewedIssue>> {
unimplemented!()
}
}

fn args(json: bool) -> IssueCountArgs {
Expand Down
11 changes: 11 additions & 0 deletions src/cmd/issue/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,17 @@ mod tests {
fn get_team(&self, _team_id: u64) -> anyhow::Result<crate::api::team::Team> {
unimplemented!()
}
fn get_user_activities(
&self,
_user_id: u64,
) -> anyhow::Result<Vec<crate::api::activity::Activity>> {
unimplemented!()
}
fn get_recently_viewed_issues(
&self,
) -> anyhow::Result<Vec<crate::api::user::RecentlyViewedIssue>> {
unimplemented!()
}
}

fn args(json: bool) -> IssueCreateArgs {
Expand Down
11 changes: 11 additions & 0 deletions src/cmd/issue/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,17 @@ mod tests {
fn get_team(&self, _team_id: u64) -> anyhow::Result<crate::api::team::Team> {
unimplemented!()
}
fn get_user_activities(
&self,
_user_id: u64,
) -> anyhow::Result<Vec<crate::api::activity::Activity>> {
unimplemented!()
}
fn get_recently_viewed_issues(
&self,
) -> anyhow::Result<Vec<crate::api::user::RecentlyViewedIssue>> {
unimplemented!()
}
}

fn args(json: bool) -> IssueDeleteArgs {
Expand Down
11 changes: 11 additions & 0 deletions src/cmd/issue/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,17 @@ mod tests {
fn get_team(&self, _team_id: u64) -> anyhow::Result<crate::api::team::Team> {
unimplemented!()
}
fn get_user_activities(
&self,
_user_id: u64,
) -> anyhow::Result<Vec<crate::api::activity::Activity>> {
unimplemented!()
}
fn get_recently_viewed_issues(
&self,
) -> anyhow::Result<Vec<crate::api::user::RecentlyViewedIssue>> {
unimplemented!()
}
}

fn args(json: bool) -> IssueListArgs {
Expand Down
11 changes: 11 additions & 0 deletions src/cmd/issue/show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,17 @@ mod tests {
fn get_team(&self, _team_id: u64) -> anyhow::Result<crate::api::team::Team> {
unimplemented!()
}
fn get_user_activities(
&self,
_user_id: u64,
) -> anyhow::Result<Vec<crate::api::activity::Activity>> {
unimplemented!()
}
fn get_recently_viewed_issues(
&self,
) -> anyhow::Result<Vec<crate::api::user::RecentlyViewedIssue>> {
unimplemented!()
}
}

fn args(json: bool) -> IssueShowArgs {
Expand Down
Loading
Loading