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    ];
460
461    for (family, description) in families {
462        let group_name = format!("default.{}", family);
463        let group = DbParameterGroup {
464            db_parameter_group_name: group_name.clone(),
465            db_parameter_group_arn: Arn::new(
466                "rds",
467                region,
468                account_id,
469                &format!("pg:{group_name}"),
470            )
471            .to_string(),
472            db_parameter_group_family: family.to_string(),
473            description: description.to_string(),
474            parameters: HashMap::new(),
475            tags: Vec::new(),
476        };
477        groups.insert(group_name, group);
478    }
479
480    groups
481}
482
483pub const RDS_SNAPSHOT_SCHEMA_VERSION: u32 = 2;
484
485#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
486pub struct RdsSnapshot {
487    pub schema_version: u32,
488    #[serde(default)]
489    pub accounts: Option<fakecloud_core::multi_account::MultiAccountState<RdsState>>,
490    #[serde(default)]
491    pub state: Option<RdsState>,
492}
493
494#[cfg(test)]
495mod tests {
496    use chrono::Utc;
497
498    use super::{
499        default_engine_versions, default_orderable_options, default_parameter_groups, DbInstance,
500        RdsState,
501    };
502
503    #[test]
504    fn new_initializes_account_and_region() {
505        let state = RdsState::new("123456789012", "us-east-1");
506
507        assert_eq!(state.account_id, "123456789012");
508        assert_eq!(state.region, "us-east-1");
509        assert!(state.instances.is_empty());
510        assert!(state.in_progress_instance_ids.is_empty());
511    }
512
513    #[test]
514    fn reset_clears_instances() {
515        let mut state = RdsState::new("123456789012", "us-east-1");
516        let created_at = Utc::now();
517        state.instances.insert(
518            "db-1".to_string(),
519            DbInstance {
520                db_instance_identifier: "db-1".to_string(),
521                db_instance_arn: "arn:aws:rds:us-east-1:123456789012:db:db-1".to_string(),
522                db_instance_class: "db.t3.micro".to_string(),
523                engine: "postgres".to_string(),
524                engine_version: "16.3".to_string(),
525                db_instance_status: "available".to_string(),
526                master_username: "admin".to_string(),
527                db_name: Some("postgres".to_string()),
528                endpoint_address: "127.0.0.1".to_string(),
529                port: 5432,
530                allocated_storage: 20,
531                publicly_accessible: true,
532                deletion_protection: false,
533                created_at,
534                dbi_resource_id: "db-test".to_string(),
535                master_user_password: "secret123".to_string(),
536                container_id: "container-id".to_string(),
537                host_port: 15432,
538                tags: Vec::new(),
539                read_replica_source_db_instance_identifier: None,
540                read_replica_db_instance_identifiers: Vec::new(),
541                vpc_security_group_ids: Vec::new(),
542                db_parameter_group_name: None,
543                backup_retention_period: 1,
544                preferred_backup_window: "03:00-04:00".to_string(),
545                latest_restorable_time: Some(created_at),
546                option_group_name: None,
547                multi_az: false,
548                pending_modified_values: None,
549            },
550        );
551
552        state.reset();
553
554        assert!(state.instances.is_empty());
555        assert!(state.in_progress_instance_ids.is_empty());
556    }
557
558    #[test]
559    fn default_engine_versions_are_postgres_metadata() {
560        let versions = default_engine_versions();
561
562        assert_eq!(versions.len(), 9); // 4 postgres + 3 mysql + 2 mariadb
563                                       // Check first postgres version
564        assert_eq!(versions[0].engine, "postgres");
565        assert_eq!(versions[0].engine_version, "16.3");
566        assert_eq!(versions[0].db_parameter_group_family, "postgres16");
567    }
568
569    #[test]
570    fn default_orderable_options_match_engine_versions() {
571        let versions = default_engine_versions();
572        let options = default_orderable_options();
573
574        assert_eq!(options.len(), 63); // 9 versions * 7 instance classes
575                                       // Verify all engines and versions have orderable options
576        for version in &versions {
577            assert!(options.iter().any(|opt| {
578                opt.engine == version.engine && opt.engine_version == version.engine_version
579            }));
580        }
581    }
582
583    #[test]
584    fn begin_instance_creation_rejects_duplicate_identifiers() {
585        let mut state = RdsState::new("123456789012", "us-east-1");
586
587        assert!(state.begin_instance_creation("db-1"));
588        assert!(!state.begin_instance_creation("db-1"));
589
590        state.cancel_instance_creation("db-1");
591        assert!(state.begin_instance_creation("db-1"));
592    }
593
594    #[test]
595    fn arn_helpers_format_correctly() {
596        let state = RdsState::new("123456789012", "eu-west-1");
597        assert!(state.db_instance_arn("mydb").contains(":db:mydb"));
598        assert!(state.db_snapshot_arn("snap1").contains(":snapshot:snap1"));
599        assert!(state.db_subnet_group_arn("sng").contains("sng"));
600        assert!(state.db_parameter_group_arn("pg").contains("pg"));
601    }
602
603    #[test]
604    fn next_dbi_resource_id_format() {
605        let state = RdsState::new("123456789012", "us-east-1");
606        let id = state.next_dbi_resource_id();
607        assert!(id.starts_with("db-"));
608        assert!(id.len() > 3);
609    }
610
611    #[test]
612    fn default_engine_versions_list_not_empty() {
613        let versions = default_engine_versions();
614        assert!(!versions.is_empty());
615    }
616
617    #[test]
618    fn default_orderable_options_list_not_empty() {
619        let opts = default_orderable_options();
620        assert!(!opts.is_empty());
621    }
622
623    #[test]
624    fn default_parameter_groups_returned_per_family() {
625        let groups = default_parameter_groups("123456789012", "us-east-1");
626        assert!(!groups.is_empty());
627    }
628
629    fn make_instance(id: &str) -> DbInstance {
630        let created_at = Utc::now();
631        DbInstance {
632            db_instance_identifier: id.to_string(),
633            db_instance_arn: format!("arn:aws:rds:us-east-1:123:db:{id}"),
634            db_instance_class: "db.t3.micro".to_string(),
635            engine: "postgres".to_string(),
636            engine_version: "16.3".to_string(),
637            db_instance_status: "available".to_string(),
638            master_username: "admin".to_string(),
639            db_name: None,
640            endpoint_address: "x".to_string(),
641            port: 5432,
642            allocated_storage: 20,
643            publicly_accessible: false,
644            deletion_protection: false,
645            created_at,
646            dbi_resource_id: "d".to_string(),
647            master_user_password: "p".to_string(),
648            container_id: "c".to_string(),
649            host_port: 0,
650            tags: Vec::new(),
651            read_replica_source_db_instance_identifier: None,
652            read_replica_db_instance_identifiers: Vec::new(),
653            vpc_security_group_ids: Vec::new(),
654            db_parameter_group_name: None,
655            backup_retention_period: 0,
656            preferred_backup_window: String::new(),
657            latest_restorable_time: None,
658            option_group_name: None,
659            multi_az: false,
660            pending_modified_values: None,
661        }
662    }
663
664    #[test]
665    fn finish_instance_creation_moves_from_pending_to_instances() {
666        let mut state = RdsState::new("123456789012", "us-east-1");
667        assert!(state.begin_instance_creation("db-x"));
668        assert!(state.in_progress_instance_ids.contains("db-x"));
669        state.finish_instance_creation(make_instance("db-x"));
670        assert!(!state.in_progress_instance_ids.contains("db-x"));
671        assert!(state.instances.contains_key("db-x"));
672    }
673
674    #[test]
675    fn cancel_instance_creation_drops_pending() {
676        let mut state = RdsState::new("123456789012", "us-east-1");
677        state.begin_instance_creation("db-y");
678        state.cancel_instance_creation("db-y");
679        assert!(!state.in_progress_instance_ids.contains("db-y"));
680    }
681
682    #[test]
683    fn begin_instance_creation_rejects_when_already_created() {
684        let mut state = RdsState::new("123456789012", "us-east-1");
685        state
686            .instances
687            .insert("db-z".to_string(), make_instance("db-z"));
688        assert!(!state.begin_instance_creation("db-z"));
689    }
690
691    #[test]
692    fn reset_restores_default_parameter_groups() {
693        let mut state = RdsState::new("123456789012", "us-east-1");
694        state.parameter_groups.clear();
695        state.reset();
696        assert!(!state.parameter_groups.is_empty());
697    }
698
699    #[test]
700    fn arn_helpers_include_region_and_account() {
701        let state = RdsState::new("111122223333", "ap-southeast-2");
702        let arn = state.db_instance_arn("my-db");
703        assert!(arn.contains("111122223333"));
704        assert!(arn.contains("ap-southeast-2"));
705        let snap = state.db_snapshot_arn("snap");
706        assert!(snap.contains("snapshot:snap"));
707    }
708
709    #[test]
710    fn next_dbi_resource_id_unique_across_calls() {
711        let state = RdsState::new("123", "us-east-1");
712        let a = state.next_dbi_resource_id();
713        let b = state.next_dbi_resource_id();
714        assert_ne!(a, b);
715    }
716}