fakecloud_support/state.rs
1//! Account-partitioned, serializable state for AWS Support (`support`).
2//!
3//! Cases are stored as their already-output-valid `CaseDetails` wire object
4//! (`serde_json::Value`, camelCase members matching the awsJson1_1 member
5//! names) so a `DescribeCases` echoes exactly what `CreateCase` persisted. The
6//! per-case communication thread lives in a side map keyed by case id, and
7//! attachment sets / individual attachments live in their own maps so
8//! `DescribeAttachment` has a single source of truth.
9//!
10//! The Trusted Advisor refresh status is a per-check state machine
11//! (`none -> enqueued -> processing -> success`) stored in `ta_refresh`;
12//! `RefreshTrustedAdvisorCheck` enqueues and each
13//! `DescribeTrustedAdvisorCheckRefreshStatuses` read advances it one step.
14
15use std::collections::BTreeMap;
16use std::sync::Arc;
17
18use parking_lot::RwLock;
19use serde::{Deserialize, Serialize};
20use serde_json::Value;
21
22use fakecloud_core::multi_account::{AccountState, MultiAccountState};
23
24pub const SUPPORT_SNAPSHOT_SCHEMA_VERSION: u32 = 1;
25
26/// Per-account AWS Support state.
27#[derive(Debug, Clone, Default, Serialize, Deserialize)]
28pub struct SupportData {
29 /// The account id this partition belongs to (for id synthesis).
30 #[serde(default)]
31 pub account_id: String,
32 /// The region this partition belongs to.
33 #[serde(default)]
34 pub region: String,
35
36 /// Support cases keyed by `caseId`; each value is the `CaseDetails` wire
37 /// object.
38 #[serde(default)]
39 pub cases: BTreeMap<String, Value>,
40 /// Per-case communication thread keyed by `caseId`; each entry is a
41 /// `Communication` wire object, newest last.
42 #[serde(default)]
43 pub communications: BTreeMap<String, Vec<Value>>,
44
45 /// Attachment sets keyed by `attachmentSetId`. Each value carries
46 /// `expiryTime` and the list of member `attachmentId`s.
47 #[serde(default)]
48 pub attachment_sets: BTreeMap<String, Value>,
49 /// Individual attachments keyed by `attachmentId`; each value carries
50 /// `fileName` and base64 `data` (the `Attachment` wire object).
51 #[serde(default)]
52 pub attachments: BTreeMap<String, Value>,
53
54 /// Trusted Advisor per-check refresh status keyed by `checkId`; one of
55 /// `none` / `enqueued` / `processing` / `success`.
56 #[serde(default)]
57 pub ta_refresh: BTreeMap<String, String>,
58}
59
60impl AccountState for SupportData {
61 fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
62 Self {
63 account_id: account_id.to_string(),
64 region: region.to_string(),
65 ..Default::default()
66 }
67 }
68}
69
70impl SupportData {
71 /// There is no timer-driven lifecycle to resume: the Trusted Advisor
72 /// refresh state machine advances only on an explicit read, so a restart
73 /// leaves it exactly as persisted. Kept for symmetry with the other
74 /// services' persistence hook. Always returns `false` (nothing settled).
75 pub fn reconcile(&mut self) -> bool {
76 false
77 }
78
79 /// Advance one check's refresh status one step toward `success`, returning
80 /// the resulting status. `none`/absent stays `none` until an explicit
81 /// refresh enqueues it.
82 pub fn advance_refresh(&mut self, check_id: &str) -> String {
83 let cur = self
84 .ta_refresh
85 .get(check_id)
86 .cloned()
87 .unwrap_or_else(|| "none".to_string());
88 let next = match cur.as_str() {
89 "enqueued" => "processing",
90 "processing" => "success",
91 other => other,
92 };
93 self.ta_refresh
94 .insert(check_id.to_string(), next.to_string());
95 next.to_string()
96 }
97}
98
99pub type SharedSupportState = Arc<RwLock<MultiAccountState<SupportData>>>;
100
101#[derive(Debug, Serialize, Deserialize)]
102pub struct SupportSnapshot {
103 pub schema_version: u32,
104 pub accounts: MultiAccountState<SupportData>,
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 fn data() -> SupportData {
112 SupportData::new_for_account("000000000000", "us-east-1", "")
113 }
114
115 #[test]
116 fn refresh_advances_through_states() {
117 let mut d = data();
118 // Unrefreshed check stays none.
119 assert_eq!(d.advance_refresh("Qch7DwouX1"), "none");
120 // Enqueue, then each read advances one step.
121 d.ta_refresh.insert("Qch7DwouX1".into(), "enqueued".into());
122 assert_eq!(d.advance_refresh("Qch7DwouX1"), "processing");
123 assert_eq!(d.advance_refresh("Qch7DwouX1"), "success");
124 // Terminal: stays success.
125 assert_eq!(d.advance_refresh("Qch7DwouX1"), "success");
126 }
127
128 #[test]
129 fn reconcile_is_noop() {
130 let mut d = data();
131 assert!(!d.reconcile());
132 }
133}