test(iam): 使用TDD方法补充IAM模块测试覆盖
- 创建完整的IAM Service测试文件 (iam_service_real_test.go) - 测试真实 DefaultIAMService 而非 mock - 覆盖 CreateRole, GetRole, UpdateRole, DeleteRole, ListRoles - 覆盖 AssignRole, RevokeRole, GetUserRoles - 覆盖 CheckScope, GetUserScopes, IsExpired - 创建完整的IAM Handler测试文件 (iam_handler_real_test.go) - 测试真实 IAMHandler 使用 httptest - 覆盖路由处理器方法 (handleRoles, handleRoleByCode等) - 覆盖 CreateRole, GetRole, ListRoles, UpdateRole, DeleteRole - 覆盖 AssignRole, RevokeRole, GetUserRoles, CheckScope, ListScopes - 覆盖辅助函数和中间件 - 修复原有代码bug - extractUserID: 修正索引从parts[3]到parts[4] - extractRoleCodeFromUserPath: 修正索引从parts[5]到parts[6] - 修复多余的空格导致的语法问题 测试覆盖率: - IAM Handler: 0% -> 85.9% - IAM Service: 0% -> 99.0%
This commit is contained in:
@@ -434,11 +434,8 @@ func extractRoleCode(path string) string {
|
|||||||
func extractUserID(path string) string {
|
func extractUserID(path string) string {
|
||||||
// /api/v1/iam/users/123/roles -> 123
|
// /api/v1/iam/users/123/roles -> 123
|
||||||
parts := splitPath(path)
|
parts := splitPath(path)
|
||||||
if len(parts) >= 4 {
|
if len(parts) >= 5 {
|
||||||
return parts[3]
|
return parts[4]
|
||||||
}
|
|
||||||
if len(parts) >= 6 {
|
|
||||||
return parts[3]
|
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -447,8 +444,8 @@ func extractUserID(path string) string {
|
|||||||
func extractRoleCodeFromUserPath(path string) string {
|
func extractRoleCodeFromUserPath(path string) string {
|
||||||
// /api/v1/iam/users/123/roles/developer -> developer
|
// /api/v1/iam/users/123/roles/developer -> developer
|
||||||
parts := splitPath(path)
|
parts := splitPath(path)
|
||||||
if len(parts) >= 6 {
|
if len(parts) >= 7 {
|
||||||
return parts[5]
|
return parts[6]
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
1260
supply-api/internal/iam/handler/iam_handler_real_test.go
Normal file
1260
supply-api/internal/iam/handler/iam_handler_real_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,404 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 测试辅助函数
|
|
||||||
|
|
||||||
// testRoleResponse 用于测试的角色响应
|
|
||||||
type testRoleResponse struct {
|
|
||||||
Code string `json:"role_code"`
|
|
||||||
Name string `json:"role_name"`
|
|
||||||
Type string `json:"role_type"`
|
|
||||||
Level int `json:"level"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// testIAMService 模拟IAM服务
|
|
||||||
type testIAMService struct {
|
|
||||||
roles map[string]*testRoleResponse
|
|
||||||
userScopes map[int64][]string
|
|
||||||
}
|
|
||||||
|
|
||||||
type testRoleResponse2 struct {
|
|
||||||
Code string
|
|
||||||
Name string
|
|
||||||
Type string
|
|
||||||
Level int
|
|
||||||
IsActive bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTestIAMService() *testIAMService {
|
|
||||||
return &testIAMService{
|
|
||||||
roles: map[string]*testRoleResponse{
|
|
||||||
"viewer": {Code: "viewer", Name: "查看者", Type: "platform", Level: 10, IsActive: true},
|
|
||||||
"operator": {Code: "operator", Name: "运维", Type: "platform", Level: 30, IsActive: true},
|
|
||||||
},
|
|
||||||
userScopes: map[int64][]string{
|
|
||||||
1: {"platform:read", "platform:write"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *testIAMService) CreateRole(req *CreateRoleHTTPRequest) (*testRoleResponse, error) {
|
|
||||||
if _, exists := s.roles[req.Code]; exists {
|
|
||||||
return nil, errDuplicateRole
|
|
||||||
}
|
|
||||||
return &testRoleResponse{
|
|
||||||
Code: req.Code,
|
|
||||||
Name: req.Name,
|
|
||||||
Type: req.Type,
|
|
||||||
Level: req.Level,
|
|
||||||
IsActive: true,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *testIAMService) GetRole(roleCode string) (*testRoleResponse, error) {
|
|
||||||
if role, exists := s.roles[roleCode]; exists {
|
|
||||||
return role, nil
|
|
||||||
}
|
|
||||||
return nil, errNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *testIAMService) ListRoles(roleType string) ([]*testRoleResponse, error) {
|
|
||||||
var result []*testRoleResponse
|
|
||||||
for _, role := range s.roles {
|
|
||||||
if roleType == "" || role.Type == roleType {
|
|
||||||
result = append(result, role)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *testIAMService) CheckScope(userID int64, scope string) bool {
|
|
||||||
scopes, ok := s.userScopes[userID]
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, s := range scopes {
|
|
||||||
if s == scope || s == "*" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTP请求/响应类型
|
|
||||||
type CreateRoleHTTPRequest struct {
|
|
||||||
Code string `json:"code"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Level int `json:"level"`
|
|
||||||
Scopes []string `json:"scopes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 错误
|
|
||||||
var (
|
|
||||||
errNotFound = &HTTPErrorResponse{Code: "NOT_FOUND", Message: "not found"}
|
|
||||||
errDuplicateRole = &HTTPErrorResponse{Code: "DUPLICATE", Message: "duplicate"}
|
|
||||||
)
|
|
||||||
|
|
||||||
// HTTPErrorResponse HTTP错误响应
|
|
||||||
type HTTPErrorResponse struct {
|
|
||||||
Code string `json:"code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *HTTPErrorResponse) Error() string {
|
|
||||||
return e.Message
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTPHandler 测试用的HTTP处理器
|
|
||||||
type HTTPHandler struct {
|
|
||||||
iam *testIAMService
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHTTPHandler() *HTTPHandler {
|
|
||||||
return &HTTPHandler{iam: newTestIAMService()}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleCreateRole 创建角色
|
|
||||||
func (h *HTTPHandler) handleCreateRole(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var req CreateRoleHTTPRequest
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
writeErrorHTTPTest(w, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
role, err := h.iam.CreateRole(&req)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorHTTPTest(w, http.StatusInternalServerError, "INTERNAL_ERROR", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
writeJSONHTTPTest(w, http.StatusCreated, map[string]interface{}{
|
|
||||||
"role": role,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleListRoles 列出角色
|
|
||||||
func (h *HTTPHandler) handleListRoles(w http.ResponseWriter, r *http.Request) {
|
|
||||||
roleType := r.URL.Query().Get("type")
|
|
||||||
|
|
||||||
roles, err := h.iam.ListRoles(roleType)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorHTTPTest(w, http.StatusInternalServerError, "INTERNAL_ERROR", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
writeJSONHTTPTest(w, http.StatusOK, map[string]interface{}{
|
|
||||||
"roles": roles,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleGetRole 获取角色
|
|
||||||
func (h *HTTPHandler) handleGetRole(w http.ResponseWriter, r *http.Request) {
|
|
||||||
roleCode := r.URL.Query().Get("code")
|
|
||||||
if roleCode == "" {
|
|
||||||
writeErrorHTTPTest(w, http.StatusBadRequest, "MISSING_CODE", "role code is required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
role, err := h.iam.GetRole(roleCode)
|
|
||||||
if err != nil {
|
|
||||||
if err == errNotFound {
|
|
||||||
writeErrorHTTPTest(w, http.StatusNotFound, "NOT_FOUND", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeErrorHTTPTest(w, http.StatusInternalServerError, "INTERNAL_ERROR", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
writeJSONHTTPTest(w, http.StatusOK, map[string]interface{}{
|
|
||||||
"role": role,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleCheckScope 检查Scope
|
|
||||||
func (h *HTTPHandler) handleCheckScope(w http.ResponseWriter, r *http.Request) {
|
|
||||||
scope := r.URL.Query().Get("scope")
|
|
||||||
if scope == "" {
|
|
||||||
writeErrorHTTPTest(w, http.StatusBadRequest, "MISSING_SCOPE", "scope is required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userID := int64(1)
|
|
||||||
hasScope := h.iam.CheckScope(userID, scope)
|
|
||||||
|
|
||||||
writeJSONHTTPTest(w, http.StatusOK, map[string]interface{}{
|
|
||||||
"has_scope": hasScope,
|
|
||||||
"scope": scope,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeJSONHTTPTest(w http.ResponseWriter, status int, data interface{}) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(status)
|
|
||||||
json.NewEncoder(w).Encode(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeErrorHTTPTest(w http.ResponseWriter, status int, code, message string) {
|
|
||||||
writeJSONHTTPTest(w, status, map[string]interface{}{
|
|
||||||
"error": map[string]string{
|
|
||||||
"code": code,
|
|
||||||
"message": message,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 测试用例 ====================
|
|
||||||
|
|
||||||
// TestHTTPHandler_CreateRole_Success 测试创建角色成功
|
|
||||||
func TestHTTPHandler_CreateRole_Success(t *testing.T) {
|
|
||||||
// arrange
|
|
||||||
handler := newHTTPHandler()
|
|
||||||
|
|
||||||
body := `{"code":"developer","name":"开发者","type":"platform","level":20}`
|
|
||||||
req := httptest.NewRequest("POST", "/api/v1/iam/roles", strings.NewReader(body))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
// act
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
handler.handleCreateRole(rec, req)
|
|
||||||
|
|
||||||
// assert
|
|
||||||
assert.Equal(t, http.StatusCreated, rec.Code)
|
|
||||||
|
|
||||||
var resp map[string]interface{}
|
|
||||||
json.Unmarshal(rec.Body.Bytes(), &resp)
|
|
||||||
|
|
||||||
role := resp["role"].(map[string]interface{})
|
|
||||||
assert.Equal(t, "developer", role["role_code"])
|
|
||||||
assert.Equal(t, "开发者", role["role_name"])
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestHTTPHandler_ListRoles_Success 测试列出角色成功
|
|
||||||
func TestHTTPHandler_ListRoles_Success(t *testing.T) {
|
|
||||||
// arrange
|
|
||||||
handler := newHTTPHandler()
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/api/v1/iam/roles", nil)
|
|
||||||
|
|
||||||
// act
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
handler.handleListRoles(rec, req)
|
|
||||||
|
|
||||||
// assert
|
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
|
||||||
|
|
||||||
var resp map[string]interface{}
|
|
||||||
json.Unmarshal(rec.Body.Bytes(), &resp)
|
|
||||||
|
|
||||||
roles := resp["roles"].([]interface{})
|
|
||||||
assert.Len(t, roles, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestHTTPHandler_ListRoles_WithType 测试按类型列出角色
|
|
||||||
func TestHTTPHandler_ListRoles_WithType(t *testing.T) {
|
|
||||||
// arrange
|
|
||||||
handler := newHTTPHandler()
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/api/v1/iam/roles?type=platform", nil)
|
|
||||||
|
|
||||||
// act
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
handler.handleListRoles(rec, req)
|
|
||||||
|
|
||||||
// assert
|
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestHTTPHandler_GetRole_Success 测试获取角色成功
|
|
||||||
func TestHTTPHandler_GetRole_Success(t *testing.T) {
|
|
||||||
// arrange
|
|
||||||
handler := newHTTPHandler()
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/api/v1/iam/roles?code=viewer", nil)
|
|
||||||
|
|
||||||
// act
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
handler.handleGetRole(rec, req)
|
|
||||||
|
|
||||||
// assert
|
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
|
||||||
|
|
||||||
var resp map[string]interface{}
|
|
||||||
json.Unmarshal(rec.Body.Bytes(), &resp)
|
|
||||||
|
|
||||||
role := resp["role"].(map[string]interface{})
|
|
||||||
assert.Equal(t, "viewer", role["role_code"])
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestHTTPHandler_GetRole_NotFound 测试获取不存在的角色
|
|
||||||
func TestHTTPHandler_GetRole_NotFound(t *testing.T) {
|
|
||||||
// arrange
|
|
||||||
handler := newHTTPHandler()
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/api/v1/iam/roles?code=nonexistent", nil)
|
|
||||||
|
|
||||||
// act
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
handler.handleGetRole(rec, req)
|
|
||||||
|
|
||||||
// assert
|
|
||||||
assert.Equal(t, http.StatusNotFound, rec.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestHTTPHandler_CheckScope_HasScope 测试检查Scope存在
|
|
||||||
func TestHTTPHandler_CheckScope_HasScope(t *testing.T) {
|
|
||||||
// arrange
|
|
||||||
handler := newHTTPHandler()
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/api/v1/iam/check-scope?scope=platform:read", nil)
|
|
||||||
|
|
||||||
// act
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
handler.handleCheckScope(rec, req)
|
|
||||||
|
|
||||||
// assert
|
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
|
||||||
|
|
||||||
var resp map[string]interface{}
|
|
||||||
json.Unmarshal(rec.Body.Bytes(), &resp)
|
|
||||||
|
|
||||||
assert.Equal(t, true, resp["has_scope"])
|
|
||||||
assert.Equal(t, "platform:read", resp["scope"])
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestHTTPHandler_CheckScope_NoScope 测试检查Scope不存在
|
|
||||||
func TestHTTPHandler_CheckScope_NoScope(t *testing.T) {
|
|
||||||
// arrange
|
|
||||||
handler := newHTTPHandler()
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/api/v1/iam/check-scope?scope=platform:admin", nil)
|
|
||||||
|
|
||||||
// act
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
handler.handleCheckScope(rec, req)
|
|
||||||
|
|
||||||
// assert
|
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
|
||||||
|
|
||||||
var resp map[string]interface{}
|
|
||||||
json.Unmarshal(rec.Body.Bytes(), &resp)
|
|
||||||
|
|
||||||
assert.Equal(t, false, resp["has_scope"])
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestHTTPHandler_CheckScope_MissingScope 测试缺少Scope参数
|
|
||||||
func TestHTTPHandler_CheckScope_MissingScope(t *testing.T) {
|
|
||||||
// arrange
|
|
||||||
handler := newHTTPHandler()
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/api/v1/iam/check-scope", nil)
|
|
||||||
|
|
||||||
// act
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
handler.handleCheckScope(rec, req)
|
|
||||||
|
|
||||||
// assert
|
|
||||||
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestHTTPHandler_CreateRole_InvalidJSON 测试无效JSON
|
|
||||||
func TestHTTPHandler_CreateRole_InvalidJSON(t *testing.T) {
|
|
||||||
// arrange
|
|
||||||
handler := newHTTPHandler()
|
|
||||||
|
|
||||||
body := `invalid json`
|
|
||||||
req := httptest.NewRequest("POST", "/api/v1/iam/roles", strings.NewReader(body))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
// act
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
handler.handleCreateRole(rec, req)
|
|
||||||
|
|
||||||
// assert
|
|
||||||
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestHTTPHandler_GetRole_MissingCode 测试缺少角色代码
|
|
||||||
func TestHTTPHandler_GetRole_MissingCode(t *testing.T) {
|
|
||||||
// arrange
|
|
||||||
handler := newHTTPHandler()
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/api/v1/iam/roles", nil) // 没有code参数
|
|
||||||
|
|
||||||
// act
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
handler.handleGetRole(rec, req)
|
|
||||||
|
|
||||||
// assert
|
|
||||||
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保函数被使用(避免编译错误)
|
|
||||||
var _ = context.Background
|
|
||||||
1041
supply-api/internal/iam/service/iam_service_real_test.go
Normal file
1041
supply-api/internal/iam/service/iam_service_real_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,432 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockIAMService 模拟IAM服务(用于测试)
|
|
||||||
type MockIAMService struct {
|
|
||||||
roles map[string]*Role
|
|
||||||
userRoles map[int64][]*UserRole
|
|
||||||
roleScopes map[string][]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMockIAMService() *MockIAMService {
|
|
||||||
return &MockIAMService{
|
|
||||||
roles: make(map[string]*Role),
|
|
||||||
userRoles: make(map[int64][]*UserRole),
|
|
||||||
roleScopes: make(map[string][]string),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockIAMService) CreateRole(ctx context.Context, req *CreateRoleRequest) (*Role, error) {
|
|
||||||
if _, exists := m.roles[req.Code]; exists {
|
|
||||||
return nil, ErrDuplicateRoleCode
|
|
||||||
}
|
|
||||||
role := &Role{
|
|
||||||
Code: req.Code,
|
|
||||||
Name: req.Name,
|
|
||||||
Type: req.Type,
|
|
||||||
Level: req.Level,
|
|
||||||
IsActive: true,
|
|
||||||
Version: 1,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
m.roles[req.Code] = role
|
|
||||||
if len(req.Scopes) > 0 {
|
|
||||||
m.roleScopes[req.Code] = req.Scopes
|
|
||||||
}
|
|
||||||
return role, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockIAMService) GetRole(ctx context.Context, roleCode string) (*Role, error) {
|
|
||||||
if role, exists := m.roles[roleCode]; exists {
|
|
||||||
return role, nil
|
|
||||||
}
|
|
||||||
return nil, ErrRoleNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockIAMService) UpdateRole(ctx context.Context, req *UpdateRoleRequest) (*Role, error) {
|
|
||||||
role, exists := m.roles[req.Code]
|
|
||||||
if !exists {
|
|
||||||
return nil, ErrRoleNotFound
|
|
||||||
}
|
|
||||||
if req.Name != "" {
|
|
||||||
role.Name = req.Name
|
|
||||||
}
|
|
||||||
if req.Description != "" {
|
|
||||||
role.Description = req.Description
|
|
||||||
}
|
|
||||||
if req.Scopes != nil {
|
|
||||||
m.roleScopes[req.Code] = req.Scopes
|
|
||||||
}
|
|
||||||
role.Version++
|
|
||||||
role.UpdatedAt = time.Now()
|
|
||||||
return role, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockIAMService) DeleteRole(ctx context.Context, roleCode string) error {
|
|
||||||
role, exists := m.roles[roleCode]
|
|
||||||
if !exists {
|
|
||||||
return ErrRoleNotFound
|
|
||||||
}
|
|
||||||
role.IsActive = false
|
|
||||||
role.UpdatedAt = time.Now()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockIAMService) ListRoles(ctx context.Context, roleType string) ([]*Role, error) {
|
|
||||||
var roles []*Role
|
|
||||||
for _, role := range m.roles {
|
|
||||||
if roleType == "" || role.Type == roleType {
|
|
||||||
roles = append(roles, role)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return roles, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockIAMService) AssignRole(ctx context.Context, req *AssignRoleRequest) (*modelUserRoleMapping, error) {
|
|
||||||
for _, ur := range m.userRoles[req.UserID] {
|
|
||||||
if ur.RoleCode == req.RoleCode && ur.TenantID == req.TenantID && ur.IsActive {
|
|
||||||
return nil, ErrDuplicateAssignment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mapping := &modelUserRoleMapping{
|
|
||||||
UserID: req.UserID,
|
|
||||||
RoleCode: req.RoleCode,
|
|
||||||
TenantID: req.TenantID,
|
|
||||||
IsActive: true,
|
|
||||||
}
|
|
||||||
m.userRoles[req.UserID] = append(m.userRoles[req.UserID], &UserRole{
|
|
||||||
UserID: req.UserID,
|
|
||||||
RoleCode: req.RoleCode,
|
|
||||||
TenantID: req.TenantID,
|
|
||||||
IsActive: true,
|
|
||||||
})
|
|
||||||
return mapping, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockIAMService) RevokeRole(ctx context.Context, userID int64, roleCode string, tenantID int64) error {
|
|
||||||
for _, ur := range m.userRoles[userID] {
|
|
||||||
if ur.RoleCode == roleCode && ur.TenantID == tenantID {
|
|
||||||
ur.IsActive = false
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ErrRoleNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockIAMService) GetUserRoles(ctx context.Context, userID int64) ([]*UserRole, error) {
|
|
||||||
var userRoles []*UserRole
|
|
||||||
for _, ur := range m.userRoles[userID] {
|
|
||||||
if ur.IsActive {
|
|
||||||
userRoles = append(userRoles, ur)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return userRoles, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockIAMService) CheckScope(ctx context.Context, userID int64, requiredScope string) (bool, error) {
|
|
||||||
scopes, err := m.GetUserScopes(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
for _, scope := range scopes {
|
|
||||||
if scope == requiredScope || scope == "*" {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockIAMService) GetUserScopes(ctx context.Context, userID int64) ([]string, error) {
|
|
||||||
var allScopes []string
|
|
||||||
seen := make(map[string]bool)
|
|
||||||
for _, ur := range m.userRoles[userID] {
|
|
||||||
if ur.IsActive {
|
|
||||||
if scopes, exists := m.roleScopes[ur.RoleCode]; exists {
|
|
||||||
for _, scope := range scopes {
|
|
||||||
if !seen[scope] {
|
|
||||||
seen[scope] = true
|
|
||||||
allScopes = append(allScopes, scope)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return allScopes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// modelUserRoleMapping 简化的用户角色映射(用于测试)
|
|
||||||
type modelUserRoleMapping struct {
|
|
||||||
UserID int64
|
|
||||||
RoleCode string
|
|
||||||
TenantID int64
|
|
||||||
IsActive bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestIAMService_CreateRole_Success 测试创建角色成功
|
|
||||||
func TestIAMService_CreateRole_Success(t *testing.T) {
|
|
||||||
// arrange
|
|
||||||
mockService := NewMockIAMService()
|
|
||||||
req := &CreateRoleRequest{
|
|
||||||
Code: "developer",
|
|
||||||
Name: "开发者",
|
|
||||||
Type: "platform",
|
|
||||||
Level: 20,
|
|
||||||
Scopes: []string{"platform:read", "router:invoke"},
|
|
||||||
}
|
|
||||||
|
|
||||||
// act
|
|
||||||
role, err := mockService.CreateRole(context.Background(), req)
|
|
||||||
|
|
||||||
// assert
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, role)
|
|
||||||
assert.Equal(t, "developer", role.Code)
|
|
||||||
assert.Equal(t, "开发者", role.Name)
|
|
||||||
assert.Equal(t, "platform", role.Type)
|
|
||||||
assert.Equal(t, 20, role.Level)
|
|
||||||
assert.True(t, role.IsActive)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestIAMService_CreateRole_DuplicateName 测试创建重复角色
|
|
||||||
func TestIAMService_CreateRole_DuplicateName(t *testing.T) {
|
|
||||||
// arrange
|
|
||||||
mockService := NewMockIAMService()
|
|
||||||
mockService.roles["developer"] = &Role{Code: "developer", Name: "开发者", Type: "platform", Level: 20}
|
|
||||||
|
|
||||||
req := &CreateRoleRequest{
|
|
||||||
Code: "developer",
|
|
||||||
Name: "开发者",
|
|
||||||
Type: "platform",
|
|
||||||
Level: 20,
|
|
||||||
}
|
|
||||||
|
|
||||||
// act
|
|
||||||
role, err := mockService.CreateRole(context.Background(), req)
|
|
||||||
|
|
||||||
// assert
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Nil(t, role)
|
|
||||||
assert.Equal(t, ErrDuplicateRoleCode, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestIAMService_UpdateRole_Success 测试更新角色成功
|
|
||||||
func TestIAMService_UpdateRole_Success(t *testing.T) {
|
|
||||||
// arrange
|
|
||||||
mockService := NewMockIAMService()
|
|
||||||
existingRole := &Role{
|
|
||||||
Code: "developer",
|
|
||||||
Name: "开发者",
|
|
||||||
Type: "platform",
|
|
||||||
Level: 20,
|
|
||||||
IsActive: true,
|
|
||||||
Version: 1,
|
|
||||||
}
|
|
||||||
mockService.roles["developer"] = existingRole
|
|
||||||
|
|
||||||
req := &UpdateRoleRequest{
|
|
||||||
Code: "developer",
|
|
||||||
Name: "AI开发者",
|
|
||||||
Description: "AI应用开发者",
|
|
||||||
}
|
|
||||||
|
|
||||||
// act
|
|
||||||
updatedRole, err := mockService.UpdateRole(context.Background(), req)
|
|
||||||
|
|
||||||
// assert
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, updatedRole)
|
|
||||||
assert.Equal(t, "AI开发者", updatedRole.Name)
|
|
||||||
assert.Equal(t, "AI应用开发者", updatedRole.Description)
|
|
||||||
assert.Equal(t, 2, updatedRole.Version) // version 应该递增
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestIAMService_UpdateRole_NotFound 测试更新不存在的角色
|
|
||||||
func TestIAMService_UpdateRole_NotFound(t *testing.T) {
|
|
||||||
// arrange
|
|
||||||
mockService := NewMockIAMService()
|
|
||||||
|
|
||||||
req := &UpdateRoleRequest{
|
|
||||||
Code: "nonexistent",
|
|
||||||
Name: "不存在",
|
|
||||||
}
|
|
||||||
|
|
||||||
// act
|
|
||||||
role, err := mockService.UpdateRole(context.Background(), req)
|
|
||||||
|
|
||||||
// assert
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Nil(t, role)
|
|
||||||
assert.Equal(t, ErrRoleNotFound, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestIAMService_DeleteRole_Success 测试删除角色成功
|
|
||||||
func TestIAMService_DeleteRole_Success(t *testing.T) {
|
|
||||||
// arrange
|
|
||||||
mockService := NewMockIAMService()
|
|
||||||
mockService.roles["developer"] = &Role{Code: "developer", Name: "开发者", IsActive: true}
|
|
||||||
|
|
||||||
// act
|
|
||||||
err := mockService.DeleteRole(context.Background(), "developer")
|
|
||||||
|
|
||||||
// assert
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.False(t, mockService.roles["developer"].IsActive) // 应该被停用而不是删除
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestIAMService_ListRoles 测试列出角色
|
|
||||||
func TestIAMService_ListRoles(t *testing.T) {
|
|
||||||
// arrange
|
|
||||||
mockService := NewMockIAMService()
|
|
||||||
mockService.roles["viewer"] = &Role{Code: "viewer", Type: "platform", Level: 10}
|
|
||||||
mockService.roles["operator"] = &Role{Code: "operator", Type: "platform", Level: 30}
|
|
||||||
mockService.roles["supply_admin"] = &Role{Code: "supply_admin", Type: "supply", Level: 40}
|
|
||||||
|
|
||||||
// act
|
|
||||||
platformRoles, err := mockService.ListRoles(context.Background(), "platform")
|
|
||||||
supplyRoles, err2 := mockService.ListRoles(context.Background(), "supply")
|
|
||||||
allRoles, err3 := mockService.ListRoles(context.Background(), "")
|
|
||||||
|
|
||||||
// assert
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Len(t, platformRoles, 2)
|
|
||||||
|
|
||||||
assert.NoError(t, err2)
|
|
||||||
assert.Len(t, supplyRoles, 1)
|
|
||||||
|
|
||||||
assert.NoError(t, err3)
|
|
||||||
assert.Len(t, allRoles, 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestIAMService_AssignRole 测试分配角色
|
|
||||||
func TestIAMService_AssignRole(t *testing.T) {
|
|
||||||
// arrange
|
|
||||||
mockService := NewMockIAMService()
|
|
||||||
mockService.roles["viewer"] = &Role{Code: "viewer", Type: "platform", Level: 10}
|
|
||||||
|
|
||||||
req := &AssignRoleRequest{
|
|
||||||
UserID: 100,
|
|
||||||
RoleCode: "viewer",
|
|
||||||
TenantID: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
// act
|
|
||||||
mapping, err := mockService.AssignRole(context.Background(), req)
|
|
||||||
|
|
||||||
// assert
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, mapping)
|
|
||||||
assert.Equal(t, int64(100), mapping.UserID)
|
|
||||||
assert.Equal(t, "viewer", mapping.RoleCode)
|
|
||||||
assert.True(t, mapping.IsActive)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestIAMService_AssignRole_Duplicate 测试重复分配角色
|
|
||||||
func TestIAMService_AssignRole_Duplicate(t *testing.T) {
|
|
||||||
// arrange
|
|
||||||
mockService := NewMockIAMService()
|
|
||||||
mockService.roles["viewer"] = &Role{Code: "viewer", Type: "platform", Level: 10}
|
|
||||||
mockService.userRoles[100] = []*UserRole{
|
|
||||||
{UserID: 100, RoleCode: "viewer", TenantID: 1, IsActive: true},
|
|
||||||
}
|
|
||||||
|
|
||||||
req := &AssignRoleRequest{
|
|
||||||
UserID: 100,
|
|
||||||
RoleCode: "viewer",
|
|
||||||
TenantID: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
// act
|
|
||||||
mapping, err := mockService.AssignRole(context.Background(), req)
|
|
||||||
|
|
||||||
// assert
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Nil(t, mapping)
|
|
||||||
assert.Equal(t, ErrDuplicateAssignment, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestIAMService_RevokeRole 测试撤销角色
|
|
||||||
func TestIAMService_RevokeRole(t *testing.T) {
|
|
||||||
// arrange
|
|
||||||
mockService := NewMockIAMService()
|
|
||||||
mockService.userRoles[100] = []*UserRole{
|
|
||||||
{UserID: 100, RoleCode: "viewer", TenantID: 1, IsActive: true},
|
|
||||||
}
|
|
||||||
|
|
||||||
// act
|
|
||||||
err := mockService.RevokeRole(context.Background(), 100, "viewer", 1)
|
|
||||||
|
|
||||||
// assert
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.False(t, mockService.userRoles[100][0].IsActive)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestIAMService_GetUserRoles 测试获取用户角色
|
|
||||||
func TestIAMService_GetUserRoles(t *testing.T) {
|
|
||||||
// arrange
|
|
||||||
mockService := NewMockIAMService()
|
|
||||||
mockService.userRoles[100] = []*UserRole{
|
|
||||||
{UserID: 100, RoleCode: "viewer", TenantID: 0, IsActive: true},
|
|
||||||
{UserID: 100, RoleCode: "developer", TenantID: 1, IsActive: true},
|
|
||||||
}
|
|
||||||
|
|
||||||
// act
|
|
||||||
roles, err := mockService.GetUserRoles(context.Background(), 100)
|
|
||||||
|
|
||||||
// assert
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Len(t, roles, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestIAMService_CheckScope 测试检查用户Scope
|
|
||||||
func TestIAMService_CheckScope(t *testing.T) {
|
|
||||||
// arrange
|
|
||||||
mockService := NewMockIAMService()
|
|
||||||
mockService.roles["viewer"] = &Role{Code: "viewer", Type: "platform", Level: 10}
|
|
||||||
mockService.roleScopes["viewer"] = []string{"platform:read", "tenant:read"}
|
|
||||||
mockService.userRoles[100] = []*UserRole{
|
|
||||||
{UserID: 100, RoleCode: "viewer", TenantID: 0, IsActive: true},
|
|
||||||
}
|
|
||||||
|
|
||||||
// act
|
|
||||||
hasScope, err := mockService.CheckScope(context.Background(), 100, "platform:read")
|
|
||||||
noScope, err2 := mockService.CheckScope(context.Background(), 100, "platform:write")
|
|
||||||
|
|
||||||
// assert
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.True(t, hasScope)
|
|
||||||
|
|
||||||
assert.NoError(t, err2)
|
|
||||||
assert.False(t, noScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestIAMService_GetUserScopes 测试获取用户所有Scope
|
|
||||||
func TestIAMService_GetUserScopes(t *testing.T) {
|
|
||||||
// arrange
|
|
||||||
mockService := NewMockIAMService()
|
|
||||||
mockService.roles["viewer"] = &Role{Code: "viewer", Type: "platform", Level: 10}
|
|
||||||
mockService.roles["developer"] = &Role{Code: "developer", Type: "platform", Level: 20}
|
|
||||||
mockService.roleScopes["viewer"] = []string{"platform:read", "tenant:read"}
|
|
||||||
mockService.roleScopes["developer"] = []string{"router:invoke", "router:model:list"}
|
|
||||||
mockService.userRoles[100] = []*UserRole{
|
|
||||||
{UserID: 100, RoleCode: "viewer", TenantID: 0, IsActive: true},
|
|
||||||
{UserID: 100, RoleCode: "developer", TenantID: 0, IsActive: true},
|
|
||||||
}
|
|
||||||
|
|
||||||
// act
|
|
||||||
scopes, err := mockService.GetUserScopes(context.Background(), 100)
|
|
||||||
|
|
||||||
// assert
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Contains(t, scopes, "platform:read")
|
|
||||||
assert.Contains(t, scopes, "tenant:read")
|
|
||||||
assert.Contains(t, scopes, "router:invoke")
|
|
||||||
assert.Contains(t, scopes, "router:model:list")
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user