Skip to content

Commit 6cacc64

Browse files
committed
test(e2e): add unit tests for VM extension version parsing and comparison
Extract GetLatestVMExtensionImageVersion logic into a testable unexported function accepting a vmExtensionImageVersionLister interface. Add 26 table-driven tests covering parseVersion, vmExtensionVersion.cmp, and getLatestVMExtensionImageVersion including edge cases for nil names, malformed versions, API errors, and empty responses. Signed-off-by: Suraj Deshmukh <suraj.deshmukh@microsoft.com>
1 parent 7400daf commit 6cacc64

File tree

2 files changed

+317
-1
lines changed

2 files changed

+317
-1
lines changed

e2e/config/azure.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -746,11 +746,23 @@ func (a *AzureClient) DeleteSnapshot(ctx context.Context, resourceGroupName, sna
746746
return nil
747747
}
748748

749+
// vmExtensionImageVersionLister abstracts the ListVersions method of the VM extension images client for testability.
750+
type vmExtensionImageVersionLister interface {
751+
ListVersions(ctx context.Context, location string, publisherName string, typeParam string,
752+
options *armcompute.VirtualMachineExtensionImagesClientListVersionsOptions,
753+
) (armcompute.VirtualMachineExtensionImagesClientListVersionsResponse, error)
754+
}
755+
749756
// GetLatestVMExtensionImageVersion lists VM extension images for a given extension name and returns the latest version.
750757
// This is equivalent to: az vm extension image list -n Compute.AKS.Linux.AKSNode --latest
751758
func (a *AzureClient) GetLatestVMExtensionImageVersion(ctx context.Context, location, extType, extPublisher string) (string, error) {
759+
return getLatestVMExtensionImageVersion(ctx, a.VMExtensionImages, location, extType, extPublisher)
760+
}
761+
762+
// getLatestVMExtensionImageVersion lists VM extension images using the provided lister and returns the latest version.
763+
func getLatestVMExtensionImageVersion(ctx context.Context, lister vmExtensionImageVersionLister, location, extType, extPublisher string) (string, error) {
752764
// List extension versions
753-
resp, err := a.VMExtensionImages.ListVersions(ctx, location, extPublisher, extType, &armcompute.VirtualMachineExtensionImagesClientListVersionsOptions{})
765+
resp, err := lister.ListVersions(ctx, location, extPublisher, extType, &armcompute.VirtualMachineExtensionImagesClientListVersionsOptions{})
754766
if err != nil {
755767
return "", fmt.Errorf("listing extension versions: %w", err)
756768
}

e2e/config/azure_vmext_test.go

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
package config
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
"testing"
8+
9+
"github.com/Azure/agentbaker/e2e/toolkit"
10+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
11+
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7"
12+
)
13+
14+
// mockVMExtensionImageVersionLister implements vmExtensionImageVersionLister for testing.
15+
type mockVMExtensionImageVersionLister struct {
16+
resp armcompute.VirtualMachineExtensionImagesClientListVersionsResponse
17+
err error
18+
}
19+
20+
func (m *mockVMExtensionImageVersionLister) ListVersions(
21+
ctx context.Context,
22+
location string,
23+
publisherName string,
24+
typeParam string,
25+
options *armcompute.VirtualMachineExtensionImagesClientListVersionsOptions,
26+
) (armcompute.VirtualMachineExtensionImagesClientListVersionsResponse, error) {
27+
return m.resp, m.err
28+
}
29+
30+
// makeVersionResponse builds a ListVersionsResponse from a list of version name pointers.
31+
// Pass nil to represent an image with a nil Name field.
32+
func makeVersionResponse(versions ...*string) armcompute.VirtualMachineExtensionImagesClientListVersionsResponse {
33+
images := make([]*armcompute.VirtualMachineExtensionImage, len(versions))
34+
for i, v := range versions {
35+
images[i] = &armcompute.VirtualMachineExtensionImage{Name: v}
36+
}
37+
return armcompute.VirtualMachineExtensionImagesClientListVersionsResponse{
38+
VirtualMachineExtensionImageArray: images,
39+
}
40+
}
41+
42+
func Test_parseVersion(t *testing.T) {
43+
tests := []struct {
44+
name string
45+
inputName *string
46+
expectedMajor int
47+
expectedMinor int
48+
expectedPatch int
49+
}{
50+
{
51+
name: "three-part version",
52+
inputName: to.Ptr("1.0.1"),
53+
expectedMajor: 1,
54+
expectedMinor: 0,
55+
expectedPatch: 1,
56+
},
57+
{
58+
name: "two-part version",
59+
inputName: to.Ptr("1.151"),
60+
expectedMajor: 1,
61+
expectedMinor: 151,
62+
expectedPatch: 0,
63+
},
64+
{
65+
name: "single-part version",
66+
inputName: to.Ptr("5"),
67+
expectedMajor: 5,
68+
expectedMinor: 0,
69+
expectedPatch: 0,
70+
},
71+
{
72+
name: "nil name",
73+
inputName: nil,
74+
expectedMajor: 0,
75+
expectedMinor: 0,
76+
expectedPatch: 0,
77+
},
78+
{
79+
name: "non-numeric parts",
80+
inputName: to.Ptr("abc.def.ghi"),
81+
expectedMajor: 0,
82+
expectedMinor: 0,
83+
expectedPatch: 0,
84+
},
85+
{
86+
name: "partially numeric",
87+
inputName: to.Ptr("2.abc.3"),
88+
expectedMajor: 2,
89+
expectedMinor: 0,
90+
expectedPatch: 3,
91+
},
92+
{
93+
name: "empty string",
94+
inputName: to.Ptr(""),
95+
expectedMajor: 0,
96+
expectedMinor: 0,
97+
expectedPatch: 0,
98+
},
99+
{
100+
name: "extra parts ignored",
101+
inputName: to.Ptr("1.2.3.4"),
102+
expectedMajor: 1,
103+
expectedMinor: 2,
104+
expectedPatch: 3,
105+
},
106+
{
107+
name: "large numbers",
108+
inputName: to.Ptr("100.200.300"),
109+
expectedMajor: 100,
110+
expectedMinor: 200,
111+
expectedPatch: 300,
112+
},
113+
{
114+
name: "leading zeros",
115+
inputName: to.Ptr("01.02.03"),
116+
expectedMajor: 1,
117+
expectedMinor: 2,
118+
expectedPatch: 3,
119+
},
120+
}
121+
122+
for _, tt := range tests {
123+
t.Run(tt.name, func(t *testing.T) {
124+
ctx := toolkit.ContextWithT(context.Background(), t)
125+
img := &armcompute.VirtualMachineExtensionImage{Name: tt.inputName}
126+
result := parseVersion(ctx, img)
127+
128+
if result.major != tt.expectedMajor {
129+
t.Errorf("major: got %d, want %d", result.major, tt.expectedMajor)
130+
}
131+
if result.minor != tt.expectedMinor {
132+
t.Errorf("minor: got %d, want %d", result.minor, tt.expectedMinor)
133+
}
134+
if result.patch != tt.expectedPatch {
135+
t.Errorf("patch: got %d, want %d", result.patch, tt.expectedPatch)
136+
}
137+
if result.original != img {
138+
t.Errorf("original: got %p, want %p", result.original, img)
139+
}
140+
})
141+
}
142+
}
143+
144+
func Test_vmExtensionVersion_cmp(t *testing.T) {
145+
tests := []struct {
146+
name string
147+
a vmExtensionVersion
148+
b vmExtensionVersion
149+
expected int
150+
}{
151+
{
152+
name: "equal",
153+
a: vmExtensionVersion{major: 1, minor: 2, patch: 3},
154+
b: vmExtensionVersion{major: 1, minor: 2, patch: 3},
155+
expected: 0,
156+
},
157+
{
158+
name: "a higher major",
159+
a: vmExtensionVersion{major: 2, minor: 0, patch: 0},
160+
b: vmExtensionVersion{major: 1, minor: 9, patch: 9},
161+
expected: 1,
162+
},
163+
{
164+
name: "a lower major",
165+
a: vmExtensionVersion{major: 1, minor: 9, patch: 9},
166+
b: vmExtensionVersion{major: 2, minor: 0, patch: 0},
167+
expected: -1,
168+
},
169+
{
170+
name: "same major, a higher minor",
171+
a: vmExtensionVersion{major: 1, minor: 5, patch: 0},
172+
b: vmExtensionVersion{major: 1, minor: 3, patch: 9},
173+
expected: 1,
174+
},
175+
{
176+
name: "same major, a lower minor",
177+
a: vmExtensionVersion{major: 1, minor: 3, patch: 9},
178+
b: vmExtensionVersion{major: 1, minor: 5, patch: 0},
179+
expected: -1,
180+
},
181+
{
182+
name: "same major+minor, a higher patch",
183+
a: vmExtensionVersion{major: 1, minor: 2, patch: 5},
184+
b: vmExtensionVersion{major: 1, minor: 2, patch: 3},
185+
expected: 1,
186+
},
187+
{
188+
name: "same major+minor, a lower patch",
189+
a: vmExtensionVersion{major: 1, minor: 2, patch: 3},
190+
b: vmExtensionVersion{major: 1, minor: 2, patch: 5},
191+
expected: -1,
192+
},
193+
{
194+
name: "both zero",
195+
a: vmExtensionVersion{major: 0, minor: 0, patch: 0},
196+
b: vmExtensionVersion{major: 0, minor: 0, patch: 0},
197+
expected: 0,
198+
},
199+
{
200+
name: "zero vs non-zero",
201+
a: vmExtensionVersion{major: 0, minor: 0, patch: 0},
202+
b: vmExtensionVersion{major: 0, minor: 0, patch: 1},
203+
expected: -1,
204+
},
205+
}
206+
207+
for _, tt := range tests {
208+
t.Run(tt.name, func(t *testing.T) {
209+
got := tt.a.cmp(tt.b)
210+
if got != tt.expected {
211+
t.Errorf("(%v).cmp(%v) = %d, want %d", tt.a, tt.b, got, tt.expected)
212+
}
213+
})
214+
}
215+
}
216+
217+
func Test_getLatestVMExtensionImageVersion(t *testing.T) {
218+
tests := []struct {
219+
name string
220+
mock *mockVMExtensionImageVersionLister
221+
expected string
222+
errContains string
223+
}{
224+
{
225+
name: "multiple versions, returns latest",
226+
mock: &mockVMExtensionImageVersionLister{
227+
resp: makeVersionResponse(to.Ptr("1.0.0"), to.Ptr("2.1.0"), to.Ptr("1.5.3")),
228+
},
229+
expected: "2.1.0",
230+
},
231+
{
232+
name: "single version",
233+
mock: &mockVMExtensionImageVersionLister{
234+
resp: makeVersionResponse(to.Ptr("3.2.1")),
235+
},
236+
expected: "3.2.1",
237+
},
238+
{
239+
name: "two-part versions",
240+
mock: &mockVMExtensionImageVersionLister{
241+
resp: makeVersionResponse(to.Ptr("1.100"), to.Ptr("1.151"), to.Ptr("1.50")),
242+
},
243+
expected: "1.151",
244+
},
245+
{
246+
name: "API error propagated",
247+
mock: &mockVMExtensionImageVersionLister{
248+
err: fmt.Errorf("network failure"),
249+
},
250+
errContains: "listing extension versions",
251+
},
252+
{
253+
name: "empty list",
254+
mock: &mockVMExtensionImageVersionLister{
255+
resp: makeVersionResponse(),
256+
},
257+
errContains: "no extension versions found",
258+
},
259+
{
260+
name: "all nil names",
261+
mock: &mockVMExtensionImageVersionLister{
262+
resp: makeVersionResponse(nil),
263+
},
264+
errContains: "latest extension version has nil name",
265+
},
266+
{
267+
name: "mix valid and malformed",
268+
mock: &mockVMExtensionImageVersionLister{
269+
resp: makeVersionResponse(to.Ptr("abc"), to.Ptr("1.2.3"), to.Ptr("xyz")),
270+
},
271+
expected: "1.2.3",
272+
},
273+
}
274+
275+
for _, tt := range tests {
276+
t.Run(tt.name, func(t *testing.T) {
277+
ctx := toolkit.ContextWithT(context.Background(), t)
278+
got, err := getLatestVMExtensionImageVersion(
279+
ctx,
280+
tt.mock,
281+
"eastus",
282+
"TestExtension",
283+
"TestPublisher",
284+
)
285+
286+
if tt.errContains != "" {
287+
if err == nil {
288+
t.Fatalf("expected error containing %q, got nil", tt.errContains)
289+
}
290+
if !strings.Contains(err.Error(), tt.errContains) {
291+
t.Errorf("error %q does not contain %q", err.Error(), tt.errContains)
292+
}
293+
return
294+
}
295+
296+
if err != nil {
297+
t.Fatalf("unexpected error: %v", err)
298+
}
299+
if got != tt.expected {
300+
t.Errorf("got %q, want %q", got, tt.expected)
301+
}
302+
})
303+
}
304+
}

0 commit comments

Comments
 (0)