Skip to main content

winterbaume_support/
handlers.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Arc;
4
5use serde_json::{Value, json};
6use winterbaume_core::{
7    BackendState, MockRequest, MockResponse, MockService, StateChangeNotifier, StatefulService,
8};
9
10use crate::state::{SupportError, SupportState};
11use crate::views::SupportStateView;
12use crate::wire;
13
14fn json_error_response(status: u16, error_type: &str, message: &str) -> MockResponse {
15    MockResponse::json(
16        status,
17        json!({"__type": error_type, "message": message}).to_string(),
18    )
19}
20
21pub struct SupportService {
22    pub(crate) state: Arc<BackendState<SupportState>>,
23    pub(crate) notifier: StateChangeNotifier<SupportStateView>,
24}
25
26impl SupportService {
27    pub fn new() -> Self {
28        Self {
29            state: Arc::new(BackendState::new()),
30            notifier: StateChangeNotifier::new(),
31        }
32    }
33}
34
35impl Default for SupportService {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl MockService for SupportService {
42    fn service_name(&self) -> &str {
43        "support"
44    }
45
46    fn url_patterns(&self) -> Vec<&str> {
47        vec![
48            r"https?://support\..*\.amazonaws\.com",
49            r"https?://support\.amazonaws\.com",
50        ]
51    }
52
53    fn handle(
54        &self,
55        request: MockRequest,
56    ) -> Pin<Box<dyn Future<Output = MockResponse> + Send + '_>> {
57        Box::pin(async move { self.dispatch(request).await })
58    }
59}
60
61impl SupportService {
62    async fn dispatch(&self, request: MockRequest) -> MockResponse {
63        let region = winterbaume_core::auth::extract_region_from_uri(&request.uri);
64        let account_id = winterbaume_core::default_account_id();
65
66        let action = request
67            .headers
68            .get("x-amz-target")
69            .and_then(|v| v.to_str().ok())
70            .and_then(|v| v.split('.').next_back())
71            .map(|s| s.to_string());
72
73        let action = match action {
74            Some(a) => a,
75            None => {
76                return json_error_response(400, "MissingAction", "Missing X-Amz-Target header");
77            }
78        };
79
80        if serde_json::from_slice::<Value>(&request.body).is_err() {
81            return json_error_response(400, "SerializationException", "Invalid JSON body");
82        }
83        let body_bytes: &[u8] = &request.body;
84
85        let state = self.state.get(account_id, &region);
86
87        let response = match action.as_str() {
88            "CreateCase" => self.handle_create_case(&state, body_bytes).await,
89            "DescribeCases" => self.handle_describe_cases(&state, body_bytes).await,
90            "ResolveCase" => self.handle_resolve_case(&state, body_bytes).await,
91            "DescribeServices" => self.handle_describe_services(&state, body_bytes).await,
92            "DescribeTrustedAdvisorChecks" => {
93                self.handle_describe_trusted_advisor_checks(body_bytes)
94                    .await
95            }
96            "RefreshTrustedAdvisorCheck" => {
97                self.handle_refresh_trusted_advisor_check(&state, body_bytes)
98                    .await
99            }
100            // --- Unimplemented operations (auto-generated stubs) ---
101            "AddAttachmentsToSet" => json_error_response(
102                501,
103                "NotImplementedError",
104                "AddAttachmentsToSet is not yet implemented in winterbaume-support",
105            ),
106            "AddCommunicationToCase" => json_error_response(
107                501,
108                "NotImplementedError",
109                "AddCommunicationToCase is not yet implemented in winterbaume-support",
110            ),
111            "DescribeAttachment" => json_error_response(
112                501,
113                "NotImplementedError",
114                "DescribeAttachment is not yet implemented in winterbaume-support",
115            ),
116            "DescribeCommunications" => json_error_response(
117                501,
118                "NotImplementedError",
119                "DescribeCommunications is not yet implemented in winterbaume-support",
120            ),
121            "DescribeCreateCaseOptions" => json_error_response(
122                501,
123                "NotImplementedError",
124                "DescribeCreateCaseOptions is not yet implemented in winterbaume-support",
125            ),
126            "DescribeSeverityLevels" => json_error_response(
127                501,
128                "NotImplementedError",
129                "DescribeSeverityLevels is not yet implemented in winterbaume-support",
130            ),
131            "DescribeSupportedLanguages" => json_error_response(
132                501,
133                "NotImplementedError",
134                "DescribeSupportedLanguages is not yet implemented in winterbaume-support",
135            ),
136            "DescribeTrustedAdvisorCheckRefreshStatuses" => json_error_response(
137                501,
138                "NotImplementedError",
139                "DescribeTrustedAdvisorCheckRefreshStatuses is not yet implemented in winterbaume-support",
140            ),
141            "DescribeTrustedAdvisorCheckResult" => json_error_response(
142                501,
143                "NotImplementedError",
144                "DescribeTrustedAdvisorCheckResult is not yet implemented in winterbaume-support",
145            ),
146            "DescribeTrustedAdvisorCheckSummaries" => json_error_response(
147                501,
148                "NotImplementedError",
149                "DescribeTrustedAdvisorCheckSummaries is not yet implemented in winterbaume-support",
150            ),
151            _ => json_error_response(400, "InvalidAction", &format!("Unknown operation {action}")),
152        };
153        if response.status / 100 == 2 {
154            self.notify_state_changed(account_id, &region).await;
155        }
156        response
157    }
158
159    async fn handle_create_case(
160        &self,
161        state: &Arc<tokio::sync::RwLock<SupportState>>,
162        body: &[u8],
163    ) -> MockResponse {
164        let input = match wire::deserialize_create_case_request(body) {
165            Ok(v) => v,
166            Err(e) => return json_error_response(400, "ValidationException", &e),
167        };
168        if input.subject.is_empty() {
169            return json_error_response(400, "InvalidParameterValue", "subject is required");
170        }
171        if input.communication_body.is_empty() {
172            return json_error_response(
173                400,
174                "InvalidParameterValue",
175                "communicationBody is required",
176            );
177        }
178        let subject = input.subject.as_str();
179        let communication_body = input.communication_body.as_str();
180        let service_code = input.service_code.as_deref();
181        let severity_code = input.severity_code.as_deref();
182        let category_code = input.category_code.as_deref();
183        let language = input.language.as_deref();
184        let cc_email_addresses: Vec<String> = input.cc_email_addresses.unwrap_or_default();
185
186        let mut state = state.write().await;
187        match state.create_case(
188            subject,
189            communication_body,
190            service_code,
191            severity_code,
192            category_code,
193            cc_email_addresses,
194            language,
195        ) {
196            Ok(case) => wire::serialize_create_case_response(&wire::CreateCaseResponse {
197                case_id: Some(case.case_id.clone()),
198            }),
199            Err(e) => support_error_response(&e),
200        }
201    }
202
203    async fn handle_describe_cases(
204        &self,
205        state: &Arc<tokio::sync::RwLock<SupportState>>,
206        body: &[u8],
207    ) -> MockResponse {
208        let input = match wire::deserialize_describe_cases_request(body) {
209            Ok(v) => v,
210            Err(e) => return json_error_response(400, "ValidationException", &e),
211        };
212        let case_id_list = input.case_id_list;
213        let include_resolved = input.include_resolved_cases.unwrap_or(false);
214
215        let state = state.read().await;
216        let cases = state.describe_cases(case_id_list.as_deref(), include_resolved);
217
218        let entries: Vec<wire::CaseDetails> = cases
219            .iter()
220            .map(|c| wire::CaseDetails {
221                case_id: Some(c.case_id.clone()),
222                display_id: Some(c.display_id.clone()),
223                subject: Some(c.subject.clone()),
224                status: Some(c.status.clone()),
225                service_code: Some(c.service_code.clone()),
226                category_code: Some(c.category_code.clone()),
227                severity_code: Some(c.severity_code.clone()),
228                submitted_by: Some(c.submitted_by.clone()),
229                time_created: Some(c.time_created.clone()),
230                cc_email_addresses: Some(c.cc_email_addresses.clone()),
231                language: Some(c.language.clone()),
232                ..Default::default()
233            })
234            .collect();
235
236        wire::serialize_describe_cases_response(&wire::DescribeCasesResponse {
237            cases: Some(entries),
238            ..Default::default()
239        })
240    }
241
242    async fn handle_resolve_case(
243        &self,
244        state: &Arc<tokio::sync::RwLock<SupportState>>,
245        body: &[u8],
246    ) -> MockResponse {
247        let input = match wire::deserialize_resolve_case_request(body) {
248            Ok(v) => v,
249            Err(e) => return json_error_response(400, "ValidationException", &e),
250        };
251        let case_id = input.case_id.as_deref();
252
253        let mut state = state.write().await;
254        match state.resolve_case(case_id) {
255            Ok((initial, final_status)) => {
256                wire::serialize_resolve_case_response(&wire::ResolveCaseResponse {
257                    initial_case_status: Some(initial),
258                    final_case_status: Some(final_status),
259                })
260            }
261            Err(e) => support_error_response(&e),
262        }
263    }
264
265    async fn handle_describe_services(
266        &self,
267        state: &Arc<tokio::sync::RwLock<SupportState>>,
268        body: &[u8],
269    ) -> MockResponse {
270        let input = match wire::deserialize_describe_services_request(body) {
271            Ok(v) => v,
272            Err(e) => return json_error_response(400, "ValidationException", &e),
273        };
274        let service_code_list = input.service_code_list;
275
276        let state = state.read().await;
277        let services = state.describe_services(service_code_list.as_deref());
278
279        let entries: Vec<wire::Service> = services
280            .iter()
281            .map(|s| wire::Service {
282                code: Some(s.code.clone()),
283                name: Some(s.name.clone()),
284                categories: Some(
285                    s.categories
286                        .iter()
287                        .map(|c| wire::Category {
288                            code: Some(c.code.clone()),
289                            name: Some(c.name.clone()),
290                        })
291                        .collect(),
292                ),
293            })
294            .collect();
295
296        wire::serialize_describe_services_response(&wire::DescribeServicesResponse {
297            services: Some(entries),
298        })
299    }
300
301    // STUB[no-telemetry]: DescribeTrustedAdvisorChecks returns a static set of three well-known
302    //   check IDs. Real Trusted Advisor check metadata is account-level and driven by real
303    //   infrastructure telemetry; the mock has no such data and returns a fixed catalogue.
304    async fn handle_describe_trusted_advisor_checks(&self, body: &[u8]) -> MockResponse {
305        let input = match wire::deserialize_describe_trusted_advisor_checks_request(body) {
306            Ok(v) => v,
307            Err(e) => return json_error_response(400, "ValidationException", &e),
308        };
309        let _language = input.language;
310
311        // Return a small set of mock Trusted Advisor checks
312        let checks = vec![
313            wire::TrustedAdvisorCheckDescription {
314                id: Some("1iG5NDGVre".to_string()),
315                name: Some("Security Groups - Specific Ports Unrestricted".to_string()),
316                description: Some("Checks security groups for rules that allow unrestricted access to specific ports.".to_string()),
317                category: Some("security".to_string()),
318                metadata: Some(vec!["Region", "Security Group Name", "Security Group ID", "Protocol", "Port", "Status", "IP Address"]
319                    .into_iter().map(String::from).collect()),
320            },
321            wire::TrustedAdvisorCheckDescription {
322                id: Some("HCP4007jGY".to_string()),
323                name: Some("S3 Bucket Permissions".to_string()),
324                description: Some("Checks buckets in Amazon S3 that have open access permissions.".to_string()),
325                category: Some("security".to_string()),
326                metadata: Some(vec!["Region", "Bucket Name", "ACL Allows List", "ACL Allows Upload/Delete", "Status"]
327                    .into_iter().map(String::from).collect()),
328            },
329            wire::TrustedAdvisorCheckDescription {
330                id: Some("Qch7DwouX1".to_string()),
331                name: Some("Low Utilization Amazon EC2 Instances".to_string()),
332                description: Some("Checks the Amazon Elastic Compute Cloud (Amazon EC2) instances that were running at any time during the last 14 days.".to_string()),
333                category: Some("cost_optimizing".to_string()),
334                metadata: Some(vec!["Region", "Instance ID", "Instance Name", "Instance Type", "Estimated Monthly Savings", "Day 1-14 CPU Utilization"]
335                    .into_iter().map(String::from).collect()),
336            },
337        ];
338
339        wire::serialize_describe_trusted_advisor_checks_response(
340            &wire::DescribeTrustedAdvisorChecksResponse {
341                checks: Some(checks),
342            },
343        )
344    }
345
346    async fn handle_refresh_trusted_advisor_check(
347        &self,
348        state: &Arc<tokio::sync::RwLock<SupportState>>,
349        body: &[u8],
350    ) -> MockResponse {
351        let input = match wire::deserialize_refresh_trusted_advisor_check_request(body) {
352            Ok(v) => v,
353            Err(e) => return json_error_response(400, "ValidationException", &e),
354        };
355        if input.check_id.is_empty() {
356            return json_error_response(400, "InvalidParameterValue", "checkId is required");
357        }
358        let check_id = input.check_id.as_str();
359
360        let mut state = state.write().await;
361        let status = state.next_refresh_status(check_id).to_string();
362
363        wire::serialize_refresh_trusted_advisor_check_response(
364            &wire::RefreshTrustedAdvisorCheckResponse {
365                status: Some(wire::TrustedAdvisorCheckRefreshStatus {
366                    check_id: Some(check_id.to_string()),
367                    status: Some(status),
368                    millis_until_next_refreshable: Some(3600000),
369                }),
370            },
371        )
372    }
373}
374
375fn support_error_response(err: &SupportError) -> MockResponse {
376    match err {
377        SupportError::CaseIdNotFound => MockResponse::json(
378            400,
379            json!({"__type": "CaseIdNotFound", "message": err.to_string()}).to_string(),
380        ),
381    }
382}