feat: subscribe to RabbitMQ trade signals and notify by rules
Consume configurable queues, format signals (including period), share NotifyService with HTTP, and drop duplicate bodies within 1h.
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
package subscriber
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"aiaa-notification-service/internal/model"
|
||||
"aiaa-notification-service/internal/notify"
|
||||
"aiaa-notification-service/internal/subscriber/tradesignal"
|
||||
)
|
||||
|
||||
func TestDecideRetry(t *testing.T) {
|
||||
if DecideRetry(0, 3) != DispositionRetry {
|
||||
t.Fatal("first failure should retry")
|
||||
}
|
||||
if DecideRetry(3, 3) != DispositionDLQ {
|
||||
t.Fatal("retry 4 > 3 should dlq")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryCount(t *testing.T) {
|
||||
if RetryCount(nil) != 0 {
|
||||
t.Fatal()
|
||||
}
|
||||
if RetryCount(map[string]any{"x-retry-count": int32(2)}) != 2 {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func enabledSrc() *model.Source {
|
||||
return &model.Source{ID: 1, Name: "trade-signal", Status: 1}
|
||||
}
|
||||
|
||||
func TestHandleInvalidJSONAck(t *testing.T) {
|
||||
d := HandleMessage(context.Background(), HandleInput{Body: []byte(`{`), SourceName: "trade-signal", MaxRetry: 3},
|
||||
tradesignal.NewConverter(nil),
|
||||
func(context.Context, string) (*model.Source, error) { return enabledSrc(), nil },
|
||||
func(context.Context, notify.Request) (notify.Result, error) {
|
||||
t.Fatal("process should not run")
|
||||
return notify.Result{}, nil
|
||||
})
|
||||
if d != DispositionAck {
|
||||
t.Fatalf("%v", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleMissingSourceAck(t *testing.T) {
|
||||
d := HandleMessage(context.Background(), HandleInput{
|
||||
Body: []byte(`{"action":"OPEN","symbol":"BTCUSDT"}`), SourceName: "trade-signal", MaxRetry: 3,
|
||||
}, tradesignal.NewConverter(nil),
|
||||
func(context.Context, string) (*model.Source, error) { return nil, errors.New("not found") },
|
||||
func(context.Context, notify.Request) (notify.Result, error) {
|
||||
t.Fatal("process")
|
||||
return notify.Result{}, nil
|
||||
})
|
||||
if d != DispositionAck {
|
||||
t.Fatalf("%v", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleDisabledSourceAck(t *testing.T) {
|
||||
d := HandleMessage(context.Background(), HandleInput{
|
||||
Body: []byte(`{"action":"OPEN"}`), SourceName: "trade-signal", MaxRetry: 3,
|
||||
}, tradesignal.NewConverter(nil),
|
||||
func(context.Context, string) (*model.Source, error) {
|
||||
return &model.Source{ID: 1, Name: "trade-signal", Status: 0}, nil
|
||||
},
|
||||
func(context.Context, notify.Request) (notify.Result, error) {
|
||||
t.Fatal("process")
|
||||
return notify.Result{}, nil
|
||||
})
|
||||
if d != DispositionAck {
|
||||
t.Fatalf("%v", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleProcessUnprocessableAck(t *testing.T) {
|
||||
d := HandleMessage(context.Background(), HandleInput{
|
||||
Body: []byte(`{"action":"OPEN"}`), SourceName: "trade-signal", MaxRetry: 3,
|
||||
}, tradesignal.NewConverter(nil),
|
||||
func(context.Context, string) (*model.Source, error) { return enabledSrc(), nil },
|
||||
func(context.Context, notify.Request) (notify.Result, error) {
|
||||
return notify.Result{}, notify.ErrUnprocessable
|
||||
})
|
||||
if d != DispositionAck {
|
||||
t.Fatalf("%v", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleProcessErrorRetryThenDLQ(t *testing.T) {
|
||||
process := func(context.Context, notify.Request) (notify.Result, error) {
|
||||
return notify.Result{}, errors.New("db down")
|
||||
}
|
||||
lookup := func(context.Context, string) (*model.Source, error) { return enabledSrc(), nil }
|
||||
conv := tradesignal.NewConverter(nil)
|
||||
body := []byte(`{"action":"OPEN"}`)
|
||||
d := HandleMessage(context.Background(), HandleInput{Body: body, SourceName: "s", MaxRetry: 3}, conv, lookup, process)
|
||||
if d != DispositionRetry {
|
||||
t.Fatalf("%v", d)
|
||||
}
|
||||
d = HandleMessage(context.Background(), HandleInput{
|
||||
Body: body, Headers: map[string]any{"x-retry-count": 3}, SourceName: "s", MaxRetry: 3,
|
||||
}, conv, lookup, process)
|
||||
if d != DispositionDLQ {
|
||||
t.Fatalf("%v", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleSuccessAckPassesEventAndFormatted(t *testing.T) {
|
||||
var got notify.Request
|
||||
d := HandleMessage(context.Background(), HandleInput{
|
||||
Body: []byte(`{"action":"CLOSE","symbol":"ETHUSDT","period":"4h","price":1}`),
|
||||
SourceName: "trade-signal", MaxRetry: 3,
|
||||
}, tradesignal.NewConverter(nil),
|
||||
func(context.Context, string) (*model.Source, error) { return enabledSrc(), nil },
|
||||
func(_ context.Context, req notify.Request) (notify.Result, error) {
|
||||
got = req
|
||||
return notify.Result{Matched: true, Channels: []string{"dingtalk:1"}}, nil
|
||||
})
|
||||
if d != DispositionAck {
|
||||
t.Fatalf("%v", d)
|
||||
}
|
||||
if got.Event != "trade.close" {
|
||||
t.Fatalf("event=%q", got.Event)
|
||||
}
|
||||
if got.Data["period"] != "4h" {
|
||||
t.Fatalf("period=%v", got.Data["period"])
|
||||
}
|
||||
formatted, _ := got.Data["formatted"].(string)
|
||||
if !strings.Contains(formatted, "周期: 4h") {
|
||||
t.Fatalf("formatted=%s", formatted)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user