Files
sub2api/backend/internal/service/platform_catalog_test.go
T
SavitarCandClaude Opus 5.5 7ffa38a8ae feat(frontend): 平台列表与多协议账号表单改由平台清单驱动
后端新增 BuildPlatformCatalog:平台清单加上多协议供应商 profile(接入模式、
各协议默认基址、内置分流规则),由测试 TestFrontendBuiltinPlatformCatalogInSync
写成前端内置清单 constants/platformCatalog.builtin.json 并校验一致。前端随后端
一同构建发布,直接使用该清单,取代 credentialsBuilder.ts 中需要手工同步的默认
端点与 OpenCode 分流规则。

改为读取平台清单:
- 平台选项 CONCRETE_PLATFORM_OPTIONS / GROUP_PLATFORM_OPTIONS、平台展示名
  platformLabel 以及各处 i18n 平台名的默认值;
- 渠道定价平台与组合分组平台、用户平台额度、上游模型同步可用平台;
- 多协议供应商判断、原生协议能力、各接入模式的默认端点与内置分流规则、
  请求头覆写资格;
- 系统默认额度与各登录方式默认额度:此前前端只列出 5 个平台(新增国产厂商
  与 OpenCode 时漏改),后端一直支持全部平台,现在按清单列出全部平台。

没有专属界面的多协议供应商走通用表单:创建弹窗列出其按钮,接入模式、可选
协议、默认端点与分流规则都来自 profile(按模型分流的供应商显示分流规则编辑
器);编辑弹窗同样适用。未登记专属图标 / 配色的平台使用通用图标与中性配色。
平台按钮分三行:国际平台、国产厂商、多模型聚合平台(OpenCode 与平台清单中
登记的其他多协议供应商)。没有内置模型列表的供应商不预填模型白名单(此前
回落 Claude 模型列表,新账号只接受这些模型),API Key 输入框也不再显示
Claude 的占位符与说明。
AccountPlatform / GroupPlatform 类型放宽为"内置平台 | 任意字符串"。新登记
平台只需重新生成内置清单,无需修改前端代码。

另修正编辑弹窗两处遗漏 MiniMax 的默认端点回退(清空 Base URL 时曾回落到
Anthropic 默认地址)。

测试:credentialsBuilder 以改动前实现为基准,逐一比较全部平台 × 接入模式 ×
协议的默认端点与各判断;替换清单后平台选项、展示名、额度列表随之更新;创建 /
编辑弹窗对走通用表单的供应商(按模型 / 按入站两种分流)提交的凭证。更新了
两个固定旧列表写法的测试。平台清单与账号表单的测试加入 CI 前端关键测试列表
(FRONTEND_CRITICAL_VITEST)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:10:33 +08:00

141 lines
5.2 KiB
Go

