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,118 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"aiaa-notification-service/internal/engine"
|
||||
"aiaa-notification-service/internal/model"
|
||||
)
|
||||
|
||||
type fakeMatcher struct {
|
||||
rule *model.Rule
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeMatcher) Match(context.Context, int, string) (*model.Rule, error) {
|
||||
return f.rule, f.err
|
||||
}
|
||||
|
||||
type fakeTemplates struct {
|
||||
tmpl *model.Template
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeTemplates) GetTemplate(context.Context, int) (*model.Template, error) {
|
||||
return f.tmpl, f.err
|
||||
}
|
||||
|
||||
type fakeRouter struct{ channels []string }
|
||||
|
||||
func (f *fakeRouter) Route(context.Context, *model.Rule, string, string) []string {
|
||||
return f.channels
|
||||
}
|
||||
|
||||
type fakeLogs struct{}
|
||||
|
||||
func (f *fakeLogs) CreateMessageLog(context.Context, *model.MessageLog) error { return nil }
|
||||
|
||||
func newSvc(m *fakeMatcher, t *fakeTemplates, rt *fakeRouter) *Service {
|
||||
return NewService(m, t, engine.NewRenderer(), rt, &fakeLogs{})
|
||||
}
|
||||
|
||||
func TestProcessNoRule(t *testing.T) {
|
||||
svc := newSvc(&fakeMatcher{err: errors.New("no rule")}, &fakeTemplates{}, &fakeRouter{})
|
||||
res, err := svc.Process(context.Background(), Request{
|
||||
Source: &model.Source{ID: 1, Name: "trade-signal"},
|
||||
Event: "trade.open",
|
||||
Data: map[string]interface{}{"symbol": "BTCUSDT"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.Matched {
|
||||
t.Fatal("expected unmatched")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessFiltered(t *testing.T) {
|
||||
raw := json.RawMessage(`[{"field":"symbol","op":"eq","value":"ETHUSDT"}]`)
|
||||
svc := newSvc(&fakeMatcher{rule: &model.Rule{ID: 1, TemplateID: 1, Conditions: &raw}}, &fakeTemplates{}, &fakeRouter{})
|
||||
res, err := svc.Process(context.Background(), Request{
|
||||
Source: &model.Source{ID: 1, Name: "trade-signal"},
|
||||
Event: "trade.open",
|
||||
Data: map[string]interface{}{"symbol": "BTCUSDT"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !res.Matched || !res.Filtered || res.Reason != "condition not met" {
|
||||
t.Fatalf("%+v", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessMatched(t *testing.T) {
|
||||
svc := newSvc(
|
||||
&fakeMatcher{rule: &model.Rule{ID: 9, TemplateID: 1}},
|
||||
&fakeTemplates{tmpl: &model.Template{ID: 1, Content: "{{.formatted}}"}},
|
||||
&fakeRouter{channels: []string{"dingtalk:3"}},
|
||||
)
|
||||
res, err := svc.Process(context.Background(), Request{
|
||||
Source: &model.Source{ID: 1, Name: "trade-signal"},
|
||||
Event: "trade.open",
|
||||
Data: map[string]interface{}{"formatted": "多单开仓"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !res.Matched || res.Filtered || len(res.Channels) != 1 || res.Channels[0] != "dingtalk:3" {
|
||||
t.Fatalf("%+v", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessInvalidConditions(t *testing.T) {
|
||||
raw := json.RawMessage(`not-json`)
|
||||
svc := newSvc(&fakeMatcher{rule: &model.Rule{ID: 1, TemplateID: 1, Conditions: &raw}}, &fakeTemplates{}, &fakeRouter{})
|
||||
_, err := svc.Process(context.Background(), Request{
|
||||
Source: &model.Source{ID: 1, Name: "s"},
|
||||
Event: "e",
|
||||
Data: map[string]interface{}{},
|
||||
})
|
||||
if !errors.Is(err, ErrUnprocessable) {
|
||||
t.Fatalf("err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessTemplateMissing(t *testing.T) {
|
||||
svc := newSvc(&fakeMatcher{rule: &model.Rule{ID: 1, TemplateID: 1}}, &fakeTemplates{err: errors.New("nope")}, &fakeRouter{})
|
||||
_, err := svc.Process(context.Background(), Request{
|
||||
Source: &model.Source{ID: 1, Name: "s"},
|
||||
Event: "e",
|
||||
Data: map[string]interface{}{},
|
||||
})
|
||||
if err == nil || errors.Is(err, ErrUnprocessable) {
|
||||
t.Fatalf("want retryable error, got %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user