From 4d8a005a1e32390b28a08c9e3eaae414ef21202e Mon Sep 17 00:00:00 2001 From: Steven Kessler Date: Thu, 12 Sep 2024 10:56:26 -0400 Subject: [PATCH] feat(tests): added some simple test cases --- .github/workflows/build.yaml | 2 +- .github/workflows/code-quality.yaml | 7 ++- go.mod | 6 +++ go.sum | 12 +++++ main.go | 42 ++++++++++------ main_test.go | 78 +++++++++++++++++++++++++++++ 6 files changed, 129 insertions(+), 18 deletions(-) create mode 100644 main_test.go diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 5d39cfe..7bdf8af 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -60,7 +60,7 @@ jobs: - name: Build id: docker_build - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: platforms: linux/amd64,linux/arm64 push: false diff --git a/.github/workflows/code-quality.yaml b/.github/workflows/code-quality.yaml index ce0d4ca..2d2e550 100644 --- a/.github/workflows/code-quality.yaml +++ b/.github/workflows/code-quality.yaml @@ -23,10 +23,13 @@ jobs: cache: false - name: Lint - uses: golangci/golangci-lint-action@v4 + uses: golangci/golangci-lint-action@v6 with: version: latest + - name: Test + run: go test + lint-chart: name: Lint chart @@ -39,7 +42,7 @@ jobs: fetch-depth: 0 - name: Set up Helm - uses: azure/setup-helm@v3 + uses: azure/setup-helm@v4 with: version: v3.12.1 diff --git a/go.mod b/go.mod index 740b50c..9947834 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/sqs v1.34.8 github.com/prometheus/client_golang v1.20.3 github.com/rs/zerolog v1.33.0 + github.com/stretchr/testify v1.9.0 ) require ( @@ -24,13 +25,18 @@ require ( github.com/aws/smithy-go v1.20.4 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/klauspost/compress v1.17.9 // indirect + github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.59.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/stretchr/objx v0.5.2 // indirect golang.org/x/sys v0.25.0 // indirect google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 0a75c51..9d00317 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,7 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -38,6 +39,10 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -59,9 +64,13 @@ github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJ github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -71,5 +80,8 @@ golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 8f5e2a8..032cc38 100644 --- a/main.go +++ b/main.go @@ -19,11 +19,16 @@ import ( ) // Default to checking queues every 30 seconds -const defaultMonitorInterval = 30 +const defaultMonitorInterval = 30 * time.Second -var svc = getSqsClient() +// SQSClientInterface defines the interface for SQS operations we use +type SQSClientInterface interface { + GetQueueAttributes(ctx context.Context, params *sqs.GetQueueAttributesInput, optFns ...func(*sqs.Options)) (*sqs.GetQueueAttributesOutput, error) +} + +var svc SQSClientInterface -var monitorInterval = getMonitorInterval() +var monitorInterval time.Duration var labelNames = []string{"queue"} @@ -48,7 +53,7 @@ type queueResult struct { QueueResults *sqs.GetQueueAttributesOutput } -func getSqsClient() *sqs.Client { +func getSqsClient() SQSClientInterface { cfg, err := config.LoadDefaultConfig(context.TODO()) if err != nil { log.Error().Str("errorMessage", err.Error()).Msg("error loading AWS config") @@ -62,15 +67,15 @@ func getMonitorInterval() time.Duration { monitorIntervalStr, varSet := os.LookupEnv("SQS_MONITOR_INTERVAL_SECONDS") if !varSet || monitorIntervalStr == "" { log.Warn().Msg(fmt.Sprintf("Monitor interval not set, defaulting to %v", defaultMonitorInterval)) - return time.Duration(defaultMonitorInterval) + return defaultMonitorInterval } monitorInterval, err := strconv.Atoi(monitorIntervalStr) if err != nil { - log.Error().Str("errorMessage", err.Error()).Msg("bad value for SQS_MONITOR_INTERVAL") - os.Exit(1) + log.Warn().Str("errorMessage", err.Error()).Msg("Invalid value for SQS_MONITOR_INTERVAL, using default") + return defaultMonitorInterval } - return time.Duration(monitorInterval) + return time.Duration(monitorInterval) * time.Second } func monitorQueue(queueURL string, c chan queueResult) { @@ -89,7 +94,8 @@ func monitorQueue(queueURL string, c chan queueResult) { resp, err := svc.GetQueueAttributes(context.TODO(), params) if err != nil { log.Error().Str("errorMessage", err.Error()).Msg("error checking queue") - os.Exit(1) + c <- queueResult{queueURL, queueName, nil} // Send a result with nil QueueResults to indicate error + return } c <- queueResult{queueURL, queueName, resp} @@ -104,6 +110,9 @@ func monitorQueues(queueUrls []string) { for i := 0; i < len(queueUrls); i++ { queueResult := <-c + if queueResult.QueueResults == nil { + continue // Skip this queue if there was an error + } for attrib := range queueResult.QueueResults.Attributes { prop := queueResult.QueueResults.Attributes[attrib] nMessages, _ := strconv.ParseFloat(prop, 64) @@ -115,18 +124,17 @@ func monitorQueues(queueUrls []string) { case "ApproximateNumberOfMessagesNotVisible": promMessagesNotVisible.WithLabelValues(queueResult.QueueName).Set(nMessages) default: - log.Error().Msg(fmt.Sprintf("unknown attribute %v", attrib)) - os.Exit(1) + log.Warn().Msg(fmt.Sprintf("unknown attribute %v", attrib)) } } } - time.Sleep(monitorInterval * time.Second) + time.Sleep(monitorInterval) } } // Return an empty 200 response for healthchecks -func healthcheck(http.ResponseWriter, *http.Request) { +func healthcheck(w http.ResponseWriter, r *http.Request) { } func main() { @@ -142,13 +150,17 @@ func main() { port = "8080" } - log.Info().Int("interval", int(monitorInterval)).Strs("queueUrls", queueUrls).Str("port", port).Msg("Starting queue monitors") + monitorInterval = getMonitorInterval() + + log.Info().Dur("interval", monitorInterval).Strs("queueUrls", queueUrls).Str("port", port).Msg("Starting queue monitors") + + svc = getSqsClient() go monitorQueues(queueUrls) http.Handle("/metrics", promhttp.Handler()) http.HandleFunc("/healthz", healthcheck) - err := http.ListenAndServe(":" + port, nil) + err := http.ListenAndServe(":"+port, nil) if err != nil { log.Error().Str("errorMessage", err.Error()).Msg("Could not start http listener") os.Exit(1) diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..513d694 --- /dev/null +++ b/main_test.go @@ -0,0 +1,78 @@ +package main + +import ( + "context" + "os" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/service/sqs" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +// MockSQSClient is a mock of the SQS client +type MockSQSClient struct { + mock.Mock +} + +// GetQueueAttributes mocks the GetQueueAttributes method +func (m *MockSQSClient) GetQueueAttributes(ctx context.Context, params *sqs.GetQueueAttributesInput, optFns ...func(*sqs.Options)) (*sqs.GetQueueAttributesOutput, error) { + args := m.Called(ctx, params, optFns) + return args.Get(0).(*sqs.GetQueueAttributesOutput), args.Error(1) +} + +func TestGetMonitorInterval(t *testing.T) { + tests := []struct { + name string + envValue string + expectedResult time.Duration + }{ + {"Default value", "", 30 * time.Second}, + {"Custom value", "60", 60 * time.Second}, + {"Invalid value", "invalid", 30 * time.Second}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Setenv("SQS_MONITOR_INTERVAL_SECONDS", tt.envValue) + defer os.Unsetenv("SQS_MONITOR_INTERVAL_SECONDS") + + result := getMonitorInterval() + assert.Equal(t, tt.expectedResult, result) + }) + } +} + +func TestMonitorQueue(t *testing.T) { + mockClient := new(MockSQSClient) + originalSvc := svc + svc = mockClient + defer func() { svc = originalSvc }() + + queueURL := "https://sqs.us-west-2.amazonaws.com/123456789012/MyQueue" + expectedOutput := &sqs.GetQueueAttributesOutput{ + Attributes: map[string]string{ + "ApproximateNumberOfMessages": "10", + "ApproximateNumberOfMessagesDelayed": "5", + "ApproximateNumberOfMessagesNotVisible": "2", + }, + } + + mockClient.On("GetQueueAttributes", mock.Anything, mock.Anything, mock.Anything).Return(expectedOutput, nil) + + c := make(chan queueResult) + go monitorQueue(queueURL, c) + + result := <-c + + assert.Equal(t, queueURL, result.QueueURL) + assert.Equal(t, "MyQueue", result.QueueName) + assert.Equal(t, expectedOutput, result.QueueResults) + + mockClient.AssertExpectations(t) +} + +func TestHealthcheck(t *testing.T) { + assert.NotPanics(t, func() { healthcheck(nil, nil) }) +}