Skip to main content

winterbaume_workspaces/
handlers.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Arc;
4
5use serde_json::{Value, json};
6use winterbaume_core::{
7    BackendState, MockRequest, MockResponse, MockService, StateChangeNotifier, StatefulService,
8    default_account_id,
9};
10
11use crate::model;
12use crate::state::WorkSpacesState;
13use crate::types::{
14    ClientProperties, IpRule, RebootRequest, RebuildRequest, SelfservicePermissions, StartRequest,
15    StopRequest, Tag, TerminateRequest, Workspace, WorkspaceCreationProperties, WorkspaceDirectory,
16    WorkspaceProperties, WorkspaceRequest, WorkspacesPool,
17};
18use crate::views::WorkSpacesStateView;
19use crate::wire;
20
21/// Convert an RFC 3339 timestamp string into the wire's `f64` epoch seconds.
22fn rfc3339_to_epoch(s: &str) -> Option<f64> {
23    chrono::DateTime::parse_from_rfc3339(s)
24        .ok()
25        .map(|dt| dt.timestamp() as f64)
26}
27
28/// WorkSpaces service handler that processes awsJson1.1 protocol requests.
29pub struct WorkSpacesService {
30    pub(crate) state: Arc<BackendState<WorkSpacesState>>,
31    pub(crate) notifier: StateChangeNotifier<WorkSpacesStateView>,
32}
33
34impl WorkSpacesService {
35    pub fn new() -> Self {
36        Self {
37            state: Arc::new(BackendState::new()),
38            notifier: StateChangeNotifier::new(),
39        }
40    }
41}
42
43impl Default for WorkSpacesService {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49impl MockService for WorkSpacesService {
50    fn service_name(&self) -> &str {
51        "workspaces"
52    }
53
54    fn url_patterns(&self) -> Vec<&str> {
55        vec![
56            r"https?://workspaces\..*\.amazonaws\.com",
57            r"https?://workspaces\.amazonaws\.com",
58        ]
59    }
60
61    fn handle(
62        &self,
63        request: MockRequest,
64    ) -> Pin<Box<dyn Future<Output = MockResponse> + Send + '_>> {
65        Box::pin(async move { self.dispatch(request).await })
66    }
67}
68
69impl WorkSpacesService {
70    async fn dispatch(&self, request: MockRequest) -> MockResponse {
71        let region = winterbaume_core::auth::extract_region_from_uri(&request.uri);
72        let account_id = default_account_id();
73
74        // Extract action from X-Amz-Target header
75        // Format: "WorkspacesService.CreateWorkspaces"
76        let action = request
77            .headers
78            .get("x-amz-target")
79            .and_then(|v| v.to_str().ok())
80            .and_then(|v| v.split('.').next_back())
81            .map(|s| s.to_string());
82
83        let action = match action {
84            Some(a) => a,
85            None => {
86                return json_error_response(400, "MissingAction", "Missing X-Amz-Target header");
87            }
88        };
89
90        // Validate the body is well-formed JSON up-front; the typed deserialisers in
91        // `wire` re-parse the bytes per operation. An empty body is treated as `{}`.
92        let body_bytes: &[u8] = if request.body.is_empty() {
93            b"{}"
94        } else {
95            if serde_json::from_slice::<Value>(&request.body).is_err() {
96                return json_error_response(400, "SerializationException", "Invalid JSON body");
97            }
98            &request.body
99        };
100
101        let state = self.state.get(account_id, &region);
102
103        let response = match action.as_str() {
104            "CreateWorkspaces" => {
105                self.handle_create_workspaces(&state, body_bytes, account_id, &region)
106                    .await
107            }
108            "DescribeWorkspaces" => self.handle_describe_workspaces(&state, body_bytes).await,
109            "TerminateWorkspaces" => self.handle_terminate_workspaces(&state, body_bytes).await,
110            "DescribeWorkspaceDirectories" => {
111                self.handle_describe_workspace_directories(&state, body_bytes)
112                    .await
113            }
114            "CreateTags" => self.handle_create_tags(&state, body_bytes).await,
115            "DescribeTags" => self.handle_describe_tags(&state, body_bytes).await,
116            "CreateWorkspaceImage" => {
117                self.handle_create_workspace_image(&state, body_bytes, account_id)
118                    .await
119            }
120            "DescribeWorkspaceImages" => {
121                self.handle_describe_workspace_images(&state, body_bytes)
122                    .await
123            }
124            "DescribeWorkspaceImagePermissions" => {
125                self.handle_describe_workspace_image_permissions(&state, body_bytes)
126                    .await
127            }
128            "UpdateWorkspaceImagePermission" => {
129                self.handle_update_workspace_image_permission(&state, body_bytes)
130                    .await
131            }
132            "DescribeClientProperties" => {
133                self.handle_describe_client_properties(&state, body_bytes)
134                    .await
135            }
136            "ModifyClientProperties" => {
137                self.handle_modify_client_properties(&state, body_bytes)
138                    .await
139            }
140            "ModifySelfservicePermissions" => {
141                self.handle_modify_selfservice_permissions(&state, body_bytes)
142                    .await
143            }
144            "ModifyWorkspaceCreationProperties" => {
145                self.handle_modify_workspace_creation_properties(&state, body_bytes)
146                    .await
147            }
148            "RegisterWorkspaceDirectory" => {
149                self.handle_register_workspace_directory(&state, body_bytes, account_id, &region)
150                    .await
151            }
152            "DeregisterWorkspaceDirectory" => {
153                self.handle_deregister_workspace_directory(&state, body_bytes)
154                    .await
155            }
156            "DeleteTags" => self.handle_delete_tags(&state, body_bytes).await,
157            "StartWorkspaces" => self.handle_start_workspaces(&state, body_bytes).await,
158            "StopWorkspaces" => self.handle_stop_workspaces(&state, body_bytes).await,
159            "RebootWorkspaces" => self.handle_reboot_workspaces(&state, body_bytes).await,
160            "RebuildWorkspaces" => self.handle_rebuild_workspaces(&state, body_bytes).await,
161            "RestoreWorkspace" => self.handle_restore_workspace(&state, body_bytes).await,
162            "ModifyWorkspaceProperties" => {
163                self.handle_modify_workspace_properties(&state, body_bytes)
164                    .await
165            }
166            "ModifyWorkspaceState" => self.handle_modify_workspace_state(&state, body_bytes).await,
167            "DescribeWorkspacesConnectionStatus" => {
168                self.handle_describe_workspaces_connection_status(&state, body_bytes)
169                    .await
170            }
171            "CreateIpGroup" => {
172                self.handle_create_ip_group(&state, body_bytes, account_id)
173                    .await
174            }
175            "DescribeIpGroups" => self.handle_describe_ip_groups(&state, body_bytes).await,
176            "DeleteIpGroup" => self.handle_delete_ip_group(&state, body_bytes).await,
177            "AuthorizeIpRules" => self.handle_authorize_ip_rules(&state, body_bytes).await,
178            "RevokeIpRules" => self.handle_revoke_ip_rules(&state, body_bytes).await,
179            "UpdateRulesOfIpGroup" => {
180                self.handle_update_rules_of_ip_group(&state, body_bytes)
181                    .await
182            }
183            "AssociateIpGroups" => self.handle_associate_ip_groups(&state, body_bytes).await,
184            "DisassociateIpGroups" => self.handle_disassociate_ip_groups(&state, body_bytes).await,
185            "CreateConnectionAlias" => {
186                self.handle_create_connection_alias(&state, body_bytes, account_id)
187                    .await
188            }
189            "DescribeConnectionAliases" => {
190                self.handle_describe_connection_aliases(&state, body_bytes)
191                    .await
192            }
193            "DeleteConnectionAlias" => {
194                self.handle_delete_connection_alias(&state, body_bytes)
195                    .await
196            }
197            "AssociateConnectionAlias" => {
198                self.handle_associate_connection_alias(&state, body_bytes)
199                    .await
200            }
201            "DisassociateConnectionAlias" => {
202                self.handle_disassociate_connection_alias(&state, body_bytes)
203                    .await
204            }
205            "DescribeConnectionAliasPermissions" => {
206                self.handle_describe_connection_alias_permissions(&state, body_bytes)
207                    .await
208            }
209            "UpdateConnectionAliasPermission" => {
210                self.handle_update_connection_alias_permission(&state, body_bytes)
211                    .await
212            }
213            "DescribeWorkspaceBundles" => {
214                self.handle_describe_workspace_bundles(&state, body_bytes)
215                    .await
216            }
217            "CreateWorkspaceBundle" => {
218                self.handle_create_workspace_bundle(&state, body_bytes, account_id)
219                    .await
220            }
221            "DeleteWorkspaceBundle" => {
222                self.handle_delete_workspace_bundle(&state, body_bytes)
223                    .await
224            }
225            "DeleteWorkspaceImage" => self.handle_delete_workspace_image(&state, body_bytes).await,
226            "CreateWorkspacesPool" => {
227                self.handle_create_workspaces_pool(&state, body_bytes, account_id, &region)
228                    .await
229            }
230            "DescribeWorkspacesPools" => {
231                self.handle_describe_workspaces_pools(&state, body_bytes)
232                    .await
233            }
234            "TerminateWorkspacesPool" => {
235                self.handle_terminate_workspaces_pool(&state, body_bytes)
236                    .await
237            }
238            "UpdateWorkspacesPool" => self.handle_update_workspaces_pool(&state, body_bytes).await,
239            "StartWorkspacesPool" => self.handle_start_workspaces_pool(&state, body_bytes).await,
240            "StopWorkspacesPool" => self.handle_stop_workspaces_pool(&state, body_bytes).await,
241            _ => json_error_response(
242                400,
243                "InvalidAction",
244                &format!("Could not find operation {action} for WorkSpaces"),
245            ),
246        };
247        if response.status / 100 == 2 {
248            self.notify_state_changed(account_id, &region).await;
249        }
250        response
251    }
252
253    async fn handle_create_workspaces(
254        &self,
255        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
256        body: &[u8],
257        account_id: &str,
258        region: &str,
259    ) -> MockResponse {
260        let input = match wire::deserialize_create_workspaces_request(body) {
261            Ok(v) => v,
262            Err(e) => return json_error_response(400, "ValidationException", &e),
263        };
264        let requests: Vec<WorkspaceRequest> = input
265            .workspaces
266            .into_iter()
267            .filter_map(|item| {
268                if item.directory_id.is_empty()
269                    || item.user_name.is_empty()
270                    || item.bundle_id.is_empty()
271                {
272                    return None;
273                }
274                let workspace_properties =
275                    item.workspace_properties.map(|props| WorkspaceProperties {
276                        running_mode: props.running_mode,
277                        running_mode_auto_stop_timeout_in_minutes: props
278                            .running_mode_auto_stop_timeout_in_minutes,
279                        root_volume_size_gib: props.root_volume_size_gib,
280                        user_volume_size_gib: props.user_volume_size_gib,
281                        compute_type_name: props.compute_type_name,
282                    });
283                let tags: Option<Vec<Tag>> = item.tags.map(|arr| {
284                    arr.into_iter()
285                        .map(|t| Tag {
286                            key: t.key,
287                            value: t.value.unwrap_or_default(),
288                        })
289                        .collect()
290                });
291                Some(WorkspaceRequest {
292                    directory_id: item.directory_id,
293                    user_name: item.user_name,
294                    bundle_id: item.bundle_id,
295                    volume_encryption_key: item.volume_encryption_key,
296                    user_volume_encryption_enabled: item.user_volume_encryption_enabled,
297                    root_volume_encryption_enabled: item.root_volume_encryption_enabled,
298                    workspace_properties,
299                    tags,
300                })
301            })
302            .collect();
303
304        let mut state = state.write().await;
305        let (pending, failed) = state.create_workspaces(&requests, account_id, region);
306
307        let pending_wire: Vec<model::Workspace> = pending.iter().map(workspace_to_model).collect();
308        let failed_wire: Vec<model::FailedCreateWorkspaceRequest> = failed
309            .iter()
310            .map(|f| model::FailedCreateWorkspaceRequest {
311                workspace_request: f
312                    .workspace_request
313                    .as_ref()
314                    .map(|r| model::WorkspaceRequest {
315                        directory_id: r.directory_id.clone(),
316                        user_name: r.user_name.clone(),
317                        bundle_id: r.bundle_id.clone(),
318                        ..Default::default()
319                    }),
320                error_code: Some(f.error_code.clone()),
321                error_message: Some(f.error_message.clone()),
322            })
323            .collect();
324
325        let result = model::CreateWorkspacesResult {
326            pending_requests: Some(pending_wire),
327            failed_requests: Some(failed_wire),
328        };
329        wire::serialize_create_workspaces_response(&result)
330    }
331
332    async fn handle_describe_workspaces(
333        &self,
334        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
335        body: &[u8],
336    ) -> MockResponse {
337        let input = match wire::deserialize_describe_workspaces_request(body) {
338            Ok(v) => v,
339            Err(e) => return json_error_response(400, "ValidationException", &e),
340        };
341        let workspace_ids = input.workspace_ids;
342        let directory_id = input.directory_id.as_deref();
343        let user_name = input.user_name.as_deref();
344
345        let state = state.read().await;
346        let workspaces =
347            state.describe_workspaces(workspace_ids.as_deref(), directory_id, user_name);
348
349        let entries: Vec<model::Workspace> =
350            workspaces.iter().map(|ws| workspace_to_model(ws)).collect();
351
352        let result = model::DescribeWorkspacesResult {
353            workspaces: Some(entries),
354            next_token: None,
355        };
356        wire::serialize_describe_workspaces_response(&result)
357    }
358
359    async fn handle_terminate_workspaces(
360        &self,
361        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
362        body: &[u8],
363    ) -> MockResponse {
364        let input = match wire::deserialize_terminate_workspaces_request(body) {
365            Ok(v) => v,
366            Err(e) => return json_error_response(400, "ValidationException", &e),
367        };
368        let requests: Vec<TerminateRequest> = input
369            .terminate_workspace_requests
370            .into_iter()
371            .map(|item| TerminateRequest {
372                workspace_id: item.workspace_id,
373            })
374            .collect();
375
376        let mut state = state.write().await;
377        let failed = state.terminate_workspaces(&requests);
378
379        let failed_wire: Vec<model::FailedWorkspaceChangeRequest> = failed
380            .iter()
381            .map(|f| model::FailedWorkspaceChangeRequest {
382                workspace_id: Some(f.workspace_id.clone()),
383                error_code: Some(f.error_code.clone()),
384                error_message: Some(f.error_message.clone()),
385            })
386            .collect();
387
388        let result = model::TerminateWorkspacesResult {
389            failed_requests: Some(failed_wire),
390        };
391        wire::serialize_terminate_workspaces_response(&result)
392    }
393
394    async fn handle_describe_workspace_directories(
395        &self,
396        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
397        body: &[u8],
398    ) -> MockResponse {
399        let input = match wire::deserialize_describe_workspace_directories_request(body) {
400            Ok(v) => v,
401            Err(e) => return json_error_response(400, "ValidationException", &e),
402        };
403        let directory_ids = input.directory_ids;
404
405        let state = state.read().await;
406        let directories = state.describe_workspace_directories(directory_ids.as_deref());
407
408        let entries: Vec<model::WorkspaceDirectory> = directories
409            .iter()
410            .map(|dir| directory_to_model(dir))
411            .collect();
412
413        let result = model::DescribeWorkspaceDirectoriesResult {
414            directories: Some(entries),
415            next_token: None,
416        };
417        wire::serialize_describe_workspace_directories_response(&result)
418    }
419
420    async fn handle_create_tags(
421        &self,
422        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
423        body: &[u8],
424    ) -> MockResponse {
425        let input = match wire::deserialize_create_tags_request(body) {
426            Ok(v) => v,
427            Err(e) => return json_error_response(400, "ValidationException", &e),
428        };
429        if input.resource_id.is_empty() {
430            return json_error_response(400, "ValidationException", "Missing 'ResourceId' field");
431        }
432        let resource_id = input.resource_id;
433        let tags: Vec<Tag> = input
434            .tags
435            .into_iter()
436            .map(|t| Tag {
437                key: t.key,
438                value: t.value.unwrap_or_default(),
439            })
440            .collect();
441
442        let mut state = state.write().await;
443        state.create_tags(&resource_id, &tags);
444
445        let result = model::CreateTagsResult {};
446        wire::serialize_create_tags_response(&result)
447    }
448
449    async fn handle_describe_tags(
450        &self,
451        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
452        body: &[u8],
453    ) -> MockResponse {
454        let input = match wire::deserialize_describe_tags_request(body) {
455            Ok(v) => v,
456            Err(e) => return json_error_response(400, "ValidationException", &e),
457        };
458        if input.resource_id.is_empty() {
459            return json_error_response(400, "ValidationException", "Missing 'ResourceId' field");
460        }
461        let resource_id = input.resource_id;
462
463        let state = state.read().await;
464        let tags = state.describe_tags(&resource_id);
465
466        let tag_list: Vec<model::Tag> = tags
467            .iter()
468            .map(|t| model::Tag {
469                key: t.key.clone(),
470                value: Some(t.value.clone()),
471            })
472            .collect();
473
474        let result = model::DescribeTagsResult {
475            tag_list: Some(tag_list),
476        };
477        wire::serialize_describe_tags_response(&result)
478    }
479
480    async fn handle_create_workspace_image(
481        &self,
482        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
483        body: &[u8],
484        account_id: &str,
485    ) -> MockResponse {
486        let input = match wire::deserialize_create_workspace_image_request(body) {
487            Ok(v) => v,
488            Err(e) => return json_error_response(400, "ValidationException", &e),
489        };
490        if input.name.is_empty() {
491            return json_error_response(400, "ValidationException", "Missing 'Name' field");
492        }
493        let name = input.name;
494        if input.description.is_empty() {
495            return json_error_response(400, "ValidationException", "Missing 'Description' field");
496        }
497        let description = input.description;
498        if input.workspace_id.is_empty() {
499            return json_error_response(400, "ValidationException", "Missing 'WorkspaceId' field");
500        }
501        let workspace_id = input.workspace_id;
502
503        let tags: Option<Vec<Tag>> = input.tags.map(|arr| {
504            arr.into_iter()
505                .map(|t| Tag {
506                    key: t.key,
507                    value: t.value.unwrap_or_default(),
508                })
509                .collect()
510        });
511
512        let mut state = state.write().await;
513        match state.create_workspace_image(
514            &name,
515            &description,
516            &workspace_id,
517            account_id,
518            tags.as_deref(),
519        ) {
520            Ok(image) => {
521                let result = model::CreateWorkspaceImageResult {
522                    image_id: Some(image.image_id),
523                    name: Some(image.name),
524                    description: Some(image.description),
525                    state: Some(image.state),
526                    operating_system: Some(model::OperatingSystem {
527                        r#type: image.operating_system_type,
528                    }),
529                    owner_account_id: Some(image.owner_account_id),
530                    required_tenancy: Some(image.required_tenancy),
531                    created: rfc3339_to_epoch(&image.created),
532                };
533                wire::serialize_create_workspace_image_response(&result)
534            }
535            Err(e) => workspaces_error_response(&e),
536        }
537    }
538
539    async fn handle_describe_workspace_images(
540        &self,
541        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
542        body: &[u8],
543    ) -> MockResponse {
544        let input = match wire::deserialize_describe_workspace_images_request(body) {
545            Ok(v) => v,
546            Err(e) => return json_error_response(400, "ValidationException", &e),
547        };
548        let image_ids = input.image_ids;
549
550        let state = state.read().await;
551        let images = state.describe_workspace_images(image_ids.as_deref());
552
553        let entries: Vec<model::WorkspaceImage> = images
554            .iter()
555            .map(|img| model::WorkspaceImage {
556                image_id: Some(img.image_id.clone()),
557                name: Some(img.name.clone()),
558                description: Some(img.description.clone()),
559                state: Some(img.state.clone()),
560                operating_system: Some(model::OperatingSystem {
561                    r#type: img.operating_system_type.clone(),
562                }),
563                owner_account_id: Some(img.owner_account_id.clone()),
564                required_tenancy: Some(img.required_tenancy.clone()),
565                created: rfc3339_to_epoch(&img.created),
566                ..Default::default()
567            })
568            .collect();
569
570        let result = model::DescribeWorkspaceImagesResult {
571            images: Some(entries),
572            next_token: None,
573        };
574        wire::serialize_describe_workspace_images_response(&result)
575    }
576
577    async fn handle_describe_workspace_image_permissions(
578        &self,
579        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
580        body: &[u8],
581    ) -> MockResponse {
582        let input = match wire::deserialize_describe_workspace_image_permissions_request(body) {
583            Ok(v) => v,
584            Err(e) => return json_error_response(400, "ValidationException", &e),
585        };
586        if input.image_id.is_empty() {
587            return json_error_response(400, "ValidationException", "Missing 'ImageId' field");
588        }
589        let image_id = input.image_id;
590
591        let state = state.read().await;
592        match state.describe_workspace_image_permissions(&image_id) {
593            Ok(shared_ids) => {
594                let perms: Vec<model::ImagePermission> = shared_ids
595                    .iter()
596                    .map(|id| model::ImagePermission {
597                        shared_account_id: Some(id.clone()),
598                    })
599                    .collect();
600
601                let result = model::DescribeWorkspaceImagePermissionsResult {
602                    image_id: Some(image_id),
603                    image_permissions: Some(perms),
604                    next_token: None,
605                };
606                wire::serialize_describe_workspace_image_permissions_response(&result)
607            }
608            Err(e) => workspaces_error_response(&e),
609        }
610    }
611
612    async fn handle_update_workspace_image_permission(
613        &self,
614        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
615        body: &[u8],
616    ) -> MockResponse {
617        let input = match wire::deserialize_update_workspace_image_permission_request(body) {
618            Ok(v) => v,
619            Err(e) => return json_error_response(400, "ValidationException", &e),
620        };
621        if input.image_id.is_empty() {
622            return json_error_response(400, "ValidationException", "Missing 'ImageId' field");
623        }
624        let image_id = input.image_id;
625        if input.shared_account_id.is_empty() {
626            return json_error_response(
627                400,
628                "ValidationException",
629                "Missing 'SharedAccountId' field",
630            );
631        }
632        let shared_account_id = input.shared_account_id;
633        let allow_copy_image = input.allow_copy_image;
634
635        let mut state = state.write().await;
636        match state.update_workspace_image_permission(
637            &image_id,
638            &shared_account_id,
639            allow_copy_image,
640        ) {
641            Ok(()) => {
642                let result = model::UpdateWorkspaceImagePermissionResult {};
643                wire::serialize_update_workspace_image_permission_response(&result)
644            }
645            Err(e) => workspaces_error_response(&e),
646        }
647    }
648
649    async fn handle_describe_client_properties(
650        &self,
651        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
652        body: &[u8],
653    ) -> MockResponse {
654        let input = match wire::deserialize_describe_client_properties_request(body) {
655            Ok(v) => v,
656            Err(e) => return json_error_response(400, "ValidationException", &e),
657        };
658        let resource_ids = input.resource_ids;
659
660        let state = state.read().await;
661        let props = state.describe_client_properties(&resource_ids);
662
663        let entries: Vec<model::ClientPropertiesResult> = props
664            .iter()
665            .map(|(id, cp)| model::ClientPropertiesResult {
666                resource_id: Some(id.clone()),
667                client_properties: Some(model::ClientProperties {
668                    reconnect_enabled: cp.reconnect_enabled.clone(),
669                    log_upload_enabled: cp.log_upload_enabled.clone(),
670                }),
671            })
672            .collect();
673
674        let result = model::DescribeClientPropertiesResult {
675            client_properties_list: Some(entries),
676        };
677        wire::serialize_describe_client_properties_response(&result)
678    }
679
680    async fn handle_modify_client_properties(
681        &self,
682        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
683        body: &[u8],
684    ) -> MockResponse {
685        let input = match wire::deserialize_modify_client_properties_request(body) {
686            Ok(v) => v,
687            Err(e) => return json_error_response(400, "ValidationException", &e),
688        };
689        if input.resource_id.is_empty() {
690            return json_error_response(400, "ValidationException", "Missing 'ResourceId' field");
691        }
692        let resource_id = input.resource_id;
693
694        let client_props = ClientProperties {
695            reconnect_enabled: input.client_properties.reconnect_enabled,
696            log_upload_enabled: input.client_properties.log_upload_enabled,
697        };
698
699        let mut state = state.write().await;
700        state.modify_client_properties(&resource_id, &client_props);
701
702        let result = model::ModifyClientPropertiesResult {};
703        wire::serialize_modify_client_properties_response(&result)
704    }
705
706    async fn handle_modify_selfservice_permissions(
707        &self,
708        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
709        body: &[u8],
710    ) -> MockResponse {
711        let input = match wire::deserialize_modify_selfservice_permissions_request(body) {
712            Ok(v) => v,
713            Err(e) => return json_error_response(400, "ValidationException", &e),
714        };
715        if input.resource_id.is_empty() {
716            return json_error_response(400, "ValidationException", "Missing 'ResourceId' field");
717        }
718        let resource_id = input.resource_id;
719
720        let sp = input.selfservice_permissions;
721        let perms = SelfservicePermissions {
722            restart_workspace: sp.restart_workspace,
723            increase_volume_size: sp.increase_volume_size,
724            change_compute_type: sp.change_compute_type,
725            switch_running_mode: sp.switch_running_mode,
726            rebuild_workspace: sp.rebuild_workspace,
727        };
728
729        let mut state = state.write().await;
730        match state.modify_selfservice_permissions(&resource_id, &perms) {
731            Ok(()) => {
732                let result = model::ModifySelfservicePermissionsResult {};
733                wire::serialize_modify_selfservice_permissions_response(&result)
734            }
735            Err(e) => workspaces_error_response(&e),
736        }
737    }
738
739    async fn handle_modify_workspace_creation_properties(
740        &self,
741        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
742        body: &[u8],
743    ) -> MockResponse {
744        let input = match wire::deserialize_modify_workspace_creation_properties_request(body) {
745            Ok(v) => v,
746            Err(e) => return json_error_response(400, "ValidationException", &e),
747        };
748        if input.resource_id.is_empty() {
749            return json_error_response(400, "ValidationException", "Missing 'ResourceId' field");
750        }
751        let resource_id = input.resource_id;
752
753        let wcp = input.workspace_creation_properties;
754        let props = WorkspaceCreationProperties {
755            custom_security_group_id: wcp.custom_security_group_id,
756            default_ou: wcp.default_ou,
757            enable_internet_access: wcp.enable_internet_access,
758            enable_maintenance_mode: wcp.enable_maintenance_mode,
759            user_enabled_as_local_administrator: wcp.user_enabled_as_local_administrator,
760            instance_iam_role_arn: wcp.instance_iam_role_arn,
761        };
762
763        let mut state = state.write().await;
764        match state.modify_workspace_creation_properties(&resource_id, &props) {
765            Ok(()) => {
766                let result = model::ModifyWorkspaceCreationPropertiesResult {};
767                wire::serialize_modify_workspace_creation_properties_response(&result)
768            }
769            Err(e) => workspaces_error_response(&e),
770        }
771    }
772
773    async fn handle_register_workspace_directory(
774        &self,
775        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
776        body: &[u8],
777        account_id: &str,
778        region: &str,
779    ) -> MockResponse {
780        let input = match wire::deserialize_register_workspace_directory_request(body) {
781            Ok(v) => v,
782            Err(e) => return json_error_response(400, "ValidationException", &e),
783        };
784        let directory_id = match input.directory_id {
785            Some(id) if !id.is_empty() => id,
786            _ => {
787                return json_error_response(
788                    400,
789                    "ValidationException",
790                    "Missing 'DirectoryId' field",
791                );
792            }
793        };
794
795        let mut state = state.write().await;
796        match state.register_workspace_directory(&directory_id, account_id, region) {
797            Ok(dir) => {
798                let result = model::RegisterWorkspaceDirectoryResult {
799                    directory_id: Some(dir.directory_id),
800                    state: Some(dir.state),
801                };
802                wire::serialize_register_workspace_directory_response(&result)
803            }
804            Err(e) => workspaces_error_response(&e),
805        }
806    }
807
808    async fn handle_deregister_workspace_directory(
809        &self,
810        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
811        body: &[u8],
812    ) -> MockResponse {
813        let input = match wire::deserialize_deregister_workspace_directory_request(body) {
814            Ok(v) => v,
815            Err(e) => return json_error_response(400, "ValidationException", &e),
816        };
817        if input.directory_id.is_empty() {
818            return json_error_response(400, "ValidationException", "Missing 'DirectoryId' field");
819        }
820        let directory_id = input.directory_id;
821
822        let mut state = state.write().await;
823        match state.deregister_workspace_directory(&directory_id) {
824            Ok(()) => {
825                let result = model::DeregisterWorkspaceDirectoryResult {};
826                wire::serialize_deregister_workspace_directory_response(&result)
827            }
828            Err(e) => workspaces_error_response(&e),
829        }
830    }
831
832    async fn handle_delete_tags(
833        &self,
834        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
835        body: &[u8],
836    ) -> MockResponse {
837        let input = match wire::deserialize_delete_tags_request(body) {
838            Ok(v) => v,
839            Err(e) => return json_error_response(400, "ValidationException", &e),
840        };
841        if input.resource_id.is_empty() {
842            return json_error_response(400, "ValidationException", "Missing 'ResourceId' field");
843        }
844        let resource_id = input.resource_id;
845        let tag_keys = input.tag_keys;
846
847        let mut state = state.write().await;
848        state.delete_tags(&resource_id, &tag_keys);
849
850        let result = model::DeleteTagsResult {};
851        wire::serialize_delete_tags_response(&result)
852    }
853
854    async fn handle_start_workspaces(
855        &self,
856        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
857        body: &[u8],
858    ) -> MockResponse {
859        let input = match wire::deserialize_start_workspaces_request(body) {
860            Ok(v) => v,
861            Err(e) => return json_error_response(400, "ValidationException", &e),
862        };
863        let requests: Vec<StartRequest> = input
864            .start_workspace_requests
865            .into_iter()
866            .filter_map(|item| {
867                item.workspace_id
868                    .map(|workspace_id| StartRequest { workspace_id })
869            })
870            .collect();
871
872        let mut state = state.write().await;
873        let failed = state.start_workspaces(&requests);
874
875        let failed_wire: Vec<model::FailedWorkspaceChangeRequest> = failed
876            .iter()
877            .map(|f| model::FailedWorkspaceChangeRequest {
878                workspace_id: Some(f.workspace_id.clone()),
879                error_code: Some(f.error_code.clone()),
880                error_message: Some(f.error_message.clone()),
881            })
882            .collect();
883        let result = model::StartWorkspacesResult {
884            failed_requests: Some(failed_wire),
885        };
886        wire::serialize_start_workspaces_response(&result)
887    }
888
889    async fn handle_stop_workspaces(
890        &self,
891        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
892        body: &[u8],
893    ) -> MockResponse {
894        let input = match wire::deserialize_stop_workspaces_request(body) {
895            Ok(v) => v,
896            Err(e) => return json_error_response(400, "ValidationException", &e),
897        };
898        let requests: Vec<StopRequest> = input
899            .stop_workspace_requests
900            .into_iter()
901            .filter_map(|item| {
902                item.workspace_id
903                    .map(|workspace_id| StopRequest { workspace_id })
904            })
905            .collect();
906
907        let mut state = state.write().await;
908        let failed = state.stop_workspaces(&requests);
909
910        let failed_wire: Vec<model::FailedWorkspaceChangeRequest> = failed
911            .iter()
912            .map(|f| model::FailedWorkspaceChangeRequest {
913                workspace_id: Some(f.workspace_id.clone()),
914                error_code: Some(f.error_code.clone()),
915                error_message: Some(f.error_message.clone()),
916            })
917            .collect();
918        let result = model::StopWorkspacesResult {
919            failed_requests: Some(failed_wire),
920        };
921        wire::serialize_stop_workspaces_response(&result)
922    }
923
924    async fn handle_reboot_workspaces(
925        &self,
926        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
927        body: &[u8],
928    ) -> MockResponse {
929        let input = match wire::deserialize_reboot_workspaces_request(body) {
930            Ok(v) => v,
931            Err(e) => return json_error_response(400, "ValidationException", &e),
932        };
933        let requests: Vec<RebootRequest> = input
934            .reboot_workspace_requests
935            .into_iter()
936            .map(|item| RebootRequest {
937                workspace_id: item.workspace_id,
938            })
939            .collect();
940
941        let mut state = state.write().await;
942        let failed = state.reboot_workspaces(&requests);
943
944        let failed_wire: Vec<model::FailedWorkspaceChangeRequest> = failed
945            .iter()
946            .map(|f| model::FailedWorkspaceChangeRequest {
947                workspace_id: Some(f.workspace_id.clone()),
948                error_code: Some(f.error_code.clone()),
949                error_message: Some(f.error_message.clone()),
950            })
951            .collect();
952        let result = model::RebootWorkspacesResult {
953            failed_requests: Some(failed_wire),
954        };
955        wire::serialize_reboot_workspaces_response(&result)
956    }
957
958    async fn handle_rebuild_workspaces(
959        &self,
960        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
961        body: &[u8],
962    ) -> MockResponse {
963        let input = match wire::deserialize_rebuild_workspaces_request(body) {
964            Ok(v) => v,
965            Err(e) => return json_error_response(400, "ValidationException", &e),
966        };
967        let requests: Vec<RebuildRequest> = input
968            .rebuild_workspace_requests
969            .into_iter()
970            .map(|item| RebuildRequest {
971                workspace_id: item.workspace_id,
972            })
973            .collect();
974
975        let mut state = state.write().await;
976        let failed = state.rebuild_workspaces(&requests);
977
978        let failed_wire: Vec<model::FailedWorkspaceChangeRequest> = failed
979            .iter()
980            .map(|f| model::FailedWorkspaceChangeRequest {
981                workspace_id: Some(f.workspace_id.clone()),
982                error_code: Some(f.error_code.clone()),
983                error_message: Some(f.error_message.clone()),
984            })
985            .collect();
986        let result = model::RebuildWorkspacesResult {
987            failed_requests: Some(failed_wire),
988        };
989        wire::serialize_rebuild_workspaces_response(&result)
990    }
991
992    async fn handle_restore_workspace(
993        &self,
994        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
995        body: &[u8],
996    ) -> MockResponse {
997        let input = match wire::deserialize_restore_workspace_request(body) {
998            Ok(v) => v,
999            Err(e) => return json_error_response(400, "ValidationException", &e),
1000        };
1001        if input.workspace_id.is_empty() {
1002            return json_error_response(400, "ValidationException", "Missing 'WorkspaceId' field");
1003        }
1004        let workspace_id = input.workspace_id;
1005
1006        let mut state = state.write().await;
1007        match state.restore_workspace(&workspace_id) {
1008            Ok(()) => {
1009                let result = model::RestoreWorkspaceResult {};
1010                wire::serialize_restore_workspace_response(&result)
1011            }
1012            Err(e) => workspaces_error_response(&e),
1013        }
1014    }
1015
1016    async fn handle_modify_workspace_properties(
1017        &self,
1018        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1019        body: &[u8],
1020    ) -> MockResponse {
1021        let input = match wire::deserialize_modify_workspace_properties_request(body) {
1022            Ok(v) => v,
1023            Err(e) => return json_error_response(400, "ValidationException", &e),
1024        };
1025        if input.workspace_id.is_empty() {
1026            return json_error_response(400, "ValidationException", "Missing 'WorkspaceId' field");
1027        }
1028        let workspace_id = input.workspace_id;
1029
1030        let (
1031            running_mode,
1032            root_volume_size_gib,
1033            user_volume_size_gib,
1034            compute_type_name,
1035            running_mode_auto_stop,
1036        ) = match input.workspace_properties {
1037            Some(p) => (
1038                p.running_mode,
1039                p.root_volume_size_gib,
1040                p.user_volume_size_gib,
1041                p.compute_type_name,
1042                p.running_mode_auto_stop_timeout_in_minutes,
1043            ),
1044            None => (None, None, None, None, None),
1045        };
1046
1047        let mut state = state.write().await;
1048        match state.modify_workspace_properties(
1049            &workspace_id,
1050            running_mode.as_deref(),
1051            root_volume_size_gib,
1052            user_volume_size_gib,
1053            compute_type_name.as_deref(),
1054            running_mode_auto_stop,
1055        ) {
1056            Ok(()) => {
1057                let result = model::ModifyWorkspacePropertiesResult {};
1058                wire::serialize_modify_workspace_properties_response(&result)
1059            }
1060            Err(e) => workspaces_error_response(&e),
1061        }
1062    }
1063
1064    async fn handle_modify_workspace_state(
1065        &self,
1066        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1067        body: &[u8],
1068    ) -> MockResponse {
1069        let input = match wire::deserialize_modify_workspace_state_request(body) {
1070            Ok(v) => v,
1071            Err(e) => return json_error_response(400, "ValidationException", &e),
1072        };
1073        if input.workspace_id.is_empty() {
1074            return json_error_response(400, "ValidationException", "Missing 'WorkspaceId' field");
1075        }
1076        let workspace_id = input.workspace_id;
1077        if input.workspace_state.is_empty() {
1078            return json_error_response(
1079                400,
1080                "ValidationException",
1081                "Missing 'WorkspaceState' field",
1082            );
1083        }
1084        let workspace_state = input.workspace_state;
1085
1086        let mut state = state.write().await;
1087        match state.modify_workspace_state(&workspace_id, &workspace_state) {
1088            Ok(()) => {
1089                let result = model::ModifyWorkspaceStateResult {};
1090                wire::serialize_modify_workspace_state_response(&result)
1091            }
1092            Err(e) => workspaces_error_response(&e),
1093        }
1094    }
1095
1096    async fn handle_describe_workspaces_connection_status(
1097        &self,
1098        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1099        body: &[u8],
1100    ) -> MockResponse {
1101        let input = match wire::deserialize_describe_workspaces_connection_status_request(body) {
1102            Ok(v) => v,
1103            Err(e) => return json_error_response(400, "ValidationException", &e),
1104        };
1105        let workspace_ids = input.workspace_ids;
1106
1107        let state = state.read().await;
1108        let statuses = state.describe_workspaces_connection_status(workspace_ids.as_deref());
1109
1110        let entries: Vec<model::WorkspaceConnectionStatus> = statuses
1111            .iter()
1112            .map(|(id, st)| model::WorkspaceConnectionStatus {
1113                workspace_id: Some(id.to_string()),
1114                connection_state: Some(if *st == "AVAILABLE" {
1115                    "CONNECTED".to_string()
1116                } else {
1117                    "DISCONNECTED".to_string()
1118                }),
1119                ..Default::default()
1120            })
1121            .collect();
1122
1123        let result = model::DescribeWorkspacesConnectionStatusResult {
1124            workspaces_connection_status: Some(entries),
1125            next_token: None,
1126        };
1127        wire::serialize_describe_workspaces_connection_status_response(&result)
1128    }
1129
1130    // --- IP Group handlers ---
1131
1132    async fn handle_create_ip_group(
1133        &self,
1134        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1135        body: &[u8],
1136        account_id: &str,
1137    ) -> MockResponse {
1138        let input = match wire::deserialize_create_ip_group_request(body) {
1139            Ok(v) => v,
1140            Err(e) => return json_error_response(400, "ValidationException", &e),
1141        };
1142        if input.group_name.is_empty() {
1143            return json_error_response(400, "ValidationException", "Missing 'GroupName' field");
1144        }
1145        let group_name = input.group_name;
1146        let group_desc = input.group_desc;
1147        let user_rules: Vec<IpRule> = input
1148            .user_rules
1149            .unwrap_or_default()
1150            .into_iter()
1151            .filter_map(|item| {
1152                item.ip_rule.map(|ip_rule| IpRule {
1153                    ip_rule,
1154                    rule_desc: item.rule_desc,
1155                })
1156            })
1157            .collect();
1158
1159        let mut state = state.write().await;
1160        let group =
1161            state.create_ip_group(&group_name, group_desc.as_deref(), user_rules, account_id);
1162
1163        let result = model::CreateIpGroupResult {
1164            group_id: Some(group.group_id),
1165        };
1166        wire::serialize_create_ip_group_response(&result)
1167    }
1168
1169    async fn handle_describe_ip_groups(
1170        &self,
1171        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1172        body: &[u8],
1173    ) -> MockResponse {
1174        let input = match wire::deserialize_describe_ip_groups_request(body) {
1175            Ok(v) => v,
1176            Err(e) => return json_error_response(400, "ValidationException", &e),
1177        };
1178        let group_ids = input.group_ids;
1179
1180        let state = state.read().await;
1181        let groups = state.describe_ip_groups(group_ids.as_deref());
1182
1183        let entries: Vec<model::WorkspacesIpGroup> = groups
1184            .iter()
1185            .map(|g| model::WorkspacesIpGroup {
1186                group_id: Some(g.group_id.clone()),
1187                group_name: Some(g.group_name.clone()),
1188                group_desc: g.group_desc.clone(),
1189                user_rules: Some(
1190                    g.user_rules
1191                        .iter()
1192                        .map(|r| model::IpRuleItem {
1193                            ip_rule: Some(r.ip_rule.clone()),
1194                            rule_desc: r.rule_desc.clone(),
1195                        })
1196                        .collect(),
1197                ),
1198            })
1199            .collect();
1200
1201        let result = model::DescribeIpGroupsResult {
1202            result: Some(entries),
1203            next_token: None,
1204        };
1205        wire::serialize_describe_ip_groups_response(&result)
1206    }
1207
1208    async fn handle_delete_ip_group(
1209        &self,
1210        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1211        body: &[u8],
1212    ) -> MockResponse {
1213        let input = match wire::deserialize_delete_ip_group_request(body) {
1214            Ok(v) => v,
1215            Err(e) => return json_error_response(400, "ValidationException", &e),
1216        };
1217        if input.group_id.is_empty() {
1218            return json_error_response(400, "ValidationException", "Missing 'GroupId' field");
1219        }
1220        let group_id = input.group_id;
1221
1222        let mut state = state.write().await;
1223        match state.delete_ip_group(&group_id) {
1224            Ok(()) => {
1225                let result = model::DeleteIpGroupResult {};
1226                wire::serialize_delete_ip_group_response(&result)
1227            }
1228            Err(e) => workspaces_error_response(&e),
1229        }
1230    }
1231
1232    async fn handle_authorize_ip_rules(
1233        &self,
1234        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1235        body: &[u8],
1236    ) -> MockResponse {
1237        let input = match wire::deserialize_authorize_ip_rules_request(body) {
1238            Ok(v) => v,
1239            Err(e) => return json_error_response(400, "ValidationException", &e),
1240        };
1241        if input.group_id.is_empty() {
1242            return json_error_response(400, "ValidationException", "Missing 'GroupId' field");
1243        }
1244        let group_id = input.group_id;
1245        let user_rules: Vec<IpRule> = input
1246            .user_rules
1247            .into_iter()
1248            .filter_map(|item| {
1249                item.ip_rule.map(|ip_rule| IpRule {
1250                    ip_rule,
1251                    rule_desc: item.rule_desc,
1252                })
1253            })
1254            .collect();
1255
1256        let mut state = state.write().await;
1257        match state.authorize_ip_rules(&group_id, user_rules) {
1258            Ok(()) => {
1259                let result = model::AuthorizeIpRulesResult {};
1260                wire::serialize_authorize_ip_rules_response(&result)
1261            }
1262            Err(e) => workspaces_error_response(&e),
1263        }
1264    }
1265
1266    async fn handle_revoke_ip_rules(
1267        &self,
1268        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1269        body: &[u8],
1270    ) -> MockResponse {
1271        let input = match wire::deserialize_revoke_ip_rules_request(body) {
1272            Ok(v) => v,
1273            Err(e) => return json_error_response(400, "ValidationException", &e),
1274        };
1275        if input.group_id.is_empty() {
1276            return json_error_response(400, "ValidationException", "Missing 'GroupId' field");
1277        }
1278        let group_id = input.group_id;
1279        let user_rules = input.user_rules;
1280
1281        let mut state = state.write().await;
1282        match state.revoke_ip_rules(&group_id, &user_rules) {
1283            Ok(()) => {
1284                let result = model::RevokeIpRulesResult {};
1285                wire::serialize_revoke_ip_rules_response(&result)
1286            }
1287            Err(e) => workspaces_error_response(&e),
1288        }
1289    }
1290
1291    async fn handle_update_rules_of_ip_group(
1292        &self,
1293        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1294        body: &[u8],
1295    ) -> MockResponse {
1296        let input = match wire::deserialize_update_rules_of_ip_group_request(body) {
1297            Ok(v) => v,
1298            Err(e) => return json_error_response(400, "ValidationException", &e),
1299        };
1300        if input.group_id.is_empty() {
1301            return json_error_response(400, "ValidationException", "Missing 'GroupId' field");
1302        }
1303        let group_id = input.group_id;
1304        let user_rules: Vec<IpRule> = input
1305            .user_rules
1306            .into_iter()
1307            .filter_map(|item| {
1308                item.ip_rule.map(|ip_rule| IpRule {
1309                    ip_rule,
1310                    rule_desc: item.rule_desc,
1311                })
1312            })
1313            .collect();
1314
1315        let mut state = state.write().await;
1316        match state.update_rules_of_ip_group(&group_id, user_rules) {
1317            Ok(()) => {
1318                let result = model::UpdateRulesOfIpGroupResult {};
1319                wire::serialize_update_rules_of_ip_group_response(&result)
1320            }
1321            Err(e) => workspaces_error_response(&e),
1322        }
1323    }
1324
1325    async fn handle_associate_ip_groups(
1326        &self,
1327        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1328        body: &[u8],
1329    ) -> MockResponse {
1330        let input = match wire::deserialize_associate_ip_groups_request(body) {
1331            Ok(v) => v,
1332            Err(e) => return json_error_response(400, "ValidationException", &e),
1333        };
1334        if input.directory_id.is_empty() {
1335            return json_error_response(400, "ValidationException", "Missing 'DirectoryId' field");
1336        }
1337        let directory_id = input.directory_id;
1338        let group_ids = input.group_ids;
1339
1340        let mut state = state.write().await;
1341        match state.associate_ip_groups(&directory_id, &group_ids) {
1342            Ok(()) => {
1343                let result = model::AssociateIpGroupsResult {};
1344                wire::serialize_associate_ip_groups_response(&result)
1345            }
1346            Err(e) => workspaces_error_response(&e),
1347        }
1348    }
1349
1350    async fn handle_disassociate_ip_groups(
1351        &self,
1352        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1353        body: &[u8],
1354    ) -> MockResponse {
1355        let input = match wire::deserialize_disassociate_ip_groups_request(body) {
1356            Ok(v) => v,
1357            Err(e) => return json_error_response(400, "ValidationException", &e),
1358        };
1359        if input.directory_id.is_empty() {
1360            return json_error_response(400, "ValidationException", "Missing 'DirectoryId' field");
1361        }
1362        let directory_id = input.directory_id;
1363        let group_ids = input.group_ids;
1364
1365        let mut state = state.write().await;
1366        match state.disassociate_ip_groups(&directory_id, &group_ids) {
1367            Ok(()) => {
1368                let result = model::DisassociateIpGroupsResult {};
1369                wire::serialize_disassociate_ip_groups_response(&result)
1370            }
1371            Err(e) => workspaces_error_response(&e),
1372        }
1373    }
1374
1375    // --- Connection Alias handlers ---
1376
1377    async fn handle_create_connection_alias(
1378        &self,
1379        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1380        body: &[u8],
1381        account_id: &str,
1382    ) -> MockResponse {
1383        let input = match wire::deserialize_create_connection_alias_request(body) {
1384            Ok(v) => v,
1385            Err(e) => return json_error_response(400, "ValidationException", &e),
1386        };
1387        if input.connection_string.is_empty() {
1388            return json_error_response(
1389                400,
1390                "ValidationException",
1391                "Missing 'ConnectionString' field",
1392            );
1393        }
1394        let connection_string = input.connection_string;
1395
1396        let mut state = state.write().await;
1397        let alias = state.create_connection_alias(&connection_string, account_id);
1398
1399        let result = model::CreateConnectionAliasResult {
1400            alias_id: Some(alias.alias_id),
1401        };
1402        wire::serialize_create_connection_alias_response(&result)
1403    }
1404
1405    async fn handle_describe_connection_aliases(
1406        &self,
1407        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1408        body: &[u8],
1409    ) -> MockResponse {
1410        let input = match wire::deserialize_describe_connection_aliases_request(body) {
1411            Ok(v) => v,
1412            Err(e) => return json_error_response(400, "ValidationException", &e),
1413        };
1414        let alias_ids = input.alias_ids;
1415        let resource_id = input.resource_id.as_deref();
1416
1417        let state = state.read().await;
1418        let aliases = state.describe_connection_aliases(alias_ids.as_deref(), resource_id);
1419
1420        let entries: Vec<model::ConnectionAlias> = aliases
1421            .iter()
1422            .map(|a| model::ConnectionAlias {
1423                alias_id: Some(a.alias_id.clone()),
1424                connection_string: Some(a.connection_string.clone()),
1425                owner_account_id: Some(a.owner_account_id.clone()),
1426                state: Some(a.state.clone()),
1427                associations: if a.associations.is_empty() {
1428                    None
1429                } else {
1430                    Some(
1431                        a.associations
1432                            .iter()
1433                            .map(|assoc| model::ConnectionAliasAssociation {
1434                                connection_identifier: Some(assoc.connection_identifier.clone()),
1435                                resource_id: assoc.resource_id.clone(),
1436                                associated_account_id: assoc.associated_account_id.clone(),
1437                                ..Default::default()
1438                            })
1439                            .collect(),
1440                    )
1441                },
1442            })
1443            .collect();
1444
1445        let result = model::DescribeConnectionAliasesResult {
1446            connection_aliases: Some(entries),
1447            next_token: None,
1448        };
1449        wire::serialize_describe_connection_aliases_response(&result)
1450    }
1451
1452    async fn handle_delete_connection_alias(
1453        &self,
1454        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1455        body: &[u8],
1456    ) -> MockResponse {
1457        let input = match wire::deserialize_delete_connection_alias_request(body) {
1458            Ok(v) => v,
1459            Err(e) => return json_error_response(400, "ValidationException", &e),
1460        };
1461        if input.alias_id.is_empty() {
1462            return json_error_response(400, "ValidationException", "Missing 'AliasId' field");
1463        }
1464        let alias_id = input.alias_id;
1465
1466        let mut state = state.write().await;
1467        match state.delete_connection_alias(&alias_id) {
1468            Ok(()) => {
1469                let result = model::DeleteConnectionAliasResult {};
1470                wire::serialize_delete_connection_alias_response(&result)
1471            }
1472            Err(e) => workspaces_error_response(&e),
1473        }
1474    }
1475
1476    async fn handle_associate_connection_alias(
1477        &self,
1478        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1479        body: &[u8],
1480    ) -> MockResponse {
1481        let input = match wire::deserialize_associate_connection_alias_request(body) {
1482            Ok(v) => v,
1483            Err(e) => return json_error_response(400, "ValidationException", &e),
1484        };
1485        if input.alias_id.is_empty() {
1486            return json_error_response(400, "ValidationException", "Missing 'AliasId' field");
1487        }
1488        let alias_id = input.alias_id;
1489        if input.resource_id.is_empty() {
1490            return json_error_response(400, "ValidationException", "Missing 'ResourceId' field");
1491        }
1492        let resource_id = input.resource_id;
1493
1494        let mut state = state.write().await;
1495        match state.associate_connection_alias(&alias_id, &resource_id) {
1496            Ok(connection_identifier) => {
1497                let result = model::AssociateConnectionAliasResult {
1498                    connection_identifier: Some(connection_identifier),
1499                };
1500                wire::serialize_associate_connection_alias_response(&result)
1501            }
1502            Err(e) => workspaces_error_response(&e),
1503        }
1504    }
1505
1506    async fn handle_disassociate_connection_alias(
1507        &self,
1508        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1509        body: &[u8],
1510    ) -> MockResponse {
1511        let input = match wire::deserialize_disassociate_connection_alias_request(body) {
1512            Ok(v) => v,
1513            Err(e) => return json_error_response(400, "ValidationException", &e),
1514        };
1515        if input.alias_id.is_empty() {
1516            return json_error_response(400, "ValidationException", "Missing 'AliasId' field");
1517        }
1518        let alias_id = input.alias_id;
1519
1520        let mut state = state.write().await;
1521        match state.disassociate_connection_alias(&alias_id) {
1522            Ok(()) => {
1523                let result = model::DisassociateConnectionAliasResult {};
1524                wire::serialize_disassociate_connection_alias_response(&result)
1525            }
1526            Err(e) => workspaces_error_response(&e),
1527        }
1528    }
1529
1530    async fn handle_describe_connection_alias_permissions(
1531        &self,
1532        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1533        body: &[u8],
1534    ) -> MockResponse {
1535        let input = match wire::deserialize_describe_connection_alias_permissions_request(body) {
1536            Ok(v) => v,
1537            Err(e) => return json_error_response(400, "ValidationException", &e),
1538        };
1539        if input.alias_id.is_empty() {
1540            return json_error_response(400, "ValidationException", "Missing 'AliasId' field");
1541        }
1542        let alias_id = input.alias_id;
1543
1544        let state = state.read().await;
1545        match state.describe_connection_alias_permissions(&alias_id) {
1546            Ok(perms) => {
1547                let entries: Vec<model::ConnectionAliasPermission> = perms
1548                    .iter()
1549                    .map(|p| model::ConnectionAliasPermission {
1550                        shared_account_id: p.shared_account_id.clone(),
1551                        allow_association: p.allow_association,
1552                    })
1553                    .collect();
1554
1555                let result = model::DescribeConnectionAliasPermissionsResult {
1556                    alias_id: Some(alias_id),
1557                    connection_alias_permissions: Some(entries),
1558                    next_token: None,
1559                };
1560                wire::serialize_describe_connection_alias_permissions_response(&result)
1561            }
1562            Err(e) => workspaces_error_response(&e),
1563        }
1564    }
1565
1566    async fn handle_update_connection_alias_permission(
1567        &self,
1568        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1569        body: &[u8],
1570    ) -> MockResponse {
1571        let input = match wire::deserialize_update_connection_alias_permission_request(body) {
1572            Ok(v) => v,
1573            Err(e) => return json_error_response(400, "ValidationException", &e),
1574        };
1575        if input.alias_id.is_empty() {
1576            return json_error_response(400, "ValidationException", "Missing 'AliasId' field");
1577        }
1578        let alias_id = input.alias_id;
1579        let perm = input.connection_alias_permission;
1580        if perm.shared_account_id.is_empty() {
1581            return json_error_response(
1582                400,
1583                "ValidationException",
1584                "Missing 'SharedAccountId' in ConnectionAliasPermission",
1585            );
1586        }
1587        let shared_account_id = perm.shared_account_id;
1588        let allow_association = perm.allow_association;
1589
1590        let mut state = state.write().await;
1591        match state.update_connection_alias_permission(
1592            &alias_id,
1593            &shared_account_id,
1594            allow_association,
1595        ) {
1596            Ok(()) => {
1597                let result = model::UpdateConnectionAliasPermissionResult {};
1598                wire::serialize_update_connection_alias_permission_response(&result)
1599            }
1600            Err(e) => workspaces_error_response(&e),
1601        }
1602    }
1603
1604    // --- Bundle handlers ---
1605
1606    async fn handle_describe_workspace_bundles(
1607        &self,
1608        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1609        body: &[u8],
1610    ) -> MockResponse {
1611        let input = match wire::deserialize_describe_workspace_bundles_request(body) {
1612            Ok(v) => v,
1613            Err(e) => return json_error_response(400, "ValidationException", &e),
1614        };
1615        let bundle_ids = input.bundle_ids;
1616        let owner = input.owner.as_deref();
1617
1618        let state = state.read().await;
1619        let bundles = state.describe_workspace_bundles(bundle_ids.as_deref(), owner);
1620
1621        let entries: Vec<model::WorkspaceBundle> = bundles
1622            .iter()
1623            .map(|b| model::WorkspaceBundle {
1624                bundle_id: Some(b.bundle_id.clone()),
1625                name: Some(b.name.clone()),
1626                owner: b.owner.clone(),
1627                description: b.description.clone(),
1628                bundle_type: b.bundle_type.clone(),
1629                compute_type: b.compute_type_name.as_ref().map(|n| model::ComputeType {
1630                    name: Some(n.clone()),
1631                }),
1632                creation_time: rfc3339_to_epoch(&b.creation_time),
1633                root_storage: b.root_storage_capacity.map(|c| model::RootStorage {
1634                    capacity: c.to_string(),
1635                }),
1636                user_storage: b.user_storage_capacity.map(|c| model::UserStorage {
1637                    capacity: c.to_string(),
1638                }),
1639                ..Default::default()
1640            })
1641            .collect();
1642
1643        let result = model::DescribeWorkspaceBundlesResult {
1644            bundles: Some(entries),
1645            next_token: None,
1646        };
1647        wire::serialize_describe_workspace_bundles_response(&result)
1648    }
1649
1650    async fn handle_create_workspace_bundle(
1651        &self,
1652        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1653        body: &[u8],
1654        account_id: &str,
1655    ) -> MockResponse {
1656        let input = match wire::deserialize_create_workspace_bundle_request(body) {
1657            Ok(v) => v,
1658            Err(e) => return json_error_response(400, "ValidationException", &e),
1659        };
1660        if input.bundle_name.is_empty() {
1661            return json_error_response(400, "ValidationException", "Missing 'BundleName' field");
1662        }
1663        let bundle_name = input.bundle_name;
1664        let bundle_description = if input.bundle_description.is_empty() {
1665            None
1666        } else {
1667            Some(input.bundle_description)
1668        };
1669        if input.image_id.is_empty() {
1670            return json_error_response(400, "ValidationException", "Missing 'ImageId' field");
1671        }
1672        let image_id = input.image_id;
1673        let compute_type = input
1674            .compute_type
1675            .name
1676            .unwrap_or_else(|| "VALUE".to_string());
1677        let user_storage = input.user_storage.capacity.parse::<i32>().unwrap_or(50);
1678        let root_storage = input
1679            .root_storage
1680            .and_then(|rs| rs.capacity.parse::<i32>().ok());
1681
1682        let mut state = state.write().await;
1683        match state.create_workspace_bundle(
1684            &bundle_name,
1685            bundle_description.as_deref(),
1686            &image_id,
1687            &compute_type,
1688            user_storage,
1689            root_storage,
1690            account_id,
1691        ) {
1692            Ok(bundle) => {
1693                let wb = model::WorkspaceBundle {
1694                    bundle_id: Some(bundle.bundle_id.clone()),
1695                    name: Some(bundle.name.clone()),
1696                    owner: bundle.owner.clone(),
1697                    description: bundle.description.clone(),
1698                    compute_type: bundle
1699                        .compute_type_name
1700                        .as_ref()
1701                        .map(|n| model::ComputeType {
1702                            name: Some(n.clone()),
1703                        }),
1704                    creation_time: rfc3339_to_epoch(&bundle.creation_time),
1705                    root_storage: bundle.root_storage_capacity.map(|c| model::RootStorage {
1706                        capacity: c.to_string(),
1707                    }),
1708                    user_storage: bundle.user_storage_capacity.map(|c| model::UserStorage {
1709                        capacity: c.to_string(),
1710                    }),
1711                    ..Default::default()
1712                };
1713                let result = model::CreateWorkspaceBundleResult {
1714                    workspace_bundle: Some(wb),
1715                };
1716                wire::serialize_create_workspace_bundle_response(&result)
1717            }
1718            Err(e) => workspaces_error_response(&e),
1719        }
1720    }
1721
1722    async fn handle_delete_workspace_bundle(
1723        &self,
1724        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1725        body: &[u8],
1726    ) -> MockResponse {
1727        let input = match wire::deserialize_delete_workspace_bundle_request(body) {
1728            Ok(v) => v,
1729            Err(e) => return json_error_response(400, "ValidationException", &e),
1730        };
1731        let bundle_id = match input.bundle_id {
1732            Some(id) if !id.is_empty() => id,
1733            _ => {
1734                return json_error_response(400, "ValidationException", "Missing 'BundleId' field");
1735            }
1736        };
1737
1738        let mut state = state.write().await;
1739        match state.delete_workspace_bundle(&bundle_id) {
1740            Ok(()) => {
1741                let result = model::DeleteWorkspaceBundleResult {};
1742                wire::serialize_delete_workspace_bundle_response(&result)
1743            }
1744            Err(e) => workspaces_error_response(&e),
1745        }
1746    }
1747
1748    async fn handle_delete_workspace_image(
1749        &self,
1750        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1751        body: &[u8],
1752    ) -> MockResponse {
1753        let input = match wire::deserialize_delete_workspace_image_request(body) {
1754            Ok(v) => v,
1755            Err(e) => return json_error_response(400, "ValidationException", &e),
1756        };
1757        if input.image_id.is_empty() {
1758            return json_error_response(400, "ValidationException", "Missing 'ImageId' field");
1759        }
1760        let image_id = input.image_id;
1761
1762        let mut state = state.write().await;
1763        match state.delete_workspace_image(&image_id) {
1764            Ok(()) => {
1765                let result = model::DeleteWorkspaceImageResult {};
1766                wire::serialize_delete_workspace_image_response(&result)
1767            }
1768            Err(e) => workspaces_error_response(&e),
1769        }
1770    }
1771
1772    // --- Pool handlers ---
1773
1774    async fn handle_create_workspaces_pool(
1775        &self,
1776        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1777        body: &[u8],
1778        account_id: &str,
1779        region: &str,
1780    ) -> MockResponse {
1781        let input = match wire::deserialize_create_workspaces_pool_request(body) {
1782            Ok(v) => v,
1783            Err(e) => return json_error_response(400, "ValidationException", &e),
1784        };
1785        if input.pool_name.is_empty() {
1786            return json_error_response(400, "ValidationException", "Missing 'PoolName' field");
1787        }
1788        let pool_name = input.pool_name;
1789        if input.bundle_id.is_empty() {
1790            return json_error_response(400, "ValidationException", "Missing 'BundleId' field");
1791        }
1792        let bundle_id = input.bundle_id;
1793        if input.directory_id.is_empty() {
1794            return json_error_response(400, "ValidationException", "Missing 'DirectoryId' field");
1795        }
1796        let directory_id = input.directory_id;
1797        let description = if input.description.is_empty() {
1798            None
1799        } else {
1800            Some(input.description)
1801        };
1802
1803        let mut state = state.write().await;
1804        let pool = state.create_workspaces_pool(
1805            &pool_name,
1806            description.as_deref(),
1807            &bundle_id,
1808            &directory_id,
1809            account_id,
1810            region,
1811        );
1812
1813        let pool_model = pool_to_model(&pool);
1814        let result = model::CreateWorkspacesPoolResult {
1815            workspaces_pool: Some(pool_model),
1816        };
1817        wire::serialize_create_workspaces_pool_response(&result)
1818    }
1819
1820    async fn handle_describe_workspaces_pools(
1821        &self,
1822        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1823        body: &[u8],
1824    ) -> MockResponse {
1825        let input = match wire::deserialize_describe_workspaces_pools_request(body) {
1826            Ok(v) => v,
1827            Err(e) => return json_error_response(400, "ValidationException", &e),
1828        };
1829        let pool_ids = input.pool_ids;
1830
1831        let state = state.read().await;
1832        let pools = state.describe_workspaces_pools(pool_ids.as_deref());
1833
1834        let entries: Vec<model::WorkspacesPool> = pools.iter().map(|p| pool_to_model(p)).collect();
1835
1836        let result = model::DescribeWorkspacesPoolsResult {
1837            workspaces_pools: Some(entries),
1838            next_token: None,
1839        };
1840        wire::serialize_describe_workspaces_pools_response(&result)
1841    }
1842
1843    async fn handle_terminate_workspaces_pool(
1844        &self,
1845        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1846        body: &[u8],
1847    ) -> MockResponse {
1848        let input = match wire::deserialize_terminate_workspaces_pool_request(body) {
1849            Ok(v) => v,
1850            Err(e) => return json_error_response(400, "ValidationException", &e),
1851        };
1852        if input.pool_id.is_empty() {
1853            return json_error_response(400, "ValidationException", "Missing 'PoolId' field");
1854        }
1855        let pool_id = input.pool_id;
1856
1857        let mut state = state.write().await;
1858        match state.terminate_workspaces_pool(&pool_id) {
1859            Ok(pool) => {
1860                let pool_model = pool_to_model(&pool);
1861                let result = model::TerminateWorkspacesPoolResult {};
1862                let _ = pool_model;
1863                wire::serialize_terminate_workspaces_pool_response(&result)
1864            }
1865            Err(e) => workspaces_error_response(&e),
1866        }
1867    }
1868
1869    async fn handle_update_workspaces_pool(
1870        &self,
1871        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1872        body: &[u8],
1873    ) -> MockResponse {
1874        let input = match wire::deserialize_update_workspaces_pool_request(body) {
1875            Ok(v) => v,
1876            Err(e) => return json_error_response(400, "ValidationException", &e),
1877        };
1878        if input.pool_id.is_empty() {
1879            return json_error_response(400, "ValidationException", "Missing 'PoolId' field");
1880        }
1881        let pool_id = input.pool_id;
1882        let description = input.description.as_deref();
1883        let bundle_id = input.bundle_id.as_deref();
1884        let directory_id = input.directory_id.as_deref();
1885
1886        let mut state = state.write().await;
1887        match state.update_workspaces_pool(&pool_id, description, bundle_id, directory_id) {
1888            Ok(pool) => {
1889                let pool_model = pool_to_model(&pool);
1890                let result = model::UpdateWorkspacesPoolResult {
1891                    workspaces_pool: Some(pool_model),
1892                };
1893                wire::serialize_update_workspaces_pool_response(&result)
1894            }
1895            Err(e) => workspaces_error_response(&e),
1896        }
1897    }
1898
1899    async fn handle_start_workspaces_pool(
1900        &self,
1901        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1902        body: &[u8],
1903    ) -> MockResponse {
1904        let input = match wire::deserialize_start_workspaces_pool_request(body) {
1905            Ok(v) => v,
1906            Err(e) => return json_error_response(400, "ValidationException", &e),
1907        };
1908        if input.pool_id.is_empty() {
1909            return json_error_response(400, "ValidationException", "Missing 'PoolId' field");
1910        }
1911        let pool_id = input.pool_id;
1912
1913        let mut state = state.write().await;
1914        match state.start_workspaces_pool(&pool_id) {
1915            Ok(()) => {
1916                let result = model::StartWorkspacesPoolResult {};
1917                wire::serialize_start_workspaces_pool_response(&result)
1918            }
1919            Err(e) => workspaces_error_response(&e),
1920        }
1921    }
1922
1923    async fn handle_stop_workspaces_pool(
1924        &self,
1925        state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1926        body: &[u8],
1927    ) -> MockResponse {
1928        let input = match wire::deserialize_stop_workspaces_pool_request(body) {
1929            Ok(v) => v,
1930            Err(e) => return json_error_response(400, "ValidationException", &e),
1931        };
1932        if input.pool_id.is_empty() {
1933            return json_error_response(400, "ValidationException", "Missing 'PoolId' field");
1934        }
1935        let pool_id = input.pool_id;
1936
1937        let mut state = state.write().await;
1938        match state.stop_workspaces_pool(&pool_id) {
1939            Ok(()) => {
1940                let result = model::StopWorkspacesPoolResult {};
1941                wire::serialize_stop_workspaces_pool_response(&result)
1942            }
1943            Err(e) => workspaces_error_response(&e),
1944        }
1945    }
1946}
1947
1948// --- Utility functions ---
1949
1950/// Convert a state Workspace to the wire model Workspace.
1951fn workspace_to_model(ws: &Workspace) -> model::Workspace {
1952    model::Workspace {
1953        workspace_id: Some(ws.workspace_id.clone()),
1954        directory_id: Some(ws.directory_id.clone()),
1955        user_name: Some(ws.user_name.clone()),
1956        bundle_id: Some(ws.bundle_id.clone()),
1957        state: Some(ws.state.clone()),
1958        ip_address: Some(ws.ip_address.clone()),
1959        computer_name: Some(ws.computer_name.clone()),
1960        subnet_id: Some(ws.subnet_id.clone()),
1961        root_volume_encryption_enabled: Some(ws.root_volume_encryption_enabled),
1962        user_volume_encryption_enabled: Some(ws.user_volume_encryption_enabled),
1963        volume_encryption_key: ws.volume_encryption_key.clone(),
1964        workspace_properties: Some(model::WorkspaceProperties {
1965            running_mode: Some(ws.running_mode.clone()),
1966            running_mode_auto_stop_timeout_in_minutes: Some(
1967                ws.running_mode_auto_stop_timeout_in_minutes,
1968            ),
1969            root_volume_size_gib: Some(ws.root_volume_size_gib),
1970            user_volume_size_gib: Some(ws.user_volume_size_gib),
1971            ..Default::default()
1972        }),
1973        ..Default::default()
1974    }
1975}
1976
1977/// Convert a state WorkspaceDirectory to the wire model WorkspaceDirectory.
1978fn directory_to_model(dir: &WorkspaceDirectory) -> model::WorkspaceDirectory {
1979    model::WorkspaceDirectory {
1980        directory_id: Some(dir.directory_id.clone()),
1981        directory_name: Some(dir.directory_name.clone()),
1982        directory_type: Some(dir.directory_type.clone()),
1983        alias: Some(dir.alias.clone()),
1984        state: Some(dir.state.clone()),
1985        registration_code: Some(dir.registration_code.clone()),
1986        workspace_security_group_id: Some(dir.workspace_security_group_id.clone()),
1987        iam_role_id: Some(dir.iam_role_id.clone()),
1988        ..Default::default()
1989    }
1990}
1991
1992/// Convert a state WorkspacesPool to the wire model WorkspacesPool.
1993fn pool_to_model(pool: &WorkspacesPool) -> model::WorkspacesPool {
1994    model::WorkspacesPool {
1995        pool_id: Some(pool.pool_id.clone()),
1996        pool_arn: Some(pool.pool_arn.clone()),
1997        pool_name: Some(pool.pool_name.clone()),
1998        description: pool.description.clone(),
1999        state: Some(pool.state.clone()),
2000        bundle_id: Some(pool.bundle_id.clone()),
2001        directory_id: Some(pool.directory_id.clone()),
2002        created_at: rfc3339_to_epoch(&pool.created_at),
2003        ..Default::default()
2004    }
2005}
2006
2007fn json_error_response(status: u16, code: &str, message: &str) -> MockResponse {
2008    let body = json!({
2009        "__type": code,
2010        "message": message,
2011    });
2012    MockResponse::json(status, body.to_string())
2013}
2014
2015fn workspaces_error_response(e: &crate::state::WorkSpacesError) -> MockResponse {
2016    use crate::state::WorkSpacesError;
2017    let (status, error_type) = match e {
2018        WorkSpacesError::WorkspaceNotFound(_) => (400u16, "ResourceNotFoundException"),
2019        WorkSpacesError::ImageNotFound(_) => (400u16, "ResourceNotFoundException"),
2020        WorkSpacesError::DirectoryNotFound(_) => (400u16, "ResourceNotFoundException"),
2021        WorkSpacesError::DirectoryAlreadyRegistered(_) => {
2022            (400u16, "ResourceAlreadyExistsException")
2023        }
2024        WorkSpacesError::IpGroupNotFound(_) => (400u16, "ResourceNotFoundException"),
2025        WorkSpacesError::ConnectionAliasNotFound(_) => (400u16, "ResourceNotFoundException"),
2026        WorkSpacesError::BundleNotFound(_) => (400u16, "ResourceNotFoundException"),
2027        WorkSpacesError::PoolNotFound(_) => (400u16, "ResourceNotFoundException"),
2028    };
2029    json_error_response(status, error_type, &e.to_string())
2030}