You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by vi...@apache.org on 2025/12/15 07:54:02 UTC

(cloudstack-kubernetes-provider) 01/01: add some unit tests

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 <vi...@gmail.com>
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)
+			}
+		})
+	}
+}