302 lines
9.8 KiB
Go
Executable File
302 lines
9.8 KiB
Go
Executable File
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 + user3(user3消费为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_at(user3)
|
||
{
|
||
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)
|
||
}
|
||
}
|
||
}
|