1use std::sync::Arc;
20
21use async_trait::async_trait;
22use http::StatusCode;
23use serde_json::{json, Value};
24use tokio::sync::Mutex as AsyncMutex;
25
26use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
27use fakecloud_persistence::SnapshotStore;
28
29use crate::catalog;
30use crate::shared::{
31 current_year, display_id, iso_now, new_attachment_id, new_attachment_set_id, new_case_id,
32 str_member,
33};
34use crate::state::{SharedSupportState, SupportData};
35
36pub const SUPPORT_ACTIONS: &[&str] = &[
38 "AddAttachmentsToSet",
39 "AddCommunicationToCase",
40 "CreateCase",
41 "DescribeAttachment",
42 "DescribeCases",
43 "DescribeCommunications",
44 "DescribeCreateCaseOptions",
45 "DescribeServices",
46 "DescribeSeverityLevels",
47 "DescribeSupportedLanguages",
48 "DescribeTrustedAdvisorCheckRefreshStatuses",
49 "DescribeTrustedAdvisorCheckResult",
50 "DescribeTrustedAdvisorCheckSummaries",
51 "DescribeTrustedAdvisorChecks",
52 "RefreshTrustedAdvisorCheck",
53 "ResolveCase",
54];
55
56fn is_mutating_action(action: &str) -> bool {
63 !action.starts_with("Describe")
64}
65
66pub struct SupportService {
67 state: SharedSupportState,
68 snapshot_store: Option<Arc<dyn SnapshotStore>>,
69 snapshot_lock: Arc<AsyncMutex<()>>,
70}
71
72impl SupportService {
73 pub fn new(state: SharedSupportState) -> Self {
74 Self {
75 state,
76 snapshot_store: None,
77 snapshot_lock: Arc::new(AsyncMutex::new(())),
78 }
79 }
80
81 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
82 self.snapshot_store = Some(store);
83 self
84 }
85
86 async fn save(&self) {
87 crate::persistence::save_snapshot(
88 &self.state,
89 self.snapshot_store.clone(),
90 &self.snapshot_lock,
91 )
92 .await;
93 }
94
95 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
97 let store = self.snapshot_store.clone()?;
98 let state = self.state.clone();
99 let lock = self.snapshot_lock.clone();
100 Some(Arc::new(move || {
101 let state = state.clone();
102 let store = store.clone();
103 let lock = lock.clone();
104 Box::pin(async move {
105 crate::persistence::save_snapshot(&state, Some(store), &lock).await;
106 })
107 }))
108 }
109
110 fn with_account_mut<R>(&self, req: &AwsRequest, f: impl FnOnce(&mut SupportData) -> R) -> R {
112 let mut guard = self.state.write();
113 let acct = guard.get_or_create(&req.account_id);
114 f(acct)
115 }
116}
117
118#[async_trait]
119impl AwsService for SupportService {
120 fn service_name(&self) -> &str {
121 "support"
122 }
123
124 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
125 let action = req.action.clone();
126 if let Err(msg) = crate::validate::validate_input(&action, &req.json_body()) {
127 return Err(validation_error(msg));
128 }
129 let result = self.dispatch(&action, &req);
130 if is_mutating_action(&action)
131 && matches!(result.as_ref(), Ok(resp) if resp.status.is_success())
132 {
133 self.save().await;
134 }
135 result
136 }
137
138 fn supported_actions(&self) -> &[&str] {
139 SUPPORT_ACTIONS
140 }
141}
142
143impl SupportService {
144 fn dispatch(&self, action: &str, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
145 let body = req.json_body();
146 match action {
147 "CreateCase" => self.create_case(req, &body),
149 "DescribeCases" => self.describe_cases(req, &body),
150 "DescribeCommunications" => self.describe_communications(req, &body),
151 "AddCommunicationToCase" => self.add_communication_to_case(req, &body),
152 "ResolveCase" => self.resolve_case(req, &body),
153 "AddAttachmentsToSet" => self.add_attachments_to_set(req, &body),
154 "DescribeAttachment" => self.describe_attachment(req, &body),
155 "DescribeTrustedAdvisorChecks" => self.describe_ta_checks(&body),
157 "DescribeTrustedAdvisorCheckResult" => self.describe_ta_check_result(&body),
158 "DescribeTrustedAdvisorCheckSummaries" => self.describe_ta_check_summaries(&body),
159 "DescribeTrustedAdvisorCheckRefreshStatuses" => {
160 self.describe_ta_refresh_statuses(req, &body)
161 }
162 "RefreshTrustedAdvisorCheck" => self.refresh_ta_check(req, &body),
163 "DescribeSeverityLevels" => self.describe_severity_levels(&body),
165 "DescribeServices" => self.describe_services(&body),
166 "DescribeCreateCaseOptions" => self.describe_create_case_options(&body),
167 "DescribeSupportedLanguages" => self.describe_supported_languages(&body),
168 _ => Err(AwsServiceError::action_not_implemented(
169 self.service_name(),
170 action,
171 )),
172 }
173 }
174
175 fn create_case(&self, req: &AwsRequest, body: &Value) -> Result<AwsResponse, AwsServiceError> {
178 let account = req.account_id.clone();
179 let subject = str_member(body, "subject").unwrap_or_default().to_string();
180 let comm_body = str_member(body, "communicationBody")
181 .unwrap_or_default()
182 .to_string();
183 let service_code = str_member(body, "serviceCode")
184 .unwrap_or("general-info-and-getting-started")
185 .to_string();
186 let severity_code = str_member(body, "severityCode")
187 .unwrap_or("normal")
188 .to_string();
189 let category_code = str_member(body, "categoryCode")
190 .unwrap_or("other")
191 .to_string();
192 let language = str_member(body, "language").unwrap_or("en").to_string();
193 let cc = body
194 .get("ccEmailAddresses")
195 .cloned()
196 .unwrap_or_else(|| json!([]));
197 let attachment_set_id = str_member(body, "attachmentSetId").map(str::to_string);
198 let submitted_by = format!("arn:aws:iam::{account}:root");
199 let now = iso_now();
200
201 self.with_account_mut(req, |d| {
202 let attachment_set = match &attachment_set_id {
204 Some(id) => {
205 let set = d
206 .attachment_sets
207 .get(id)
208 .ok_or_else(|| attachment_set_id_not_found(id))?;
209 attachment_set_summary(d, set)
210 }
211 None => json!([]),
212 };
213
214 let case_id = new_case_id(&account, current_year());
215 let disp = display_id(&case_id);
216 let case = json!({
217 "caseId": case_id,
218 "displayId": disp,
219 "subject": subject,
220 "status": "opened",
221 "serviceCode": service_code,
222 "categoryCode": category_code,
223 "severityCode": severity_code,
224 "submittedBy": submitted_by,
225 "timeCreated": now,
226 "ccEmailAddresses": cc,
227 "language": language,
228 });
229 let comm = json!({
230 "caseId": case_id,
231 "body": comm_body,
232 "submittedBy": submitted_by,
233 "timeCreated": now,
234 "attachmentSet": attachment_set,
235 });
236 d.cases.insert(case_id.clone(), case);
237 d.communications.insert(case_id.clone(), vec![comm]);
238 Ok(ok(json!({ "caseId": case_id })))
239 })
240 }
241
242 fn describe_cases(
243 &self,
244 req: &AwsRequest,
245 body: &Value,
246 ) -> Result<AwsResponse, AwsServiceError> {
247 let case_id_list: Option<Vec<String>> =
248 body.get("caseIdList").and_then(Value::as_array).map(|a| {
249 a.iter()
250 .filter_map(|v| v.as_str().map(String::from))
251 .collect()
252 });
253 let display_id_filter = str_member(body, "displayId").map(str::to_string);
254 let after = str_member(body, "afterTime").map(str::to_string);
255 let before = str_member(body, "beforeTime").map(str::to_string);
256 let include_resolved = body
257 .get("includeResolvedCases")
258 .and_then(Value::as_bool)
259 .unwrap_or(false);
260 let include_comms = body
261 .get("includeCommunications")
262 .and_then(Value::as_bool)
263 .unwrap_or(true);
264 let max = body
265 .get("maxResults")
266 .and_then(Value::as_u64)
267 .map(|n| n as usize);
268 let start = decode_token(str_member(body, "nextToken"));
269
270 self.with_account_mut(req, |d| {
271 let mut selected: Vec<Value> = Vec::new();
273 if let Some(ids) = &case_id_list {
274 for id in ids {
275 let case = d.cases.get(id).ok_or_else(|| case_id_not_found(id))?;
276 selected.push(case.clone());
277 }
278 } else {
279 for case in d.cases.values() {
280 selected.push(case.clone());
281 }
282 }
283
284 selected.retain(|c| {
286 let status = c.get("status").and_then(Value::as_str).unwrap_or("");
287 if !include_resolved && status == "resolved" {
288 return false;
289 }
290 if let Some(disp) = &display_id_filter {
291 if c.get("displayId").and_then(Value::as_str) != Some(disp.as_str()) {
292 return false;
293 }
294 }
295 let created = c.get("timeCreated").and_then(Value::as_str).unwrap_or("");
296 if let Some(a) = &after {
297 if created < a.as_str() {
298 return false;
299 }
300 }
301 if let Some(b) = &before {
302 if created > b.as_str() {
303 return false;
304 }
305 }
306 true
307 });
308
309 let (page, next) = paginate(&selected, start, max);
311 let cases: Vec<Value> = page
312 .iter()
313 .map(|c| {
314 let mut c = c.clone();
315 let cid = c
316 .get("caseId")
317 .and_then(Value::as_str)
318 .unwrap_or("")
319 .to_string();
320 if include_comms {
321 let comms = d.communications.get(&cid).cloned().unwrap_or_default();
322 c.as_object_mut().unwrap().insert(
323 "recentCommunications".to_string(),
324 json!({ "communications": comms }),
325 );
326 }
327 c
328 })
329 .collect();
330
331 let mut out = json!({ "cases": cases });
332 if let Some(tok) = next {
333 out.as_object_mut()
334 .unwrap()
335 .insert("nextToken".to_string(), json!(tok));
336 }
337 Ok(ok(out))
338 })
339 }
340
341 fn describe_communications(
342 &self,
343 req: &AwsRequest,
344 body: &Value,
345 ) -> Result<AwsResponse, AwsServiceError> {
346 let case_id = str_member(body, "caseId").unwrap_or_default().to_string();
347 let after = str_member(body, "afterTime").map(str::to_string);
348 let before = str_member(body, "beforeTime").map(str::to_string);
349 let max = body
350 .get("maxResults")
351 .and_then(Value::as_u64)
352 .map(|n| n as usize);
353 let start = decode_token(str_member(body, "nextToken"));
354
355 self.with_account_mut(req, |d| {
356 if !d.cases.contains_key(&case_id) {
357 return Err(case_id_not_found(&case_id));
358 }
359 let all = d.communications.get(&case_id).cloned().unwrap_or_default();
360 let filtered: Vec<Value> = all
361 .into_iter()
362 .filter(|c| {
363 let created = c.get("timeCreated").and_then(Value::as_str).unwrap_or("");
364 after.as_deref().map(|a| created >= a).unwrap_or(true)
365 && before.as_deref().map(|b| created <= b).unwrap_or(true)
366 })
367 .collect();
368 let (page, next) = paginate(&filtered, start, max);
369 let mut out = json!({ "communications": page });
370 if let Some(tok) = next {
371 out.as_object_mut()
372 .unwrap()
373 .insert("nextToken".to_string(), json!(tok));
374 }
375 Ok(ok(out))
376 })
377 }
378
379 fn add_communication_to_case(
380 &self,
381 req: &AwsRequest,
382 body: &Value,
383 ) -> Result<AwsResponse, AwsServiceError> {
384 let account = req.account_id.clone();
385 let case_id = str_member(body, "caseId").unwrap_or_default().to_string();
386 let comm_body = str_member(body, "communicationBody")
387 .unwrap_or_default()
388 .to_string();
389 let attachment_set_id = str_member(body, "attachmentSetId").map(str::to_string);
390 let submitted_by = format!("arn:aws:iam::{account}:root");
391 let now = iso_now();
392
393 self.with_account_mut(req, |d| {
394 if !d.cases.contains_key(&case_id) {
395 return Err(case_id_not_found(&case_id));
396 }
397 let attachment_set = match &attachment_set_id {
398 Some(id) => {
399 let set = d
400 .attachment_sets
401 .get(id)
402 .ok_or_else(|| attachment_set_id_not_found(id))?;
403 attachment_set_summary(d, set)
404 }
405 None => json!([]),
406 };
407 let comm = json!({
408 "caseId": case_id,
409 "body": comm_body,
410 "submittedBy": submitted_by,
411 "timeCreated": now,
412 "attachmentSet": attachment_set,
413 });
414 d.communications
415 .entry(case_id.clone())
416 .or_default()
417 .push(comm);
418 Ok(ok(json!({ "result": true })))
419 })
420 }
421
422 fn resolve_case(&self, req: &AwsRequest, body: &Value) -> Result<AwsResponse, AwsServiceError> {
423 let case_id = str_member(body, "caseId").unwrap_or_default().to_string();
424 self.with_account_mut(req, |d| {
425 let case = d
426 .cases
427 .get_mut(&case_id)
428 .ok_or_else(|| case_id_not_found(&case_id))?;
429 let initial = case
430 .get("status")
431 .and_then(Value::as_str)
432 .unwrap_or("opened")
433 .to_string();
434 case.as_object_mut()
435 .unwrap()
436 .insert("status".to_string(), json!("resolved"));
437 Ok(ok(json!({
438 "initialCaseStatus": initial,
439 "finalCaseStatus": "resolved",
440 })))
441 })
442 }
443
444 fn add_attachments_to_set(
445 &self,
446 req: &AwsRequest,
447 body: &Value,
448 ) -> Result<AwsResponse, AwsServiceError> {
449 let requested_set = str_member(body, "attachmentSetId").map(str::to_string);
450 let attachments = body
451 .get("attachments")
452 .and_then(Value::as_array)
453 .cloned()
454 .unwrap_or_default();
455 let expiry = (chrono::Utc::now() + chrono::Duration::hours(1))
457 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
458 .to_string();
459
460 self.with_account_mut(req, |d| {
461 let set_id = match &requested_set {
462 Some(id) => {
463 if !d.attachment_sets.contains_key(id) {
464 return Err(attachment_set_id_not_found(id));
465 }
466 id.clone()
467 }
468 None => new_attachment_set_id(),
469 };
470
471 let mut ids: Vec<String> = d
472 .attachment_sets
473 .get(&set_id)
474 .and_then(|s| s.get("attachmentIds"))
475 .and_then(Value::as_array)
476 .map(|a| {
477 a.iter()
478 .filter_map(|v| v.as_str().map(String::from))
479 .collect()
480 })
481 .unwrap_or_default();
482
483 for att in &attachments {
484 let att_id = new_attachment_id();
485 let file_name = att.get("fileName").cloned().unwrap_or(json!(""));
486 let data = att.get("data").cloned().unwrap_or(json!(""));
487 d.attachments.insert(
488 att_id.clone(),
489 json!({ "fileName": file_name, "data": data }),
490 );
491 ids.push(att_id);
492 }
493
494 d.attachment_sets.insert(
495 set_id.clone(),
496 json!({ "attachmentIds": ids, "expiryTime": expiry }),
497 );
498 Ok(ok(json!({
499 "attachmentSetId": set_id,
500 "expiryTime": expiry,
501 })))
502 })
503 }
504
505 fn describe_attachment(
506 &self,
507 req: &AwsRequest,
508 body: &Value,
509 ) -> Result<AwsResponse, AwsServiceError> {
510 let attachment_id = str_member(body, "attachmentId")
511 .unwrap_or_default()
512 .to_string();
513 self.with_account_mut(req, |d| {
514 let att = d
515 .attachments
516 .get(&attachment_id)
517 .ok_or_else(|| attachment_id_not_found(&attachment_id))?;
518 Ok(ok(json!({ "attachment": att })))
519 })
520 }
521
522 fn describe_ta_checks(&self, _body: &Value) -> Result<AwsResponse, AwsServiceError> {
525 Ok(ok(json!({ "checks": catalog::ta_checks() })))
526 }
527
528 fn describe_ta_check_result(&self, body: &Value) -> Result<AwsResponse, AwsServiceError> {
529 let check_id = str_member(body, "checkId").unwrap_or_default().to_string();
530 Ok(ok(json!({
531 "result": ta_check_result(&check_id),
532 })))
533 }
534
535 fn describe_ta_check_summaries(&self, body: &Value) -> Result<AwsResponse, AwsServiceError> {
536 let ids = string_list(body, "checkIds");
537 let summaries: Vec<Value> = ids.iter().map(|id| ta_check_summary(id)).collect();
538 Ok(ok(json!({ "summaries": summaries })))
539 }
540
541 fn describe_ta_refresh_statuses(
542 &self,
543 req: &AwsRequest,
544 body: &Value,
545 ) -> Result<AwsResponse, AwsServiceError> {
546 let ids = string_list(body, "checkIds");
547 let statuses = self.with_account_mut(req, |d| {
548 ids.iter()
549 .map(|id| {
550 let status = d.advance_refresh(id);
551 json!({
552 "checkId": id,
553 "status": status,
554 "millisUntilNextRefreshable": refresh_interval_ms(&status),
555 })
556 })
557 .collect::<Vec<Value>>()
558 });
559 Ok(ok(json!({ "statuses": statuses })))
560 }
561
562 fn refresh_ta_check(
563 &self,
564 req: &AwsRequest,
565 body: &Value,
566 ) -> Result<AwsResponse, AwsServiceError> {
567 let check_id = str_member(body, "checkId").unwrap_or_default().to_string();
568 self.with_account_mut(req, |d| {
569 d.ta_refresh
570 .insert(check_id.clone(), "enqueued".to_string());
571 });
572 Ok(ok(json!({
573 "status": {
574 "checkId": check_id,
575 "status": "enqueued",
576 "millisUntilNextRefreshable": refresh_interval_ms("enqueued"),
577 }
578 })))
579 }
580
581 fn describe_severity_levels(&self, _body: &Value) -> Result<AwsResponse, AwsServiceError> {
584 Ok(ok(json!({ "severityLevels": catalog::severity_levels() })))
585 }
586
587 fn describe_services(&self, body: &Value) -> Result<AwsResponse, AwsServiceError> {
588 let filter = body
589 .get("serviceCodeList")
590 .and_then(Value::as_array)
591 .map(|a| {
592 a.iter()
593 .filter_map(|v| v.as_str().map(String::from))
594 .collect::<Vec<_>>()
595 });
596 Ok(ok(json!({
597 "services": catalog::services(filter.as_deref()),
598 })))
599 }
600
601 fn describe_create_case_options(&self, _body: &Value) -> Result<AwsResponse, AwsServiceError> {
602 Ok(ok(json!({
603 "languageAvailability": "AVAILABLE",
604 "communicationTypes": [
605 { "type": "web", "supportedHours": [], "datesWithoutSupport": [] }
606 ],
607 })))
608 }
609
610 fn describe_supported_languages(&self, _body: &Value) -> Result<AwsResponse, AwsServiceError> {
611 Ok(ok(json!({
612 "supportedLanguages": [
613 { "code": "en", "language": "English", "display": "English" },
614 { "code": "ja", "language": "Japanese", "display": "日本語" }
615 ],
616 })))
617 }
618}
619
620fn ta_check_result(check_id: &str) -> Value {
623 json!({
624 "checkId": check_id,
625 "timestamp": iso_now(),
626 "status": "ok",
627 "resourcesSummary": zero_resources_summary(),
628 "categorySpecificSummary": category_specific_summary(check_id),
629 "flaggedResources": [],
630 })
631}
632
633fn ta_check_summary(check_id: &str) -> Value {
634 json!({
635 "checkId": check_id,
636 "timestamp": iso_now(),
637 "status": "ok",
638 "hasFlaggedResources": false,
639 "resourcesSummary": zero_resources_summary(),
640 "categorySpecificSummary": category_specific_summary(check_id),
641 })
642}
643
644fn zero_resources_summary() -> Value {
645 json!({
646 "resourcesProcessed": 0,
647 "resourcesFlagged": 0,
648 "resourcesIgnored": 0,
649 "resourcesSuppressed": 0,
650 })
651}
652
653fn category_specific_summary(check_id: &str) -> Value {
654 let _ = catalog::check_category(check_id);
657 json!({
658 "costOptimizing": {
659 "estimatedMonthlySavings": 0.0,
660 "estimatedPercentMonthlySavings": 0.0,
661 }
662 })
663}
664
665fn refresh_interval_ms(status: &str) -> i64 {
668 match status {
669 "success" | "none" => 3_600_000,
670 _ => 0,
671 }
672}
673
674fn decode_token(token: Option<&str>) -> usize {
678 token.and_then(|t| t.parse::<usize>().ok()).unwrap_or(0)
679}
680
681fn paginate(items: &[Value], start: usize, max: Option<usize>) -> (Vec<Value>, Option<String>) {
683 let start = start.min(items.len());
684 let remaining = &items[start..];
685 match max {
686 Some(m) if m < remaining.len() => {
687 let page = remaining[..m].to_vec();
688 (page, Some((start + m).to_string()))
689 }
690 _ => (remaining.to_vec(), None),
691 }
692}
693
694fn attachment_set_summary(d: &SupportData, set: &Value) -> Value {
697 let ids = set
698 .get("attachmentIds")
699 .and_then(Value::as_array)
700 .cloned()
701 .unwrap_or_default();
702 let details: Vec<Value> = ids
703 .iter()
704 .filter_map(|v| v.as_str())
705 .map(|id| {
706 let file_name = d
707 .attachments
708 .get(id)
709 .and_then(|a| a.get("fileName"))
710 .cloned()
711 .unwrap_or(json!(""));
712 json!({ "attachmentId": id, "fileName": file_name })
713 })
714 .collect();
715 json!(details)
716}
717
718fn string_list(body: &Value, name: &str) -> Vec<String> {
719 body.get(name)
720 .and_then(Value::as_array)
721 .map(|a| {
722 a.iter()
723 .filter_map(|v| v.as_str().map(String::from))
724 .collect()
725 })
726 .unwrap_or_default()
727}
728
729fn validation_error(msg: impl Into<String>) -> AwsServiceError {
732 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "ValidationException", msg.into())
733}
734
735fn case_id_not_found(id: &str) -> AwsServiceError {
736 AwsServiceError::aws_error(
737 StatusCode::BAD_REQUEST,
738 "CaseIdNotFound",
739 format!("Case {id} was not found."),
740 )
741}
742
743fn attachment_id_not_found(id: &str) -> AwsServiceError {
744 AwsServiceError::aws_error(
745 StatusCode::BAD_REQUEST,
746 "AttachmentIdNotFound",
747 format!("Attachment {id} was not found."),
748 )
749}
750
751fn attachment_set_id_not_found(id: &str) -> AwsServiceError {
752 AwsServiceError::aws_error(
753 StatusCode::BAD_REQUEST,
754 "AttachmentSetIdNotFound",
755 format!("Attachment set {id} was not found."),
756 )
757}
758
759fn ok(value: Value) -> AwsResponse {
760 AwsResponse::ok_json(value)
761}
762
763#[cfg(test)]
764mod tests {
765 use super::*;
766 use fakecloud_core::multi_account::MultiAccountState;
767 use parking_lot::RwLock;
768
769 fn service() -> SupportService {
770 SupportService::new(Arc::new(RwLock::new(MultiAccountState::new(
771 "000000000000",
772 "us-east-1",
773 "",
774 ))))
775 }
776
777 fn req(action: &str, body: Value) -> AwsRequest {
778 AwsRequest {
779 service: "support".to_string(),
780 action: action.to_string(),
781 region: "us-east-1".to_string(),
782 account_id: "000000000000".to_string(),
783 request_id: "test".to_string(),
784 headers: http::HeaderMap::new(),
785 query_params: std::collections::HashMap::new(),
786 body: bytes::Bytes::from(serde_json::to_vec(&body).unwrap()),
787 body_stream: parking_lot::Mutex::new(None),
788 path_segments: Vec::new(),
789 raw_path: String::new(),
790 raw_query: String::new(),
791 method: http::Method::POST,
792 is_query_protocol: false,
793 access_key_id: None,
794 principal: None,
795 }
796 }
797
798 fn body_of(resp: &AwsResponse) -> Value {
799 serde_json::from_slice(resp.body.expect_bytes()).unwrap()
800 }
801
802 fn expect_err(r: Result<AwsResponse, AwsServiceError>) -> AwsServiceError {
805 match r {
806 Ok(_) => panic!("expected an error response"),
807 Err(e) => e,
808 }
809 }
810
811 #[test]
812 fn case_lifecycle() {
813 let svc = service();
814 let created = svc
816 .create_case(
817 &req("CreateCase", json!({})),
818 &json!({ "subject": "Cannot connect", "communicationBody": "help please" }),
819 )
820 .unwrap();
821 let case_id = body_of(&created)["caseId"].as_str().unwrap().to_string();
822 assert!(case_id.starts_with("case-000000000000-"));
823
824 let described = svc
826 .describe_cases(&req("DescribeCases", json!({})), &json!({}))
827 .unwrap();
828 let out = body_of(&described);
829 assert_eq!(out["cases"].as_array().unwrap().len(), 1);
830 let comms = &out["cases"][0]["recentCommunications"]["communications"];
831 assert_eq!(comms.as_array().unwrap().len(), 1);
832 assert_eq!(comms[0]["body"], "help please");
833
834 let added = svc
836 .add_communication_to_case(
837 &req("AddCommunicationToCase", json!({})),
838 &json!({ "caseId": case_id, "communicationBody": "any update?" }),
839 )
840 .unwrap();
841 assert_eq!(body_of(&added)["result"], true);
842 let comms = svc
843 .describe_communications(
844 &req("DescribeCommunications", json!({})),
845 &json!({ "caseId": case_id }),
846 )
847 .unwrap();
848 assert_eq!(
849 body_of(&comms)["communications"].as_array().unwrap().len(),
850 2
851 );
852
853 let resolved = svc
855 .resolve_case(
856 &req("ResolveCase", json!({})),
857 &json!({ "caseId": case_id }),
858 )
859 .unwrap();
860 let out = body_of(&resolved);
861 assert_eq!(out["initialCaseStatus"], "opened");
862 assert_eq!(out["finalCaseStatus"], "resolved");
863
864 let default_list = svc
866 .describe_cases(&req("DescribeCases", json!({})), &json!({}))
867 .unwrap();
868 assert_eq!(body_of(&default_list)["cases"].as_array().unwrap().len(), 0);
869 let with_resolved = svc
870 .describe_cases(
871 &req("DescribeCases", json!({})),
872 &json!({ "includeResolvedCases": true }),
873 )
874 .unwrap();
875 assert_eq!(
876 body_of(&with_resolved)["cases"].as_array().unwrap().len(),
877 1
878 );
879 }
880
881 #[test]
882 fn unknown_case_is_case_id_not_found() {
883 let svc = service();
884 let err = expect_err(svc.describe_communications(
885 &req("DescribeCommunications", json!({})),
886 &json!({ "caseId": "case-nope" }),
887 ));
888 assert!(format!("{err:?}").contains("CaseIdNotFound"));
889 }
890
891 #[test]
892 fn attachment_set_lifecycle() {
893 let svc = service();
894 let created = svc
895 .add_attachments_to_set(
896 &req("AddAttachmentsToSet", json!({})),
897 &json!({ "attachments": [{ "fileName": "log.txt", "data": "aGVsbG8=" }] }),
898 )
899 .unwrap();
900 let out = body_of(&created);
901 let set_id = out["attachmentSetId"].as_str().unwrap().to_string();
902 assert!(set_id.starts_with("as-"));
903 assert!(out["expiryTime"].is_string());
904
905 let extended = svc
907 .add_attachments_to_set(
908 &req("AddAttachmentsToSet", json!({})),
909 &json!({ "attachmentSetId": set_id, "attachments": [{ "fileName": "b.txt", "data": "eA==" }] }),
910 )
911 .unwrap();
912 assert_eq!(body_of(&extended)["attachmentSetId"], set_id);
913
914 let err = expect_err(svc.add_attachments_to_set(
916 &req("AddAttachmentsToSet", json!({})),
917 &json!({ "attachmentSetId": "as-missing", "attachments": [] }),
918 ));
919 assert!(format!("{err:?}").contains("AttachmentSetIdNotFound"));
920 }
921
922 #[test]
923 fn describe_attachment_unknown_id() {
924 let svc = service();
925 let err = expect_err(svc.describe_attachment(
926 &req("DescribeAttachment", json!({})),
927 &json!({ "attachmentId": "attachment-x" }),
928 ));
929 assert!(format!("{err:?}").contains("AttachmentIdNotFound"));
930 }
931
932 #[test]
933 fn severity_levels_and_ta_catalogue() {
934 let svc = service();
935 let sev = body_of(&svc.describe_severity_levels(&json!({})).unwrap());
936 assert_eq!(sev["severityLevels"].as_array().unwrap().len(), 5);
937 let checks = body_of(&svc.describe_ta_checks(&json!({})).unwrap());
938 assert!(!checks["checks"].as_array().unwrap().is_empty());
939 }
940
941 #[test]
942 fn ta_refresh_state_machine() {
943 let svc = service();
944 let enq = body_of(
946 &svc.refresh_ta_check(
947 &req("RefreshTrustedAdvisorCheck", json!({})),
948 &json!({ "checkId": "Qch7DwouX1" }),
949 )
950 .unwrap(),
951 );
952 assert_eq!(enq["status"]["status"], "enqueued");
953 let step1 = body_of(
955 &svc.describe_ta_refresh_statuses(
956 &req("DescribeTrustedAdvisorCheckRefreshStatuses", json!({})),
957 &json!({ "checkIds": ["Qch7DwouX1"] }),
958 )
959 .unwrap(),
960 );
961 assert_eq!(step1["statuses"][0]["status"], "processing");
962 let step2 = body_of(
963 &svc.describe_ta_refresh_statuses(
964 &req("DescribeTrustedAdvisorCheckRefreshStatuses", json!({})),
965 &json!({ "checkIds": ["Qch7DwouX1"] }),
966 )
967 .unwrap(),
968 );
969 assert_eq!(step2["statuses"][0]["status"], "success");
970 }
971
972 #[test]
973 fn describe_cases_paginates() {
974 let svc = service();
975 for _ in 0..3 {
976 svc.create_case(
977 &req("CreateCase", json!({})),
978 &json!({ "subject": "s", "communicationBody": "b" }),
979 )
980 .unwrap();
981 }
982 let page1 = body_of(
983 &svc.describe_cases(
984 &req("DescribeCases", json!({})),
985 &json!({ "maxResults": 2 }),
986 )
987 .unwrap(),
988 );
989 assert_eq!(page1["cases"].as_array().unwrap().len(), 2);
990 let token = page1["nextToken"].as_str().unwrap();
991 let page2 = body_of(
992 &svc.describe_cases(
993 &req("DescribeCases", json!({})),
994 &json!({ "maxResults": 2, "nextToken": token }),
995 )
996 .unwrap(),
997 );
998 assert_eq!(page2["cases"].as_array().unwrap().len(), 1);
999 assert!(page2.get("nextToken").is_none());
1000 }
1001}