Skip to content

Commit 2d6ef47

Browse files
authored
Merge pull request #102 from WilboMo/testFilterAvailableUpdates
Add unit test for filterAvailableUpdates
2 parents 58fc96e + 89ea9c0 commit 2d6ef47

File tree

1 file changed

+286
-0
lines changed

1 file changed

+286
-0
lines changed

updater/aws_test.go

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,292 @@ import (
1515
"github.com/stretchr/testify/require"
1616
)
1717

18+
func TestFilterAvailableUpdates(t *testing.T) {
19+
instances := []instance{
20+
{
21+
instanceID: "inst-id-1",
22+
containerInstanceID: "cont-inst-1",
23+
},
24+
{
25+
instanceID: "inst-id-2",
26+
containerInstanceID: "cont-inst-2",
27+
},
28+
{
29+
instanceID: "inst-id-3",
30+
containerInstanceID: "cont-inst-3",
31+
},
32+
{
33+
instanceID: "inst-id-4",
34+
containerInstanceID: "cont-inst-4",
35+
},
36+
{
37+
instanceID: "inst-id-5",
38+
containerInstanceID: "cont-inst-5",
39+
},
40+
}
41+
expected := []instance{
42+
{
43+
instanceID: "inst-id-1",
44+
containerInstanceID: "cont-inst-1",
45+
bottlerocketVersion: "v1.0.5",
46+
},
47+
{
48+
instanceID: "inst-id-2",
49+
containerInstanceID: "cont-inst-2",
50+
bottlerocketVersion: "v1.0.5",
51+
},
52+
{
53+
instanceID: "inst-id-5",
54+
containerInstanceID: "cont-inst-5",
55+
bottlerocketVersion: "v1.0.5",
56+
},
57+
}
58+
responses := map[string]string{
59+
"inst-id-1": `{"update_state": "Available", "active_partition": { "image": { "version": "v1.0.5"}}}`,
60+
"inst-id-2": `{"update_state": "Ready", "active_partition": { "image": { "version": "v1.0.5"}}}`,
61+
"inst-id-3": `{"update_state": "Idle", "active_partition": { "image": { "version": "v1.1.1"}}}`,
62+
"inst-id-4": `{"update_state": "Staged", "active_partition": { "image": { "version": "v1.1.1"}}}`,
63+
"inst-id-5": `{"update_state": "Available", "active_partition": { "image": { "version": "v1.0.5"}}}`,
64+
}
65+
sendCommandCalls := 0
66+
commandWaiterCalls := 0
67+
getCommandInvocationCalls := 0
68+
mockSSM := MockSSM{
69+
GetCommandInvocationFn: func(input *ssm.GetCommandInvocationInput) (*ssm.GetCommandInvocationOutput, error) {
70+
getCommandInvocationCalls++
71+
return &ssm.GetCommandInvocationOutput{
72+
Status: aws.String("Success"),
73+
StandardOutputContent: aws.String(responses[*input.InstanceId]),
74+
}, nil
75+
},
76+
SendCommandFn: func(input *ssm.SendCommandInput) (*ssm.SendCommandOutput, error) {
77+
sendCommandCalls++
78+
return &ssm.SendCommandOutput{
79+
Command: &ssm.Command{
80+
CommandId: aws.String("command-id"),
81+
DocumentName: aws.String("check-document"),
82+
},
83+
}, nil
84+
},
85+
WaitUntilCommandExecutedWithContextFn: func(ctx aws.Context, input *ssm.GetCommandInvocationInput, opts ...request.WaiterOption) error {
86+
commandWaiterCalls++
87+
assert.Equal(t, "command-id", aws.StringValue(input.CommandId))
88+
return nil
89+
},
90+
}
91+
u := updater{ssm: mockSSM, checkDocument: "check-document"}
92+
actual, err := u.filterAvailableUpdates(instances)
93+
require.NoError(t, err)
94+
assert.Equal(t, expected, actual, "Should only contain instances in Aavailable or Ready update state")
95+
assert.Equal(t, 1, sendCommandCalls, "should send commands for each page")
96+
assert.Equal(t, 5, commandWaiterCalls, "should wait for each instance")
97+
assert.Equal(t, 5, getCommandInvocationCalls, "should collect output for each instance")
98+
}
99+
100+
func TestPaginatedFilterAvailableUpdatesSuccess(t *testing.T) {
101+
checkPattern := `{"update_state": "%s", "active_partition": { "image": { "version": "%s"}}}`
102+
expected := make([]instance, 0)
103+
instances := make([]instance, 0)
104+
getOut := &ssm.GetCommandInvocationOutput{
105+
Status: aws.String("Success"),
106+
StandardOutputContent: aws.String(fmt.Sprintf(checkPattern, updateStateAvailable, "v1.0.5")),
107+
}
108+
109+
for i := 0; i < 100; i++ { // 100 is chosen here to reprsent 2 full pages of SSM (limited to 50 per page)
110+
containerID := "cont-inst-br" + strconv.Itoa(i)
111+
ec2ID := "ec2-id-br" + strconv.Itoa(i)
112+
instances = append(instances, instance{
113+
instanceID: ec2ID,
114+
containerInstanceID: containerID,
115+
})
116+
expected = append(expected, instance{
117+
instanceID: ec2ID,
118+
containerInstanceID: containerID,
119+
bottlerocketVersion: "v1.0.5",
120+
})
121+
}
122+
123+
sendCommandCalls := 0
124+
commandWaiterCalls := 0
125+
getCommandInvocationCalls := 0
126+
mockSSM := MockSSM{
127+
GetCommandInvocationFn: func(input *ssm.GetCommandInvocationInput) (*ssm.GetCommandInvocationOutput, error) {
128+
getCommandInvocationCalls++
129+
return getOut, nil
130+
},
131+
SendCommandFn: func(input *ssm.SendCommandInput) (*ssm.SendCommandOutput, error) {
132+
sendCommandCalls++
133+
return &ssm.SendCommandOutput{
134+
Command: &ssm.Command{
135+
CommandId: aws.String("command-id"),
136+
DocumentName: aws.String("check-document"),
137+
},
138+
}, nil
139+
},
140+
WaitUntilCommandExecutedWithContextFn: func(ctx aws.Context, input *ssm.GetCommandInvocationInput, opts ...request.WaiterOption) error {
141+
commandWaiterCalls++
142+
assert.Equal(t, "command-id", aws.StringValue(input.CommandId))
143+
return nil
144+
},
145+
}
146+
u := updater{ssm: mockSSM}
147+
actual, err := u.filterAvailableUpdates(instances)
148+
require.NoError(t, err)
149+
assert.EqualValues(t, expected, actual, "should contain all instances")
150+
assert.Equal(t, 2, sendCommandCalls, "should send commands for each page")
151+
assert.Equal(t, 100, commandWaiterCalls, "should wait for each instance")
152+
assert.Equal(t, 100, getCommandInvocationCalls, "should collect output for each instance")
153+
}
154+
155+
func TestPaginatedFilterAvailableUpdatesAllFail(t *testing.T) {
156+
instances := make([]instance, 0)
157+
158+
for i := 0; i < 100; i++ {
159+
containerID := "cont-inst-br" + strconv.Itoa(i)
160+
ec2ID := "ec2-id-br" + strconv.Itoa(i)
161+
instances = append(instances, instance{
162+
instanceID: ec2ID,
163+
containerInstanceID: containerID,
164+
})
165+
}
166+
167+
sendCommandCalls := 0
168+
mockSSM := MockSSM{
169+
SendCommandFn: func(input *ssm.SendCommandInput) (*ssm.SendCommandOutput, error) {
170+
sendCommandCalls++
171+
return nil, errors.New("Failed to send document")
172+
},
173+
}
174+
u := updater{ssm: mockSSM}
175+
actual, err := u.filterAvailableUpdates(instances)
176+
require.Error(t, err)
177+
assert.Contains(t, err.Error(), "Failed to send document")
178+
assert.Empty(t, actual)
179+
assert.Equal(t, 2, sendCommandCalls, "should send commands for each page")
180+
}
181+
182+
func TestPaginatedFilterAvailableUpdatesInPageFailures(t *testing.T) {
183+
instances := make([]instance, 0)
184+
checkPattern := `{"update_state": "%s", "active_partition": { "image": { "version": "%s"}}}`
185+
for i := 0; i < 120; i++ { // 120 chosen here to ensure multiple pages are tested and that number instances divides by 3 evenly
186+
containerID := "cont-inst-br" + strconv.Itoa(i)
187+
ec2ID := "ec2-id-br" + strconv.Itoa(i)
188+
instances = append(instances, instance{
189+
instanceID: ec2ID,
190+
containerInstanceID: containerID,
191+
})
192+
}
193+
194+
sendCommandCalls := 0
195+
commandWaiterCalls := 0
196+
getCommandInvocationCalls := 0
197+
count := 0
198+
mockSSM := MockSSM{
199+
GetCommandInvocationFn: func(input *ssm.GetCommandInvocationInput) (*ssm.GetCommandInvocationOutput, error) {
200+
count++
201+
getCommandInvocationCalls++
202+
switch count % 3 {
203+
case 0:
204+
return nil, errors.New("Failed to get command output") // validate getCommandResult failure
205+
case 1:
206+
return &ssm.GetCommandInvocationOutput{
207+
Status: aws.String("Success"),
208+
StandardOutputContent: aws.String("{}"),
209+
}, nil // validates parseCommandOutput failure
210+
case 2:
211+
return &ssm.GetCommandInvocationOutput{
212+
Status: aws.String("Success"),
213+
StandardOutputContent: aws.String(fmt.Sprintf(checkPattern, updateStateAvailable, "v1.0.5")),
214+
}, nil // validate success case
215+
}
216+
return nil, nil
217+
},
218+
SendCommandFn: func(input *ssm.SendCommandInput) (*ssm.SendCommandOutput, error) {
219+
sendCommandCalls++
220+
return &ssm.SendCommandOutput{
221+
Command: &ssm.Command{
222+
CommandId: aws.String("command-id"),
223+
DocumentName: aws.String("check-document"),
224+
},
225+
}, nil
226+
},
227+
WaitUntilCommandExecutedWithContextFn: func(ctx aws.Context, input *ssm.GetCommandInvocationInput, opts ...request.WaiterOption) error {
228+
assert.Equal(t, "command-id", aws.StringValue(input.CommandId))
229+
commandWaiterCalls++
230+
return nil
231+
},
232+
}
233+
u := updater{ssm: mockSSM}
234+
actual, err := u.filterAvailableUpdates(instances)
235+
require.NoError(t, err)
236+
assert.EqualValues(t, 40, len(actual), "Every 3rd instance of 120 should succeed")
237+
assert.Equal(t, 3, sendCommandCalls, "should send commands for each page")
238+
assert.Equal(t, 120, commandWaiterCalls, "should wait for each instance")
239+
assert.Equal(t, 120, getCommandInvocationCalls, "should collect output for each instance")
240+
}
241+
242+
func TestPaginatedFilterAvailableUpdatesSingleErr(t *testing.T) {
243+
checkPattern := `{"update_state": "%s", "active_partition": { "image": { "version": "%s"}}}`
244+
expected := make([]instance, 0)
245+
instances := make([]instance, 0)
246+
getOut := &ssm.GetCommandInvocationOutput{
247+
Status: aws.String("Success"),
248+
StandardOutputContent: aws.String(fmt.Sprintf(checkPattern, updateStateAvailable, "v1.0.5")),
249+
}
250+
251+
for i := 0; i < 100; i++ {
252+
containerID := "cont-inst-br" + strconv.Itoa(i)
253+
ec2ID := "ec2-id-br" + strconv.Itoa(i)
254+
instances = append(instances, instance{
255+
instanceID: ec2ID,
256+
containerInstanceID: containerID,
257+
})
258+
expected = append(expected, instance{
259+
instanceID: ec2ID,
260+
containerInstanceID: containerID,
261+
bottlerocketVersion: "v1.0.5",
262+
})
263+
}
264+
265+
pageErrors := []error{errors.New("Failed to send document"), nil}
266+
267+
sendCommandCalls := 0
268+
commandWaiterCalls := 0
269+
getCommandInvocationCalls := 0
270+
callCount := 0
271+
mockSSM := MockSSM{
272+
GetCommandInvocationFn: func(input *ssm.GetCommandInvocationInput) (*ssm.GetCommandInvocationOutput, error) {
273+
getCommandInvocationCalls++
274+
return getOut, nil
275+
},
276+
SendCommandFn: func(input *ssm.SendCommandInput) (*ssm.SendCommandOutput, error) {
277+
require.Less(t, callCount, len(pageErrors))
278+
failErr := pageErrors[callCount]
279+
callCount++
280+
sendCommandCalls++
281+
return &ssm.SendCommandOutput{
282+
Command: &ssm.Command{
283+
CommandId: aws.String("command-id"),
284+
DocumentName: aws.String("check-document"),
285+
},
286+
}, failErr
287+
},
288+
WaitUntilCommandExecutedWithContextFn: func(ctx aws.Context, input *ssm.GetCommandInvocationInput, opts ...request.WaiterOption) error {
289+
assert.Equal(t, "command-id", aws.StringValue(input.CommandId))
290+
commandWaiterCalls++
291+
return nil
292+
},
293+
}
294+
u := updater{ssm: mockSSM}
295+
actual, err := u.filterAvailableUpdates(instances)
296+
297+
require.NoError(t, err)
298+
assert.EqualValues(t, actual, expected[50:], "Should only contain instances from the 2nd page")
299+
assert.Equal(t, 2, sendCommandCalls, "should send commands for each page")
300+
assert.Equal(t, 50, commandWaiterCalls, "should wait for each instance")
301+
assert.Equal(t, 50, getCommandInvocationCalls, "should collect output for each instance")
302+
}
303+
18304
func TestGetCommandResult(t *testing.T) {
19305
cases := []struct {
20306
name string

0 commit comments

Comments
 (0)