mangadex_api/v5/author/id/
put.rs1use derive_builder::Builder;
43use serde::Serialize;
44use url::Url;
45use uuid::Uuid;
46
47use crate::HttpClientRef;
48use mangadex_api_schema::v5::{AuthorData, LocalizedString};
49
50#[cfg_attr(
51 feature = "deserializable-endpoint",
52 derive(serde::Deserialize, getset::Getters, getset::Setters)
53)]
54#[derive(Debug, Serialize, Clone, Builder)]
55#[serde(rename_all = "camelCase")]
56#[builder(
57 setter(into, strip_option),
58 build_fn(error = "mangadex_api_types::error::BuilderError")
59)]
60#[cfg_attr(feature = "non_exhaustive", non_exhaustive)]
61pub struct UpdateAuthor {
62 #[doc(hidden)]
64 #[serde(skip)]
65 #[builder(pattern = "immutable")]
66 #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
67 pub http_client: HttpClientRef,
68
69 #[serde(skip_serializing)]
70 pub author_id: Uuid,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
73 #[builder(default)]
74 pub name: Option<String>,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 #[builder(default)]
77 pub biography: Option<LocalizedString>,
78 #[serde(skip_serializing_if = "Option::is_none")]
82 #[builder(default)]
83 pub twitter: Option<Option<Url>>,
84 #[serde(skip_serializing_if = "Option::is_none")]
88 #[builder(default)]
89 pub pixiv: Option<Option<Url>>,
90 #[serde(skip_serializing_if = "Option::is_none")]
94 #[builder(default)]
95 pub melon_book: Option<Option<Url>>,
96 #[serde(skip_serializing_if = "Option::is_none")]
100 #[builder(default)]
101 pub fan_box: Option<Option<Url>>,
102 #[serde(skip_serializing_if = "Option::is_none")]
106 #[builder(default)]
107 pub booth: Option<Option<Url>>,
108 #[serde(skip_serializing_if = "Option::is_none")]
112 #[builder(default)]
113 pub nico_video: Option<Option<Url>>,
114 #[serde(skip_serializing_if = "Option::is_none")]
118 #[builder(default)]
119 pub skeb: Option<Option<Url>>,
120 #[serde(skip_serializing_if = "Option::is_none")]
124 #[builder(default)]
125 pub fantia: Option<Option<Url>>,
126 #[serde(skip_serializing_if = "Option::is_none")]
130 #[builder(default)]
131 pub tumblr: Option<Option<Url>>,
132 #[serde(skip_serializing_if = "Option::is_none")]
136 #[builder(default)]
137 pub youtube: Option<Option<Url>>,
138 #[serde(skip_serializing_if = "Option::is_none")]
144 #[builder(default)]
145 pub weibo: Option<Option<Url>>,
146 #[serde(skip_serializing_if = "Option::is_none")]
150 #[builder(default)]
151 pub naver: Option<Option<Url>>,
152 #[serde(skip_serializing_if = "Option::is_none")]
154 #[builder(default)]
155 pub website: Option<Option<Url>>,
156 pub version: u32,
158}
159
160endpoint! {
161 PUT ("/author/{}", author_id),
162 #[body auth] UpdateAuthor,
163 #[rate_limited] AuthorData,
164 UpdateAuthorBuilder
165}
166
167#[cfg(test)]
168mod tests {
169 use fake::faker::lorem::en::Sentence;
170 use fake::faker::name::en::Name;
171 use fake::Fake;
172 use serde_json::json;
173 use time::OffsetDateTime;
174 use url::Url;
175 use uuid::Uuid;
176 use wiremock::matchers::{body_json, header, method, path_regex};
177 use wiremock::{Mock, MockServer, ResponseTemplate};
178
179 use crate::v5::AuthTokens;
180 use crate::{HttpClient, MangaDexClient};
181 use mangadex_api_types::MangaDexDateTime;
182
183 #[tokio::test]
184 async fn update_author_fires_a_request_to_base_url() -> anyhow::Result<()> {
185 let mock_server = MockServer::start().await;
186 let http_client = HttpClient::builder()
187 .base_url(Url::parse(&mock_server.uri())?)
188 .auth_tokens(AuthTokens {
189 session: "sessiontoken".to_string(),
190 refresh: "refreshtoken".to_string(),
191 })
192 .build()?;
193 let mangadex_client = MangaDexClient::new_with_http_client(http_client);
194
195 let author_id = Uuid::new_v4();
196 let author_name: String = Name().fake();
197 let author_biography: String = Sentence(1..2).fake();
198
199 let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
200
201 let expected_body = json!({
202 "website": "https://example.org/",
203 "version": 2
204 });
205 let response_body = json!({
206 "result": "ok",
207 "response": "entity",
208 "data": {
209 "id": author_id,
210 "type": "author",
211 "attributes": {
212 "name": author_name,
213 "imageUrl": "",
214 "biography": {
215 "en": author_biography,
216 },
217 "twitter": null,
218 "pixiv": null,
219 "melonBook": null,
220 "fanBox": null,
221 "booth": null,
222 "nicoVideo": null,
223 "skeb": null,
224 "fantia": null,
225 "tumblr": null,
226 "youtube": null,
227 "weibo": null,
228 "naver": null,
229 "website": "https://example.org",
230 "version": 2,
231 "createdAt": datetime.to_string(),
232 "updatedAt": datetime.to_string(),
233 },
234 "relationships": []
235 }
236 });
237
238 Mock::given(method("PUT"))
239 .and(path_regex(r"/author/[0-9a-fA-F-]+"))
240 .and(header("Authorization", "Bearer sessiontoken"))
241 .and(header("Content-Type", "application/json"))
242 .and(body_json(expected_body))
243 .respond_with(
244 ResponseTemplate::new(200)
245 .insert_header("x-ratelimit-retry-after", "1698723860")
246 .insert_header("x-ratelimit-limit", "40")
247 .insert_header("x-ratelimit-remaining", "39")
248 .set_body_json(response_body),
249 )
250 .expect(1)
251 .mount(&mock_server)
252 .await;
253
254 let res = mangadex_client
255 .author()
256 .id(author_id)
257 .put()
258 .website(Some(Url::parse("https://example.org").unwrap()))
259 .version(2u32)
260 .send()
261 .await?;
262
263 assert_eq!(
264 res.body.data.attributes.website,
265 Some(Url::parse("https://example.org").unwrap())
266 );
267 assert_eq!(res.body.data.attributes.version, 2);
268
269 Ok(())
270 }
271}