Skip to content

Commit a557204

Browse files
authored
Merge pull request #72 from 23prime/feature/53-user-add-update-delete-recently-viewed-stars
feat: bl user add/update/delete, recently-viewed-projects/wikis, star list/count
2 parents a840d48 + 403d901 commit a557204

14 files changed

Lines changed: 1692 additions & 20 deletions

File tree

src/api/mod.rs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ use project::{
3333
use space::Space;
3434
use space_notification::SpaceNotification;
3535
use team::Team;
36-
use user::{RecentlyViewedIssue, User};
36+
use user::{RecentlyViewedIssue, RecentlyViewedProject, RecentlyViewedWiki, Star, StarCount, User};
3737
use wiki::{Wiki, WikiAttachment, WikiHistory, WikiListItem};
3838

3939
/// Abstraction over the Backlog HTTP API.
@@ -242,6 +242,33 @@ pub trait BacklogApi {
242242
) -> Result<Vec<RecentlyViewedIssue>> {
243243
unimplemented!()
244244
}
245+
fn add_user(&self, _params: &[(String, String)]) -> Result<User> {
246+
unimplemented!()
247+
}
248+
fn update_user(&self, _user_id: u64, _params: &[(String, String)]) -> Result<User> {
249+
unimplemented!()
250+
}
251+
fn delete_user(&self, _user_id: u64) -> Result<User> {
252+
unimplemented!()
253+
}
254+
fn get_recently_viewed_projects(
255+
&self,
256+
_params: &[(String, String)],
257+
) -> Result<Vec<RecentlyViewedProject>> {
258+
unimplemented!()
259+
}
260+
fn get_recently_viewed_wikis(
261+
&self,
262+
_params: &[(String, String)],
263+
) -> Result<Vec<RecentlyViewedWiki>> {
264+
unimplemented!()
265+
}
266+
fn get_user_stars(&self, _user_id: u64, _params: &[(String, String)]) -> Result<Vec<Star>> {
267+
unimplemented!()
268+
}
269+
fn count_user_stars(&self, _user_id: u64, _params: &[(String, String)]) -> Result<StarCount> {
270+
unimplemented!()
271+
}
245272
fn get_notifications(&self, _params: &[(String, String)]) -> Result<Vec<Notification>> {
246273
unimplemented!()
247274
}
@@ -500,6 +527,40 @@ impl BacklogApi for BacklogClient {
500527
self.get_recently_viewed_issues(params)
501528
}
502529

530+
fn add_user(&self, params: &[(String, String)]) -> Result<User> {
531+
self.add_user(params)
532+
}
533+
534+
fn update_user(&self, user_id: u64, params: &[(String, String)]) -> Result<User> {
535+
self.update_user(user_id, params)
536+
}
537+
538+
fn delete_user(&self, user_id: u64) -> Result<User> {
539+
self.delete_user(user_id)
540+
}
541+
542+
fn get_recently_viewed_projects(
543+
&self,
544+
params: &[(String, String)],
545+
) -> Result<Vec<RecentlyViewedProject>> {
546+
self.get_recently_viewed_projects(params)
547+
}
548+
549+
fn get_recently_viewed_wikis(
550+
&self,
551+
params: &[(String, String)],
552+
) -> Result<Vec<RecentlyViewedWiki>> {
553+
self.get_recently_viewed_wikis(params)
554+
}
555+
556+
fn get_user_stars(&self, user_id: u64, params: &[(String, String)]) -> Result<Vec<Star>> {
557+
self.get_user_stars(user_id, params)
558+
}
559+
560+
fn count_user_stars(&self, user_id: u64, params: &[(String, String)]) -> Result<StarCount> {
561+
self.count_user_stars(user_id, params)
562+
}
563+
503564
fn get_notifications(&self, params: &[(String, String)]) -> Result<Vec<Notification>> {
504565
self.get_notifications(params)
505566
}

src/api/user.rs

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use std::collections::BTreeMap;
55
use super::BacklogClient;
66
use crate::api::activity::Activity;
77
use crate::api::issue::Issue;
8+
use crate::api::project::Project;
9+
use crate::api::wiki::WikiListItem;
810

911
fn deserialize<T: serde::de::DeserializeOwned>(value: serde_json::Value, ctx: &str) -> Result<T> {
1012
serde_json::from_value(value.clone()).map_err(|e| {
@@ -42,6 +44,42 @@ pub struct RecentlyViewedIssue {
4244
pub extra: BTreeMap<String, serde_json::Value>,
4345
}
4446

47+
#[derive(Debug, Clone, Serialize, Deserialize)]
48+
#[serde(rename_all = "camelCase")]
49+
pub struct RecentlyViewedProject {
50+
pub project: Project,
51+
pub updated: String,
52+
#[serde(flatten)]
53+
pub extra: BTreeMap<String, serde_json::Value>,
54+
}
55+
56+
#[derive(Debug, Clone, Serialize, Deserialize)]
57+
#[serde(rename_all = "camelCase")]
58+
pub struct RecentlyViewedWiki {
59+
pub page: WikiListItem,
60+
pub updated: String,
61+
#[serde(flatten)]
62+
pub extra: BTreeMap<String, serde_json::Value>,
63+
}
64+
65+
#[derive(Debug, Clone, Serialize, Deserialize)]
66+
#[serde(rename_all = "camelCase")]
67+
pub struct Star {
68+
pub id: u64,
69+
pub comment: Option<String>,
70+
pub url: String,
71+
pub title: String,
72+
pub presenter: User,
73+
pub created: String,
74+
#[serde(flatten)]
75+
pub extra: BTreeMap<String, serde_json::Value>,
76+
}
77+
78+
#[derive(Debug, Clone, Serialize, Deserialize)]
79+
pub struct StarCount {
80+
pub count: u64,
81+
}
82+
4583
impl BacklogClient {
4684
pub fn get_myself(&self) -> Result<User> {
4785
let value = self.get("/users/myself")?;
@@ -74,6 +112,47 @@ impl BacklogClient {
74112
let value = self.get_with_query("/users/myself/recentlyViewedIssues", params)?;
75113
deserialize(value, "recently viewed issues response")
76114
}
115+
116+
pub fn add_user(&self, params: &[(String, String)]) -> Result<User> {
117+
let value = self.post_form("/users", params)?;
118+
deserialize(value, "user response")
119+
}
120+
121+
pub fn update_user(&self, user_id: u64, params: &[(String, String)]) -> Result<User> {
122+
let value = self.patch_form(&format!("/users/{user_id}"), params)?;
123+
deserialize(value, "user response")
124+
}
125+
126+
pub fn delete_user(&self, user_id: u64) -> Result<User> {
127+
let value = self.delete_req(&format!("/users/{user_id}"))?;
128+
deserialize(value, "user response")
129+
}
130+
131+
pub fn get_recently_viewed_projects(
132+
&self,
133+
params: &[(String, String)],
134+
) -> Result<Vec<RecentlyViewedProject>> {
135+
let value = self.get_with_query("/users/myself/recentlyViewedProjects", params)?;
136+
deserialize(value, "recently viewed projects response")
137+
}
138+
139+
pub fn get_recently_viewed_wikis(
140+
&self,
141+
params: &[(String, String)],
142+
) -> Result<Vec<RecentlyViewedWiki>> {
143+
let value = self.get_with_query("/users/myself/recentlyViewedWikis", params)?;
144+
deserialize(value, "recently viewed wikis response")
145+
}
146+
147+
pub fn get_user_stars(&self, user_id: u64, params: &[(String, String)]) -> Result<Vec<Star>> {
148+
let value = self.get_with_query(&format!("/users/{user_id}/stars"), params)?;
149+
deserialize(value, "user stars response")
150+
}
151+
152+
pub fn count_user_stars(&self, user_id: u64, params: &[(String, String)]) -> Result<StarCount> {
153+
let value = self.get_with_query(&format!("/users/{user_id}/stars/count"), params)?;
154+
deserialize(value, "star count response")
155+
}
77156
}
78157

79158
#[cfg(test)]
@@ -82,6 +161,8 @@ mod tests {
82161
use httpmock::prelude::*;
83162
use serde_json::json;
84163

164+
const TEST_KEY: &str = "test-key";
165+
85166
fn user_json() -> serde_json::Value {
86167
json!({
87168
"id": 123,
@@ -287,4 +368,168 @@ mod tests {
287368
assert_eq!(user.user_id, None);
288369
assert_eq!(user.mail_address, None);
289370
}
371+
372+
#[test]
373+
fn add_user_returns_parsed_struct() {
374+
let server = MockServer::start();
375+
server.mock(|when, then| {
376+
when.method(POST).path("/users");
377+
then.status(201).json_body(user_json());
378+
});
379+
let client = BacklogClient::new_with(&server.base_url(), TEST_KEY).unwrap();
380+
let user = client
381+
.add_user(&[
382+
("userId".to_string(), "john".to_string()),
383+
("password".to_string(), "secret".to_string()),
384+
("name".to_string(), "John Doe".to_string()),
385+
("mailAddress".to_string(), "john@example.com".to_string()),
386+
("roleType".to_string(), "1".to_string()),
387+
])
388+
.unwrap();
389+
assert_eq!(user.id, 123);
390+
assert_eq!(user.name, "John Doe");
391+
}
392+
393+
#[test]
394+
fn add_user_returns_error_on_api_failure() {
395+
let server = MockServer::start();
396+
server.mock(|when, then| {
397+
when.method(POST).path("/users");
398+
then.status(403)
399+
.json_body(json!({"errors": [{"message": "Forbidden"}]}));
400+
});
401+
let client = BacklogClient::new_with(&server.base_url(), TEST_KEY).unwrap();
402+
let err = client.add_user(&[]).unwrap_err();
403+
assert!(err.to_string().contains("Forbidden"));
404+
}
405+
406+
#[test]
407+
fn update_user_returns_parsed_struct() {
408+
let server = MockServer::start();
409+
server.mock(|when, then| {
410+
when.method(httpmock::Method::PATCH).path("/users/123");
411+
then.status(200).json_body(user_json());
412+
});
413+
let client = BacklogClient::new_with(&server.base_url(), TEST_KEY).unwrap();
414+
let user = client
415+
.update_user(123, &[("name".to_string(), "New Name".to_string())])
416+
.unwrap();
417+
assert_eq!(user.id, 123);
418+
}
419+
420+
#[test]
421+
fn update_user_returns_error_on_not_found() {
422+
let server = MockServer::start();
423+
server.mock(|when, then| {
424+
when.method(httpmock::Method::PATCH).path("/users/999");
425+
then.status(404)
426+
.json_body(json!({"errors": [{"message": "No user"}]}));
427+
});
428+
let client = BacklogClient::new_with(&server.base_url(), TEST_KEY).unwrap();
429+
let err = client.update_user(999, &[]).unwrap_err();
430+
assert!(err.to_string().contains("No user"));
431+
}
432+
433+
#[test]
434+
fn delete_user_returns_parsed_struct() {
435+
let server = MockServer::start();
436+
server.mock(|when, then| {
437+
when.method(DELETE).path("/users/123");
438+
then.status(200).json_body(user_json());
439+
});
440+
let client = BacklogClient::new_with(&server.base_url(), TEST_KEY).unwrap();
441+
let user = client.delete_user(123).unwrap();
442+
assert_eq!(user.id, 123);
443+
}
444+
445+
#[test]
446+
fn delete_user_returns_error_on_not_found() {
447+
let server = MockServer::start();
448+
server.mock(|when, then| {
449+
when.method(DELETE).path("/users/999");
450+
then.status(404)
451+
.json_body(json!({"errors": [{"message": "No user"}]}));
452+
});
453+
let client = BacklogClient::new_with(&server.base_url(), TEST_KEY).unwrap();
454+
let err = client.delete_user(999).unwrap_err();
455+
assert!(err.to_string().contains("No user"));
456+
}
457+
458+
#[test]
459+
fn get_recently_viewed_projects_returns_list() {
460+
let server = MockServer::start();
461+
server.mock(|when, then| {
462+
when.method(GET)
463+
.path("/users/myself/recentlyViewedProjects");
464+
then.status(200).json_body(json!([{
465+
"project": {
466+
"id": 1, "projectKey": "TEST", "name": "Test Project",
467+
"chartEnabled": false, "subtaskingEnabled": false,
468+
"projectLeaderCanEditProjectLeader": false,
469+
"textFormattingRule": "markdown", "archived": false
470+
},
471+
"updated": "2024-06-01T00:00:00Z"
472+
}]));
473+
});
474+
let client = BacklogClient::new_with(&server.base_url(), TEST_KEY).unwrap();
475+
let items = client.get_recently_viewed_projects(&[]).unwrap();
476+
assert_eq!(items.len(), 1);
477+
assert_eq!(items[0].project.project_key, "TEST");
478+
}
479+
480+
#[test]
481+
fn get_recently_viewed_wikis_returns_list() {
482+
let server = MockServer::start();
483+
server.mock(|when, then| {
484+
when.method(GET).path("/users/myself/recentlyViewedWikis");
485+
then.status(200).json_body(json!([{
486+
"page": {
487+
"id": 1, "projectId": 1, "name": "Home",
488+
"tags": [],
489+
"createdUser": {"id": 1, "userId": "admin", "name": "Admin", "roleType": 1},
490+
"created": "2024-01-01T00:00:00Z",
491+
"updatedUser": {"id": 1, "userId": "admin", "name": "Admin", "roleType": 1},
492+
"updated": "2024-06-01T00:00:00Z"
493+
},
494+
"updated": "2024-06-01T00:00:00Z"
495+
}]));
496+
});
497+
let client = BacklogClient::new_with(&server.base_url(), TEST_KEY).unwrap();
498+
let items = client.get_recently_viewed_wikis(&[]).unwrap();
499+
assert_eq!(items.len(), 1);
500+
assert_eq!(items[0].page.name, "Home");
501+
}
502+
503+
#[test]
504+
fn get_user_stars_returns_list() {
505+
let server = MockServer::start();
506+
server.mock(|when, then| {
507+
when.method(GET).path("/users/123/stars");
508+
then.status(200).json_body(json!([{
509+
"id": 1,
510+
"comment": null,
511+
"url": "https://example.com/issue/1",
512+
"title": "Issue title",
513+
"presenter": {"id": 2, "userId": "alice", "name": "Alice", "roleType": 1},
514+
"created": "2024-01-01T00:00:00Z"
515+
}]));
516+
});
517+
let client = BacklogClient::new_with(&server.base_url(), TEST_KEY).unwrap();
518+
let stars = client.get_user_stars(123, &[]).unwrap();
519+
assert_eq!(stars.len(), 1);
520+
assert_eq!(stars[0].title, "Issue title");
521+
assert_eq!(stars[0].comment, None);
522+
}
523+
524+
#[test]
525+
fn count_user_stars_returns_count() {
526+
let server = MockServer::start();
527+
server.mock(|when, then| {
528+
when.method(GET).path("/users/123/stars/count");
529+
then.status(200).json_body(json!({"count": 42}));
530+
});
531+
let client = BacklogClient::new_with(&server.base_url(), TEST_KEY).unwrap();
532+
let result = client.count_user_stars(123, &[]).unwrap();
533+
assert_eq!(result.count, 42);
534+
}
290535
}

0 commit comments

Comments
 (0)