bindbox-game/internal/api/admin/users_admin_test.go
2026-03-05 12:50:06 +08:00

302 lines
9.8 KiB
Go
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package admin
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/logger"
"bindbox-game/internal/proposal"
"bindbox-game/internal/repository/mysql"
// dao/model not required in sqlite DDL
)
func Test_ListAppUsers_FilterByID(t *testing.T) {
repo, err := mysql.NewSQLiteRepoForTest()
if err != nil {
t.Fatal(err)
}
if err := repo.GetDbW().Exec(`CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
nickname TEXT NOT NULL,
avatar TEXT,
mobile TEXT,
openid TEXT,
unionid TEXT,
invite_code TEXT NOT NULL,
inviter_id INTEGER,
status INTEGER NOT NULL,
douyin_id TEXT,
channel_id INTEGER,
douyin_user_id TEXT,
remark TEXT NOT NULL DEFAULT ''
)`).Error; err != nil {
t.Fatal(err)
}
if err := repo.GetDbW().Exec(`CREATE TABLE channels (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, code TEXT)`).Error; err != nil {
t.Fatal(err)
}
if err := repo.GetDbW().Exec(`INSERT INTO users (nickname,invite_code,status) VALUES ('u1','c1',1)`).Error; err != nil {
t.Fatal(err)
}
if err := repo.GetDbW().Exec(`INSERT INTO users (nickname,invite_code,status) VALUES ('u2','c2',1)`).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)
}
dummyAuth := func(ctx core.Context) (proposal.SessionUserInfo, core.BusinessError) {
return proposal.SessionUserInfo{Id: 1, IsSuper: 1}, nil
}
adminGroup := mux.Group("/api/admin", core.WrapAuthHandler(dummyAuth))
adminGroup.GET("/users", New(lg, repo, nil).ListAppUsers())
// request with id filter = 1
rr := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/admin/users?id=1&page=1&page_size=20", bytes.NewBufferString(""))
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("code=%d body=%s", rr.Code, rr.Body.String())
}
var rsp map[string]interface{}
if err := json.Unmarshal([]byte(rr.Body.String()), &rsp); err != nil {
t.Fatal(err)
}
if int(rsp["total"].(float64)) != 1 {
t.Fatalf("expect total=1 got %v", rsp["total"])
}
}
func Test_ListUserInvites_WithDateFilterAndHasConsumeOnly(t *testing.T) {
repo, mux := setupListInvitesTestRouter(t)
seedListInvitesTestData(t, repo)
type listInvitesResp struct {
Total int64 `json:"total"`
List []struct {
ID int64 `json:"id"`
TotalConsume int64 `json:"total_consume"`
TotalAssetValue int64 `json:"total_asset_value"`
} `json:"list"`
Summary struct {
TotalConsume int64 `json:"total_consume"`
TotalAsset int64 `json:"total_asset"`
TotalProfit int64 `json:"total_profit"`
} `json:"summary"`
}
// 1) 2026-01 区间,仅看有消费:仅返回 user2
{
rr := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/admin/users/1/invites?page=1&page_size=20&startDate=2026-01-01&endDate=2026-01-31&hasConsumeOnly=true", bytes.NewBufferString(""))
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("jan code=%d body=%s", rr.Code, rr.Body.String())
}
var rsp listInvitesResp
if err := json.Unmarshal(rr.Body.Bytes(), &rsp); err != nil {
t.Fatal(err)
}
if rsp.Total != 1 {
t.Fatalf("jan expect total=1 got %d", rsp.Total)
}
if len(rsp.List) != 1 || rsp.List[0].ID != 2 {
t.Fatalf("jan expect only user2 got %+v", rsp.List)
}
if rsp.List[0].TotalConsume != 100 {
t.Fatalf("jan expect user2 consume=100 got %d", rsp.List[0].TotalConsume)
}
if rsp.List[0].TotalAssetValue != 300 {
t.Fatalf("jan expect user2 asset=300 got %d", rsp.List[0].TotalAssetValue)
}
if rsp.Summary.TotalConsume != 100 || rsp.Summary.TotalAsset != 300 || rsp.Summary.TotalProfit != 200 {
t.Fatalf("jan summary mismatch %+v", rsp.Summary)
}
}
// 2) 同区间,包含全部下线:返回 user2 + user3user3消费为0
{
rr := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/admin/users/1/invites?page=1&page_size=20&startDate=2026-01-01&endDate=2026-01-31&hasConsumeOnly=false", bytes.NewBufferString(""))
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("jan all code=%d body=%s", rr.Code, rr.Body.String())
}
var rsp listInvitesResp
if err := json.Unmarshal(rr.Body.Bytes(), &rsp); err != nil {
t.Fatal(err)
}
if rsp.Total != 2 {
t.Fatalf("jan all expect total=2 got %d", rsp.Total)
}
byID := map[int64]struct {
consume int64
asset int64
}{}
for _, row := range rsp.List {
byID[row.ID] = struct {
consume int64
asset int64
}{consume: row.TotalConsume, asset: row.TotalAssetValue}
}
if byID[2].consume != 100 || byID[2].asset != 300 {
t.Fatalf("jan all user2 mismatch %+v", byID[2])
}
if byID[3].consume != 0 || byID[3].asset != 0 {
t.Fatalf("jan all user3 mismatch %+v", byID[3])
}
}
// 3) 2026-02 区间,仅看有消费:验证 paid_at 为空回落 created_atuser3
{
rr := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/admin/users/1/invites?page=1&page_size=20&startDate=2026-02-01&endDate=2026-02-28&hasConsumeOnly=true", bytes.NewBufferString(""))
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("feb code=%d body=%s", rr.Code, rr.Body.String())
}
var rsp listInvitesResp
if err := json.Unmarshal(rr.Body.Bytes(), &rsp); err != nil {
t.Fatal(err)
}
if rsp.Total != 1 {
t.Fatalf("feb expect total=1 got %d", rsp.Total)
}
if len(rsp.List) != 1 || rsp.List[0].ID != 3 {
t.Fatalf("feb expect only user3 got %+v", rsp.List)
}
if rsp.List[0].TotalConsume != 200 {
t.Fatalf("feb expect user3 consume=200 got %d", rsp.List[0].TotalConsume)
}
if rsp.List[0].TotalAssetValue != 400 {
t.Fatalf("feb expect user3 asset=400 got %d", rsp.List[0].TotalAssetValue)
}
}
}
func Test_ListUserInvites_InvalidDateParams(t *testing.T) {
repo, mux := setupListInvitesTestRouter(t)
seedListInvitesTestData(t, repo)
cases := []url.Values{
{"page": {"1"}, "page_size": {"20"}, "startDate": {"2026-01-01"}},
{"page": {"1"}, "page_size": {"20"}, "startDate": {"2026/01/01"}, "endDate": {"2026-01-31"}},
{"page": {"1"}, "page_size": {"20"}, "startDate": {"2026-02-01"}, "endDate": {"2026-01-31"}},
{"page": {"1"}, "page_size": {"20"}, "startDate": {"2024-01-01"}, "endDate": {"2025-02-01"}},
}
for _, query := range cases {
rr := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/admin/users/1/invites?"+query.Encode(), bytes.NewBufferString(""))
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusBadRequest {
t.Fatalf("query=%s expect 400 got %d body=%s", query.Encode(), rr.Code, rr.Body.String())
}
}
}
func setupListInvitesTestRouter(t *testing.T) (mysql.Repo, http.Handler) {
t.Helper()
repo, err := mysql.NewSQLiteRepoForTest()
if err != nil {
t.Fatal(err)
}
ddls := []string{
`CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
nickname TEXT NOT NULL,
avatar TEXT,
mobile TEXT,
openid TEXT,
unionid TEXT,
invite_code TEXT NOT NULL,
inviter_id INTEGER,
status INTEGER NOT NULL,
douyin_id TEXT,
channel_id INTEGER,
douyin_user_id TEXT,
remark TEXT NOT NULL DEFAULT ''
)`,
`CREATE TABLE orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
status INTEGER NOT NULL,
actual_amount INTEGER NOT NULL,
paid_at DATETIME,
created_at DATETIME NOT NULL
)`,
`CREATE TABLE user_inventory (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
product_id INTEGER,
value_cents INTEGER NOT NULL DEFAULT 0,
status INTEGER NOT NULL,
created_at DATETIME NOT NULL
)`,
`CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
price INTEGER NOT NULL DEFAULT 0
)`,
}
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)
}
dummyAuth := func(ctx core.Context) (proposal.SessionUserInfo, core.BusinessError) {
return proposal.SessionUserInfo{Id: 1, IsSuper: 1}, nil
}
adminGroup := mux.Group("/api/admin", core.WrapAuthHandler(dummyAuth))
adminGroup.GET("/users/:user_id/invites", New(lg, repo, nil).ListUserInvites())
return repo, mux
}
func seedListInvitesTestData(t *testing.T, repo mysql.Repo) {
t.Helper()
sqls := []string{
`INSERT INTO users (id, nickname, invite_code, inviter_id, status, created_at, updated_at, remark) VALUES (1, 'inviter', 'i1', 0, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', '')`,
`INSERT INTO users (id, nickname, invite_code, inviter_id, status, created_at, updated_at, remark) VALUES (2, 'invitee-a', 'a1', 1, 1, '2026-01-02 00:00:00', '2026-01-02 00:00:00', '')`,
`INSERT INTO users (id, nickname, invite_code, inviter_id, status, created_at, updated_at, remark) VALUES (3, 'invitee-b', 'b1', 1, 1, '2026-01-03 00:00:00', '2026-01-03 00:00:00', '')`,
`INSERT INTO products (id, price) VALUES (1, 1000), (2, 400)`,
`INSERT INTO orders (user_id, status, actual_amount, paid_at, created_at) VALUES (2, 2, 100, '2026-01-06 10:00:00', '2026-01-05 09:00:00')`,
`INSERT INTO orders (user_id, status, actual_amount, paid_at, created_at) VALUES (3, 2, 200, NULL, '2026-02-05 11:00:00')`,
`INSERT INTO orders (user_id, status, actual_amount, paid_at, created_at) VALUES (3, 1, 999, '2026-01-10 10:00:00', '2026-01-10 10:00:00')`,
`INSERT INTO user_inventory (user_id, product_id, value_cents, status, created_at) VALUES (2, 1, 300, 1, '2026-01-06 12:00:00')`,
`INSERT INTO user_inventory (user_id, product_id, value_cents, status, created_at) VALUES (3, 2, 0, 1, '2026-02-06 12:00:00')`,
}
for _, sql := range sqls {
if err := repo.GetDbW().Exec(sql).Error; err != nil {
t.Fatal(err)
}
}
}