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}
189
190#[derive(Debug, Clone, PartialEq, Eq)]
191pub struct EngineVersionInfo {
192    pub engine: String,
193    pub engine_version: String,
194    pub db_parameter_group_family: String,
195    pub db_engine_description: String,
196    pub db_engine_version_description: String,
197    pub status: String,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct OrderableDbInstanceOption {
202    pub engine: String,
203    pub engine_version: String,
204    pub db_instance_class: String,
205    pub license_model: String,
206    pub storage_type: String,
207    pub min_storage_size: i32,
208    pub max_storage_size: i32,
209}
210
211#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
212pub struct DbSubnetGroup {
213    pub db_subnet_group_name: String,
214    pub db_subnet_group_arn: String,
215    pub db_subnet_group_description: String,
216    pub vpc_id: String,
217    pub subnet_ids: Vec<String>,
218    pub subnet_availability_zones: Vec<String>,
219    pub tags: Vec<RdsTag>,
220}
221
222#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
223pub struct DbParameterGroup {
224    pub db_parameter_group_name: String,
225    pub db_parameter_group_arn: String,
226    pub db_parameter_group_family: String,
227    pub description: String,
228    pub parameters: HashMap<String, String>,
229    pub tags: Vec<RdsTag>,
230}
231
232impl RdsState {
233    pub fn new(account_id: &str, region: &str) -> Self {
234        Self {
235            account_id: account_id.to_string(),
236            region: region.to_string(),
237            instances: HashMap::new(),
238            in_progress_instance_ids: HashSet::new(),
239            snapshots: HashMap::new(),
240            subnet_groups: HashMap::new(),
241            parameter_groups: default_parameter_groups(account_id, region),
242        }
243    }
244
245    pub fn reset(&mut self) {
246        self.instances.clear();
247        self.in_progress_instance_ids.clear();
248        self.snapshots.clear();
249        self.subnet_groups.clear();
250        self.parameter_groups = default_parameter_groups(&self.account_id, &self.region);
251    }
252
253    pub fn db_instance_arn(&self, db_instance_identifier: &str) -> String {
254        Arn::new(
255            "rds",
256            &self.region,
257            &self.account_id,
258            &format!("db:{db_instance_identifier}"),
259        )
260        .to_string()
261    }
262
263    pub fn db_snapshot_arn(&self, db_snapshot_identifier: &str) -> String {
264        Arn::new(
265            "rds",
266            &self.region,
267            &self.account_id,
268            &format!("snapshot:{db_snapshot_identifier}"),
269        )
270        .to_string()
271    }
272
273    pub fn db_subnet_group_arn(&self, db_subnet_group_name: &str) -> String {
274        Arn::new(
275            "rds",
276            &self.region,
277            &self.account_id,
278            &format!("subgrp:{db_subnet_group_name}"),
279        )
280        .to_string()
281    }
282
283    pub fn db_parameter_group_arn(&self, db_parameter_group_name: &str) -> String {
284        Arn::new(
285            "rds",
286            &self.region,
287            &self.account_id,
288            &format!("pg:{db_parameter_group_name}"),
289        )
290        .to_string()
291    }
292
293    pub fn next_dbi_resource_id(&self) -> String {
294        format!("db-{}", Uuid::new_v4().simple())
295    }
296
297    pub fn begin_instance_creation(&mut self, db_instance_identifier: &str) -> bool {
298        if self.instances.contains_key(db_instance_identifier)
299            || self
300                .in_progress_instance_ids
301                .contains(db_instance_identifier)
302        {
303            return false;
304        }
305
306        self.in_progress_instance_ids
307            .insert(db_instance_identifier.to_string());
308        true
309    }
310
311    pub fn finish_instance_creation(&mut self, instance: DbInstance) {
312        self.in_progress_instance_ids
313            .remove(&instance.db_instance_identifier);
314        self.instances
315            .insert(instance.db_instance_identifier.clone(), instance);
316    }
317
318    pub fn cancel_instance_creation(&mut self, db_instance_identifier: &str) {
319        self.in_progress_instance_ids.remove(db_instance_identifier);
320    }
321}
322
323pub fn default_engine_versions() -> Vec<EngineVersionInfo> {
324    vec![
325        // PostgreSQL versions
326        EngineVersionInfo {
327            engine: "postgres".to_string(),
328            engine_version: "16.3".to_string(),
329            db_parameter_group_family: "postgres16".to_string(),
330            db_engine_description: "PostgreSQL".to_string(),
331            db_engine_version_description: "PostgreSQL 16.3".to_string(),
332            status: "available".to_string(),
333        },
334        EngineVersionInfo {
335            engine: "postgres".to_string(),
336            engine_version: "15.5".to_string(),
337            db_parameter_group_family: "postgres15".to_string(),
338            db_engine_description: "PostgreSQL".to_string(),
339            db_engine_version_description: "PostgreSQL 15.5".to_string(),
340            status: "available".to_string(),
341        },
342        EngineVersionInfo {
343            engine: "postgres".to_string(),
344            engine_version: "14.10".to_string(),
345            db_parameter_group_family: "postgres14".to_string(),
346            db_engine_description: "PostgreSQL".to_string(),
347            db_engine_version_description: "PostgreSQL 14.10".to_string(),
348            status: "available".to_string(),
349        },
350        EngineVersionInfo {
351            engine: "postgres".to_string(),
352            engine_version: "13.13".to_string(),
353            db_parameter_group_family: "postgres13".to_string(),
354            db_engine_description: "PostgreSQL".to_string(),
355            db_engine_version_description: "PostgreSQL 13.13".to_string(),
356            status: "available".to_string(),
357        },
358        // MySQL versions
359        EngineVersionInfo {
360            engine: "mysql".to_string(),
361            engine_version: "8.0.35".to_string(),
362            db_parameter_group_family: "mysql8.0".to_string(),
363            db_engine_description: "MySQL Community Edition".to_string(),
364            db_engine_version_description: "MySQL 8.0.35".to_string(),
365            status: "available".to_string(),
366        },
367        EngineVersionInfo {
368            engine: "mysql".to_string(),
369            engine_version: "8.0.28".to_string(),
370            db_parameter_group_family: "mysql8.0".to_string(),
371            db_engine_description: "MySQL Community Edition".to_string(),
372            db_engine_version_description: "MySQL 8.0.28".to_string(),
373            status: "available".to_string(),
374        },
375        EngineVersionInfo {
376            engine: "mysql".to_string(),
377            engine_version: "5.7.44".to_string(),
378            db_parameter_group_family: "mysql5.7".to_string(),
379            db_engine_description: "MySQL Community Edition".to_string(),
380            db_engine_version_description: "MySQL 5.7.44".to_string(),
381            status: "available".to_string(),
382        },
383        // MariaDB versions
384        EngineVersionInfo {
385            engine: "mariadb".to_string(),
386            engine_version: "10.11.6".to_string(),
387            db_parameter_group_family: "mariadb10.11".to_string(),
388            db_engine_description: "MariaDB Community Edition".to_string(),
389            db_engine_version_description: "MariaDB 10.11.6".to_string(),
390            status: "available".to_string(),
391        },
392        EngineVersionInfo {
393            engine: "mariadb".to_string(),
394            engine_version: "10.6.16".to_string(),
395            db_parameter_group_family: "mariadb10.6".to_string(),
396            db_engine_description: "MariaDB Community Edition".to_string(),
397            db_engine_version_description: "MariaDB 10.6.16".to_string(),
398            status: "available".to_string(),
399        },
400    ]
401}
402
403pub fn default_orderable_options() -> Vec<OrderableDbInstanceOption> {
404    let mut options = Vec::new();
405    let engines_and_versions = vec![
406        ("postgres", "16.3", "postgresql-license"),
407        ("postgres", "15.5", "postgresql-license"),
408        ("postgres", "14.10", "postgresql-license"),
409        ("postgres", "13.13", "postgresql-license"),
410        ("mysql", "8.0.35", "general-public-license"),
411        ("mysql", "8.0.28", "general-public-license"),
412        ("mysql", "5.7.44", "general-public-license"),
413        ("mariadb", "10.11.6", "general-public-license"),
414        ("mariadb", "10.6.16", "general-public-license"),
415    ];
416
417    for (engine, version, license) in engines_and_versions {
418        for class in SUPPORTED_INSTANCE_CLASSES {
419            options.push(OrderableDbInstanceOption {
420                engine: engine.to_string(),
421                engine_version: version.to_string(),
422                db_instance_class: class.to_string(),
423                license_model: license.to_string(),
424                storage_type: "gp2".to_string(),
425                min_storage_size: 20,
426                max_storage_size: 16384,
427            });
428        }
429    }
430
431    options
432}
433
434pub fn default_parameter_groups(
435    account_id: &str,
436    region: &str,
437) -> HashMap<String, DbParameterGroup> {
438    let mut groups = HashMap::new();
439
440    let families = vec![
441        ("postgres16", "Default parameter group for postgres16"),
442        ("postgres15", "Default parameter group for postgres15"),
443        ("postgres14", "Default parameter group for postgres14"),
444        ("postgres13", "Default parameter group for postgres13"),
445        ("mysql8.0", "Default parameter group for mysql8.0"),
446        ("mysql5.7", "Default parameter group for mysql5.7"),
447        ("mariadb10.11", "Default parameter group for mariadb10.11"),
448        ("mariadb10.6", "Default parameter group for mariadb10.6"),
449    ];
450
451    for (family, description) in families {
452        let group_name = format!("default.{}", family);
453        let group = DbParameterGroup {
454            db_parameter_group_name: group_name.clone(),
455            db_parameter_group_arn: Arn::new(
456                "rds",
457                region,
458                account_id,
459                &format!("pg:{group_name}"),
460            )
461            .to_string(),
462            db_parameter_group_family: family.to_string(),
463            description: description.to_string(),
464            parameters: HashMap::new(),
465            tags: Vec::new(),
466        };
467        groups.insert(group_name, group);
468    }
469
470    groups
471}
472
473pub const RDS_SNAPSHOT_SCHEMA_VERSION: u32 = 2;
474
475#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
476pub struct RdsSnapshot {
477    pub schema_version: u32,
478    #[serde(default)]
479    pub accounts: Option<fakecloud_core::multi_account::MultiAccountState<RdsState>>,
480    #[serde(default)]
481    pub state: Option<RdsState>,
482}
483
484#[cfg(test)]
485mod tests {
486    use chrono::Utc;
487
488    use super::{
489        default_engine_versions, default_orderable_options, default_parameter_groups, DbInstance,
490        RdsState,
491    };
492
493    #[test]
494    fn new_initializes_account_and_region() {
495        let state = RdsState::new("123456789012", "us-east-1");
496
497        assert_eq!(state.account_id, "123456789012");
498        assert_eq!(state.region, "us-east-1");
499        assert!(state.instances.is_empty());
500        assert!(state.in_progress_instance_ids.is_empty());
501    }
502
503    #[test]
504    fn reset_clears_instances() {
505        let mut state = RdsState::new("123456789012", "us-east-1");
506        let created_at = Utc::now();
507        state.instances.insert(
508            "db-1".to_string(),
509            DbInstance {
510                db_instance_identifier: "db-1".to_string(),
511                db_instance_arn: "arn:aws:rds:us-east-1:123456789012:db:db-1".to_string(),
512                db_instance_class: "db.t3.micro".to_string(),
513                engine: "postgres".to_string(),
514                engine_version: "16.3".to_string(),
515                db_instance_status: "available".to_string(),
516                master_username: "admin".to_string(),
517                db_name: Some("postgres".to_string()),
518                endpoint_address: "127.0.0.1".to_string(),
519                port: 5432,
520                allocated_storage: 20,
521                publicly_accessible: true,
522                deletion_protection: false,
523                created_at,
524                dbi_resource_id: "db-test".to_string(),
525                master_user_password: "secret123".to_string(),
526                container_id: "container-id".to_string(),
527                host_port: 15432,
528                tags: Vec::new(),
529                read_replica_source_db_instance_identifier: None,
530                read_replica_db_instance_identifiers: Vec::new(),
531                vpc_security_group_ids: Vec::new(),
532                db_parameter_group_name: None,
533                backup_retention_period: 1,
534                preferred_backup_window: "03:00-04:00".to_string(),
535                latest_restorable_time: Some(created_at),
536                option_group_name: None,
537                multi_az: false,
538                pending_modified_values: None,
539            },
540        );
541
542        state.reset();
543
544        assert!(state.instances.is_empty());
545        assert!(state.in_progress_instance_ids.is_empty());
546    }
547
548    #[test]
549    fn default_engine_versions_are_postgres_metadata() {
550        let versions = default_engine_versions();
551
552        assert_eq!(versions.len(), 9); // 4 postgres + 3 mysql + 2 mariadb
553                                       // Check first postgres version
554        assert_eq!(versions[0].engine, "postgres");
555        assert_eq!(versions[0].engine_version, "16.3");
556        assert_eq!(versions[0].db_parameter_group_family, "postgres16");
557    }
558
559    #[test]
560    fn default_orderable_options_match_engine_versions() {
561        let versions = default_engine_versions();
562        let options = default_orderable_options();
563
564        assert_eq!(options.len(), 63); // 9 versions * 7 instance classes
565                                       // Verify all engines and versions have orderable options
566        for version in &versions {
567            assert!(options.iter().any(|opt| {
568                opt.engine == version.engine && opt.engine_version == version.engine_version
569            }));
570        }
571    }
572
573    #[test]
574    fn begin_instance_creation_rejects_duplicate_identifiers() {
575        let mut state = RdsState::new("123456789012", "us-east-1");
576
577        assert!(state.begin_instance_creation("db-1"));
578        assert!(!state.begin_instance_creation("db-1"));
579
580        state.cancel_instance_creation("db-1");
581        assert!(state.begin_instance_creation("db-1"));
582    }
583
584    #[test]
585    fn arn_helpers_format_correctly() {
586        let state = RdsState::new("123456789012", "eu-west-1");
587        assert!(state.db_instance_arn("mydb").contains(":db:mydb"));
588        assert!(state.db_snapshot_arn("snap1").contains(":snapshot:snap1"));
589        assert!(state.db_subnet_group_arn("sng").contains("sng"));
590        assert!(state.db_parameter_group_arn("pg").contains("pg"));
591    }
592
593    #[test]
594    fn next_dbi_resource_id_format() {
595        let state = RdsState::new("123456789012", "us-east-1");
596        let id = state.next_dbi_resource_id();
597        assert!(id.starts_with("db-"));
598        assert!(id.len() > 3);
599    }
600
601    #[test]
602    fn default_engine_versions_list_not_empty() {
603        let versions = default_engine_versions();
604        assert!(!versions.is_empty());
605    }
606
607    #[test]
608    fn default_orderable_options_list_not_empty() {
609        let opts = default_orderable_options();
610        assert!(!opts.is_empty());
611    }
612
613    #[test]
614    fn default_parameter_groups_returned_per_family() {
615        let groups = default_parameter_groups("123456789012", "us-east-1");
616        assert!(!groups.is_empty());
617    }
618
619    fn make_instance(id: &str) -> DbInstance {
620        let created_at = Utc::now();
621        DbInstance {
622            db_instance_identifier: id.to_string(),
623            db_instance_arn: format!("arn:aws:rds:us-east-1:123:db:{id}"),
624            db_instance_class: "db.t3.micro".to_string(),
625            engine: "postgres".to_string(),
626            engine_version: "16.3".to_string(),
627            db_instance_status: "available".to_string(),
628            master_username: "admin".to_string(),
629            db_name: None,
630            endpoint_address: "x".to_string(),
631            port: 5432,
632            allocated_storage: 20,
633            publicly_accessible: false,
634            deletion_protection: false,
635            created_at,
636            dbi_resource_id: "d".to_string(),
637            master_user_password: "p".to_string(),
638            container_id: "c".to_string(),
639            host_port: 0,
640            tags: Vec::new(),
641            read_replica_source_db_instance_identifier: None,
642            read_replica_db_instance_identifiers: Vec::new(),
643            vpc_security_group_ids: Vec::new(),
644            db_parameter_group_name: None,
645            backup_retention_period: 0,
646            preferred_backup_window: String::new(),
647            latest_restorable_time: None,
648            option_group_name: None,
649            multi_az: false,
650            pending_modified_values: None,
651        }
652    }
653
654    #[test]
655    fn finish_instance_creation_moves_from_pending_to_instances() {
656        let mut state = RdsState::new("123456789012", "us-east-1");
657        assert!(state.begin_instance_creation("db-x"));
658        assert!(state.in_progress_instance_ids.contains("db-x"));
659        state.finish_instance_creation(make_instance("db-x"));
660        assert!(!state.in_progress_instance_ids.contains("db-x"));
661        assert!(state.instances.contains_key("db-x"));
662    }
663
664    #[test]
665    fn cancel_instance_creation_drops_pending() {
666        let mut state = RdsState::new("123456789012", "us-east-1");
667        state.begin_instance_creation("db-y");
668        state.cancel_instance_creation("db-y");
669        assert!(!state.in_progress_instance_ids.contains("db-y"));
670    }
671
672    #[test]
673    fn begin_instance_creation_rejects_when_already_created() {
674        let mut state = RdsState::new("123456789012", "us-east-1");
675        state
676            .instances
677            .insert("db-z".to_string(), make_instance("db-z"));
678        assert!(!state.begin_instance_creation("db-z"));
679    }
680
681    #[test]
682    fn reset_restores_default_parameter_groups() {
683        let mut state = RdsState::new("123456789012", "us-east-1");
684        state.parameter_groups.clear();
685        state.reset();
686        assert!(!state.parameter_groups.is_empty());
687    }
688
689    #[test]
690    fn arn_helpers_include_region_and_account() {
691        let state = RdsState::new("111122223333", "ap-southeast-2");
692        let arn = state.db_instance_arn("my-db");
693        assert!(arn.contains("111122223333"));
694        assert!(arn.contains("ap-southeast-2"));
695        let snap = state.db_snapshot_arn("snap");
696        assert!(snap.contains("snapshot:snap"));
697    }
698
699    #[test]
700    fn next_dbi_resource_id_unique_across_calls() {
701        let state = RdsState::new("123", "us-east-1");
702        let a = state.next_dbi_resource_id();
703        let b = state.next_dbi_resource_id();
704        assert_ne!(a, b);
705    }
706}