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    /// Available service updates keyed by ServiceUpdateName, seeded with a
601    /// small realistic set and read by DescribeServiceUpdates.
602    #[serde(default)]
603    pub service_updates: BTreeMap<String, ServiceUpdate>,
604    /// Update actions keyed by "{service_update_name}#{target_kind}#{id}".
605    /// Materialized lazily from (service_updates x clusters/replication groups)
606    /// and transitioned by BatchApplyUpdateAction / BatchStopUpdateAction.
607    #[serde(default)]
608    pub update_actions: BTreeMap<String, UpdateAction>,
609}
610
611impl ElastiCacheState {
612    pub fn new(account_id: &str, region: &str) -> Self {
613        let parameter_groups = default_parameter_groups(account_id, region);
614        let subnet_groups = default_subnet_groups(account_id, region);
615        let users = default_users(account_id, region);
616        let mut tags: BTreeMap<String, Vec<(String, String)>> = subnet_groups
617            .values()
618            .map(|g| (g.arn.clone(), Vec::new()))
619            .collect();
620        for user in users.values() {
621            tags.insert(user.arn.clone(), Vec::new());
622        }
623        Self {
624            account_id: account_id.to_string(),
625            region: region.to_string(),
626            parameter_groups,
627            subnet_groups,
628            reserved_cache_nodes: BTreeMap::new(),
629            reserved_cache_nodes_offerings: default_reserved_cache_nodes_offerings(),
630            cache_clusters: BTreeMap::new(),
631            replication_groups: BTreeMap::new(),
632            global_replication_groups: BTreeMap::new(),
633            users,
634            user_groups: BTreeMap::new(),
635            snapshots: BTreeMap::new(),
636            serverless_caches: BTreeMap::new(),
637            serverless_cache_snapshots: BTreeMap::new(),
638            tags,
639            in_progress_cache_cluster_ids: HashSet::new(),
640            delete_requested_cache_clusters: HashSet::new(),
641            in_progress_replication_group_ids: HashSet::new(),
642            in_progress_serverless_cache_names: HashSet::new(),
643            security_groups: BTreeMap::new(),
644            parameter_group_parameters: BTreeMap::new(),
645            events: Vec::new(),
646            migrations: BTreeMap::new(),
647            service_updates: default_service_updates(),
648            update_actions: BTreeMap::new(),
649        }
650    }
651
652    pub fn reset(&mut self) {
653        self.parameter_groups = default_parameter_groups(&self.account_id, &self.region);
654        self.subnet_groups = default_subnet_groups(&self.account_id, &self.region);
655        self.reserved_cache_nodes.clear();
656        self.reserved_cache_nodes_offerings = default_reserved_cache_nodes_offerings();
657        self.cache_clusters.clear();
658        self.replication_groups.clear();
659        self.global_replication_groups.clear();
660        self.users = default_users(&self.account_id, &self.region);
661        self.user_groups.clear();
662        self.snapshots.clear();
663        self.serverless_caches.clear();
664        self.serverless_cache_snapshots.clear();
665        self.tags.clear();
666        for g in self.subnet_groups.values() {
667            self.tags.insert(g.arn.clone(), Vec::new());
668        }
669        for user in self.users.values() {
670            self.tags.insert(user.arn.clone(), Vec::new());
671        }
672        self.in_progress_cache_cluster_ids.clear();
673        self.in_progress_replication_group_ids.clear();
674        self.in_progress_serverless_cache_names.clear();
675        self.security_groups.clear();
676        self.parameter_group_parameters.clear();
677        self.events.clear();
678        self.migrations.clear();
679        self.service_updates = default_service_updates();
680        self.update_actions.clear();
681    }
682
683    /// Ensure an UpdateAction exists for every (service_update, target) pair,
684    /// where targets are this account's cache clusters and replication groups.
685    /// AWS auto-creates these when a service update is released; we materialize
686    /// them lazily so DescribeUpdateActions and the batch ops share state.
687    pub fn ensure_update_actions(&mut self) {
688        let cluster_ids: Vec<String> = self.cache_clusters.keys().cloned().collect();
689        let group_ids: Vec<String> = self.replication_groups.keys().cloned().collect();
690        // Reconcile: drop actions whose target cluster/replication group (or
691        // service update) no longer exists, so deleted targets aren't treated
692        // as valid on subsequent Describe/BatchApply/BatchStop calls.
693        self.update_actions.retain(|_, ua| {
694            let su_exists = self.service_updates.contains_key(&ua.service_update_name);
695            let target_exists = match (&ua.cache_cluster_id, &ua.replication_group_id) {
696                (Some(cc), _) => cluster_ids.contains(cc),
697                (None, Some(rg)) => group_ids.contains(rg),
698                (None, None) => false,
699            };
700            su_exists && target_exists
701        });
702        for su in self.service_updates.values() {
703            for cc in &cluster_ids {
704                let key = format!("{}#cc#{}", su.service_update_name, cc);
705                self.update_actions
706                    .entry(key)
707                    .or_insert_with(|| new_update_action(su, None, Some(cc.clone())));
708            }
709            for rg in &group_ids {
710                let key = format!("{}#rg#{}", su.service_update_name, rg);
711                self.update_actions
712                    .entry(key)
713                    .or_insert_with(|| new_update_action(su, Some(rg.clone()), None));
714            }
715        }
716    }
717
718    pub fn begin_cache_cluster_creation(&mut self, cache_cluster_id: &str) -> bool {
719        if self.cache_clusters.contains_key(cache_cluster_id)
720            || self
721                .in_progress_cache_cluster_ids
722                .contains(cache_cluster_id)
723        {
724            return false;
725        }
726        self.in_progress_cache_cluster_ids
727            .insert(cache_cluster_id.to_string());
728        true
729    }
730
731    pub fn finish_cache_cluster_creation(&mut self, cluster: CacheCluster) {
732        self.in_progress_cache_cluster_ids
733            .remove(&cluster.cache_cluster_id);
734        self.tags.insert(cluster.arn.clone(), Vec::new());
735        self.cache_clusters
736            .insert(cluster.cache_cluster_id.clone(), cluster);
737    }
738
739    pub fn cancel_cache_cluster_creation(&mut self, cache_cluster_id: &str) {
740        self.in_progress_cache_cluster_ids.remove(cache_cluster_id);
741    }
742
743    /// True if `id` is currently being created (in the lock-drop window of
744    /// CreateCacheCluster, before it is inserted into `cache_clusters`).
745    pub fn cache_cluster_creation_in_progress(&self, cache_cluster_id: &str) -> bool {
746        self.in_progress_cache_cluster_ids
747            .contains(cache_cluster_id)
748    }
749
750    /// Record that a DeleteCacheCluster arrived for `id` while it was still
751    /// being created. The create's finish step calls
752    /// [`take_cache_cluster_delete_request`] to detect this and reap the
753    /// container instead of resurrecting the deleted cluster
754    /// (bug-audit 2026-05-28, 4.3).
755    pub fn request_cache_cluster_delete_during_creation(&mut self, cache_cluster_id: &str) {
756        self.delete_requested_cache_clusters
757            .insert(cache_cluster_id.to_string());
758    }
759
760    /// Consume a pending delete request for `id` (set while it was creating).
761    /// Returns true if one was present, in which case the caller must drop the
762    /// in-progress marker and reap any started container without inserting the
763    /// cluster.
764    pub fn take_cache_cluster_delete_request(&mut self, cache_cluster_id: &str) -> bool {
765        self.delete_requested_cache_clusters
766            .remove(cache_cluster_id)
767    }
768
769    pub fn begin_replication_group_creation(&mut self, replication_group_id: &str) -> bool {
770        if self.replication_groups.contains_key(replication_group_id)
771            || self
772                .in_progress_replication_group_ids
773                .contains(replication_group_id)
774        {
775            return false;
776        }
777        self.in_progress_replication_group_ids
778            .insert(replication_group_id.to_string());
779        true
780    }
781
782    pub fn finish_replication_group_creation(&mut self, group: ReplicationGroup) {
783        self.in_progress_replication_group_ids
784            .remove(&group.replication_group_id);
785        self.tags.insert(group.arn.clone(), Vec::new());
786        self.replication_groups
787            .insert(group.replication_group_id.clone(), group);
788    }
789
790    pub fn cancel_replication_group_creation(&mut self, replication_group_id: &str) {
791        self.in_progress_replication_group_ids
792            .remove(replication_group_id);
793    }
794
795    pub fn begin_serverless_cache_creation(&mut self, serverless_cache_name: &str) -> bool {
796        if self.serverless_caches.contains_key(serverless_cache_name)
797            || self
798                .in_progress_serverless_cache_names
799                .contains(serverless_cache_name)
800        {
801            return false;
802        }
803        self.in_progress_serverless_cache_names
804            .insert(serverless_cache_name.to_string());
805        true
806    }
807
808    pub fn finish_serverless_cache_creation(&mut self, cache: ServerlessCache) {
809        self.in_progress_serverless_cache_names
810            .remove(&cache.serverless_cache_name);
811        self.tags.insert(cache.arn.clone(), Vec::new());
812        self.serverless_caches
813            .insert(cache.serverless_cache_name.clone(), cache);
814    }
815
816    pub fn cancel_serverless_cache_creation(&mut self, serverless_cache_name: &str) {
817        self.in_progress_serverless_cache_names
818            .remove(serverless_cache_name);
819    }
820
821    pub fn register_arn(&mut self, arn: &str) {
822        self.tags.entry(arn.to_string()).or_default();
823    }
824
825    pub fn has_arn(&self, arn: &str) -> bool {
826        self.tags.contains_key(arn)
827    }
828}
829
830/// A small, realistic set of always-available service updates. AWS accounts
831/// always have some outstanding service updates; these are read by
832/// DescribeServiceUpdates and drive DescribeUpdateActions / batch ops.
833fn default_service_updates() -> BTreeMap<String, ServiceUpdate> {
834    let mut m = BTreeMap::new();
835    for (name, release, end, apply_by, severity, engine, desc) in [
836        (
837            "elasticache-20240301-redis-security",
838            "2024-03-01T00:00:00Z",
839            "2024-09-01T00:00:00Z",
840            "2024-04-01T00:00:00Z",
841            "important",
842            "redis",
843            "Security update for ElastiCache for Redis",
844        ),
845        (
846            "elasticache-20240601-memcached-security",
847            "2024-06-01T00:00:00Z",
848            "2024-12-01T00:00:00Z",
849            "2024-07-01T00:00:00Z",
850            "medium",
851            "memcached",
852            "Security update for ElastiCache for Memcached",
853        ),
854    ] {
855        m.insert(
856            name.to_string(),
857            ServiceUpdate {
858                service_update_name: name.to_string(),
859                service_update_release_date: release.to_string(),
860                service_update_end_date: end.to_string(),
861                service_update_severity: severity.to_string(),
862                service_update_status: "available".to_string(),
863                service_update_recommended_apply_by_date: apply_by.to_string(),
864                service_update_type: "security-update".to_string(),
865                engine: engine.to_string(),
866                engine_version: String::new(),
867                auto_update_after_recommended_apply_by_date: true,
868                estimated_update_time: "30 minutes".to_string(),
869                service_update_description: desc.to_string(),
870            },
871        );
872    }
873    m
874}
875
876/// Build a fresh, not-yet-applied UpdateAction for a service update against a
877/// single target (replication group or cache cluster).
878fn new_update_action(
879    su: &ServiceUpdate,
880    replication_group_id: Option<String>,
881    cache_cluster_id: Option<String>,
882) -> UpdateAction {
883    UpdateAction {
884        replication_group_id,
885        cache_cluster_id,
886        service_update_name: su.service_update_name.clone(),
887        service_update_release_date: su.service_update_release_date.clone(),
888        service_update_severity: su.service_update_severity.clone(),
889        service_update_status: su.service_update_status.clone(),
890        service_update_recommended_apply_by_date: su
891            .service_update_recommended_apply_by_date
892            .clone(),
893        service_update_type: su.service_update_type.clone(),
894        update_action_available_date: su.service_update_release_date.clone(),
895        update_action_status: "not-applied".to_string(),
896        nodes_updated: "0/0".to_string(),
897        update_action_status_modified_date: su.service_update_release_date.clone(),
898        sla_met: "n/a".to_string(),
899        estimated_update_time: su.estimated_update_time.clone(),
900        engine: su.engine.clone(),
901    }
902}
903
904fn default_reserved_cache_nodes_offerings() -> Vec<ReservedCacheNodesOffering> {
905    vec![
906        ReservedCacheNodesOffering {
907            reserved_cache_nodes_offering_id: "off-cache-t3-micro-redis-1yr-no-upfront".to_string(),
908            cache_node_type: "cache.t3.micro".to_string(),
909            duration: 31_536_000,
910            fixed_price: 0.0,
911            usage_price: 0.011,
912            product_description: "redis".to_string(),
913            offering_type: "No Upfront".to_string(),
914            recurring_charges: Vec::new(),
915        },
916        ReservedCacheNodesOffering {
917            reserved_cache_nodes_offering_id: "off-cache-t3-small-redis-1yr-partial-upfront"
918                .to_string(),
919            cache_node_type: "cache.t3.small".to_string(),
920            duration: 31_536_000,
921            fixed_price: 120.0,
922            usage_price: 0.007,
923            product_description: "redis".to_string(),
924            offering_type: "Partial Upfront".to_string(),
925            recurring_charges: Vec::new(),
926        },
927        ReservedCacheNodesOffering {
928            reserved_cache_nodes_offering_id: "off-cache-m5-large-memcached-3yr-no-upfront"
929                .to_string(),
930            cache_node_type: "cache.m5.large".to_string(),
931            duration: 94_608_000,
932            fixed_price: 0.0,
933            usage_price: 0.033,
934            product_description: "memcached".to_string(),
935            offering_type: "No Upfront".to_string(),
936            recurring_charges: Vec::new(),
937        },
938        ReservedCacheNodesOffering {
939            reserved_cache_nodes_offering_id: "off-cache-r6g-large-redis-3yr-all-upfront"
940                .to_string(),
941            cache_node_type: "cache.r6g.large".to_string(),
942            duration: 94_608_000,
943            fixed_price: 1_550.0,
944            usage_price: 0.0,
945            product_description: "redis".to_string(),
946            offering_type: "All Upfront".to_string(),
947            recurring_charges: vec![RecurringCharge {
948                recurring_charge_amount: 0.0,
949                recurring_charge_frequency: "Hourly".to_string(),
950            }],
951        },
952    ]
953}
954
955pub fn default_engine_versions() -> Vec<CacheEngineVersion> {
956    vec![
957        CacheEngineVersion {
958            engine: "redis".to_string(),
959            engine_version: "7.1".to_string(),
960            cache_parameter_group_family: "redis7".to_string(),
961            cache_engine_description: "Redis".to_string(),
962            cache_engine_version_description: "Redis 7.1".to_string(),
963        },
964        CacheEngineVersion {
965            engine: "valkey".to_string(),
966            engine_version: "8.0".to_string(),
967            cache_parameter_group_family: "valkey8".to_string(),
968            cache_engine_description: "Valkey".to_string(),
969            cache_engine_version_description: "Valkey 8.0".to_string(),
970        },
971        CacheEngineVersion {
972            engine: "memcached".to_string(),
973            engine_version: "1.6.22".to_string(),
974            cache_parameter_group_family: "memcached1.6".to_string(),
975            cache_engine_description: "Memcached".to_string(),
976            cache_engine_version_description: "Memcached 1.6.22".to_string(),
977        },
978    ]
979}
980
981fn default_parameter_groups(account_id: &str, region: &str) -> Vec<CacheParameterGroup> {
982    vec![
983        CacheParameterGroup {
984            cache_parameter_group_name: "default.redis7".to_string(),
985            cache_parameter_group_family: "redis7".to_string(),
986            description: "Default parameter group for redis7".to_string(),
987            is_global: false,
988            arn: Arn::new(
989                "elasticache",
990                region,
991                account_id,
992                "parametergroup:default.redis7",
993            )
994            .to_string(),
995        },
996        CacheParameterGroup {
997            cache_parameter_group_name: "default.valkey8".to_string(),
998            cache_parameter_group_family: "valkey8".to_string(),
999            description: "Default parameter group for valkey8".to_string(),
1000            is_global: false,
1001            arn: Arn::new(
1002                "elasticache",
1003                region,
1004                account_id,
1005                "parametergroup:default.valkey8",
1006            )
1007            .to_string(),
1008        },
1009        CacheParameterGroup {
1010            cache_parameter_group_name: "default.memcached1.6".to_string(),
1011            cache_parameter_group_family: "memcached1.6".to_string(),
1012            description: "Default parameter group for memcached1.6".to_string(),
1013            is_global: false,
1014            arn: Arn::new(
1015                "elasticache",
1016                region,
1017                account_id,
1018                "parametergroup:default.memcached1.6",
1019            )
1020            .to_string(),
1021        },
1022    ]
1023}
1024
1025fn default_subnet_groups(account_id: &str, region: &str) -> BTreeMap<String, CacheSubnetGroup> {
1026    let default_group = CacheSubnetGroup {
1027        cache_subnet_group_name: "default".to_string(),
1028        cache_subnet_group_description: "Default CacheSubnetGroup".to_string(),
1029        vpc_id: "vpc-00000000".to_string(),
1030        subnet_ids: vec!["subnet-00000000".to_string()],
1031        arn: Arn::new("elasticache", region, account_id, "subnetgroup:default").to_string(),
1032    };
1033    let mut map = BTreeMap::new();
1034    map.insert("default".to_string(), default_group);
1035    map
1036}
1037
1038pub fn default_parameters_for_family(family: &str) -> Vec<EngineDefaultParameter> {
1039    match family {
1040        "redis7" => vec![
1041            EngineDefaultParameter {
1042                parameter_name: "maxmemory-policy".to_string(),
1043                parameter_value: "volatile-lru".to_string(),
1044                description: "Max memory policy".to_string(),
1045                source: "system".to_string(),
1046                data_type: "string".to_string(),
1047                allowed_values: "volatile-lru,allkeys-lru,volatile-lfu,allkeys-lfu,volatile-random,allkeys-random,volatile-ttl,noeviction".to_string(),
1048                is_modifiable: true,
1049                minimum_engine_version: "7.0.0".to_string(),
1050            },
1051            EngineDefaultParameter {
1052                parameter_name: "cluster-enabled".to_string(),
1053                parameter_value: "no".to_string(),
1054                description: "Enable or disable Redis Cluster mode".to_string(),
1055                source: "system".to_string(),
1056                data_type: "string".to_string(),
1057                allowed_values: "yes,no".to_string(),
1058                is_modifiable: false,
1059                minimum_engine_version: "7.0.0".to_string(),
1060            },
1061            EngineDefaultParameter {
1062                parameter_name: "activedefrag".to_string(),
1063                parameter_value: "no".to_string(),
1064                description: "Enable active defragmentation".to_string(),
1065                source: "system".to_string(),
1066                data_type: "string".to_string(),
1067                allowed_values: "yes,no".to_string(),
1068                is_modifiable: true,
1069                minimum_engine_version: "7.0.0".to_string(),
1070            },
1071        ],
1072        "valkey8" => vec![
1073            EngineDefaultParameter {
1074                parameter_name: "maxmemory-policy".to_string(),
1075                parameter_value: "volatile-lru".to_string(),
1076                description: "Max memory policy".to_string(),
1077                source: "system".to_string(),
1078                data_type: "string".to_string(),
1079                allowed_values: "volatile-lru,allkeys-lru,volatile-lfu,allkeys-lfu,volatile-random,allkeys-random,volatile-ttl,noeviction".to_string(),
1080                is_modifiable: true,
1081                minimum_engine_version: "8.0.0".to_string(),
1082            },
1083            EngineDefaultParameter {
1084                parameter_name: "cluster-enabled".to_string(),
1085                parameter_value: "no".to_string(),
1086                description: "Enable or disable cluster mode".to_string(),
1087                source: "system".to_string(),
1088                data_type: "string".to_string(),
1089                allowed_values: "yes,no".to_string(),
1090                is_modifiable: false,
1091                minimum_engine_version: "8.0.0".to_string(),
1092            },
1093            EngineDefaultParameter {
1094                parameter_name: "activedefrag".to_string(),
1095                parameter_value: "no".to_string(),
1096                description: "Enable active defragmentation".to_string(),
1097                source: "system".to_string(),
1098                data_type: "string".to_string(),
1099                allowed_values: "yes,no".to_string(),
1100                is_modifiable: true,
1101                minimum_engine_version: "8.0.0".to_string(),
1102            },
1103        ],
1104        "memcached1.6" => vec![
1105            EngineDefaultParameter {
1106                parameter_name: "max_item_size".to_string(),
1107                parameter_value: "1048576".to_string(),
1108                description: "Maximum item size".to_string(),
1109                source: "system".to_string(),
1110                data_type: "integer".to_string(),
1111                allowed_values: "1048576-1073741824".to_string(),
1112                is_modifiable: true,
1113                minimum_engine_version: "1.4.5".to_string(),
1114            },
1115            EngineDefaultParameter {
1116                parameter_name: "max_simultaneous_connections".to_string(),
1117                parameter_value: "65000".to_string(),
1118                description: "Maximum number of concurrent connections".to_string(),
1119                source: "system".to_string(),
1120                data_type: "integer".to_string(),
1121                allowed_values: "1-65000".to_string(),
1122                is_modifiable: false,
1123                minimum_engine_version: "1.4.5".to_string(),
1124            },
1125        ],
1126        _ => Vec::new(),
1127    }
1128}
1129
1130fn default_users(account_id: &str, region: &str) -> BTreeMap<String, ElastiCacheUser> {
1131    let mut map = BTreeMap::new();
1132    map.insert(
1133        "default".to_string(),
1134        ElastiCacheUser {
1135            user_id: "default".to_string(),
1136            user_name: "default".to_string(),
1137            engine: "redis".to_string(),
1138            access_string: "on ~* +@all".to_string(),
1139            status: "active".to_string(),
1140            authentication_type: "no-password".to_string(),
1141            password_count: 0,
1142            arn: Arn::new("elasticache", region, account_id, "user:default").to_string(),
1143            minimum_engine_version: "6.0".to_string(),
1144            user_group_ids: Vec::new(),
1145        },
1146    );
1147    map
1148}
1149
1150pub const ELASTICACHE_SNAPSHOT_SCHEMA_VERSION: u32 = 2;
1151
1152#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1153pub struct ElastiCacheSnapshot {
1154    pub schema_version: u32,
1155    #[serde(default)]
1156    pub accounts: Option<fakecloud_core::multi_account::MultiAccountState<ElastiCacheState>>,
1157    #[serde(default)]
1158    pub state: Option<ElastiCacheState>,
1159}
1160
1161#[cfg(test)]
1162mod tests {
1163    use super::*;
1164
1165    #[test]
1166    fn default_engine_versions_contains_redis_valkey_memcached() {
1167        let versions = default_engine_versions();
1168        assert_eq!(versions.len(), 3);
1169        assert_eq!(versions[0].engine, "redis");
1170        assert_eq!(versions[0].engine_version, "7.1");
1171        assert_eq!(versions[1].engine, "valkey");
1172        assert_eq!(versions[1].engine_version, "8.0");
1173        assert_eq!(versions[2].engine, "memcached");
1174        assert_eq!(versions[2].engine_version, "1.6.22");
1175    }
1176
1177    #[test]
1178    fn state_new_creates_default_parameter_groups() {
1179        let state = ElastiCacheState::new("123456789012", "us-east-1");
1180        assert_eq!(state.parameter_groups.len(), 3);
1181        assert_eq!(
1182            state.parameter_groups[0].cache_parameter_group_name,
1183            "default.redis7"
1184        );
1185        assert_eq!(
1186            state.parameter_groups[1].cache_parameter_group_name,
1187            "default.valkey8"
1188        );
1189        assert_eq!(
1190            state.parameter_groups[2].cache_parameter_group_name,
1191            "default.memcached1.6"
1192        );
1193    }
1194
1195    #[test]
1196    fn state_new_creates_default_subnet_group() {
1197        let state = ElastiCacheState::new("123456789012", "us-east-1");
1198        assert_eq!(state.subnet_groups.len(), 1);
1199        let default = state.subnet_groups.get("default").unwrap();
1200        assert_eq!(default.cache_subnet_group_name, "default");
1201        assert_eq!(
1202            default.cache_subnet_group_description,
1203            "Default CacheSubnetGroup"
1204        );
1205        assert_eq!(default.vpc_id, "vpc-00000000");
1206        assert!(!default.subnet_ids.is_empty());
1207        assert!(default.arn.contains("subnetgroup:default"));
1208    }
1209
1210    #[test]
1211    fn reset_restores_default_parameter_groups() {
1212        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1213        state.parameter_groups.clear();
1214        assert!(state.parameter_groups.is_empty());
1215        state.reset();
1216        assert_eq!(state.parameter_groups.len(), 3);
1217    }
1218
1219    #[test]
1220    fn reset_restores_default_subnet_groups() {
1221        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1222        state.subnet_groups.clear();
1223        assert!(state.subnet_groups.is_empty());
1224        state.reset();
1225        assert_eq!(state.subnet_groups.len(), 1);
1226        assert!(state.subnet_groups.contains_key("default"));
1227    }
1228
1229    #[test]
1230    fn default_parameters_for_redis7_returns_parameters() {
1231        let params = default_parameters_for_family("redis7");
1232        assert_eq!(params.len(), 3);
1233        assert_eq!(params[0].parameter_name, "maxmemory-policy");
1234    }
1235
1236    #[test]
1237    fn default_parameters_for_unknown_family_returns_empty() {
1238        let params = default_parameters_for_family("unknown");
1239        assert!(params.is_empty());
1240    }
1241
1242    #[test]
1243    fn state_new_has_empty_replication_groups() {
1244        let state = ElastiCacheState::new("123456789012", "us-east-1");
1245        assert!(state.replication_groups.is_empty());
1246    }
1247
1248    #[test]
1249    fn state_new_has_empty_global_replication_groups() {
1250        let state = ElastiCacheState::new("123456789012", "us-east-1");
1251        assert!(state.global_replication_groups.is_empty());
1252    }
1253
1254    #[test]
1255    fn state_new_has_empty_cache_clusters() {
1256        let state = ElastiCacheState::new("123456789012", "us-east-1");
1257        assert!(state.cache_clusters.is_empty());
1258    }
1259
1260    #[test]
1261    fn state_new_has_empty_serverless_caches() {
1262        let state = ElastiCacheState::new("123456789012", "us-east-1");
1263        assert!(state.serverless_caches.is_empty());
1264        assert!(state.serverless_cache_snapshots.is_empty());
1265    }
1266
1267    #[test]
1268    fn begin_cache_cluster_creation_rejects_duplicate_ids() {
1269        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1270
1271        assert!(state.begin_cache_cluster_creation("cluster-1"));
1272        assert!(!state.begin_cache_cluster_creation("cluster-1"));
1273
1274        state.cancel_cache_cluster_creation("cluster-1");
1275        assert!(state.begin_cache_cluster_creation("cluster-1"));
1276    }
1277
1278    #[test]
1279    fn begin_replication_group_creation_rejects_duplicate_ids() {
1280        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1281
1282        assert!(state.begin_replication_group_creation("rg-1"));
1283        assert!(!state.begin_replication_group_creation("rg-1"));
1284
1285        state.cancel_replication_group_creation("rg-1");
1286        assert!(state.begin_replication_group_creation("rg-1"));
1287    }
1288
1289    #[test]
1290    fn begin_serverless_cache_creation_rejects_duplicate_names() {
1291        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1292
1293        assert!(state.begin_serverless_cache_creation("cache-1"));
1294        assert!(!state.begin_serverless_cache_creation("cache-1"));
1295
1296        state.cancel_serverless_cache_creation("cache-1");
1297        assert!(state.begin_serverless_cache_creation("cache-1"));
1298    }
1299
1300    #[test]
1301    fn finish_serverless_cache_creation_registers_cache_and_tags() {
1302        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1303        assert!(state.begin_serverless_cache_creation("cache-1"));
1304
1305        let cache = ServerlessCache {
1306            serverless_cache_name: "cache-1".to_string(),
1307            description: "test".to_string(),
1308            engine: "redis".to_string(),
1309            major_engine_version: "7.1".to_string(),
1310            full_engine_version: "7.1".to_string(),
1311            status: "available".to_string(),
1312            endpoint: ServerlessCacheEndpoint {
1313                address: "127.0.0.1".to_string(),
1314                port: 6379,
1315            },
1316            reader_endpoint: ServerlessCacheEndpoint {
1317                address: "127.0.0.1".to_string(),
1318                port: 6379,
1319            },
1320            arn: "arn:aws:elasticache:us-east-1:123456789012:serverlesscache:cache-1".to_string(),
1321            created_at: "2024-01-01T00:00:00Z".to_string(),
1322            cache_usage_limits: None,
1323            security_group_ids: Vec::new(),
1324            subnet_ids: Vec::new(),
1325            kms_key_id: None,
1326            user_group_id: None,
1327            snapshot_retention_limit: None,
1328            daily_snapshot_time: None,
1329            container_id: "cid".to_string(),
1330            host_port: 6379,
1331        };
1332
1333        state.finish_serverless_cache_creation(cache.clone());
1334
1335        assert!(state.serverless_caches.contains_key("cache-1"));
1336        assert!(state.tags.contains_key(&cache.arn));
1337    }
1338
1339    #[test]
1340    fn state_new_creates_default_user() {
1341        let state = ElastiCacheState::new("123456789012", "us-east-1");
1342        assert_eq!(state.users.len(), 1);
1343        let default = state.users.get("default").unwrap();
1344        assert_eq!(default.user_id, "default");
1345        assert_eq!(default.user_name, "default");
1346        assert_eq!(default.engine, "redis");
1347        assert_eq!(default.access_string, "on ~* +@all");
1348        assert_eq!(default.status, "active");
1349        assert_eq!(default.authentication_type, "no-password");
1350        assert_eq!(default.password_count, 0);
1351        assert!(default.arn.contains("user:default"));
1352    }
1353
1354    #[test]
1355    fn state_new_has_empty_user_groups() {
1356        let state = ElastiCacheState::new("123456789012", "us-east-1");
1357        assert!(state.user_groups.is_empty());
1358    }
1359
1360    #[test]
1361    fn reset_restores_default_user() {
1362        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1363        state.users.clear();
1364        assert!(state.users.is_empty());
1365        state.reset();
1366        assert_eq!(state.users.len(), 1);
1367        assert!(state.users.contains_key("default"));
1368    }
1369
1370    #[test]
1371    fn reset_clears_user_groups() {
1372        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1373        state.user_groups.insert(
1374            "my-group".to_string(),
1375            ElastiCacheUserGroup {
1376                user_group_id: "my-group".to_string(),
1377                engine: "redis".to_string(),
1378                status: "active".to_string(),
1379                user_ids: vec!["default".to_string()],
1380                arn: "arn:aws:elasticache:us-east-1:123456789012:usergroup:my-group".to_string(),
1381                minimum_engine_version: "6.0".to_string(),
1382                pending_changes: None,
1383                replication_groups: Vec::new(),
1384            },
1385        );
1386        assert_eq!(state.user_groups.len(), 1);
1387        state.reset();
1388        assert!(state.user_groups.is_empty());
1389    }
1390
1391    #[test]
1392    fn state_new_has_empty_snapshots() {
1393        let state = ElastiCacheState::new("123456789012", "us-east-1");
1394        assert!(state.snapshots.is_empty());
1395    }
1396
1397    #[test]
1398    fn reset_clears_snapshots() {
1399        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1400        state.snapshots.insert(
1401            "my-snapshot".to_string(),
1402            CacheSnapshot {
1403                snapshot_name: "my-snapshot".to_string(),
1404                replication_group_id: "rg-1".to_string(),
1405                replication_group_description: "test".to_string(),
1406                snapshot_status: "available".to_string(),
1407                cache_node_type: "cache.t3.micro".to_string(),
1408                engine: "redis".to_string(),
1409                engine_version: "7.1".to_string(),
1410                num_cache_clusters: 1,
1411                arn: "arn:aws:elasticache:us-east-1:123456789012:snapshot:my-snapshot".to_string(),
1412                created_at: "2024-01-01T00:00:00Z".to_string(),
1413                snapshot_source: "manual".to_string(),
1414                rdb_path: None,
1415            },
1416        );
1417        assert_eq!(state.snapshots.len(), 1);
1418        state.reset();
1419        assert!(state.snapshots.is_empty());
1420    }
1421
1422    #[test]
1423    fn reset_clears_replication_groups() {
1424        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1425        state.replication_groups.insert(
1426            "my-group".to_string(),
1427            ReplicationGroup {
1428                replication_group_id: "my-group".to_string(),
1429                description: "test".to_string(),
1430                global_replication_group_id: None,
1431                global_replication_group_role: None,
1432                status: "available".to_string(),
1433                cache_node_type: "cache.t3.micro".to_string(),
1434                engine: "redis".to_string(),
1435                engine_version: "7.1".to_string(),
1436                num_cache_clusters: 1,
1437                automatic_failover_enabled: false,
1438                endpoint_address: "127.0.0.1".to_string(),
1439                endpoint_port: 6379,
1440                arn: "arn:aws:elasticache:us-east-1:123456789012:replicationgroup:my-group"
1441                    .to_string(),
1442                created_at: "2024-01-01T00:00:00Z".to_string(),
1443                container_id: "abc123".to_string(),
1444                host_port: 12345,
1445                member_clusters: vec!["my-group-001".to_string()],
1446                snapshot_retention_limit: 0,
1447                snapshot_window: "05:00-09:00".to_string(),
1448                transit_encryption_enabled: false,
1449                at_rest_encryption_enabled: false,
1450                cluster_enabled: false,
1451                kms_key_id: None,
1452                auth_token_enabled: false,
1453                user_group_ids: Vec::new(),
1454                multi_az_enabled: false,
1455                log_delivery_configurations: Vec::new(),
1456                data_tiering: None,
1457                ip_discovery: None,
1458                network_type: None,
1459                transit_encryption_mode: None,
1460                num_node_groups: 1,
1461                configuration_endpoint_address: None,
1462                configuration_endpoint_port: None,
1463                replicas_per_node_group: None,
1464                auth_token: None,
1465                port: 6379,
1466                notification_topic_arn: None,
1467                cluster_mode: None,
1468                data_tiering_enabled: None,
1469                notification_topic_status: None,
1470                cache_parameter_group_name: None,
1471                cache_subnet_group_name: None,
1472                security_group_ids: Vec::new(),
1473                preferred_maintenance_window: None,
1474                snapshot_name: None,
1475                snapshot_arns: Vec::new(),
1476                auto_minor_version_upgrade: true,
1477            },
1478        );
1479        assert_eq!(state.replication_groups.len(), 1);
1480        state.reset();
1481        assert!(state.replication_groups.is_empty());
1482    }
1483
1484    #[test]
1485    fn reset_clears_global_replication_groups() {
1486        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1487        state.global_replication_groups.insert(
1488            "global-rg".to_string(),
1489            GlobalReplicationGroup {
1490                global_replication_group_id: "global-rg".to_string(),
1491                global_replication_group_description: "test".to_string(),
1492                status: "available".to_string(),
1493                cache_node_type: "cache.t3.micro".to_string(),
1494                engine: "redis".to_string(),
1495                engine_version: "7.1".to_string(),
1496                members: vec![GlobalReplicationGroupMember {
1497                    replication_group_id: "rg-1".to_string(),
1498                    replication_group_region: "us-east-1".to_string(),
1499                    role: "primary".to_string(),
1500                    automatic_failover: false,
1501                    status: "associated".to_string(),
1502                }],
1503                cluster_enabled: false,
1504                arn: "arn:aws:elasticache:us-east-1:123456789012:globalreplicationgroup:global-rg"
1505                    .to_string(),
1506                num_node_groups: 1,
1507            },
1508        );
1509        assert_eq!(state.global_replication_groups.len(), 1);
1510        state.reset();
1511        assert!(state.global_replication_groups.is_empty());
1512    }
1513
1514    #[test]
1515    fn reset_clears_cache_clusters() {
1516        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1517        state.cache_clusters.insert(
1518            "classic-cluster".to_string(),
1519            CacheCluster {
1520                cache_cluster_id: "classic-cluster".to_string(),
1521                cache_node_type: "cache.t3.micro".to_string(),
1522                engine: "redis".to_string(),
1523                engine_version: "7.1".to_string(),
1524                cache_cluster_status: "available".to_string(),
1525                num_cache_nodes: 1,
1526                preferred_availability_zone: "us-east-1a".to_string(),
1527                cache_subnet_group_name: Some("default".to_string()),
1528                auto_minor_version_upgrade: true,
1529                arn: "arn:aws:elasticache:us-east-1:123456789012:cluster:classic-cluster"
1530                    .to_string(),
1531                created_at: "2024-01-01T00:00:00Z".to_string(),
1532                endpoint_address: "127.0.0.1".to_string(),
1533                endpoint_port: 6379,
1534                container_id: "abc123".to_string(),
1535                host_port: 12345,
1536                replication_group_id: None,
1537                cache_parameter_group_name: None,
1538                security_group_ids: Vec::new(),
1539                log_delivery_configurations: Vec::new(),
1540                transit_encryption_enabled: false,
1541                at_rest_encryption_enabled: false,
1542                auth_token_enabled: false,
1543                port: 6379,
1544                preferred_maintenance_window: None,
1545                preferred_availability_zones: Vec::new(),
1546                notification_topic_arn: None,
1547                cache_security_group_names: Vec::new(),
1548                snapshot_arns: Vec::new(),
1549                snapshot_name: None,
1550                snapshot_retention_limit: 0,
1551                snapshot_window: None,
1552                outpost_mode: None,
1553                preferred_outpost_arn: None,
1554                network_type: None,
1555                ip_discovery: None,
1556                az_mode: None,
1557                auth_token: None,
1558                kms_key_id: None,
1559                transit_encryption_mode: None,
1560                data_tiering_enabled: None,
1561                cluster_mode: None,
1562                preferred_outpost_arns: Vec::new(),
1563            },
1564        );
1565        assert_eq!(state.cache_clusters.len(), 1);
1566        state.reset();
1567        assert!(state.cache_clusters.is_empty());
1568    }
1569
1570    #[test]
1571    fn reset_restores_reserved_cache_node_metadata() {
1572        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1573        state.reserved_cache_nodes.insert(
1574            "rcn-a".to_string(),
1575            ReservedCacheNode {
1576                reserved_cache_node_id: "rcn-a".to_string(),
1577                reserved_cache_nodes_offering_id: "offering-a".to_string(),
1578                cache_node_type: "cache.t3.micro".to_string(),
1579                start_time: "2024-01-01T00:00:00Z".to_string(),
1580                duration: 31_536_000,
1581                fixed_price: 0.0,
1582                usage_price: 0.011,
1583                cache_node_count: 1,
1584                product_description: "redis".to_string(),
1585                offering_type: "No Upfront".to_string(),
1586                state: "payment-pending".to_string(),
1587                recurring_charges: Vec::new(),
1588                reservation_arn:
1589                    "arn:aws:elasticache:us-east-1:123456789012:reserved-instance:test".to_string(),
1590            },
1591        );
1592        state.reserved_cache_nodes_offerings.clear();
1593
1594        state.reset();
1595
1596        assert!(state.reserved_cache_nodes.is_empty());
1597        assert!(!state.reserved_cache_nodes_offerings.is_empty());
1598    }
1599
1600    #[test]
1601    fn reset_clears_serverless_cache_state() {
1602        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1603        state.serverless_caches.insert(
1604            "serverless".to_string(),
1605            ServerlessCache {
1606                serverless_cache_name: "serverless".to_string(),
1607                description: "test".to_string(),
1608                engine: "redis".to_string(),
1609                major_engine_version: "7.1".to_string(),
1610                full_engine_version: "7.1".to_string(),
1611                status: "available".to_string(),
1612                endpoint: ServerlessCacheEndpoint {
1613                    address: "127.0.0.1".to_string(),
1614                    port: 6379,
1615                },
1616                reader_endpoint: ServerlessCacheEndpoint {
1617                    address: "127.0.0.1".to_string(),
1618                    port: 6379,
1619                },
1620                arn: "arn:aws:elasticache:us-east-1:123456789012:serverlesscache:serverless"
1621                    .to_string(),
1622                created_at: "2024-01-01T00:00:00Z".to_string(),
1623                cache_usage_limits: None,
1624                security_group_ids: Vec::new(),
1625                subnet_ids: Vec::new(),
1626                kms_key_id: None,
1627                user_group_id: None,
1628                snapshot_retention_limit: None,
1629                daily_snapshot_time: None,
1630                container_id: "cid".to_string(),
1631                host_port: 6379,
1632            },
1633        );
1634        state.serverless_cache_snapshots.insert(
1635            "snap-1".to_string(),
1636            ServerlessCacheSnapshot {
1637                serverless_cache_snapshot_name: "snap-1".to_string(),
1638                arn: "arn:aws:elasticache:us-east-1:123456789012:serverlesssnapshot:snap-1"
1639                    .to_string(),
1640                kms_key_id: None,
1641                snapshot_type: "manual".to_string(),
1642                status: "available".to_string(),
1643                create_time: "2024-01-01T00:00:00Z".to_string(),
1644                expiry_time: None,
1645                bytes_used_for_cache: None,
1646                serverless_cache_name: "serverless".to_string(),
1647                engine: "redis".to_string(),
1648                major_engine_version: "7.1".to_string(),
1649            },
1650        );
1651
1652        state.reset();
1653
1654        assert!(state.serverless_caches.is_empty());
1655        assert!(state.serverless_cache_snapshots.is_empty());
1656    }
1657}