✨ Update agent guidelines and improve documentation structure. Refactor AGENTS.md to streamline commands and code style guidelines, enhancing clarity and usability. Update README.md with refined NATS consumer configuration details and observability metrics. Modify .gitignore to exclude dynamically generated Prometheus target files. Enhance configuration files for development and production environments, ensuring consistency and clarity in settings.
This commit is contained in:
@@ -1,196 +0,0 @@
|
||||
//go:build integration
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"caatsm/internal/infra/config"
|
||||
loginfra "caatsm/internal/infra/log"
|
||||
natsinfra "caatsm/internal/infra/nats"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAdvisoryDLQHandler(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Start NATS container
|
||||
natsContainer, natsURL := startNATS(ctx, t)
|
||||
defer func() {
|
||||
_ = natsContainer.Terminate(context.Background())
|
||||
}()
|
||||
|
||||
// Connect to NATS
|
||||
nc, err := nats.Connect(natsURL)
|
||||
require.NoError(t, err)
|
||||
defer nc.Close()
|
||||
|
||||
js, err := nc.JetStream()
|
||||
require.NoError(t, err)
|
||||
|
||||
streamName := "TEST_ADVISORY_STREAM"
|
||||
consumerName := "test-advisory-consumer"
|
||||
dlqSubject := "test.dlq"
|
||||
|
||||
// Create stream
|
||||
streamConfig := &nats.StreamConfig{
|
||||
Name: streamName,
|
||||
Subjects: []string{"test.orders.*"},
|
||||
Storage: nats.FileStorage,
|
||||
}
|
||||
_, err = js.AddStream(streamConfig)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_ = js.DeleteStream(streamName)
|
||||
}()
|
||||
|
||||
// Create DLQ stream
|
||||
dlqStreamConfig := &nats.StreamConfig{
|
||||
Name: "TEST_DLQ_STREAM",
|
||||
Subjects: []string{dlqSubject},
|
||||
Storage: nats.FileStorage,
|
||||
}
|
||||
_, err = js.AddStream(dlqStreamConfig)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_ = js.DeleteStream("TEST_DLQ_STREAM")
|
||||
}()
|
||||
|
||||
// Create consumer with MaxDeliver = 2 for testing
|
||||
consumerConfig := &nats.ConsumerConfig{
|
||||
Durable: consumerName,
|
||||
AckPolicy: nats.AckExplicitPolicy,
|
||||
MaxDeliver: 2, // Low value for testing
|
||||
AckWait: 5 * time.Second,
|
||||
}
|
||||
_, err = js.AddConsumer(streamName, consumerConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a mock telemetry recorder
|
||||
telemetry := &mockTelemetryRecorder{}
|
||||
|
||||
// Create a simple logger for the test
|
||||
logger, err := loginfra.ProvideLogger(&config.Config{
|
||||
Log: config.LogConfig{
|
||||
Level: "info",
|
||||
Format: "console",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer logger.Sync()
|
||||
|
||||
// Create advisory DLQ handler
|
||||
handler, err := natsinfra.NewAdvisoryDLQHandler(
|
||||
js,
|
||||
nc,
|
||||
streamName,
|
||||
consumerName,
|
||||
dlqSubject,
|
||||
logger,
|
||||
telemetry,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Start handler in background
|
||||
handlerCtx, handlerCancel := context.WithCancel(ctx)
|
||||
defer handlerCancel()
|
||||
go func() {
|
||||
_ = handler.Start(handlerCtx)
|
||||
}()
|
||||
|
||||
// Give handler time to subscribe
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Publish a message that will fail processing
|
||||
testSubject := "test.orders.1"
|
||||
testData := []byte("test message data")
|
||||
_, err = js.Publish(testSubject, testData)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create pull subscription and fetch message
|
||||
sub, err := js.PullSubscribe(testSubject, consumerName, nats.Bind(streamName, consumerName))
|
||||
require.NoError(t, err)
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
// Fetch and NAK the message multiple times to exhaust MaxDeliver
|
||||
msgs, err := sub.Fetch(1, nats.MaxWait(2*time.Second))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, msgs, 1)
|
||||
|
||||
msg := msgs[0]
|
||||
// NAK first time
|
||||
err = msg.Nak()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for redelivery and NAK again to exhaust MaxDeliver
|
||||
time.Sleep(6 * time.Second) // Wait for ack_wait + some buffer
|
||||
|
||||
msgs, err = sub.Fetch(1, nats.MaxWait(2*time.Second))
|
||||
if err == nil && len(msgs) > 0 {
|
||||
// NAK second time to exhaust MaxDeliver
|
||||
err = msgs[0].Nak()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Wait for advisory message to be processed
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Verify message was published to DLQ
|
||||
dlqSub, err := js.SubscribeSync(dlqSubject)
|
||||
require.NoError(t, err)
|
||||
defer dlqSub.Unsubscribe()
|
||||
|
||||
dlqMsg, err := dlqSub.NextMsg(5 * time.Second)
|
||||
if assert.NoError(t, err, "Expected message in DLQ") {
|
||||
var payload map[string]interface{}
|
||||
err = json.Unmarshal(dlqMsg.Data, &payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify payload structure
|
||||
assert.Equal(t, streamName, payload["stream"])
|
||||
assert.Equal(t, consumerName, payload["consumer"])
|
||||
assert.True(t, payload["advisory_source"].(bool))
|
||||
assert.Contains(t, payload["error"].(string), "exhausted max_deliver")
|
||||
assert.Equal(t, string(testData), payload["body"])
|
||||
}
|
||||
|
||||
// Verify telemetry was called
|
||||
assert.Greater(t, telemetry.getDLQMessages(), 0, "Expected DLQ message to be recorded")
|
||||
}
|
||||
|
||||
// mockTelemetryRecorder implements TelemetryRecorder for testing
|
||||
type mockTelemetryRecorder struct {
|
||||
mu sync.RWMutex
|
||||
dlqMessages int
|
||||
dlqFailures int
|
||||
}
|
||||
|
||||
func (m *mockTelemetryRecorder) RecordDLQMessage(ctx context.Context, stream, consumer string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.dlqMessages++
|
||||
}
|
||||
|
||||
func (m *mockTelemetryRecorder) RecordDLQPublishFailure(ctx context.Context, stream, consumer string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.dlqFailures++
|
||||
}
|
||||
|
||||
func (m *mockTelemetryRecorder) getDLQMessages() int {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.dlqMessages
|
||||
}
|
||||
Reference in New Issue
Block a user