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
18pub 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 #[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 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 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 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 ("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); 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); 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}