2026-07-12 00:47:20 +08:00

138 lines
5.8 KiB
Go
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 dashboard
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/gin-gonic/gin"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/config"
"hyapp-admin-server/internal/middleware"
)
func TestUserProfileOverviewAggregatesNaturalUsersAndDailyStatistics(t *testing.T) {
userDB, sqlMock, err := sqlmock.New()
if err != nil {
t.Fatalf("create user sqlmock: %v", err)
}
defer userDB.Close()
sqlMock.ExpectQuery(`(?s)SELECT CASE.*COALESCE\(gender, ''\).*FROM users.*WHERE app_code = \?.*profile_completed = 1.*game_robot.*quick_account.*GROUP BY gender_key.*ORDER BY user_count DESC`).
WithArgs("huwaa").
WillReturnRows(sqlmock.NewRows([]string{"gender_key", "user_count"}).
AddRow("male", int64(4)).
AddRow("female", int64(2)).
AddRow("unknown", int64(1)))
sqlMock.ExpectQuery(`(?s)WITH natural_users AS.*FROM users.*app_code = \?.*profile_completed = 1.*game_robot.*quick_account.*ranked_login AS.*ROW_NUMBER\(\) OVER.*PARTITION BY audit.user_id.*ORDER BY audit.created_at_ms DESC, audit.id DESC.*audit.app_code = \?.*audit.result = 'success'.*audit.blocked = 0.*audit.login_type IN \('password', 'third_party', 'refresh'\).*LEFT JOIN ranked_login.*latest.login_rank = 1.*GROUP BY version_key`).
WithArgs("huwaa", "huwaa").
WillReturnRows(sqlmock.NewRows([]string{"version_key", "user_count"}).
AddRow("2.4.0", int64(4)).
AddRow("unknown", int64(3)))
requested := map[string]bool{}
statisticsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requested[r.URL.Path] = true
query := r.URL.Query()
if query.Get("app_code") != "huwaa" || query.Get("stat_tz") != "Asia/Shanghai" ||
query.Get("start_ms") != "1783699200000" || query.Get("end_ms") != "1783785600000" {
http.Error(w, "unexpected statistics query: "+r.URL.RawQuery, http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
switch r.URL.Path {
case "/internal/v1/statistics/overview":
_ = json.NewEncoder(w).Encode(map[string]any{"active_users": 5, "new_users": 2})
default:
http.NotFound(w, r)
}
}))
defer statisticsServer.Close()
service := NewService(nil, config.Config{StatisticsService: config.StatisticsServiceConfig{
BaseURL: statisticsServer.URL,
RequestTimeout: time.Second,
}}, nil, WithUserDB(userDB))
router := newDashboardUserProfileTestRouter(NewWithService(service), []string{"overview:view"})
request := httptest.NewRequest(http.MethodGet,
"/api/v1/dashboard/user-profile-overview?app_code=Huwaa&stat_tz=Asia%2FShanghai&start_ms=1783699200000&end_ms=1783785600000", nil)
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("status mismatch: got=%d body=%s", recorder.Code, recorder.Body.String())
}
var envelope struct {
Code int `json:"code"`
Data UserProfileOverview `json:"data"`
}
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
t.Fatalf("decode response: %v", err)
}
if envelope.Code != 0 || envelope.Data.AppCode != "huwaa" || envelope.Data.TotalUsers != 7 ||
envelope.Data.NewUsers != 2 || envelope.Data.ActiveUsers != 5 || envelope.Data.UpdatedAtMS <= 0 {
t.Fatalf("overview mismatch: %+v", envelope.Data)
}
if len(envelope.Data.GenderDistribution) != 3 ||
envelope.Data.GenderDistribution[0] != (UserProfileDistributionItem{Key: "male", Label: "男", Count: 4}) ||
envelope.Data.GenderDistribution[2] != (UserProfileDistributionItem{Key: "unknown", Label: "未知", Count: 1}) {
t.Fatalf("gender distribution mismatch: %+v", envelope.Data.GenderDistribution)
}
if len(envelope.Data.AppVersionDistribution) != 2 ||
envelope.Data.AppVersionDistribution[0] != (UserProfileDistributionItem{Key: "2.4.0", Label: "2.4.0", Count: 4}) ||
envelope.Data.AppVersionDistribution[1] != (UserProfileDistributionItem{Key: "unknown", Label: "未知版本", Count: 3}) {
t.Fatalf("version distribution mismatch: %+v", envelope.Data.AppVersionDistribution)
}
// 版本分布以全部自然用户为分母并直接查询 login_auditstatistics-service 只保留新增/DAU 口径。
if len(requested) != 1 || !requested["/internal/v1/statistics/overview"] {
t.Fatalf("statistics calls mismatch: %+v", requested)
}
if err := sqlMock.ExpectationsWereMet(); err != nil {
t.Fatalf("sql expectations mismatch: %v", err)
}
}
func TestGenderDistributionLabelMapsFlutterValues(t *testing.T) {
tests := map[string]string{
"male": "男",
"female": "女",
"non_binary": "非二元",
"unknown": "未知",
}
for input, expected := range tests {
if actual := genderDistributionLabel(input); actual != expected {
t.Fatalf("gender label mismatch: input=%q got=%q want=%q", input, actual, expected)
}
}
}
func TestUserProfileOverviewRouteRequiresOverviewPermission(t *testing.T) {
router := newDashboardUserProfileTestRouter(NewWithService(&DashboardService{}), nil)
request := httptest.NewRequest(http.MethodGet, "/api/v1/dashboard/user-profile-overview", nil)
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusForbidden {
t.Fatalf("status mismatch: got=%d body=%s", recorder.Code, recorder.Body.String())
}
}
func newDashboardUserProfileTestRouter(handler *Handler, permissions []string) *gin.Engine {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(func(c *gin.Context) {
// 复现 AuthRequired/AppCode 已完成的上下文写入,让路由测试同时覆盖 overview:view 中间件和 query 优先级。
c.Request = c.Request.WithContext(appctx.WithContext(c.Request.Context(), "lalu"))
c.Set(middleware.ContextPermissions, permissions)
c.Next()
})
protected := router.Group("/api/v1")
RegisterRoutes(protected, handler)
return router
}