Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion ci/ci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,12 @@ These tests validate that:
package citest

import (
"fmt"
"os"
"slices"
"strings"
"testing"
"time"

"github.com/apache/cloudstack-go/v2/cloudstack"
)
Expand All @@ -61,6 +63,7 @@ type testResources struct {
serviceOfferingID1 string
serviceOfferingID2 string
networkID string
networkName string
templateID string
networkOfferingID string
}
Expand Down Expand Up @@ -90,6 +93,55 @@ func TestCloudstackAPI(t *testing.T) {
// Set up test environment
setupTestEnvironment(t, client, resources)

t.Run("NetworkNameLookups", func(t *testing.T) {
for _, tc := range []struct {
name string
wantID string
}{
{name: resources.networkName, wantID: resources.networkID},
{name: "missing-" + resources.networkID},
} {
t.Run(tc.name, func(t *testing.T) {
wantCount := 0
if tc.wantID != "" {
wantCount = 1
}
id, count, err := client.Network.GetNetworkID(tc.name)
t.Logf("GetNetworkID(%q): id=%q count=%d error=%v", tc.name, id, count, err)
if id != tc.wantID || count != wantCount || (err != nil) != (tc.wantID == "") {
t.Errorf("wanted network ID %q and count %d", tc.wantID, wantCount)
}

network, count, err := client.Network.GetNetworkByName(tc.name)
id = ""
if network != nil {
id = network.Id
}
t.Logf("GetNetworkByName(%q): id=%q count=%d error=%v", tc.name, id, count, err)
if id != tc.wantID || count != wantCount || (err != nil) != (tc.wantID == "") {
t.Errorf("wanted network ID %q and count %d", tc.wantID, wantCount)
}
})
}
t.Run("PartialName", func(t *testing.T) {
name := strings.TrimPrefix(resources.networkName, "ci-")
id, count, err := client.Network.GetNetworkID(name)
t.Logf("GetNetworkID(%q): id=%q count=%d error=%v", name, id, count, err)
if id != "" || err == nil || count < 0 || count > 1 {
t.Error("partial name must not resolve to a network")
}
network, count, err := client.Network.GetNetworkByName(name)
id = ""
if network != nil {
id = network.Id
}
t.Logf("GetNetworkByName(%q): id=%q count=%d error=%v", name, id, count, err)
if network != nil || err == nil || count < 0 || count > 1 {
t.Error("partial name must not resolve to a network")
}
})
})

// Run the actual tests
t.Run("BasicTypes", func(t *testing.T) {
testBasicTypes(t, client, resources)
Expand All @@ -114,7 +166,8 @@ func setupTestEnvironment(t *testing.T, client *cloudstack.CloudStackClient, res
resources.networkOfferingID = getTestNetworkOffering(t, client)

// Create test network
resources.networkID = createTestNetwork(t, client, "ci-test-network", resources.networkOfferingID, resources.zoneID)
resources.networkName = fmt.Sprintf("ci-test-network-%d", time.Now().UnixNano())
resources.networkID = createTestNetwork(t, client, resources.networkName, resources.networkOfferingID, resources.zoneID)

// Get template ID
resources.templateID = getTestTemplate(t, client, resources.zoneID)
Expand Down
3 changes: 2 additions & 1 deletion cloudstack/NetworkService.go
Original file line number Diff line number Diff line change
Expand Up @@ -4855,6 +4855,7 @@ func (s *NetworkService) GetNetworkID(name string, opts ...OptionFunc) (string,
p.p = make(map[string]interface{})

p.p["name"] = name
p.p["keyword"] = name

for _, fn := range append(s.cs.options, opts...) {
if err := fn(s.cs, p); err != nil {
Expand All @@ -4871,7 +4872,7 @@ func (s *NetworkService) GetNetworkID(name string, opts ...OptionFunc) (string,
return "", l.Count, fmt.Errorf("No match found for %s: %+v", name, l)
}

if l.Count == 1 {
if l.Count == 1 && l.Networks[0].Name == name {
return l.Networks[0].Id, l.Count, nil
}

Expand Down
10 changes: 9 additions & 1 deletion generate/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -1666,6 +1666,10 @@ func (s *service) generateHelperFuncs(a *API) {
pn(" p.p = make(map[string]interface{})")
pn("")
pn(" p.p[\"%s\"] = %s", v, v)
// CloudStack < 4.22 ignores listNetworks' name filter: https://github.com/apache/cloudstack-go/issues/140.
if a.Name == "listNetworks" && v == "name" {
pn(" p.p[\"keyword\"] = name")
Comment thread
Copilot marked this conversation as resolved.
}
for _, ap := range a.Params {
if ap.Required || isRequiredParam(a, ap) {
pn(" p.p[\"%s\"] = %s", ap.Name, s.parseParamName(ap.Name))
Expand Down Expand Up @@ -1699,7 +1703,11 @@ func (s *service) generateHelperFuncs(a *API) {
pn(" return \"\", l.Count, fmt.Errorf(\"No match found for %%s: %%+v\", %s, l)", v)
pn(" }")
pn("")
pn(" if l.Count == 1 {")
if a.Name == "listNetworks" {
pn(" if l.Count == 1 && l.%s[0].Name == %s {", ln, v)
} else {
pn(" if l.Count == 1 {")
}
pn(" return l.%s[0].Id, l.Count, nil", ln)
pn(" }")
pn("")
Expand Down
121 changes: 121 additions & 0 deletions test/GetNetworkByNameRegression_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//
// 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 test

import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/apache/cloudstack-go/v2/cloudstack"
)

func TestNetworkLookupCompatibility(t *testing.T) {
const networkID = "11111111-2222-4333-8444-555555555555"
const zoneID = "22222222-3333-4444-8555-666666666666"
existing := &cloudstack.Network{Id: networkID, Name: "existing"}
multiple := []*cloudstack.Network{
{Id: "33333333-4444-4555-8666-777777777777", Name: "existing-extra"},
{Id: "44444444-5555-4666-8777-888888888888", Name: "other"},
existing,
}
cases := []struct {
name string
lookup string
networks []*cloudstack.Network
wantID string
wantCount int
}{
{"single match", "existing", []*cloudstack.Network{existing}, networkID, 1},
{"single partial match", "exist", []*cloudstack.Network{existing}, "", 1},
{"missing with one network", "missing", []*cloudstack.Network{existing}, "", 0},
{"exact match among partial matches", "existing", multiple, networkID, 2},
{"missing with multiple networks", "missing", multiple, "", 0},
}

for _, supportsName := range []bool{false, true} {
version := "legacy"
if supportsName {
version = "current"
}
t.Run(version, func(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
wantCount := tc.wantCount
if supportsName && wantCount > 1 {
wantCount = 1
}
if supportsName && tc.wantID == "" {
wantCount = 0
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
if q.Get("command") != "listNetworks" || q.Get("zoneid") != zoneID {
t.Errorf("unexpected request parameters: %v", q)
http.Error(w, "unexpected request parameters", http.StatusBadRequest)
return
}
var matches []*cloudstack.Network
for _, network := range tc.networks {
if id := q.Get("id"); id != "" && network.Id != id {
continue
}
if keyword := q.Get("keyword"); !strings.Contains(network.Name, keyword) {
continue
}
if name := q.Get("name"); supportsName && name != "" && network.Name != name {
continue
}
matches = append(matches, network)
}
w.Header().Set("Content-Type", "application/json")
response := map[string]*cloudstack.ListNetworksResponse{
"listnetworksresponse": {Count: len(matches), Networks: matches},
}
if err := json.NewEncoder(w).Encode(response); err != nil {
t.Errorf("encode response: %v", err)
}
}))
defer server.Close()
client := cloudstack.NewClient(server.URL, "APIKEY", "SECRETKEY", true)

id, count, err := client.Network.GetNetworkID(tc.lookup, cloudstack.WithZone(zoneID))
if id != tc.wantID || count != wantCount || (err != nil) != (tc.wantID == "") {
t.Errorf("GetNetworkID(%q) = %q, %d, %v; want %q, %d",
tc.lookup, id, count, err, tc.wantID, wantCount)
}

network, count, err := client.Network.GetNetworkByName(tc.lookup, cloudstack.WithZone(zoneID))
if tc.wantID == "" {
if network != nil || count != wantCount || err == nil {
t.Errorf("GetNetworkByName(%q) = %+v, %d, %v; want no match",
tc.lookup, network, count, err)
}
} else if err != nil || network == nil || network.Id != tc.wantID || count != 1 {
t.Errorf("GetNetworkByName(%q) = %+v, %d, %v; want network %s",
tc.lookup, network, count, err, tc.wantID)
}
})
}
})
}
}