diff --git a/go.mod b/go.mod index 6c17dcf..ada0d4b 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( ) require ( + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.16 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect @@ -27,22 +28,31 @@ require ( github.com/aws/smithy-go v1.25.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/mock v1.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/nexus-rpc/sdk-go v0.6.0 // indirect + github.com/onsi/ginkgo/v2 v2.28.3 // indirect + github.com/onsi/gomega v1.40.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/robfig/cron v1.2.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.11.1 // indirect go.temporal.io/api v1.62.11 // indirect - golang.org/x/net v0.49.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.33.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/text v0.36.0 // indirect golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.44.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect google.golang.org/grpc v1.79.3 // indirect diff --git a/go.sum b/go.sum index f5866c7..ee977b4 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8= github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU= @@ -42,6 +44,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= @@ -50,6 +54,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= @@ -66,12 +72,17 @@ github.com/labstack/echo/v5 v5.1.1 h1:4QkvKoS8ps5ch49t8b72QS9Z581ytgxhTzxuB/CBA2 github.com/labstack/echo/v5 v5.1.1/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo= github.com/nexus-rpc/sdk-go v0.6.0 h1:QRgnP2zTbxEbiyWG/aXH8uSC5LV/Mg1fqb19jb4DBlo= github.com/nexus-rpc/sdk-go v0.6.0/go.mod h1:FHdPfVQwRuJFZFTF0Y2GOAxCrbIBNrcPna9slkGKPYk= +github.com/onsi/ginkgo/v2 v2.28.3 h1:4JvMdwtFU0imd8fHx25OJXoDMRexnf8v5NHKYSTTji4= +github.com/onsi/ginkgo/v2 v2.28.3/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= +github.com/onsi/gomega v1.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc= +github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -95,12 +106,16 @@ go.temporal.io/api v1.62.11 h1:MWDaooDvOJCIRb1atqeZX2ErDPNTsNc3/mMEVEvvaVU= go.temporal.io/api v1.62.11/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= go.temporal.io/sdk v1.43.0 h1:jHX/T2ZyBVjAtpQ/69NoMS6a+J0CpJAe+naqSB1gkvY= go.temporal.io/sdk v1.43.0/go.mod h1:w9XuJzV25JhnJqUzxJWJISpp5q/EyeCtRKHvhW3lIoQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -108,12 +123,16 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -122,11 +141,15 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -134,6 +157,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/workflows/eks_test.go b/internal/workflows/eks_test.go index bfdaa91..41c68b2 100644 --- a/internal/workflows/eks_test.go +++ b/internal/workflows/eks_test.go @@ -2,33 +2,16 @@ package workflows import ( "errors" - "testing" - "github.com/Amertz08/gitops-example/internal/activities" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/suite" + + "github.com/Amertz08/gitops-example/internal/activities" "go.temporal.io/sdk/temporal" "go.temporal.io/sdk/testsuite" ) -type EKSWorkflowTestSuite struct { - suite.Suite - testsuite.WorkflowTestSuite - env *testsuite.TestWorkflowEnvironment -} - -func (s *EKSWorkflowTestSuite) SetupTest() { - s.env = s.NewTestWorkflowEnvironment() -} - -func (s *EKSWorkflowTestSuite) AfterTest(_, _ string) { - s.env.AssertExpectations(s.T()) -} - -func TestEKSWorkflowSuite(t *testing.T) { - suite.Run(t, new(EKSWorkflowTestSuite)) -} - func validEKSInput() SpinUpEKSInput { return SpinUpEKSInput{ Region: "us-east-1", @@ -44,63 +27,102 @@ func validEKSInput() SpinUpEKSInput { } } -func (s *EKSWorkflowTestSuite) Test_SpinUpEKS_InvalidInput() { - s.env.ExecuteWorkflow(SpinUpEKSWorkflow, SpinUpEKSInput{}) +var _ = Describe("SpinUpEKSWorkflow", func() { + var ( + testSuite testsuite.WorkflowTestSuite + env *testsuite.TestWorkflowEnvironment + ) - s.True(s.env.IsWorkflowCompleted()) - err := s.env.GetWorkflowError() - s.Error(err) - var appErr *temporal.ApplicationError - s.True(errors.As(err, &appErr)) - s.Equal("InvalidInput", appErr.Type()) -} + BeforeEach(func() { + env = testSuite.NewTestWorkflowEnvironment() + }) + + AfterEach(func() { + env.AssertExpectations(GinkgoT()) + }) -func (s *EKSWorkflowTestSuite) Test_SpinUpEKS_Success() { - aws := &activities.AWSActivities{} - s.env.OnActivity(aws.CreateEKSCluster, mock.Anything, mock.Anything).Return(nil) - s.env.OnActivity(aws.CreateNodeGroup, mock.Anything, mock.Anything).Return(nil) + Context("with invalid input", func() { + It("returns a non-retryable InvalidInput error", func() { + env.ExecuteWorkflow(SpinUpEKSWorkflow, SpinUpEKSInput{}) + + Expect(env.IsWorkflowCompleted()).To(BeTrue()) + err := env.GetWorkflowError() + Expect(err).To(HaveOccurred()) + var appErr *temporal.ApplicationError + Expect(errors.As(err, &appErr)).To(BeTrue()) + Expect(appErr.Type()).To(Equal("InvalidInput")) + }) + }) - s.env.ExecuteWorkflow(SpinUpEKSWorkflow, validEKSInput()) + Context("with valid input", func() { + It("creates the EKS cluster and node group", func() { + aws := &activities.AWSActivities{} + env.OnActivity(aws.CreateEKSCluster, mock.Anything, mock.Anything).Return(nil) + env.OnActivity(aws.CreateNodeGroup, mock.Anything, mock.Anything).Return(nil) - s.True(s.env.IsWorkflowCompleted()) - s.NoError(s.env.GetWorkflowError()) -} + env.ExecuteWorkflow(SpinUpEKSWorkflow, validEKSInput()) -// CreateNodeGroup failing must trigger saga compensation to delete the cluster. -func (s *EKSWorkflowTestSuite) Test_SpinUpEKS_NodeGroupFailure_CompensatesCluster() { - aws := &activities.AWSActivities{} - s.env.OnActivity(aws.CreateEKSCluster, mock.Anything, mock.Anything).Return(nil) - s.env.OnActivity(aws.CreateNodeGroup, mock.Anything, mock.Anything). - Return(errors.New("node group quota exceeded")) - s.env.OnActivity(aws.DeleteEKSCluster, mock.Anything, mock.Anything).Return(nil).Once() + Expect(env.IsWorkflowCompleted()).To(BeTrue()) + Expect(env.GetWorkflowError()).NotTo(HaveOccurred()) + }) + }) - s.env.ExecuteWorkflow(SpinUpEKSWorkflow, validEKSInput()) + Context("when CreateNodeGroup fails", func() { + It("compensates by deleting the EKS cluster", func() { + aws := &activities.AWSActivities{} + env.OnActivity(aws.CreateEKSCluster, mock.Anything, mock.Anything).Return(nil) + env.OnActivity(aws.CreateNodeGroup, mock.Anything, mock.Anything). + Return(errors.New("node group quota exceeded")) + env.OnActivity(aws.DeleteEKSCluster, mock.Anything, mock.Anything).Return(nil).Once() - s.True(s.env.IsWorkflowCompleted()) - s.Error(s.env.GetWorkflowError()) -} + env.ExecuteWorkflow(SpinUpEKSWorkflow, validEKSInput()) -func (s *EKSWorkflowTestSuite) Test_SpinDownEKS_InvalidInput() { - s.env.ExecuteWorkflow(SpinDownEKSWorkflow, SpinDownEKSInput{}) + Expect(env.IsWorkflowCompleted()).To(BeTrue()) + Expect(env.GetWorkflowError()).To(HaveOccurred()) + }) + }) +}) - s.True(s.env.IsWorkflowCompleted()) - err := s.env.GetWorkflowError() - s.Error(err) - var appErr *temporal.ApplicationError - s.True(errors.As(err, &appErr)) - s.Equal("InvalidInput", appErr.Type()) -} +var _ = Describe("SpinDownEKSWorkflow", func() { + var ( + testSuite testsuite.WorkflowTestSuite + env *testsuite.TestWorkflowEnvironment + ) -func (s *EKSWorkflowTestSuite) Test_SpinDownEKS_Success() { - aws := &activities.AWSActivities{} - s.env.OnActivity(aws.DeleteNodeGroup, mock.Anything, mock.Anything).Return(nil) - s.env.OnActivity(aws.DeleteEKSCluster, mock.Anything, mock.Anything).Return(nil) + BeforeEach(func() { + env = testSuite.NewTestWorkflowEnvironment() + }) - s.env.ExecuteWorkflow(SpinDownEKSWorkflow, SpinDownEKSInput{ - Region: "us-east-1", - ClusterName: "my-cluster", + AfterEach(func() { + env.AssertExpectations(GinkgoT()) }) - s.True(s.env.IsWorkflowCompleted()) - s.NoError(s.env.GetWorkflowError()) -} + Context("with invalid input", func() { + It("returns a non-retryable InvalidInput error", func() { + env.ExecuteWorkflow(SpinDownEKSWorkflow, SpinDownEKSInput{}) + + Expect(env.IsWorkflowCompleted()).To(BeTrue()) + err := env.GetWorkflowError() + Expect(err).To(HaveOccurred()) + var appErr *temporal.ApplicationError + Expect(errors.As(err, &appErr)).To(BeTrue()) + Expect(appErr.Type()).To(Equal("InvalidInput")) + }) + }) + + Context("with valid input", func() { + It("deletes the node group then the EKS cluster", func() { + aws := &activities.AWSActivities{} + env.OnActivity(aws.DeleteNodeGroup, mock.Anything, mock.Anything).Return(nil) + env.OnActivity(aws.DeleteEKSCluster, mock.Anything, mock.Anything).Return(nil) + + env.ExecuteWorkflow(SpinDownEKSWorkflow, SpinDownEKSInput{ + Region: "us-east-1", + ClusterName: "my-cluster", + }) + + Expect(env.IsWorkflowCompleted()).To(BeTrue()) + Expect(env.GetWorkflowError()).NotTo(HaveOccurred()) + }) + }) +}) diff --git a/internal/workflows/iam_test.go b/internal/workflows/iam_test.go index aa8ac74..69eb144 100644 --- a/internal/workflows/iam_test.go +++ b/internal/workflows/iam_test.go @@ -2,108 +2,130 @@ package workflows import ( "errors" - "testing" - "github.com/Amertz08/gitops-example/internal/activities" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/suite" + + "github.com/Amertz08/gitops-example/internal/activities" "go.temporal.io/sdk/temporal" "go.temporal.io/sdk/testsuite" ) -type IAMWorkflowTestSuite struct { - suite.Suite - testsuite.WorkflowTestSuite - env *testsuite.TestWorkflowEnvironment -} - -func (s *IAMWorkflowTestSuite) SetupTest() { - s.env = s.NewTestWorkflowEnvironment() -} - -func (s *IAMWorkflowTestSuite) AfterTest(_, _ string) { - s.env.AssertExpectations(s.T()) -} - -func TestIAMWorkflowSuite(t *testing.T) { - suite.Run(t, new(IAMWorkflowTestSuite)) -} - -func (s *IAMWorkflowTestSuite) Test_SpinUpIAM_InvalidInput() { - s.env.ExecuteWorkflow(SpinUpIAMWorkflow, SpinUpEKSIAMInput{}) - - s.True(s.env.IsWorkflowCompleted()) - err := s.env.GetWorkflowError() - s.Error(err) - var appErr *temporal.ApplicationError - s.True(errors.As(err, &appErr)) - s.Equal("InvalidInput", appErr.Type()) -} - -func (s *IAMWorkflowTestSuite) Test_SpinUpIAM_Success() { - aws := &activities.AWSActivities{} - s.env.OnActivity(aws.CreateIAMRole, mock.Anything, mock.Anything). - Return("arn:aws:iam::123:role/cluster-role", nil).Once() - s.env.OnActivity(aws.CreateIAMRole, mock.Anything, mock.Anything). - Return("arn:aws:iam::123:role/node-role", nil).Once() - - s.env.ExecuteWorkflow(SpinUpIAMWorkflow, SpinUpEKSIAMInput{ - ClusterName: "my-cluster", - Environment: "prod", - Team: "platform", +var _ = Describe("SpinUpIAMWorkflow", func() { + var ( + testSuite testsuite.WorkflowTestSuite + env *testsuite.TestWorkflowEnvironment + ) + + BeforeEach(func() { + env = testSuite.NewTestWorkflowEnvironment() + }) + + AfterEach(func() { + env.AssertExpectations(GinkgoT()) + }) + + Context("with invalid input", func() { + It("returns a non-retryable InvalidInput error", func() { + env.ExecuteWorkflow(SpinUpIAMWorkflow, SpinUpEKSIAMInput{}) + + Expect(env.IsWorkflowCompleted()).To(BeTrue()) + err := env.GetWorkflowError() + Expect(err).To(HaveOccurred()) + var appErr *temporal.ApplicationError + Expect(errors.As(err, &appErr)).To(BeTrue()) + Expect(appErr.Type()).To(Equal("InvalidInput")) + }) }) - s.True(s.env.IsWorkflowCompleted()) - s.NoError(s.env.GetWorkflowError()) - - var out SpinUpEKSIAMOutput - s.NoError(s.env.GetWorkflowResult(&out)) - s.Equal("arn:aws:iam::123:role/cluster-role", out.ClusterRoleARN) - s.Equal("my-cluster-eks-cluster-role", out.ClusterRoleName) - s.Equal("arn:aws:iam::123:role/node-role", out.NodeRoleARN) - s.Equal("my-cluster-eks-node-role", out.NodeRoleName) -} - -// When node role creation fails, the saga must compensate the already-created cluster role. -func (s *IAMWorkflowTestSuite) Test_SpinUpIAM_NodeRoleFailure_CompensatesClusterRole() { - aws := &activities.AWSActivities{} - s.env.OnActivity(aws.CreateIAMRole, mock.Anything, mock.Anything). - Return("arn:aws:iam::123:role/cluster-role", nil).Once() - s.env.OnActivity(aws.CreateIAMRole, mock.Anything, mock.Anything). - Return("", errors.New("IAM quota exceeded")) - s.env.OnActivity(aws.DeleteIAMRole, mock.Anything, mock.Anything). - Return(nil).Once() - - s.env.ExecuteWorkflow(SpinUpIAMWorkflow, SpinUpEKSIAMInput{ - ClusterName: "my-cluster", - Environment: "prod", - Team: "platform", + Context("with valid input", func() { + It("creates both IAM roles and returns their ARNs and names", func() { + aws := &activities.AWSActivities{} + env.OnActivity(aws.CreateIAMRole, mock.Anything, mock.Anything). + Return("arn:aws:iam::123:role/cluster-role", nil).Once() + env.OnActivity(aws.CreateIAMRole, mock.Anything, mock.Anything). + Return("arn:aws:iam::123:role/node-role", nil).Once() + + env.ExecuteWorkflow(SpinUpIAMWorkflow, SpinUpEKSIAMInput{ + ClusterName: "my-cluster", + Environment: "prod", + Team: "platform", + }) + + Expect(env.IsWorkflowCompleted()).To(BeTrue()) + Expect(env.GetWorkflowError()).NotTo(HaveOccurred()) + + var out SpinUpEKSIAMOutput + Expect(env.GetWorkflowResult(&out)).NotTo(HaveOccurred()) + Expect(out.ClusterRoleARN).To(Equal("arn:aws:iam::123:role/cluster-role")) + Expect(out.ClusterRoleName).To(Equal("my-cluster-eks-cluster-role")) + Expect(out.NodeRoleARN).To(Equal("arn:aws:iam::123:role/node-role")) + Expect(out.NodeRoleName).To(Equal("my-cluster-eks-node-role")) + }) }) - s.True(s.env.IsWorkflowCompleted()) - s.Error(s.env.GetWorkflowError()) -} + Context("when node role creation fails", func() { + It("compensates by deleting the cluster role", func() { + aws := &activities.AWSActivities{} + env.OnActivity(aws.CreateIAMRole, mock.Anything, mock.Anything). + Return("arn:aws:iam::123:role/cluster-role", nil).Once() + env.OnActivity(aws.CreateIAMRole, mock.Anything, mock.Anything). + Return("", errors.New("IAM quota exceeded")) + env.OnActivity(aws.DeleteIAMRole, mock.Anything, mock.Anything). + Return(nil).Once() + + env.ExecuteWorkflow(SpinUpIAMWorkflow, SpinUpEKSIAMInput{ + ClusterName: "my-cluster", + Environment: "prod", + Team: "platform", + }) + + Expect(env.IsWorkflowCompleted()).To(BeTrue()) + Expect(env.GetWorkflowError()).To(HaveOccurred()) + }) + }) +}) -func (s *IAMWorkflowTestSuite) Test_SpinDownIAM_InvalidInput() { - s.env.ExecuteWorkflow(SpinDownIAMWorkflow, SpinDownEKSIAMInput{}) +var _ = Describe("SpinDownIAMWorkflow", func() { + var ( + testSuite testsuite.WorkflowTestSuite + env *testsuite.TestWorkflowEnvironment + ) - s.True(s.env.IsWorkflowCompleted()) - err := s.env.GetWorkflowError() - s.Error(err) - var appErr *temporal.ApplicationError - s.True(errors.As(err, &appErr)) - s.Equal("InvalidInput", appErr.Type()) -} + BeforeEach(func() { + env = testSuite.NewTestWorkflowEnvironment() + }) -func (s *IAMWorkflowTestSuite) Test_SpinDownIAM_Success() { - aws := &activities.AWSActivities{} - s.env.OnActivity(aws.DeleteIAMRole, mock.Anything, mock.Anything).Return(nil).Times(2) + AfterEach(func() { + env.AssertExpectations(GinkgoT()) + }) - s.env.ExecuteWorkflow(SpinDownIAMWorkflow, SpinDownEKSIAMInput{ - ClusterRoleName: "my-cluster-eks-cluster-role", - NodeRoleName: "my-cluster-eks-node-role", + Context("with invalid input", func() { + It("returns a non-retryable InvalidInput error", func() { + env.ExecuteWorkflow(SpinDownIAMWorkflow, SpinDownEKSIAMInput{}) + + Expect(env.IsWorkflowCompleted()).To(BeTrue()) + err := env.GetWorkflowError() + Expect(err).To(HaveOccurred()) + var appErr *temporal.ApplicationError + Expect(errors.As(err, &appErr)).To(BeTrue()) + Expect(appErr.Type()).To(Equal("InvalidInput")) + }) }) - s.True(s.env.IsWorkflowCompleted()) - s.NoError(s.env.GetWorkflowError()) -} + Context("with valid input", func() { + It("deletes both IAM roles", func() { + aws := &activities.AWSActivities{} + env.OnActivity(aws.DeleteIAMRole, mock.Anything, mock.Anything).Return(nil).Times(2) + + env.ExecuteWorkflow(SpinDownIAMWorkflow, SpinDownEKSIAMInput{ + ClusterRoleName: "my-cluster-eks-cluster-role", + NodeRoleName: "my-cluster-eks-node-role", + }) + + Expect(env.IsWorkflowCompleted()).To(BeTrue()) + Expect(env.GetWorkflowError()).NotTo(HaveOccurred()) + }) + }) +}) diff --git a/internal/workflows/infrastructure_test.go b/internal/workflows/infrastructure_test.go index f919160..21bf13c 100644 --- a/internal/workflows/infrastructure_test.go +++ b/internal/workflows/infrastructure_test.go @@ -2,54 +2,29 @@ package workflows import ( "errors" - "testing" - "github.com/Amertz08/gitops-example/internal/activities" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/suite" + + "github.com/Amertz08/gitops-example/internal/activities" "go.temporal.io/sdk/temporal" "go.temporal.io/sdk/testsuite" ) -type InfraWorkflowTestSuite struct { - suite.Suite - testsuite.WorkflowTestSuite - env *testsuite.TestWorkflowEnvironment -} - -func (s *InfraWorkflowTestSuite) SetupTest() { - s.env = s.NewTestWorkflowEnvironment() - s.env.RegisterWorkflow(SpinUpNetworkWorkflow) - s.env.RegisterWorkflow(SpinUpIAMWorkflow) - s.env.RegisterWorkflow(SpinUpEKSWorkflow) - s.env.RegisterWorkflow(SpinDownEKSWorkflow) - s.env.RegisterWorkflow(SpinDownNetworkWorkflow) - s.env.RegisterWorkflow(SpinDownIAMWorkflow) -} - -func (s *InfraWorkflowTestSuite) AfterTest(_, _ string) { - s.env.AssertExpectations(s.T()) -} - -func TestInfraWorkflowSuite(t *testing.T) { - suite.Run(t, new(InfraWorkflowTestSuite)) -} - -// mockHappyPathNetworkActivities mocks all network activities to return success. -func (s *InfraWorkflowTestSuite) mockHappyPathNetworkActivities() { +func mockHappyPathNetworkActivities(env *testsuite.TestWorkflowEnvironment) { aws := &activities.AWSActivities{} - s.env.OnActivity(aws.CreateVPC, mock.Anything, mock.Anything).Return("vpc-123", nil) - s.env.OnActivity(aws.CreateSubnets, mock.Anything, mock.Anything). + env.OnActivity(aws.CreateVPC, mock.Anything, mock.Anything).Return("vpc-123", nil) + env.OnActivity(aws.CreateSubnets, mock.Anything, mock.Anything). Return([]string{"subnet-a", "subnet-b"}, nil) - s.env.OnActivity(aws.CreateInternetGateway, mock.Anything, mock.Anything).Return("igw-123", nil) - s.env.OnActivity(aws.ConfigureRouteTables, mock.Anything, mock.Anything).Return(nil) + env.OnActivity(aws.CreateInternetGateway, mock.Anything, mock.Anything).Return("igw-123", nil) + env.OnActivity(aws.ConfigureRouteTables, mock.Anything, mock.Anything).Return(nil) } -// mockHappyPathEKSActivities mocks all EKS activities to return success. -func (s *InfraWorkflowTestSuite) mockHappyPathEKSActivities() { +func mockHappyPathEKSActivities(env *testsuite.TestWorkflowEnvironment) { aws := &activities.AWSActivities{} - s.env.OnActivity(aws.CreateEKSCluster, mock.Anything, mock.Anything).Return(nil) - s.env.OnActivity(aws.CreateNodeGroup, mock.Anything, mock.Anything).Return(nil) + env.OnActivity(aws.CreateEKSCluster, mock.Anything, mock.Anything).Return(nil) + env.OnActivity(aws.CreateNodeGroup, mock.Anything, mock.Anything).Return(nil) } func validSpinUpInput() SpinUpInput { @@ -63,98 +38,144 @@ func validSpinUpInput() SpinUpInput { } } -func (s *InfraWorkflowTestSuite) Test_SpinUp_InvalidInput() { - s.env.ExecuteWorkflow(SpinUpWorkflow, SpinUpInput{}) - - s.True(s.env.IsWorkflowCompleted()) - err := s.env.GetWorkflowError() - s.Error(err) - var appErr *temporal.ApplicationError - s.True(errors.As(err, &appErr)) - s.Equal("InvalidInput", appErr.Type()) -} - -// When no role ARNs are supplied, SpinUpWorkflow must run SpinUpIAMWorkflow concurrently with -// SpinUpNetworkWorkflow, then use the resulting ARNs for SpinUpEKSWorkflow. -func (s *InfraWorkflowTestSuite) Test_SpinUp_Success_WithoutPreSuppliedARNs() { - aws := &activities.AWSActivities{} - s.env.OnActivity(aws.CreateIAMRole, mock.Anything, mock.Anything). - Return("arn:aws:iam::123:role/cluster-role", nil).Once() - s.env.OnActivity(aws.CreateIAMRole, mock.Anything, mock.Anything). - Return("arn:aws:iam::123:role/node-role", nil).Once() - s.mockHappyPathNetworkActivities() - s.mockHappyPathEKSActivities() - - s.env.ExecuteWorkflow(SpinUpWorkflow, validSpinUpInput()) +var _ = Describe("SpinUpWorkflow", func() { + var ( + testSuite testsuite.WorkflowTestSuite + env *testsuite.TestWorkflowEnvironment + ) + + BeforeEach(func() { + env = testSuite.NewTestWorkflowEnvironment() + env.RegisterWorkflow(SpinUpNetworkWorkflow) + env.RegisterWorkflow(SpinUpIAMWorkflow) + env.RegisterWorkflow(SpinUpEKSWorkflow) + }) - s.True(s.env.IsWorkflowCompleted()) - s.NoError(s.env.GetWorkflowError()) -} + AfterEach(func() { + env.AssertExpectations(GinkgoT()) + }) -// When role ARNs are pre-supplied, SpinUpWorkflow must skip IAM creation entirely. -func (s *InfraWorkflowTestSuite) Test_SpinUp_Success_WithPreSuppliedARNs() { - s.mockHappyPathNetworkActivities() - s.mockHappyPathEKSActivities() + Context("with invalid input", func() { + It("returns a non-retryable InvalidInput error", func() { + env.ExecuteWorkflow(SpinUpWorkflow, SpinUpInput{}) + + Expect(env.IsWorkflowCompleted()).To(BeTrue()) + err := env.GetWorkflowError() + Expect(err).To(HaveOccurred()) + var appErr *temporal.ApplicationError + Expect(errors.As(err, &appErr)).To(BeTrue()) + Expect(appErr.Type()).To(Equal("InvalidInput")) + }) + }) - input := validSpinUpInput() - input.ClusterRoleARN = "arn:aws:iam::123:role/existing-cluster-role" - input.NodeRoleARN = "arn:aws:iam::123:role/existing-node-role" - s.env.ExecuteWorkflow(SpinUpWorkflow, input) + Context("without pre-supplied IAM role ARNs", func() { + It("runs SpinUpIAMWorkflow concurrently with SpinUpNetworkWorkflow then SpinUpEKSWorkflow", + func() { + aws := &activities.AWSActivities{} + env.OnActivity(aws.CreateIAMRole, mock.Anything, mock.Anything). + Return("arn:aws:iam::123:role/cluster-role", nil).Once() + env.OnActivity(aws.CreateIAMRole, mock.Anything, mock.Anything). + Return("arn:aws:iam::123:role/node-role", nil).Once() + mockHappyPathNetworkActivities(env) + mockHappyPathEKSActivities(env) + + env.ExecuteWorkflow(SpinUpWorkflow, validSpinUpInput()) + + Expect(env.IsWorkflowCompleted()).To(BeTrue()) + Expect(env.GetWorkflowError()).NotTo(HaveOccurred()) + }) + }) - s.True(s.env.IsWorkflowCompleted()) - s.NoError(s.env.GetWorkflowError()) -} + Context("with pre-supplied IAM role ARNs", func() { + It("skips SpinUpIAMWorkflow and uses the provided ARNs directly", func() { + mockHappyPathNetworkActivities(env) + mockHappyPathEKSActivities(env) -func (s *InfraWorkflowTestSuite) Test_SpinDown_InvalidInput() { - s.env.ExecuteWorkflow(SpinDownWorkflow, SpinDownInput{}) + input := validSpinUpInput() + input.ClusterRoleARN = "arn:aws:iam::123:role/existing-cluster-role" + input.NodeRoleARN = "arn:aws:iam::123:role/existing-node-role" + env.ExecuteWorkflow(SpinUpWorkflow, input) - s.True(s.env.IsWorkflowCompleted()) - err := s.env.GetWorkflowError() - s.Error(err) - var appErr *temporal.ApplicationError - s.True(errors.As(err, &appErr)) - s.Equal("InvalidInput", appErr.Type()) -} + Expect(env.IsWorkflowCompleted()).To(BeTrue()) + Expect(env.GetWorkflowError()).NotTo(HaveOccurred()) + }) + }) +}) + +var _ = Describe("SpinDownWorkflow", func() { + var ( + testSuite testsuite.WorkflowTestSuite + env *testsuite.TestWorkflowEnvironment + ) + + BeforeEach(func() { + env = testSuite.NewTestWorkflowEnvironment() + env.RegisterWorkflow(SpinDownEKSWorkflow) + env.RegisterWorkflow(SpinDownNetworkWorkflow) + env.RegisterWorkflow(SpinDownIAMWorkflow) + }) -// SpinDownWorkflow tears down EKS, network, and (when role names are provided) IAM. -func (s *InfraWorkflowTestSuite) Test_SpinDown_Success_WithRoles() { - aws := &activities.AWSActivities{} - s.env.OnActivity(aws.DeleteNodeGroup, mock.Anything, mock.Anything).Return(nil) - s.env.OnActivity(aws.DeleteEKSCluster, mock.Anything, mock.Anything).Return(nil) - s.env.OnActivity(aws.DeleteSubnets, mock.Anything, mock.Anything).Return(nil) - s.env.OnActivity(aws.DeleteRouteTables, mock.Anything, mock.Anything).Return(nil) - s.env.OnActivity(aws.DetachDeleteInternetGateway, mock.Anything, mock.Anything).Return(nil) - s.env.OnActivity(aws.DeleteVPC, mock.Anything, mock.Anything).Return(nil) - s.env.OnActivity(aws.DeleteIAMRole, mock.Anything, mock.Anything).Return(nil).Times(2) - - s.env.ExecuteWorkflow(SpinDownWorkflow, SpinDownInput{ - Region: "us-east-1", - ClusterName: "my-cluster", - VpcID: "vpc-123", - ClusterRoleName: "my-cluster-eks-cluster-role", - NodeRoleName: "my-cluster-eks-node-role", + AfterEach(func() { + env.AssertExpectations(GinkgoT()) }) - s.True(s.env.IsWorkflowCompleted()) - s.NoError(s.env.GetWorkflowError()) -} + Context("with invalid input", func() { + It("returns a non-retryable InvalidInput error", func() { + env.ExecuteWorkflow(SpinDownWorkflow, SpinDownInput{}) + + Expect(env.IsWorkflowCompleted()).To(BeTrue()) + err := env.GetWorkflowError() + Expect(err).To(HaveOccurred()) + var appErr *temporal.ApplicationError + Expect(errors.As(err, &appErr)).To(BeTrue()) + Expect(appErr.Type()).To(Equal("InvalidInput")) + }) + }) -// When role names are empty, SpinDownWorkflow must skip the IAM teardown child workflow. -func (s *InfraWorkflowTestSuite) Test_SpinDown_Success_WithoutRoles() { - aws := &activities.AWSActivities{} - s.env.OnActivity(aws.DeleteNodeGroup, mock.Anything, mock.Anything).Return(nil) - s.env.OnActivity(aws.DeleteEKSCluster, mock.Anything, mock.Anything).Return(nil) - s.env.OnActivity(aws.DeleteSubnets, mock.Anything, mock.Anything).Return(nil) - s.env.OnActivity(aws.DeleteRouteTables, mock.Anything, mock.Anything).Return(nil) - s.env.OnActivity(aws.DetachDeleteInternetGateway, mock.Anything, mock.Anything).Return(nil) - s.env.OnActivity(aws.DeleteVPC, mock.Anything, mock.Anything).Return(nil) - - s.env.ExecuteWorkflow(SpinDownWorkflow, SpinDownInput{ - Region: "us-east-1", - ClusterName: "my-cluster", - VpcID: "vpc-123", + Context("with role names provided", func() { + It("tears down EKS, network, and IAM roles", func() { + aws := &activities.AWSActivities{} + env.OnActivity(aws.DeleteNodeGroup, mock.Anything, mock.Anything).Return(nil) + env.OnActivity(aws.DeleteEKSCluster, mock.Anything, mock.Anything).Return(nil) + env.OnActivity(aws.DeleteSubnets, mock.Anything, mock.Anything).Return(nil) + env.OnActivity(aws.DeleteRouteTables, mock.Anything, mock.Anything).Return(nil) + env.OnActivity(aws.DetachDeleteInternetGateway, mock.Anything, mock.Anything). + Return(nil) + env.OnActivity(aws.DeleteVPC, mock.Anything, mock.Anything).Return(nil) + env.OnActivity(aws.DeleteIAMRole, mock.Anything, mock.Anything).Return(nil).Times(2) + + env.ExecuteWorkflow(SpinDownWorkflow, SpinDownInput{ + Region: "us-east-1", + ClusterName: "my-cluster", + VpcID: "vpc-123", + ClusterRoleName: "my-cluster-eks-cluster-role", + NodeRoleName: "my-cluster-eks-node-role", + }) + + Expect(env.IsWorkflowCompleted()).To(BeTrue()) + Expect(env.GetWorkflowError()).NotTo(HaveOccurred()) + }) }) - s.True(s.env.IsWorkflowCompleted()) - s.NoError(s.env.GetWorkflowError()) -} + Context("without role names", func() { + It("tears down EKS and network, skipping the IAM child workflow", func() { + aws := &activities.AWSActivities{} + env.OnActivity(aws.DeleteNodeGroup, mock.Anything, mock.Anything).Return(nil) + env.OnActivity(aws.DeleteEKSCluster, mock.Anything, mock.Anything).Return(nil) + env.OnActivity(aws.DeleteSubnets, mock.Anything, mock.Anything).Return(nil) + env.OnActivity(aws.DeleteRouteTables, mock.Anything, mock.Anything).Return(nil) + env.OnActivity(aws.DetachDeleteInternetGateway, mock.Anything, mock.Anything). + Return(nil) + env.OnActivity(aws.DeleteVPC, mock.Anything, mock.Anything).Return(nil) + + env.ExecuteWorkflow(SpinDownWorkflow, SpinDownInput{ + Region: "us-east-1", + ClusterName: "my-cluster", + VpcID: "vpc-123", + }) + + Expect(env.IsWorkflowCompleted()).To(BeTrue()) + Expect(env.GetWorkflowError()).NotTo(HaveOccurred()) + }) + }) +}) diff --git a/internal/workflows/suite_test.go b/internal/workflows/suite_test.go new file mode 100644 index 0000000..0e77502 --- /dev/null +++ b/internal/workflows/suite_test.go @@ -0,0 +1,13 @@ +package workflows + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestWorkflows(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Workflows Suite") +} diff --git a/internal/workflows/vpc_test.go b/internal/workflows/vpc_test.go index 160538e..4648b7c 100644 --- a/internal/workflows/vpc_test.go +++ b/internal/workflows/vpc_test.go @@ -2,124 +2,149 @@ package workflows import ( "errors" - "testing" - "github.com/Amertz08/gitops-example/internal/activities" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/suite" + + "github.com/Amertz08/gitops-example/internal/activities" "go.temporal.io/sdk/temporal" "go.temporal.io/sdk/testsuite" ) -type VPCWorkflowTestSuite struct { - suite.Suite - testsuite.WorkflowTestSuite - env *testsuite.TestWorkflowEnvironment -} - -func (s *VPCWorkflowTestSuite) SetupTest() { - s.env = s.NewTestWorkflowEnvironment() -} - -func (s *VPCWorkflowTestSuite) AfterTest(_, _ string) { - s.env.AssertExpectations(s.T()) -} - -func TestVPCWorkflowSuite(t *testing.T) { - suite.Run(t, new(VPCWorkflowTestSuite)) -} - -func (s *VPCWorkflowTestSuite) Test_SpinUpNetwork_InvalidInput() { - s.env.ExecuteWorkflow(SpinUpNetworkWorkflow, SpinUpNetworkInput{}) - - s.True(s.env.IsWorkflowCompleted()) - err := s.env.GetWorkflowError() - s.Error(err) - var appErr *temporal.ApplicationError - s.True(errors.As(err, &appErr)) - s.Equal("InvalidInput", appErr.Type()) -} - -func (s *VPCWorkflowTestSuite) Test_SpinUpNetwork_Success() { - aws := &activities.AWSActivities{} - s.env.OnActivity(aws.CreateVPC, mock.Anything, mock.Anything).Return("vpc-123", nil) - s.env.OnActivity(aws.CreateSubnets, mock.Anything, mock.Anything). - Return([]string{"subnet-a", "subnet-b"}, nil) - s.env.OnActivity(aws.CreateInternetGateway, mock.Anything, mock.Anything).Return("igw-123", nil) - s.env.OnActivity(aws.ConfigureRouteTables, mock.Anything, mock.Anything).Return(nil) - - s.env.ExecuteWorkflow(SpinUpNetworkWorkflow, SpinUpNetworkInput{ - Region: "us-east-1", - Environment: "prod", - Team: "platform", +var _ = Describe("SpinUpNetworkWorkflow", func() { + var ( + testSuite testsuite.WorkflowTestSuite + env *testsuite.TestWorkflowEnvironment + ) + + BeforeEach(func() { + env = testSuite.NewTestWorkflowEnvironment() + }) + + AfterEach(func() { + env.AssertExpectations(GinkgoT()) + }) + + Context("with invalid input", func() { + It("returns a non-retryable InvalidInput error", func() { + env.ExecuteWorkflow(SpinUpNetworkWorkflow, SpinUpNetworkInput{}) + + Expect(env.IsWorkflowCompleted()).To(BeTrue()) + err := env.GetWorkflowError() + Expect(err).To(HaveOccurred()) + var appErr *temporal.ApplicationError + Expect(errors.As(err, &appErr)).To(BeTrue()) + Expect(appErr.Type()).To(Equal("InvalidInput")) + }) }) - s.True(s.env.IsWorkflowCompleted()) - s.NoError(s.env.GetWorkflowError()) - - var out SpinUpNetworkOutput - s.NoError(s.env.GetWorkflowResult(&out)) - s.Equal("vpc-123", out.VpcID) - s.Equal([]string{"subnet-a", "subnet-b"}, out.SubnetIDs) -} - -// CreateVPC fails before any compensation is registered, so no deletes should occur. -func (s *VPCWorkflowTestSuite) Test_SpinUpNetwork_CreateVPCFailure() { - aws := &activities.AWSActivities{} - s.env.OnActivity(aws.CreateVPC, mock.Anything, mock.Anything). - Return("", errors.New("VPC limit reached")) - - s.env.ExecuteWorkflow(SpinUpNetworkWorkflow, SpinUpNetworkInput{ - Region: "us-east-1", - Environment: "prod", - Team: "platform", + Context("with valid input", func() { + It("creates VPC, subnets, IGW, and route tables, returning VPC ID and subnet IDs", func() { + aws := &activities.AWSActivities{} + env.OnActivity(aws.CreateVPC, mock.Anything, mock.Anything).Return("vpc-123", nil) + env.OnActivity(aws.CreateSubnets, mock.Anything, mock.Anything). + Return([]string{"subnet-a", "subnet-b"}, nil) + env.OnActivity(aws.CreateInternetGateway, mock.Anything, mock.Anything). + Return("igw-123", nil) + env.OnActivity(aws.ConfigureRouteTables, mock.Anything, mock.Anything).Return(nil) + + env.ExecuteWorkflow(SpinUpNetworkWorkflow, SpinUpNetworkInput{ + Region: "us-east-1", + Environment: "prod", + Team: "platform", + }) + + Expect(env.IsWorkflowCompleted()).To(BeTrue()) + Expect(env.GetWorkflowError()).NotTo(HaveOccurred()) + + var out SpinUpNetworkOutput + Expect(env.GetWorkflowResult(&out)).NotTo(HaveOccurred()) + Expect(out.VpcID).To(Equal("vpc-123")) + Expect(out.SubnetIDs).To(Equal([]string{"subnet-a", "subnet-b"})) + }) }) - s.True(s.env.IsWorkflowCompleted()) - s.Error(s.env.GetWorkflowError()) -} - -// CreateSubnets fails after VPC is created; saga must compensate by deleting the VPC. -func (s *VPCWorkflowTestSuite) Test_SpinUpNetwork_SubnetsFailure_CompensatesVPC() { - aws := &activities.AWSActivities{} - s.env.OnActivity(aws.CreateVPC, mock.Anything, mock.Anything).Return("vpc-123", nil) - s.env.OnActivity(aws.CreateSubnets, mock.Anything, mock.Anything). - Return([]string(nil), errors.New("subnet CIDR conflict")) - s.env.OnActivity(aws.DeleteVPC, mock.Anything, mock.Anything).Return(nil).Once() - - s.env.ExecuteWorkflow(SpinUpNetworkWorkflow, SpinUpNetworkInput{ - Region: "us-east-1", - Environment: "prod", - Team: "platform", + Context("when CreateVPC fails", func() { + It("returns an error without triggering any compensation", func() { + aws := &activities.AWSActivities{} + env.OnActivity(aws.CreateVPC, mock.Anything, mock.Anything). + Return("", errors.New("VPC limit reached")) + + env.ExecuteWorkflow(SpinUpNetworkWorkflow, SpinUpNetworkInput{ + Region: "us-east-1", + Environment: "prod", + Team: "platform", + }) + + Expect(env.IsWorkflowCompleted()).To(BeTrue()) + Expect(env.GetWorkflowError()).To(HaveOccurred()) + }) }) - s.True(s.env.IsWorkflowCompleted()) - s.Error(s.env.GetWorkflowError()) -} - -func (s *VPCWorkflowTestSuite) Test_SpinDownNetwork_InvalidInput() { - s.env.ExecuteWorkflow(SpinDownNetworkWorkflow, SpinDownNetworkInput{}) - - s.True(s.env.IsWorkflowCompleted()) - err := s.env.GetWorkflowError() - s.Error(err) - var appErr *temporal.ApplicationError - s.True(errors.As(err, &appErr)) - s.Equal("InvalidInput", appErr.Type()) -} - -func (s *VPCWorkflowTestSuite) Test_SpinDownNetwork_Success() { - aws := &activities.AWSActivities{} - s.env.OnActivity(aws.DeleteSubnets, mock.Anything, mock.Anything).Return(nil) - s.env.OnActivity(aws.DeleteRouteTables, mock.Anything, mock.Anything).Return(nil) - s.env.OnActivity(aws.DetachDeleteInternetGateway, mock.Anything, mock.Anything).Return(nil) - s.env.OnActivity(aws.DeleteVPC, mock.Anything, mock.Anything).Return(nil) - - s.env.ExecuteWorkflow(SpinDownNetworkWorkflow, SpinDownNetworkInput{ - Region: "us-east-1", - VpcID: "vpc-123", + Context("when CreateSubnets fails after VPC is created", func() { + It("compensates by deleting the VPC", func() { + aws := &activities.AWSActivities{} + env.OnActivity(aws.CreateVPC, mock.Anything, mock.Anything).Return("vpc-123", nil) + env.OnActivity(aws.CreateSubnets, mock.Anything, mock.Anything). + Return([]string(nil), errors.New("subnet CIDR conflict")) + env.OnActivity(aws.DeleteVPC, mock.Anything, mock.Anything).Return(nil).Once() + + env.ExecuteWorkflow(SpinUpNetworkWorkflow, SpinUpNetworkInput{ + Region: "us-east-1", + Environment: "prod", + Team: "platform", + }) + + Expect(env.IsWorkflowCompleted()).To(BeTrue()) + Expect(env.GetWorkflowError()).To(HaveOccurred()) + }) }) +}) - s.True(s.env.IsWorkflowCompleted()) - s.NoError(s.env.GetWorkflowError()) -} +var _ = Describe("SpinDownNetworkWorkflow", func() { + var ( + testSuite testsuite.WorkflowTestSuite + env *testsuite.TestWorkflowEnvironment + ) + + BeforeEach(func() { + env = testSuite.NewTestWorkflowEnvironment() + }) + + AfterEach(func() { + env.AssertExpectations(GinkgoT()) + }) + + Context("with invalid input", func() { + It("returns a non-retryable InvalidInput error", func() { + env.ExecuteWorkflow(SpinDownNetworkWorkflow, SpinDownNetworkInput{}) + + Expect(env.IsWorkflowCompleted()).To(BeTrue()) + err := env.GetWorkflowError() + Expect(err).To(HaveOccurred()) + var appErr *temporal.ApplicationError + Expect(errors.As(err, &appErr)).To(BeTrue()) + Expect(appErr.Type()).To(Equal("InvalidInput")) + }) + }) + + Context("with valid input", func() { + It("deletes subnets, route tables, IGW, and VPC in order", func() { + aws := &activities.AWSActivities{} + env.OnActivity(aws.DeleteSubnets, mock.Anything, mock.Anything).Return(nil) + env.OnActivity(aws.DeleteRouteTables, mock.Anything, mock.Anything).Return(nil) + env.OnActivity(aws.DetachDeleteInternetGateway, mock.Anything, mock.Anything). + Return(nil) + env.OnActivity(aws.DeleteVPC, mock.Anything, mock.Anything).Return(nil) + + env.ExecuteWorkflow(SpinDownNetworkWorkflow, SpinDownNetworkInput{ + Region: "us-east-1", + VpcID: "vpc-123", + }) + + Expect(env.IsWorkflowCompleted()).To(BeTrue()) + Expect(env.GetWorkflowError()).NotTo(HaveOccurred()) + }) + }) +})