Skip to main content

azure_lite_rs/api/
networking.rs

1//! Azure Networking API client.
2//!
3//! Thin wrapper over generated ops. All URL construction and HTTP methods
4//! are in `ops::networking::NetworkingOps`. This layer adds:
5//! - Ergonomic method signatures (auto-injects subscription_id from client)
6
7use crate::{
8    AzureHttpClient, Result,
9    ops::networking::NetworkingOps,
10    types::networking::{
11        LoadBalancer, LoadBalancerCreateRequest, LoadBalancerListResult, NetworkSecurityGroup,
12        NetworkSecurityGroupCreateRequest, NetworkSecurityGroupListResult, SecurityRule,
13        SecurityRuleListResult, Subnet, SubnetListResult, VirtualNetwork,
14        VirtualNetworkCreateRequest, VirtualNetworkListResult,
15    },
16};
17
18/// Client for the Azure Networking API.
19///
20/// Wraps the raw [`NetworkingOps`] with ergonomic signatures that
21/// auto-inject `subscription_id` from the parent [`AzureHttpClient`].
22pub struct NetworkingClient<'a> {
23    ops: NetworkingOps<'a>,
24    client: &'a AzureHttpClient,
25}
26
27impl<'a> NetworkingClient<'a> {
28    /// Create a new Azure Networking API client.
29    pub(crate) fn new(client: &'a AzureHttpClient) -> Self {
30        Self {
31            ops: NetworkingOps::new(client),
32            client,
33        }
34    }
35
36    // --- VNet operations ---
37
38    /// Gets all virtual networks in a resource group.
39    pub async fn list_vnets(&self, resource_group_name: &str) -> Result<VirtualNetworkListResult> {
40        self.ops
41            .list_vnets(self.client.subscription_id(), resource_group_name)
42            .await
43    }
44
45    /// Gets all virtual networks in the subscription.
46    pub async fn list_vnets_all(&self) -> Result<VirtualNetworkListResult> {
47        self.ops.list_vnets_all(self.client.subscription_id()).await
48    }
49
50    /// Gets the specified virtual network.
51    pub async fn get_vnet(
52        &self,
53        resource_group_name: &str,
54        vnet_name: &str,
55    ) -> Result<VirtualNetwork> {
56        self.ops
57            .get_vnet(
58                self.client.subscription_id(),
59                resource_group_name,
60                vnet_name,
61                "",
62            )
63            .await
64    }
65
66    /// Creates or updates a virtual network.
67    pub async fn create_vnet(
68        &self,
69        resource_group_name: &str,
70        vnet_name: &str,
71        body: &VirtualNetworkCreateRequest,
72    ) -> Result<VirtualNetwork> {
73        self.ops
74            .create_vnet(
75                self.client.subscription_id(),
76                resource_group_name,
77                vnet_name,
78                body,
79            )
80            .await
81    }
82
83    /// Deletes the specified virtual network.
84    pub async fn delete_vnet(&self, resource_group_name: &str, vnet_name: &str) -> Result<()> {
85        self.ops
86            .delete_vnet(
87                self.client.subscription_id(),
88                resource_group_name,
89                vnet_name,
90            )
91            .await
92    }
93
94    /// Gets all subnets in a virtual network.
95    pub async fn list_subnets(
96        &self,
97        resource_group_name: &str,
98        vnet_name: &str,
99    ) -> Result<SubnetListResult> {
100        self.ops
101            .list_subnets(
102                self.client.subscription_id(),
103                resource_group_name,
104                vnet_name,
105            )
106            .await
107    }
108
109    /// Gets the specified subnet.
110    pub async fn get_subnet(
111        &self,
112        resource_group_name: &str,
113        vnet_name: &str,
114        subnet_name: &str,
115    ) -> Result<Subnet> {
116        self.ops
117            .get_subnet(
118                self.client.subscription_id(),
119                resource_group_name,
120                vnet_name,
121                subnet_name,
122                "",
123            )
124            .await
125    }
126
127    // --- NSG operations ---
128
129    /// Gets all network security groups in a resource group.
130    pub async fn list_nsgs(
131        &self,
132        resource_group_name: &str,
133    ) -> Result<NetworkSecurityGroupListResult> {
134        self.ops
135            .list_nsgs(self.client.subscription_id(), resource_group_name)
136            .await
137    }
138
139    /// Gets all network security groups in the subscription.
140    pub async fn list_nsgs_all(&self) -> Result<NetworkSecurityGroupListResult> {
141        self.ops.list_nsgs_all(self.client.subscription_id()).await
142    }
143
144    /// Gets the specified network security group.
145    pub async fn get_nsg(
146        &self,
147        resource_group_name: &str,
148        nsg_name: &str,
149    ) -> Result<NetworkSecurityGroup> {
150        self.ops
151            .get_nsg(
152                self.client.subscription_id(),
153                resource_group_name,
154                nsg_name,
155                "",
156            )
157            .await
158    }
159
160    /// Creates or updates a network security group.
161    pub async fn create_nsg(
162        &self,
163        resource_group_name: &str,
164        nsg_name: &str,
165        body: &NetworkSecurityGroupCreateRequest,
166    ) -> Result<NetworkSecurityGroup> {
167        self.ops
168            .create_nsg(
169                self.client.subscription_id(),
170                resource_group_name,
171                nsg_name,
172                body,
173            )
174            .await
175    }
176
177    /// Deletes the specified network security group.
178    pub async fn delete_nsg(&self, resource_group_name: &str, nsg_name: &str) -> Result<()> {
179        self.ops
180            .delete_nsg(self.client.subscription_id(), resource_group_name, nsg_name)
181            .await
182    }
183
184    /// Gets all security rules in a network security group.
185    pub async fn list_security_rules(
186        &self,
187        resource_group_name: &str,
188        nsg_name: &str,
189    ) -> Result<SecurityRuleListResult> {
190        self.ops
191            .list_security_rules(self.client.subscription_id(), resource_group_name, nsg_name)
192            .await
193    }
194
195    /// Gets the specified network security rule.
196    pub async fn get_security_rule(
197        &self,
198        resource_group_name: &str,
199        nsg_name: &str,
200        rule_name: &str,
201    ) -> Result<SecurityRule> {
202        self.ops
203            .get_security_rule(
204                self.client.subscription_id(),
205                resource_group_name,
206                nsg_name,
207                rule_name,
208            )
209            .await
210    }
211
212    /// Creates or updates a security rule in a network security group.
213    pub async fn create_security_rule(
214        &self,
215        resource_group_name: &str,
216        nsg_name: &str,
217        rule_name: &str,
218        body: &SecurityRule,
219    ) -> Result<SecurityRule> {
220        self.ops
221            .create_security_rule(
222                self.client.subscription_id(),
223                resource_group_name,
224                nsg_name,
225                rule_name,
226                body,
227            )
228            .await
229    }
230
231    /// Deletes the specified network security rule.
232    pub async fn delete_security_rule(
233        &self,
234        resource_group_name: &str,
235        nsg_name: &str,
236        rule_name: &str,
237    ) -> Result<()> {
238        self.ops
239            .delete_security_rule(
240                self.client.subscription_id(),
241                resource_group_name,
242                nsg_name,
243                rule_name,
244            )
245            .await
246    }
247
248    // --- Load Balancer operations ---
249
250    /// Gets all the load balancers in a resource group.
251    pub async fn list_load_balancers(
252        &self,
253        resource_group_name: &str,
254    ) -> Result<LoadBalancerListResult> {
255        self.ops
256            .list_load_balancers(self.client.subscription_id(), resource_group_name)
257            .await
258    }
259
260    /// Gets all the load balancers in the subscription.
261    pub async fn list_load_balancers_all(&self) -> Result<LoadBalancerListResult> {
262        self.ops
263            .list_load_balancers_all(self.client.subscription_id())
264            .await
265    }
266
267    /// Gets the specified load balancer.
268    pub async fn get_load_balancer(
269        &self,
270        resource_group_name: &str,
271        lb_name: &str,
272    ) -> Result<LoadBalancer> {
273        self.ops
274            .get_load_balancer(
275                self.client.subscription_id(),
276                resource_group_name,
277                lb_name,
278                "",
279            )
280            .await
281    }
282
283    /// Creates or updates a load balancer.
284    pub async fn create_load_balancer(
285        &self,
286        resource_group_name: &str,
287        lb_name: &str,
288        body: &LoadBalancerCreateRequest,
289    ) -> Result<LoadBalancer> {
290        self.ops
291            .create_load_balancer(
292                self.client.subscription_id(),
293                resource_group_name,
294                lb_name,
295                body,
296            )
297            .await
298    }
299
300    /// Deletes the specified load balancer.
301    pub async fn delete_load_balancer(
302        &self,
303        resource_group_name: &str,
304        lb_name: &str,
305    ) -> Result<()> {
306        self.ops
307            .delete_load_balancer(self.client.subscription_id(), resource_group_name, lb_name)
308            .await
309    }
310
311    // --- Network Interface operations ---
312
313    /// Deletes the specified network interface.
314    pub async fn delete_network_interface(
315        &self,
316        resource_group_name: &str,
317        nic_name: &str,
318    ) -> Result<()> {
319        self.ops
320            .delete_network_interface(self.client.subscription_id(), resource_group_name, nic_name)
321            .await
322    }
323
324    // --- NAT Gateway operations ---
325
326    /// Deletes the specified NAT gateway.
327    pub async fn delete_nat_gateway(
328        &self,
329        resource_group_name: &str,
330        nat_gateway_name: &str,
331    ) -> Result<()> {
332        self.ops
333            .delete_nat_gateway(
334                self.client.subscription_id(),
335                resource_group_name,
336                nat_gateway_name,
337            )
338            .await
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    // ---- VNet tests ----
347
348    #[tokio::test]
349    async fn list_vnets_returns_empty_list() {
350        let mut mock = crate::MockClient::new();
351        mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks")
352            .returning_json(serde_json::json!({ "value": [] }));
353
354        let client = AzureHttpClient::from_mock(mock);
355        let result = client
356            .networking()
357            .list_vnets("my-rg")
358            .await
359            .expect("list_vnets failed");
360        assert!(result.value.is_empty());
361        assert!(result.next_link.is_none());
362    }
363
364    #[tokio::test]
365    async fn list_vnets_returns_vnet_with_fields() {
366        let mut mock = crate::MockClient::new();
367        mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks")
368            .returning_json(serde_json::json!({
369                "value": [{
370                    "id": "/subscriptions/sub/resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/vnet1",
371                    "name": "vnet1",
372                    "type": "Microsoft.Network/virtualNetworks",
373                    "location": "eastus",
374                    "properties": {
375                        "provisioningState": "Succeeded",
376                        "addressSpace": { "addressPrefixes": ["10.0.0.0/16"] }
377                    }
378                }]
379            }));
380
381        let client = AzureHttpClient::from_mock(mock);
382        let result = client
383            .networking()
384            .list_vnets("my-rg")
385            .await
386            .expect("list_vnets failed");
387        assert_eq!(result.value.len(), 1);
388        let vnet = &result.value[0];
389        assert_eq!(vnet.name.as_deref(), Some("vnet1"));
390        assert_eq!(vnet.location.as_deref(), Some("eastus"));
391        assert_eq!(
392            vnet.properties
393                .as_ref()
394                .and_then(|p| p.provisioning_state.as_deref()),
395            Some("Succeeded"),
396        );
397        assert_eq!(
398            vnet.properties
399                .as_ref()
400                .and_then(|p| p.address_space.as_ref())
401                .map(|a| a.address_prefixes.as_slice()),
402            Some(["10.0.0.0/16".to_string()].as_slice()),
403        );
404    }
405
406    #[tokio::test]
407    async fn list_vnets_all_uses_subscription_scope() {
408        let mut mock = crate::MockClient::new();
409        mock.expect_get(
410            "/subscriptions/test-subscription-id/providers/Microsoft.Network/virtualNetworks",
411        )
412        .returning_json(serde_json::json!({ "value": [] }));
413
414        let client = AzureHttpClient::from_mock(mock);
415        let result = client
416            .networking()
417            .list_vnets_all()
418            .await
419            .expect("list_vnets_all failed");
420        assert!(result.value.is_empty());
421    }
422
423    #[tokio::test]
424    async fn get_vnet_injects_subscription_id() {
425        let mut mock = crate::MockClient::new();
426        mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/my-vnet")
427            .returning_json(serde_json::json!({
428                "id": "/subscriptions/sub/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/my-vnet",
429                "name": "my-vnet",
430                "type": "Microsoft.Network/virtualNetworks",
431                "location": "westus2",
432                "properties": {
433                    "provisioningState": "Succeeded",
434                    "addressSpace": { "addressPrefixes": ["10.1.0.0/16"] }
435                }
436            }));
437
438        let client = AzureHttpClient::from_mock(mock);
439        let vnet = client
440            .networking()
441            .get_vnet("rg1", "my-vnet")
442            .await
443            .expect("get_vnet failed");
444        assert_eq!(vnet.name.as_deref(), Some("my-vnet"));
445        assert_eq!(vnet.location.as_deref(), Some("westus2"));
446        assert!(vnet.id.is_some());
447    }
448
449    #[tokio::test]
450    async fn create_vnet_sends_put_and_returns_vnet() {
451        let mut mock = crate::MockClient::new();
452        mock.expect_put("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/new-vnet")
453            .returning_json(serde_json::json!({
454                "name": "new-vnet",
455                "location": "eastus",
456                "properties": {
457                    "provisioningState": "Updating",
458                    "addressSpace": { "addressPrefixes": ["10.2.0.0/16"] }
459                }
460            }));
461
462        let client = AzureHttpClient::from_mock(mock);
463        let request = VirtualNetworkCreateRequest {
464            location: "eastus".into(),
465            properties: Some(crate::types::networking::VirtualNetworkPropertiesFormat {
466                address_space: Some(crate::types::networking::AddressSpace {
467                    address_prefixes: vec!["10.2.0.0/16".into()],
468                }),
469                ..Default::default()
470            }),
471            ..Default::default()
472        };
473        let vnet = client
474            .networking()
475            .create_vnet("rg1", "new-vnet", &request)
476            .await
477            .expect("create_vnet failed");
478        assert_eq!(vnet.name.as_deref(), Some("new-vnet"));
479        assert_eq!(
480            vnet.properties
481                .as_ref()
482                .and_then(|p| p.provisioning_state.as_deref()),
483            Some("Updating"),
484        );
485    }
486
487    #[tokio::test]
488    async fn delete_vnet_sends_delete() {
489        let mut mock = crate::MockClient::new();
490        mock.expect_delete("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/old-vnet")
491            .returning_json(serde_json::json!(null));
492
493        let client = AzureHttpClient::from_mock(mock);
494        client
495            .networking()
496            .delete_vnet("rg1", "old-vnet")
497            .await
498            .expect("delete_vnet failed");
499    }
500
501    #[tokio::test]
502    async fn list_subnets_returns_subnet_list() {
503        let mut mock = crate::MockClient::new();
504        mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets")
505            .returning_json(serde_json::json!({
506                "value": [{
507                    "id": "/subscriptions/sub/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/default",
508                    "name": "default",
509                    "properties": { "addressPrefix": "10.0.0.0/24", "provisioningState": "Succeeded" }
510                }]
511            }));
512
513        let client = AzureHttpClient::from_mock(mock);
514        let result = client
515            .networking()
516            .list_subnets("rg1", "my-vnet")
517            .await
518            .expect("list_subnets failed");
519        assert_eq!(result.value.len(), 1);
520        assert_eq!(result.value[0].name.as_deref(), Some("default"));
521        assert_eq!(
522            result.value[0]
523                .properties
524                .as_ref()
525                .and_then(|p| p.address_prefix.as_deref()),
526            Some("10.0.0.0/24"),
527        );
528    }
529
530    #[tokio::test]
531    async fn get_subnet_returns_address_prefix() {
532        let mut mock = crate::MockClient::new();
533        mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/default")
534            .returning_json(serde_json::json!({
535                "id": "/subscriptions/sub/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/default",
536                "name": "default",
537                "properties": { "addressPrefix": "10.0.0.0/24", "provisioningState": "Succeeded" }
538            }));
539
540        let client = AzureHttpClient::from_mock(mock);
541        let subnet = client
542            .networking()
543            .get_subnet("rg1", "my-vnet", "default")
544            .await
545            .expect("get_subnet failed");
546        assert_eq!(subnet.name.as_deref(), Some("default"));
547        assert_eq!(
548            subnet
549                .properties
550                .as_ref()
551                .and_then(|p| p.address_prefix.as_deref()),
552            Some("10.0.0.0/24"),
553        );
554    }
555
556    // ---- NSG tests ----
557
558    #[tokio::test]
559    async fn list_nsgs_returns_nsg_list() {
560        let mut mock = crate::MockClient::new();
561        mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Network/networkSecurityGroups")
562            .returning_json(serde_json::json!({
563                "value": [{
564                    "id": "/subscriptions/sub/resourceGroups/rg1/providers/Microsoft.Network/networkSecurityGroups/my-nsg",
565                    "name": "my-nsg",
566                    "type": "Microsoft.Network/networkSecurityGroups",
567                    "location": "eastus",
568                    "properties": {
569                        "provisioningState": "Succeeded",
570                        "securityRules": [],
571                        "defaultSecurityRules": []
572                    }
573                }]
574            }));
575
576        let client = AzureHttpClient::from_mock(mock);
577        let result = client
578            .networking()
579            .list_nsgs("rg1")
580            .await
581            .expect("list_nsgs failed");
582        assert_eq!(result.value.len(), 1);
583        let nsg = &result.value[0];
584        assert_eq!(nsg.name.as_deref(), Some("my-nsg"));
585        assert_eq!(nsg.location.as_deref(), Some("eastus"));
586        assert_eq!(
587            nsg.properties
588                .as_ref()
589                .and_then(|p| p.provisioning_state.as_deref()),
590            Some("Succeeded"),
591        );
592    }
593
594    #[tokio::test]
595    async fn list_nsgs_all_uses_subscription_scope() {
596        let mut mock = crate::MockClient::new();
597        mock.expect_get(
598            "/subscriptions/test-subscription-id/providers/Microsoft.Network/networkSecurityGroups",
599        )
600        .returning_json(serde_json::json!({ "value": [] }));
601
602        let client = AzureHttpClient::from_mock(mock);
603        let result = client
604            .networking()
605            .list_nsgs_all()
606            .await
607            .expect("list_nsgs_all failed");
608        assert!(result.value.is_empty());
609    }
610
611    #[tokio::test]
612    async fn get_nsg_returns_nsg_with_default_rules() {
613        let mut mock = crate::MockClient::new();
614        mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Network/networkSecurityGroups/my-nsg")
615            .returning_json(serde_json::json!({
616                "id": "/subscriptions/sub/resourceGroups/rg1/providers/Microsoft.Network/networkSecurityGroups/my-nsg",
617                "name": "my-nsg",
618                "location": "eastus",
619                "properties": {
620                    "provisioningState": "Succeeded",
621                    "securityRules": [],
622                    "defaultSecurityRules": [
623                        {
624                            "name": "AllowVnetInBound",
625                            "properties": {
626                                "priority": 65000,
627                                "access": "Allow",
628                                "direction": "Inbound",
629                                "protocol": "*"
630                            }
631                        }
632                    ]
633                }
634            }));
635
636        let client = AzureHttpClient::from_mock(mock);
637        let nsg = client
638            .networking()
639            .get_nsg("rg1", "my-nsg")
640            .await
641            .expect("get_nsg failed");
642        assert_eq!(nsg.name.as_deref(), Some("my-nsg"));
643        assert!(nsg.id.is_some());
644        let default_rules = &nsg.properties.as_ref().unwrap().default_security_rules;
645        assert_eq!(default_rules.len(), 1);
646        assert_eq!(default_rules[0].name.as_deref(), Some("AllowVnetInBound"));
647    }
648
649    #[tokio::test]
650    async fn create_nsg_sends_put_and_returns_nsg() {
651        let mut mock = crate::MockClient::new();
652        mock.expect_put("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Network/networkSecurityGroups/new-nsg")
653            .returning_json(serde_json::json!({
654                "name": "new-nsg",
655                "location": "eastus",
656                "properties": {
657                    "provisioningState": "Updating",
658                    "securityRules": [],
659                    "defaultSecurityRules": []
660                }
661            }));
662
663        let client = AzureHttpClient::from_mock(mock);
664        let request = NetworkSecurityGroupCreateRequest {
665            location: "eastus".into(),
666            ..Default::default()
667        };
668        let nsg = client
669            .networking()
670            .create_nsg("rg1", "new-nsg", &request)
671            .await
672            .expect("create_nsg failed");
673        assert_eq!(nsg.name.as_deref(), Some("new-nsg"));
674        assert_eq!(
675            nsg.properties
676                .as_ref()
677                .and_then(|p| p.provisioning_state.as_deref()),
678            Some("Updating"),
679        );
680    }
681
682    #[tokio::test]
683    async fn delete_nsg_sends_delete() {
684        let mut mock = crate::MockClient::new();
685        mock.expect_delete("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Network/networkSecurityGroups/old-nsg")
686            .returning_json(serde_json::json!(null));
687
688        let client = AzureHttpClient::from_mock(mock);
689        client
690            .networking()
691            .delete_nsg("rg1", "old-nsg")
692            .await
693            .expect("delete_nsg failed");
694    }
695
696    // ---- Security Rule tests ----
697
698    #[tokio::test]
699    async fn list_security_rules_returns_empty_initially() {
700        let mut mock = crate::MockClient::new();
701        mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Network/networkSecurityGroups/my-nsg/securityRules")
702            .returning_json(serde_json::json!({ "value": [] }));
703
704        let client = AzureHttpClient::from_mock(mock);
705        let result = client
706            .networking()
707            .list_security_rules("rg1", "my-nsg")
708            .await
709            .expect("list_security_rules failed");
710        assert!(result.value.is_empty());
711    }
712
713    #[tokio::test]
714    async fn list_security_rules_returns_rule_with_fields() {
715        let mut mock = crate::MockClient::new();
716        mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Network/networkSecurityGroups/my-nsg/securityRules")
717            .returning_json(serde_json::json!({
718                "value": [{
719                    "name": "allow-http",
720                    "properties": {
721                        "priority": 100,
722                        "protocol": "Tcp",
723                        "access": "Allow",
724                        "direction": "Inbound",
725                        "sourcePortRange": "*",
726                        "destinationPortRange": "80",
727                        "sourceAddressPrefix": "*",
728                        "destinationAddressPrefix": "*",
729                        "provisioningState": "Succeeded"
730                    }
731                }]
732            }));
733
734        let client = AzureHttpClient::from_mock(mock);
735        let result = client
736            .networking()
737            .list_security_rules("rg1", "my-nsg")
738            .await
739            .expect("list_security_rules failed");
740        assert_eq!(result.value.len(), 1);
741        let rule = &result.value[0];
742        assert_eq!(rule.name.as_deref(), Some("allow-http"));
743        assert_eq!(rule.properties.as_ref().and_then(|p| p.priority), Some(100),);
744        assert_eq!(
745            rule.properties.as_ref().and_then(|p| p.access.as_deref()),
746            Some("Allow"),
747        );
748        assert_eq!(
749            rule.properties
750                .as_ref()
751                .and_then(|p| p.destination_port_range.as_deref()),
752            Some("80"),
753        );
754    }
755
756    #[tokio::test]
757    async fn get_security_rule_returns_rule() {
758        let mut mock = crate::MockClient::new();
759        mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Network/networkSecurityGroups/my-nsg/securityRules/my-rule")
760            .returning_json(serde_json::json!({
761                "name": "my-rule",
762                "properties": {
763                    "priority": 200,
764                    "protocol": "Tcp",
765                    "access": "Deny",
766                    "direction": "Outbound",
767                    "sourcePortRange": "*",
768                    "destinationPortRange": "443",
769                    "sourceAddressPrefix": "*",
770                    "destinationAddressPrefix": "Internet"
771                }
772            }));
773
774        let client = AzureHttpClient::from_mock(mock);
775        let rule = client
776            .networking()
777            .get_security_rule("rg1", "my-nsg", "my-rule")
778            .await
779            .expect("get_security_rule failed");
780        assert_eq!(rule.name.as_deref(), Some("my-rule"));
781        assert_eq!(
782            rule.properties
783                .as_ref()
784                .and_then(|p| p.direction.as_deref()),
785            Some("Outbound"),
786        );
787        assert_eq!(
788            rule.properties.as_ref().and_then(|p| p.access.as_deref()),
789            Some("Deny"),
790        );
791        assert_eq!(
792            rule.properties
793                .as_ref()
794                .and_then(|p| p.destination_port_range.as_deref()),
795            Some("443"),
796        );
797    }
798
799    #[tokio::test]
800    async fn create_security_rule_sends_put_and_returns_rule() {
801        let mut mock = crate::MockClient::new();
802        mock.expect_put("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Network/networkSecurityGroups/my-nsg/securityRules/new-rule")
803            .returning_json(serde_json::json!({
804                "name": "new-rule",
805                "properties": {
806                    "priority": 300,
807                    "protocol": "Tcp",
808                    "access": "Allow",
809                    "direction": "Inbound",
810                    "sourcePortRange": "*",
811                    "destinationPortRange": "8080",
812                    "sourceAddressPrefix": "*",
813                    "destinationAddressPrefix": "*",
814                    "provisioningState": "Succeeded"
815                }
816            }));
817
818        let client = AzureHttpClient::from_mock(mock);
819        let body = SecurityRule {
820            name: Some("new-rule".into()),
821            properties: Some(crate::types::networking::SecurityRulePropertiesFormat {
822                protocol: Some("Tcp".into()),
823                source_port_range: Some("*".into()),
824                destination_port_range: Some("8080".into()),
825                source_address_prefix: Some("*".into()),
826                destination_address_prefix: Some("*".into()),
827                access: Some("Allow".into()),
828                priority: Some(300),
829                direction: Some("Inbound".into()),
830                ..Default::default()
831            }),
832            ..Default::default()
833        };
834        let rule = client
835            .networking()
836            .create_security_rule("rg1", "my-nsg", "new-rule", &body)
837            .await
838            .expect("create_security_rule failed");
839        assert_eq!(rule.name.as_deref(), Some("new-rule"));
840        assert_eq!(rule.properties.as_ref().and_then(|p| p.priority), Some(300),);
841        assert_eq!(
842            rule.properties
843                .as_ref()
844                .and_then(|p| p.provisioning_state.as_deref()),
845            Some("Succeeded"),
846        );
847    }
848
849    #[tokio::test]
850    async fn delete_security_rule_sends_delete() {
851        let mut mock = crate::MockClient::new();
852        mock.expect_delete("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Network/networkSecurityGroups/my-nsg/securityRules/old-rule")
853            .returning_json(serde_json::json!(null));
854
855        let client = AzureHttpClient::from_mock(mock);
856        client
857            .networking()
858            .delete_security_rule("rg1", "my-nsg", "old-rule")
859            .await
860            .expect("delete_security_rule failed");
861    }
862
863    // ---- Load Balancer tests ----
864
865    #[tokio::test]
866    async fn list_load_balancers_returns_empty_list() {
867        let mut mock = crate::MockClient::new();
868        mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Network/loadBalancers")
869            .returning_json(serde_json::json!({ "value": [] }));
870
871        let client = AzureHttpClient::from_mock(mock);
872        let result = client
873            .networking()
874            .list_load_balancers("rg1")
875            .await
876            .expect("list_load_balancers failed");
877        assert!(result.value.is_empty());
878    }
879
880    #[tokio::test]
881    async fn list_load_balancers_returns_lb_with_standard_sku() {
882        let mut mock = crate::MockClient::new();
883        mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Network/loadBalancers")
884            .returning_json(serde_json::json!({
885                "value": [{
886                    "id": "/subscriptions/sub/resourceGroups/rg1/providers/Microsoft.Network/loadBalancers/my-lb",
887                    "name": "my-lb",
888                    "type": "Microsoft.Network/loadBalancers",
889                    "location": "eastus",
890                    "sku": { "name": "Standard", "tier": "Regional" },
891                    "properties": {
892                        "provisioningState": "Succeeded",
893                        "frontendIPConfigurations": [],
894                        "backendAddressPools": []
895                    }
896                }]
897            }));
898
899        let client = AzureHttpClient::from_mock(mock);
900        let result = client
901            .networking()
902            .list_load_balancers("rg1")
903            .await
904            .expect("list_load_balancers failed");
905        assert_eq!(result.value.len(), 1);
906        let lb = &result.value[0];
907        assert_eq!(lb.name.as_deref(), Some("my-lb"));
908        assert_eq!(lb.location.as_deref(), Some("eastus"));
909        assert_eq!(
910            lb.sku.as_ref().and_then(|s| s.name.as_deref()),
911            Some("Standard")
912        );
913        assert_eq!(
914            lb.properties
915                .as_ref()
916                .and_then(|p| p.provisioning_state.as_deref()),
917            Some("Succeeded"),
918        );
919    }
920
921    #[tokio::test]
922    async fn list_load_balancers_all_uses_subscription_scope() {
923        let mut mock = crate::MockClient::new();
924        mock.expect_get(
925            "/subscriptions/test-subscription-id/providers/Microsoft.Network/loadBalancers",
926        )
927        .returning_json(serde_json::json!({ "value": [] }));
928
929        let client = AzureHttpClient::from_mock(mock);
930        let result = client
931            .networking()
932            .list_load_balancers_all()
933            .await
934            .expect("list_load_balancers_all failed");
935        assert!(result.value.is_empty());
936    }
937
938    #[tokio::test]
939    async fn get_load_balancer_deserializes_frontend_ips() {
940        let mut mock = crate::MockClient::new();
941        mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Network/loadBalancers/my-lb")
942            .returning_json(serde_json::json!({
943                "id": "/subscriptions/sub/resourceGroups/rg1/providers/Microsoft.Network/loadBalancers/my-lb",
944                "name": "my-lb",
945                "location": "eastus",
946                "sku": { "name": "Standard" },
947                "properties": {
948                    "provisioningState": "Succeeded",
949                    "frontendIPConfigurations": [{
950                        "name": "frontend",
951                        "properties": {
952                            "privateIPAllocationMethod": "Dynamic",
953                            "provisioningState": "Succeeded"
954                        }
955                    }]
956                }
957            }));
958
959        let client = AzureHttpClient::from_mock(mock);
960        let lb = client
961            .networking()
962            .get_load_balancer("rg1", "my-lb")
963            .await
964            .expect("get_load_balancer failed");
965        assert_eq!(lb.name.as_deref(), Some("my-lb"));
966        assert!(lb.id.is_some());
967        let frontend_ips = &lb.properties.as_ref().unwrap().frontend_ip_configurations;
968        assert_eq!(frontend_ips.len(), 1);
969        assert_eq!(frontend_ips[0].name.as_deref(), Some("frontend"));
970        assert_eq!(
971            frontend_ips[0]
972                .properties
973                .as_ref()
974                .and_then(|p| p.private_ip_allocation_method.as_deref()),
975            Some("Dynamic"),
976        );
977    }
978
979    #[tokio::test]
980    async fn create_load_balancer_sends_put_and_returns_lb() {
981        let mut mock = crate::MockClient::new();
982        mock.expect_put("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Network/loadBalancers/new-lb")
983            .returning_json(serde_json::json!({
984                "name": "new-lb",
985                "location": "eastus",
986                "sku": { "name": "Standard" },
987                "properties": {
988                    "provisioningState": "Succeeded",
989                    "frontendIPConfigurations": []
990                }
991            }));
992
993        let client = AzureHttpClient::from_mock(mock);
994        let request = LoadBalancerCreateRequest {
995            location: "eastus".into(),
996            sku: Some(crate::types::networking::LoadBalancerSku {
997                name: Some("Standard".into()),
998                ..Default::default()
999            }),
1000            ..Default::default()
1001        };
1002        let lb = client
1003            .networking()
1004            .create_load_balancer("rg1", "new-lb", &request)
1005            .await
1006            .expect("create_load_balancer failed");
1007        assert_eq!(lb.name.as_deref(), Some("new-lb"));
1008        assert_eq!(
1009            lb.properties
1010                .as_ref()
1011                .and_then(|p| p.provisioning_state.as_deref()),
1012            Some("Succeeded"),
1013        );
1014    }
1015
1016    #[tokio::test]
1017    async fn delete_load_balancer_sends_delete() {
1018        let mut mock = crate::MockClient::new();
1019        mock.expect_delete("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Network/loadBalancers/old-lb")
1020            .returning_json(serde_json::json!(null));
1021
1022        let client = AzureHttpClient::from_mock(mock);
1023        client
1024            .networking()
1025            .delete_load_balancer("rg1", "old-lb")
1026            .await
1027            .expect("delete_load_balancer failed");
1028    }
1029
1030    // ---- Network Interface tests ----
1031
1032    #[tokio::test]
1033    async fn delete_network_interface_sends_delete() {
1034        let mut mock = crate::MockClient::new();
1035        mock.expect_delete("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Network/networkInterfaces/old-nic")
1036            .returning_json(serde_json::json!(null));
1037
1038        let client = AzureHttpClient::from_mock(mock);
1039        client
1040            .networking()
1041            .delete_network_interface("rg1", "old-nic")
1042            .await
1043            .expect("delete_network_interface failed");
1044    }
1045
1046    // ---- NAT Gateway tests ----
1047
1048    #[tokio::test]
1049    async fn delete_nat_gateway_sends_delete() {
1050        let mut mock = crate::MockClient::new();
1051        mock.expect_delete("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Network/natGateways/old-natgw")
1052            .returning_json(serde_json::json!(null));
1053
1054        let client = AzureHttpClient::from_mock(mock);
1055        client
1056            .networking()
1057            .delete_nat_gateway("rg1", "old-natgw")
1058            .await
1059            .expect("delete_nat_gateway failed");
1060    }
1061}