Skip to main content

aws_lite_rs/api/
ec2.rs

1//! Amazon EC2 API client.
2//!
3//! Thin wrapper over generated ops. All URL construction and HTTP methods
4//! are in `ops::ec2::Ec2Ops`. This layer adds:
5//! - Ergonomic method signatures
6
7use crate::{
8    AwsHttpClient, Result,
9    ops::ec2::Ec2Ops,
10    types::ec2::{
11        AssociateIamInstanceProfileRequest, AssociateIamInstanceProfileResponse,
12        AuthorizeSecurityGroupIngressRequest, AuthorizeSecurityGroupIngressResponse,
13        CreateFlowLogsRequest, CreateFlowLogsResponse, CreateSnapshotRequest, CreateTagsRequest,
14        DeleteNatGatewayRequest, DeleteNatGatewayResponse, DeleteSecurityGroupRequest,
15        DeleteSecurityGroupResponse, DeleteSnapshotRequest, DeleteVolumeRequest,
16        DeleteVpcEndpointsRequest, DeleteVpcEndpointsResponse, DeregisterImageRequest,
17        DeregisterImageResponse, DescribeAddressesRequest, DescribeAddressesResponse,
18        DescribeFlowLogsRequest, DescribeFlowLogsResponse, DescribeImagesRequest,
19        DescribeImagesResponse, DescribeInstancesRequest, DescribeInstancesResponse,
20        DescribeLaunchTemplateVersionsRequest, DescribeLaunchTemplateVersionsResponse,
21        DescribeLaunchTemplatesRequest, DescribeLaunchTemplatesResponse,
22        DescribeNatGatewaysRequest, DescribeNatGatewaysResponse, DescribeNetworkAclsRequest,
23        DescribeNetworkAclsResponse, DescribeRouteTablesRequest, DescribeRouteTablesResponse,
24        DescribeSecurityGroupsRequest, DescribeSecurityGroupsResponse,
25        DescribeSnapshotAttributeRequest, DescribeSnapshotAttributeResponse,
26        DescribeSnapshotsRequest, DescribeSnapshotsResponse, DescribeVolumesRequest,
27        DescribeVolumesResponse, DescribeVpcEndpointsRequest, DescribeVpcEndpointsResponse,
28        DescribeVpcPeeringConnectionsRequest, DescribeVpcPeeringConnectionsResponse,
29        DescribeVpcsRequest, DescribeVpcsResponse, DetachVolumeRequest,
30        EnableEbsEncryptionByDefaultRequest, EnableEbsEncryptionByDefaultResponse,
31        EnableImageBlockPublicAccessRequest, EnableImageBlockPublicAccessResponse,
32        EnableSnapshotBlockPublicAccessRequest, EnableSnapshotBlockPublicAccessResponse,
33        GetEbsEncryptionByDefaultRequest, GetEbsEncryptionByDefaultResponse,
34        ModifyImageAttributeRequest, ModifyInstanceAttributeRequest,
35        ModifyInstanceMetadataOptionsRequest, ModifyInstanceMetadataOptionsResponse,
36        ModifySnapshotAttributeRequest, ModifyVolumeRequest, ModifyVolumeResponse,
37        MonitorInstancesRequest, MonitorInstancesResponse, ReleaseAddressRequest,
38        RevokeSecurityGroupEgressRequest, RevokeSecurityGroupEgressResponse,
39        RevokeSecurityGroupIngressRequest, RevokeSecurityGroupIngressResponse, Snapshot,
40        StartInstancesRequest, StartInstancesResponse, StopInstancesRequest, StopInstancesResponse,
41        TerminateInstancesRequest, TerminateInstancesResponse, VolumeAttachment,
42        VpcPeeringConnection,
43    },
44};
45
46/// Client for the Amazon EC2 API
47pub struct Ec2Client<'a> {
48    ops: Ec2Ops<'a>,
49}
50
51impl<'a> Ec2Client<'a> {
52    /// Create a new Amazon EC2 API client
53    pub(crate) fn new(client: &'a AwsHttpClient) -> Self {
54        Self {
55            ops: Ec2Ops::new(client),
56        }
57    }
58
59    /// Describes the specified instances or all instances.
60    pub async fn describe_instances(
61        &self,
62        body: &DescribeInstancesRequest,
63    ) -> Result<DescribeInstancesResponse> {
64        self.ops.describe_instances(body).await
65    }
66
67    /// Describes the specified EBS volumes or all EBS volumes.
68    pub async fn describe_volumes(
69        &self,
70        body: &DescribeVolumesRequest,
71    ) -> Result<DescribeVolumesResponse> {
72        self.ops.describe_volumes(body).await
73    }
74
75    /// Describes the specified EBS snapshots.
76    pub async fn describe_snapshots(
77        &self,
78        body: &DescribeSnapshotsRequest,
79    ) -> Result<DescribeSnapshotsResponse> {
80        self.ops.describe_snapshots(body).await
81    }
82
83    /// Describes the specified images (AMIs).
84    pub async fn describe_images(
85        &self,
86        body: &DescribeImagesRequest,
87    ) -> Result<DescribeImagesResponse> {
88        self.ops.describe_images(body).await
89    }
90
91    /// Describes the specified security groups.
92    pub async fn describe_security_groups(
93        &self,
94        body: &DescribeSecurityGroupsRequest,
95    ) -> Result<DescribeSecurityGroupsResponse> {
96        self.ops.describe_security_groups(body).await
97    }
98
99    /// Describes the specified Elastic IP addresses.
100    pub async fn describe_addresses(
101        &self,
102        body: &DescribeAddressesRequest,
103    ) -> Result<DescribeAddressesResponse> {
104        self.ops.describe_addresses(body).await
105    }
106
107    /// Describes the specified NAT gateways.
108    pub async fn describe_nat_gateways(
109        &self,
110        body: &DescribeNatGatewaysRequest,
111    ) -> Result<DescribeNatGatewaysResponse> {
112        self.ops.describe_nat_gateways(body).await
113    }
114
115    /// Describes the specified route tables.
116    pub async fn describe_route_tables(
117        &self,
118        body: &DescribeRouteTablesRequest,
119    ) -> Result<DescribeRouteTablesResponse> {
120        self.ops.describe_route_tables(body).await
121    }
122
123    /// Describes the specified network ACLs.
124    pub async fn describe_network_acls(
125        &self,
126        body: &DescribeNetworkAclsRequest,
127    ) -> Result<DescribeNetworkAclsResponse> {
128        self.ops.describe_network_acls(body).await
129    }
130
131    /// Describes the specified flow logs.
132    pub async fn describe_flow_logs(
133        &self,
134        body: &DescribeFlowLogsRequest,
135    ) -> Result<DescribeFlowLogsResponse> {
136        self.ops.describe_flow_logs(body).await
137    }
138
139    /// Describes the specified VPCs.
140    pub async fn describe_vpcs(&self, body: &DescribeVpcsRequest) -> Result<DescribeVpcsResponse> {
141        self.ops.describe_vpcs(body).await
142    }
143
144    /// Describes the specified VPC endpoints.
145    pub async fn describe_vpc_endpoints(
146        &self,
147        body: &DescribeVpcEndpointsRequest,
148    ) -> Result<DescribeVpcEndpointsResponse> {
149        self.ops.describe_vpc_endpoints(body).await
150    }
151
152    // ── VPC Peering ────────────────────────────────────────────────────────
153
154    /// Describes one or more VPC peering connections.
155    ///
156    /// Optionally filter by connection IDs. Passing an empty slice returns all peering
157    /// connections in the current account/region.
158    ///
159    /// CIS 6.6: used to enumerate peering connections and cross-reference with
160    /// `describe_route_tables` to verify that route tables don't enable direct access
161    /// between peered VPCs where it shouldn't exist.
162    pub async fn describe_vpc_peering_connections(
163        &self,
164        body: &DescribeVpcPeeringConnectionsRequest,
165    ) -> Result<DescribeVpcPeeringConnectionsResponse> {
166        self.ops.describe_vpc_peering_connections(body).await
167    }
168
169    /// Return all VPC peering connections as a flat `Vec`.
170    pub async fn list_vpc_peering_connections(&self) -> Result<Vec<VpcPeeringConnection>> {
171        let body = DescribeVpcPeeringConnectionsRequest {
172            ..Default::default()
173        };
174        let resp = self.ops.describe_vpc_peering_connections(&body).await?;
175        Ok(resp.vpc_peering_connections)
176    }
177
178    /// Describes the specified launch templates.
179    pub async fn describe_launch_templates(
180        &self,
181        body: &DescribeLaunchTemplatesRequest,
182    ) -> Result<DescribeLaunchTemplatesResponse> {
183        self.ops.describe_launch_templates(body).await
184    }
185
186    /// Describes the specified launch template versions.
187    pub async fn describe_launch_template_versions(
188        &self,
189        body: &DescribeLaunchTemplateVersionsRequest,
190    ) -> Result<DescribeLaunchTemplateVersionsResponse> {
191        self.ops.describe_launch_template_versions(body).await
192    }
193
194    /// Describes the specified attribute of the specified snapshot.
195    pub async fn describe_snapshot_attribute(
196        &self,
197        body: &DescribeSnapshotAttributeRequest,
198    ) -> Result<DescribeSnapshotAttributeResponse> {
199        self.ops.describe_snapshot_attribute(body).await
200    }
201
202    /// Describes whether EBS encryption by default is enabled for the account.
203    pub async fn get_ebs_encryption_by_default(
204        &self,
205        body: &GetEbsEncryptionByDefaultRequest,
206    ) -> Result<GetEbsEncryptionByDefaultResponse> {
207        self.ops.get_ebs_encryption_by_default(body).await
208    }
209
210    /// Shuts down the specified instances.
211    pub async fn terminate_instances(
212        &self,
213        body: &TerminateInstancesRequest,
214    ) -> Result<TerminateInstancesResponse> {
215        self.ops.terminate_instances(body).await
216    }
217
218    /// Stops the specified instances.
219    pub async fn stop_instances(
220        &self,
221        body: &StopInstancesRequest,
222    ) -> Result<StopInstancesResponse> {
223        self.ops.stop_instances(body).await
224    }
225
226    /// Starts the specified instances.
227    pub async fn start_instances(
228        &self,
229        body: &StartInstancesRequest,
230    ) -> Result<StartInstancesResponse> {
231        self.ops.start_instances(body).await
232    }
233
234    /// Modifies the specified attribute of the specified instance.
235    pub async fn modify_instance_attribute(
236        &self,
237        body: &ModifyInstanceAttributeRequest,
238    ) -> Result<()> {
239        self.ops.modify_instance_attribute(body).await
240    }
241
242    /// Modify the instance metadata parameters on a running or stopped instance.
243    pub async fn modify_instance_metadata_options(
244        &self,
245        body: &ModifyInstanceMetadataOptionsRequest,
246    ) -> Result<ModifyInstanceMetadataOptionsResponse> {
247        self.ops.modify_instance_metadata_options(body).await
248    }
249
250    /// Enables detailed monitoring for the specified instances.
251    pub async fn monitor_instances(
252        &self,
253        body: &MonitorInstancesRequest,
254    ) -> Result<MonitorInstancesResponse> {
255        self.ops.monitor_instances(body).await
256    }
257
258    /// Associates an IAM instance profile with a running or stopped instance.
259    pub async fn associate_iam_instance_profile(
260        &self,
261        body: &AssociateIamInstanceProfileRequest,
262    ) -> Result<AssociateIamInstanceProfileResponse> {
263        self.ops.associate_iam_instance_profile(body).await
264    }
265
266    /// Detaches an EBS volume from an instance.
267    pub async fn detach_volume(&self, body: &DetachVolumeRequest) -> Result<VolumeAttachment> {
268        self.ops.detach_volume(body).await
269    }
270
271    /// Deletes the specified EBS volume.
272    pub async fn delete_volume(&self, body: &DeleteVolumeRequest) -> Result<()> {
273        self.ops.delete_volume(body).await
274    }
275
276    /// Modifies the size, IOPS, throughput, or type of an EBS volume.
277    pub async fn modify_volume(&self, body: &ModifyVolumeRequest) -> Result<ModifyVolumeResponse> {
278        self.ops.modify_volume(body).await
279    }
280
281    /// Creates a snapshot of an EBS volume.
282    pub async fn create_snapshot(&self, body: &CreateSnapshotRequest) -> Result<Snapshot> {
283        self.ops.create_snapshot(body).await
284    }
285
286    /// Deletes the specified snapshot.
287    pub async fn delete_snapshot(&self, body: &DeleteSnapshotRequest) -> Result<()> {
288        self.ops.delete_snapshot(body).await
289    }
290
291    /// Modifies the specified snapshot attribute.
292    pub async fn modify_snapshot_attribute(
293        &self,
294        body: &ModifySnapshotAttributeRequest,
295    ) -> Result<()> {
296        self.ops.modify_snapshot_attribute(body).await
297    }
298
299    /// Enables the block public access for snapshots setting.
300    pub async fn enable_snapshot_block_public_access(
301        &self,
302        body: &EnableSnapshotBlockPublicAccessRequest,
303    ) -> Result<EnableSnapshotBlockPublicAccessResponse> {
304        self.ops.enable_snapshot_block_public_access(body).await
305    }
306
307    /// Deregisters the specified AMI.
308    pub async fn deregister_image(
309        &self,
310        body: &DeregisterImageRequest,
311    ) -> Result<DeregisterImageResponse> {
312        self.ops.deregister_image(body).await
313    }
314
315    /// Modifies the specified attribute of the specified AMI.
316    pub async fn modify_image_attribute(&self, body: &ModifyImageAttributeRequest) -> Result<()> {
317        self.ops.modify_image_attribute(body).await
318    }
319
320    /// Enables the block public access for AMIs setting.
321    pub async fn enable_image_block_public_access(
322        &self,
323        body: &EnableImageBlockPublicAccessRequest,
324    ) -> Result<EnableImageBlockPublicAccessResponse> {
325        self.ops.enable_image_block_public_access(body).await
326    }
327
328    /// Removes the specified inbound rules from a security group.
329    pub async fn revoke_security_group_ingress(
330        &self,
331        body: &RevokeSecurityGroupIngressRequest,
332    ) -> Result<RevokeSecurityGroupIngressResponse> {
333        self.ops.revoke_security_group_ingress(body).await
334    }
335
336    /// Removes the specified outbound rules from a security group.
337    pub async fn revoke_security_group_egress(
338        &self,
339        body: &RevokeSecurityGroupEgressRequest,
340    ) -> Result<RevokeSecurityGroupEgressResponse> {
341        self.ops.revoke_security_group_egress(body).await
342    }
343
344    /// Adds the specified inbound rules to a security group.
345    pub async fn authorize_security_group_ingress(
346        &self,
347        body: &AuthorizeSecurityGroupIngressRequest,
348    ) -> Result<AuthorizeSecurityGroupIngressResponse> {
349        self.ops.authorize_security_group_ingress(body).await
350    }
351
352    /// Deletes the specified security group.
353    pub async fn delete_security_group(
354        &self,
355        body: &DeleteSecurityGroupRequest,
356    ) -> Result<DeleteSecurityGroupResponse> {
357        self.ops.delete_security_group(body).await
358    }
359
360    /// Releases the specified Elastic IP address.
361    pub async fn release_address(&self, body: &ReleaseAddressRequest) -> Result<()> {
362        self.ops.release_address(body).await
363    }
364
365    /// Deletes the specified NAT gateway.
366    pub async fn delete_nat_gateway(
367        &self,
368        body: &DeleteNatGatewayRequest,
369    ) -> Result<DeleteNatGatewayResponse> {
370        self.ops.delete_nat_gateway(body).await
371    }
372
373    /// Deletes the specified VPC endpoints.
374    pub async fn delete_vpc_endpoints(
375        &self,
376        body: &DeleteVpcEndpointsRequest,
377    ) -> Result<DeleteVpcEndpointsResponse> {
378        self.ops.delete_vpc_endpoints(body).await
379    }
380
381    /// Creates one or more flow logs.
382    pub async fn create_flow_logs(
383        &self,
384        body: &CreateFlowLogsRequest,
385    ) -> Result<CreateFlowLogsResponse> {
386        self.ops.create_flow_logs(body).await
387    }
388
389    /// Adds or overwrites only the specified tags for the specified resources.
390    pub async fn create_tags(&self, body: &CreateTagsRequest) -> Result<()> {
391        self.ops.create_tags(body).await
392    }
393
394    /// Enables EBS encryption by default for the account.
395    pub async fn enable_ebs_encryption_by_default(
396        &self,
397        body: &EnableEbsEncryptionByDefaultRequest,
398    ) -> Result<EnableEbsEncryptionByDefaultResponse> {
399        self.ops.enable_ebs_encryption_by_default(body).await
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use crate::AwsHttpClient;
406    use crate::mock_client::MockClient;
407    use crate::test_support::ec2_mock_helpers::Ec2MockHelpers;
408
409    /// EC2 responses don't use <ActionResult> wrapper. Data is directly inside
410    /// <ActionResponse> with a <requestId> sibling element.
411    fn ec2_response(action: &str, inner: &str) -> Vec<u8> {
412        format!(
413            "<{action}Response>\
414               <requestId>test-request-id</requestId>\
415               {inner}\
416             </{action}Response>"
417        )
418        .into_bytes()
419    }
420
421    #[tokio::test]
422    async fn describe_instances_returns_parsed_reservation() {
423        let mut mock = MockClient::new();
424        mock.expect_describe_instances()
425            .returning_bytes(ec2_response(
426                "DescribeInstances",
427                "<reservationSet>\
428                   <item>\
429                     <reservationId>r-0123456789abcdef0</reservationId>\
430                   </item>\
431                 </reservationSet>",
432            ));
433
434        let client = AwsHttpClient::from_mock(mock);
435        let body = crate::types::ec2::DescribeInstancesRequest::default();
436        let response = client.ec2().describe_instances(&body).await.unwrap();
437
438        assert_eq!(response.reservations.len(), 1);
439        assert_eq!(
440            response.reservations[0].reservation_id.as_deref(),
441            Some("r-0123456789abcdef0")
442        );
443    }
444
445    #[tokio::test]
446    async fn describe_instances_handles_empty_response() {
447        let mut mock = MockClient::new();
448        mock.expect_describe_instances()
449            .returning_bytes(ec2_response("DescribeInstances", ""));
450
451        let client = AwsHttpClient::from_mock(mock);
452        let body = crate::types::ec2::DescribeInstancesRequest::default();
453        let response = client.ec2().describe_instances(&body).await.unwrap();
454        assert!(response.reservations.is_empty());
455    }
456
457    #[tokio::test]
458    async fn describe_launch_templates_returns_parsed_templates() {
459        let mut mock = MockClient::new();
460        mock.expect_describe_launch_templates()
461            .returning_bytes(ec2_response(
462                "DescribeLaunchTemplates",
463                "<launchTemplates>\
464                   <item>\
465                     <launchTemplateId>lt-0123456789abcdef0</launchTemplateId>\
466                     <launchTemplateName>my-template</launchTemplateName>\
467                   </item>\
468                 </launchTemplates>",
469            ));
470
471        let client = AwsHttpClient::from_mock(mock);
472        let body = crate::types::ec2::DescribeLaunchTemplatesRequest::default();
473        let response = client.ec2().describe_launch_templates(&body).await.unwrap();
474
475        assert_eq!(response.launch_templates.len(), 1);
476        let lt = &response.launch_templates[0];
477        assert_eq!(
478            lt.launch_template_id.as_deref(),
479            Some("lt-0123456789abcdef0")
480        );
481        assert_eq!(lt.launch_template_name.as_deref(), Some("my-template"));
482    }
483
484    #[tokio::test]
485    async fn describe_launch_templates_handles_empty_response() {
486        let mut mock = MockClient::new();
487        mock.expect_describe_launch_templates()
488            .returning_bytes(ec2_response("DescribeLaunchTemplates", ""));
489
490        let client = AwsHttpClient::from_mock(mock);
491        let body = crate::types::ec2::DescribeLaunchTemplatesRequest::default();
492        let response = client.ec2().describe_launch_templates(&body).await.unwrap();
493        assert!(response.launch_templates.is_empty());
494    }
495
496    #[tokio::test]
497    async fn describe_launch_template_versions_returns_parsed_versions() {
498        let mut mock = MockClient::new();
499        mock.expect_describe_launch_template_versions()
500            .returning_bytes(ec2_response(
501                "DescribeLaunchTemplateVersions",
502                "<launchTemplateVersionSet>\
503                   <item>\
504                     <launchTemplateId>lt-0123456789abcdef0</launchTemplateId>\
505                     <launchTemplateName>my-template</launchTemplateName>\
506                     <versionNumber>1</versionNumber>\
507                     <launchTemplateData>\
508                       <imageId>ami-abcdef01</imageId>\
509                       <instanceType>t3.micro</instanceType>\
510                       <metadataOptions>\
511                         <httpTokens>required</httpTokens>\
512                       </metadataOptions>\
513                     </launchTemplateData>\
514                   </item>\
515                 </launchTemplateVersionSet>",
516            ));
517
518        let client = AwsHttpClient::from_mock(mock);
519        let body = crate::types::ec2::DescribeLaunchTemplateVersionsRequest {
520            launch_template_id: Some("lt-0123456789abcdef0".to_string()),
521            ..Default::default()
522        };
523        let response = client
524            .ec2()
525            .describe_launch_template_versions(&body)
526            .await
527            .unwrap();
528
529        assert_eq!(response.launch_template_versions.len(), 1);
530        let ver = &response.launch_template_versions[0];
531        assert_eq!(
532            ver.launch_template_id.as_deref(),
533            Some("lt-0123456789abcdef0")
534        );
535        assert_eq!(ver.launch_template_name.as_deref(), Some("my-template"));
536        assert_eq!(ver.version_number, Some(1));
537
538        let data = ver.launch_template_data.as_ref().unwrap();
539        assert_eq!(data.image_id.as_deref(), Some("ami-abcdef01"));
540        assert_eq!(data.instance_type.as_deref(), Some("t3.micro"));
541        let metadata = data.metadata_options.as_ref().unwrap();
542        assert_eq!(metadata.http_tokens.as_deref(), Some("required"));
543    }
544
545    #[tokio::test]
546    async fn describe_launch_template_versions_handles_empty_response() {
547        let mut mock = MockClient::new();
548        mock.expect_describe_launch_template_versions()
549            .returning_bytes(ec2_response("DescribeLaunchTemplateVersions", ""));
550
551        let client = AwsHttpClient::from_mock(mock);
552        let body = crate::types::ec2::DescribeLaunchTemplateVersionsRequest::default();
553        let response = client
554            .ec2()
555            .describe_launch_template_versions(&body)
556            .await
557            .unwrap();
558        assert!(response.launch_template_versions.is_empty());
559    }
560
561    // =========================================================================
562    // Task 3.3: DescribeVolumes, DetachVolume, DeleteVolume, ModifyVolume
563    // =========================================================================
564
565    #[tokio::test]
566    async fn describe_volumes_returns_parsed_volumes() {
567        let mut mock = MockClient::new();
568        mock.expect_describe_volumes().returning_bytes(ec2_response(
569            "DescribeVolumes",
570            "<volumeSet>\
571               <item>\
572                 <volumeId>vol-0123456789abcdef0</volumeId>\
573                 <size>100</size>\
574                 <volumeType>gp3</volumeType>\
575                 <status>available</status>\
576                 <encrypted>true</encrypted>\
577                 <availabilityZone>eu-central-1a</availabilityZone>\
578                 <tagSet>\
579                   <item>\
580                     <key>Name</key>\
581                     <value>test-volume</value>\
582                   </item>\
583                 </tagSet>\
584               </item>\
585             </volumeSet>\
586             <nextToken>page-2-token</nextToken>",
587        ));
588
589        let client = AwsHttpClient::from_mock(mock);
590        let body = crate::types::ec2::DescribeVolumesRequest::default();
591        let response = client.ec2().describe_volumes(&body).await.unwrap();
592
593        assert_eq!(response.volumes.len(), 1);
594        let vol = &response.volumes[0];
595        assert_eq!(vol.volume_id.as_deref(), Some("vol-0123456789abcdef0"));
596        assert_eq!(vol.size, Some(100));
597        assert_eq!(vol.volume_type.as_deref(), Some("gp3"));
598        assert_eq!(vol.state.as_deref(), Some("available"));
599        assert_eq!(vol.encrypted, Some(true));
600        assert_eq!(vol.availability_zone.as_deref(), Some("eu-central-1a"));
601        assert_eq!(vol.tags.len(), 1);
602        assert_eq!(vol.tags[0].key.as_deref(), Some("Name"));
603        assert_eq!(vol.tags[0].value.as_deref(), Some("test-volume"));
604        assert_eq!(response.next_token.as_deref(), Some("page-2-token"));
605    }
606
607    #[tokio::test]
608    async fn describe_volumes_handles_empty_response() {
609        let mut mock = MockClient::new();
610        mock.expect_describe_volumes()
611            .returning_bytes(ec2_response("DescribeVolumes", ""));
612
613        let client = AwsHttpClient::from_mock(mock);
614        let body = crate::types::ec2::DescribeVolumesRequest::default();
615        let response = client.ec2().describe_volumes(&body).await.unwrap();
616        assert!(response.volumes.is_empty());
617        assert!(response.next_token.is_none());
618    }
619
620    #[tokio::test]
621    async fn modify_volume_returns_parsed_modification() {
622        let mut mock = MockClient::new();
623        mock.expect_modify_volume().returning_bytes(ec2_response(
624            "ModifyVolume",
625            "<volumeModification>\
626               <volumeId>vol-0123456789abcdef0</volumeId>\
627               <modificationState>modifying</modificationState>\
628               <targetIops>4000</targetIops>\
629               <targetVolumeType>gp3</targetVolumeType>\
630               <targetThroughput>250</targetThroughput>\
631             </volumeModification>",
632        ));
633
634        let client = AwsHttpClient::from_mock(mock);
635        let body = crate::types::ec2::ModifyVolumeRequest {
636            volume_id: "vol-0123456789abcdef0".into(),
637            iops: Some(4000),
638            ..Default::default()
639        };
640        let response = client.ec2().modify_volume(&body).await.unwrap();
641
642        let modification = response.volume_modification.unwrap();
643        assert_eq!(
644            modification.volume_id.as_deref(),
645            Some("vol-0123456789abcdef0")
646        );
647        assert_eq!(
648            modification.modification_state.as_deref(),
649            Some("modifying")
650        );
651        assert_eq!(modification.target_iops, Some(4000));
652        assert_eq!(modification.target_volume_type.as_deref(), Some("gp3"));
653        assert_eq!(modification.target_throughput, Some(250));
654    }
655
656    #[tokio::test]
657    async fn detach_volume_returns_parsed_attachment() {
658        let mut mock = MockClient::new();
659        mock.expect_detach_volume().returning_bytes(ec2_response(
660            "DetachVolume",
661            "<volumeId>vol-0123456789abcdef0</volumeId>\
662             <instanceId>i-0123456789abcdef0</instanceId>\
663             <device>/dev/sdf</device>\
664             <status>detaching</status>",
665        ));
666
667        let client = AwsHttpClient::from_mock(mock);
668        let body = crate::types::ec2::DetachVolumeRequest {
669            volume_id: "vol-0123456789abcdef0".into(),
670            instance_id: Some("i-0123456789abcdef0".into()),
671        };
672        let response = client.ec2().detach_volume(&body).await.unwrap();
673
674        assert_eq!(response.volume_id.as_deref(), Some("vol-0123456789abcdef0"));
675        assert_eq!(response.instance_id.as_deref(), Some("i-0123456789abcdef0"));
676        assert_eq!(response.device.as_deref(), Some("/dev/sdf"));
677        assert_eq!(response.state.as_deref(), Some("detaching"));
678    }
679
680    #[tokio::test]
681    async fn delete_volume_succeeds() {
682        let mut mock = MockClient::new();
683        mock.expect_delete_volume()
684            .returning_bytes(ec2_response("DeleteVolume", ""));
685
686        let client = AwsHttpClient::from_mock(mock);
687        let body = crate::types::ec2::DeleteVolumeRequest {
688            volume_id: "vol-0123456789abcdef0".into(),
689        };
690        let result = client.ec2().delete_volume(&body).await;
691        assert!(result.is_ok());
692    }
693
694    // =========================================================================
695    // Task 3.4: DescribeSnapshots, CreateSnapshot, DeleteSnapshot,
696    //           DescribeSnapshotAttribute, ModifySnapshotAttribute
697    // =========================================================================
698
699    #[tokio::test]
700    async fn describe_snapshots_returns_parsed_snapshots() {
701        let mut mock = MockClient::new();
702        mock.expect_describe_snapshots()
703            .returning_bytes(ec2_response(
704                "DescribeSnapshots",
705                "<snapshotSet>\
706                   <item>\
707                     <snapshotId>snap-0123456789abcdef0</snapshotId>\
708                     <volumeId>vol-0123456789abcdef0</volumeId>\
709                     <volumeSize>100</volumeSize>\
710                     <status>completed</status>\
711                     <encrypted>false</encrypted>\
712                     <startTime>2026-01-15T10:00:00.000Z</startTime>\
713                     <tagSet>\
714                       <item>\
715                         <key>Name</key>\
716                         <value>test-snapshot</value>\
717                       </item>\
718                     </tagSet>\
719                   </item>\
720                 </snapshotSet>\
721                 <nextToken>page-2-token</nextToken>",
722            ));
723
724        let client = AwsHttpClient::from_mock(mock);
725        let body = crate::types::ec2::DescribeSnapshotsRequest::default();
726        let response = client.ec2().describe_snapshots(&body).await.unwrap();
727
728        assert_eq!(response.snapshots.len(), 1);
729        let snap = &response.snapshots[0];
730        assert_eq!(snap.snapshot_id.as_deref(), Some("snap-0123456789abcdef0"));
731        assert_eq!(snap.volume_id.as_deref(), Some("vol-0123456789abcdef0"));
732        assert_eq!(snap.volume_size, Some(100));
733        assert_eq!(snap.state.as_deref(), Some("completed"));
734        assert_eq!(snap.encrypted, Some(false));
735        assert_eq!(snap.tags.len(), 1);
736        assert_eq!(snap.tags[0].key.as_deref(), Some("Name"));
737        assert_eq!(response.next_token.as_deref(), Some("page-2-token"));
738    }
739
740    #[tokio::test]
741    async fn describe_snapshots_handles_empty_response() {
742        let mut mock = MockClient::new();
743        mock.expect_describe_snapshots()
744            .returning_bytes(ec2_response("DescribeSnapshots", ""));
745
746        let client = AwsHttpClient::from_mock(mock);
747        let body = crate::types::ec2::DescribeSnapshotsRequest::default();
748        let response = client.ec2().describe_snapshots(&body).await.unwrap();
749        assert!(response.snapshots.is_empty());
750    }
751
752    #[tokio::test]
753    async fn create_snapshot_returns_parsed_snapshot() {
754        let mut mock = MockClient::new();
755        mock.expect_create_snapshot().returning_bytes(ec2_response(
756            "CreateSnapshot",
757            "<snapshotId>snap-0123456789abcdef0</snapshotId>\
758                 <volumeId>vol-0123456789abcdef0</volumeId>\
759                 <volumeSize>50</volumeSize>\
760                 <status>pending</status>\
761                 <encrypted>true</encrypted>",
762        ));
763
764        let client = AwsHttpClient::from_mock(mock);
765        let body = crate::types::ec2::CreateSnapshotRequest {
766            volume_id: "vol-0123456789abcdef0".into(),
767            description: Some("test snapshot".into()),
768        };
769        let response = client.ec2().create_snapshot(&body).await.unwrap();
770
771        assert_eq!(
772            response.snapshot_id.as_deref(),
773            Some("snap-0123456789abcdef0")
774        );
775        assert_eq!(response.volume_id.as_deref(), Some("vol-0123456789abcdef0"));
776        assert_eq!(response.volume_size, Some(50));
777        assert_eq!(response.state.as_deref(), Some("pending"));
778        assert_eq!(response.encrypted, Some(true));
779    }
780
781    #[tokio::test]
782    async fn delete_snapshot_succeeds() {
783        let mut mock = MockClient::new();
784        mock.expect_delete_snapshot()
785            .returning_bytes(ec2_response("DeleteSnapshot", ""));
786
787        let client = AwsHttpClient::from_mock(mock);
788        let body = crate::types::ec2::DeleteSnapshotRequest {
789            snapshot_id: "snap-0123456789abcdef0".into(),
790        };
791        let result = client.ec2().delete_snapshot(&body).await;
792        assert!(result.is_ok());
793    }
794
795    #[tokio::test]
796    async fn describe_snapshot_attribute_returns_permissions() {
797        let mut mock = MockClient::new();
798        mock.expect_describe_snapshot_attribute()
799            .returning_bytes(ec2_response(
800                "DescribeSnapshotAttribute",
801                "<snapshotId>snap-0123456789abcdef0</snapshotId>\
802                 <createVolumePermission>\
803                   <item>\
804                     <userId>123456789012</userId>\
805                   </item>\
806                 </createVolumePermission>",
807            ));
808
809        let client = AwsHttpClient::from_mock(mock);
810        let body = crate::types::ec2::DescribeSnapshotAttributeRequest {
811            snapshot_id: "snap-0123456789abcdef0".into(),
812            attribute: "createVolumePermission".into(),
813        };
814        let response = client
815            .ec2()
816            .describe_snapshot_attribute(&body)
817            .await
818            .unwrap();
819
820        assert_eq!(
821            response.snapshot_id.as_deref(),
822            Some("snap-0123456789abcdef0")
823        );
824        assert_eq!(response.create_volume_permissions.len(), 1);
825        assert_eq!(
826            response.create_volume_permissions[0].user_id.as_deref(),
827            Some("123456789012")
828        );
829    }
830
831    #[tokio::test]
832    async fn modify_snapshot_attribute_succeeds() {
833        let mut mock = MockClient::new();
834        mock.expect_modify_snapshot_attribute()
835            .returning_bytes(ec2_response("ModifySnapshotAttribute", ""));
836
837        let client = AwsHttpClient::from_mock(mock);
838        let body = crate::types::ec2::ModifySnapshotAttributeRequest {
839            snapshot_id: "snap-0123456789abcdef0".into(),
840            attribute: Some("createVolumePermission".into()),
841            ..Default::default()
842        };
843        let result = client.ec2().modify_snapshot_attribute(&body).await;
844        assert!(result.is_ok());
845    }
846
847    // =========================================================================
848    // Task 3.5: DescribeImages, DeregisterImage, ModifyImageAttribute
849    // =========================================================================
850
851    #[tokio::test]
852    async fn describe_images_returns_parsed_images() {
853        let mut mock = MockClient::new();
854        mock.expect_describe_images().returning_bytes(ec2_response(
855            "DescribeImages",
856            "<imagesSet>\
857                   <item>\
858                     <imageId>ami-0123456789abcdef0</imageId>\
859                     <name>test-ami</name>\
860                     <imageState>available</imageState>\
861                     <isPublic>false</isPublic>\
862                     <imageType>machine</imageType>\
863                     <platformDetails>Linux/UNIX</platformDetails>\
864                     <creationDate>2026-01-15T10:00:00.000Z</creationDate>\
865                     <description>A test AMI</description>\
866                     <blockDeviceMapping>\
867                       <item>\
868                         <deviceName>/dev/xvda</deviceName>\
869                       </item>\
870                     </blockDeviceMapping>\
871                     <tagSet>\
872                       <item>\
873                         <key>Name</key>\
874                         <value>test-image</value>\
875                       </item>\
876                     </tagSet>\
877                   </item>\
878                 </imagesSet>",
879        ));
880
881        let client = AwsHttpClient::from_mock(mock);
882        let body = crate::types::ec2::DescribeImagesRequest {
883            owners: vec!["self".into()],
884            ..Default::default()
885        };
886        let response = client.ec2().describe_images(&body).await.unwrap();
887
888        assert_eq!(response.images.len(), 1);
889        let img = &response.images[0];
890        assert_eq!(img.image_id.as_deref(), Some("ami-0123456789abcdef0"));
891        assert_eq!(img.name.as_deref(), Some("test-ami"));
892        assert_eq!(img.state.as_deref(), Some("available"));
893        assert_eq!(img.public, Some(false));
894        assert_eq!(img.image_type.as_deref(), Some("machine"));
895        assert_eq!(img.platform_details.as_deref(), Some("Linux/UNIX"));
896        assert_eq!(
897            img.creation_date.as_deref(),
898            Some("2026-01-15T10:00:00.000Z")
899        );
900        assert_eq!(img.description.as_deref(), Some("A test AMI"));
901        assert_eq!(img.block_device_mappings.len(), 1);
902        assert_eq!(
903            img.block_device_mappings[0].device_name.as_deref(),
904            Some("/dev/xvda")
905        );
906        assert_eq!(img.tags.len(), 1);
907        assert_eq!(img.tags[0].key.as_deref(), Some("Name"));
908        assert_eq!(img.tags[0].value.as_deref(), Some("test-image"));
909    }
910
911    #[tokio::test]
912    async fn describe_images_handles_empty_response() {
913        let mut mock = MockClient::new();
914        mock.expect_describe_images()
915            .returning_bytes(ec2_response("DescribeImages", ""));
916
917        let client = AwsHttpClient::from_mock(mock);
918        let body = crate::types::ec2::DescribeImagesRequest::default();
919        let response = client.ec2().describe_images(&body).await.unwrap();
920        assert!(response.images.is_empty());
921    }
922
923    #[tokio::test]
924    async fn deregister_image_returns_parsed_response() {
925        let mut mock = MockClient::new();
926        mock.expect_deregister_image().returning_bytes(ec2_response(
927            "DeregisterImage",
928            "<deleteSnapshotResultSet>\
929                   <item>\
930                     <snapshotId>snap-0123456789abcdef0</snapshotId>\
931                     <return>true</return>\
932                   </item>\
933                 </deleteSnapshotResultSet>",
934        ));
935
936        let client = AwsHttpClient::from_mock(mock);
937        let body = crate::types::ec2::DeregisterImageRequest {
938            image_id: "ami-0123456789abcdef0".into(),
939        };
940        let response = client.ec2().deregister_image(&body).await.unwrap();
941
942        assert_eq!(response.delete_snapshot_results.len(), 1);
943    }
944
945    #[tokio::test]
946    async fn deregister_image_handles_empty_result() {
947        let mut mock = MockClient::new();
948        mock.expect_deregister_image()
949            .returning_bytes(ec2_response("DeregisterImage", ""));
950
951        let client = AwsHttpClient::from_mock(mock);
952        let body = crate::types::ec2::DeregisterImageRequest {
953            image_id: "ami-0123456789abcdef0".into(),
954        };
955        let response = client.ec2().deregister_image(&body).await.unwrap();
956        assert!(response.delete_snapshot_results.is_empty());
957    }
958
959    #[tokio::test]
960    async fn modify_image_attribute_succeeds() {
961        let mut mock = MockClient::new();
962        mock.expect_modify_image_attribute()
963            .returning_bytes(ec2_response("ModifyImageAttribute", ""));
964
965        let client = AwsHttpClient::from_mock(mock);
966        let body = crate::types::ec2::ModifyImageAttributeRequest {
967            image_id: "ami-0123456789abcdef0".into(),
968            ..Default::default()
969        };
970        let result = client.ec2().modify_image_attribute(&body).await;
971        assert!(result.is_ok());
972    }
973
974    // =========================================================================
975    // Task 3.6: DescribeSecurityGroups, AuthorizeSecurityGroupIngress,
976    //           RevokeSecurityGroupIngress, RevokeSecurityGroupEgress,
977    //           DeleteSecurityGroup
978    // =========================================================================
979
980    #[tokio::test]
981    async fn describe_security_groups_returns_parsed_groups() {
982        let mut mock = MockClient::new();
983        mock.expect_describe_security_groups()
984            .returning_bytes(ec2_response(
985                "DescribeSecurityGroups",
986                "<securityGroupInfo>\
987                   <item>\
988                     <groupId>sg-0123456789abcdef0</groupId>\
989                     <groupName>test-sg</groupName>\
990                     <groupDescription>A test security group</groupDescription>\
991                     <vpcId>vpc-abc123</vpcId>\
992                     <ipPermissions>\
993                       <item>\
994                         <ipProtocol>tcp</ipProtocol>\
995                         <fromPort>22</fromPort>\
996                         <toPort>22</toPort>\
997                         <ipRanges>\
998                           <item>\
999                             <cidrIp>10.0.0.0/8</cidrIp>\
1000                           </item>\
1001                         </ipRanges>\
1002                       </item>\
1003                     </ipPermissions>\
1004                     <ipPermissionsEgress>\
1005                       <item>\
1006                         <ipProtocol>-1</ipProtocol>\
1007                         <ipRanges>\
1008                           <item>\
1009                             <cidrIp>0.0.0.0/0</cidrIp>\
1010                           </item>\
1011                         </ipRanges>\
1012                       </item>\
1013                     </ipPermissionsEgress>\
1014                     <tagSet>\
1015                       <item>\
1016                         <key>Name</key>\
1017                         <value>my-sg</value>\
1018                       </item>\
1019                     </tagSet>\
1020                   </item>\
1021                 </securityGroupInfo>",
1022            ));
1023
1024        let client = AwsHttpClient::from_mock(mock);
1025        let body = crate::types::ec2::DescribeSecurityGroupsRequest::default();
1026        let response = client.ec2().describe_security_groups(&body).await.unwrap();
1027
1028        assert_eq!(response.security_groups.len(), 1);
1029        let sg = &response.security_groups[0];
1030        assert_eq!(sg.group_id.as_deref(), Some("sg-0123456789abcdef0"));
1031        assert_eq!(sg.group_name.as_deref(), Some("test-sg"));
1032        assert_eq!(sg.description.as_deref(), Some("A test security group"));
1033        assert_eq!(sg.vpc_id.as_deref(), Some("vpc-abc123"));
1034        assert_eq!(sg.ip_permissions.len(), 1);
1035        let perm = &sg.ip_permissions[0];
1036        assert_eq!(perm.ip_protocol.as_deref(), Some("tcp"));
1037        assert_eq!(perm.from_port, Some(22));
1038        assert_eq!(perm.to_port, Some(22));
1039        assert_eq!(perm.ip_ranges.len(), 1);
1040        assert_eq!(perm.ip_ranges[0].cidr_ip.as_deref(), Some("10.0.0.0/8"));
1041        assert_eq!(sg.ip_permissions_egress.len(), 1);
1042        assert_eq!(sg.tags.len(), 1);
1043        assert_eq!(sg.tags[0].key.as_deref(), Some("Name"));
1044    }
1045
1046    #[tokio::test]
1047    async fn describe_security_groups_handles_empty_response() {
1048        let mut mock = MockClient::new();
1049        mock.expect_describe_security_groups()
1050            .returning_bytes(ec2_response("DescribeSecurityGroups", ""));
1051
1052        let client = AwsHttpClient::from_mock(mock);
1053        let body = crate::types::ec2::DescribeSecurityGroupsRequest::default();
1054        let response = client.ec2().describe_security_groups(&body).await.unwrap();
1055        assert!(response.security_groups.is_empty());
1056    }
1057
1058    #[tokio::test]
1059    async fn authorize_security_group_ingress_returns_rules() {
1060        let mut mock = MockClient::new();
1061        mock.expect_authorize_security_group_ingress()
1062            .returning_bytes(ec2_response(
1063                "AuthorizeSecurityGroupIngress",
1064                "<securityGroupRuleSet>\
1065                   <item>\
1066                     <securityGroupRuleId>sgr-0123456789abcdef0</securityGroupRuleId>\
1067                     <groupId>sg-0123456789abcdef0</groupId>\
1068                     <ipProtocol>tcp</ipProtocol>\
1069                     <fromPort>443</fromPort>\
1070                     <toPort>443</toPort>\
1071                     <cidrIpv4>0.0.0.0/0</cidrIpv4>\
1072                   </item>\
1073                 </securityGroupRuleSet>",
1074            ));
1075
1076        let client = AwsHttpClient::from_mock(mock);
1077        let body = crate::types::ec2::AuthorizeSecurityGroupIngressRequest {
1078            group_id: Some("sg-0123456789abcdef0".into()),
1079            ip_permissions: vec![crate::types::ec2::IpPermission {
1080                ip_protocol: Some("tcp".into()),
1081                from_port: Some(443),
1082                to_port: Some(443),
1083                ip_ranges: vec![crate::types::ec2::IpRange {
1084                    cidr_ip: Some("0.0.0.0/0".into()),
1085                    ..Default::default()
1086                }],
1087                ..Default::default()
1088            }],
1089        };
1090        let response = client
1091            .ec2()
1092            .authorize_security_group_ingress(&body)
1093            .await
1094            .unwrap();
1095
1096        assert_eq!(response.security_group_rules.len(), 1);
1097        let rule = &response.security_group_rules[0];
1098        assert_eq!(
1099            rule.security_group_rule_id.as_deref(),
1100            Some("sgr-0123456789abcdef0")
1101        );
1102        assert_eq!(rule.group_id.as_deref(), Some("sg-0123456789abcdef0"));
1103        assert_eq!(rule.ip_protocol.as_deref(), Some("tcp"));
1104        assert_eq!(rule.from_port, Some(443));
1105        assert_eq!(rule.to_port, Some(443));
1106        assert_eq!(rule.cidr_ipv4.as_deref(), Some("0.0.0.0/0"));
1107    }
1108
1109    #[tokio::test]
1110    async fn revoke_security_group_ingress_succeeds() {
1111        let mut mock = MockClient::new();
1112        mock.expect_revoke_security_group_ingress()
1113            .returning_bytes(ec2_response("RevokeSecurityGroupIngress", ""));
1114
1115        let client = AwsHttpClient::from_mock(mock);
1116        let body = crate::types::ec2::RevokeSecurityGroupIngressRequest {
1117            group_id: Some("sg-0123456789abcdef0".into()),
1118            ip_permissions: vec![crate::types::ec2::IpPermission {
1119                ip_protocol: Some("tcp".into()),
1120                from_port: Some(22),
1121                to_port: Some(22),
1122                ..Default::default()
1123            }],
1124        };
1125        let result = client.ec2().revoke_security_group_ingress(&body).await;
1126        assert!(result.is_ok());
1127    }
1128
1129    #[tokio::test]
1130    async fn revoke_security_group_egress_succeeds() {
1131        let mut mock = MockClient::new();
1132        mock.expect_revoke_security_group_egress()
1133            .returning_bytes(ec2_response("RevokeSecurityGroupEgress", ""));
1134
1135        let client = AwsHttpClient::from_mock(mock);
1136        let body = crate::types::ec2::RevokeSecurityGroupEgressRequest {
1137            group_id: "sg-0123456789abcdef0".into(),
1138            ip_permissions: vec![crate::types::ec2::IpPermission {
1139                ip_protocol: Some("-1".into()),
1140                ..Default::default()
1141            }],
1142        };
1143        let result = client.ec2().revoke_security_group_egress(&body).await;
1144        assert!(result.is_ok());
1145    }
1146
1147    #[tokio::test]
1148    async fn delete_security_group_succeeds() {
1149        let mut mock = MockClient::new();
1150        mock.expect_delete_security_group()
1151            .returning_bytes(ec2_response(
1152                "DeleteSecurityGroup",
1153                "<groupId>sg-0123456789abcdef0</groupId>",
1154            ));
1155
1156        let client = AwsHttpClient::from_mock(mock);
1157        let body = crate::types::ec2::DeleteSecurityGroupRequest {
1158            group_id: Some("sg-0123456789abcdef0".into()),
1159        };
1160        let response = client.ec2().delete_security_group(&body).await.unwrap();
1161        assert_eq!(response.group_id.as_deref(), Some("sg-0123456789abcdef0"));
1162    }
1163
1164    #[tokio::test]
1165    async fn describe_addresses_returns_parsed_addresses() {
1166        let mut mock = MockClient::new();
1167        mock.expect_describe_addresses()
1168            .returning_bytes(ec2_response(
1169                "DescribeAddresses",
1170                "<addressesSet>\
1171                   <item>\
1172                     <allocationId>eipalloc-0123456789abcdef0</allocationId>\
1173                     <publicIp>203.0.113.25</publicIp>\
1174                     <domain>vpc</domain>\
1175                   </item>\
1176                 </addressesSet>",
1177            ));
1178
1179        let client = AwsHttpClient::from_mock(mock);
1180        let body = crate::types::ec2::DescribeAddressesRequest::default();
1181        let response = client.ec2().describe_addresses(&body).await.unwrap();
1182
1183        assert_eq!(response.addresses.len(), 1);
1184        let addr = &response.addresses[0];
1185        assert_eq!(
1186            addr.allocation_id.as_deref(),
1187            Some("eipalloc-0123456789abcdef0")
1188        );
1189        assert_eq!(addr.public_ip.as_deref(), Some("203.0.113.25"));
1190        assert_eq!(addr.domain.as_deref(), Some("vpc"));
1191    }
1192
1193    #[tokio::test]
1194    async fn describe_addresses_handles_empty_response() {
1195        let mut mock = MockClient::new();
1196        mock.expect_describe_addresses()
1197            .returning_bytes(ec2_response("DescribeAddresses", ""));
1198
1199        let client = AwsHttpClient::from_mock(mock);
1200        let body = crate::types::ec2::DescribeAddressesRequest::default();
1201        let response = client.ec2().describe_addresses(&body).await.unwrap();
1202        assert!(response.addresses.is_empty());
1203    }
1204
1205    #[tokio::test]
1206    async fn release_address_succeeds() {
1207        let mut mock = MockClient::new();
1208        mock.expect_release_address()
1209            .returning_bytes(ec2_response("ReleaseAddress", ""));
1210
1211        let client = AwsHttpClient::from_mock(mock);
1212        let body = crate::types::ec2::ReleaseAddressRequest {
1213            allocation_id: Some("eipalloc-0123456789abcdef0".into()),
1214        };
1215        let result = client.ec2().release_address(&body).await;
1216        assert!(result.is_ok());
1217    }
1218
1219    #[tokio::test]
1220    async fn describe_nat_gateways_returns_parsed_gateways() {
1221        let mut mock = MockClient::new();
1222        mock.expect_describe_nat_gateways()
1223            .returning_bytes(ec2_response(
1224                "DescribeNatGateways",
1225                "<natGatewaySet>\
1226                   <item>\
1227                     <natGatewayId>nat-0123456789abcdef0</natGatewayId>\
1228                     <state>available</state>\
1229                     <vpcId>vpc-0abc123</vpcId>\
1230                     <subnetId>subnet-0abc123</subnetId>\
1231                     <natGatewayAddressSet>\
1232                       <item>\
1233                         <allocationId>eipalloc-0abc123</allocationId>\
1234                         <publicIp>198.51.100.1</publicIp>\
1235                       </item>\
1236                     </natGatewayAddressSet>\
1237                   </item>\
1238                 </natGatewaySet>",
1239            ));
1240
1241        let client = AwsHttpClient::from_mock(mock);
1242        let body = crate::types::ec2::DescribeNatGatewaysRequest::default();
1243        let response = client.ec2().describe_nat_gateways(&body).await.unwrap();
1244
1245        assert_eq!(response.nat_gateways.len(), 1);
1246        let ngw = &response.nat_gateways[0];
1247        assert_eq!(ngw.nat_gateway_id.as_deref(), Some("nat-0123456789abcdef0"));
1248        assert_eq!(ngw.state.as_deref(), Some("available"));
1249        assert_eq!(ngw.vpc_id.as_deref(), Some("vpc-0abc123"));
1250        assert_eq!(ngw.nat_gateway_addresses.len(), 1);
1251        assert_eq!(
1252            ngw.nat_gateway_addresses[0].public_ip.as_deref(),
1253            Some("198.51.100.1")
1254        );
1255    }
1256
1257    #[tokio::test]
1258    async fn delete_nat_gateway_returns_id() {
1259        let mut mock = MockClient::new();
1260        mock.expect_delete_nat_gateway()
1261            .returning_bytes(ec2_response(
1262                "DeleteNatGateway",
1263                "<natGatewayId>nat-0123456789abcdef0</natGatewayId>",
1264            ));
1265
1266        let client = AwsHttpClient::from_mock(mock);
1267        let body = crate::types::ec2::DeleteNatGatewayRequest {
1268            nat_gateway_id: "nat-0123456789abcdef0".into(),
1269        };
1270        let response = client.ec2().delete_nat_gateway(&body).await.unwrap();
1271        assert_eq!(
1272            response.nat_gateway_id.as_deref(),
1273            Some("nat-0123456789abcdef0")
1274        );
1275    }
1276
1277    #[tokio::test]
1278    async fn describe_vpc_endpoints_returns_parsed_endpoints() {
1279        let mut mock = MockClient::new();
1280        mock.expect_describe_vpc_endpoints()
1281            .returning_bytes(ec2_response(
1282                "DescribeVpcEndpoints",
1283                "<vpcEndpointSet>\
1284                   <item>\
1285                     <vpcEndpointId>vpce-0123456789abcdef0</vpcEndpointId>\
1286                     <vpcId>vpc-0abc123</vpcId>\
1287                     <serviceName>com.amazonaws.eu-central-1.s3</serviceName>\
1288                     <state>available</state>\
1289                   </item>\
1290                 </vpcEndpointSet>",
1291            ));
1292
1293        let client = AwsHttpClient::from_mock(mock);
1294        let body = crate::types::ec2::DescribeVpcEndpointsRequest::default();
1295        let response = client.ec2().describe_vpc_endpoints(&body).await.unwrap();
1296
1297        assert_eq!(response.vpc_endpoints.len(), 1);
1298        let vpce = &response.vpc_endpoints[0];
1299        assert_eq!(
1300            vpce.vpc_endpoint_id.as_deref(),
1301            Some("vpce-0123456789abcdef0")
1302        );
1303        assert_eq!(vpce.vpc_id.as_deref(), Some("vpc-0abc123"));
1304        assert_eq!(
1305            vpce.service_name.as_deref(),
1306            Some("com.amazonaws.eu-central-1.s3")
1307        );
1308        assert_eq!(vpce.state.as_deref(), Some("available"));
1309    }
1310
1311    #[tokio::test]
1312    async fn delete_vpc_endpoints_succeeds() {
1313        let mut mock = MockClient::new();
1314        mock.expect_delete_vpc_endpoints()
1315            .returning_bytes(ec2_response("DeleteVpcEndpoints", "<unsuccessful/>"));
1316
1317        let client = AwsHttpClient::from_mock(mock);
1318        let body = crate::types::ec2::DeleteVpcEndpointsRequest {
1319            vpc_endpoint_ids: vec!["vpce-0123456789abcdef0".into()],
1320        };
1321        let response = client.ec2().delete_vpc_endpoints(&body).await.unwrap();
1322        assert!(response.unsuccessful.is_empty());
1323    }
1324
1325    #[tokio::test]
1326    async fn describe_vpcs_returns_parsed_vpcs() {
1327        let mut mock = MockClient::new();
1328        mock.expect_describe_vpcs().returning_bytes(ec2_response(
1329            "DescribeVpcs",
1330            "<vpcSet>\
1331                   <item>\
1332                     <vpcId>vpc-0abc123</vpcId>\
1333                     <cidrBlock>10.0.0.0/16</cidrBlock>\
1334                     <state>available</state>\
1335                     <isDefault>true</isDefault>\
1336                   </item>\
1337                 </vpcSet>",
1338        ));
1339
1340        let client = AwsHttpClient::from_mock(mock);
1341        let body = crate::types::ec2::DescribeVpcsRequest::default();
1342        let response = client.ec2().describe_vpcs(&body).await.unwrap();
1343
1344        assert_eq!(response.vpcs.len(), 1);
1345        let vpc = &response.vpcs[0];
1346        assert_eq!(vpc.vpc_id.as_deref(), Some("vpc-0abc123"));
1347        assert_eq!(vpc.cidr_block.as_deref(), Some("10.0.0.0/16"));
1348        assert_eq!(vpc.state.as_deref(), Some("available"));
1349        assert_eq!(vpc.is_default, Some(true));
1350    }
1351
1352    #[tokio::test]
1353    async fn describe_route_tables_returns_parsed_tables() {
1354        let mut mock = MockClient::new();
1355        mock.expect_describe_route_tables()
1356            .returning_bytes(ec2_response(
1357                "DescribeRouteTables",
1358                "<routeTableSet>\
1359                   <item>\
1360                     <routeTableId>rtb-0abc123</routeTableId>\
1361                     <vpcId>vpc-0abc123</vpcId>\
1362                     <routeSet>\
1363                       <item>\
1364                         <destinationCidrBlock>10.0.0.0/16</destinationCidrBlock>\
1365                         <gatewayId>local</gatewayId>\
1366                         <state>active</state>\
1367                       </item>\
1368                     </routeSet>\
1369                   </item>\
1370                 </routeTableSet>",
1371            ));
1372
1373        let client = AwsHttpClient::from_mock(mock);
1374        let body = crate::types::ec2::DescribeRouteTablesRequest::default();
1375        let response = client.ec2().describe_route_tables(&body).await.unwrap();
1376
1377        assert_eq!(response.route_tables.len(), 1);
1378        let rt = &response.route_tables[0];
1379        assert_eq!(rt.route_table_id.as_deref(), Some("rtb-0abc123"));
1380        assert_eq!(rt.vpc_id.as_deref(), Some("vpc-0abc123"));
1381        assert_eq!(rt.routes.len(), 1);
1382        assert_eq!(
1383            rt.routes[0].destination_cidr_block.as_deref(),
1384            Some("10.0.0.0/16")
1385        );
1386        assert_eq!(rt.routes[0].state.as_deref(), Some("active"));
1387    }
1388
1389    #[tokio::test]
1390    async fn describe_network_acls_returns_parsed_acls() {
1391        let mut mock = MockClient::new();
1392        mock.expect_describe_network_acls()
1393            .returning_bytes(ec2_response(
1394                "DescribeNetworkAcls",
1395                "<networkAclSet>\
1396                   <item>\
1397                     <networkAclId>acl-0abc123</networkAclId>\
1398                     <vpcId>vpc-0abc123</vpcId>\
1399                     <default>true</default>\
1400                     <entrySet>\
1401                       <item>\
1402                         <ruleNumber>100</ruleNumber>\
1403                         <protocol>-1</protocol>\
1404                         <ruleAction>allow</ruleAction>\
1405                         <egress>false</egress>\
1406                         <cidrBlock>0.0.0.0/0</cidrBlock>\
1407                       </item>\
1408                     </entrySet>\
1409                   </item>\
1410                 </networkAclSet>",
1411            ));
1412
1413        let client = AwsHttpClient::from_mock(mock);
1414        let body = crate::types::ec2::DescribeNetworkAclsRequest::default();
1415        let response = client.ec2().describe_network_acls(&body).await.unwrap();
1416
1417        assert_eq!(response.network_acls.len(), 1);
1418        let acl = &response.network_acls[0];
1419        assert_eq!(acl.network_acl_id.as_deref(), Some("acl-0abc123"));
1420        assert_eq!(acl.vpc_id.as_deref(), Some("vpc-0abc123"));
1421        assert_eq!(acl.is_default, Some(true));
1422        assert_eq!(acl.entries.len(), 1);
1423        assert_eq!(acl.entries[0].rule_number, Some(100));
1424        assert_eq!(acl.entries[0].rule_action.as_deref(), Some("allow"));
1425    }
1426
1427    #[tokio::test]
1428    async fn describe_flow_logs_returns_parsed_logs() {
1429        let mut mock = MockClient::new();
1430        mock.expect_describe_flow_logs()
1431            .returning_bytes(ec2_response(
1432                "DescribeFlowLogs",
1433                "<flowLogSet>\
1434                   <item>\
1435                     <flowLogId>fl-0abc123</flowLogId>\
1436                     <resourceId>vpc-0abc123</resourceId>\
1437                     <trafficType>ALL</trafficType>\
1438                     <logGroupName>/vpc/flow-logs</logGroupName>\
1439                     <flowLogStatus>ACTIVE</flowLogStatus>\
1440                   </item>\
1441                 </flowLogSet>",
1442            ));
1443
1444        let client = AwsHttpClient::from_mock(mock);
1445        let body = crate::types::ec2::DescribeFlowLogsRequest::default();
1446        let response = client.ec2().describe_flow_logs(&body).await.unwrap();
1447
1448        assert_eq!(response.flow_logs.len(), 1);
1449        let fl = &response.flow_logs[0];
1450        assert_eq!(fl.flow_log_id.as_deref(), Some("fl-0abc123"));
1451        assert_eq!(fl.resource_id.as_deref(), Some("vpc-0abc123"));
1452        assert_eq!(fl.traffic_type.as_deref(), Some("ALL"));
1453        assert_eq!(fl.log_group_name.as_deref(), Some("/vpc/flow-logs"));
1454        assert_eq!(fl.flow_log_status.as_deref(), Some("ACTIVE"));
1455    }
1456
1457    #[tokio::test]
1458    async fn create_flow_logs_returns_ids() {
1459        let mut mock = MockClient::new();
1460        mock.expect_create_flow_logs().returning_bytes(ec2_response(
1461            "CreateFlowLogs",
1462            "<flowLogIdSet>\
1463                   <item>fl-0abc123</item>\
1464                 </flowLogIdSet>\
1465                 <unsuccessful/>",
1466        ));
1467
1468        let client = AwsHttpClient::from_mock(mock);
1469        let body = crate::types::ec2::CreateFlowLogsRequest {
1470            resource_ids: vec!["vpc-0abc123".into()],
1471            resource_type: "VPC".into(),
1472            traffic_type: "ALL".into(),
1473            log_group_name: Some("/vpc/flow-logs".into()),
1474            deliver_logs_permission_arn: Some("arn:aws:iam::123456789012:role/flow-logs".into()),
1475        };
1476        let response = client.ec2().create_flow_logs(&body).await.unwrap();
1477        assert_eq!(response.flow_log_ids.len(), 1);
1478        assert_eq!(response.flow_log_ids[0], "fl-0abc123");
1479        assert!(response.unsuccessful.is_empty());
1480    }
1481
1482    #[tokio::test]
1483    async fn get_ebs_encryption_by_default_returns_state() {
1484        let mut mock = MockClient::new();
1485        mock.expect_get_ebs_encryption_by_default()
1486            .returning_bytes(ec2_response(
1487                "GetEbsEncryptionByDefault",
1488                "<ebsEncryptionByDefault>true</ebsEncryptionByDefault>",
1489            ));
1490
1491        let client = AwsHttpClient::from_mock(mock);
1492        let body = crate::types::ec2::GetEbsEncryptionByDefaultRequest::default();
1493        let response = client
1494            .ec2()
1495            .get_ebs_encryption_by_default(&body)
1496            .await
1497            .unwrap();
1498        assert_eq!(response.ebs_encryption_by_default, Some(true));
1499    }
1500
1501    #[tokio::test]
1502    async fn enable_ebs_encryption_by_default_succeeds() {
1503        let mut mock = MockClient::new();
1504        mock.expect_enable_ebs_encryption_by_default()
1505            .returning_bytes(ec2_response(
1506                "EnableEbsEncryptionByDefault",
1507                "<ebsEncryptionByDefault>true</ebsEncryptionByDefault>",
1508            ));
1509
1510        let client = AwsHttpClient::from_mock(mock);
1511        let body = crate::types::ec2::EnableEbsEncryptionByDefaultRequest::default();
1512        let response = client
1513            .ec2()
1514            .enable_ebs_encryption_by_default(&body)
1515            .await
1516            .unwrap();
1517        assert_eq!(response.ebs_encryption_by_default, Some(true));
1518    }
1519
1520    #[tokio::test]
1521    async fn terminate_instances_returns_state_changes() {
1522        let mut mock = MockClient::new();
1523        mock.expect_terminate_instances()
1524            .returning_bytes(ec2_response(
1525                "TerminateInstances",
1526                "<instancesSet>\
1527                   <item>\
1528                     <instanceId>i-0123456789abcdef0</instanceId>\
1529                     <currentState><code>32</code><name>shutting-down</name></currentState>\
1530                     <previousState><code>16</code><name>running</name></previousState>\
1531                   </item>\
1532                 </instancesSet>",
1533            ));
1534
1535        let client = AwsHttpClient::from_mock(mock);
1536        let body = crate::types::ec2::TerminateInstancesRequest {
1537            instance_ids: vec!["i-0123456789abcdef0".into()],
1538        };
1539        let response = client.ec2().terminate_instances(&body).await.unwrap();
1540
1541        assert_eq!(response.terminating_instances.len(), 1);
1542        let sc = &response.terminating_instances[0];
1543        assert_eq!(sc.instance_id.as_deref(), Some("i-0123456789abcdef0"));
1544        assert_eq!(
1545            sc.current_state.as_ref().unwrap().name.as_deref(),
1546            Some("shutting-down")
1547        );
1548        assert_eq!(
1549            sc.previous_state.as_ref().unwrap().name.as_deref(),
1550            Some("running")
1551        );
1552    }
1553
1554    #[tokio::test]
1555    async fn stop_instances_returns_state_changes() {
1556        let mut mock = MockClient::new();
1557        mock.expect_stop_instances().returning_bytes(ec2_response(
1558            "StopInstances",
1559            "<instancesSet>\
1560                   <item>\
1561                     <instanceId>i-0123456789abcdef0</instanceId>\
1562                     <currentState><code>64</code><name>stopping</name></currentState>\
1563                     <previousState><code>16</code><name>running</name></previousState>\
1564                   </item>\
1565                 </instancesSet>",
1566        ));
1567
1568        let client = AwsHttpClient::from_mock(mock);
1569        let body = crate::types::ec2::StopInstancesRequest {
1570            instance_ids: vec!["i-0123456789abcdef0".into()],
1571        };
1572        let response = client.ec2().stop_instances(&body).await.unwrap();
1573
1574        assert_eq!(response.stopping_instances.len(), 1);
1575        let sc = &response.stopping_instances[0];
1576        assert_eq!(sc.instance_id.as_deref(), Some("i-0123456789abcdef0"));
1577        assert_eq!(
1578            sc.current_state.as_ref().unwrap().name.as_deref(),
1579            Some("stopping")
1580        );
1581    }
1582
1583    #[tokio::test]
1584    async fn start_instances_returns_state_changes() {
1585        let mut mock = MockClient::new();
1586        mock.expect_start_instances().returning_bytes(ec2_response(
1587            "StartInstances",
1588            "<instancesSet>\
1589                   <item>\
1590                     <instanceId>i-0123456789abcdef0</instanceId>\
1591                     <currentState><code>0</code><name>pending</name></currentState>\
1592                     <previousState><code>80</code><name>stopped</name></previousState>\
1593                   </item>\
1594                 </instancesSet>",
1595        ));
1596
1597        let client = AwsHttpClient::from_mock(mock);
1598        let body = crate::types::ec2::StartInstancesRequest {
1599            instance_ids: vec!["i-0123456789abcdef0".into()],
1600        };
1601        let response = client.ec2().start_instances(&body).await.unwrap();
1602
1603        assert_eq!(response.starting_instances.len(), 1);
1604        let sc = &response.starting_instances[0];
1605        assert_eq!(sc.instance_id.as_deref(), Some("i-0123456789abcdef0"));
1606        assert_eq!(
1607            sc.current_state.as_ref().unwrap().name.as_deref(),
1608            Some("pending")
1609        );
1610        assert_eq!(
1611            sc.previous_state.as_ref().unwrap().name.as_deref(),
1612            Some("stopped")
1613        );
1614    }
1615
1616    #[tokio::test]
1617    async fn modify_instance_metadata_options_returns_response() {
1618        let mut mock = MockClient::new();
1619        mock.expect_modify_instance_metadata_options()
1620            .returning_bytes(ec2_response(
1621                "ModifyInstanceMetadataOptions",
1622                "<instanceId>i-0123456789abcdef0</instanceId>\
1623                 <instanceMetadataOptions>\
1624                   <httpTokens>required</httpTokens>\
1625                   <httpEndpoint>enabled</httpEndpoint>\
1626                 </instanceMetadataOptions>",
1627            ));
1628
1629        let client = AwsHttpClient::from_mock(mock);
1630        let body = crate::types::ec2::ModifyInstanceMetadataOptionsRequest {
1631            instance_id: "i-0123456789abcdef0".into(),
1632            http_tokens: Some("required".into()),
1633            http_endpoint: Some("enabled".into()),
1634        };
1635        let response = client
1636            .ec2()
1637            .modify_instance_metadata_options(&body)
1638            .await
1639            .unwrap();
1640
1641        assert_eq!(response.instance_id.as_deref(), Some("i-0123456789abcdef0"));
1642        let opts = response.instance_metadata_options.as_ref().unwrap();
1643        assert_eq!(opts.http_tokens.as_deref(), Some("required"));
1644        assert_eq!(opts.http_endpoint.as_deref(), Some("enabled"));
1645    }
1646
1647    #[tokio::test]
1648    async fn monitor_instances_returns_monitorings() {
1649        let mut mock = MockClient::new();
1650        mock.expect_monitor_instances()
1651            .returning_bytes(ec2_response(
1652                "MonitorInstances",
1653                "<instancesSet>\
1654                   <item>\
1655                     <instanceId>i-0123456789abcdef0</instanceId>\
1656                     <monitoring><state>pending</state></monitoring>\
1657                   </item>\
1658                 </instancesSet>",
1659            ));
1660
1661        let client = AwsHttpClient::from_mock(mock);
1662        let body = crate::types::ec2::MonitorInstancesRequest {
1663            instance_ids: vec!["i-0123456789abcdef0".into()],
1664        };
1665        let response = client.ec2().monitor_instances(&body).await.unwrap();
1666
1667        assert_eq!(response.instance_monitorings.len(), 1);
1668        let mon = &response.instance_monitorings[0];
1669        assert_eq!(mon.instance_id.as_deref(), Some("i-0123456789abcdef0"));
1670        assert_eq!(
1671            mon.monitoring.as_ref().unwrap().state.as_deref(),
1672            Some("pending")
1673        );
1674    }
1675
1676    #[tokio::test]
1677    async fn associate_iam_instance_profile_returns_association() {
1678        let mut mock = MockClient::new();
1679        mock.expect_associate_iam_instance_profile()
1680            .returning_bytes(ec2_response(
1681                "AssociateIamInstanceProfile",
1682                "<iamInstanceProfileAssociation>\
1683                   <associationId>iip-assoc-0abc123</associationId>\
1684                   <instanceId>i-0123456789abcdef0</instanceId>\
1685                   <state>associating</state>\
1686                 </iamInstanceProfileAssociation>",
1687            ));
1688
1689        let client = AwsHttpClient::from_mock(mock);
1690        let body = crate::types::ec2::AssociateIamInstanceProfileRequest {
1691            iam_instance_profile: crate::types::ec2::IamInstanceProfileSpecification {
1692                arn: Some("arn:aws:iam::123456789012:instance-profile/test".into()),
1693                name: None,
1694            },
1695            instance_id: "i-0123456789abcdef0".into(),
1696        };
1697        let response = client
1698            .ec2()
1699            .associate_iam_instance_profile(&body)
1700            .await
1701            .unwrap();
1702
1703        let assoc = response.iam_instance_profile_association.as_ref().unwrap();
1704        assert_eq!(assoc.association_id.as_deref(), Some("iip-assoc-0abc123"));
1705        assert_eq!(assoc.instance_id.as_deref(), Some("i-0123456789abcdef0"));
1706        assert_eq!(assoc.state.as_deref(), Some("associating"));
1707    }
1708
1709    #[tokio::test]
1710    async fn create_tags_succeeds() {
1711        let mut mock = MockClient::new();
1712        mock.expect_create_tags()
1713            .returning_bytes(ec2_response("CreateTags", ""));
1714
1715        let client = AwsHttpClient::from_mock(mock);
1716        let body = crate::types::ec2::CreateTagsRequest {
1717            resources: vec!["i-0123456789abcdef0".into()],
1718            tags: vec![crate::types::ec2::Tag {
1719                key: Some("Name".into()),
1720                value: Some("test".into()),
1721            }],
1722        };
1723        let result = client.ec2().create_tags(&body).await;
1724        assert!(result.is_ok());
1725    }
1726
1727    #[tokio::test]
1728    async fn modify_instance_attribute_succeeds() {
1729        let mut mock = MockClient::new();
1730        mock.expect_modify_instance_attribute()
1731            .returning_bytes(ec2_response("ModifyInstanceAttribute", ""));
1732
1733        let client = AwsHttpClient::from_mock(mock);
1734        let body = crate::types::ec2::ModifyInstanceAttributeRequest {
1735            instance_id: "i-0123456789abcdef0".into(),
1736        };
1737        let result = client.ec2().modify_instance_attribute(&body).await;
1738        assert!(result.is_ok());
1739    }
1740
1741    #[tokio::test]
1742    async fn enable_snapshot_block_public_access_returns_state() {
1743        let mut mock = MockClient::new();
1744        mock.expect_enable_snapshot_block_public_access()
1745            .returning_bytes(ec2_response(
1746                "EnableSnapshotBlockPublicAccess",
1747                "<state>block-all-sharing</state>",
1748            ));
1749
1750        let client = AwsHttpClient::from_mock(mock);
1751        let body = crate::types::ec2::EnableSnapshotBlockPublicAccessRequest {
1752            state: "block-all-sharing".into(),
1753        };
1754        let response = client
1755            .ec2()
1756            .enable_snapshot_block_public_access(&body)
1757            .await
1758            .unwrap();
1759        assert_eq!(response.state.as_deref(), Some("block-all-sharing"));
1760    }
1761
1762    #[tokio::test]
1763    async fn enable_image_block_public_access_returns_state() {
1764        let mut mock = MockClient::new();
1765        mock.expect_enable_image_block_public_access()
1766            .returning_bytes(ec2_response(
1767                "EnableImageBlockPublicAccess",
1768                "<imageBlockPublicAccessState>block-new-sharing</imageBlockPublicAccessState>",
1769            ));
1770
1771        let client = AwsHttpClient::from_mock(mock);
1772        let body = crate::types::ec2::EnableImageBlockPublicAccessRequest {
1773            image_block_public_access_state: "block-new-sharing".into(),
1774        };
1775        let response = client
1776            .ec2()
1777            .enable_image_block_public_access(&body)
1778            .await
1779            .unwrap();
1780        assert_eq!(
1781            response.image_block_public_access_state.as_deref(),
1782            Some("block-new-sharing")
1783        );
1784    }
1785
1786    // ── VPC Peering ────────────────────────────────────────────────────────
1787
1788    #[tokio::test]
1789    async fn describe_vpc_peering_connections_returns_connections() {
1790        let mut mock = MockClient::new();
1791        mock.expect_describe_vpc_peering_connections()
1792            .returning_bytes(ec2_response(
1793                "DescribeVpcPeeringConnections",
1794                "<vpcPeeringConnectionSet>\
1795                   <item>\
1796                     <vpcPeeringConnectionId>pcx-0a1b2c3d4e5f67890</vpcPeeringConnectionId>\
1797                     <status><code>active</code><message>Active</message></status>\
1798                     <accepterVpcInfo>\
1799                       <vpcId>vpc-0accepter</vpcId>\
1800                       <ownerId>111111111111</ownerId>\
1801                       <cidrBlock>10.1.0.0/16</cidrBlock>\
1802                       <region>eu-central-1</region>\
1803                     </accepterVpcInfo>\
1804                     <requesterVpcInfo>\
1805                       <vpcId>vpc-0requester</vpcId>\
1806                       <ownerId>222222222222</ownerId>\
1807                       <cidrBlock>10.2.0.0/16</cidrBlock>\
1808                       <region>eu-west-1</region>\
1809                     </requesterVpcInfo>\
1810                   </item>\
1811                 </vpcPeeringConnectionSet>",
1812            ));
1813
1814        let client = AwsHttpClient::from_mock(mock);
1815        let body = crate::types::ec2::DescribeVpcPeeringConnectionsRequest::default();
1816        let response = client
1817            .ec2()
1818            .describe_vpc_peering_connections(&body)
1819            .await
1820            .unwrap();
1821
1822        assert_eq!(response.vpc_peering_connections.len(), 1);
1823        let pcx = &response.vpc_peering_connections[0];
1824        assert_eq!(
1825            pcx.vpc_peering_connection_id.as_deref(),
1826            Some("pcx-0a1b2c3d4e5f67890")
1827        );
1828        let status = pcx.status.as_ref().expect("status should be set");
1829        assert_eq!(status.code.as_deref(), Some("active"));
1830        let accepter = pcx.accepter_vpc_info.as_ref().expect("accepter_vpc_info");
1831        assert_eq!(accepter.vpc_id.as_deref(), Some("vpc-0accepter"));
1832        assert_eq!(accepter.cidr_block.as_deref(), Some("10.1.0.0/16"));
1833        let requester = pcx.requester_vpc_info.as_ref().expect("requester_vpc_info");
1834        assert_eq!(requester.vpc_id.as_deref(), Some("vpc-0requester"));
1835        assert_eq!(requester.cidr_block.as_deref(), Some("10.2.0.0/16"));
1836    }
1837
1838    #[tokio::test]
1839    async fn describe_vpc_peering_connections_handles_empty() {
1840        let mut mock = MockClient::new();
1841        mock.expect_describe_vpc_peering_connections()
1842            .returning_bytes(ec2_response(
1843                "DescribeVpcPeeringConnections",
1844                "<vpcPeeringConnectionSet/>",
1845            ));
1846
1847        let client = AwsHttpClient::from_mock(mock);
1848        let body = crate::types::ec2::DescribeVpcPeeringConnectionsRequest::default();
1849        let response = client
1850            .ec2()
1851            .describe_vpc_peering_connections(&body)
1852            .await
1853            .unwrap();
1854
1855        assert!(response.vpc_peering_connections.is_empty());
1856    }
1857}