Skip to main content

winterbaume_workspacesweb/
handlers.rs

1use std::collections::HashMap;
2use std::future::Future;
3use std::pin::Pin;
4use std::sync::Arc;
5
6use http::header::HeaderName;
7use serde_json::json;
8use winterbaume_core::{
9    BackendState, MockRequest, MockResponse, MockService, StateChangeNotifier, StatefulService,
10    default_account_id,
11};
12
13use crate::state::{WorkspacesWebError, WorkspacesWebState};
14use crate::types;
15use crate::views::WorkspacesWebStateView;
16use crate::wire;
17
18const X_AMZN_ERRORTYPE: HeaderName = HeaderName::from_static("x-amzn-errortype");
19
20pub struct WorkspacesWebService {
21    pub(crate) state: Arc<BackendState<WorkspacesWebState>>,
22    pub(crate) notifier: StateChangeNotifier<WorkspacesWebStateView>,
23}
24
25impl WorkspacesWebService {
26    pub fn new() -> Self {
27        Self {
28            state: Arc::new(BackendState::new()),
29            notifier: StateChangeNotifier::new(),
30        }
31    }
32}
33
34impl Default for WorkspacesWebService {
35    fn default() -> Self {
36        Self::new()
37    }
38}
39
40impl MockService for WorkspacesWebService {
41    fn service_name(&self) -> &str {
42        "workspaces-web"
43    }
44
45    fn url_patterns(&self) -> Vec<&str> {
46        vec![r"https?://workspaces-web\.(.+)\.amazonaws\.com"]
47    }
48
49    fn handle(
50        &self,
51        request: MockRequest,
52    ) -> Pin<Box<dyn Future<Output = MockResponse> + Send + '_>> {
53        Box::pin(async move { self.dispatch(request).await })
54    }
55}
56
57fn tags_from_input(tags: &[wire::Tag]) -> HashMap<String, String> {
58    tags.iter()
59        .map(|t| (t.key.clone(), t.value.clone()))
60        .collect()
61}
62
63impl WorkspacesWebService {
64    async fn dispatch(&self, request: MockRequest) -> MockResponse {
65        let region = winterbaume_core::auth::extract_region_from_uri(&request.uri);
66        let account_id = default_account_id();
67        let state = self.state.get(account_id, &region);
68
69        let (path, query_string) = extract_path_and_query(&request.uri);
70        let method = request.method.as_str();
71
72        let segments: Vec<&str> = path.trim_start_matches('/').split('/').collect();
73
74        if segments.is_empty() || segments[0].is_empty() {
75            return rest_json_error(404, "UnknownOperationException", "Not found");
76        }
77
78        let query_map = parse_query_string(&query_string);
79
80        let response = match segments[0] {
81            "portals" => {
82                self.dispatch_portals(
83                    method, &segments, &request, &state, account_id, &region, &query_map,
84                )
85                .await
86            }
87            "portalIdp" => {
88                self.dispatch_portal_idp(method, &segments, &request, &state, &query_map)
89                    .await
90            }
91            "browserSettings" => {
92                self.dispatch_browser_settings(
93                    method, &segments, &request, &state, account_id, &region, &query_map,
94                )
95                .await
96            }
97            "networkSettings" => {
98                self.dispatch_network_settings(
99                    method, &segments, &request, &state, account_id, &region, &query_map,
100                )
101                .await
102            }
103            "userAccessLoggingSettings" => {
104                self.dispatch_user_access_logging_settings(
105                    method, &segments, &request, &state, account_id, &region, &query_map,
106                )
107                .await
108            }
109            "userSettings" => {
110                self.dispatch_user_settings(
111                    method, &segments, &request, &state, account_id, &region, &query_map,
112                )
113                .await
114            }
115            "identityProviders" => {
116                self.dispatch_identity_providers(
117                    method, &segments, &request, &state, account_id, &region, &query_map,
118                )
119                .await
120            }
121            "ipAccessSettings" => {
122                self.dispatch_ip_access_settings(
123                    method, &segments, &request, &state, account_id, &region, &query_map,
124                )
125                .await
126            }
127            "trustStores" => {
128                self.dispatch_trust_stores(
129                    method, &segments, &request, &state, account_id, &region, &query_map,
130                )
131                .await
132            }
133            "dataProtectionSettings" => {
134                self.dispatch_data_protection_settings(
135                    method, &segments, &request, &state, account_id, &region, &query_map,
136                )
137                .await
138            }
139            "sessionLoggers" => {
140                self.dispatch_session_loggers(
141                    method, &segments, &request, &state, account_id, &region, &query_map,
142                )
143                .await
144            }
145            "tags" => {
146                self.dispatch_tags(method, &segments, &request, &state, &query_map)
147                    .await
148            }
149            _ => rest_json_error(404, "UnknownOperationException", "Not found"),
150        };
151        if response.status / 100 == 2 {
152            self.notify_state_changed(account_id, &region).await;
153        }
154        response
155    }
156
157    // ── Portal routes ───────────────────────────────────────────────
158
159    #[allow(clippy::too_many_arguments)]
160    async fn dispatch_portals(
161        &self,
162        method: &str,
163        segments: &[&str],
164        request: &MockRequest,
165        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
166        account_id: &str,
167        region: &str,
168        query: &HashMap<String, String>,
169    ) -> MockResponse {
170        if segments.len() == 1 {
171            match method {
172                "POST" => {
173                    self.handle_create_portal(state, request, &[], query, account_id, region)
174                        .await
175                }
176                "GET" => self.handle_list_portals(state, request, &[], query).await,
177                _ => rest_json_error(404, "UnknownOperationException", "Not found"),
178            }
179        } else {
180            // Check if the last segment is an association sub-resource
181            let last = *segments.last().unwrap();
182            // Handle /portals/{portalId}/sessions[/{sessionId}]
183            if let Some(sessions_idx) = segments.iter().position(|&s| s == "sessions") {
184                let portal_id = decode_arn_from_segments(&segments[1..sessions_idx]);
185                if sessions_idx + 1 >= segments.len() {
186                    if method == "GET" {
187                        let labels: &[(&str, &str)] = &[("portalId", portal_id.as_str())];
188                        return self
189                            .handle_list_sessions(state, request, labels, query)
190                            .await;
191                    }
192                } else {
193                    let session_id = decode_arn_from_segments(&segments[sessions_idx + 1..]);
194                    let labels: &[(&str, &str)] = &[
195                        ("portalId", portal_id.as_str()),
196                        ("sessionId", session_id.as_str()),
197                    ];
198                    match method {
199                        "GET" => {
200                            return self.handle_get_session(state, request, labels, query).await;
201                        }
202                        "DELETE" => {
203                            return self
204                                .handle_expire_session(state, request, labels, query)
205                                .await;
206                        }
207                        _ => {}
208                    }
209                }
210            }
211            // Handle /portals/{portalArn+}/identityProviders
212            if last == "identityProviders" && method == "GET" {
213                let portal_arn = decode_arn_from_segments(&segments[1..segments.len() - 1]);
214                let labels: &[(&str, &str)] = &[("portalArn", portal_arn.as_str())];
215                return self
216                    .handle_list_identity_providers(state, request, labels, query)
217                    .await;
218            }
219            match last {
220                "browserSettings" if method == "PUT" => {
221                    let portal_arn = decode_arn_from_segments(&segments[1..segments.len() - 1]);
222                    let labels: &[(&str, &str)] = &[("portalArn", portal_arn.as_str())];
223                    self.handle_associate_browser_settings(state, request, labels, query)
224                        .await
225                }
226                "browserSettings" if method == "DELETE" => {
227                    let portal_arn = decode_arn_from_segments(&segments[1..segments.len() - 1]);
228                    let labels: &[(&str, &str)] = &[("portalArn", portal_arn.as_str())];
229                    self.handle_disassociate_browser_settings(state, request, labels, query)
230                        .await
231                }
232                "networkSettings" if method == "PUT" => {
233                    let portal_arn = decode_arn_from_segments(&segments[1..segments.len() - 1]);
234                    let labels: &[(&str, &str)] = &[("portalArn", portal_arn.as_str())];
235                    self.handle_associate_network_settings(state, request, labels, query)
236                        .await
237                }
238                "networkSettings" if method == "DELETE" => {
239                    let portal_arn = decode_arn_from_segments(&segments[1..segments.len() - 1]);
240                    let labels: &[(&str, &str)] = &[("portalArn", portal_arn.as_str())];
241                    self.handle_disassociate_network_settings(state, request, labels, query)
242                        .await
243                }
244                "userAccessLoggingSettings" if method == "PUT" => {
245                    let portal_arn = decode_arn_from_segments(&segments[1..segments.len() - 1]);
246                    let labels: &[(&str, &str)] = &[("portalArn", portal_arn.as_str())];
247                    self.handle_associate_user_access_logging_settings(
248                        state, request, labels, query,
249                    )
250                    .await
251                }
252                "userAccessLoggingSettings" if method == "DELETE" => {
253                    let portal_arn = decode_arn_from_segments(&segments[1..segments.len() - 1]);
254                    let labels: &[(&str, &str)] = &[("portalArn", portal_arn.as_str())];
255                    self.handle_disassociate_user_access_logging_settings(
256                        state, request, labels, query,
257                    )
258                    .await
259                }
260                "userSettings" if method == "PUT" => {
261                    let portal_arn = decode_arn_from_segments(&segments[1..segments.len() - 1]);
262                    let labels: &[(&str, &str)] = &[("portalArn", portal_arn.as_str())];
263                    self.handle_associate_user_settings(state, request, labels, query)
264                        .await
265                }
266                "userSettings" if method == "DELETE" => {
267                    let portal_arn = decode_arn_from_segments(&segments[1..segments.len() - 1]);
268                    let labels: &[(&str, &str)] = &[("portalArn", portal_arn.as_str())];
269                    self.handle_disassociate_user_settings(state, request, labels, query)
270                        .await
271                }
272                "ipAccessSettings" if method == "PUT" => {
273                    let portal_arn = decode_arn_from_segments(&segments[1..segments.len() - 1]);
274                    let labels: &[(&str, &str)] = &[("portalArn", portal_arn.as_str())];
275                    self.handle_associate_ip_access_settings(state, request, labels, query)
276                        .await
277                }
278                "ipAccessSettings" if method == "DELETE" => {
279                    let portal_arn = decode_arn_from_segments(&segments[1..segments.len() - 1]);
280                    let labels: &[(&str, &str)] = &[("portalArn", portal_arn.as_str())];
281                    self.handle_disassociate_ip_access_settings(state, request, labels, query)
282                        .await
283                }
284                "trustStores" if method == "PUT" => {
285                    let portal_arn = decode_arn_from_segments(&segments[1..segments.len() - 1]);
286                    let labels: &[(&str, &str)] = &[("portalArn", portal_arn.as_str())];
287                    self.handle_associate_trust_store(state, request, labels, query)
288                        .await
289                }
290                "trustStores" if method == "DELETE" => {
291                    let portal_arn = decode_arn_from_segments(&segments[1..segments.len() - 1]);
292                    let labels: &[(&str, &str)] = &[("portalArn", portal_arn.as_str())];
293                    self.handle_disassociate_trust_store(state, request, labels, query)
294                        .await
295                }
296                "dataProtectionSettings" if method == "PUT" => {
297                    let portal_arn = decode_arn_from_segments(&segments[1..segments.len() - 1]);
298                    let labels: &[(&str, &str)] = &[("portalArn", portal_arn.as_str())];
299                    self.handle_associate_data_protection_settings(state, request, labels, query)
300                        .await
301                }
302                "dataProtectionSettings" if method == "DELETE" => {
303                    let portal_arn = decode_arn_from_segments(&segments[1..segments.len() - 1]);
304                    let labels: &[(&str, &str)] = &[("portalArn", portal_arn.as_str())];
305                    self.handle_disassociate_data_protection_settings(state, request, labels, query)
306                        .await
307                }
308                "sessionLogger" if method == "PUT" => {
309                    let portal_arn = decode_arn_from_segments(&segments[1..segments.len() - 1]);
310                    let labels: &[(&str, &str)] = &[("portalArn", portal_arn.as_str())];
311                    self.handle_associate_session_logger(state, request, labels, query)
312                        .await
313                }
314                "sessionLogger" if method == "DELETE" => {
315                    let portal_arn = decode_arn_from_segments(&segments[1..segments.len() - 1]);
316                    let labels: &[(&str, &str)] = &[("portalArn", portal_arn.as_str())];
317                    self.handle_disassociate_session_logger(state, request, labels, query)
318                        .await
319                }
320                _ => {
321                    let portal_arn = decode_arn_from_segments(&segments[1..]);
322                    let labels: &[(&str, &str)] = &[("portalArn", portal_arn.as_str())];
323                    match method {
324                        "GET" => self.handle_get_portal(state, request, labels, query).await,
325                        "DELETE" => {
326                            self.handle_delete_portal(state, request, labels, query)
327                                .await
328                        }
329                        "PUT" => {
330                            self.handle_update_portal(state, request, labels, query)
331                                .await
332                        }
333                        _ => rest_json_error(404, "UnknownOperationException", "Not found"),
334                    }
335                }
336            }
337        }
338    }
339
340    async fn dispatch_portal_idp(
341        &self,
342        method: &str,
343        segments: &[&str],
344        request: &MockRequest,
345        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
346        query: &HashMap<String, String>,
347    ) -> MockResponse {
348        if segments.len() > 1 && method == "GET" {
349            let portal_arn = decode_arn_from_segments(&segments[1..]);
350            let labels: &[(&str, &str)] = &[("portalArn", portal_arn.as_str())];
351            self.handle_get_portal_service_provider_metadata(state, request, labels, query)
352                .await
353        } else {
354            rest_json_error(404, "UnknownOperationException", "Not found")
355        }
356    }
357
358    // ── BrowserSettings routes ──────────────────────────────────────
359
360    #[allow(clippy::too_many_arguments)]
361    async fn dispatch_browser_settings(
362        &self,
363        method: &str,
364        segments: &[&str],
365        request: &MockRequest,
366        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
367        account_id: &str,
368        region: &str,
369        query: &HashMap<String, String>,
370    ) -> MockResponse {
371        if segments.len() == 1 {
372            match method {
373                "POST" => {
374                    self.handle_create_browser_settings(
375                        state,
376                        request,
377                        &[],
378                        query,
379                        account_id,
380                        region,
381                    )
382                    .await
383                }
384                "GET" => {
385                    self.handle_list_browser_settings(state, request, &[], query)
386                        .await
387                }
388                _ => rest_json_error(404, "UnknownOperationException", "Not found"),
389            }
390        } else {
391            let arn = decode_arn_from_segments(&segments[1..]);
392            let labels: &[(&str, &str)] = &[("browserSettingsArn", arn.as_str())];
393            match method {
394                "GET" => {
395                    self.handle_get_browser_settings(state, request, labels, query)
396                        .await
397                }
398                "DELETE" => {
399                    self.handle_delete_browser_settings(state, request, labels, query)
400                        .await
401                }
402                "PATCH" => {
403                    self.handle_update_browser_settings(state, request, labels, query)
404                        .await
405                }
406                _ => rest_json_error(404, "UnknownOperationException", "Not found"),
407            }
408        }
409    }
410
411    // ── NetworkSettings routes ──────────────────────────────────────
412
413    #[allow(clippy::too_many_arguments)]
414    async fn dispatch_network_settings(
415        &self,
416        method: &str,
417        segments: &[&str],
418        request: &MockRequest,
419        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
420        account_id: &str,
421        region: &str,
422        query: &HashMap<String, String>,
423    ) -> MockResponse {
424        if segments.len() == 1 {
425            match method {
426                "POST" => {
427                    self.handle_create_network_settings(
428                        state,
429                        request,
430                        &[],
431                        query,
432                        account_id,
433                        region,
434                    )
435                    .await
436                }
437                "GET" => {
438                    self.handle_list_network_settings(state, request, &[], query)
439                        .await
440                }
441                _ => rest_json_error(404, "UnknownOperationException", "Not found"),
442            }
443        } else {
444            let arn = decode_arn_from_segments(&segments[1..]);
445            let labels: &[(&str, &str)] = &[("networkSettingsArn", arn.as_str())];
446            match method {
447                "GET" => {
448                    self.handle_get_network_settings(state, request, labels, query)
449                        .await
450                }
451                "DELETE" => {
452                    self.handle_delete_network_settings(state, request, labels, query)
453                        .await
454                }
455                "PATCH" => {
456                    self.handle_update_network_settings(state, request, labels, query)
457                        .await
458                }
459                _ => rest_json_error(404, "UnknownOperationException", "Not found"),
460            }
461        }
462    }
463
464    // ── UserAccessLoggingSettings routes ────────────────────────────
465
466    #[allow(clippy::too_many_arguments)]
467    async fn dispatch_user_access_logging_settings(
468        &self,
469        method: &str,
470        segments: &[&str],
471        request: &MockRequest,
472        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
473        account_id: &str,
474        region: &str,
475        query: &HashMap<String, String>,
476    ) -> MockResponse {
477        if segments.len() == 1 {
478            match method {
479                "POST" => {
480                    self.handle_create_user_access_logging_settings(
481                        state,
482                        request,
483                        &[],
484                        query,
485                        account_id,
486                        region,
487                    )
488                    .await
489                }
490                "GET" => {
491                    self.handle_list_user_access_logging_settings(state, request, &[], query)
492                        .await
493                }
494                _ => rest_json_error(404, "UnknownOperationException", "Not found"),
495            }
496        } else {
497            let arn = decode_arn_from_segments(&segments[1..]);
498            let labels: &[(&str, &str)] = &[("userAccessLoggingSettingsArn", arn.as_str())];
499            match method {
500                "GET" => {
501                    self.handle_get_user_access_logging_settings(state, request, labels, query)
502                        .await
503                }
504                "DELETE" => {
505                    self.handle_delete_user_access_logging_settings(state, request, labels, query)
506                        .await
507                }
508                "PATCH" => {
509                    self.handle_update_user_access_logging_settings(state, request, labels, query)
510                        .await
511                }
512                _ => rest_json_error(404, "UnknownOperationException", "Not found"),
513            }
514        }
515    }
516
517    // ── UserSettings routes ─────────────────────────────────────────
518
519    #[allow(clippy::too_many_arguments)]
520    async fn dispatch_user_settings(
521        &self,
522        method: &str,
523        segments: &[&str],
524        request: &MockRequest,
525        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
526        account_id: &str,
527        region: &str,
528        query: &HashMap<String, String>,
529    ) -> MockResponse {
530        if segments.len() == 1 {
531            match method {
532                "POST" => {
533                    self.handle_create_user_settings(state, request, &[], query, account_id, region)
534                        .await
535                }
536                "GET" => {
537                    self.handle_list_user_settings(state, request, &[], query)
538                        .await
539                }
540                _ => rest_json_error(404, "UnknownOperationException", "Not found"),
541            }
542        } else {
543            let arn = decode_arn_from_segments(&segments[1..]);
544            let labels: &[(&str, &str)] = &[("userSettingsArn", arn.as_str())];
545            match method {
546                "GET" => {
547                    self.handle_get_user_settings(state, request, labels, query)
548                        .await
549                }
550                "DELETE" => {
551                    self.handle_delete_user_settings(state, request, labels, query)
552                        .await
553                }
554                "PATCH" => {
555                    self.handle_update_user_settings(state, request, labels, query)
556                        .await
557                }
558                _ => rest_json_error(404, "UnknownOperationException", "Not found"),
559            }
560        }
561    }
562
563    // ── IdentityProvider routes ─────────────────────────────────────
564
565    #[allow(clippy::too_many_arguments)]
566    async fn dispatch_identity_providers(
567        &self,
568        method: &str,
569        segments: &[&str],
570        request: &MockRequest,
571        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
572        account_id: &str,
573        region: &str,
574        query: &HashMap<String, String>,
575    ) -> MockResponse {
576        if segments.len() == 1 {
577            if method == "POST" {
578                self.handle_create_identity_provider(state, request, &[], query, account_id, region)
579                    .await
580            } else {
581                rest_json_error(404, "UnknownOperationException", "Not found")
582            }
583        } else {
584            let arn = decode_arn_from_segments(&segments[1..]);
585            let labels: &[(&str, &str)] = &[("identityProviderArn", arn.as_str())];
586            match method {
587                "GET" => {
588                    self.handle_get_identity_provider(state, request, labels, query)
589                        .await
590                }
591                "DELETE" => {
592                    self.handle_delete_identity_provider(state, request, labels, query)
593                        .await
594                }
595                "PATCH" => {
596                    self.handle_update_identity_provider(state, request, labels, query)
597                        .await
598                }
599                _ => rest_json_error(404, "UnknownOperationException", "Not found"),
600            }
601        }
602    }
603
604    // ── IpAccessSettings routes ─────────────────────────────────────
605
606    #[allow(clippy::too_many_arguments)]
607    async fn dispatch_ip_access_settings(
608        &self,
609        method: &str,
610        segments: &[&str],
611        request: &MockRequest,
612        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
613        account_id: &str,
614        region: &str,
615        query: &HashMap<String, String>,
616    ) -> MockResponse {
617        if segments.len() == 1 {
618            match method {
619                "POST" => {
620                    self.handle_create_ip_access_settings(
621                        state,
622                        request,
623                        &[],
624                        query,
625                        account_id,
626                        region,
627                    )
628                    .await
629                }
630                "GET" => {
631                    self.handle_list_ip_access_settings(state, request, &[], query)
632                        .await
633                }
634                _ => rest_json_error(404, "UnknownOperationException", "Not found"),
635            }
636        } else {
637            let arn = decode_arn_from_segments(&segments[1..]);
638            let labels: &[(&str, &str)] = &[("ipAccessSettingsArn", arn.as_str())];
639            match method {
640                "GET" => {
641                    self.handle_get_ip_access_settings(state, request, labels, query)
642                        .await
643                }
644                "DELETE" => {
645                    self.handle_delete_ip_access_settings(state, request, labels, query)
646                        .await
647                }
648                "PATCH" => {
649                    self.handle_update_ip_access_settings(state, request, labels, query)
650                        .await
651                }
652                _ => rest_json_error(404, "UnknownOperationException", "Not found"),
653            }
654        }
655    }
656
657    // ── TrustStore routes ───────────────────────────────────────────
658
659    #[allow(clippy::too_many_arguments)]
660    async fn dispatch_trust_stores(
661        &self,
662        method: &str,
663        segments: &[&str],
664        request: &MockRequest,
665        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
666        account_id: &str,
667        region: &str,
668        query: &HashMap<String, String>,
669    ) -> MockResponse {
670        if segments.len() == 1 {
671            match method {
672                "POST" => {
673                    self.handle_create_trust_store(state, request, &[], query, account_id, region)
674                        .await
675                }
676                "GET" => {
677                    self.handle_list_trust_stores(state, request, &[], query)
678                        .await
679                }
680                _ => rest_json_error(404, "UnknownOperationException", "Not found"),
681            }
682        } else {
683            let last = *segments.last().unwrap();
684            if last == "certificate" {
685                let arn = decode_arn_from_segments(&segments[1..segments.len() - 1]);
686                let labels: &[(&str, &str)] = &[("trustStoreArn", arn.as_str())];
687                return self
688                    .handle_get_trust_store_certificate(state, request, labels, query)
689                    .await;
690            }
691            if last == "certificates" {
692                let arn = decode_arn_from_segments(&segments[1..segments.len() - 1]);
693                let labels: &[(&str, &str)] = &[("trustStoreArn", arn.as_str())];
694                return self
695                    .handle_list_trust_store_certificates(state, request, labels, query)
696                    .await;
697            }
698            let arn = decode_arn_from_segments(&segments[1..]);
699            let labels: &[(&str, &str)] = &[("trustStoreArn", arn.as_str())];
700            match method {
701                "GET" => {
702                    self.handle_get_trust_store(state, request, labels, query)
703                        .await
704                }
705                "DELETE" => {
706                    self.handle_delete_trust_store(state, request, labels, query)
707                        .await
708                }
709                "PATCH" => {
710                    self.handle_update_trust_store(state, request, labels, query)
711                        .await
712                }
713                _ => rest_json_error(404, "UnknownOperationException", "Not found"),
714            }
715        }
716    }
717
718    // ── DataProtectionSettings routes ───────────────────────────────
719
720    #[allow(clippy::too_many_arguments)]
721    async fn dispatch_data_protection_settings(
722        &self,
723        method: &str,
724        segments: &[&str],
725        request: &MockRequest,
726        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
727        account_id: &str,
728        region: &str,
729        query: &HashMap<String, String>,
730    ) -> MockResponse {
731        if segments.len() == 1 {
732            match method {
733                "POST" => {
734                    self.handle_create_data_protection_settings(
735                        state,
736                        request,
737                        &[],
738                        query,
739                        account_id,
740                        region,
741                    )
742                    .await
743                }
744                "GET" => {
745                    self.handle_list_data_protection_settings(state, request, &[], query)
746                        .await
747                }
748                _ => rest_json_error(404, "UnknownOperationException", "Not found"),
749            }
750        } else {
751            let arn = decode_arn_from_segments(&segments[1..]);
752            let labels: &[(&str, &str)] = &[("dataProtectionSettingsArn", arn.as_str())];
753            match method {
754                "GET" => {
755                    self.handle_get_data_protection_settings(state, request, labels, query)
756                        .await
757                }
758                "DELETE" => {
759                    self.handle_delete_data_protection_settings(state, request, labels, query)
760                        .await
761                }
762                "PATCH" => {
763                    self.handle_update_data_protection_settings(state, request, labels, query)
764                        .await
765                }
766                _ => rest_json_error(404, "UnknownOperationException", "Not found"),
767            }
768        }
769    }
770
771    // ── SessionLogger routes ─────────────────────────────────────────
772
773    #[allow(clippy::too_many_arguments)]
774    async fn dispatch_session_loggers(
775        &self,
776        method: &str,
777        segments: &[&str],
778        request: &MockRequest,
779        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
780        account_id: &str,
781        region: &str,
782        query: &HashMap<String, String>,
783    ) -> MockResponse {
784        if segments.len() == 1 {
785            match method {
786                "POST" => {
787                    self.handle_create_session_logger(
788                        state,
789                        request,
790                        &[],
791                        query,
792                        account_id,
793                        region,
794                    )
795                    .await
796                }
797                "GET" => {
798                    self.handle_list_session_loggers(state, request, &[], query)
799                        .await
800                }
801                _ => rest_json_error(404, "UnknownOperationException", "Not found"),
802            }
803        } else {
804            let arn = decode_arn_from_segments(&segments[1..]);
805            let labels: &[(&str, &str)] = &[("sessionLoggerArn", arn.as_str())];
806            match method {
807                "GET" => {
808                    self.handle_get_session_logger(state, request, labels, query)
809                        .await
810                }
811                "DELETE" => {
812                    self.handle_delete_session_logger(state, request, labels, query)
813                        .await
814                }
815                "POST" => {
816                    self.handle_update_session_logger(state, request, labels, query)
817                        .await
818                }
819                _ => rest_json_error(404, "UnknownOperationException", "Not found"),
820            }
821        }
822    }
823
824    async fn dispatch_tags(
825        &self,
826        method: &str,
827        segments: &[&str],
828        request: &MockRequest,
829        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
830        query: &HashMap<String, String>,
831    ) -> MockResponse {
832        if segments.len() < 2 {
833            return rest_json_error(400, "ValidationException", "Missing resource ARN");
834        }
835        let resource_arn = decode_arn_from_segments(&segments[1..]);
836        let labels: &[(&str, &str)] = &[("resourceArn", resource_arn.as_str())];
837        match method {
838            "GET" => {
839                self.handle_list_tags_for_resource(state, request, labels, query)
840                    .await
841            }
842            "POST" => {
843                self.handle_tag_resource(state, request, labels, query)
844                    .await
845            }
846            "DELETE" => {
847                self.handle_untag_resource(state, request, labels, query)
848                    .await
849            }
850            _ => rest_json_error(404, "UnknownOperationException", "Not found"),
851        }
852    }
853
854    // ── Portal handlers ─────────────────────────────────────────────
855
856    async fn handle_create_portal(
857        &self,
858        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
859        request: &MockRequest,
860        labels: &[(&str, &str)],
861        query: &HashMap<String, String>,
862        account_id: &str,
863        region: &str,
864    ) -> MockResponse {
865        let input = match wire::deserialize_create_portal_request(request, labels, query) {
866            Ok(v) => v,
867            Err(e) => return rest_json_error(400, "ValidationException", &e),
868        };
869        let display_name = input.display_name.as_deref();
870        let tags = tags_from_input(input.tags.as_deref().unwrap_or(&[]));
871        let mut state = state.write().await;
872        match state.create_portal(display_name, account_id, region, tags) {
873            Ok(portal) => {
874                let resp = wire::CreatePortalResponse {
875                    portal_arn: Some(portal.portal_arn.clone()),
876                    portal_endpoint: Some(portal.portal_endpoint.clone()),
877                };
878                wire::serialize_create_portal_response(&resp)
879            }
880            Err(e) => workspaces_web_error_response(&e),
881        }
882    }
883
884    async fn handle_get_portal(
885        &self,
886        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
887        request: &MockRequest,
888        labels: &[(&str, &str)],
889        query: &HashMap<String, String>,
890    ) -> MockResponse {
891        let input = match wire::deserialize_get_portal_request(request, labels, query) {
892            Ok(v) => v,
893            Err(e) => return rest_json_error(400, "ValidationException", &e),
894        };
895        let state = state.read().await;
896        match state.get_portal(&input.portal_arn) {
897            Ok(portal) => {
898                let resp = wire::GetPortalResponse {
899                    portal: Some(portal_to_wire(portal)),
900                };
901                wire::serialize_get_portal_response(&resp)
902            }
903            Err(e) => workspaces_web_error_response(&e),
904        }
905    }
906
907    async fn handle_delete_portal(
908        &self,
909        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
910        request: &MockRequest,
911        labels: &[(&str, &str)],
912        query: &HashMap<String, String>,
913    ) -> MockResponse {
914        let input = match wire::deserialize_delete_portal_request(request, labels, query) {
915            Ok(v) => v,
916            Err(e) => return rest_json_error(400, "ValidationException", &e),
917        };
918        let mut state = state.write().await;
919        match state.delete_portal(&input.portal_arn) {
920            Ok(()) => wire::serialize_delete_portal_response(&wire::DeletePortalResponse {}),
921            Err(e) => workspaces_web_error_response(&e),
922        }
923    }
924
925    async fn handle_list_portals(
926        &self,
927        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
928        request: &MockRequest,
929        labels: &[(&str, &str)],
930        query: &HashMap<String, String>,
931    ) -> MockResponse {
932        if let Err(e) = wire::deserialize_list_portals_request(request, labels, query) {
933            return rest_json_error(400, "ValidationException", &e);
934        }
935        let state = state.read().await;
936        let portals = state.list_portals();
937        let entries: Vec<wire::PortalSummary> =
938            portals.iter().map(|p| portal_to_summary(p)).collect();
939        let resp = wire::ListPortalsResponse {
940            next_token: None,
941            portals: Some(entries),
942        };
943        wire::serialize_list_portals_response(&resp)
944    }
945
946    async fn handle_update_portal(
947        &self,
948        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
949        request: &MockRequest,
950        labels: &[(&str, &str)],
951        query: &HashMap<String, String>,
952    ) -> MockResponse {
953        let input = match wire::deserialize_update_portal_request(request, labels, query) {
954            Ok(v) => v,
955            Err(e) => return rest_json_error(400, "ValidationException", &e),
956        };
957        let mut st = state.write().await;
958        match st.update_portal(&input.portal_arn, input.display_name.as_deref()) {
959            Ok(portal) => {
960                let resp = wire::UpdatePortalResponse {
961                    portal: Some(portal_to_wire(portal)),
962                };
963                wire::serialize_update_portal_response(&resp)
964            }
965            Err(e) => workspaces_web_error_response(&e),
966        }
967    }
968
969    async fn handle_get_portal_service_provider_metadata(
970        &self,
971        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
972        request: &MockRequest,
973        labels: &[(&str, &str)],
974        query: &HashMap<String, String>,
975    ) -> MockResponse {
976        let input = match wire::deserialize_get_portal_service_provider_metadata_request(
977            request, labels, query,
978        ) {
979            Ok(v) => v,
980            Err(e) => return rest_json_error(400, "ValidationException", &e),
981        };
982        let st = state.read().await;
983        match st.get_portal(&input.portal_arn) {
984            Ok(_) => {
985                let saml_metadata = format!(
986                    r#"<?xml version="1.0" encoding="UTF-8"?><EntityDescriptor entityID="{0}"/>"#,
987                    input.portal_arn
988                );
989                let resp = wire::GetPortalServiceProviderMetadataResponse {
990                    portal_arn: Some(input.portal_arn.clone()),
991                    service_provider_saml_metadata: Some(saml_metadata),
992                };
993                wire::serialize_get_portal_service_provider_metadata_response(&resp)
994            }
995            Err(e) => workspaces_web_error_response(&e),
996        }
997    }
998
999    // ── Disassociate handlers ───────────────────────────────────────
1000
1001    async fn handle_disassociate_browser_settings(
1002        &self,
1003        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1004        request: &MockRequest,
1005        labels: &[(&str, &str)],
1006        query: &HashMap<String, String>,
1007    ) -> MockResponse {
1008        let input =
1009            match wire::deserialize_disassociate_browser_settings_request(request, labels, query) {
1010                Ok(v) => v,
1011                Err(e) => return rest_json_error(400, "ValidationException", &e),
1012            };
1013        let mut st = state.write().await;
1014        match st.disassociate_browser_settings(&input.portal_arn) {
1015            Ok(()) => wire::serialize_disassociate_browser_settings_response(
1016                &wire::DisassociateBrowserSettingsResponse {},
1017            ),
1018            Err(e) => workspaces_web_error_response(&e),
1019        }
1020    }
1021
1022    async fn handle_disassociate_network_settings(
1023        &self,
1024        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1025        request: &MockRequest,
1026        labels: &[(&str, &str)],
1027        query: &HashMap<String, String>,
1028    ) -> MockResponse {
1029        let input =
1030            match wire::deserialize_disassociate_network_settings_request(request, labels, query) {
1031                Ok(v) => v,
1032                Err(e) => return rest_json_error(400, "ValidationException", &e),
1033            };
1034        let mut st = state.write().await;
1035        match st.disassociate_network_settings(&input.portal_arn) {
1036            Ok(()) => wire::serialize_disassociate_network_settings_response(
1037                &wire::DisassociateNetworkSettingsResponse {},
1038            ),
1039            Err(e) => workspaces_web_error_response(&e),
1040        }
1041    }
1042
1043    async fn handle_disassociate_user_access_logging_settings(
1044        &self,
1045        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1046        request: &MockRequest,
1047        labels: &[(&str, &str)],
1048        query: &HashMap<String, String>,
1049    ) -> MockResponse {
1050        let input = match wire::deserialize_disassociate_user_access_logging_settings_request(
1051            request, labels, query,
1052        ) {
1053            Ok(v) => v,
1054            Err(e) => return rest_json_error(400, "ValidationException", &e),
1055        };
1056        let mut st = state.write().await;
1057        match st.disassociate_user_access_logging_settings(&input.portal_arn) {
1058            Ok(()) => wire::serialize_disassociate_user_access_logging_settings_response(
1059                &wire::DisassociateUserAccessLoggingSettingsResponse {},
1060            ),
1061            Err(e) => workspaces_web_error_response(&e),
1062        }
1063    }
1064
1065    async fn handle_disassociate_user_settings(
1066        &self,
1067        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1068        request: &MockRequest,
1069        labels: &[(&str, &str)],
1070        query: &HashMap<String, String>,
1071    ) -> MockResponse {
1072        let input =
1073            match wire::deserialize_disassociate_user_settings_request(request, labels, query) {
1074                Ok(v) => v,
1075                Err(e) => return rest_json_error(400, "ValidationException", &e),
1076            };
1077        let mut st = state.write().await;
1078        match st.disassociate_user_settings(&input.portal_arn) {
1079            Ok(()) => wire::serialize_disassociate_user_settings_response(
1080                &wire::DisassociateUserSettingsResponse {},
1081            ),
1082            Err(e) => workspaces_web_error_response(&e),
1083        }
1084    }
1085
1086    // ── BrowserSettings handlers ────────────────────────────────────
1087
1088    async fn handle_create_browser_settings(
1089        &self,
1090        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1091        request: &MockRequest,
1092        labels: &[(&str, &str)],
1093        query: &HashMap<String, String>,
1094        account_id: &str,
1095        region: &str,
1096    ) -> MockResponse {
1097        let input = match wire::deserialize_create_browser_settings_request(request, labels, query)
1098        {
1099            Ok(v) => v,
1100            Err(e) => return rest_json_error(400, "ValidationException", &e),
1101        };
1102        let browser_policy = input.browser_policy.as_deref();
1103        let tags = tags_from_input(input.tags.as_deref().unwrap_or(&[]));
1104
1105        let mut state = state.write().await;
1106        match state.create_browser_settings(browser_policy, account_id, region, tags) {
1107            Ok(bs) => {
1108                let resp = wire::CreateBrowserSettingsResponse {
1109                    browser_settings_arn: Some(bs.browser_settings_arn.clone()),
1110                };
1111                wire::serialize_create_browser_settings_response(&resp)
1112            }
1113            Err(e) => workspaces_web_error_response(&e),
1114        }
1115    }
1116
1117    async fn handle_get_browser_settings(
1118        &self,
1119        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1120        request: &MockRequest,
1121        labels: &[(&str, &str)],
1122        query: &HashMap<String, String>,
1123    ) -> MockResponse {
1124        let input = match wire::deserialize_get_browser_settings_request(request, labels, query) {
1125            Ok(v) => v,
1126            Err(e) => return rest_json_error(400, "ValidationException", &e),
1127        };
1128        let state = state.read().await;
1129        match state.get_browser_settings(&input.browser_settings_arn) {
1130            Ok(bs) => {
1131                let resp = wire::GetBrowserSettingsResponse {
1132                    browser_settings: Some(wire::BrowserSettings {
1133                        browser_settings_arn: Some(bs.browser_settings_arn.clone()),
1134                        browser_policy: bs.browser_policy.clone(),
1135                        associated_portal_arns: Some(bs.associated_portal_arns.clone()),
1136                        ..Default::default()
1137                    }),
1138                };
1139                wire::serialize_get_browser_settings_response(&resp)
1140            }
1141            Err(e) => workspaces_web_error_response(&e),
1142        }
1143    }
1144
1145    async fn handle_delete_browser_settings(
1146        &self,
1147        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1148        request: &MockRequest,
1149        labels: &[(&str, &str)],
1150        query: &HashMap<String, String>,
1151    ) -> MockResponse {
1152        let input = match wire::deserialize_delete_browser_settings_request(request, labels, query)
1153        {
1154            Ok(v) => v,
1155            Err(e) => return rest_json_error(400, "ValidationException", &e),
1156        };
1157        let mut state = state.write().await;
1158        match state.delete_browser_settings(&input.browser_settings_arn) {
1159            Ok(()) => wire::serialize_delete_browser_settings_response(
1160                &wire::DeleteBrowserSettingsResponse {},
1161            ),
1162            Err(e) => workspaces_web_error_response(&e),
1163        }
1164    }
1165
1166    async fn handle_list_browser_settings(
1167        &self,
1168        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1169        request: &MockRequest,
1170        labels: &[(&str, &str)],
1171        query: &HashMap<String, String>,
1172    ) -> MockResponse {
1173        if let Err(e) = wire::deserialize_list_browser_settings_request(request, labels, query) {
1174            return rest_json_error(400, "ValidationException", &e);
1175        }
1176        let state = state.read().await;
1177        let items = state.list_browser_settings();
1178        let entries: Vec<wire::BrowserSettingsSummary> = items
1179            .iter()
1180            .map(|bs| wire::BrowserSettingsSummary {
1181                browser_settings_arn: Some(bs.browser_settings_arn.clone()),
1182            })
1183            .collect();
1184        let resp = wire::ListBrowserSettingsResponse {
1185            browser_settings: Some(entries),
1186            next_token: None,
1187        };
1188        wire::serialize_list_browser_settings_response(&resp)
1189    }
1190
1191    async fn handle_update_browser_settings(
1192        &self,
1193        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1194        request: &MockRequest,
1195        labels: &[(&str, &str)],
1196        query: &HashMap<String, String>,
1197    ) -> MockResponse {
1198        let input = match wire::deserialize_update_browser_settings_request(request, labels, query)
1199        {
1200            Ok(v) => v,
1201            Err(e) => return rest_json_error(400, "ValidationException", &e),
1202        };
1203        let mut st = state.write().await;
1204        match st.update_browser_settings(&input.browser_settings_arn, input.browser_policy) {
1205            Ok(bs) => {
1206                let resp = wire::UpdateBrowserSettingsResponse {
1207                    browser_settings: Some(wire::BrowserSettings {
1208                        browser_settings_arn: Some(bs.browser_settings_arn.clone()),
1209                        browser_policy: bs.browser_policy.clone(),
1210                        associated_portal_arns: Some(bs.associated_portal_arns.clone()),
1211                        ..Default::default()
1212                    }),
1213                };
1214                wire::serialize_update_browser_settings_response(&resp)
1215            }
1216            Err(e) => workspaces_web_error_response(&e),
1217        }
1218    }
1219
1220    // ── NetworkSettings handlers ────────────────────────────────────
1221
1222    async fn handle_create_network_settings(
1223        &self,
1224        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1225        request: &MockRequest,
1226        labels: &[(&str, &str)],
1227        query: &HashMap<String, String>,
1228        account_id: &str,
1229        region: &str,
1230    ) -> MockResponse {
1231        let input = match wire::deserialize_create_network_settings_request(request, labels, query)
1232        {
1233            Ok(v) => v,
1234            Err(e) => return rest_json_error(400, "ValidationException", &e),
1235        };
1236        if input.vpc_id.is_empty() {
1237            return rest_json_error(400, "ValidationException", "Missing 'vpcId'");
1238        }
1239        if input.subnet_ids.is_empty() {
1240            return rest_json_error(400, "ValidationException", "Missing 'subnetIds'");
1241        }
1242        if input.security_group_ids.is_empty() {
1243            return rest_json_error(400, "ValidationException", "Missing 'securityGroupIds'");
1244        }
1245        let tags = tags_from_input(input.tags.as_deref().unwrap_or(&[]));
1246
1247        let mut state = state.write().await;
1248        match state.create_network_settings(
1249            &input.vpc_id,
1250            input.subnet_ids,
1251            input.security_group_ids,
1252            account_id,
1253            region,
1254            tags,
1255        ) {
1256            Ok(ns) => {
1257                let resp = wire::CreateNetworkSettingsResponse {
1258                    network_settings_arn: Some(ns.network_settings_arn.clone()),
1259                };
1260                wire::serialize_create_network_settings_response(&resp)
1261            }
1262            Err(e) => workspaces_web_error_response(&e),
1263        }
1264    }
1265
1266    async fn handle_get_network_settings(
1267        &self,
1268        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1269        request: &MockRequest,
1270        labels: &[(&str, &str)],
1271        query: &HashMap<String, String>,
1272    ) -> MockResponse {
1273        let input = match wire::deserialize_get_network_settings_request(request, labels, query) {
1274            Ok(v) => v,
1275            Err(e) => return rest_json_error(400, "ValidationException", &e),
1276        };
1277        let state = state.read().await;
1278        match state.get_network_settings(&input.network_settings_arn) {
1279            Ok(ns) => {
1280                let resp = wire::GetNetworkSettingsResponse {
1281                    network_settings: Some(wire::NetworkSettings {
1282                        network_settings_arn: Some(ns.network_settings_arn.clone()),
1283                        vpc_id: Some(ns.vpc_id.clone()),
1284                        subnet_ids: Some(ns.subnet_ids.clone()),
1285                        security_group_ids: Some(ns.security_group_ids.clone()),
1286                        associated_portal_arns: Some(ns.associated_portal_arns.clone()),
1287                    }),
1288                };
1289                wire::serialize_get_network_settings_response(&resp)
1290            }
1291            Err(e) => workspaces_web_error_response(&e),
1292        }
1293    }
1294
1295    async fn handle_delete_network_settings(
1296        &self,
1297        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1298        request: &MockRequest,
1299        labels: &[(&str, &str)],
1300        query: &HashMap<String, String>,
1301    ) -> MockResponse {
1302        let input = match wire::deserialize_delete_network_settings_request(request, labels, query)
1303        {
1304            Ok(v) => v,
1305            Err(e) => return rest_json_error(400, "ValidationException", &e),
1306        };
1307        let mut state = state.write().await;
1308        match state.delete_network_settings(&input.network_settings_arn) {
1309            Ok(()) => wire::serialize_delete_network_settings_response(
1310                &wire::DeleteNetworkSettingsResponse {},
1311            ),
1312            Err(e) => workspaces_web_error_response(&e),
1313        }
1314    }
1315
1316    async fn handle_list_network_settings(
1317        &self,
1318        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1319        request: &MockRequest,
1320        labels: &[(&str, &str)],
1321        query: &HashMap<String, String>,
1322    ) -> MockResponse {
1323        if let Err(e) = wire::deserialize_list_network_settings_request(request, labels, query) {
1324            return rest_json_error(400, "ValidationException", &e);
1325        }
1326        let state = state.read().await;
1327        let items = state.list_network_settings();
1328        let entries: Vec<wire::NetworkSettingsSummary> = items
1329            .iter()
1330            .map(|ns| wire::NetworkSettingsSummary {
1331                network_settings_arn: Some(ns.network_settings_arn.clone()),
1332                vpc_id: Some(ns.vpc_id.clone()),
1333            })
1334            .collect();
1335        let resp = wire::ListNetworkSettingsResponse {
1336            network_settings: Some(entries),
1337            next_token: None,
1338        };
1339        wire::serialize_list_network_settings_response(&resp)
1340    }
1341
1342    async fn handle_update_network_settings(
1343        &self,
1344        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1345        request: &MockRequest,
1346        labels: &[(&str, &str)],
1347        query: &HashMap<String, String>,
1348    ) -> MockResponse {
1349        let input = match wire::deserialize_update_network_settings_request(request, labels, query)
1350        {
1351            Ok(v) => v,
1352            Err(e) => return rest_json_error(400, "ValidationException", &e),
1353        };
1354        let mut st = state.write().await;
1355        match st.update_network_settings(
1356            &input.network_settings_arn,
1357            input.vpc_id,
1358            input.subnet_ids,
1359            input.security_group_ids,
1360        ) {
1361            Ok(ns) => {
1362                let resp = wire::UpdateNetworkSettingsResponse {
1363                    network_settings: Some(wire::NetworkSettings {
1364                        network_settings_arn: Some(ns.network_settings_arn.clone()),
1365                        vpc_id: Some(ns.vpc_id.clone()),
1366                        subnet_ids: Some(ns.subnet_ids.clone()),
1367                        security_group_ids: Some(ns.security_group_ids.clone()),
1368                        associated_portal_arns: Some(ns.associated_portal_arns.clone()),
1369                    }),
1370                };
1371                wire::serialize_update_network_settings_response(&resp)
1372            }
1373            Err(e) => workspaces_web_error_response(&e),
1374        }
1375    }
1376
1377    // ── UserAccessLoggingSettings handlers ──────────────────────────
1378
1379    async fn handle_create_user_access_logging_settings(
1380        &self,
1381        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1382        request: &MockRequest,
1383        labels: &[(&str, &str)],
1384        query: &HashMap<String, String>,
1385        account_id: &str,
1386        region: &str,
1387    ) -> MockResponse {
1388        let input = match wire::deserialize_create_user_access_logging_settings_request(
1389            request, labels, query,
1390        ) {
1391            Ok(v) => v,
1392            Err(e) => return rest_json_error(400, "ValidationException", &e),
1393        };
1394        if input.kinesis_stream_arn.is_empty() {
1395            return rest_json_error(400, "ValidationException", "Missing 'kinesisStreamArn'");
1396        }
1397        let tags = tags_from_input(input.tags.as_deref().unwrap_or(&[]));
1398
1399        let mut state = state.write().await;
1400        match state.create_user_access_logging_settings(
1401            &input.kinesis_stream_arn,
1402            account_id,
1403            region,
1404            tags,
1405        ) {
1406            Ok(ual) => {
1407                let resp = wire::CreateUserAccessLoggingSettingsResponse {
1408                    user_access_logging_settings_arn: Some(
1409                        ual.user_access_logging_settings_arn.clone(),
1410                    ),
1411                };
1412                wire::serialize_create_user_access_logging_settings_response(&resp)
1413            }
1414            Err(e) => workspaces_web_error_response(&e),
1415        }
1416    }
1417
1418    async fn handle_get_user_access_logging_settings(
1419        &self,
1420        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1421        request: &MockRequest,
1422        labels: &[(&str, &str)],
1423        query: &HashMap<String, String>,
1424    ) -> MockResponse {
1425        let input = match wire::deserialize_get_user_access_logging_settings_request(
1426            request, labels, query,
1427        ) {
1428            Ok(v) => v,
1429            Err(e) => return rest_json_error(400, "ValidationException", &e),
1430        };
1431        let state = state.read().await;
1432        match state.get_user_access_logging_settings(&input.user_access_logging_settings_arn) {
1433            Ok(ual) => {
1434                let resp = wire::GetUserAccessLoggingSettingsResponse {
1435                    user_access_logging_settings: Some(wire::UserAccessLoggingSettings {
1436                        user_access_logging_settings_arn: Some(
1437                            ual.user_access_logging_settings_arn.clone(),
1438                        ),
1439                        kinesis_stream_arn: Some(ual.kinesis_stream_arn.clone()),
1440                        associated_portal_arns: Some(ual.associated_portal_arns.clone()),
1441                    }),
1442                };
1443                wire::serialize_get_user_access_logging_settings_response(&resp)
1444            }
1445            Err(e) => workspaces_web_error_response(&e),
1446        }
1447    }
1448
1449    async fn handle_delete_user_access_logging_settings(
1450        &self,
1451        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1452        request: &MockRequest,
1453        labels: &[(&str, &str)],
1454        query: &HashMap<String, String>,
1455    ) -> MockResponse {
1456        let input = match wire::deserialize_delete_user_access_logging_settings_request(
1457            request, labels, query,
1458        ) {
1459            Ok(v) => v,
1460            Err(e) => return rest_json_error(400, "ValidationException", &e),
1461        };
1462        let mut state = state.write().await;
1463        match state.delete_user_access_logging_settings(&input.user_access_logging_settings_arn) {
1464            Ok(()) => wire::serialize_delete_user_access_logging_settings_response(
1465                &wire::DeleteUserAccessLoggingSettingsResponse {},
1466            ),
1467            Err(e) => workspaces_web_error_response(&e),
1468        }
1469    }
1470
1471    async fn handle_list_user_access_logging_settings(
1472        &self,
1473        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1474        request: &MockRequest,
1475        labels: &[(&str, &str)],
1476        query: &HashMap<String, String>,
1477    ) -> MockResponse {
1478        if let Err(e) =
1479            wire::deserialize_list_user_access_logging_settings_request(request, labels, query)
1480        {
1481            return rest_json_error(400, "ValidationException", &e);
1482        }
1483        let state = state.read().await;
1484        let items = state.list_user_access_logging_settings();
1485        let entries: Vec<wire::UserAccessLoggingSettingsSummary> = items
1486            .iter()
1487            .map(|ual| wire::UserAccessLoggingSettingsSummary {
1488                user_access_logging_settings_arn: Some(
1489                    ual.user_access_logging_settings_arn.clone(),
1490                ),
1491                kinesis_stream_arn: Some(ual.kinesis_stream_arn.clone()),
1492            })
1493            .collect();
1494        let resp = wire::ListUserAccessLoggingSettingsResponse {
1495            user_access_logging_settings: Some(entries),
1496            next_token: None,
1497        };
1498        wire::serialize_list_user_access_logging_settings_response(&resp)
1499    }
1500
1501    async fn handle_update_user_access_logging_settings(
1502        &self,
1503        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1504        request: &MockRequest,
1505        labels: &[(&str, &str)],
1506        query: &HashMap<String, String>,
1507    ) -> MockResponse {
1508        let input = match wire::deserialize_update_user_access_logging_settings_request(
1509            request, labels, query,
1510        ) {
1511            Ok(v) => v,
1512            Err(e) => return rest_json_error(400, "ValidationException", &e),
1513        };
1514        let mut st = state.write().await;
1515        match st.update_user_access_logging_settings(
1516            &input.user_access_logging_settings_arn,
1517            input.kinesis_stream_arn,
1518        ) {
1519            Ok(ual) => {
1520                let resp = wire::UpdateUserAccessLoggingSettingsResponse {
1521                    user_access_logging_settings: Some(wire::UserAccessLoggingSettings {
1522                        user_access_logging_settings_arn: Some(
1523                            ual.user_access_logging_settings_arn.clone(),
1524                        ),
1525                        kinesis_stream_arn: Some(ual.kinesis_stream_arn.clone()),
1526                        associated_portal_arns: Some(ual.associated_portal_arns.clone()),
1527                    }),
1528                };
1529                wire::serialize_update_user_access_logging_settings_response(&resp)
1530            }
1531            Err(e) => workspaces_web_error_response(&e),
1532        }
1533    }
1534
1535    // ── UserSettings handlers ───────────────────────────────────────
1536
1537    async fn handle_create_user_settings(
1538        &self,
1539        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1540        request: &MockRequest,
1541        labels: &[(&str, &str)],
1542        query: &HashMap<String, String>,
1543        account_id: &str,
1544        region: &str,
1545    ) -> MockResponse {
1546        let input = match wire::deserialize_create_user_settings_request(request, labels, query) {
1547            Ok(v) => v,
1548            Err(e) => return rest_json_error(400, "ValidationException", &e),
1549        };
1550        if input.copy_allowed.is_empty() {
1551            return rest_json_error(400, "ValidationException", "Missing 'copyAllowed'");
1552        }
1553        if input.paste_allowed.is_empty() {
1554            return rest_json_error(400, "ValidationException", "Missing 'pasteAllowed'");
1555        }
1556        if input.download_allowed.is_empty() {
1557            return rest_json_error(400, "ValidationException", "Missing 'downloadAllowed'");
1558        }
1559        if input.upload_allowed.is_empty() {
1560            return rest_json_error(400, "ValidationException", "Missing 'uploadAllowed'");
1561        }
1562        if input.print_allowed.is_empty() {
1563            return rest_json_error(400, "ValidationException", "Missing 'printAllowed'");
1564        }
1565        let tags = tags_from_input(input.tags.as_deref().unwrap_or(&[]));
1566
1567        let mut state = state.write().await;
1568        match state.create_user_settings(
1569            &input.copy_allowed,
1570            &input.paste_allowed,
1571            &input.download_allowed,
1572            &input.upload_allowed,
1573            &input.print_allowed,
1574            input.disconnect_timeout_in_minutes,
1575            input.idle_disconnect_timeout_in_minutes,
1576            account_id,
1577            region,
1578            tags,
1579        ) {
1580            Ok(us) => {
1581                let resp = wire::CreateUserSettingsResponse {
1582                    user_settings_arn: Some(us.user_settings_arn.clone()),
1583                };
1584                wire::serialize_create_user_settings_response(&resp)
1585            }
1586            Err(e) => workspaces_web_error_response(&e),
1587        }
1588    }
1589
1590    async fn handle_get_user_settings(
1591        &self,
1592        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1593        request: &MockRequest,
1594        labels: &[(&str, &str)],
1595        query: &HashMap<String, String>,
1596    ) -> MockResponse {
1597        let input = match wire::deserialize_get_user_settings_request(request, labels, query) {
1598            Ok(v) => v,
1599            Err(e) => return rest_json_error(400, "ValidationException", &e),
1600        };
1601        let state = state.read().await;
1602        match state.get_user_settings(&input.user_settings_arn) {
1603            Ok(us) => {
1604                let resp = wire::GetUserSettingsResponse {
1605                    user_settings: Some(wire::UserSettings {
1606                        user_settings_arn: Some(us.user_settings_arn.clone()),
1607                        copy_allowed: Some(us.copy_allowed.clone()),
1608                        paste_allowed: Some(us.paste_allowed.clone()),
1609                        download_allowed: Some(us.download_allowed.clone()),
1610                        upload_allowed: Some(us.upload_allowed.clone()),
1611                        print_allowed: Some(us.print_allowed.clone()),
1612                        disconnect_timeout_in_minutes: us.disconnect_timeout_in_minutes,
1613                        idle_disconnect_timeout_in_minutes: us.idle_disconnect_timeout_in_minutes,
1614                        associated_portal_arns: Some(us.associated_portal_arns.clone()),
1615                        ..Default::default()
1616                    }),
1617                };
1618                wire::serialize_get_user_settings_response(&resp)
1619            }
1620            Err(e) => workspaces_web_error_response(&e),
1621        }
1622    }
1623
1624    async fn handle_delete_user_settings(
1625        &self,
1626        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1627        request: &MockRequest,
1628        labels: &[(&str, &str)],
1629        query: &HashMap<String, String>,
1630    ) -> MockResponse {
1631        let input = match wire::deserialize_delete_user_settings_request(request, labels, query) {
1632            Ok(v) => v,
1633            Err(e) => return rest_json_error(400, "ValidationException", &e),
1634        };
1635        let mut state = state.write().await;
1636        match state.delete_user_settings(&input.user_settings_arn) {
1637            Ok(()) => {
1638                wire::serialize_delete_user_settings_response(&wire::DeleteUserSettingsResponse {})
1639            }
1640            Err(e) => workspaces_web_error_response(&e),
1641        }
1642    }
1643
1644    async fn handle_list_user_settings(
1645        &self,
1646        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1647        request: &MockRequest,
1648        labels: &[(&str, &str)],
1649        query: &HashMap<String, String>,
1650    ) -> MockResponse {
1651        if let Err(e) = wire::deserialize_list_user_settings_request(request, labels, query) {
1652            return rest_json_error(400, "ValidationException", &e);
1653        }
1654        let state = state.read().await;
1655        let items = state.list_user_settings();
1656        let entries: Vec<wire::UserSettingsSummary> = items
1657            .iter()
1658            .map(|us| wire::UserSettingsSummary {
1659                user_settings_arn: Some(us.user_settings_arn.clone()),
1660                copy_allowed: Some(us.copy_allowed.clone()),
1661                paste_allowed: Some(us.paste_allowed.clone()),
1662                download_allowed: Some(us.download_allowed.clone()),
1663                upload_allowed: Some(us.upload_allowed.clone()),
1664                print_allowed: Some(us.print_allowed.clone()),
1665                disconnect_timeout_in_minutes: us.disconnect_timeout_in_minutes,
1666                idle_disconnect_timeout_in_minutes: us.idle_disconnect_timeout_in_minutes,
1667                ..Default::default()
1668            })
1669            .collect();
1670        let resp = wire::ListUserSettingsResponse {
1671            user_settings: Some(entries),
1672            next_token: None,
1673        };
1674        wire::serialize_list_user_settings_response(&resp)
1675    }
1676
1677    async fn handle_update_user_settings(
1678        &self,
1679        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1680        request: &MockRequest,
1681        labels: &[(&str, &str)],
1682        query: &HashMap<String, String>,
1683    ) -> MockResponse {
1684        let input = match wire::deserialize_update_user_settings_request(request, labels, query) {
1685            Ok(v) => v,
1686            Err(e) => return rest_json_error(400, "ValidationException", &e),
1687        };
1688        let mut st = state.write().await;
1689        match st.update_user_settings(
1690            &input.user_settings_arn,
1691            input.copy_allowed,
1692            input.paste_allowed,
1693            input.download_allowed,
1694            input.upload_allowed,
1695            input.print_allowed,
1696            input.disconnect_timeout_in_minutes,
1697            input.idle_disconnect_timeout_in_minutes,
1698        ) {
1699            Ok(us) => {
1700                let resp = wire::UpdateUserSettingsResponse {
1701                    user_settings: Some(wire::UserSettings {
1702                        user_settings_arn: Some(us.user_settings_arn.clone()),
1703                        copy_allowed: Some(us.copy_allowed.clone()),
1704                        paste_allowed: Some(us.paste_allowed.clone()),
1705                        download_allowed: Some(us.download_allowed.clone()),
1706                        upload_allowed: Some(us.upload_allowed.clone()),
1707                        print_allowed: Some(us.print_allowed.clone()),
1708                        disconnect_timeout_in_minutes: us.disconnect_timeout_in_minutes,
1709                        idle_disconnect_timeout_in_minutes: us.idle_disconnect_timeout_in_minutes,
1710                        associated_portal_arns: Some(us.associated_portal_arns.clone()),
1711                        ..Default::default()
1712                    }),
1713                };
1714                wire::serialize_update_user_settings_response(&resp)
1715            }
1716            Err(e) => workspaces_web_error_response(&e),
1717        }
1718    }
1719
1720    // ── IdentityProvider handlers ───────────────────────────────────
1721
1722    async fn handle_create_identity_provider(
1723        &self,
1724        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1725        request: &MockRequest,
1726        labels: &[(&str, &str)],
1727        query: &HashMap<String, String>,
1728        account_id: &str,
1729        region: &str,
1730    ) -> MockResponse {
1731        let input = match wire::deserialize_create_identity_provider_request(request, labels, query)
1732        {
1733            Ok(v) => v,
1734            Err(e) => return rest_json_error(400, "ValidationException", &e),
1735        };
1736        if input.portal_arn.is_empty() {
1737            return rest_json_error(400, "ValidationException", "Missing 'portalArn'");
1738        }
1739        if input.identity_provider_name.is_empty() {
1740            return rest_json_error(400, "ValidationException", "Missing 'identityProviderName'");
1741        }
1742        if input.identity_provider_type.is_empty() {
1743            return rest_json_error(400, "ValidationException", "Missing 'identityProviderType'");
1744        }
1745        let mut st = state.write().await;
1746        match st.create_identity_provider(
1747            &input.portal_arn,
1748            &input.identity_provider_name,
1749            &input.identity_provider_type,
1750            input.identity_provider_details,
1751            account_id,
1752            region,
1753        ) {
1754            Ok(ip) => {
1755                let resp = wire::CreateIdentityProviderResponse {
1756                    identity_provider_arn: Some(ip.identity_provider_arn.clone()),
1757                };
1758                wire::serialize_create_identity_provider_response(&resp)
1759            }
1760            Err(e) => workspaces_web_error_response(&e),
1761        }
1762    }
1763
1764    async fn handle_get_identity_provider(
1765        &self,
1766        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1767        request: &MockRequest,
1768        labels: &[(&str, &str)],
1769        query: &HashMap<String, String>,
1770    ) -> MockResponse {
1771        let input = match wire::deserialize_get_identity_provider_request(request, labels, query) {
1772            Ok(v) => v,
1773            Err(e) => return rest_json_error(400, "ValidationException", &e),
1774        };
1775        let st = state.read().await;
1776        match st.get_identity_provider(&input.identity_provider_arn) {
1777            Ok(ip) => {
1778                let resp = wire::GetIdentityProviderResponse {
1779                    identity_provider: Some(wire::IdentityProvider {
1780                        identity_provider_arn: Some(ip.identity_provider_arn.clone()),
1781                        identity_provider_name: Some(ip.identity_provider_name.clone()),
1782                        identity_provider_type: Some(ip.identity_provider_type.clone()),
1783                        identity_provider_details: Some(ip.identity_provider_details.clone()),
1784                    }),
1785                };
1786                wire::serialize_get_identity_provider_response(&resp)
1787            }
1788            Err(e) => workspaces_web_error_response(&e),
1789        }
1790    }
1791
1792    async fn handle_delete_identity_provider(
1793        &self,
1794        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1795        request: &MockRequest,
1796        labels: &[(&str, &str)],
1797        query: &HashMap<String, String>,
1798    ) -> MockResponse {
1799        let input = match wire::deserialize_delete_identity_provider_request(request, labels, query)
1800        {
1801            Ok(v) => v,
1802            Err(e) => return rest_json_error(400, "ValidationException", &e),
1803        };
1804        let mut st = state.write().await;
1805        match st.delete_identity_provider(&input.identity_provider_arn) {
1806            Ok(()) => wire::serialize_delete_identity_provider_response(
1807                &wire::DeleteIdentityProviderResponse {},
1808            ),
1809            Err(e) => workspaces_web_error_response(&e),
1810        }
1811    }
1812
1813    async fn handle_list_identity_providers(
1814        &self,
1815        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1816        request: &MockRequest,
1817        labels: &[(&str, &str)],
1818        query: &HashMap<String, String>,
1819    ) -> MockResponse {
1820        let input = match wire::deserialize_list_identity_providers_request(request, labels, query)
1821        {
1822            Ok(v) => v,
1823            Err(e) => return rest_json_error(400, "ValidationException", &e),
1824        };
1825        let st = state.read().await;
1826        let items = st.list_identity_providers(&input.portal_arn);
1827        let entries: Vec<wire::IdentityProviderSummary> = items
1828            .iter()
1829            .map(|ip| wire::IdentityProviderSummary {
1830                identity_provider_arn: Some(ip.identity_provider_arn.clone()),
1831                identity_provider_name: Some(ip.identity_provider_name.clone()),
1832                identity_provider_type: Some(ip.identity_provider_type.clone()),
1833            })
1834            .collect();
1835        let resp = wire::ListIdentityProvidersResponse {
1836            identity_providers: Some(entries),
1837            next_token: None,
1838        };
1839        wire::serialize_list_identity_providers_response(&resp)
1840    }
1841
1842    async fn handle_update_identity_provider(
1843        &self,
1844        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1845        request: &MockRequest,
1846        labels: &[(&str, &str)],
1847        query: &HashMap<String, String>,
1848    ) -> MockResponse {
1849        let input = match wire::deserialize_update_identity_provider_request(request, labels, query)
1850        {
1851            Ok(v) => v,
1852            Err(e) => return rest_json_error(400, "ValidationException", &e),
1853        };
1854        let mut st = state.write().await;
1855        match st.update_identity_provider(
1856            &input.identity_provider_arn,
1857            input.identity_provider_name.as_deref(),
1858            input.identity_provider_type.as_deref(),
1859            input.identity_provider_details,
1860        ) {
1861            Ok(ip) => {
1862                let resp = wire::UpdateIdentityProviderResponse {
1863                    identity_provider: Some(wire::IdentityProvider {
1864                        identity_provider_arn: Some(ip.identity_provider_arn.clone()),
1865                        identity_provider_name: Some(ip.identity_provider_name.clone()),
1866                        identity_provider_type: Some(ip.identity_provider_type.clone()),
1867                        identity_provider_details: Some(ip.identity_provider_details.clone()),
1868                    }),
1869                };
1870                wire::serialize_update_identity_provider_response(&resp)
1871            }
1872            Err(e) => workspaces_web_error_response(&e),
1873        }
1874    }
1875
1876    // ── IpAccessSettings handlers ───────────────────────────────────
1877
1878    async fn handle_create_ip_access_settings(
1879        &self,
1880        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1881        request: &MockRequest,
1882        labels: &[(&str, &str)],
1883        query: &HashMap<String, String>,
1884        account_id: &str,
1885        region: &str,
1886    ) -> MockResponse {
1887        let input =
1888            match wire::deserialize_create_ip_access_settings_request(request, labels, query) {
1889                Ok(v) => v,
1890                Err(e) => return rest_json_error(400, "ValidationException", &e),
1891            };
1892        let ip_rules: Vec<types::IpRule> = input
1893            .ip_rules
1894            .into_iter()
1895            .map(|r| types::IpRule {
1896                ip_range: r.ip_range,
1897                description: r.description,
1898            })
1899            .collect();
1900        let display_name = input.display_name;
1901        let description = input.description;
1902        let tags = tags_from_input(input.tags.as_deref().unwrap_or(&[]));
1903        let mut st = state.write().await;
1904        match st.create_ip_access_settings(
1905            ip_rules,
1906            display_name,
1907            description,
1908            account_id,
1909            region,
1910            tags,
1911        ) {
1912            Ok(ias) => {
1913                let resp = wire::CreateIpAccessSettingsResponse {
1914                    ip_access_settings_arn: Some(ias.ip_access_settings_arn.clone()),
1915                };
1916                wire::serialize_create_ip_access_settings_response(&resp)
1917            }
1918            Err(e) => workspaces_web_error_response(&e),
1919        }
1920    }
1921
1922    async fn handle_get_ip_access_settings(
1923        &self,
1924        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1925        request: &MockRequest,
1926        labels: &[(&str, &str)],
1927        query: &HashMap<String, String>,
1928    ) -> MockResponse {
1929        let input = match wire::deserialize_get_ip_access_settings_request(request, labels, query) {
1930            Ok(v) => v,
1931            Err(e) => return rest_json_error(400, "ValidationException", &e),
1932        };
1933        let st = state.read().await;
1934        match st.get_ip_access_settings(&input.ip_access_settings_arn) {
1935            Ok(ias) => {
1936                let resp = wire::GetIpAccessSettingsResponse {
1937                    ip_access_settings: Some(ip_access_settings_to_wire(ias)),
1938                };
1939                wire::serialize_get_ip_access_settings_response(&resp)
1940            }
1941            Err(e) => workspaces_web_error_response(&e),
1942        }
1943    }
1944
1945    async fn handle_delete_ip_access_settings(
1946        &self,
1947        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1948        request: &MockRequest,
1949        labels: &[(&str, &str)],
1950        query: &HashMap<String, String>,
1951    ) -> MockResponse {
1952        let input =
1953            match wire::deserialize_delete_ip_access_settings_request(request, labels, query) {
1954                Ok(v) => v,
1955                Err(e) => return rest_json_error(400, "ValidationException", &e),
1956            };
1957        let mut st = state.write().await;
1958        match st.delete_ip_access_settings(&input.ip_access_settings_arn) {
1959            Ok(()) => wire::serialize_delete_ip_access_settings_response(
1960                &wire::DeleteIpAccessSettingsResponse {},
1961            ),
1962            Err(e) => workspaces_web_error_response(&e),
1963        }
1964    }
1965
1966    async fn handle_list_ip_access_settings(
1967        &self,
1968        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1969        request: &MockRequest,
1970        labels: &[(&str, &str)],
1971        query: &HashMap<String, String>,
1972    ) -> MockResponse {
1973        if let Err(e) = wire::deserialize_list_ip_access_settings_request(request, labels, query) {
1974            return rest_json_error(400, "ValidationException", &e);
1975        }
1976        let st = state.read().await;
1977        let items = st.list_ip_access_settings();
1978        let entries: Vec<wire::IpAccessSettingsSummary> = items
1979            .iter()
1980            .map(|ias| wire::IpAccessSettingsSummary {
1981                ip_access_settings_arn: Some(ias.ip_access_settings_arn.clone()),
1982                display_name: ias.display_name.clone(),
1983                description: ias.description.clone(),
1984                creation_date: Some(ias.creation_date.timestamp() as f64),
1985            })
1986            .collect();
1987        let resp = wire::ListIpAccessSettingsResponse {
1988            ip_access_settings: Some(entries),
1989            next_token: None,
1990        };
1991        wire::serialize_list_ip_access_settings_response(&resp)
1992    }
1993
1994    async fn handle_update_ip_access_settings(
1995        &self,
1996        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
1997        request: &MockRequest,
1998        labels: &[(&str, &str)],
1999        query: &HashMap<String, String>,
2000    ) -> MockResponse {
2001        let input =
2002            match wire::deserialize_update_ip_access_settings_request(request, labels, query) {
2003                Ok(v) => v,
2004                Err(e) => return rest_json_error(400, "ValidationException", &e),
2005            };
2006        let ip_rules: Option<Vec<types::IpRule>> = input.ip_rules.map(|rules| {
2007            rules
2008                .into_iter()
2009                .map(|r| types::IpRule {
2010                    ip_range: r.ip_range,
2011                    description: r.description,
2012                })
2013                .collect()
2014        });
2015        let mut st = state.write().await;
2016        match st.update_ip_access_settings(
2017            &input.ip_access_settings_arn,
2018            ip_rules,
2019            input.display_name,
2020            input.description,
2021        ) {
2022            Ok(ias) => {
2023                let resp = wire::UpdateIpAccessSettingsResponse {
2024                    ip_access_settings: Some(ip_access_settings_to_wire(ias)),
2025                };
2026                wire::serialize_update_ip_access_settings_response(&resp)
2027            }
2028            Err(e) => workspaces_web_error_response(&e),
2029        }
2030    }
2031
2032    async fn handle_associate_ip_access_settings(
2033        &self,
2034        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2035        request: &MockRequest,
2036        labels: &[(&str, &str)],
2037        query: &HashMap<String, String>,
2038    ) -> MockResponse {
2039        let input =
2040            match wire::deserialize_associate_ip_access_settings_request(request, labels, query) {
2041                Ok(v) => v,
2042                Err(e) => return rest_json_error(400, "ValidationException", &e),
2043            };
2044        let mut st = state.write().await;
2045        match st.associate_ip_access_settings(&input.portal_arn, &input.ip_access_settings_arn) {
2046            Ok(()) => {
2047                let resp = wire::AssociateIpAccessSettingsResponse {
2048                    portal_arn: Some(input.portal_arn.clone()),
2049                    ip_access_settings_arn: Some(input.ip_access_settings_arn.clone()),
2050                };
2051                wire::serialize_associate_ip_access_settings_response(&resp)
2052            }
2053            Err(e) => workspaces_web_error_response(&e),
2054        }
2055    }
2056
2057    async fn handle_disassociate_ip_access_settings(
2058        &self,
2059        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2060        request: &MockRequest,
2061        labels: &[(&str, &str)],
2062        query: &HashMap<String, String>,
2063    ) -> MockResponse {
2064        let input =
2065            match wire::deserialize_disassociate_ip_access_settings_request(request, labels, query)
2066            {
2067                Ok(v) => v,
2068                Err(e) => return rest_json_error(400, "ValidationException", &e),
2069            };
2070        let mut st = state.write().await;
2071        match st.disassociate_ip_access_settings(&input.portal_arn) {
2072            Ok(()) => wire::serialize_disassociate_ip_access_settings_response(
2073                &wire::DisassociateIpAccessSettingsResponse {},
2074            ),
2075            Err(e) => workspaces_web_error_response(&e),
2076        }
2077    }
2078
2079    // ── TrustStore handlers ─────────────────────────────────────────
2080
2081    async fn handle_create_trust_store(
2082        &self,
2083        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2084        request: &MockRequest,
2085        labels: &[(&str, &str)],
2086        query: &HashMap<String, String>,
2087        account_id: &str,
2088        region: &str,
2089    ) -> MockResponse {
2090        let input = match wire::deserialize_create_trust_store_request(request, labels, query) {
2091            Ok(v) => v,
2092            Err(e) => return rest_json_error(400, "ValidationException", &e),
2093        };
2094        let tags = tags_from_input(input.tags.as_deref().unwrap_or(&[]));
2095        let mut st = state.write().await;
2096        match st.create_trust_store(input.certificate_list, account_id, region, tags) {
2097            Ok(ts) => {
2098                let resp = wire::CreateTrustStoreResponse {
2099                    trust_store_arn: Some(ts.trust_store_arn.clone()),
2100                };
2101                wire::serialize_create_trust_store_response(&resp)
2102            }
2103            Err(e) => workspaces_web_error_response(&e),
2104        }
2105    }
2106
2107    async fn handle_get_trust_store(
2108        &self,
2109        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2110        request: &MockRequest,
2111        labels: &[(&str, &str)],
2112        query: &HashMap<String, String>,
2113    ) -> MockResponse {
2114        let input = match wire::deserialize_get_trust_store_request(request, labels, query) {
2115            Ok(v) => v,
2116            Err(e) => return rest_json_error(400, "ValidationException", &e),
2117        };
2118        let st = state.read().await;
2119        match st.get_trust_store(&input.trust_store_arn) {
2120            Ok(ts) => {
2121                let resp = wire::GetTrustStoreResponse {
2122                    trust_store: Some(wire::TrustStore {
2123                        trust_store_arn: Some(ts.trust_store_arn.clone()),
2124                        associated_portal_arns: Some(ts.associated_portal_arns.clone()),
2125                    }),
2126                };
2127                wire::serialize_get_trust_store_response(&resp)
2128            }
2129            Err(e) => workspaces_web_error_response(&e),
2130        }
2131    }
2132
2133    async fn handle_delete_trust_store(
2134        &self,
2135        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2136        request: &MockRequest,
2137        labels: &[(&str, &str)],
2138        query: &HashMap<String, String>,
2139    ) -> MockResponse {
2140        let input = match wire::deserialize_delete_trust_store_request(request, labels, query) {
2141            Ok(v) => v,
2142            Err(e) => return rest_json_error(400, "ValidationException", &e),
2143        };
2144        let mut st = state.write().await;
2145        match st.delete_trust_store(&input.trust_store_arn) {
2146            Ok(()) => {
2147                wire::serialize_delete_trust_store_response(&wire::DeleteTrustStoreResponse {})
2148            }
2149            Err(e) => workspaces_web_error_response(&e),
2150        }
2151    }
2152
2153    async fn handle_list_trust_stores(
2154        &self,
2155        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2156        request: &MockRequest,
2157        labels: &[(&str, &str)],
2158        query: &HashMap<String, String>,
2159    ) -> MockResponse {
2160        if let Err(e) = wire::deserialize_list_trust_stores_request(request, labels, query) {
2161            return rest_json_error(400, "ValidationException", &e);
2162        }
2163        let st = state.read().await;
2164        let items = st.list_trust_stores();
2165        let entries: Vec<wire::TrustStoreSummary> = items
2166            .iter()
2167            .map(|ts| wire::TrustStoreSummary {
2168                trust_store_arn: Some(ts.trust_store_arn.clone()),
2169            })
2170            .collect();
2171        let resp = wire::ListTrustStoresResponse {
2172            trust_stores: Some(entries),
2173            next_token: None,
2174        };
2175        wire::serialize_list_trust_stores_response(&resp)
2176    }
2177
2178    async fn handle_update_trust_store(
2179        &self,
2180        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2181        request: &MockRequest,
2182        labels: &[(&str, &str)],
2183        query: &HashMap<String, String>,
2184    ) -> MockResponse {
2185        let input = match wire::deserialize_update_trust_store_request(request, labels, query) {
2186            Ok(v) => v,
2187            Err(e) => return rest_json_error(400, "ValidationException", &e),
2188        };
2189        let certs_to_add = input.certificates_to_add.unwrap_or_default();
2190        let thumbprints_to_delete = input.certificates_to_delete.unwrap_or_default();
2191        let mut st = state.write().await;
2192        match st.update_trust_store(&input.trust_store_arn, certs_to_add, thumbprints_to_delete) {
2193            Ok(ts) => {
2194                let resp = wire::UpdateTrustStoreResponse {
2195                    trust_store_arn: Some(ts.trust_store_arn.clone()),
2196                };
2197                wire::serialize_update_trust_store_response(&resp)
2198            }
2199            Err(e) => workspaces_web_error_response(&e),
2200        }
2201    }
2202
2203    async fn handle_get_trust_store_certificate(
2204        &self,
2205        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2206        request: &MockRequest,
2207        labels: &[(&str, &str)],
2208        query: &HashMap<String, String>,
2209    ) -> MockResponse {
2210        let input =
2211            match wire::deserialize_get_trust_store_certificate_request(request, labels, query) {
2212                Ok(v) => v,
2213                Err(e) => return rest_json_error(400, "ValidationException", &e),
2214            };
2215        let st = state.read().await;
2216        match st.get_trust_store(&input.trust_store_arn) {
2217            Ok(ts) => {
2218                let cert = ts
2219                    .certificate_list
2220                    .iter()
2221                    .find(|c| c.thumbprint == input.thumbprint);
2222                let wire_cert = cert.map(|c| wire::Certificate {
2223                    body: Some(c.body.clone()),
2224                    issuer: c.issuer.clone(),
2225                    subject: c.subject.clone(),
2226                    thumbprint: Some(c.thumbprint.clone()),
2227                    ..Default::default()
2228                });
2229                let resp = wire::GetTrustStoreCertificateResponse {
2230                    trust_store_arn: Some(input.trust_store_arn.clone()),
2231                    certificate: wire_cert,
2232                };
2233                wire::serialize_get_trust_store_certificate_response(&resp)
2234            }
2235            Err(e) => workspaces_web_error_response(&e),
2236        }
2237    }
2238
2239    async fn handle_list_trust_store_certificates(
2240        &self,
2241        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2242        request: &MockRequest,
2243        labels: &[(&str, &str)],
2244        query: &HashMap<String, String>,
2245    ) -> MockResponse {
2246        let input =
2247            match wire::deserialize_list_trust_store_certificates_request(request, labels, query) {
2248                Ok(v) => v,
2249                Err(e) => return rest_json_error(400, "ValidationException", &e),
2250            };
2251        let st = state.read().await;
2252        match st.get_trust_store(&input.trust_store_arn) {
2253            Ok(ts) => {
2254                let certs: Vec<wire::CertificateSummary> = ts
2255                    .certificate_list
2256                    .iter()
2257                    .map(|c| wire::CertificateSummary {
2258                        thumbprint: Some(c.thumbprint.clone()),
2259                        issuer: c.issuer.clone(),
2260                        subject: c.subject.clone(),
2261                        ..Default::default()
2262                    })
2263                    .collect();
2264                let resp = wire::ListTrustStoreCertificatesResponse {
2265                    trust_store_arn: Some(input.trust_store_arn.clone()),
2266                    certificate_list: Some(certs),
2267                    next_token: None,
2268                };
2269                wire::serialize_list_trust_store_certificates_response(&resp)
2270            }
2271            Err(e) => workspaces_web_error_response(&e),
2272        }
2273    }
2274
2275    async fn handle_associate_trust_store(
2276        &self,
2277        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2278        request: &MockRequest,
2279        labels: &[(&str, &str)],
2280        query: &HashMap<String, String>,
2281    ) -> MockResponse {
2282        let input = match wire::deserialize_associate_trust_store_request(request, labels, query) {
2283            Ok(v) => v,
2284            Err(e) => return rest_json_error(400, "ValidationException", &e),
2285        };
2286        let mut st = state.write().await;
2287        match st.associate_trust_store(&input.portal_arn, &input.trust_store_arn) {
2288            Ok(()) => {
2289                let resp = wire::AssociateTrustStoreResponse {
2290                    portal_arn: Some(input.portal_arn.clone()),
2291                    trust_store_arn: Some(input.trust_store_arn.clone()),
2292                };
2293                wire::serialize_associate_trust_store_response(&resp)
2294            }
2295            Err(e) => workspaces_web_error_response(&e),
2296        }
2297    }
2298
2299    async fn handle_disassociate_trust_store(
2300        &self,
2301        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2302        request: &MockRequest,
2303        labels: &[(&str, &str)],
2304        query: &HashMap<String, String>,
2305    ) -> MockResponse {
2306        let input = match wire::deserialize_disassociate_trust_store_request(request, labels, query)
2307        {
2308            Ok(v) => v,
2309            Err(e) => return rest_json_error(400, "ValidationException", &e),
2310        };
2311        let mut st = state.write().await;
2312        match st.disassociate_trust_store(&input.portal_arn) {
2313            Ok(()) => wire::serialize_disassociate_trust_store_response(
2314                &wire::DisassociateTrustStoreResponse {},
2315            ),
2316            Err(e) => workspaces_web_error_response(&e),
2317        }
2318    }
2319
2320    // ── DataProtectionSettings handlers ─────────────────────────────
2321
2322    async fn handle_create_data_protection_settings(
2323        &self,
2324        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2325        request: &MockRequest,
2326        labels: &[(&str, &str)],
2327        query: &HashMap<String, String>,
2328        account_id: &str,
2329        region: &str,
2330    ) -> MockResponse {
2331        let input =
2332            match wire::deserialize_create_data_protection_settings_request(request, labels, query)
2333            {
2334                Ok(v) => v,
2335                Err(e) => return rest_json_error(400, "ValidationException", &e),
2336            };
2337        let display_name = input.display_name;
2338        let description = input.description;
2339        let tags = tags_from_input(input.tags.as_deref().unwrap_or(&[]));
2340        let mut st = state.write().await;
2341        match st.create_data_protection_settings(
2342            display_name,
2343            description,
2344            account_id,
2345            region,
2346            tags,
2347        ) {
2348            Ok(dps) => {
2349                let resp = wire::CreateDataProtectionSettingsResponse {
2350                    data_protection_settings_arn: Some(dps.data_protection_settings_arn.clone()),
2351                };
2352                wire::serialize_create_data_protection_settings_response(&resp)
2353            }
2354            Err(e) => workspaces_web_error_response(&e),
2355        }
2356    }
2357
2358    async fn handle_get_data_protection_settings(
2359        &self,
2360        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2361        request: &MockRequest,
2362        labels: &[(&str, &str)],
2363        query: &HashMap<String, String>,
2364    ) -> MockResponse {
2365        let input =
2366            match wire::deserialize_get_data_protection_settings_request(request, labels, query) {
2367                Ok(v) => v,
2368                Err(e) => return rest_json_error(400, "ValidationException", &e),
2369            };
2370        let st = state.read().await;
2371        match st.get_data_protection_settings(&input.data_protection_settings_arn) {
2372            Ok(dps) => {
2373                let resp = wire::GetDataProtectionSettingsResponse {
2374                    data_protection_settings: Some(wire::DataProtectionSettings {
2375                        data_protection_settings_arn: Some(
2376                            dps.data_protection_settings_arn.clone(),
2377                        ),
2378                        display_name: dps.display_name.clone(),
2379                        description: dps.description.clone(),
2380                        associated_portal_arns: Some(dps.associated_portal_arns.clone()),
2381                        creation_date: Some(dps.creation_date.timestamp() as f64),
2382                        ..Default::default()
2383                    }),
2384                };
2385                wire::serialize_get_data_protection_settings_response(&resp)
2386            }
2387            Err(e) => workspaces_web_error_response(&e),
2388        }
2389    }
2390
2391    async fn handle_delete_data_protection_settings(
2392        &self,
2393        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2394        request: &MockRequest,
2395        labels: &[(&str, &str)],
2396        query: &HashMap<String, String>,
2397    ) -> MockResponse {
2398        let input =
2399            match wire::deserialize_delete_data_protection_settings_request(request, labels, query)
2400            {
2401                Ok(v) => v,
2402                Err(e) => return rest_json_error(400, "ValidationException", &e),
2403            };
2404        let mut st = state.write().await;
2405        match st.delete_data_protection_settings(&input.data_protection_settings_arn) {
2406            Ok(()) => wire::serialize_delete_data_protection_settings_response(
2407                &wire::DeleteDataProtectionSettingsResponse {},
2408            ),
2409            Err(e) => workspaces_web_error_response(&e),
2410        }
2411    }
2412
2413    async fn handle_list_data_protection_settings(
2414        &self,
2415        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2416        request: &MockRequest,
2417        labels: &[(&str, &str)],
2418        query: &HashMap<String, String>,
2419    ) -> MockResponse {
2420        if let Err(e) =
2421            wire::deserialize_list_data_protection_settings_request(request, labels, query)
2422        {
2423            return rest_json_error(400, "ValidationException", &e);
2424        }
2425        let st = state.read().await;
2426        let items = st.list_data_protection_settings();
2427        let entries: Vec<wire::DataProtectionSettingsSummary> = items
2428            .iter()
2429            .map(|dps| wire::DataProtectionSettingsSummary {
2430                data_protection_settings_arn: Some(dps.data_protection_settings_arn.clone()),
2431                display_name: dps.display_name.clone(),
2432                description: dps.description.clone(),
2433                creation_date: Some(dps.creation_date.timestamp() as f64),
2434            })
2435            .collect();
2436        let resp = wire::ListDataProtectionSettingsResponse {
2437            data_protection_settings: Some(entries),
2438            next_token: None,
2439        };
2440        wire::serialize_list_data_protection_settings_response(&resp)
2441    }
2442
2443    async fn handle_update_data_protection_settings(
2444        &self,
2445        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2446        request: &MockRequest,
2447        labels: &[(&str, &str)],
2448        query: &HashMap<String, String>,
2449    ) -> MockResponse {
2450        let input =
2451            match wire::deserialize_update_data_protection_settings_request(request, labels, query)
2452            {
2453                Ok(v) => v,
2454                Err(e) => return rest_json_error(400, "ValidationException", &e),
2455            };
2456        let mut st = state.write().await;
2457        match st.update_data_protection_settings(
2458            &input.data_protection_settings_arn,
2459            input.display_name,
2460            input.description,
2461        ) {
2462            Ok(dps) => {
2463                let resp = wire::UpdateDataProtectionSettingsResponse {
2464                    data_protection_settings: Some(wire::DataProtectionSettings {
2465                        data_protection_settings_arn: Some(
2466                            dps.data_protection_settings_arn.clone(),
2467                        ),
2468                        display_name: dps.display_name.clone(),
2469                        description: dps.description.clone(),
2470                        associated_portal_arns: Some(dps.associated_portal_arns.clone()),
2471                        creation_date: Some(dps.creation_date.timestamp() as f64),
2472                        ..Default::default()
2473                    }),
2474                };
2475                wire::serialize_update_data_protection_settings_response(&resp)
2476            }
2477            Err(e) => workspaces_web_error_response(&e),
2478        }
2479    }
2480
2481    async fn handle_associate_data_protection_settings(
2482        &self,
2483        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2484        request: &MockRequest,
2485        labels: &[(&str, &str)],
2486        query: &HashMap<String, String>,
2487    ) -> MockResponse {
2488        let input = match wire::deserialize_associate_data_protection_settings_request(
2489            request, labels, query,
2490        ) {
2491            Ok(v) => v,
2492            Err(e) => return rest_json_error(400, "ValidationException", &e),
2493        };
2494        let mut st = state.write().await;
2495        match st.associate_data_protection_settings(
2496            &input.portal_arn,
2497            &input.data_protection_settings_arn,
2498        ) {
2499            Ok(()) => {
2500                let resp = wire::AssociateDataProtectionSettingsResponse {
2501                    portal_arn: Some(input.portal_arn.clone()),
2502                    data_protection_settings_arn: Some(input.data_protection_settings_arn.clone()),
2503                };
2504                wire::serialize_associate_data_protection_settings_response(&resp)
2505            }
2506            Err(e) => workspaces_web_error_response(&e),
2507        }
2508    }
2509
2510    async fn handle_disassociate_data_protection_settings(
2511        &self,
2512        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2513        request: &MockRequest,
2514        labels: &[(&str, &str)],
2515        query: &HashMap<String, String>,
2516    ) -> MockResponse {
2517        let input = match wire::deserialize_disassociate_data_protection_settings_request(
2518            request, labels, query,
2519        ) {
2520            Ok(v) => v,
2521            Err(e) => return rest_json_error(400, "ValidationException", &e),
2522        };
2523        let mut st = state.write().await;
2524        match st.disassociate_data_protection_settings(&input.portal_arn) {
2525            Ok(()) => wire::serialize_disassociate_data_protection_settings_response(
2526                &wire::DisassociateDataProtectionSettingsResponse {},
2527            ),
2528            Err(e) => workspaces_web_error_response(&e),
2529        }
2530    }
2531
2532    // ── SessionLogger handlers ──────────────────────────────────────
2533
2534    async fn handle_create_session_logger(
2535        &self,
2536        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2537        request: &MockRequest,
2538        labels: &[(&str, &str)],
2539        query: &HashMap<String, String>,
2540        account_id: &str,
2541        region: &str,
2542    ) -> MockResponse {
2543        let input = match wire::deserialize_create_session_logger_request(request, labels, query) {
2544            Ok(v) => v,
2545            Err(e) => return rest_json_error(400, "ValidationException", &e),
2546        };
2547        let display_name = input.display_name;
2548        let tags = tags_from_input(input.tags.as_deref().unwrap_or(&[]));
2549        let mut st = state.write().await;
2550        match st.create_session_logger(display_name, account_id, region, tags) {
2551            Ok(sl) => {
2552                let resp = wire::CreateSessionLoggerResponse {
2553                    session_logger_arn: Some(sl.session_logger_arn.clone()),
2554                };
2555                wire::serialize_create_session_logger_response(&resp)
2556            }
2557            Err(e) => workspaces_web_error_response(&e),
2558        }
2559    }
2560
2561    async fn handle_get_session_logger(
2562        &self,
2563        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2564        request: &MockRequest,
2565        labels: &[(&str, &str)],
2566        query: &HashMap<String, String>,
2567    ) -> MockResponse {
2568        let input = match wire::deserialize_get_session_logger_request(request, labels, query) {
2569            Ok(v) => v,
2570            Err(e) => return rest_json_error(400, "ValidationException", &e),
2571        };
2572        let st = state.read().await;
2573        match st.get_session_logger(&input.session_logger_arn) {
2574            Ok(sl) => {
2575                let resp = wire::GetSessionLoggerResponse {
2576                    session_logger: Some(wire::SessionLogger {
2577                        session_logger_arn: Some(sl.session_logger_arn.clone()),
2578                        display_name: sl.display_name.clone(),
2579                        associated_portal_arns: Some(sl.associated_portal_arns.clone()),
2580                        creation_date: Some(sl.creation_date.timestamp() as f64),
2581                        ..Default::default()
2582                    }),
2583                };
2584                wire::serialize_get_session_logger_response(&resp)
2585            }
2586            Err(e) => workspaces_web_error_response(&e),
2587        }
2588    }
2589
2590    async fn handle_delete_session_logger(
2591        &self,
2592        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2593        request: &MockRequest,
2594        labels: &[(&str, &str)],
2595        query: &HashMap<String, String>,
2596    ) -> MockResponse {
2597        let input = match wire::deserialize_delete_session_logger_request(request, labels, query) {
2598            Ok(v) => v,
2599            Err(e) => return rest_json_error(400, "ValidationException", &e),
2600        };
2601        let mut st = state.write().await;
2602        match st.delete_session_logger(&input.session_logger_arn) {
2603            Ok(()) => wire::serialize_delete_session_logger_response(
2604                &wire::DeleteSessionLoggerResponse {},
2605            ),
2606            Err(e) => workspaces_web_error_response(&e),
2607        }
2608    }
2609
2610    async fn handle_list_session_loggers(
2611        &self,
2612        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2613        request: &MockRequest,
2614        labels: &[(&str, &str)],
2615        query: &HashMap<String, String>,
2616    ) -> MockResponse {
2617        if let Err(e) = wire::deserialize_list_session_loggers_request(request, labels, query) {
2618            return rest_json_error(400, "ValidationException", &e);
2619        }
2620        let st = state.read().await;
2621        let items = st.list_session_loggers();
2622        let entries: Vec<wire::SessionLoggerSummary> = items
2623            .iter()
2624            .map(|sl| wire::SessionLoggerSummary {
2625                session_logger_arn: Some(sl.session_logger_arn.clone()),
2626                display_name: sl.display_name.clone(),
2627                creation_date: Some(sl.creation_date.timestamp() as f64),
2628                log_configuration: None,
2629            })
2630            .collect();
2631        let resp = wire::ListSessionLoggersResponse {
2632            session_loggers: Some(entries),
2633            next_token: None,
2634        };
2635        wire::serialize_list_session_loggers_response(&resp)
2636    }
2637
2638    async fn handle_update_session_logger(
2639        &self,
2640        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2641        request: &MockRequest,
2642        labels: &[(&str, &str)],
2643        query: &HashMap<String, String>,
2644    ) -> MockResponse {
2645        let input = match wire::deserialize_update_session_logger_request(request, labels, query) {
2646            Ok(v) => v,
2647            Err(e) => return rest_json_error(400, "ValidationException", &e),
2648        };
2649        let mut st = state.write().await;
2650        match st.update_session_logger(&input.session_logger_arn, input.display_name) {
2651            Ok(sl) => {
2652                let resp = wire::UpdateSessionLoggerResponse {
2653                    session_logger: Some(wire::SessionLogger {
2654                        session_logger_arn: Some(sl.session_logger_arn.clone()),
2655                        display_name: sl.display_name.clone(),
2656                        associated_portal_arns: Some(sl.associated_portal_arns.clone()),
2657                        creation_date: Some(sl.creation_date.timestamp() as f64),
2658                        ..Default::default()
2659                    }),
2660                };
2661                wire::serialize_update_session_logger_response(&resp)
2662            }
2663            Err(e) => workspaces_web_error_response(&e),
2664        }
2665    }
2666
2667    async fn handle_associate_session_logger(
2668        &self,
2669        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2670        request: &MockRequest,
2671        labels: &[(&str, &str)],
2672        query: &HashMap<String, String>,
2673    ) -> MockResponse {
2674        let input = match wire::deserialize_associate_session_logger_request(request, labels, query)
2675        {
2676            Ok(v) => v,
2677            Err(e) => return rest_json_error(400, "ValidationException", &e),
2678        };
2679        let mut st = state.write().await;
2680        match st.associate_session_logger(&input.portal_arn, &input.session_logger_arn) {
2681            Ok(()) => {
2682                let resp = wire::AssociateSessionLoggerResponse {
2683                    portal_arn: Some(input.portal_arn.clone()),
2684                    session_logger_arn: Some(input.session_logger_arn.clone()),
2685                };
2686                wire::serialize_associate_session_logger_response(&resp)
2687            }
2688            Err(e) => workspaces_web_error_response(&e),
2689        }
2690    }
2691
2692    async fn handle_disassociate_session_logger(
2693        &self,
2694        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2695        request: &MockRequest,
2696        labels: &[(&str, &str)],
2697        query: &HashMap<String, String>,
2698    ) -> MockResponse {
2699        let input =
2700            match wire::deserialize_disassociate_session_logger_request(request, labels, query) {
2701                Ok(v) => v,
2702                Err(e) => return rest_json_error(400, "ValidationException", &e),
2703            };
2704        let mut st = state.write().await;
2705        match st.disassociate_session_logger(&input.portal_arn) {
2706            Ok(()) => wire::serialize_disassociate_session_logger_response(
2707                &wire::DisassociateSessionLoggerResponse {},
2708            ),
2709            Err(e) => workspaces_web_error_response(&e),
2710        }
2711    }
2712
2713    // ── Session handlers ─────────────────────────────────────────────
2714
2715    async fn handle_get_session(
2716        &self,
2717        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2718        request: &MockRequest,
2719        labels: &[(&str, &str)],
2720        query: &HashMap<String, String>,
2721    ) -> MockResponse {
2722        let input = match wire::deserialize_get_session_request(request, labels, query) {
2723            Ok(v) => v,
2724            Err(e) => return rest_json_error(400, "ValidationException", &e),
2725        };
2726        let st = state.read().await;
2727        match st.get_session(&input.portal_id, &input.session_id) {
2728            Ok(s) => {
2729                let resp = wire::GetSessionResponse {
2730                    session: Some(wire::Session {
2731                        session_id: Some(s.session_id.clone()),
2732                        portal_arn: Some(s.portal_id.clone()),
2733                        username: s.username.clone(),
2734                        client_ip_addresses: Some(s.client_ip_addresses.clone()),
2735                        status: Some(s.status.clone()),
2736                        start_time: Some(s.start_time.timestamp() as f64),
2737                        end_time: s.end_time.map(|t| t.timestamp() as f64),
2738                    }),
2739                };
2740                wire::serialize_get_session_response(&resp)
2741            }
2742            Err(e) => workspaces_web_error_response(&e),
2743        }
2744    }
2745
2746    async fn handle_list_sessions(
2747        &self,
2748        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2749        request: &MockRequest,
2750        labels: &[(&str, &str)],
2751        query: &HashMap<String, String>,
2752    ) -> MockResponse {
2753        let input = match wire::deserialize_list_sessions_request(request, labels, query) {
2754            Ok(v) => v,
2755            Err(e) => return rest_json_error(400, "ValidationException", &e),
2756        };
2757        let st = state.read().await;
2758        let sessions = st.list_sessions(&input.portal_id);
2759        let entries: Vec<wire::SessionSummary> = sessions
2760            .iter()
2761            .map(|s| wire::SessionSummary {
2762                session_id: Some(s.session_id.clone()),
2763                portal_arn: Some(s.portal_id.clone()),
2764                username: s.username.clone(),
2765                status: Some(s.status.clone()),
2766                start_time: Some(s.start_time.timestamp() as f64),
2767                end_time: s.end_time.map(|t| t.timestamp() as f64),
2768            })
2769            .collect();
2770        let resp = wire::ListSessionsResponse {
2771            sessions: Some(entries),
2772            next_token: None,
2773        };
2774        wire::serialize_list_sessions_response(&resp)
2775    }
2776
2777    async fn handle_expire_session(
2778        &self,
2779        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2780        request: &MockRequest,
2781        labels: &[(&str, &str)],
2782        query: &HashMap<String, String>,
2783    ) -> MockResponse {
2784        let input = match wire::deserialize_expire_session_request(request, labels, query) {
2785            Ok(v) => v,
2786            Err(e) => return rest_json_error(400, "ValidationException", &e),
2787        };
2788        let mut st = state.write().await;
2789        match st.expire_session(&input.portal_id, &input.session_id) {
2790            Ok(()) => wire::serialize_expire_session_response(&wire::ExpireSessionResponse {}),
2791            Err(e) => workspaces_web_error_response(&e),
2792        }
2793    }
2794
2795    // ── Association handlers ────────────────────────────────────────
2796
2797    async fn handle_associate_browser_settings(
2798        &self,
2799        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2800        request: &MockRequest,
2801        labels: &[(&str, &str)],
2802        query: &HashMap<String, String>,
2803    ) -> MockResponse {
2804        let input =
2805            match wire::deserialize_associate_browser_settings_request(request, labels, query) {
2806                Ok(v) => v,
2807                Err(e) => return rest_json_error(400, "ValidationException", &e),
2808            };
2809        let mut state = state.write().await;
2810        match state.associate_browser_settings(&input.portal_arn, &input.browser_settings_arn) {
2811            Ok(()) => {
2812                let resp = wire::AssociateBrowserSettingsResponse {
2813                    portal_arn: Some(input.portal_arn.clone()),
2814                    browser_settings_arn: Some(input.browser_settings_arn.clone()),
2815                };
2816                wire::serialize_associate_browser_settings_response(&resp)
2817            }
2818            Err(e) => workspaces_web_error_response(&e),
2819        }
2820    }
2821
2822    async fn handle_associate_network_settings(
2823        &self,
2824        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2825        request: &MockRequest,
2826        labels: &[(&str, &str)],
2827        query: &HashMap<String, String>,
2828    ) -> MockResponse {
2829        let input =
2830            match wire::deserialize_associate_network_settings_request(request, labels, query) {
2831                Ok(v) => v,
2832                Err(e) => return rest_json_error(400, "ValidationException", &e),
2833            };
2834        let mut state = state.write().await;
2835        match state.associate_network_settings(&input.portal_arn, &input.network_settings_arn) {
2836            Ok(()) => {
2837                let resp = wire::AssociateNetworkSettingsResponse {
2838                    portal_arn: Some(input.portal_arn.clone()),
2839                    network_settings_arn: Some(input.network_settings_arn.clone()),
2840                };
2841                wire::serialize_associate_network_settings_response(&resp)
2842            }
2843            Err(e) => workspaces_web_error_response(&e),
2844        }
2845    }
2846
2847    async fn handle_associate_user_access_logging_settings(
2848        &self,
2849        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2850        request: &MockRequest,
2851        labels: &[(&str, &str)],
2852        query: &HashMap<String, String>,
2853    ) -> MockResponse {
2854        let input = match wire::deserialize_associate_user_access_logging_settings_request(
2855            request, labels, query,
2856        ) {
2857            Ok(v) => v,
2858            Err(e) => return rest_json_error(400, "ValidationException", &e),
2859        };
2860        let mut state = state.write().await;
2861        match state.associate_user_access_logging_settings(
2862            &input.portal_arn,
2863            &input.user_access_logging_settings_arn,
2864        ) {
2865            Ok(()) => {
2866                let resp = wire::AssociateUserAccessLoggingSettingsResponse {
2867                    portal_arn: Some(input.portal_arn.clone()),
2868                    user_access_logging_settings_arn: Some(
2869                        input.user_access_logging_settings_arn.clone(),
2870                    ),
2871                };
2872                wire::serialize_associate_user_access_logging_settings_response(&resp)
2873            }
2874            Err(e) => workspaces_web_error_response(&e),
2875        }
2876    }
2877
2878    async fn handle_associate_user_settings(
2879        &self,
2880        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2881        request: &MockRequest,
2882        labels: &[(&str, &str)],
2883        query: &HashMap<String, String>,
2884    ) -> MockResponse {
2885        let input = match wire::deserialize_associate_user_settings_request(request, labels, query)
2886        {
2887            Ok(v) => v,
2888            Err(e) => return rest_json_error(400, "ValidationException", &e),
2889        };
2890        let mut state = state.write().await;
2891        match state.associate_user_settings(&input.portal_arn, &input.user_settings_arn) {
2892            Ok(()) => {
2893                let resp = wire::AssociateUserSettingsResponse {
2894                    portal_arn: Some(input.portal_arn.clone()),
2895                    user_settings_arn: Some(input.user_settings_arn.clone()),
2896                };
2897                wire::serialize_associate_user_settings_response(&resp)
2898            }
2899            Err(e) => workspaces_web_error_response(&e),
2900        }
2901    }
2902
2903    // ── Tag handlers ────────────────────────────────────────────────
2904
2905    async fn handle_list_tags_for_resource(
2906        &self,
2907        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2908        request: &MockRequest,
2909        labels: &[(&str, &str)],
2910        query: &HashMap<String, String>,
2911    ) -> MockResponse {
2912        let input = match wire::deserialize_list_tags_for_resource_request(request, labels, query) {
2913            Ok(v) => v,
2914            Err(e) => return rest_json_error(400, "ValidationException", &e),
2915        };
2916        let state = state.read().await;
2917        match state.get_tags_for_resource(&input.resource_arn) {
2918            Ok(tags) => {
2919                let wire_tags: Vec<wire::Tag> = tags
2920                    .iter()
2921                    .map(|(k, v)| wire::Tag {
2922                        key: k.clone(),
2923                        value: v.clone(),
2924                    })
2925                    .collect();
2926                let resp = wire::ListTagsForResourceResponse {
2927                    tags: Some(wire_tags),
2928                };
2929                wire::serialize_list_tags_for_resource_response(&resp)
2930            }
2931            Err(e) => workspaces_web_error_response(&e),
2932        }
2933    }
2934
2935    async fn handle_tag_resource(
2936        &self,
2937        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2938        request: &MockRequest,
2939        labels: &[(&str, &str)],
2940        query: &HashMap<String, String>,
2941    ) -> MockResponse {
2942        let input = match wire::deserialize_tag_resource_request(request, labels, query) {
2943            Ok(v) => v,
2944            Err(e) => return rest_json_error(400, "ValidationException", &e),
2945        };
2946        let tags = tags_from_input(&input.tags);
2947        if tags.is_empty() {
2948            return rest_json_error(400, "ValidationException", "Missing 'tags'");
2949        }
2950        let mut state = state.write().await;
2951        match state.tag_resource(&input.resource_arn, tags) {
2952            Ok(()) => wire::serialize_tag_resource_response(&wire::TagResourceResponse {}),
2953            Err(e) => workspaces_web_error_response(&e),
2954        }
2955    }
2956
2957    async fn handle_untag_resource(
2958        &self,
2959        state: &Arc<tokio::sync::RwLock<WorkspacesWebState>>,
2960        request: &MockRequest,
2961        labels: &[(&str, &str)],
2962        query: &HashMap<String, String>,
2963    ) -> MockResponse {
2964        // Note: AWS SDK sends tagKeys as repeated query params (tagKeys=k1&tagKeys=k2).
2965        // The wire deserialiser splits a single comma-separated value, but our parse
2966        // routine stores duplicates as suffixed keys (`tagKeys`, `tagKeys_1`, ...).
2967        // Collect every value whose key starts with "tagKeys" to preserve the existing
2968        // multi-value behaviour.
2969        let _ = wire::deserialize_untag_resource_request(request, labels, query);
2970        let resource_arn = labels
2971            .iter()
2972            .find(|(k, _)| *k == "resourceArn")
2973            .map(|(_, v)| v.to_string())
2974            .unwrap_or_default();
2975        let tag_keys: Vec<String> = query
2976            .iter()
2977            .filter(|(k, _)| k.starts_with("tagKeys"))
2978            .map(|(_, v)| v.clone())
2979            .collect();
2980
2981        let mut state = state.write().await;
2982        match state.untag_resource(&resource_arn, &tag_keys) {
2983            Ok(()) => wire::serialize_untag_resource_response(&wire::UntagResourceResponse {}),
2984            Err(e) => workspaces_web_error_response(&e),
2985        }
2986    }
2987}
2988
2989// ── Conversion helpers ──────────────────────────────────────────────
2990
2991fn portal_to_wire(portal: &types::Portal) -> wire::Portal {
2992    wire::Portal {
2993        portal_arn: Some(portal.portal_arn.clone()),
2994        portal_endpoint: Some(portal.portal_endpoint.clone()),
2995        display_name: Some(portal.display_name.clone()),
2996        portal_status: Some(portal.portal_status.clone()),
2997        browser_type: Some(portal.browser_type.clone()),
2998        renderer_type: Some(portal.renderer_type.clone()),
2999        creation_date: Some(portal.creation_date.timestamp() as f64),
3000        browser_settings_arn: portal.browser_settings_arn.clone(),
3001        network_settings_arn: portal.network_settings_arn.clone(),
3002        user_access_logging_settings_arn: portal.user_access_logging_settings_arn.clone(),
3003        user_settings_arn: portal.user_settings_arn.clone(),
3004        trust_store_arn: portal.trust_store_arn.clone(),
3005        ip_access_settings_arn: portal.ip_access_settings_arn.clone(),
3006        data_protection_settings_arn: portal.data_protection_settings_arn.clone(),
3007        session_logger_arn: portal.session_logger_arn.clone(),
3008        ..Default::default()
3009    }
3010}
3011
3012fn portal_to_summary(portal: &types::Portal) -> wire::PortalSummary {
3013    wire::PortalSummary {
3014        portal_arn: Some(portal.portal_arn.clone()),
3015        portal_endpoint: Some(portal.portal_endpoint.clone()),
3016        display_name: Some(portal.display_name.clone()),
3017        portal_status: Some(portal.portal_status.clone()),
3018        browser_type: Some(portal.browser_type.clone()),
3019        renderer_type: Some(portal.renderer_type.clone()),
3020        creation_date: Some(portal.creation_date.timestamp() as f64),
3021        browser_settings_arn: portal.browser_settings_arn.clone(),
3022        network_settings_arn: portal.network_settings_arn.clone(),
3023        user_access_logging_settings_arn: portal.user_access_logging_settings_arn.clone(),
3024        user_settings_arn: portal.user_settings_arn.clone(),
3025        trust_store_arn: portal.trust_store_arn.clone(),
3026        ip_access_settings_arn: portal.ip_access_settings_arn.clone(),
3027        data_protection_settings_arn: portal.data_protection_settings_arn.clone(),
3028        session_logger_arn: portal.session_logger_arn.clone(),
3029        ..Default::default()
3030    }
3031}
3032
3033fn ip_access_settings_to_wire(ias: &types::IpAccessSettings) -> wire::IpAccessSettings {
3034    wire::IpAccessSettings {
3035        ip_access_settings_arn: Some(ias.ip_access_settings_arn.clone()),
3036        display_name: ias.display_name.clone(),
3037        description: ias.description.clone(),
3038        ip_rules: Some(
3039            ias.ip_rules
3040                .iter()
3041                .map(|r| wire::IpRule {
3042                    ip_range: r.ip_range.clone(),
3043                    description: r.description.clone(),
3044                })
3045                .collect(),
3046        ),
3047        associated_portal_arns: Some(ias.associated_portal_arns.clone()),
3048        creation_date: Some(ias.creation_date.timestamp() as f64),
3049        ..Default::default()
3050    }
3051}
3052
3053// ── URI / query helpers ─────────────────────────────────────────────
3054
3055fn extract_path_and_query(uri: &str) -> (String, String) {
3056    let after_host = if let Some(idx) = uri.find("amazonaws.com") {
3057        &uri[idx + "amazonaws.com".len()..]
3058    } else {
3059        uri
3060    };
3061
3062    if let Some(q) = after_host.find('?') {
3063        (after_host[..q].to_string(), after_host[q + 1..].to_string())
3064    } else {
3065        (after_host.to_string(), String::new())
3066    }
3067}
3068
3069fn parse_query_string(qs: &str) -> HashMap<String, String> {
3070    let mut map = HashMap::new();
3071    if qs.is_empty() {
3072        return map;
3073    }
3074    for pair in qs.split('&') {
3075        if let Some((k, v)) = pair.split_once('=') {
3076            let key = percent_decode(k);
3077            let val = percent_decode(v);
3078            // For repeated params (like tagKeys), store with index suffix
3079            if let std::collections::hash_map::Entry::Vacant(e) = map.entry(key.clone()) {
3080                e.insert(val);
3081            } else {
3082                let mut i = 1;
3083                loop {
3084                    let indexed_key = format!("{key}_{i}");
3085                    if let std::collections::hash_map::Entry::Vacant(e) = map.entry(indexed_key) {
3086                        e.insert(val);
3087                        break;
3088                    }
3089                    i += 1;
3090                }
3091            }
3092        }
3093    }
3094    map
3095}
3096
3097/// Decode a resource ARN from path segments.
3098fn decode_arn_from_segments(segments: &[&str]) -> String {
3099    let joined = segments.join("/");
3100    percent_decode(&joined)
3101}
3102
3103fn percent_decode(s: &str) -> String {
3104    let mut result = String::with_capacity(s.len());
3105    let mut bytes = s.bytes();
3106    while let Some(b) = bytes.next() {
3107        match b {
3108            b'%' => {
3109                let hi = bytes.next().and_then(hex_val);
3110                let lo = bytes.next().and_then(hex_val);
3111                if let (Some(hi), Some(lo)) = (hi, lo) {
3112                    result.push((hi << 4 | lo) as char);
3113                }
3114            }
3115            b'+' => result.push(' '),
3116            _ => result.push(b as char),
3117        }
3118    }
3119    result
3120}
3121
3122fn hex_val(b: u8) -> Option<u8> {
3123    match b {
3124        b'0'..=b'9' => Some(b - b'0'),
3125        b'a'..=b'f' => Some(b - b'a' + 10),
3126        b'A'..=b'F' => Some(b - b'A' + 10),
3127        _ => None,
3128    }
3129}
3130
3131fn workspaces_web_error_response(err: &WorkspacesWebError) -> MockResponse {
3132    let (status, error_type) = match err {
3133        WorkspacesWebError::ResourceNotFound(_) => (404, "ResourceNotFoundException"),
3134        WorkspacesWebError::SessionNotFound(_) => (404, "ResourceNotFoundException"),
3135    };
3136    let body = json!({
3137        "Type": "User",
3138        "Message": err.to_string(),
3139    });
3140    let mut resp = MockResponse::rest_json(status, body.to_string());
3141    resp.headers
3142        .insert(X_AMZN_ERRORTYPE, error_type.parse().unwrap());
3143    resp
3144}
3145
3146fn rest_json_error(status: u16, code: &str, message: &str) -> MockResponse {
3147    let body = json!({
3148        "Type": "User",
3149        "Message": message,
3150    });
3151    let mut resp = MockResponse::rest_json(status, body.to_string());
3152    resp.headers.insert(X_AMZN_ERRORTYPE, code.parse().unwrap());
3153    resp
3154}