413 lines
21 KiB
Go
413 lines
21 KiB
Go
package activitytemplate
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"hyapp-admin-server/internal/integration/activityclient"
|
|
"hyapp-admin-server/internal/integration/statisticsclient"
|
|
"hyapp-admin-server/internal/integration/userclient"
|
|
"hyapp-admin-server/internal/middleware"
|
|
activityv1 "hyapp.local/api/proto/activity/v1"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
type revisionConflictClient struct {
|
|
activityclient.Client
|
|
}
|
|
|
|
type activityTemplateDataOwnerClient struct {
|
|
activityclient.Client
|
|
leaderboard *activityv1.AdminListActivityTemplateLeaderboardResponse
|
|
rewards *activityv1.ListActivityTemplateRewardJobsResponse
|
|
retryReward *activityv1.RetryActivityTemplateRewardJobResponse
|
|
claims *activityv1.ListActivityTemplateTaskClaimsResponse
|
|
retryClaim *activityv1.RetryActivityTemplateTaskClaimResponse
|
|
lastClaims *activityv1.ListActivityTemplateTaskClaimsRequest
|
|
lastRetry *activityv1.RetryActivityTemplateTaskClaimRequest
|
|
lastRewards *activityv1.ListActivityTemplateRewardJobsRequest
|
|
lastRewardRetry *activityv1.RetryActivityTemplateRewardJobRequest
|
|
}
|
|
|
|
type activityTemplateVersionsOwnerClient struct {
|
|
activityclient.Client
|
|
response *activityv1.ListActivityTemplateVersionsResponse
|
|
}
|
|
|
|
func (f activityTemplateVersionsOwnerClient) ListActivityTemplateVersions(context.Context, *activityv1.ListActivityTemplateVersionsRequest) (*activityv1.ListActivityTemplateVersionsResponse, error) {
|
|
return f.response, nil
|
|
}
|
|
|
|
func (f activityTemplateDataOwnerClient) ListActivityTemplateLeaderboard(context.Context, *activityv1.AdminListActivityTemplateLeaderboardRequest) (*activityv1.AdminListActivityTemplateLeaderboardResponse, error) {
|
|
return f.leaderboard, nil
|
|
}
|
|
|
|
func (f *activityTemplateDataOwnerClient) ListActivityTemplateRewardJobs(_ context.Context, req *activityv1.ListActivityTemplateRewardJobsRequest) (*activityv1.ListActivityTemplateRewardJobsResponse, error) {
|
|
f.lastRewards = req
|
|
return f.rewards, nil
|
|
}
|
|
|
|
func (f *activityTemplateDataOwnerClient) RetryActivityTemplateRewardJob(_ context.Context, req *activityv1.RetryActivityTemplateRewardJobRequest) (*activityv1.RetryActivityTemplateRewardJobResponse, error) {
|
|
f.lastRewardRetry = req
|
|
return f.retryReward, nil
|
|
}
|
|
|
|
func (f *activityTemplateDataOwnerClient) ListActivityTemplateTaskClaims(_ context.Context, req *activityv1.ListActivityTemplateTaskClaimsRequest) (*activityv1.ListActivityTemplateTaskClaimsResponse, error) {
|
|
f.lastClaims = req
|
|
return f.claims, nil
|
|
}
|
|
|
|
func (f *activityTemplateDataOwnerClient) RetryActivityTemplateTaskClaim(_ context.Context, req *activityv1.RetryActivityTemplateTaskClaimRequest) (*activityv1.RetryActivityTemplateTaskClaimResponse, error) {
|
|
f.lastRetry = req
|
|
return f.retryClaim, nil
|
|
}
|
|
|
|
type captureActivityTemplateUsers struct {
|
|
calls int
|
|
userIDs []int64
|
|
users map[int64]*userclient.User
|
|
err error
|
|
}
|
|
|
|
func (f *captureActivityTemplateUsers) BatchGetUsers(_ context.Context, req userclient.BatchGetUsersRequest) (map[int64]*userclient.User, error) {
|
|
f.calls++
|
|
f.userIDs = append([]int64(nil), req.UserIDs...)
|
|
return f.users, f.err
|
|
}
|
|
|
|
type captureActivityTemplateDataClient struct {
|
|
query statisticsclient.ActivityTemplateDataQuery
|
|
}
|
|
|
|
func (c *captureActivityTemplateDataClient) ActivityTemplateData(_ context.Context, query statisticsclient.ActivityTemplateDataQuery) (statisticsclient.ActivityTemplateData, error) {
|
|
c.query = query
|
|
return statisticsclient.ActivityTemplateData{
|
|
Overview: statisticsclient.ActivityTemplateOverview{TemplateID: query.TemplateID, PublishedVersion: query.PublishedVersion, VisitCount: 13},
|
|
Trend: []statisticsclient.ActivityTemplateTrendPoint{{StatDay: "2026-07-14", VisitCount: 7}}, Gifts: []statisticsclient.ActivityTemplateGiftStat{},
|
|
Tasks: []statisticsclient.ActivityTemplateTaskStat{}, Granularity: "day", Timezone: "UTC",
|
|
StartMS: query.StartMS, EndMS: query.EndMS, RegionID: query.RegionID,
|
|
}, nil
|
|
}
|
|
|
|
func (revisionConflictClient) UpdateActivityTemplate(context.Context, *activityv1.UpdateActivityTemplateRequest) (*activityv1.UpdateActivityTemplateResponse, error) {
|
|
return nil, status.Error(codes.FailedPrecondition, "activity template revision changed")
|
|
}
|
|
|
|
func TestUpdateMapsRevisionConflictToHTTP409(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Request = httptest.NewRequest(http.MethodPut, "/api/v1/admin/activities/templates/acttpl_1", strings.NewReader(`{
|
|
"template_code":"gift_challenge_2026",
|
|
"name":"Gift Challenge",
|
|
"activity_type":"gift_challenge",
|
|
"expected_revision":7
|
|
}`))
|
|
ctx.Request.Header.Set("Content-Type", "application/json")
|
|
ctx.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}
|
|
|
|
New(revisionConflictClient{}, nil).Update(ctx)
|
|
|
|
if recorder.Code != http.StatusConflict {
|
|
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusConflict, recorder.Body.String())
|
|
}
|
|
var body struct {
|
|
Code int `json:"code"`
|
|
}
|
|
if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if body.Code != 40900 {
|
|
t.Fatalf("response code = %d, want 40900", body.Code)
|
|
}
|
|
}
|
|
|
|
func TestListRejectsMalformedTimeFilterBeforeCallingOwnerService(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates?start_ms=not-a-time", nil)
|
|
|
|
New(nil, nil).List(ctx)
|
|
|
|
if recorder.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusBadRequest, recorder.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestVersionsExposeActualRuntimeWindow(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates/acttpl_1/versions?page=1&page_size=20", nil)
|
|
ctx.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}
|
|
client := activityTemplateVersionsOwnerClient{response: &activityv1.ListActivityTemplateVersionsResponse{
|
|
Total: 1,
|
|
Versions: []*activityv1.ActivityTemplateVersion{{
|
|
TemplateId: "acttpl_1", VersionNo: 1, Snapshot: &activityv1.ActivityTemplate{TemplateId: "acttpl_1", LifecycleStatus: "disabled"},
|
|
PublishedByAdminId: 90001, PublishedAtMs: 100, RuntimeFromMs: 100, RuntimeToMs: 500,
|
|
}},
|
|
}}
|
|
|
|
New(client, nil).Versions(ctx)
|
|
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("status = %d; body=%s", recorder.Code, recorder.Body.String())
|
|
}
|
|
var body struct {
|
|
Data struct {
|
|
Items []struct {
|
|
RuntimeFromMS int64 `json:"runtime_from_ms"`
|
|
RuntimeToMS int64 `json:"runtime_to_ms"`
|
|
Snapshot struct {
|
|
LifecycleStatus string `json:"lifecycle_status"`
|
|
} `json:"snapshot"`
|
|
} `json:"items"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if len(body.Data.Items) != 1 || body.Data.Items[0].RuntimeFromMS != 100 || body.Data.Items[0].RuntimeToMS != 500 || body.Data.Items[0].Snapshot.LifecycleStatus != "disabled" {
|
|
t.Fatalf("version runtime response = %+v", body.Data.Items)
|
|
}
|
|
}
|
|
|
|
func TestDataNormalizesRequestedWindowToUTCWholeDays(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
stats := &captureActivityTemplateDataClient{}
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
start := time.Date(2026, 7, 14, 12, 30, 0, 0, time.UTC).UnixMilli()
|
|
end := time.Date(2026, 7, 15, 8, 45, 0, 0, time.UTC).UnixMilli()
|
|
ctx.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates/acttpl_1/data?version_no=3®ion_id=7&start_ms="+strconv.FormatInt(start, 10)+"&end_ms="+strconv.FormatInt(end, 10), nil)
|
|
ctx.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}
|
|
|
|
New(nil, nil, stats).Data(ctx)
|
|
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("status = %d; body=%s", recorder.Code, recorder.Body.String())
|
|
}
|
|
wantStart := time.Date(2026, 7, 14, 0, 0, 0, 0, time.UTC).UnixMilli()
|
|
wantEnd := time.Date(2026, 7, 16, 0, 0, 0, 0, time.UTC).UnixMilli()
|
|
if stats.query.TemplateID != "acttpl_1" || stats.query.PublishedVersion != 3 || stats.query.RegionID != 7 ||
|
|
stats.query.StartMS != wantStart || stats.query.EndMS != wantEnd {
|
|
t.Fatalf("normalized statistics query = %+v", stats.query)
|
|
}
|
|
var body struct {
|
|
Data statisticsclient.ActivityTemplateData `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if body.Data.StartMS != wantStart || body.Data.EndMS != wantEnd || body.Data.Granularity != "day" || body.Data.Timezone != "UTC" ||
|
|
body.Data.Overview.VisitCount != 13 || len(body.Data.Trend) != 1 || body.Data.Trend[0].VisitCount != 7 {
|
|
t.Fatalf("effective range not echoed: %+v", body.Data)
|
|
}
|
|
}
|
|
|
|
func TestLeaderboardAndRewardsBatchDecorateUsersWithoutOwningProfileFacts(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
owner := activityTemplateDataOwnerClient{
|
|
leaderboard: &activityv1.AdminListActivityTemplateLeaderboardResponse{
|
|
Period: &activityv1.ActivityTemplateRankPeriod{TemplateId: "acttpl_1", BoardType: "daily", PeriodKey: "2026-07-14"},
|
|
Entries: []*activityv1.ActivityTemplateLeaderboardEntry{
|
|
{RankNo: 1, UserId: 501, Score: 900}, {RankNo: 2, UserId: 501, Score: 800}, {RankNo: 3, UserId: 502, Score: 700},
|
|
}, Total: 3,
|
|
},
|
|
rewards: &activityv1.ListActivityTemplateRewardJobsResponse{
|
|
Jobs: []*activityv1.ActivityTemplateRewardJob{{RewardJobId: "job-1", UserId: 502, Status: "granted"}}, Total: 1,
|
|
},
|
|
}
|
|
users := &captureActivityTemplateUsers{users: map[int64]*userclient.User{
|
|
501: {UserID: 501, DisplayUserID: "L501", Username: "first", Avatar: "first.png"},
|
|
502: {UserID: 502, DisplayUserID: "L502", Username: "second", Avatar: "second.png"},
|
|
}}
|
|
handler := New(&owner, nil).BindUserBatchGetter(users)
|
|
|
|
leaderboardRecorder := httptest.NewRecorder()
|
|
leaderboardContext, _ := gin.CreateTestContext(leaderboardRecorder)
|
|
leaderboardContext.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates/acttpl_1/leaderboard?version_no=1&board_type=daily&period_key=2026-07-14", nil)
|
|
leaderboardContext.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}
|
|
handler.Leaderboard(leaderboardContext)
|
|
if leaderboardRecorder.Code != http.StatusOK || users.calls != 1 || len(users.userIDs) != 2 || users.userIDs[0] != 501 || users.userIDs[1] != 502 {
|
|
t.Fatalf("leaderboard batch enrichment: status=%d calls=%d ids=%v body=%s", leaderboardRecorder.Code, users.calls, users.userIDs, leaderboardRecorder.Body.String())
|
|
}
|
|
var leaderboardBody struct {
|
|
Data struct {
|
|
Items []struct {
|
|
User *activityTemplateUserDTO `json:"user"`
|
|
} `json:"items"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(leaderboardRecorder.Body.Bytes(), &leaderboardBody); err != nil || len(leaderboardBody.Data.Items) != 3 || leaderboardBody.Data.Items[0].User == nil || leaderboardBody.Data.Items[0].User.DisplayUserID != "L501" {
|
|
t.Fatalf("leaderboard user decoration mismatch: err=%v body=%s", err, leaderboardRecorder.Body.String())
|
|
}
|
|
|
|
users.calls, users.userIDs = 0, nil
|
|
rewardsRecorder := httptest.NewRecorder()
|
|
rewardsContext, _ := gin.CreateTestContext(rewardsRecorder)
|
|
rewardsContext.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates/acttpl_1/rewards?version_no=1", nil)
|
|
rewardsContext.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}
|
|
handler.RewardJobs(rewardsContext)
|
|
if rewardsRecorder.Code != http.StatusOK || users.calls != 1 || len(users.userIDs) != 1 || users.userIDs[0] != 502 || !strings.Contains(rewardsRecorder.Body.String(), `"display_user_id":"L502"`) {
|
|
t.Fatalf("reward user decoration: status=%d calls=%d ids=%v body=%s", rewardsRecorder.Code, users.calls, users.userIDs, rewardsRecorder.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestRewardJobsRequiresPublishedVersion(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates/acttpl_1/rewards", nil)
|
|
ctx.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}
|
|
|
|
New(&activityTemplateDataOwnerClient{}, nil).RewardJobs(ctx)
|
|
|
|
if recorder.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusBadRequest, recorder.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestRewardJobsExposeDeadBackoffAndAllowExplicitRevival(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
dead := &activityv1.ActivityTemplateRewardJob{
|
|
RewardJobId: "job-dead", TemplateId: "acttpl_1", VersionNo: 2, UserId: 502,
|
|
Status: "dead", FailureReason: "maximum delivery attempts exhausted", AttemptCount: 8,
|
|
MaxAttempts: 8, NextRetryAtMs: 0,
|
|
}
|
|
running := proto.Clone(dead).(*activityv1.ActivityTemplateRewardJob)
|
|
running.Status, running.AttemptCount, running.MaxAttempts = "running", 9, 9
|
|
owner := &activityTemplateDataOwnerClient{
|
|
rewards: &activityv1.ListActivityTemplateRewardJobsResponse{Jobs: []*activityv1.ActivityTemplateRewardJob{dead}, Total: 1},
|
|
retryReward: &activityv1.RetryActivityTemplateRewardJobResponse{Job: running},
|
|
}
|
|
handler := New(owner, nil)
|
|
|
|
listRecorder := httptest.NewRecorder()
|
|
listContext, _ := gin.CreateTestContext(listRecorder)
|
|
listContext.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates/acttpl_1/rewards?version_no=2&status=dead", nil)
|
|
listContext.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}
|
|
handler.RewardJobs(listContext)
|
|
if listRecorder.Code != http.StatusOK || owner.lastRewards == nil || owner.lastRewards.GetStatus() != "dead" ||
|
|
!strings.Contains(listRecorder.Body.String(), `"max_attempts":8`) || !strings.Contains(listRecorder.Body.String(), `"next_retry_at_ms":0`) {
|
|
t.Fatalf("dead reward list mismatch: status=%d request=%+v body=%s", listRecorder.Code, owner.lastRewards, listRecorder.Body.String())
|
|
}
|
|
|
|
retryRecorder := httptest.NewRecorder()
|
|
retryContext, _ := gin.CreateTestContext(retryRecorder)
|
|
retryContext.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/activities/templates/acttpl_1/rewards/job-dead/retry", nil)
|
|
retryContext.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}, {Key: "reward_job_id", Value: "job-dead"}}
|
|
retryContext.Set(middleware.ContextUserID, uint(90001))
|
|
handler.RetryRewardJob(retryContext)
|
|
if retryRecorder.Code != http.StatusOK || owner.lastRewardRetry == nil || owner.lastRewardRetry.GetRewardJobId() != "job-dead" ||
|
|
owner.lastRewardRetry.GetOperatorAdminId() != 90001 || !strings.Contains(retryRecorder.Body.String(), `"max_attempts":9`) {
|
|
t.Fatalf("dead reward revival mismatch: status=%d request=%+v body=%s", retryRecorder.Code, owner.lastRewardRetry, retryRecorder.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestTaskClaimsFilterEnrichAndRetryPreserveImmutableAttribution(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
rewardItem := &activityv1.ActivityTemplateRewardItem{ItemType: "resource", ResourceId: 91, ResourceType: "avatar_frame", Name: "Frame", Quantity: 1}
|
|
claim := &activityv1.ActivityTemplateTaskClaim{
|
|
ClaimId: "claim-1", CommandId: "device-command", TemplateId: "acttpl_1", TemplateCode: "summer",
|
|
VersionNo: 3, TaskDay: "2026-07-14", TaskKey: "send_gift", UserId: 503,
|
|
RewardResourceGroupId: 81, WalletCommandId: "wallet-command", WalletGrantId: "wallet-grant",
|
|
Status: "failed", FailureReason: "timeout", AttemptCount: 2,
|
|
RewardSnapshotId: "rgs-task", RewardSnapshotHash: strings.Repeat("a", 64), RewardName: "Task reward",
|
|
RewardItems: []*activityv1.ActivityTemplateRewardItem{rewardItem}, EarningRegionId: 210, AttributionAtMs: 1784073000000,
|
|
}
|
|
owner := &activityTemplateDataOwnerClient{
|
|
claims: &activityv1.ListActivityTemplateTaskClaimsResponse{Claims: []*activityv1.ActivityTemplateTaskClaim{claim}, Total: 1},
|
|
retryClaim: &activityv1.RetryActivityTemplateTaskClaimResponse{Claim: claim},
|
|
}
|
|
users := &captureActivityTemplateUsers{users: map[int64]*userclient.User{
|
|
503: {UserID: 503, DisplayUserID: "L503", Username: "winner", Avatar: "winner.png"},
|
|
}}
|
|
handler := New(owner, nil).BindUserBatchGetter(users)
|
|
|
|
listRecorder := httptest.NewRecorder()
|
|
listContext, _ := gin.CreateTestContext(listRecorder)
|
|
listContext.Request = httptest.NewRequest(http.MethodGet,
|
|
"/api/v1/admin/activities/templates/acttpl_1/task-claims?version_no=3&task_day=2026-07-14&task_key=send_gift&user_id=503&status=failed&page=2&page_size=10", nil)
|
|
listContext.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}
|
|
handler.TaskClaims(listContext)
|
|
|
|
if listRecorder.Code != http.StatusOK || owner.lastClaims == nil || owner.lastClaims.GetVersionNo() != 3 ||
|
|
owner.lastClaims.GetTaskDay() != "2026-07-14" || owner.lastClaims.GetTaskKey() != "send_gift" ||
|
|
owner.lastClaims.GetUserId() != 503 || owner.lastClaims.GetStatus() != "failed" || owner.lastClaims.GetPage() != 2 || owner.lastClaims.GetPageSize() != 10 {
|
|
t.Fatalf("task claim filters mismatch: status=%d request=%+v body=%s", listRecorder.Code, owner.lastClaims, listRecorder.Body.String())
|
|
}
|
|
var listBody struct {
|
|
Data struct {
|
|
Items []taskClaimDTO `json:"items"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(listRecorder.Body.Bytes(), &listBody); err != nil || len(listBody.Data.Items) != 1 {
|
|
t.Fatalf("decode task claims: err=%v body=%s", err, listRecorder.Body.String())
|
|
}
|
|
item := listBody.Data.Items[0]
|
|
if item.RewardSnapshotHash != strings.Repeat("a", 64) || item.RewardName != "Task reward" || len(item.RewardItems) != 1 ||
|
|
item.EarningRegionID != 210 || item.AttributionAtMS != 1784073000000 || item.User == nil || item.User.DisplayUserID != "L503" {
|
|
t.Fatalf("task claim immutable/admin view mismatch: %+v", item)
|
|
}
|
|
|
|
retryRecorder := httptest.NewRecorder()
|
|
retryContext, _ := gin.CreateTestContext(retryRecorder)
|
|
retryContext.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/activities/templates/acttpl_1/task-claims/claim-1/retry", nil)
|
|
retryContext.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}, {Key: "claim_id", Value: "claim-1"}}
|
|
retryContext.Set(middleware.ContextUserID, uint(90001))
|
|
handler.RetryTaskClaim(retryContext)
|
|
if retryRecorder.Code != http.StatusOK || owner.lastRetry == nil || owner.lastRetry.GetTemplateId() != "acttpl_1" ||
|
|
owner.lastRetry.GetClaimId() != "claim-1" || owner.lastRetry.GetOperatorAdminId() != 90001 {
|
|
t.Fatalf("task claim retry mismatch: status=%d request=%+v body=%s", retryRecorder.Code, owner.lastRetry, retryRecorder.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAdminRewardDTOsExposePublishedSnapshots(t *testing.T) {
|
|
item := &activityv1.ActivityTemplateRewardItem{ItemType: "wallet_asset", WalletAssetType: "coin", WalletAssetAmount: 500, Name: "Coins"}
|
|
template := templateFromProto(&activityv1.ActivityTemplate{
|
|
Tasks: []*activityv1.ActivityTemplateTask{{TaskKey: "visit", RewardSnapshotId: "task-snapshot", RewardSnapshotHash: strings.Repeat("b", 64), RewardName: "Visit reward", RewardItems: []*activityv1.ActivityTemplateRewardItem{item}}},
|
|
RankRewards: []*activityv1.ActivityTemplateRankReward{{BoardType: "total", RankFrom: 1, RankTo: 1, RewardSnapshotId: "rank-snapshot", RewardSnapshotHash: strings.Repeat("c", 64), RewardName: "Champion", RewardItems: []*activityv1.ActivityTemplateRewardItem{item}}},
|
|
})
|
|
job := rewardJobFromProto(&activityv1.ActivityTemplateRewardJob{
|
|
RewardJobId: "job-1", RewardSnapshotId: "rank-snapshot", RewardSnapshotHash: strings.Repeat("c", 64), RewardName: "Champion", RewardItems: []*activityv1.ActivityTemplateRewardItem{item},
|
|
}, nil)
|
|
if len(template.Tasks) != 1 || template.Tasks[0].RewardSnapshotHash != strings.Repeat("b", 64) || len(template.Tasks[0].RewardItems) != 1 ||
|
|
len(template.RankRewards) != 1 || template.RankRewards[0].RewardSnapshotHash != strings.Repeat("c", 64) || len(template.RankRewards[0].RewardItems) != 1 ||
|
|
job.RewardSnapshotID != "rank-snapshot" || job.RewardSnapshotHash != strings.Repeat("c", 64) || len(job.RewardItems) != 1 {
|
|
t.Fatalf("published reward snapshots were dropped: template=%+v job=%+v", template, job)
|
|
}
|
|
}
|
|
|
|
func TestLeaderboardProfileFailureDoesNotHideRankFacts(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
owner := activityTemplateDataOwnerClient{leaderboard: &activityv1.AdminListActivityTemplateLeaderboardResponse{
|
|
Period: &activityv1.ActivityTemplateRankPeriod{TemplateId: "acttpl_1", BoardType: "total", PeriodKey: "total"},
|
|
Entries: []*activityv1.ActivityTemplateLeaderboardEntry{{RankNo: 1, UserId: 501, Score: 900}}, Total: 1,
|
|
}}
|
|
users := &captureActivityTemplateUsers{err: errors.New("user service unavailable")}
|
|
handler := New(&owner, nil).BindUserBatchGetter(users)
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/activities/templates/acttpl_1/leaderboard?version_no=1&board_type=total", nil)
|
|
ctx.Params = gin.Params{{Key: "template_id", Value: "acttpl_1"}}
|
|
|
|
handler.Leaderboard(ctx)
|
|
|
|
if recorder.Code != http.StatusOK || users.calls != 1 || !strings.Contains(recorder.Body.String(), `"score":900`) || strings.Contains(recorder.Body.String(), `"user":`) {
|
|
t.Fatalf("profile degradation hid or changed rank facts: status=%d body=%s", recorder.Code, recorder.Body.String())
|
|
}
|
|
}
|