Skip to content

Commit d0b6326

Browse files
ROSAENG-60112 | test: add unit tests for pkg/aws (Phase 1A) (#3299)
Add meaningful tests for previously untested files and functions in pkg/aws, focusing on ROSA-specific business logic. Test files: - idps_test.go: OIDC provider Create/Has/Delete - quota_test.go: GetServiceQuota lookup - policy_document_test.go: parse, interpolate, principals, actions - helpers_test.go: ARN validators, name builders, isSTS, resolveSTSRole - cloudformation_test.go: CheckStackReady, UpdateStack no-op, DeleteOsdCcsAdminUser, buildInput helpers - sts_test.go: ValidateRoleARNAccountID, SortRolesByLinkedRole, DeleteUserRole, DeleteOCMRole - policies_test.go: hasCompatibleMajorMinorVersionTags, IsPolicyCompatible
1 parent d7aca27 commit d0b6326

7 files changed

Lines changed: 2117 additions & 0 deletions

File tree

pkg/aws/cloudformation_test.go

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
package aws
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
gomock "go.uber.org/mock/gomock"
8+
9+
awsSdk "github.com/aws/aws-sdk-go-v2/aws"
10+
"github.com/aws/aws-sdk-go-v2/service/cloudformation"
11+
cloudformationtypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
12+
"github.com/aws/smithy-go"
13+
. "github.com/onsi/ginkgo/v2"
14+
. "github.com/onsi/gomega"
15+
"github.com/sirupsen/logrus"
16+
17+
"github.com/openshift/rosa/pkg/aws/mocks"
18+
)
19+
20+
var _ = Describe("CloudFormation", func() {
21+
var (
22+
client Client
23+
mockCtrl *gomock.Controller
24+
mockCfAPI *mocks.MockCloudFormationApiClient
25+
)
26+
27+
BeforeEach(func() {
28+
mockCtrl = gomock.NewController(GinkgoT())
29+
mockCfAPI = mocks.NewMockCloudFormationApiClient(mockCtrl)
30+
client = New(
31+
awsSdk.Config{},
32+
NewLoggerWrapper(logrus.New(), nil),
33+
mocks.NewMockIamApiClient(mockCtrl),
34+
mocks.NewMockEc2ApiClient(mockCtrl),
35+
mocks.NewMockOrganizationsApiClient(mockCtrl),
36+
mocks.NewMockS3ApiClient(mockCtrl),
37+
mocks.NewMockSecretsManagerApiClient(mockCtrl),
38+
mocks.NewMockStsApiClient(mockCtrl),
39+
mockCfAPI,
40+
mocks.NewMockServiceQuotasApiClient(mockCtrl),
41+
mocks.NewMockServiceQuotasApiClient(mockCtrl),
42+
&AccessKey{},
43+
false,
44+
)
45+
})
46+
47+
AfterEach(func() {
48+
mockCtrl.Finish()
49+
})
50+
51+
Context("CheckStackReadyOrNotExisting", func() {
52+
stackName := "test-stack"
53+
54+
It("Returns ready when stack is CREATE_COMPLETE", func() {
55+
mockCfAPI.EXPECT().ListStacks(gomock.Any(), gomock.Any()).Return(
56+
&cloudformation.ListStacksOutput{
57+
StackSummaries: []cloudformationtypes.StackSummary{
58+
{
59+
StackName: awsSdk.String(stackName),
60+
StackStatus: cloudformationtypes.StackStatusCreateComplete,
61+
},
62+
},
63+
}, nil)
64+
65+
ready, status, err := client.CheckStackReadyOrNotExisting(stackName)
66+
Expect(err).NotTo(HaveOccurred())
67+
Expect(ready).To(BeTrue())
68+
Expect(*status).To(Equal("CREATE_COMPLETE"))
69+
})
70+
71+
It("Returns ready when stack is UPDATE_COMPLETE", func() {
72+
mockCfAPI.EXPECT().ListStacks(gomock.Any(), gomock.Any()).Return(
73+
&cloudformation.ListStacksOutput{
74+
StackSummaries: []cloudformationtypes.StackSummary{
75+
{
76+
StackName: awsSdk.String(stackName),
77+
StackStatus: cloudformationtypes.StackStatusUpdateComplete,
78+
},
79+
},
80+
}, nil)
81+
82+
ready, status, err := client.CheckStackReadyOrNotExisting(stackName)
83+
Expect(err).NotTo(HaveOccurred())
84+
Expect(ready).To(BeTrue())
85+
Expect(*status).To(Equal("UPDATE_COMPLETE"))
86+
})
87+
88+
It("Skips DELETE_COMPLETE stacks (treated as non-existing)", func() {
89+
mockCfAPI.EXPECT().ListStacks(gomock.Any(), gomock.Any()).Return(
90+
&cloudformation.ListStacksOutput{
91+
StackSummaries: []cloudformationtypes.StackSummary{
92+
{
93+
StackName: awsSdk.String(stackName),
94+
StackStatus: cloudformationtypes.StackStatusDeleteComplete,
95+
},
96+
},
97+
}, nil)
98+
99+
ready, status, err := client.CheckStackReadyOrNotExisting(stackName)
100+
Expect(err).NotTo(HaveOccurred())
101+
Expect(ready).To(BeFalse())
102+
Expect(status).To(BeNil())
103+
})
104+
105+
It("Returns error with suggestion for ROLLBACK_COMPLETE stack", func() {
106+
mockCfAPI.EXPECT().ListStacks(gomock.Any(), gomock.Any()).Return(
107+
&cloudformation.ListStacksOutput{
108+
StackSummaries: []cloudformationtypes.StackSummary{
109+
{
110+
StackName: awsSdk.String(stackName),
111+
StackStatus: cloudformationtypes.StackStatusRollbackComplete,
112+
},
113+
},
114+
}, nil)
115+
116+
ready, status, err := client.CheckStackReadyOrNotExisting(stackName)
117+
Expect(err).To(HaveOccurred())
118+
Expect(ready).To(BeFalse())
119+
Expect(*status).To(Equal("ROLLBACK_COMPLETE"))
120+
Expect(err.Error()).To(ContainSubstring(
121+
"exists with status ROLLBACK_COMPLETE. Expected status is CREATE_COMPLETE"))
122+
Expect(err.Error()).To(ContainSubstring("rosa init --delete-stack"))
123+
})
124+
125+
It("Returns not found when no stacks match", func() {
126+
mockCfAPI.EXPECT().ListStacks(gomock.Any(), gomock.Any()).Return(
127+
&cloudformation.ListStacksOutput{
128+
StackSummaries: []cloudformationtypes.StackSummary{
129+
{
130+
StackName: awsSdk.String("other-stack"),
131+
StackStatus: cloudformationtypes.StackStatusCreateComplete,
132+
},
133+
},
134+
}, nil)
135+
136+
ready, status, err := client.CheckStackReadyOrNotExisting(stackName)
137+
Expect(err).NotTo(HaveOccurred())
138+
Expect(ready).To(BeFalse())
139+
Expect(status).To(BeNil())
140+
})
141+
142+
It("Returns not found when stack list is empty", func() {
143+
mockCfAPI.EXPECT().ListStacks(gomock.Any(), gomock.Any()).Return(
144+
&cloudformation.ListStacksOutput{
145+
StackSummaries: []cloudformationtypes.StackSummary{},
146+
}, nil)
147+
148+
ready, status, err := client.CheckStackReadyOrNotExisting(stackName)
149+
Expect(err).NotTo(HaveOccurred())
150+
Expect(ready).To(BeFalse())
151+
Expect(status).To(BeNil())
152+
})
153+
154+
It("Propagates ListStacks API error", func() {
155+
mockCfAPI.EXPECT().ListStacks(gomock.Any(), gomock.Any()).Return(
156+
nil, fmt.Errorf("access denied"))
157+
158+
ready, status, err := client.CheckStackReadyOrNotExisting(stackName)
159+
Expect(err).To(HaveOccurred())
160+
Expect(err.Error()).To(ContainSubstring("access denied"))
161+
Expect(ready).To(BeFalse())
162+
Expect(status).To(BeNil())
163+
})
164+
})
165+
166+
Context("DeleteOsdCcsAdminUser", func() {
167+
stackName := "test-stack"
168+
169+
It("Returns nil on TokenAlreadyExistsException", func() {
170+
mockCfAPI.EXPECT().DeleteStack(gomock.Any(), gomock.Any(), gomock.Any()).
171+
DoAndReturn(func(_ context.Context, input *cloudformation.DeleteStackInput,
172+
_ ...func(*cloudformation.Options)) (*cloudformation.DeleteStackOutput, error) {
173+
Expect(*input.StackName).To(Equal(stackName))
174+
return nil, &cloudformationtypes.TokenAlreadyExistsException{
175+
Message: awsSdk.String("token exists"),
176+
}
177+
})
178+
179+
err := client.DeleteOsdCcsAdminUser(stackName)
180+
Expect(err).NotTo(HaveOccurred())
181+
})
182+
183+
It("Propagates non-TokenAlreadyExistsException errors", func() {
184+
mockCfAPI.EXPECT().DeleteStack(gomock.Any(), gomock.Any(), gomock.Any()).
185+
DoAndReturn(func(_ context.Context, input *cloudformation.DeleteStackInput,
186+
_ ...func(*cloudformation.Options)) (*cloudformation.DeleteStackOutput, error) {
187+
Expect(*input.StackName).To(Equal(stackName))
188+
return nil, fmt.Errorf("internal server error")
189+
})
190+
191+
err := client.DeleteOsdCcsAdminUser(stackName)
192+
Expect(err).To(HaveOccurred())
193+
Expect(err.Error()).To(ContainSubstring("internal server error"))
194+
})
195+
})
196+
197+
Context("UpdateStack", func() {
198+
stackName := "test-stack"
199+
templateBody := `{"AWSTemplateFormatVersion":"2010-09-09"}`
200+
201+
It("Returns nil when 'No updates are to be performed'", func() {
202+
mockCfAPI.EXPECT().UpdateStack(gomock.Any(), gomock.Any(), gomock.Any()).
203+
DoAndReturn(func(_ context.Context, input *cloudformation.UpdateStackInput,
204+
_ ...func(*cloudformation.Options)) (*cloudformation.UpdateStackOutput, error) {
205+
Expect(*input.StackName).To(Equal(stackName))
206+
Expect(*input.TemplateBody).To(Equal(templateBody))
207+
return nil, &smithy.GenericAPIError{
208+
Code: "ValidationError",
209+
Message: "No updates are to be performed",
210+
}
211+
})
212+
213+
awsC := client.(*awsClient)
214+
err := awsC.UpdateStack(templateBody, stackName)
215+
Expect(err).NotTo(HaveOccurred())
216+
})
217+
218+
It("Returns error for non-no-op ValidationError", func() {
219+
mockCfAPI.EXPECT().UpdateStack(gomock.Any(), gomock.Any(), gomock.Any()).
220+
DoAndReturn(func(_ context.Context, _ *cloudformation.UpdateStackInput,
221+
_ ...func(*cloudformation.Options)) (*cloudformation.UpdateStackOutput, error) {
222+
return nil, &smithy.GenericAPIError{
223+
Code: "ValidationError",
224+
Message: "Template format error",
225+
}
226+
})
227+
228+
awsC := client.(*awsClient)
229+
err := awsC.UpdateStack(templateBody, stackName)
230+
Expect(err).To(HaveOccurred())
231+
Expect(err.Error()).To(ContainSubstring("Template format error"))
232+
})
233+
234+
It("Returns error for non-ValidationError API error", func() {
235+
mockCfAPI.EXPECT().UpdateStack(gomock.Any(), gomock.Any(), gomock.Any()).
236+
DoAndReturn(func(_ context.Context, _ *cloudformation.UpdateStackInput,
237+
_ ...func(*cloudformation.Options)) (*cloudformation.UpdateStackOutput, error) {
238+
return nil, fmt.Errorf("throttling exception")
239+
})
240+
241+
awsC := client.(*awsClient)
242+
err := awsC.UpdateStack(templateBody, stackName)
243+
Expect(err).To(HaveOccurred())
244+
Expect(err.Error()).To(ContainSubstring("throttling exception"))
245+
})
246+
})
247+
248+
Context("buildCreateStackInput", func() {
249+
It("Sets correct capabilities, name, and template", func() {
250+
body := `{"Resources":{}}`
251+
name := "my-stack"
252+
params := []cloudformationtypes.Parameter{
253+
{ParameterKey: awsSdk.String("Env"), ParameterValue: awsSdk.String("prod")},
254+
}
255+
tags := []cloudformationtypes.Tag{
256+
{Key: awsSdk.String("team"), Value: awsSdk.String("platform")},
257+
}
258+
259+
input := buildCreateStackInput(body, name, params, tags)
260+
261+
Expect(*input.StackName).To(Equal(name))
262+
Expect(*input.TemplateBody).To(Equal(body))
263+
Expect(input.Capabilities).To(HaveLen(3))
264+
Expect(input.Capabilities).To(ContainElements(
265+
cloudformationtypes.CapabilityCapabilityIam,
266+
cloudformationtypes.CapabilityCapabilityNamedIam,
267+
cloudformationtypes.CapabilityCapabilityAutoExpand,
268+
))
269+
Expect(input.Parameters).To(HaveLen(1))
270+
Expect(*input.Parameters[0].ParameterKey).To(Equal("Env"))
271+
Expect(*input.Parameters[0].ParameterValue).To(Equal("prod"))
272+
Expect(input.Tags).To(HaveLen(1))
273+
Expect(*input.Tags[0].Key).To(Equal("team"))
274+
Expect(*input.Tags[0].Value).To(Equal("platform"))
275+
})
276+
277+
It("Handles empty params and tags", func() {
278+
input := buildCreateStackInput("body", "stack", nil, nil)
279+
280+
Expect(*input.StackName).To(Equal("stack"))
281+
Expect(*input.TemplateBody).To(Equal("body"))
282+
Expect(input.Capabilities).To(HaveLen(3))
283+
Expect(input.Parameters).To(BeNil())
284+
Expect(input.Tags).To(BeNil())
285+
})
286+
})
287+
288+
Context("buildUpdateStackInput", func() {
289+
It("Sets correct capabilities, name, and template", func() {
290+
body := `{"Resources":{}}`
291+
name := "my-stack"
292+
293+
input := buildUpdateStackInput(body, name)
294+
295+
Expect(*input.StackName).To(Equal(name))
296+
Expect(*input.TemplateBody).To(Equal(body))
297+
Expect(input.Capabilities).To(HaveLen(3))
298+
Expect(input.Capabilities).To(ContainElements(
299+
cloudformationtypes.CapabilityCapabilityIam,
300+
cloudformationtypes.CapabilityCapabilityNamedIam,
301+
cloudformationtypes.CapabilityCapabilityAutoExpand,
302+
))
303+
})
304+
})
305+
})

0 commit comments

Comments
 (0)