bitwarden_test/play/
play.rs1use std::{future::Future, panic::AssertUnwindSafe, sync::Arc};
4
5use futures::FutureExt;
6use uuid::Uuid;
7
8use super::{
9 CreateSceneRequest, CreateSceneResponse, PlayConfig, PlayHttpClient, PlayResult, Query,
10 QueryRequest, Scene, SceneTemplate,
11};
12
13pub struct PlayBuilder {
38 config: Option<PlayConfig>,
39}
40
41impl PlayBuilder {
42 fn new() -> Self {
44 Self { config: None }
45 }
46
47 pub fn config(mut self, config: PlayConfig) -> Self {
51 self.config = Some(config);
52 self
53 }
54
55 pub async fn run<F, Fut, T>(self, f: F) -> T
65 where
66 F: FnOnce(Play) -> Fut,
67 Fut: Future<Output = T>,
68 {
69 let config = self.config.unwrap_or_else(PlayConfig::from_env);
70 let play = Play::new_internal(config);
71
72 let result = AssertUnwindSafe(f(play.clone())).catch_unwind().await;
74
75 if let Err(e) = play.clean().await {
77 tracing::warn!("Play cleanup failed: {:?}", e);
78 }
79
80 match result {
82 Ok(value) => value,
83 Err(panic) => std::panic::resume_unwind(panic),
84 }
85 }
86}
87
88#[derive(Clone)]
118pub struct Play {
119 client: Arc<PlayHttpClient>,
120}
121
122impl Play {
123 pub fn builder() -> PlayBuilder {
138 PlayBuilder::new()
139 }
140
141 fn new_internal(config: PlayConfig) -> Self {
143 let play_id = Uuid::new_v4().to_string();
144 let client = Arc::new(PlayHttpClient::new(play_id, config));
145 Play { client }
146 }
147
148 pub async fn scene<T>(&self, arguments: &T::Arguments) -> PlayResult<Scene<T>>
152 where
153 T: SceneTemplate,
154 {
155 let request = CreateSceneRequest {
156 template: T::template_name(),
157 arguments,
158 };
159
160 let response: CreateSceneResponse<T::Result> =
161 self.client.post_seeder("/seed/", &request).await?;
162
163 Ok(Scene::new(response.result, response.mangle_map))
164 }
165
166 pub async fn query<Q>(&self, arguments: &Q::Args) -> PlayResult<Q>
168 where
169 Q: Query,
170 {
171 let request = QueryRequest {
172 template: Q::template_name(),
173 arguments,
174 };
175
176 let result: Q::Result = self.client.post_seeder("/seed/query", &request).await?;
177
178 Ok(Q::from_result(result))
179 }
180
181 pub async fn clean(&self) -> PlayResult<()> {
186 self.client
187 .delete_seeder(&format!("/seed/{}", self.client.play_id()))
188 .await
189 }
190
191 pub fn play_id(&self) -> &str {
193 self.client.play_id()
194 }
195
196 pub fn config(&self) -> &PlayConfig {
198 self.client.config()
199 }
200}
201
202impl Default for PlayBuilder {
203 fn default() -> Self {
204 Self::new()
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use serde::{Deserialize, Serialize};
211 use wiremock::{
212 Mock, MockServer, ResponseTemplate,
213 matchers::{method, path},
214 };
215
216 use super::*;
217
218 async fn play_with_mock_server() -> (Play, MockServer) {
220 let server = MockServer::start().await;
221 Mock::given(method("DELETE"))
222 .respond_with(ResponseTemplate::new(200))
223 .mount(&server)
224 .await;
225 let config = PlayConfig::new(
226 "https://api.example.com",
227 "https://identity.example.com",
228 server.uri(),
229 );
230 (Play::new_internal(config), server)
231 }
232
233 #[tokio::test]
234 async fn test_play_instances() {
235 let server = MockServer::start().await;
236 Mock::given(method("DELETE"))
237 .respond_with(ResponseTemplate::new(200))
238 .mount(&server)
239 .await;
240
241 let config = PlayConfig::new(
242 "https://api.example.com",
243 "https://identity.example.com",
244 server.uri(),
245 );
246
247 Play::builder()
248 .config(config.clone())
249 .run(|play1| async move {
250 assert!(Uuid::parse_str(play1.play_id()).is_ok());
252 assert_eq!(play1.config().seeder_url, server.uri());
253 })
254 .await;
255 }
256
257 #[tokio::test]
258 async fn test_unique_play_ids() {
259 let server = MockServer::start().await;
260 Mock::given(method("DELETE"))
261 .respond_with(ResponseTemplate::new(200))
262 .mount(&server)
263 .await;
264
265 let config = PlayConfig::new(
266 "https://api.example.com",
267 "https://identity.example.com",
268 server.uri(),
269 );
270
271 let play1 = Play::new_internal(config.clone());
272 let play2 = Play::new_internal(config);
273
274 assert!(Uuid::parse_str(play1.play_id()).is_ok());
276 assert!(Uuid::parse_str(play2.play_id()).is_ok());
277 assert_ne!(play1.play_id(), play2.play_id());
278 }
279
280 struct MockScene;
282
283 #[derive(Clone, Serialize)]
284 struct MockSceneArgs {
285 name: String,
286 }
287
288 #[derive(Deserialize)]
289 struct MockSceneResult {
290 data: String,
291 }
292
293 impl SceneTemplate for MockScene {
294 type Arguments = MockSceneArgs;
295 type Result = MockSceneResult;
296
297 fn template_name() -> &'static str {
298 "MockScene"
299 }
300 }
301
302 #[derive(Debug, Clone)]
303 struct MockQuery {
304 args: MockQueryArgs,
305 value: i32,
306 }
307
308 #[derive(Debug, Clone, Serialize)]
309 struct MockQueryArgs {
310 id: String,
311 }
312
313 impl Query for MockQuery {
314 type Args = MockQueryArgs;
315 type Result = MockQueryResult;
316
317 fn template_name() -> &'static str {
318 "MockQuery"
319 }
320
321 fn args(&self) -> &Self::Args {
322 &self.args
323 }
324
325 fn from_result(result: Self::Result) -> Self {
326 Self {
327 args: MockQueryArgs { id: String::new() },
328 value: result.value,
329 }
330 }
331 }
332
333 #[derive(Deserialize)]
334 struct MockQueryResult {
335 value: i32,
336 }
337
338 #[tokio::test]
339 async fn test_scene_and_query() {
340 let (play, server) = play_with_mock_server().await;
341
342 Mock::given(method("POST"))
344 .and(path("/seed/"))
345 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
346 "result": { "data": "test-data" },
347 "mangleMap": { "[email protected]": "[email protected]" }
348 })))
349 .mount(&server)
350 .await;
351
352 let scene = play
353 .scene::<MockScene>(&MockSceneArgs {
354 name: "test".into(),
355 })
356 .await
357 .expect("scene creation should succeed");
358 assert_eq!(scene.result().data, "test-data");
359 assert_eq!(
360 scene.get_mangled("[email protected]"),
361 "[email protected]"
362 );
363
364 Mock::given(method("POST"))
366 .and(path("/seed/query"))
367 .respond_with(
368 ResponseTemplate::new(200).set_body_json(serde_json::json!({ "value": 42 })),
369 )
370 .mount(&server)
371 .await;
372
373 let result = play
374 .query::<MockQuery>(&MockQueryArgs { id: "test".into() })
375 .await
376 .expect("query should succeed");
377 assert_eq!(result.value, 42);
378 }
379
380 #[tokio::test]
381 async fn test_server_error_handling() {
382 let (play, server) = play_with_mock_server().await;
383
384 Mock::given(method("POST"))
385 .and(path("/seed/"))
386 .respond_with(ResponseTemplate::new(500))
387 .mount(&server)
388 .await;
389
390 let result = play
391 .scene::<MockScene>(&MockSceneArgs {
392 name: "test".into(),
393 })
394 .await;
395
396 assert!(matches!(
397 result,
398 Err(super::super::PlayError::ServerError { status: 500, .. })
399 ));
400 }
401
402 #[tokio::test]
403 async fn test_builder_runs_cleanup() {
404 let server = MockServer::start().await;
405 Mock::given(method("DELETE"))
406 .respond_with(ResponseTemplate::new(200))
407 .expect(1)
408 .mount(&server)
409 .await;
410
411 let config = PlayConfig::new(
412 "https://api.example.com",
413 "https://identity.example.com",
414 server.uri(),
415 );
416
417 Play::builder()
418 .config(config)
419 .run(|_play| async move {
420 })
422 .await;
423
424 }
426
427 #[tokio::test]
428 async fn test_clean() {
429 let server = MockServer::start().await;
430 Mock::given(method("DELETE"))
431 .respond_with(ResponseTemplate::new(200))
432 .expect(1)
433 .mount(&server)
434 .await;
435
436 let config = PlayConfig::new(
437 "https://api.example.com",
438 "https://identity.example.com",
439 server.uri(),
440 );
441 let play = Play::new_internal(config);
442
443 assert!(play.clean().await.is_ok());
444 }
445}