mangadex_api/v5/manga/manga_id/relation/
post.rs1use derive_builder::Builder;
49use serde::Serialize;
50use uuid::Uuid;
51
52use crate::HttpClientRef;
53use mangadex_api_schema::v5::MangaRelationListResponse;
54use mangadex_api_types::MangaRelation;
55
56#[cfg_attr(
57 feature = "deserializable-endpoint",
58 derive(serde::Deserialize, getset::Getters, getset::Setters)
59)]
60#[derive(Debug, Serialize, Clone, Builder, Default)]
61#[serde(rename_all = "camelCase")]
62#[builder(
63 setter(into),
64 build_fn(error = "mangadex_api_types::error::BuilderError")
65)]
66pub struct CreateMangaRelation {
67 #[doc(hidden)]
69 #[serde(skip)]
70 #[builder(pattern = "immutable")]
71 #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
72 pub http_client: HttpClientRef,
73
74 #[serde(skip_serializing)]
75 pub manga_id: Uuid,
76 pub target_manga: Uuid,
77 pub relation: MangaRelation,
78}
79
80endpoint! {
81 POST ("/manga/{}/relation", manga_id),
82 #[body auth] CreateMangaRelation,
83 #[flatten_result] MangaRelationListResponse,
84 CreateMangaRelationBuilder
85}
86
87#[cfg(test)]
88mod tests {
89 use serde_json::json;
90 use url::Url;
91 use uuid::Uuid;
92 use wiremock::matchers::{body_json, header, method, path_regex};
93 use wiremock::{Mock, MockServer, ResponseTemplate};
94
95 use crate::v5::AuthTokens;
96 use crate::{HttpClient, MangaDexClient};
97 use mangadex_api_types::error::Error;
98 use mangadex_api_types::{MangaRelation, RelationshipType, ResponseType};
99
100 #[tokio::test]
101 async fn create_manga_relation_fires_a_request_to_base_url() -> anyhow::Result<()> {
102 let mock_server = MockServer::start().await;
103 let http_client: HttpClient = HttpClient::builder()
104 .base_url(Url::parse(&mock_server.uri())?)
105 .auth_tokens(AuthTokens {
106 session: "sessiontoken".to_string(),
107 refresh: "refreshtoken".to_string(),
108 })
109 .build()?;
110 let mangadex_client = MangaDexClient::new_with_http_client(http_client);
111
112 let manga_id = Uuid::new_v4();
113 let target_manga_id = Uuid::new_v4();
114 let expected_body = json!({
115 "targetManga": target_manga_id,
116 "relation": "spin_off"
117 });
118 let response_body = json!({
119 "result": "ok",
120 "response": "collection",
121 "data": [
122 {
123 "id": "0b92f446-4ee0-4c15-9e5c-6ae1211e785b",
124 "type": "manga_relation",
125 "attributes": {
126 "relation": "doujinshi",
127 "version": 1
128 },
129 "relationships": [
130 {
131 "id": "7944cc53-86e6-4135-898f-47c5c8d0647c",
132 "type": "manga"
133 }
134 ]
135 },
136 {
137 "id": "31b831b7-aac5-4797-b3eb-a41575cd4399",
138 "type": "manga_relation",
139 "attributes": {
140 "relation": "doujinshi",
141 "version": 1
142 },
143 "relationships": [
144 {
145 "id": "119327ab-9b32-4841-9068-02264c15e118",
146 "type": "manga"
147 }
148 ]
149 },
150 {
151 "id": "53815c02-b357-4e23-b8e7-0d6d114ea420",
152 "type": "manga_relation",
153 "attributes": {
154 "relation": "doujinshi",
155 "version": 1
156 },
157 "relationships": [
158 {
159 "id": "25e26436-7eb7-4505-8711-6e014bb16fd7",
160 "type": "manga"
161 }
162 ]
163 },
164 {
165 "id": "6958767b-54c5-4b4c-8f0f-579a36389f68",
166 "type": "manga_relation",
167 "attributes": {
168 "relation": "doujinshi",
169 "version": 1
170 },
171 "relationships": [
172 {
173 "id": "0736a46a-1a34-4411-b665-a1e45ebf54a9",
174 "type": "manga"
175 }
176 ]
177 },
178 {
179 "id": "b358b2f5-beab-484a-9daf-880ad6085225",
180 "type": "manga_relation",
181 "attributes": {
182 "relation": "spin_off",
183 "version": 1
184 },
185 "relationships": [
186 {
187 "id": "1e4deefe-9eb8-4183-837a-f24002adb318",
188 "type": "manga"
189 }
190 ]
191 }
192 ],
193 "limit": 5,
194 "offset": 0,
195 "total": 5
196 });
197
198 Mock::given(method("POST"))
199 .and(path_regex(r"/manga/[0-9a-fA-F-]+/relation"))
200 .and(header("Authorization", "Bearer sessiontoken"))
201 .and(header("Content-Type", "application/json"))
202 .and(body_json(expected_body))
203 .respond_with(ResponseTemplate::new(201).set_body_json(response_body))
204 .expect(1)
205 .mount(&mock_server)
206 .await;
207
208 let res = mangadex_client
209 .manga()
210 .manga_id(manga_id)
211 .relation()
212 .post()
213 .target_manga(target_manga_id)
214 .relation(MangaRelation::SpinOff)
215 .send()
216 .await?;
217
218 let related = &res.data[0];
219 assert_eq!(res.response, ResponseType::Collection);
220 assert_eq!(related.type_, RelationshipType::MangaRelation);
221 assert_eq!(related.attributes.relation, MangaRelation::Doujinshi);
222 assert_eq!(related.attributes.version, 1);
223 assert_eq!(related.relationships[0].type_, RelationshipType::Manga);
224 assert!(related.relationships[0].related.is_none());
225 assert!(related.relationships[0].attributes.is_none());
226
227 Ok(())
228 }
229
230 #[tokio::test]
231 async fn create_manga_relation_requires_auth() -> anyhow::Result<()> {
232 let mock_server = MockServer::start().await;
233 let http_client: HttpClient = HttpClient::builder()
234 .base_url(Url::parse(&mock_server.uri())?)
235 .build()?;
236 let mangadex_client = MangaDexClient::new_with_http_client(http_client);
237
238 let manga_id = Uuid::new_v4();
239 let target_manga_id = Uuid::new_v4();
240 let error_id = Uuid::new_v4();
241 let expected_body = json!({
242 "targetManga": target_manga_id,
243 "relation": "sequel"
244 });
245 let response_body = json!({
246 "result": "error",
247 "errors": [{
248 "id": error_id.to_string(),
249 "status": 403,
250 "title": "Forbidden",
251 "detail": "You must be logged in to continue."
252 }]
253 });
254
255 Mock::given(method("POST"))
256 .and(path_regex(r"/manga/[0-9a-fA-F-]+/relation"))
257 .and(header("Content-Type", "application/json"))
258 .and(body_json(expected_body))
259 .respond_with(ResponseTemplate::new(403).set_body_json(response_body))
260 .expect(0)
261 .mount(&mock_server)
262 .await;
263
264 let res = mangadex_client
265 .manga()
266 .manga_id(manga_id)
267 .relation()
268 .post()
269 .target_manga(target_manga_id)
270 .relation(MangaRelation::Sequel)
271 .send()
272 .await
273 .expect_err("expected error");
274
275 match res {
276 Error::MissingTokens => {}
277 _ => panic!("unexpected error: {:#?}", res),
278 }
279
280 Ok(())
281 }
282}