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
355fn cluster_json(c: &Cluster) -> Value {
356    json!({
357        "Name": c.name,
358        "Description": c.description,
359        "Status": c.status,
360        "NumberOfShards": c.number_of_shards,
361        "Shards": c.shards.iter().map(shard_json).collect::<Vec<_>>(),
362        "AvailabilityMode": c.availability_mode,
363        "ClusterEndpoint": endpoint_json(&c.cluster_endpoint),
364        "NodeType": c.node_type,
365        "Engine": c.engine,
366        "EngineVersion": c.engine_version,
367        "EnginePatchVersion": c.engine_patch_version,
368        "ParameterGroupName": c.parameter_group_name,
369        "ParameterGroupStatus": c.parameter_group_status,
370        "SecurityGroups": c.security_group_ids.iter().map(|id| json!({ "SecurityGroupId": id, "Status": "active" })).collect::<Vec<_>>(),
371        "SubnetGroupName": c.subnet_group_name,
372        "TLSEnabled": c.tls_enabled,
373        "KmsKeyId": c.kms_key_id,
374        "ARN": c.arn,
375        "SnsTopicArn": c.sns_topic_arn,
376        "SnsTopicStatus": c.sns_topic_arn.as_ref().map(|_| "active"),
377        "SnapshotRetentionLimit": c.snapshot_retention_limit,
378        "MaintenanceWindow": c.maintenance_window,
379        "SnapshotWindow": c.snapshot_window,
380        "ACLName": c.acl_name,
381        "AutoMinorVersionUpgrade": c.auto_minor_version_upgrade,
382        "DataTiering": c.data_tiering,
383        "NetworkType": c.network_type,
384        "IpDiscovery": c.ip_discovery,
385    })
386}
387
388fn acl_json(a: &Acl) -> Value {
389    json!({
390        "Name": a.name,
391        "Status": a.status,
392        "UserNames": a.user_names,
393        "MinimumEngineVersion": a.minimum_engine_version,
394        "Clusters": a.clusters,
395        "ARN": a.arn,
396    })
397}
398
399fn user_json(u: &User) -> Value {
400    json!({
401        "Name": u.name,
402        "Status": u.status,
403        "AccessString": u.access_string,
404        "ACLNames": u.acl_names,
405        "MinimumEngineVersion": u.minimum_engine_version,
406        "Authentication": {
407            "Type": u.authentication.type_,
408            "PasswordCount": u.authentication.password_count,
409        },
410        "ARN": u.arn,
411    })
412}
413
414fn pg_json(p: &ParameterGroup) -> Value {
415    json!({
416        "Name": p.name,
417        "Family": p.family,
418        "Description": p.description,
419        "ARN": p.arn,
420    })
421}
422
423fn sg_json(g: &SubnetGroup) -> Value {
424    json!({
425        "Name": g.name,
426        "Description": g.description,
427        "VpcId": g.vpc_id,
428        "Subnets": g.subnet_ids.iter().map(|id| json!({
429            "Identifier": id,
430            "AvailabilityZone": { "Name": "us-east-1a" },
431            "SupportedNetworkTypes": ["ipv4"],
432        })).collect::<Vec<_>>(),
433        "ARN": g.arn,
434        "SupportedNetworkTypes": g.supported_network_types,
435    })
436}
437
438fn snapshot_json(s: &Snapshot) -> Value {
439    json!({
440        "Name": s.name,
441        "Status": s.status,
442        "Source": s.source,
443        "KmsKeyId": s.kms_key_id,
444        "ARN": s.arn,
445        "ClusterConfiguration": s.cluster_configuration,
446        "DataTiering": s.data_tiering,
447    })
448}
449
450fn mrc_json(m: &MultiRegionCluster) -> Value {
451    json!({
452        "MultiRegionClusterName": m.name,
453        "Description": m.description,
454        "Status": m.status,
455        "NodeType": m.node_type,
456        "Engine": m.engine,
457        "EngineVersion": m.engine_version,
458        "NumberOfShards": m.number_of_shards,
459        "Clusters": m.clusters,
460        "MultiRegionParameterGroupName": m.multi_region_parameter_group_name,
461        "TLSEnabled": m.tls_enabled,
462        "ARN": m.arn,
463    })
464}
465
466fn reserved_node_json(r: &ReservedNode) -> Value {
467    json!({
468        "ReservationId": r.reservation_id,
469        "ReservedNodesOfferingId": r.reserved_nodes_offering_id,
470        "NodeType": r.node_type,
471        "StartTime": r.start_time.timestamp(),
472        "Duration": r.duration,
473        "FixedPrice": r.fixed_price,
474        "NodeCount": r.node_count,
475        "OfferingType": r.offering_type,
476        "State": r.state,
477        "ARN": r.arn,
478    })
479}
480
481// ===== handlers =====
482
483impl MemoryDbService {
484    fn create_cluster(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
485        let b = parse(req)?;
486        let name = req_str(&b, "ClusterName")?.to_string();
487        let node_type = req_str(&b, "NodeType")?.to_string();
488        let acl_name = req_str(&b, "ACLName")?.to_string();
489        let mut accounts = self.state.write();
490        let st = accounts.get_or_create(&req.account_id);
491        if st.clusters.contains_key(&name) {
492            return Err(fault(
493                "ClusterAlreadyExistsFault",
494                &format!("Cluster {name} already exists."),
495            ));
496        }
497        // AWS caps: up to 500 shards per cluster, 0-5 replicas per shard.
498        // Validate before building node vectors so an out-of-range request is a
499        // clean 400 rather than an unbounded allocation.
500        let num_shards = b.get("NumShards").and_then(Value::as_i64).unwrap_or(1);
501        if !(1..=500).contains(&num_shards) {
502            return Err(fault(
503                "InvalidParameterValueException",
504                "NumShards must be between 1 and 500.",
505            ));
506        }
507        let num_shards = num_shards as i32;
508        let replicas = b
509            .get("NumReplicasPerShard")
510            .and_then(Value::as_i64)
511            .unwrap_or(1);
512        if !(0..=5).contains(&replicas) {
513            return Err(fault(
514                "InvalidParameterValueException",
515                "NumReplicasPerShard must be between 0 and 5.",
516            ));
517        }
518        let replicas = replicas as i32;
519        // SnapshotRetentionLimit: AWS accepts 0-35 days.
520        let snapshot_retention_limit = int_field(&b, "SnapshotRetentionLimit", 0, 0, 35)?;
521        let port = b.get("Port").and_then(Value::as_i64).unwrap_or(6379) as i32;
522        let engine = opt_str(&b, "Engine").unwrap_or_else(|| DEFAULT_ENGINE.to_string());
523        let engine_version =
524            opt_str(&b, "EngineVersion").unwrap_or_else(|| DEFAULT_ENGINE_VERSION.to_string());
525        let cluster_arn = arn("cluster", &req.region, &req.account_id, &name);
526        let endpoint = Endpoint {
527            address: format!(
528                "clustercfg.{name}.abc123.memorydb.{}.amazonaws.com",
529                req.region
530            ),
531            port,
532        };
533        let shards = (0..num_shards)
534            .map(|i| {
535                let sname = format!("{:04}", i + 1);
536                let nodes = (0..=replicas)
537                    .map(|j| Node {
538                        name: format!("{name}-{sname}-{:03}", j + 1),
539                        status: "creating".to_string(),
540                        availability_zone: format!("{}a", req.region),
541                        create_time: Utc::now(),
542                        endpoint: Endpoint {
543                            address: format!(
544                                "{name}-{sname}-{:03}.{name}.abc123.memorydb.{}.amazonaws.com",
545                                j + 1,
546                                req.region
547                            ),
548                            port,
549                        },
550                    })
551                    .collect::<Vec<_>>();
552                Shard {
553                    name: sname,
554                    status: "creating".to_string(),
555                    slots: "0-16383".to_string(),
556                    number_of_nodes: nodes.len() as i32,
557                    nodes,
558                }
559            })
560            .collect();
561        let cluster = Cluster {
562            name: name.clone(),
563            description: opt_str(&b, "Description"),
564            status: "creating".to_string(),
565            number_of_shards: num_shards,
566            shards,
567            node_type,
568            engine,
569            engine_version,
570            engine_patch_version: DEFAULT_PATCH_VERSION.to_string(),
571            parameter_group_name: opt_str(&b, "ParameterGroupName")
572                .unwrap_or_else(|| "default.memorydb-redis7".to_string()),
573            parameter_group_status: "in-sync".to_string(),
574            security_group_ids: str_list(&b, "SecurityGroupIds"),
575            subnet_group_name: opt_str(&b, "SubnetGroupName")
576                .unwrap_or_else(|| "default".to_string()),
577            tls_enabled: b.get("TLSEnabled").and_then(Value::as_bool).unwrap_or(true),
578            kms_key_id: opt_str(&b, "KmsKeyId"),
579            arn: cluster_arn.clone(),
580            sns_topic_arn: opt_str(&b, "SnsTopicArn"),
581            snapshot_retention_limit,
582            maintenance_window: opt_str(&b, "MaintenanceWindow")
583                .unwrap_or_else(|| "wed:08:00-wed:09:00".to_string()),
584            snapshot_window: opt_str(&b, "SnapshotWindow")
585                .unwrap_or_else(|| "05:00-06:00".to_string()),
586            acl_name,
587            auto_minor_version_upgrade: b
588                .get("AutoMinorVersionUpgrade")
589                .and_then(Value::as_bool)
590                .unwrap_or(true),
591            data_tiering: opt_str(&b, "DataTiering")
592                .map(|d| {
593                    if d == "true" {
594                        "true".to_string()
595                    } else {
596                        "false".to_string()
597                    }
598                })
599                .unwrap_or_else(|| "false".to_string()),
600            availability_mode: if replicas > 0 {
601                "MultiAZ".to_string()
602            } else {
603                "SingleAZ".to_string()
604            },
605            cluster_endpoint: endpoint,
606            network_type: opt_str(&b, "NetworkType").unwrap_or_else(|| "ipv4".to_string()),
607            ip_discovery: opt_str(&b, "IpDiscovery").unwrap_or_else(|| "ipv4".to_string()),
608            port,
609            container_id: None,
610            created_at: Utc::now(),
611        };
612        let tags = parse_tags(&b);
613        if !tags.is_empty() {
614            st.tags.insert(cluster_arn, tags);
615        }
616        // Maintain the ACL -> clusters back-reference so DescribeACLs lists the
617        // clusters using each ACL (AWS keeps this bidirectional).
618        if let Some(acl) = st.acls.get_mut(&cluster.acl_name) {
619            if !acl.clusters.contains(&name) {
620                acl.clusters.push(name.clone());
621            }
622        }
623        st.clusters.insert(name, cluster.clone());
624        ok(json!({ "Cluster": cluster_json(&cluster) }))
625    }
626
627    fn describe_clusters(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
628        let b = parse(req)?;
629        let mut accounts = self.state.write();
630        let st = accounts.get_or_create(&req.account_id);
631        // Lazy transition creating -> available.
632        for c in st.clusters.values_mut() {
633            if c.status == "creating" {
634                c.status = "available".to_string();
635                for sh in &mut c.shards {
636                    sh.status = "available".to_string();
637                    for n in &mut sh.nodes {
638                        n.status = "available".to_string();
639                    }
640                }
641            }
642        }
643        if let Some(name) = opt_str(&b, "ClusterName") {
644            let Some(c) = st.clusters.get(&name) else {
645                return Err(fault(
646                    "ClusterNotFoundFault",
647                    &format!("Cluster {name} not found."),
648                ));
649            };
650            return ok(json!({ "Clusters": [cluster_json(c)] }));
651        }
652        let items: Vec<Value> = st.clusters.values().map(cluster_json).collect();
653        let (page, next) = paginate(items, &b, 100);
654        ok(json!({ "Clusters": page, "NextToken": next }))
655    }
656
657    fn update_cluster(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
658        let b = parse(req)?;
659        let name = req_str(&b, "ClusterName")?.to_string();
660        let mut accounts = self.state.write();
661        let st = accounts.get_or_create(&req.account_id);
662        let Some(c) = st.clusters.get_mut(&name) else {
663            return Err(fault(
664                "ClusterNotFoundFault",
665                &format!("Cluster {name} not found."),
666            ));
667        };
668        if let Some(d) = opt_str(&b, "Description") {
669            c.description = Some(d);
670        }
671        if let Some(v) = opt_str(&b, "EngineVersion") {
672            c.engine_version = v;
673        }
674        if let Some(v) = opt_str(&b, "Engine") {
675            c.engine = v;
676        }
677        if let Some(v) = opt_str(&b, "NodeType") {
678            c.node_type = v;
679        }
680        if let Some(v) = opt_str(&b, "ParameterGroupName") {
681            c.parameter_group_name = v;
682        }
683        // Track an ACL reassignment so the ACL -> clusters back-references can be
684        // moved after the cluster borrow is released.
685        let acl_change = opt_str(&b, "ACLName").map(|new_acl| {
686            let old_acl = c.acl_name.clone();
687            c.acl_name = new_acl.clone();
688            (old_acl, new_acl)
689        });
690        if let Some(v) = opt_str(&b, "MaintenanceWindow") {
691            c.maintenance_window = v;
692        }
693        if let Some(v) = opt_str(&b, "SnapshotWindow") {
694            c.snapshot_window = v;
695        }
696        if b.get("SnapshotRetentionLimit").is_some() {
697            c.snapshot_retention_limit = int_field(&b, "SnapshotRetentionLimit", 0, 0, 35)?;
698        }
699        if let Some(sg) = b.get("SecurityGroupIds").and_then(Value::as_array) {
700            c.security_group_ids = sg
701                .iter()
702                .filter_map(|x| x.as_str().map(str::to_string))
703                .collect();
704        }
705        let out = cluster_json(c);
706        // Apply any ACL reassignment to the ACL -> clusters back-references.
707        if let Some((old_acl, new_acl)) = acl_change {
708            if old_acl != new_acl {
709                if let Some(a) = st.acls.get_mut(&old_acl) {
710                    a.clusters.retain(|cn| cn != &name);
711                }
712                if let Some(a) = st.acls.get_mut(&new_acl) {
713                    if !a.clusters.contains(&name) {
714                        a.clusters.push(name.clone());
715                    }
716                }
717            }
718        }
719        ok(json!({ "Cluster": out }))
720    }
721
722    fn delete_cluster(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
723        let b = parse(req)?;
724        let name = req_str(&b, "ClusterName")?.to_string();
725        let mut accounts = self.state.write();
726        let st = accounts.get_or_create(&req.account_id);
727        let Some(mut c) = st.clusters.remove(&name) else {
728            return Err(fault(
729                "ClusterNotFoundFault",
730                &format!("Cluster {name} not found."),
731            ));
732        };
733        c.status = "deleting".to_string();
734        st.tags.remove(&c.arn);
735        // Drop the ACL -> clusters back-reference.
736        if let Some(a) = st.acls.get_mut(&c.acl_name) {
737            a.clusters.retain(|cn| cn != &name);
738        }
739        ok(json!({ "Cluster": cluster_json(&c) }))
740    }
741
742    fn batch_update_cluster(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
743        let b = parse(req)?;
744        let names = str_list(&b, "ClusterNames");
745        let accounts = self.state.read();
746        let st = accounts.get(&req.account_id);
747        let mut processed = Vec::new();
748        let mut unprocessed = Vec::new();
749        for n in &names {
750            match st.and_then(|s| s.clusters.get(n)) {
751                Some(c) => processed.push(cluster_json(c)),
752                // A name that doesn't resolve to a cluster comes back in
753                // UnprocessedClusters, not silently dropped.
754                None => unprocessed.push(json!({
755                    "ClusterName": n,
756                    "ErrorType": "ClusterNotFoundFault",
757                    "ErrorMessage": format!("Cluster {n} not found."),
758                })),
759            }
760        }
761        ok(json!({ "ProcessedClusters": processed, "UnprocessedClusters": unprocessed }))
762    }
763
764    fn failover_shard(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
765        let b = parse(req)?;
766        let name = req_str(&b, "ClusterName")?.to_string();
767        // ShardName is required by the model and must name an existing shard.
768        let shard_name = req_str(&b, "ShardName")?.to_string();
769        let accounts = self.state.read();
770        let st = accounts.get(&req.account_id);
771        let Some(c) = st.and_then(|s| s.clusters.get(&name)) else {
772            return Err(fault(
773                "ClusterNotFoundFault",
774                &format!("Cluster {name} not found."),
775            ));
776        };
777        if !c.shards.iter().any(|s| s.name == shard_name) {
778            return Err(fault(
779                "ShardNotFoundFault",
780                &format!("Shard {shard_name} not found in cluster {name}."),
781            ));
782        }
783        ok(json!({ "Cluster": cluster_json(c) }))
784    }
785
786    fn create_acl(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
787        let b = parse(req)?;
788        let name = req_str(&b, "ACLName")?.to_string();
789        let mut accounts = self.state.write();
790        let st = accounts.get_or_create(&req.account_id);
791        if st.acls.contains_key(&name) {
792            return Err(fault(
793                "ACLAlreadyExistsFault",
794                &format!("ACL {name} already exists."),
795            ));
796        }
797        let acl_arn = arn("acl", &req.region, &req.account_id, &name);
798        let acl = Acl {
799            name: name.clone(),
800            status: "active".to_string(),
801            user_names: str_list(&b, "UserNames"),
802            minimum_engine_version: "6.2".to_string(),
803            clusters: vec![],
804            arn: acl_arn.clone(),
805        };
806        let tags = parse_tags(&b);
807        if !tags.is_empty() {
808            st.tags.insert(acl_arn, tags);
809        }
810        st.acls.insert(name.clone(), acl.clone());
811        // Maintain the User -> ACLNames back-reference (AWS keeps it bidirectional).
812        for u in &acl.user_names {
813            if let Some(user) = st.users.get_mut(u) {
814                if !user.acl_names.contains(&name) {
815                    user.acl_names.push(name.clone());
816                }
817            }
818        }
819        ok(json!({ "ACL": acl_json(&acl) }))
820    }
821
822    fn describe_acls(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
823        let b = parse(req)?;
824        let accounts = self.state.read();
825        let st = accounts.get(&req.account_id);
826        let Some(st) = st else {
827            return ok(json!({ "ACLs": [] }));
828        };
829        if let Some(name) = opt_str(&b, "ACLName") {
830            let Some(a) = st.acls.get(&name) else {
831                return Err(fault("ACLNotFoundFault", &format!("ACL {name} not found.")));
832            };
833            return ok(json!({ "ACLs": [acl_json(a)] }));
834        }
835        let items: Vec<Value> = st.acls.values().map(acl_json).collect();
836        let (page, next) = paginate(items, &b, 100);
837        ok(json!({ "ACLs": page, "NextToken": next }))
838    }
839
840    fn update_acl(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
841        let b = parse(req)?;
842        let name = req_str(&b, "ACLName")?.to_string();
843        let mut accounts = self.state.write();
844        let st = accounts.get_or_create(&req.account_id);
845        let Some(a) = st.acls.get_mut(&name) else {
846            return Err(fault("ACLNotFoundFault", &format!("ACL {name} not found.")));
847        };
848        let mut added = Vec::new();
849        for u in str_list(&b, "UserNamesToAdd") {
850            if !a.user_names.contains(&u) {
851                a.user_names.push(u.clone());
852                added.push(u);
853            }
854        }
855        let remove = str_list(&b, "UserNamesToRemove");
856        let removed: Vec<String> = a
857            .user_names
858            .iter()
859            .filter(|u| remove.contains(u))
860            .cloned()
861            .collect();
862        a.user_names.retain(|u| !remove.contains(u));
863        let out = acl_json(a);
864        // Sync the User -> ACLNames back-reference for the affected users.
865        for u in &added {
866            if let Some(user) = st.users.get_mut(u) {
867                if !user.acl_names.contains(&name) {
868                    user.acl_names.push(name.clone());
869                }
870            }
871        }
872        for u in &removed {
873            if let Some(user) = st.users.get_mut(u) {
874                user.acl_names.retain(|an| an != &name);
875            }
876        }
877        ok(json!({ "ACL": out }))
878    }
879
880    fn delete_acl(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
881        let b = parse(req)?;
882        let name = req_str(&b, "ACLName")?.to_string();
883        let mut accounts = self.state.write();
884        let st = accounts.get_or_create(&req.account_id);
885        let Some(mut a) = st.acls.remove(&name) else {
886            return Err(fault("ACLNotFoundFault", &format!("ACL {name} not found.")));
887        };
888        a.status = "deleting".to_string();
889        st.tags.remove(&a.arn);
890        // Drop the ACL from each member user's ACLNames back-reference.
891        for u in &a.user_names {
892            if let Some(user) = st.users.get_mut(u) {
893                user.acl_names.retain(|an| an != &name);
894            }
895        }
896        ok(json!({ "ACL": acl_json(&a) }))
897    }
898
899    fn create_user(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
900        let b = parse(req)?;
901        let name = req_str(&b, "UserName")?.to_string();
902        validate_user_name(&name)?;
903        let access_string = req_str(&b, "AccessString")?.to_string();
904        // AuthenticationMode is required by the model; its Type must be one of the
905        // InputAuthenticationType enum values (`password` | `iam`), and a
906        // `password`-type user must ship at least one password.
907        let auth = b.get("AuthenticationMode").ok_or_else(|| {
908            fault(
909                "InvalidParameterValueException",
910                "AuthenticationMode is required.",
911            )
912        })?;
913        let auth_type = auth
914            .get("Type")
915            .and_then(Value::as_str)
916            .ok_or_else(|| {
917                fault(
918                    "InvalidParameterValueException",
919                    "AuthenticationMode.Type is required.",
920                )
921            })?
922            .to_string();
923        if !matches!(auth_type.as_str(), "password" | "iam") {
924            return Err(fault(
925                "InvalidParameterValueException",
926                &format!(
927                    "Invalid AuthenticationMode.Type: {auth_type}. Valid values: password, iam."
928                ),
929            ));
930        }
931        let pw_count = auth
932            .get("Passwords")
933            .and_then(Value::as_array)
934            .map(|a| a.len() as i32)
935            .unwrap_or(0);
936        if auth_type == "password" && pw_count == 0 {
937            return Err(fault(
938                "InvalidParameterValueException",
939                "A password-authenticated user requires at least one password.",
940            ));
941        }
942        let mut accounts = self.state.write();
943        let st = accounts.get_or_create(&req.account_id);
944        if st.users.contains_key(&name) {
945            return Err(fault(
946                "UserAlreadyExistsFault",
947                &format!("User {name} already exists."),
948            ));
949        }
950        let user_arn = arn("user", &req.region, &req.account_id, &name);
951        let user = User {
952            name: name.clone(),
953            status: "active".to_string(),
954            access_string,
955            acl_names: vec![],
956            minimum_engine_version: "6.2".to_string(),
957            authentication: UserAuthentication {
958                type_: if auth_type == "iam" {
959                    "iam".to_string()
960                } else {
961                    auth_type
962                },
963                password_count: pw_count,
964            },
965            arn: user_arn.clone(),
966        };
967        let tags = parse_tags(&b);
968        if !tags.is_empty() {
969            st.tags.insert(user_arn, tags);
970        }
971        st.users.insert(name, user.clone());
972        ok(json!({ "User": user_json(&user) }))
973    }
974
975    fn describe_users(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
976        let b = parse(req)?;
977        let accounts = self.state.read();
978        let st = accounts.get(&req.account_id);
979        let Some(st) = st else {
980            return ok(json!({ "Users": [] }));
981        };
982        if let Some(name) = opt_str(&b, "UserName") {
983            let Some(u) = st.users.get(&name) else {
984                return Err(fault(
985                    "UserNotFoundFault",
986                    &format!("User {name} not found."),
987                ));
988            };
989            return ok(json!({ "Users": [user_json(u)] }));
990        }
991        let items: Vec<Value> = st.users.values().map(user_json).collect();
992        let (page, next) = paginate(items, &b, 100);
993        ok(json!({ "Users": page, "NextToken": next }))
994    }
995
996    fn update_user(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
997        let b = parse(req)?;
998        let name = req_str(&b, "UserName")?.to_string();
999        let mut accounts = self.state.write();
1000        let st = accounts.get_or_create(&req.account_id);
1001        let Some(u) = st.users.get_mut(&name) else {
1002            return Err(fault(
1003                "UserNotFoundFault",
1004                &format!("User {name} not found."),
1005            ));
1006        };
1007        if let Some(a) = opt_str(&b, "AccessString") {
1008            u.access_string = a;
1009        }
1010        if let Some(auth) = b.get("AuthenticationMode") {
1011            if let Some(t) = auth.get("Type").and_then(Value::as_str) {
1012                u.authentication.type_ = t.to_string();
1013            }
1014            if let Some(p) = auth.get("Passwords").and_then(Value::as_array) {
1015                u.authentication.password_count = p.len() as i32;
1016            }
1017        }
1018        let out = user_json(u);
1019        ok(json!({ "User": out }))
1020    }
1021
1022    fn delete_user(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1023        let b = parse(req)?;
1024        let name = req_str(&b, "UserName")?.to_string();
1025        validate_user_name(&name)?;
1026        let mut accounts = self.state.write();
1027        let st = accounts.get_or_create(&req.account_id);
1028        let Some(mut u) = st.users.remove(&name) else {
1029            return Err(fault(
1030                "UserNotFoundFault",
1031                &format!("User {name} not found."),
1032            ));
1033        };
1034        u.status = "deleting".to_string();
1035        st.tags.remove(&u.arn);
1036        ok(json!({ "User": user_json(&u) }))
1037    }
1038
1039    fn create_parameter_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1040        let b = parse(req)?;
1041        let name = req_str(&b, "ParameterGroupName")?.to_string();
1042        let family = req_str(&b, "Family")?.to_string();
1043        let mut accounts = self.state.write();
1044        let st = accounts.get_or_create(&req.account_id);
1045        if st.parameter_groups.contains_key(&name) {
1046            return Err(fault(
1047                "ParameterGroupAlreadyExistsFault",
1048                &format!("Parameter group {name} already exists."),
1049            ));
1050        }
1051        let pg_arn = arn("parametergroup", &req.region, &req.account_id, &name);
1052        let pg = ParameterGroup {
1053            name: name.clone(),
1054            family,
1055            description: opt_str(&b, "Description").unwrap_or_default(),
1056            arn: pg_arn.clone(),
1057            parameters: BTreeMap::new(),
1058        };
1059        let tags = parse_tags(&b);
1060        if !tags.is_empty() {
1061            st.tags.insert(pg_arn, tags);
1062        }
1063        st.parameter_groups.insert(name, pg.clone());
1064        ok(json!({ "ParameterGroup": pg_json(&pg) }))
1065    }
1066
1067    fn describe_parameter_groups(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1068        let b = parse(req)?;
1069        let accounts = self.state.read();
1070        let st = accounts.get(&req.account_id);
1071        let Some(st) = st else {
1072            return ok(json!({ "ParameterGroups": [] }));
1073        };
1074        if let Some(name) = opt_str(&b, "ParameterGroupName") {
1075            let Some(p) = st.parameter_groups.get(&name) else {
1076                return Err(fault(
1077                    "ParameterGroupNotFoundFault",
1078                    &format!("Parameter group {name} not found."),
1079                ));
1080            };
1081            return ok(json!({ "ParameterGroups": [pg_json(p)] }));
1082        }
1083        let items: Vec<Value> = st.parameter_groups.values().map(pg_json).collect();
1084        let (page, next) = paginate(items, &b, 100);
1085        ok(json!({ "ParameterGroups": page, "NextToken": next }))
1086    }
1087
1088    fn update_parameter_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1089        let b = parse(req)?;
1090        let name = req_str(&b, "ParameterGroupName")?.to_string();
1091        let mut accounts = self.state.write();
1092        let st = accounts.get_or_create(&req.account_id);
1093        let Some(p) = st.parameter_groups.get_mut(&name) else {
1094            return Err(fault(
1095                "ParameterGroupNotFoundFault",
1096                &format!("Parameter group {name} not found."),
1097            ));
1098        };
1099        if let Some(arr) = b.get("ParameterNameValues").and_then(Value::as_array) {
1100            for pv in arr {
1101                if let (Some(pn), Some(pval)) = (
1102                    pv.get("ParameterName").and_then(Value::as_str),
1103                    pv.get("ParameterValue").and_then(Value::as_str),
1104                ) {
1105                    p.parameters.insert(pn.to_string(), pval.to_string());
1106                }
1107            }
1108        }
1109        let out = pg_json(p);
1110        ok(json!({ "ParameterGroup": out }))
1111    }
1112
1113    fn reset_parameter_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1114        let b = parse(req)?;
1115        let name = req_str(&b, "ParameterGroupName")?.to_string();
1116        let mut accounts = self.state.write();
1117        let st = accounts.get_or_create(&req.account_id);
1118        let Some(p) = st.parameter_groups.get_mut(&name) else {
1119            return Err(fault(
1120                "ParameterGroupNotFoundFault",
1121                &format!("Parameter group {name} not found."),
1122            ));
1123        };
1124        let all = b
1125            .get("AllParameters")
1126            .and_then(Value::as_bool)
1127            .unwrap_or(false);
1128        if all {
1129            p.parameters.clear();
1130        } else {
1131            for pv in b
1132                .get("ParameterNames")
1133                .and_then(Value::as_array)
1134                .into_iter()
1135                .flatten()
1136            {
1137                if let Some(pn) = pv.as_str() {
1138                    p.parameters.remove(pn);
1139                }
1140            }
1141        }
1142        let out = pg_json(p);
1143        ok(json!({ "ParameterGroup": out }))
1144    }
1145
1146    fn delete_parameter_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1147        let b = parse(req)?;
1148        let name = req_str(&b, "ParameterGroupName")?.to_string();
1149        let mut accounts = self.state.write();
1150        let st = accounts.get_or_create(&req.account_id);
1151        let Some(p) = st.parameter_groups.remove(&name) else {
1152            return Err(fault(
1153                "ParameterGroupNotFoundFault",
1154                &format!("Parameter group {name} not found."),
1155            ));
1156        };
1157        st.tags.remove(&p.arn);
1158        ok(json!({ "ParameterGroup": pg_json(&p) }))
1159    }
1160
1161    fn describe_parameters(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1162        let b = parse(req)?;
1163        let name = req_str(&b, "ParameterGroupName")?.to_string();
1164        let accounts = self.state.read();
1165        let st = accounts.get(&req.account_id);
1166        let Some(p) = st.and_then(|s| s.parameter_groups.get(&name)) else {
1167            return Err(fault(
1168                "ParameterGroupNotFoundFault",
1169                &format!("Parameter group {name} not found."),
1170            ));
1171        };
1172        let params: Vec<Value> = p
1173            .parameters
1174            .iter()
1175            .map(|(k, v)| {
1176                json!({
1177                    "Name": k, "Value": v, "Description": "", "DataType": "string",
1178                    "AllowedValues": "", "MinimumEngineVersion": "6.2",
1179                })
1180            })
1181            .collect();
1182        ok(json!({ "Parameters": params, "NextToken": Value::Null }))
1183    }
1184
1185    fn create_subnet_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1186        let b = parse(req)?;
1187        let name = req_str(&b, "SubnetGroupName")?.to_string();
1188        let subnet_ids = str_list(&b, "SubnetIds");
1189        let mut accounts = self.state.write();
1190        let st = accounts.get_or_create(&req.account_id);
1191        if st.subnet_groups.contains_key(&name) {
1192            return Err(fault(
1193                "SubnetGroupAlreadyExistsFault",
1194                &format!("Subnet group {name} already exists."),
1195            ));
1196        }
1197        let sg_arn = arn("subnetgroup", &req.region, &req.account_id, &name);
1198        let sg = SubnetGroup {
1199            name: name.clone(),
1200            description: opt_str(&b, "Description").unwrap_or_default(),
1201            vpc_id: "vpc-00000000".to_string(),
1202            subnet_ids,
1203            arn: sg_arn.clone(),
1204            supported_network_types: vec!["ipv4".to_string()],
1205        };
1206        let tags = parse_tags(&b);
1207        if !tags.is_empty() {
1208            st.tags.insert(sg_arn, tags);
1209        }
1210        st.subnet_groups.insert(name, sg.clone());
1211        ok(json!({ "SubnetGroup": sg_json(&sg) }))
1212    }
1213
1214    fn describe_subnet_groups(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1215        let b = parse(req)?;
1216        let accounts = self.state.read();
1217        let st = accounts.get(&req.account_id);
1218        let Some(st) = st else {
1219            return ok(json!({ "SubnetGroups": [] }));
1220        };
1221        if let Some(name) = opt_str(&b, "SubnetGroupName") {
1222            let Some(g) = st.subnet_groups.get(&name) else {
1223                return Err(fault(
1224                    "SubnetGroupNotFoundFault",
1225                    &format!("Subnet group {name} not found."),
1226                ));
1227            };
1228            return ok(json!({ "SubnetGroups": [sg_json(g)] }));
1229        }
1230        let items: Vec<Value> = st.subnet_groups.values().map(sg_json).collect();
1231        let (page, next) = paginate(items, &b, 100);
1232        ok(json!({ "SubnetGroups": page, "NextToken": next }))
1233    }
1234
1235    fn update_subnet_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1236        let b = parse(req)?;
1237        let name = req_str(&b, "SubnetGroupName")?.to_string();
1238        let mut accounts = self.state.write();
1239        let st = accounts.get_or_create(&req.account_id);
1240        let Some(g) = st.subnet_groups.get_mut(&name) else {
1241            return Err(fault(
1242                "SubnetGroupNotFoundFault",
1243                &format!("Subnet group {name} not found."),
1244            ));
1245        };
1246        if let Some(d) = opt_str(&b, "Description") {
1247            g.description = d;
1248        }
1249        let ids = str_list(&b, "SubnetIds");
1250        if !ids.is_empty() {
1251            g.subnet_ids = ids;
1252        }
1253        let out = sg_json(g);
1254        ok(json!({ "SubnetGroup": out }))
1255    }
1256
1257    fn delete_subnet_group(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1258        let b = parse(req)?;
1259        let name = req_str(&b, "SubnetGroupName")?.to_string();
1260        let mut accounts = self.state.write();
1261        let st = accounts.get_or_create(&req.account_id);
1262        let Some(g) = st.subnet_groups.remove(&name) else {
1263            return Err(fault(
1264                "SubnetGroupNotFoundFault",
1265                &format!("Subnet group {name} not found."),
1266            ));
1267        };
1268        st.tags.remove(&g.arn);
1269        ok(json!({ "SubnetGroup": sg_json(&g) }))
1270    }
1271
1272    fn create_snapshot(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1273        let b = parse(req)?;
1274        let cluster_name = req_str(&b, "ClusterName")?.to_string();
1275        let snap_name = req_str(&b, "SnapshotName")?.to_string();
1276        let mut accounts = self.state.write();
1277        let st = accounts.get_or_create(&req.account_id);
1278        if st.snapshots.contains_key(&snap_name) {
1279            return Err(fault(
1280                "SnapshotAlreadyExistsFault",
1281                &format!("Snapshot {snap_name} already exists."),
1282            ));
1283        }
1284        let Some(c) = st.clusters.get(&cluster_name) else {
1285            return Err(fault(
1286                "ClusterNotFoundFault",
1287                &format!("Cluster {cluster_name} not found."),
1288            ));
1289        };
1290        let cfg = json!({
1291            "Name": c.name, "Description": c.description, "NodeType": c.node_type,
1292            "Engine": c.engine, "EngineVersion": c.engine_version, "MaintenanceWindow": c.maintenance_window,
1293            "TopicArn": c.sns_topic_arn, "Port": c.port, "ParameterGroupName": c.parameter_group_name,
1294            "SubnetGroupName": c.subnet_group_name, "VpcId": "vpc-00000000",
1295            "SnapshotRetentionLimit": c.snapshot_retention_limit, "SnapshotWindow": c.snapshot_window,
1296            "NumShards": c.number_of_shards,
1297        });
1298        let snap_arn = arn("snapshot", &req.region, &req.account_id, &snap_name);
1299        let snap = Snapshot {
1300            name: snap_name.clone(),
1301            status: "available".to_string(),
1302            source: "manual".to_string(),
1303            kms_key_id: opt_str(&b, "KmsKeyId"),
1304            arn: snap_arn.clone(),
1305            data_tiering: c.data_tiering.clone(),
1306            cluster_configuration: cfg,
1307        };
1308        let tags = parse_tags(&b);
1309        if !tags.is_empty() {
1310            st.tags.insert(snap_arn, tags);
1311        }
1312        st.snapshots.insert(snap_name, snap.clone());
1313        ok(json!({ "Snapshot": snapshot_json(&snap) }))
1314    }
1315
1316    fn copy_snapshot(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1317        let b = parse(req)?;
1318        let source = req_str(&b, "SourceSnapshotName")?.to_string();
1319        let target = req_str(&b, "TargetSnapshotName")?.to_string();
1320        let mut accounts = self.state.write();
1321        let st = accounts.get_or_create(&req.account_id);
1322        let Some(src) = st.snapshots.get(&source).cloned() else {
1323            return Err(fault(
1324                "SnapshotNotFoundFault",
1325                &format!("Snapshot {source} not found."),
1326            ));
1327        };
1328        if st.snapshots.contains_key(&target) {
1329            return Err(fault(
1330                "SnapshotAlreadyExistsFault",
1331                &format!("Snapshot {target} already exists."),
1332            ));
1333        }
1334        let snap_arn = arn("snapshot", &req.region, &req.account_id, &target);
1335        let snap = Snapshot {
1336            name: target.clone(),
1337            status: "available".to_string(),
1338            source: "manual".to_string(),
1339            kms_key_id: opt_str(&b, "KmsKeyId").or(src.kms_key_id),
1340            arn: snap_arn,
1341            data_tiering: src.data_tiering,
1342            cluster_configuration: src.cluster_configuration,
1343        };
1344        st.snapshots.insert(target, snap.clone());
1345        ok(json!({ "Snapshot": snapshot_json(&snap) }))
1346    }
1347
1348    fn describe_snapshots(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1349        let b = parse(req)?;
1350        let accounts = self.state.read();
1351        let st = accounts.get(&req.account_id);
1352        let Some(st) = st else {
1353            return ok(json!({ "Snapshots": [] }));
1354        };
1355        if let Some(name) = opt_str(&b, "SnapshotName") {
1356            let Some(s) = st.snapshots.get(&name) else {
1357                return Err(fault(
1358                    "SnapshotNotFoundFault",
1359                    &format!("Snapshot {name} not found."),
1360                ));
1361            };
1362            return ok(json!({ "Snapshots": [snapshot_json(s)] }));
1363        }
1364        let cluster_filter = opt_str(&b, "ClusterName");
1365        let items: Vec<Value> = st
1366            .snapshots
1367            .values()
1368            .filter(|s| {
1369                cluster_filter.as_ref().is_none_or(|cn| {
1370                    s.cluster_configuration.get("Name").and_then(Value::as_str) == Some(cn)
1371                })
1372            })
1373            .map(snapshot_json)
1374            .collect();
1375        let (page, next) = paginate(items, &b, 50);
1376        ok(json!({ "Snapshots": page, "NextToken": next }))
1377    }
1378
1379    fn delete_snapshot(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1380        let b = parse(req)?;
1381        let name = req_str(&b, "SnapshotName")?.to_string();
1382        let mut accounts = self.state.write();
1383        let st = accounts.get_or_create(&req.account_id);
1384        let Some(mut s) = st.snapshots.remove(&name) else {
1385            return Err(fault(
1386                "SnapshotNotFoundFault",
1387                &format!("Snapshot {name} not found."),
1388            ));
1389        };
1390        s.status = "deleting".to_string();
1391        st.tags.remove(&s.arn);
1392        ok(json!({ "Snapshot": snapshot_json(&s) }))
1393    }
1394
1395    fn create_multi_region_cluster(
1396        &self,
1397        req: &AwsRequest,
1398    ) -> Result<AwsResponse, AwsServiceError> {
1399        let b = parse(req)?;
1400        let suffix = req_str(&b, "MultiRegionClusterNameSuffix")?.to_string();
1401        let node_type = req_str(&b, "NodeType")?.to_string();
1402        let name = format!("virxk-{suffix}");
1403        let mut accounts = self.state.write();
1404        let st = accounts.get_or_create(&req.account_id);
1405        if st.multi_region_clusters.contains_key(&name) {
1406            return Err(fault(
1407                "MultiRegionClusterAlreadyExistsFault",
1408                &format!("Multi-region cluster {name} already exists."),
1409            ));
1410        }
1411        let mrc_arn = arn("multiregioncluster", &req.region, &req.account_id, &name);
1412        let mrc = MultiRegionCluster {
1413            name: name.clone(),
1414            description: opt_str(&b, "Description"),
1415            status: "creating".to_string(),
1416            node_type,
1417            engine: opt_str(&b, "Engine").unwrap_or_else(|| DEFAULT_ENGINE.to_string()),
1418            engine_version: opt_str(&b, "EngineVersion")
1419                .unwrap_or_else(|| DEFAULT_ENGINE_VERSION.to_string()),
1420            number_of_shards: int_field(&b, "NumShards", 1, 1, 500)?,
1421            multi_region_parameter_group_name: opt_str(&b, "MultiRegionParameterGroupName"),
1422            tls_enabled: b.get("TLSEnabled").and_then(Value::as_bool).unwrap_or(true),
1423            arn: mrc_arn.clone(),
1424            clusters: json!([]),
1425        };
1426        let tags = parse_tags(&b);
1427        if !tags.is_empty() {
1428            st.tags.insert(mrc_arn, tags);
1429        }
1430        st.multi_region_clusters.insert(name, mrc.clone());
1431        ok(json!({ "MultiRegionCluster": mrc_json(&mrc) }))
1432    }
1433
1434    fn describe_multi_region_clusters(
1435        &self,
1436        req: &AwsRequest,
1437    ) -> Result<AwsResponse, AwsServiceError> {
1438        let b = parse(req)?;
1439        let mut accounts = self.state.write();
1440        let st = accounts.get_or_create(&req.account_id);
1441        for m in st.multi_region_clusters.values_mut() {
1442            if m.status == "creating" {
1443                m.status = "available".to_string();
1444            }
1445        }
1446        if let Some(name) = opt_str(&b, "MultiRegionClusterName") {
1447            let Some(m) = st.multi_region_clusters.get(&name) else {
1448                return Err(fault(
1449                    "MultiRegionClusterNotFoundFault",
1450                    &format!("Multi-region cluster {name} not found."),
1451                ));
1452            };
1453            return ok(json!({ "MultiRegionClusters": [mrc_json(m)] }));
1454        }
1455        let items: Vec<Value> = st.multi_region_clusters.values().map(mrc_json).collect();
1456        let (page, next) = paginate(items, &b, 100);
1457        ok(json!({ "MultiRegionClusters": page, "NextToken": next }))
1458    }
1459
1460    fn update_multi_region_cluster(
1461        &self,
1462        req: &AwsRequest,
1463    ) -> Result<AwsResponse, AwsServiceError> {
1464        let b = parse(req)?;
1465        let name = req_str(&b, "MultiRegionClusterName")?.to_string();
1466        let mut accounts = self.state.write();
1467        let st = accounts.get_or_create(&req.account_id);
1468        let Some(m) = st.multi_region_clusters.get_mut(&name) else {
1469            return Err(fault(
1470                "MultiRegionClusterNotFoundFault",
1471                &format!("Multi-region cluster {name} not found."),
1472            ));
1473        };
1474        if let Some(d) = opt_str(&b, "Description") {
1475            m.description = Some(d);
1476        }
1477        if let Some(v) = opt_str(&b, "EngineVersion") {
1478            m.engine_version = v;
1479        }
1480        if let Some(v) = opt_str(&b, "NodeType") {
1481            m.node_type = v;
1482        }
1483        let out = mrc_json(m);
1484        ok(json!({ "MultiRegionCluster": out }))
1485    }
1486
1487    fn delete_multi_region_cluster(
1488        &self,
1489        req: &AwsRequest,
1490    ) -> Result<AwsResponse, AwsServiceError> {
1491        let b = parse(req)?;
1492        let name = req_str(&b, "MultiRegionClusterName")?.to_string();
1493        let mut accounts = self.state.write();
1494        let st = accounts.get_or_create(&req.account_id);
1495        let Some(mut m) = st.multi_region_clusters.remove(&name) else {
1496            return Err(fault(
1497                "MultiRegionClusterNotFoundFault",
1498                &format!("Multi-region cluster {name} not found."),
1499            ));
1500        };
1501        m.status = "deleting".to_string();
1502        st.tags.remove(&m.arn);
1503        ok(json!({ "MultiRegionCluster": mrc_json(&m) }))
1504    }
1505
1506    fn list_allowed_mr_updates(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1507        let b = parse(req)?;
1508        let name = req_str(&b, "MultiRegionClusterName")?.to_string();
1509        let accounts = self.state.read();
1510        if accounts
1511            .get(&req.account_id)
1512            .and_then(|s| s.multi_region_clusters.get(&name))
1513            .is_none()
1514        {
1515            return Err(fault(
1516                "MultiRegionClusterNotFoundFault",
1517                &format!("Multi-region cluster {name} not found."),
1518            ));
1519        }
1520        ok(
1521            json!({ "ScaleUpNodeTypes": ["db.r7g.large", "db.r7g.xlarge"], "ScaleDownNodeTypes": [] }),
1522        )
1523    }
1524
1525    fn describe_mr_parameter_groups(
1526        &self,
1527        req: &AwsRequest,
1528    ) -> Result<AwsResponse, AwsServiceError> {
1529        let _ = parse(req)?;
1530        ok(json!({ "MultiRegionParameterGroups": [], "NextToken": Value::Null }))
1531    }
1532
1533    fn describe_mr_parameters(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1534        let b = parse(req)?;
1535        req_str(&b, "MultiRegionParameterGroupName")?;
1536        ok(json!({ "MultiRegionParameters": [], "NextToken": Value::Null }))
1537    }
1538
1539    fn tag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1540        let b = parse(req)?;
1541        let resource_arn = req_str(&b, "ResourceArn")?.to_string();
1542        let mut accounts = self.state.write();
1543        let st = accounts.get_or_create(&req.account_id);
1544        if !arn_exists(st, &resource_arn) {
1545            return Err(fault(
1546                "InvalidARNFault",
1547                &format!("{resource_arn} does not refer to a known resource."),
1548            ));
1549        }
1550        let entry = st.tags.entry(resource_arn).or_default();
1551        for (k, v) in parse_tags(&b) {
1552            entry.insert(k, v);
1553        }
1554        let all = entry.clone();
1555        ok(json!({ "TagList": tags_json(&all) }))
1556    }
1557
1558    fn untag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1559        let b = parse(req)?;
1560        let resource_arn = req_str(&b, "ResourceArn")?.to_string();
1561        let mut accounts = self.state.write();
1562        let st = accounts.get_or_create(&req.account_id);
1563        if !arn_exists(st, &resource_arn) {
1564            return Err(fault(
1565                "InvalidARNFault",
1566                &format!("{resource_arn} does not refer to a known resource."),
1567            ));
1568        }
1569        let keys = str_list(&b, "TagKeys");
1570        let entry = st.tags.entry(resource_arn).or_default();
1571        for k in &keys {
1572            entry.remove(k);
1573        }
1574        let all = entry.clone();
1575        ok(json!({ "TagList": tags_json(&all) }))
1576    }
1577
1578    fn list_tags(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1579        let b = parse(req)?;
1580        let resource_arn = req_str(&b, "ResourceArn")?.to_string();
1581        let accounts = self.state.read();
1582        let st = accounts.get(&req.account_id);
1583        let Some(st) = st else {
1584            return ok(json!({ "TagList": [] }));
1585        };
1586        if !arn_exists(st, &resource_arn) {
1587            return Err(fault(
1588                "InvalidARNFault",
1589                &format!("{resource_arn} does not refer to a known resource."),
1590            ));
1591        }
1592        let tags = st.tags.get(&resource_arn).cloned().unwrap_or_default();
1593        ok(json!({ "TagList": tags_json(&tags) }))
1594    }
1595
1596    fn describe_engine_versions(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1597        let _ = parse(req)?;
1598        let versions = json!([
1599            { "Engine": "redis", "EngineVersion": "7.1", "EnginePatchVersion": "7.1.1", "ParameterGroupFamily": "memorydb_redis7" },
1600            { "Engine": "redis", "EngineVersion": "6.2", "EnginePatchVersion": "6.2.6", "ParameterGroupFamily": "memorydb_redis6" },
1601            { "Engine": "valkey", "EngineVersion": "7.2", "EnginePatchVersion": "7.2.0", "ParameterGroupFamily": "memorydb_valkey7" },
1602        ]);
1603        ok(json!({ "EngineVersions": versions, "NextToken": Value::Null }))
1604    }
1605
1606    fn describe_events(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1607        let b = parse(req)?;
1608        if let Some(st) = opt_str(&b, "SourceType") {
1609            const VALID: &[&str] = &[
1610                "node",
1611                "parameter_group",
1612                "subnet_group",
1613                "cluster",
1614                "user",
1615                "acl",
1616            ];
1617            if !VALID.contains(&st.as_str()) {
1618                return Err(fault(
1619                    "InvalidParameterValueException",
1620                    &format!("Invalid SourceType: {st}"),
1621                ));
1622            }
1623        }
1624        ok(json!({ "Events": [], "NextToken": Value::Null }))
1625    }
1626
1627    fn describe_service_updates(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1628        let _ = parse(req)?;
1629        ok(json!({ "ServiceUpdates": [], "NextToken": Value::Null }))
1630    }
1631
1632    fn describe_reserved_nodes(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1633        let b = parse(req)?;
1634        let accounts = self.state.read();
1635        let st = accounts.get(&req.account_id);
1636        let Some(st) = st else {
1637            return ok(json!({ "ReservedNodes": [] }));
1638        };
1639        let items: Vec<Value> = st.reserved_nodes.values().map(reserved_node_json).collect();
1640        let (page, next) = paginate(items, &b, 100);
1641        ok(json!({ "ReservedNodes": page, "NextToken": next }))
1642    }
1643
1644    fn describe_reserved_nodes_offerings(
1645        &self,
1646        req: &AwsRequest,
1647    ) -> Result<AwsResponse, AwsServiceError> {
1648        let _ = parse(req)?;
1649        let offerings = json!([
1650            {
1651                "ReservedNodesOfferingId": "00000000-1111-2222-3333-444444444444",
1652                "NodeType": "db.r6g.large", "Duration": 31536000, "FixedPrice": 1200.0,
1653                "OfferingType": "All Upfront", "RecurringCharges": [],
1654            }
1655        ]);
1656        ok(json!({ "ReservedNodesOfferings": offerings, "NextToken": Value::Null }))
1657    }
1658
1659    fn purchase_reserved_nodes_offering(
1660        &self,
1661        req: &AwsRequest,
1662    ) -> Result<AwsResponse, AwsServiceError> {
1663        let b = parse(req)?;
1664        let offering_id = req_str(&b, "ReservedNodesOfferingId")?.to_string();
1665        let reservation_id = opt_str(&b, "ReservationId")
1666            .unwrap_or_else(|| format!("ri-{}", uuid::Uuid::new_v4().simple()));
1667        let count = int_field(&b, "NodeCount", 1, 1, i32::MAX as i64)?;
1668        let mut accounts = self.state.write();
1669        let st = accounts.get_or_create(&req.account_id);
1670        let node_arn = arn(
1671            "reservednode",
1672            &req.region,
1673            &req.account_id,
1674            &reservation_id,
1675        );
1676        let rn = ReservedNode {
1677            reservation_id: reservation_id.clone(),
1678            reserved_nodes_offering_id: offering_id,
1679            node_type: "db.r6g.large".to_string(),
1680            start_time: Utc::now(),
1681            duration: 31536000,
1682            fixed_price: 1200.0,
1683            node_count: count,
1684            offering_type: "All Upfront".to_string(),
1685            state: "active".to_string(),
1686            arn: node_arn,
1687        };
1688        st.reserved_nodes.insert(reservation_id, rn.clone());
1689        ok(json!({ "ReservedNode": reserved_node_json(&rn) }))
1690    }
1691
1692    fn list_allowed_node_type_updates(
1693        &self,
1694        req: &AwsRequest,
1695    ) -> Result<AwsResponse, AwsServiceError> {
1696        let b = parse(req)?;
1697        let name = req_str(&b, "ClusterName")?.to_string();
1698        let accounts = self.state.read();
1699        if accounts
1700            .get(&req.account_id)
1701            .and_then(|s| s.clusters.get(&name))
1702            .is_none()
1703        {
1704            return Err(fault(
1705                "ClusterNotFoundFault",
1706                &format!("Cluster {name} not found."),
1707            ));
1708        }
1709        ok(json!({
1710            "ScaleUpNodeTypes": ["db.r6g.xlarge", "db.r6g.2xlarge"],
1711            "ScaleDownNodeTypes": ["db.r6g.large"],
1712        }))
1713    }
1714}
1715
1716/// Validate a `UserName` against the model constraints: length >= 1 and
1717/// pattern `^[a-zA-Z][a-zA-Z0-9-]*$`.
1718fn validate_user_name(name: &str) -> Result<(), AwsServiceError> {
1719    let valid = !name.is_empty()
1720        && name.starts_with(|c: char| c.is_ascii_alphabetic())
1721        && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-');
1722    if valid {
1723        Ok(())
1724    } else {
1725        Err(fault(
1726            "InvalidParameterValueException",
1727            &format!("Invalid UserName: {name}"),
1728        ))
1729    }
1730}
1731
1732/// Whether a given ARN refers to any resource in this account's state.
1733fn arn_exists(st: &MemoryDbState, resource_arn: &str) -> bool {
1734    // An empty ARN never refers to a resource (guards against matching a
1735    // resource that happens to carry an empty ARN string).
1736    if resource_arn.is_empty() {
1737        return false;
1738    }
1739    st.clusters.values().any(|c| c.arn == resource_arn)
1740        || st.acls.values().any(|a| a.arn == resource_arn)
1741        || st.users.values().any(|u| u.arn == resource_arn)
1742        || st.parameter_groups.values().any(|p| p.arn == resource_arn)
1743        || st.subnet_groups.values().any(|g| g.arn == resource_arn)
1744        || st.snapshots.values().any(|s| s.arn == resource_arn)
1745        || st
1746            .multi_region_clusters
1747            .values()
1748            .any(|m| m.arn == resource_arn)
1749        || st.reserved_nodes.values().any(|r| r.arn == resource_arn)
1750}
1751
1752#[cfg(test)]
1753mod tests {
1754    use super::*;
1755    use fakecloud_core::multi_account::MultiAccountState;
1756
1757    fn service() -> MemoryDbService {
1758        let state = Arc::new(parking_lot::RwLock::new(MultiAccountState::new(
1759            "123456789012",
1760            "us-east-1",
1761            "http://localhost:4566",
1762        )));
1763        MemoryDbService::new(state)
1764    }
1765
1766    fn req(action: &str, body: Value) -> AwsRequest {
1767        AwsRequest {
1768            service: "memorydb".to_string(),
1769            action: action.to_string(),
1770            region: "us-east-1".to_string(),
1771            account_id: "123456789012".to_string(),
1772            request_id: "rid".to_string(),
1773            headers: http::HeaderMap::new(),
1774            query_params: std::collections::HashMap::new(),
1775            body: bytes::Bytes::from(serde_json::to_vec(&body).unwrap()),
1776            body_stream: parking_lot::Mutex::new(None),
1777            path_segments: vec![],
1778            raw_path: "/".to_string(),
1779            raw_query: String::new(),
1780            method: http::Method::POST,
1781            is_query_protocol: false,
1782            access_key_id: None,
1783            principal: None,
1784        }
1785    }
1786
1787    fn call(s: &MemoryDbService, action: &str, body: Value) -> Result<Value, AwsServiceError> {
1788        let resp = dispatch(s, &req(action, body))?;
1789        Ok(serde_json::from_slice(resp.body.expect_bytes()).unwrap())
1790    }
1791
1792    #[test]
1793    fn create_cluster_round_trips_and_describes() {
1794        let s = service();
1795        let out = call(
1796            &s,
1797            "CreateCluster",
1798            json!({"ClusterName": "app", "NodeType": "db.r6g.large", "ACLName": "open-access"}),
1799        )
1800        .unwrap();
1801        assert_eq!(out["Cluster"]["Name"], "app");
1802
1803        let desc = call(&s, "DescribeClusters", json!({"ClusterName": "app"})).unwrap();
1804        let clusters = desc["Clusters"].as_array().unwrap();
1805        assert_eq!(clusters.len(), 1);
1806        // Lazily transitions creating -> available on describe.
1807        assert_eq!(clusters[0]["Status"], "available");
1808    }
1809
1810    #[test]
1811    fn create_cluster_rejects_out_of_range_shard_count() {
1812        let s = service();
1813        // A huge NumShards must be rejected, not allocated (OOM guard).
1814        let err = call(
1815            &s,
1816            "CreateCluster",
1817            json!({"ClusterName": "big", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumShards": 2_000_000_000i64}),
1818        )
1819        .unwrap_err();
1820        assert!(
1821            err.message().contains("NumShards"),
1822            "got: {}",
1823            err.message()
1824        );
1825    }
1826
1827    #[test]
1828    fn create_cluster_rejects_out_of_range_replica_count() {
1829        let s = service();
1830        let err = call(
1831            &s,
1832            "CreateCluster",
1833            json!({"ClusterName": "r", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumReplicasPerShard": 99}),
1834        )
1835        .unwrap_err();
1836        assert!(
1837            err.message().contains("NumReplicasPerShard"),
1838            "got: {}",
1839            err.message()
1840        );
1841    }
1842
1843    #[test]
1844    fn create_user_rejects_invalid_name() {
1845        let s = service();
1846        // Must start with a letter.
1847        let err = call(
1848            &s,
1849            "CreateUser",
1850            json!({"UserName": "1bad", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "no-password-required"}}),
1851        )
1852        .unwrap_err();
1853        assert!(err.message().contains("UserName"), "got: {}", err.message());
1854    }
1855
1856    #[test]
1857    fn default_acl_and_user_are_seeded() {
1858        let s = service();
1859        let acls = call(&s, "DescribeACLs", json!({})).unwrap();
1860        assert!(acls["ACLs"]
1861            .as_array()
1862            .unwrap()
1863            .iter()
1864            .any(|a| a["Name"] == "open-access"));
1865        let users = call(&s, "DescribeUsers", json!({})).unwrap();
1866        assert!(users["Users"]
1867            .as_array()
1868            .unwrap()
1869            .iter()
1870            .any(|u| u["Name"] == "default"));
1871    }
1872
1873    #[test]
1874    fn seeded_default_acl_and_user_have_populated_arns() {
1875        let s = service();
1876        let acls = call(&s, "DescribeACLs", json!({"ACLName": "open-access"})).unwrap();
1877        assert_eq!(
1878            acls["ACLs"][0]["ARN"],
1879            "arn:aws:memorydb:us-east-1:123456789012:acl/open-access"
1880        );
1881        let users = call(&s, "DescribeUsers", json!({"UserName": "default"})).unwrap();
1882        assert_eq!(
1883            users["Users"][0]["ARN"],
1884            "arn:aws:memorydb:us-east-1:123456789012:user/default"
1885        );
1886    }
1887
1888    #[test]
1889    fn not_found_faults_return_http_404() {
1890        let s = service();
1891        let err = call(&s, "DescribeClusters", json!({"ClusterName": "nope"})).unwrap_err();
1892        assert_eq!(err.status(), http::StatusCode::NOT_FOUND);
1893        // A validation error stays a 400.
1894        let bad = call(
1895            &s,
1896            "CreateCluster",
1897            json!({"ClusterName": "c", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumShards": 9999}),
1898        )
1899        .unwrap_err();
1900        assert_eq!(bad.status(), http::StatusCode::BAD_REQUEST);
1901    }
1902
1903    #[test]
1904    fn create_cluster_populates_acl_clusters_backref() {
1905        let s = service();
1906        call(
1907            &s,
1908            "CreateCluster",
1909            json!({"ClusterName": "c1", "NodeType": "db.r6g.large", "ACLName": "open-access"}),
1910        )
1911        .unwrap();
1912        let acls = call(&s, "DescribeACLs", json!({"ACLName": "open-access"})).unwrap();
1913        let clusters = acls["ACLs"][0]["Clusters"].as_array().unwrap();
1914        assert!(clusters.iter().any(|c| c == "c1"), "got: {clusters:?}");
1915
1916        // Deleting the cluster removes the back-reference.
1917        call(&s, "DeleteCluster", json!({"ClusterName": "c1"})).unwrap();
1918        let acls = call(&s, "DescribeACLs", json!({"ACLName": "open-access"})).unwrap();
1919        assert!(acls["ACLs"][0]["Clusters"].as_array().unwrap().is_empty());
1920    }
1921
1922    #[test]
1923    fn acl_membership_syncs_user_acl_names_backref() {
1924        let s = service();
1925        call(
1926            &s,
1927            "CreateUser",
1928            json!({"UserName": "bob", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "iam"}}),
1929        )
1930        .unwrap();
1931        call(
1932            &s,
1933            "CreateACL",
1934            json!({"ACLName": "team", "UserNames": ["bob"]}),
1935        )
1936        .unwrap();
1937        let users = call(&s, "DescribeUsers", json!({"UserName": "bob"})).unwrap();
1938        let acl_names = users["Users"][0]["ACLNames"].as_array().unwrap();
1939        assert!(acl_names.iter().any(|a| a == "team"), "got: {acl_names:?}");
1940
1941        // Removing the user from the ACL drops the back-reference.
1942        call(
1943            &s,
1944            "UpdateACL",
1945            json!({"ACLName": "team", "UserNamesToRemove": ["bob"]}),
1946        )
1947        .unwrap();
1948        let users = call(&s, "DescribeUsers", json!({"UserName": "bob"})).unwrap();
1949        assert!(users["Users"][0]["ACLNames"]
1950            .as_array()
1951            .unwrap()
1952            .iter()
1953            .all(|a| a != "team"));
1954    }
1955
1956    #[test]
1957    fn tag_resource_rejects_empty_arn() {
1958        let s = service();
1959        let err = call(
1960            &s,
1961            "TagResource",
1962            json!({"ResourceArn": "", "Tags": [{"Key": "k", "Value": "v"}]}),
1963        )
1964        .unwrap_err();
1965        assert_eq!(err.status(), http::StatusCode::BAD_REQUEST);
1966        assert!(
1967            err.message().contains("known resource") || err.message().contains("ARN"),
1968            "got: {}",
1969            err.message()
1970        );
1971    }
1972
1973    #[test]
1974    fn create_user_requires_valid_authentication_mode() {
1975        let s = service();
1976        // Missing AuthenticationMode -> rejected.
1977        let e1 = call(
1978            &s,
1979            "CreateUser",
1980            json!({"UserName": "u1", "AccessString": "on ~* &* +@all"}),
1981        )
1982        .unwrap_err();
1983        assert!(
1984            e1.message().contains("AuthenticationMode"),
1985            "got: {}",
1986            e1.message()
1987        );
1988        // password type with zero passwords -> rejected.
1989        let e2 = call(
1990            &s,
1991            "CreateUser",
1992            json!({"UserName": "u2", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "password"}}),
1993        )
1994        .unwrap_err();
1995        assert!(e2.message().contains("password"), "got: {}", e2.message());
1996        // invalid Type -> rejected.
1997        let e3 = call(
1998            &s,
1999            "CreateUser",
2000            json!({"UserName": "u3", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "kerberos"}}),
2001        )
2002        .unwrap_err();
2003        assert!(
2004            e3.message().contains("kerberos") || e3.message().contains("Type"),
2005            "got: {}",
2006            e3.message()
2007        );
2008        // iam type -> accepted.
2009        call(
2010            &s,
2011            "CreateUser",
2012            json!({"UserName": "u4", "AccessString": "on ~* &* +@all", "AuthenticationMode": {"Type": "iam"}}),
2013        )
2014        .unwrap();
2015    }
2016
2017    #[test]
2018    fn failover_shard_validates_shard_name() {
2019        let s = service();
2020        call(
2021            &s,
2022            "CreateCluster",
2023            json!({"ClusterName": "fc", "NodeType": "db.r6g.large", "ACLName": "open-access", "NumShards": 1}),
2024        )
2025        .unwrap();
2026        // Unknown shard -> ShardNotFoundFault (404).
2027        let err = call(
2028            &s,
2029            "FailoverShard",
2030            json!({"ClusterName": "fc", "ShardName": "9999"}),
2031        )
2032        .unwrap_err();
2033        assert_eq!(err.status(), http::StatusCode::NOT_FOUND);
2034        // Real shard "0001" -> ok.
2035        call(
2036            &s,
2037            "FailoverShard",
2038            json!({"ClusterName": "fc", "ShardName": "0001"}),
2039        )
2040        .unwrap();
2041    }
2042
2043    #[test]
2044    fn batch_update_reports_unknown_clusters_as_unprocessed() {
2045        let s = service();
2046        call(
2047            &s,
2048            "CreateCluster",
2049            json!({"ClusterName": "known", "NodeType": "db.r6g.large", "ACLName": "open-access"}),
2050        )
2051        .unwrap();
2052        let out = call(
2053            &s,
2054            "BatchUpdateCluster",
2055            json!({"ClusterNames": ["known", "ghost"]}),
2056        )
2057        .unwrap();
2058        assert_eq!(out["ProcessedClusters"].as_array().unwrap().len(), 1);
2059        let un = out["UnprocessedClusters"].as_array().unwrap();
2060        assert_eq!(un.len(), 1);
2061        assert_eq!(un[0]["ClusterName"], "ghost");
2062    }
2063
2064    #[test]
2065    fn snapshot_retention_limit_out_of_range_rejected() {
2066        let s = service();
2067        let err = call(
2068            &s,
2069            "CreateCluster",
2070            json!({"ClusterName": "srl", "NodeType": "db.r6g.large", "ACLName": "open-access", "SnapshotRetentionLimit": 4294967297i64}),
2071        )
2072        .unwrap_err();
2073        assert_eq!(err.status(), http::StatusCode::BAD_REQUEST);
2074        assert!(
2075            err.message().contains("SnapshotRetentionLimit"),
2076            "got: {}",
2077            err.message()
2078        );
2079    }
2080}