Skip to main content

fakecloud_elasticache/
state.rs

1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3
4use fakecloud_aws::arn::Arn;
5use parking_lot::RwLock;
6
7pub type SharedElastiCacheState = Arc<RwLock<ElastiCacheState>>;
8
9#[derive(Debug, Clone)]
10pub struct CacheEngineVersion {
11    pub engine: String,
12    pub engine_version: String,
13    pub cache_parameter_group_family: String,
14    pub cache_engine_description: String,
15    pub cache_engine_version_description: String,
16}
17
18#[derive(Debug, Clone)]
19pub struct CacheParameterGroup {
20    pub cache_parameter_group_name: String,
21    pub cache_parameter_group_family: String,
22    pub description: String,
23    pub is_global: bool,
24    pub arn: String,
25}
26
27#[derive(Debug, Clone)]
28pub struct EngineDefaultParameter {
29    pub parameter_name: String,
30    pub parameter_value: String,
31    pub description: String,
32    pub source: String,
33    pub data_type: String,
34    pub allowed_values: String,
35    pub is_modifiable: bool,
36    pub minimum_engine_version: String,
37}
38
39#[derive(Debug, Clone)]
40pub struct CacheSubnetGroup {
41    pub cache_subnet_group_name: String,
42    pub cache_subnet_group_description: String,
43    pub vpc_id: String,
44    pub subnet_ids: Vec<String>,
45    pub arn: String,
46}
47
48#[derive(Debug, Clone)]
49pub struct RecurringCharge {
50    pub recurring_charge_amount: f64,
51    pub recurring_charge_frequency: String,
52}
53
54#[derive(Debug, Clone)]
55pub struct ReservedCacheNode {
56    pub reserved_cache_node_id: String,
57    pub reserved_cache_nodes_offering_id: String,
58    pub cache_node_type: String,
59    pub start_time: String,
60    pub duration: i32,
61    pub fixed_price: f64,
62    pub usage_price: f64,
63    pub cache_node_count: i32,
64    pub product_description: String,
65    pub offering_type: String,
66    pub state: String,
67    pub recurring_charges: Vec<RecurringCharge>,
68    pub reservation_arn: String,
69}
70
71#[derive(Debug, Clone)]
72pub struct ReservedCacheNodesOffering {
73    pub reserved_cache_nodes_offering_id: String,
74    pub cache_node_type: String,
75    pub duration: i32,
76    pub fixed_price: f64,
77    pub usage_price: f64,
78    pub product_description: String,
79    pub offering_type: String,
80    pub recurring_charges: Vec<RecurringCharge>,
81}
82
83#[derive(Debug, Clone)]
84pub struct CacheCluster {
85    pub cache_cluster_id: String,
86    pub cache_node_type: String,
87    pub engine: String,
88    pub engine_version: String,
89    pub cache_cluster_status: String,
90    pub num_cache_nodes: i32,
91    pub preferred_availability_zone: String,
92    pub cache_subnet_group_name: Option<String>,
93    pub auto_minor_version_upgrade: bool,
94    pub arn: String,
95    pub created_at: String,
96    pub endpoint_address: String,
97    pub endpoint_port: u16,
98    pub container_id: String,
99    pub host_port: u16,
100    pub replication_group_id: Option<String>,
101}
102
103#[derive(Debug, Clone)]
104pub struct ReplicationGroup {
105    pub replication_group_id: String,
106    pub description: String,
107    pub global_replication_group_id: Option<String>,
108    pub global_replication_group_role: Option<String>,
109    pub status: String,
110    pub cache_node_type: String,
111    pub engine: String,
112    pub engine_version: String,
113    pub num_cache_clusters: i32,
114    pub automatic_failover_enabled: bool,
115    pub endpoint_address: String,
116    pub endpoint_port: u16,
117    pub arn: String,
118    pub created_at: String,
119    pub container_id: String,
120    pub host_port: u16,
121    pub member_clusters: Vec<String>,
122    pub snapshot_retention_limit: i32,
123    pub snapshot_window: String,
124}
125
126#[derive(Debug, Clone)]
127pub struct GlobalReplicationGroupMember {
128    pub replication_group_id: String,
129    pub replication_group_region: String,
130    pub role: String,
131    pub automatic_failover: bool,
132    pub status: String,
133}
134
135#[derive(Debug, Clone)]
136pub struct GlobalReplicationGroup {
137    pub global_replication_group_id: String,
138    pub global_replication_group_description: String,
139    pub status: String,
140    pub cache_node_type: String,
141    pub engine: String,
142    pub engine_version: String,
143    pub members: Vec<GlobalReplicationGroupMember>,
144    pub cluster_enabled: bool,
145    pub arn: String,
146}
147
148#[derive(Debug, Clone)]
149pub struct ElastiCacheUser {
150    pub user_id: String,
151    pub user_name: String,
152    pub engine: String,
153    pub access_string: String,
154    pub status: String,
155    pub authentication_type: String,
156    pub password_count: i32,
157    pub arn: String,
158    pub minimum_engine_version: String,
159    pub user_group_ids: Vec<String>,
160}
161
162#[derive(Debug, Clone)]
163pub struct ElastiCacheUserGroup {
164    pub user_group_id: String,
165    pub engine: String,
166    pub status: String,
167    pub user_ids: Vec<String>,
168    pub arn: String,
169    pub minimum_engine_version: String,
170    pub pending_changes: Option<UserGroupPendingChanges>,
171    pub replication_groups: Vec<String>,
172}
173
174#[derive(Debug, Clone)]
175pub struct UserGroupPendingChanges {
176    pub user_ids_to_add: Vec<String>,
177    pub user_ids_to_remove: Vec<String>,
178}
179
180#[derive(Debug, Clone)]
181pub struct CacheSnapshot {
182    pub snapshot_name: String,
183    pub replication_group_id: String,
184    pub replication_group_description: String,
185    pub snapshot_status: String,
186    pub cache_node_type: String,
187    pub engine: String,
188    pub engine_version: String,
189    pub num_cache_clusters: i32,
190    pub arn: String,
191    pub created_at: String,
192    pub snapshot_source: String,
193}
194
195#[derive(Debug, Clone, Default)]
196pub struct ServerlessCacheUsageLimits {
197    pub data_storage: Option<ServerlessCacheDataStorage>,
198    pub ecpu_per_second: Option<ServerlessCacheEcpuPerSecond>,
199}
200
201#[derive(Debug, Clone)]
202pub struct ServerlessCacheDataStorage {
203    pub maximum: Option<i32>,
204    pub minimum: Option<i32>,
205    pub unit: Option<String>,
206}
207
208#[derive(Debug, Clone)]
209pub struct ServerlessCacheEcpuPerSecond {
210    pub maximum: Option<i32>,
211    pub minimum: Option<i32>,
212}
213
214#[derive(Debug, Clone)]
215pub struct ServerlessCacheEndpoint {
216    pub address: String,
217    pub port: u16,
218}
219
220#[derive(Debug, Clone)]
221pub struct ServerlessCache {
222    pub serverless_cache_name: String,
223    pub description: String,
224    pub engine: String,
225    pub major_engine_version: String,
226    pub full_engine_version: String,
227    pub status: String,
228    pub endpoint: ServerlessCacheEndpoint,
229    pub reader_endpoint: ServerlessCacheEndpoint,
230    pub arn: String,
231    pub created_at: String,
232    pub cache_usage_limits: Option<ServerlessCacheUsageLimits>,
233    pub security_group_ids: Vec<String>,
234    pub subnet_ids: Vec<String>,
235    pub kms_key_id: Option<String>,
236    pub user_group_id: Option<String>,
237    pub snapshot_retention_limit: Option<i32>,
238    pub daily_snapshot_time: Option<String>,
239    pub container_id: String,
240    pub host_port: u16,
241}
242
243#[derive(Debug, Clone)]
244pub struct ServerlessCacheSnapshot {
245    pub serverless_cache_snapshot_name: String,
246    pub arn: String,
247    pub kms_key_id: Option<String>,
248    pub snapshot_type: String,
249    pub status: String,
250    pub create_time: String,
251    pub expiry_time: Option<String>,
252    pub bytes_used_for_cache: Option<String>,
253    pub serverless_cache_name: String,
254    pub engine: String,
255    pub major_engine_version: String,
256}
257
258#[derive(Debug)]
259pub struct ElastiCacheState {
260    pub account_id: String,
261    pub region: String,
262    pub parameter_groups: Vec<CacheParameterGroup>,
263    pub subnet_groups: HashMap<String, CacheSubnetGroup>,
264    pub reserved_cache_nodes: HashMap<String, ReservedCacheNode>,
265    pub reserved_cache_nodes_offerings: Vec<ReservedCacheNodesOffering>,
266    pub cache_clusters: HashMap<String, CacheCluster>,
267    pub replication_groups: HashMap<String, ReplicationGroup>,
268    pub global_replication_groups: HashMap<String, GlobalReplicationGroup>,
269    pub users: HashMap<String, ElastiCacheUser>,
270    pub user_groups: HashMap<String, ElastiCacheUserGroup>,
271    pub snapshots: HashMap<String, CacheSnapshot>,
272    pub serverless_caches: HashMap<String, ServerlessCache>,
273    pub serverless_cache_snapshots: HashMap<String, ServerlessCacheSnapshot>,
274    pub tags: HashMap<String, Vec<(String, String)>>,
275    in_progress_cache_cluster_ids: HashSet<String>,
276    in_progress_replication_group_ids: HashSet<String>,
277    in_progress_serverless_cache_names: HashSet<String>,
278}
279
280impl ElastiCacheState {
281    pub fn new(account_id: &str, region: &str) -> Self {
282        let parameter_groups = default_parameter_groups(account_id, region);
283        let subnet_groups = default_subnet_groups(account_id, region);
284        let users = default_users(account_id, region);
285        let mut tags: HashMap<String, Vec<(String, String)>> = subnet_groups
286            .values()
287            .map(|g| (g.arn.clone(), Vec::new()))
288            .collect();
289        for user in users.values() {
290            tags.insert(user.arn.clone(), Vec::new());
291        }
292        Self {
293            account_id: account_id.to_string(),
294            region: region.to_string(),
295            parameter_groups,
296            subnet_groups,
297            reserved_cache_nodes: HashMap::new(),
298            reserved_cache_nodes_offerings: default_reserved_cache_nodes_offerings(),
299            cache_clusters: HashMap::new(),
300            replication_groups: HashMap::new(),
301            global_replication_groups: HashMap::new(),
302            users,
303            user_groups: HashMap::new(),
304            snapshots: HashMap::new(),
305            serverless_caches: HashMap::new(),
306            serverless_cache_snapshots: HashMap::new(),
307            tags,
308            in_progress_cache_cluster_ids: HashSet::new(),
309            in_progress_replication_group_ids: HashSet::new(),
310            in_progress_serverless_cache_names: HashSet::new(),
311        }
312    }
313
314    pub fn reset(&mut self) {
315        self.parameter_groups = default_parameter_groups(&self.account_id, &self.region);
316        self.subnet_groups = default_subnet_groups(&self.account_id, &self.region);
317        self.reserved_cache_nodes.clear();
318        self.reserved_cache_nodes_offerings = default_reserved_cache_nodes_offerings();
319        self.cache_clusters.clear();
320        self.replication_groups.clear();
321        self.global_replication_groups.clear();
322        self.users = default_users(&self.account_id, &self.region);
323        self.user_groups.clear();
324        self.snapshots.clear();
325        self.serverless_caches.clear();
326        self.serverless_cache_snapshots.clear();
327        self.tags.clear();
328        for g in self.subnet_groups.values() {
329            self.tags.insert(g.arn.clone(), Vec::new());
330        }
331        for user in self.users.values() {
332            self.tags.insert(user.arn.clone(), Vec::new());
333        }
334        self.in_progress_cache_cluster_ids.clear();
335        self.in_progress_replication_group_ids.clear();
336        self.in_progress_serverless_cache_names.clear();
337    }
338
339    pub fn begin_cache_cluster_creation(&mut self, cache_cluster_id: &str) -> bool {
340        if self.cache_clusters.contains_key(cache_cluster_id)
341            || self
342                .in_progress_cache_cluster_ids
343                .contains(cache_cluster_id)
344        {
345            return false;
346        }
347        self.in_progress_cache_cluster_ids
348            .insert(cache_cluster_id.to_string());
349        true
350    }
351
352    pub fn finish_cache_cluster_creation(&mut self, cluster: CacheCluster) {
353        self.in_progress_cache_cluster_ids
354            .remove(&cluster.cache_cluster_id);
355        self.tags.insert(cluster.arn.clone(), Vec::new());
356        self.cache_clusters
357            .insert(cluster.cache_cluster_id.clone(), cluster);
358    }
359
360    pub fn cancel_cache_cluster_creation(&mut self, cache_cluster_id: &str) {
361        self.in_progress_cache_cluster_ids.remove(cache_cluster_id);
362    }
363
364    pub fn begin_replication_group_creation(&mut self, replication_group_id: &str) -> bool {
365        if self.replication_groups.contains_key(replication_group_id)
366            || self
367                .in_progress_replication_group_ids
368                .contains(replication_group_id)
369        {
370            return false;
371        }
372        self.in_progress_replication_group_ids
373            .insert(replication_group_id.to_string());
374        true
375    }
376
377    pub fn finish_replication_group_creation(&mut self, group: ReplicationGroup) {
378        self.in_progress_replication_group_ids
379            .remove(&group.replication_group_id);
380        self.tags.insert(group.arn.clone(), Vec::new());
381        self.replication_groups
382            .insert(group.replication_group_id.clone(), group);
383    }
384
385    pub fn cancel_replication_group_creation(&mut self, replication_group_id: &str) {
386        self.in_progress_replication_group_ids
387            .remove(replication_group_id);
388    }
389
390    pub fn begin_serverless_cache_creation(&mut self, serverless_cache_name: &str) -> bool {
391        if self.serverless_caches.contains_key(serverless_cache_name)
392            || self
393                .in_progress_serverless_cache_names
394                .contains(serverless_cache_name)
395        {
396            return false;
397        }
398        self.in_progress_serverless_cache_names
399            .insert(serverless_cache_name.to_string());
400        true
401    }
402
403    pub fn finish_serverless_cache_creation(&mut self, cache: ServerlessCache) {
404        self.in_progress_serverless_cache_names
405            .remove(&cache.serverless_cache_name);
406        self.tags.insert(cache.arn.clone(), Vec::new());
407        self.serverless_caches
408            .insert(cache.serverless_cache_name.clone(), cache);
409    }
410
411    pub fn cancel_serverless_cache_creation(&mut self, serverless_cache_name: &str) {
412        self.in_progress_serverless_cache_names
413            .remove(serverless_cache_name);
414    }
415
416    pub fn register_arn(&mut self, arn: &str) {
417        self.tags.entry(arn.to_string()).or_default();
418    }
419
420    pub fn has_arn(&self, arn: &str) -> bool {
421        self.tags.contains_key(arn)
422    }
423}
424
425fn default_reserved_cache_nodes_offerings() -> Vec<ReservedCacheNodesOffering> {
426    vec![
427        ReservedCacheNodesOffering {
428            reserved_cache_nodes_offering_id: "off-cache-t3-micro-redis-1yr-no-upfront".to_string(),
429            cache_node_type: "cache.t3.micro".to_string(),
430            duration: 31_536_000,
431            fixed_price: 0.0,
432            usage_price: 0.011,
433            product_description: "redis".to_string(),
434            offering_type: "No Upfront".to_string(),
435            recurring_charges: Vec::new(),
436        },
437        ReservedCacheNodesOffering {
438            reserved_cache_nodes_offering_id: "off-cache-t3-small-redis-1yr-partial-upfront"
439                .to_string(),
440            cache_node_type: "cache.t3.small".to_string(),
441            duration: 31_536_000,
442            fixed_price: 120.0,
443            usage_price: 0.007,
444            product_description: "redis".to_string(),
445            offering_type: "Partial Upfront".to_string(),
446            recurring_charges: Vec::new(),
447        },
448        ReservedCacheNodesOffering {
449            reserved_cache_nodes_offering_id: "off-cache-m5-large-memcached-3yr-no-upfront"
450                .to_string(),
451            cache_node_type: "cache.m5.large".to_string(),
452            duration: 94_608_000,
453            fixed_price: 0.0,
454            usage_price: 0.033,
455            product_description: "memcached".to_string(),
456            offering_type: "No Upfront".to_string(),
457            recurring_charges: Vec::new(),
458        },
459        ReservedCacheNodesOffering {
460            reserved_cache_nodes_offering_id: "off-cache-r6g-large-redis-3yr-all-upfront"
461                .to_string(),
462            cache_node_type: "cache.r6g.large".to_string(),
463            duration: 94_608_000,
464            fixed_price: 1_550.0,
465            usage_price: 0.0,
466            product_description: "redis".to_string(),
467            offering_type: "All Upfront".to_string(),
468            recurring_charges: vec![RecurringCharge {
469                recurring_charge_amount: 0.0,
470                recurring_charge_frequency: "Hourly".to_string(),
471            }],
472        },
473    ]
474}
475
476pub fn default_engine_versions() -> Vec<CacheEngineVersion> {
477    vec![
478        CacheEngineVersion {
479            engine: "redis".to_string(),
480            engine_version: "7.1".to_string(),
481            cache_parameter_group_family: "redis7".to_string(),
482            cache_engine_description: "Redis".to_string(),
483            cache_engine_version_description: "Redis 7.1".to_string(),
484        },
485        CacheEngineVersion {
486            engine: "valkey".to_string(),
487            engine_version: "8.0".to_string(),
488            cache_parameter_group_family: "valkey8".to_string(),
489            cache_engine_description: "Valkey".to_string(),
490            cache_engine_version_description: "Valkey 8.0".to_string(),
491        },
492    ]
493}
494
495fn default_parameter_groups(account_id: &str, region: &str) -> Vec<CacheParameterGroup> {
496    vec![
497        CacheParameterGroup {
498            cache_parameter_group_name: "default.redis7".to_string(),
499            cache_parameter_group_family: "redis7".to_string(),
500            description: "Default parameter group for redis7".to_string(),
501            is_global: false,
502            arn: Arn::new(
503                "elasticache",
504                region,
505                account_id,
506                "parametergroup:default.redis7",
507            )
508            .to_string(),
509        },
510        CacheParameterGroup {
511            cache_parameter_group_name: "default.valkey8".to_string(),
512            cache_parameter_group_family: "valkey8".to_string(),
513            description: "Default parameter group for valkey8".to_string(),
514            is_global: false,
515            arn: Arn::new(
516                "elasticache",
517                region,
518                account_id,
519                "parametergroup:default.valkey8",
520            )
521            .to_string(),
522        },
523    ]
524}
525
526fn default_subnet_groups(account_id: &str, region: &str) -> HashMap<String, CacheSubnetGroup> {
527    let default_group = CacheSubnetGroup {
528        cache_subnet_group_name: "default".to_string(),
529        cache_subnet_group_description: "Default CacheSubnetGroup".to_string(),
530        vpc_id: "vpc-00000000".to_string(),
531        subnet_ids: vec!["subnet-00000000".to_string()],
532        arn: Arn::new("elasticache", region, account_id, "subnetgroup:default").to_string(),
533    };
534    let mut map = HashMap::new();
535    map.insert("default".to_string(), default_group);
536    map
537}
538
539pub fn default_parameters_for_family(family: &str) -> Vec<EngineDefaultParameter> {
540    match family {
541        "redis7" => vec![
542            EngineDefaultParameter {
543                parameter_name: "maxmemory-policy".to_string(),
544                parameter_value: "volatile-lru".to_string(),
545                description: "Max memory policy".to_string(),
546                source: "system".to_string(),
547                data_type: "string".to_string(),
548                allowed_values: "volatile-lru,allkeys-lru,volatile-lfu,allkeys-lfu,volatile-random,allkeys-random,volatile-ttl,noeviction".to_string(),
549                is_modifiable: true,
550                minimum_engine_version: "7.0.0".to_string(),
551            },
552            EngineDefaultParameter {
553                parameter_name: "cluster-enabled".to_string(),
554                parameter_value: "no".to_string(),
555                description: "Enable or disable Redis Cluster mode".to_string(),
556                source: "system".to_string(),
557                data_type: "string".to_string(),
558                allowed_values: "yes,no".to_string(),
559                is_modifiable: false,
560                minimum_engine_version: "7.0.0".to_string(),
561            },
562            EngineDefaultParameter {
563                parameter_name: "activedefrag".to_string(),
564                parameter_value: "no".to_string(),
565                description: "Enable active defragmentation".to_string(),
566                source: "system".to_string(),
567                data_type: "string".to_string(),
568                allowed_values: "yes,no".to_string(),
569                is_modifiable: true,
570                minimum_engine_version: "7.0.0".to_string(),
571            },
572        ],
573        "valkey8" => vec![
574            EngineDefaultParameter {
575                parameter_name: "maxmemory-policy".to_string(),
576                parameter_value: "volatile-lru".to_string(),
577                description: "Max memory policy".to_string(),
578                source: "system".to_string(),
579                data_type: "string".to_string(),
580                allowed_values: "volatile-lru,allkeys-lru,volatile-lfu,allkeys-lfu,volatile-random,allkeys-random,volatile-ttl,noeviction".to_string(),
581                is_modifiable: true,
582                minimum_engine_version: "8.0.0".to_string(),
583            },
584            EngineDefaultParameter {
585                parameter_name: "cluster-enabled".to_string(),
586                parameter_value: "no".to_string(),
587                description: "Enable or disable cluster mode".to_string(),
588                source: "system".to_string(),
589                data_type: "string".to_string(),
590                allowed_values: "yes,no".to_string(),
591                is_modifiable: false,
592                minimum_engine_version: "8.0.0".to_string(),
593            },
594            EngineDefaultParameter {
595                parameter_name: "activedefrag".to_string(),
596                parameter_value: "no".to_string(),
597                description: "Enable active defragmentation".to_string(),
598                source: "system".to_string(),
599                data_type: "string".to_string(),
600                allowed_values: "yes,no".to_string(),
601                is_modifiable: true,
602                minimum_engine_version: "8.0.0".to_string(),
603            },
604        ],
605        _ => Vec::new(),
606    }
607}
608
609fn default_users(account_id: &str, region: &str) -> HashMap<String, ElastiCacheUser> {
610    let mut map = HashMap::new();
611    map.insert(
612        "default".to_string(),
613        ElastiCacheUser {
614            user_id: "default".to_string(),
615            user_name: "default".to_string(),
616            engine: "redis".to_string(),
617            access_string: "on ~* +@all".to_string(),
618            status: "active".to_string(),
619            authentication_type: "no-password".to_string(),
620            password_count: 0,
621            arn: Arn::new("elasticache", region, account_id, "user:default").to_string(),
622            minimum_engine_version: "6.0".to_string(),
623            user_group_ids: Vec::new(),
624        },
625    );
626    map
627}
628
629#[cfg(test)]
630mod tests {
631    use super::*;
632
633    #[test]
634    fn default_engine_versions_contains_redis_and_valkey() {
635        let versions = default_engine_versions();
636        assert_eq!(versions.len(), 2);
637        assert_eq!(versions[0].engine, "redis");
638        assert_eq!(versions[0].engine_version, "7.1");
639        assert_eq!(versions[1].engine, "valkey");
640        assert_eq!(versions[1].engine_version, "8.0");
641    }
642
643    #[test]
644    fn state_new_creates_default_parameter_groups() {
645        let state = ElastiCacheState::new("123456789012", "us-east-1");
646        assert_eq!(state.parameter_groups.len(), 2);
647        assert_eq!(
648            state.parameter_groups[0].cache_parameter_group_name,
649            "default.redis7"
650        );
651        assert_eq!(
652            state.parameter_groups[1].cache_parameter_group_name,
653            "default.valkey8"
654        );
655    }
656
657    #[test]
658    fn state_new_creates_default_subnet_group() {
659        let state = ElastiCacheState::new("123456789012", "us-east-1");
660        assert_eq!(state.subnet_groups.len(), 1);
661        let default = state.subnet_groups.get("default").unwrap();
662        assert_eq!(default.cache_subnet_group_name, "default");
663        assert_eq!(
664            default.cache_subnet_group_description,
665            "Default CacheSubnetGroup"
666        );
667        assert_eq!(default.vpc_id, "vpc-00000000");
668        assert!(!default.subnet_ids.is_empty());
669        assert!(default.arn.contains("subnetgroup:default"));
670    }
671
672    #[test]
673    fn reset_restores_default_parameter_groups() {
674        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
675        state.parameter_groups.clear();
676        assert!(state.parameter_groups.is_empty());
677        state.reset();
678        assert_eq!(state.parameter_groups.len(), 2);
679    }
680
681    #[test]
682    fn reset_restores_default_subnet_groups() {
683        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
684        state.subnet_groups.clear();
685        assert!(state.subnet_groups.is_empty());
686        state.reset();
687        assert_eq!(state.subnet_groups.len(), 1);
688        assert!(state.subnet_groups.contains_key("default"));
689    }
690
691    #[test]
692    fn default_parameters_for_redis7_returns_parameters() {
693        let params = default_parameters_for_family("redis7");
694        assert_eq!(params.len(), 3);
695        assert_eq!(params[0].parameter_name, "maxmemory-policy");
696    }
697
698    #[test]
699    fn default_parameters_for_unknown_family_returns_empty() {
700        let params = default_parameters_for_family("unknown");
701        assert!(params.is_empty());
702    }
703
704    #[test]
705    fn state_new_has_empty_replication_groups() {
706        let state = ElastiCacheState::new("123456789012", "us-east-1");
707        assert!(state.replication_groups.is_empty());
708    }
709
710    #[test]
711    fn state_new_has_empty_global_replication_groups() {
712        let state = ElastiCacheState::new("123456789012", "us-east-1");
713        assert!(state.global_replication_groups.is_empty());
714    }
715
716    #[test]
717    fn state_new_has_empty_cache_clusters() {
718        let state = ElastiCacheState::new("123456789012", "us-east-1");
719        assert!(state.cache_clusters.is_empty());
720    }
721
722    #[test]
723    fn state_new_has_empty_serverless_caches() {
724        let state = ElastiCacheState::new("123456789012", "us-east-1");
725        assert!(state.serverless_caches.is_empty());
726        assert!(state.serverless_cache_snapshots.is_empty());
727    }
728
729    #[test]
730    fn begin_cache_cluster_creation_rejects_duplicate_ids() {
731        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
732
733        assert!(state.begin_cache_cluster_creation("cluster-1"));
734        assert!(!state.begin_cache_cluster_creation("cluster-1"));
735
736        state.cancel_cache_cluster_creation("cluster-1");
737        assert!(state.begin_cache_cluster_creation("cluster-1"));
738    }
739
740    #[test]
741    fn begin_replication_group_creation_rejects_duplicate_ids() {
742        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
743
744        assert!(state.begin_replication_group_creation("rg-1"));
745        assert!(!state.begin_replication_group_creation("rg-1"));
746
747        state.cancel_replication_group_creation("rg-1");
748        assert!(state.begin_replication_group_creation("rg-1"));
749    }
750
751    #[test]
752    fn begin_serverless_cache_creation_rejects_duplicate_names() {
753        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
754
755        assert!(state.begin_serverless_cache_creation("cache-1"));
756        assert!(!state.begin_serverless_cache_creation("cache-1"));
757
758        state.cancel_serverless_cache_creation("cache-1");
759        assert!(state.begin_serverless_cache_creation("cache-1"));
760    }
761
762    #[test]
763    fn finish_serverless_cache_creation_registers_cache_and_tags() {
764        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
765        assert!(state.begin_serverless_cache_creation("cache-1"));
766
767        let cache = ServerlessCache {
768            serverless_cache_name: "cache-1".to_string(),
769            description: "test".to_string(),
770            engine: "redis".to_string(),
771            major_engine_version: "7.1".to_string(),
772            full_engine_version: "7.1".to_string(),
773            status: "available".to_string(),
774            endpoint: ServerlessCacheEndpoint {
775                address: "127.0.0.1".to_string(),
776                port: 6379,
777            },
778            reader_endpoint: ServerlessCacheEndpoint {
779                address: "127.0.0.1".to_string(),
780                port: 6379,
781            },
782            arn: "arn:aws:elasticache:us-east-1:123456789012:serverlesscache:cache-1".to_string(),
783            created_at: "2024-01-01T00:00:00Z".to_string(),
784            cache_usage_limits: None,
785            security_group_ids: Vec::new(),
786            subnet_ids: Vec::new(),
787            kms_key_id: None,
788            user_group_id: None,
789            snapshot_retention_limit: None,
790            daily_snapshot_time: None,
791            container_id: "cid".to_string(),
792            host_port: 6379,
793        };
794
795        state.finish_serverless_cache_creation(cache.clone());
796
797        assert!(state.serverless_caches.contains_key("cache-1"));
798        assert!(state.tags.contains_key(&cache.arn));
799    }
800
801    #[test]
802    fn state_new_creates_default_user() {
803        let state = ElastiCacheState::new("123456789012", "us-east-1");
804        assert_eq!(state.users.len(), 1);
805        let default = state.users.get("default").unwrap();
806        assert_eq!(default.user_id, "default");
807        assert_eq!(default.user_name, "default");
808        assert_eq!(default.engine, "redis");
809        assert_eq!(default.access_string, "on ~* +@all");
810        assert_eq!(default.status, "active");
811        assert_eq!(default.authentication_type, "no-password");
812        assert_eq!(default.password_count, 0);
813        assert!(default.arn.contains("user:default"));
814    }
815
816    #[test]
817    fn state_new_has_empty_user_groups() {
818        let state = ElastiCacheState::new("123456789012", "us-east-1");
819        assert!(state.user_groups.is_empty());
820    }
821
822    #[test]
823    fn reset_restores_default_user() {
824        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
825        state.users.clear();
826        assert!(state.users.is_empty());
827        state.reset();
828        assert_eq!(state.users.len(), 1);
829        assert!(state.users.contains_key("default"));
830    }
831
832    #[test]
833    fn reset_clears_user_groups() {
834        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
835        state.user_groups.insert(
836            "my-group".to_string(),
837            ElastiCacheUserGroup {
838                user_group_id: "my-group".to_string(),
839                engine: "redis".to_string(),
840                status: "active".to_string(),
841                user_ids: vec!["default".to_string()],
842                arn: "arn:aws:elasticache:us-east-1:123456789012:usergroup:my-group".to_string(),
843                minimum_engine_version: "6.0".to_string(),
844                pending_changes: None,
845                replication_groups: Vec::new(),
846            },
847        );
848        assert_eq!(state.user_groups.len(), 1);
849        state.reset();
850        assert!(state.user_groups.is_empty());
851    }
852
853    #[test]
854    fn state_new_has_empty_snapshots() {
855        let state = ElastiCacheState::new("123456789012", "us-east-1");
856        assert!(state.snapshots.is_empty());
857    }
858
859    #[test]
860    fn reset_clears_snapshots() {
861        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
862        state.snapshots.insert(
863            "my-snapshot".to_string(),
864            CacheSnapshot {
865                snapshot_name: "my-snapshot".to_string(),
866                replication_group_id: "rg-1".to_string(),
867                replication_group_description: "test".to_string(),
868                snapshot_status: "available".to_string(),
869                cache_node_type: "cache.t3.micro".to_string(),
870                engine: "redis".to_string(),
871                engine_version: "7.1".to_string(),
872                num_cache_clusters: 1,
873                arn: "arn:aws:elasticache:us-east-1:123456789012:snapshot:my-snapshot".to_string(),
874                created_at: "2024-01-01T00:00:00Z".to_string(),
875                snapshot_source: "manual".to_string(),
876            },
877        );
878        assert_eq!(state.snapshots.len(), 1);
879        state.reset();
880        assert!(state.snapshots.is_empty());
881    }
882
883    #[test]
884    fn reset_clears_replication_groups() {
885        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
886        state.replication_groups.insert(
887            "my-group".to_string(),
888            ReplicationGroup {
889                replication_group_id: "my-group".to_string(),
890                description: "test".to_string(),
891                global_replication_group_id: None,
892                global_replication_group_role: None,
893                status: "available".to_string(),
894                cache_node_type: "cache.t3.micro".to_string(),
895                engine: "redis".to_string(),
896                engine_version: "7.1".to_string(),
897                num_cache_clusters: 1,
898                automatic_failover_enabled: false,
899                endpoint_address: "127.0.0.1".to_string(),
900                endpoint_port: 6379,
901                arn: "arn:aws:elasticache:us-east-1:123456789012:replicationgroup:my-group"
902                    .to_string(),
903                created_at: "2024-01-01T00:00:00Z".to_string(),
904                container_id: "abc123".to_string(),
905                host_port: 12345,
906                member_clusters: vec!["my-group-001".to_string()],
907                snapshot_retention_limit: 0,
908                snapshot_window: "05:00-09:00".to_string(),
909            },
910        );
911        assert_eq!(state.replication_groups.len(), 1);
912        state.reset();
913        assert!(state.replication_groups.is_empty());
914    }
915
916    #[test]
917    fn reset_clears_global_replication_groups() {
918        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
919        state.global_replication_groups.insert(
920            "global-rg".to_string(),
921            GlobalReplicationGroup {
922                global_replication_group_id: "global-rg".to_string(),
923                global_replication_group_description: "test".to_string(),
924                status: "available".to_string(),
925                cache_node_type: "cache.t3.micro".to_string(),
926                engine: "redis".to_string(),
927                engine_version: "7.1".to_string(),
928                members: vec![GlobalReplicationGroupMember {
929                    replication_group_id: "rg-1".to_string(),
930                    replication_group_region: "us-east-1".to_string(),
931                    role: "primary".to_string(),
932                    automatic_failover: false,
933                    status: "associated".to_string(),
934                }],
935                cluster_enabled: false,
936                arn: "arn:aws:elasticache:us-east-1:123456789012:globalreplicationgroup:global-rg"
937                    .to_string(),
938            },
939        );
940        assert_eq!(state.global_replication_groups.len(), 1);
941        state.reset();
942        assert!(state.global_replication_groups.is_empty());
943    }
944
945    #[test]
946    fn reset_clears_cache_clusters() {
947        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
948        state.cache_clusters.insert(
949            "classic-cluster".to_string(),
950            CacheCluster {
951                cache_cluster_id: "classic-cluster".to_string(),
952                cache_node_type: "cache.t3.micro".to_string(),
953                engine: "redis".to_string(),
954                engine_version: "7.1".to_string(),
955                cache_cluster_status: "available".to_string(),
956                num_cache_nodes: 1,
957                preferred_availability_zone: "us-east-1a".to_string(),
958                cache_subnet_group_name: Some("default".to_string()),
959                auto_minor_version_upgrade: true,
960                arn: "arn:aws:elasticache:us-east-1:123456789012:cluster:classic-cluster"
961                    .to_string(),
962                created_at: "2024-01-01T00:00:00Z".to_string(),
963                endpoint_address: "127.0.0.1".to_string(),
964                endpoint_port: 6379,
965                container_id: "abc123".to_string(),
966                host_port: 12345,
967                replication_group_id: None,
968            },
969        );
970        assert_eq!(state.cache_clusters.len(), 1);
971        state.reset();
972        assert!(state.cache_clusters.is_empty());
973    }
974
975    #[test]
976    fn reset_restores_reserved_cache_node_metadata() {
977        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
978        state.reserved_cache_nodes.insert(
979            "rcn-a".to_string(),
980            ReservedCacheNode {
981                reserved_cache_node_id: "rcn-a".to_string(),
982                reserved_cache_nodes_offering_id: "offering-a".to_string(),
983                cache_node_type: "cache.t3.micro".to_string(),
984                start_time: "2024-01-01T00:00:00Z".to_string(),
985                duration: 31_536_000,
986                fixed_price: 0.0,
987                usage_price: 0.011,
988                cache_node_count: 1,
989                product_description: "redis".to_string(),
990                offering_type: "No Upfront".to_string(),
991                state: "payment-pending".to_string(),
992                recurring_charges: Vec::new(),
993                reservation_arn:
994                    "arn:aws:elasticache:us-east-1:123456789012:reserved-instance:test".to_string(),
995            },
996        );
997        state.reserved_cache_nodes_offerings.clear();
998
999        state.reset();
1000
1001        assert!(state.reserved_cache_nodes.is_empty());
1002        assert!(!state.reserved_cache_nodes_offerings.is_empty());
1003    }
1004
1005    #[test]
1006    fn reset_clears_serverless_cache_state() {
1007        let mut state = ElastiCacheState::new("123456789012", "us-east-1");
1008        state.serverless_caches.insert(
1009            "serverless".to_string(),
1010            ServerlessCache {
1011                serverless_cache_name: "serverless".to_string(),
1012                description: "test".to_string(),
1013                engine: "redis".to_string(),
1014                major_engine_version: "7.1".to_string(),
1015                full_engine_version: "7.1".to_string(),
1016                status: "available".to_string(),
1017                endpoint: ServerlessCacheEndpoint {
1018                    address: "127.0.0.1".to_string(),
1019                    port: 6379,
1020                },
1021                reader_endpoint: ServerlessCacheEndpoint {
1022                    address: "127.0.0.1".to_string(),
1023                    port: 6379,
1024                },
1025                arn: "arn:aws:elasticache:us-east-1:123456789012:serverlesscache:serverless"
1026                    .to_string(),
1027                created_at: "2024-01-01T00:00:00Z".to_string(),
1028                cache_usage_limits: None,
1029                security_group_ids: Vec::new(),
1030                subnet_ids: Vec::new(),
1031                kms_key_id: None,
1032                user_group_id: None,
1033                snapshot_retention_limit: None,
1034                daily_snapshot_time: None,
1035                container_id: "cid".to_string(),
1036                host_port: 6379,
1037            },
1038        );
1039        state.serverless_cache_snapshots.insert(
1040            "snap-1".to_string(),
1041            ServerlessCacheSnapshot {
1042                serverless_cache_snapshot_name: "snap-1".to_string(),
1043                arn: "arn:aws:elasticache:us-east-1:123456789012:serverlesssnapshot:snap-1"
1044                    .to_string(),
1045                kms_key_id: None,
1046                snapshot_type: "manual".to_string(),
1047                status: "available".to_string(),
1048                create_time: "2024-01-01T00:00:00Z".to_string(),
1049                expiry_time: None,
1050                bytes_used_for_cache: None,
1051                serverless_cache_name: "serverless".to_string(),
1052                engine: "redis".to_string(),
1053                major_engine_version: "7.1".to_string(),
1054            },
1055        );
1056
1057        state.reset();
1058
1059        assert!(state.serverless_caches.is_empty());
1060        assert!(state.serverless_cache_snapshots.is_empty());
1061    }
1062}