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