topfans/backend/services/statisticService/service/event_service_test.go
zerosaturation bed8f8e578 feat(statistic): T4-T8 event collection framework (Event + Sink + Repo + Service + Workers)
- Event model + ToJSON
- EventSink interface + ChannelEventSink (non-blocking Submit)
- event_repo: batch INSERT ON CONFLICT DO NOTHING dedup
- event_service: 7-type whitelist + 1KB props limit + ReceivedAt auto-fill
- event_flusher: 100/1s batch + sync metric_recent_level_ups on level_up
- metric_weekly + metric_upcoming workers (5min/15min with pg_try_advisory_lock)
- partitioner: 7-day pre-create + 30-day cleanup (00:05 create / 00:30 cleanup)
- 22 unit + integration tests (model/repo/service/sink/worker)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 17:20:53 +08:00

125 lines
3.6 KiB
Go

package service
import (
"context"
"errors"
"testing"
"time"
"github.com/topfans/backend/services/statisticService/model"
"github.com/topfans/backend/services/statisticService/sink"
)
// mockSink 记录所有 submit 的事件
type mockSink struct {
events []*model.Event
err error
}
func (m *mockSink) Submit(ctx context.Context, e *model.Event) error {
if m.err != nil {
return m.err
}
m.events = append(m.events, e)
return nil
}
func (m *mockSink) SubmitBatch(ctx context.Context, es []*model.Event) error {
if m.err != nil {
return m.err
}
m.events = append(m.events, es...)
return nil
}
func (m *mockSink) Close() error { return nil }
func TestEventService_TrackEvent_Success(t *testing.T) {
ms := &mockSink{}
svc := NewEventService(ms, []string{"asset.like", "asset.mint"})
resp, err := svc.TrackEvent(context.Background(), &model.Event{
EventID: "u1", UserID: 1, StarID: 1, EventType: "asset.like",
OccurredAt: time.Now(), Properties: map[string]string{},
})
if err != nil {
t.Fatal(err)
}
if resp.Accepted != 1 || resp.Rejected != 0 {
t.Fatalf("got accepted=%d rejected=%d, want 1/0", resp.Accepted, resp.Rejected)
}
if len(ms.events) != 1 {
t.Fatal("event not submitted")
}
if ms.events[0].ReceivedAt.IsZero() {
t.Fatal("ReceivedAt should be set by service")
}
}
func TestEventService_TrackEvent_InvalidEventType(t *testing.T) {
ms := &mockSink{}
svc := NewEventService(ms, []string{"asset.like"})
_, err := svc.TrackEvent(context.Background(), &model.Event{EventID: "u1", EventType: "evil.type"})
if err == nil {
t.Fatal("expected error for invalid event type")
}
if !errors.Is(err, ErrInvalidEventType) {
t.Fatalf("expected ErrInvalidEventType, got %v", err)
}
}
func TestEventService_TrackEvent_EmptyEventID(t *testing.T) {
ms := &mockSink{}
svc := NewEventService(ms, []string{"asset.like"})
_, err := svc.TrackEvent(context.Background(), &model.Event{EventType: "asset.like"})
if !errors.Is(err, ErrInvalidEventID) {
t.Fatalf("expected ErrInvalidEventID, got %v", err)
}
}
func TestEventService_TrackEvent_PropertiesTooLarge(t *testing.T) {
ms := &mockSink{}
svc := NewEventService(ms, []string{"asset.like"})
big := make(map[string]string)
for i := 0; i < 200; i++ {
big["k"+string(rune(i))] = string(make([]byte, 100))
}
_, err := svc.TrackEvent(context.Background(), &model.Event{
EventID: "u1", EventType: "asset.like", Properties: big,
})
if !errors.Is(err, ErrPropertiesTooLarge) {
t.Fatalf("expected ErrPropertiesTooLarge, got %v", err)
}
}
func TestEventService_TrackEvent_SinkError(t *testing.T) {
ms := &mockSink{err: sink.ErrChannelFull}
svc := NewEventService(ms, []string{"asset.like"})
resp, err := svc.TrackEvent(context.Background(), &model.Event{
EventID: "u1", EventType: "asset.like",
})
if err != nil {
t.Fatal("expected no error (degraded to rejected), got", err)
}
if resp.Accepted != 0 || resp.Rejected != 1 {
t.Fatalf("expected 0/1, got %d/%d", resp.Accepted, resp.Rejected)
}
}
func TestEventService_BatchTrackEvent_Mixed(t *testing.T) {
ms := &mockSink{}
svc := NewEventService(ms, []string{"asset.like", "asset.mint"})
events := []*model.Event{
{EventID: "1", EventType: "asset.like", Properties: map[string]string{}},
{EventID: "2", EventType: "asset.mint", Properties: map[string]string{}},
{EventID: "3", EventType: "evil.type"}, // 拒绝
{EventID: "", EventType: "asset.like"}, // 拒绝
}
resp, err := svc.BatchTrackEvent(context.Background(), events)
if err != nil {
t.Fatal(err)
}
if resp.Accepted != 2 || resp.Rejected != 2 {
t.Fatalf("got %d/%d, want 2/2", resp.Accepted, resp.Rejected)
}
}