6f846a0a3c
Consume configurable queues, format signals (including period), share NotifyService with HTTP, and drop duplicate bodies within 1h.
136 lines
4.2 KiB
Go
136 lines
4.2 KiB
Go
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)
|
|
}
|
|
}
|