Skip to main content

fakecloud_rds/
state.rs

1use std::collections::{HashMap, HashSet};
2use std::fmt;
3use std::sync::Arc;
4
5use chrono::{DateTime, Utc};
6use fakecloud_aws::arn::Arn;
7use parking_lot::RwLock;
8use uuid::Uuid;
9
10pub type SharedRdsState = Arc<RwLock<fakecloud_core::multi_account::MultiAccountState<RdsState>>>;
11
12impl fakecloud_core::multi_account::AccountState for RdsState {
13    fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
14        Self::new(account_id, region)
15    }
16}
17
18/// Supported DB instance classes — single source of truth.
19pub const SUPPORTED_INSTANCE_CLASSES: &[&str] = &[
20    "db.t3.micro",
21    "db.t3.small",
22    "db.t3.medium",
23    "db.t3.large",
24    "db.t4g.micro",
25    "db.t4g.small",
26    "db.m5.large",
27];
28
29#[derive(Clone, serde::Serialize, serde::Deserialize)]
30pub struct DbInstance {
31    pub db_instance_identifier: String,
32    pub db_instance_arn: String,
33    pub db_instance_class: String,
34    pub engine: String,
35    pub engine_version: String,
36    pub db_instance_status: String,
37    pub master_username: String,
38    pub db_name: Option<String>,
39    pub endpoint_address: String,
40    pub port: i32,
41    pub allocated_storage: i32,
42    pub publicly_accessible: bool,
43    pub deletion_protection: bool,
44    pub created_at: DateTime<Utc>,
45    pub dbi_resource_id: String,
46    pub master_user_password: String,
47    pub container_id: String,
48    pub host_port: u16,
49    pub tags: Vec<RdsTag>,
50    pub read_replica_source_db_instance_identifier: Option<String>,
51    pub read_replica_db_instance_identifiers: Vec<String>,
52    pub vpc_security_group_ids: Vec<String>,
53    pub db_parameter_group_name: Option<String>,
54    pub backup_retention_period: i32,
55    pub preferred_backup_window: String,
56    pub latest_restorable_time: Option<DateTime<Utc>>,
57    pub option_group_name: Option<String>,
58    pub multi_az: bool,
59    pub pending_modified_values: Option<PendingModifiedValues>,
60}
61
62#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
63pub struct PendingModifiedValues {
64    pub db_instance_class: Option<String>,
65    pub allocated_storage: Option<i32>,
66    pub backup_retention_period: Option<i32>,
67    pub multi_az: Option<bool>,
68    pub engine_version: Option<String>,
69    pub master_user_password: Option<String>,
70}
71
72impl fmt::Debug for PendingModifiedValues {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        f.debug_struct("PendingModifiedValues")
75            .field("db_instance_class", &self.db_instance_class)
76            .field("allocated_storage", &self.allocated_storage)
77            .field("backup_retention_period", &self.backup_retention_period)
78            .field("multi_az", &self.multi_az)
79            .field("engine_version", &self.engine_version)
80            .field(
81                "master_user_password",
82                &self.master_user_password.as_ref().map(|_| "<redacted>"),
83            )
84            .finish()
85    }
86}
87
88impl fmt::Debug for DbInstance {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        f.debug_struct("DbInstance")
91            .field("db_instance_identifier", &self.db_instance_identifier)
92            .field("db_instance_arn", &self.db_instance_arn)
93            .field("db_instance_class", &self.db_instance_class)
94            .field("engine", &self.engine)
95            .field("engine_version", &self.engine_version)
96            .field("db_instance_status", &self.db_instance_status)
97            .field("master_username", &self.master_username)
98            .field("db_name", &self.db_name)
99            .field("endpoint_address", &self.endpoint_address)
100            .field("port", &self.port)
101            .field("allocated_storage", &self.allocated_storage)
102            .field("publicly_accessible", &self.publicly_accessible)
103            .field("deletion_protection", &self.deletion_protection)
104            .field("created_at", &self.created_at)
105            .field("dbi_resource_id", &self.dbi_resource_id)
106            .field("master_user_password", &"<redacted>")
107            .field("container_id", &self.container_id)
108            .field("host_port", &self.host_port)
109            .field("tags", &self.tags)
110            .field(
111                "read_replica_source_db_instance_identifier",
112                &self.read_replica_source_db_instance_identifier,
113            )
114            .field(
115                "read_replica_db_instance_identifiers",
116                &self.read_replica_db_instance_identifiers,
117            )
118            .field("vpc_security_group_ids", &self.vpc_security_group_ids)
119            .field("db_parameter_group_name", &self.db_parameter_group_name)
120            .field("backup_retention_period", &self.backup_retention_period)
121            .field("preferred_backup_window", &self.preferred_backup_window)
122            .field("latest_restorable_time", &self.latest_restorable_time)
123            .field("option_group_name", &self.option_group_name)
124            .field("multi_az", &self.multi_az)
125            .field("pending_modified_values", &self.pending_modified_values)
126            .finish()
127    }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
131pub struct RdsTag {
132    pub key: String,
133    pub value: String,
134}
135
136#[derive(Clone, serde::Serialize, serde::Deserialize)]
137pub struct DbSnapshot {
138    pub db_snapshot_identifier: String,
139    pub db_snapshot_arn: String,
140    pub db_instance_identifier: String,
141    pub snapshot_create_time: DateTime<Utc>,
142    pub engine: String,
143    pub engine_version: String,
144    pub allocated_storage: i32,
145    pub status: String,
146    pub port: i32,
147    pub master_username: String,
148    pub db_name: Option<String>,
149    pub dbi_resource_id: String,
150    pub snapshot_type: String,
151    pub master_user_password: String,
152    pub tags: Vec<RdsTag>,
153    pub dump_data: Vec<u8>,
154}
155
156impl fmt::Debug for DbSnapshot {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        f.debug_struct("DbSnapshot")
159            .field("db_snapshot_identifier", &self.db_snapshot_identifier)
160            .field("db_snapshot_arn", &self.db_snapshot_arn)
161            .field("db_instance_identifier", &self.db_instance_identifier)
162            .field("snapshot_create_time", &self.snapshot_create_time)
163            .field("engine", &self.engine)
164            .field("engine_version", &self.engine_version)
165            .field("allocated_storage", &self.allocated_storage)
166            .field("status", &self.status)
167            .field("port", &self.port)
168            .field("master_username", &self.master_username)
169            .field("db_name", &self.db_name)
170            .field("dbi_resource_id", &self.dbi_resource_id)
171            .field("snapshot_type", &self.snapshot_type)
172            .field("master_user_password", &"<redacted>")
173            .field("tags", &self.tags)
174            .field("dump_data", &format!("<{} bytes>", self.dump_data.len()))
175            .finish()
176    }
177}
178
179#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
180pub struct RdsState {
181    pub account_id: String,
182    pub region: String,
183    pub instances: HashMap<String, DbInstance>,
184    pub in_progress_instance_ids: HashSet<String>,
185    pub snapshots: HashMap<String, DbSnapshot>,
186    pub subnet_groups: HashMap<String, DbSubnetGroup>,
187    pub parameter_groups: HashMap<String, DbParameterGroup>,
188    /// Generic stores keyed by category (clusters, cluster_snapshots,
189    /// cluster_param_groups, proxies, proxy_endpoints, security_groups,
190    /// option_groups, event_subscriptions, global_clusters, integrations,
191    /// blue_green, shard_groups, custom_engine_versions, tenant_dbs,
192    /// export_tasks, etc.) so the extras handlers can persist state
193    /// without proliferating per-category fields.
194    #[serde(default)]
195    pub extras: HashMap<String, HashMap<String, serde_json::Value>>,
196}
197
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub struct EngineVersionInfo {
200    pub engine: String,
201    pub engine_version: String,
202    pub db_parameter_group_family: String,
203    pub db_engine_description: String,
204    pub db_engine_version_description: String,
205    pub status: String,
206}
207
208#[derive(Debug, Clone, PartialEq, Eq)]
209pub struct OrderableDbInstanceOption {
210    pub engine: String,
211    pub engine_version: String,
212    pub db_instance_class: String,
213    pub license_model: String,
214    pub storage_type: String,
215    pub min_storage_size: i32,
216    pub max_storage_size: i32,
217}
218
219#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
220pub struct DbSubnetGroup {
221    pub db_subnet_group_name: String,
222    pub db_subnet_group_arn: String,
223    pub db_subnet_group_description: String,
224    pub vpc_id: String,
225    pub subnet_ids: Vec<String>,
226    pub subnet_availability_zones: Vec<String>,
227    pub tags: Vec<RdsTag>,
228}
229
230#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
231pub struct DbParameterGroup {
232    pub db_parameter_group_name: String,
233    pub db_parameter_group_arn: String,
234    pub db_parameter_group_family: String,
235    pub description: String,
236    pub parameters: HashMap<String, String>,
237    pub tags: Vec<RdsTag>,
238}
239
240impl RdsState {
241    pub fn new(account_id: &str, region: &str) -> Self {
242        Self {
243            account_id: account_id.to_string(),
244            region: region.to_string(),
245            instances: HashMap::new(),
246            in_progress_instance_ids: HashSet::new(),
247            snapshots: HashMap::new(),
248            subnet_groups: HashMap::new(),
249            parameter_groups: default_parameter_groups(account_id, region),
250            extras: HashMap::new(),
251        }
252    }
253
254    pub fn reset(&mut self) {
255        self.instances.clear();
256        self.in_progress_instance_ids.clear();
257        self.snapshots.clear();
258        self.subnet_groups.clear();
259        self.parameter_groups = default_parameter_groups(&self.account_id, &self.region);
260        self.extras.clear();
261    }
262
263    pub fn db_instance_arn(&self, db_instance_identifier: &str) -> String {
264        Arn::new(
265            "rds",
266            &self.region,
267            &self.account_id,
268            &format!("db:{db_instance_identifier}"),
269        )
270        .to_string()
271    }
272
273    pub fn db_snapshot_arn(&self, db_snapshot_identifier: &str) -> String {
274        Arn::new(
275            "rds",
276            &self.region,
277            &self.account_id,
278            &format!("snapshot:{db_snapshot_identifier}"),
279        )
280        .to_string()
281    }
282
283    pub fn db_subnet_group_arn(&self, db_subnet_group_name: &str) -> String {
284        Arn::new(
285            "rds",
286            &self.region,
287            &self.account_id,
288            &format!("subgrp:{db_subnet_group_name}"),
289        )
290        .to_string()
291    }
292
293    pub fn db_parameter_group_arn(&self, db_parameter_group_name: &str) -> String {
294        Arn::new(
295            "rds",
296            &self.region,
297            &self.account_id,
298            &format!("pg:{db_parameter_group_name}"),
299        )
300        .to_string()
301    }
302
303    pub fn next_dbi_resource_id(&self) -> String {
304        format!("db-{}", Uuid::new_v4().simple())
305    }
306
307    pub fn begin_instance_creation(&mut self, db_instance_identifier: &str) -> bool {
308        if self.instances.contains_key(db_instance_identifier)
309            || self
310                .in_progress_instance_ids
311                .contains(db_instance_identifier)
312        {
313            return false;
314        }
315
316        self.in_progress_instance_ids
317            .insert(db_instance_identifier.to_string());
318        true
319    }
320
321    pub fn finish_instance_creation(&mut self, instance: DbInstance) {
322        self.in_progress_instance_ids
323            .remove(&instance.db_instance_identifier);
324        self.instances
325            .insert(instance.db_instance_identifier.clone(), instance);
326    }
327
328    pub fn cancel_instance_creation(&mut self, db_instance_identifier: &str) {
329        self.in_progress_instance_ids.remove(db_instance_identifier);
330    }
331}
332
333pub fn default_engine_versions() -> Vec<EngineVersionInfo> {
334    vec![
335        // PostgreSQL versions
336        EngineVersionInfo {
337            engine: "postgres".to_string(),
338            engine_version: "16.3".to_string(),
339            db_parameter_group_family: "postgres16".to_string(),
340            db_engine_description: "PostgreSQL".to_string(),
341            db_engine_version_description: "PostgreSQL 16.3".to_string(),
342            status: "available".to_string(),
343        },
344        EngineVersionInfo {
345            engine: "postgres".to_string(),
346            engine_version: "15.5".to_string(),
347            db_parameter_group_family: "postgres15".to_string(),
348            db_engine_description: "PostgreSQL".to_string(),
349            db_engine_version_description: "PostgreSQL 15.5".to_string(),
350            status: "available".to_string(),
351        },
352        EngineVersionInfo {
353            engine: "postgres".to_string(),
354            engine_version: "14.10".to_string(),
355            db_parameter_group_family: "postgres14".to_string(),
356            db_engine_description: "PostgreSQL".to_string(),
357            db_engine_version_description: "PostgreSQL 14.10".to_string(),
358            status: "available".to_string(),
359        },
360        EngineVersionInfo {
361            engine: "postgres".to_string(),
362            engine_version: "13.13".to_string(),
363            db_parameter_group_family: "postgres13".to_string(),
364            db_engine_description: "PostgreSQL".to_string(),
365            db_engine_version_description: "PostgreSQL 13.13".to_string(),
366            status: "available".to_string(),
367        },
368        // MySQL versions
369        EngineVersionInfo {
370            engine: "mysql".to_string(),
371            engine_version: "8.0.35".to_string(),
372            db_parameter_group_family: "mysql8.0".to_string(),
373            db_engine_description: "MySQL Community Edition".to_string(),
374            db_engine_version_description: "MySQL 8.0.35".to_string(),
375            status: "available".to_string(),
376        },
377        EngineVersionInfo {
378            engine: "mysql".to_string(),
379            engine_version: "8.0.28".to_string(),
380            db_parameter_group_family: "mysql8.0".to_string(),
381            db_engine_description: "MySQL Community Edition".to_string(),
382            db_engine_version_description: "MySQL 8.0.28".to_string(),
383            status: "available".to_string(),
384        },
385        EngineVersionInfo {
386            engine: "mysql".to_string(),
387            engine_version: "5.7.44".to_string(),
388            db_parameter_group_family: "mysql5.7".to_string(),
389            db_engine_description: "MySQL Community Edition".to_string(),
390            db_engine_version_description: "MySQL 5.7.44".to_string(),
391            status: "available".to_string(),
392        },
393        // MariaDB versions
394        EngineVersionInfo {
395            engine: "mariadb".to_string(),
396            engine_version: "10.11.6".to_string(),
397            db_parameter_group_family: "mariadb10.11".to_string(),
398            db_engine_description: "MariaDB Community Edition".to_string(),
399            db_engine_version_description: "MariaDB 10.11.6".to_string(),
400            status: "available".to_string(),
401        },
402        EngineVersionInfo {
403            engine: "mariadb".to_string(),
404            engine_version: "10.6.16".to_string(),
405            db_parameter_group_family: "mariadb10.6".to_string(),
406            db_engine_description: "MariaDB Community Edition".to_string(),
407            db_engine_version_description: "MariaDB 10.6.16".to_string(),
408            status: "available".to_string(),
409        },
410    ]
411}
412
413pub fn default_orderable_options() -> Vec<OrderableDbInstanceOption> {
414    let mut options = Vec::new();
415    let engines_and_versions = vec![
416        ("postgres", "16.3", "postgresql-license"),
417        ("postgres", "15.5", "postgresql-license"),
418        ("postgres", "14.10", "postgresql-license"),
419        ("postgres", "13.13", "postgresql-license"),
420        ("mysql", "8.0.35", "general-public-license"),
421        ("mysql", "8.0.28", "general-public-license"),
422        ("mysql", "5.7.44", "general-public-license"),
423        ("mariadb", "10.11.6", "general-public-license"),
424        ("mariadb", "10.6.16", "general-public-license"),
425    ];
426
427    for (engine, version, license) in engines_and_versions {
428        for class in SUPPORTED_INSTANCE_CLASSES {
429            options.push(OrderableDbInstanceOption {
430                engine: engine.to_string(),
431                engine_version: version.to_string(),
432                db_instance_class: class.to_string(),
433                license_model: license.to_string(),
434                storage_type: "gp2".to_string(),
435                min_storage_size: 20,
436                max_storage_size: 16384,
437            });
438        }
439    }
440
441    options
442}
443
444pub fn default_parameter_groups(
445    account_id: &str,
446    region: &str,
447) -> HashMap<String, DbParameterGroup> {
448    let mut groups = HashMap::new();
449
450    let families = vec![
451        ("postgres16", "Default parameter group for postgres16"),
452        ("postgres15", "Default parameter group for postgres15"),
453        ("postgres14", "Default parameter group for postgres14"),
454        ("postgres13", "Default parameter group for postgres13"),
455        ("mysql8.0", "Default parameter group for mysql8.0"),
456        ("mysql5.7", "Default parameter group for mysql5.7"),
457        ("mariadb10.11", "Default parameter group for mariadb10.11"),
458        ("mariadb10.6", "Default parameter group for mariadb10.6"),
459        // Heavy-engine families. The names match what
460        // `service::default_parameter_group` returns so callers that
461        // omit `DBParameterGroupName` get a hit instead of a
462        // `DBParameterGroupNotFound`.
463        ("oracle-ee-23", "Default parameter group for oracle-ee-23"),
464        ("oracle-ee-21", "Default parameter group for oracle-ee-21"),
465        ("oracle-ee-19", "Default parameter group for oracle-ee-19"),
466        ("oracle-se2-23", "Default parameter group for oracle-se2-23"),
467        ("oracle-se2-21", "Default parameter group for oracle-se2-21"),
468        ("oracle-se2-19", "Default parameter group for oracle-se2-19"),
469        (
470            "oracle-ee-cdb-23",
471            "Default parameter group for oracle-ee-cdb-23",
472        ),
473        (
474            "oracle-se2-cdb-23",
475            "Default parameter group for oracle-se2-cdb-23",
476        ),
477        (
478            "sqlserver-ee-16",
479            "Default parameter group for sqlserver-ee-16",
480        ),
481        (
482            "sqlserver-ee-15",
483            "Default parameter group for sqlserver-ee-15",
484        ),
485        (
486            "sqlserver-se-16",
487            "Default parameter group for sqlserver-se-16",
488        ),
489        (
490            "sqlserver-se-15",
491            "Default parameter group for sqlserver-se-15",
492        ),
493        (
494            "sqlserver-ex-16",
495            "Default parameter group for sqlserver-ex-16",
496        ),
497        (
498            "sqlserver-ex-15",
499            "Default parameter group for sqlserver-ex-15",
500        ),
501        (
502            "sqlserver-web-16",
503            "Default parameter group for sqlserver-web-16",
504        ),
505        (
506            "sqlserver-web-15",
507            "Default parameter group for sqlserver-web-15",
508        ),
509        ("db2-se-11.5", "Default parameter group for db2-se-11.5"),
510        ("db2-ae-11.5", "Default parameter group for db2-ae-11.5"),
511    ];
512
513    for (family, description) in families {
514        let group_name = format!("default.{}", family);
515        let group = DbParameterGroup {
516            db_parameter_group_name: group_name.clone(),
517            db_parameter_group_arn: Arn::new(
518                "rds",
519                region,
520                account_id,
521                &format!("pg:{group_name}"),
522            )
523            .to_string(),
524            db_parameter_group_family: family.to_string(),
525            description: description.to_string(),
526            parameters: HashMap::new(),
527            tags: Vec::new(),
528        };
529        groups.insert(group_name, group);
530    }
531
532    groups
533}
534
535pub const RDS_SNAPSHOT_SCHEMA_VERSION: u32 = 2;
536
537#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
538pub struct RdsSnapshot {
539    pub schema_version: u32,
540    #[serde(default)]
541    pub accounts: Option<fakecloud_core::multi_account::MultiAccountState<RdsState>>,
542    #[serde(default)]
543    pub state: Option<RdsState>,
544}
545
546#[cfg(test)]
547mod tests {
548    use chrono::Utc;
549
550    use super::{
551        default_engine_versions, default_orderable_options, default_parameter_groups, DbInstance,
552        RdsState,
553    };
554
555    #[test]
556    fn new_initializes_account_and_region() {
557        let state = RdsState::new("123456789012", "us-east-1");
558
559        assert_eq!(state.account_id, "123456789012");
560        assert_eq!(state.region, "us-east-1");
561        assert!(state.instances.is_empty());
562        assert!(state.in_progress_instance_ids.is_empty());
563    }
564
565    #[test]
566    fn reset_clears_instances() {
567        let mut state = RdsState::new("123456789012", "us-east-1");
568        let created_at = Utc::now();
569        state.instances.insert(
570            "db-1".to_string(),
571            DbInstance {
572                db_instance_identifier: "db-1".to_string(),
573                db_instance_arn: "arn:aws:rds:us-east-1:123456789012:db:db-1".to_string(),
574                db_instance_class: "db.t3.micro".to_string(),
575                engine: "postgres".to_string(),
576                engine_version: "16.3".to_string(),
577                db_instance_status: "available".to_string(),
578                master_username: "admin".to_string(),
579                db_name: Some("postgres".to_string()),
580                endpoint_address: "127.0.0.1".to_string(),
581                port: 5432,
582                allocated_storage: 20,
583                publicly_accessible: true,
584                deletion_protection: false,
585                created_at,
586                dbi_resource_id: "db-test".to_string(),
587                master_user_password: "secret123".to_string(),
588                container_id: "container-id".to_string(),
589                host_port: 15432,
590                tags: Vec::new(),
591                read_replica_source_db_instance_identifier: None,
592                read_replica_db_instance_identifiers: Vec::new(),
593                vpc_security_group_ids: Vec::new(),
594                db_parameter_group_name: None,
595                backup_retention_period: 1,
596                preferred_backup_window: "03:00-04:00".to_string(),
597                latest_restorable_time: Some(created_at),
598                option_group_name: None,
599                multi_az: false,
600                pending_modified_values: None,
601            },
602        );
603
604        state.reset();
605
606        assert!(state.instances.is_empty());
607        assert!(state.in_progress_instance_ids.is_empty());
608    }
609
610    #[test]
611    fn default_engine_versions_are_postgres_metadata() {
612        let versions = default_engine_versions();
613
614        assert_eq!(versions.len(), 9); // 4 postgres + 3 mysql + 2 mariadb
615                                       // Check first postgres version
616        assert_eq!(versions[0].engine, "postgres");
617        assert_eq!(versions[0].engine_version, "16.3");
618        assert_eq!(versions[0].db_parameter_group_family, "postgres16");
619    }
620
621    #[test]
622    fn default_orderable_options_match_engine_versions() {
623        let versions = default_engine_versions();
624        let options = default_orderable_options();
625
626        assert_eq!(options.len(), 63); // 9 versions * 7 instance classes
627                                       // Verify all engines and versions have orderable options
628        for version in &versions {
629            assert!(options.iter().any(|opt| {
630                opt.engine == version.engine && opt.engine_version == version.engine_version
631            }));
632        }
633    }
634
635    #[test]
636    fn begin_instance_creation_rejects_duplicate_identifiers() {
637        let mut state = RdsState::new("123456789012", "us-east-1");
638
639        assert!(state.begin_instance_creation("db-1"));
640        assert!(!state.begin_instance_creation("db-1"));
641
642        state.cancel_instance_creation("db-1");
643        assert!(state.begin_instance_creation("db-1"));
644    }
645
646    #[test]
647    fn arn_helpers_format_correctly() {
648        let state = RdsState::new("123456789012", "eu-west-1");
649        assert!(state.db_instance_arn("mydb").contains(":db:mydb"));
650        assert!(state.db_snapshot_arn("snap1").contains(":snapshot:snap1"));
651        assert!(state.db_subnet_group_arn("sng").contains("sng"));
652        assert!(state.db_parameter_group_arn("pg").contains("pg"));
653    }
654
655    #[test]
656    fn next_dbi_resource_id_format() {
657        let state = RdsState::new("123456789012", "us-east-1");
658        let id = state.next_dbi_resource_id();
659        assert!(id.starts_with("db-"));
660        assert!(id.len() > 3);
661    }
662
663    #[test]
664    fn default_engine_versions_list_not_empty() {
665        let versions = default_engine_versions();
666        assert!(!versions.is_empty());
667    }
668
669    #[test]
670    fn default_orderable_options_list_not_empty() {
671        let opts = default_orderable_options();
672        assert!(!opts.is_empty());
673    }
674
675    #[test]
676    fn default_parameter_groups_returned_per_family() {
677        let groups = default_parameter_groups("123456789012", "us-east-1");
678        assert!(!groups.is_empty());
679    }
680
681    fn make_instance(id: &str) -> DbInstance {
682        let created_at = Utc::now();
683        DbInstance {
684            db_instance_identifier: id.to_string(),
685            db_instance_arn: format!("arn:aws:rds:us-east-1:123:db:{id}"),
686            db_instance_class: "db.t3.micro".to_string(),
687            engine: "postgres".to_string(),
688            engine_version: "16.3".to_string(),
689            db_instance_status: "available".to_string(),
690            master_username: "admin".to_string(),
691            db_name: None,
692            endpoint_address: "x".to_string(),
693            port: 5432,
694            allocated_storage: 20,
695            publicly_accessible: false,
696            deletion_protection: false,
697            created_at,
698            dbi_resource_id: "d".to_string(),
699            master_user_password: "p".to_string(),
700            container_id: "c".to_string(),
701            host_port: 0,
702            tags: Vec::new(),
703            read_replica_source_db_instance_identifier: None,
704            read_replica_db_instance_identifiers: Vec::new(),
705            vpc_security_group_ids: Vec::new(),
706            db_parameter_group_name: None,
707            backup_retention_period: 0,
708            preferred_backup_window: String::new(),
709            latest_restorable_time: None,
710            option_group_name: None,
711            multi_az: false,
712            pending_modified_values: None,
713        }
714    }
715
716    #[test]
717    fn finish_instance_creation_moves_from_pending_to_instances() {
718        let mut state = RdsState::new("123456789012", "us-east-1");
719        assert!(state.begin_instance_creation("db-x"));
720        assert!(state.in_progress_instance_ids.contains("db-x"));
721        state.finish_instance_creation(make_instance("db-x"));
722        assert!(!state.in_progress_instance_ids.contains("db-x"));
723        assert!(state.instances.contains_key("db-x"));
724    }
725
726    #[test]
727    fn cancel_instance_creation_drops_pending() {
728        let mut state = RdsState::new("123456789012", "us-east-1");
729        state.begin_instance_creation("db-y");
730        state.cancel_instance_creation("db-y");
731        assert!(!state.in_progress_instance_ids.contains("db-y"));
732    }
733
734    #[test]
735    fn begin_instance_creation_rejects_when_already_created() {
736        let mut state = RdsState::new("123456789012", "us-east-1");
737        state
738            .instances
739            .insert("db-z".to_string(), make_instance("db-z"));
740        assert!(!state.begin_instance_creation("db-z"));
741    }
742
743    #[test]
744    fn reset_restores_default_parameter_groups() {
745        let mut state = RdsState::new("123456789012", "us-east-1");
746        state.parameter_groups.clear();
747        state.reset();
748        assert!(!state.parameter_groups.is_empty());
749    }
750
751    #[test]
752    fn arn_helpers_include_region_and_account() {
753        let state = RdsState::new("111122223333", "ap-southeast-2");
754        let arn = state.db_instance_arn("my-db");
755        assert!(arn.contains("111122223333"));
756        assert!(arn.contains("ap-southeast-2"));
757        let snap = state.db_snapshot_arn("snap");
758        assert!(snap.contains("snapshot:snap"));
759    }
760
761    #[test]
762    fn next_dbi_resource_id_unique_across_calls() {
763        let state = RdsState::new("123", "us-east-1");
764        let a = state.next_dbi_resource_id();
765        let b = state.next_dbi_resource_id();
766        assert_ne!(a, b);
767    }
768}