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) } }