crs_sync_spark_shadow_test.go 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "testing"
  6. "github.com/stretchr/testify/require"
  7. )
  8. // TestPropagateAccountProxyToShadows 外审第8轮:CRS/AdminService 改母账号 proxy 后,
  9. // 影子 proxy 必须跟随(影子 proxy 恒继承母账号,否则出站漂移)。
  10. func TestPropagateAccountProxyToShadows(t *testing.T) {
  11. ctx := context.Background()
  12. repo := newSparkShadowRepoStub()
  13. oldProxy := int64(11)
  14. mother := &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth, ProxyID: &oldProxy}
  15. require.NoError(t, repo.Create(ctx, mother))
  16. parentID := mother.ID
  17. shadow := &Account{
  18. Platform: PlatformOpenAI,
  19. Type: AccountTypeOAuth,
  20. ParentAccountID: &parentID,
  21. QuotaDimension: QuotaDimensionSpark,
  22. ProxyID: &oldProxy,
  23. }
  24. require.NoError(t, repo.Create(ctx, shadow))
  25. newProxy := int64(22)
  26. require.NoError(t, propagateAccountProxyToShadows(ctx, repo, parentID, &newProxy))
  27. got, err := repo.GetByID(ctx, shadow.ID)
  28. require.NoError(t, err)
  29. require.NotNil(t, got.ProxyID)
  30. require.Equal(t, newProxy, *got.ProxyID, "shadow proxy must follow the parent's new proxy")
  31. // 清空母 proxy 也应传播为 nil。
  32. require.NoError(t, propagateAccountProxyToShadows(ctx, repo, parentID, nil))
  33. got, err = repo.GetByID(ctx, shadow.ID)
  34. require.NoError(t, err)
  35. require.Nil(t, got.ProxyID, "clearing parent proxy must clear the shadow proxy too")
  36. }
  37. // TestGuardCRSShadowParentInvariant 外审第8/9轮:有 spark 影子的母账号经 CRS 任意分支更新后,目标结果
  38. // 必须仍是 OpenAI OAuth;否则(改 api_key 或跨平台 Anthropic/Gemini)影子读透母凭据失败、spark 全崩。
  39. func TestGuardCRSShadowParentInvariant(t *testing.T) {
  40. ctx := context.Background()
  41. repo := newSparkShadowRepoStub()
  42. mother := &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth}
  43. require.NoError(t, repo.Create(ctx, mother))
  44. parentID := mother.ID
  45. // 无影子:任何目标都放行(含改离 OpenAI OAuth)。
  46. require.NoError(t, guardCRSShadowParentInvariant(ctx, repo, mother, PlatformOpenAI, AccountTypeAPIKey))
  47. require.NoError(t, guardCRSShadowParentInvariant(ctx, repo, mother, PlatformAnthropic, AccountTypeOAuth))
  48. // 建一个影子后:
  49. shadow := &Account{
  50. Platform: PlatformOpenAI,
  51. Type: AccountTypeOAuth,
  52. ParentAccountID: &parentID,
  53. QuotaDimension: QuotaDimensionSpark,
  54. }
  55. require.NoError(t, repo.Create(ctx, shadow))
  56. // 翻成 OpenAI api_key 被拒。
  57. err := guardCRSShadowParentInvariant(ctx, repo, mother, PlatformOpenAI, AccountTypeAPIKey)
  58. require.Error(t, err, "must reject converting a shadow parent to openai api_key")
  59. require.Contains(t, err.Error(), "spark-shadow parent")
  60. // 跨平台改成 Anthropic OAuth(Type 仍 OAuth、仅 Platform 变)也被拒——第8轮只查 Type 的版本会漏。
  61. require.Error(t, guardCRSShadowParentInvariant(ctx, repo, mother, PlatformAnthropic, AccountTypeOAuth),
  62. "must reject moving a shadow parent to a non-OpenAI platform even if type stays oauth")
  63. // 改成 Gemini api_key 被拒。
  64. require.Error(t, guardCRSShadowParentInvariant(ctx, repo, mother, PlatformGemini, AccountTypeAPIKey))
  65. // 保持 OpenAI OAuth(重新同步母账号)放行,即便仍有影子。
  66. require.NoError(t, guardCRSShadowParentInvariant(ctx, repo, mother, PlatformOpenAI, AccountTypeOAuth))
  67. }