//go:build unit
package service
import (
"encoding/json"
"errors"
"os"
"testing"
"github.com/Wei-Shaw/sub2api/internal/domain"
"github.com/stretchr/testify/require"
)
func TestBuildPlatformCatalogMirrorsPlatformListAndProfiles(t *testing.T) {
catalog := BuildPlatformCatalog()
require.Equal(t, domain.CompositePrecedencePlatformIDs(), catalog.CompositePrecedence)
require.Len(t, catalog.Platforms, len(domain.ConcretePlatformIDs()))
for i, entry := range catalog.Platforms {
require.Equal(t, domain.ConcretePlatformIDs()[i], entry.ID)
profile := LookupProviderProfile(entry.ID)
if profile == nil {
require.Nil(t, entry.MultiProtocol, entry.ID)
continue
}
require.NotNil(t, entry.MultiProtocol, entry.ID)
require.Equal(t, profile.DefaultMode, entry.MultiProtocol.DefaultMode, entry.ID)
require.Equal(t, profile.DefaultMode, entry.MultiProtocol.Modes[0].Mode, entry.ID)
require.Equal(t, profile.ResponsesPath, entry.MultiProtocol.ResponsesPath, entry.ID)
require.Len(t, entry.MultiProtocol.Modes, len(profile.Modes), entry.ID)
for _, mode := range entry.MultiProtocol.Modes {
endpoints := profile.Modes[mode.Mode]
require.Equal(t, endpoints.BaseURLs, mode.BaseURLs, "%s/%s", entry.ID, mode.Mode)
require.Equal(t, len(endpoints.ProtocolRules), len(mode.ProtocolRules), "%s/%s", entry.ID, mode.Mode)
for j, rule := range endpoints.ProtocolRules {
require.Equal(t, rule, mode.ProtocolRules[j])
}
}
}
}
func TestBuildPlatformCatalogJSONShape(t *testing.T) {
raw, err := json.Marshal(BuildPlatformCatalog())
require.NoError(t, err)
type modeJSON struct {
Mode string `json:"mode"`
BaseURLs map[string]string `json:"base_urls"`
ProtocolRules []ProtocolRule `json:"protocol_rules"`
}
type platformJSON struct {
ID string `json:"id"`
DisplayName string `json:"display_name"`
Gateway string `json:"gateway"`
CNProvider *bool `json:"cn_provider"`
MultiProtocol *struct {
DefaultMode string `json:"default_mode"`
Routing string `json:"routing"`
ResponsesPath string `json:"responses_path"`
Modes []modeJSON `json:"modes"`
} `json:"multi_protocol"`
}
var decoded struct {
Platforms []platformJSON `json:"platforms"`
CompositePrecedence []string `json:"composite_precedence"`
}
require.NoError(t, json.Unmarshal(raw, &decoded))
require.Equal(t, domain.CompositePrecedencePlatformIDs(), decoded.CompositePrecedence)
byID := map[string]platformJSON{}
for _, p := range decoded.Platforms {
byID[p.ID] = p
}
anthropic := byID[PlatformAnthropic]
require.Equal(t, "Anthropic", anthropic.DisplayName)
require.Equal(t, "anthropic", anthropic.Gateway)
require.NotNil(t, anthropic.CNProvider)
require.False(t, *anthropic.CNProvider)
require.Nil(t, anthropic.MultiProtocol)
require.NotContains(t, string(raw), `"multi_protocol":null`)
deepseek := byID[PlatformDeepseek].MultiProtocol
require.NotNil(t, deepseek)
require.Equal(t, "by_inbound", deepseek.Routing)
require.Equal(t, "/responses", deepseek.ResponsesPath)
openCode := byID[PlatformOpenCodeGo].MultiProtocol
require.NotNil(t, openCode)
require.Equal(t, "by_model", openCode.Routing)
require.Equal(t, AccountModeGo, openCode.DefaultMode)
require.Len(t, openCode.Modes, 2)
require.Equal(t, AccountModeGo, openCode.Modes[0].Mode)
require.Equal(t, DefaultOpenCodeGoBaseURL, openCode.Modes[0].BaseURLs[APIProtocolChatCompletions])
require.Equal(t, DefaultOpenCodeGoProtocolRules(), openCode.Modes[0].ProtocolRules)
require.Equal(t, AccountModeZen, openCode.Modes[1].Mode)
}
// 下发的是副本:调用方修改结果不影响内置 profile。
func TestBuildPlatformCatalogReturnsCopies(t *testing.T) {
catalog := BuildPlatformCatalog()
for _, entry := range catalog.Platforms {
if entry.MultiProtocol == nil {
continue
}
for _, mode := range entry.MultiProtocol.Modes {
for protocol := range mode.BaseURLs {
mode.BaseURLs[protocol] = "mutated"
}
for i := range mode.ProtocolRules {
mode.ProtocolRules[i].Protocol = "mutated"
}
}
}
require.Equal(t, DefaultOpenCodeGoBaseURL, LookupProviderProfile(PlatformOpenCodeGo).DefaultBaseURL(AccountModeGo, APIProtocolChatCompletions))
require.NotEqual(t, "mutated", LookupProviderProfile(PlatformOpenCodeGo).Modes[AccountModeGo].ProtocolRules[0].Protocol)
}
// 前端内置的平台清单(接口加载前与加载失败时使用)必须与后端一致。
// 更新平台清单或 profile 后运行:
//
// UPDATE_PLATFORM_CATALOG=1 go test -tags unit ./internal/service/ -run TestFrontendBuiltinPlatformCatalogInSync
func TestFrontendBuiltinPlatformCatalogInSync(t *testing.T) {
const path = "../../../frontend/src/constants/platformCatalog.builtin.json"
want, err := json.MarshalIndent(BuildPlatformCatalog(), "", " ")
require.NoError(t, err)
want = append(want, '\n')
if os.Getenv("UPDATE_PLATFORM_CATALOG") == "1" {
require.NoError(t, os.WriteFile(path, want, 0o644))
return
}
got, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
t.Skip("frontend sources not available")
}
require.NoError(t, err)
require.JSONEq(t, string(want), string(got),
"frontend/src/constants/platformCatalog.builtin.json 与后端平台清单不一致,按测试注释重新生成")
}