Files
lijiaoqiao/supply-api/internal/iam/middleware/scope_auth_test.go
Your Name 89104bd0db feat(P1/P2): 完成TDD开发及P1/P2设计文档
## 设计文档
- 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规范
2026-04-02 23:35:53 +08:00

440 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package middleware
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"lijiaoqiao/supply-api/internal/middleware"
)
// TestScopeAuth_CheckScope_SuperAdminHasAllScopes 测试超级管理员拥有所有Scope
func TestScopeAuth_CheckScope_SuperAdminHasAllScopes(t *testing.T) {
// arrange
// 创建超级管理员token claims
claims := &IAMTokenClaims{
SubjectID: "user:1",
Role: "super_admin",
Scope: []string{"*"}, // 通配符Scope代表所有权限
TenantID: 0,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act
hasScope := CheckScope(ctx, "platform:read")
hasScope2 := CheckScope(ctx, "supply:account:write")
hasScope3 := CheckScope(ctx, "consumer:apikey:create")
// assert
assert.True(t, hasScope, "super_admin should have platform:read")
assert.True(t, hasScope2, "super_admin should have supply:account:write")
assert.True(t, hasScope3, "super_admin should have consumer:apikey:create")
}
// TestScopeAuth_CheckScope_ViewerHasReadOnly 测试Viewer只有只读权限
func TestScopeAuth_CheckScope_ViewerHasReadOnly(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:2",
Role: "viewer",
Scope: []string{"platform:read", "tenant:read", "billing:read"},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act & assert
assert.True(t, CheckScope(ctx, "platform:read"), "viewer should have platform:read")
assert.True(t, CheckScope(ctx, "tenant:read"), "viewer should have tenant:read")
assert.True(t, CheckScope(ctx, "billing:read"), "viewer should have billing:read")
assert.False(t, CheckScope(ctx, "platform:write"), "viewer should NOT have platform:write")
assert.False(t, CheckScope(ctx, "tenant:write"), "viewer should NOT have tenant:write")
assert.False(t, CheckScope(ctx, "supply:account:write"), "viewer should NOT have supply:account:write")
}
// TestScopeAuth_CheckScope_Denied 测试Scope被拒绝
func TestScopeAuth_CheckScope_Denied(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:3",
Role: "viewer",
Scope: []string{"platform:read"},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act & assert
assert.False(t, CheckScope(ctx, "platform:write"), "viewer should NOT have platform:write")
assert.False(t, CheckScope(ctx, "supply:account:write"), "viewer should NOT have supply:account:write")
}
// TestScopeAuth_CheckScope_MissingTokenClaims 测试缺少Token Claims
func TestScopeAuth_CheckScope_MissingTokenClaims(t *testing.T) {
// arrange
ctx := context.Background() // 没有token claims
// act
hasScope := CheckScope(ctx, "platform:read")
// assert
assert.False(t, hasScope, "should return false when token claims are missing")
}
// TestScopeAuth_CheckScope_EmptyScope 测试空Scope要求
func TestScopeAuth_CheckScope_EmptyScope(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:4",
Role: "viewer",
Scope: []string{"platform:read"},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act
hasEmptyScope := CheckScope(ctx, "")
// assert
assert.True(t, hasEmptyScope, "empty scope should always pass")
}
// TestScopeAuth_CheckMultipleScopes 测试检查多个Scope需要全部满足
func TestScopeAuth_CheckMultipleScopes(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:5",
Role: "operator",
Scope: []string{"platform:read", "platform:write", "tenant:read", "tenant:write"},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act & assert
assert.True(t, CheckAllScopes(ctx, []string{"platform:read", "platform:write"}), "operator should have both read and write")
assert.True(t, CheckAllScopes(ctx, []string{"tenant:read", "tenant:write"}), "operator should have both tenant scopes")
assert.False(t, CheckAllScopes(ctx, []string{"platform:read", "platform:admin"}), "operator should NOT have platform:admin")
}
// TestScopeAuth_CheckAnyScope 测试检查多个Scope只需满足其一
func TestScopeAuth_CheckAnyScope(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:6",
Role: "viewer",
Scope: []string{"platform:read"},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act & assert
assert.True(t, CheckAnyScope(ctx, []string{"platform:read", "platform:write"}), "should pass with one matching scope")
assert.False(t, CheckAnyScope(ctx, []string{"platform:write", "platform:admin"}), "should fail when no scopes match")
assert.True(t, CheckAnyScope(ctx, []string{}), "empty scope list should pass")
}
// TestScopeAuth_GetIAMTokenClaims 测试从Context获取IAMTokenClaims
func TestScopeAuth_GetIAMTokenClaims(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:7",
Role: "org_admin",
Scope: []string{"platform:read", "platform:write"},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act
retrievedClaims := GetIAMTokenClaims(ctx)
// assert
assert.NotNil(t, retrievedClaims)
assert.Equal(t, claims.SubjectID, retrievedClaims.SubjectID)
assert.Equal(t, claims.Role, retrievedClaims.Role)
assert.Equal(t, claims.Scope, retrievedClaims.Scope)
}
// TestScopeAuth_GetIAMTokenClaims_Missing 测试获取不存在的IAMTokenClaims
func TestScopeAuth_GetIAMTokenClaims_Missing(t *testing.T) {
// arrange
ctx := context.Background()
// act
retrievedClaims := GetIAMTokenClaims(ctx)
// assert
assert.Nil(t, retrievedClaims)
}
// TestScopeAuth_HasRole 测试用户角色检查
func TestScopeAuth_HasRole(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:8",
Role: "operator",
Scope: []string{"platform:read"},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act & assert
assert.True(t, HasRole(ctx, "operator"))
assert.False(t, HasRole(ctx, "viewer"))
assert.False(t, HasRole(ctx, "admin"))
}
// TestScopeAuth_HasRole_MissingClaims 测试缺少Claims时的角色检查
func TestScopeAuth_HasRole_MissingClaims(t *testing.T) {
// arrange
ctx := context.Background()
// act & assert
assert.False(t, HasRole(ctx, "operator"))
}
// TestScopeRoleAuthzMiddleware_WithScope 测试带Scope要求的中间件
func TestScopeRoleAuthzMiddleware_WithScope(t *testing.T) {
// arrange
scopeAuth := NewScopeAuthMiddleware()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok"}`))
})
// 创建一个带scope验证的handler
wrappedHandler := scopeAuth.RequireScope("platform:write")(handler)
// 创建一个带有token claims的请求
claims := &IAMTokenClaims{
SubjectID: "user:9",
Role: "operator",
Scope: []string{"platform:read", "platform:write"},
TenantID: 1,
}
req := httptest.NewRequest("GET", "/test", nil)
req = req.WithContext(context.WithValue(req.Context(), IAMTokenClaimsKey, *claims))
// act
rec := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rec, req)
// assert
assert.Equal(t, http.StatusOK, rec.Code)
}
// TestScopeRoleAuthzMiddleware_Denied 测试Scope不足时中间件拒绝
func TestScopeRoleAuthzMiddleware_Denied(t *testing.T) {
// arrange
scopeAuth := NewScopeAuthMiddleware()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
wrappedHandler := scopeAuth.RequireScope("platform:admin")(handler)
claims := &IAMTokenClaims{
SubjectID: "user:10",
Role: "viewer",
Scope: []string{"platform:read"}, // viewer没有platform:admin
TenantID: 1,
}
req := httptest.NewRequest("GET", "/test", nil)
req = req.WithContext(context.WithValue(req.Context(), IAMTokenClaimsKey, *claims))
// act
rec := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rec, req)
// assert
assert.Equal(t, http.StatusForbidden, rec.Code)
}
// TestScopeRoleAuthzMiddleware_MissingClaims 测试缺少Claims时中间件拒绝
func TestScopeRoleAuthzMiddleware_MissingClaims(t *testing.T) {
// arrange
scopeAuth := NewScopeAuthMiddleware()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
wrappedHandler := scopeAuth.RequireScope("platform:read")(handler)
req := httptest.NewRequest("GET", "/test", nil)
// 不设置token claims
// act
rec := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rec, req)
// assert
assert.Equal(t, http.StatusUnauthorized, rec.Code)
}
// TestScopeRoleAuthzMiddleware_RequireAllScopes 测试要求所有Scope的中间件
func TestScopeRoleAuthzMiddleware_RequireAllScopes(t *testing.T) {
// arrange
scopeAuth := NewScopeAuthMiddleware()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
wrappedHandler := scopeAuth.RequireAllScopes([]string{"platform:read", "tenant:read"})(handler)
claims := &IAMTokenClaims{
SubjectID: "user:11",
Role: "operator",
Scope: []string{"platform:read", "platform:write", "tenant:read"},
TenantID: 1,
}
req := httptest.NewRequest("GET", "/test", nil)
req = req.WithContext(context.WithValue(req.Context(), IAMTokenClaimsKey, *claims))
// act
rec := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rec, req)
// assert
assert.Equal(t, http.StatusOK, rec.Code)
}
// TestScopeRoleAuthzMiddleware_RequireAllScopes_Denied 测试要求所有Scope但不足时拒绝
func TestScopeRoleAuthzMiddleware_RequireAllScopes_Denied(t *testing.T) {
// arrange
scopeAuth := NewScopeAuthMiddleware()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
wrappedHandler := scopeAuth.RequireAllScopes([]string{"platform:read", "platform:admin"})(handler)
claims := &IAMTokenClaims{
SubjectID: "user:12",
Role: "viewer",
Scope: []string{"platform:read"}, // viewer没有platform:admin
TenantID: 1,
}
req := httptest.NewRequest("GET", "/test", nil)
req = req.WithContext(context.WithValue(req.Context(), IAMTokenClaimsKey, *claims))
// act
rec := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rec, req)
// assert
assert.Equal(t, http.StatusForbidden, rec.Code)
}
// TestScopeAuth_HasRoleLevel 测试角色层级检查
func TestScopeAuth_HasRoleLevel(t *testing.T) {
// arrange
testCases := []struct {
role string
minLevel int
expected bool
}{
{"super_admin", 50, true},
{"super_admin", 100, true},
{"org_admin", 50, true},
{"org_admin", 60, false},
{"operator", 30, true},
{"operator", 40, false},
{"viewer", 10, true},
{"viewer", 20, false},
}
for _, tc := range testCases {
claims := &IAMTokenClaims{
SubjectID: "user:test",
Role: tc.role,
Scope: []string{},
TenantID: 1,
}
ctx := context.WithValue(context.Background(), IAMTokenClaimsKey, *claims)
// act
result := HasRoleLevel(ctx, tc.minLevel)
// assert
assert.Equal(t, tc.expected, result, "role=%s, minLevel=%d", tc.role, tc.minLevel)
}
}
// TestGetRoleLevel 测试获取角色层级
func TestGetRoleLevel(t *testing.T) {
testCases := []struct {
role string
expected int
}{
{"super_admin", 100},
{"org_admin", 50},
{"supply_admin", 40},
{"operator", 30},
{"developer", 20},
{"viewer", 10},
{"unknown_role", 0},
}
for _, tc := range testCases {
// act
level := GetRoleLevel(tc.role)
// assert
assert.Equal(t, tc.expected, level, "role=%s", tc.role)
}
}
// TestScopeAuth_WithIAMClaims 测试设置IAM Claims到Context
func TestScopeAuth_WithIAMClaims(t *testing.T) {
// arrange
claims := &IAMTokenClaims{
SubjectID: "user:13",
Role: "org_admin",
Scope: []string{"platform:read"},
TenantID: 1,
}
// act
ctx := WithIAMClaims(context.Background(), claims)
retrievedClaims := GetIAMTokenClaims(ctx)
// assert
assert.NotNil(t, retrievedClaims)
assert.Equal(t, claims.SubjectID, retrievedClaims.SubjectID)
assert.Equal(t, claims.Role, retrievedClaims.Role)
}
// TestGetClaimsFromLegacy 测试从原有TokenClaims转换
func TestGetClaimsFromLegacy(t *testing.T) {
// arrange
legacyClaims := &middleware.TokenClaims{
SubjectID: "user:14",
Role: "viewer",
Scope: []string{"platform:read"},
TenantID: 1,
}
// act
iamClaims := GetClaimsFromLegacy(legacyClaims)
// assert
assert.NotNil(t, iamClaims)
assert.Equal(t, legacyClaims.SubjectID, iamClaims.SubjectID)
assert.Equal(t, legacyClaims.Role, iamClaims.Role)
assert.Equal(t, legacyClaims.Scope, iamClaims.Scope)
assert.Equal(t, legacyClaims.TenantID, iamClaims.TenantID)
}