## 核心功能 - 添加 AdminUpdateAPIKeyGroupID 服务方法,支持绑定/解绑/保持不变三态语义 - 实现 UserRepository.AddGroupToAllowedGroups 接口,自动同步专属分组权限 - 添加 HTTP PUT /api-keys/:id handler 端点,支持管理员直接修改 API Key 分组 ## 事务一致性 - 使用 ent Tx 保证专属分组绑定时「添加权限」和「更新 Key」的原子性 - Repository 方法支持 clientFromContext,兼容事务内调用 - 事务失败时自动回滚,避免权限孤立 ## 业务逻辑 - 订阅类型分组阻断,需通过订阅管理流程 - 非活跃分组拒绝绑定 - 负 ID 和非法 ID 验证 - 自动授权响应,告知管理员成功授权的分组 ## 代码质量 - 16 个单元测试覆盖所有业务路径和边界用例 - 7 个 handler 集成测试覆盖 HTTP 层 - GroupRepo stub 返回克隆副本,防止测试间数据泄漏 - API 类型安全修复(PaginatedResponse<ApiKey>) - 前端 ref 回调类型对齐 Vue 规范 ## 国际化支持 - 中英文提示信息完整 - 自动授权成功/失败提示
203 lines
6.4 KiB
Go
203 lines
6.4 KiB
Go
package admin
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func setupAPIKeyHandler(adminSvc service.AdminService) *gin.Engine {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
h := NewAdminAPIKeyHandler(adminSvc)
|
|
router.PUT("/api/v1/admin/api-keys/:id", h.UpdateGroup)
|
|
return router
|
|
}
|
|
|
|
func TestAdminAPIKeyHandler_UpdateGroup_InvalidID(t *testing.T) {
|
|
router := setupAPIKeyHandler(newStubAdminService())
|
|
body := `{"group_id": 2}`
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPut, "/api/v1/admin/api-keys/abc", bytes.NewBufferString(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, http.StatusBadRequest, rec.Code)
|
|
require.Contains(t, rec.Body.String(), "Invalid API key ID")
|
|
}
|
|
|
|
func TestAdminAPIKeyHandler_UpdateGroup_InvalidJSON(t *testing.T) {
|
|
router := setupAPIKeyHandler(newStubAdminService())
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPut, "/api/v1/admin/api-keys/10", bytes.NewBufferString(`{bad json`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, http.StatusBadRequest, rec.Code)
|
|
require.Contains(t, rec.Body.String(), "Invalid request")
|
|
}
|
|
|
|
func TestAdminAPIKeyHandler_UpdateGroup_KeyNotFound(t *testing.T) {
|
|
router := setupAPIKeyHandler(newStubAdminService())
|
|
body := `{"group_id": 2}`
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPut, "/api/v1/admin/api-keys/999", bytes.NewBufferString(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(rec, req)
|
|
|
|
// ErrAPIKeyNotFound maps to 404
|
|
require.Equal(t, http.StatusNotFound, rec.Code)
|
|
}
|
|
|
|
func TestAdminAPIKeyHandler_UpdateGroup_BindGroup(t *testing.T) {
|
|
router := setupAPIKeyHandler(newStubAdminService())
|
|
body := `{"group_id": 2}`
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPut, "/api/v1/admin/api-keys/10", bytes.NewBufferString(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
|
|
var resp struct {
|
|
Code int `json:"code"`
|
|
Data json.RawMessage `json:"data"`
|
|
}
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
|
require.Equal(t, 0, resp.Code)
|
|
|
|
var data struct {
|
|
APIKey struct {
|
|
ID int64 `json:"id"`
|
|
GroupID *int64 `json:"group_id"`
|
|
} `json:"api_key"`
|
|
AutoGrantedGroupAccess bool `json:"auto_granted_group_access"`
|
|
}
|
|
require.NoError(t, json.Unmarshal(resp.Data, &data))
|
|
require.Equal(t, int64(10), data.APIKey.ID)
|
|
require.NotNil(t, data.APIKey.GroupID)
|
|
require.Equal(t, int64(2), *data.APIKey.GroupID)
|
|
}
|
|
|
|
func TestAdminAPIKeyHandler_UpdateGroup_Unbind(t *testing.T) {
|
|
svc := newStubAdminService()
|
|
gid := int64(2)
|
|
svc.apiKeys[0].GroupID = &gid
|
|
router := setupAPIKeyHandler(svc)
|
|
body := `{"group_id": 0}`
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPut, "/api/v1/admin/api-keys/10", bytes.NewBufferString(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
|
|
var resp struct {
|
|
Data struct {
|
|
APIKey struct {
|
|
GroupID *int64 `json:"group_id"`
|
|
} `json:"api_key"`
|
|
} `json:"data"`
|
|
}
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
|
require.Nil(t, resp.Data.APIKey.GroupID)
|
|
}
|
|
|
|
func TestAdminAPIKeyHandler_UpdateGroup_ServiceError(t *testing.T) {
|
|
svc := &failingUpdateGroupService{
|
|
stubAdminService: newStubAdminService(),
|
|
err: errors.New("internal failure"),
|
|
}
|
|
router := setupAPIKeyHandler(svc)
|
|
body := `{"group_id": 2}`
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPut, "/api/v1/admin/api-keys/10", bytes.NewBufferString(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, http.StatusInternalServerError, rec.Code)
|
|
}
|
|
|
|
// H2: empty body → group_id is nil → no-op, returns original key
|
|
func TestAdminAPIKeyHandler_UpdateGroup_EmptyBody_NoChange(t *testing.T) {
|
|
router := setupAPIKeyHandler(newStubAdminService())
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPut, "/api/v1/admin/api-keys/10", bytes.NewBufferString(`{}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
|
|
var resp struct {
|
|
Code int `json:"code"`
|
|
Data struct {
|
|
APIKey struct {
|
|
ID int64 `json:"id"`
|
|
} `json:"api_key"`
|
|
} `json:"data"`
|
|
}
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
|
require.Equal(t, 0, resp.Code)
|
|
require.Equal(t, int64(10), resp.Data.APIKey.ID)
|
|
}
|
|
|
|
// M2: service returns GROUP_NOT_ACTIVE → handler maps to 400
|
|
func TestAdminAPIKeyHandler_UpdateGroup_GroupNotActive(t *testing.T) {
|
|
svc := &failingUpdateGroupService{
|
|
stubAdminService: newStubAdminService(),
|
|
err: infraerrors.BadRequest("GROUP_NOT_ACTIVE", "target group is not active"),
|
|
}
|
|
router := setupAPIKeyHandler(svc)
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPut, "/api/v1/admin/api-keys/10", bytes.NewBufferString(`{"group_id": 5}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, http.StatusBadRequest, rec.Code)
|
|
require.Contains(t, rec.Body.String(), "GROUP_NOT_ACTIVE")
|
|
}
|
|
|
|
// M2: service returns INVALID_GROUP_ID → handler maps to 400
|
|
func TestAdminAPIKeyHandler_UpdateGroup_NegativeGroupID(t *testing.T) {
|
|
svc := &failingUpdateGroupService{
|
|
stubAdminService: newStubAdminService(),
|
|
err: infraerrors.BadRequest("INVALID_GROUP_ID", "group_id must be non-negative"),
|
|
}
|
|
router := setupAPIKeyHandler(svc)
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPut, "/api/v1/admin/api-keys/10", bytes.NewBufferString(`{"group_id": -5}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(rec, req)
|
|
|
|
require.Equal(t, http.StatusBadRequest, rec.Code)
|
|
require.Contains(t, rec.Body.String(), "INVALID_GROUP_ID")
|
|
}
|
|
|
|
// failingUpdateGroupService overrides AdminUpdateAPIKeyGroupID to return an error.
|
|
type failingUpdateGroupService struct {
|
|
*stubAdminService
|
|
err error
|
|
}
|
|
|
|
func (f *failingUpdateGroupService) AdminUpdateAPIKeyGroupID(_ context.Context, _ int64, _ *int64) (*service.AdminUpdateAPIKeyGroupIDResult, error) {
|
|
return nil, f.err
|
|
}
|