This is an automated email from the ASF dual-hosted git repository.

vishesh pushed a commit to branch add-unit-tests
in repository 
https://gitbox.apache.org/repos/asf/cloudstack-kubernetes-provider.git

commit 0874e77d50eedf2248c9409a546088376447ceae
Author: vishesh92 <[email protected]>
AuthorDate: Mon Dec 15 13:05:47 2025 +0530

    add some unit tests
---
 cloudstack_instances_test.go    |   75 +
 cloudstack_loadbalancer_test.go | 2924 +++++++++++++++++++++++++++++++++++++++
 cloudstack_test.go              |   40 +
 3 files changed, 3039 insertions(+)

diff --git a/cloudstack_instances_test.go b/cloudstack_instances_test.go
index 4210e305..791d5617 100644
--- a/cloudstack_instances_test.go
+++ b/cloudstack_instances_test.go
@@ -174,3 +174,78 @@ func TestNodeAddresses(t *testing.T) {
                })
        }
 }
+
+func TestGetProviderIDFromInstanceID(t *testing.T) {
+       cs := &CSCloud{}
+
+       tests := []struct {
+               name       string
+               instanceID string
+               want       string
+       }{
+               {
+                       name:       "valid instance ID",
+                       instanceID: "vm-123",
+                       want:       "external-cloudstack://vm-123",
+               },
+               {
+                       name:       "empty instance ID",
+                       instanceID: "",
+                       want:       "external-cloudstack://",
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       got := cs.getProviderIDFromInstanceID(tt.instanceID)
+                       if got != tt.want {
+                               t.Errorf("getProviderIDFromInstanceID(%q) = %q, 
want %q", tt.instanceID, got, tt.want)
+                       }
+               })
+       }
+}
+
+func TestGetInstanceIDFromProviderID(t *testing.T) {
+       cs := &CSCloud{}
+
+       tests := []struct {
+               name       string
+               providerID string
+               want       string
+       }{
+               {
+                       name:       "full provider ID format",
+                       providerID: "external-cloudstack://vm-123",
+                       want:       "vm-123",
+               },
+               {
+                       name:       "instance ID only - backward compatibility",
+                       providerID: "vm-123",
+                       want:       "vm-123",
+               },
+               {
+                       name:       "empty string",
+                       providerID: "",
+                       want:       "",
+               },
+               {
+                       name:       "invalid format - no separator",
+                       providerID: "external-cloudstack-vm-123",
+                       want:       "external-cloudstack-vm-123",
+               },
+               {
+                       name:       "different provider prefix",
+                       providerID: "aws://i-1234567890abcdef0",
+                       want:       "i-1234567890abcdef0",
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       got := cs.getInstanceIDFromProviderID(tt.providerID)
+                       if got != tt.want {
+                               t.Errorf("getInstanceIDFromProviderID(%q) = %q, 
want %q", tt.providerID, got, tt.want)
+                       }
+               })
+       }
+}
diff --git a/cloudstack_loadbalancer_test.go b/cloudstack_loadbalancer_test.go
index 847361a0..b863fc17 100644
--- a/cloudstack_loadbalancer_test.go
+++ b/cloudstack_loadbalancer_test.go
@@ -20,6 +20,7 @@
 package cloudstack
 
 import (
+       "fmt"
        "reflect"
        "sort"
        "strings"
@@ -764,3 +765,2926 @@ func TestCheckLoadBalancerRule(t *testing.T) {
                }
        })
 }
