diff --git a/internal/commands/mcr/mcr_prompts.go b/internal/commands/mcr/mcr_prompts.go index febbc407..ea3c4160 100644 --- a/internal/commands/mcr/mcr_prompts.go +++ b/internal/commands/mcr/mcr_prompts.go @@ -448,6 +448,10 @@ func promptForPrefixFilterListDetails(mcrUID string, noColor bool) (*megaport.Cr }, } + if err := validation.ValidatePrefixFilterListRequest(req); err != nil { + return nil, err + } + return req, nil } diff --git a/internal/commands/mcr/mcr_prompts_test.go b/internal/commands/mcr/mcr_prompts_test.go index 16ffeffd..4b9cafc1 100644 --- a/internal/commands/mcr/mcr_prompts_test.go +++ b/internal/commands/mcr/mcr_prompts_test.go @@ -503,6 +503,38 @@ func TestPromptForPrefixFilterListDetails_NoEntries(t *testing.T) { assert.Contains(t, err.Error(), "at least one entry is required") } +func TestPromptForPrefixFilterListDetails_InvalidPrefix(t *testing.T) { + originalPrompt := utils.GetResourcePrompt() + defer func() { utils.SetResourcePrompt(originalPrompt) }() + + // Prefix that isn't a CIDR passes the entry prompt but must fail + // request-level validation before anything reaches the API. + utils.SetResourcePrompt(mockPromptSequence([]string{ + "My PFL", "IPv4", + "banana", "permit", "", "", + "", // stop adding entries + })) + + _, err := promptForPrefixFilterListDetails("mcr-123", true) + assert.Error(t, err) + assert.Contains(t, err.Error(), "must be a valid IPv4 CIDR notation") +} + +func TestPromptForPrefixFilterListDetails_WrongFamilyPrefix(t *testing.T) { + originalPrompt := utils.GetResourcePrompt() + defer func() { utils.SetResourcePrompt(originalPrompt) }() + + utils.SetResourcePrompt(mockPromptSequence([]string{ + "My PFL", "IPv6", + "10.0.0.0/8", "permit", "", "", + "", // stop adding entries + })) + + _, err := promptForPrefixFilterListDetails("mcr-123", true) + assert.Error(t, err) + assert.Contains(t, err.Error(), "must be a valid IPv6 CIDR notation") +} + func TestPromptForIPSecTunnelCount(t *testing.T) { tests := []struct { name string diff --git a/internal/commands/mve/mve_prompts.go b/internal/commands/mve/mve_prompts.go index 7b88115e..cf64e03c 100644 --- a/internal/commands/mve/mve_prompts.go +++ b/internal/commands/mve/mve_prompts.go @@ -393,6 +393,9 @@ func promptMVEVnics(noColor bool) ([]megaport.MVENetworkInterface, error) { if err != nil { return nil, err } + if err := validation.ValidateVLAN(vlan); err != nil { + return nil, err + } } vnics = append(vnics, megaport.MVENetworkInterface{ diff --git a/internal/commands/mve/mve_prompts_test.go b/internal/commands/mve/mve_prompts_test.go index d880bd05..13298c09 100644 --- a/internal/commands/mve/mve_prompts_test.go +++ b/internal/commands/mve/mve_prompts_test.go @@ -400,6 +400,17 @@ func TestPromptMVEVnics_InvalidVLAN(t *testing.T) { assert.Contains(t, err.Error(), "invalid VLAN ID") } +func TestPromptMVEVnics_OutOfRangeVLAN(t *testing.T) { + original := utils.GetResourcePrompt() + defer func() { utils.SetResourcePrompt(original) }() + + utils.SetResourcePrompt(mockPromptSequence([]string{"eth0", "4100"})) + + _, err := promptMVEVnics(true) + assert.Error(t, err) + assert.Contains(t, err.Error(), "VLAN ID") +} + // promptMVEVendorConfig tests — cisco/palo_alto admin password handling func TestPromptMVEVendorConfig_Cisco_WithAdminPassword(t *testing.T) { diff --git a/internal/commands/vxc/vxc_prompts_partner.go b/internal/commands/vxc/vxc_prompts_partner.go index 3792f641..7091f490 100644 --- a/internal/commands/vxc/vxc_prompts_partner.go +++ b/internal/commands/vxc/vxc_prompts_partner.go @@ -3,7 +3,6 @@ package vxc import ( "context" "fmt" - "strconv" "strings" "github.com/megaport/megaport-cli/internal/utils" @@ -98,8 +97,11 @@ func promptAWSConfig(noColor bool) (*megaport.VXCPartnerConfigAWS, error) { if err != nil { return nil, err } + if ownerAccount == "" { + return nil, fmt.Errorf("owner account ID is required") + } - connectionName, err := utils.ResourcePrompt("vxc", "Enter connection name (required): ", noColor) + connectionName, err := utils.ResourcePrompt("vxc", "Enter connection name (optional, defaults to MEGAPORT): ", noColor) if err != nil { return nil, err } @@ -108,12 +110,12 @@ func promptAWSConfig(noColor bool) (*megaport.VXCPartnerConfigAWS, error) { if err != nil { return nil, err } - var asn int - if asnStr != "" { - asn, err = validation.ParseInt("ASN", asnStr) - if err != nil { - return nil, err - } + if asnStr == "" { + return nil, fmt.Errorf("ASN is required") + } + asn, err := validation.ParseInt("ASN", asnStr) + if err != nil { + return nil, err } amazonASNStr, err := utils.ResourcePrompt("vxc", "Enter Amazon ASN (optional): ", noColor) @@ -177,6 +179,9 @@ func promptAzureConfig(ctx context.Context, svc megaport.VXCService, noColor boo if err != nil { return nil, "", err } + if serviceKey == "" { + return nil, "", fmt.Errorf("service key is required") + } portChoice, err := utils.ResourcePrompt("vxc", "Enter port choice (primary/secondary, optional, default value is primary): ", noColor) if err != nil { @@ -268,9 +273,15 @@ func promptAzurePeeringConfig(noColor bool) (megaport.PartnerOrderAzurePeeringCo if err != nil { return megaport.PartnerOrderAzurePeeringConfig{}, err } - vlan, err := strconv.Atoi(vlanStr) - if err != nil { - vlan = 0 + var vlan int + if vlanStr != "" { + vlan, err = validation.ParseInt("VLAN ID", vlanStr) + if err != nil { + return megaport.PartnerOrderAzurePeeringConfig{}, err + } + if err := validation.ValidateVLAN(vlan); err != nil { + return megaport.PartnerOrderAzurePeeringConfig{}, err + } } return megaport.PartnerOrderAzurePeeringConfig{ @@ -289,6 +300,9 @@ func promptGoogleConfig(ctx context.Context, svc megaport.VXCService, noColor bo if err != nil { return nil, "", err } + if pairingKey == "" { + return nil, "", fmt.Errorf("pairing key is required") + } uid, err := getPartnerPortUID(ctx, svc, pairingKey, "GOOGLE") if err != nil { @@ -306,6 +320,9 @@ func promptOracleConfig(ctx context.Context, svc megaport.VXCService, noColor bo if err != nil { return nil, "", err } + if virtualCircuitId == "" { + return nil, "", fmt.Errorf("virtual circuit ID is required") + } uid, err := getPartnerPortUID(ctx, svc, virtualCircuitId, "ORACLE") if err != nil { @@ -323,24 +340,25 @@ func promptIBMConfig(noColor bool) (*megaport.VXCPartnerConfigIBM, error) { if err != nil { return nil, err } + if accountID == "" { + return nil, fmt.Errorf("account ID is required") + } - name, err := utils.ResourcePrompt("vxc", "Enter name (required): ", noColor) + name, err := utils.ResourcePrompt("vxc", "Enter name (optional, defaults to MEGAPORT): ", noColor) if err != nil { return nil, err } - if name == "" { - return nil, fmt.Errorf("name is required") - } - - var customerASN int customerASNStr, err := utils.ResourcePrompt("vxc", "Enter customer ASN (required if opposite end is not an MCR): ", noColor) if err != nil { return nil, err } - customerASN, err = validation.ParseInt("customer ASN", customerASNStr) - if err != nil { - return nil, err + var customerASN int + if customerASNStr != "" { + customerASN, err = validation.ParseInt("customer ASN", customerASNStr) + if err != nil { + return nil, err + } } customerIPAddress, err := utils.ResourcePrompt("vxc", "Enter customer IP address (optional): ", noColor) diff --git a/internal/commands/vxc/vxc_prompts_test.go b/internal/commands/vxc/vxc_prompts_test.go index dfa00a5f..459acbe5 100644 --- a/internal/commands/vxc/vxc_prompts_test.go +++ b/internal/commands/vxc/vxc_prompts_test.go @@ -66,6 +66,16 @@ func TestPromptAWSConfig(t *testing.T) { assert.Equal(t, "", cfg.Type) }, }, + { + name: "connection name left blank (API defaults to MEGAPORT)", + responses: []string{"AWS", "123456789", "", "65000", "", "", "", "", "", "private"}, + verify: func(t *testing.T, cfg *megaport.VXCPartnerConfigAWS) { + assert.Equal(t, "AWS", cfg.ConnectType) + assert.Equal(t, "123456789", cfg.OwnerAccount) + assert.Equal(t, "", cfg.ConnectionName) + assert.Equal(t, 65000, cfg.ASN) + }, + }, } for _, tc := range tests { @@ -81,6 +91,37 @@ func TestPromptAWSConfig(t *testing.T) { } } +func TestPromptAWSConfig_RequiresCredentials(t *testing.T) { + tests := []struct { + name string + responses []string + errContains string + }{ + { + name: "empty owner account rejected", + responses: []string{"AWS", ""}, + errContains: "owner account ID is required", + }, + { + name: "empty ASN rejected", + responses: []string{"AWS", "123456789", "my-conn", ""}, + errContains: "ASN is required", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cleanup := mockPrompts(tc.responses) + defer cleanup() + + cfg, err := promptAWSConfig(true) + assert.Error(t, err) + assert.Nil(t, cfg) + assert.Contains(t, err.Error(), tc.errContains) + }) + } +} + func TestPromptGoogleConfig(t *testing.T) { tests := []struct { name string @@ -124,6 +165,17 @@ func TestPromptGoogleConfig(t *testing.T) { } } +func TestPromptGoogleConfig_RequiresPairingKey(t *testing.T) { + cleanup := mockPrompts([]string{""}) + defer cleanup() + + cfg, uid, err := promptGoogleConfig(context.Background(), &MockVXCService{}, true) + assert.Error(t, err) + assert.Nil(t, cfg) + assert.Empty(t, uid) + assert.Contains(t, err.Error(), "pairing key is required") +} + func TestPromptOracleConfig(t *testing.T) { tests := []struct { name string @@ -165,6 +217,17 @@ func TestPromptOracleConfig(t *testing.T) { } } +func TestPromptOracleConfig_RequiresVirtualCircuitID(t *testing.T) { + cleanup := mockPrompts([]string{""}) + defer cleanup() + + cfg, uid, err := promptOracleConfig(context.Background(), &MockVXCService{}, true) + assert.Error(t, err) + assert.Nil(t, cfg) + assert.Empty(t, uid) + assert.Contains(t, err.Error(), "virtual circuit ID is required") +} + func TestPromptIBMConfig(t *testing.T) { tests := []struct { name string @@ -195,6 +258,28 @@ func TestPromptIBMConfig(t *testing.T) { assert.Equal(t, "6.7.8.9", cfg.ProviderIPAddress) }, }, + { + name: "name left blank (API defaults to MEGAPORT)", + responses: []string{"acct-000", "", "65001", "4.5.6.7", "8.9.10.11"}, + verify: func(t *testing.T, cfg *megaport.VXCPartnerConfigIBM) { + assert.Equal(t, "IBM", cfg.ConnectType) + assert.Equal(t, "acct-000", cfg.AccountID) + assert.Equal(t, "", cfg.Name) + assert.Equal(t, 65001, cfg.CustomerASN) + }, + }, + { + name: "customer ASN left blank", + responses: []string{"acct-789", "ibm-blank-asn", "", "3.4.5.6", "7.8.9.10"}, + verify: func(t *testing.T, cfg *megaport.VXCPartnerConfigIBM) { + assert.Equal(t, "IBM", cfg.ConnectType) + assert.Equal(t, "acct-789", cfg.AccountID) + assert.Equal(t, "ibm-blank-asn", cfg.Name) + assert.Equal(t, 0, cfg.CustomerASN) + assert.Equal(t, "3.4.5.6", cfg.CustomerIPAddress) + assert.Equal(t, "7.8.9.10", cfg.ProviderIPAddress) + }, + }, } for _, tc := range tests { @@ -210,6 +295,16 @@ func TestPromptIBMConfig(t *testing.T) { } } +func TestPromptIBMConfig_RequiresAccountID(t *testing.T) { + cleanup := mockPrompts([]string{""}) + defer cleanup() + + cfg, err := promptIBMConfig(true) + assert.Error(t, err) + assert.Nil(t, cfg) + assert.Contains(t, err.Error(), "account ID is required") +} + func TestPromptBFDConfig(t *testing.T) { tests := []struct { name string @@ -442,6 +537,28 @@ func TestPromptAzureConfig(t *testing.T) { assert.Equal(t, "azure-uid-1", uid) } +func TestPromptAzureConfig_RequiresServiceKey(t *testing.T) { + cleanup := mockPrompts([]string{""}) + defer cleanup() + + mockSvc := &MockVXCService{ + ListPartnerPortsResponse: &megaport.ListPartnerPortsResponse{ + Data: megaport.PartnerLookup{ + Megaports: []megaport.PartnerLookupItem{ + {ProductUID: "azure-uid-1", Type: "primary"}, + }, + }, + }, + } + + ctx := context.Background() + cfg, uid, err := promptAzureConfig(ctx, mockSvc, true) + assert.Error(t, err) + assert.Nil(t, cfg) + assert.Empty(t, uid) + assert.Contains(t, err.Error(), "service key is required") +} + func TestPromptAzurePeeringConfig(t *testing.T) { cleanup := mockPrompts([]string{ "Microsoft", // peering type @@ -465,6 +582,57 @@ func TestPromptAzurePeeringConfig(t *testing.T) { assert.Equal(t, 200, peer.VLAN) } +func TestPromptAzurePeeringConfig_InvalidVLAN(t *testing.T) { + cleanup := mockPrompts([]string{ + "Microsoft", // peering type + "12076", // peer ASN + "10.0.0.0/30", + "10.0.0.4/30", + "10.2.0.0/16", + "key123", + "not-a-number", + }) + defer cleanup() + + _, err := promptAzurePeeringConfig(true) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid VLAN") +} + +func TestPromptAzurePeeringConfig_OutOfRangeVLAN(t *testing.T) { + cleanup := mockPrompts([]string{ + "Microsoft", // peering type + "12076", // peer ASN + "10.0.0.0/30", + "10.0.0.4/30", + "10.2.0.0/16", + "key123", + "4100", + }) + defer cleanup() + + _, err := promptAzurePeeringConfig(true) + assert.Error(t, err) + assert.Contains(t, err.Error(), "VLAN ID") +} + +func TestPromptAzurePeeringConfig_EmptyVLANDefaultsToZero(t *testing.T) { + cleanup := mockPrompts([]string{ + "Microsoft", // peering type + "12076", // peer ASN + "10.0.0.0/30", + "10.0.0.4/30", + "10.2.0.0/16", + "key123", + "", + }) + defer cleanup() + + peer, err := promptAzurePeeringConfig(true) + assert.NoError(t, err) + assert.Equal(t, 0, peer.VLAN) +} + func TestPromptPartnerConfig(t *testing.T) { tests := []struct { name string diff --git a/internal/validation/mcr.go b/internal/validation/mcr.go index 4b76a04b..b362dc66 100644 --- a/internal/validation/mcr.go +++ b/internal/validation/mcr.go @@ -2,6 +2,7 @@ package validation import ( "fmt" + "net" "strings" megaport "github.com/megaport/megaportgo" @@ -98,6 +99,7 @@ func ValidateMCRRequest(req *megaport.BuyMCRRequest) error { // - At least one entry must be provided in the prefix filter list // - For each entry: // - Prefix cannot be empty +// - Prefix must be a valid CIDR consistent with the list's address family // - Action must be "permit" or "deny" // // Returns: @@ -117,16 +119,53 @@ func ValidatePrefixFilterListRequest(req *megaport.CreateMCRPrefixFilterListRequ return NewValidationError("entries", req.PrefixFilterList.Entries, "must contain at least one entry") } - // Validate each entry - for i, entry := range req.PrefixFilterList.Entries { + return validatePrefixFilterEntries(req.PrefixFilterList.Entries, req.PrefixFilterList.AddressFamily) +} + +// validatePrefixFilterEntries validates each prefix filter entry's prefix as a +// CIDR consistent with the list's declared address family, that the action is +// permit or deny, and that any GE/LE bounds fit the family's prefix length +// (GE/LE are optional; 0 means unset, matching their omitempty JSON encoding). +func validatePrefixFilterEntries(entries []*megaport.MCRPrefixListEntry, addressFamily string) error { + var maxPrefixLen int + switch addressFamily { + case "IPv4": + maxPrefixLen = 32 + case "IPv6": + maxPrefixLen = 128 + default: + return NewValidationError("address family", addressFamily, "must be IPv4 or IPv6") + } + for i, entry := range entries { + if entry == nil { + return NewValidationError(fmt.Sprintf("entry index %d", i), nil, "entry cannot be nil") + } if entry.Prefix == "" { - return NewValidationError("entry prefix index", i, "prefix cannot be empty") + return NewValidationError(fmt.Sprintf("entry prefix index %d", i), entry.Prefix, "prefix cannot be empty") + } + if addressFamily == "IPv4" { + if err := ValidateCIDR(entry.Prefix, fmt.Sprintf("entry prefix index %d", i)); err != nil { + return err + } + } else { + ip, _, err := net.ParseCIDR(entry.Prefix) + if err != nil || ip.To4() != nil { + return NewValidationError(fmt.Sprintf("entry prefix index %d", i), entry.Prefix, "must be a valid IPv6 CIDR notation") + } } if entry.Action != "permit" && entry.Action != "deny" { return NewValidationError("entry action", entry.Action, "must be permit or deny") } + if entry.Ge != 0 && (entry.Ge < 0 || entry.Ge > maxPrefixLen) { + return NewValidationError(fmt.Sprintf("entry GE index %d", i), entry.Ge, fmt.Sprintf("must be between 0 and %d for %s", maxPrefixLen, addressFamily)) + } + if entry.Le != 0 && (entry.Le < 0 || entry.Le > maxPrefixLen) { + return NewValidationError(fmt.Sprintf("entry LE index %d", i), entry.Le, fmt.Sprintf("must be between 0 and %d for %s", maxPrefixLen, addressFamily)) + } + if entry.Ge != 0 && entry.Le != 0 && entry.Ge > entry.Le { + return NewValidationError(fmt.Sprintf("entry GE index %d", i), entry.Ge, "must not exceed the LE value") + } } - return nil } @@ -138,26 +177,24 @@ func ValidatePrefixFilterListRequest(req *megaport.CreateMCRPrefixFilterListRequ // // Validation checks: // - If entries are provided: +// - Address family must be provided and a valid value ("IPv4" or "IPv6") // - For each entry: // - Prefix cannot be empty +// - Prefix must be a valid CIDR consistent with the list's address family // - Action must be "permit" or "deny" // // Returns: // - A ValidationError if any validation check fails // - nil if all validation checks pass func ValidateUpdatePrefixFilterList(prefixFilterList *megaport.MCRPrefixFilterList) error { - // If entries are provided, validate them - if len(prefixFilterList.Entries) > 0 { - // Validate each entry - for i, entry := range prefixFilterList.Entries { - if entry.Prefix == "" { - return NewValidationError("entry prefix index", i, "prefix cannot be empty") - } - if entry.Action != "permit" && entry.Action != "deny" { - return NewValidationError("entry action", entry.Action, "must be permit or deny") - } - } + if len(prefixFilterList.Entries) == 0 { + return nil } - - return nil + if prefixFilterList.AddressFamily == "" { + return NewValidationError("address family", prefixFilterList.AddressFamily, "cannot be empty") + } + if prefixFilterList.AddressFamily != "IPv4" && prefixFilterList.AddressFamily != "IPv6" { + return NewValidationError("address family", prefixFilterList.AddressFamily, "must be IPv4 or IPv6") + } + return validatePrefixFilterEntries(prefixFilterList.Entries, prefixFilterList.AddressFamily) } diff --git a/internal/validation/mcr_test.go b/internal/validation/mcr_test.go index 43d86b71..373e1e12 100644 --- a/internal/validation/mcr_test.go +++ b/internal/validation/mcr_test.go @@ -8,6 +8,15 @@ import ( "github.com/stretchr/testify/assert" ) +func TestValidatePrefixFilterEntriesRejectsUnknownAddressFamily(t *testing.T) { + entries := []*megaport.MCRPrefixListEntry{ + {Action: "permit", Prefix: "10.0.0.0/8"}, + } + err := validatePrefixFilterEntries(entries, "IPv5") + assert.IsType(t, &ValidationError{}, err) + assert.Equal(t, "Invalid address family: IPv5 - must be IPv4 or IPv6", err.Error()) +} + func TestValidateIPSecTunnelCount(t *testing.T) { tests := []struct { name string @@ -216,6 +225,21 @@ func TestValidatePrefixFilterListRequest(t *testing.T) { }, wantErr: false, }, + { + name: "Empty entry prefix", + req: &megaport.CreateMCRPrefixFilterListRequest{ + MCRID: "mcr-uid-123", + PrefixFilterList: megaport.MCRPrefixFilterList{ + Description: "Test filter list", + AddressFamily: "IPv4", + Entries: []*megaport.MCRPrefixListEntry{ + {Action: "permit", Prefix: ""}, + }, + }, + }, + wantErr: true, + errText: "Invalid entry prefix index 0: - prefix cannot be empty", + }, { name: "Missing description", req: &megaport.CreateMCRPrefixFilterListRequest{ @@ -287,6 +311,125 @@ func TestValidatePrefixFilterListRequest(t *testing.T) { wantErr: true, errText: "Invalid entries: [] - must contain at least one entry", }, + { + name: "Nil entry in entries", + req: &megaport.CreateMCRPrefixFilterListRequest{ + MCRID: "mcr-uid-123", + PrefixFilterList: megaport.MCRPrefixFilterList{ + Description: "Test filter list", + AddressFamily: "IPv4", + Entries: []*megaport.MCRPrefixListEntry{ + {Action: "permit", Prefix: "10.0.0.0/8"}, + nil, + }, + }, + }, + wantErr: true, + errText: "Invalid entry index 1: - entry cannot be nil", + }, + { + name: "Valid GE/LE bounds", + req: &megaport.CreateMCRPrefixFilterListRequest{ + MCRID: "mcr-uid-123", + PrefixFilterList: megaport.MCRPrefixFilterList{ + Description: "Test filter list", + AddressFamily: "IPv4", + Entries: []*megaport.MCRPrefixListEntry{ + {Action: "permit", Prefix: "10.0.0.0/8", Ge: 16, Le: 24}, + }, + }, + }, + wantErr: false, + }, + { + name: "GE above IPv4 maximum", + req: &megaport.CreateMCRPrefixFilterListRequest{ + MCRID: "mcr-uid-123", + PrefixFilterList: megaport.MCRPrefixFilterList{ + Description: "Test filter list", + AddressFamily: "IPv4", + Entries: []*megaport.MCRPrefixListEntry{ + {Action: "permit", Prefix: "10.0.0.0/8", Ge: 33}, + }, + }, + }, + wantErr: true, + errText: "Invalid entry GE index 0: 33 - must be between 0 and 32 for IPv4", + }, + { + name: "Negative GE", + req: &megaport.CreateMCRPrefixFilterListRequest{ + MCRID: "mcr-uid-123", + PrefixFilterList: megaport.MCRPrefixFilterList{ + Description: "Test filter list", + AddressFamily: "IPv4", + Entries: []*megaport.MCRPrefixListEntry{ + {Action: "permit", Prefix: "10.0.0.0/8", Ge: -1}, + }, + }, + }, + wantErr: true, + errText: "Invalid entry GE index 0: -1 - must be between 0 and 32 for IPv4", + }, + { + name: "LE above IPv4 maximum", + req: &megaport.CreateMCRPrefixFilterListRequest{ + MCRID: "mcr-uid-123", + PrefixFilterList: megaport.MCRPrefixFilterList{ + Description: "Test filter list", + AddressFamily: "IPv4", + Entries: []*megaport.MCRPrefixListEntry{ + {Action: "permit", Prefix: "10.0.0.0/8", Le: 33}, + }, + }, + }, + wantErr: true, + errText: "Invalid entry LE index 0: 33 - must be between 0 and 32 for IPv4", + }, + { + name: "GE greater than LE", + req: &megaport.CreateMCRPrefixFilterListRequest{ + MCRID: "mcr-uid-123", + PrefixFilterList: megaport.MCRPrefixFilterList{ + Description: "Test filter list", + AddressFamily: "IPv4", + Entries: []*megaport.MCRPrefixListEntry{ + {Action: "permit", Prefix: "10.0.0.0/8", Ge: 28, Le: 24}, + }, + }, + }, + wantErr: true, + errText: "Invalid entry GE index 0: 28 - must not exceed the LE value", + }, + { + name: "IPv6 GE/LE beyond IPv4 range accepted", + req: &megaport.CreateMCRPrefixFilterListRequest{ + MCRID: "mcr-uid-123", + PrefixFilterList: megaport.MCRPrefixFilterList{ + Description: "Test filter list", + AddressFamily: "IPv6", + Entries: []*megaport.MCRPrefixListEntry{ + {Action: "permit", Prefix: "2001:db8::/32", Ge: 48, Le: 64}, + }, + }, + }, + wantErr: false, + }, + { + name: "IPv6 LE above maximum", + req: &megaport.CreateMCRPrefixFilterListRequest{ + MCRID: "mcr-uid-123", + PrefixFilterList: megaport.MCRPrefixFilterList{ + Description: "Test filter list", + AddressFamily: "IPv6", + Entries: []*megaport.MCRPrefixListEntry{ + {Action: "permit", Prefix: "2001:db8::/32", Le: 129}, + }, + }, + }, + wantErr: true, + errText: "Invalid entry LE index 0: 129 - must be between 0 and 128 for IPv6", + }, } for _, tt := range tests { @@ -335,7 +478,7 @@ func TestValidateUpdatePrefixFilterList(t *testing.T) { name: "Invalid entry action", req: &megaport.MCRPrefixFilterList{ Description: "Updated filter list", - AddressFamily: "IPv6", + AddressFamily: "IPv4", Entries: []*megaport.MCRPrefixListEntry{ {Action: "allow", Prefix: "10.0.0.0/8"}, }, @@ -343,6 +486,77 @@ func TestValidateUpdatePrefixFilterList(t *testing.T) { wantErr: true, errText: "Invalid entry action: allow - must be permit or deny", }, + { + name: "Invalid prefix CIDR", + req: &megaport.MCRPrefixFilterList{ + Description: "Updated filter list", + AddressFamily: "IPv4", + Entries: []*megaport.MCRPrefixListEntry{ + {Action: "permit", Prefix: "not-a-cidr"}, + }, + }, + wantErr: true, + errText: "Invalid entry prefix index 0: not-a-cidr - must be a valid IPv4 CIDR notation", + }, + { + name: "Prefix does not match declared IPv6 address family", + req: &megaport.MCRPrefixFilterList{ + Description: "Updated filter list", + AddressFamily: "IPv6", + Entries: []*megaport.MCRPrefixListEntry{ + {Action: "permit", Prefix: "10.0.0.0/8"}, + }, + }, + wantErr: true, + errText: "Invalid entry prefix index 0: 10.0.0.0/8 - must be a valid IPv6 CIDR notation", + }, + { + name: "Valid IPv6 update with entries", + req: &megaport.MCRPrefixFilterList{ + Description: "Updated filter list", + AddressFamily: "IPv6", + Entries: []*megaport.MCRPrefixListEntry{ + {Action: "permit", Prefix: "2001:db8::/32"}, + }, + }, + wantErr: false, + }, + { + name: "GE greater than LE rejected on update", + req: &megaport.MCRPrefixFilterList{ + Description: "Updated filter list", + AddressFamily: "IPv4", + Entries: []*megaport.MCRPrefixListEntry{ + {Action: "permit", Prefix: "10.0.0.0/8", Ge: 28, Le: 24}, + }, + }, + wantErr: true, + errText: "Invalid entry GE index 0: 28 - must not exceed the LE value", + }, + { + name: "Empty address family with entries rejected", + req: &megaport.MCRPrefixFilterList{ + Description: "Updated filter list", + AddressFamily: "", + Entries: []*megaport.MCRPrefixListEntry{ + {Action: "permit", Prefix: "10.0.0.0/8"}, + }, + }, + wantErr: true, + errText: "Invalid address family: - cannot be empty", + }, + { + name: "Invalid address family with entries rejected", + req: &megaport.MCRPrefixFilterList{ + Description: "Updated filter list", + AddressFamily: "IPv5", + Entries: []*megaport.MCRPrefixListEntry{ + {Action: "permit", Prefix: "10.0.0.0/8"}, + }, + }, + wantErr: true, + errText: "Invalid address family: IPv5 - must be IPv4 or IPv6", + }, } for _, tt := range tests { diff --git a/internal/validation/mve.go b/internal/validation/mve.go index ec78249b..7d5fd022 100644 --- a/internal/validation/mve.go +++ b/internal/validation/mve.go @@ -3,6 +3,7 @@ package validation import ( "fmt" "strings" + "unicode/utf8" megaport "github.com/megaport/megaportgo" ) @@ -89,7 +90,7 @@ func ValidateBuyMVERequest(req *megaport.BuyMVERequest) error { if req.Name == "" { return NewValidationError("MVE name", req.Name, "cannot be empty") } - if len(req.Name) > MaxMVENameLength { + if utf8.RuneCountInString(req.Name) > MaxMVENameLength { return NewValidationError("MVE name", req.Name, fmt.Sprintf("cannot exceed %d characters", MaxMVENameLength)) } if err := ValidateContractTerm(req.Term); err != nil { @@ -120,6 +121,7 @@ func ValidateBuyMVERequest(req *megaport.BuyMVERequest) error { // // Validation checks: // - At least one updateable field must be provided (name, cost center, contract term, or vNICs) +// - If a name is provided, it cannot exceed the maximum length (MaxMVENameLength) // - If contract term is provided, it must be valid (typically 1, 12, 24, 36, 48, or 60 months) // // Returns: @@ -134,6 +136,10 @@ func ValidateUpdateMVERequest(req *megaport.ModifyMVERequest) error { return NewValidationError("update request", req, "at least one field must be provided for update") } + if req.Name != "" && utf8.RuneCountInString(req.Name) > MaxMVENameLength { + return NewValidationError("MVE name", req.Name, fmt.Sprintf("cannot exceed %d characters", MaxMVENameLength)) + } + // If contract term is provided, validate it if req.ContractTermMonths != nil { term := *req.ContractTermMonths @@ -173,7 +179,7 @@ func ValidateMVERequest(name string, term int, locationID int) error { if name == "" { return NewValidationError("MVE name", name, "cannot be empty") } - if len(name) > MaxMVENameLength { + if utf8.RuneCountInString(name) > MaxMVENameLength { return NewValidationError("MVE name", name, fmt.Sprintf("cannot exceed %d characters", MaxMVENameLength)) } if err := ValidateContractTerm(term); err != nil { diff --git a/internal/validation/mve_test.go b/internal/validation/mve_test.go index d83754a8..a2d9011d 100644 --- a/internal/validation/mve_test.go +++ b/internal/validation/mve_test.go @@ -2,6 +2,7 @@ package validation import ( "fmt" + "strings" "testing" megaport "github.com/megaport/megaportgo" @@ -56,6 +57,21 @@ func TestValidateMVERequest(t *testing.T) { wantErr: true, errText: "Invalid MVE name: This name is way too long and should exceed the 64 character limit for MVE product names which will cause validation to fail - cannot exceed 64 characters", }, + { + name: "64 multibyte character name accepted", + productName: strings.Repeat("日", MaxMVENameLength), + term: 12, + locationID: 123, + wantErr: false, + }, + { + name: "65 multibyte character name rejected", + productName: strings.Repeat("日", MaxMVENameLength+1), + term: 12, + locationID: 123, + wantErr: true, + errText: fmt.Sprintf("Invalid MVE name: %s - cannot exceed %d characters", strings.Repeat("日", MaxMVENameLength+1), MaxMVENameLength), + }, } for _, tt := range tests { @@ -357,6 +373,24 @@ func TestValidateBuyMVERequest(t *testing.T) { wantErr: true, errText: "Invalid vendor config: - cannot be nil", }, + { + name: "MVE name too long", + req: &megaport.BuyMVERequest{ + Name: strings.Repeat("A", MaxMVENameLength+1), + Term: 12, + LocationID: 100, + VendorConfig: &megaport.CiscoConfig{ + Vendor: "cisco", + ImageID: 123, + ProductSize: "MEDIUM", + AdminSSHPublicKey: "ssh-rsa AAAA...", + SSHPublicKey: "ssh-rsa AAAA...", + ManageLocally: true, + }, + }, + wantErr: true, + errText: fmt.Sprintf("Invalid MVE name: %s - cannot exceed %d characters", strings.Repeat("A", MaxMVENameLength+1), MaxMVENameLength), + }, } for _, tt := range tests { @@ -433,6 +467,23 @@ func TestValidateUpdateMVERequest(t *testing.T) { wantErr: true, errText: "vnics[1].description", }, + { + name: "Update name at max length accepted", + req: &megaport.ModifyMVERequest{ + MVEID: "mve-uid-123", + Name: strings.Repeat("日", MaxMVENameLength), + }, + wantErr: false, + }, + { + name: "Update name exceeding max length rejected", + req: &megaport.ModifyMVERequest{ + MVEID: "mve-uid-123", + Name: strings.Repeat("日", MaxMVENameLength+1), + }, + wantErr: true, + errText: fmt.Sprintf("cannot exceed %d characters", MaxMVENameLength), + }, } for _, tt := range tests { diff --git a/internal/validation/port.go b/internal/validation/port.go index 9dbc1352..eb8429e7 100644 --- a/internal/validation/port.go +++ b/internal/validation/port.go @@ -3,6 +3,7 @@ package validation import ( "fmt" "slices" + "unicode/utf8" megaport "github.com/megaport/megaportgo" ) @@ -56,7 +57,7 @@ func ValidatePortName(name string) error { } // The spec says names can be up to MaxPortNameLength characters (inclusive) - if len(name) > MaxPortNameLength { + if utf8.RuneCountInString(name) > MaxPortNameLength { return NewValidationError("port name", name, fmt.Sprintf("cannot exceed %d characters", MaxPortNameLength)) } @@ -83,7 +84,7 @@ func ValidatePortRequest(req *megaport.BuyPortRequest) error { if req.Name == "" { return NewValidationError("port name", req.Name, "cannot be empty") } - if len(req.Name) > MaxPortNameLength { + if utf8.RuneCountInString(req.Name) > MaxPortNameLength { return NewValidationError("port name", req.Name, fmt.Sprintf("cannot exceed %d characters", MaxPortNameLength)) } if req.LocationId <= 0 { @@ -119,7 +120,7 @@ func ValidateLAGPortRequest(req *megaport.BuyPortRequest) error { if req.Name == "" { return NewValidationError("port name", req.Name, "cannot be empty") } - if len(req.Name) > MaxPortNameLength { + if utf8.RuneCountInString(req.Name) > MaxPortNameLength { return NewValidationError("port name", req.Name, fmt.Sprintf("cannot exceed %d characters", MaxPortNameLength)) } if req.LocationId <= 0 { diff --git a/internal/validation/port_test.go b/internal/validation/port_test.go index a8a18feb..19dd081e 100644 --- a/internal/validation/port_test.go +++ b/internal/validation/port_test.go @@ -63,6 +63,15 @@ func TestValidatePortRequest(t *testing.T) { wantErr: true, errText: "Invalid location ID: 0 - must be a positive integer", }, + { + name: "Port name too long", + portName: strings.Repeat("A", MaxPortNameLength+1), + term: 12, + portSpeed: 10000, + locationID: 100, + wantErr: true, + errText: fmt.Sprintf("Invalid port name: %s - cannot exceed %d characters", strings.Repeat("A", MaxPortNameLength+1), MaxPortNameLength), + }, } for _, tt := range tests { @@ -196,6 +205,18 @@ func TestValidateLAGPortRequest(t *testing.T) { wantErr: true, errText: fmt.Sprintf("Invalid contract term: 5 - must be one of: %v", ValidContractTerms), }, + { + name: "LAG port name too long", + req: &megaport.BuyPortRequest{ + Name: strings.Repeat("A", MaxPortNameLength+1), + LocationId: 100, + PortSpeed: 10000, + LagCount: 2, + Term: 12, + }, + wantErr: true, + errText: fmt.Sprintf("Invalid port name: %s - cannot exceed %d characters", strings.Repeat("A", MaxPortNameLength+1), MaxPortNameLength), + }, } for _, tt := range tests { @@ -224,6 +245,8 @@ func TestValidatePortName(t *testing.T) { {"Single character (min non-empty)", "A", false}, {"64 character port name", strings.Repeat("A", MaxPortNameLength), false}, {"65 character port name", strings.Repeat("A", MaxPortNameLength+1), true}, + {"64 multibyte character port name", strings.Repeat("日", MaxPortNameLength), false}, + {"65 multibyte character port name", strings.Repeat("日", MaxPortNameLength+1), true}, } for _, tt := range tests { diff --git a/internal/validation/vxc.go b/internal/validation/vxc.go index 0fd59e77..0b85f94f 100644 --- a/internal/validation/vxc.go +++ b/internal/validation/vxc.go @@ -3,6 +3,7 @@ package validation import ( "fmt" "strings" + "unicode/utf8" megaport "github.com/megaport/megaportgo" ) @@ -193,18 +194,23 @@ func ValidateVXCRequest(req *megaport.BuyVXCRequest) error { // - config: The AWS partner configuration to validate // // Validation checks include: +// - Configuration cannot be nil // - Connect type must be provided and be one of the valid types ('AWS', 'AWSHC', 'private', 'public') // - Owner account must be provided (AWS account ID) // - ASN must be provided and within the valid range (1-4294967295) // - If customer IP address is provided, it must be in valid IPv4 CIDR notation // - If Amazon IP address is provided, it must be in valid IPv4 CIDR notation -// - If connection name is provided, it must not exceed 255 characters +// - Connection name is optional (the API defaults it to "MEGAPORT"); when +// provided it must not exceed 255 characters // - For 'AWS' connect type with a specified connection type, it must be 'private' or 'public' // // Returns: // - A ValidationError if any validation check fails // - nil if all validation checks pass func ValidateAWSPartnerConfig(config *megaport.VXCPartnerConfigAWS) error { + if config == nil { + return NewValidationError("AWS partner config", nil, "cannot be nil") + } if config.ConnectType == "" { return NewValidationError("AWS connect type", config.ConnectType, "cannot be empty") } @@ -224,8 +230,8 @@ func ValidateAWSPartnerConfig(config *megaport.VXCPartnerConfigAWS) error { return NewValidationError("AWS owner account", config.OwnerAccount, "cannot be empty") } - if config.ConnectionName != "" && len(config.ConnectionName) > 255 { - return NewValidationError("AWS connection name", config.ConnectionName, "cannot exceed 255 characters") + if config.ConnectionName != "" && utf8.RuneCountInString(config.ConnectionName) > MaxAWSConnectionNameLength { + return NewValidationError("AWS connection name", config.ConnectionName, fmt.Sprintf("cannot exceed %d characters", MaxAWSConnectionNameLength)) } if config.CustomerIPAddress != "" { if err := ValidateCIDR(config.CustomerIPAddress, "AWS customer IP address"); err != nil { @@ -301,12 +307,16 @@ func ValidateAzurePartnerConfig(config *megaport.VXCPartnerConfigAzure) error { // - config: The Google partner configuration to validate // // Validation checks include: +// - Configuration cannot be nil // - Pairing key must be provided (required for Google Cloud connections) // // Returns: // - A ValidationError if any validation check fails // - nil if all validation checks pass func ValidateGooglePartnerConfig(config *megaport.VXCPartnerConfigGoogle) error { + if config == nil { + return NewValidationError("Google partner config", nil, "cannot be nil") + } if config.PairingKey == "" { return NewValidationError("Google pairing key", config.PairingKey, "cannot be empty") } @@ -320,12 +330,16 @@ func ValidateGooglePartnerConfig(config *megaport.VXCPartnerConfigGoogle) error // - config: The Oracle partner configuration to validate // // Validation checks include: +// - Configuration cannot be nil // - Virtual Circuit ID must be provided (required for Oracle Cloud connections) // // Returns: // - A ValidationError if any validation check fails // - nil if all validation checks pass func ValidateOraclePartnerConfig(config *megaport.VXCPartnerConfigOracle) error { + if config == nil { + return NewValidationError("Oracle partner config", nil, "cannot be nil") + } if config.VirtualCircuitId == "" { return NewValidationError("Oracle virtual circuit ID", config.VirtualCircuitId, "cannot be empty") } @@ -339,11 +353,13 @@ func ValidateOraclePartnerConfig(config *megaport.VXCPartnerConfigOracle) error // - config: The IBM partner configuration to validate // // Validation checks include: +// - Configuration cannot be nil // - Account ID must be provided // - Account ID must be exactly 32 characters (IBMAccountIDLength) // - Account ID must contain only hexadecimal characters (0-9, a-f, A-F) -// - If connection name is provided, it must not exceed the maximum length (MaxIBMNameLength) -// - If connection name is provided, it must contain only allowed characters (0-9, a-z, A-Z, /, -, _, ,) +// - Connection name is optional (the API defaults it to "MEGAPORT"); when +// provided it must not exceed the maximum length (MaxIBMNameLength) and +// must contain only allowed characters (0-9, a-z, A-Z, /, -, _, ,) // - If customer IP address is provided, it must be in valid IPv4 CIDR notation // - If provider IP address is provided, it must be in valid IPv4 CIDR notation // @@ -351,6 +367,9 @@ func ValidateOraclePartnerConfig(config *megaport.VXCPartnerConfigOracle) error // - A ValidationError if any validation check fails // - nil if all validation checks pass func ValidateIBMPartnerConfig(config *megaport.VXCPartnerConfigIBM) error { + if config == nil { + return NewValidationError("IBM partner config", nil, "cannot be nil") + } if config.AccountID == "" { return NewValidationError("IBM account ID", config.AccountID, "cannot be empty") } @@ -362,11 +381,13 @@ func ValidateIBMPartnerConfig(config *megaport.VXCPartnerConfigIBM) error { return NewValidationError("IBM account ID", config.AccountID, "must contain only hexadecimal characters (0-9, a-f, A-F)") } } - if config.Name != "" && len(config.Name) > MaxIBMNameLength { - return NewValidationError("IBM connection name", config.Name, fmt.Sprintf("cannot exceed %d characters", MaxIBMNameLength)) - } - if config.Name != "" && !isValidIBMName(config.Name) { - return NewValidationError("IBM connection name", config.Name, "must only contain characters 0-9, a-z, A-Z, /, -, _, or ,") + if config.Name != "" { + if utf8.RuneCountInString(config.Name) > MaxIBMNameLength { + return NewValidationError("IBM connection name", config.Name, fmt.Sprintf("cannot exceed %d characters", MaxIBMNameLength)) + } + if !isValidIBMName(config.Name) { + return NewValidationError("IBM connection name", config.Name, "must only contain characters 0-9, a-z, A-Z, /, -, _, or ,") + } } if config.CustomerIPAddress != "" { if err := ValidateCIDR(config.CustomerIPAddress, "IBM customer IP address"); err != nil { @@ -391,6 +412,20 @@ func isValidIBMName(name string) bool { return true } +// ValidateTransitPartnerConfig validates a Transit partner configuration for a +// VXC connection. Transit carries no partner-specific fields beyond the +// connect type, so this only guards against a nil config or a connect type +// that doesn't match "TRANSIT". +func ValidateTransitPartnerConfig(config *megaport.VXCPartnerConfigTransit) error { + if config == nil { + return NewValidationError("Transit partner config", nil, "cannot be nil") + } + if config.ConnectType != "TRANSIT" { + return NewValidationError("Transit connect type", config.ConnectType, "must be 'TRANSIT'") + } + return nil +} + // ValidateVrouterPartnerConfig validates a vRouter partner configuration: it // requires at least one interface and validates each interface's VLAN, IP // addresses, NAT IPs, routes, BFD, BGP connections, interface type, and IPsec @@ -473,6 +508,7 @@ func ValidateVrouterPartnerConfig(config *megaport.VXCOrderVrouterPartnerConfig) // - ValidateOraclePartnerConfig // - ValidateIBMPartnerConfig // - ValidateVrouterPartnerConfig +// - ValidateTransitPartnerConfig // - Configuration type must be one of the supported types // // Returns: @@ -490,6 +526,8 @@ func ValidateVXCPartnerConfig(config megaport.VXCPartnerConfiguration) error { return ValidateOraclePartnerConfig(v) case *megaport.VXCPartnerConfigIBM: return ValidateIBMPartnerConfig(v) + case *megaport.VXCPartnerConfigTransit: + return ValidateTransitPartnerConfig(v) case *megaport.VXCOrderVrouterPartnerConfig: return ValidateVrouterPartnerConfig(v) default: diff --git a/internal/validation/vxc_test.go b/internal/validation/vxc_test.go index d3220fd6..79110b68 100644 --- a/internal/validation/vxc_test.go +++ b/internal/validation/vxc_test.go @@ -3,6 +3,7 @@ package validation import ( "fmt" "math" + "strings" "testing" megaport "github.com/megaport/megaportgo" @@ -663,9 +664,10 @@ func TestValidateVXCRequest(t *testing.T) { PortUID: "a-end-uid", BEndConfiguration: megaport.VXCOrderEndpointConfiguration{ PartnerConfig: &megaport.VXCPartnerConfigAWS{ - ConnectType: "AWS", - OwnerAccount: "12345", - ASN: 65000, + ConnectType: "AWS", + OwnerAccount: "12345", + ASN: 65000, + ConnectionName: "MyAWSConnection", }, }, }, @@ -841,6 +843,7 @@ func TestValidateAWSPartnerConfig(t *testing.T) { ownerAccount: "123456789012", asn: 65000, customerIPAddress: "invalid-ip", + awsName: "MyAWSConnection", wantErr: true, errText: "Invalid AWS customer IP address: invalid-ip - must be a valid IPv4 CIDR notation", // Updated error message }, @@ -850,6 +853,7 @@ func TestValidateAWSPartnerConfig(t *testing.T) { ownerAccount: "123456789012", asn: 65000, amazonIPAddress: "192.168.1.2/33", // Invalid mask + awsName: "MyAWSConnection", wantErr: true, errText: "Invalid AWS Amazon IP address: 192.168.1.2/33 - must be a valid IPv4 CIDR notation", // Updated error message }, @@ -862,15 +866,41 @@ func TestValidateAWSPartnerConfig(t *testing.T) { wantErr: true, errText: "Invalid AWS connection name: ", // Error message includes the long name, truncated here }, + { + name: "AWS name at max length with multibyte characters accepted", + connectType: "AWS", + ownerAccount: "123456789012", + asn: 65000, + awsName: strings.Repeat("日", MaxAWSConnectionNameLength), + wantErr: false, + }, + { + name: "AWS name over max length with multibyte characters rejected", + connectType: "AWS", + ownerAccount: "123456789012", + asn: 65000, + awsName: strings.Repeat("日", MaxAWSConnectionNameLength+1), + wantErr: true, + errText: "Invalid AWS connection name: ", + }, { name: "Invalid AWS type for AWS connect type", connectType: "AWS", ownerAccount: "123456789012", asn: 65000, awsType: "invalid", + awsName: "MyAWSConnection", wantErr: true, errText: "Invalid AWS type: invalid - must be 'private' or 'public' for AWS connect type", }, + { + name: "Empty connection name is valid (API defaults to MEGAPORT)", + connectType: "AWS", + ownerAccount: "123456789012", + asn: 65000, + awsName: "", + wantErr: false, + }, } for _, tt := range tests { @@ -899,6 +929,12 @@ func TestValidateAWSPartnerConfig(t *testing.T) { } } +func TestValidateAWSPartnerConfig_NilConfig(t *testing.T) { + err := ValidateAWSPartnerConfig(nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Invalid AWS partner config: - cannot be nil") +} + // TestValidateAWSPartnerConfig_HighASN mirrors TestValidateBGPConnectionConfig_HighASN: // it confirms the AWS validator agrees with ValidateASN at the 32-bit max boundary. func TestValidateAWSPartnerConfig_HighASN(t *testing.T) { @@ -971,6 +1007,12 @@ func TestValidateGooglePartnerConfig(t *testing.T) { } } +func TestValidateGooglePartnerConfig_NilConfig(t *testing.T) { + err := ValidateGooglePartnerConfig(nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Invalid Google partner config: - cannot be nil") +} + func TestValidateOraclePartnerConfig(t *testing.T) { tests := []struct { name string @@ -1000,6 +1042,12 @@ func TestValidateOraclePartnerConfig(t *testing.T) { } } +func TestValidateOraclePartnerConfig_NilConfig(t *testing.T) { + err := ValidateOraclePartnerConfig(nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Invalid Oracle partner config: - cannot be nil") +} + func TestValidateIBMPartnerConfig(t *testing.T) { validAccountID := "abcdef0123456789abcdef0123456789" // 32 hex chars tests := []struct { @@ -1062,6 +1110,7 @@ func TestValidateIBMPartnerConfig(t *testing.T) { { name: "Invalid customer IP", accountID: validAccountID, + ibmName: "MyIBMConnection", customerIPAddress: "invalid-ip", wantErr: true, errText: "Invalid IBM customer IP address: invalid-ip - must be a valid IPv4 CIDR notation", @@ -1069,10 +1118,17 @@ func TestValidateIBMPartnerConfig(t *testing.T) { { name: "Invalid provider IP", accountID: validAccountID, + ibmName: "MyIBMConnection", providerIPAddress: "10.1.1.2/33", // Invalid mask wantErr: true, errText: "Invalid IBM provider IP address: 10.1.1.2/33 - must be a valid IPv4 CIDR notation", }, + { + name: "Empty connection name is valid (API defaults to MEGAPORT)", + accountID: validAccountID, + ibmName: "", + wantErr: false, + }, } for _, tt := range tests { @@ -1097,6 +1153,12 @@ func TestValidateIBMPartnerConfig(t *testing.T) { } } +func TestValidateIBMPartnerConfig_NilConfig(t *testing.T) { + err := ValidateIBMPartnerConfig(nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Invalid IBM partner config: - cannot be nil") +} + func TestValidateVXCPartnerConfig(t *testing.T) { tests := []struct { name string @@ -1107,9 +1169,10 @@ func TestValidateVXCPartnerConfig(t *testing.T) { { name: "Valid AWS partner config", config: &megaport.VXCPartnerConfigAWS{ // Use struct pointer - ConnectType: "AWS", - OwnerAccount: "123456789012", - ASN: 65000, + ConnectType: "AWS", + OwnerAccount: "123456789012", + ASN: 65000, + ConnectionName: "MyAWSConnection", }, wantErr: false, }, @@ -1142,6 +1205,7 @@ func TestValidateVXCPartnerConfig(t *testing.T) { config: &megaport.VXCPartnerConfigIBM{ // Use struct pointer ConnectType: "IBM", // Assuming ConnectType is needed AccountID: "abcdef0123456789abcdef0123456789", + Name: "MyIBMConnection", }, wantErr: false, }, @@ -1159,6 +1223,13 @@ func TestValidateVXCPartnerConfig(t *testing.T) { }, wantErr: false, }, + { + name: "Valid Transit partner config", + config: &megaport.VXCPartnerConfigTransit{ + ConnectType: "TRANSIT", + }, + wantErr: false, + }, { name: "Missing partner type (nil config)", // Test case for nil config config: nil, @@ -1198,6 +1269,14 @@ func TestValidateVXCPartnerConfig(t *testing.T) { wantErr: true, errText: "Invalid Azure service key: - cannot be empty", }, + { + name: "Invalid Transit config details", + config: &megaport.VXCPartnerConfigTransit{ + ConnectType: "", // Invalid connect type + }, + wantErr: true, + errText: "Invalid Transit connect type: - must be 'TRANSIT'", + }, { name: "Invalid vRouter config details", config: &megaport.VXCOrderVrouterPartnerConfig{ @@ -1227,6 +1306,59 @@ func TestValidateVXCPartnerConfig(t *testing.T) { } } +func TestValidateTransitPartnerConfig(t *testing.T) { + tests := []struct { + name string + config *megaport.VXCPartnerConfigTransit + wantErr bool + errText string + }{ + { + name: "Valid Transit config", + config: &megaport.VXCPartnerConfigTransit{ + ConnectType: "TRANSIT", + }, + wantErr: false, + }, + { + name: "Nil config", + config: nil, + wantErr: true, + errText: "Invalid Transit partner config: - cannot be nil", + }, + { + name: "Empty connect type", + config: &megaport.VXCPartnerConfigTransit{ + ConnectType: "", + }, + wantErr: true, + errText: "Invalid Transit connect type: - must be 'TRANSIT'", + }, + { + name: "Wrong connect type", + config: &megaport.VXCPartnerConfigTransit{ + ConnectType: "AWS", + }, + wantErr: true, + errText: "Invalid Transit connect type: AWS - must be 'TRANSIT'", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateTransitPartnerConfig(tt.config) + if (err != nil) != tt.wantErr { + t.Errorf("ValidateTransitPartnerConfig() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err != nil && tt.wantErr { + assert.IsType(t, &ValidationError{}, err, "Expected ValidationError type") + assert.Equal(t, tt.errText, err.Error(), "Error message mismatch") + } + }) + } +} + func TestIsValidIBMName(t *testing.T) { tests := []struct { name string