bindbox-game/internal/api/activity/threshold_activities_app_test.go

243 lines
9.7 KiB
Go

package app
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/logger"
"bindbox-game/internal/proposal"
"bindbox-game/internal/repository/mysql"
)
func setupThresholdAppTestRouter(t *testing.T) (mysql.Repo, http.Handler) {
t.Helper()
repo, err := mysql.NewSQLiteRepoForTest()
if err != nil {
t.Fatal(err)
}
ddls := []string{
`CREATE TABLE threshold_activities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
type TEXT NOT NULL,
qualification_mode TEXT NOT NULL,
spend_threshold_amount INTEGER NOT NULL DEFAULT 0,
invite_threshold_count INTEGER NOT NULL DEFAULT 0,
invite_effective_amount INTEGER NOT NULL DEFAULT 0,
min_participants INTEGER NOT NULL DEFAULT 1,
start_time DATETIME NOT NULL,
end_time DATETIME NOT NULL,
draw_time DATETIME NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
description TEXT,
cover_image TEXT NOT NULL DEFAULT '',
draw_batch TEXT NOT NULL DEFAULT '',
abort_reason TEXT NOT NULL DEFAULT '',
aborted_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
deleted_at DATETIME
)`,
`CREATE TABLE threshold_activity_prizes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
activity_id INTEGER NOT NULL,
reward_type TEXT NOT NULL,
reward_ref_id INTEGER NOT NULL,
reward_name_snapshot TEXT NOT NULL DEFAULT '',
reward_image_snapshot TEXT NOT NULL DEFAULT '',
reward_value_snapshot_cents INTEGER NOT NULL DEFAULT 0,
cost_snapshot_cents INTEGER NOT NULL DEFAULT 0,
quantity INTEGER NOT NULL DEFAULT 0,
remaining_quantity INTEGER NOT NULL DEFAULT 0,
sort INTEGER NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE TABLE threshold_activity_participants (
id INTEGER PRIMARY KEY AUTOINCREMENT,
activity_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
period_key TEXT NOT NULL,
qualification_source TEXT NOT NULL DEFAULT '',
paid_amount_snapshot INTEGER NOT NULL DEFAULT 0,
effective_invite_count_snapshot INTEGER NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(activity_id, user_id, period_key)
)`,
`CREATE TABLE threshold_activity_winners (
id INTEGER PRIMARY KEY AUTOINCREMENT,
activity_id INTEGER NOT NULL,
prize_id INTEGER NOT NULL,
reward_type TEXT NOT NULL,
reward_ref_id INTEGER NOT NULL,
prize_name_snapshot TEXT NOT NULL DEFAULT '',
prize_image_snapshot TEXT NOT NULL DEFAULT '',
prize_value_snapshot_cents INTEGER NOT NULL DEFAULT 0,
user_id INTEGER NOT NULL,
grant_record_type TEXT NOT NULL DEFAULT '',
grant_record_id INTEGER NOT NULL DEFAULT 0,
cost_cents INTEGER NOT NULL DEFAULT 0,
draw_batch TEXT NOT NULL DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nickname TEXT NOT NULL,
avatar TEXT,
invite_code TEXT NOT NULL,
inviter_id INTEGER,
status INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
deleted_at DATETIME
)`,
`CREATE TABLE user_invites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
inviter_id INTEGER NOT NULL,
invitee_id INTEGER NOT NULL,
invite_code TEXT NOT NULL DEFAULT '',
reward_points INTEGER NOT NULL DEFAULT 0,
rewarded_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
deleted_at DATETIME,
is_effective BOOLEAN NOT NULL DEFAULT 0,
accumulated_amount INTEGER NOT NULL DEFAULT 0
)`,
`CREATE TABLE orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
user_id INTEGER NOT NULL,
order_no TEXT NOT NULL,
source_type INTEGER NOT NULL DEFAULT 1,
total_amount INTEGER NOT NULL DEFAULT 0,
discount_amount INTEGER NOT NULL DEFAULT 0,
points_amount INTEGER NOT NULL DEFAULT 0,
actual_amount INTEGER NOT NULL DEFAULT 0,
status INTEGER NOT NULL DEFAULT 1,
pay_preorder_id INTEGER NOT NULL DEFAULT 0,
paid_at DATETIME,
cancelled_at DATETIME,
user_address_id INTEGER NOT NULL DEFAULT 0,
is_consumed INTEGER NOT NULL DEFAULT 0,
points_ledger_id INTEGER NOT NULL DEFAULT 0,
coupon_id INTEGER NOT NULL DEFAULT 0,
item_card_id INTEGER NOT NULL DEFAULT 0,
remark TEXT,
ext_order_id TEXT NOT NULL DEFAULT ''
)`,
}
for _, ddl := range ddls {
if err := repo.GetDbW().Exec(ddl).Error; err != nil {
t.Fatal(err)
}
}
lg, err := logger.NewCustomLogger(logger.WithOutputInConsole())
if err != nil {
t.Fatal(err)
}
mux, err := core.New(lg)
if err != nil {
t.Fatal(err)
}
h := New(lg, repo, nil, nil)
public := mux.Group("/api/app")
public.GET("/threshold-activities", h.ListThresholdActivities())
public.GET("/threshold-activities/:id", h.GetThresholdActivity())
dummyAuth := func(ctx core.Context) (proposal.SessionUserInfo, core.BusinessError) {
return proposal.SessionUserInfo{Id: 88}, nil
}
auth := mux.Group("/api/app", core.WrapAuthHandler(dummyAuth))
auth.GET("/threshold-activities/:id/my", h.GetThresholdActivity())
auth.POST("/threshold-activities/:id/join", h.JoinThresholdActivity())
return repo, mux
}
func TestThresholdAppListAndGetMy(t *testing.T) {
repo, mux := setupThresholdAppTestRouter(t)
now := time.Now()
if err := repo.GetDbW().Exec(`INSERT INTO threshold_activities (id, title, type, qualification_mode, spend_threshold_amount, invite_threshold_count, invite_effective_amount, min_participants, start_time, end_time, draw_time, status, description, cover_image, draw_batch, abort_reason, created_at, updated_at) VALUES (1, 'App活动', 'daily', 'spend_only', 1000, 0, 0, 1, ?, ?, ?, 'active', 'desc', '', '', '', ?, ?)`, now.Add(-time.Hour), now.Add(24*time.Hour), now.Add(25*time.Hour), now, now).Error; err != nil {
t.Fatal(err)
}
if err := repo.GetDbW().Exec(`INSERT INTO orders (user_id, order_no, source_type, actual_amount, total_amount, status, paid_at, created_at, updated_at, cancelled_at) VALUES (88, 'APP_OK', 1, 1200, 1200, 2, ?, ?, ?, ?)`, now, now, now, time.Unix(0, 0)).Error; err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/api/app/threshold-activities?page=1&page_size=20", bytes.NewReader([]byte{}))
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("list code=%d body=%s", rr.Code, rr.Body.String())
}
var listed struct {
Total int64 `json:"total"`
List []map[string]any `json:"list"`
}
if err := json.Unmarshal(rr.Body.Bytes(), &listed); err != nil {
t.Fatal(err)
}
if listed.Total != 1 || len(listed.List) != 1 {
t.Fatalf("expected 1 app activity, got total=%d len=%d", listed.Total, len(listed.List))
}
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodGet, "/api/app/threshold-activities/1/my", bytes.NewReader([]byte{}))
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("get my code=%d body=%s", rr.Code, rr.Body.String())
}
var detail map[string]any
if err := json.Unmarshal(rr.Body.Bytes(), &detail); err != nil {
t.Fatal(err)
}
if detail["can_join"] != true {
t.Fatalf("expected can_join=true, got %v", detail["can_join"])
}
progress := detail["qualification_progress"].(map[string]any)
if int(progress["current_paid"].(float64)) != 1200 {
t.Fatalf("expected current_paid=1200, got %v", progress["current_paid"])
}
}
func TestThresholdAppJoinSuccessAndRejectUnqualified(t *testing.T) {
repo, mux := setupThresholdAppTestRouter(t)
now := time.Now()
if err := repo.GetDbW().Exec(`INSERT INTO threshold_activities (id, title, type, qualification_mode, spend_threshold_amount, invite_threshold_count, invite_effective_amount, min_participants, start_time, end_time, draw_time, status, description, cover_image, draw_batch, abort_reason, created_at, updated_at) VALUES (2, 'Join活动', 'daily', 'spend_only', 1000, 0, 0, 1, ?, ?, ?, 'active', 'desc', '', '', '', ?, ?)`, now.Add(-time.Hour), now.Add(24*time.Hour), now.Add(25*time.Hour), now, now).Error; err != nil {
t.Fatal(err)
}
if err := repo.GetDbW().Exec(`INSERT INTO orders (user_id, order_no, source_type, actual_amount, total_amount, status, paid_at, created_at, updated_at, cancelled_at) VALUES (88, 'JOIN_OK', 1, 1500, 1500, 2, ?, ?, ?, ?)`, now, now, now, time.Unix(0, 0)).Error; err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/api/app/threshold-activities/2/join", bytes.NewReader([]byte{}))
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("join success code=%d body=%s", rr.Code, rr.Body.String())
}
var count int64
if err := repo.GetDbW().Table("threshold_activity_participants").Where("activity_id = ? AND user_id = ?", 2, 88).Count(&count).Error; err != nil {
t.Fatal(err)
}
if count != 1 {
t.Fatalf("expected 1 participant after join, got %d", count)
}
if err := repo.GetDbW().Exec(`INSERT INTO threshold_activities (id, title, type, qualification_mode, spend_threshold_amount, invite_threshold_count, invite_effective_amount, min_participants, start_time, end_time, draw_time, status, description, cover_image, draw_batch, abort_reason, created_at, updated_at) VALUES (3, 'Join失败活动', 'daily', 'spend_only', 5000, 0, 0, 1, ?, ?, ?, 'active', 'desc', '', '', '', ?, ?)`, now.Add(-time.Hour), now.Add(24*time.Hour), now.Add(25*time.Hour), now, now).Error; err != nil {
t.Fatal(err)
}
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodPost, "/api/app/threshold-activities/3/join", bytes.NewReader([]byte{}))
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusBadRequest {
t.Fatalf("expected 400 for unqualified join, got %d body=%s", rr.Code, rr.Body.String())
}
}