2025-11-14 21:42:04 +08:00
|
|
|
package app
|
|
|
|
|
|
|
|
|
|
import (
|
2025-11-14 23:18:10 +08:00
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
2025-11-14 21:42:04 +08:00
|
|
|
"caatsm/internal/adapter/parser"
|
2025-11-17 11:47:23 +08:00
|
|
|
"caatsm/internal/adapter/dto"
|
|
|
|
|
"caatsm/internal/infra/telemetry"
|
|
|
|
|
"caatsm/internal/port"
|
2025-11-14 21:42:04 +08:00
|
|
|
|
2025-11-14 23:18:10 +08:00
|
|
|
"github.com/google/uuid"
|
2025-11-15 09:01:24 +08:00
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
|
|
|
. "github.com/onsi/gomega"
|
2025-11-14 21:42:04 +08:00
|
|
|
"go.uber.org/zap"
|
2025-11-14 23:18:10 +08:00
|
|
|
"go.uber.org/zap/zaptest/observer"
|
2025-11-14 21:42:04 +08:00
|
|
|
)
|
|
|
|
|
|
2025-11-15 09:01:24 +08:00
|
|
|
var _ = Describe("MessageProcessor", func() {
|
|
|
|
|
var (
|
|
|
|
|
repo *stubRepository
|
|
|
|
|
pub *stubPublisher
|
|
|
|
|
proc *MessageProcessor
|
|
|
|
|
ctx context.Context
|
|
|
|
|
parserStub *stubParser
|
|
|
|
|
)
|
2025-11-14 23:18:10 +08:00
|
|
|
|
2025-11-15 09:01:24 +08:00
|
|
|
BeforeEach(func() {
|
|
|
|
|
repo = &stubRepository{}
|
|
|
|
|
pub = &stubPublisher{}
|
|
|
|
|
parserStub = &stubParser{}
|
|
|
|
|
proc = newTestProcessor(parserStub, repo, pub)
|
|
|
|
|
ctx = context.Background()
|
|
|
|
|
})
|
2025-11-14 23:18:10 +08:00
|
|
|
|
2025-11-15 09:01:24 +08:00
|
|
|
Describe("Handle", func() {
|
|
|
|
|
It("returns a permanent error when payload is empty", func() {
|
|
|
|
|
err := proc.Handle(ctx, nil, "id-1")
|
|
|
|
|
Expect(err).To(HaveOccurred())
|
|
|
|
|
Expect(IsPermanent(err)).To(BeTrue())
|
|
|
|
|
})
|
2025-11-14 23:18:10 +08:00
|
|
|
|
2025-11-15 09:01:24 +08:00
|
|
|
It("records raw messages when parser returns nil", func() {
|
|
|
|
|
parserStub.value = nil
|
|
|
|
|
err := proc.Handle(ctx, []byte("payload"), "id-2")
|
|
|
|
|
Expect(err).To(HaveOccurred())
|
|
|
|
|
Expect(IsPermanent(err)).To(BeTrue())
|
|
|
|
|
Expect(repo.rawCount()).To(Equal(1))
|
|
|
|
|
})
|
2025-11-14 23:18:10 +08:00
|
|
|
|
2025-11-15 09:01:24 +08:00
|
|
|
It("preserves UUIDs and appends nats message id comment", func() {
|
|
|
|
|
originalUUID := uuid.NewString()
|
2025-11-17 11:47:23 +08:00
|
|
|
parserStub.value = &dto.ParsedTelegram{Uuid: originalUUID, Parsed: true, Status: dto.MessageStatusParsed}
|
2025-11-14 23:18:10 +08:00
|
|
|
|
2025-11-15 09:01:24 +08:00
|
|
|
Expect(proc.Handle(ctx, []byte("payload"), "msg-123")).To(Succeed())
|
2025-11-14 23:18:10 +08:00
|
|
|
|
2025-11-15 09:01:24 +08:00
|
|
|
Expect(repo.last()).NotTo(BeNil())
|
|
|
|
|
Expect(repo.last().Uuid).To(Equal(originalUUID))
|
|
|
|
|
Expect(repo.last().Comments).To(ContainSubstring("nats_msg_id=msg-123"))
|
|
|
|
|
Expect(pub.last).NotTo(BeNil())
|
|
|
|
|
})
|
2025-11-14 23:18:10 +08:00
|
|
|
|
2025-11-15 09:01:24 +08:00
|
|
|
It("treats publisher failures as permanent and stores raw entries", func() {
|
2025-11-17 11:47:23 +08:00
|
|
|
parserStub.value = &dto.ParsedTelegram{Parsed: true, Status: dto.MessageStatusParsed}
|
2025-11-15 09:01:24 +08:00
|
|
|
pub.err = errors.New("publish failed")
|
2025-11-14 23:18:10 +08:00
|
|
|
|
2025-11-15 09:01:24 +08:00
|
|
|
err := proc.Handle(ctx, []byte("payload"), "id-3")
|
|
|
|
|
Expect(err).To(HaveOccurred())
|
|
|
|
|
Expect(IsPermanent(err)).To(BeTrue())
|
|
|
|
|
Expect(repo.last()).NotTo(BeNil())
|
|
|
|
|
Expect(repo.rawCount()).To(Equal(1))
|
2025-11-17 11:47:23 +08:00
|
|
|
Expect(repo.lastRaw().Status).To(Equal(dto.MessageStatusParsed))
|
2025-11-16 09:58:49 +08:00
|
|
|
Expect(repo.lastRaw().ErrorReason).To(ContainSubstring("publish failed"))
|
2025-11-15 09:01:24 +08:00
|
|
|
})
|
2025-11-14 23:18:10 +08:00
|
|
|
|
2025-11-15 09:01:24 +08:00
|
|
|
It("sets timestamps when missing", func() {
|
2025-11-17 11:47:23 +08:00
|
|
|
parserStub.value = &dto.ParsedTelegram{
|
2025-11-15 09:01:24 +08:00
|
|
|
Uuid: uuid.NewString(),
|
|
|
|
|
Parsed: true,
|
2025-11-17 11:47:23 +08:00
|
|
|
Status: dto.MessageStatusParsed,
|
2025-11-15 09:01:24 +08:00
|
|
|
}
|
|
|
|
|
pub.err = nil
|
2025-11-14 23:18:10 +08:00
|
|
|
|
2025-11-15 09:01:24 +08:00
|
|
|
start := time.Now()
|
|
|
|
|
Expect(proc.Handle(ctx, []byte("payload"), "msg-4")).To(Succeed())
|
2025-11-14 23:18:10 +08:00
|
|
|
|
2025-11-15 09:01:24 +08:00
|
|
|
saved := repo.last()
|
|
|
|
|
Expect(saved).NotTo(BeNil())
|
|
|
|
|
Expect(saved.ReceivedAt).NotTo(BeZero())
|
|
|
|
|
Expect(saved.ParsedAt).NotTo(BeZero())
|
|
|
|
|
Expect(saved.ReceivedAt.After(start.Add(-time.Second))).To(BeTrue())
|
|
|
|
|
Expect(saved.ParsedAt.After(start.Add(-time.Second))).To(BeTrue())
|
|
|
|
|
})
|
2025-11-14 23:18:10 +08:00
|
|
|
|
2025-11-15 09:01:24 +08:00
|
|
|
It("does not override provided timestamps", func() {
|
|
|
|
|
received := time.Now().Add(-2 * time.Minute)
|
|
|
|
|
parsedAt := time.Now().Add(-1 * time.Minute)
|
2025-11-17 11:47:23 +08:00
|
|
|
parserStub.value = &dto.ParsedTelegram{
|
2025-11-15 09:01:24 +08:00
|
|
|
Uuid: uuid.NewString(),
|
|
|
|
|
Parsed: true,
|
2025-11-17 11:47:23 +08:00
|
|
|
Status: dto.MessageStatusParsed,
|
2025-11-15 09:01:24 +08:00
|
|
|
ReceivedAt: received,
|
|
|
|
|
ParsedAt: parsedAt,
|
|
|
|
|
}
|
2025-11-14 23:18:10 +08:00
|
|
|
|
2025-11-15 09:01:24 +08:00
|
|
|
Expect(proc.Handle(ctx, []byte("payload"), "msg-5")).To(Succeed())
|
|
|
|
|
Expect(repo.last().ReceivedAt).To(Equal(received))
|
|
|
|
|
Expect(repo.last().ParsedAt).To(Equal(parsedAt))
|
|
|
|
|
})
|
2025-11-14 23:18:10 +08:00
|
|
|
|
2025-11-15 09:01:24 +08:00
|
|
|
It("logs truncated previews when parsing fails", func() {
|
|
|
|
|
core, logs := observer.New(zap.WarnLevel)
|
|
|
|
|
logger := zap.New(core)
|
|
|
|
|
parserStub = &stubParser{
|
2025-11-17 11:47:23 +08:00
|
|
|
value: &dto.ParsedTelegram{
|
2025-11-15 09:01:24 +08:00
|
|
|
Content: strings.Repeat("x", 1024),
|
|
|
|
|
Parsed: false,
|
2025-11-17 11:47:23 +08:00
|
|
|
Status: dto.MessageStatusBodyError,
|
2025-11-15 09:01:24 +08:00
|
|
|
ErrorReason: "parse failure",
|
|
|
|
|
},
|
|
|
|
|
err: errors.New("parse failure"),
|
|
|
|
|
}
|
2025-11-16 13:14:46 +08:00
|
|
|
proc = NewMessageProcessor(parserStub, repo, pub, telemetry.NewNoop(), logger)
|
2025-11-14 23:18:10 +08:00
|
|
|
|
2025-11-15 09:01:24 +08:00
|
|
|
err := proc.Handle(ctx, []byte("raw"), "msg-6")
|
|
|
|
|
Expect(err).To(HaveOccurred())
|
|
|
|
|
Expect(IsPermanent(err)).To(BeTrue())
|
2025-11-14 23:18:10 +08:00
|
|
|
|
2025-11-15 09:01:24 +08:00
|
|
|
entries := logs.FilterMessage("Message failed to parse").All()
|
|
|
|
|
Expect(entries).NotTo(BeEmpty())
|
|
|
|
|
preview, ok := entries[0].ContextMap()["content_preview"].(string)
|
|
|
|
|
Expect(ok).To(BeTrue())
|
|
|
|
|
Expect(len(preview)).To(BeNumerically("<=", 256))
|
|
|
|
|
})
|
|
|
|
|
})
|
2025-11-14 23:18:10 +08:00
|
|
|
|
2025-11-15 09:01:24 +08:00
|
|
|
Describe("truncateContent", func() {
|
|
|
|
|
It("keeps length at limit with ellipsis", func() {
|
|
|
|
|
longContent := strings.Repeat("a", 1024)
|
|
|
|
|
Expect(truncateContent(longContent, 256)).To(HaveLen(256))
|
|
|
|
|
Expect(truncateContent(longContent, 256)).To(HaveSuffix("..."))
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
})
|
2025-11-14 23:18:10 +08:00
|
|
|
|
2025-11-17 11:47:23 +08:00
|
|
|
func newTestProcessor(p parser.Parser, repo port.Repository, pub port.Publisher) *MessageProcessor {
|
2025-11-16 13:14:46 +08:00
|
|
|
return NewMessageProcessor(p, repo, pub, telemetry.NewNoop(), zap.NewNop())
|
2025-11-14 23:18:10 +08:00
|
|
|
}
|
|
|
|
|
|
2025-11-14 21:42:04 +08:00
|
|
|
type stubParser struct {
|
2025-11-17 11:47:23 +08:00
|
|
|
value *dto.ParsedTelegram
|
2025-11-15 09:01:24 +08:00
|
|
|
err error
|
2025-11-14 21:42:04 +08:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 11:47:23 +08:00
|
|
|
func (s *stubParser) Parse(rawText string) (*dto.ParsedTelegram, error) {
|
2025-11-15 09:01:24 +08:00
|
|
|
return s.value, s.err
|
2025-11-14 21:42:04 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type stubRepository struct {
|
2025-11-17 11:47:23 +08:00
|
|
|
inserted []*dto.ParsedTelegram
|
|
|
|
|
raw []*dto.ParsedTelegram
|
2025-11-14 21:42:04 +08:00
|
|
|
err error
|
2025-11-15 09:01:24 +08:00
|
|
|
rawErr error
|
2025-11-14 21:42:04 +08:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 11:47:23 +08:00
|
|
|
func (s *stubRepository) InsertOne(ctx context.Context, msg *dto.ParsedTelegram) error {
|
2025-11-14 21:42:04 +08:00
|
|
|
if s.err != nil {
|
|
|
|
|
return s.err
|
|
|
|
|
}
|
|
|
|
|
s.inserted = append(s.inserted, msg)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 11:47:23 +08:00
|
|
|
func (s *stubRepository) InsertBatch(ctx context.Context, msgs []*dto.ParsedTelegram) error {
|
2025-11-14 21:42:04 +08:00
|
|
|
return errors.New("not implemented")
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 11:47:23 +08:00
|
|
|
func (s *stubRepository) InsertRaw(ctx context.Context, msg *dto.ParsedTelegram) error {
|
2025-11-15 09:01:24 +08:00
|
|
|
if s.rawErr != nil {
|
|
|
|
|
return s.rawErr
|
|
|
|
|
}
|
|
|
|
|
s.raw = append(s.raw, msg)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 11:47:23 +08:00
|
|
|
func (s *stubRepository) last() *dto.ParsedTelegram {
|
2025-11-14 23:18:10 +08:00
|
|
|
if len(s.inserted) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return s.inserted[len(s.inserted)-1]
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 11:47:23 +08:00
|
|
|
func (s *stubRepository) lastRaw() *dto.ParsedTelegram {
|
2025-11-15 09:01:24 +08:00
|
|
|
if len(s.raw) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return s.raw[len(s.raw)-1]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *stubRepository) rawCount() int {
|
|
|
|
|
return len(s.raw)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-14 21:42:04 +08:00
|
|
|
type stubPublisher struct {
|
|
|
|
|
last interface{}
|
|
|
|
|
err error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *stubPublisher) Publish(message interface{}) error {
|
|
|
|
|
s.last = message
|
|
|
|
|
return s.err
|
|
|
|
|
}
|