blob: 78896abf8c84131ac5eb28e47e6dc058ae5de14e [file]
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
package cloudstack
import (
"net"
"strings"
"testing"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
func TestParseCIDRv6_DefaultGateway(t *testing.T) {
d := schema.TestResourceDataRaw(t, resourceCloudStackNetwork().Schema, map[string]interface{}{
"ip6cidr": "2001:db8::/64",
})
result, err := parseCIDRv6(d, false)
if err != nil {
t.Fatalf("parseCIDRv6 failed: %v", err)
}
// Default gateway should be network address + 1
expectedGateway := "2001:db8::1"
if result["ip6gateway"] != expectedGateway {
t.Errorf("Expected gateway %s, got %s", expectedGateway, result["ip6gateway"])
}
// When specifyiprange is false, startipv6 and endipv6 should not be set
if _, ok := result["startipv6"]; ok {
t.Errorf("startipv6 should not be set when specifyiprange is false")
}
if _, ok := result["endipv6"]; ok {
t.Errorf("endipv6 should not be set when specifyiprange is false")
}
}
func TestParseCIDRv6_CustomGateway(t *testing.T) {
d := schema.TestResourceDataRaw(t, resourceCloudStackNetwork().Schema, map[string]interface{}{
"ip6cidr": "2001:db8::/64",
"ip6gateway": "2001:db8::1",
})
result, err := parseCIDRv6(d, false)
if err != nil {
t.Fatalf("parseCIDRv6 failed: %v", err)
}
expectedGateway := "2001:db8::1"
if result["ip6gateway"] != expectedGateway {
t.Errorf("Expected gateway %s, got %s", expectedGateway, result["ip6gateway"])
}
}
func TestParseCIDRv6_WithIPRange(t *testing.T) {
d := schema.TestResourceDataRaw(t, resourceCloudStackNetwork().Schema, map[string]interface{}{
"ip6cidr": "2001:db8::/64",
})
result, err := parseCIDRv6(d, true)
if err != nil {
t.Fatalf("parseCIDRv6 failed: %v", err)
}
// Check gateway (should be network address + 1)
expectedGateway := "2001:db8::1"
if result["ip6gateway"] != expectedGateway {
t.Errorf("Expected gateway %s, got %s", expectedGateway, result["ip6gateway"])
}
// Check start IP (should be network address + 2)
expectedStartIP := "2001:db8::2"
if result["startipv6"] != expectedStartIP {
t.Errorf("Expected start IP %s, got %s", expectedStartIP, result["startipv6"])
}
// Check end IP (should be the last address in the /64 range)
expectedEndIP := "2001:db8::ffff:ffff:ffff:ffff"
if result["endipv6"] != expectedEndIP {
t.Errorf("Expected end IP %s, got %s", expectedEndIP, result["endipv6"])
}
}
func TestParseCIDRv6_CustomIPRange(t *testing.T) {
d := schema.TestResourceDataRaw(t, resourceCloudStackNetwork().Schema, map[string]interface{}{
"ip6cidr": "2001:db8:1::/64",
"startipv6": "2001:db8:1::100",
"endipv6": "2001:db8:1::200",
})
result, err := parseCIDRv6(d, true)
if err != nil {
t.Fatalf("parseCIDRv6 failed: %v", err)
}
// Check that custom values are used
if result["startipv6"] != "2001:db8:1::100" {
t.Errorf("Expected custom start IP 2001:db8:1::100, got %s", result["startipv6"])
}
if result["endipv6"] != "2001:db8:1::200" {
t.Errorf("Expected custom end IP 2001:db8:1::200, got %s", result["endipv6"])
}
}
func TestParseCIDRv6_SmallerPrefix(t *testing.T) {
d := schema.TestResourceDataRaw(t, resourceCloudStackNetwork().Schema, map[string]interface{}{
"ip6cidr": "2001:db8::/48",
})
result, err := parseCIDRv6(d, true)
if err != nil {
t.Fatalf("parseCIDRv6 failed: %v", err)
}
// For a /48, the end IP should have the last 80 bits set to 1
expectedEndIP := "2001:db8:0:ffff:ffff:ffff:ffff:ffff"
if result["endipv6"] != expectedEndIP {
t.Errorf("Expected end IP %s, got %s", expectedEndIP, result["endipv6"])
}
}
func TestParseCIDRv6_RejectsIPv4(t *testing.T) {
d := schema.TestResourceDataRaw(t, resourceCloudStackNetwork().Schema, map[string]interface{}{
"ip6cidr": "10.0.0.0/24",
})
_, err := parseCIDRv6(d, false)
if err == nil {
t.Fatal("parseCIDRv6 should reject IPv4 CIDR")
}
expectedError := "ip6cidr must be an IPv6 CIDR, got IPv4"
if !strings.HasPrefix(err.Error(), expectedError) {
t.Errorf("Expected error message to start with '%s', got '%s'", expectedError, err.Error())
}
}
func TestParseCIDRv6_Prefix128_NoIPRange(t *testing.T) {
// /128 is a single address - should fail even without IP range
d := schema.TestResourceDataRaw(t, resourceCloudStackNetwork().Schema, map[string]interface{}{
"ip6cidr": "2001:db8::1/128",
})
_, err := parseCIDRv6(d, false)
if err == nil {
t.Fatal("parseCIDRv6 should reject /128 prefix (single address)")
}
expectedError := "ip6cidr prefix /128 is too small"
if !strings.HasPrefix(err.Error(), expectedError) {
t.Errorf("Expected error message to start with '%s', got '%s'", expectedError, err.Error())
}
}
func TestParseCIDRv6_Prefix127_NoIPRange(t *testing.T) {
// /127 has 2 addresses - should work without IP range (only needs gateway)
d := schema.TestResourceDataRaw(t, resourceCloudStackNetwork().Schema, map[string]interface{}{
"ip6cidr": "2001:db8::/127",
})
result, err := parseCIDRv6(d, false)
if err != nil {
t.Fatalf("parseCIDRv6 should accept /127 prefix without IP range: %v", err)
}
// Should have gateway
if _, ok := result["ip6gateway"]; !ok {
t.Error("Expected ip6gateway to be set")
}
// Should not have start/end IP
if _, ok := result["startipv6"]; ok {
t.Error("startipv6 should not be set when specifyiprange is false")
}
}
func TestParseCIDRv6_Prefix127_WithIPRange(t *testing.T) {
// /127 has only 2 addresses - should fail with IP range (needs 3+ addresses)
d := schema.TestResourceDataRaw(t, resourceCloudStackNetwork().Schema, map[string]interface{}{
"ip6cidr": "2001:db8::/127",
})
_, err := parseCIDRv6(d, true)
if err == nil {
t.Fatal("parseCIDRv6 should reject /127 prefix with IP range (only 2 addresses)")
}
expectedError := "ip6cidr prefix /127 is too small for automatic IP range generation"
if !strings.HasPrefix(err.Error(), expectedError) {
t.Errorf("Expected error message to start with '%s', got '%s'", expectedError, err.Error())
}
}
func TestParseCIDRv6_Prefix126_WithIPRange(t *testing.T) {
// /126 has 4 addresses - should work with IP range
d := schema.TestResourceDataRaw(t, resourceCloudStackNetwork().Schema, map[string]interface{}{
"ip6cidr": "2001:db8::/126",
})
result, err := parseCIDRv6(d, true)
if err != nil {
t.Fatalf("parseCIDRv6 should accept /126 prefix with IP range: %v", err)
}
// Should have gateway, start, and end
if _, ok := result["ip6gateway"]; !ok {
t.Error("Expected ip6gateway to be set")
}
if _, ok := result["startipv6"]; !ok {
t.Error("Expected startipv6 to be set")
}
if _, ok := result["endipv6"]; !ok {
t.Error("Expected endipv6 to be set")
}
// Verify the end IP is correct for /126 (last 2 bits set to 1)
expectedEndIP := "2001:db8::3"
if result["endipv6"] != expectedEndIP {
t.Errorf("Expected end IP %s for /126, got %s", expectedEndIP, result["endipv6"])
}
}
func TestParseCIDRv6_NonZeroNetworkAddress(t *testing.T) {
// Test with a CIDR where the network address doesn't end in ::0
// This tests the fix for proper IPv6 address arithmetic with carry
d := schema.TestResourceDataRaw(t, resourceCloudStackNetwork().Schema, map[string]interface{}{
"ip6cidr": "2001:db8::4/126",
})
result, err := parseCIDRv6(d, true)
if err != nil {
t.Fatalf("parseCIDRv6 failed: %v", err)
}
// For 2001:db8::4/126, the network is 2001:db8::4
// Gateway should be network + 1 = 2001:db8::5
expectedGateway := "2001:db8::5"
if result["ip6gateway"] != expectedGateway {
t.Errorf("Expected gateway %s, got %s", expectedGateway, result["ip6gateway"])
}
// Start IP should be network + 2 = 2001:db8::6
expectedStartIP := "2001:db8::6"
if result["startipv6"] != expectedStartIP {
t.Errorf("Expected start IP %s, got %s", expectedStartIP, result["startipv6"])
}
// End IP should be network + 3 = 2001:db8::7 (last address in /126)
expectedEndIP := "2001:db8::7"
if result["endipv6"] != expectedEndIP {
t.Errorf("Expected end IP %s, got %s", expectedEndIP, result["endipv6"])
}
}
func TestParseCIDRv6_NonAlignedPrefix(t *testing.T) {
// Test with a /124 prefix where network address has non-zero low-order bits
d := schema.TestResourceDataRaw(t, resourceCloudStackNetwork().Schema, map[string]interface{}{
"ip6cidr": "2001:db8::f0/124",
})
result, err := parseCIDRv6(d, true)
if err != nil {
t.Fatalf("parseCIDRv6 failed: %v", err)
}
// For 2001:db8::f0/124, the network is 2001:db8::f0
// Gateway should be network + 1 = 2001:db8::f1
expectedGateway := "2001:db8::f1"
if result["ip6gateway"] != expectedGateway {
t.Errorf("Expected gateway %s, got %s", expectedGateway, result["ip6gateway"])
}
// Start IP should be network + 2 = 2001:db8::f2
expectedStartIP := "2001:db8::f2"
if result["startipv6"] != expectedStartIP {
t.Errorf("Expected start IP %s, got %s", expectedStartIP, result["startipv6"])
}
// End IP should be 2001:db8::ff (last address in /124)
expectedEndIP := "2001:db8::ff"
if result["endipv6"] != expectedEndIP {
t.Errorf("Expected end IP %s, got %s", expectedEndIP, result["endipv6"])
}
}
func TestAddToIPv6_CarryAcrossBytes(t *testing.T) {
// Exercise addToIPv6 directly with addresses that force a carry from the
// low byte into higher bytes (which a masked network address never would).
cases := []struct {
name string
input string
offset uint64
expected string
}{
{"carry into next byte", "2001:db8::ff", 1, "2001:db8::100"},
{"carry across two bytes", "2001:db8::ffff", 1, "2001:db8::1:0"},
{"offset of two with carry", "2001:db8::ff", 2, "2001:db8::101"},
{"no carry", "2001:db8::", 1, "2001:db8::1"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := addToIPv6(net.ParseIP(tc.input), tc.offset).String()
if got != tc.expected {
t.Errorf("addToIPv6(%s, %d) = %s, expected %s", tc.input, tc.offset, got, tc.expected)
}
})
}
}