2026-05-14 17:44:16 +08:00

200 lines
7.5 KiB
Go

package achievement_test
import (
"context"
"fmt"
"testing"
"time"
"google.golang.org/grpc"
walletv1 "hyapp.local/api/proto/wallet/v1"
"hyapp/pkg/appcode"
domain "hyapp/services/activity-service/internal/domain/achievement"
achievementservice "hyapp/services/activity-service/internal/service/achievement"
"hyapp/services/activity-service/internal/testutil/mysqltest"
)
func TestAchievementUnlockBadgeAndRewardFlow(t *testing.T) {
svc, wallet := newAchievementService(t)
ctx := appcode.WithContext(context.Background(), "lalu")
definition, created, err := svc.UpsertAchievementDefinition(ctx, domain.DefinitionCommand{
CollectionID: domain.CollectionOrdinary,
CollectionType: domain.CollectionOrdinary,
AchievementType: domain.TypeOrdinary,
Title: "任务达人",
Status: domain.StatusActive,
PrimaryBadgeResourceID: 7001,
RewardResourceGroupID: 9001,
AutoPinPolicy: domain.AutoPinIfEmpty,
HonorVisibility: true,
OperatorAdminID: 90001,
Conditions: []domain.ConditionCommand{
{MetricType: "task_completed_count", TargetValue: 2, TargetUnit: "count"},
},
})
if err != nil || !created {
t.Fatalf("UpsertAchievementDefinition failed: created=%v err=%v", created, err)
}
first, err := svc.ConsumeAchievementEvent(ctx, domain.Event{
EventID: "ach-event-1",
EventType: "TaskCompleted",
SourceService: "activity-service",
UserID: 10001,
MetricType: "task_completed_count",
Value: 1,
OccurredAtMS: fixedAchievementNow().UnixMilli(),
})
if err != nil {
t.Fatalf("ConsumeAchievementEvent first failed: %v", err)
}
if first.MatchedAchievementCount != 1 || first.UnlockedAchievementCount != 0 {
t.Fatalf("first event mismatch: %+v", first)
}
second, err := svc.ConsumeAchievementEvent(ctx, domain.Event{
EventID: "ach-event-2",
EventType: "TaskCompleted",
SourceService: "activity-service",
UserID: 10001,
MetricType: "task_completed_count",
Value: 1,
OccurredAtMS: fixedAchievementNow().UnixMilli(),
})
if err != nil {
t.Fatalf("ConsumeAchievementEvent second failed: %v", err)
}
if second.UnlockedAchievementCount != 1 || second.RewardJobCount != 1 {
t.Fatalf("second event mismatch: %+v", second)
}
duplicate, err := svc.ConsumeAchievementEvent(ctx, domain.Event{
EventID: "ach-event-2",
EventType: "TaskCompleted",
SourceService: "activity-service",
UserID: 10001,
MetricType: "task_completed_count",
Value: 1,
OccurredAtMS: fixedAchievementNow().UnixMilli(),
})
if err != nil || duplicate.Status != domain.EventStatusConsumed {
t.Fatalf("duplicate event should return existing consumed status: result=%+v err=%v", duplicate, err)
}
items, total, err := svc.ListAchievements(ctx, domain.ListQuery{UserID: 10001, CollectionType: domain.CollectionOrdinary})
if err != nil {
t.Fatalf("ListAchievements failed: %v", err)
}
if total != 1 || len(items) != 1 || items[0].AchievementID != definition.AchievementID || items[0].UserStatus != domain.UnlockStatusUnlocked || items[0].ProgressValue != 2 {
t.Fatalf("achievement read mismatch: total=%d items=%+v", total, items)
}
profile, err := svc.ListBadgeProfile(ctx, 10001)
if err != nil {
t.Fatalf("ListBadgeProfile failed: %v", err)
}
if len(profile.HonorBadges) != 1 || profile.HonorBadges[0].ResourceID != 7001 {
t.Fatalf("honor badge mismatch: %+v", profile)
}
if len(profile.ProfileTileBadges) != 1 || profile.ProfileTileBadges[0].ResourceID != 7001 {
t.Fatalf("profile tile badge mismatch: %+v", profile)
}
if _, err := svc.SetBadgeDisplay(ctx, domain.DisplayCommand{
UserID: 10001,
Slot: domain.BadgeSlotProfileTile,
CommandID: "cmd-badge-display-1",
Items: []domain.BadgeDisplayItem{
{ResourceID: 7001},
},
}); err != nil {
t.Fatalf("SetBadgeDisplay failed: %v", err)
}
claimed, processed, success, failure, hasMore, err := svc.ProcessAchievementRewardBatch(ctx, "run-1", "worker-1", 10, time.Second)
if err != nil {
t.Fatalf("ProcessAchievementRewardBatch failed: %v", err)
}
if claimed != 1 || processed != 1 || success != 1 || failure != 0 || hasMore || len(wallet.grants) != 1 {
t.Fatalf("reward batch mismatch: claimed=%d processed=%d success=%d failure=%d has_more=%v grants=%d", claimed, processed, success, failure, hasMore, len(wallet.grants))
}
}
func TestWalletBadgeGrantProjectsDisplaySlots(t *testing.T) {
svc, _ := newAchievementService(t)
ctx := appcode.WithContext(context.Background(), "lalu")
result, err := svc.ConsumeAchievementEvent(ctx, domain.Event{
EventID: "wallet-badge-event-1",
EventType: "ResourceGranted",
SourceService: "wallet-service",
UserID: 10001,
MetricType: "wallet_badge_grant",
Value: 1,
OccurredAtMS: fixedAchievementNow().UnixMilli(),
DimensionsJSON: `{"grant_id":"grant-badge-1","grant_source":"admin","items":[{"resource_id":8001,"resource_type":"badge","entitlement_id":"ent-badge-1","metadata_json":"{\"badge_form\":\"tile\"}"},{"resource_id":8002,"resource_type":"badge","entitlement_id":"ent-badge-2","metadata_json":"{\"badge_form\":\"strip\",\"default_slot\":\"profile_strip\"}"}]}`,
})
if err != nil {
t.Fatalf("ConsumeAchievementEvent wallet badge failed: %v", err)
}
if result.Status != domain.EventStatusConsumed {
t.Fatalf("wallet badge event should be consumed, got %+v", result)
}
profile, err := svc.ListBadgeProfile(ctx, 10001)
if err != nil {
t.Fatalf("ListBadgeProfile failed: %v", err)
}
if len(profile.HonorBadges) != 1 || profile.HonorBadges[0].ResourceID != 8001 || profile.HonorBadges[0].SourceType != domain.SourceSystemGrant {
t.Fatalf("honor badge projection mismatch: %+v", profile)
}
if len(profile.ProfileTileBadges) != 1 || profile.ProfileTileBadges[0].ResourceID != 8001 {
t.Fatalf("profile tile projection mismatch: %+v", profile)
}
if len(profile.StripBadges) != 1 || profile.StripBadges[0].ResourceID != 8002 || profile.StripBadges[0].BadgeForm != domain.BadgeFormStrip {
t.Fatalf("strip badge projection mismatch: %+v", profile)
}
duplicate, err := svc.ConsumeAchievementEvent(ctx, domain.Event{
EventID: "wallet-badge-event-1",
EventType: "ResourceGranted",
SourceService: "wallet-service",
UserID: 10001,
MetricType: "wallet_badge_grant",
Value: 1,
OccurredAtMS: fixedAchievementNow().UnixMilli(),
DimensionsJSON: `{"grant_id":"grant-badge-1","grant_source":"admin","items":[]}`,
})
if err != nil || duplicate.Status != domain.EventStatusConsumed {
t.Fatalf("duplicate wallet badge event should return existing status: result=%+v err=%v", duplicate, err)
}
}
func newAchievementService(t *testing.T) (*achievementservice.Service, *fakeAchievementWallet) {
t.Helper()
repository := mysqltest.NewRepository(t)
wallet := &fakeAchievementWallet{}
svc := achievementservice.New(repository, wallet)
svc.SetClock(fixedAchievementNow)
return svc, wallet
}
func fixedAchievementNow() time.Time {
return time.Date(2026, 5, 14, 4, 0, 0, 0, time.UTC)
}
type fakeAchievementWallet struct {
grants []*walletv1.GrantResourceGroupRequest
}
func (f *fakeAchievementWallet) GrantResourceGroup(_ context.Context, req *walletv1.GrantResourceGroupRequest, _ ...grpc.CallOption) (*walletv1.ResourceGrantResponse, error) {
f.grants = append(f.grants, req)
return &walletv1.ResourceGrantResponse{
Grant: &walletv1.ResourceGrant{
GrantId: fmt.Sprintf("achievement-grant-%d", len(f.grants)),
Status: "succeeded",
},
}, nil
}