Skip to main content

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}