- 提升用户为管理员 / 创建管理员账号纳入敏感操作:handler 级 EnforceStepUp 门控 (admin API key 拒绝、未启用 TOTP 拒绝、无 grant 返回 STEP_UP_REQUIRED), 目标已是管理员的日常编辑不触发 - 管理员启用/停用 2FA 一律使用密码验证(默认通知邮箱常收不到验证码), verification-method 按用户角色返回;普通用户行为不变 - 用户编辑/创建弹窗接入 useStepUp:命中 STEP_UP_REQUIRED 弹 TOTP 验证并自动重试 - 审计日志清理入口与其他敏感操作对齐:未启用 2FA 时直接提示先启用 TOTP, 不再弹出无法完成的验证码输入框(后端强制现场 TOTP 语义不变) - 审计日志页重构:DataTable 布局、详情弹窗分区展示、时间范围改为 ops 同款 下拉(预设窗口 + 自定义起止支持时分)
115 lines
3.3 KiB
Go
115 lines
3.3 KiB
Go
//go:build unit
|
|
|
|
package admin
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestUserHandlerListIncludesActivityFieldsAndSortParams(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
lastLoginAt := time.Date(2026, 4, 20, 8, 0, 0, 0, time.UTC)
|
|
lastActiveAt := lastLoginAt.Add(30 * time.Minute)
|
|
lastUsedAt := lastLoginAt.Add(90 * time.Minute)
|
|
|
|
adminSvc := newStubAdminService()
|
|
adminSvc.users = []service.User{
|
|
{
|
|
ID: 7,
|
|
Email: "activity@example.com",
|
|
Username: "activity-user",
|
|
Role: service.RoleUser,
|
|
Status: service.StatusActive,
|
|
LastActiveAt: &lastActiveAt,
|
|
LastUsedAt: &lastUsedAt,
|
|
CreatedAt: lastLoginAt.Add(-24 * time.Hour),
|
|
UpdatedAt: lastLoginAt,
|
|
},
|
|
}
|
|
handler := NewUserHandler(adminSvc, nil, nil, nil, nil, nil)
|
|
|
|
recorder := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(recorder)
|
|
c.Request = httptest.NewRequest(
|
|
http.MethodGet,
|
|
"/api/v1/admin/users?sort_by=last_used_at&sort_order=asc&search=activity",
|
|
nil,
|
|
)
|
|
|
|
handler.List(c)
|
|
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.Equal(t, "last_used_at", adminSvc.lastListUsers.sortBy)
|
|
require.Equal(t, "asc", adminSvc.lastListUsers.sortOrder)
|
|
require.Equal(t, "activity", adminSvc.lastListUsers.filters.Search)
|
|
|
|
var resp struct {
|
|
Code int `json:"code"`
|
|
Data struct {
|
|
Items []struct {
|
|
LastActiveAt *time.Time `json:"last_active_at"`
|
|
LastUsedAt *time.Time `json:"last_used_at"`
|
|
} `json:"items"`
|
|
} `json:"data"`
|
|
}
|
|
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp))
|
|
require.Equal(t, 0, resp.Code)
|
|
require.Len(t, resp.Data.Items, 1)
|
|
require.WithinDuration(t, lastActiveAt, *resp.Data.Items[0].LastActiveAt, time.Second)
|
|
require.WithinDuration(t, lastUsedAt, *resp.Data.Items[0].LastUsedAt, time.Second)
|
|
}
|
|
|
|
func TestUserHandlerGetByIDIncludesActivityFields(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
lastLoginAt := time.Date(2026, 4, 20, 8, 0, 0, 0, time.UTC)
|
|
lastActiveAt := lastLoginAt.Add(30 * time.Minute)
|
|
lastUsedAt := lastLoginAt.Add(90 * time.Minute)
|
|
|
|
adminSvc := newStubAdminService()
|
|
adminSvc.users = []service.User{
|
|
{
|
|
ID: 8,
|
|
Email: "detail@example.com",
|
|
Username: "detail-user",
|
|
Role: service.RoleUser,
|
|
Status: service.StatusActive,
|
|
LastActiveAt: &lastActiveAt,
|
|
LastUsedAt: &lastUsedAt,
|
|
CreatedAt: lastLoginAt.Add(-24 * time.Hour),
|
|
UpdatedAt: lastLoginAt,
|
|
},
|
|
}
|
|
handler := NewUserHandler(adminSvc, nil, nil, nil, nil, nil)
|
|
|
|
recorder := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(recorder)
|
|
c.Params = gin.Params{{Key: "id", Value: "8"}}
|
|
c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/users/8", nil)
|
|
|
|
handler.GetByID(c)
|
|
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
|
|
var resp struct {
|
|
Code int `json:"code"`
|
|
Data struct {
|
|
LastActiveAt *time.Time `json:"last_active_at"`
|
|
LastUsedAt *time.Time `json:"last_used_at"`
|
|
} `json:"data"`
|
|
}
|
|
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp))
|
|
require.Equal(t, 0, resp.Code)
|
|
require.WithinDuration(t, lastActiveAt, *resp.Data.LastActiveAt, time.Second)
|
|
require.WithinDuration(t, lastUsedAt, *resp.Data.LastUsedAt, time.Second)
|
|
}
|