138 lines
5.8 KiB
Go
138 lines
5.8 KiB
Go
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_audit;statistics-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
|
||
}
|