package userleaderboard import ( "context" "fmt" "os" "testing" "time" ) func TestQueryWindowUsesExistingUTCBoundaries(t *testing.T) { now := time.Date(2026, 5, 20, 15, 30, 0, 0, time.FixedZone("CST", 8*60*60)) todayStart, todayEnd := QueryWindow(PeriodToday, now) if todayStart != time.Date(2026, 5, 20, 0, 0, 0, 0, time.UTC) { t.Fatalf("today start = %s", todayStart) } if !todayEnd.Equal(now.UTC()) { t.Fatalf("today end = %s", todayEnd) } weekStart, _ := QueryWindow(PeriodWeek, now) if weekStart != time.Date(2026, 5, 18, 0, 0, 0, 0, time.UTC) { t.Fatalf("week start = %s", weekStart) } monthStart, _ := QueryWindow(PeriodMonth, now) if monthStart != time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC) { t.Fatalf("month start = %s", monthStart) } } func TestNormalizeAliases(t *testing.T) { if NormalizeBoardType("receiver") != BoardReceived { t.Fatal("receiver should normalize to received") } if NormalizeBoardType("gift_sent") != BoardSent { t.Fatal("gift_sent should normalize to sent") } if NormalizeBoardType("gaming") != BoardGame { t.Fatal("gaming should normalize to game") } if NormalizePeriod("monthly") != PeriodMonth { t.Fatal("monthly should normalize to month") } } func TestStoreKeyPrefixAndBucketAreStable(t *testing.T) { store := NewStore(nil, "activity:user_leaderboard:") weekStart := time.Date(2026, 12, 28, 0, 0, 0, 0, time.UTC) if got := store.scoreKey(" HyApp_Prod ", BoardSent, PeriodWeek, weekStart); got != "activity:user_leaderboard:hyapp_prod:sent:week:202653:scores" { t.Fatalf("score key mismatch: %s", got) } if got := store.itemKey("hyapp_prod", BoardReceived, PeriodToday, time.Date(2026, 6, 26, 0, 0, 0, 0, time.UTC), "10001"); got != "activity:user_leaderboard:hyapp_prod:received:today:20260626:users:10001" { t.Fatalf("item key mismatch: %s", got) } if got := store.scoreKey("huwaa", BoardGame, PeriodMonth, time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)); got != "activity:user_leaderboard:huwaa:game:month:202607:scores" { t.Fatalf("game score key mismatch: %s", got) } } func TestApplyGameEventUsesDedicatedRedisBucket(t *testing.T) { redisAddr := os.Getenv("HYAPP_TEST_REDIS_ADDR") if redisAddr == "" { t.Skip("HYAPP_TEST_REDIS_ADDR is not set") } ctx := context.Background() client, err := NewRedisClient(ctx, redisAddr, "", 0) if err != nil { t.Fatalf("connect test Redis: %v", err) } defer client.Close() prefix := fmt.Sprintf("test:user_leaderboard:%d", time.Now().UnixNano()) defer func() { keys, _, scanErr := client.Scan(ctx, 0, prefix+":*", 100).Result() if scanErr == nil && len(keys) > 0 { _ = client.Del(ctx, keys...).Err() } }() store := NewStore(client, prefix) occurredAt := time.Date(2026, 7, 15, 12, 0, 0, 0, time.UTC) event := GameEvent{ AppCode: "huwaa", EventID: "game_level:test_order_1", UserID: 20002, CoinSpent: 120, OccurredAtMS: occurredAt.UnixMilli(), } applied, err := store.ApplyGameEvent(ctx, event) if err != nil || !applied { t.Fatalf("first game event should apply: applied=%v err=%v", applied, err) } applied, err = store.ApplyGameEvent(ctx, event) if err != nil || applied { t.Fatalf("duplicate game event should no-op: applied=%v err=%v", applied, err) } page, err := store.List(ctx, Query{ AppCode: "huwaa", BoardType: BoardGame, Period: PeriodToday, Page: 1, PageSize: 20, Now: occurredAt.Add(time.Minute), }) if err != nil { t.Fatalf("list game leaderboard: %v", err) } if page.BoardType != BoardGame || page.Total != 1 || len(page.Items) != 1 || page.Items[0].UserID != "20002" || page.Items[0].GiftValue != 120 || page.Items[0].GiftCount != 0 || page.Items[0].TransactionCount != 1 || page.Items[0].LastGiftAtMS != occurredAt.UnixMilli() { t.Fatalf("game leaderboard item mismatch: %+v", page) } sent, err := store.List(ctx, Query{AppCode: "huwaa", BoardType: BoardSent, Period: PeriodToday, Page: 1, PageSize: 20, Now: occurredAt.Add(time.Minute)}) if err != nil || sent.Total != 0 { t.Fatalf("game event must not leak into sent board: page=%+v err=%v", sent, err) } }