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: "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 ("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); 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); 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}