Files
sub2api/backend/internal/handler/admin/available_channel_handler_test.go
erio 654cfb6480 feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.

Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
  same-platform ModelPricing.Models; trailing "*" keys expand via
  pricing prefix match; platforms without a mapping produce no
  entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
  to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
  filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
  DTO (id, status, billing_model_source, restrict_models, groups,
  supported_models).
- User route GET /api/v1/channels/available applies three filters:
  Status==active, visible-group intersection, and platform filter
  on supported_models (prevents cross-platform leak when a channel
  links to both a user-accessible group and an inaccessible one on
  another platform). Response is a plain array (matches the
  /groups/available sibling shape). Field whitelist omits
  billing_model_source, restrict_models, ids, status, sort_order.

Frontend
- New /admin/available-channels and /available-channels views backed
  by a shared AvailableChannelsTable component (admin adds status +
  billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
  shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
  user nav.
- i18n: zh + en coverage for both namespaces.

Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
  nothing, cross-platform bleed, case-insensitive dedup, empty
  platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
  case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
  platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
  fallback.

Refs: issue #1729
2026-04-21 00:27:10 +08:00

58 lines
1.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//go:build unit
package admin
import (
"encoding/json"
"testing"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/stretchr/testify/require"
)
func TestAvailableChannelToAdminResponse_IncludesFullDTO(t *testing.T) {
// 管理员视图应包含 id / status / billing_model_source / restrict_models 等
// 管理字段BillingModelSource 为空时应默认回填 channel_mapped。
input := service.AvailableChannel{
ID: 42,
Name: "ch",
Description: "d",
Status: service.StatusActive,
BillingModelSource: "", // 验证默认值填充
RestrictModels: true,
Groups: []service.AvailableGroupRef{
{ID: 1, Name: "g1", Platform: "anthropic"},
},
SupportedModels: []service.SupportedModel{
{Name: "claude-sonnet-4-6", Platform: "anthropic"},
},
}
resp := AvailableChannelToAdminResponse(input)
require.Equal(t, int64(42), resp.ID)
require.Equal(t, "ch", resp.Name)
require.Equal(t, service.StatusActive, resp.Status)
require.Equal(t, service.BillingModelSourceChannelMapped, resp.BillingModelSource)
require.True(t, resp.RestrictModels)
require.Len(t, resp.Groups, 1)
require.Len(t, resp.SupportedModels, 1)
// JSON 层验证管理字段确实会被序列化。
raw, err := json.Marshal(resp)
require.NoError(t, err)
var decoded map[string]any
require.NoError(t, json.Unmarshal(raw, &decoded))
for _, key := range []string{"id", "status", "billing_model_source", "restrict_models", "groups", "supported_models"} {
_, exists := decoded[key]
require.Truef(t, exists, "admin DTO must expose %q", key)
}
}
func TestAvailableChannelToAdminResponse_PreservesExplicitBillingSource(t *testing.T) {
input := service.AvailableChannel{
BillingModelSource: service.BillingModelSourceUpstream,
}
resp := AvailableChannelToAdminResponse(input)
require.Equal(t, service.BillingModelSourceUpstream, resp.BillingModelSource)
}