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: "11.4.5".to_string(),
397            db_parameter_group_family: "mariadb11.4".to_string(),
398            db_engine_description: "MariaDB Community Edition".to_string(),
399            db_engine_version_description: "MariaDB 11.4.5".to_string(),
400            status: "available".to_string(),
401        },
402        EngineVersionInfo {
403            engine: "mariadb".to_string(),
404            engine_version: "10.11.6".to_string(),
405            db_parameter_group_family: "mariadb10.11".to_string(),
406            db_engine_description: "MariaDB Community Edition".to_string(),
407            db_engine_version_description: "MariaDB 10.11.6".to_string(),
408            status: "available".to_string(),
409        },
410        EngineVersionInfo {
411            engine: "mariadb".to_string(),
412            engine_version: "10.6.16".to_string(),
413            db_parameter_group_family: "mariadb10.6".to_string(),
414            db_engine_description: "MariaDB Community Edition".to_string(),
415            db_engine_version_description: "MariaDB 10.6.16".to_string(),
416            status: "available".to_string(),
417        },
418    ]
419}
420
421pub fn default_orderable_options() -> Vec<OrderableDbInstanceOption> {
422    let mut options = Vec::new();
423    let engines_and_versions = vec![
424        ("postgres", "16.3", "postgresql-license"),
425        ("postgres", "15.5", "postgresql-license"),
426        ("postgres", "14.10", "postgresql-license"),
427        ("postgres", "13.13", "postgresql-license"),
428        ("mysql", "8.0.35", "general-public-license"),
429        ("mysql", "8.0.28", "general-public-license"),
430        ("mysql", "5.7.44", "general-public-license"),
431        ("mariadb", "11.4.5", "general-public-license"),
432        ("mariadb", "10.11.6", "general-public-license"),
433        ("mariadb", "10.6.16", "general-public-license"),
434    ];
435
436    for (engine, version, license) in engines_and_versions {
437        for class in SUPPORTED_INSTANCE_CLASSES {
438            options.push(OrderableDbInstanceOption {
439                engine: engine.to_string(),
440                engine_version: version.to_string(),
441                db_instance_class: class.to_string(),
442                license_model: license.to_string(),
443                storage_type: "gp2".to_string(),
444                min_storage_size: 20,
445                max_storage_size: 16384,
446            });
447        }
448    }
449
450    options
451}
452
453pub fn default_parameter_groups(
454    account_id: &str,
455    region: &str,
456) -> HashMap<String, DbParameterGroup> {
457    let mut groups = HashMap::new();
458
459    let families = vec![
460        ("postgres16", "Default parameter group for postgres16"),
461        ("postgres15", "Default parameter group for postgres15"),
462        ("postgres14", "Default parameter group for postgres14"),
463        ("postgres13", "Default parameter group for postgres13"),
464        ("mysql8.0", "Default parameter group for mysql8.0"),
465        ("mysql5.7", "Default parameter group for mysql5.7"),
466        ("mariadb11.4", "Default parameter group for mariadb11.4"),
467        ("mariadb10.11", "Default parameter group for mariadb10.11"),
468        ("mariadb10.6", "Default parameter group for mariadb10.6"),
469        // Heavy-engine families. The names match what
470        // `service::default_parameter_group` returns so callers that
471        // omit `DBParameterGroupName` get a hit instead of a
472        // `DBParameterGroupNotFound`.
473        ("oracle-ee-23", "Default parameter group for oracle-ee-23"),
474        ("oracle-ee-21", "Default parameter group for oracle-ee-21"),
475        ("oracle-ee-19", "Default parameter group for oracle-ee-19"),
476        ("oracle-se2-23", "Default parameter group for oracle-se2-23"),
477        ("oracle-se2-21", "Default parameter group for oracle-se2-21"),
478        ("oracle-se2-19", "Default parameter group for oracle-se2-19"),
479        (
480            "oracle-ee-cdb-23",
481            "Default parameter group for oracle-ee-cdb-23",
482        ),
483        (
484            "oracle-se2-cdb-23",
485            "Default parameter group for oracle-se2-cdb-23",
486        ),
487        (
488            "sqlserver-ee-16",
489            "Default parameter group for sqlserver-ee-16",
490        ),
491        (
492            "sqlserver-ee-15",
493            "Default parameter group for sqlserver-ee-15",
494        ),
495        (
496            "sqlserver-se-16",
497            "Default parameter group for sqlserver-se-16",
498        ),
499        (
500            "sqlserver-se-15",
501            "Default parameter group for sqlserver-se-15",
502        ),
503        (
504            "sqlserver-ex-16",
505            "Default parameter group for sqlserver-ex-16",
506        ),
507        (
508            "sqlserver-ex-15",
509            "Default parameter group for sqlserver-ex-15",
510        ),
511        (
512            "sqlserver-web-16",
513            "Default parameter group for sqlserver-web-16",
514        ),
515        (
516            "sqlserver-web-15",
517            "Default parameter group for sqlserver-web-15",
518        ),
519        ("db2-se-11.5", "Default parameter group for db2-se-11.5"),
520        ("db2-ae-11.5", "Default parameter group for db2-ae-11.5"),
521    ];
522
523    for (family, description) in families {
524        let group_name = format!("default.{}", family);
525        let group = DbParameterGroup {
526            db_parameter_group_name: group_name.clone(),
527            db_parameter_group_arn: Arn::new(
528                "rds",
529                region,
530                account_id,
531                &format!("pg:{group_name}"),
532            )
533            .to_string(),
534            db_parameter_group_family: family.to_string(),
535            description: description.to_string(),
536            parameters: HashMap::new(),
537            tags: Vec::new(),
538        };
539        groups.insert(group_name, group);
540    }
541
542    groups
543}
544
545pub const RDS_SNAPSHOT_SCHEMA_VERSION: u32 = 2;
546
547#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
548pub struct RdsSnapshot {
549    pub schema_version: u32,
550    #[serde(default)]
551    pub accounts: Option<fakecloud_core::multi_account::MultiAccountState<RdsState>>,
552    #[serde(default)]
553    pub state: Option<RdsState>,
554}
555
556#[cfg(test)]
557mod tests {
558    use chrono::Utc;
559
560    use super::{
561        default_engine_versions, default_orderable_options, default_parameter_groups, DbInstance,
562        RdsState,
563    };
564
565    #[test]
566    fn new_initializes_account_and_region() {
567        let state = RdsState::new("123456789012", "us-east-1");
568
569        assert_eq!(state.account_id, "123456789012");
570        assert_eq!(state.region, "us-east-1");
571        assert!(state.instances.is_empty());
572        assert!(state.in_progress_instance_ids.is_empty());
573    }
574
575    #[test]
576    fn reset_clears_instances() {
577        let mut state = RdsState::new("123456789012", "us-east-1");
578        let created_at = Utc::now();
579        state.instances.insert(
580            "db-1".to_string(),
581            DbInstance {
582                db_instance_identifier: "db-1".to_string(),
583                db_instance_arn: "arn:aws:rds:us-east-1:123456789012:db:db-1".to_string(),
584                db_instance_class: "db.t3.micro".to_string(),
585                engine: "postgres".to_string(),
586                engine_version: "16.3".to_string(),
587                db_instance_status: "available".to_string(),
588                master_username: "admin".to_string(),
589                db_name: Some("postgres".to_string()),
590                endpoint_address: "127.0.0.1".to_string(),
591                port: 5432,
592                allocated_storage: 20,
593                publicly_accessible: true,
594                deletion_protection: false,
595                created_at,
596                dbi_resource_id: "db-test".to_string(),
597                master_user_password: "secret123".to_string(),
598                container_id: "container-id".to_string(),
599                host_port: 15432,
600                tags: Vec::new(),
601                read_replica_source_db_instance_identifier: None,
602                read_replica_db_instance_identifiers: Vec::new(),
603                vpc_security_group_ids: Vec::new(),
604                db_parameter_group_name: None,
605                backup_retention_period: 1,
606                preferred_backup_window: "03:00-04:00".to_string(),
607                latest_restorable_time: Some(created_at),
608                option_group_name: None,
609                multi_az: false,
610                pending_modified_values: None,
611            },
612        );
613
614        state.reset();
615
616        assert!(state.instances.is_empty());
617        assert!(state.in_progress_instance_ids.is_empty());
618    }
619
620    #[test]
621    fn default_engine_versions_are_postgres_metadata() {
622        let versions = default_engine_versions();
623
624        assert_eq!(versions.len(), 10); // 4 postgres + 3 mysql + 3 mariadb
625                                        // Check first postgres version
626        assert_eq!(versions[0].engine, "postgres");
627        assert_eq!(versions[0].engine_version, "16.3");
628        assert_eq!(versions[0].db_parameter_group_family, "postgres16");
629    }
630
631    #[test]
632    fn default_orderable_options_match_engine_versions() {
633        let versions = default_engine_versions();
634        let options = default_orderable_options();
635
636        assert_eq!(options.len(), 70); // 10 versions * 7 instance classes
637                                       // Verify all engines and versions have orderable options
638        for version in &versions {
639            assert!(options.iter().any(|opt| {
640                opt.engine == version.engine && opt.engine_version == version.engine_version
641            }));
642        }
643    }
644
645    #[test]
646    fn begin_instance_creation_rejects_duplicate_identifiers() {
647        let mut state = RdsState::new("123456789012", "us-east-1");
648
649        assert!(state.begin_instance_creation("db-1"));
650        assert!(!state.begin_instance_creation("db-1"));
651
652        state.cancel_instance_creation("db-1");
653        assert!(state.begin_instance_creation("db-1"));
654    }
655
656    #[test]
657    fn arn_helpers_format_correctly() {
658        let state = RdsState::new("123456789012", "eu-west-1");
659        assert!(state.db_instance_arn("mydb").contains(":db:mydb"));
660        assert!(state.db_snapshot_arn("snap1").contains(":snapshot:snap1"));
661        assert!(state.db_subnet_group_arn("sng").contains("sng"));
662        assert!(state.db_parameter_group_arn("pg").contains("pg"));
663    }
664
665    #[test]
666    fn next_dbi_resource_id_format() {
667        let state = RdsState::new("123456789012", "us-east-1");
668        let id = state.next_dbi_resource_id();
669        assert!(id.starts_with("db-"));
670        assert!(id.len() > 3);
671    }
672
673    #[test]
674    fn default_engine_versions_list_not_empty() {
675        let versions = default_engine_versions();
676        assert!(!versions.is_empty());
677    }
678
679    #[test]
680    fn default_orderable_options_list_not_empty() {
681        let opts = default_orderable_options();
682        assert!(!opts.is_empty());
683    }
684
685    #[test]
686    fn default_parameter_groups_returned_per_family() {
687        let groups = default_parameter_groups("123456789012", "us-east-1");
688        assert!(!groups.is_empty());
689    }
690
691    fn make_instance(id: &str) -> DbInstance {
692        let created_at = Utc::now();
693        DbInstance {
694            db_instance_identifier: id.to_string(),
695            db_instance_arn: format!("arn:aws:rds:us-east-1:123:db:{id}"),
696            db_instance_class: "db.t3.micro".to_string(),
697            engine: "postgres".to_string(),
698            engine_version: "16.3".to_string(),
699            db_instance_status: "available".to_string(),
700            master_username: "admin".to_string(),
701            db_name: None,
702            endpoint_address: "x".to_string(),
703            port: 5432,
704            allocated_storage: 20,
705            publicly_accessible: false,
706            deletion_protection: false,
707            created_at,
708            dbi_resource_id: "d".to_string(),
709            master_user_password: "p".to_string(),
710            container_id: "c".to_string(),
711            host_port: 0,
712            tags: Vec::new(),
713            read_replica_source_db_instance_identifier: None,
714            read_replica_db_instance_identifiers: Vec::new(),
715            vpc_security_group_ids: Vec::new(),
716            db_parameter_group_name: None,
717            backup_retention_period: 0,
718            preferred_backup_window: String::new(),
719            latest_restorable_time: None,
720            option_group_name: None,
721            multi_az: false,
722            pending_modified_values: None,
723        }
724    }
725
726    #[test]
727    fn finish_instance_creation_moves_from_pending_to_instances() {
728        let mut state = RdsState::new("123456789012", "us-east-1");
729        assert!(state.begin_instance_creation("db-x"));
730        assert!(state.in_progress_instance_ids.contains("db-x"));
731        state.finish_instance_creation(make_instance("db-x"));
732        assert!(!state.in_progress_instance_ids.contains("db-x"));
733        assert!(state.instances.contains_key("db-x"));
734    }
735
736    #[test]
737    fn cancel_instance_creation_drops_pending() {
738        let mut state = RdsState::new("123456789012", "us-east-1");
739        state.begin_instance_creation("db-y");
740        state.cancel_instance_creation("db-y");
741        assert!(!state.in_progress_instance_ids.contains("db-y"));
742    }
743
744    #[test]
745    fn begin_instance_creation_rejects_when_already_created() {
746        let mut state = RdsState::new("123456789012", "us-east-1");
747        state
748            .instances
749            .insert("db-z".to_string(), make_instance("db-z"));
750        assert!(!state.begin_instance_creation("db-z"));
751    }
752
753    #[test]
754    fn reset_restores_default_parameter_groups() {
755        let mut state = RdsState::new("123456789012", "us-east-1");
756        state.parameter_groups.clear();
757        state.reset();
758        assert!(!state.parameter_groups.is_empty());
759    }
760
761    #[test]
762    fn arn_helpers_include_region_and_account() {
763        let state = RdsState::new("111122223333", "ap-southeast-2");
764        let arn = state.db_instance_arn("my-db");
765        assert!(arn.contains("111122223333"));
766        assert!(arn.contains("ap-southeast-2"));
767        let snap = state.db_snapshot_arn("snap");
768        assert!(snap.contains("snapshot:snap"));
769    }
770
771    #[test]
772    fn next_dbi_resource_id_unique_across_calls() {
773        let state = RdsState::new("123", "us-east-1");
774        let a = state.next_dbi_resource_id();
775        let b = state.next_dbi_resource_id();
776        assert_ne!(a, b);
777    }
778}