Files
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

167 lines
5.4 KiB
Go

package middleware
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
type stubStepUpGrantChecker struct {
granted bool
err error
}
func (s stubStepUpGrantChecker) HasStepUpGrant(ctx context.Context, userID int64, sessionKey string) (bool, error) {
return s.granted, s.err
}
type stubStepUpUserReader struct {
user *service.User
err error
}
func (s stubStepUpUserReader) GetByID(ctx context.Context, id int64) (*service.User, error) {
return s.user, s.err
}
type stubStepUpSettingReader struct {
enabled bool
}
func (s stubStepUpSettingReader) IsStepUpEnabled(ctx context.Context) bool {
return s.enabled
}
// stepUpEnabled 功能开关开启的设置桩,供既有门控分支测试使用。
var stepUpEnabled = stubStepUpSettingReader{enabled: true}
func newStepUpTestContext(t *testing.T) (*gin.Context, *httptest.ResponseRecorder) {
t.Helper()
gin.SetMode(gin.TestMode)
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPost, "/sensitive", nil)
return c, rec
}
func TestEnforceStepUpRejectsAdminAPIKey(t *testing.T) {
c, rec := newStepUpTestContext(t)
c.Set("auth_method", service.AuditAuthMethodAdminAPIKey)
ok := enforceStepUp(c, stubStepUpGrantChecker{granted: true}, stubStepUpUserReader{user: &service.User{TotpEnabled: true}}, stepUpEnabled)
require.False(t, ok)
require.True(t, c.IsAborted())
require.Equal(t, http.StatusForbidden, rec.Code)
require.Contains(t, rec.Body.String(), "STEP_UP_ADMIN_API_KEY_FORBIDDEN")
}
func TestEnforceStepUpRequiresAuthSubject(t *testing.T) {
c, rec := newStepUpTestContext(t)
ok := enforceStepUp(c, stubStepUpGrantChecker{granted: true}, stubStepUpUserReader{user: &service.User{TotpEnabled: true}}, stepUpEnabled)
require.False(t, ok)
require.Equal(t, http.StatusUnauthorized, rec.Code)
}
func TestEnforceStepUpRequiresTotpEnabled(t *testing.T) {
c, rec := newStepUpTestContext(t)
c.Set(string(ContextKeyUser), AuthSubject{UserID: 1})
ok := enforceStepUp(c, stubStepUpGrantChecker{granted: true}, stubStepUpUserReader{user: &service.User{ID: 1, TotpEnabled: false}}, stepUpEnabled)
require.False(t, ok)
require.Equal(t, http.StatusForbidden, rec.Code)
require.Contains(t, rec.Body.String(), "STEP_UP_TOTP_NOT_ENABLED")
}
func TestEnforceStepUpFailsClosedOnGrantError(t *testing.T) {
c, rec := newStepUpTestContext(t)
c.Set(string(ContextKeyUser), AuthSubject{UserID: 1})
ok := enforceStepUp(c, stubStepUpGrantChecker{err: errors.New("redis down")}, stubStepUpUserReader{user: &service.User{ID: 1, TotpEnabled: true}}, stepUpEnabled)
require.False(t, ok)
require.Equal(t, http.StatusServiceUnavailable, rec.Code)
require.Contains(t, rec.Body.String(), "STEP_UP_UNAVAILABLE")
}
func TestEnforceStepUpRequiresGrant(t *testing.T) {
c, rec := newStepUpTestContext(t)
c.Set(string(ContextKeyUser), AuthSubject{UserID: 1})
ok := enforceStepUp(c, stubStepUpGrantChecker{granted: false}, stubStepUpUserReader{user: &service.User{ID: 1, TotpEnabled: true}}, stepUpEnabled)
require.False(t, ok)
require.Equal(t, http.StatusForbidden, rec.Code)
require.Contains(t, rec.Body.String(), "STEP_UP_REQUIRED")
}
func TestEnforceStepUpPassesWithGrant(t *testing.T) {
c, _ := newStepUpTestContext(t)
c.Set(string(ContextKeyUser), AuthSubject{UserID: 1})
ok := enforceStepUp(c, stubStepUpGrantChecker{granted: true}, stubStepUpUserReader{user: &service.User{ID: 1, TotpEnabled: true}}, stepUpEnabled)
require.True(t, ok)
require.False(t, c.IsAborted())
}
// 功能开关关闭时:不论 TOTP/grant/凭证类型,一律放行(恢复门控引入前行为)。
func TestEnforceStepUpDisabledSkipsAllChecks(t *testing.T) {
disabled := stubStepUpSettingReader{enabled: false}
t.Run("no totp, no grant", func(t *testing.T) {
c, _ := newStepUpTestContext(t)
c.Set(string(ContextKeyUser), AuthSubject{UserID: 1})
ok := enforceStepUp(c, stubStepUpGrantChecker{granted: false}, stubStepUpUserReader{user: &service.User{ID: 1, TotpEnabled: false}}, disabled)
require.True(t, ok)
require.False(t, c.IsAborted())
})
t.Run("admin api key", func(t *testing.T) {
c, _ := newStepUpTestContext(t)
c.Set("auth_method", service.AuditAuthMethodAdminAPIKey)
ok := enforceStepUp(c, stubStepUpGrantChecker{granted: false}, stubStepUpUserReader{user: nil, err: errors.New("should not be called")}, disabled)
require.True(t, ok)
require.False(t, c.IsAborted())
})
}
// settings 为 nil 时保持门控(fail-closed),避免装配缺陷静默关闭安全控制。
func TestEnforceStepUpNilSettingsFailsClosed(t *testing.T) {
c, rec := newStepUpTestContext(t)
c.Set(string(ContextKeyUser), AuthSubject{UserID: 1})
ok := enforceStepUp(c, stubStepUpGrantChecker{granted: false}, stubStepUpUserReader{user: &service.User{ID: 1, TotpEnabled: true}}, nil)
require.False(t, ok)
require.Equal(t, http.StatusForbidden, rec.Code)
require.Contains(t, rec.Body.String(), "STEP_UP_REQUIRED")
}
// EnforceStepUp 收到 nil *service.SettingService 时不得因 typed-nil 装箱绕过门控:
// 未认证请求仍应被拦截(401),而不是当作"开关关闭"放行。
func TestEnforceStepUpTypedNilSettingServiceFailsClosed(t *testing.T) {
require.Nil(t, stepUpSettingsOrNil(nil))
c, rec := newStepUpTestContext(t)
ok := EnforceStepUp(c, nil, nil, nil)
require.False(t, ok)
require.Equal(t, http.StatusUnauthorized, rec.Code)
}