init push
This commit is contained in:
236
internal/repository/sqs/email_queue_test.go
Normal file
236
internal/repository/sqs/email_queue_test.go
Normal file
@@ -0,0 +1,236 @@
|
||||
package sqs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
awsv2 "github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sqs"
|
||||
sqstypes "github.com/aws/aws-sdk-go-v2/service/sqs/types"
|
||||
"github.com/aws/smithy-go"
|
||||
|
||||
"wucher/internal/config"
|
||||
"wucher/internal/queue"
|
||||
"wucher/internal/resilience"
|
||||
)
|
||||
|
||||
type mockAPI struct {
|
||||
sendInput *sqs.SendMessageInput
|
||||
receiveInput *sqs.ReceiveMessageInput
|
||||
deleteBatchInput *sqs.DeleteMessageBatchInput
|
||||
changeVisibilityInput *sqs.ChangeMessageVisibilityInput
|
||||
getAttributesInput *sqs.GetQueueAttributesInput
|
||||
sendErr error
|
||||
receiveErr error
|
||||
deleteBatchErr error
|
||||
changeVisibilityErr error
|
||||
getAttributesErr error
|
||||
sendCalls int
|
||||
receiveCalls int
|
||||
}
|
||||
|
||||
func (m *mockAPI) SendMessage(_ context.Context, params *sqs.SendMessageInput, _ ...func(*sqs.Options)) (*sqs.SendMessageOutput, error) {
|
||||
m.sendCalls++
|
||||
m.sendInput = params
|
||||
if m.sendErr != nil {
|
||||
return nil, m.sendErr
|
||||
}
|
||||
return &sqs.SendMessageOutput{}, nil
|
||||
}
|
||||
|
||||
func (m *mockAPI) ReceiveMessage(_ context.Context, params *sqs.ReceiveMessageInput, _ ...func(*sqs.Options)) (*sqs.ReceiveMessageOutput, error) {
|
||||
m.receiveCalls++
|
||||
m.receiveInput = params
|
||||
if m.receiveErr != nil {
|
||||
return nil, m.receiveErr
|
||||
}
|
||||
return &sqs.ReceiveMessageOutput{
|
||||
Messages: []sqstypes.Message{{
|
||||
MessageId: awsv2.String("msg-1"),
|
||||
ReceiptHandle: awsv2.String("receipt-1"),
|
||||
Body: awsv2.String(`{"version":"v1","message_id":"msg-1","kind":"email.dispatch","occurred_at":"2026-03-19T00:00:00Z","payload":{"to":"user@example.com","subject":"Hello","body":"World"}}`),
|
||||
MessageAttributes: map[string]sqstypes.MessageAttributeValue{
|
||||
"correlation_id": {DataType: awsv2.String("String"), StringValue: awsv2.String("req-1")},
|
||||
},
|
||||
Attributes: map[string]string{
|
||||
string(sqstypes.MessageSystemAttributeNameApproximateReceiveCount): "3",
|
||||
string(sqstypes.MessageSystemAttributeNameSentTimestamp): "1710806400000",
|
||||
},
|
||||
}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mockAPI) DeleteMessageBatch(_ context.Context, params *sqs.DeleteMessageBatchInput, _ ...func(*sqs.Options)) (*sqs.DeleteMessageBatchOutput, error) {
|
||||
m.deleteBatchInput = params
|
||||
if m.deleteBatchErr != nil {
|
||||
return nil, m.deleteBatchErr
|
||||
}
|
||||
return &sqs.DeleteMessageBatchOutput{}, nil
|
||||
}
|
||||
|
||||
func (m *mockAPI) ChangeMessageVisibility(_ context.Context, params *sqs.ChangeMessageVisibilityInput, _ ...func(*sqs.Options)) (*sqs.ChangeMessageVisibilityOutput, error) {
|
||||
m.changeVisibilityInput = params
|
||||
if m.changeVisibilityErr != nil {
|
||||
return nil, m.changeVisibilityErr
|
||||
}
|
||||
return &sqs.ChangeMessageVisibilityOutput{}, nil
|
||||
}
|
||||
|
||||
func (m *mockAPI) GetQueueAttributes(_ context.Context, params *sqs.GetQueueAttributesInput, _ ...func(*sqs.Options)) (*sqs.GetQueueAttributesOutput, error) {
|
||||
m.getAttributesInput = params
|
||||
if m.getAttributesErr != nil {
|
||||
return nil, m.getAttributesErr
|
||||
}
|
||||
return &sqs.GetQueueAttributesOutput{
|
||||
Attributes: map[string]string{
|
||||
string(sqstypes.QueueAttributeNameApproximateNumberOfMessages): "5",
|
||||
string(sqstypes.QueueAttributeNameApproximateNumberOfMessagesNotVisible): "2",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestEmailQueuePublishReceiveAckRetryDeadLetter(t *testing.T) {
|
||||
api := &mockAPI{}
|
||||
cfg := config.SQSConfig{
|
||||
Region: "ap-southeast-1",
|
||||
QueueURL: "https://sqs.ap-southeast-1.amazonaws.com/123/test",
|
||||
QueueType: queue.QueueTypeFIFO,
|
||||
MessageGroupID: "email",
|
||||
VisibilityTimeout: 30 * time.Second,
|
||||
WaitTimeSeconds: 20,
|
||||
}
|
||||
q, err := NewEmailQueue(api, cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("new queue: %v", err)
|
||||
}
|
||||
|
||||
err = q.Publish(context.Background(), queue.OutboundMessage{
|
||||
ID: "msg-1",
|
||||
Body: []byte(`{"hello":"world"}`),
|
||||
Attributes: map[string]string{"correlation_id": "req-1"},
|
||||
MessageGroupID: "email",
|
||||
DeduplicationID: "dedup-1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("publish: %v", err)
|
||||
}
|
||||
if api.sendInput == nil || awsv2.ToString(api.sendInput.MessageGroupId) != "email" {
|
||||
t.Fatalf("expected fifo publish settings")
|
||||
}
|
||||
|
||||
deliveries, err := q.Receive(context.Background(), 5)
|
||||
if err != nil {
|
||||
t.Fatalf("receive: %v", err)
|
||||
}
|
||||
if len(deliveries) != 1 || deliveries[0].ReceiveCount != 3 {
|
||||
t.Fatalf("unexpected deliveries: %+v", deliveries)
|
||||
}
|
||||
|
||||
if err := q.Ack(context.Background(), deliveries); err != nil {
|
||||
t.Fatalf("ack: %v", err)
|
||||
}
|
||||
if api.deleteBatchInput == nil || len(api.deleteBatchInput.Entries) != 1 {
|
||||
t.Fatalf("expected batch delete")
|
||||
}
|
||||
|
||||
if err := q.Retry(context.Background(), deliveries[0], 45*time.Second); err != nil {
|
||||
t.Fatalf("retry: %v", err)
|
||||
}
|
||||
if api.changeVisibilityInput == nil || api.changeVisibilityInput.VisibilityTimeout != 45 {
|
||||
t.Fatalf("expected retry visibility timeout 45")
|
||||
}
|
||||
|
||||
if err := q.DeadLetter(context.Background(), deliveries[0], "permanent failure", nil); err != nil {
|
||||
t.Fatalf("dead-letter: %v", err)
|
||||
}
|
||||
if got := awsv2.ToString(api.sendInput.MessageBody); got != `{"hello":"world"}` {
|
||||
t.Fatalf("expected dead-letter path to leave message managed by SQS redrive, got send body %q", got)
|
||||
}
|
||||
|
||||
depth, err := q.ApproximateDepth(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("depth: %v", err)
|
||||
}
|
||||
if depth != 7 {
|
||||
t.Fatalf("expected depth 7, got %d", depth)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateConfigRejectsInsecureEndpoint(t *testing.T) {
|
||||
err := validateConfig(config.SQSConfig{
|
||||
Region: "ap-southeast-1",
|
||||
QueueURL: "https://sqs.ap-southeast-1.amazonaws.com/123/test",
|
||||
Endpoint: "http://localhost:4566",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected insecure endpoint validation error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResilientAPI_CircuitBreaker(t *testing.T) {
|
||||
newExecutor := func() *resilience.Executor {
|
||||
return resilience.NewExecutor("sqs", config.CircuitBreakerPolicy{
|
||||
Enabled: true,
|
||||
Timeout: time.Second,
|
||||
MinRequests: 2,
|
||||
FailureRatio: 0.5,
|
||||
ConsecutiveFailures: 2,
|
||||
}, slog.New(slog.NewJSONHandler(io.Discard, nil)), resilience.ClassifySQSError)
|
||||
}
|
||||
|
||||
t.Run("client faults are excluded", func(t *testing.T) {
|
||||
api := &mockAPI{
|
||||
sendErr: sqsAPIError{code: "AccessDenied", fault: smithy.FaultClient},
|
||||
}
|
||||
wrapped := NewResilientAPI(api, newExecutor())
|
||||
|
||||
for range 3 {
|
||||
_, err := wrapped.SendMessage(context.Background(), &sqs.SendMessageInput{})
|
||||
if err == nil {
|
||||
t.Fatalf("expected access denied error")
|
||||
}
|
||||
var openErr *resilience.OpenError
|
||||
if errors.As(err, &openErr) {
|
||||
t.Fatalf("expected excluded error to keep breaker closed, got %v", err)
|
||||
}
|
||||
}
|
||||
if api.sendCalls != 3 {
|
||||
t.Fatalf("expected all excluded calls to reach API, got %d", api.sendCalls)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("server faults open the breaker", func(t *testing.T) {
|
||||
api := &mockAPI{
|
||||
receiveErr: sqsAPIError{code: "InternalError", fault: smithy.FaultServer},
|
||||
}
|
||||
wrapped := NewResilientAPI(api, newExecutor())
|
||||
|
||||
for range 2 {
|
||||
if _, err := wrapped.ReceiveMessage(context.Background(), &sqs.ReceiveMessageInput{}); err == nil {
|
||||
t.Fatalf("expected receive error")
|
||||
}
|
||||
}
|
||||
_, err := wrapped.ReceiveMessage(context.Background(), &sqs.ReceiveMessageInput{})
|
||||
var openErr *resilience.OpenError
|
||||
if !errors.As(err, &openErr) {
|
||||
t.Fatalf("expected circuit open error, got %v", err)
|
||||
}
|
||||
if api.receiveCalls != 2 {
|
||||
t.Fatalf("expected breaker to fail fast after opening, got %d receive calls", api.receiveCalls)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type sqsAPIError struct {
|
||||
code string
|
||||
fault smithy.ErrorFault
|
||||
}
|
||||
|
||||
func (e sqsAPIError) Error() string { return e.code }
|
||||
func (e sqsAPIError) ErrorCode() string { return e.code }
|
||||
func (e sqsAPIError) ErrorMessage() string { return e.code }
|
||||
func (e sqsAPIError) ErrorFault() smithy.ErrorFault { return e.fault }
|
||||
Reference in New Issue
Block a user