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, ®ion);
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 "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, ®ion).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 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 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}