| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- package admin
- import (
- "bytes"
- "context"
- "encoding/json"
- "net/http"
- "net/http/httptest"
- "testing"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/Wei-Shaw/sub2api/internal/server/middleware"
- "github.com/Wei-Shaw/sub2api/internal/service"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/require"
- )
- // step-up 开关转换的门控测试。
- // 测试环境不注入认证上下文/userService,因此一旦触发校验会以 401/403/500 中止;
- // 借此区分「触发了转换校验」与「直接放行到常规保存(200)」。
- func newStepUpSwitchTestHandler(t *testing.T, stored map[string]string) (*SettingHandler, *settingHandlerRepoStub) {
- t.Helper()
- gin.SetMode(gin.TestMode)
- repo := &settingHandlerRepoStub{values: stored}
- svc := service.NewSettingService(repo, &config.Config{Default: config.DefaultConfig{UserConcurrency: 5}})
- return NewSettingHandler(svc, nil, nil, nil, nil, nil, nil), repo
- }
- func doUpdateSettings(t *testing.T, h *SettingHandler, body map[string]any, prepare func(c *gin.Context)) *httptest.ResponseRecorder {
- t.Helper()
- rawBody, err := json.Marshal(body)
- require.NoError(t, err)
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPut, "/api/v1/admin/settings", bytes.NewReader(rawBody))
- c.Request.Header.Set("Content-Type", "application/json")
- if prepare != nil {
- prepare(c)
- }
- h.UpdateSettings(c)
- return rec
- }
- // 开启开关(false→true):无认证上下文时拒绝,且带专用错误标记。
- func TestUpdateSettingsEnableStepUpRejectsWithoutSession(t *testing.T) {
- h, repo := newStepUpSwitchTestHandler(t, map[string]string{})
- rec := doUpdateSettings(t, h, map[string]any{"step_up_enabled": true}, nil)
- require.Equal(t, http.StatusForbidden, rec.Code)
- require.Contains(t, rec.Body.String(), "STEP_UP_ENABLE_REQUIRES_TOTP")
- require.NotEqual(t, "true", repo.values[service.SettingKeyStepUpEnabled])
- }
- // 开启开关:admin API key(机器凭证)一律拒绝,reason 与门控保持一致便于前端分流。
- func TestUpdateSettingsEnableStepUpRejectsAdminAPIKey(t *testing.T) {
- h, _ := newStepUpSwitchTestHandler(t, map[string]string{})
- rec := doUpdateSettings(t, h, map[string]any{"step_up_enabled": true}, func(c *gin.Context) {
- c.Set("auth_method", service.AuditAuthMethodAdminAPIKey)
- })
- require.Equal(t, http.StatusForbidden, rec.Code)
- require.Contains(t, rec.Body.String(), "STEP_UP_ADMIN_API_KEY_FORBIDDEN")
- }
- // 开启开关:有认证会话但 userService 未注入时 fail-closed(500),不得放行。
- func TestUpdateSettingsEnableStepUpFailsClosedWithoutUserService(t *testing.T) {
- h, repo := newStepUpSwitchTestHandler(t, map[string]string{})
- rec := doUpdateSettings(t, h, map[string]any{"step_up_enabled": true}, func(c *gin.Context) {
- c.Set(string(middleware.ContextKeyUser), middleware.AuthSubject{UserID: 1})
- })
- require.Equal(t, http.StatusInternalServerError, rec.Code)
- require.NotEqual(t, "true", repo.values[service.SettingKeyStepUpEnabled])
- }
- // 关闭开关(true→false)本身是敏感操作:无认证上下文时被 step-up 门控以 401 拦截。
- func TestUpdateSettingsDisableStepUpRequiresStepUp(t *testing.T) {
- h, repo := newStepUpSwitchTestHandler(t, map[string]string{
- service.SettingKeyStepUpEnabled: "true",
- })
- rec := doUpdateSettings(t, h, map[string]any{"step_up_enabled": false}, nil)
- require.Equal(t, http.StatusUnauthorized, rec.Code)
- require.Equal(t, "true", repo.values[service.SettingKeyStepUpEnabled])
- }
- // 关闭开关:admin API key 被 step-up 门控以 403 拦截。
- func TestUpdateSettingsDisableStepUpRejectsAdminAPIKey(t *testing.T) {
- h, repo := newStepUpSwitchTestHandler(t, map[string]string{
- service.SettingKeyStepUpEnabled: "true",
- })
- rec := doUpdateSettings(t, h, map[string]any{"step_up_enabled": false}, func(c *gin.Context) {
- c.Set("auth_method", service.AuditAuthMethodAdminAPIKey)
- })
- require.Equal(t, http.StatusForbidden, rec.Code)
- require.Contains(t, rec.Body.String(), "STEP_UP_ADMIN_API_KEY_FORBIDDEN")
- require.Equal(t, "true", repo.values[service.SettingKeyStepUpEnabled])
- }
- // 无状态转换(false→false):不触发任何转换校验,常规保存成功且默认持久化为 false。
- func TestUpdateSettingsStepUpNoTransitionSkipsGate(t *testing.T) {
- h, repo := newStepUpSwitchTestHandler(t, map[string]string{})
- rec := doUpdateSettings(t, h, map[string]any{"step_up_enabled": false}, nil)
- require.Equal(t, http.StatusOK, rec.Code)
- require.Equal(t, "false", repo.values[service.SettingKeyStepUpEnabled])
- // 会话 IP/UA 绑定默认关闭:未显式提交时持久化 false。
- require.Equal(t, "false", repo.values[service.SettingKeySessionBindingEnabled])
- }
- // 保持开启(true→true):不触发转换校验,常规保存不被打断。
- func TestUpdateSettingsStepUpKeepEnabledSkipsGate(t *testing.T) {
- h, repo := newStepUpSwitchTestHandler(t, map[string]string{
- service.SettingKeyStepUpEnabled: "true",
- })
- rec := doUpdateSettings(t, h, map[string]any{"step_up_enabled": true}, nil)
- require.Equal(t, http.StatusOK, rec.Code)
- require.Equal(t, "true", repo.values[service.SettingKeyStepUpEnabled])
- }
- // 省略字段=保持现值:不含 step_up_enabled/session_binding_enabled 的旧客户端全量保存
- // 不得把已开启的安全开关静默重置,也不触发任何转换门控。
- func TestUpdateSettingsOmittedSecuritySwitchesKeepStoredValues(t *testing.T) {
- h, repo := newStepUpSwitchTestHandler(t, map[string]string{
- service.SettingKeyStepUpEnabled: "true",
- service.SettingKeySessionBindingEnabled: "true",
- })
- rec := doUpdateSettings(t, h, map[string]any{"registration_enabled": true}, nil)
- require.Equal(t, http.StatusOK, rec.Code)
- require.Equal(t, "true", repo.values[service.SettingKeyStepUpEnabled])
- require.Equal(t, "true", repo.values[service.SettingKeySessionBindingEnabled])
- }
- // 省略字段在开关本就关闭时同样保持关闭(默认值路径)。
- func TestUpdateSettingsOmittedSecuritySwitchesKeepDisabled(t *testing.T) {
- h, repo := newStepUpSwitchTestHandler(t, map[string]string{})
- rec := doUpdateSettings(t, h, map[string]any{"registration_enabled": true}, nil)
- require.Equal(t, http.StatusOK, rec.Code)
- require.Equal(t, "false", repo.values[service.SettingKeyStepUpEnabled])
- require.Equal(t, "false", repo.values[service.SettingKeySessionBindingEnabled])
- }
- func TestUpdateSettingsForwardedClientIPHeadersOmittedPreservesAndEmptyClears(t *testing.T) {
- h, repo := newStepUpSwitchTestHandler(t, map[string]string{
- service.SettingKeyForwardedClientIPHeaders: `["X-Cdn-Ip","True-Client-Ip"]`,
- })
- preserved := doUpdateSettings(t, h, map[string]any{"registration_enabled": true}, nil)
- require.Equal(t, http.StatusOK, preserved.Code)
- require.JSONEq(t, `["X-Cdn-Ip","True-Client-Ip"]`, repo.values[service.SettingKeyForwardedClientIPHeaders])
- require.Contains(t, preserved.Body.String(), `"forwarded_client_ip_headers":["X-Cdn-Ip","True-Client-Ip"]`)
- cleared := doUpdateSettings(t, h, map[string]any{"forwarded_client_ip_headers": []string{}}, nil)
- require.Equal(t, http.StatusOK, cleared.Code)
- require.JSONEq(t, `[]`, repo.values[service.SettingKeyForwardedClientIPHeaders])
- require.Contains(t, cleared.Body.String(), `"forwarded_client_ip_headers":[]`)
- }
- func TestUpdateSettingsMalformedForwardedClientIPHeadersRemainFailClosedWhenOmitted(t *testing.T) {
- cfg := &config.Config{Default: config.DefaultConfig{UserConcurrency: 5}}
- repo := &settingHandlerRepoStub{values: map[string]string{
- service.SettingKeyAPIKeyACLTrustForwardedIP: "true",
- service.SettingKeyForwardedClientIPHeaders: `{"not":"an array"}`,
- }}
- svc := service.NewSettingService(repo, cfg)
- require.ErrorContains(t, svc.LoadForwardedClientIPSettings(context.Background()), "load forwarded client ip headers")
- require.False(t, cfg.ForwardedClientIPSettings().TrustForwardedIP)
- h := NewSettingHandler(svc, nil, nil, nil, nil, nil, nil)
- rec := doUpdateSettings(t, h, map[string]any{"registration_enabled": true}, nil)
- require.Equal(t, http.StatusOK, rec.Code)
- require.Equal(t, "false", repo.values[service.SettingKeyAPIKeyACLTrustForwardedIP])
- require.JSONEq(t, `[]`, repo.values[service.SettingKeyForwardedClientIPHeaders])
- runtimeSettings := cfg.ForwardedClientIPSettings()
- require.False(t, runtimeSettings.TrustForwardedIP)
- require.Empty(t, runtimeSettings.Headers)
- require.Contains(t, rec.Body.String(), `"api_key_acl_trust_forwarded_ip":false`)
- require.Contains(t, rec.Body.String(), `"forwarded_client_ip_headers":[]`)
- }
- func TestUpdateSettingsRejectsInvalidForwardedClientIPHeader(t *testing.T) {
- h, repo := newStepUpSwitchTestHandler(t, map[string]string{
- service.SettingKeyForwardedClientIPHeaders: `["X-Existing-IP"]`,
- })
- rec := doUpdateSettings(t, h, map[string]any{
- "forwarded_client_ip_headers": []string{"X Invalid"},
- }, nil)
- require.Equal(t, http.StatusBadRequest, rec.Code)
- require.JSONEq(t, `["X-Existing-IP"]`, repo.values[service.SettingKeyForwardedClientIPHeaders])
- }
|