diff --git a/cloudstack/GetRawValueGuard_test.go b/cloudstack/GetRawValueGuard_test.go new file mode 100644 index 0000000..8fa4bae --- /dev/null +++ b/cloudstack/GetRawValueGuard_test.go @@ -0,0 +1,36 @@ +// +// 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 "testing" + +// getRawValue must not panic when a count-wrapped response carries an empty +// data array ({"count":0,"":[]}); it should return a descriptive error. +func TestGetRawValueEmptyArrayReturnsErrorNotPanic(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Fatalf("getRawValue panicked on a count-wrapped empty array: %v", r) + } + }() + _, err := getRawValue([]byte(`{"count":0,"entity":[]}`)) + if err == nil { + t.Fatal("expected an error for a count-wrapped empty array, got nil") + } +} diff --git a/cloudstack/HypervisorService.go b/cloudstack/HypervisorService.go index 605070e..688b203 100644 --- a/cloudstack/HypervisorService.go +++ b/cloudstack/HypervisorService.go @@ -229,7 +229,7 @@ func (s *HypervisorService) ListHypervisorCapabilities(p *ListHypervisorCapabili type ListHypervisorCapabilitiesResponse struct { Count int `json:"count"` - HypervisorCapabilities []*HypervisorCapability `json:"hypervisorcapability"` + HypervisorCapabilities []*HypervisorCapability `json:"hypervisorCapabilities"` } type HypervisorCapability struct { diff --git a/cloudstack/LoadBalancerService.go b/cloudstack/LoadBalancerService.go index c66a8c0..e525e53 100644 --- a/cloudstack/LoadBalancerService.go +++ b/cloudstack/LoadBalancerService.go @@ -3539,7 +3539,7 @@ func (s *LoadBalancerService) ListLBHealthCheckPolicies(p *ListLBHealthCheckPoli type ListLBHealthCheckPoliciesResponse struct { Count int `json:"count"` - LBHealthCheckPolicies []*LBHealthCheckPolicy `json:"lbhealthcheckpolicy"` + LBHealthCheckPolicies []*LBHealthCheckPolicy `json:"healthcheckpolicies"` } type LBHealthCheckPolicy struct { @@ -3782,7 +3782,7 @@ func (s *LoadBalancerService) ListLBStickinessPolicies(p *ListLBStickinessPolici type ListLBStickinessPoliciesResponse struct { Count int `json:"count"` - LBStickinessPolicies []*LBStickinessPolicy `json:"lbstickinesspolicy"` + LBStickinessPolicies []*LBStickinessPolicy `json:"stickinesspolicies"` } type LBStickinessPolicy struct { diff --git a/cloudstack/NetworkService.go b/cloudstack/NetworkService.go index 96f1a2e..cccb28f 100644 --- a/cloudstack/NetworkService.go +++ b/cloudstack/NetworkService.go @@ -8475,7 +8475,7 @@ func (s *NetworkService) ListGuestNetworkIpv6Prefixes(p *ListGuestNetworkIpv6Pre type ListGuestNetworkIpv6PrefixesResponse struct { Count int `json:"count"` - GuestNetworkIpv6Prefixes []*GuestNetworkIpv6Prefixe `json:"guestnetworkipv6prefixe"` + GuestNetworkIpv6Prefixes []*GuestNetworkIpv6Prefixe `json:"guestnetworkipv6prefix"` } type GuestNetworkIpv6Prefixe struct { diff --git a/cloudstack/cloudstack.go b/cloudstack/cloudstack.go index e16a934..240174e 100644 --- a/cloudstack/cloudstack.go +++ b/cloudstack/cloudstack.go @@ -653,6 +653,9 @@ func getRawValue(b json.RawMessage) (json.RawMessage, error) { if err := json.Unmarshal(v, &resp); err != nil { return nil, err } + if len(resp) == 0 { + return nil, fmt.Errorf("Unable to extract raw value: empty array for key %q in:\n\n%s\n\n", k, string(b)) + } return resp[0], nil } } diff --git a/generate/generate.go b/generate/generate.go index 448b614..9f3dd64 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -2096,6 +2096,18 @@ func (s *service) generateResponseType(a *API) { case "quotaSummary": pn(" Count int `json:\"count\"`") pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "summary") + case "listHypervisorCapabilities": + pn(" Count int `json:\"count\"`") + pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "hypervisorCapabilities") + case "listGuestNetworkIpv6Prefixes": + pn(" Count int `json:\"count\"`") + pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "guestnetworkipv6prefix") + case "listLBHealthCheckPolicies": + pn(" Count int `json:\"count\"`") + pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "healthcheckpolicies") + case "listLBStickinessPolicies": + pn(" Count int `json:\"count\"`") + pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), "stickinesspolicies") default: pn(" Count int `json:\"count\"`") pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln), strings.ToLower(parseSingular(ln))) diff --git a/test/ListResponseJSONTagsRegression_test.go b/test/ListResponseJSONTagsRegression_test.go new file mode 100644 index 0000000..1552328 --- /dev/null +++ b/test/ListResponseJSONTagsRegression_test.go @@ -0,0 +1,73 @@ +// +// 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" + "testing" + + "github.com/apache/cloudstack-go/v2/cloudstack" +) + +// Regression test for list-response JSON-tag mismatches: the response struct +// tag must match the object key CloudStack actually returns, otherwise Count +// parses but the slice stays nil (silent data loss). Keys below are the +// authoritative server object names (setObjectName in the CloudStack source). +func TestListResponseJSONTagsPopulateSlices(t *testing.T) { + t.Run("HypervisorCapabilities", func(t *testing.T) { + var r cloudstack.ListHypervisorCapabilitiesResponse + if err := json.Unmarshal([]byte(`{"count":1,"hypervisorCapabilities":[{"id":"h1"}]}`), &r); err != nil { + t.Fatal(err) + } + if len(r.HypervisorCapabilities) != 1 { + t.Fatalf("expected 1 item under key 'hypervisorCapabilities', got %d (nil slice = wrong json tag)", len(r.HypervisorCapabilities)) + } + }) + + t.Run("GuestNetworkIpv6Prefixes", func(t *testing.T) { + var r cloudstack.ListGuestNetworkIpv6PrefixesResponse + if err := json.Unmarshal([]byte(`{"count":1,"guestnetworkipv6prefix":[{"id":"p1"}]}`), &r); err != nil { + t.Fatal(err) + } + if len(r.GuestNetworkIpv6Prefixes) != 1 { + t.Fatalf("expected 1 item under key 'guestnetworkipv6prefix', got %d", len(r.GuestNetworkIpv6Prefixes)) + } + }) + + t.Run("LBHealthCheckPolicies", func(t *testing.T) { + var r cloudstack.ListLBHealthCheckPoliciesResponse + if err := json.Unmarshal([]byte(`{"count":1,"healthcheckpolicies":[{"lbruleid":"r1"}]}`), &r); err != nil { + t.Fatal(err) + } + if len(r.LBHealthCheckPolicies) != 1 { + t.Fatalf("expected 1 item under key 'healthcheckpolicies', got %d", len(r.LBHealthCheckPolicies)) + } + }) + + t.Run("LBStickinessPolicies", func(t *testing.T) { + var r cloudstack.ListLBStickinessPoliciesResponse + if err := json.Unmarshal([]byte(`{"count":1,"stickinesspolicies":[{"lbruleid":"r1"}]}`), &r); err != nil { + t.Fatal(err) + } + if len(r.LBStickinessPolicies) != 1 { + t.Fatalf("expected 1 item under key 'stickinesspolicies', got %d", len(r.LBStickinessPolicies)) + } + }) +}