bitwarden_test/play/
http_client.rs1use reqwest::{Client, RequestBuilder, Response};
4use serde::{Serialize, de::DeserializeOwned};
5use tracing::debug;
6
7use super::{PlayConfig, PlayError, PlayResult};
8
9#[derive(Debug, Clone)]
11pub(crate) struct PlayHttpClient {
12 client: Client,
13 play_id: String,
14 config: PlayConfig,
15}
16
17impl PlayHttpClient {
18 pub(crate) fn new(play_id: String, config: PlayConfig) -> Self {
20 let client = bitwarden_api_api::new_http_client();
21
22 Self {
23 client,
24 play_id,
25 config,
26 }
27 }
28
29 pub(crate) fn play_id(&self) -> &str {
31 &self.play_id
32 }
33
34 pub(crate) fn config(&self) -> &PlayConfig {
36 &self.config
37 }
38
39 fn with_play_id(&self, builder: RequestBuilder) -> RequestBuilder {
41 builder.header("x-play-id", &self.play_id)
42 }
43
44 pub(crate) async fn post_seeder<T: Serialize, R: DeserializeOwned>(
46 &self,
47 path: &str,
48 body: &T,
49 ) -> PlayResult<R> {
50 let url = format!("{}{}", self.config.seeder_url, path);
51
52 debug!(
53 method = "POST",
54 url = %url,
55 play_id = %self.play_id,
56 body = ?serde_json::to_string(body).ok(),
57 "Play request"
58 );
59
60 let response = self
61 .with_play_id(self.client.post(&url))
62 .json(body)
63 .send()
64 .await?;
65
66 self.handle_json_response(response).await
67 }
68
69 pub(crate) async fn delete_seeder(&self, path: &str) -> PlayResult<()> {
71 let url = format!("{}{}", self.config.seeder_url, path);
72
73 debug!(
74 method = "DELETE",
75 url = %url,
76 play_id = %self.play_id,
77 "Play request"
78 );
79
80 let response = self.with_play_id(self.client.delete(&url)).send().await?;
81
82 let status = response.status();
83 debug!(status = %status, "Play response");
84
85 if status.is_success() {
86 Ok(())
87 } else {
88 let body = response.text().await.unwrap_or_default();
89 debug!(body = %body, "Play error response body");
90 Err(PlayError::Response {
91 status: status.as_u16(),
92 body,
93 })
94 }
95 }
96
97 async fn handle_json_response<R: DeserializeOwned>(&self, response: Response) -> PlayResult<R> {
99 let status = response.status();
100
101 if status.is_success() {
102 let body = response.text().await?;
103 debug!(status = %status, body = %body, "Play response");
104 Ok(serde_json::from_str(&body)?)
105 } else {
106 let body = response.text().await.unwrap_or_default();
107 debug!(status = %status, body = %body, "Play error response");
108 Err(PlayError::Response {
109 status: status.as_u16(),
110 body,
111 })
112 }
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use wiremock::{
119 Mock, MockServer, ResponseTemplate,
120 matchers::{header, method, path},
121 };
122
123 use super::*;
124
125 fn create_test_config(seeder_url: &str) -> PlayConfig {
126 PlayConfig::new(
127 "https://api.example.com",
128 "https://identity.example.com",
129 seeder_url,
130 )
131 }
132
133 #[test]
134 fn test_new_stores_play_id_and_config() {
135 let config = create_test_config("http://localhost:5047");
136 let client = PlayHttpClient::new("test-play-id".to_string(), config);
137
138 assert_eq!(client.play_id(), "test-play-id");
139 assert_eq!(client.config().api_url, "https://api.example.com");
140 assert_eq!(client.config().identity_url, "https://identity.example.com");
141 assert_eq!(client.config().seeder_url, "http://localhost:5047");
142 }
143
144 #[tokio::test]
145 async fn test_post_seeder() {
146 let mock_server = MockServer::start().await;
147
148 Mock::given(method("POST"))
149 .and(path("/seed/"))
150 .and(header("x-play-id", "test-play-id"))
151 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
152 "id": 42,
153 "name": "test-user"
154 })))
155 .expect(1)
156 .mount(&mock_server)
157 .await;
158
159 let config = create_test_config(&mock_server.uri());
160 let client = PlayHttpClient::new("test-play-id".to_string(), config);
161
162 #[derive(serde::Deserialize, Debug, PartialEq)]
163 struct TestResponse {
164 id: i32,
165 name: String,
166 }
167
168 let result: TestResponse = client
169 .post_seeder("/seed/", &serde_json::json!({}))
170 .await
171 .unwrap();
172
173 assert_eq!(result.id, 42);
174 assert_eq!(result.name, "test-user");
175 }
176
177 #[tokio::test]
178 async fn test_post_seeder_handles_server_error() {
179 let mock_server = MockServer::start().await;
180
181 Mock::given(method("POST"))
182 .and(path("/seed/"))
183 .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
184 .mount(&mock_server)
185 .await;
186
187 let config = create_test_config(&mock_server.uri());
188 let client = PlayHttpClient::new("test-id".to_string(), config);
189
190 let result: PlayResult<serde_json::Value> =
191 client.post_seeder("/seed/", &serde_json::json!({})).await;
192
193 match result {
194 Err(PlayError::Response { status, body }) => {
195 assert_eq!(status, 500);
196 assert_eq!(body, "Internal Server Error");
197 }
198 _ => panic!("Expected ServerError"),
199 }
200 }
201
202 #[tokio::test]
203 async fn test_delete_seeder_sends_correct_request() {
204 let mock_server = MockServer::start().await;
205
206 Mock::given(method("DELETE"))
207 .and(path("/seed/test-play-id"))
208 .and(header("x-play-id", "test-play-id"))
209 .respond_with(ResponseTemplate::new(200))
210 .expect(1)
211 .mount(&mock_server)
212 .await;
213
214 let config = create_test_config(&mock_server.uri());
215 let client = PlayHttpClient::new("test-play-id".to_string(), config);
216
217 let result = client.delete_seeder("/seed/test-play-id").await;
218
219 assert!(result.is_ok());
220 }
221
222 #[tokio::test]
223 async fn test_delete_seeder_handles_server_error() {
224 let mock_server = MockServer::start().await;
225
226 Mock::given(method("DELETE"))
227 .and(path("/seed/test-id"))
228 .respond_with(ResponseTemplate::new(404).set_body_string("Not found"))
229 .mount(&mock_server)
230 .await;
231
232 let config = create_test_config(&mock_server.uri());
233 let client = PlayHttpClient::new("test-id".to_string(), config);
234
235 let result = client.delete_seeder("/seed/test-id").await;
236
237 match result {
238 Err(PlayError::Response { status, body }) => {
239 assert_eq!(status, 404);
240 assert_eq!(body, "Not found");
241 }
242 _ => panic!("Expected ServerError"),
243 }
244 }
245}