Skip to main content

fakecloud_memorydb/
service.rs

1//! MemoryDB (`memorydb`) awsJson1.1 dispatch + operation handlers.
2//!
3//! Full 45-op control plane. Clusters transition `creating` -> `available`
4//! lazily on describe (deterministic, no background timer needed for the
5//! control-plane contract). The Redis/Valkey data-plane container backing is a
6//! follow-on, mirroring how Aurora DSQL shipped its control plane first.
7
8use std::collections::BTreeMap;
9use std::sync::Arc;
10
11use async_trait::async_trait;
12use chrono::Utc;
13use http::StatusCode;
14use serde_json::{json, Value};
15use tokio::sync::Mutex as AsyncMutex;
16
17use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
18use fakecloud_persistence::SnapshotStore;
19
20use crate::persistence::save_snapshot;
21use crate::state::{
22    Acl, Cluster, Endpoint, MemoryDbState, MultiRegionCluster, Node, ParameterGroup, ReservedNode,
23    Shard, SharedMemoryDbState, Snapshot, SubnetGroup, User, UserAuthentication,
24};
25
26/// Every operation name in the MemoryDB Smithy model.
27pub const MEMORYDB_ACTIONS: &[&str] = &[
28    "BatchUpdateCluster",
29    "CopySnapshot",
30    "CreateACL",
31    "CreateCluster",
32    "CreateMultiRegionCluster",
33    "CreateParameterGroup",
34    "CreateSnapshot",
35    "CreateSubnetGroup",
36    "CreateUser",
37    "DeleteACL",
38    "DeleteCluster",
39    "DeleteMultiRegionCluster",
40    "DeleteParameterGroup",
41    "DeleteSnapshot",
42    "DeleteSubnetGroup",
43    "DeleteUser",
44    "DescribeACLs",
45    "DescribeClusters",
46    "DescribeEngineVersions",
47    "DescribeEvents",
48    "DescribeMultiRegionClusters",
49    "DescribeMultiRegionParameterGroups",
50    "DescribeMultiRegionParameters",
51    "DescribeParameterGroups",
52    "DescribeParameters",
53    "DescribeReservedNodes",
54    "DescribeReservedNodesOfferings",
55    "DescribeServiceUpdates",
56    "DescribeSnapshots",
57    "DescribeSubnetGroups",
58    "DescribeUsers",
59    "FailoverShard",
60    "ListAllowedMultiRegionClusterUpdates",
61    "ListAllowedNodeTypeUpdates",
62    "ListTags",
63    "PurchaseReservedNodesOffering",
64    "ResetParameterGroup",
65    "TagResource",
66    "UntagResource",
67    "UpdateACL",
68    "UpdateCluster",
69    "UpdateMultiRegionCluster",
70    "UpdateParameterGroup",
71    "UpdateSubnetGroup",
72    "UpdateUser",
73];
74
75const DEFAULT_ENGINE: &str = "redis";
76const DEFAULT_ENGINE_VERSION: &str = "7.1";
77const DEFAULT_PATCH_VERSION: &str = "7.1.1";
78
79pub struct MemoryDbService {
80    state: SharedMemoryDbState,
81    snapshot_store: Option<Arc<dyn SnapshotStore>>,
82    snapshot_lock: Arc<AsyncMutex<()>>,
83}
84
85impl MemoryDbService {
86    pub fn new(state: SharedMemoryDbState) -> Self {
87        Self {
88            state,
89            snapshot_store: None,
90            snapshot_lock: Arc::new(AsyncMutex::new(())),
91        }
92    }
93
94    pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
95        self.snapshot_store = Some(store);
96        self
97    }
98
99    async fn save(&self) {
100        save_snapshot(
101            &self.state,
102            self.snapshot_store.clone(),
103            &self.snapshot_lock,
104        )
105        .await;
106    }
107}
108
109#[async_trait]
110impl AwsService for MemoryDbService {
111    fn service_name(&self) -> &str {
112        "memorydb"
113    }
114
115    async fn handle(&self, request: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
116        let mutates = is_mutating(request.action.as_str());
117        let result = dispatch(self, &request);
118        if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
119            self.save().await;
120        }
121        result
122    }
123
124    fn supported_actions(&self) -> &[&str] {
125        MEMORYDB_ACTIONS
126    }
127}
128
129fn is_mutating(action: &str) -> bool {
130    // Note: FailoverShard and BatchUpdateCluster do not persist any state (they
131    // validate + echo), so they are intentionally NOT flagged mutating — no
132    // redundant snapshot write on those calls.
133    action.starts_with("Create")
134        || action.starts_with("Delete")
135        || action.starts_with("Update")
136        || action.starts_with("Tag")
137        || action.starts_with("Untag")
138        || action.starts_with("Copy")
139        || action.starts_with("Reset")
140        || action.starts_with("Purchase")
141}
142
143fn dispatch(s: &MemoryDbService, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
144    match req.action.as_str() {
145        "CreateCluster" => s.create_cluster(req),
146        "DescribeClusters" => s.describe_clusters(req),
147        "UpdateCluster" => s.update_cluster(req),
148        "DeleteCluster" => s.delete_cluster(req),
149        "BatchUpdateCluster" => s.batch_update_cluster(req),
150        "FailoverShard" => s.failover_shard(req),
151        "CreateACL" => s.create_acl(req),
152        "DescribeACLs" => s.describe_acls(req),
153        "UpdateACL" => s.update_acl(req),
154        "DeleteACL" => s.delete_acl(req),
155        "CreateUser" => s.create_user(req),
156        "DescribeUsers" => s.describe_users(req),
157        "UpdateUser" => s.update_user(req),
158        "DeleteUser" => s.delete_user(req),
159        "CreateParameterGroup" => s.create_parameter_group(req),
160        "DescribeParameterGroups" => s.describe_parameter_groups(req),
161        "UpdateParameterGroup" => s.update_parameter_group(req),
162        "ResetParameterGroup" => s.reset_parameter_group(req),
163        "DeleteParameterGroup" => s.delete_parameter_group(req),
164        "DescribeParameters" => s.describe_parameters(req),
165        "CreateSubnetGroup" => s.create_subnet_group(req),
166        "DescribeSubnetGroups" => s.describe_subnet_groups(req),
167        "UpdateSubnetGroup" => s.update_subnet_group(req),
168        "DeleteSubnetGroup" => s.delete_subnet_group(req),
169        "CreateSnapshot" => s.create_snapshot(req),
170        "CopySnapshot" => s.copy_snapshot(req),
171        "DescribeSnapshots" => s.describe_snapshots(req),
172        "DeleteSnapshot" => s.delete_snapshot(req),
173        "CreateMultiRegionCluster" => s.create_multi_region_cluster(req),
174        "DescribeMultiRegionClusters" => s.describe_multi_region_clusters(req),
175        "UpdateMultiRegionCluster" => s.update_multi_region_cluster(req),
176        "DeleteMultiRegionCluster" => s.delete_multi_region_cluster(req),
177        "ListAllowedMultiRegionClusterUpdates" => s.list_allowed_mr_updates(req),
178        "DescribeMultiRegionParameterGroups" => s.describe_mr_parameter_groups(req),
179        "DescribeMultiRegionParameters" => s.describe_mr_parameters(req),
180        "TagResource" => s.tag_resource(req),
181        "UntagResource" => s.untag_resource(req),
182        "ListTags" => s.list_tags(req),
183        "DescribeEngineVersions" => s.describe_engine_versions(req),
184        "DescribeEvents" => s.describe_events(req),
185        "DescribeServiceUpdates" => s.describe_service_updates(req),
186        "DescribeReservedNodes" => s.describe_reserved_nodes(req),
187        "DescribeReservedNodesOfferings" => s.describe_reserved_nodes_offerings(req),
188        "PurchaseReservedNodesOffering" => s.purchase_reserved_nodes_offering(req),
189        "ListAllowedNodeTypeUpdates" => s.list_allowed_node_type_updates(req),
190        _ => Err(AwsServiceError::action_not_implemented(
191            s.service_name(),
192            &req.action,
193        )),
194    }
195}
196
197// ===== helpers =====
198
199fn ok(v: Value) -> Result<AwsResponse, AwsServiceError> {
200    Ok(AwsResponse::json_value(StatusCode::OK, v))
201}
202
203fn parse(req: &AwsRequest) -> Result<Value, AwsServiceError> {
204    if req.body.is_empty() {
205        return Ok(json!({}));
206    }
207    serde_json::from_slice(&req.body).map_err(|e| {
208        fault(
209            "InvalidParameterValueException",
210            &format!("malformed body: {e}"),
211        )
212    })
213}
214
215fn fault(code: &str, msg: &str) -> AwsServiceError {
216    // The MemoryDB model declares `httpError: 404` for the entire
217    // `*NotFoundFault` family (ClusterNotFoundFault, ACLNotFoundFault, ...),
218    // with the single exception of ServiceLinkedRoleNotFoundFault (400).
219    // Everything else (InvalidParameterValueException, *AlreadyExistsFault,
220    // etc.) is a 400.
221    let status = if code.ends_with("NotFoundFault") && code != "ServiceLinkedRoleNotFoundFault" {
222        StatusCode::NOT_FOUND
223    } else {
224        StatusCode::BAD_REQUEST
225    };
226    AwsServiceError::aws_error(status, code, msg)
227}
228
229fn req_str<'a>(b: &'a Value, f: &str) -> Result<&'a str, AwsServiceError> {
230    b.get(f).and_then(Value::as_str).ok_or_else(|| {
231        fault(
232            "InvalidParameterValueException",
233            &format!("{f} is required."),
234        )
235    })
236}
237
238fn opt_str(b: &Value, f: &str) -> Option<String> {
239    b.get(f).and_then(Value::as_str).map(str::to_string)
240}
241
242fn str_list(b: &Value, f: &str) -> Vec<String> {
243    b.get(f)
244        .and_then(Value::as_array)
245        .map(|a| {
246            a.iter()
247                .filter_map(|x| x.as_str().map(str::to_string))
248                .collect()
249        })
250        .unwrap_or_default()
251}
252
253fn arn(kind: &str, region: &str, account: &str, name: &str) -> String {
254    format!("arn:aws:memorydb:{region}:{account}:{kind}/{name}")
255}
256
257/// Read an optional integer request field, defaulting when absent and
258/// range-validating before the `as i32` cast so an out-of-range value is a
259/// clean 400 rather than a silent truncation/wrap.
260fn int_field(
261    b: &Value,
262    field: &str,
263    default: i64,
264    min: i64,
265    max: i64,
266) -> Result<i32, AwsServiceError> {
267    let v = b.get(field).and_then(Value::as_i64).unwrap_or(default);
268    if !(min..=max).contains(&v) {
269        return Err(fault(
270            "InvalidParameterValueException",
271            &format!("{field} must be between {min} and {max}."),
272        ));
273    }
274    Ok(v as i32)
275}
276
277fn parse_tags(b: &Value) -> BTreeMap<String, String> {
278    let mut out = BTreeMap::new();
279    if let Some(arr) = b.get("Tags").and_then(Value::as_array) {
280        for t in arr {
281            if let (Some(k), Some(v)) = (
282                t.get("Key").and_then(Value::as_str),
283                t.get("Value").and_then(Value::as_str),
284            ) {
285                out.insert(k.to_string(), v.to_string());
286            }
287        }
288    }
289    out
290}
291
292fn tags_json(tags: &BTreeMap<String, String>) -> Vec<Value> {
293    tags.iter()
294        .map(|(k, v)| json!({ "Key": k, "Value": v }))
295        .collect()
296}
297
298/// Decode a numeric next-token; unparseable/empty -> page start 0 (`None` for
299/// a genuinely bad token so callers return a terminal empty page).
300fn page_start(b: &Value) -> Option<usize> {
301    match b.get("NextToken").and_then(Value::as_str) {
302        None | Some("") => Some(0),
303        Some(t) => t.parse::<usize>().ok(),
304    }
305}
306
307fn max_results(b: &Value, default: usize) -> usize {
308    b.get("MaxResults")
309        .and_then(Value::as_i64)
310        .map(|n| n.max(1) as usize)
311        .unwrap_or(default)
312}
313
314/// Paginate a pre-sorted Vec of JSON items into (page, next_token).
315fn paginate(items: Vec<Value>, b: &Value, default_max: usize) -> (Vec<Value>, Option<String>) {
316    let Some(start) = page_start(b) else {
317        return (Vec::new(), None);
318    };
319    let max = max_results(b, default_max);
320    let total = items.len();
321    if start >= total {
322        return (Vec::new(), None);
323    }
324    let end = start.saturating_add(max).min(total);
325    let next = (end < total).then(|| end.to_string());
326    (items[start..end].to_vec(), next)
327}
328
329// ===== JSON builders (member names match the Smithy model) =====
330
331fn endpoint_json(e: &Endpoint) -> Value {
332    json!({ "Address": e.address, "Port": e.port })
333}
334
335fn node_json(n: &Node) -> Value {
336    json!({
337        "Name": n.name,
338        "Status": n.status,
339        "AvailabilityZone": n.availability_zone,
340        "CreateTime": n.create_time.timestamp(),
341        "Endpoint": endpoint_json(&n.endpoint),
342    })
343}
344
345fn shard_json(sh: &Shard) -> Value {
346    json!({
347        "Name": sh.name,
348        "Status": sh.status,
349        "Slots": sh.slots,
350        "Nodes": sh.nodes.iter().map(node_json).collect::<Vec<_>>(),
351        "NumberOfNodes": sh.number_of_nodes,
352    })
353}
354
355/// `cluster_json`, but drops the per-shard `Shards` detail when `show_shards`
356/// is false (DescribeClusters `ShowShardDetails=false`).
357fn cluster_json_shards(c: &Cluster, show_shards: bool) -> Value {
358    let mut v = cluster_json(c);
359    if !show_shards {
360        if let Some(obj) = v.as_object_mut() {
361            obj.remove("Shards");
362        }
363    }
364    v
365}
366
367fn cluster_json(c: &Cluster) -> Value {
368    json!({
369        "Name": c.name,
370        "Description": c.description,
371        "Status": c.status,
372        "NumberOfShards": c.number_of_shards,
373        "Shards": c.shards.iter().map(shard_json).collect::<Vec<_>>(),
374        "AvailabilityMode": c.availability_mode,
375        "ClusterEndpoint": endpoint_json(&c.cluster_endpoint),
376        "NodeType": c.node_type,
377        "Engine": c.engine,
378        "EngineVersion": c.engine_version,
379        "EnginePatchVersion": c.engine_patch_version,
380        "ParameterGroupName": c.parameter_group_name,
381        "ParameterGroupStatus": c.parameter_group_status,
382        "SecurityGroups": c.security_group_ids.iter().map(|id| json!({ "SecurityGroupId": id, "Status": "active" })).collect::<Vec<_>>(),
383        "SubnetGroupName": c.subnet_group_name,
384        "TLSEnabled": c.tls_enabled,
385        "KmsKeyId": c.kms_key_id,
386        "ARN": c.arn,
387        "SnsTopicArn": c.sns_topic_arn,
388        "SnsTopicStatus": c.sns_topic_arn.as_ref().map(|_| "active"),
389        "SnapshotRetentionLimit": c.snapshot_retention_limit,
390        "MaintenanceWindow": c.maintenance_window,
391        "SnapshotWindow": c.snapshot_window,
392        "ACLName": c.acl_name,
393        "AutoMinorVersionUpgrade": c.auto_minor_version_upgrade,
394        "DataTiering": c.data_tiering,
395        "NetworkType": c.network_type,
396        "IpDiscovery": c.ip_discovery,
397    })
398}
399
400fn acl_json(a: &Acl) -> Value {
401    json!({
402        "Name": a.name,
403        "Status": a.status,
404        "UserNames": a.user_names,
405        "MinimumEngineVersion": a.minimum_engine_version,
406        "Clusters": a.clusters,
407        "ARN": a.arn,
408    })
409}
410
411fn user_json(u: &User) -> Value {
412    json!({
413        "Name": u.name,
414        "Status": u.status,
415        "AccessString": u.access_string,
416        "ACLNames": u.acl_names,
417        "MinimumEngineVersion": u.minimum_engine_version,
418        "Authentication": {
419            "Type": u.authentication.type_,
420            "PasswordCount": u.authentication.password_count,
421        },
422        "ARN": u.arn,
423    })
424}
425
426fn pg_json(p: &ParameterGroup) -> Value {
427    json!({
428        "Name": p.name,
429        "Family": p.family,
430        "Description": p.description,
431        "ARN": p.arn,
432    })
433}
434
435fn sg_json(g: &SubnetGroup) -> Value {
436    json!({
437        "Name": g.name,
438        "Description": g.description,
439        "VpcId": g.vpc_id,
440        "Subnets": g.subnet_ids.iter().map(|id| json!({
441            "Identifier": id,
442            "AvailabilityZone": { "Name": "us-east-1a" },
443            "SupportedNetworkTypes": ["ipv4"],
444        })).collect::<Vec<_>>(),
445        "ARN": g.arn,
446        "SupportedNetworkTypes": g.supported_network_types,
447    })
448}
449
450fn snapshot_json(s: &Snapshot) -> Value {
451    snapshot_json_detail(s, true)
452}
453
454/// `snapshot_json`, dropping the heavy `ClusterConfiguration` when `show_detail`
455/// is false (DescribeSnapshots `ShowDetail=false`).
456fn snapshot_json_detail(s: &Snapshot, show_detail: bool) -> Value {
457    let mut v = json!({
458        "Name": s.name,
459        "Status": s.status,
460        "Source": s.source,
461        "KmsKeyId": s.kms_key_id,
462        "ARN": s.arn,
463        "ClusterConfiguration": s.cluster_configuration,
464        "DataTiering": s.data_tiering,
465    });
466    if !show_detail {
467        if let Some(obj) = v.as_object_mut() {
468            obj.remove("ClusterConfiguration");
469        }
470    }
471    v
472}
473
474fn mrc_json(m: &MultiRegionCluster) -> Value {
475    json!({
476        "MultiRegionClusterName": m.name,
477        "Description": m.description,
478        "Status": m.status,
479        "NodeType": m.node_type,
480        "Engine": m.engine,
481        "EngineVersion": m.engine_version,
482        "NumberOfShards": m.number_of_shards,
483        "Clusters": m.clusters,
484        "MultiRegionParameterGroupName": m.multi_region_parameter_group_name,
485        "TLSEnabled": m.tls_enabled,
486        "ARN": m.arn,
487    })
488}
489
490fn reserved_node_json(r: &ReservedNode) -> Value {
491    json!({
492        "ReservationId": r.reservation_id,
493        "ReservedNodesOfferingId": r.reserved_nodes_offering_id,
494        "NodeType": r.node_type,
495        "StartTime": r.start_time.timestamp(),
496        "Duration": r.duration,
497        "FixedPrice": r.fixed_price,
498        "NodeCount": r.node_count,
499        "OfferingType": r.offering_type,
500        "State": r.state,
501        "ARN": r.arn,
502    })
503}
504
505// ===== handlers =====
506
507impl MemoryDbService {
508    fn create_cluster(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
509        let b = parse(req)?;
510        let name = req_str(&b, "ClusterName")?.to_string();
511        let node_type = req_str(&b, "NodeType")?.to_string();
512        let acl_name = req_str(&b, "ACLName")?.to_string();
513        let mut accounts = self.state.write();
514        let st = accounts.get_or_create(&req.account_id);
515        if st.clusters.contains_key(&name) {
516            return Err(fault(
517                "ClusterAlreadyExistsFault",
518                &format!("Cluster {name} already exists."),
519            ));
520        }
521        // AWS caps: up to 500 shards per cluster, 0-5 replicas per shard.
522        // Validate before building node vectors so an out-of-range request is a
523        // clean 400 rather than an unbounded allocation.
524        let num_shards = b.get("NumShards").and_then(Value::as_i64).unwrap_or(1);
525        if !(1..=500).contains(&num_shards) {
526            return Err(fault(
527                "InvalidParameterValueException",
528                "NumShards must be between 1 and 500.",
529            ));
530        }
531        let num_shards = num_shards as i32;
532        let replicas = b
533            .get("NumReplicasPerShard")
534            .and_then(Value::as_i64)
535            .unwrap_or(1);
536        if !(0..=5).contains(&replicas) {
537            return Err(fault(
538                "InvalidParameterValueException",
539                "NumReplicasPerShard must be between 0 and 5.",
540            ));
541        }
542        let replicas = replicas as i32;
543        // SnapshotRetentionLimit: AWS accepts 0-35 days.
544        let snapshot_retention_limit = int_field(&b, "SnapshotRetentionLimit", 0, 0, 35)?;
545        let port = b.get("Port").and_then(Value::as_i64).unwrap_or(6379) as i32;
546        let engine = opt_str(&b, "Engine").unwrap_or_else(|| DEFAULT_ENGINE.to_string());
547        let engine_version =
548            opt_str(&b, "EngineVersion").unwrap_or_else(|| DEFAULT_ENGINE_VERSION.to_string());
549        let cluster_arn = arn("cluster", &req.region, &req.account_id, &name);
550        let endpoint = Endpoint {
551            address: format!(
552                "clustercfg.{name}.abc123.memorydb.{}.amazonaws.com",
553                req.region
554            ),
555            port,
556        };
557        let shards = (0..num_shards)
558            .map(|i| {
559                let sname = format!("{:04}", i + 1);
560                let nodes = (0..=replicas)
561                    .map(|j| Node {
562                        name: format!("{name}-{sname}-{:03}", j + 1),
563                        status: "creating".to_string(),
564                        availability_zone: format!("{}a", req.region),
565                        create_time: Utc::now(),
566                        endpoint: Endpoint {
567                            address: format!(
568                                "{name}-{sname}-{:03}.{name}.abc123.memorydb.{}.amazonaws.com",
569                                j + 1,
570                                req.region
571                            ),
572                            port,
573                        },
574                    })
575                    .collect::<Vec<_>>();
576                // Partition the 16384 keyspace slots contiguously across shards,
577                // the way MemoryDB/Redis Cluster does (shard 1 `0-8191`,
578                // shard 2 `8192-16383`, ...), instead of giving every shard the
579                // full range.
580                let start = (i as i64 * 16384 / num_shards as i64) as i32;
581                let end = ((i as i64 + 1) * 16384 / num_shards as i64) as i32 - 1;
582                Shard {
583                    name: sname,
584                    status: "creating".to_string(),
585                    slots: format!("{start}-{end}"),
586                    number_of_nodes: nodes.len() as i32,
587                    nodes,
588                }
589            })
590            .collect();
591        let cluster = Cluster {
592            name: name.clone(),
593            description: opt_str(&b, "Description"),
594            status: "creating".to_string(),
595            number_of_shards: num_shards,
596            shards,
597            node_type,
598            engine,
599            engine_version,
600            engine_patch_version: DEFAULT_PATCH_VERSION.to_string(),
601            parameter_group_name: opt_str(&b, "ParameterGroupName")
602                .unwrap_or_else(|| "default.memorydb-redis7".to_string()),
603            parameter_group_status: "in-sync".to_string(),
604            security_group_ids: str_list(&b, "SecurityGroupIds"),
605            subnet_group_name: opt_str(&b, "SubnetGroupName")
606                .unwrap_or_else(|| "default".to_string()),
607            tls_enabled: b.get("TLSEnabled").and_then(Value::as_bool).unwrap_or(true),
608            kms_key_id: opt_str(&b, "KmsKeyId"),
609            arn: cluster_arn.clone(),
610            sns_topic_arn: opt_str(&b, "SnsTopicArn"),
611            snapshot_retention_limit,
612            maintenance_window: opt_str(&b, "MaintenanceWindow")
613                .unwrap_or_else(|| "wed:08:00-wed:09:00".to_string()),
614            snapshot_window: opt_str(&b, "SnapshotWindow")
615                .unwrap_or_else(|| "05:00-06:00".to_string()),
616            acl_name,
617            auto_minor_version_upgrade: b
618                .get("AutoMinorVersionUpgrade")
619                .and_then(Value::as_bool)
620                .unwrap_or(true),
621            data_tiering: opt_str(&b, "DataTiering")
622                .map(|d| {
623                    if d == "true" {
624                        "true".to_string()
625                    } else {
626                        "false".to_string()
627                    }
628                })
629                .unwrap_or_else(|| "false".to_string()),
630            availability_mode: if replicas > 0 {
631                "MultiAZ".to_string()
632            } else {
633                "SingleAZ".to_string()
634            },
635            cluster_endpoint: endpoint,
636            network_type: opt_str(&b, "NetworkType").unwrap_or_else(|| "ipv4".to_string()),
637            ip_discovery: opt_str(&b, "IpDiscovery").unwrap_or_else(|| "ipv4".to_string()),
638            port,
639            container_id: None,
640            created_at: Utc::now(),
641        };
642        let tags = parse_tags(&b);
643        if !tags.is_empty() {
644            st.tags.insert(cluster_arn, tags);
645        }
646        // Maintain the ACL -> clusters back-reference so DescribeACLs lists the
647        // clusters using each ACL (AWS keeps this bidirectional).
648        if let Some(acl) = st.acls.get_mut(&cluster.acl_name) {
649            if !acl.clusters.contains(&name) {
650                acl.clusters.push(name.clone());
651            }
652        }
653        st.clusters.insert(name, cluster.clone());
654        ok(json!({ "Cluster": cluster_json(&cluster) }))
655    }
656
657    fn describe_clusters(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
658        let b = parse(req)?;
659        // ShowShardDetails defaults to true; when explicitly false AWS omits the
660        // per-shard `Shards` detail from each cluster.
661        let show_shards = b
662            .get("ShowShardDetails")
663            .and_then(Value::as_bool)
664            .unwrap_or(true);
665        let mut accounts = self.state.write();
666        let st = accounts.get_or_create(&req.account_id);
667        // Lazy transition creating -> available. This mutates under a write lock
668        // on a Describe (non-persisted) path, but the transition is idempotent
669        // and self-healing: after a restart the next Describe re-applies it, and
670        // the first mutating op snapshots the settled state. No data is lost.
671        for c in st.clusters.values_mut() {
672            if c.status == "creating" {
673                c.status = "available".to_string();
674                for sh in &mut c.shards {
675                    sh.status = "available".to_string();
676                    for n in &mut sh.nodes {
677                        n.status = "available".to_string();
678                    }
679                }
680            }
681        }
682        if let Some(name) = opt_str(&b, "ClusterName") {
683            let Some(c) = st.clusters.get(&name) else {
684                return Err(fault(
685                    "ClusterNotFoundFault",
686                    &format!("Cluster {name} not found."),
687                ));
688            };
689            return ok(json!({ "Clusters": [cluster_json_shards(c, show_shards)] }));
690        }
691        let items: Vec<Value> = st
692            .clusters
693            .values()
694            .map(|c| cluster_json_shards(c, show_shards))
695            .collect();
696        let (page, next) = paginate(items, &b, 100);
697        ok(json!({ "Clusters": page, "NextToken": next }))
698    }
699
700    fn update_cluster(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
701        let b = parse(req)?;
702        let name = req_str(&b, "ClusterName")?.to_string();
703        let mut accounts = self.state.write();
704        let st = accounts.get_or_create(&req.account_id);
705        let Some(c) = st.clusters.get_mut(&name) else {
706            return Err(fault(
707                "ClusterNotFoundFault",
708                &format!("Cluster {name} not found."),
709            ));
710        };
711        if let Some(d) = opt_str(&b, "Description") {
712            c.description = Some(d);
713        }
714        if let Some(v) = opt_str(&b, "EngineVersion") {
715            c.engine_version = v;
716        }
717        if let Some(v) = opt_str(&b, "Engine") {
718            c.engine = v;
719        }
720        if let Some(v) = opt_str(&b, "NodeType") {
721            c.node_type = v;
722        }
723        if let Some(v) = opt_str(&b, "ParameterGroupName") {
724            c.parameter_group_name = v;
725        }
726        // Track an ACL reassignment so the ACL -> clusters back-references can be
727        // moved after the cluster borrow is released.
728        let acl_change = opt_str(&b, "ACLName").map(|new_acl| {
729            let old_acl = c.acl_name.clone();
730            c.acl_name = new_acl.clone();
731            (old_acl, new_acl)
732        });
733        if let Some(v) = opt_str(&b, "MaintenanceWindow") {
734            c.maintenance_window = v;
735        }
736        if let Some(v) = opt_str(&b, "SnapshotWindow") {
737            c.snapshot_window = v;
738        }
739        if b.get("SnapshotRetentionLimit").is_some() {
740            c.snapshot_retention_limit = int_field(&b, "SnapshotRetentionLimit", 0, 0, 35)?;
741        }
742        if let Some(sg) = b.get("SecurityGroupIds").and_then(Value::as_array) {
743            c.security_group_ids = sg
744                .iter()
745                .filter_map(|x| x.as_str().map(str::to_string))
746                .collect();
747        }
748        let out = cluster_json(c);
749        // Apply any ACL reassignment to the ACL -> clusters back-references.
750        if let Some((old_acl, new_acl)) = acl_change {
751            if old_acl != new_acl {
752                if let Some(a) = st.acls.get_mut(&old_acl) {
753                    a.clusters.retain(|cn| cn != &name);
754                }
755                if let Some(a) = st.acls.get_mut(&new_acl) {
756                    if !a.clusters.contains(&name) {
757                        a.clusters.push(name.clone());
758                    }
759                }
760            }
761        }
762        ok(json!({ "Cluster": out }))
763    }
764
765    fn delete_cluster(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
766        let b = parse(req)?;
767        let name = req_str(&b, "ClusterName")?.to_string();
768        let mut accounts = self.state.write();
769        let st = accounts.get_or_create(&req.account_id);
770        let Some(mut c) = st.clusters.remove(&name) else {
771            return Err(fault(
772                "ClusterNotFoundFault",
773                &format!("Cluster {name} not found."),
774            ));
775        };
776        c.status = "deleting".to_string();
777        st.tags.remove(&c.arn);
778        // Drop the ACL -> clusters back-reference.
779        if let Some(a) = st.acls.get_mut(&c.acl_name) {
780            a.clusters.retain(|cn| cn != &name);
781        }
782        ok(json!({ "Cluster": cluster_json(&c) }))
783    }
784
785    fn batch_update_cluster(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
786        let b = parse(req)?;
787        let names = str_list(&b, "ClusterNames");
788        let accounts = self.state.read();
789        let st = accounts.get(&req.account_id);
790        let mut processed = Vec::new();
791        let mut unprocessed = Vec::new();
792        for n in &names {
793            match st.and_then(|s| s.clusters.get(n)) {
794                Some(c) => processed.push(cluster_json(c)),
795                // A name that doesn't resolve to a cluster comes back in
796                // UnprocessedClusters, not silently dropped.
797                None => unprocessed.push(json!({
798                    "ClusterName": n,
799                    "ErrorType": "ClusterNotFoundFault",
800                    "ErrorMessage": format!("Cluster {n} not found."),
801                })),
802            }
803        }
804        ok(json!({ "ProcessedClusters": processed, "UnprocessedClusters": unprocessed }))
805    }
806
807    fn failover_shard(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
808        let b = parse(req)?;
809        let name = req_str(&b, "ClusterName")?.to_string();
810        // ShardName is required by the model and must name an existing shard.
811        let shard_name = req_str(&b, "ShardName")?.to_string();
812        let accounts = self.state.read();
813        let st = accounts.get(&req.account_id);
814        let Some(c) = st.and_then(|s| s.clusters.get(&name)) else {
815            return Err(fault(
816                "ClusterNotFoundFault",
817                &format!("Cluster {name} not found."),
818            ));
819        };
820        if !c.shards.iter().any(|s| s.name == shard_name) {
821            return Err(fault(
822                "ShardNotFoundFault",
823                &format!("Shard {shard_name} not found in cluster {name}."),
824            ));
825        }
826        ok(json!({ "Cluster": cluster_json(c) }))
827    }
828
829    fn create_acl(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
830        let b = parse(req)?;
831        let name = req_str(&b, "ACLName")?.to_string();
832        let mut accounts = self.state.write();
833        let st = accounts.get_or_create(&req.account_id);
834        if st.acls.contains_key(&name) {
835            return Err(fault(
836                "ACLAlreadyExistsFault",
837                &format!("ACL {name} already exists."),
838            ));
839        }
840        let acl_arn = arn("acl", &req.region, &req.account_id, &name);
841        let acl = Acl {
842            name: name.clone(),
843            status: "active".to_string(),
844            user_names: str_list(&b, "UserNames"),
845            minimum_engine_version: "6.2".to_string(),
846            clusters: vec![],
847            arn: acl_arn.clone(),
848        };
849        let tags = parse_tags(&b);
850        if !tags.is_empty() {
851            st.tags.insert(acl_arn, tags);
852        }
853        st.acls.insert(name.clone(), acl.clone());
854        // Maintain the User -> ACLNames back-reference (AWS keeps it bidirectional).
855        for u in &acl.user_names {
856            if let Some(user) = st.users.get_mut(u) {
857                if !user.acl_names.contains(&name) {
858                    user.acl_names.push(name.clone());
859                }
860            }
861        }
862        ok(json!({ "ACL": acl_json(&acl) }))
863    }
864
865    fn describe_acls(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
866        let b = parse(req)?;
867        let accounts = self.state.read();
868        let st = accounts.get(&req.account_id);
869        let Some(st) = st else {
870            return ok(json!({ "ACLs": [] }));
871        };
872        if let Some(name) = opt_str(&b, "ACLName") {
873            let Some(a) = st.acls.get(&name) else {
874                return Err(fault("ACLNotFoundFault", &format!("ACL {name} not found.")));
875            };
876            return ok(json!({ "ACLs": [acl_json(a)] }));
877        }
878        let items: Vec<Value> = st.acls.values().map(acl_json).collect();
879        let (page, next) = paginate(items, &b, 100);
880        ok(json!({ "ACLs": page, "NextToken": next }))
881    }
882
883    fn update_acl(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
884        let b = parse(req)?;
885        let name = req_str(&b, "ACLName")?.to_string();
886        let mut accounts = self.state.write();
887        let st = accounts.get_or_create(&req.account_id);
888        let Some(a) = st.acls.get_mut(&name) else {
889            return Err(fault("ACLNotFoundFault", &format!("ACL {name} not found.")));
890        };
891        let mut added = Vec::new();
892        for u in str_list(&b, "UserNamesToAdd") {
893            if !a.user_names.contains(&u) {
894                a.user_names.push(u.clone());
895                added.push(u);
896            }
897        }
898        let remove = str_list(&b, "UserNamesToRemove");
899        let removed: Vec<String> = a
900            .user_names
901            .iter()
902            .filter(|u| remove.contains(u))
903            .cloned()
904            .collect();
905        a.user_names.retain(|u| !remove.contains(u));
906        let out = acl_json(a);
907        // Sync the User -> ACLNames back-reference for the affected users.
908        for u in &added {
909            if let Some(user) = st.users.get_mut(u) {
910                if !user.acl_names.contains(&name) {
911                    user.acl_names.push(name.clone());
912                }
913            }
914        }
915        for u in &removed {
916            if let Some(user) = st.users.get_mut(u) {
917                user.acl_names.retain(|an| an != &name);
918            }
919        }
920        ok(json!({ "ACL": out }))
921    }
922
923    fn delete_acl(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
924        let b = parse(req)?;
925        let name = req_str(&b, "ACLName")?.to_string();
926        let mut accounts = self.state.write();
927        let st = accounts.get_or_create(&req.account_id);
928        let Some(mut a) = st.acls.remove(&name) else {
929            return Err(fault("ACLNotFoundFault", &format!("ACL {name} not found.")));
930        };
931        a.status = "deleting".to_string();
932        st.tags.remove(&a.arn);
933        // Drop the ACL from each member user's ACLNames back-reference.
934        for u in &a.user_names {
935            if let Some(user) = st.users.get_mut(u) {
936                user.acl_names.retain(|an| an != &name);
937            }
938        }
939        ok(json!({ "ACL": acl_json(&a) }))
940    }
941
942    fn create_user(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
943        let b = parse(req)?;
944        let name = req_str(&b, "UserName")?.to_string();
945        validate_user_name(&name)?;
946        let access_string = req_str(&b, "AccessString")?.to_string();
947        // AuthenticationMode is required by the model; its Type must be one of the
948        // InputAuthenticationType enum values (`password` | `iam`), and a
949        // `password`-type user must ship at least one password.
950        let auth = b.get("AuthenticationMode").ok_or_else(|| {
951            fault(
952                "InvalidParameterValueException",
953                "AuthenticationMode is required.",
954            )
955        })?;
956        let auth_type = auth
957            .get("Type")
958            .and_then(Value::as_str)
959            .ok_or_else(|| {
960                fault(
961                    "InvalidParameterValueException",
962                    "AuthenticationMode.Type is required.",
963                )
964            })?
965            .to_string();
966        if !matches!(auth_type.as_str(), "password" | "iam") {
967            return Err(fault(
968                "InvalidParameterValueException",
969                &format!(
970                    "Invalid AuthenticationMode.Type: {auth_type}. Valid values: password, iam."
971                ),
972            ));
973        }
974        let pw_count = auth
975            .get("Passwords")
976            .and_then(Value::as_array)
977            .map(|a| a.len() as i32)
978            .unwrap_or(0);
979        if auth_type == "password" && pw_count == 0 {
980            return Err(fault(
981                "InvalidParameterValueException",
982                "A password-authenticated user requires at least one password.",
983            ));
984        }
985        let mut accounts = self.state.write();
986        let st = accounts.get_or_create(&req.account_id);
987        if st.users.contains_key(&name) {
988            return Err(fault(
989                "UserAlreadyExistsFault",
990                &format!("User {name} already exists."),
991            ));
992        }
993        let user_arn = arn("user", &req.region, &req.account_id, &name);
994        let user = User {
995            name: name.clone(),
996            status: "active".to_string(),
997            access_string,
998            acl_names: vec![],
999            minimum_engine_version: "6.2".to_string(),
1000            authentication: UserAuthentication {
1001                type_: if auth_type == "iam" {
1002                    "iam".to_string()
1003                } else {
1004                    auth_type
1005                },
1006                password_count: pw_count,
1007            },
1008            arn: user_arn.clone(),
1009        };
1010        let tags = parse_tags(&b);
1011        if !tags.is_empty() {
1012            st.tags.insert(user_arn, tags);
1013        }
1014        st.users.insert(name, user.clone());
1015        ok(json!({ "User": user_json(&user) }))
1016    }
1017
1018    fn describe_users(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1019        let b = parse(req)?;
1020        let accounts = self.state.read();
1021        let st = accounts.get(&req.account_id);
1022        let Some(st) = st else {
1023            return ok(json!({ "Users": [] }));
1024        };
1025        if let Some(name) = opt_str(&b, "UserName") {
1026            let Some(u) = st.users.get(&name) else {
1027                return Err(fault(
1028                    "UserNotFoundFault",
1029                    &format!("User {name} not found."),
1030                ));
1031            };
1032            return ok(json!({ "Users": [user_json(u)] }));
1033        }
1034        let items: Vec<Value> = st.users.values().map(user_json).collect();
1035        let (page, next) = paginate(items, &b, 100);
1036        ok(json!({ "Users": page, "NextToken": next }))
1037    }
1038
1039    fn update_user(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1040        let b = parse(req)?;
1041        let name = req_str(&b, "UserName")?.to_string();
1042        let mut accounts = self.state.write();
1043        let st = accounts.get_or_create(&req.account_id);
1044        let Some(u) = st.users.get_mut(&name) else {
1045            return Err(fault(
1046                "UserNotFoundFault",
1047                &format!("User {name} not found."),
1048            ));
1049        };
1050        if let Some(a) = opt_str(&b, "AccessString") {
1051            u.access_string = a;
1052        }
1053        if let Some(auth) = b.get("AuthenticationMode") {
1054            if let Some(t) = auth.get("Type").and_then(Value::as_str) {
1055                u.authentication.type_ = t.to_string();
1056            }
1057            if let Some(p) = auth.get("Passwords").and_then(Value::as_array) {
1058                u.authentication.password_count = p.len() as i32;
1059            }
1060        }
1061        let out = user_json(u);
1062        ok(json!({ "User": out }))
1063    }
1064
1065    fn delete_user(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1066        let b = parse(req)?;
1067        let name = req_str(&b, "UserName")?.to_string();
1068        validate_user_name(&name)?;
1069        let mut accounts = self.state.write();
1070        let st = accounts.get_or_create(&req.account_id);
1071        let Some(mut u) = st.users.remove(&name) else {
1072            return Err(fault(
1073                "UserNotFoundFault",
1074                &format!("User {name} not found."),
1075            ));
1076        };
1077        u.status = "deleting".to_string();
1078        st.tags.remove(&u.arn);
1079        ok(json!({ "User": user_json(&u) }))
1080    }
1081
1082    fn create_parameter_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1083        let b = parse(req)?;
1084        let name = req_str(&b, "ParameterGroupName")?.to_string();
1085        let family = req_str(&b, "Family")?.to_string();
1086        let mut accounts = self.state.write();
1087        let st = accounts.get_or_create(&req.account_id);
1088        if st.parameter_groups.contains_key(&name) {
1089            return Err(fault(
1090                "ParameterGroupAlreadyExistsFault",
1091                &format!("Parameter group {name} already exists."),
1092            ));
1093        }
1094        let pg_arn = arn("parametergroup", &req.region, &req.account_id, &name);
1095        let pg = ParameterGroup {
1096            name: name.clone(),
1097            family,
1098            description: opt_str(&b, "Description").unwrap_or_default(),
1099            arn: pg_arn.clone(),
1100            parameters: BTreeMap::new(),
1101        };
1102        let tags = parse_tags(&b);
1103        if !tags.is_empty() {
1104            st.tags.insert(pg_arn, tags);
1105        }
1106        st.parameter_groups.insert(name, pg.clone());
1107        ok(json!({ "ParameterGroup": pg_json(&pg) }))
1108    }
1109
1110    fn describe_parameter_groups(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1111        let b = parse(req)?;
1112        let accounts = self.state.read();
1113        let st = accounts.get(&req.account_id);
1114        let Some(st) = st else {
1115            return ok(json!({ "ParameterGroups": [] }));
1116        };
1117        if let Some(name) = opt_str(&b, "ParameterGroupName") {
1118            let Some(p) = st.parameter_groups.get(&name) else {
1119                return Err(fault(
1120                    "ParameterGroupNotFoundFault",
1121                    &format!("Parameter group {name} not found."),
1122                ));
1123            };
1124            return ok(json!({ "ParameterGroups": [pg_json(p)] }));
1125        }
1126        let items: Vec<Value> = st.parameter_groups.values().map(pg_json).collect();
1127        let (page, next) = paginate(items, &b, 100);
1128        ok(json!({ "ParameterGroups": page, "NextToken": next }))
1129    }
1130
1131    fn update_parameter_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1132        let b = parse(req)?;
1133        let name = req_str(&b, "ParameterGroupName")?.to_string();
1134        let mut accounts = self.state.write();
1135        let st = accounts.get_or_create(&req.account_id);
1136        let Some(p) = st.parameter_groups.get_mut(&name) else {
1137            return Err(fault(
1138                "ParameterGroupNotFoundFault",
1139                &format!("Parameter group {name} not found."),
1140            ));
1141        };
1142        if let Some(arr) = b.get("ParameterNameValues").and_then(Value::as_array) {
1143            for pv in arr {
1144                if let (Some(pn), Some(pval)) = (
1145                    pv.get("ParameterName").and_then(Value::as_str),
1146                    pv.get("ParameterValue").and_then(Value::as_str),
1147                ) {
1148                    p.parameters.insert(pn.to_string(), pval.to_string());
1149                }
1150            }
1151        }
1152        let out = pg_json(p);
1153        ok(json!({ "ParameterGroup": out }))
1154    }
1155
1156    fn reset_parameter_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1157        let b = parse(req)?;
1158        let name = req_str(&b, "ParameterGroupName")?.to_string();
1159        let mut accounts = self.state.write();
1160        let st = accounts.get_or_create(&req.account_id);
1161        let Some(p) = st.parameter_groups.get_mut(&name) else {
1162            return Err(fault(
1163                "ParameterGroupNotFoundFault",
1164                &format!("Parameter group {name} not found."),
1165            ));
1166        };
1167        let all = b
1168            .get("AllParameters")
1169            .and_then(Value::as_bool)
1170            .unwrap_or(false);
1171        if all {
1172            p.parameters.clear();
1173        } else {
1174            for pv in b
1175                .get("ParameterNames")
1176                .and_then(Value::as_array)
1177                .into_iter()
1178                .flatten()
1179            {
1180                if let Some(pn) = pv.as_str() {
1181                    p.parameters.remove(pn);
1182                }
1183            }
1184        }
1185        let out = pg_json(p);
1186        ok(json!({ "ParameterGroup": out }))
1187    }
1188
1189    fn delete_parameter_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1190        let b = parse(req)?;
1191        let name = req_str(&b, "ParameterGroupName")?.to_string();
1192        let mut accounts = self.state.write();
1193        let st = accounts.get_or_create(&req.account_id);
1194        let Some(p) = st.parameter_groups.remove(&name) else {
1195            return Err(fault(
1196                "ParameterGroupNotFoundFault",
1197                &format!("Parameter group {name} not found."),
1198            ));
1199        };
1200        st.tags.remove(&p.arn);
1201        ok(json!({ "ParameterGroup": pg_json(&p) }))
1202    }
1203
1204    fn describe_parameters(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1205        let b = parse(req)?;
1206        let name = req_str(&b, "ParameterGroupName")?.to_string();
1207        let accounts = self.state.read();
1208        let st = accounts.get(&req.account_id);
1209        let Some(p) = st.and_then(|s| s.parameter_groups.get(&name)) else {
1210            return Err(fault(
1211                "ParameterGroupNotFoundFault",
1212                &format!("Parameter group {name} not found."),
1213            ));
1214        };
1215        let params: Vec<Value> = p
1216            .parameters
1217            .iter()
1218            .map(|(k, v)| {
1219                json!({
1220                    "Name": k, "Value": v, "Description": "", "DataType": "string",
1221                    "AllowedValues": "", "MinimumEngineVersion": "6.2",
1222                })
1223            })
1224            .collect();
1225        let (page, next) = paginate(params, &b, 100);
1226        ok(json!({ "Parameters": page, "NextToken": next }))
1227    }
1228
1229    fn create_subnet_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1230        let b = parse(req)?;
1231        let name = req_str(&b, "SubnetGroupName")?.to_string();
1232        let subnet_ids = str_list(&b, "SubnetIds");
1233        let mut accounts = self.state.write();
1234        let st = accounts.get_or_create(&req.account_id);
1235        if st.subnet_groups.contains_key(&name) {
1236            return Err(fault(
1237                "SubnetGroupAlreadyExistsFault",
1238                &format!("Subnet group {name} already exists."),
1239            ));
1240        }
1241        let sg_arn = arn("subnetgroup", &req.region, &req.account_id, &name);
1242        let sg = SubnetGroup {
1243            name: name.clone(),
1244            description: opt_str(&b, "Description").unwrap_or_default(),
1245            vpc_id: "vpc-00000000".to_string(),
1246            subnet_ids,
1247            arn: sg_arn.clone(),
1248            supported_network_types: vec!["ipv4".to_string()],
1249        };
1250        let tags = parse_tags(&b);
1251        if !tags.is_empty() {
1252            st.tags.insert(sg_arn, tags);
1253        }
1254        st.subnet_groups.insert(name, sg.clone());
1255        ok(json!({ "SubnetGroup": sg_json(&sg) }))
1256    }
1257
1258    fn describe_subnet_groups(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1259        let b = parse(req)?;
1260        let accounts = self.state.read();
1261        let st = accounts.get(&req.account_id);
1262        let Some(st) = st else {
1263            return ok(json!({ "SubnetGroups": [] }));
1264        };
1265        if let Some(name) = opt_str(&b, "SubnetGroupName") {
1266            let Some(g) = st.subnet_groups.get(&name) else {
1267                return Err(fault(
1268                    "SubnetGroupNotFoundFault",
1269                    &format!("Subnet group {name} not found."),
1270                ));
1271            };
1272            return ok(json!({ "SubnetGroups": [sg_json(g)] }));
1273        }
1274        let items: Vec<Value> = st.subnet_groups.values().map(sg_json).collect();
1275        let (page, next) = paginate(items, &b, 100);
1276        ok(json!({ "SubnetGroups": page, "NextToken": next }))
1277    }
1278
1279    fn update_subnet_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1280        let b = parse(req)?;
1281        let name = req_str(&b, "SubnetGroupName")?.to_string();
1282        let mut accounts = self.state.write();
1283        let st = accounts.get_or_create(&req.account_id);
1284        let Some(g) = st.subnet_groups.get_mut(&name) else {
1285            return Err(fault(
1286                "SubnetGroupNotFoundFault",
1287                &format!("Subnet group {name} not found."),
1288            ));
1289        };
1290        if let Some(d) = opt_str(&b, "Description") {
1291            g.description = d;
1292        }
1293        let ids = str_list(&b, "SubnetIds");
1294        if !ids.is_empty() {
1295            g.subnet_ids = ids;
1296        }
1297        let out = sg_json(g);
1298        ok(json!({ "SubnetGroup": out }))
1299    }
1300
1301    fn delete_subnet_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1302        let b = parse(req)?;
1303        let name = req_str(&b, "SubnetGroupName")?.to_string();
1304        let mut accounts = self.state.write();
1305        let st = accounts.get_or_create(&req.account_id);
1306        let Some(g) = st.subnet_groups.remove(&name) else {
1307            return Err(fault(
1308                "SubnetGroupNotFoundFault",
1309                &format!("Subnet group {name} not found."),
1310            ));
1311        };
1312        st.tags.remove(&g.arn);
1313        ok(json!({ "SubnetGroup": sg_json(&g) }))
1314    }
1315
1316    fn create_snapshot(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1317        let b = parse(req)?;
1318        let cluster_name = req_str(&b, "ClusterName")?.to_string();
1319        let snap_name = req_str(&b, "SnapshotName")?.to_string();
1320        let mut accounts = self.state.write();
1321        let st = accounts.get_or_create(&req.account_id);
1322        if st.snapshots.contains_key(&snap_name) {
1323            return Err(fault(
1324                "SnapshotAlreadyExistsFault",
1325                &format!("Snapshot {snap_name} already exists."),
1326            ));
1327        }
1328        let Some(c) = st.clusters.get(&cluster_name) else {
1329            return Err(fault(
1330                "ClusterNotFoundFault",
1331                &format!("Cluster {cluster_name} not found."),
1332            ));
1333        };
1334        let cfg = json!({
1335            "Name": c.name, "Description": c.description, "NodeType": c.node_type,
1336            "Engine": c.engine, "EngineVersion": c.engine_version, "MaintenanceWindow": c.maintenance_window,
1337            "TopicArn": c.sns_topic_arn, "Port": c.port, "ParameterGroupName": c.parameter_group_name,
1338            "SubnetGroupName": c.subnet_group_name, "VpcId": "vpc-00000000",
1339            "SnapshotRetentionLimit": c.snapshot_retention_limit, "SnapshotWindow": c.snapshot_window,
1340            "NumShards": c.number_of_shards,
1341        });
1342        let snap_arn = arn("snapshot", &req.region, &req.account_id, &snap_name);
1343        let snap = Snapshot {
1344            name: snap_name.clone(),
1345            status: "available".to_string(),
1346            source: "manual".to_string(),
1347            kms_key_id: opt_str(&b, "KmsKeyId"),
1348            arn: snap_arn.clone(),
1349            data_tiering: c.data_tiering.clone(),
1350            cluster_configuration: cfg,
1351        };
1352        let tags = parse_tags(&b);
1353        if !tags.is_empty() {
1354            st.tags.insert(snap_arn, tags);
1355        }
1356        st.snapshots.insert(snap_name, snap.clone());
1357        ok(json!({ "Snapshot": snapshot_json(&snap) }))
1358    }
1359
1360    fn copy_snapshot(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1361        let b = parse(req)?;
1362        let source = req_str(&b, "SourceSnapshotName")?.to_string();
1363        let target = req_str(&b, "TargetSnapshotName")?.to_string();
1364        let mut accounts = self.state.write();
1365        let st = accounts.get_or_create(&req.account_id);
1366        let Some(src) = st.snapshots.get(&source).cloned() else {
1367            return Err(fault(
1368                "SnapshotNotFoundFault",
1369                &format!("Snapshot {source} not found."),
1370            ));
1371        };
1372        if st.snapshots.contains_key(&target) {
1373            return Err(fault(
1374                "SnapshotAlreadyExistsFault",
1375                &format!("Snapshot {target} already exists."),
1376            ));
1377        }
1378        let snap_arn = arn("snapshot", &req.region, &req.account_id, &target);
1379        let snap = Snapshot {
1380            name: target.clone(),
1381            status: "available".to_string(),
1382            source: "manual".to_string(),
1383            kms_key_id: opt_str(&b, "KmsKeyId").or(src.kms_key_id),
1384            arn: snap_arn,
1385            data_tiering: src.data_tiering,
1386            cluster_configuration: src.cluster_configuration,
1387        };
1388        st.snapshots.insert(target, snap.clone());
1389        ok(json!({ "Snapshot": snapshot_json(&snap) }))
1390    }
1391
1392    fn describe_snapshots(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1393        let b = parse(req)?;
1394        let accounts = self.state.read();
1395        let st = accounts.get(&req.account_id);
1396        let Some(st) = st else {
1397            return ok(json!({ "Snapshots": [] }));
1398        };
1399        // ShowDetail defaults to true; false omits ClusterConfiguration.
1400        let show_detail = b.get("ShowDetail").and_then(Value::as_bool).unwrap_or(true);
1401        if let Some(name) = opt_str(&b, "SnapshotName") {
1402            let Some(s) = st.snapshots.get(&name) else {
1403                return Err(fault(
1404                    "SnapshotNotFoundFault",
1405                    &format!("Snapshot {name} not found."),
1406                ));
1407            };
1408            return ok(json!({ "Snapshots": [snapshot_json_detail(s, show_detail)] }));
1409        }
1410        let cluster_filter = opt_str(&b, "ClusterName");
1411        // Source filters by snapshot origin; "all" (or absent) matches everything.
1412        let source_filter = opt_str(&b, "Source").filter(|s| s != "all");
1413        let items: Vec<Value> = st
1414            .snapshots
1415            .values()
1416            .filter(|s| {
1417                cluster_filter.as_ref().is_none_or(|cn| {
1418                    s.cluster_configuration.get("Name").and_then(Value::as_str) == Some(cn)
1419                })
1420            })
1421            .filter(|s| source_filter.as_ref().is_none_or(|src| &s.source == src))
1422            .map(|s| snapshot_json_detail(s, show_detail))
1423            .collect();
1424        let (page, next) = paginate(items, &b, 50);
1425        ok(json!({ "Snapshots": page, "NextToken": next }))
1426    }
1427
1428    fn delete_snapshot(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1429        let b = parse(req)?;
1430        let name = req_str(&b, "SnapshotName")?.to_string();
1431        let mut accounts = self.state.write();
1432        let st = accounts.get_or_create(&req.account_id);
1433        let Some(mut s) = st.snapshots.remove(&name) else {
1434            return Err(fault(
1435                "SnapshotNotFoundFault",
1436                &format!("Snapshot {name} not found."),
1437            ));
1438        };
1439        s.status = "deleting".to_string();
1440        st.tags.remove(&s.arn);
1441        ok(json!({ "Snapshot": snapshot_json(&s) }))
1442    }
1443
1444    fn create_multi_region_cluster(
1445        &self,
1446        req: &AwsRequest,
1447    ) -> Result<AwsResponse, AwsServiceError> {
1448        let b = parse(req)?;
1449        let suffix = req_str(&b, "MultiRegionClusterNameSuffix")?.to_string();
1450        let node_type = req_str(&b, "NodeType")?.to_string();
1451        let name = format!("virxk-{suffix}");
1452        let mut accounts = self.state.write();
1453        let st = accounts.get_or_create(&req.account_id);
1454        if st.multi_region_clusters.contains_key(&name) {
1455            return Err(fault(
1456                "MultiRegionClusterAlreadyExistsFault",
1457                &format!("Multi-region cluster {name} already exists."),
1458            ));
1459        }
1460        let mrc_arn = arn("multiregioncluster", &req.region, &req.account_id, &name);
1461        let mrc = MultiRegionCluster {
1462            name: name.clone(),
1463            description: opt_str(&b, "Description"),
1464            status: "creating".to_string(),
1465            node_type,
1466            engine: opt_str(&b, "Engine").unwrap_or_else(|| DEFAULT_ENGINE.to_string()),
1467            engine_version: opt_str(&b, "EngineVersion")
1468                .unwrap_or_else(|| DEFAULT_ENGINE_VERSION.to_string()),
1469            number_of_shards: int_field(&b, "NumShards", 1, 1, 500)?,
1470            multi_region_parameter_group_name: opt_str(&b, "MultiRegionParameterGroupName"),
1471            tls_enabled: b.get("TLSEnabled").and_then(Value::as_bool).unwrap_or(true),
1472            arn: mrc_arn.clone(),
1473            clusters: json!([]),
1474        };
1475        let tags = parse_tags(&b);
1476        if !tags.is_empty() {
1477            st.tags.insert(mrc_arn, tags);
1478        }
1479        st.multi_region_clusters.insert(name, mrc.clone());
1480        ok(json!({ "MultiRegionCluster": mrc_json(&mrc) }))
1481    }
1482
1483    fn describe_multi_region_clusters(
1484        &self,
1485        req: &AwsRequest,
1486    ) -> Result<AwsResponse, AwsServiceError> {
1487        let b = parse(req)?;
1488        let mut accounts = self.state.write();
1489        let st = accounts.get_or_create(&req.account_id);
1490        for m in st.multi_region_clusters.values_mut() {
1491            if m.status == "creating" {
1492                m.status = "available".to_string();
1493            }
1494        }
1495        if let Some(name) = opt_str(&b, "MultiRegionClusterName") {
1496            let Some(m) = st.multi_region_clusters.get(&name) else {
1497                return Err(fault(
1498                    "MultiRegionClusterNotFoundFault",
1499                    &format!("Multi-region cluster {name} not found."),
1500                ));
1501            };
1502            return ok(json!({ "MultiRegionClusters": [mrc_json(m)] }));
1503        }
1504        let items: Vec<Value> = st.multi_region_clusters.values().map(mrc_json).collect();
1505        let (page, next) = paginate(items, &b, 100);
1506        ok(json!({ "MultiRegionClusters": page, "NextToken": next }))
1507    }
1508
1509    fn update_multi_region_cluster(
1510        &self,
1511        req: &AwsRequest,
1512    ) -> Result<AwsResponse, AwsServiceError> {
1513        let b = parse(req)?;
1514        let name = req_str(&b, "MultiRegionClusterName")?.to_string();
1515        let mut accounts = self.state.write();
1516        let st = accounts.get_or_create(&req.account_id);
1517        let Some(m) = st.multi_region_clusters.get_mut(&name) else {
1518            return Err(fault(
1519                "MultiRegionClusterNotFoundFault",
1520                &format!("Multi-region cluster {name} not found."),
1521            ));
1522        };
1523        if let Some(d) = opt_str(&b, "Description") {
1524            m.description = Some(d);
1525        }
1526        if let Some(v) = opt_str(&b, "EngineVersion") {
1527            m.engine_version = v;
1528        }
1529        if let Some(v) = opt_str(&b, "NodeType") {
1530            m.node_type = v;
1531        }
1532        let out = mrc_json(m);
1533        ok(json!({ "MultiRegionCluster": out }))
1534    }
1535
1536    fn delete_multi_region_cluster(
1537        &self,
1538        req: &AwsRequest,
1539    ) -> Result<AwsResponse, AwsServiceError> {
1540        let b = parse(req)?;
1541        let name = req_str(&b, "MultiRegionClusterName")?.to_string();
1542        let mut accounts = self.state.write();
1543        let st = accounts.get_or_create(&req.account_id);
1544        let Some(mut m) = st.multi_region_clusters.remove(&name) else {
1545            return Err(fault(
1546                "MultiRegionClusterNotFoundFault",
1547                &format!("Multi-region cluster {name} not found."),
1548            ));
1549        };
1550        m.status = "deleting".to_string();
1551        st.tags.remove(&m.arn);
1552        ok(json!({ "MultiRegionCluster": mrc_json(&m) }))
1553    }
1554
1555    fn list_allowed_mr_updates(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1556        let b = parse(req)?;
1557        let name = req_str(&b, "MultiRegionClusterName")?.to_string();
1558        let accounts = self.state.read();
1559        if accounts
1560            .get(&req.account_id)
1561            .and_then(|s| s.multi_region_clusters.get(&name))
1562            .is_none()
1563        {
1564            return Err(fault(
1565                "MultiRegionClusterNotFoundFault",
1566                &format!("Multi-region cluster {name} not found."),
1567            ));
1568        }
1569        ok(
1570            json!({ "ScaleUpNodeTypes": ["db.r7g.large", "db.r7g.xlarge"], "ScaleDownNodeTypes": [] }),
1571        )
1572    }
1573
1574    fn describe_mr_parameter_groups(
1575        &self,
1576        req: &AwsRequest,
1577    ) -> Result<AwsResponse, AwsServiceError> {
1578        let _ = parse(req)?;
1579        ok(json!({ "MultiRegionParameterGroups": [], "NextToken": Value::Null }))
1580    }
1581
1582    fn describe_mr_parameters(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1583        let b = parse(req)?;
1584        req_str(&b, "MultiRegionParameterGroupName")?;
1585        ok(json!({ "MultiRegionParameters": [], "NextToken": Value::Null }))
1586    }
1587
1588    fn tag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1589        let b = parse(req)?;
1590        let resource_arn = req_str(&b, "ResourceArn")?.to_string();
1591        let mut accounts = self.state.write();
1592        let st = accounts.get_or_create(&req.account_id);
1593        if !arn_exists(st, &resource_arn) {
1594            return Err(fault(
1595                "InvalidARNFault",
1596                &format!("{resource_arn} does not refer to a known resource."),
1597            ));
1598        }
1599        let entry = st.tags.entry(resource_arn).or_default();
1600        for (k, v) in parse_tags(&b) {
1601            entry.insert(k, v);
1602        }
1603        let all = entry.clone();
1604        ok(json!({ "TagList": tags_json(&all) }))
1605    }
1606
1607    fn untag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1608        let b = parse(req)?;
1609        let resource_arn = req_str(&b, "ResourceArn")?.to_string();
1610        let mut accounts = self.state.write();
1611        let st = accounts.get_or_create(&req.account_id);
1612        if !arn_exists(st, &resource_arn) {
1613            return Err(fault(
1614                "InvalidARNFault",
1615                &format!("{resource_arn} does not refer to a known resource."),
1616            ));
1617        }
1618        let keys = str_list(&b, "TagKeys");
1619        let entry = st.tags.entry(resource_arn).or_default();
1620        for k in &keys {
1621            entry.remove(k);
1622        }
1623        let all = entry.clone();
1624        ok(json!({ "TagList": tags_json(&all) }))
1625    }
1626
1627    fn list_tags(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1628        let b = parse(req)?;
1629        let resource_arn = req_str(&b, "ResourceArn")?.to_string();
1630        let accounts = self.state.read();
1631        let st = accounts.get(&req.account_id);
1632        let Some(st) = st else {
1633            return ok(json!({ "TagList": [] }));
1634        };
1635        if !arn_exists(st, &resource_arn) {
1636            return Err(fault(
1637                "InvalidARNFault",
1638                &format!("{resource_arn} does not refer to a known resource."),
1639            ));
1640        }
1641        let tags = st.tags.get(&resource_arn).cloned().unwrap_or_default();
1642        ok(json!({ "TagList": tags_json(&tags) }))
1643    }
1644
1645    fn describe_engine_versions(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1646        let _ = parse(req)?;
1647        let versions = json!([
1648            { "Engine": "redis", "EngineVersion": "7.1", "EnginePatchVersion": "7.1.1", "ParameterGroupFamily": "memorydb_redis7" },
1649            { "Engine": "redis", "EngineVersion": "6.2", "EnginePatchVersion": "6.2.6", "ParameterGroupFamily": "memorydb_redis6" },
1650            { "Engine": "valkey", "EngineVersion": "7.2", "EnginePatchVersion": "7.2.0", "ParameterGroupFamily": "memorydb_valkey7" },
1651        ]);
1652        ok(json!({ "EngineVersions": versions, "NextToken": Value::Null }))
1653    }
1654
1655    fn describe_events(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1656        let b = parse(req)?;
1657        if let Some(st) = opt_str(&b, "SourceType") {
1658            const VALID: &[&str] = &[
1659                "node",
1660                "parameter_group",
1661                "subnet_group",
1662                "cluster",
1663                "user",
1664                "acl",
1665            ];
1666            if !VALID.contains(&st.as_str()) {
1667                return Err(fault(
1668                    "InvalidParameterValueException",
1669                    &format!("Invalid SourceType: {st}"),
1670                ));
1671            }
1672        }
1673        ok(json!({ "Events": [], "NextToken": Value::Null }))
1674    }
1675
1676    fn describe_service_updates(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1677        let _ = parse(req)?;
1678        ok(json!({ "ServiceUpdates": [], "NextToken": Value::Null }))
1679    }
1680
1681    fn describe_reserved_nodes(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1682        let b = parse(req)?;
1683        let accounts = self.state.read();
1684        let st = accounts.get(&req.account_id);
1685        let Some(st) = st else {
1686            return ok(json!({ "ReservedNodes": [] }));
1687        };
1688        let items: Vec<Value> = st.reserved_nodes.values().map(reserved_node_json).collect();
1689        let (page, next) = paginate(items, &b, 100);
1690        ok(json!({ "ReservedNodes": page, "NextToken": next }))
1691    }
1692
1693    fn describe_reserved_nodes_offerings(
1694        &self,
1695        req: &AwsRequest,
1696    ) -> Result<AwsResponse, AwsServiceError> {
1697        let _ = parse(req)?;
1698        let offerings = json!([
1699            {
1700                "ReservedNodesOfferingId": "00000000-1111-2222-3333-444444444444",
1701                "NodeType": "db.r6g.large", "Duration": 31536000, "FixedPrice": 1200.0,
1702                "OfferingType": "All Upfront", "RecurringCharges": [],
1703            }
1704        ]);
1705        ok(json!({ "ReservedNodesOfferings": offerings, "NextToken": Value::Null }))
1706    }
1707
1708    fn purchase_reserved_nodes_offering(
1709        &self,
1710        req: &AwsRequest,
1711    ) -> Result<AwsResponse, AwsServiceError> {
1712        let b = parse(req)?;
1713        let offering_id = req_str(&b, "ReservedNodesOfferingId")?.to_string();
1714        let reservation_id = opt_str(&b, "ReservationId")
1715            .unwrap_or_else(|| format!("ri-{}", uuid::Uuid::new_v4().simple()));
1716        let count = int_field(&b, "NodeCount", 1, 1, i32::MAX as i64)?;
1717        let mut accounts = self.state.write();
1718        let st = accounts.get_or_create(&req.account_id);
1719        let node_arn = arn(
1720            "reservednode",
1721            &req.region,
1722            &req.account_id,
1723            &reservation_id,
1724        );
1725        let rn = ReservedNode {
1726            reservation_id: reservation_id.clone(),
1727            reserved_nodes_offering_id: offering_id,
1728            node_type: "db.r6g.large".to_string(),
1729            start_time: Utc::now(),
1730            duration: 31536000,
1731            fixed_price: 1200.0,
1732            node_count: count,
1733            offering_type: "All Upfront".to_string(),
1734            state: "active".to_string(),
1735            arn: node_arn,
1736        };
1737        st.reserved_nodes.insert(reservation_id, rn.clone());
1738        ok(json!({ "ReservedNode": reserved_node_json(&rn) }))
1739    }
1740
1741    fn list_allowed_node_type_updates(
1742        &self,
1743        req: &AwsRequest,
1744    ) -> Result<AwsResponse, AwsServiceError> {
1745        let b = parse(req)?;
1746        let name = req_str(&b, "ClusterName")?.to_string();
1747        let accounts = self.state.read();
1748        if accounts
1749            .get(&req.account_id)
1750            .and_then(|s| s.clusters.get(&name))
1751            .is_none()
1752        {
1753            return Err(fault(
1754                "ClusterNotFoundFault",
1755                &format!("Cluster {name} not found."),
1756            ));
1757        }
1758        ok(json!({
1759            "ScaleUpNodeTypes": ["db.r6g.xlarge", "db.r6g.2xlarge"],
1760            "ScaleDownNodeTypes": ["db.r6g.large"],
1761        }))
1762    }
1763}
1764
1765/// Validate a `UserName` against the model constraints: length >= 1 and
1766/// pattern `^[a-zA-Z][a-zA-Z0-9-]*$`.
1767fn validate_user_name(name: &str) -> Result<(), AwsServiceError> {
1768    let valid = !name.is_empty()
1769        && name.starts_with(|c: char| c.is_ascii_alphabetic())
1770        && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-');
1771    if valid {
1772        Ok(())
1773    } else {
1774        Err(fault(
1775            "InvalidParameterValueException",
1776            &format!("Invalid UserName: {name}"),
1777        ))
1778    }
1779}
1780
1781/// Whether a given ARN refers to any resource in this account's state.
1782fn arn_exists(st: &MemoryDbState, resource_arn: &str) -> bool {
1783    // An empty ARN never refers to a resource (guards against matching a
1784    // resource that happens to carry an empty ARN string).
1785    if resource_arn.is_empty() {
1786        return false;
1787    }
1788    st.clusters.values().any(|c| c.arn == resource_arn)
1789        || st.acls.values().any(|a| a.arn == resource_arn)
1790        || st.users.values().any(|u| u.arn == resource_arn)
1791        || st.parameter_groups.values().any(|p| p.arn == resource_arn)
1792        || st.subnet_groups.values().any(|g| g.arn == resource_arn)
1793        || st.snapshots.values().any(|s| s.arn == resource_arn)
1794        || st
1795            .multi_region_clusters
1796            .values()
1797            .any(|m| m.arn == resource_arn)
1798        || st.reserved_nodes.values().any(|r| r.arn == resource_arn)
1799}
1800
1801#[cfg(test)]
1802mod tests {
1803    use super::*;
1804    use fakecloud_core::multi_account::MultiAccountState;
1805
1806    fn service() -> MemoryDbService {
1807        let state = Arc::new(parking_lot::RwLock::new(MultiAccountState::new(
1808            "123456789012",
1809            "us-east-1",
1810            "http://localhost:4566",
1811        )));
1812        MemoryDbService::new(state)
1813    }
1814
1815    fn req(action: &str, body: Value) -> AwsRequest {
1816        AwsRequest {
1817            service: "memorydb".to_string(),
1818            action: action.to_string(),
1819            region: "us-east-1".to_string(),
1820            account_id: "123456789012".to_string(),
1821            request_id: "rid".to_string(),
1822            headers: http::HeaderMap::new(),
1823            query_params: std::collections::HashMap::new(),
1824            body: bytes::Bytes::from(serde_json::to_vec(&body).unwrap()),
1825            body_stream: parking_lot::Mutex::new(None),
1826            path_segments: vec![],
1827            raw_path: "/".to_string(),
1828            raw_query: String::new(),
1829            method: http::Method::POST,
1830            is_query_protocol: false,
1831            access_key_id: None,
1832            principal: None,
1833        }
1834    }
1835
1836    fn call(s: &MemoryDbService, action: &str, body: Value) -> Result<Value, AwsServiceError> {
1837        let resp = dispatch(s, &req(action, body))?;
1838        Ok(serde_json::from_slice(resp.body.expect_bytes()).unwrap())
1839    }
1840
1841    #[test]
1842    fn create_cluster_round_trips_and_describes() {
1843        let s = service();
1844        let out = call(
1845            &s,
1846            "CreateCluster",
1847            json!({"ClusterName": "app", "NodeType": "db.r6g.large", "ACLName": "open-access"}),
1848        )
1849        .unwrap();
1850        assert_eq!(out["Cluster"]["Name"], "app");
1851
1852        let desc = call(&s, "DescribeClusters", json!({"ClusterName": "app"})).unwrap();
1853        let clusters = desc["Clusters"].as_array().unwrap();
1854        assert_eq!(clusters.len(), 1);
1855        // Lazily transitions creating -> available on describe.
1856        assert_eq!(clusters[0]["Status"], "available");
1857    }
1858
1859    #[test]
1860    fn create_cluster_rejects_out_of_range_shard_count() {
1861        let s = service();
1862        // A huge NumShards must be rejected, not allocated (OOM guard).
1863        let err = call(
1864            &s,
1865            "CreateCluster",
1866            json!({"ClusterName": "big", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumShards": 2_000_000_000i64}),
1867        )
1868        .unwrap_err();
1869        assert!(
1870            err.message().contains("NumShards"),
1871            "got: {}",
1872            err.message()
1873        );
1874    }
1875
1876    #[test]
1877    fn create_cluster_rejects_out_of_range_replica_count() {
1878        let s = service();
1879        let err = call(
1880            &s,
1881            "CreateCluster",
1882            json!({"ClusterName": "r", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumReplicasPerShard": 99}),
1883        )
1884        .unwrap_err();
1885        assert!(
1886            err.message().contains("NumReplicasPerShard"),
1887            "got: {}",
1888            err.message()
1889        );
1890    }
1891
1892    #[test]
1893    fn create_user_rejects_invalid_name() {
1894        let s = service();
1895        // Must start with a letter.
1896        let err = call(
1897            &s,
1898            "CreateUser",
1899            json!({"UserName": "1bad", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "no-password-required"}}),
1900        )
1901        .unwrap_err();
1902        assert!(err.message().contains("UserName"), "got: {}", err.message());
1903    }
1904
1905    #[test]
1906    fn default_acl_and_user_are_seeded() {
1907        let s = service();
1908        let acls = call(&s, "DescribeACLs", json!({})).unwrap();
1909        assert!(acls["ACLs"]
1910            .as_array()
1911            .unwrap()
1912            .iter()
1913            .any(|a| a["Name"] == "open-access"));
1914        let users = call(&s, "DescribeUsers", json!({})).unwrap();
1915        assert!(users["Users"]
1916            .as_array()
1917            .unwrap()
1918            .iter()
1919            .any(|u| u["Name"] == "default"));
1920    }
1921
1922    #[test]
1923    fn seeded_default_acl_and_user_have_populated_arns() {
1924        let s = service();
1925        let acls = call(&s, "DescribeACLs", json!({"ACLName": "open-access"})).unwrap();
1926        assert_eq!(
1927            acls["ACLs"][0]["ARN"],
1928            "arn:aws:memorydb:us-east-1:123456789012:acl/open-access"
1929        );
1930        let users = call(&s, "DescribeUsers", json!({"UserName": "default"})).unwrap();
1931        assert_eq!(
1932            users["Users"][0]["ARN"],
1933            "arn:aws:memorydb:us-east-1:123456789012:user/default"
1934        );
1935    }
1936
1937    #[test]
1938    fn not_found_faults_return_http_404() {
1939        let s = service();
1940        let err = call(&s, "DescribeClusters", json!({"ClusterName": "nope"})).unwrap_err();
1941        assert_eq!(err.status(), http::StatusCode::NOT_FOUND);
1942        // A validation error stays a 400.
1943        let bad = call(
1944            &s,
1945            "CreateCluster",
1946            json!({"ClusterName": "c", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumShards": 9999}),
1947        )
1948        .unwrap_err();
1949        assert_eq!(bad.status(), http::StatusCode::BAD_REQUEST);
1950    }
1951
1952    #[test]
1953    fn create_cluster_populates_acl_clusters_backref() {
1954        let s = service();
1955        call(
1956            &s,
1957            "CreateCluster",
1958            json!({"ClusterName": "c1", "NodeType": "db.r6g.large", "ACLName": "open-access"}),
1959        )
1960        .unwrap();
1961        let acls = call(&s, "DescribeACLs", json!({"ACLName": "open-access"})).unwrap();
1962        let clusters = acls["ACLs"][0]["Clusters"].as_array().unwrap();
1963        assert!(clusters.iter().any(|c| c == "c1"), "got: {clusters:?}");
1964
1965        // Deleting the cluster removes the back-reference.
1966        call(&s, "DeleteCluster", json!({"ClusterName": "c1"})).unwrap();
1967        let acls = call(&s, "DescribeACLs", json!({"ACLName": "open-access"})).unwrap();
1968        assert!(acls["ACLs"][0]["Clusters"].as_array().unwrap().is_empty());
1969    }
1970
1971    #[test]
1972    fn acl_membership_syncs_user_acl_names_backref() {
1973        let s = service();
1974        call(
1975            &s,
1976            "CreateUser",
1977            json!({"UserName": "bob", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "iam"}}),
1978        )
1979        .unwrap();
1980        call(
1981            &s,
1982            "CreateACL",
1983            json!({"ACLName": "team", "UserNames": ["bob"]}),
1984        )
1985        .unwrap();
1986        let users = call(&s, "DescribeUsers", json!({"UserName": "bob"})).unwrap();
1987        let acl_names = users["Users"][0]["ACLNames"].as_array().unwrap();
1988        assert!(acl_names.iter().any(|a| a == "team"), "got: {acl_names:?}");
1989
1990        // Removing the user from the ACL drops the back-reference.
1991        call(
1992            &s,
1993            "UpdateACL",
1994            json!({"ACLName": "team", "UserNamesToRemove": ["bob"]}),
1995        )
1996        .unwrap();
1997        let users = call(&s, "DescribeUsers", json!({"UserName": "bob"})).unwrap();
1998        assert!(users["Users"][0]["ACLNames"]
1999            .as_array()
2000            .unwrap()
2001            .iter()
2002            .all(|a| a != "team"));
2003    }
2004
2005    #[test]
2006    fn tag_resource_rejects_empty_arn() {
2007        let s = service();
2008        let err = call(
2009            &s,
2010            "TagResource",
2011            json!({"ResourceArn": "", "Tags": [{"Key": "k", "Value": "v"}]}),
2012        )
2013        .unwrap_err();
2014        assert_eq!(err.status(), http::StatusCode::BAD_REQUEST);
2015        assert!(
2016            err.message().contains("known resource") || err.message().contains("ARN"),
2017            "got: {}",
2018            err.message()
2019        );
2020    }
2021
2022    #[test]
2023    fn create_user_requires_valid_authentication_mode() {
2024        let s = service();
2025        // Missing AuthenticationMode -> rejected.
2026        let e1 = call(
2027            &s,
2028            "CreateUser",
2029            json!({"UserName": "u1", "AccessString": "on ~* &* +@all"}),
2030        )
2031        .unwrap_err();
2032        assert!(
2033            e1.message().contains("AuthenticationMode"),
2034            "got: {}",
2035            e1.message()
2036        );
2037        // password type with zero passwords -> rejected.
2038        let e2 = call(
2039            &s,
2040            "CreateUser",
2041            json!({"UserName": "u2", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "password"}}),
2042        )
2043        .unwrap_err();
2044        assert!(e2.message().contains("password"), "got: {}", e2.message());
2045        // invalid Type -> rejected.
2046        let e3 = call(
2047            &s,
2048            "CreateUser",
2049            json!({"UserName": "u3", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "kerberos"}}),
2050        )
2051        .unwrap_err();
2052        assert!(
2053            e3.message().contains("kerberos") || e3.message().contains("Type"),
2054            "got: {}",
2055            e3.message()
2056        );
2057        // iam type -> accepted.
2058        call(
2059            &s,
2060            "CreateUser",
2061            json!({"UserName": "u4", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "iam"}}),
2062        )
2063        .unwrap();
2064    }
2065
2066    #[test]
2067    fn failover_shard_validates_shard_name() {
2068        let s = service();
2069        call(
2070            &s,
2071            "CreateCluster",
2072            json!({"ClusterName": "fc", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumShards": 1}),
2073        )
2074        .unwrap();
2075        // Unknown shard -> ShardNotFoundFault (404).
2076        let err = call(
2077            &s,
2078            "FailoverShard",
2079            json!({"ClusterName": "fc", "ShardName": "9999"}),
2080        )
2081        .unwrap_err();
2082        assert_eq!(err.status(), http::StatusCode::NOT_FOUND);
2083        // Real shard "0001" -> ok.
2084        call(
2085            &s,
2086            "FailoverShard",
2087            json!({"ClusterName": "fc", "ShardName": "0001"}),
2088        )
2089        .unwrap();
2090    }
2091
2092    #[test]
2093    fn batch_update_reports_unknown_clusters_as_unprocessed() {
2094        let s = service();
2095        call(
2096            &s,
2097            "CreateCluster",
2098            json!({"ClusterName": "known", "NodeType": "db.r6g.large", "ACLName": "open-access"}),
2099        )
2100        .unwrap();
2101        let out = call(
2102            &s,
2103            "BatchUpdateCluster",
2104            json!({"ClusterNames": ["known", "ghost"]}),
2105        )
2106        .unwrap();
2107        assert_eq!(out["ProcessedClusters"].as_array().unwrap().len(), 1);
2108        let un = out["UnprocessedClusters"].as_array().unwrap();
2109        assert_eq!(un.len(), 1);
2110        assert_eq!(un[0]["ClusterName"], "ghost");
2111    }
2112
2113    #[test]
2114    fn snapshot_retention_limit_out_of_range_rejected() {
2115        let s = service();
2116        let err = call(
2117            &s,
2118            "CreateCluster",
2119            json!({"ClusterName": "srl", "NodeType": "db.r6g.large", "ACLName": "open-access", "SnapshotRetentionLimit": 4294967297i64}),
2120        )
2121        .unwrap_err();
2122        assert_eq!(err.status(), http::StatusCode::BAD_REQUEST);
2123        assert!(
2124            err.message().contains("SnapshotRetentionLimit"),
2125            "got: {}",
2126            err.message()
2127        );
2128    }
2129
2130    #[test]
2131    fn shard_slots_are_partitioned_across_shards() {
2132        let s = service();
2133        call(
2134            &s,
2135            "CreateCluster",
2136            json!({"ClusterName": "sh", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumShards": 2}),
2137        )
2138        .unwrap();
2139        let desc = call(&s, "DescribeClusters", json!({"ClusterName": "sh"})).unwrap();
2140        let shards = desc["Clusters"][0]["Shards"].as_array().unwrap();
2141        assert_eq!(shards.len(), 2);
2142        assert_eq!(shards[0]["Slots"], "0-8191");
2143        assert_eq!(shards[1]["Slots"], "8192-16383");
2144    }
2145
2146    #[test]
2147    fn describe_clusters_show_shard_details_false_omits_shards() {
2148        let s = service();
2149        call(
2150            &s,
2151            "CreateCluster",
2152            json!({"ClusterName": "hide", "NodeType": "db.r6g.large", "ACLName": "open-access"}),
2153        )
2154        .unwrap();
2155        let with = call(&s, "DescribeClusters", json!({"ClusterName": "hide"})).unwrap();
2156        assert!(with["Clusters"][0].get("Shards").is_some());
2157        let without = call(
2158            &s,
2159            "DescribeClusters",
2160            json!({"ClusterName": "hide", "ShowShardDetails": false}),
2161        )
2162        .unwrap();
2163        assert!(without["Clusters"][0].get("Shards").is_none());
2164    }
2165
2166    #[test]
2167    fn describe_snapshots_honors_show_detail_and_source() {
2168        let s = service();
2169        call(
2170            &s,
2171            "CreateCluster",
2172            json!({"ClusterName": "snapc", "NodeType": "db.r6g.large", "ACLName": "open-access"}),
2173        )
2174        .unwrap();
2175        call(
2176            &s,
2177            "CreateSnapshot",
2178            json!({"ClusterName": "snapc", "SnapshotName": "s1"}),
2179        )
2180        .unwrap();
2181        // ShowDetail=false drops ClusterConfiguration.
2182        let brief = call(
2183            &s,
2184            "DescribeSnapshots",
2185            json!({"SnapshotName": "s1", "ShowDetail": false}),
2186        )
2187        .unwrap();
2188        assert!(brief["Snapshots"][0].get("ClusterConfiguration").is_none());
2189        // Source=system matches nothing (created snapshots are user-source).
2190        let sys = call(&s, "DescribeSnapshots", json!({"Source": "system"})).unwrap();
2191        assert!(sys["Snapshots"].as_array().unwrap().is_empty());
2192    }
2193
2194    #[test]
2195    fn default_parameter_groups_are_seeded() {
2196        let s = service();
2197        let groups = call(&s, "DescribeParameterGroups", json!({})).unwrap();
2198        let names: Vec<&str> = groups["ParameterGroups"]
2199            .as_array()
2200            .unwrap()
2201            .iter()
2202            .filter_map(|g| g["Name"].as_str())
2203            .collect();
2204        for expected in [
2205            "default.memorydb-redis7",
2206            "default.memorydb-redis6",
2207            "default.memorydb-valkey7",
2208        ] {
2209            assert!(
2210                names.contains(&expected),
2211                "missing {expected}; got {names:?}"
2212            );
2213        }
2214        // The default group is readable by name (the TF provider reads it).
2215        let one = call(
2216            &s,
2217            "DescribeParameterGroups",
2218            json!({"ParameterGroupName": "default.memorydb-redis7"}),
2219        )
2220        .unwrap();
2221        assert_eq!(one["ParameterGroups"][0]["Family"], "memorydb_redis7");
2222    }
2223}