105 lines
4.3 KiB
Go
105 lines
4.3 KiB
Go
package activitymq
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestActivityTemplateFactRoundTrip(t *testing.T) {
|
|
occurredAt := time.Date(2026, time.July, 14, 8, 30, 0, 0, time.UTC).UnixMilli()
|
|
want := ActivityTemplateFactMessage{
|
|
AppCode: "lalu", EventID: "activity:g1", EventType: EventTypeGiftScored,
|
|
TemplateID: "template-1", TemplateCode: "summer_rank", PublishedVersion: 3,
|
|
StatDay: "2026-07-14", RegionID: 9, UserID: 1001, GiftID: "rose",
|
|
GiftCountDelta: 2, CoinAmountDelta: 200, OccurredAtMS: occurredAt,
|
|
}
|
|
body, err := EncodeActivityTemplateFactMessage(want)
|
|
if err != nil {
|
|
t.Fatalf("encode: %v", err)
|
|
}
|
|
got, err := DecodeActivityTemplateFactMessage(body)
|
|
if err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if got.MessageType != MessageTypeActivityTemplateFact || got.EventID != want.EventID || got.CoinAmountDelta != 200 {
|
|
t.Fatalf("unexpected round trip: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestActivityTemplateFactRejectsMismatchedDayAndIncompleteDimensions(t *testing.T) {
|
|
base := ActivityTemplateFactMessage{
|
|
AppCode: "lalu", EventID: "activity:t1", EventType: EventTypeTaskProgressed,
|
|
TemplateID: "template-1", TemplateCode: "summer_rank", PublishedVersion: 1,
|
|
StatDay: "2026-07-13", UserID: 1001, TaskKey: "send_gift", TaskType: "gift",
|
|
ProgressDelta: 1, OccurredAtMS: time.Date(2026, time.July, 14, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
|
}
|
|
if _, err := EncodeActivityTemplateFactMessage(base); err == nil || !strings.Contains(err.Error(), "does not match") {
|
|
t.Fatalf("expected stat day mismatch, got %v", err)
|
|
}
|
|
base.StatDay = "2026-07-14"
|
|
base.TaskKey = ""
|
|
if _, err := EncodeActivityTemplateFactMessage(base); err == nil {
|
|
t.Fatal("expected incomplete task identity rejection")
|
|
}
|
|
}
|
|
|
|
func TestTaskClaimedUsesImmutableEarningAttributionDay(t *testing.T) {
|
|
earningAt := time.Date(2026, time.July, 14, 23, 50, 0, 0, time.UTC).UnixMilli()
|
|
claimedAt := time.Date(2026, time.July, 17, 9, 15, 0, 0, time.UTC).UnixMilli()
|
|
want := ActivityTemplateFactMessage{
|
|
AppCode: "lalu", EventID: "activity:claim:delayed", EventType: EventTypeTaskClaimed,
|
|
TemplateID: "template-1", TemplateCode: "summer_rank", PublishedVersion: 3,
|
|
StatDay: "2026-07-14", RegionID: 9, UserID: 1001, TaskKey: "send_gift", TaskType: "gift",
|
|
ClaimedDelta: 1, OccurredAtMS: claimedAt, AttributionAtMS: earningAt,
|
|
}
|
|
body, err := EncodeActivityTemplateFactMessage(want)
|
|
if err != nil {
|
|
t.Fatalf("encode delayed task claim: %v", err)
|
|
}
|
|
got, err := DecodeActivityTemplateFactMessage(body)
|
|
if err != nil {
|
|
t.Fatalf("decode delayed task claim: %v", err)
|
|
}
|
|
if got.StatDay != "2026-07-14" || got.OccurredAtMS != claimedAt || got.AttributionAtMS != earningAt {
|
|
t.Fatalf("task claim attribution changed: %+v", got)
|
|
}
|
|
|
|
// A claim cannot relabel its earning day independently from the persisted
|
|
// attribution timestamp, otherwise retry time or a moved user region could
|
|
// rewrite already-earned statistics.
|
|
want.AttributionAtMS = time.Date(2026, time.July, 15, 0, 0, 0, 0, time.UTC).UnixMilli()
|
|
if _, err := EncodeActivityTemplateFactMessage(want); err == nil || !strings.Contains(err.Error(), "attribution") {
|
|
t.Fatalf("expected attribution mismatch rejection, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestActivityVisitedRequiresPositiveVisitDelta(t *testing.T) {
|
|
visitedAt := time.Date(2026, time.July, 14, 8, 30, 0, 0, time.UTC).UnixMilli()
|
|
fact := ActivityTemplateFactMessage{
|
|
AppCode: "lalu", EventID: "activity:visit:1", EventType: EventTypeActivityVisited,
|
|
TemplateID: "template-1", TemplateCode: "summer_rank", PublishedVersion: 3,
|
|
StatDay: "2026-07-14", RegionID: 9, UserID: 1001, OccurredAtMS: visitedAt,
|
|
}
|
|
if _, err := EncodeActivityTemplateFactMessage(fact); err == nil {
|
|
t.Fatal("expected zero visit delta rejection")
|
|
}
|
|
fact.VisitCountDelta = 1
|
|
if _, err := EncodeActivityTemplateFactMessage(fact); err != nil {
|
|
t.Fatalf("encode activity visit: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestActivityTemplateFactTagExpressionIsSafeAndDeterministic(t *testing.T) {
|
|
expression, err := LegacyCompatibleTagExpression(EventTypeTaskClaimed, EventTypeGiftScored, EventTypeTaskClaimed)
|
|
if err != nil {
|
|
t.Fatalf("expression: %v", err)
|
|
}
|
|
if expression != "activity_template_fact || GiftScored || TaskClaimed" {
|
|
t.Fatalf("unexpected expression: %s", expression)
|
|
}
|
|
if _, err := EventTypeTag("GiftScored || *"); err == nil {
|
|
t.Fatal("expected unsafe tag rejection")
|
|
}
|
|
}
|