Files
pangolin/server/internal/scheduler/wiring_provision_test.go
wangjia 5cc63f9e28 feat(scheduler): provision 真实接线适配器(1/2) — orchestrate.ProvisionService over provision.Service
把 scheduler 的 stub provision 换成真实 provision.Service(#14)的适配器:
- 类型映射:orchestrate.NodeSpec → provision.NodeSpec(ProviderID string→int64、
  Role/Tier 转换、reality/name 字段);CreateNode 返回 node.UUID 作 orchestrate ID。
- uuid↔int64:Destroy/RotateIP 用 provision.Store.GetNodeByUUID 解析。
- ListProviders:tier→pool(pro=premium/free=consumable) + region 过滤 + enabled 过滤。
- 依赖小接口(provisionSvc/provisionResolver)以便单测;映射全单测覆盖
  (spec 映射/uuid 解析/pool+region+enabled 过滤)。
- e2e(真实创建/销毁节点)待有机群 + 厂商凭证后验证。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 09:14:54 +08:00

133 lines
4.3 KiB
Go

package scheduler
import (
"context"
"errors"
"testing"
"github.com/wangjia/pangolin/server/internal/provision"
"github.com/wangjia/pangolin/server/internal/scheduler/orchestrate"
)
// fakeProvSvc records calls and returns canned results.
type fakeProvSvc struct {
createSpec provision.NodeSpec
createKey string
created *provision.Node
destroyID int64
rotateID int64
rotated *provision.Node
providers []*provision.Provider
lastPool provision.Pool
err error
}
func (f *fakeProvSvc) CreateNode(_ context.Context, spec provision.NodeSpec, key string) (*provision.Node, error) {
f.createSpec, f.createKey = spec, key
return f.created, f.err
}
func (f *fakeProvSvc) DestroyNode(_ context.Context, id int64) error { f.destroyID = id; return f.err }
func (f *fakeProvSvc) RotateIP(_ context.Context, id int64) (*provision.Node, error) {
f.rotateID = id
return f.rotated, f.err
}
func (f *fakeProvSvc) ListProviders(_ context.Context, pool provision.Pool) ([]*provision.Provider, error) {
f.lastPool = pool
return f.providers, f.err
}
// fakeResolver maps known UUIDs → ids.
type fakeResolver struct{ byUUID map[string]*provision.Node }
func (f fakeResolver) GetNodeByUUID(_ context.Context, uuid string) (*provision.Node, error) {
n, ok := f.byUUID[uuid]
if !ok {
return nil, errors.New("not found")
}
return n, nil
}
func TestProvisionAdapter_CreateNode_MapsSpecAndReturnsUUID(t *testing.T) {
svc := &fakeProvSvc{created: &provision.Node{ID: 7, UUID: "uuid-7"}}
a := NewProvisionAdapter(svc, fakeResolver{})
id, err := a.CreateNode(context.Background(), orchestrate.NodeSpec{
Tier: "pro", Region: "hkg", Role: "exit", ProviderID: "42",
RealitySNI: "www.apple.com", RealityPBK: "pbk", HY2Port: 443,
NameZH: "香港", NameEn: "HK",
}, "idem-1")
if err != nil {
t.Fatalf("CreateNode: %v", err)
}
if id != "uuid-7" {
t.Errorf("returned id = %q, want node UUID uuid-7", id)
}
if svc.createKey != "idem-1" {
t.Errorf("idempotency key = %q, want idem-1", svc.createKey)
}
s := svc.createSpec
if s.Region != "hkg" || s.Role != provision.Role("exit") || s.Tier != provision.TierPro ||
s.ProviderID != 42 || s.RealitySNI != "www.apple.com" || s.RealityPBK != "pbk" ||
s.HY2Port != 443 || s.NameZH != "香港" || s.NameEn != "HK" {
t.Errorf("spec mapped wrong: %+v", s)
}
}
func TestProvisionAdapter_DestroyAndRotate_ResolveUUID(t *testing.T) {
svc := &fakeProvSvc{rotated: &provision.Node{ID: 9, UUID: "uuid-9"}}
res := fakeResolver{byUUID: map[string]*provision.Node{"uuid-9": {ID: 9, UUID: "uuid-9"}}}
a := NewProvisionAdapter(svc, res)
if err := a.DestroyNode(context.Background(), "uuid-9"); err != nil {
t.Fatalf("DestroyNode: %v", err)
}
if svc.destroyID != 9 {
t.Errorf("destroy resolved id = %d, want 9", svc.destroyID)
}
newID, err := a.RotateIP(context.Background(), "uuid-9")
if err != nil {
t.Fatalf("RotateIP: %v", err)
}
if svc.rotateID != 9 || newID != "uuid-9" {
t.Errorf("rotate id=%d newID=%q, want 9 / uuid-9", svc.rotateID, newID)
}
// Unknown UUID must error, not panic.
if err := a.DestroyNode(context.Background(), "nope"); err == nil {
t.Error("DestroyNode with unknown uuid should error")
}
}
func TestProvisionAdapter_ListProviders_PoolAndRegionFilter(t *testing.T) {
svc := &fakeProvSvc{providers: []*provision.Provider{
{ID: 1, Regions: []string{"hkg", "tyo"}, Enabled: true},
{ID: 2, Regions: []string{"sin"}, Enabled: true}, // wrong region
{ID: 3, Regions: nil, Enabled: true}, // all regions
{ID: 4, Regions: []string{"hkg"}, Enabled: false}, // disabled
}}
a := NewProvisionAdapter(svc, fakeResolver{})
got, err := a.ListProviders(context.Background(), "pro", "hkg")
if err != nil {
t.Fatalf("ListProviders: %v", err)
}
if svc.lastPool != provision.PoolPremium {
t.Errorf("pool = %q, want premium (pro tier)", svc.lastPool)
}
// Expect providers 1 (region match) and 3 (all regions); 2 wrong region, 4 disabled.
ids := map[string]bool{}
for _, p := range got {
ids[p.ID] = true
}
if !ids["1"] || !ids["3"] || ids["2"] || ids["4"] {
t.Errorf("filtered providers = %v, want {1,3}", ids)
}
// free tier → consumable pool.
_, _ = a.ListProviders(context.Background(), "free", "")
if svc.lastPool != provision.PoolConsumable {
t.Errorf("pool = %q, want consumable (free tier)", svc.lastPool)
}
}