Files
sub2api/backend/internal/securityaudit/prompt_config_test.go
T
李建琦 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

457 lines
22 KiB
Go

package securityaudit
import (
"context"
"encoding/json"
"errors"
"strings"
"testing"
"github.com/Wei-Shaw/sub2api/internal/config"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/stretchr/testify/require"
)
type prefixEncryptor struct{}
func (prefixEncryptor) Encrypt(value string) (string, error) { return "enc:" + value, nil }
func (prefixEncryptor) Decrypt(value string) (string, error) {
if !strings.HasPrefix(value, "enc:") {
return "", errors.New("cipher: message authentication failed")
}
return value[4:], nil
}
// testTotpKeyConfig mirrors a deployment with a fixed TOTP_ENCRYPTION_KEY so
// unit tests may persist endpoint tokens.
func testTotpKeyConfig() *config.Config {
return &config.Config{Totp: config.TotpConfig{EncryptionKeyConfigured: true}}
}
func TestDefaultConfigIsOff(t *testing.T) {
storage, err := ParseStorageConfig("")
require.NoError(t, err)
require.False(t, storage.Enabled)
require.False(t, storage.BlockingLatestTurnOnly)
active, err := ActiveFromStorage(storage, true, prefixEncryptor{})
require.NoError(t, err)
require.Equal(t, ModeOff, active.EffectiveMode())
require.Equal(t, AllScannerIDs, storage.Scanners)
publicJSON, err := json.Marshal(PublicFromStorage(storage, true, nil))
require.NoError(t, err)
require.Contains(t, string(publicJSON), `"group_ids":[]`)
require.Contains(t, string(publicJSON), `"endpoints":[]`)
}
func TestBlockingLatestTurnOnlyConfigRoundTrip(t *testing.T) {
manager := &ConfigManager{encryptor: prefixEncryptor{}, encryptionKeyConfigured: true}
request := UpdateConfigRequest{
ExpectedConfigVersion: 1, Enabled: true, BlockingEnabled: true, BlockingLatestTurnOnly: true,
Strategy: "priority", WorkerCount: 1, QueueCapacity: 10, Scanners: []string{"pii"}, AllGroups: true,
Endpoints: []UpdateEndpoint{{
ID: "guard-1", Name: "Guard", Protocol: "openai_compatible", BaseURL: "http://127.0.0.1:8080",
Model: DefaultGuardModel, TimeoutMS: 1000, InputLimit: 1000, Enabled: true,
}},
}
next, err := manager.buildNextStorage(DefaultStorageConfig(), request, 9)
require.NoError(t, err)
require.True(t, next.BlockingLatestTurnOnly)
require.Contains(t, changeSummary(next), `"blocking_latest_turn_only":true`)
active, err := ActiveFromStorage(next, true, prefixEncryptor{})
require.NoError(t, err)
require.True(t, active.BlockingLatestTurnOnly)
public := PublicFromStorage(next, true, nil)
require.True(t, public.BlockingLatestTurnOnly)
}
func TestConfigRejectsBlockingWithoutAudit(t *testing.T) {
storage := DefaultStorageConfig()
storage.BlockingEnabled = true
require.Error(t, validateStorageConfig(storage))
}
func TestPublicConfigNeverMarshalsToken(t *testing.T) {
storage := DefaultStorageConfig()
storage.Endpoints = []StorageEndpoint{{ID: "one", Name: "One", Protocol: "openai_compatible", BaseURL: "http://127.0.0.1:8080", Model: DefaultGuardModel, TokenCiphertext: "GUARD_TOKEN_CANARY_SECRET", TimeoutMS: 1000, InputLimit: 1000, Enabled: true}}
public := PublicFromStorage(storage, true, nil)
raw, err := json.Marshal(public)
require.NoError(t, err)
require.NotContains(t, string(raw), "GUARD_TOKEN_CANARY_SECRET")
require.NotContains(t, string(raw), "ciphertext")
require.True(t, public.Endpoints[0].HasToken)
}
func TestConfigRuntimeLoadErrorIsStableBoundedAndSecretFree(t *testing.T) {
const canary = "CONFIG_LOAD_CANARY_SECRET"
manager := &ConfigManager{clock: fixedClock{}}
manager.recordLoadError(errors.New("decrypt failed for token " + canary + " Authorization: Bearer " + canary))
_, _, _, message := manager.RuntimeState()
require.Equal(t, stableErrorMessage("config_load_failed"), message)
require.NotContains(t, message, canary)
require.LessOrEqual(t, len([]rune(message)), 160)
}
func TestConfigManagerPublicRequiresSuccessfullyLoadedSnapshot(t *testing.T) {
t.Run("absent persisted setting is legitimate default", func(t *testing.T) {
manager := NewConfigManager(nil, staticSettingRepository{values: map[string]string{
SettingKeyPromptAuditConfig: "",
SettingKeyRiskControl: "false",
}}, nil, prefixEncryptor{}, testTotpKeyConfig())
require.NoError(t, manager.Reload(context.Background()))
public, err := manager.Public()
require.NoError(t, err)
require.Equal(t, int64(1), public.ConfigVersion)
require.False(t, public.Enabled)
})
t.Run("unparseable persisted config is unavailable", func(t *testing.T) {
const canary = "persisted-token-canary"
manager := NewConfigManager(nil, staticSettingRepository{values: map[string]string{
// Endpoint without id/name fails validation, so no trustworthy
// snapshot can be installed from this raw value.
SettingKeyPromptAuditConfig: `{"enabled":true,"config_version":9,"endpoints":[{"token_ciphertext":"` + canary + `"}]}`,
SettingKeyRiskControl: "true",
}}, nil, prefixEncryptor{}, testTotpKeyConfig())
require.Error(t, manager.Reload(context.Background()))
public, err := manager.Public()
require.Error(t, err)
require.Empty(t, public)
require.Equal(t, ErrorCodeConfigUnavailable, infraerrors.Reason(err))
require.NotContains(t, err.Error(), canary)
})
t.Run("reload failure preserves last successfully loaded snapshot", func(t *testing.T) {
storage := DefaultStorageConfig()
storage.ConfigVersion = 4
storage.ChangeSummary = "trusted snapshot"
raw, err := json.Marshal(storage)
require.NoError(t, err)
repository := &switchableSettingRepository{staticSettingRepository: staticSettingRepository{values: map[string]string{
SettingKeyPromptAuditConfig: string(raw),
SettingKeyRiskControl: "false",
}}}
manager := NewConfigManager(nil, repository, nil, prefixEncryptor{}, testTotpKeyConfig())
require.NoError(t, manager.Reload(context.Background()))
repository.loadErr = errors.New("settings unavailable")
require.Error(t, manager.Reload(context.Background()))
public, err := manager.Public()
require.NoError(t, err)
require.Equal(t, int64(4), public.ConfigVersion)
require.Equal(t, "trusted snapshot", public.ChangeSummary)
})
}
// Regression coverage for issue #4887: a persisted config whose endpoint token
// can no longer be decrypted (encryption key changed or auto-generated per
// boot) must stay visible and editable for admins instead of falling back to a
// default v1 config that makes every save fail the CAS version check.
func TestConfigManagerUndecryptableTokenKeepsConfigVisibleAndRecoverable(t *testing.T) {
const canary = "persisted-token-canary"
persisted := `{"enabled":true,"blocking_enabled":false,"config_version":9,"endpoints":[{"id":"g1","name":"Guard","protocol":"openai_compatible","base_url":"http://127.0.0.1:8080","model":"m","token_ciphertext":"` + canary + `","timeout_ms":1000,"input_limit":1000,"enabled":true}]}`
manager := NewConfigManager(nil, staticSettingRepository{values: map[string]string{
SettingKeyPromptAuditConfig: persisted,
SettingKeyRiskControl: "true",
}}, nil, prefixEncryptor{}, testTotpKeyConfig())
require.NoError(t, manager.Reload(context.Background()), "an undecryptable token must not fail the whole config load")
public, err := manager.Public()
require.NoError(t, err)
require.Equal(t, int64(9), public.ConfigVersion, "admins must see the real persisted version so CAS saves can succeed")
require.Len(t, public.Endpoints, 1)
require.True(t, public.Endpoints[0].HasToken)
require.Equal(t, "invalid", public.Endpoints[0].TokenStatus)
raw, err := json.Marshal(public)
require.NoError(t, err)
require.NotContains(t, string(raw), canary)
active, ok := manager.Active()
require.True(t, ok)
require.Len(t, active.Endpoints, 1)
require.False(t, active.Endpoints[0].Enabled, "an endpoint with an undecryptable token must not be used at runtime")
require.True(t, active.Endpoints[0].TokenInvalid)
require.Empty(t, active.Endpoints[0].Token)
require.Empty(t, active.EnabledEndpoints())
require.Equal(t, []string{"g1"}, active.InvalidTokenEndpointIDs())
expected, activeVersion, _, _ := manager.RuntimeState()
require.Equal(t, int64(9), expected)
require.Equal(t, int64(9), activeVersion)
}
func TestConfigManagerUndecryptableTokenStillFailsClosedForBlockingIntent(t *testing.T) {
persisted := `{"enabled":true,"blocking_enabled":true,"config_version":9,"endpoints":[{"id":"g1","name":"Guard","protocol":"openai_compatible","base_url":"http://127.0.0.1:8080","model":"m","token_ciphertext":"undecryptable","timeout_ms":1000,"input_limit":1000,"enabled":true}]}`
manager := NewConfigManager(nil, staticSettingRepository{values: map[string]string{
SettingKeyPromptAuditConfig: persisted,
SettingKeyRiskControl: "true",
}}, nil, prefixEncryptor{}, testTotpKeyConfig())
require.NoError(t, manager.Reload(context.Background()))
require.Equal(t, ModeBlocking, manager.EffectiveMode())
service := &PromptService{config: manager, evaluator: NewGuardEvaluator(NewOpenAICompatibleScanner(), nil, nil)}
decision, err := service.Evaluate(context.Background(), Request{
Protocol: "openai_chat_completions",
Body: []byte(`{"messages":[{"role":"user","content":"hi"}]}`),
})
require.Error(t, err, "blocking intent with no usable endpoint must not let requests pass unaudited")
require.Nil(t, decision)
var guardErr *GuardError
require.ErrorAs(t, err, &guardErr)
require.Equal(t, ErrorCodeUnavailable, guardErr.Code)
}
func TestBuildNextStoragePreserveReplaceAndClearToken(t *testing.T) {
manager := &ConfigManager{encryptor: prefixEncryptor{}, encryptionKeyConfigured: true}
current := DefaultStorageConfig()
current.Endpoints = []StorageEndpoint{{ID: "one", Name: "One", Protocol: "openai_compatible", BaseURL: "http://127.0.0.1:8080", Model: DefaultGuardModel, TokenCiphertext: "enc:old", TimeoutMS: 1000, InputLimit: 1000}}
base := UpdateConfigRequest{ExpectedConfigVersion: 1, Strategy: "priority", WorkerCount: 1, QueueCapacity: 10, Scanners: []string{"PII"}, AllGroups: true,
Endpoints: []UpdateEndpoint{{ID: "one", Name: "One", Protocol: "openai_compatible", BaseURL: "http://127.0.0.1:8080", TimeoutMS: 1000, InputLimit: 1000}}}
preserved, err := manager.buildNextStorage(current, base, 9)
require.NoError(t, err)
require.Equal(t, "enc:old", preserved.Endpoints[0].TokenCiphertext)
replacedReq := base
replacedReq.Endpoints = append([]UpdateEndpoint(nil), base.Endpoints...)
replacedReq.Endpoints[0].Token = "new"
replaced, err := manager.buildNextStorage(current, replacedReq, 9)
require.NoError(t, err)
require.Equal(t, "enc:new", replaced.Endpoints[0].TokenCiphertext)
clearedReq := base
clearedReq.Endpoints = append([]UpdateEndpoint(nil), base.Endpoints...)
clearedReq.Endpoints[0].ClearToken = true
cleared, err := manager.buildNextStorage(current, clearedReq, 9)
require.NoError(t, err)
require.Empty(t, cleared.Endpoints[0].TokenCiphertext)
}
// Without a fixed encryption key the per-boot auto-generated key would make a
// freshly saved token undecryptable after the next restart (issue #4887), so
// saving a new token must be rejected with an actionable error. Preserving or
// clearing an existing ciphertext stays allowed so admins can still edit or
// disable the feature.
func TestBuildNextStorageRejectsNewTokenWithoutConfiguredEncryptionKey(t *testing.T) {
manager := &ConfigManager{encryptor: prefixEncryptor{}, encryptionKeyConfigured: false}
current := DefaultStorageConfig()
current.Endpoints = []StorageEndpoint{{ID: "one", Name: "One", Protocol: "openai_compatible", BaseURL: "http://127.0.0.1:8080", Model: DefaultGuardModel, TokenCiphertext: "enc:old", TimeoutMS: 1000, InputLimit: 1000}}
base := UpdateConfigRequest{ExpectedConfigVersion: 1, Strategy: "priority", WorkerCount: 1, QueueCapacity: 10, Scanners: []string{"PII"}, AllGroups: true,
Endpoints: []UpdateEndpoint{{ID: "one", Name: "One", Protocol: "openai_compatible", BaseURL: "http://127.0.0.1:8080", TimeoutMS: 1000, InputLimit: 1000}}}
newTokenReq := base
newTokenReq.Endpoints = append([]UpdateEndpoint(nil), base.Endpoints...)
newTokenReq.Endpoints[0].Token = "fresh-token"
_, err := manager.buildNextStorage(current, newTokenReq, 9)
require.Error(t, err)
require.Equal(t, ErrorCodeEncryptionKeyRequired, infraerrors.Reason(err))
preserved, err := manager.buildNextStorage(current, base, 9)
require.NoError(t, err)
require.Equal(t, "enc:old", preserved.Endpoints[0].TokenCiphertext)
clearedReq := base
clearedReq.Endpoints = append([]UpdateEndpoint(nil), base.Endpoints...)
clearedReq.Endpoints[0].ClearToken = true
cleared, err := manager.buildNextStorage(current, clearedReq, 9)
require.NoError(t, err)
require.Empty(t, cleared.Endpoints[0].TokenCiphertext)
}
func TestEffectiveModeTruthTable(t *testing.T) {
tests := []struct {
risk, enabled, blocking bool
want Mode
}{
{false, false, false, ModeOff}, {false, true, true, ModeOff}, {true, false, false, ModeOff},
{true, true, false, ModeAsync}, {true, true, true, ModeBlocking},
}
for _, tt := range tests {
cfg := ActiveConfig{RiskControlEnabled: tt.risk, Enabled: tt.enabled, BlockingEnabled: tt.blocking}
require.Equal(t, tt.want, cfg.EffectiveMode())
}
}
func TestConfigManagerColdStartOnlyFailsClosedForExplicitBlockingIntent(t *testing.T) {
manager := &ConfigManager{}
manager.observeExpectedState(`{"enabled":true,"blocking_enabled":false,"config_version":42}`, true)
require.Equal(t, int64(42), manager.expected.Load())
require.Equal(t, ModeOff, manager.EffectiveMode(), "an async config version must not imply blocking")
require.False(t, manager.BlockingActivationDegraded())
manager.observeExpectedState(`{"enabled":true,"blocking_enabled":true,"config_version":43}`, false)
require.Equal(t, ModeOff, manager.EffectiveMode(), "the global risk-control switch still gates blocking")
manager.observeExpectedState(`{"enabled":true,"blocking_enabled":true,"config_version":44}`, true)
require.Equal(t, ModeBlocking, manager.EffectiveMode())
require.True(t, manager.BlockingActivationDegraded())
manager.observeExpectedState(`{"enabled":true`, true)
require.Equal(t, ModeBlocking, manager.EffectiveMode(), "undecodable storage must not erase the last known strict intent")
}
func TestConfigManagerStaleWeakerSnapshotFailsClosedWhenBlockingExpected(t *testing.T) {
manager := &ConfigManager{}
async := ActiveConfig{RiskControlEnabled: true, Enabled: true, BlockingEnabled: false, ConfigVersion: 1}
manager.snapshot.Store(&activeConfigSnapshot{active: async, storage: DefaultStorageConfig(), loadedAt: fixedClock{}.Now()})
manager.expected.Store(2)
manager.expectedBlocking.Store(true)
require.True(t, manager.BlockingActivationDegraded())
require.Equal(t, ModeBlocking, manager.EffectiveMode())
service := &PromptService{config: manager, evaluator: NewGuardEvaluator(nil, nil, nil)}
decision, err := service.Evaluate(context.Background(), Request{Protocol: "openai_chat_completions", Body: []byte(`{"messages":[{"role":"user","content":"hi"}]}`)})
require.Error(t, err)
require.Nil(t, decision)
var guardErr *GuardError
require.ErrorAs(t, err, &guardErr)
require.Equal(t, ErrorCodeUnavailable, guardErr.Code)
}
type errorSettingRepository struct{ staticSettingRepository }
func (errorSettingRepository) GetMultiple(context.Context, []string) (map[string]string, error) {
return nil, errors.New("settings unavailable")
}
type switchableSettingRepository struct {
staticSettingRepository
loadErr error
}
func (r *switchableSettingRepository) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
if r.loadErr != nil {
return nil, r.loadErr
}
return r.staticSettingRepository.GetMultiple(ctx, keys)
}
func TestConfigManagerStartupLoadFailureDoesNotBlockWhenBlockingNotIntended(t *testing.T) {
// Settings unavailable and no prior blocking intent: stay ModeOff so the
// gateway remains usable and admins can still disable/configure Prompt Audit.
manager := NewConfigManager(nil, errorSettingRepository{}, nil, prefixEncryptor{}, testTotpKeyConfig())
err := manager.Start(context.Background())
require.Error(t, err)
require.True(t, manager.configUntrusted.Load())
require.False(t, manager.BlockingActivationDegraded())
require.Equal(t, ModeOff, manager.EffectiveMode())
service := &PromptService{config: manager, evaluator: NewGuardEvaluator(nil, nil, nil)}
decision, evalErr := service.Evaluate(context.Background(), Request{
Protocol: "openai_chat_completions",
Body: []byte(`{"messages":[{"role":"user","content":"hi"}]}`),
})
require.NoError(t, evalErr)
require.NotNil(t, decision)
require.Equal(t, DecisionAllow, decision.Kind)
require.NoError(t, manager.Shutdown(context.Background()))
}
func TestConfigManagerStartupLoadFailureFailsClosedWhenBlockingIntended(t *testing.T) {
manager := NewConfigManager(nil, errorSettingRepository{}, nil, prefixEncryptor{}, testTotpKeyConfig())
// Simulate intent observed before a later load failure (e.g. decrypt error).
manager.observeExpectedState(`{"enabled":true,"blocking_enabled":true,"config_version":3}`, true)
manager.markConfigUntrusted()
require.True(t, manager.BlockingActivationDegraded())
require.Equal(t, ModeBlocking, manager.EffectiveMode())
service := &PromptService{config: manager, evaluator: NewGuardEvaluator(nil, nil, nil)}
decision, err := service.Evaluate(context.Background(), Request{
Protocol: "openai_chat_completions",
Body: []byte(`{"messages":[{"role":"user","content":"hi"}]}`),
})
require.Error(t, err)
require.Nil(t, decision)
var guardErr *GuardError
require.ErrorAs(t, err, &guardErr)
require.Equal(t, ErrorCodeUnavailable, guardErr.Code)
}
func TestConfigManagerUntrustedClearsOnSuccessfulDisable(t *testing.T) {
// After a degraded fail-closed period, saving disabled config must restore ModeOff.
manager := &ConfigManager{encryptor: prefixEncryptor{}, clock: fixedClock{}}
manager.observeExpectedState(`{"enabled":true,"blocking_enabled":true,"config_version":5}`, true)
manager.markConfigUntrusted()
require.Equal(t, ModeBlocking, manager.EffectiveMode())
// Install a trusted disabled snapshot the same way Save does after commit.
disabled := DefaultStorageConfig()
disabled.ConfigVersion = 6
disabled.Enabled = false
disabled.BlockingEnabled = false
active, err := ActiveFromStorage(disabled, true, manager.encryptor)
require.NoError(t, err)
manager.expected.Store(disabled.ConfigVersion)
manager.expectedBlocking.Store(false)
manager.snapshot.Store(&activeConfigSnapshot{storage: disabled, active: active, loadedAt: manager.clock.Now()})
manager.configUntrusted.Store(false)
require.False(t, manager.BlockingActivationDegraded())
require.Equal(t, ModeOff, manager.EffectiveMode())
service := &PromptService{config: manager, evaluator: NewGuardEvaluator(nil, nil, nil)}
decision, evalErr := service.Evaluate(context.Background(), Request{
Protocol: "openai_chat_completions",
Body: []byte(`{"messages":[{"role":"user","content":"hi"}]}`),
})
require.NoError(t, evalErr)
require.Equal(t, DecisionAllow, decision.Kind)
}
func TestConfigManagerUntrustedWithoutBlockingDoesNotForceBlockingMode(t *testing.T) {
manager := &ConfigManager{}
manager.observeExpectedState(`{"enabled":true,"blocking_enabled":false,"config_version":2}`, true)
manager.markConfigUntrusted()
require.False(t, manager.expectedBlocking.Load())
require.False(t, manager.BlockingActivationDegraded())
require.Equal(t, ModeOff, manager.EffectiveMode(), "async intent + untrusted must not force blocking unavailable")
}
func TestParseLegacyConfigDefaultsMissingFieldsWithoutEnablingBlocking(t *testing.T) {
storage, err := ParseStorageConfig(`{"enabled":false,"config_version":9}`)
require.NoError(t, err)
require.False(t, storage.BlockingEnabled)
require.Equal(t, "priority", storage.Strategy)
require.Equal(t, DefaultWorkerCount, storage.WorkerCount)
require.Equal(t, DefaultQueueCapacity, storage.QueueCapacity)
require.Equal(t, AllScannerIDs, storage.Scanners)
require.True(t, storage.AllGroups)
}
func TestUpdateConfigStrictBoundsAndKnownValues(t *testing.T) {
valid := promptAuditUpdateRequest(1, 1, "")
require.NoError(t, validateUpdateConfigRequest(valid))
tests := []struct {
name string
mutate func(*UpdateConfigRequest)
reason string
}{
{name: "strategy", mutate: func(req *UpdateConfigRequest) { req.Strategy = "round_robin" }, reason: "prompt_audit_invalid_strategy"},
{name: "worker low", mutate: func(req *UpdateConfigRequest) { req.WorkerCount = 0 }, reason: "prompt_audit_invalid_worker_count"},
{name: "worker high", mutate: func(req *UpdateConfigRequest) { req.WorkerCount = MaxWorkerCount + 1 }, reason: "prompt_audit_invalid_worker_count"},
{name: "capacity low", mutate: func(req *UpdateConfigRequest) { req.QueueCapacity = 0 }, reason: "prompt_audit_invalid_queue_capacity"},
{name: "capacity high", mutate: func(req *UpdateConfigRequest) { req.QueueCapacity = MaxQueueCapacity + 1 }, reason: "prompt_audit_invalid_queue_capacity"},
{name: "unknown scanner", mutate: func(req *UpdateConfigRequest) { req.Scanners = []string{"made_up"} }, reason: "prompt_audit_invalid_scanner"},
{name: "group required", mutate: func(req *UpdateConfigRequest) { req.AllGroups = false; req.GroupIDs = nil }, reason: "prompt_audit_groups_required"},
{name: "group positive", mutate: func(req *UpdateConfigRequest) { req.AllGroups = false; req.GroupIDs = []int64{0} }, reason: "prompt_audit_invalid_group"},
{name: "timeout low", mutate: func(req *UpdateConfigRequest) { req.Endpoints[0].TimeoutMS = MinTimeoutMS - 1 }, reason: "prompt_audit_invalid_timeout"},
{name: "timeout high", mutate: func(req *UpdateConfigRequest) { req.Endpoints[0].TimeoutMS = MaxTimeoutMS + 1 }, reason: "prompt_audit_invalid_timeout"},
{name: "input low", mutate: func(req *UpdateConfigRequest) { req.Endpoints[0].InputLimit = MinInputLimit - 1 }, reason: "prompt_audit_invalid_input_limit"},
{name: "input high", mutate: func(req *UpdateConfigRequest) { req.Endpoints[0].InputLimit = MaxInputLimit + 1 }, reason: "prompt_audit_invalid_input_limit"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := valid
req.Scanners = append([]string(nil), valid.Scanners...)
req.GroupIDs = append([]int64(nil), valid.GroupIDs...)
req.Endpoints = append([]UpdateEndpoint(nil), valid.Endpoints...)
tt.mutate(&req)
err := validateUpdateConfigRequest(req)
require.Error(t, err)
require.Equal(t, tt.reason, infraerrors.Reason(err))
})
}
}