setting_service_codex_policy_test.go 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. package service
  2. import (
  3. "context"
  4. "encoding/json"
  5. "reflect"
  6. "testing"
  7. "github.com/Wei-Shaw/sub2api/internal/config"
  8. "github.com/Wei-Shaw/sub2api/internal/pkg/openai"
  9. "github.com/stretchr/testify/require"
  10. )
  11. func TestGetCodexRestrictionPolicy(t *testing.T) {
  12. svc := NewSettingService(&codexPolicyMigrationRepoStub{values: map[string]string{
  13. SettingKeyMinCodexVersion: "0.141.0",
  14. SettingKeyMaxCodexVersion: "0.200.0",
  15. SettingKeyCodexCLIOnlyWhitelist: `[{"originator":"opencode","ua_contains":["opencode/"]}]`,
  16. SettingKeyCodexCLIOnlyBlacklist: `[{"originator":"evil"}]`,
  17. }}, &config.Config{})
  18. pol := svc.GetCodexRestrictionPolicy(context.Background())
  19. require.Equal(t, "0.141.0", pol.MinCodexVersion)
  20. require.Equal(t, "0.200.0", pol.MaxCodexVersion)
  21. require.Len(t, pol.Whitelist, 1)
  22. require.Equal(t, "opencode", pol.Whitelist[0].Originator)
  23. require.Equal(t, []string{"opencode/"}, pol.Whitelist[0].UAContains)
  24. require.Len(t, pol.Blacklist, 1)
  25. require.Equal(t, "evil", pol.Blacklist[0].Originator)
  26. }
  27. func TestGetCodexRestrictionPolicy_DefaultsSafe(t *testing.T) {
  28. svc := NewSettingService(&codexPolicyMigrationRepoStub{values: map[string]string{}}, &config.Config{})
  29. pol := svc.GetCodexRestrictionPolicy(context.Background())
  30. require.Empty(t, pol.MinCodexVersion)
  31. require.Empty(t, pol.Whitelist)
  32. require.Empty(t, pol.Blacklist)
  33. }
  34. func TestGetCodexRestrictionPolicy_InvalidJSONSafe(t *testing.T) {
  35. svc := NewSettingService(&codexPolicyMigrationRepoStub{values: map[string]string{
  36. SettingKeyCodexCLIOnlyWhitelist: "not-json",
  37. SettingKeyCodexCLIOnlyBlacklist: "{bad",
  38. }}, &config.Config{})
  39. pol := svc.GetCodexRestrictionPolicy(context.Background())
  40. require.Empty(t, pol.Whitelist, "非法 JSON → 安全空名单")
  41. require.Empty(t, pol.Blacklist, "非法 JSON → 安全空名单")
  42. }
  43. type codexPolicyMigrationRepoStub struct {
  44. values map[string]string
  45. sets map[string]string
  46. }
  47. func (s *codexPolicyMigrationRepoStub) Get(ctx context.Context, key string) (*Setting, error) {
  48. panic("unused")
  49. }
  50. func (s *codexPolicyMigrationRepoStub) GetValue(ctx context.Context, key string) (string, error) {
  51. if v, ok := s.values[key]; ok {
  52. return v, nil
  53. }
  54. return "", ErrSettingNotFound
  55. }
  56. func (s *codexPolicyMigrationRepoStub) Set(ctx context.Context, key, value string) error {
  57. if s.sets == nil {
  58. s.sets = map[string]string{}
  59. }
  60. s.sets[key] = value
  61. s.values[key] = value
  62. return nil
  63. }
  64. func (s *codexPolicyMigrationRepoStub) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
  65. panic("unused")
  66. }
  67. func (s *codexPolicyMigrationRepoStub) SetMultiple(ctx context.Context, settings map[string]string) error {
  68. panic("unused")
  69. }
  70. func (s *codexPolicyMigrationRepoStub) GetAll(ctx context.Context) (map[string]string, error) {
  71. panic("unused")
  72. }
  73. func (s *codexPolicyMigrationRepoStub) Delete(ctx context.Context, key string) error {
  74. panic("unused")
  75. }
  76. func TestMigrateOpenAIAllowClaudeCodeCodexPluginSetting(t *testing.T) {
  77. t.Run("legacy true appends Claude Code entry to whitelist", func(t *testing.T) {
  78. repo := &codexPolicyMigrationRepoStub{values: map[string]string{
  79. SettingKeyOpenAIAllowClaudeCodeCodexPlugin: "true",
  80. SettingKeyCodexCLIOnlyWhitelist: `[{"originator":"opencode","ua_contains":["opencode/"]}]`,
  81. }}
  82. svc := NewSettingService(repo, &config.Config{})
  83. require.NoError(t, svc.MigrateOpenAIAllowClaudeCodeCodexPluginSetting(context.Background()))
  84. raw := repo.sets[SettingKeyCodexCLIOnlyWhitelist]
  85. require.NotEmpty(t, raw)
  86. var entries []struct {
  87. Originator string `json:"originator"`
  88. UAContains []string `json:"ua_contains"`
  89. }
  90. require.NoError(t, json.Unmarshal([]byte(raw), &entries))
  91. require.Len(t, entries, 2)
  92. require.Equal(t, "opencode", entries[0].Originator)
  93. require.Equal(t, "Claude Code", entries[1].Originator)
  94. require.Equal(t, []string{"Claude Code/"}, entries[1].UAContains)
  95. })
  96. t.Run("legacy true does not duplicate existing Claude Code entry", func(t *testing.T) {
  97. repo := &codexPolicyMigrationRepoStub{values: map[string]string{
  98. SettingKeyOpenAIAllowClaudeCodeCodexPlugin: "true",
  99. SettingKeyCodexCLIOnlyWhitelist: `[{"originator":"Claude Code","ua_contains":["Claude Code/"]}]`,
  100. }}
  101. svc := NewSettingService(repo, &config.Config{})
  102. require.NoError(t, svc.MigrateOpenAIAllowClaudeCodeCodexPluginSetting(context.Background()))
  103. _, wrote := repo.sets[SettingKeyCodexCLIOnlyWhitelist]
  104. require.False(t, wrote)
  105. })
  106. }
  107. func TestGetCodexRestrictionPolicy_AllowAppServerClients(t *testing.T) {
  108. t.Run("显式 true 开启", func(t *testing.T) {
  109. svc := NewSettingService(&codexPolicyMigrationRepoStub{values: map[string]string{
  110. SettingKeyCodexCLIOnlyAllowAppServerClients: "true",
  111. }}, &config.Config{})
  112. require.True(t, svc.GetCodexRestrictionPolicy(context.Background()).AllowAppServerClients)
  113. })
  114. t.Run("缺失默认 false", func(t *testing.T) {
  115. svc := NewSettingService(&codexPolicyMigrationRepoStub{values: map[string]string{}}, &config.Config{})
  116. require.False(t, svc.GetCodexRestrictionPolicy(context.Background()).AllowAppServerClients)
  117. })
  118. t.Run("非 true 值视为 false", func(t *testing.T) {
  119. svc := NewSettingService(&codexPolicyMigrationRepoStub{values: map[string]string{
  120. SettingKeyCodexCLIOnlyAllowAppServerClients: "1",
  121. }}, &config.Config{})
  122. require.False(t, svc.GetCodexRestrictionPolicy(context.Background()).AllowAppServerClients)
  123. })
  124. }
  125. func TestGetCodexRestrictionPolicy_EngineFingerprintSignals(t *testing.T) {
  126. t.Run("键缺失 → 默认种子(只勾x-codex-)", func(t *testing.T) {
  127. svc := NewSettingService(&codexPolicyMigrationRepoStub{values: map[string]string{}}, &config.Config{})
  128. pol := svc.GetCodexRestrictionPolicy(context.Background())
  129. require.True(t, len(pol.EngineFingerprintSignals) > 0)
  130. require.True(t, openaiEngineSignalsEqual(pol.EngineFingerprintSignals, openai.DefaultEngineFingerprintSignals))
  131. })
  132. t.Run("显式配置 → 原样采用", func(t *testing.T) {
  133. raw := `[{"type":"header_exact","match":["session-id"],"required":true}]`
  134. svc := NewSettingService(&codexPolicyMigrationRepoStub{values: map[string]string{
  135. SettingKeyCodexCLIOnlyEngineFingerprintSignals: raw,
  136. }}, &config.Config{})
  137. pol := svc.GetCodexRestrictionPolicy(context.Background())
  138. require.Len(t, pol.EngineFingerprintSignals, 1)
  139. require.Equal(t, "session-id", pol.EngineFingerprintSignals[0].Match[0])
  140. })
  141. t.Run("非法JSON → 回落默认种子", func(t *testing.T) {
  142. svc := NewSettingService(&codexPolicyMigrationRepoStub{values: map[string]string{
  143. SettingKeyCodexCLIOnlyEngineFingerprintSignals: "not json",
  144. }}, &config.Config{})
  145. pol := svc.GetCodexRestrictionPolicy(context.Background())
  146. require.True(t, openaiEngineSignalsEqual(pol.EngineFingerprintSignals, openai.DefaultEngineFingerprintSignals))
  147. })
  148. }
  149. func openaiEngineSignalsEqual(a, b []openai.EngineFingerprintSignal) bool {
  150. return reflect.DeepEqual(a, b)
  151. }
  152. func TestMigrateCodexBodyFingerprintToSignals(t *testing.T) {
  153. t.Run("信号键已存在 → 不动", func(t *testing.T) {
  154. repo := &codexPolicyMigrationRepoStub{values: map[string]string{
  155. SettingKeyCodexCLIOnlyEngineFingerprintSignals: `[{"type":"header_exact","match":["session-id"],"required":true}]`,
  156. SettingKeyCodexCLIOnlyAllowBodyEngineFingerprint: "true",
  157. }}
  158. svc := NewSettingService(repo, &config.Config{})
  159. require.NoError(t, svc.MigrateCodexBodyFingerprintToSignals(context.Background()))
  160. require.Equal(t, `[{"type":"header_exact","match":["session-id"],"required":true}]`, repo.values[SettingKeyCodexCLIOnlyEngineFingerprintSignals])
  161. })
  162. t.Run("信号键缺失 + 旧body=true → 写种子且body行Required=true", func(t *testing.T) {
  163. repo := &codexPolicyMigrationRepoStub{values: map[string]string{
  164. SettingKeyCodexCLIOnlyAllowBodyEngineFingerprint: "true",
  165. }}
  166. svc := NewSettingService(repo, &config.Config{})
  167. require.NoError(t, svc.MigrateCodexBodyFingerprintToSignals(context.Background()))
  168. sigs, ok := openai.ParseEngineFingerprintSignals(repo.values[SettingKeyCodexCLIOnlyEngineFingerprintSignals])
  169. require.True(t, ok)
  170. var bodyReq bool
  171. for _, s := range sigs {
  172. if s.Type == openai.FingerprintSignalBodyPath {
  173. bodyReq = s.Required
  174. }
  175. }
  176. require.True(t, bodyReq)
  177. })
  178. t.Run("信号键缺失 + 旧body=false/缺 → 写种子(body不勾)", func(t *testing.T) {
  179. repo := &codexPolicyMigrationRepoStub{values: map[string]string{}}
  180. svc := NewSettingService(repo, &config.Config{})
  181. require.NoError(t, svc.MigrateCodexBodyFingerprintToSignals(context.Background()))
  182. require.Equal(t, openai.DefaultEngineFingerprintSignalsJSON(), repo.values[SettingKeyCodexCLIOnlyEngineFingerprintSignals])
  183. })
  184. }
  185. func TestMigrateGrokDefaultTextModel(t *testing.T) {
  186. t.Run("upgrades legacy built-in default", func(t *testing.T) {
  187. repo := &codexPolicyMigrationRepoStub{values: map[string]string{
  188. SettingKeyGrokDefaultTextModel: "grok-4.5",
  189. }}
  190. svc := NewSettingService(repo, &config.Config{})
  191. require.NoError(t, svc.MigrateGrokDefaultTextModel(context.Background()))
  192. require.Equal(t, "grok-4.6", repo.values[SettingKeyGrokDefaultTextModel])
  193. require.Equal(t, "grok-4.6", repo.sets[SettingKeyGrokDefaultTextModel])
  194. })
  195. t.Run("does not overwrite an explicit model", func(t *testing.T) {
  196. repo := &codexPolicyMigrationRepoStub{values: map[string]string{
  197. SettingKeyGrokDefaultTextModel: "grok-4.3",
  198. }}
  199. svc := NewSettingService(repo, &config.Config{})
  200. require.NoError(t, svc.MigrateGrokDefaultTextModel(context.Background()))
  201. require.Equal(t, "grok-4.3", repo.values[SettingKeyGrokDefaultTextModel])
  202. _, wrote := repo.sets[SettingKeyGrokDefaultTextModel]
  203. require.False(t, wrote)
  204. })
  205. t.Run("missing setting is left for normal defaults", func(t *testing.T) {
  206. repo := &codexPolicyMigrationRepoStub{values: map[string]string{}}
  207. svc := NewSettingService(repo, &config.Config{})
  208. require.NoError(t, svc.MigrateGrokDefaultTextModel(context.Background()))
  209. _, wrote := repo.sets[SettingKeyGrokDefaultTextModel]
  210. require.False(t, wrote)
  211. })
  212. }