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,89 @@
|
||||
package subscriber
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"aiaa-notification-service/internal/model"
|
||||
"aiaa-notification-service/internal/notify"
|
||||
"aiaa-notification-service/internal/subscriber/tradesignal"
|
||||
)
|
||||
|
||||
func TestMessageHashStable(t *testing.T) {
|
||||
a := MessageHash([]byte(`{"action":"OPEN","signalId":"s1"}`))
|
||||
b := MessageHash([]byte(`{"action":"OPEN","signalId":"s1"}`))
|
||||
c := MessageHash([]byte(`{"action":"CLOSE","signalId":"s1"}`))
|
||||
if a == "" || a != b {
|
||||
t.Fatalf("hash should be stable, a=%q b=%q", a, b)
|
||||
}
|
||||
if a == c {
|
||||
t.Fatal("different bodies should hash differently")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemoryDeduperClaimOnce(t *testing.T) {
|
||||
d := NewMemoryDeduper()
|
||||
ok, err := d.Claim(context.Background(), "abc")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("first claim ok=%v err=%v", ok, err)
|
||||
}
|
||||
ok, err = d.Claim(context.Background(), "abc")
|
||||
if err != nil || ok {
|
||||
t.Fatalf("second claim should miss, ok=%v err=%v", ok, err)
|
||||
}
|
||||
if err := d.Release(context.Background(), "abc"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ok, err = d.Claim(context.Background(), "abc")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("after release should claim, ok=%v err=%v", ok, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleDuplicateAckSkipsProcess(t *testing.T) {
|
||||
dedup := NewMemoryDeduper()
|
||||
var n atomic.Int32
|
||||
process := func(context.Context, notify.Request) (notify.Result, error) {
|
||||
n.Add(1)
|
||||
return notify.Result{Matched: true}, nil
|
||||
}
|
||||
lookup := func(context.Context, string) (*model.Source, error) { return enabledSrc(), nil }
|
||||
conv := tradesignal.NewConverter(nil)
|
||||
body := []byte(`{"action":"OPEN","signalId":"dup-1"}`)
|
||||
in := HandleInput{Body: body, SourceName: "trade-signal", MaxRetry: 3, Deduper: dedup}
|
||||
|
||||
if d := HandleMessage(context.Background(), in, conv, lookup, process); d != DispositionAck {
|
||||
t.Fatalf("first=%v", d)
|
||||
}
|
||||
if d := HandleMessage(context.Background(), in, conv, lookup, process); d != DispositionAck {
|
||||
t.Fatalf("dup=%v", d)
|
||||
}
|
||||
if n.Load() != 1 {
|
||||
t.Fatalf("process called %d times, want 1", n.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleProcessErrorReleasesDedup(t *testing.T) {
|
||||
dedup := NewMemoryDeduper()
|
||||
var n atomic.Int32
|
||||
process := func(context.Context, notify.Request) (notify.Result, error) {
|
||||
n.Add(1)
|
||||
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","signalId":"retry-1"}`)
|
||||
in := HandleInput{Body: body, SourceName: "s", MaxRetry: 3, Deduper: dedup}
|
||||
|
||||
if d := HandleMessage(context.Background(), in, conv, lookup, process); d != DispositionRetry {
|
||||
t.Fatalf("first=%v", d)
|
||||
}
|
||||
if d := HandleMessage(context.Background(), in, conv, lookup, process); d != DispositionRetry {
|
||||
t.Fatalf("retry should process again, got %v", d)
|
||||
}
|
||||
if n.Load() != 2 {
|
||||
t.Fatalf("process called %d times, want 2", n.Load())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user