Skip to main content

fakecloud_elasticache/
state.rs

1use std::collections::{BTreeMap, HashSet};
2use std::sync::Arc;
3
4use fakecloud_aws::arn::Arn;
5use parking_lot::RwLock;
6
7pub type SharedElastiCacheState =
8    Arc<RwLock<fakecloud_core::multi_account::MultiAccountState<ElastiCacheState>>>;
9
10impl fakecloud_core::multi_account::AccountState for ElastiCacheState {
11    fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
12        Self::new(account_id, region)
13    }
14}
15
16#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
17pub struct CacheEngineVersion {
18    pub engine: String,
19    pub engine_version: String,
20    pub cache_parameter_group_family: String,
21    pub cache_engine_description: String,
22    pub cache_engine_version_description: String,
23}
24
25#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
26pub struct CacheParameterGroup {
27    pub cache_parameter_group_name: String,
28    pub cache_parameter_group_family: String,
29    pub description: String,
30    pub is_global: bool,
31    pub arn: String,
32}
33
34#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
35pub struct EngineDefaultParameter {
36    pub parameter_name: String,
37    pub parameter_value: String,
38    pub description: String,
39    pub source: String,
40    pub data_type: String,
41    pub allowed_values: String,
42    pub is_modifiable: bool,
43    pub minimum_engine_version: String,
44}
45
46#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
47pub struct CacheSubnetGroup {
48    pub cache_subnet_group_name: String,
49    pub cache_subnet_group_description: String,
50    pub vpc_id: String,
51    pub subnet_ids: Vec<String>,
52    pub arn: String,
53}
54
55#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
56pub struct RecurringCharge {
57    pub recurring_charge_amount: f64,
58    pub recurring_charge_frequency: String,
59}
60
61#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
62pub struct ReservedCacheNode {
63    pub reserved_cache_node_id: String,
64    pub reserved_cache_nodes_offering_id: String,
65    pub cache_node_type: String,
66    pub start_time: String,
67    pub duration: i32,
68    pub fixed_price: f64,
69    pub usage_price: f64,
70    pub cache_node_count: i32,
71    pub product_description: String,
72    pub offering_type: String,
73    pub state: String,
74    pub recurring_charges: Vec<RecurringCharge>,
75    pub reservation_arn: String,
76}
77
78#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
79pub struct ReservedCacheNodesOffering {
80    pub reserved_cache_nodes_offering_id: String,
81    pub cache_node_type: String,
82    pub duration: i32,
83    pub fixed_price: f64,
84    pub usage_price: f64,
85    pub product_description: String,
86    pub offering_type: String,
87    pub recurring_charges: Vec<RecurringCharge>,
88}
89
90#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
91pub struct CacheCluster {
92    pub cache_cluster_id: String,
93    pub cache_node_type: String,
94    pub engine: String,
95    pub engine_version: String,
96    pub cache_cluster_status: String,
97    pub num_cache_nodes: i32,
98    pub preferred_availability_zone: String,
99    pub cache_subnet_group_name: Option<String>,
100    pub auto_minor_version_upgrade: bool,
101    pub arn: String,
102    pub created_at: String,
103    pub endpoint_address: String,
104    pub endpoint_port: u16,
105    pub container_id: String,
106    pub host_port: u16,
107    pub replication_group_id: Option<String>,
108    /// `CacheParameterGroup.CacheParameterGroupName` — group bound at
109    /// create / modify time. Real AWS always emits this membership;
110    /// fakecloud previously omitted the element entirely.
111    #[serde(default)]
112    pub cache_parameter_group_name: Option<String>,
113    /// VPC security group ids attached at create time. Echoed via
114    /// `<SecurityGroups>` for parity with AWS DescribeCacheClusters.
115    #[serde(default)]
116    pub security_group_ids: Vec<String>,
117    /// `LogDeliveryConfigurations` — destinations + log types attached
118    /// to the cluster. Round-tripped only.
119    #[serde(default)]
120    pub log_delivery_configurations: Vec<LogDeliveryConfiguration>,
121    /// In-transit encryption flag. Real AWS always emits this; defaults
122    /// to `false` for unencrypted clusters.
123    #[serde(default)]
124    pub transit_encryption_enabled: bool,
125    /// At-rest encryption flag.
126    #[serde(default)]
127    pub at_rest_encryption_enabled: bool,
128    /// `AuthTokenEnabled` — true when an AUTH token was supplied.
129    #[serde(default)]
130    pub auth_token_enabled: bool,
131    /// Configured `Port` from the create request. Stored separately
132    /// from `endpoint_port`/`host_port` so the engine default
133    /// (6379 redis / 11211 memcached) round-trips even when the
134    /// container listens elsewhere.
135    #[serde(default)]
136    pub port: u16,
137    /// `PreferredMaintenanceWindow` from the request, e.g. `sun:23:00-mon:01:30`.
138    #[serde(default)]
139    pub preferred_maintenance_window: Option<String>,
140    /// `PreferredAvailabilityZones.member.N` — populated for memcached clusters
141    /// pinning each node to a specific AZ.
142    #[serde(default)]
143    pub preferred_availability_zones: Vec<String>,
144    /// `NotificationTopicArn` for cluster events.
145    #[serde(default)]
146    pub notification_topic_arn: Option<String>,
147    /// Legacy EC2-Classic security group names.
148    #[serde(default)]
149    pub cache_security_group_names: Vec<String>,
150    /// `SnapshotArns.member.N` — RDB seed snapshot S3 ARNs (redis only).
151    #[serde(default)]
152    pub snapshot_arns: Vec<String>,
153    /// `SnapshotName` — replication-group / cluster snapshot to seed from.
154    #[serde(default)]
155    pub snapshot_name: Option<String>,
156    /// `SnapshotRetentionLimit` — daily snapshots to keep.
157    #[serde(default)]
158    pub snapshot_retention_limit: i32,
159    /// `SnapshotWindow` — time range when automatic snapshots run.
160    #[serde(default)]
161    pub snapshot_window: Option<String>,
162    /// `OutpostMode` — `single-outpost` or `cross-outpost`.
163    #[serde(default)]
164    pub outpost_mode: Option<String>,
165    /// `PreferredOutpostArn` — ARN of the AWS Outpost the cluster pins to.
166    #[serde(default)]
167    pub preferred_outpost_arn: Option<String>,
168    /// `NetworkType` — `ipv4`, `ipv6`, or `dual_stack`.
169    #[serde(default)]
170    pub network_type: Option<String>,
171    /// `IpDiscovery` — `ipv4` or `ipv6`.
172    #[serde(default)]
173    pub ip_discovery: Option<String>,
174    /// `AZMode` — `single-az` or `cross-az` (memcached multi-node).
175    #[serde(default)]
176    pub az_mode: Option<String>,
177    /// Raw AUTH token. Stored verbatim so a future modify can
178    /// compare/rotate; never echoed back in describe XML.
179    #[serde(default)]
180    pub auth_token: Option<String>,
181    /// `KmsKeyId` — at-rest encryption key passed at create time.
182    /// AWS doesn't echo this on `DescribeCacheClusters`, but real
183    /// SDKs (terraform plan diff, compliance scans) read it from
184    /// state, so we round-trip it on the struct.
185    #[serde(default)]
186    pub kms_key_id: Option<String>,
187    /// `TransitEncryptionMode` — `preferred` or `required`. Round-tripped
188    /// onto `DescribeCacheClusters` exactly like AWS does.
189    #[serde(default)]
190    pub transit_encryption_mode: Option<String>,
191    /// `DataTieringEnabled` toggle (Redis r6gd only). Stored verbatim
192    /// so terraform plan diff and DescribeCacheClusters round-trip.
193    #[serde(default)]
194    pub data_tiering_enabled: Option<bool>,
195    /// `ClusterMode` input — `compatible` / `enabled` / `disabled`.
196    /// Stored separately from `cluster_enabled` because the input
197    /// allows the tri-state `compatible` value.
198    #[serde(default)]
199    pub cluster_mode: Option<String>,
200    /// `PreferredOutpostArns.member.N` — cross-outpost cluster placement.
201    /// Round-tripped from input; not echoed by AWS but kept on the struct
202    /// so the original request shape is preserved.
203    #[serde(default)]
204    pub preferred_outpost_arns: Vec<String>,
205}
206
207#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
208pub struct ReplicationGroup {
209    pub replication_group_id: String,
210    pub description: String,
211    pub global_replication_group_id: Option<String>,
212    pub global_replication_group_role: Option<String>,
213    pub status: String,
214    pub cache_node_type: String,
215    pub engine: String,
216    pub engine_version: String,
217    pub num_cache_clusters: i32,
218    pub automatic_failover_enabled: bool,
219    pub endpoint_address: String,
220    pub endpoint_port: u16,
221    pub arn: String,
222    pub created_at: String,
223    pub container_id: String,
224    pub host_port: u16,
225    pub member_clusters: Vec<String>,
226    pub snapshot_retention_limit: i32,
227    pub snapshot_window: String,
228    /// Stored at create / modify time so DescribeReplicationGroups returns
229    /// the actual configuration instead of canned defaults. AWS always
230    /// emits these flags; SDKs that read them (terraform plan diff,
231    /// compliance checks) saw stale `false` for everyone.
232    #[serde(default)]
233    pub transit_encryption_enabled: bool,
234    #[serde(default)]
235    pub at_rest_encryption_enabled: bool,
236    #[serde(default)]
237    pub cluster_enabled: bool,
238    #[serde(default)]
239    pub kms_key_id: Option<String>,
240    #[serde(default)]
241    pub auth_token_enabled: bool,
242    #[serde(default)]
243    pub user_group_ids: Vec<String>,
244    #[serde(default)]
245    pub multi_az_enabled: bool,
246    #[serde(default)]
247    pub log_delivery_configurations: Vec<LogDeliveryConfiguration>,
248    #[serde(default)]
249    pub data_tiering: Option<String>,
250    #[serde(default)]
251    pub ip_discovery: Option<String>,
252    #[serde(default)]
253    pub network_type: Option<String>,
254    #[serde(default)]
255    pub transit_encryption_mode: Option<String>,
256    #[serde(default)]
257    pub num_node_groups: i32,
258    #[serde(default)]
259    pub configuration_endpoint_address: Option<String>,
260    #[serde(default)]
261    pub configuration_endpoint_port: Option<u16>,
262    #[serde(default)]
263    pub replicas_per_node_group: Option<i32>,
264    /// Raw AUTH token. Stored verbatim so a future `ModifyReplicationGroup`
265    /// can compare/rotate it; never echoed back in describe XML.
266    #[serde(default)]
267    pub auth_token: Option<String>,
268    /// Configured `Port` from the create request. AWS returns this on
269    /// `<NodeGroups>.<PrimaryEndpoint>.<Port>` once the cluster is real;
270    /// fakecloud uses the real container host port for connectivity but
271    /// echoes the requested value through pending modifications.
272    #[serde(default)]
273    pub port: u16,
274    /// SNS topic ARN for replication-group events.
275    #[serde(default)]
276    pub notification_topic_arn: Option<String>,
277    /// `ClusterMode` input — distinct from the derived `cluster_enabled`
278    /// flag. Valid values: `enabled` / `disabled` / `compatible`.
279    #[serde(default)]
280    pub cluster_mode: Option<String>,
281    /// `DataTieringEnabled` boolean as supplied by the request. The
282    /// existing `data_tiering` string field is the response-shape
283    /// `enabled`/`disabled` projection.
284    #[serde(default)]
285    pub data_tiering_enabled: Option<bool>,
286    /// `NotificationTopicStatus` from the most recent ModifyReplicationGroup
287    /// call. Defaults to `active` when emitting describe XML if unset.
288    #[serde(default)]
289    pub notification_topic_status: Option<String>,
290    /// `CacheParameterGroupName` from the create / modify request.
291    /// Echoed via `<CacheParameterGroup>` in describe XML.
292    #[serde(default)]
293    pub cache_parameter_group_name: Option<String>,
294    /// `CacheSubnetGroupName` from the create request. Persisted so
295    /// `ModifyReplicationGroup` and tooling like terraform plan diff
296    /// can recover the original placement.
297    #[serde(default)]
298    pub cache_subnet_group_name: Option<String>,
299    /// VPC security group ids attached at create / modify time. AWS
300    /// echoes these via `<SecurityGroups>` once the underlying clusters
301    /// land, so we persist them on the replication group as well.
302    #[serde(default)]
303    pub security_group_ids: Vec<String>,
304    /// `PreferredMaintenanceWindow` from the request, e.g.
305    /// `sun:23:00-mon:01:30`. Round-tripped onto member clusters and
306    /// echoed where AWS does.
307    #[serde(default)]
308    pub preferred_maintenance_window: Option<String>,
309    /// `SnapshotName` — replication-group snapshot used to seed the
310    /// new group. Stored verbatim for restore lineage; not echoed on
311    /// describe since AWS only emits `SnapshottingClusterId`.
312    #[serde(default)]
313    pub snapshot_name: Option<String>,
314    /// `SnapshotArns.member.N` — RDB seed snapshot S3 ARNs (redis only).
315    #[serde(default)]
316    pub snapshot_arns: Vec<String>,
317    /// `AutoMinorVersionUpgrade` toggle. AWS always emits this on the
318    /// describe response (default `true`) — tracked so ModifyReplicationGroup
319    /// can flip it.
320    #[serde(default = "default_auto_minor_version_upgrade")]
321    pub auto_minor_version_upgrade: bool,
322}
323
324fn default_auto_minor_version_upgrade() -> bool {
325    true
326}
327
328/// AWS's LogDeliveryConfiguration shape, retained verbatim so we can
329/// echo the exact request back. Stored as raw fields for both
330/// CloudWatch + Firehose destinations.
331#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
332pub struct LogDeliveryConfiguration {
333    pub log_type: String,
334    pub destination_type: String,
335    pub destination_details: Option<String>,
336    pub log_format: String,
337    pub status: String,
338}
339
340#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
341pub struct GlobalReplicationGroupMember {
342    pub replication_group_id: String,
343    pub replication_group_region: String,
344    pub role: String,
345    pub automatic_failover: bool,
346    pub status: String,
347}
348
349#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
350pub struct GlobalReplicationGroup {
351    pub global_replication_group_id: String,
352    pub global_replication_group_description: String,
353    pub status: String,
354    pub cache_node_type: String,
355    pub engine: String,
356    pub engine_version: String,
357    pub members: Vec<GlobalReplicationGroupMember>,
358    pub cluster_enabled: bool,
359    pub arn: String,
360    /// Number of global node groups (shards). Adjusted by the
361    /// Increase/Decrease/RebalanceNodeGroupsInGlobalReplicationGroup ops
362    /// and reflected in DescribeGlobalReplicationGroups. Defaults to 1.
363    #[serde(default = "default_global_node_group_count")]
364    pub num_node_groups: i32,
365}
366
367fn default_global_node_group_count() -> i32 {
368    1
369}
370
371#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
372pub struct ElastiCacheUser {
373    pub user_id: String,
374    pub user_name: String,
375    pub engine: String,
376    pub access_string: String,
377    pub status: String,
378    pub authentication_type: String,
379    pub password_count: i32,
380    pub arn: String,
381    pub minimum_engine_version: String,
382    pub user_group_ids: Vec<String>,
383}
384
385#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
386pub struct ElastiCacheUserGroup {
387    pub user_group_id: String,
388    pub engine: String,
389    pub status: String,
390    pub user_ids: Vec<String>,
391    pub arn: String,
392    pub minimum_engine_version: String,
393    pub pending_changes: Option<UserGroupPendingChanges>,
394    pub replication_groups: Vec<String>,
395}
396
397#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
398pub struct UserGroupPendingChanges {
399    pub user_ids_to_add: Vec<String>,
400    pub user_ids_to_remove: Vec<String>,
401}
402
403#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
404pub struct CacheSnapshot {
405    pub snapshot_name: String,
406    pub replication_group_id: String,
407    pub replication_group_description: String,
408    pub snapshot_status: String,
409    pub cache_node_type: String,
410    pub engine: String,
411    pub engine_version: String,
412    pub num_cache_clusters: i32,
413    pub arn: String,
414    pub created_at: String,
415    pub snapshot_source: String,
416    /// Path to the dumped RDB file on the local disk, if the runtime was
417    /// available at snapshot-create time.
418    pub rdb_path: Option<String>,
419}
420
421#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
422pub struct ServerlessCacheUsageLimits {
423    pub data_storage: Option<ServerlessCacheDataStorage>,
424    pub ecpu_per_second: Option<ServerlessCacheEcpuPerSecond>,
425}
426
427#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
428pub struct ServerlessCacheDataStorage {
429    pub maximum: Option<i32>,
430    pub minimum: Option<i32>,
431    pub unit: Option<String>,
432}
433
434#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
435pub struct ServerlessCacheEcpuPerSecond {
436    pub maximum: Option<i32>,
437    pub minimum: Option<i32>,
438}
439
440#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
441pub struct ServerlessCacheEndpoint {
442    pub address: String,
443    pub port: u16,
444}
445
446#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
447pub struct ServerlessCache {
448    pub serverless_cache_name: String,
449    pub description: String,
450    pub engine: String,
451    pub major_engine_version: String,
452    pub full_engine_version: String,
453    pub status: String,
454    pub endpoint: ServerlessCacheEndpoint,
455    pub reader_endpoint: ServerlessCacheEndpoint,
456    pub arn: String,
457    pub created_at: String,
458    pub cache_usage_limits: Option<ServerlessCacheUsageLimits>,
459    pub security_group_ids: Vec<String>,
460    pub subnet_ids: Vec<String>,
461    pub kms_key_id: Option<String>,
462    pub user_group_id: Option<String>,
463    pub snapshot_retention_limit: Option<i32>,
464    pub daily_snapshot_time: Option<String>,
465    pub container_id: String,
466    pub host_port: u16,
467}
468
469#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
470pub struct ServerlessCacheSnapshot {
471    pub serverless_cache_snapshot_name: String,
472    pub arn: String,
473    pub kms_key_id: Option<String>,
474    pub snapshot_type: String,
475    pub status: String,
476    pub create_time: String,
477    pub expiry_time: Option<String>,
478    pub bytes_used_for_cache: Option<String>,
479    pub serverless_cache_name: String,
480    pub engine: String,
481    pub major_engine_version: String,
482}
483
484#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
485pub struct CacheSecurityGroup {
486    pub cache_security_group_name: String,
487    pub description: String,
488    pub owner_id: String,
489    pub arn: String,
490    pub ec2_security_groups: Vec<Ec2SecurityGroupAuth>,
491}
492
493#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
494pub struct Ec2SecurityGroupAuth {
495    pub status: String,
496    pub ec2_security_group_name: String,
497    pub ec2_security_group_owner_id: String,
498}
499
500#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
501pub struct CacheParameter {
502    pub parameter_name: String,
503    pub parameter_value: String,
504    pub description: String,
505    pub source: String,
506    pub data_type: String,
507    pub allowed_values: String,
508    pub is_modifiable: bool,
509    pub minimum_engine_version: String,
510}
511
512#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
513pub struct CacheEvent {
514    pub source_identifier: String,
515    pub source_type: String,
516    pub message: String,
517    pub date: String,
518}
519
520#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
521pub struct ServiceUpdate {
522    pub service_update_name: String,
523    pub service_update_release_date: String,
524    pub service_update_end_date: String,
525    pub service_update_severity: String,
526    pub service_update_status: String,
527    pub service_update_recommended_apply_by_date: String,
528    pub service_update_type: String,
529    pub engine: String,
530    pub engine_version: String,
531    pub auto_update_after_recommended_apply_by_date: bool,
532    pub estimated_update_time: String,
533    pub service_update_description: String,
534}
535
536#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
537pub struct UpdateAction {
538    pub replication_group_id: Option<String>,
539    pub cache_cluster_id: Option<String>,
540    pub service_update_name: String,
541    pub service_update_release_date: String,
542    pub service_update_severity: String,
543    pub service_update_status: String,
544    pub service_update_recommended_apply_by_date: String,
545    pub service_update_type: String,
546    pub update_action_available_date: String,
547    pub update_action_status: String,
548    pub nodes_updated: String,
549    pub update_action_status_modified_date: String,
550    pub sla_met: String,
551    pub estimated_update_time: String,
552    pub engine: String,
553}
554
555#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
556pub struct Migration {
557    pub replication_group_id: String,
558    pub customer_node_endpoint_address: String,
559    pub customer_node_endpoint_port: i32,
560    pub status: String,
561    pub started_at: String,
562}
563
564#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
565pub struct ElastiCacheState {
566    pub account_id: String,
567    pub region: String,
568    pub parameter_groups: Vec<CacheParameterGroup>,
569    pub subnet_groups: BTreeMap<String, CacheSubnetGroup>,
570    pub reserved_cache_nodes: BTreeMap<String, ReservedCacheNode>,
571    pub reserved_cache_nodes_offerings: Vec<ReservedCacheNodesOffering>,
572    pub cache_clusters: BTreeMap<String, CacheCluster>,
573    pub replication_groups: BTreeMap<String, ReplicationGroup>,
574    pub global_replication_groups: BTreeMap<String, GlobalReplicationGroup>,
575    pub users: BTreeMap<String, ElastiCacheUser>,
576    pub user_groups: BTreeMap<String, ElastiCacheUserGroup>,
577    pub snapshots: BTreeMap<String, CacheSnapshot>,
578    pub serverless_caches: BTreeMap<String, ServerlessCache>,
579    pub serverless_cache_snapshots: BTreeMap<String, ServerlessCacheSnapshot>,
580    pub tags: BTreeMap<String, Vec<(String, String)>>,
581    in_progress_cache_cluster_ids: HashSet<String>,
582    /// Cache cluster ids whose DeleteCacheCluster arrived while the cluster was
583    /// still being created (its container started during the lock-drop window of
584    /// CreateCacheCluster). The create's finish step consults this so it reaps
585    /// the container and does NOT resurrect the deleted cluster
586    /// (bug-audit 2026-05-28, 4.3).
587    #[serde(default)]
588    delete_requested_cache_clusters: HashSet<String>,
589    in_progress_replication_group_ids: HashSet<String>,
590    in_progress_serverless_cache_names: HashSet<String>,
591    #[serde(default)]
592    pub security_groups: BTreeMap<String, CacheSecurityGroup>,
593    #[serde(default)]
594    pub parameter_group_parameters: BTreeMap<String, Vec<CacheParameter>>,
595    #[serde(default)]
596    pub events: Vec<CacheEvent>,
597    /// Active migrations keyed by replication group id.
598    #[serde(default)]
599    pub migrations: BTreeMap<String, Migration>,
600}
601
602impl ElastiCacheState {
603    pub fn new(account_id: &str, region: &str) -> Self {
604        let parameter_groups = default_parameter_groups(account_id, region);
605        let subnet_groups = default_subnet_groups(account_id, region);
606        let users = default_users(account_id, region);
607        let mut tags: BTreeMap<String, Vec<(String, String)>> = subnet_groups
608            .values()
609            .map(|g| (g.arn.clone(), Vec::new()))
610            .collect();
611        for user in users.values() {
612            tags.insert(user.arn.clone(), Vec::new());
613        }
614        Self {
615            account_id: account_id.to_string(),
616            region: region.to_string(),
617            parameter_groups,
618            subnet_groups,
619            reserved_cache_nodes: BTreeMap::new(),
620            reserved_cache_nodes_offerings: default_reserved_cache_nodes_offerings(),
621            cache_clusters: BTreeMap::new(),
622            replication_groups: BTreeMap::new(),
623            global_replication_groups: BTreeMap::new(),
624            users,
625            user_groups: BTreeMap::new(),
626            snapshots: BTreeMap::new(),
627            serverless_caches: BTreeMap::new(),
628            serverless_cache_snapshots: BTreeMap::new(),
629            tags,
630            in_progress_cache_cluster_ids: HashSet::new(),
631            delete_requested_cache_clusters: HashSet::new(),
632            in_progress_replication_group_ids: HashSet::new(),
633            in_progress_serverless_cache_names: HashSet::new(),
634            security_groups: BTreeMap::new(),
635            parameter_group_parameters: BTreeMap::new(),
636            events: Vec::new(),
637            migrations: BTreeMap::new(),
638        }
639    }
640
641    pub fn reset(&mut self) {
642        self.parameter_groups = default_parameter_groups(&self.account_id, &self.region);
643        self.subnet_groups = default_subnet_groups(&self.account_id, &self.region);
644        self.reserved_cache_nodes.clear();
645        self.reserved_cache_nodes_offerings = default_reserved_cache_nodes_offerings();
646        self.cache_clusters.clear();
647        self.replication_groups.clear();
648        self.global_replication_groups.clear();
649        self.users = default_users(&self.account_id, &self.region);
650        self.user_groups.clear();
651        self.snapshots.clear();
652        self.serverless_caches.clear();
653        self.serverless_cache_snapshots.clear();
654        self.tags.clear();
655        for g in self.subnet_groups.values() {
656            self.tags.insert(g.arn.clone(), Vec::new());
657        }
658        for user in self.users.values() {
659            self.tags.insert(user.arn.clone(), Vec::new());
660        }
661        self.in_progress_cache_cluster_ids.clear();
662        self.in_progress_replication_group_ids.clear();
663        self.in_progress_serverless_cache_names.clear();
664        self.security_groups.clear();
665        self.parameter_group_parameters.clear();
666        self.events.clear();
667        self.migrations.clear();
668    }
669
670    pub fn begin_cache_cluster_creation(&mut self, cache_cluster_id: &str) -> bool {
671        if self.cache_clusters.contains_key(cache_cluster_id)
672            || self
673                .in_progress_cache_cluster_ids
674                .contains(cache_cluster_id)
675        {
676            return false;
677        }
678        self.in_progress_cache_cluster_ids
679            .insert(cache_cluster_id.to_string());
680        true
681    }
682
683    pub fn finish_cache_cluster_creation(&mut self, cluster: CacheCluster) {
684        self.in_progress_cache_cluster_ids
685            .remove(&cluster.cache_cluster_id);
686        self.tags.insert(cluster.arn.clone(), Vec::new());
687        self.cache_clusters
688            .insert(cluster.cache_cluster_id.clone(), cluster);
689    }
690
691    pub fn cancel_cache_cluster_creation(&mut self, cache_cluster_id: &str) {
692        self.in_progress_cache_cluster_ids.remove(cache_cluster_id);
693    }
694
695    /// True if `id` is currently being created (in the lock-drop window of
696    /// CreateCacheCluster, before it is inserted into `cache_clusters`).
697    pub fn cache_cluster_creation_in_progress(&self, cache_cluster_id: &str) -> bool {
698        self.in_progress_cache_cluster_ids
699            .contains(cache_cluster_id)
700    }
701
702    /// Record that a DeleteCacheCluster arrived for `id` while it was still
703    /// being created. The create's finish step calls
704    /// [`take_cache_cluster_delete_request`] to detect this and reap the
705    /// container instead of resurrecting the deleted cluster
706    /// (bug-audit 2026-05-28, 4.3).
707    pub fn request_cache_cluster_delete_during_creation(&mut self, cache_cluster_id: &str) {
708        self.delete_requested_cache_clusters
709            .insert(cache_cluster_id.to_string());
710    }
711
712    /// Consume a pending delete request for `id` (set while it was creating).
713    /// Returns true if one was present, in which case the caller must drop the
714    /// in-progress marker and reap any started container without inserting the
715    /// cluster.
716    pub fn take_cache_cluster_delete_request(&mut self, cache_cluster_id: &str) -> bool {
717        self.delete_requested_cache_clusters
718            .remove(cache_cluster_id)
719    }
720
721    pub fn begin_replication_group_creation(&mut self, replication_group_id: &str) -> bool {
722        if self.replication_groups.contains_key(replication_group_id)
723            || self
724                .in_progress_replication_group_ids
725                .contains(replication_group_id)
726        {
727            return false;
728        }
729        self.in_progress_replication_group_ids
730            .insert(replication_group_id.to_string());
731        true
732    }
733
734    pub fn finish_replication_group_creation(&mut self, group: ReplicationGroup) {
735        self.in_progress_replication_group_ids
736            .remove(&group.replication_group_id);
737        self.tags.insert(group.arn.clone(), Vec::new());
738        self.replication_groups
739            .insert(group.replication_group_id.clone(), group);
740    }
741
742    pub fn cancel_replication_group_creation(&mut self, replication_group_id: &str) {
743        self.in_progress_replication_group_ids
744            .remove(replication_group_id);
745    }
746
747    pub fn begin_serverless_cache_creation(&mut self, serverless_cache_name: &str) -> bool {
748        if self.serverless_caches.contains_key(serverless_cache_name)
749            || self
750                .in_progress_serverless_cache_names
751                .contains(serverless_cache_name)
752        {
753            return false;
754        }
755        self.in_progress_serverless_cache_names
756            .insert(serverless_cache_name.to_string());
757        true
758    }
759
760    pub fn finish_serverless_cache_creation(&mut self, cache: ServerlessCache) {
761        self.in_progress_serverless_cache_names
762            .remove(&cache.serverless_cache_name);
763        self.tags.insert(cache.arn.clone(), Vec::new());
764        self.serverless_caches
765            .insert(cache.serverless_cache_name.clone(), cache);
766    }
767
768    pub fn cancel_serverless_cache_creation(&mut self, serverless_cache_name: &str) {
769        self.in_progress_serverless_cache_names
770            .remove(serverless_cache_name);
771    }
772
773    pub fn register_arn(&mut self, arn: &str) {
774        self.tags.entry(arn.to_string()).or_default();
775    }
776
777    pub fn has_arn(&self, arn: &str) -> bool {
778        self.tags.contains_key(arn)
779    }
780}
781
782fn default_reserved_cache_nodes_offerings() -> Vec<ReservedCacheNodesOffering> {
783    vec![
784        ReservedCacheNodesOffering {
785            reserved_cache_nodes_offering_id: "off-cache-t3-micro-redis-1yr-no-upfront".to_string(),
786            cache_node_type: "cache.t3.micro".to_string(),
787            duration: 31_536_000,
788            fixed_price: 0.0,
789            usage_price: 0.011,
790            product_description: "redis".to_string(),
791            offering_type: "No Upfront".to_string(),
792            recurring_charges: Vec::new(),
793        },
794        ReservedCacheNodesOffering {
795            reserved_cache_nodes_offering_id: "off-cache-t3-small-redis-1yr-partial-upfront"
796                .to_string(),
797            cache_node_type: "cache.t3.small".to_string(),
798            duration: 31_536_000,
799            fixed_price: 120.0,
800            usage_price: 0.007,
801            product_description: "redis".to_string(),
802            offering_type: "Partial Upfront".to_string(),
803            recurring_charges: Vec::new(),
804        },
805        ReservedCacheNodesOffering {
806            reserved_cache_nodes_offering_id: "off-cache-m5-large-memcached-3yr-no-upfront"
807                .to_string(),
808            cache_node_type: "cache.m5.large".to_string(),
809            duration: 94_608_000,
810            fixed_price: 0.0,
811            usage_price: 0.033,
812            product_description: "memcached".to_string(),
813            offering_type: "No Upfront".to_string(),
814            recurring_charges: Vec::new(),
815        },
816        ReservedCacheNodesOffering {
817            reserved_cache_nodes_offering_id: "off-cache-r6g-large-redis-3yr-all-upfront"
818                .to_string(),
819            cache_node_type: "cache.r6g.large".to_string(),
820            duration: 94_608_000,
821            fixed_price: 1_550.0,
822            usage_price: 0.0,
823            product_description: "redis".to_string(),
824            offering_type: "All Upfront".to_string(),
825            recurring_charges: vec![RecurringCharge {
826                recurring_charge_amount: 0.0,
827                recurring_charge_frequency: "Hourly".to_string(),
828            }],
829        },
830    ]
831}
832
833pub fn default_engine_versions() -> Vec<CacheEngineVersion> {
834    vec![
835        CacheEngineVersion {
836            engine: "redis".to_string(),
837            engine_version: "7.1".to_string(),
838            cache_parameter_group_family: "redis7".to_string(),
839            cache_engine_description: "Redis".to_string(),
840            cache_engine_version_description: "Redis 7.1".to_string(),
841        },
842        CacheEngineVersion {
843            engine: "valkey".to_string(),
844            engine_version: "8.0".to_string(),
845            cache_parameter_group_family: "valkey8".to_string(),
846            cache_engine_description: "Valkey".to_string(),
847            cache_engine_version_description: "Valkey 8.0".to_string(),
848        },
849        CacheEngineVersion {
850            engine: "memcached".to_string(),
851            engine_version: "1.6.22".to_string(),
852            cache_parameter_group_family: "memcached1.6".to_string(),
853            cache_engine_description: "Memcached".to_string(),
854            cache_engine_version_description: "Memcached 1.6.22".to_string(),
855        },
856    ]
857}
858
859fn default_parameter_groups(account_id: &str, region: &str) -> Vec<CacheParameterGroup> {
860    vec![
861        CacheParameterGroup {
862            cache_parameter_group_name: "default.redis7".to_string(),
863            cache_parameter_group_family: "redis7".to_string(),
864            description: "Default parameter group for redis7".to_string(),
865            is_global: false,
866            arn: Arn::new(
867                "elasticache",
868                region,
869                account_id,
870                "parametergroup:default.redis7",
871            )
872            .to_string(),
873        },
874        CacheParameterGroup {
875            cache_parameter_group_name: "default.valkey8".to_string(),
876            cache_parameter_group_family: "valkey8".to_string(),
877            description: "Default parameter group for valkey8".to_string(),
878            is_global: false,
879            arn: Arn::new(
880                "elasticache",
881                region,
882                account_id,
883                "parametergroup:default.valkey8",
884            )
885            .to_string(),
886        },
887        CacheParameterGroup {
888            cache_parameter_group_name: "default.memcached1.6".to_string(),
889            cache_parameter_group_family: "memcached1.6".to_string(),
890            description: "Default parameter group for memcached1.6".to_string(),
891            is_global: false,
892            arn: Arn::new(
893                "elasticache",
894                region,
895                account_id,
896                "parametergroup:default.memcached1.6",
897            )
898            .to_string(),
899        },
900    ]
901}
902
903fn default_subnet_groups(account_id: &str, region: &str) -> BTreeMap<String, CacheSubnetGroup> {
904    let default_group = CacheSubnetGroup {
905        cache_subnet_group_name: "default".to_string(),
906        cache_subnet_group_description: "Default CacheSubnetGroup".to_string(),
907        vpc_id: "vpc-00000000".to_string(),
908        subnet_ids: vec!["subnet-00000000".to_string()],
909        arn: Arn::new("elasticache", region, account_id, "subnetgroup:default").to_string(),
910    };
911    let mut map = BTreeMap::new();
912    map.insert("default".to_string(), default_group);
913    map
914}
915
916pub fn default_parameters_for_family(family: &str) -> Vec<EngineDefaultParameter> {
917    match family {
918        "redis7" => vec![
919            EngineDefaultParameter {
920                parameter_name: "maxmemory-policy".to_string(),
921                parameter_value: "volatile-lru".to_string(),
922                description: "Max memory policy".to_string(),
923                source: "system".to_string(),
924                data_type: "string".to_string(),
925                allowed_values: "volatile-lru,allkeys-lru,volatile-lfu,allkeys-lfu,volatile-random,allkeys-random,volatile-ttl,noeviction".to_string(),
926                is_modifiable: true,
927                minimum_engine_version: "7.0.0".to_string(),
928            },
929            EngineDefaultParameter {
930                parameter_name: "cluster-enabled".to_string(),
931                parameter_value: "no".to_string(),
932                description: "Enable or disable Redis Cluster mode".to_string(),
933                source: "system".to_string(),
934                data_type: "string".to_string(),
935                allowed_values: "yes,no".to_string(),
936                is_modifiable: false,
937                minimum_engine_version: "7.0.0".to_string(),
938            },
939            EngineDefaultParameter {
940                parameter_name: "activedefrag".to_string(),
941                parameter_value: "no".to_string(),
942                description: "Enable active defragmentation".to_string(),
943                source: "system".to_string(),
944                data_type: "string".to_string(),
945                allowed_values: "yes,no".to_string(),
946                is_modifiable: true,
947                minimum_engine_version: "7.0.0".to_string(),
948            },
949        ],
950        "valkey8" => vec![
951            EngineDefaultParameter {
952                parameter_name: "maxmemory-policy".to_string(),
953                parameter_value: "volatile-lru".to_string(),
954                description: "Max memory policy".to_string(),
955                source: "system".to_string(),
956                data_type: "string".to_string(),
957                allowed_values: "volatile-lru,allkeys-lru,volatile-lfu,allkeys-lfu,volatile-random,allkeys-random,volatile-ttl,noeviction".to_string(),
958                is_modifiable: true,
959                minimum_engine_version: "8.0.0".to_string(),
960            },
961            EngineDefaultParameter {
962                parameter_name: "cluster-enabled".to_string(),
963                parameter_value: "no".to_string(),
964                description: "Enable or disable cluster mode".to_string(),
965                source: "system".to_string(),
966                data_type: "string".to_string(),
967                allowed_values: "yes,no".to_string(),
968                is_modifiable: false,
969                minimum_engine_version: "8.0.0".to_string(),
970            },
971            EngineDefaultParameter {
972                parameter_name: "activedefrag".to_string(),
973                parameter_value: "no".to_string(),
974                description: "Enable active defragmentation".to_string(),
975                source: "system".to_string(),
976                data_type: "string".to_string(),
977                allowed_values: "yes,no".to_string(),
978                is_modifiable: true,
979                minimum_engine_version: "8.0.0".to_string(),
980            },
981        ],
982        "memcached1.6" => vec![
983            EngineDefaultParameter {
984                parameter_name: "max_item_size".to_string(),
985                parameter_value: "1048576".to_string(),
986                description: "Maximum item size".to_string(),
987                source: "system".to_string(),
988                data_type: "integer".to_string(),
989                allowed_values: "1048576-1073741824".to_string(),
990                is_modifiable: true,
991                minimum_engine_version: "1.4.5".to_string(),
992            },
993            EngineDefaultParameter {
994                parameter_name: "max_simultaneous_connections".to_string(),
995                parameter_value: "65000".to_string(),
996                description: "Maximum number of concurrent connections".to_string(),
997                source: "system".to_string(),
998                data_type: "integer".to_string(),
999                allowed_values: "1-65000".to_string(),
1000                is_modifiable: false,
1001                minimum_engine_version: "1.4.5".to_string(),
1002            },
1003        ],
1004        _ => Vec::new(),
1005    }
1006}
1007
1008fn default_users(account_id: &str, region: &str) -> BTreeMap<String, ElastiCacheUser> {
1009    let mut map = BTreeMap::new();
1010    map.insert(
1011        "default".to_string(),
1012        ElastiCacheUser {
1013            user_id: "default".to_string(),
1014            user_name: "default".to_string(),
1015            engine: "redis".to_string(),
1016            access_string: "on ~* +@all".to_string(),
1017            status: "active".to_string(),
1018            authentication_type: "no-password".to_string(),
1019            password_count: 0,
1020            arn: Arn::new("elasticache", region, account_id, "user:default").to_string(),
1021            minimum_engine_version: "6.0".to_string(),
1022            user_group_ids: Vec::new(),
1023        },
1024    );
1025    map
1026}
1027
1028pub const ELASTICACHE_SNAPSHOT_SCHEMA_VERSION: u32 = 2;
1029
1030#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1031pub struct ElastiCacheSnapshot {
1032    pub schema_version: u32,
1033    #[serde(default)]
1034    pub accounts: Option<fakecloud_core::multi_account::MultiAccountState<ElastiCacheState>>,
1035    #[serde(default)]
1036    pub state: Option<ElastiCacheState>,
1037}
1038
1039#[cfg(test)]
1040mod tests {
1041    use super::*;
1042
1043    #[test]
1044    fn default_engine_versions_contains_redis_valkey_memcached() {
1045        let versions = default_engine_versions();
1046        assert_eq!(versions.len(), 3);
1047        assert_eq!(versions[0].engine, "redis");
1048        assert_eq!(versions[0].engine_version, "7.1");
1049        assert_eq!(versions[1].engine, "valkey");
1050        assert_eq!(versions[1].engine_version, "8.0");
1051        assert_eq!(versions[2].engine, "memcached");
1052        assert_eq!(versions[2].engine_version, "1.6.22");
1053    }
1054
1055    #[test]
1056    fn state_new_creates_default_parameter_groups() {
1057        let state = ElastiCacheState::new("123456789012", "us-east-1");
1058        assert_eq!(state.parameter_groups.len(), 3);
1059        assert_eq!(
1060            state.parameter_groups[0].cache_parameter_group_name,
1061            "default.redis7"
1062        );
1063        assert_eq!(
1064            state.parameter_groups[1].cache_parameter_group_name,
1065            "default.valkey8"
1066        );
1067        assert_eq!(
1068            state.parameter_groups[2].cache_parameter_group_name,
1069            "default.memcached1.6"
1070        );
1071    }
1072
1073    #[test]
1074    fn state_new_creates_default_subnet_group() {
1075        let state = ElastiCacheState::new("123456789012", "us-east-1");
1076        assert_eq!(state.subnet_groups.len(), 1);
1077        let default = state.subnet_groups.get("default").unwrap();
1078        assert_eq!(default.cache_subnet_group_name, "default");
1079        assert_eq!(
1080            default.cache_subnet_group_description,
1081            "Default CacheSubnetGroup"
1082        );
1083        assert_eq!(default.vpc_id, "vpc-00000000");
1084        assert!(!default.subnet_ids.is_empty());
1085        assert!(default.arn.contains("subnetgroup:default"));
1086    }
1087
1088    #[test]
1089    fn reset_restores_default_parameter_groups() {
1090        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1091        state.parameter_groups.clear();
1092        assert!(state.parameter_groups.is_empty());
1093        state.reset();
1094        assert_eq!(state.parameter_groups.len(), 3);
1095    }
1096
1097    #[test]
1098    fn reset_restores_default_subnet_groups() {
1099        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1100        state.subnet_groups.clear();
1101        assert!(state.subnet_groups.is_empty());
1102        state.reset();
1103        assert_eq!(state.subnet_groups.len(), 1);
1104        assert!(state.subnet_groups.contains_key("default"));
1105    }
1106
1107    #[test]
1108    fn default_parameters_for_redis7_returns_parameters() {
1109        let params = default_parameters_for_family("redis7");
1110        assert_eq!(params.len(), 3);
1111        assert_eq!(params[0].parameter_name, "maxmemory-policy");
1112    }
1113
1114    #[test]
1115    fn default_parameters_for_unknown_family_returns_empty() {
1116        let params = default_parameters_for_family("unknown");
1117        assert!(params.is_empty());
1118    }
1119
1120    #[test]
1121    fn state_new_has_empty_replication_groups() {
1122        let state = ElastiCacheState::new("123456789012", "us-east-1");
1123        assert!(state.replication_groups.is_empty());
1124    }
1125
1126    #[test]
1127    fn state_new_has_empty_global_replication_groups() {
1128        let state = ElastiCacheState::new("123456789012", "us-east-1");
1129        assert!(state.global_replication_groups.is_empty());
1130    }
1131
1132    #[test]
1133    fn state_new_has_empty_cache_clusters() {
1134        let state = ElastiCacheState::new("123456789012", "us-east-1");
1135        assert!(state.cache_clusters.is_empty());
1136    }
1137
1138    #[test]
1139    fn state_new_has_empty_serverless_caches() {
1140        let state = ElastiCacheState::new("123456789012", "us-east-1");
1141        assert!(state.serverless_caches.is_empty());
1142        assert!(state.serverless_cache_snapshots.is_empty());
1143    }
1144
1145    #[test]
1146    fn begin_cache_cluster_creation_rejects_duplicate_ids() {
1147        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1148
1149        assert!(state.begin_cache_cluster_creation("cluster-1"));
1150        assert!(!state.begin_cache_cluster_creation("cluster-1"));
1151
1152        state.cancel_cache_cluster_creation("cluster-1");
1153        assert!(state.begin_cache_cluster_creation("cluster-1"));
1154    }
1155
1156    #[test]
1157    fn begin_replication_group_creation_rejects_duplicate_ids() {
1158        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1159
1160        assert!(state.begin_replication_group_creation("rg-1"));
1161        assert!(!state.begin_replication_group_creation("rg-1"));
1162
1163        state.cancel_replication_group_creation("rg-1");
1164        assert!(state.begin_replication_group_creation("rg-1"));
1165    }
1166
1167    #[test]
1168    fn begin_serverless_cache_creation_rejects_duplicate_names() {
1169        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1170
1171        assert!(state.begin_serverless_cache_creation("cache-1"));
1172        assert!(!state.begin_serverless_cache_creation("cache-1"));
1173
1174        state.cancel_serverless_cache_creation("cache-1");
1175        assert!(state.begin_serverless_cache_creation("cache-1"));
1176    }
1177
1178    #[test]
1179    fn finish_serverless_cache_creation_registers_cache_and_tags() {
1180        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1181        assert!(state.begin_serverless_cache_creation("cache-1"));
1182
1183        let cache = ServerlessCache {
1184            serverless_cache_name: "cache-1".to_string(),
1185            description: "test".to_string(),
1186            engine: "redis".to_string(),
1187            major_engine_version: "7.1".to_string(),
1188            full_engine_version: "7.1".to_string(),
1189            status: "available".to_string(),
1190            endpoint: ServerlessCacheEndpoint {
1191                address: "127.0.0.1".to_string(),
1192                port: 6379,
1193            },
1194            reader_endpoint: ServerlessCacheEndpoint {
1195                address: "127.0.0.1".to_string(),
1196                port: 6379,
1197            },
1198            arn: "arn:aws:elasticache:us-east-1:123456789012:serverlesscache:cache-1".to_string(),
1199            created_at: "2024-01-01T00:00:00Z".to_string(),
1200            cache_usage_limits: None,
1201            security_group_ids: Vec::new(),
1202            subnet_ids: Vec::new(),
1203            kms_key_id: None,
1204            user_group_id: None,
1205            snapshot_retention_limit: None,
1206            daily_snapshot_time: None,
1207            container_id: "cid".to_string(),
1208            host_port: 6379,
1209        };
1210
1211        state.finish_serverless_cache_creation(cache.clone());
1212
1213        assert!(state.serverless_caches.contains_key("cache-1"));
1214        assert!(state.tags.contains_key(&cache.arn));
1215    }
1216
1217    #[test]
1218    fn state_new_creates_default_user() {
1219        let state = ElastiCacheState::new("123456789012", "us-east-1");
1220        assert_eq!(state.users.len(), 1);
1221        let default = state.users.get("default").unwrap();
1222        assert_eq!(default.user_id, "default");
1223        assert_eq!(default.user_name, "default");
1224        assert_eq!(default.engine, "redis");
1225        assert_eq!(default.access_string, "on ~* +@all");
1226        assert_eq!(default.status, "active");
1227        assert_eq!(default.authentication_type, "no-password");
1228        assert_eq!(default.password_count, 0);
1229        assert!(default.arn.contains("user:default"));
1230    }
1231
1232    #[test]
1233    fn state_new_has_empty_user_groups() {
1234        let state = ElastiCacheState::new("123456789012", "us-east-1");
1235        assert!(state.user_groups.is_empty());
1236    }
1237
1238    #[test]
1239    fn reset_restores_default_user() {
1240        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1241        state.users.clear();
1242        assert!(state.users.is_empty());
1243        state.reset();
1244        assert_eq!(state.users.len(), 1);
1245        assert!(state.users.contains_key("default"));
1246    }
1247
1248    #[test]
1249    fn reset_clears_user_groups() {
1250        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1251        state.user_groups.insert(
1252            "my-group".to_string(),
1253            ElastiCacheUserGroup {
1254                user_group_id: "my-group".to_string(),
1255                engine: "redis".to_string(),
1256                status: "active".to_string(),
1257                user_ids: vec!["default".to_string()],
1258                arn: "arn:aws:elasticache:us-east-1:123456789012:usergroup:my-group".to_string(),
1259                minimum_engine_version: "6.0".to_string(),
1260                pending_changes: None,
1261                replication_groups: Vec::new(),
1262            },
1263        );
1264        assert_eq!(state.user_groups.len(), 1);
1265        state.reset();
1266        assert!(state.user_groups.is_empty());
1267    }
1268
1269    #[test]
1270    fn state_new_has_empty_snapshots() {
1271        let state = ElastiCacheState::new("123456789012", "us-east-1");
1272        assert!(state.snapshots.is_empty());
1273    }
1274
1275    #[test]
1276    fn reset_clears_snapshots() {
1277        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1278        state.snapshots.insert(
1279            "my-snapshot".to_string(),
1280            CacheSnapshot {
1281                snapshot_name: "my-snapshot".to_string(),
1282                replication_group_id: "rg-1".to_string(),
1283                replication_group_description: "test".to_string(),
1284                snapshot_status: "available".to_string(),
1285                cache_node_type: "cache.t3.micro".to_string(),
1286                engine: "redis".to_string(),
1287                engine_version: "7.1".to_string(),
1288                num_cache_clusters: 1,
1289                arn: "arn:aws:elasticache:us-east-1:123456789012:snapshot:my-snapshot".to_string(),
1290                created_at: "2024-01-01T00:00:00Z".to_string(),
1291                snapshot_source: "manual".to_string(),
1292                rdb_path: None,
1293            },
1294        );
1295        assert_eq!(state.snapshots.len(), 1);
1296        state.reset();
1297        assert!(state.snapshots.is_empty());
1298    }
1299
1300    #[test]
1301    fn reset_clears_replication_groups() {
1302        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1303        state.replication_groups.insert(
1304            "my-group".to_string(),
1305            ReplicationGroup {
1306                replication_group_id: "my-group".to_string(),
1307                description: "test".to_string(),
1308                global_replication_group_id: None,
1309                global_replication_group_role: None,
1310                status: "available".to_string(),
1311                cache_node_type: "cache.t3.micro".to_string(),
1312                engine: "redis".to_string(),
1313                engine_version: "7.1".to_string(),
1314                num_cache_clusters: 1,
1315                automatic_failover_enabled: false,
1316                endpoint_address: "127.0.0.1".to_string(),
1317                endpoint_port: 6379,
1318                arn: "arn:aws:elasticache:us-east-1:123456789012:replicationgroup:my-group"
1319                    .to_string(),
1320                created_at: "2024-01-01T00:00:00Z".to_string(),
1321                container_id: "abc123".to_string(),
1322                host_port: 12345,
1323                member_clusters: vec!["my-group-001".to_string()],
1324                snapshot_retention_limit: 0,
1325                snapshot_window: "05:00-09:00".to_string(),
1326                transit_encryption_enabled: false,
1327                at_rest_encryption_enabled: false,
1328                cluster_enabled: false,
1329                kms_key_id: None,
1330                auth_token_enabled: false,
1331                user_group_ids: Vec::new(),
1332                multi_az_enabled: false,
1333                log_delivery_configurations: Vec::new(),
1334                data_tiering: None,
1335                ip_discovery: None,
1336                network_type: None,
1337                transit_encryption_mode: None,
1338                num_node_groups: 1,
1339                configuration_endpoint_address: None,
1340                configuration_endpoint_port: None,
1341                replicas_per_node_group: None,
1342                auth_token: None,
1343                port: 6379,
1344                notification_topic_arn: None,
1345                cluster_mode: None,
1346                data_tiering_enabled: None,
1347                notification_topic_status: None,
1348                cache_parameter_group_name: None,
1349                cache_subnet_group_name: None,
1350                security_group_ids: Vec::new(),
1351                preferred_maintenance_window: None,
1352                snapshot_name: None,
1353                snapshot_arns: Vec::new(),
1354                auto_minor_version_upgrade: true,
1355            },
1356        );
1357        assert_eq!(state.replication_groups.len(), 1);
1358        state.reset();
1359        assert!(state.replication_groups.is_empty());
1360    }
1361
1362    #[test]
1363    fn reset_clears_global_replication_groups() {
1364        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1365        state.global_replication_groups.insert(
1366            "global-rg".to_string(),
1367            GlobalReplicationGroup {
1368                global_replication_group_id: "global-rg".to_string(),
1369                global_replication_group_description: "test".to_string(),
1370                status: "available".to_string(),
1371                cache_node_type: "cache.t3.micro".to_string(),
1372                engine: "redis".to_string(),
1373                engine_version: "7.1".to_string(),
1374                members: vec![GlobalReplicationGroupMember {
1375                    replication_group_id: "rg-1".to_string(),
1376                    replication_group_region: "us-east-1".to_string(),
1377                    role: "primary".to_string(),
1378                    automatic_failover: false,
1379                    status: "associated".to_string(),
1380                }],
1381                cluster_enabled: false,
1382                arn: "arn:aws:elasticache:us-east-1:123456789012:globalreplicationgroup:global-rg"
1383                    .to_string(),
1384                num_node_groups: 1,
1385            },
1386        );
1387        assert_eq!(state.global_replication_groups.len(), 1);
1388        state.reset();
1389        assert!(state.global_replication_groups.is_empty());
1390    }
1391
1392    #[test]
1393    fn reset_clears_cache_clusters() {
1394        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1395        state.cache_clusters.insert(
1396            "classic-cluster".to_string(),
1397            CacheCluster {
1398                cache_cluster_id: "classic-cluster".to_string(),
1399                cache_node_type: "cache.t3.micro".to_string(),
1400                engine: "redis".to_string(),
1401                engine_version: "7.1".to_string(),
1402                cache_cluster_status: "available".to_string(),
1403                num_cache_nodes: 1,
1404                preferred_availability_zone: "us-east-1a".to_string(),
1405                cache_subnet_group_name: Some("default".to_string()),
1406                auto_minor_version_upgrade: true,
1407                arn: "arn:aws:elasticache:us-east-1:123456789012:cluster:classic-cluster"
1408                    .to_string(),
1409                created_at: "2024-01-01T00:00:00Z".to_string(),
1410                endpoint_address: "127.0.0.1".to_string(),
1411                endpoint_port: 6379,
1412                container_id: "abc123".to_string(),
1413                host_port: 12345,
1414                replication_group_id: None,
1415                cache_parameter_group_name: None,
1416                security_group_ids: Vec::new(),
1417                log_delivery_configurations: Vec::new(),
1418                transit_encryption_enabled: false,
1419                at_rest_encryption_enabled: false,
1420                auth_token_enabled: false,
1421                port: 6379,
1422                preferred_maintenance_window: None,
1423                preferred_availability_zones: Vec::new(),
1424                notification_topic_arn: None,
1425                cache_security_group_names: Vec::new(),
1426                snapshot_arns: Vec::new(),
1427                snapshot_name: None,
1428                snapshot_retention_limit: 0,
1429                snapshot_window: None,
1430                outpost_mode: None,
1431                preferred_outpost_arn: None,
1432                network_type: None,
1433                ip_discovery: None,
1434                az_mode: None,
1435                auth_token: None,
1436                kms_key_id: None,
1437                transit_encryption_mode: None,
1438                data_tiering_enabled: None,
1439                cluster_mode: None,
1440                preferred_outpost_arns: Vec::new(),
1441            },
1442        );
1443        assert_eq!(state.cache_clusters.len(), 1);
1444        state.reset();
1445        assert!(state.cache_clusters.is_empty());
1446    }
1447
1448    #[test]
1449    fn reset_restores_reserved_cache_node_metadata() {
1450        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1451        state.reserved_cache_nodes.insert(
1452            "rcn-a".to_string(),
1453            ReservedCacheNode {
1454                reserved_cache_node_id: "rcn-a".to_string(),
1455                reserved_cache_nodes_offering_id: "offering-a".to_string(),
1456                cache_node_type: "cache.t3.micro".to_string(),
1457                start_time: "2024-01-01T00:00:00Z".to_string(),
1458                duration: 31_536_000,
1459                fixed_price: 0.0,
1460                usage_price: 0.011,
1461                cache_node_count: 1,
1462                product_description: "redis".to_string(),
1463                offering_type: "No Upfront".to_string(),
1464                state: "payment-pending".to_string(),
1465                recurring_charges: Vec::new(),
1466                reservation_arn:
1467                    "arn:aws:elasticache:us-east-1:123456789012:reserved-instance:test".to_string(),
1468            },
1469        );
1470        state.reserved_cache_nodes_offerings.clear();
1471
1472        state.reset();
1473
1474        assert!(state.reserved_cache_nodes.is_empty());
1475        assert!(!state.reserved_cache_nodes_offerings.is_empty());
1476    }
1477
1478    #[test]
1479    fn reset_clears_serverless_cache_state() {
1480        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1481        state.serverless_caches.insert(
1482            "serverless".to_string(),
1483            ServerlessCache {
1484                serverless_cache_name: "serverless".to_string(),
1485                description: "test".to_string(),
1486                engine: "redis".to_string(),
1487                major_engine_version: "7.1".to_string(),
1488                full_engine_version: "7.1".to_string(),
1489                status: "available".to_string(),
1490                endpoint: ServerlessCacheEndpoint {
1491                    address: "127.0.0.1".to_string(),
1492                    port: 6379,
1493                },
1494                reader_endpoint: ServerlessCacheEndpoint {
1495                    address: "127.0.0.1".to_string(),
1496                    port: 6379,
1497                },
1498                arn: "arn:aws:elasticache:us-east-1:123456789012:serverlesscache:serverless"
1499                    .to_string(),
1500                created_at: "2024-01-01T00:00:00Z".to_string(),
1501                cache_usage_limits: None,
1502                security_group_ids: Vec::new(),
1503                subnet_ids: Vec::new(),
1504                kms_key_id: None,
1505                user_group_id: None,
1506                snapshot_retention_limit: None,
1507                daily_snapshot_time: None,
1508                container_id: "cid".to_string(),
1509                host_port: 6379,
1510            },
1511        );
1512        state.serverless_cache_snapshots.insert(
1513            "snap-1".to_string(),
1514            ServerlessCacheSnapshot {
1515                serverless_cache_snapshot_name: "snap-1".to_string(),
1516                arn: "arn:aws:elasticache:us-east-1:123456789012:serverlesssnapshot:snap-1"
1517                    .to_string(),
1518                kms_key_id: None,
1519                snapshot_type: "manual".to_string(),
1520                status: "available".to_string(),
1521                create_time: "2024-01-01T00:00:00Z".to_string(),
1522                expiry_time: None,
1523                bytes_used_for_cache: None,
1524                serverless_cache_name: "serverless".to_string(),
1525                engine: "redis".to_string(),
1526                major_engine_version: "7.1".to_string(),
1527            },
1528        );
1529
1530        state.reset();
1531
1532        assert!(state.serverless_caches.is_empty());
1533        assert!(state.serverless_cache_snapshots.is_empty());
1534    }
1535}