Skip to main content

fakecloud_ssm/service/
mod.rs

1mod associations;
2mod automation;
3mod commands;
4mod compliance;
5mod documents;
6mod instances;
7mod inventory;
8mod maintenance;
9mod misc;
10mod ops;
11mod parameters;
12mod patches;
13mod resource_sync;
14mod sessions;
15mod tags;
16
17use async_trait::async_trait;
18use http::StatusCode;
19
20use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
21
22use crate::state::SharedSsmState;
23
24use fakecloud_secretsmanager::state::SharedSecretsManagerState;
25
26const PARAMETER_VERSION_LIMIT: i64 = 100;
27
28pub struct SsmService {
29    state: SharedSsmState,
30    secretsmanager_state: Option<SharedSecretsManagerState>,
31}
32
33impl SsmService {
34    pub fn new(state: SharedSsmState) -> Self {
35        Self {
36            state,
37            secretsmanager_state: None,
38        }
39    }
40
41    pub fn with_secretsmanager(mut self, sm_state: SharedSecretsManagerState) -> Self {
42        self.secretsmanager_state = Some(sm_state);
43        self
44    }
45}
46
47#[async_trait]
48impl AwsService for SsmService {
49    fn service_name(&self) -> &str {
50        "ssm"
51    }
52
53    async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
54        match req.action.as_str() {
55            "PutParameter" => self.put_parameter(&req),
56            "GetParameter" => self.get_parameter(&req),
57            "GetParameters" => self.get_parameters(&req),
58            "GetParametersByPath" => self.get_parameters_by_path(&req),
59            "DeleteParameter" => self.delete_parameter(&req),
60            "DeleteParameters" => self.delete_parameters(&req),
61            "DescribeParameters" => self.describe_parameters(&req),
62            "GetParameterHistory" => self.get_parameter_history(&req),
63            "AddTagsToResource" => self.add_tags_to_resource(&req),
64            "RemoveTagsFromResource" => self.remove_tags_from_resource(&req),
65            "ListTagsForResource" => self.list_tags_for_resource(&req),
66            "LabelParameterVersion" => self.label_parameter_version(&req),
67            "UnlabelParameterVersion" => self.unlabel_parameter_version(&req),
68            "CreateDocument" => self.create_document(&req),
69            "GetDocument" => self.get_document(&req),
70            "DeleteDocument" => self.delete_document(&req),
71            "UpdateDocument" => self.update_document(&req),
72            "DescribeDocument" => self.describe_document(&req),
73            "UpdateDocumentDefaultVersion" => self.update_document_default_version(&req),
74            "ListDocuments" => self.list_documents(&req),
75            "DescribeDocumentPermission" => self.describe_document_permission(&req),
76            "ModifyDocumentPermission" => self.modify_document_permission(&req),
77            "SendCommand" => self.send_command(&req),
78            "ListCommands" => self.list_commands(&req),
79            "GetCommandInvocation" => self.get_command_invocation(&req),
80            "ListCommandInvocations" => self.list_command_invocations(&req),
81            "CancelCommand" => self.cancel_command(&req),
82            "CreateMaintenanceWindow" => self.create_maintenance_window(&req),
83            "DescribeMaintenanceWindows" => self.describe_maintenance_windows(&req),
84            "GetMaintenanceWindow" => self.get_maintenance_window(&req),
85            "DeleteMaintenanceWindow" => self.delete_maintenance_window(&req),
86            "UpdateMaintenanceWindow" => self.update_maintenance_window(&req),
87            "RegisterTargetWithMaintenanceWindow" => {
88                self.register_target_with_maintenance_window(&req)
89            }
90            "DeregisterTargetFromMaintenanceWindow" => {
91                self.deregister_target_from_maintenance_window(&req)
92            }
93            "DescribeMaintenanceWindowTargets" => self.describe_maintenance_window_targets(&req),
94            "RegisterTaskWithMaintenanceWindow" => self.register_task_with_maintenance_window(&req),
95            "DeregisterTaskFromMaintenanceWindow" => {
96                self.deregister_task_from_maintenance_window(&req)
97            }
98            "DescribeMaintenanceWindowTasks" => self.describe_maintenance_window_tasks(&req),
99            "CreatePatchBaseline" => self.create_patch_baseline(&req),
100            "DeletePatchBaseline" => self.delete_patch_baseline(&req),
101            "DescribePatchBaselines" => self.describe_patch_baselines(&req),
102            "GetPatchBaseline" => self.get_patch_baseline(&req),
103            "RegisterPatchBaselineForPatchGroup" => {
104                self.register_patch_baseline_for_patch_group(&req)
105            }
106            "DeregisterPatchBaselineForPatchGroup" => {
107                self.deregister_patch_baseline_for_patch_group(&req)
108            }
109            "GetPatchBaselineForPatchGroup" => self.get_patch_baseline_for_patch_group(&req),
110            "DescribePatchGroups" => self.describe_patch_groups(&req),
111            // Associations
112            "CreateAssociation" => self.create_association(&req),
113            "DescribeAssociation" => self.describe_association(&req),
114            "DeleteAssociation" => self.delete_association(&req),
115            "ListAssociations" => self.list_associations(&req),
116            "UpdateAssociation" => self.update_association(&req),
117            "ListAssociationVersions" => self.list_association_versions(&req),
118            "UpdateAssociationStatus" => self.update_association_status(&req),
119            "StartAssociationsOnce" => self.start_associations_once(&req),
120            "CreateAssociationBatch" => self.create_association_batch(&req),
121            "DescribeAssociationExecutions" => self.describe_association_executions(&req),
122            "DescribeAssociationExecutionTargets" => {
123                self.describe_association_execution_targets(&req)
124            }
125            // OpsItems
126            "CreateOpsItem" => self.create_ops_item(&req),
127            "GetOpsItem" => self.get_ops_item(&req),
128            "UpdateOpsItem" => self.update_ops_item(&req),
129            "DeleteOpsItem" => self.delete_ops_item(&req),
130            "DescribeOpsItems" => self.describe_ops_items(&req),
131            // Document extras
132            "ListDocumentVersions" => self.list_document_versions(&req),
133            "ListDocumentMetadataHistory" => self.list_document_metadata_history(&req),
134            "UpdateDocumentMetadata" => self.update_document_metadata(&req),
135            // Resource policies
136            "PutResourcePolicy" => self.put_resource_policy(&req),
137            "GetResourcePolicies" => self.get_resource_policies(&req),
138            "DeleteResourcePolicy" => self.delete_resource_policy(&req),
139            // Inventory
140            "PutInventory" => self.put_inventory(&req),
141            "GetInventory" => self.get_inventory(&req),
142            "GetInventorySchema" => self.get_inventory_schema(&req),
143            "ListInventoryEntries" => self.list_inventory_entries(&req),
144            "DeleteInventory" => self.delete_inventory(&req),
145            "DescribeInventoryDeletions" => self.describe_inventory_deletions(&req),
146            // Compliance
147            "PutComplianceItems" => self.put_compliance_items(&req),
148            "ListComplianceItems" => self.list_compliance_items(&req),
149            "ListComplianceSummaries" => self.list_compliance_summaries(&req),
150            "ListResourceComplianceSummaries" => self.list_resource_compliance_summaries(&req),
151            // Maintenance window details
152            "UpdateMaintenanceWindowTarget" => self.update_maintenance_window_target(&req),
153            "UpdateMaintenanceWindowTask" => self.update_maintenance_window_task(&req),
154            "GetMaintenanceWindowTask" => self.get_maintenance_window_task(&req),
155            "GetMaintenanceWindowExecution" => self.get_maintenance_window_execution(&req),
156            "GetMaintenanceWindowExecutionTask" => self.get_maintenance_window_execution_task(&req),
157            "GetMaintenanceWindowExecutionTaskInvocation" => {
158                self.get_maintenance_window_execution_task_invocation(&req)
159            }
160            "DescribeMaintenanceWindowExecutions" => {
161                self.describe_maintenance_window_executions(&req)
162            }
163            "DescribeMaintenanceWindowExecutionTasks" => {
164                self.describe_maintenance_window_execution_tasks(&req)
165            }
166            "DescribeMaintenanceWindowExecutionTaskInvocations" => {
167                self.describe_maintenance_window_execution_task_invocations(&req)
168            }
169            "DescribeMaintenanceWindowSchedule" => self.describe_maintenance_window_schedule(&req),
170            "DescribeMaintenanceWindowsForTarget" => {
171                self.describe_maintenance_windows_for_target(&req)
172            }
173            "CancelMaintenanceWindowExecution" => self.cancel_maintenance_window_execution(&req),
174            // Patch management details
175            "UpdatePatchBaseline" => self.update_patch_baseline(&req),
176            "DescribeInstancePatchStates" => self.describe_instance_patch_states(&req),
177            "DescribeInstancePatchStatesForPatchGroup" => {
178                self.describe_instance_patch_states_for_patch_group(&req)
179            }
180            "DescribeInstancePatches" => self.describe_instance_patches(&req),
181            "DescribeEffectivePatchesForPatchBaseline" => {
182                self.describe_effective_patches_for_patch_baseline(&req)
183            }
184            "GetDeployablePatchSnapshotForInstance" => {
185                self.get_deployable_patch_snapshot_for_instance(&req)
186            }
187            // Resource data sync
188            "CreateResourceDataSync" => self.create_resource_data_sync(&req),
189            "DeleteResourceDataSync" => self.delete_resource_data_sync(&req),
190            "ListResourceDataSync" => self.list_resource_data_sync(&req),
191            "UpdateResourceDataSync" => self.update_resource_data_sync(&req),
192            // OpsItem related items
193            "AssociateOpsItemRelatedItem" => self.associate_ops_item_related_item(&req),
194            "DisassociateOpsItemRelatedItem" => self.disassociate_ops_item_related_item(&req),
195            "ListOpsItemRelatedItems" => self.list_ops_item_related_items(&req),
196            "ListOpsItemEvents" => self.list_ops_item_events(&req),
197            // OpsMetadata
198            "CreateOpsMetadata" => self.create_ops_metadata(&req),
199            "GetOpsMetadata" => self.get_ops_metadata(&req),
200            "UpdateOpsMetadata" => self.update_ops_metadata(&req),
201            "DeleteOpsMetadata" => self.delete_ops_metadata(&req),
202            "ListOpsMetadata" => self.list_ops_metadata(&req),
203            // OpsMetadata extras
204            "GetOpsSummary" => self.get_ops_summary(&req),
205            // Automation
206            "StartAutomationExecution" => self.start_automation_execution(&req),
207            "StopAutomationExecution" => self.stop_automation_execution(&req),
208            "GetAutomationExecution" => self.get_automation_execution(&req),
209            "DescribeAutomationExecutions" => self.describe_automation_executions(&req),
210            "DescribeAutomationStepExecutions" => self.describe_automation_step_executions(&req),
211            "SendAutomationSignal" => self.send_automation_signal(&req),
212            "StartChangeRequestExecution" => self.start_change_request_execution(&req),
213            "StartExecutionPreview" => self.start_execution_preview(&req),
214            "GetExecutionPreview" => self.get_execution_preview(&req),
215            // Sessions
216            "StartSession" => self.start_session(&req),
217            "ResumeSession" => self.resume_session(&req),
218            "TerminateSession" => self.terminate_session(&req),
219            "DescribeSessions" => self.describe_sessions(&req),
220            "StartAccessRequest" => self.start_access_request(&req),
221            "GetAccessToken" => self.get_access_token(&req),
222            // Managed instances
223            "CreateActivation" => self.create_activation(&req),
224            "DeleteActivation" => self.delete_activation(&req),
225            "DescribeActivations" => self.describe_activations(&req),
226            "DeregisterManagedInstance" => self.deregister_managed_instance(&req),
227            "DescribeInstanceInformation" => self.describe_instance_information(&req),
228            "DescribeInstanceProperties" => self.describe_instance_properties(&req),
229            "UpdateManagedInstanceRole" => self.update_managed_instance_role(&req),
230            // Other
231            "ListNodes" => self.list_nodes(&req),
232            "ListNodesSummary" => self.list_nodes_summary(&req),
233            "DescribeEffectiveInstanceAssociations" => {
234                self.describe_effective_instance_associations(&req)
235            }
236            "DescribeInstanceAssociationsStatus" => {
237                self.describe_instance_associations_status(&req)
238            }
239            // Stubs
240            "GetConnectionStatus" => self.get_connection_status(&req),
241            "GetCalendarState" => self.get_calendar_state(&req),
242            "DescribePatchGroupState" => self.describe_patch_group_state(&req),
243            "DescribePatchProperties" => self.describe_patch_properties(&req),
244            "GetDefaultPatchBaseline" => self.get_default_patch_baseline(&req),
245            "RegisterDefaultPatchBaseline" => self.register_default_patch_baseline(&req),
246            "DescribeAvailablePatches" => self.describe_available_patches(&req),
247            "GetServiceSetting" => self.get_service_setting(&req),
248            "ResetServiceSetting" => self.reset_service_setting(&req),
249            "UpdateServiceSetting" => self.update_service_setting(&req),
250            _ => Err(AwsServiceError::action_not_implemented("ssm", &req.action)),
251        }
252    }
253
254    fn supported_actions(&self) -> &[&str] {
255        &[
256            "PutParameter",
257            "GetParameter",
258            "GetParameters",
259            "GetParametersByPath",
260            "DeleteParameter",
261            "DeleteParameters",
262            "DescribeParameters",
263            "GetParameterHistory",
264            "AddTagsToResource",
265            "RemoveTagsFromResource",
266            "ListTagsForResource",
267            "LabelParameterVersion",
268            "UnlabelParameterVersion",
269            "CreateDocument",
270            "GetDocument",
271            "DeleteDocument",
272            "UpdateDocument",
273            "DescribeDocument",
274            "UpdateDocumentDefaultVersion",
275            "ListDocuments",
276            "DescribeDocumentPermission",
277            "ModifyDocumentPermission",
278            "SendCommand",
279            "ListCommands",
280            "GetCommandInvocation",
281            "ListCommandInvocations",
282            "CancelCommand",
283            "CreateMaintenanceWindow",
284            "DescribeMaintenanceWindows",
285            "GetMaintenanceWindow",
286            "DeleteMaintenanceWindow",
287            "UpdateMaintenanceWindow",
288            "RegisterTargetWithMaintenanceWindow",
289            "DeregisterTargetFromMaintenanceWindow",
290            "DescribeMaintenanceWindowTargets",
291            "RegisterTaskWithMaintenanceWindow",
292            "DeregisterTaskFromMaintenanceWindow",
293            "DescribeMaintenanceWindowTasks",
294            "CreatePatchBaseline",
295            "DeletePatchBaseline",
296            "DescribePatchBaselines",
297            "GetPatchBaseline",
298            "RegisterPatchBaselineForPatchGroup",
299            "DeregisterPatchBaselineForPatchGroup",
300            "GetPatchBaselineForPatchGroup",
301            "DescribePatchGroups",
302            // Associations
303            "CreateAssociation",
304            "DescribeAssociation",
305            "DeleteAssociation",
306            "ListAssociations",
307            "UpdateAssociation",
308            "ListAssociationVersions",
309            "UpdateAssociationStatus",
310            "StartAssociationsOnce",
311            "CreateAssociationBatch",
312            "DescribeAssociationExecutions",
313            "DescribeAssociationExecutionTargets",
314            // OpsItems
315            "CreateOpsItem",
316            "GetOpsItem",
317            "UpdateOpsItem",
318            "DeleteOpsItem",
319            "DescribeOpsItems",
320            // Document extras
321            "ListDocumentVersions",
322            "ListDocumentMetadataHistory",
323            "UpdateDocumentMetadata",
324            // Resource policies
325            "PutResourcePolicy",
326            "GetResourcePolicies",
327            "DeleteResourcePolicy",
328            // Inventory
329            "PutInventory",
330            "GetInventory",
331            "GetInventorySchema",
332            "ListInventoryEntries",
333            "DeleteInventory",
334            "DescribeInventoryDeletions",
335            // Compliance
336            "PutComplianceItems",
337            "ListComplianceItems",
338            "ListComplianceSummaries",
339            "ListResourceComplianceSummaries",
340            // Maintenance window details
341            "UpdateMaintenanceWindowTarget",
342            "UpdateMaintenanceWindowTask",
343            "GetMaintenanceWindowTask",
344            "GetMaintenanceWindowExecution",
345            "GetMaintenanceWindowExecutionTask",
346            "GetMaintenanceWindowExecutionTaskInvocation",
347            "DescribeMaintenanceWindowExecutions",
348            "DescribeMaintenanceWindowExecutionTasks",
349            "DescribeMaintenanceWindowExecutionTaskInvocations",
350            "DescribeMaintenanceWindowSchedule",
351            "DescribeMaintenanceWindowsForTarget",
352            "CancelMaintenanceWindowExecution",
353            // Patch management details
354            "UpdatePatchBaseline",
355            "DescribeInstancePatchStates",
356            "DescribeInstancePatchStatesForPatchGroup",
357            "DescribeInstancePatches",
358            "DescribeEffectivePatchesForPatchBaseline",
359            "GetDeployablePatchSnapshotForInstance",
360            // Resource data sync
361            "CreateResourceDataSync",
362            "DeleteResourceDataSync",
363            "ListResourceDataSync",
364            "UpdateResourceDataSync",
365            // OpsItem related items
366            "AssociateOpsItemRelatedItem",
367            "DisassociateOpsItemRelatedItem",
368            "ListOpsItemRelatedItems",
369            "ListOpsItemEvents",
370            // OpsMetadata
371            "CreateOpsMetadata",
372            "GetOpsMetadata",
373            "UpdateOpsMetadata",
374            "DeleteOpsMetadata",
375            "ListOpsMetadata",
376            // OpsMetadata extras
377            "GetOpsSummary",
378            // Automation
379            "StartAutomationExecution",
380            "StopAutomationExecution",
381            "GetAutomationExecution",
382            "DescribeAutomationExecutions",
383            "DescribeAutomationStepExecutions",
384            "SendAutomationSignal",
385            "StartChangeRequestExecution",
386            "StartExecutionPreview",
387            "GetExecutionPreview",
388            // Sessions
389            "StartSession",
390            "ResumeSession",
391            "TerminateSession",
392            "DescribeSessions",
393            "StartAccessRequest",
394            "GetAccessToken",
395            // Managed instances
396            "CreateActivation",
397            "DeleteActivation",
398            "DescribeActivations",
399            "DeregisterManagedInstance",
400            "DescribeInstanceInformation",
401            "DescribeInstanceProperties",
402            "UpdateManagedInstanceRole",
403            // Other
404            "ListNodes",
405            "ListNodesSummary",
406            "DescribeEffectiveInstanceAssociations",
407            "DescribeInstanceAssociationsStatus",
408            // Stubs
409            "GetConnectionStatus",
410            "GetCalendarState",
411            "DescribePatchGroupState",
412            "DescribePatchProperties",
413            "GetDefaultPatchBaseline",
414            "RegisterDefaultPatchBaseline",
415            "DescribeAvailablePatches",
416            "GetServiceSetting",
417            "ResetServiceSetting",
418            "UpdateServiceSetting",
419        ]
420    }
421}
422
423fn missing(name: &str) -> AwsServiceError {
424    AwsServiceError::aws_error(
425        StatusCode::BAD_REQUEST,
426        "ValidationException",
427        format!("The request must contain the parameter {name}"),
428    )
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434    use parking_lot::RwLock;
435    use serde_json::{json, Value};
436    use std::collections::HashMap;
437    use std::sync::Arc;
438
439    fn make_service() -> SsmService {
440        let state: SharedSsmState = Arc::new(RwLock::new(crate::state::SsmState::new(
441            "123456789012",
442            "us-east-1",
443        )));
444        SsmService::new(state)
445    }
446
447    fn make_request(action: &str, body: Value) -> AwsRequest {
448        AwsRequest {
449            service: "ssm".to_string(),
450            action: action.to_string(),
451            region: "us-east-1".to_string(),
452            account_id: "123456789012".to_string(),
453            request_id: "test-id".to_string(),
454            headers: http::HeaderMap::new(),
455            query_params: HashMap::new(),
456            body: serde_json::to_vec(&body).unwrap().into(),
457            path_segments: vec![],
458            raw_path: "/".to_string(),
459            raw_query: String::new(),
460            method: http::Method::POST,
461            is_query_protocol: false,
462            access_key_id: None,
463        }
464    }
465
466    fn send_command(svc: &SsmService, doc_name: &str) -> String {
467        let req = make_request(
468            "SendCommand",
469            json!({
470                "DocumentName": doc_name,
471                "InstanceIds": ["i-1234567890abcdef0"]
472            }),
473        );
474        let resp = svc.send_command(&req).unwrap();
475        let body: Value = serde_json::from_slice(&resp.body).unwrap();
476        body["Command"]["CommandId"].as_str().unwrap().to_string()
477    }
478
479    #[test]
480    fn list_commands_pagination() {
481        let svc = make_service();
482
483        // Send 3 commands
484        let mut command_ids = Vec::new();
485        for i in 0..3 {
486            command_ids.push(send_command(&svc, &format!("AWS-RunShellScript-{i}")));
487        }
488
489        // First page: MaxResults=1
490        let req = make_request("ListCommands", json!({ "MaxResults": 1 }));
491        let resp = svc.list_commands(&req).unwrap();
492        let body: Value = serde_json::from_slice(&resp.body).unwrap();
493        assert_eq!(body["Commands"].as_array().unwrap().len(), 1);
494        let token = body["NextToken"].as_str().unwrap();
495
496        // Second page
497        let req = make_request(
498            "ListCommands",
499            json!({ "MaxResults": 1, "NextToken": token }),
500        );
501        let resp = svc.list_commands(&req).unwrap();
502        let body: Value = serde_json::from_slice(&resp.body).unwrap();
503        assert_eq!(body["Commands"].as_array().unwrap().len(), 1);
504        let token = body["NextToken"].as_str().unwrap();
505
506        // Third page (last)
507        let req = make_request(
508            "ListCommands",
509            json!({ "MaxResults": 1, "NextToken": token }),
510        );
511        let resp = svc.list_commands(&req).unwrap();
512        let body: Value = serde_json::from_slice(&resp.body).unwrap();
513        assert_eq!(body["Commands"].as_array().unwrap().len(), 1);
514        assert!(body.get("NextToken").is_none() || body["NextToken"].is_null());
515    }
516
517    #[test]
518    fn send_command_response_omits_non_shape_fields() {
519        let svc = make_service();
520
521        // Create a document first
522        let req = make_request(
523            "CreateDocument",
524            json!({
525                "Name": "TestDoc",
526                "Content": "{\"schemaVersion\":\"2.2\",\"mainSteps\":[]}",
527                "DocumentType": "Command"
528            }),
529        );
530        svc.create_document(&req).unwrap();
531
532        let req = make_request(
533            "SendCommand",
534            json!({
535                "DocumentName": "TestDoc",
536                "InstanceIds": ["i-1234567890abcdef0"],
537                "DocumentHash": "abc123hash",
538                "DocumentHashType": "Sha256",
539                "ServiceRoleArn": "arn:aws:iam::123456789012:role/MyRole"
540            }),
541        );
542        let resp = svc.send_command(&req).unwrap();
543        let body: Value = serde_json::from_slice(&resp.body).unwrap();
544        let cmd = &body["Command"];
545
546        // These fields are not part of the Smithy Command output shape
547        assert!(
548            !cmd.as_object().unwrap().contains_key("DocumentHash"),
549            "DocumentHash should not be in SendCommand response"
550        );
551        assert!(
552            !cmd.as_object().unwrap().contains_key("DocumentHashType"),
553            "DocumentHashType should not be in SendCommand response"
554        );
555        assert!(
556            !cmd.as_object().unwrap().contains_key("ServiceRoleArn"),
557            "ServiceRoleArn should not be in SendCommand response"
558        );
559
560        // Ensure expected fields are still present
561        assert!(cmd["CommandId"].is_string());
562        assert_eq!(cmd["DocumentName"].as_str().unwrap(), "TestDoc");
563    }
564
565    #[test]
566    fn describe_maintenance_windows_pagination() {
567        let svc = make_service();
568
569        // Create 3 maintenance windows (min MaxResults for this API is 10,
570        // so we create 11 to test pagination with the minimum page size)
571        for i in 0..11 {
572            let req = make_request(
573                "CreateMaintenanceWindow",
574                json!({
575                    "Name": format!("test-window-{i:02}"),
576                    "Schedule": "cron(0 2 ? * SUN *)",
577                    "Duration": 3,
578                    "Cutoff": 1,
579                    "AllowUnassociatedTargets": true
580                }),
581            );
582            svc.create_maintenance_window(&req).unwrap();
583        }
584
585        // First page: MaxResults=10 (minimum allowed)
586        let req = make_request("DescribeMaintenanceWindows", json!({ "MaxResults": 10 }));
587        let resp = svc.describe_maintenance_windows(&req).unwrap();
588        let body: Value = serde_json::from_slice(&resp.body).unwrap();
589        assert_eq!(body["WindowIdentities"].as_array().unwrap().len(), 10);
590        let token = body["NextToken"].as_str().unwrap();
591
592        // Second page (1 remaining)
593        let req = make_request(
594            "DescribeMaintenanceWindows",
595            json!({ "MaxResults": 10, "NextToken": token }),
596        );
597        let resp = svc.describe_maintenance_windows(&req).unwrap();
598        let body: Value = serde_json::from_slice(&resp.body).unwrap();
599        assert_eq!(body["WindowIdentities"].as_array().unwrap().len(), 1);
600        assert!(body.get("NextToken").is_none() || body["NextToken"].is_null());
601    }
602
603    // -- Associations --
604
605    #[test]
606    fn association_crud() {
607        let svc = make_service();
608
609        // Create
610        let req = make_request(
611            "CreateAssociation",
612            json!({
613                "Name": "AWS-RunShellScript",
614                "Targets": [{"Key": "InstanceIds", "Values": ["i-1234567890abcdef0"]}],
615                "ScheduleExpression": "rate(1 hour)",
616                "AssociationName": "my-assoc",
617            }),
618        );
619        let resp = svc.create_association(&req).unwrap();
620        let body: Value = serde_json::from_slice(&resp.body).unwrap();
621        let assoc_id = body["AssociationDescription"]["AssociationId"]
622            .as_str()
623            .unwrap()
624            .to_string();
625        assert_eq!(
626            body["AssociationDescription"]["Name"].as_str().unwrap(),
627            "AWS-RunShellScript"
628        );
629
630        // Describe
631        let req = make_request("DescribeAssociation", json!({ "AssociationId": assoc_id }));
632        let resp = svc.describe_association(&req).unwrap();
633        let body: Value = serde_json::from_slice(&resp.body).unwrap();
634        assert_eq!(
635            body["AssociationDescription"]["AssociationName"]
636                .as_str()
637                .unwrap(),
638            "my-assoc"
639        );
640
641        // List
642        let req = make_request("ListAssociations", json!({}));
643        let resp = svc.list_associations(&req).unwrap();
644        let body: Value = serde_json::from_slice(&resp.body).unwrap();
645        assert_eq!(body["Associations"].as_array().unwrap().len(), 1);
646
647        // Update
648        let req = make_request(
649            "UpdateAssociation",
650            json!({
651                "AssociationId": assoc_id,
652                "AssociationName": "updated-assoc",
653            }),
654        );
655        let resp = svc.update_association(&req).unwrap();
656        let body: Value = serde_json::from_slice(&resp.body).unwrap();
657        assert_eq!(
658            body["AssociationDescription"]["AssociationName"]
659                .as_str()
660                .unwrap(),
661            "updated-assoc"
662        );
663
664        // ListAssociationVersions
665        let req = make_request(
666            "ListAssociationVersions",
667            json!({ "AssociationId": assoc_id }),
668        );
669        let resp = svc.list_association_versions(&req).unwrap();
670        let body: Value = serde_json::from_slice(&resp.body).unwrap();
671        assert_eq!(body["AssociationVersions"].as_array().unwrap().len(), 2);
672
673        // Delete
674        let req = make_request("DeleteAssociation", json!({ "AssociationId": assoc_id }));
675        svc.delete_association(&req).unwrap();
676
677        // Verify deleted
678        let req = make_request("DescribeAssociation", json!({ "AssociationId": assoc_id }));
679        assert!(svc.describe_association(&req).is_err());
680    }
681
682    #[test]
683    fn association_batch_create() {
684        let svc = make_service();
685        let req = make_request(
686            "CreateAssociationBatch",
687            json!({
688                "Entries": [
689                    {"Name": "AWS-RunShellScript", "Targets": [{"Key": "InstanceIds", "Values": ["i-001"]}]},
690                    {"Name": "AWS-RunShellScript", "Targets": [{"Key": "InstanceIds", "Values": ["i-002"]}]},
691                ]
692            }),
693        );
694        let resp = svc.create_association_batch(&req).unwrap();
695        let body: Value = serde_json::from_slice(&resp.body).unwrap();
696        assert_eq!(body["Successful"].as_array().unwrap().len(), 2);
697        assert!(body["Failed"].as_array().unwrap().is_empty());
698    }
699
700    #[test]
701    fn start_associations_once_noop() {
702        let svc = make_service();
703        let req = make_request(
704            "StartAssociationsOnce",
705            json!({ "AssociationIds": ["fake-id"] }),
706        );
707        svc.start_associations_once(&req).unwrap();
708    }
709
710    // -- OpsItems --
711
712    #[test]
713    fn ops_item_crud() {
714        let svc = make_service();
715
716        // Create
717        let req = make_request(
718            "CreateOpsItem",
719            json!({
720                "Title": "Test OpsItem",
721                "Source": "test",
722                "Description": "A test ops item",
723            }),
724        );
725        let resp = svc.create_ops_item(&req).unwrap();
726        let body: Value = serde_json::from_slice(&resp.body).unwrap();
727        let ops_item_id = body["OpsItemId"].as_str().unwrap().to_string();
728
729        // Get
730        let req = make_request("GetOpsItem", json!({ "OpsItemId": ops_item_id }));
731        let resp = svc.get_ops_item(&req).unwrap();
732        let body: Value = serde_json::from_slice(&resp.body).unwrap();
733        assert_eq!(body["OpsItem"]["Title"].as_str().unwrap(), "Test OpsItem");
734        assert_eq!(body["OpsItem"]["Status"].as_str().unwrap(), "Open");
735
736        // Update
737        let req = make_request(
738            "UpdateOpsItem",
739            json!({
740                "OpsItemId": ops_item_id,
741                "Title": "Updated OpsItem",
742                "Status": "Resolved",
743            }),
744        );
745        svc.update_ops_item(&req).unwrap();
746
747        // Verify update
748        let req = make_request("GetOpsItem", json!({ "OpsItemId": ops_item_id }));
749        let resp = svc.get_ops_item(&req).unwrap();
750        let body: Value = serde_json::from_slice(&resp.body).unwrap();
751        assert_eq!(
752            body["OpsItem"]["Title"].as_str().unwrap(),
753            "Updated OpsItem"
754        );
755        assert_eq!(body["OpsItem"]["Status"].as_str().unwrap(), "Resolved");
756
757        // Describe
758        let req = make_request("DescribeOpsItems", json!({}));
759        let resp = svc.describe_ops_items(&req).unwrap();
760        let body: Value = serde_json::from_slice(&resp.body).unwrap();
761        assert_eq!(body["OpsItemSummaries"].as_array().unwrap().len(), 1);
762
763        // Delete
764        let req = make_request("DeleteOpsItem", json!({ "OpsItemId": ops_item_id }));
765        svc.delete_ops_item(&req).unwrap();
766
767        // Verify deleted
768        let req = make_request("GetOpsItem", json!({ "OpsItemId": ops_item_id }));
769        assert!(svc.get_ops_item(&req).is_err());
770    }
771
772    // -- Resource policies --
773
774    #[test]
775    fn resource_policy_crud() {
776        let svc = make_service();
777        let resource_arn = "arn:aws:ssm:us-east-1:123456789012:parameter/test";
778
779        // Put
780        let req = make_request(
781            "PutResourcePolicy",
782            json!({
783                "ResourceArn": resource_arn,
784                "Policy": r#"{"Version":"2012-10-17","Statement":[]}"#,
785            }),
786        );
787        let resp = svc.put_resource_policy(&req).unwrap();
788        let body: Value = serde_json::from_slice(&resp.body).unwrap();
789        let policy_id = body["PolicyId"].as_str().unwrap().to_string();
790        let policy_hash = body["PolicyHash"].as_str().unwrap().to_string();
791
792        // Get
793        let req = make_request(
794            "GetResourcePolicies",
795            json!({ "ResourceArn": resource_arn }),
796        );
797        let resp = svc.get_resource_policies(&req).unwrap();
798        let body: Value = serde_json::from_slice(&resp.body).unwrap();
799        assert_eq!(body["Policies"].as_array().unwrap().len(), 1);
800
801        // Delete
802        let req = make_request(
803            "DeleteResourcePolicy",
804            json!({
805                "ResourceArn": resource_arn,
806                "PolicyId": policy_id,
807                "PolicyHash": policy_hash,
808            }),
809        );
810        svc.delete_resource_policy(&req).unwrap();
811
812        // Verify deleted
813        let req = make_request(
814            "GetResourcePolicies",
815            json!({ "ResourceArn": resource_arn }),
816        );
817        let resp = svc.get_resource_policies(&req).unwrap();
818        let body: Value = serde_json::from_slice(&resp.body).unwrap();
819        assert!(body["Policies"].as_array().unwrap().is_empty());
820    }
821
822    // -- Stubs --
823
824    #[test]
825    fn get_connection_status_returns_connected() {
826        let svc = make_service();
827        let req = make_request(
828            "GetConnectionStatus",
829            json!({ "Target": "i-1234567890abcdef0" }),
830        );
831        let resp = svc.get_connection_status(&req).unwrap();
832        let body: Value = serde_json::from_slice(&resp.body).unwrap();
833        assert_eq!(body["Status"].as_str().unwrap(), "connected");
834    }
835
836    #[test]
837    fn get_calendar_state_returns_open() {
838        let svc = make_service();
839        let req = make_request(
840            "GetCalendarState",
841            json!({ "CalendarNames": ["arn:aws:ssm:us-east-1:123456789012:document/cal"] }),
842        );
843        let resp = svc.get_calendar_state(&req).unwrap();
844        let body: Value = serde_json::from_slice(&resp.body).unwrap();
845        assert_eq!(body["State"].as_str().unwrap(), "OPEN");
846    }
847
848    #[test]
849    fn service_setting_crud() {
850        let svc = make_service();
851
852        // Get default
853        let req = make_request(
854            "GetServiceSetting",
855            json!({ "SettingId": "/ssm/parameter-store/high-throughput-enabled" }),
856        );
857        let resp = svc.get_service_setting(&req).unwrap();
858        let body: Value = serde_json::from_slice(&resp.body).unwrap();
859        assert_eq!(
860            body["ServiceSetting"]["Status"].as_str().unwrap(),
861            "Default"
862        );
863
864        // Update
865        let req = make_request(
866            "UpdateServiceSetting",
867            json!({
868                "SettingId": "/ssm/parameter-store/high-throughput-enabled",
869                "SettingValue": "true",
870            }),
871        );
872        svc.update_service_setting(&req).unwrap();
873
874        // Verify
875        let req = make_request(
876            "GetServiceSetting",
877            json!({ "SettingId": "/ssm/parameter-store/high-throughput-enabled" }),
878        );
879        let resp = svc.get_service_setting(&req).unwrap();
880        let body: Value = serde_json::from_slice(&resp.body).unwrap();
881        assert_eq!(
882            body["ServiceSetting"]["Status"].as_str().unwrap(),
883            "Customized"
884        );
885        assert_eq!(
886            body["ServiceSetting"]["SettingValue"].as_str().unwrap(),
887            "true"
888        );
889
890        // Reset
891        let req = make_request(
892            "ResetServiceSetting",
893            json!({ "SettingId": "/ssm/parameter-store/high-throughput-enabled" }),
894        );
895        svc.reset_service_setting(&req).unwrap();
896
897        // Verify reset
898        let req = make_request(
899            "GetServiceSetting",
900            json!({ "SettingId": "/ssm/parameter-store/high-throughput-enabled" }),
901        );
902        let resp = svc.get_service_setting(&req).unwrap();
903        let body: Value = serde_json::from_slice(&resp.body).unwrap();
904        assert_eq!(
905            body["ServiceSetting"]["Status"].as_str().unwrap(),
906            "Default"
907        );
908    }
909
910    #[test]
911    fn list_document_versions_works() {
912        let svc = make_service();
913
914        // Create a document
915        let req = make_request(
916            "CreateDocument",
917            json!({
918                "Name": "TestDocVer",
919                "Content": r#"{"schemaVersion":"2.2","mainSteps":[]}"#,
920                "DocumentType": "Command",
921            }),
922        );
923        svc.create_document(&req).unwrap();
924
925        // List versions
926        let req = make_request("ListDocumentVersions", json!({ "Name": "TestDocVer" }));
927        let resp = svc.list_document_versions(&req).unwrap();
928        let body: Value = serde_json::from_slice(&resp.body).unwrap();
929        assert!(!body["DocumentVersions"].as_array().unwrap().is_empty());
930    }
931
932    #[test]
933    fn describe_patch_group_state_returns_zeros() {
934        let svc = make_service();
935        let req = make_request(
936            "DescribePatchGroupState",
937            json!({ "PatchGroup": "test-group" }),
938        );
939        let resp = svc.describe_patch_group_state(&req).unwrap();
940        let body: Value = serde_json::from_slice(&resp.body).unwrap();
941        assert_eq!(body["Instances"].as_i64().unwrap(), 0);
942    }
943
944    #[test]
945    fn get_default_patch_baseline_works() {
946        let svc = make_service();
947        let req = make_request(
948            "GetDefaultPatchBaseline",
949            json!({ "OperatingSystem": "WINDOWS" }),
950        );
951        let resp = svc.get_default_patch_baseline(&req).unwrap();
952        let body: Value = serde_json::from_slice(&resp.body).unwrap();
953        assert!(body["BaselineId"].is_string());
954    }
955
956    #[test]
957    fn describe_available_patches_returns_empty() {
958        let svc = make_service();
959        let req = make_request("DescribeAvailablePatches", json!({}));
960        let resp = svc.describe_available_patches(&req).unwrap();
961        let body: Value = serde_json::from_slice(&resp.body).unwrap();
962        assert!(body["Patches"].as_array().unwrap().is_empty());
963    }
964
965    #[test]
966    fn describe_patch_properties_returns_empty() {
967        let svc = make_service();
968        let req = make_request(
969            "DescribePatchProperties",
970            json!({ "OperatingSystem": "WINDOWS", "Property": "PRODUCT" }),
971        );
972        let resp = svc.describe_patch_properties(&req).unwrap();
973        let body: Value = serde_json::from_slice(&resp.body).unwrap();
974        assert!(body["Properties"].as_array().unwrap().is_empty());
975    }
976
977    // ── Inventory ─────────────────────────────────────────────────
978
979    #[test]
980    fn inventory_lifecycle() {
981        let svc = make_service();
982
983        // PutInventory
984        let req = make_request(
985            "PutInventory",
986            json!({
987                "InstanceId": "i-1234567890abcdef0",
988                "Items": [{
989                    "TypeName": "AWS:Application",
990                    "SchemaVersion": "1.1",
991                    "CaptureTime": "2024-01-01T00:00:00Z",
992                    "Content": [
993                        {"Name": "TestApp", "Version": "1.0"},
994                        {"Name": "AnotherApp", "Version": "2.0"},
995                    ]
996                }]
997            }),
998        );
999        svc.put_inventory(&req).unwrap();
1000
1001        // GetInventory
1002        let req = make_request("GetInventory", json!({}));
1003        let resp = svc.get_inventory(&req).unwrap();
1004        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1005        assert_eq!(body["Entities"].as_array().unwrap().len(), 1);
1006        assert_eq!(
1007            body["Entities"][0]["Id"].as_str().unwrap(),
1008            "i-1234567890abcdef0"
1009        );
1010
1011        // ListInventoryEntries
1012        let req = make_request(
1013            "ListInventoryEntries",
1014            json!({
1015                "InstanceId": "i-1234567890abcdef0",
1016                "TypeName": "AWS:Application",
1017            }),
1018        );
1019        let resp = svc.list_inventory_entries(&req).unwrap();
1020        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1021        assert_eq!(body["Entries"].as_array().unwrap().len(), 2);
1022        assert_eq!(body["TypeName"].as_str().unwrap(), "AWS:Application");
1023
1024        // GetInventorySchema
1025        let req = make_request("GetInventorySchema", json!({}));
1026        let resp = svc.get_inventory_schema(&req).unwrap();
1027        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1028        assert!(!body["Schemas"].as_array().unwrap().is_empty());
1029
1030        // DeleteInventory
1031        let req = make_request("DeleteInventory", json!({ "TypeName": "AWS:Application" }));
1032        let resp = svc.delete_inventory(&req).unwrap();
1033        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1034        assert!(body["DeletionId"].is_string());
1035
1036        // DescribeInventoryDeletions
1037        let req = make_request("DescribeInventoryDeletions", json!({}));
1038        let resp = svc.describe_inventory_deletions(&req).unwrap();
1039        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1040        assert_eq!(body["InventoryDeletions"].as_array().unwrap().len(), 1);
1041
1042        // Verify inventory deleted
1043        let req = make_request(
1044            "ListInventoryEntries",
1045            json!({
1046                "InstanceId": "i-1234567890abcdef0",
1047                "TypeName": "AWS:Application",
1048            }),
1049        );
1050        let resp = svc.list_inventory_entries(&req).unwrap();
1051        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1052        assert!(body["Entries"].as_array().unwrap().is_empty());
1053    }
1054
1055    // ── Compliance ────────────────────────────────────────────────
1056
1057    #[test]
1058    fn compliance_lifecycle() {
1059        let svc = make_service();
1060
1061        // PutComplianceItems
1062        let req = make_request(
1063            "PutComplianceItems",
1064            json!({
1065                "ResourceId": "i-1234567890abcdef0",
1066                "ResourceType": "ManagedInstance",
1067                "ComplianceType": "Custom:PatchTest",
1068                "ExecutionSummary": {
1069                    "ExecutionTime": "2024-01-01T00:00:00Z",
1070                },
1071                "Items": [
1072                    {
1073                        "Id": "patch-1",
1074                        "Title": "Security patch 1",
1075                        "Severity": "CRITICAL",
1076                        "Status": "COMPLIANT",
1077                    },
1078                    {
1079                        "Id": "patch-2",
1080                        "Title": "Security patch 2",
1081                        "Severity": "HIGH",
1082                        "Status": "NON_COMPLIANT",
1083                    },
1084                ],
1085            }),
1086        );
1087        svc.put_compliance_items(&req).unwrap();
1088
1089        // ListComplianceItems
1090        let req = make_request("ListComplianceItems", json!({}));
1091        let resp = svc.list_compliance_items(&req).unwrap();
1092        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1093        assert_eq!(body["ComplianceItems"].as_array().unwrap().len(), 2);
1094
1095        // ListComplianceSummaries
1096        let req = make_request("ListComplianceSummaries", json!({}));
1097        let resp = svc.list_compliance_summaries(&req).unwrap();
1098        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1099        assert_eq!(body["ComplianceSummaryItems"].as_array().unwrap().len(), 1);
1100
1101        // ListResourceComplianceSummaries
1102        let req = make_request("ListResourceComplianceSummaries", json!({}));
1103        let resp = svc.list_resource_compliance_summaries(&req).unwrap();
1104        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1105        assert_eq!(
1106            body["ResourceComplianceSummaryItems"]
1107                .as_array()
1108                .unwrap()
1109                .len(),
1110            1
1111        );
1112    }
1113
1114    // ── Maintenance Window Details ────────────────────────────────
1115
1116    fn create_mw_with_target_and_task(svc: &SsmService) -> (String, String, String) {
1117        // Create a window
1118        let req = make_request(
1119            "CreateMaintenanceWindow",
1120            json!({
1121                "Name": "test-mw",
1122                "Schedule": "cron(0 2 ? * SUN *)",
1123                "Duration": 3,
1124                "Cutoff": 1,
1125                "AllowUnassociatedTargets": true,
1126            }),
1127        );
1128        let resp = svc.create_maintenance_window(&req).unwrap();
1129        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1130        let window_id = body["WindowId"].as_str().unwrap().to_string();
1131
1132        // Register target
1133        let req = make_request(
1134            "RegisterTargetWithMaintenanceWindow",
1135            json!({
1136                "WindowId": window_id,
1137                "ResourceType": "INSTANCE",
1138                "Targets": [{"Key": "InstanceIds", "Values": ["i-001"]}],
1139                "Name": "test-target",
1140            }),
1141        );
1142        let resp = svc.register_target_with_maintenance_window(&req).unwrap();
1143        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1144        let target_id = body["WindowTargetId"].as_str().unwrap().to_string();
1145
1146        // Register task
1147        let req = make_request(
1148            "RegisterTaskWithMaintenanceWindow",
1149            json!({
1150                "WindowId": window_id,
1151                "TaskArn": "AWS-RunShellScript",
1152                "TaskType": "RUN_COMMAND",
1153                "Targets": [{"Key": "WindowTargetIds", "Values": [target_id]}],
1154                "Name": "test-task",
1155            }),
1156        );
1157        let resp = svc.register_task_with_maintenance_window(&req).unwrap();
1158        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1159        let task_id = body["WindowTaskId"].as_str().unwrap().to_string();
1160
1161        (window_id, target_id, task_id)
1162    }
1163
1164    #[test]
1165    fn maintenance_window_update_target_and_task() {
1166        let svc = make_service();
1167        let (window_id, target_id, task_id) = create_mw_with_target_and_task(&svc);
1168
1169        // Update target
1170        let req = make_request(
1171            "UpdateMaintenanceWindowTarget",
1172            json!({
1173                "WindowId": window_id,
1174                "WindowTargetId": target_id,
1175                "Name": "updated-target",
1176            }),
1177        );
1178        let resp = svc.update_maintenance_window_target(&req).unwrap();
1179        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1180        assert_eq!(body["Name"].as_str().unwrap(), "updated-target");
1181
1182        // Get task
1183        let req = make_request(
1184            "GetMaintenanceWindowTask",
1185            json!({
1186                "WindowId": window_id,
1187                "WindowTaskId": task_id,
1188            }),
1189        );
1190        let resp = svc.get_maintenance_window_task(&req).unwrap();
1191        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1192        assert_eq!(body["TaskArn"].as_str().unwrap(), "AWS-RunShellScript");
1193        assert_eq!(body["Name"].as_str().unwrap(), "test-task");
1194
1195        // Update task
1196        let req = make_request(
1197            "UpdateMaintenanceWindowTask",
1198            json!({
1199                "WindowId": window_id,
1200                "WindowTaskId": task_id,
1201                "Name": "updated-task",
1202                "MaxConcurrency": "10",
1203            }),
1204        );
1205        let resp = svc.update_maintenance_window_task(&req).unwrap();
1206        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1207        assert_eq!(body["Name"].as_str().unwrap(), "updated-task");
1208        assert_eq!(body["MaxConcurrency"].as_str().unwrap(), "10");
1209    }
1210
1211    #[test]
1212    fn maintenance_window_execution_lifecycle() {
1213        let svc = make_service();
1214        let (window_id, _, _) = create_mw_with_target_and_task(&svc);
1215
1216        let exec_id = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
1217        let task_exec_id = "11111111-2222-3333-4444-555555555555";
1218
1219        // Manually insert an execution for testing
1220        {
1221            let now = chrono::Utc::now();
1222            let mut state = svc.state.write();
1223            let exec = crate::state::MaintenanceWindowExecution {
1224                window_execution_id: exec_id.to_string(),
1225                window_id: window_id.clone(),
1226                status: "IN_PROGRESS".to_string(),
1227                start_time: now,
1228                end_time: None,
1229                tasks: vec![crate::state::MaintenanceWindowExecutionTask {
1230                    task_execution_id: task_exec_id.to_string(),
1231                    window_execution_id: exec_id.to_string(),
1232                    task_arn: "AWS-RunShellScript".to_string(),
1233                    task_type: "RUN_COMMAND".to_string(),
1234                    status: "IN_PROGRESS".to_string(),
1235                    start_time: now,
1236                    end_time: None,
1237                    invocations: vec![crate::state::MaintenanceWindowExecutionTaskInvocation {
1238                        invocation_id: "inv-001".to_string(),
1239                        task_execution_id: task_exec_id.to_string(),
1240                        window_execution_id: exec_id.to_string(),
1241                        execution_id: Some("cmd-001".to_string()),
1242                        status: "IN_PROGRESS".to_string(),
1243                        start_time: now,
1244                        end_time: None,
1245                        parameters: None,
1246                        owner_information: None,
1247                        window_target_id: None,
1248                        status_details: None,
1249                    }],
1250                }],
1251            };
1252            state.maintenance_window_executions.push(exec);
1253        }
1254
1255        // DescribeMaintenanceWindowExecutions
1256        let req = make_request(
1257            "DescribeMaintenanceWindowExecutions",
1258            json!({ "WindowId": window_id }),
1259        );
1260        let resp = svc.describe_maintenance_window_executions(&req).unwrap();
1261        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1262        assert_eq!(body["WindowExecutions"].as_array().unwrap().len(), 1);
1263
1264        // GetMaintenanceWindowExecution
1265        let req = make_request(
1266            "GetMaintenanceWindowExecution",
1267            json!({ "WindowExecutionId": exec_id }),
1268        );
1269        let resp = svc.get_maintenance_window_execution(&req).unwrap();
1270        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1271        assert_eq!(body["Status"].as_str().unwrap(), "IN_PROGRESS");
1272
1273        // DescribeMaintenanceWindowExecutionTasks
1274        let req = make_request(
1275            "DescribeMaintenanceWindowExecutionTasks",
1276            json!({ "WindowExecutionId": exec_id }),
1277        );
1278        let resp = svc
1279            .describe_maintenance_window_execution_tasks(&req)
1280            .unwrap();
1281        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1282        assert_eq!(
1283            body["WindowExecutionTaskIdentities"]
1284                .as_array()
1285                .unwrap()
1286                .len(),
1287            1
1288        );
1289
1290        // GetMaintenanceWindowExecutionTask
1291        let req = make_request(
1292            "GetMaintenanceWindowExecutionTask",
1293            json!({
1294                "WindowExecutionId": exec_id,
1295                "TaskId": task_exec_id,
1296            }),
1297        );
1298        let resp = svc.get_maintenance_window_execution_task(&req).unwrap();
1299        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1300        assert_eq!(body["TaskArn"].as_str().unwrap(), "AWS-RunShellScript");
1301
1302        // DescribeMaintenanceWindowExecutionTaskInvocations
1303        let req = make_request(
1304            "DescribeMaintenanceWindowExecutionTaskInvocations",
1305            json!({
1306                "WindowExecutionId": exec_id,
1307                "TaskId": task_exec_id,
1308            }),
1309        );
1310        let resp = svc
1311            .describe_maintenance_window_execution_task_invocations(&req)
1312            .unwrap();
1313        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1314        assert_eq!(
1315            body["WindowExecutionTaskInvocationIdentities"]
1316                .as_array()
1317                .unwrap()
1318                .len(),
1319            1
1320        );
1321
1322        // GetMaintenanceWindowExecutionTaskInvocation
1323        let req = make_request(
1324            "GetMaintenanceWindowExecutionTaskInvocation",
1325            json!({
1326                "WindowExecutionId": exec_id,
1327                "TaskId": task_exec_id,
1328                "InvocationId": "inv-001",
1329            }),
1330        );
1331        let resp = svc
1332            .get_maintenance_window_execution_task_invocation(&req)
1333            .unwrap();
1334        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1335        assert_eq!(body["ExecutionId"].as_str().unwrap(), "cmd-001");
1336
1337        // CancelMaintenanceWindowExecution
1338        let req = make_request(
1339            "CancelMaintenanceWindowExecution",
1340            json!({ "WindowExecutionId": exec_id }),
1341        );
1342        let resp = svc.cancel_maintenance_window_execution(&req).unwrap();
1343        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1344        assert_eq!(body["WindowExecutionId"].as_str().unwrap(), exec_id);
1345
1346        // DescribeMaintenanceWindowSchedule
1347        let req = make_request("DescribeMaintenanceWindowSchedule", json!({}));
1348        let resp = svc.describe_maintenance_window_schedule(&req).unwrap();
1349        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1350        assert!(body["ScheduledWindowExecutions"]
1351            .as_array()
1352            .unwrap()
1353            .is_empty());
1354
1355        // DescribeMaintenanceWindowsForTarget
1356        let req = make_request(
1357            "DescribeMaintenanceWindowsForTarget",
1358            json!({
1359                "ResourceType": "INSTANCE",
1360                "Targets": [{"Key": "InstanceIds", "Values": ["i-001"]}],
1361            }),
1362        );
1363        let resp = svc.describe_maintenance_windows_for_target(&req).unwrap();
1364        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1365        assert_eq!(body["WindowIdentities"].as_array().unwrap().len(), 1);
1366    }
1367
1368    // ── Patch baseline update ─────────────────────────────────────
1369
1370    #[test]
1371    fn update_patch_baseline_works() {
1372        let svc = make_service();
1373
1374        // Create
1375        let req = make_request(
1376            "CreatePatchBaseline",
1377            json!({
1378                "Name": "test-baseline",
1379                "OperatingSystem": "AMAZON_LINUX_2",
1380                "Description": "original description",
1381            }),
1382        );
1383        let resp = svc.create_patch_baseline(&req).unwrap();
1384        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1385        let baseline_id = body["BaselineId"].as_str().unwrap().to_string();
1386
1387        // Update
1388        let req = make_request(
1389            "UpdatePatchBaseline",
1390            json!({
1391                "BaselineId": baseline_id,
1392                "Name": "updated-baseline",
1393                "Description": "updated description",
1394                "ApprovedPatches": ["KB001", "KB002"],
1395            }),
1396        );
1397        let resp = svc.update_patch_baseline(&req).unwrap();
1398        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1399        assert_eq!(body["Name"].as_str().unwrap(), "updated-baseline");
1400        assert_eq!(body["Description"].as_str().unwrap(), "updated description");
1401        assert_eq!(body["ApprovedPatches"].as_array().unwrap().len(), 2);
1402    }
1403
1404    // ── Resource data sync ────────────────────────────────────────
1405
1406    #[test]
1407    fn resource_data_sync_lifecycle() {
1408        let svc = make_service();
1409
1410        // Create
1411        let req = make_request(
1412            "CreateResourceDataSync",
1413            json!({
1414                "SyncName": "test-sync",
1415                "SyncType": "SyncFromSource",
1416                "SyncSource": {
1417                    "SourceType": "AWS",
1418                    "SourceRegions": ["us-east-1"],
1419                },
1420            }),
1421        );
1422        svc.create_resource_data_sync(&req).unwrap();
1423
1424        // List
1425        let req = make_request("ListResourceDataSync", json!({}));
1426        let resp = svc.list_resource_data_sync(&req).unwrap();
1427        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1428        assert_eq!(body["ResourceDataSyncItems"].as_array().unwrap().len(), 1);
1429        assert_eq!(
1430            body["ResourceDataSyncItems"][0]["SyncName"]
1431                .as_str()
1432                .unwrap(),
1433            "test-sync"
1434        );
1435
1436        // Update
1437        let req = make_request(
1438            "UpdateResourceDataSync",
1439            json!({
1440                "SyncName": "test-sync",
1441                "SyncType": "SyncFromSource",
1442                "SyncSource": {
1443                    "SourceType": "AWS",
1444                    "SourceRegions": ["us-east-1", "us-west-2"],
1445                },
1446            }),
1447        );
1448        svc.update_resource_data_sync(&req).unwrap();
1449
1450        // Delete
1451        let req = make_request("DeleteResourceDataSync", json!({ "SyncName": "test-sync" }));
1452        svc.delete_resource_data_sync(&req).unwrap();
1453
1454        // Verify deleted
1455        let req = make_request("ListResourceDataSync", json!({}));
1456        let resp = svc.list_resource_data_sync(&req).unwrap();
1457        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1458        assert!(body["ResourceDataSyncItems"].as_array().unwrap().is_empty());
1459    }
1460
1461    // ── Patch stubs ───────────────────────────────────────────────
1462
1463    #[test]
1464    fn describe_instance_patch_states_returns_empty() {
1465        let svc = make_service();
1466        let req = make_request(
1467            "DescribeInstancePatchStates",
1468            json!({ "InstanceIds": ["i-001"] }),
1469        );
1470        let resp = svc.describe_instance_patch_states(&req).unwrap();
1471        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1472        assert!(body["InstancePatchStates"].as_array().unwrap().is_empty());
1473    }
1474
1475    #[test]
1476    fn describe_instance_patches_returns_empty() {
1477        let svc = make_service();
1478        let req = make_request("DescribeInstancePatches", json!({ "InstanceId": "i-001" }));
1479        let resp = svc.describe_instance_patches(&req).unwrap();
1480        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1481        assert!(body["Patches"].as_array().unwrap().is_empty());
1482    }
1483
1484    #[test]
1485    fn describe_effective_patches_returns_empty() {
1486        let svc = make_service();
1487        let req = make_request(
1488            "DescribeEffectivePatchesForPatchBaseline",
1489            json!({ "BaselineId": "pb-12345678901234567" }),
1490        );
1491        let resp = svc
1492            .describe_effective_patches_for_patch_baseline(&req)
1493            .unwrap();
1494        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1495        assert!(body["EffectivePatches"].as_array().unwrap().is_empty());
1496    }
1497
1498    #[test]
1499    fn get_ops_summary_returns_empty() {
1500        let svc = make_service();
1501        let req = make_request("GetOpsSummary", json!({}));
1502        let resp = svc.get_ops_summary(&req).unwrap();
1503        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1504        assert!(body["Entities"].as_array().unwrap().is_empty());
1505    }
1506
1507    // ── OpsItem Related Items ─────────────────────────────────────
1508
1509    #[test]
1510    fn associate_and_disassociate_ops_item_related_item() {
1511        let svc = make_service();
1512        // Create an ops item first
1513        let req = make_request(
1514            "CreateOpsItem",
1515            json!({ "Title": "Test", "Source": "test", "Description": "test desc" }),
1516        );
1517        let resp = svc.create_ops_item(&req).unwrap();
1518        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1519        let ops_item_id = body["OpsItemId"].as_str().unwrap().to_string();
1520
1521        // Associate
1522        let req = make_request(
1523            "AssociateOpsItemRelatedItem",
1524            json!({
1525                "OpsItemId": ops_item_id,
1526                "AssociationType": "IsParentOf",
1527                "ResourceType": "AWS::SSMIncidents::IncidentRecord",
1528                "ResourceUri": "arn:aws:ssm-incidents::123456789012:incident-record/test"
1529            }),
1530        );
1531        let resp = svc.associate_ops_item_related_item(&req).unwrap();
1532        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1533        let assoc_id = body["AssociationId"].as_str().unwrap().to_string();
1534
1535        // List
1536        let req = make_request(
1537            "ListOpsItemRelatedItems",
1538            json!({ "OpsItemId": ops_item_id }),
1539        );
1540        let resp = svc.list_ops_item_related_items(&req).unwrap();
1541        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1542        assert_eq!(body["Summaries"].as_array().unwrap().len(), 1);
1543
1544        // Disassociate
1545        let req = make_request(
1546            "DisassociateOpsItemRelatedItem",
1547            json!({ "OpsItemId": ops_item_id, "AssociationId": assoc_id }),
1548        );
1549        svc.disassociate_ops_item_related_item(&req).unwrap();
1550    }
1551
1552    #[test]
1553    fn list_ops_item_events_returns_empty() {
1554        let svc = make_service();
1555        let req = make_request("ListOpsItemEvents", json!({}));
1556        let resp = svc.list_ops_item_events(&req).unwrap();
1557        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1558        assert!(body["Summaries"].as_array().unwrap().is_empty());
1559    }
1560
1561    // ── OpsMetadata ───────────────────────────────────────────────
1562
1563    #[test]
1564    fn ops_metadata_lifecycle() {
1565        let svc = make_service();
1566
1567        // Create
1568        let req = make_request(
1569            "CreateOpsMetadata",
1570            json!({
1571                "ResourceId": "test-resource",
1572                "Metadata": { "key1": { "Value": "val1" } }
1573            }),
1574        );
1575        let resp = svc.create_ops_metadata(&req).unwrap();
1576        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1577        let arn = body["OpsMetadataArn"].as_str().unwrap().to_string();
1578
1579        // Get
1580        let req = make_request("GetOpsMetadata", json!({ "OpsMetadataArn": arn }));
1581        let resp = svc.get_ops_metadata(&req).unwrap();
1582        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1583        assert_eq!(body["ResourceId"].as_str().unwrap(), "test-resource");
1584
1585        // Update
1586        let req = make_request(
1587            "UpdateOpsMetadata",
1588            json!({
1589                "OpsMetadataArn": arn,
1590                "MetadataToUpdate": { "key2": { "Value": "val2" } }
1591            }),
1592        );
1593        svc.update_ops_metadata(&req).unwrap();
1594
1595        // List
1596        let req = make_request("ListOpsMetadata", json!({}));
1597        let resp = svc.list_ops_metadata(&req).unwrap();
1598        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1599        assert_eq!(body["OpsMetadataList"].as_array().unwrap().len(), 1);
1600
1601        // Delete
1602        let req = make_request("DeleteOpsMetadata", json!({ "OpsMetadataArn": arn }));
1603        svc.delete_ops_metadata(&req).unwrap();
1604    }
1605
1606    // ── Automation ────────────────────────────────────────────────
1607
1608    #[test]
1609    fn automation_execution_lifecycle() {
1610        let svc = make_service();
1611
1612        // Start
1613        let req = make_request(
1614            "StartAutomationExecution",
1615            json!({ "DocumentName": "AWS-RunShellScript" }),
1616        );
1617        let resp = svc.start_automation_execution(&req).unwrap();
1618        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1619        let exec_id = body["AutomationExecutionId"].as_str().unwrap().to_string();
1620
1621        // Get
1622        let req = make_request(
1623            "GetAutomationExecution",
1624            json!({ "AutomationExecutionId": exec_id }),
1625        );
1626        let resp = svc.get_automation_execution(&req).unwrap();
1627        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1628        assert_eq!(
1629            body["AutomationExecution"]["AutomationExecutionStatus"]
1630                .as_str()
1631                .unwrap(),
1632            "InProgress"
1633        );
1634
1635        // Describe
1636        let req = make_request("DescribeAutomationExecutions", json!({}));
1637        let resp = svc.describe_automation_executions(&req).unwrap();
1638        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1639        assert_eq!(
1640            body["AutomationExecutionMetadataList"]
1641                .as_array()
1642                .unwrap()
1643                .len(),
1644            1
1645        );
1646
1647        // DescribeSteps
1648        let req = make_request(
1649            "DescribeAutomationStepExecutions",
1650            json!({ "AutomationExecutionId": exec_id }),
1651        );
1652        let resp = svc.describe_automation_step_executions(&req).unwrap();
1653        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1654        assert!(body["StepExecutions"].as_array().unwrap().is_empty());
1655
1656        // Signal
1657        let req = make_request(
1658            "SendAutomationSignal",
1659            json!({ "AutomationExecutionId": exec_id, "SignalType": "Approve" }),
1660        );
1661        svc.send_automation_signal(&req).unwrap();
1662
1663        // Stop
1664        let req = make_request(
1665            "StopAutomationExecution",
1666            json!({ "AutomationExecutionId": exec_id }),
1667        );
1668        svc.stop_automation_execution(&req).unwrap();
1669    }
1670
1671    #[test]
1672    fn start_change_request_execution_works() {
1673        let svc = make_service();
1674        let req = make_request(
1675            "StartChangeRequestExecution",
1676            json!({
1677                "DocumentName": "AWS-ChangeManager",
1678                "Runbooks": [{ "DocumentName": "AWS-RunShellScript" }]
1679            }),
1680        );
1681        let resp = svc.start_change_request_execution(&req).unwrap();
1682        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1683        assert!(body["AutomationExecutionId"].as_str().is_some());
1684    }
1685
1686    #[test]
1687    fn execution_preview_lifecycle() {
1688        let svc = make_service();
1689
1690        let req = make_request(
1691            "StartExecutionPreview",
1692            json!({ "DocumentName": "AWS-RunShellScript" }),
1693        );
1694        let resp = svc.start_execution_preview(&req).unwrap();
1695        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1696        let preview_id = body["ExecutionPreviewId"].as_str().unwrap().to_string();
1697
1698        let req = make_request(
1699            "GetExecutionPreview",
1700            json!({ "ExecutionPreviewId": preview_id }),
1701        );
1702        let resp = svc.get_execution_preview(&req).unwrap();
1703        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1704        assert_eq!(body["Status"].as_str().unwrap(), "Success");
1705    }
1706
1707    // ── Sessions ──────────────────────────────────────────────────
1708
1709    #[test]
1710    fn session_lifecycle() {
1711        let svc = make_service();
1712
1713        // Start
1714        let req = make_request("StartSession", json!({ "Target": "i-001" }));
1715        let resp = svc.start_session(&req).unwrap();
1716        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1717        let session_id = body["SessionId"].as_str().unwrap().to_string();
1718        assert!(body["TokenValue"].as_str().is_some());
1719
1720        // Resume
1721        let req = make_request("ResumeSession", json!({ "SessionId": session_id }));
1722        let resp = svc.resume_session(&req).unwrap();
1723        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1724        assert_eq!(body["SessionId"].as_str().unwrap(), session_id);
1725
1726        // Describe (Active)
1727        let req = make_request("DescribeSessions", json!({ "State": "Active" }));
1728        let resp = svc.describe_sessions(&req).unwrap();
1729        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1730        assert_eq!(body["Sessions"].as_array().unwrap().len(), 1);
1731
1732        // Terminate
1733        let req = make_request("TerminateSession", json!({ "SessionId": session_id }));
1734        svc.terminate_session(&req).unwrap();
1735
1736        // Describe (History)
1737        let req = make_request("DescribeSessions", json!({ "State": "History" }));
1738        let resp = svc.describe_sessions(&req).unwrap();
1739        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1740        assert_eq!(body["Sessions"].as_array().unwrap().len(), 1);
1741    }
1742
1743    #[test]
1744    fn start_access_request_and_get_token() {
1745        let svc = make_service();
1746
1747        let req = make_request(
1748            "StartAccessRequest",
1749            json!({ "Reason": "test", "Targets": [{"Key": "InstanceIds", "Values": ["i-001"]}] }),
1750        );
1751        let resp = svc.start_access_request(&req).unwrap();
1752        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1753        let ar_id = body["AccessRequestId"].as_str().unwrap().to_string();
1754
1755        let req = make_request("GetAccessToken", json!({ "AccessRequestId": ar_id }));
1756        let resp = svc.get_access_token(&req).unwrap();
1757        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1758        assert!(body["Credentials"]["AccessKeyId"].as_str().is_some());
1759        assert_eq!(body["AccessRequestStatus"].as_str(), Some("Approved"));
1760    }
1761
1762    // ── Managed Instances ─────────────────────────────────────────
1763
1764    #[test]
1765    fn activation_lifecycle() {
1766        let svc = make_service();
1767
1768        // Create
1769        let req = make_request(
1770            "CreateActivation",
1771            json!({ "IamRole": "SSMServiceRole", "Description": "test" }),
1772        );
1773        let resp = svc.create_activation(&req).unwrap();
1774        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1775        let activation_id = body["ActivationId"].as_str().unwrap().to_string();
1776        assert!(body["ActivationCode"].as_str().is_some());
1777
1778        // Describe
1779        let req = make_request("DescribeActivations", json!({}));
1780        let resp = svc.describe_activations(&req).unwrap();
1781        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1782        assert_eq!(body["ActivationList"].as_array().unwrap().len(), 1);
1783
1784        // Delete
1785        let req = make_request("DeleteActivation", json!({ "ActivationId": activation_id }));
1786        svc.delete_activation(&req).unwrap();
1787    }
1788
1789    #[test]
1790    fn deregister_managed_instance_no_error() {
1791        let svc = make_service();
1792        let req = make_request(
1793            "DeregisterManagedInstance",
1794            json!({ "InstanceId": "mi-01234567890123456" }),
1795        );
1796        svc.deregister_managed_instance(&req).unwrap();
1797    }
1798
1799    #[test]
1800    fn describe_instance_information_empty() {
1801        let svc = make_service();
1802        let req = make_request("DescribeInstanceInformation", json!({}));
1803        let resp = svc.describe_instance_information(&req).unwrap();
1804        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1805        assert!(body["InstanceInformationList"]
1806            .as_array()
1807            .unwrap()
1808            .is_empty());
1809    }
1810
1811    #[test]
1812    fn describe_instance_properties_empty() {
1813        let svc = make_service();
1814        let req = make_request("DescribeInstanceProperties", json!({}));
1815        let resp = svc.describe_instance_properties(&req).unwrap();
1816        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1817        assert!(body["InstanceProperties"].as_array().unwrap().is_empty());
1818    }
1819
1820    // ── Other ─────────────────────────────────────────────────────
1821
1822    #[test]
1823    fn list_nodes_returns_empty() {
1824        let svc = make_service();
1825        let req = make_request("ListNodes", json!({}));
1826        let resp = svc.list_nodes(&req).unwrap();
1827        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1828        assert!(body["Nodes"].as_array().unwrap().is_empty());
1829    }
1830
1831    #[test]
1832    fn list_nodes_summary_returns_empty() {
1833        let svc = make_service();
1834        let req = make_request(
1835            "ListNodesSummary",
1836            json!({ "Aggregators": [{"AggregatorType": "Count"}] }),
1837        );
1838        let resp = svc.list_nodes_summary(&req).unwrap();
1839        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1840        assert!(body["Summary"].as_array().unwrap().is_empty());
1841    }
1842
1843    #[test]
1844    fn describe_effective_instance_associations_empty() {
1845        let svc = make_service();
1846        let req = make_request(
1847            "DescribeEffectiveInstanceAssociations",
1848            json!({ "InstanceId": "i-001" }),
1849        );
1850        let resp = svc.describe_effective_instance_associations(&req).unwrap();
1851        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1852        assert!(body["Associations"].as_array().unwrap().is_empty());
1853    }
1854
1855    #[test]
1856    fn describe_instance_associations_status_empty() {
1857        let svc = make_service();
1858        let req = make_request(
1859            "DescribeInstanceAssociationsStatus",
1860            json!({ "InstanceId": "i-001" }),
1861        );
1862        let resp = svc.describe_instance_associations_status(&req).unwrap();
1863        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1864        assert!(body["InstanceAssociationStatusInfos"]
1865            .as_array()
1866            .unwrap()
1867            .is_empty());
1868    }
1869
1870    // ── Parameter Labels ─────────────────────────────────────────
1871
1872    fn put_param(svc: &SsmService, name: &str, value: &str) -> i64 {
1873        let req = make_request(
1874            "PutParameter",
1875            json!({
1876                "Name": name,
1877                "Value": value,
1878                "Type": "String",
1879                "Overwrite": true,
1880            }),
1881        );
1882        let resp = svc.put_parameter(&req).unwrap();
1883        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1884        body["Version"].as_i64().unwrap()
1885    }
1886
1887    #[test]
1888    fn label_and_unlabel_parameter_version() {
1889        let svc = make_service();
1890
1891        // Create parameter with two versions
1892        put_param(&svc, "/label/test", "v1");
1893        put_param(&svc, "/label/test", "v2");
1894
1895        // Label version 1
1896        let req = make_request(
1897            "LabelParameterVersion",
1898            json!({
1899                "Name": "/label/test",
1900                "ParameterVersion": 1,
1901                "Labels": ["prod", "stable"],
1902            }),
1903        );
1904        let resp = svc.label_parameter_version(&req).unwrap();
1905        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1906        assert!(body["InvalidLabels"].as_array().unwrap().is_empty());
1907        assert_eq!(body["ParameterVersion"].as_i64().unwrap(), 1);
1908
1909        // Get parameter history — version 1 should have labels
1910        let req = make_request("GetParameterHistory", json!({ "Name": "/label/test" }));
1911        let resp = svc.get_parameter_history(&req).unwrap();
1912        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1913        let params = body["Parameters"].as_array().unwrap();
1914        let v1 = params
1915            .iter()
1916            .find(|p| p["Version"].as_i64() == Some(1))
1917            .unwrap();
1918        let labels = v1["Labels"].as_array().unwrap();
1919        assert!(labels.iter().any(|l| l.as_str() == Some("prod")));
1920        assert!(labels.iter().any(|l| l.as_str() == Some("stable")));
1921
1922        // Unlabel version 1
1923        let req = make_request(
1924            "UnlabelParameterVersion",
1925            json!({
1926                "Name": "/label/test",
1927                "ParameterVersion": 1,
1928                "Labels": ["prod"],
1929            }),
1930        );
1931        let resp = svc.unlabel_parameter_version(&req).unwrap();
1932        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1933        assert!(body["InvalidLabels"].as_array().unwrap().is_empty());
1934        let removed = body["RemovedLabels"].as_array().unwrap();
1935        assert_eq!(removed.len(), 1);
1936        assert_eq!(removed[0].as_str().unwrap(), "prod");
1937    }
1938
1939    #[test]
1940    fn label_parameter_version_defaults_to_latest() {
1941        let svc = make_service();
1942        put_param(&svc, "/label/default", "v1");
1943        put_param(&svc, "/label/default", "v2");
1944
1945        // Label without specifying version — should target latest (2)
1946        let req = make_request(
1947            "LabelParameterVersion",
1948            json!({
1949                "Name": "/label/default",
1950                "Labels": ["latest-label"],
1951            }),
1952        );
1953        let resp = svc.label_parameter_version(&req).unwrap();
1954        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1955        assert_eq!(body["ParameterVersion"].as_i64().unwrap(), 2);
1956    }
1957
1958    #[test]
1959    fn label_parameter_version_invalid_labels() {
1960        let svc = make_service();
1961        put_param(&svc, "/label/invalid", "v1");
1962
1963        // Labels starting with aws/ssm or containing / are invalid
1964        let req = make_request(
1965            "LabelParameterVersion",
1966            json!({
1967                "Name": "/label/invalid",
1968                "Labels": ["aws-reserved", "valid-label"],
1969            }),
1970        );
1971        let resp = svc.label_parameter_version(&req).unwrap();
1972        let body: Value = serde_json::from_slice(&resp.body).unwrap();
1973        let invalid = body["InvalidLabels"].as_array().unwrap();
1974        assert_eq!(invalid.len(), 1);
1975        assert_eq!(invalid[0].as_str().unwrap(), "aws-reserved");
1976    }
1977
1978    #[test]
1979    fn label_parameter_version_not_found() {
1980        let svc = make_service();
1981        put_param(&svc, "/label/notfound", "v1");
1982
1983        let req = make_request(
1984            "LabelParameterVersion",
1985            json!({
1986                "Name": "/label/notfound",
1987                "ParameterVersion": 999,
1988                "Labels": ["test"],
1989            }),
1990        );
1991        let result = svc.label_parameter_version(&req);
1992        assert!(result.is_err());
1993    }
1994
1995    #[test]
1996    fn unlabel_parameter_version_returns_invalid_for_missing_labels() {
1997        let svc = make_service();
1998        put_param(&svc, "/label/missing", "v1");
1999
2000        let req = make_request(
2001            "UnlabelParameterVersion",
2002            json!({
2003                "Name": "/label/missing",
2004                "ParameterVersion": 1,
2005                "Labels": ["nonexistent"],
2006            }),
2007        );
2008        let resp = svc.unlabel_parameter_version(&req).unwrap();
2009        let body: Value = serde_json::from_slice(&resp.body).unwrap();
2010        let invalid = body["InvalidLabels"].as_array().unwrap();
2011        assert_eq!(invalid.len(), 1);
2012        assert_eq!(invalid[0].as_str().unwrap(), "nonexistent");
2013    }
2014
2015    #[test]
2016    fn unlabel_parameter_version_requires_parameter_version() {
2017        let svc = make_service();
2018        put_param(&svc, "/label/reqver", "v1");
2019
2020        // Omit ParameterVersion — should fail with ValidationException
2021        let req = make_request(
2022            "UnlabelParameterVersion",
2023            json!({
2024                "Name": "/label/reqver",
2025                "Labels": ["some-label"],
2026            }),
2027        );
2028        let result = svc.unlabel_parameter_version(&req);
2029        assert!(result.is_err());
2030    }
2031
2032    // ── Document Operations ──────────────────────────────────────
2033
2034    fn create_doc(svc: &SsmService, name: &str) {
2035        let req = make_request(
2036            "CreateDocument",
2037            json!({
2038                "Name": name,
2039                "Content": r#"{"schemaVersion":"2.2","mainSteps":[]}"#,
2040                "DocumentType": "Command",
2041            }),
2042        );
2043        svc.create_document(&req).unwrap();
2044    }
2045
2046    #[test]
2047    fn update_document_and_default_version() {
2048        let svc = make_service();
2049        create_doc(&svc, "TestDoc");
2050
2051        // Update document (creates version 2)
2052        let req = make_request(
2053            "UpdateDocument",
2054            json!({
2055                "Name": "TestDoc",
2056                "Content": r#"{"schemaVersion":"2.2","description":"v2","mainSteps":[]}"#,
2057                "VersionName": "release-2",
2058            }),
2059        );
2060        let resp = svc.update_document(&req).unwrap();
2061        let body: Value = serde_json::from_slice(&resp.body).unwrap();
2062        let desc = &body["DocumentDescription"];
2063        assert_eq!(desc["DocumentVersion"].as_str().unwrap(), "2");
2064        assert_eq!(desc["VersionName"].as_str().unwrap(), "release-2");
2065
2066        // List document versions
2067        let req = make_request("ListDocumentVersions", json!({ "Name": "TestDoc" }));
2068        let resp = svc.list_document_versions(&req).unwrap();
2069        let body: Value = serde_json::from_slice(&resp.body).unwrap();
2070        assert_eq!(body["DocumentVersions"].as_array().unwrap().len(), 2);
2071
2072        // Update default version to 2
2073        let req = make_request(
2074            "UpdateDocumentDefaultVersion",
2075            json!({
2076                "Name": "TestDoc",
2077                "DocumentVersion": "2",
2078            }),
2079        );
2080        let resp = svc.update_document_default_version(&req).unwrap();
2081        let body: Value = serde_json::from_slice(&resp.body).unwrap();
2082        assert_eq!(body["Description"]["DefaultVersion"].as_str().unwrap(), "2");
2083
2084        // Verify describe_document now shows version 2 as default
2085        let req = make_request("DescribeDocument", json!({ "Name": "TestDoc" }));
2086        let resp = svc.describe_document(&req).unwrap();
2087        let body: Value = serde_json::from_slice(&resp.body).unwrap();
2088        assert_eq!(body["Document"]["DefaultVersion"].as_str().unwrap(), "2");
2089    }
2090
2091    #[test]
2092    fn update_document_duplicate_content_fails() {
2093        let svc = make_service();
2094        create_doc(&svc, "DupDoc");
2095
2096        // Try to update with same content
2097        let req = make_request(
2098            "UpdateDocument",
2099            json!({
2100                "Name": "DupDoc",
2101                "Content": r#"{"schemaVersion":"2.2","mainSteps":[]}"#,
2102            }),
2103        );
2104        let result = svc.update_document(&req);
2105        assert!(result.is_err());
2106    }
2107
2108    #[test]
2109    fn document_permissions_modify_and_describe() {
2110        let svc = make_service();
2111        create_doc(&svc, "PermDoc");
2112
2113        // Add permission
2114        let req = make_request(
2115            "ModifyDocumentPermission",
2116            json!({
2117                "Name": "PermDoc",
2118                "PermissionType": "Share",
2119                "AccountIdsToAdd": ["111111111111", "222222222222"],
2120            }),
2121        );
2122        svc.modify_document_permission(&req).unwrap();
2123
2124        // Describe permission
2125        let req = make_request(
2126            "DescribeDocumentPermission",
2127            json!({
2128                "Name": "PermDoc",
2129                "PermissionType": "Share",
2130            }),
2131        );
2132        let resp = svc.describe_document_permission(&req).unwrap();
2133        let body: Value = serde_json::from_slice(&resp.body).unwrap();
2134        let ids = body["AccountIds"].as_array().unwrap();
2135        assert_eq!(ids.len(), 2);
2136
2137        // Remove one permission
2138        let req = make_request(
2139            "ModifyDocumentPermission",
2140            json!({
2141                "Name": "PermDoc",
2142                "PermissionType": "Share",
2143                "AccountIdsToRemove": ["111111111111"],
2144            }),
2145        );
2146        svc.modify_document_permission(&req).unwrap();
2147
2148        // Verify only one remains
2149        let req = make_request(
2150            "DescribeDocumentPermission",
2151            json!({
2152                "Name": "PermDoc",
2153                "PermissionType": "Share",
2154            }),
2155        );
2156        let resp = svc.describe_document_permission(&req).unwrap();
2157        let body: Value = serde_json::from_slice(&resp.body).unwrap();
2158        let ids = body["AccountIds"].as_array().unwrap();
2159        assert_eq!(ids.len(), 1);
2160        assert_eq!(ids[0].as_str().unwrap(), "222222222222");
2161    }
2162
2163    #[test]
2164    fn modify_document_permission_invalid_type() {
2165        let svc = make_service();
2166        create_doc(&svc, "PermDoc2");
2167
2168        let req = make_request(
2169            "ModifyDocumentPermission",
2170            json!({
2171                "Name": "PermDoc2",
2172                "PermissionType": "Invalid",
2173                "AccountIdsToAdd": ["111111111111"],
2174            }),
2175        );
2176        let result = svc.modify_document_permission(&req);
2177        assert!(result.is_err());
2178    }
2179
2180    // ── Maintenance Window Targets and Tasks ─────────────────────
2181
2182    #[test]
2183    fn describe_maintenance_window_targets_and_tasks() {
2184        let svc = make_service();
2185        let (window_id, _target_id, _task_id) = create_mw_with_target_and_task(&svc);
2186
2187        // Describe targets
2188        let req = make_request(
2189            "DescribeMaintenanceWindowTargets",
2190            json!({ "WindowId": window_id }),
2191        );
2192        let resp = svc.describe_maintenance_window_targets(&req).unwrap();
2193        let body: Value = serde_json::from_slice(&resp.body).unwrap();
2194        let targets = body["Targets"].as_array().unwrap();
2195        assert_eq!(targets.len(), 1);
2196        assert_eq!(targets[0]["Name"].as_str().unwrap(), "test-target");
2197
2198        // Describe tasks
2199        let req = make_request(
2200            "DescribeMaintenanceWindowTasks",
2201            json!({ "WindowId": window_id }),
2202        );
2203        let resp = svc.describe_maintenance_window_tasks(&req).unwrap();
2204        let body: Value = serde_json::from_slice(&resp.body).unwrap();
2205        let tasks = body["Tasks"].as_array().unwrap();
2206        assert_eq!(tasks.len(), 1);
2207        assert_eq!(tasks[0]["TaskArn"].as_str().unwrap(), "AWS-RunShellScript");
2208        assert_eq!(tasks[0]["Name"].as_str().unwrap(), "test-task");
2209    }
2210
2211    // ── Patch Baselines ──────────────────────────────────────────
2212
2213    fn create_baseline(svc: &SsmService, name: &str) -> String {
2214        let req = make_request(
2215            "CreatePatchBaseline",
2216            json!({
2217                "Name": name,
2218                "OperatingSystem": "AMAZON_LINUX_2",
2219                "Description": "test baseline",
2220            }),
2221        );
2222        let resp = svc.create_patch_baseline(&req).unwrap();
2223        let body: Value = serde_json::from_slice(&resp.body).unwrap();
2224        body["BaselineId"].as_str().unwrap().to_string()
2225    }
2226
2227    #[test]
2228    fn patch_baseline_get_and_delete() {
2229        let svc = make_service();
2230        let baseline_id = create_baseline(&svc, "get-del-baseline");
2231
2232        // Get
2233        let req = make_request("GetPatchBaseline", json!({ "BaselineId": baseline_id }));
2234        let resp = svc.get_patch_baseline(&req).unwrap();
2235        let body: Value = serde_json::from_slice(&resp.body).unwrap();
2236        assert_eq!(body["Name"].as_str().unwrap(), "get-del-baseline");
2237        assert_eq!(body["OperatingSystem"].as_str().unwrap(), "AMAZON_LINUX_2");
2238        assert_eq!(body["Description"].as_str().unwrap(), "test baseline");
2239
2240        // Delete
2241        let req = make_request("DeletePatchBaseline", json!({ "BaselineId": baseline_id }));
2242        svc.delete_patch_baseline(&req).unwrap();
2243
2244        // Get should fail
2245        let req = make_request("GetPatchBaseline", json!({ "BaselineId": baseline_id }));
2246        let result = svc.get_patch_baseline(&req);
2247        assert!(result.is_err());
2248    }
2249
2250    #[test]
2251    fn describe_patch_baselines_with_filter() {
2252        let svc = make_service();
2253        create_baseline(&svc, "alpha-baseline");
2254        create_baseline(&svc, "beta-baseline");
2255
2256        // Filter by NAME_PREFIX
2257        let req = make_request(
2258            "DescribePatchBaselines",
2259            json!({
2260                "Filters": [{"Key": "NAME_PREFIX", "Values": ["alpha"]}],
2261            }),
2262        );
2263        let resp = svc.describe_patch_baselines(&req).unwrap();
2264        let body: Value = serde_json::from_slice(&resp.body).unwrap();
2265        let baselines = body["BaselineIdentities"].as_array().unwrap();
2266        assert_eq!(baselines.len(), 1);
2267        assert_eq!(
2268            baselines[0]["BaselineName"].as_str().unwrap(),
2269            "alpha-baseline"
2270        );
2271    }
2272
2273    #[test]
2274    fn patch_group_register_and_deregister() {
2275        let svc = make_service();
2276        let baseline_id = create_baseline(&svc, "pg-baseline");
2277
2278        // Register patch group
2279        let req = make_request(
2280            "RegisterPatchBaselineForPatchGroup",
2281            json!({
2282                "BaselineId": baseline_id,
2283                "PatchGroup": "production",
2284            }),
2285        );
2286        let resp = svc.register_patch_baseline_for_patch_group(&req).unwrap();
2287        let body: Value = serde_json::from_slice(&resp.body).unwrap();
2288        assert_eq!(body["PatchGroup"].as_str().unwrap(), "production");
2289
2290        // Get patch baseline for group
2291        let req = make_request(
2292            "GetPatchBaselineForPatchGroup",
2293            json!({
2294                "PatchGroup": "production",
2295                "OperatingSystem": "AMAZON_LINUX_2",
2296            }),
2297        );
2298        let resp = svc.get_patch_baseline_for_patch_group(&req).unwrap();
2299        let body: Value = serde_json::from_slice(&resp.body).unwrap();
2300        assert_eq!(body["BaselineId"].as_str().unwrap(), baseline_id);
2301
2302        // Describe patch groups
2303        let req = make_request("DescribePatchGroups", json!({}));
2304        let resp = svc.describe_patch_groups(&req).unwrap();
2305        let body: Value = serde_json::from_slice(&resp.body).unwrap();
2306        let mappings = body["Mappings"].as_array().unwrap();
2307        assert_eq!(mappings.len(), 1);
2308        assert_eq!(mappings[0]["PatchGroup"].as_str().unwrap(), "production");
2309
2310        // Deregister
2311        let req = make_request(
2312            "DeregisterPatchBaselineForPatchGroup",
2313            json!({
2314                "BaselineId": baseline_id,
2315                "PatchGroup": "production",
2316            }),
2317        );
2318        svc.deregister_patch_baseline_for_patch_group(&req).unwrap();
2319
2320        // Verify removed
2321        let req = make_request("DescribePatchGroups", json!({}));
2322        let resp = svc.describe_patch_groups(&req).unwrap();
2323        let body: Value = serde_json::from_slice(&resp.body).unwrap();
2324        assert!(body["Mappings"].as_array().unwrap().is_empty());
2325    }
2326
2327    #[test]
2328    fn delete_patch_baseline_removes_patch_groups() {
2329        let svc = make_service();
2330        let baseline_id = create_baseline(&svc, "del-pg-baseline");
2331
2332        // Register a patch group
2333        let req = make_request(
2334            "RegisterPatchBaselineForPatchGroup",
2335            json!({
2336                "BaselineId": baseline_id,
2337                "PatchGroup": "staging",
2338            }),
2339        );
2340        svc.register_patch_baseline_for_patch_group(&req).unwrap();
2341
2342        // Delete baseline
2343        let req = make_request("DeletePatchBaseline", json!({ "BaselineId": baseline_id }));
2344        svc.delete_patch_baseline(&req).unwrap();
2345
2346        // Patch groups should be cleaned up
2347        let req = make_request("DescribePatchGroups", json!({}));
2348        let resp = svc.describe_patch_groups(&req).unwrap();
2349        let body: Value = serde_json::from_slice(&resp.body).unwrap();
2350        assert!(body["Mappings"].as_array().unwrap().is_empty());
2351    }
2352
2353    // ── Command Details ──────────────────────────────────────────
2354
2355    #[test]
2356    fn get_command_invocation_success() {
2357        let svc = make_service();
2358        let cmd_id = send_command(&svc, "AWS-RunShellScript");
2359
2360        let req = make_request(
2361            "GetCommandInvocation",
2362            json!({
2363                "CommandId": cmd_id,
2364                "InstanceId": "i-1234567890abcdef0",
2365            }),
2366        );
2367        let resp = svc.get_command_invocation(&req).unwrap();
2368        let body: Value = serde_json::from_slice(&resp.body).unwrap();
2369        assert_eq!(body["CommandId"].as_str().unwrap(), cmd_id);
2370        assert_eq!(body["InstanceId"].as_str().unwrap(), "i-1234567890abcdef0");
2371        assert_eq!(body["Status"].as_str().unwrap(), "Success");
2372    }
2373
2374    #[test]
2375    fn get_command_invocation_wrong_instance_fails() {
2376        let svc = make_service();
2377        let cmd_id = send_command(&svc, "AWS-RunShellScript");
2378
2379        let req = make_request(
2380            "GetCommandInvocation",
2381            json!({
2382                "CommandId": cmd_id,
2383                "InstanceId": "i-0000000000000000f",
2384            }),
2385        );
2386        let result = svc.get_command_invocation(&req);
2387        assert!(result.is_err());
2388    }
2389
2390    #[test]
2391    fn list_command_invocations() {
2392        let svc = make_service();
2393        let cmd_id = send_command(&svc, "AWS-RunShellScript");
2394
2395        // List all invocations
2396        let req = make_request("ListCommandInvocations", json!({}));
2397        let resp = svc.list_command_invocations(&req).unwrap();
2398        let body: Value = serde_json::from_slice(&resp.body).unwrap();
2399        let invocations = body["CommandInvocations"].as_array().unwrap();
2400        assert!(!invocations.is_empty());
2401        assert_eq!(invocations[0]["CommandId"].as_str().unwrap(), cmd_id);
2402        assert_eq!(
2403            invocations[0]["InstanceId"].as_str().unwrap(),
2404            "i-1234567890abcdef0"
2405        );
2406
2407        // Filter by CommandId
2408        let req = make_request("ListCommandInvocations", json!({ "CommandId": cmd_id }));
2409        let resp = svc.list_command_invocations(&req).unwrap();
2410        let body: Value = serde_json::from_slice(&resp.body).unwrap();
2411        assert_eq!(body["CommandInvocations"].as_array().unwrap().len(), 1);
2412    }
2413}