diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ddc7214..73c9b3f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,6 +14,6 @@ jobs: with: go-version: stable - name: golangci-lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v7 with: - version: v1.60 \ No newline at end of file + version: v2.11.3 diff --git a/.golangci.yml b/.golangci.yml index 083e762..bd005c9 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,69 +1,50 @@ -# options for analysis running -run: - # timeout for analysis, e.g. 30s, 5m, default is 1m - deadline: 2m - -issues: - # Only report issues for changes since master - new-from-rev: origin/master - -# output configuration options -output: - # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" - format: colored-line-number - -linters-settings: - errcheck: - # report about not checking of errors in type assertions: `a := b.(MyStruct)`; - # default is false: such cases aren't reported by default. - check-type-assertions: true - - # Function length check - funlen: - lines: 60 - statements: 40 - - # Report deeply nested if statements - nestif: - # minimal complexity of if statements to report, 5 by default - min-complexity: 4 - - gofmt: - # simplify code: gofmt with `-s` option, true by default - simplify: true +version: "2" - golint: - # minimal confidence for issues, default is 0.8 - min-confidence: 0.8 - - govet: - # report about shadowed variables - check-shadowing: true - enable-all: true - disable: - # Do not check field memory alignment because in most cases the performance gain is not worth the headache - - fieldalignment +run: + timeout: 2m linters: - # Disable the default linters so we can explicitly name the linters we want - disable-all: true - - # List of enabled linters + default: none enable: - ##################### - # Default linters - ##################### - - gofmt - # Checks error handling - errcheck - # Linter for Go source code that specializes in simplifying a code - - gosimple - # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose - # arguments do not align with the format string - govet - # Detects when assignments to existing variables are not used - ineffassign - # Static code analytics - staticcheck - # Unused checks - - unused \ No newline at end of file + - unused + settings: + errcheck: + check-type-assertions: true + funlen: + lines: 60 + statements: 40 + govet: + disable: + - fieldalignment + enable-all: true + nestif: + min-complexity: 4 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ +issues: + new-from-rev: origin/master +formatters: + enable: + - gofmt + settings: + gofmt: + simplify: true + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/internal/examples/memory/main.go b/internal/examples/memory/main.go index b189e05..664212f 100644 --- a/internal/examples/memory/main.go +++ b/internal/examples/memory/main.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "fmt" "log" "os" "os/signal" @@ -18,12 +19,16 @@ type myMessage struct { func (e *myMessage) UnmarshalJSON(data []byte) error { var raw map[string]any - err := json.Unmarshal(data, &raw) - if err != nil { + if err := json.Unmarshal(data, &raw); err != nil { return err } - e.name = raw["name"].(string) + name, ok := raw["name"].(string) + if !ok { + return fmt.Errorf("missing or invalid field: name") + } + + e.name = name return nil } diff --git a/internal/examples/memory/main_test.go b/internal/examples/memory/main_test.go new file mode 100644 index 0000000..4682f9d --- /dev/null +++ b/internal/examples/memory/main_test.go @@ -0,0 +1,42 @@ +package main + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMyMessage_UnmarshalJSON_Success(t *testing.T) { + var m myMessage + err := json.Unmarshal([]byte(`{"name":"hello"}`), &m) + assert.NoError(t, err) + assert.Equal(t, "hello", m.name) +} + +func TestMyMessage_UnmarshalJSON_MissingField(t *testing.T) { + var m myMessage + err := json.Unmarshal([]byte(`{}`), &m) + assert.Error(t, err) + assert.Contains(t, err.Error(), "missing or invalid field: name") +} + +func TestMyMessage_UnmarshalJSON_WrongType(t *testing.T) { + var m myMessage + err := json.Unmarshal([]byte(`{"name": 42}`), &m) + assert.Error(t, err) + assert.Contains(t, err.Error(), "missing or invalid field: name") +} + +func TestMyMessage_UnmarshalJSON_InvalidJSON(t *testing.T) { + var m myMessage + err := json.Unmarshal([]byte(`not-json`), &m) + assert.Error(t, err) +} + +func TestMyMessage_MarshalJSON(t *testing.T) { + m := myMessage{name: "hello"} + data, err := json.Marshal(&m) + assert.NoError(t, err) + assert.JSONEq(t, `{"name":"hello"}`, string(data)) +} diff --git a/internal/examples/sqs/message.go b/internal/examples/sqs/message.go index 56e3343..a3be867 100644 --- a/internal/examples/sqs/message.go +++ b/internal/examples/sqs/message.go @@ -2,6 +2,7 @@ package sqs import ( "encoding/json" + "fmt" ) type MyEvent struct { @@ -10,12 +11,16 @@ type MyEvent struct { func (e *MyEvent) UnmarshalJSON(data []byte) error { var raw map[string]any - err := json.Unmarshal(data, &raw) - if err != nil { + if err := json.Unmarshal(data, &raw); err != nil { return err } - e.Name = raw["name"].(string) + name, ok := raw["name"].(string) + if !ok { + return fmt.Errorf("missing or invalid field: name") + } + + e.Name = name return nil } diff --git a/internal/examples/sqs/message_test.go b/internal/examples/sqs/message_test.go new file mode 100644 index 0000000..ea9c21f --- /dev/null +++ b/internal/examples/sqs/message_test.go @@ -0,0 +1,42 @@ +package sqs + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMyEvent_UnmarshalJSON_Success(t *testing.T) { + var e MyEvent + err := json.Unmarshal([]byte(`{"name":"test-event"}`), &e) + assert.NoError(t, err) + assert.Equal(t, "test-event", e.Name) +} + +func TestMyEvent_UnmarshalJSON_MissingField(t *testing.T) { + var e MyEvent + err := json.Unmarshal([]byte(`{}`), &e) + assert.Error(t, err) + assert.Contains(t, err.Error(), "missing or invalid field: name") +} + +func TestMyEvent_UnmarshalJSON_WrongType(t *testing.T) { + var e MyEvent + err := json.Unmarshal([]byte(`{"name": 42}`), &e) + assert.Error(t, err) + assert.Contains(t, err.Error(), "missing or invalid field: name") +} + +func TestMyEvent_UnmarshalJSON_InvalidJSON(t *testing.T) { + var e MyEvent + err := json.Unmarshal([]byte(`not-json`), &e) + assert.Error(t, err) +} + +func TestMyEvent_MarshalJSON(t *testing.T) { + e := MyEvent{Name: "test-event"} + data, err := json.Marshal(&e) + assert.NoError(t, err) + assert.JSONEq(t, `{"name":"test-event"}`, string(data)) +} diff --git a/sqs/sqs.go b/sqs/sqs.go index de2afdd..b8ff138 100644 --- a/sqs/sqs.go +++ b/sqs/sqs.go @@ -1,7 +1,6 @@ package sqs import ( - "errors" "fmt" "net/url" "os" @@ -37,12 +36,7 @@ func New(options ...Option) (*Driver, error) { } if driver.sqsClient == nil { - clientCredentials, err := getCredentials() - if err != nil { - return nil, err - } - - client, err := createClient(driver.url, driver.region, clientCredentials) + client, err := createClient(driver.url, driver.region, getCredentials()) if err != nil { return nil, err } @@ -59,16 +53,18 @@ func New(options ...Option) (*Driver, error) { return driver, nil } -func getCredentials() (*credentials.Credentials, error) { +// getCredentials returns explicit credentials when the legacy env vars are set, +// or nil to fall through to the AWS SDK default credential chain (which supports +// ECS/Pod Identity, instance profiles, env vars, and shared credentials files). +func getCredentials() *credentials.Credentials { if os.Getenv("AWS_SHARED_CREDENTIALS_FILE") != "" { - return credentials.NewSharedCredentials("", ""), nil - } else if os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "" { - return credentials.NewEnvCredentials(), nil + return credentials.NewSharedCredentials("", "") } - - return nil, errors.New( - "missing AWS_SHARED_CREDENTIALS_FILE and AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY env vars", - ) + if os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "" { + return credentials.NewEnvCredentials() + } + // nil tells the SDK to use its built-in default chain, including Pod Identity. + return nil } func createClient(queueUrl string, region string, clientCredentials *credentials.Credentials) (*sqs.SQS, error) { diff --git a/sqs/sqs_test.go b/sqs/sqs_test.go index 7adf42f..86ee312 100644 --- a/sqs/sqs_test.go +++ b/sqs/sqs_test.go @@ -55,7 +55,34 @@ func (suite *SQSTestSuite) TestNewWithDefaultOptions() { _, err := sqs.New() suite.Error(err) - suite.Contains(err.Error(), "missing") + suite.Contains(err.Error(), "error creating sqs client") +} + +// TestNewWithEnvCredentials verifies that static key/secret env vars are accepted. +func (suite *SQSTestSuite) TestNewWithEnvCredentials() { + os.Setenv("AWS_ACCESS_KEY_ID", "AKIAIOSFODNN7EXAMPLE") + os.Setenv("AWS_SECRET_ACCESS_KEY", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY") + + _, err := sqs.New( + sqs.WithUrl("https://sqs.eu-central-1.amazonaws.com"), + sqs.WithRegion("us-east-1"), + ) + + suite.Nil(err) +} + +// TestNewWithNoCredentialEnvVars verifies that the driver can be created when no +// explicit credential env vars are set, falling through to the AWS SDK default +// credential chain (which covers Pod Identity, instance profiles, etc.). +func (suite *SQSTestSuite) TestNewWithNoCredentialEnvVars() { + _, err := sqs.New( + sqs.WithUrl("https://sqs.eu-central-1.amazonaws.com"), + sqs.WithRegion("us-east-1"), + ) + + // The driver should be created without error — credential resolution is + // deferred to the first actual API call, not at construction time. + suite.Nil(err) } func (suite *SQSTestSuite) TestNew_InvalidQueueURL() {