+
+func TestRuleToString(t *testing.T) {
+       tests := []struct {
+               name string
+               rule *cloudstack.FirewallRule
+               want string
+       }{
+               {
+                       name: "TCP rule",
+                       rule: &cloudstack.FirewallRule{
+                               Protocol:  "tcp",
+                               Cidrlist:  "10.0.0.0/8",
+                               Ipaddress: "203.0.113.1",
+                               Startport: 80,
+                               Endport:   80,
+                       },
+                       want: "{[10.0.0.0/8] -> 203.0.113.1:[80-80] (tcp)}",
+               },
+               {
+                       name: "UDP rule",
+                       rule: &cloudstack.FirewallRule{
+                               Protocol:  "udp",
+                               Cidrlist:  "192.168.0.0/16",
+                               Ipaddress: "203.0.113.2",
+                               Startport: 53,
+                               Endport:   53,
+                       },
+                       want: "{[192.168.0.0/16] -> 203.0.113.2:[53-53] (udp)}",
+               },
+               {
+                       name: "TCP rule with port range",
+                       rule: &cloudstack.FirewallRule{
+                               Protocol:  "tcp",
+                               Cidrlist:  "0.0.0.0/0",
+                               Ipaddress: "203.0.113.3",
+                               Startport: 8000,
+                               Endport:   8999,
+                       },
+                       want: "{[0.0.0.0/0] -> 203.0.113.3:[8000-8999] (tcp)}",
+               },
+               {
+                       name: "ICMP rule",
+                       rule: &cloudstack.FirewallRule{
+                               Protocol:  "icmp",
+                               Cidrlist:  "10.0.0.0/8",
+                               Ipaddress: "203.0.113.4",
+                               Icmptype:  8,
+                               Icmpcode:  0,
+                       },
+                       want: "{[10.0.0.0/8] -> 203.0.113.4 [8,0] (icmp)}",
+               },
+               {
+                       name: "unknown protocol",
+                       rule: &cloudstack.FirewallRule{
+                               Protocol:  "gre",
+                               Cidrlist:  "10.0.0.0/8",
+                               Ipaddress: "203.0.113.6",
+                       },
+                       want: "{[10.0.0.0/8] -> 203.0.113.6 (gre)}",
+               },
+               {
+                       name: "nil rule",
+                       rule: nil,
+                       want: "nil",
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       got := ruleToString(tt.rule)
+                       if got != tt.want {
+                               t.Errorf("ruleToString() = %q, want %q", got, 
tt.want)
+                       }
+               })
+       }
+}
+
+func TestRulesToString(t *testing.T) {
+       tests := []struct {
+               name  string
+               rules []*cloudstack.FirewallRule
+               want  string
+       }{
+               {
+                       name:  "empty list",
+                       rules: []*cloudstack.FirewallRule{},
+                       want:  "",
+               },
+               {
+                       name: "single rule",
+                       rules: []*cloudstack.FirewallRule{
+                               {
+                                       Protocol:  "tcp",
+                                       Cidrlist:  "10.0.0.0/8",
+                                       Ipaddress: "203.0.113.1",
+                                       Startport: 80,
+                                       Endport:   80,
+                               },
+                       },
+                       want: "{[10.0.0.0/8] -> 203.0.113.1:[80-80] (tcp)}",
+               },
+               {
+                       name: "multiple rules",
+                       rules: []*cloudstack.FirewallRule{
+                               {
+                                       Protocol:  "tcp",
+                                       Cidrlist:  "10.0.0.0/8",
+                                       Ipaddress: "203.0.113.1",
+                                       Startport: 80,
+                                       Endport:   80,
+                               },
+                               {
+                                       Protocol:  "udp",
+                                       Cidrlist:  "192.168.0.0/16",
+                                       Ipaddress: "203.0.113.2",
+                                       Startport: 53,
+                                       Endport:   53,
+                               },
+                               {
+                                       Protocol:  "icmp",
+                                       Cidrlist:  "0.0.0.0/0",
+                                       Ipaddress: "203.0.113.3",
+                                       Icmptype:  8,
+                                       Icmpcode:  0,
+                               },
+                       },
+                       want: "{[10.0.0.0/8] -> 203.0.113.1:[80-80] (tcp)}, 
{[192.168.0.0/16] -> 203.0.113.2:[53-53] (udp)}, {[0.0.0.0/0] -> 203.0.113.3 
[8,0] (icmp)}",
+               },
+               {
+                       name: "rules with nil rule",
+                       rules: []*cloudstack.FirewallRule{
+                               {
+                                       Protocol:  "tcp",
+                                       Cidrlist:  "10.0.0.0/8",
+                                       Ipaddress: "203.0.113.1",
+                                       Startport: 80,
+                                       Endport:   80,
+                               },
+                               nil,
+                               {
+                                       Protocol:  "udp",
+                                       Cidrlist:  "192.168.0.0/16",
+                                       Ipaddress: "203.0.113.2",
+                                       Startport: 53,
+                                       Endport:   53,
+                               },
+                       },
+                       want: "{[10.0.0.0/8] -> 203.0.113.1:[80-80] (tcp)}, 
nil, {[192.168.0.0/16] -> 203.0.113.2:[53-53] (udp)}",
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       got := rulesToString(tt.rules)
+                       if got != tt.want {
+                               t.Errorf("rulesToString() = %q, want %q", got, 
tt.want)
+                       }
+               })
+       }
+}
+
+func TestRulesMapToString(t *testing.T) {
+       tests := []struct {
+               name  string
+               rules map[*cloudstack.FirewallRule]bool
+               want  string
+       }{
+               {
+                       name:  "empty map",
+                       rules: map[*cloudstack.FirewallRule]bool{},
+                       want:  "",
+               },
+               {
+                       name: "single rule",
+                       rules: map[*cloudstack.FirewallRule]bool{
+                               {
+                                       Protocol:  "tcp",
+                                       Cidrlist:  "10.0.0.0/8",
+                                       Ipaddress: "203.0.113.1",
+                                       Startport: 80,
+                                       Endport:   80,
+                               }: true,
+                       },
+                       want: "{[10.0.0.0/8] -> 203.0.113.1:[80-80] (tcp)}",
+               },
+               {
+                       name: "multiple rules",
+                       rules: map[*cloudstack.FirewallRule]bool{
+                               {
+                                       Protocol:  "tcp",
+                                       Cidrlist:  "10.0.0.0/8",
+                                       Ipaddress: "203.0.113.1",
+                                       Startport: 80,
+                                       Endport:   80,
+                               }: true,
+                               {
+                                       Protocol:  "udp",
+                                       Cidrlist:  "192.168.0.0/16",
+                                       Ipaddress: "203.0.113.2",
+                                       Startport: 53,
+                                       Endport:   53,
+                               }: false,
+                               {
+                                       Protocol:  "icmp",
+                                       Cidrlist:  "0.0.0.0/0",
+                                       Ipaddress: "203.0.113.3",
+                                       Icmptype:  8,
+                                       Icmpcode:  0,
+                               }: true,
+                       },
+                       // Note: Map iteration order is non-deterministic, so 
we need to check
+                       // that all rules are present, not the exact order
+                       want: "", // We'll check this differently
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       got := rulesMapToString(tt.rules)
+
+                       if tt.want == "" {
+                               // For maps, we can't predict order, so check 
that all rules are present
+                               if len(tt.rules) == 0 {
+                                       if got != "" {
+                                               t.Errorf("rulesMapToString() = 
%q, want empty string", got)
+                                       }
+                                       return
+                               }
+
+                               // Check that all rules are present in the 
output
+                               expectedRules := make([]string, 0, 
len(tt.rules))
+                               for rule := range tt.rules {
+                                       expectedRules = append(expectedRules, 
ruleToString(rule))
+                               }
+
+                               // Split the output and check each rule is 
present
+                               if got != "" {
+                                       parts := strings.Split(got, ", ")
+                                       if len(parts) != len(expectedRules) {
+                                               t.Errorf("rulesMapToString() 
returned %d rules, want %d", len(parts), len(expectedRules))
+                                               return
+                                       }
+
+                                       // Check that all expected rules are in 
the output
+                                       for _, expectedRule := range 
expectedRules {
+                                               found := false
+                                               for _, part := range parts {
+                                                       if part == expectedRule 
{
+                                                               found = true
+                                                               break
+                                                       }
+                                               }
+                                               if !found {
+                                                       
t.Errorf("rulesMapToString() missing rule %q in output %q", expectedRule, got)
+                                               }
+                                       }
+                               } else if len(expectedRules) > 0 {
+                                       t.Errorf("rulesMapToString() = empty 
string, want rules to be present")
+                               }
+                       } else {
+                               if got != tt.want {
+                                       t.Errorf("rulesMapToString() = %q, want 
%q", got, tt.want)
+                               }
+                       }
+               })
+       }
+}
+
+func TestGetPublicIPAddress(t *testing.T) {
+       t.Run("IP found and allocated", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+               listParams := &cloudstack.ListPublicIpAddressesParams{}
+               resp := &cloudstack.ListPublicIpAddressesResponse{
+                       Count: 1,
+                       PublicIpAddresses: []*cloudstack.PublicIpAddress{
+                               {
+                                       Id:        "ip-123",
+                                       Ipaddress: "203.0.113.1",
+                                       Allocated: "2023-01-01T00:00:00+0000",
+                               },
+                       },
+               }
+
+               gomock.InOrder(
+                       
mockAddress.EXPECT().NewListPublicIpAddressesParams().Return(listParams),
+                       
mockAddress.EXPECT().ListPublicIpAddresses(gomock.Any()).Return(resp, nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Address: mockAddress,
+                       },
+               }
+
+               err := lb.getPublicIPAddress("203.0.113.1")
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if lb.ipAddr != "203.0.113.1" {
+                       t.Errorf("ipAddr = %q, want %q", lb.ipAddr, 
"203.0.113.1")
+               }
+               if lb.ipAddrID != "ip-123" {
+                       t.Errorf("ipAddrID = %q, want %q", lb.ipAddrID, 
"ip-123")
+               }
+       })
+
+       t.Run("IP found but not allocated - associates", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+               mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+               listParams := &cloudstack.ListPublicIpAddressesParams{}
+               resp := &cloudstack.ListPublicIpAddressesResponse{
+                       Count: 1,
+                       PublicIpAddresses: []*cloudstack.PublicIpAddress{
+                               {
+                                       Id:        "ip-123",
+                                       Ipaddress: "203.0.113.1",
+                                       Allocated: "",
+                               },
+                       },
+               }
+
+               networkResp := &cloudstack.Network{
+                       Id:      "net-123",
+                       Vpcid:   "",
+                       Service: []cloudstack.NetworkServiceInternal{},
+               }
+
+               associateParams := &cloudstack.AssociateIpAddressParams{}
+               associateResp := &cloudstack.AssociateIpAddressResponse{
+                       Id:        "ip-123",
+                       Ipaddress: "203.0.113.1",
+               }
+
+               gomock.InOrder(
+                       
mockAddress.EXPECT().NewListPublicIpAddressesParams().Return(listParams),
+                       
mockAddress.EXPECT().ListPublicIpAddresses(gomock.Any()).Return(resp, nil),
+                       mockNetwork.EXPECT().GetNetworkByID("net-123", 
gomock.Any()).Return(networkResp, 1, nil),
+                       
mockAddress.EXPECT().NewAssociateIpAddressParams().Return(associateParams),
+                       
mockAddress.EXPECT().AssociateIpAddress(gomock.Any()).Return(associateResp, 
nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Address: mockAddress,
+                               Network: mockNetwork,
+                       },
+                       networkID: "net-123",
+                       ipAddr:    "203.0.113.1",
+               }
+
+               err := lb.getPublicIPAddress("203.0.113.1")
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+       })
+
+       t.Run("IP not found", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+               listParams := &cloudstack.ListPublicIpAddressesParams{}
+               resp := &cloudstack.ListPublicIpAddressesResponse{
+                       Count:             0,
+                       PublicIpAddresses: []*cloudstack.PublicIpAddress{},
+               }
+
+               gomock.InOrder(
+                       
mockAddress.EXPECT().NewListPublicIpAddressesParams().Return(listParams),
+                       
mockAddress.EXPECT().ListPublicIpAddresses(gomock.Any()).Return(resp, nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Address: mockAddress,
+                       },
+               }
+
+               err := lb.getPublicIPAddress("203.0.113.1")
+               if err == nil {
+                       t.Fatalf("expected error for IP not found")
+               }
+               if !strings.Contains(err.Error(), "could not find IP address") {
+                       t.Errorf("error message = %q, want to contain 'could 
not find IP address'", err.Error())
+               }
+       })
+
+       t.Run("multiple IPs found", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+               listParams := &cloudstack.ListPublicIpAddressesParams{}
+               resp := &cloudstack.ListPublicIpAddressesResponse{
+                       Count: 2,
+                       PublicIpAddresses: []*cloudstack.PublicIpAddress{
+                               {Id: "ip-1", Ipaddress: "203.0.113.1"},
+                               {Id: "ip-2", Ipaddress: "203.0.113.1"},
+                       },
+               }
+
+               gomock.InOrder(
+                       
mockAddress.EXPECT().NewListPublicIpAddressesParams().Return(listParams),
+                       
mockAddress.EXPECT().ListPublicIpAddresses(gomock.Any()).Return(resp, nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Address: mockAddress,
+                       },
+               }
+
+               err := lb.getPublicIPAddress("203.0.113.1")
+               if err == nil {
+                       t.Fatalf("expected error for multiple IPs found")
+               }
+               if !strings.Contains(err.Error(), "Found 2 addresses") {
+                       t.Errorf("error message = %q, want to contain 'Found 2 
addresses'", err.Error())
+               }
+       })
+
+       t.Run("error retrieving IP", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+               listParams := &cloudstack.ListPublicIpAddressesParams{}
+               apiErr := fmt.Errorf("API error")
+
+               gomock.InOrder(
+                       
mockAddress.EXPECT().NewListPublicIpAddressesParams().Return(listParams),
+                       
mockAddress.EXPECT().ListPublicIpAddresses(gomock.Any()).Return(nil, apiErr),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Address: mockAddress,
+                       },
+               }
+
+               err := lb.getPublicIPAddress("203.0.113.1")
+               if err == nil {
+                       t.Fatalf("expected error")
+               }
+               if !strings.Contains(err.Error(), "error retrieving IP 
address") {
+                       t.Errorf("error message = %q, want to contain 'error 
retrieving IP address'", err.Error())
+               }
+       })
+
+       t.Run("project ID handling", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+               listParams := &cloudstack.ListPublicIpAddressesParams{}
+               resp := &cloudstack.ListPublicIpAddressesResponse{
+                       Count: 1,
+                       PublicIpAddresses: []*cloudstack.PublicIpAddress{
+                               {
+                                       Id:        "ip-123",
+                                       Ipaddress: "203.0.113.1",
+                                       Allocated: "2023-01-01T00:00:00+0000",
+                               },
+                       },
+               }
+
+               gomock.InOrder(
+                       
mockAddress.EXPECT().NewListPublicIpAddressesParams().Return(listParams),
+                       
mockAddress.EXPECT().ListPublicIpAddresses(gomock.Any()).Return(resp, nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Address: mockAddress,
+                       },
+                       projectID: "proj-123",
+               }
+
+               err := lb.getPublicIPAddress("203.0.113.1")
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+       })
+}
+
+func TestAssociatePublicIPAddress(t *testing.T) {
+       t.Run("associate IP for regular network", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+               mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+               networkResp := &cloudstack.Network{
+                       Id:      "net-123",
+                       Vpcid:   "",
+                       Service: []cloudstack.NetworkServiceInternal{},
+               }
+
+               associateParams := &cloudstack.AssociateIpAddressParams{}
+               associateResp := &cloudstack.AssociateIpAddressResponse{
+                       Id:        "ip-123",
+                       Ipaddress: "203.0.113.1",
+               }
+
+               gomock.InOrder(
+                       mockNetwork.EXPECT().GetNetworkByID("net-123", 
gomock.Any()).Return(networkResp, 1, nil),
+                       
mockAddress.EXPECT().NewAssociateIpAddressParams().Return(associateParams),
+                       
mockAddress.EXPECT().AssociateIpAddress(gomock.Any()).Return(associateResp, 
nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Address: mockAddress,
+                               Network: mockNetwork,
+                       },
+                       networkID: "net-123",
+               }
+
+               err := lb.associatePublicIPAddress()
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if lb.ipAddr != "203.0.113.1" {
+                       t.Errorf("ipAddr = %q, want %q", lb.ipAddr, 
"203.0.113.1")
+               }
+               if lb.ipAddrID != "ip-123" {
+                       t.Errorf("ipAddrID = %q, want %q", lb.ipAddrID, 
"ip-123")
+               }
+               if !lb.ipAssociatedByController {
+                       t.Errorf("ipAssociatedByController = false, want true")
+               }
+       })
+
+       t.Run("associate IP for VPC network", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+               mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+               networkResp := &cloudstack.Network{
+                       Id:      "net-123",
+                       Vpcid:   "vpc-456",
+                       Service: []cloudstack.NetworkServiceInternal{},
+               }
+
+               associateParams := &cloudstack.AssociateIpAddressParams{}
+               associateResp := &cloudstack.AssociateIpAddressResponse{
+                       Id:        "ip-123",
+                       Ipaddress: "203.0.113.1",
+               }
+
+               gomock.InOrder(
+                       mockNetwork.EXPECT().GetNetworkByID("net-123", 
gomock.Any()).Return(networkResp, 1, nil),
+                       
mockAddress.EXPECT().NewAssociateIpAddressParams().Return(associateParams),
+                       
mockAddress.EXPECT().AssociateIpAddress(gomock.Any()).Return(associateResp, 
nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Address: mockAddress,
+                               Network: mockNetwork,
+                       },
+                       networkID: "net-123",
+               }
+
+               err := lb.associatePublicIPAddress()
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+       })
+
+       t.Run("associate specific IP address", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+               mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+               networkResp := &cloudstack.Network{
+                       Id:      "net-123",
+                       Vpcid:   "",
+                       Service: []cloudstack.NetworkServiceInternal{},
+               }
+
+               associateParams := &cloudstack.AssociateIpAddressParams{}
+               associateResp := &cloudstack.AssociateIpAddressResponse{
+                       Id:        "ip-123",
+                       Ipaddress: "203.0.113.2",
+               }
+
+               gomock.InOrder(
+                       mockNetwork.EXPECT().GetNetworkByID("net-123", 
gomock.Any()).Return(networkResp, 1, nil),
+                       
mockAddress.EXPECT().NewAssociateIpAddressParams().Return(associateParams),
+                       
mockAddress.EXPECT().AssociateIpAddress(gomock.Any()).Return(associateResp, 
nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Address: mockAddress,
+                               Network: mockNetwork,
+                       },
+                       networkID: "net-123",
+                       ipAddr:    "203.0.113.2",
+               }
+
+               err := lb.associatePublicIPAddress()
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+       })
+
+       t.Run("error retrieving network", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+               apiErr := fmt.Errorf("network API error")
+
+               mockNetwork.EXPECT().GetNetworkByID("net-123", 
gomock.Any()).Return(nil, 1, apiErr)
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Network: mockNetwork,
+                       },
+                       networkID: "net-123",
+               }
+
+               err := lb.associatePublicIPAddress()
+               if err == nil {
+                       t.Fatalf("expected error")
+               }
+               if !strings.Contains(err.Error(), "error retrieving network") {
+                       t.Errorf("error message = %q, want to contain 'error 
retrieving network'", err.Error())
+               }
+       })
+
+       t.Run("network not found", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+
+               mockNetwork.EXPECT().GetNetworkByID("net-123", 
gomock.Any()).Return(nil, 0, fmt.Errorf("not found"))
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Network: mockNetwork,
+                       },
+                       networkID: "net-123",
+               }
+
+               err := lb.associatePublicIPAddress()
+               if err == nil {
+                       t.Fatalf("expected error")
+               }
+               if !strings.Contains(err.Error(), "could not find network") {
+                       t.Errorf("error message = %q, want to contain 'could 
not find network'", err.Error())
+               }
+       })
+
+       t.Run("error associating IP", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+               mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+               networkResp := &cloudstack.Network{
+                       Id:      "net-123",
+                       Vpcid:   "",
+                       Service: []cloudstack.NetworkServiceInternal{},
+               }
+
+               associateParams := &cloudstack.AssociateIpAddressParams{}
+               apiErr := fmt.Errorf("associate API error")
+
+               gomock.InOrder(
+                       mockNetwork.EXPECT().GetNetworkByID("net-123", 
gomock.Any()).Return(networkResp, 1, nil),
+                       
mockAddress.EXPECT().NewAssociateIpAddressParams().Return(associateParams),
+                       
mockAddress.EXPECT().AssociateIpAddress(gomock.Any()).Return(nil, apiErr),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Address: mockAddress,
+                               Network: mockNetwork,
+                       },
+                       networkID: "net-123",
+               }
+
+               err := lb.associatePublicIPAddress()
+               if err == nil {
+                       t.Fatalf("expected error")
+               }
+               if !strings.Contains(err.Error(), "error associating new IP 
address") {
+                       t.Errorf("error message = %q, want to contain 'error 
associating new IP address'", err.Error())
+               }
+       })
+
+       t.Run("project ID handling", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+               mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+               networkResp := &cloudstack.Network{
+                       Id:      "net-123",
+                       Vpcid:   "",
+                       Service: []cloudstack.NetworkServiceInternal{},
+               }
+
+               associateParams := &cloudstack.AssociateIpAddressParams{}
+               associateResp := &cloudstack.AssociateIpAddressResponse{
+                       Id:        "ip-123",
+                       Ipaddress: "203.0.113.1",
+               }
+
+               gomock.InOrder(
+                       mockNetwork.EXPECT().GetNetworkByID("net-123", 
gomock.Any()).Return(networkResp, 1, nil),
+                       
mockAddress.EXPECT().NewAssociateIpAddressParams().Return(associateParams),
+                       
mockAddress.EXPECT().AssociateIpAddress(gomock.Any()).Return(associateResp, 
nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Address: mockAddress,
+                               Network: mockNetwork,
+                       },
+                       networkID: "net-123",
+                       projectID: "proj-123",
+               }
+
+               err := lb.associatePublicIPAddress()
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+       })
+}
+
+func TestReleaseLoadBalancerIP(t *testing.T) {
+       t.Run("successful release", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+               disassociateParams := &cloudstack.DisassociateIpAddressParams{}
+
+               gomock.InOrder(
+                       
mockAddress.EXPECT().NewDisassociateIpAddressParams("ip-123").Return(disassociateParams),
+                       
mockAddress.EXPECT().DisassociateIpAddress(disassociateParams).Return(&cloudstack.DisassociateIpAddressResponse{},
 nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Address: mockAddress,
+                       },
+                       ipAddrID: "ip-123",
+                       ipAddr:   "203.0.113.1",
+               }
+
+               err := lb.releaseLoadBalancerIP()
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+       })
+
+       t.Run("error releasing IP", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+               disassociateParams := &cloudstack.DisassociateIpAddressParams{}
+               apiErr := fmt.Errorf("disassociate API error")
+
+               gomock.InOrder(
+                       
mockAddress.EXPECT().NewDisassociateIpAddressParams("ip-123").Return(disassociateParams),
+                       
mockAddress.EXPECT().DisassociateIpAddress(disassociateParams).Return(nil, 
apiErr),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Address: mockAddress,
+                       },
+                       ipAddrID: "ip-123",
+                       ipAddr:   "203.0.113.1",
+               }
+
+               err := lb.releaseLoadBalancerIP()
+               if err == nil {
+                       t.Fatalf("expected error")
+               }
+               if !strings.Contains(err.Error(), "error releasing load 
balancer IP") {
+                       t.Errorf("error message = %q, want to contain 'error 
releasing load balancer IP'", err.Error())
+               }
+       })
+}
+
+func TestGetLoadBalancerIP(t *testing.T) {
+       t.Run("IP specified - retrieve existing", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+               listParams := &cloudstack.ListPublicIpAddressesParams{}
+               resp := &cloudstack.ListPublicIpAddressesResponse{
+                       Count: 1,
+                       PublicIpAddresses: []*cloudstack.PublicIpAddress{
+                               {
+                                       Id:        "ip-123",
+                                       Ipaddress: "203.0.113.1",
+                                       Allocated: "2023-01-01T00:00:00+0000",
+                               },
+                       },
+               }
+
+               gomock.InOrder(
+                       
mockAddress.EXPECT().NewListPublicIpAddressesParams().Return(listParams),
+                       
mockAddress.EXPECT().ListPublicIpAddresses(gomock.Any()).Return(resp, nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Address: mockAddress,
+                       },
+               }
+
+               err := lb.getLoadBalancerIP("203.0.113.1")
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if lb.ipAddr != "203.0.113.1" {
+                       t.Errorf("ipAddr = %q, want %q", lb.ipAddr, 
"203.0.113.1")
+               }
+       })
+
+       t.Run("IP specified - associate unallocated", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+               mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+               listParams := &cloudstack.ListPublicIpAddressesParams{}
+               resp := &cloudstack.ListPublicIpAddressesResponse{
+                       Count: 1,
+                       PublicIpAddresses: []*cloudstack.PublicIpAddress{
+                               {
+                                       Id:        "ip-123",
+                                       Ipaddress: "203.0.113.1",
+                                       Allocated: "",
+                               },
+                       },
+               }
+
+               networkResp := &cloudstack.Network{
+                       Id:      "net-123",
+                       Vpcid:   "",
+                       Service: []cloudstack.NetworkServiceInternal{},
+               }
+
+               associateParams := &cloudstack.AssociateIpAddressParams{}
+               associateResp := &cloudstack.AssociateIpAddressResponse{
+                       Id:        "ip-123",
+                       Ipaddress: "203.0.113.1",
+               }
+
+               gomock.InOrder(
+                       
mockAddress.EXPECT().NewListPublicIpAddressesParams().Return(listParams),
+                       
mockAddress.EXPECT().ListPublicIpAddresses(gomock.Any()).Return(resp, nil),
+                       mockNetwork.EXPECT().GetNetworkByID("net-123", 
gomock.Any()).Return(networkResp, 1, nil),
+                       
mockAddress.EXPECT().NewAssociateIpAddressParams().Return(associateParams),
+                       
mockAddress.EXPECT().AssociateIpAddress(gomock.Any()).Return(associateResp, 
nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Address: mockAddress,
+                               Network: mockNetwork,
+                       },
+                       networkID: "net-123",
+                       ipAddr:    "203.0.113.1",
+               }
+
+               err := lb.getLoadBalancerIP("203.0.113.1")
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+       })
+
+       t.Run("no IP specified - associate new", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+               mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+               networkResp := &cloudstack.Network{
+                       Id:      "net-123",
+                       Vpcid:   "",
+                       Service: []cloudstack.NetworkServiceInternal{},
+               }
+
+               associateParams := &cloudstack.AssociateIpAddressParams{}
+               associateResp := &cloudstack.AssociateIpAddressResponse{
+                       Id:        "ip-123",
+                       Ipaddress: "203.0.113.1",
+               }
+
+               gomock.InOrder(
+                       mockNetwork.EXPECT().GetNetworkByID("net-123", 
gomock.Any()).Return(networkResp, 1, nil),
+                       
mockAddress.EXPECT().NewAssociateIpAddressParams().Return(associateParams),
+                       
mockAddress.EXPECT().AssociateIpAddress(gomock.Any()).Return(associateResp, 
nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Address: mockAddress,
+                               Network: mockNetwork,
+                       },
+                       networkID: "net-123",
+               }
+
+               err := lb.getLoadBalancerIP("")
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if lb.ipAddr != "203.0.113.1" {
+                       t.Errorf("ipAddr = %q, want %q", lb.ipAddr, 
"203.0.113.1")
+               }
+       })
+}
+
+func TestCreateLoadBalancerRule(t *testing.T) {
+       t.Run("create rule with default CIDR", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+               createParams := &cloudstack.CreateLoadBalancerRuleParams{}
+               createResp := &cloudstack.CreateLoadBalancerRuleResponse{
+                       Id:          "rule-123",
+                       Algorithm:   "roundrobin",
+                       Cidrlist:    defaultAllowedCIDR,
+                       Name:        "test-rule-tcp-80",
+                       Networkid:   "net-123",
+                       Privateport: "30000",
+                       Publicport:  "80",
+                       Publicip:    "203.0.113.1",
+                       Publicipid:  "ip-123",
+                       Protocol:    "tcp",
+               }
+
+               gomock.InOrder(
+                       
mockLB.EXPECT().NewCreateLoadBalancerRuleParams("roundrobin", 
"test-rule-tcp-80", 30000, 80).Return(createParams),
+                       
mockLB.EXPECT().CreateLoadBalancerRule(gomock.Any()).Return(createResp, nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               LoadBalancer: mockLB,
+                       },
+                       algorithm: "roundrobin",
+                       networkID: "net-123",
+                       ipAddrID:  "ip-123",
+                       ipAddr:    "203.0.113.1",
+               }
+
+               port := corev1.ServicePort{
+                       Port:     80,
+                       NodePort: 30000,
+                       Protocol: corev1.ProtocolTCP,
+               }
+               service := &corev1.Service{}
+
+               rule, err := lb.createLoadBalancerRule("test-rule-tcp-80", 
port, LoadBalancerProtocolTCP, service)
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if rule.Id != "rule-123" {
+                       t.Errorf("rule.Id = %q, want %q", rule.Id, "rule-123")
+               }
+               if rule.Name != "test-rule-tcp-80" {
+                       t.Errorf("rule.Name = %q, want %q", rule.Name, 
"test-rule-tcp-80")
+               }
+       })
+
+       t.Run("create rule with custom CIDR list", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+               createParams := &cloudstack.CreateLoadBalancerRuleParams{}
+               createResp := &cloudstack.CreateLoadBalancerRuleResponse{
+                       Id:          "rule-123",
+                       Algorithm:   "roundrobin",
+                       Cidrlist:    "10.0.0.0/8,192.168.0.0/16",
+                       Name:        "test-rule-tcp-80",
+                       Networkid:   "net-123",
+                       Privateport: "30000",
+                       Publicport:  "80",
+                       Publicip:    "203.0.113.1",
+                       Publicipid:  "ip-123",
+                       Protocol:    "tcp",
+               }
+
+               gomock.InOrder(
+                       
mockLB.EXPECT().NewCreateLoadBalancerRuleParams("roundrobin", 
"test-rule-tcp-80", 30000, 80).Return(createParams),
+                       
mockLB.EXPECT().CreateLoadBalancerRule(gomock.Any()).Return(createResp, nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               LoadBalancer: mockLB,
+                       },
+                       algorithm: "roundrobin",
+                       networkID: "net-123",
+                       ipAddrID:  "ip-123",
+                       ipAddr:    "203.0.113.1",
+               }
+
+               port := corev1.ServicePort{
+                       Port:     80,
+                       NodePort: 30000,
+                       Protocol: corev1.ProtocolTCP,
+               }
+               service := &corev1.Service{
+                       ObjectMeta: metav1.ObjectMeta{
+                               Annotations: map[string]string{
+                                       
ServiceAnnotationLoadBalancerSourceCidrs: "10.0.0.0/8,192.168.0.0/16",
+                               },
+                       },
+               }
+
+               rule, err := lb.createLoadBalancerRule("test-rule-tcp-80", 
port, LoadBalancerProtocolTCP, service)
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if rule.Cidrlist != "10.0.0.0/8,192.168.0.0/16" {
+                       t.Errorf("rule.Cidrlist = %q, want %q", rule.Cidrlist, 
"10.0.0.0/8,192.168.0.0/16")
+               }
+       })
+
+       t.Run("create rule with proxy protocol", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+               createParams := &cloudstack.CreateLoadBalancerRuleParams{}
+               createResp := &cloudstack.CreateLoadBalancerRuleResponse{
+                       Id:          "rule-123",
+                       Algorithm:   "roundrobin",
+                       Cidrlist:    defaultAllowedCIDR,
+                       Name:        "test-rule-tcp-proxy-80",
+                       Networkid:   "net-123",
+                       Privateport: "30000",
+                       Publicport:  "80",
+                       Publicip:    "203.0.113.1",
+                       Publicipid:  "ip-123",
+                       Protocol:    "tcp-proxy",
+               }
+
+               gomock.InOrder(
+                       
mockLB.EXPECT().NewCreateLoadBalancerRuleParams("roundrobin", 
"test-rule-tcp-proxy-80", 30000, 80).Return(createParams),
+                       
mockLB.EXPECT().CreateLoadBalancerRule(gomock.Any()).Return(createResp, nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               LoadBalancer: mockLB,
+                       },
+                       algorithm: "roundrobin",
+                       networkID: "net-123",
+                       ipAddrID:  "ip-123",
+                       ipAddr:    "203.0.113.1",
+               }
+
+               port := corev1.ServicePort{
+                       Port:     80,
+                       NodePort: 30000,
+                       Protocol: corev1.ProtocolTCP,
+               }
+               service := &corev1.Service{
+                       ObjectMeta: metav1.ObjectMeta{
+                               Annotations: map[string]string{
+                                       
ServiceAnnotationLoadBalancerProxyProtocol: "true",
+                               },
+                       },
+               }
+
+               rule, err := 
lb.createLoadBalancerRule("test-rule-tcp-proxy-80", port, 
LoadBalancerProtocolTCPProxy, service)
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if rule.Protocol != "tcp-proxy" {
+                       t.Errorf("rule.Protocol = %q, want %q", rule.Protocol, 
"tcp-proxy")
+               }
+       })
+
+       t.Run("error creating rule", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+               createParams := &cloudstack.CreateLoadBalancerRuleParams{}
+               apiErr := fmt.Errorf("create rule API error")
+
+               gomock.InOrder(
+                       
mockLB.EXPECT().NewCreateLoadBalancerRuleParams("roundrobin", 
"test-rule-tcp-80", 30000, 80).Return(createParams),
+                       
mockLB.EXPECT().CreateLoadBalancerRule(gomock.Any()).Return(nil, apiErr),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               LoadBalancer: mockLB,
+                       },
+                       algorithm: "roundrobin",
+                       networkID: "net-123",
+                       ipAddrID:  "ip-123",
+                       ipAddr:    "203.0.113.1",
+               }
+
+               port := corev1.ServicePort{
+                       Port:     80,
+                       NodePort: 30000,
+                       Protocol: corev1.ProtocolTCP,
+               }
+               service := &corev1.Service{}
+
+               _, err := lb.createLoadBalancerRule("test-rule-tcp-80", port, 
LoadBalancerProtocolTCP, service)
+               if err == nil {
+                       t.Fatalf("expected error")
+               }
+               if !strings.Contains(err.Error(), "error creating load balancer 
rule") {
+                       t.Errorf("error message = %q, want to contain 'error 
creating load balancer rule'", err.Error())
+               }
+       })
+
+       t.Run("invalid CIDR in annotation", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+               createParams := &cloudstack.CreateLoadBalancerRuleParams{}
+
+               gomock.InOrder(
+                       
mockLB.EXPECT().NewCreateLoadBalancerRuleParams("roundrobin", 
"test-rule-tcp-80", 30000, 80).Return(createParams),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               LoadBalancer: mockLB,
+                       },
+                       algorithm: "roundrobin",
+               }
+
+               port := corev1.ServicePort{
+                       Port:     80,
+                       NodePort: 30000,
+                       Protocol: corev1.ProtocolTCP,
+               }
+               service := &corev1.Service{
+                       ObjectMeta: metav1.ObjectMeta{
+                               Annotations: map[string]string{
+                                       
ServiceAnnotationLoadBalancerSourceCidrs: "invalid-cidr",
+                               },
+                       },
+               }
+
+               _, err := lb.createLoadBalancerRule("test-rule-tcp-80", port, 
LoadBalancerProtocolTCP, service)
+               if err == nil {
+                       t.Fatalf("expected error for invalid CIDR")
+               }
+               if !strings.Contains(err.Error(), "invalid CIDR") {
+                       t.Errorf("error message = %q, want to contain 'invalid 
CIDR'", err.Error())
+               }
+       })
+}
+
+func TestUpdateLoadBalancerRule(t *testing.T) {
+       t.Run("update algorithm", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+               updateParams := &cloudstack.UpdateLoadBalancerRuleParams{}
+
+               gomock.InOrder(
+                       
mockLB.EXPECT().NewUpdateLoadBalancerRuleParams("rule-123").Return(updateParams),
+                       
mockLB.EXPECT().UpdateLoadBalancerRule(gomock.Any()).Return(&cloudstack.UpdateLoadBalancerRuleResponse{},
 nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               LoadBalancer: mockLB,
+                       },
+                       algorithm: "source",
+                       rules: map[string]*cloudstack.LoadBalancerRule{
+                               "test-rule-tcp-80": {
+                                       Id:        "rule-123",
+                                       Algorithm: "roundrobin",
+                                       Protocol:  "tcp",
+                               },
+                       },
+               }
+
+               service := &corev1.Service{}
+
+               err := lb.updateLoadBalancerRule("test-rule-tcp-80", 
LoadBalancerProtocolTCP, service, semver.Version{Major: 4, Minor: 22, Patch: 0})
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+       })
+
+       t.Run("update protocol", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+               updateParams := &cloudstack.UpdateLoadBalancerRuleParams{}
+
+               gomock.InOrder(
+                       
mockLB.EXPECT().NewUpdateLoadBalancerRuleParams("rule-123").Return(updateParams),
+                       
mockLB.EXPECT().UpdateLoadBalancerRule(gomock.Any()).Return(&cloudstack.UpdateLoadBalancerRuleResponse{},
 nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               LoadBalancer: mockLB,
+                       },
+                       algorithm: "roundrobin",
+                       rules: map[string]*cloudstack.LoadBalancerRule{
+                               "test-rule-tcp-80": {
+                                       Id:        "rule-123",
+                                       Algorithm: "roundrobin",
+                                       Protocol:  "tcp",
+                               },
+                       },
+               }
+
+               service := &corev1.Service{}
+
+               err := lb.updateLoadBalancerRule("test-rule-tcp-80", 
LoadBalancerProtocolTCPProxy, service, semver.Version{Major: 4, Minor: 22, 
Patch: 0})
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+       })
+
+       t.Run("update CIDR list (CS >= 4.22)", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+               updateParams := &cloudstack.UpdateLoadBalancerRuleParams{}
+
+               gomock.InOrder(
+                       
mockLB.EXPECT().NewUpdateLoadBalancerRuleParams("rule-123").Return(updateParams),
+                       
mockLB.EXPECT().UpdateLoadBalancerRule(gomock.Any()).Return(&cloudstack.UpdateLoadBalancerRuleResponse{},
 nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               LoadBalancer: mockLB,
+                       },
+                       algorithm: "roundrobin",
+                       rules: map[string]*cloudstack.LoadBalancerRule{
+                               "test-rule-tcp-80": {
+                                       Id:        "rule-123",
+                                       Algorithm: "roundrobin",
+                                       Protocol:  "tcp",
+                                       Cidrlist:  defaultAllowedCIDR,
+                               },
+                       },
+               }
+
+               service := &corev1.Service{
+                       ObjectMeta: metav1.ObjectMeta{
+                               Annotations: map[string]string{
+                                       
ServiceAnnotationLoadBalancerSourceCidrs: "10.0.0.0/8",
+                               },
+                       },
+               }
+
+               err := lb.updateLoadBalancerRule("test-rule-tcp-80", 
LoadBalancerProtocolTCP, service, semver.Version{Major: 4, Minor: 22, Patch: 0})
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+       })
+
+       t.Run("error updating rule", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+               updateParams := &cloudstack.UpdateLoadBalancerRuleParams{}
+               apiErr := fmt.Errorf("update rule API error")
+
+               gomock.InOrder(
+                       
mockLB.EXPECT().NewUpdateLoadBalancerRuleParams("rule-123").Return(updateParams),
+                       
mockLB.EXPECT().UpdateLoadBalancerRule(gomock.Any()).Return(nil, apiErr),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               LoadBalancer: mockLB,
+                       },
+                       algorithm: "roundrobin",
+                       rules: map[string]*cloudstack.LoadBalancerRule{
+                               "test-rule-tcp-80": {
+                                       Id:        "rule-123",
+                                       Algorithm: "roundrobin",
+                                       Protocol:  "tcp",
+                               },
+                       },
+               }
+
+               service := &corev1.Service{}
+
+               err := lb.updateLoadBalancerRule("test-rule-tcp-80", 
LoadBalancerProtocolTCP, service, semver.Version{Major: 4, Minor: 22, Patch: 0})
+               if err == nil {
+                       t.Fatalf("expected error")
+               }
+               if err != apiErr {
+                       t.Errorf("error = %v, want %v", err, apiErr)
+               }
+       })
+}
+
+func TestDeleteLoadBalancerRule(t *testing.T) {
+       t.Run("successful deletion", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+               deleteParams := &cloudstack.DeleteLoadBalancerRuleParams{}
+
+               gomock.InOrder(
+                       
mockLB.EXPECT().NewDeleteLoadBalancerRuleParams("rule-123").Return(deleteParams),
+                       
mockLB.EXPECT().DeleteLoadBalancerRule(deleteParams).Return(&cloudstack.DeleteLoadBalancerRuleResponse{},
 nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               LoadBalancer: mockLB,
+                       },
+                       rules: map[string]*cloudstack.LoadBalancerRule{
+                               "test-rule": {
+                                       Id:   "rule-123",
+                                       Name: "test-rule",
+                               },
+                       },
+               }
+
+               rule := &cloudstack.LoadBalancerRule{
+                       Id:   "rule-123",
+                       Name: "test-rule",
+               }
+
+               err := lb.deleteLoadBalancerRule(rule)
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if _, exists := lb.rules["test-rule"]; exists {
+                       t.Errorf("expected rule to be removed from map")
+               }
+       })
+
+       t.Run("error deleting rule", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+               deleteParams := &cloudstack.DeleteLoadBalancerRuleParams{}
+               apiErr := fmt.Errorf("delete rule API error")
+
+               gomock.InOrder(
+                       
mockLB.EXPECT().NewDeleteLoadBalancerRuleParams("rule-123").Return(deleteParams),
+                       
mockLB.EXPECT().DeleteLoadBalancerRule(deleteParams).Return(nil, apiErr),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               LoadBalancer: mockLB,
+                       },
+                       rules: map[string]*cloudstack.LoadBalancerRule{
+                               "test-rule": {
+                                       Id:   "rule-123",
+                                       Name: "test-rule",
+                               },
+                       },
+               }
+
+               rule := &cloudstack.LoadBalancerRule{
+                       Id:   "rule-123",
+                       Name: "test-rule",
+               }
+
+               err := lb.deleteLoadBalancerRule(rule)
+               if err == nil {
+                       t.Fatalf("expected error")
+               }
+               if !strings.Contains(err.Error(), "error deleting load balancer 
rule") {
+                       t.Errorf("error message = %q, want to contain 'error 
deleting load balancer rule'", err.Error())
+               }
+       })
+}
+
+func TestAssignHostsToRule(t *testing.T) {
+       t.Run("successful assignment", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+               assignParams := &cloudstack.AssignToLoadBalancerRuleParams{}
+
+               gomock.InOrder(
+                       
mockLB.EXPECT().NewAssignToLoadBalancerRuleParams("rule-123").Return(assignParams),
+                       
mockLB.EXPECT().AssignToLoadBalancerRule(gomock.Any()).Return(&cloudstack.AssignToLoadBalancerRuleResponse{},
 nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               LoadBalancer: mockLB,
+                       },
+               }
+
+               rule := &cloudstack.LoadBalancerRule{
+                       Id:   "rule-123",
+                       Name: "test-rule",
+               }
+
+               err := lb.assignHostsToRule(rule, []string{"vm-1", "vm-2"})
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+       })
+
+       t.Run("error assigning hosts", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+               assignParams := &cloudstack.AssignToLoadBalancerRuleParams{}
+               apiErr := fmt.Errorf("assign API error")
+
+               gomock.InOrder(
+                       
mockLB.EXPECT().NewAssignToLoadBalancerRuleParams("rule-123").Return(assignParams),
+                       
mockLB.EXPECT().AssignToLoadBalancerRule(gomock.Any()).Return(nil, apiErr),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               LoadBalancer: mockLB,
+                       },
+               }
+
+               rule := &cloudstack.LoadBalancerRule{
+                       Id:   "rule-123",
+                       Name: "test-rule",
+               }
+
+               err := lb.assignHostsToRule(rule, []string{"vm-1"})
+               if err == nil {
+                       t.Fatalf("expected error")
+               }
+               if !strings.Contains(err.Error(), "error assigning hosts") {
+                       t.Errorf("error message = %q, want to contain 'error 
assigning hosts'", err.Error())
+               }
+       })
+
+       t.Run("empty host list", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+               assignParams := &cloudstack.AssignToLoadBalancerRuleParams{}
+
+               gomock.InOrder(
+                       
mockLB.EXPECT().NewAssignToLoadBalancerRuleParams("rule-123").Return(assignParams),
+                       
mockLB.EXPECT().AssignToLoadBalancerRule(gomock.Any()).Return(&cloudstack.AssignToLoadBalancerRuleResponse{},
 nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               LoadBalancer: mockLB,
+                       },
+               }
+
+               rule := &cloudstack.LoadBalancerRule{
+                       Id:   "rule-123",
+                       Name: "test-rule",
+               }
+
+               err := lb.assignHostsToRule(rule, []string{})
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+       })
+}
+
+func TestRemoveHostsFromRule(t *testing.T) {
+       t.Run("successful removal", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+               removeParams := &cloudstack.RemoveFromLoadBalancerRuleParams{}
+
+               gomock.InOrder(
+                       
mockLB.EXPECT().NewRemoveFromLoadBalancerRuleParams("rule-123").Return(removeParams),
+                       
mockLB.EXPECT().RemoveFromLoadBalancerRule(gomock.Any()).Return(&cloudstack.RemoveFromLoadBalancerRuleResponse{},
 nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               LoadBalancer: mockLB,
+                       },
+               }
+
+               rule := &cloudstack.LoadBalancerRule{
+                       Id:   "rule-123",
+                       Name: "test-rule",
+               }
+
+               err := lb.removeHostsFromRule(rule, []string{"vm-1", "vm-2"})
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+       })
+
+       t.Run("error removing hosts", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+               removeParams := &cloudstack.RemoveFromLoadBalancerRuleParams{}
+               apiErr := fmt.Errorf("remove API error")
+
+               gomock.InOrder(
+                       
mockLB.EXPECT().NewRemoveFromLoadBalancerRuleParams("rule-123").Return(removeParams),
+                       
mockLB.EXPECT().RemoveFromLoadBalancerRule(gomock.Any()).Return(nil, apiErr),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               LoadBalancer: mockLB,
+                       },
+               }
+
+               rule := &cloudstack.LoadBalancerRule{
+                       Id:   "rule-123",
+                       Name: "test-rule",
+               }
+
+               err := lb.removeHostsFromRule(rule, []string{"vm-1"})
+               if err == nil {
+                       t.Fatalf("expected error")
+               }
+               if !strings.Contains(err.Error(), "error removing hosts") {
+                       t.Errorf("error message = %q, want to contain 'error 
removing hosts'", err.Error())
+               }
+       })
+
+       t.Run("empty host list", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+               removeParams := &cloudstack.RemoveFromLoadBalancerRuleParams{}
+
+               gomock.InOrder(
+                       
mockLB.EXPECT().NewRemoveFromLoadBalancerRuleParams("rule-123").Return(removeParams),
+                       
mockLB.EXPECT().RemoveFromLoadBalancerRule(gomock.Any()).Return(&cloudstack.RemoveFromLoadBalancerRuleResponse{},
 nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               LoadBalancer: mockLB,
+                       },
+               }
+
+               rule := &cloudstack.LoadBalancerRule{
+                       Id:   "rule-123",
+                       Name: "test-rule",
+               }
+
+               err := lb.removeHostsFromRule(rule, []string{})
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+       })
+}
+
+func TestUpdateFirewallRule(t *testing.T) {
+       t.Run("create new firewall rule", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockFirewall := cloudstack.NewMockFirewallServiceIface(ctrl)
+               listParams := &cloudstack.ListFirewallRulesParams{}
+               listResp := &cloudstack.ListFirewallRulesResponse{
+                       Count:         0,
+                       FirewallRules: []*cloudstack.FirewallRule{},
+               }
+
+               createParams := &cloudstack.CreateFirewallRuleParams{}
+               createResp := &cloudstack.CreateFirewallRuleResponse{
+                       Id: "fw-123",
+               }
+
+               gomock.InOrder(
+                       
mockFirewall.EXPECT().NewListFirewallRulesParams().Return(listParams),
+                       
mockFirewall.EXPECT().ListFirewallRules(gomock.Any()).Return(listResp, nil),
+                       
mockFirewall.EXPECT().NewCreateFirewallRuleParams("ip-123", 
"tcp").Return(createParams),
+                       
mockFirewall.EXPECT().CreateFirewallRule(gomock.Any()).Return(createResp, nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Firewall: mockFirewall,
+                       },
+                       ipAddr: "203.0.113.1",
+               }
+
+               updated, err := lb.updateFirewallRule("ip-123", 80, 
LoadBalancerProtocolTCP, []string{"10.0.0.0/8"})
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if !updated {
+                       t.Errorf("updated = false, want true")
+               }
+       })
+
+       t.Run("rule already exists - no change", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockFirewall := cloudstack.NewMockFirewallServiceIface(ctrl)
+               listParams := &cloudstack.ListFirewallRulesParams{}
+               listResp := &cloudstack.ListFirewallRulesResponse{
+                       Count: 1,
+                       FirewallRules: []*cloudstack.FirewallRule{
+                               {
+                                       Id:          "fw-123",
+                                       Protocol:    "tcp",
+                                       Startport:   80,
+                                       Endport:     80,
+                                       Cidrlist:    "10.0.0.0/8",
+                                       Ipaddress:   "203.0.113.1",
+                                       Ipaddressid: "ip-123",
+                               },
+                       },
+               }
+
+               gomock.InOrder(
+                       
mockFirewall.EXPECT().NewListFirewallRulesParams().Return(listParams),
+                       
mockFirewall.EXPECT().ListFirewallRules(gomock.Any()).Return(listResp, nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Firewall: mockFirewall,
+                       },
+                       ipAddr: "203.0.113.1",
+               }
+
+               updated, err := lb.updateFirewallRule("ip-123", 80, 
LoadBalancerProtocolTCP, []string{"10.0.0.0/8"})
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if !updated {
+                       t.Errorf("updated = false, want true")
+               }
+       })
+
+       t.Run("update existing rule - CIDR change", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockFirewall := cloudstack.NewMockFirewallServiceIface(ctrl)
+               listParams := &cloudstack.ListFirewallRulesParams{}
+               listResp := &cloudstack.ListFirewallRulesResponse{
+                       Count: 1,
+                       FirewallRules: []*cloudstack.FirewallRule{
+                               {
+                                       Id:          "fw-123",
+                                       Protocol:    "tcp",
+                                       Startport:   80,
+                                       Endport:     80,
+                                       Cidrlist:    "192.168.0.0/16",
+                                       Ipaddress:   "203.0.113.1",
+                                       Ipaddressid: "ip-123",
+                               },
+                       },
+               }
+
+               deleteParams := &cloudstack.DeleteFirewallRuleParams{}
+               createParams := &cloudstack.CreateFirewallRuleParams{}
+               createResp := &cloudstack.CreateFirewallRuleResponse{
+                       Id: "fw-124",
+               }
+
+               gomock.InOrder(
+                       
mockFirewall.EXPECT().NewListFirewallRulesParams().Return(listParams),
+                       
mockFirewall.EXPECT().ListFirewallRules(gomock.Any()).Return(listResp, nil),
+                       
mockFirewall.EXPECT().NewDeleteFirewallRuleParams("fw-123").Return(deleteParams),
+                       
mockFirewall.EXPECT().DeleteFirewallRule(deleteParams).Return(&cloudstack.DeleteFirewallRuleResponse{},
 nil),
+                       
mockFirewall.EXPECT().NewCreateFirewallRuleParams("ip-123", 
"tcp").Return(createParams),
+                       
mockFirewall.EXPECT().CreateFirewallRule(gomock.Any()).Return(createResp, nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Firewall: mockFirewall,
+                       },
+                       ipAddr: "203.0.113.1",
+               }
+
+               updated, err := lb.updateFirewallRule("ip-123", 80, 
LoadBalancerProtocolTCP, []string{"10.0.0.0/8"})
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if !updated {
+                       t.Errorf("updated = false, want true")
+               }
+       })
+
+       t.Run("default CIDR when empty list", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockFirewall := cloudstack.NewMockFirewallServiceIface(ctrl)
+               listParams := &cloudstack.ListFirewallRulesParams{}
+               listResp := &cloudstack.ListFirewallRulesResponse{
+                       Count:         0,
+                       FirewallRules: []*cloudstack.FirewallRule{},
+               }
+
+               createParams := &cloudstack.CreateFirewallRuleParams{}
+               createResp := &cloudstack.CreateFirewallRuleResponse{
+                       Id: "fw-123",
+               }
+
+               gomock.InOrder(
+                       
mockFirewall.EXPECT().NewListFirewallRulesParams().Return(listParams),
+                       
mockFirewall.EXPECT().ListFirewallRules(gomock.Any()).Return(listResp, nil),
+                       
mockFirewall.EXPECT().NewCreateFirewallRuleParams("ip-123", 
"tcp").Return(createParams),
+                       
mockFirewall.EXPECT().CreateFirewallRule(gomock.Any()).Return(createResp, nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Firewall: mockFirewall,
+                       },
+                       ipAddr: "203.0.113.1",
+               }
+
+               updated, err := lb.updateFirewallRule("ip-123", 80, 
LoadBalancerProtocolTCP, []string{})
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if !updated {
+                       t.Errorf("updated = false, want true")
+               }
+       })
+
+       t.Run("error listing rules", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockFirewall := cloudstack.NewMockFirewallServiceIface(ctrl)
+               listParams := &cloudstack.ListFirewallRulesParams{}
+               apiErr := fmt.Errorf("list API error")
+
+               gomock.InOrder(
+                       
mockFirewall.EXPECT().NewListFirewallRulesParams().Return(listParams),
+                       
mockFirewall.EXPECT().ListFirewallRules(gomock.Any()).Return(nil, apiErr),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Firewall: mockFirewall,
+                       },
+                       ipAddr: "203.0.113.1",
+               }
+
+               _, err := lb.updateFirewallRule("ip-123", 80, 
LoadBalancerProtocolTCP, []string{"10.0.0.0/8"})
+               if err == nil {
+                       t.Fatalf("expected error")
+               }
+               if !strings.Contains(err.Error(), "error fetching firewall 
rules") {
+                       t.Errorf("error message = %q, want to contain 'error 
fetching firewall rules'", err.Error())
+               }
+       })
+
+       t.Run("error creating rule", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockFirewall := cloudstack.NewMockFirewallServiceIface(ctrl)
+               listParams := &cloudstack.ListFirewallRulesParams{}
+               listResp := &cloudstack.ListFirewallRulesResponse{
+                       Count:         0,
+                       FirewallRules: []*cloudstack.FirewallRule{},
+               }
+
+               createParams := &cloudstack.CreateFirewallRuleParams{}
+               apiErr := fmt.Errorf("create API error")
+
+               gomock.InOrder(
+                       
mockFirewall.EXPECT().NewListFirewallRulesParams().Return(listParams),
+                       
mockFirewall.EXPECT().ListFirewallRules(gomock.Any()).Return(listResp, nil),
+                       
mockFirewall.EXPECT().NewCreateFirewallRuleParams("ip-123", 
"tcp").Return(createParams),
+                       
mockFirewall.EXPECT().CreateFirewallRule(gomock.Any()).Return(nil, apiErr),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Firewall: mockFirewall,
+                       },
+                       ipAddr: "203.0.113.1",
+               }
+
+               _, err := lb.updateFirewallRule("ip-123", 80, 
LoadBalancerProtocolTCP, []string{"10.0.0.0/8"})
+               if err == nil {
+                       t.Fatalf("expected error")
+               }
+               if !strings.Contains(err.Error(), "error creating new firewall 
rule") {
+                       t.Errorf("error message = %q, want to contain 'error 
creating new firewall rule'", err.Error())
+               }
+       })
+
+       t.Run("error deleting rule - continues", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockFirewall := cloudstack.NewMockFirewallServiceIface(ctrl)
+               listParams := &cloudstack.ListFirewallRulesParams{}
+               listResp := &cloudstack.ListFirewallRulesResponse{
+                       Count: 1,
+                       FirewallRules: []*cloudstack.FirewallRule{
+                               {
+                                       Id:          "fw-123",
+                                       Protocol:    "tcp",
+                                       Startport:   80,
+                                       Endport:     80,
+                                       Cidrlist:    "192.168.0.0/16",
+                                       Ipaddress:   "203.0.113.1",
+                                       Ipaddressid: "ip-123",
+                               },
+                       },
+               }
+
+               deleteParams := &cloudstack.DeleteFirewallRuleParams{}
+               deleteErr := fmt.Errorf("delete API error")
+               createParams := &cloudstack.CreateFirewallRuleParams{}
+               createResp := &cloudstack.CreateFirewallRuleResponse{
+                       Id: "fw-124",
+               }
+
+               gomock.InOrder(
+                       
mockFirewall.EXPECT().NewListFirewallRulesParams().Return(listParams),
+                       
mockFirewall.EXPECT().ListFirewallRules(gomock.Any()).Return(listResp, nil),
+                       
mockFirewall.EXPECT().NewDeleteFirewallRuleParams("fw-123").Return(deleteParams),
+                       
mockFirewall.EXPECT().DeleteFirewallRule(deleteParams).Return(nil, deleteErr),
+                       
mockFirewall.EXPECT().NewCreateFirewallRuleParams("ip-123", 
"tcp").Return(createParams),
+                       
mockFirewall.EXPECT().CreateFirewallRule(gomock.Any()).Return(createResp, nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Firewall: mockFirewall,
+                       },
+                       ipAddr: "203.0.113.1",
+               }
+
+               updated, err := lb.updateFirewallRule("ip-123", 80, 
LoadBalancerProtocolTCP, []string{"10.0.0.0/8"})
+               // Should still return true even if delete failed
+               if err != nil && !strings.Contains(err.Error(), "error 
creating") {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if !updated {
+                       t.Errorf("updated = false, want true")
+               }
+       })
+}
+
+func TestDeleteFirewallRule(t *testing.T) {
+       t.Run("delete matching rule", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockFirewall := cloudstack.NewMockFirewallServiceIface(ctrl)
+               listParams := &cloudstack.ListFirewallRulesParams{}
+               listResp := &cloudstack.ListFirewallRulesResponse{
+                       Count: 1,
+                       FirewallRules: []*cloudstack.FirewallRule{
+                               {
+                                       Id:          "fw-123",
+                                       Protocol:    "tcp",
+                                       Startport:   80,
+                                       Endport:     80,
+                                       Ipaddressid: "ip-123",
+                               },
+                       },
+               }
+
+               deleteParams := &cloudstack.DeleteFirewallRuleParams{}
+
+               gomock.InOrder(
+                       
mockFirewall.EXPECT().NewListFirewallRulesParams().Return(listParams),
+                       
mockFirewall.EXPECT().ListFirewallRules(gomock.Any()).Return(listResp, nil),
+                       
mockFirewall.EXPECT().NewDeleteFirewallRuleParams("fw-123").Return(deleteParams),
+                       
mockFirewall.EXPECT().DeleteFirewallRule(deleteParams).Return(&cloudstack.DeleteFirewallRuleResponse{},
 nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Firewall: mockFirewall,
+                       },
+               }
+
+               deleted, err := lb.deleteFirewallRule("ip-123", 80, 
LoadBalancerProtocolTCP)
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if !deleted {
+                       t.Errorf("deleted = false, want true")
+               }
+       })
+
+       t.Run("no matching rules", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockFirewall := cloudstack.NewMockFirewallServiceIface(ctrl)
+               listParams := &cloudstack.ListFirewallRulesParams{}
+               listResp := &cloudstack.ListFirewallRulesResponse{
+                       Count:         0,
+                       FirewallRules: []*cloudstack.FirewallRule{},
+               }
+
+               gomock.InOrder(
+                       
mockFirewall.EXPECT().NewListFirewallRulesParams().Return(listParams),
+                       
mockFirewall.EXPECT().ListFirewallRules(gomock.Any()).Return(listResp, nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Firewall: mockFirewall,
+                       },
+               }
+
+               deleted, err := lb.deleteFirewallRule("ip-123", 80, 
LoadBalancerProtocolTCP)
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if deleted {
+                       t.Errorf("deleted = true, want false")
+               }
+       })
+
+       t.Run("error listing rules", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockFirewall := cloudstack.NewMockFirewallServiceIface(ctrl)
+               listParams := &cloudstack.ListFirewallRulesParams{}
+               apiErr := fmt.Errorf("list API error")
+
+               gomock.InOrder(
+                       
mockFirewall.EXPECT().NewListFirewallRulesParams().Return(listParams),
+                       
mockFirewall.EXPECT().ListFirewallRules(gomock.Any()).Return(nil, apiErr),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Firewall: mockFirewall,
+                       },
+               }
+
+               _, err := lb.deleteFirewallRule("ip-123", 80, 
LoadBalancerProtocolTCP)
+               if err == nil {
+                       t.Fatalf("expected error")
+               }
+               if !strings.Contains(err.Error(), "error fetching firewall 
rules") {
+                       t.Errorf("error message = %q, want to contain 'error 
fetching firewall rules'", err.Error())
+               }
+       })
+
+       t.Run("error deleting rule", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockFirewall := cloudstack.NewMockFirewallServiceIface(ctrl)
+               listParams := &cloudstack.ListFirewallRulesParams{}
+               listResp := &cloudstack.ListFirewallRulesResponse{
+                       Count: 1,
+                       FirewallRules: []*cloudstack.FirewallRule{
+                               {
+                                       Id:          "fw-123",
+                                       Protocol:    "tcp",
+                                       Startport:   80,
+                                       Endport:     80,
+                                       Ipaddressid: "ip-123",
+                               },
+                       },
+               }
+
+               deleteParams := &cloudstack.DeleteFirewallRuleParams{}
+               deleteErr := fmt.Errorf("delete API error")
+
+               gomock.InOrder(
+                       
mockFirewall.EXPECT().NewListFirewallRulesParams().Return(listParams),
+                       
mockFirewall.EXPECT().ListFirewallRules(gomock.Any()).Return(listResp, nil),
+                       
mockFirewall.EXPECT().NewDeleteFirewallRuleParams("fw-123").Return(deleteParams),
+                       
mockFirewall.EXPECT().DeleteFirewallRule(deleteParams).Return(nil, deleteErr),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Firewall: mockFirewall,
+                       },
+               }
+
+               deleted, err := lb.deleteFirewallRule("ip-123", 80, 
LoadBalancerProtocolTCP)
+               // Should return false if deletion failed
+               if deleted {
+                       t.Errorf("deleted = true, want false")
+               }
+               if err != deleteErr {
+                       t.Errorf("error = %v, want %v", err, deleteErr)
+               }
+       })
+}
+
+func TestUpdateNetworkACL(t *testing.T) {
+       t.Run("create new ACL rule", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+               mockNetworkACL := cloudstack.NewMockNetworkACLServiceIface(ctrl)
+               networkResp := &cloudstack.Network{
+                       Id:      "net-123",
+                       Aclid:   "acl-456",
+                       Service: []cloudstack.NetworkServiceInternal{},
+               }
+
+               aclListResp := &cloudstack.NetworkACLList{
+                       Id:   "acl-456",
+                       Name: "custom-acl",
+               }
+
+               listParams := &cloudstack.ListNetworkACLsParams{}
+               listResp := &cloudstack.ListNetworkACLsResponse{
+                       Count:       0,
+                       NetworkACLs: []*cloudstack.NetworkACL{},
+               }
+
+               createParams := &cloudstack.CreateNetworkACLParams{}
+               createResp := &cloudstack.CreateNetworkACLResponse{
+                       Id: "acl-rule-123",
+               }
+
+               gomock.InOrder(
+                       
mockNetwork.EXPECT().GetNetworkByID("net-123").Return(networkResp, 1, nil),
+                       
mockNetworkACL.EXPECT().GetNetworkACLListByID("acl-456").Return(aclListResp, 1, 
nil),
+                       
mockNetworkACL.EXPECT().NewListNetworkACLsParams().Return(listParams),
+                       
mockNetworkACL.EXPECT().ListNetworkACLs(gomock.Any()).Return(listResp, nil),
+                       
mockNetworkACL.EXPECT().NewCreateNetworkACLParams("tcp").Return(createParams),
+                       
mockNetworkACL.EXPECT().CreateNetworkACL(gomock.Any()).Return(createResp, nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Network:    mockNetwork,
+                               NetworkACL: mockNetworkACL,
+                       },
+               }
+
+               updated, err := lb.updateNetworkACL(80, 
LoadBalancerProtocolTCP, "net-123")
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if !updated {
+                       t.Errorf("updated = false, want true")
+               }
+       })
+
+       t.Run("rule already exists", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+               mockNetworkACL := cloudstack.NewMockNetworkACLServiceIface(ctrl)
+               networkResp := &cloudstack.Network{
+                       Id:      "net-123",
+                       Aclid:   "acl-456",
+                       Service: []cloudstack.NetworkServiceInternal{},
+               }
+
+               aclListResp := &cloudstack.NetworkACLList{
+                       Id:   "acl-456",
+                       Name: "custom-acl",
+               }
+
+               listParams := &cloudstack.ListNetworkACLsParams{}
+               listResp := &cloudstack.ListNetworkACLsResponse{
+                       Count: 1,
+                       NetworkACLs: []*cloudstack.NetworkACL{
+                               {
+                                       Id:        "acl-rule-123",
+                                       Protocol:  "tcp",
+                                       Startport: "80",
+                                       Endport:   "80",
+                               },
+                       },
+               }
+
+               gomock.InOrder(
+                       
mockNetwork.EXPECT().GetNetworkByID("net-123").Return(networkResp, 1, nil),
+                       
mockNetworkACL.EXPECT().GetNetworkACLListByID("acl-456").Return(aclListResp, 1, 
nil),
+                       
mockNetworkACL.EXPECT().NewListNetworkACLsParams().Return(listParams),
+                       
mockNetworkACL.EXPECT().ListNetworkACLs(gomock.Any()).Return(listResp, nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Network:    mockNetwork,
+                               NetworkACL: mockNetworkACL,
+                       },
+               }
+
+               updated, err := lb.updateNetworkACL(80, 
LoadBalancerProtocolTCP, "net-123")
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if !updated {
+                       t.Errorf("updated = false, want true")
+               }
+       })
+
+       t.Run("default ACL - skip", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+               mockNetworkACL := cloudstack.NewMockNetworkACLServiceIface(ctrl)
+               networkResp := &cloudstack.Network{
+                       Id:      "net-123",
+                       Aclid:   "acl-456",
+                       Service: []cloudstack.NetworkServiceInternal{},
+               }
+
+               aclListResp := &cloudstack.NetworkACLList{
+                       Id:   "acl-456",
+                       Name: "default_allow",
+               }
+
+               gomock.InOrder(
+                       
mockNetwork.EXPECT().GetNetworkByID("net-123").Return(networkResp, 1, nil),
+                       
mockNetworkACL.EXPECT().GetNetworkACLListByID("acl-456").Return(aclListResp, 1, 
nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Network:    mockNetwork,
+                               NetworkACL: mockNetworkACL,
+                       },
+               }
+
+               updated, err := lb.updateNetworkACL(80, 
LoadBalancerProtocolTCP, "net-123")
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if !updated {
+                       t.Errorf("updated = false, want true")
+               }
+       })
+
+       t.Run("error fetching network", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+               apiErr := fmt.Errorf("network API error")
+
+               mockNetwork.EXPECT().GetNetworkByID("net-123").Return(nil, 1, 
apiErr)
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Network: mockNetwork,
+                       },
+               }
+
+               _, err := lb.updateNetworkACL(80, LoadBalancerProtocolTCP, 
"net-123")
+               if err == nil {
+                       t.Fatalf("expected error")
+               }
+               if !strings.Contains(err.Error(), "error fetching Network") {
+                       t.Errorf("error message = %q, want to contain 'error 
fetching Network'", err.Error())
+               }
+       })
+
+       t.Run("error fetching ACL list", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+               mockNetworkACL := cloudstack.NewMockNetworkACLServiceIface(ctrl)
+               networkResp := &cloudstack.Network{
+                       Id:      "net-123",
+                       Aclid:   "acl-456",
+                       Service: []cloudstack.NetworkServiceInternal{},
+               }
+
+               apiErr := fmt.Errorf("ACL list API error")
+
+               gomock.InOrder(
+                       
mockNetwork.EXPECT().GetNetworkByID("net-123").Return(networkResp, 1, nil),
+                       
mockNetworkACL.EXPECT().GetNetworkACLListByID("acl-456").Return(nil, 0, apiErr),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Network:    mockNetwork,
+                               NetworkACL: mockNetworkACL,
+                       },
+               }
+
+               _, err := lb.updateNetworkACL(80, LoadBalancerProtocolTCP, 
"net-123")
+               if err == nil {
+                       t.Fatalf("expected error")
+               }
+               if !strings.Contains(err.Error(), "error fetching Network ACL 
List") {
+                       t.Errorf("error message = %q, want to contain 'error 
fetching Network ACL List'", err.Error())
+               }
+       })
+
+       t.Run("network not found", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+               mockNetworkACL := cloudstack.NewMockNetworkACLServiceIface(ctrl)
+               networkResp := &cloudstack.Network{
+                       Id:      "net-123",
+                       Aclid:   "acl-456",
+                       Service: []cloudstack.NetworkServiceInternal{},
+               }
+
+               aclListResp := &cloudstack.NetworkACLList{
+                       Id:   "acl-456",
+                       Name: "custom-acl",
+               }
+
+               listParams := &cloudstack.ListNetworkACLsParams{}
+               apiErr := fmt.Errorf("list ACL API error")
+
+               gomock.InOrder(
+                       
mockNetwork.EXPECT().GetNetworkByID("net-123").Return(networkResp, 1, nil),
+                       
mockNetworkACL.EXPECT().GetNetworkACLListByID("acl-456").Return(aclListResp, 1, 
nil),
+                       
mockNetworkACL.EXPECT().NewListNetworkACLsParams().Return(listParams),
+                       
mockNetworkACL.EXPECT().ListNetworkACLs(gomock.Any()).Return(nil, apiErr),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Network:    mockNetwork,
+                               NetworkACL: mockNetworkACL,
+                       },
+               }
+
+               _, err := lb.updateNetworkACL(80, LoadBalancerProtocolTCP, 
"net-123")
+               if err == nil {
+                       t.Fatalf("expected error")
+               }
+               if !strings.Contains(err.Error(), "error fetching Network ACL") 
{
+                       t.Errorf("error message = %q, want to contain 'error 
fetching Network ACL'", err.Error())
+               }
+       })
+
+       t.Run("error creating ACL rule", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+               mockNetworkACL := cloudstack.NewMockNetworkACLServiceIface(ctrl)
+               networkResp := &cloudstack.Network{
+                       Id:      "net-123",
+                       Aclid:   "acl-456",
+                       Service: []cloudstack.NetworkServiceInternal{},
+               }
+
+               aclListResp := &cloudstack.NetworkACLList{
+                       Id:   "acl-456",
+                       Name: "custom-acl",
+               }
+
+               listParams := &cloudstack.ListNetworkACLsParams{}
+               listResp := &cloudstack.ListNetworkACLsResponse{
+                       Count:       0,
+                       NetworkACLs: []*cloudstack.NetworkACL{},
+               }
+
+               createParams := &cloudstack.CreateNetworkACLParams{}
+               apiErr := fmt.Errorf("create ACL API error")
+
+               gomock.InOrder(
+                       
mockNetwork.EXPECT().GetNetworkByID("net-123").Return(networkResp, 1, nil),
+                       
mockNetworkACL.EXPECT().GetNetworkACLListByID("acl-456").Return(aclListResp, 1, 
nil),
+                       
mockNetworkACL.EXPECT().NewListNetworkACLsParams().Return(listParams),
+                       
mockNetworkACL.EXPECT().ListNetworkACLs(gomock.Any()).Return(listResp, nil),
+                       
mockNetworkACL.EXPECT().NewCreateNetworkACLParams("tcp").Return(createParams),
+                       
mockNetworkACL.EXPECT().CreateNetworkACL(gomock.Any()).Return(nil, apiErr),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               Network:    mockNetwork,
+                               NetworkACL: mockNetworkACL,
+                       },
+               }
+
+               _, err := lb.updateNetworkACL(80, LoadBalancerProtocolTCP, 
"net-123")
+               if err == nil {
+                       t.Fatalf("expected error")
+               }
+               if !strings.Contains(err.Error(), "error creating Network ACL") 
{
+                       t.Errorf("error message = %q, want to contain 'error 
creating Network ACL'", err.Error())
+               }
+       })
+}
+
+func TestDeleteNetworkACLRule(t *testing.T) {
+       t.Run("delete matching rule", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockNetworkACL := cloudstack.NewMockNetworkACLServiceIface(ctrl)
+               listParams := &cloudstack.ListNetworkACLsParams{}
+               listResp := &cloudstack.ListNetworkACLsResponse{
+                       Count: 1,
+                       NetworkACLs: []*cloudstack.NetworkACL{
+                               {
+                                       Id:        "acl-rule-123",
+                                       Protocol:  "tcp",
+                                       Startport: "80",
+                                       Endport:   "80",
+                               },
+                       },
+               }
+
+               deleteParams := &cloudstack.DeleteNetworkACLParams{}
+
+               gomock.InOrder(
+                       
mockNetworkACL.EXPECT().NewListNetworkACLsParams().Return(listParams),
+                       
mockNetworkACL.EXPECT().ListNetworkACLs(gomock.Any()).Return(listResp, nil),
+                       
mockNetworkACL.EXPECT().NewDeleteNetworkACLParams("acl-rule-123").Return(deleteParams),
+                       
mockNetworkACL.EXPECT().DeleteNetworkACL(deleteParams).Return(&cloudstack.DeleteNetworkACLResponse{},
 nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               NetworkACL: mockNetworkACL,
+                       },
+               }
+
+               deleted, err := lb.deleteNetworkACLRule(80, 
LoadBalancerProtocolTCP, "net-123")
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if !deleted {
+                       t.Errorf("deleted = false, want true")
+               }
+       })
+
+       t.Run("no matching rules", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockNetworkACL := cloudstack.NewMockNetworkACLServiceIface(ctrl)
+               listParams := &cloudstack.ListNetworkACLsParams{}
+               listResp := &cloudstack.ListNetworkACLsResponse{
+                       Count:       0,
+                       NetworkACLs: []*cloudstack.NetworkACL{},
+               }
+
+               gomock.InOrder(
+                       
mockNetworkACL.EXPECT().NewListNetworkACLsParams().Return(listParams),
+                       
mockNetworkACL.EXPECT().ListNetworkACLs(gomock.Any()).Return(listResp, nil),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               NetworkACL: mockNetworkACL,
+                       },
+               }
+
+               deleted, err := lb.deleteNetworkACLRule(80, 
LoadBalancerProtocolTCP, "net-123")
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if !deleted {
+                       t.Errorf("deleted = false, want true")
+               }
+       })
+
+       t.Run("error listing ACLs", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockNetworkACL := cloudstack.NewMockNetworkACLServiceIface(ctrl)
+               listParams := &cloudstack.ListNetworkACLsParams{}
+               apiErr := fmt.Errorf("list ACL API error")
+
+               gomock.InOrder(
+                       
mockNetworkACL.EXPECT().NewListNetworkACLsParams().Return(listParams),
+                       
mockNetworkACL.EXPECT().ListNetworkACLs(gomock.Any()).Return(nil, apiErr),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               NetworkACL: mockNetworkACL,
+                       },
+               }
+
+               _, err := lb.deleteNetworkACLRule(80, LoadBalancerProtocolTCP, 
"net-123")
+               if err == nil {
+                       t.Fatalf("expected error")
+               }
+               if !strings.Contains(err.Error(), "error fetching Network ACL 
rules") {
+                       t.Errorf("error message = %q, want to contain 'error 
fetching Network ACL rules'", err.Error())
+               }
+       })
+
+       t.Run("error deleting ACL", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockNetworkACL := cloudstack.NewMockNetworkACLServiceIface(ctrl)
+               listParams := &cloudstack.ListNetworkACLsParams{}
+               listResp := &cloudstack.ListNetworkACLsResponse{
+                       Count: 1,
+                       NetworkACLs: []*cloudstack.NetworkACL{
+                               {
+                                       Id:        "acl-rule-123",
+                                       Protocol:  "tcp",
+                                       Startport: "80",
+                                       Endport:   "80",
+                               },
+                       },
+               }
+
+               deleteParams := &cloudstack.DeleteNetworkACLParams{}
+               deleteErr := fmt.Errorf("delete ACL API error")
+
+               gomock.InOrder(
+                       
mockNetworkACL.EXPECT().NewListNetworkACLsParams().Return(listParams),
+                       
mockNetworkACL.EXPECT().ListNetworkACLs(gomock.Any()).Return(listResp, nil),
+                       
mockNetworkACL.EXPECT().NewDeleteNetworkACLParams("acl-rule-123").Return(deleteParams),
+                       
mockNetworkACL.EXPECT().DeleteNetworkACL(deleteParams).Return(nil, deleteErr),
+               )
+
+               lb := &loadBalancer{
+                       CloudStackClient: &cloudstack.CloudStackClient{
+                               NetworkACL: mockNetworkACL,
+                       },
+               }
+
+               deleted, err := lb.deleteNetworkACLRule(80, 
LoadBalancerProtocolTCP, "net-123")
+               if deleted {
+                       t.Errorf("deleted = true, want false")
+               }
+               if err != deleteErr {
+                       t.Errorf("error = %v, want %v", err, deleteErr)
+               }
+       })
+}
+
+func TestGetLoadBalancer(t *testing.T) {
+       t.Run("load balancer with existing rules", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+               listParams := &cloudstack.ListLoadBalancerRulesParams{}
+               listResp := &cloudstack.ListLoadBalancerRulesResponse{
+                       Count: 2,
+                       LoadBalancerRules: []*cloudstack.LoadBalancerRule{
+                               {
+                                       Id:          "rule-1",
+                                       Name:        "test-service-tcp-80",
+                                       Publicip:    "203.0.113.1",
+                                       Publicipid:  "ip-123",
+                                       Algorithm:   "roundrobin",
+                                       Protocol:    "tcp",
+                                       Publicport:  "80",
+                                       Privateport: "30000",
+                               },
+                               {
+                                       Id:          "rule-2",
+                                       Name:        "test-service-tcp-443",
+                                       Publicip:    "203.0.113.1",
+                                       Publicipid:  "ip-123",
+                                       Algorithm:   "roundrobin",
+                                       Protocol:    "tcp",
+                                       Publicport:  "443",
+                                       Privateport: "30443",
+                               },
+                       },
+               }
+
+               gomock.InOrder(
+                       
mockLB.EXPECT().NewListLoadBalancerRulesParams().Return(listParams),
+                       
mockLB.EXPECT().ListLoadBalancerRules(gomock.Any()).Return(listResp, nil),
+               )
+
+               cs := &CSCloud{
+                       client: &cloudstack.CloudStackClient{
+                               LoadBalancer: mockLB,
+                       },
+               }
+
+               service := &corev1.Service{
+                       ObjectMeta: metav1.ObjectMeta{
+                               Name:      "test-service",
+                               Namespace: "default",
+                       },
+               }
+
+               lb, err := cs.getLoadBalancer(service)
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if lb.ipAddr != "203.0.113.1" {
+                       t.Errorf("ipAddr = %q, want %q", lb.ipAddr, 
"203.0.113.1")
+               }
+               if lb.ipAddrID != "ip-123" {
+                       t.Errorf("ipAddrID = %q, want %q", lb.ipAddrID, 
"ip-123")
+               }
+               if len(lb.rules) != 2 {
+                       t.Errorf("rules count = %d, want %d", len(lb.rules), 2)
+               }
+       })
+
+       t.Run("load balancer with no rules", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+               listParams := &cloudstack.ListLoadBalancerRulesParams{}
+               listResp := &cloudstack.ListLoadBalancerRulesResponse{
+                       Count:             0,
+                       LoadBalancerRules: []*cloudstack.LoadBalancerRule{},
+               }
+
+               gomock.InOrder(
+                       
mockLB.EXPECT().NewListLoadBalancerRulesParams().Return(listParams),
+                       
mockLB.EXPECT().ListLoadBalancerRules(gomock.Any()).Return(listResp, nil),
+               )
+
+               cs := &CSCloud{
+                       client: &cloudstack.CloudStackClient{
+                               LoadBalancer: mockLB,
+                       },
+               }
+
+               service := &corev1.Service{
+                       ObjectMeta: metav1.ObjectMeta{
+                               Name:      "test-service",
+                               Namespace: "default",
+                       },
+               }
+
+               lb, err := cs.getLoadBalancer(service)
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if len(lb.rules) != 0 {
+                       t.Errorf("rules count = %d, want %d", len(lb.rules), 0)
+               }
+               if lb.ipAddr != "" {
+                       t.Errorf("ipAddr = %q, want empty", lb.ipAddr)
+               }
+       })
+
+       t.Run("error retrieving rules", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl)
+               listParams := &cloudstack.ListLoadBalancerRulesParams{}
+               apiErr := fmt.Errorf("list rules API error")
+
+               gomock.InOrder(
+                       
mockLB.EXPECT().NewListLoadBalancerRulesParams().Return(listParams),
+                       
mockLB.EXPECT().ListLoadBalancerRules(gomock.Any()).Return(nil, apiErr),
+               )
+
+               cs := &CSCloud{
+                       client: &cloudstack.CloudStackClient{
+                               LoadBalancer: mockLB,
+                       },
+               }
+
+               service := &corev1.Service{
+                       ObjectMeta: metav1.ObjectMeta{
+                               Name:      "test-service",
+                               Namespace: "default",
+                       },
+               }
+
+               _, err := cs.getLoadBalancer(service)
+               if err == nil {
+                       t.Fatalf("expected error")
+               }
+               if !strings.Contains(err.Error(), "error retrieving load 
balancer rules") {
+                       t.Errorf("error message = %q, want to contain 'error 
retrieving load balancer rules'", err.Error())
+               }
+       })
+}
+
+func TestGetNetworkIDFromIPAddress(t *testing.T) {
+       t.Run("successful retrieval", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+               mockNetwork := cloudstack.NewMockNetworkServiceIface(ctrl)
+               ipResp := &cloudstack.PublicIpAddress{
+                       Id:                  "ip-123",
+                       Ipaddress:           "203.0.113.1",
+                       Networkid:           "net-123",
+                       Associatednetworkid: "net-123",
+               }
+
+               networkResp := &cloudstack.Network{
+                       Id:      "net-123",
+                       Service: []cloudstack.NetworkServiceInternal{},
+               }
+
+               gomock.InOrder(
+                       
mockAddress.EXPECT().GetPublicIpAddressByID("ip-123").Return(ipResp, 1, nil),
+                       
mockNetwork.EXPECT().GetNetworkByID("net-123").Return(networkResp, 1, nil),
+               )
+
+               cs := &CSCloud{
+                       client: &cloudstack.CloudStackClient{
+                               Address: mockAddress,
+                               Network: mockNetwork,
+                       },
+               }
+
+               networkID, err := cs.getNetworkIDFromIPAddress("ip-123")
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if networkID != "net-123" {
+                       t.Errorf("networkID = %q, want %q", networkID, 
"net-123")
+               }
+       })
+
+       t.Run("IP not found", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockAddress := cloudstack.NewMockAddressServiceIface(ctrl)
+               apiErr := fmt.Errorf("IP not found")
+
+               
mockAddress.EXPECT().GetPublicIpAddressByID("ip-123").Return(nil, 0, apiErr)
+
+               cs := &CSCloud{
+                       client: &cloudstack.CloudStackClient{
+                               Address: mockAddress,
+                       },
+               }
+
+               _, err := cs.getNetworkIDFromIPAddress("ip-123")
+               if err == nil {
+                       t.Fatalf("expected error")
+               }
+               if err != apiErr {
+                       t.Errorf("error = %v, want %v", err, apiErr)
+               }
+       })
+}
+
+func TestVerifyHosts(t *testing.T) {
+       t.Run("all hosts in same network", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockVM := cloudstack.NewMockVirtualMachineServiceIface(ctrl)
+               listParams := &cloudstack.ListVirtualMachinesParams{}
+               listResp := &cloudstack.ListVirtualMachinesResponse{
+                       Count: 2,
+                       VirtualMachines: []*cloudstack.VirtualMachine{
+                               {
+                                       Id:   "vm-1",
+                                       Name: "node-1",
+                                       Nic: []cloudstack.Nic{
+                                               {Networkid: "net-123"},
+                                       },
+                               },
+                               {
+                                       Id:   "vm-2",
+                                       Name: "node-2",
+                                       Nic: []cloudstack.Nic{
+                                               {Networkid: "net-123"},
+                                       },
+                               },
+                       },
+               }
+
+               gomock.InOrder(
+                       
mockVM.EXPECT().NewListVirtualMachinesParams().Return(listParams),
+                       
mockVM.EXPECT().ListVirtualMachines(gomock.Any()).Return(listResp, nil),
+               )
+
+               cs := &CSCloud{
+                       client: &cloudstack.CloudStackClient{
+                               VirtualMachine: mockVM,
+                       },
+               }
+
+               nodes := []*corev1.Node{
+                       {ObjectMeta: metav1.ObjectMeta{Name: "node-1"}},
+                       {ObjectMeta: metav1.ObjectMeta{Name: "node-2"}},
+               }
+
+               hostIDs, networkID, err := cs.verifyHosts(nodes)
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if len(hostIDs) != 2 {
+                       t.Errorf("hostIDs count = %d, want %d", len(hostIDs), 2)
+               }
+               if networkID != "net-123" {
+                       t.Errorf("networkID = %q, want %q", networkID, 
"net-123")
+               }
+       })
+
+       t.Run("hosts in different networks", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockVM := cloudstack.NewMockVirtualMachineServiceIface(ctrl)
+               listParams := &cloudstack.ListVirtualMachinesParams{}
+               listResp := &cloudstack.ListVirtualMachinesResponse{
+                       Count: 2,
+                       VirtualMachines: []*cloudstack.VirtualMachine{
+                               {
+                                       Id:   "vm-1",
+                                       Name: "node-1",
+                                       Nic: []cloudstack.Nic{
+                                               {Networkid: "net-123"},
+                                       },
+                               },
+                               {
+                                       Id:   "vm-2",
+                                       Name: "node-2",
+                                       Nic: []cloudstack.Nic{
+                                               {Networkid: "net-456"},
+                                       },
+                               },
+                       },
+               }
+
+               gomock.InOrder(
+                       
mockVM.EXPECT().NewListVirtualMachinesParams().Return(listParams),
+                       
mockVM.EXPECT().ListVirtualMachines(gomock.Any()).Return(listResp, nil),
+               )
+
+               cs := &CSCloud{
+                       client: &cloudstack.CloudStackClient{
+                               VirtualMachine: mockVM,
+                       },
+               }
+
+               nodes := []*corev1.Node{
+                       {ObjectMeta: metav1.ObjectMeta{Name: "node-1"}},
+                       {ObjectMeta: metav1.ObjectMeta{Name: "node-2"}},
+               }
+
+               _, _, err := cs.verifyHosts(nodes)
+               if err == nil {
+                       t.Fatalf("expected error")
+               }
+               if !strings.Contains(err.Error(), "different networks") {
+                       t.Errorf("error message = %q, want to contain 
'different networks'", err.Error())
+               }
+       })
+
+       t.Run("no matching hosts", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockVM := cloudstack.NewMockVirtualMachineServiceIface(ctrl)
+               listParams := &cloudstack.ListVirtualMachinesParams{}
+               listResp := &cloudstack.ListVirtualMachinesResponse{
+                       Count:           0,
+                       VirtualMachines: []*cloudstack.VirtualMachine{},
+               }
+
+               gomock.InOrder(
+                       
mockVM.EXPECT().NewListVirtualMachinesParams().Return(listParams),
+                       
mockVM.EXPECT().ListVirtualMachines(gomock.Any()).Return(listResp, nil),
+               )
+
+               cs := &CSCloud{
+                       client: &cloudstack.CloudStackClient{
+                               VirtualMachine: mockVM,
+                       },
+               }
+
+               nodes := []*corev1.Node{
+                       {ObjectMeta: metav1.ObjectMeta{Name: "node-1"}},
+               }
+
+               _, _, err := cs.verifyHosts(nodes)
+               if err == nil {
+                       t.Fatalf("expected error")
+               }
+               if !strings.Contains(err.Error(), "none of the hosts matched") {
+                       t.Errorf("error message = %q, want to contain 'none of 
the hosts matched'", err.Error())
+               }
+       })
+
+       t.Run("FQDN node names", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockVM := cloudstack.NewMockVirtualMachineServiceIface(ctrl)
+               listParams := &cloudstack.ListVirtualMachinesParams{}
+               listResp := &cloudstack.ListVirtualMachinesResponse{
+                       Count: 1,
+                       VirtualMachines: []*cloudstack.VirtualMachine{
+                               {
+                                       Id:   "vm-1",
+                                       Name: "node-1",
+                                       Nic: []cloudstack.Nic{
+                                               {Networkid: "net-123"},
+                                       },
+                               },
+                       },
+               }
+
+               gomock.InOrder(
+                       
mockVM.EXPECT().NewListVirtualMachinesParams().Return(listParams),
+                       
mockVM.EXPECT().ListVirtualMachines(gomock.Any()).Return(listResp, nil),
+               )
+
+               cs := &CSCloud{
+                       client: &cloudstack.CloudStackClient{
+                               VirtualMachine: mockVM,
+                       },
+               }
+
+               nodes := []*corev1.Node{
+                       {ObjectMeta: metav1.ObjectMeta{Name: 
"node-1.example.com"}},
+               }
+
+               hostIDs, networkID, err := cs.verifyHosts(nodes)
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if len(hostIDs) != 1 {
+                       t.Errorf("hostIDs count = %d, want %d", len(hostIDs), 1)
+               }
+               if networkID != "net-123" {
+                       t.Errorf("networkID = %q, want %q", networkID, 
"net-123")
+               }
+       })
+
+       t.Run("case-insensitive matching", func(t *testing.T) {
+               ctrl := gomock.NewController(t)
+               t.Cleanup(ctrl.Finish)
+
+               mockVM := cloudstack.NewMockVirtualMachineServiceIface(ctrl)
+               listParams := &cloudstack.ListVirtualMachinesParams{}
+               listResp := &cloudstack.ListVirtualMachinesResponse{
+                       Count: 1,
+                       VirtualMachines: []*cloudstack.VirtualMachine{
+                               {
+                                       Id:   "vm-1",
+                                       Name: "NODE-1",
+                                       Nic: []cloudstack.Nic{
+                                               {Networkid: "net-123"},
+                                       },
+                               },
+                       },
+               }
+
+               gomock.InOrder(
+                       
mockVM.EXPECT().NewListVirtualMachinesParams().Return(listParams),
+                       
mockVM.EXPECT().ListVirtualMachines(gomock.Any()).Return(listResp, nil),
+               )
+
+               cs := &CSCloud{
+                       client: &cloudstack.CloudStackClient{
+                               VirtualMachine: mockVM,
+                       },
+               }
+
+               nodes := []*corev1.Node{
+                       {ObjectMeta: metav1.ObjectMeta{Name: "node-1"}},
+               }
+
+               hostIDs, networkID, err := cs.verifyHosts(nodes)
+               if err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if len(hostIDs) != 1 {
+                       t.Errorf("hostIDs count = %d, want %d", len(hostIDs), 1)
+               }
+               if networkID != "net-123" {
+                       t.Errorf("networkID = %q, want %q", networkID, 
"net-123")
+               }
+       })
+}
diff --git a/cloudstack_test.go b/cloudstack_test.go
index a83b45b8..87ed02fd 100644
--- a/cloudstack_test.go
+++ b/cloudstack_test.go
@@ -275,3 +275,43 @@ func TestGetManagementServerVersion(t *testing.T) {
                }
        })
 }
+
+func TestGetRegionFromZone(t *testing.T) {
+       tests := []struct {
+               name   string
+               region string
+               zone   string
+               want   string
+       }{
+               {
+                       name:   "region configured in cloud config",
+                       region: "us-east-1",
+                       zone:   "zone-1",
+                       want:   "us-east-1",
+               },
+               {
+                       name:   "region not configured, returns zone",
+                       region: "",
+                       zone:   "zone-1",
+                       want:   "zone-1",
+               },
+               {
+                       name:   "region configured with empty zone",
+                       region: "eu-central-1",
+                       zone:   "",
+                       want:   "eu-central-1",
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       cs := &CSCloud{
+                               region: tt.region,
+                       }
+                       got := cs.getRegionFromZone(tt.zone)
+                       if got != tt.want {
+                               t.Errorf("getRegionFromZone(%q) with region=%q 
= %q, want %q", tt.zone, tt.region, got, tt.want)
+                       }
+               })
+       }
+}


Reply via email to