## 设计文档 - multi_role_permission_design: 多角色权限设计 (CONDITIONAL GO) - audit_log_enhancement_design: 审计日志增强 (CONDITIONAL GO) - routing_strategy_template_design: 路由策略模板 (CONDITIONAL GO) - sso_saml_technical_research: SSO/SAML调研 (CONDITIONAL GO) - compliance_capability_package_design: 合规能力包设计 (CONDITIONAL GO) ## TDD开发成果 - IAM模块: supply-api/internal/iam/ (111个测试) - 审计日志模块: supply-api/internal/audit/ (40+测试) - 路由策略模块: gateway/internal/router/ (33+测试) - 合规能力包: gateway/internal/compliance/ + scripts/ci/compliance/ ## 规范文档 - parallel_agent_output_quality_standards: 并行Agent产出质量规范 - project_experience_summary: 项目经验总结 (v2) - 2026-04-02-p1-p2-tdd-execution-plan: TDD执行计划 ## 评审报告 - 5个CONDITIONAL GO设计文档评审报告 - fix_verification_report: 修复验证报告 - full_verification_report: 全面质量验证报告 - tdd_module_quality_verification: TDD模块质量验证 - tdd_execution_summary: TDD执行总结 依据: Superpowers执行框架 + TDD规范
433 lines
11 KiB
Go
433 lines
11 KiB
Go
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")
|
||
}
|