package provision import ( "context" "errors" "fmt" "reflect" "strings" "testing" "sub2api-cn-relay-manager/internal/host/sub2api" "sub2api-cn-relay-manager/internal/pack" ) func TestImportServiceImportSubscriptionFlow(t *testing.T) { host := &fakeHostAdapter{ batchAccounts: []sub2api.AccountRef{{ID: "account_1"}, {ID: "account_2"}}, testResults: map[string]sub2api.ProbeResult{ "account_1": {OK: true, Status: "passed"}, "account_2": {OK: true, Status: "passed"}, }, models: map[string][]sub2api.AccountModel{ "account_1": {{ID: "deepseek-chat"}}, "account_2": {{ID: "deepseek-chat"}}, }, gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}}, } svc := NewImportService(host) report, err := svc.Import(context.Background(), ImportRequest{ Provider: sampleProviderManifest(), Mode: ImportModePartial, Access: AccessRequest{ Mode: AccessModeSubscription, ProbeAPIKey: "user-key", Subscriptions: []SubscriptionTarget{{UserID: "user_1", DurationDays: 30}}, }, Keys: []string{" key-1 ", "key-2", "key-1"}, }) if err != nil { t.Fatalf("Import() error = %v", err) } if report.BatchStatus != BatchStatusSucceeded { t.Fatalf("BatchStatus = %q, want %q", report.BatchStatus, BatchStatusSucceeded) } if report.ProviderStatus != ProviderStatusActive { t.Fatalf("ProviderStatus = %q, want %q", report.ProviderStatus, ProviderStatusActive) } if report.AccessStatus != AccessStatusSubscriptionReady { t.Fatalf("AccessStatus = %q, want %q", report.AccessStatus, AccessStatusSubscriptionReady) } if !reflect.DeepEqual(report.AcceptedKeys, []string{"key-1", "key-2"}) { t.Fatalf("AcceptedKeys = %#v, want deduped normalized keys", report.AcceptedKeys) } if len(host.assignedSubscriptions) != 1 { t.Fatalf("assigned subscriptions = %d, want 1", len(host.assignedSubscriptions)) } if host.createGroupReq.SubscriptionType != "subscription" { t.Fatalf("CreateGroup subscription_type = %q, want %q", host.createGroupReq.SubscriptionType, "subscription") } if host.createGroupReq.Platform != "openai" { t.Fatalf("CreateGroup platform = %q, want %q", host.createGroupReq.Platform, "openai") } if host.gatewayProbe.ExpectedModel != "deepseek-chat" { t.Fatalf("gateway probe model = %q, want %q", host.gatewayProbe.ExpectedModel, "deepseek-chat") } } func TestImportServiceStrictModeFailsWhenAnyAccountProbeFails(t *testing.T) { host := &fakeHostAdapter{ batchAccounts: []sub2api.AccountRef{{ID: "account_1"}, {ID: "account_2"}}, testResults: map[string]sub2api.ProbeResult{ "account_1": {OK: true, Status: "passed"}, "account_2": {OK: false, Status: "failed", Message: "bad key"}, }, models: map[string][]sub2api.AccountModel{ "account_1": {{ID: "deepseek-chat"}}, "account_2": {{ID: "deepseek-chat"}}, }, } svc := NewImportService(host) report, err := svc.Import(context.Background(), ImportRequest{ Provider: sampleProviderManifest(), Mode: ImportModeStrict, Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"}, Keys: []string{"key-1", "key-2"}, }) if err == nil { t.Fatal("Import() error = nil, want strict mode failure") } if report.BatchStatus != BatchStatusFailed { t.Fatalf("BatchStatus = %q, want %q", report.BatchStatus, BatchStatusFailed) } if report.ProviderStatus != ProviderStatusFailed { t.Fatalf("ProviderStatus = %q, want %q", report.ProviderStatus, ProviderStatusFailed) } } func TestImportServiceRejectsUnknownMode(t *testing.T) { svc := NewImportService(&fakeHostAdapter{}) _, err := svc.Import(context.Background(), ImportRequest{ Provider: sampleProviderManifest(), Mode: "unknown", Access: AccessRequest{Mode: AccessModeSelfService}, Keys: []string{"key-1"}, }) if err == nil { t.Fatal("Import() error = nil, want mode validation error") } } func TestImportServiceStrictModeRollsBackCreatedResources(t *testing.T) { host := &fakeHostAdapter{ batchAccounts: []sub2api.AccountRef{{ID: "account_1"}, {ID: "account_2"}}, testResults: map[string]sub2api.ProbeResult{ "account_1": {OK: true, Status: "passed"}, "account_2": {OK: false, Status: "failed", Message: "bad key"}, }, models: map[string][]sub2api.AccountModel{ "account_1": {{ID: "deepseek-chat"}}, "account_2": {{ID: "deepseek-chat"}}, }, } svc := NewImportService(host) _, err := svc.Import(context.Background(), ImportRequest{ Provider: sampleProviderManifest(), Mode: ImportModeStrict, Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"}, Keys: []string{"key-1", "key-2"}, }) if err == nil { t.Fatal("Import() error = nil, want strict mode failure") } want := []string{"account:account_2", "account:account_1", "channel:channel_1", "group:group_1"} if !reflect.DeepEqual(host.deletedResources, want) { t.Fatalf("deleted resources = %#v, want %#v", host.deletedResources, want) } } func TestImportReusesExistingGroup(t *testing.T) { host := &fakeHostAdapter{ batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "deepseek-01"}}, testResults: map[string]sub2api.ProbeResult{ "account_1": {OK: true, Status: "ready"}, }, models: map[string][]sub2api.AccountModel{ "account_1": {{ID: "deepseek-chat"}}, }, gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}}, managedSnapshot: sub2api.ManagedResourceSnapshot{ Groups: []sub2api.NamedResource{{ID: "group_existing", Name: "DeepSeek 默认分组-self-service"}}, }, } svc := NewImportService(host) report, err := svc.Import(context.Background(), ImportRequest{ Provider: sampleProviderManifest(), Mode: ImportModePartial, Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"}, Keys: []string{"key-1"}, }) if err != nil { t.Fatalf("Import() error = %v", err) } if report.Group.ID != "group_existing" { t.Fatalf("Group.ID = %q, want reused group_existing", report.Group.ID) } if host.createGroupCalls != 0 { t.Fatalf("CreateGroup() calls = %d, want 0 when group already exists", host.createGroupCalls) } if host.createChannelCalls != 1 { t.Fatalf("CreateChannel() calls = %d, want 1", host.createChannelCalls) } } func TestImportCreatesChannelWithManifestModelMapping(t *testing.T) { host := &fakeHostAdapter{ batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "deepseek-01"}}, testResults: map[string]sub2api.ProbeResult{ "account_1": {OK: true, Status: "ready"}, }, models: map[string][]sub2api.AccountModel{ "account_1": {{ID: "deepseek-chat"}}, }, gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}}, } _, err := NewImportService(host).Import(context.Background(), ImportRequest{ Provider: sampleProviderManifest(), Mode: ImportModePartial, Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"}, Keys: []string{"key-1"}, }) if err != nil { t.Fatalf("Import() error = %v", err) } if host.createChannelReq.Name != "DeepSeek 默认渠道-self-service" { t.Fatalf("CreateChannel().Name = %q, want DeepSeek 默认渠道-self-service", host.createChannelReq.Name) } if len(host.createChannelReq.GroupIDs) != 1 || host.createChannelReq.GroupIDs[0] != "group_1" { t.Fatalf("CreateChannel().GroupIDs = %v, want [group_1]", host.createChannelReq.GroupIDs) } if got := host.createChannelReq.ModelMapping["deepseek-chat"]; got != "deepseek-chat" { t.Fatalf("CreateChannel().ModelMapping = %+v, want deepseek-chat passthrough", host.createChannelReq.ModelMapping) } if !host.createChannelReq.RestrictModels { t.Fatal("CreateChannel().RestrictModels = false, want true") } if host.createChannelReq.BillingModelSource != "channel_mapped" { t.Fatalf("CreateChannel().BillingModelSource = %q, want channel_mapped", host.createChannelReq.BillingModelSource) } if len(host.createChannelReq.ModelPricing) != 1 { t.Fatalf("CreateChannel().ModelPricing len = %d, want 1", len(host.createChannelReq.ModelPricing)) } if len(host.createChannelReq.ModelPricing[0].Models) != 2 { t.Fatalf("CreateChannel().ModelPricing[0].Models = %v, want default model coverage", host.createChannelReq.ModelPricing[0].Models) } if host.createChannelReq.ModelPricing[0].BillingMode != "token" { t.Fatalf("CreateChannel().ModelPricing[0].BillingMode = %q, want token", host.createChannelReq.ModelPricing[0].BillingMode) } if len(host.batchCreateReq.Accounts) != 1 { t.Fatalf("BatchCreateAccounts().Accounts len = %d, want 1", len(host.batchCreateReq.Accounts)) } credentials := host.batchCreateReq.Accounts[0].Credentials switch rawMapping := credentials["model_mapping"].(type) { case map[string]string: if got := rawMapping["deepseek-chat"]; got != "deepseek-chat" { t.Fatalf("BatchCreateAccounts().Credentials.model_mapping = %+v, want deepseek-chat passthrough", rawMapping) } case map[string]any: if got, _ := rawMapping["deepseek-chat"].(string); got != "deepseek-chat" { t.Fatalf("BatchCreateAccounts().Credentials.model_mapping = %+v, want deepseek-chat passthrough", rawMapping) } default: t.Fatalf("BatchCreateAccounts().Credentials = %+v, want model_mapping map", credentials) } } func sampleProviderManifest() pack.ProviderManifest { return pack.ProviderManifest{ ProviderID: "deepseek", DisplayName: "DeepSeek OpenAI Compatible", BaseURL: "https://api.deepseek.com", Platform: "openai", AccountType: "apikey", DefaultModels: []string{"deepseek-chat", "deepseek-reasoner"}, SmokeTestModel: "deepseek-chat", GroupTemplate: pack.GroupTemplate{Name: "DeepSeek 默认分组", RateMultiplier: 1}, ChannelTemplate: pack.ChannelTemplate{Name: "DeepSeek 默认渠道", ModelMapping: map[string]string{"deepseek-chat": "deepseek-chat"}}, PlanTemplate: pack.PlanTemplate{Name: "DeepSeek 默认套餐", Price: 19.9, ValidityDays: 30, ValidityUnit: "day"}, } } func TestImportReconcilesExistingChannelConfiguration(t *testing.T) { host := &fakeHostAdapter{ batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "deepseek-01"}}, testResults: map[string]sub2api.ProbeResult{ "account_1": {OK: true, Status: "ready"}, }, models: map[string][]sub2api.AccountModel{ "account_1": {{ID: "deepseek-chat"}}, }, gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}}, managedSnapshot: sub2api.ManagedResourceSnapshot{ Groups: []sub2api.NamedResource{{ID: "group_existing", Name: "DeepSeek 默认分组-self-service"}}, Channels: []sub2api.NamedResource{{ID: "channel_existing", Name: "DeepSeek 默认渠道-self-service"}}, }, } _, err := NewImportService(host).Import(context.Background(), ImportRequest{ Provider: sampleProviderManifest(), Mode: ImportModePartial, Access: AccessRequest{Mode: AccessModeSelfService, ProbeAPIKey: "user-key"}, Keys: []string{"key-1"}, }) if err != nil { t.Fatalf("Import() error = %v", err) } if host.createChannelCalls != 0 { t.Fatalf("CreateChannel() calls = %d, want 0 when channel already exists", host.createChannelCalls) } if host.updateChannelCalls != 1 { t.Fatalf("UpdateChannel() calls = %d, want 1", host.updateChannelCalls) } if host.updateChannelID != "channel_existing" { t.Fatalf("UpdateChannel() id = %q, want channel_existing", host.updateChannelID) } if len(host.updateChannelReq.ModelPricing) != 1 { t.Fatalf("UpdateChannel().ModelPricing len = %d, want 1", len(host.updateChannelReq.ModelPricing)) } } type fakeHostAdapter struct { batchAccounts []sub2api.AccountRef batchCreateReq sub2api.BatchCreateAccountsRequest testResults map[string]sub2api.ProbeResult models map[string][]sub2api.AccountModel gatewayResult sub2api.GatewayAccessResult batchCreateErr error assignErr error gatewayErr error hostVersion string assignedSubscriptions []sub2api.AssignSubscriptionRequest gatewayProbe sub2api.GatewayAccessCheckRequest deletedResources []string managedSnapshot sub2api.ManagedResourceSnapshot listManagedReq sub2api.ListManagedResourcesRequest createGroupCalls int createChannelCalls int updateChannelCalls int createPlanCalls int createGroupReq sub2api.CreateGroupRequest createChannelReq sub2api.CreateChannelRequest updateChannelID string updateChannelReq sub2api.CreateChannelRequest } func (f *fakeHostAdapter) GetHostVersion(context.Context) (string, error) { if strings.TrimSpace(f.hostVersion) == "" { return "0.1.126", nil } return f.hostVersion, nil } func (f *fakeHostAdapter) ProbeCapabilities(context.Context) (sub2api.HostCapabilities, error) { return sub2api.HostCapabilities{}, nil } func (f *fakeHostAdapter) CreateGroup(_ context.Context, req sub2api.CreateGroupRequest) (sub2api.GroupRef, error) { f.createGroupCalls++ f.createGroupReq = req return sub2api.GroupRef{ID: "group_1", Name: "g"}, nil } func (f *fakeHostAdapter) DeleteGroup(_ context.Context, groupID string) error { f.deletedResources = append(f.deletedResources, "group:"+groupID) return nil } func (f *fakeHostAdapter) CreateChannel(_ context.Context, req sub2api.CreateChannelRequest) (sub2api.ChannelRef, error) { f.createChannelCalls++ f.createChannelReq = req return sub2api.ChannelRef{ID: "channel_1", Name: "c"}, nil } func (f *fakeHostAdapter) UpdateChannel(_ context.Context, channelID string, req sub2api.CreateChannelRequest) error { f.updateChannelCalls++ f.updateChannelID = channelID f.updateChannelReq = req return nil } func (f *fakeHostAdapter) DeleteChannel(_ context.Context, channelID string) error { f.deletedResources = append(f.deletedResources, "channel:"+channelID) return nil } func (f *fakeHostAdapter) CreatePlan(context.Context, sub2api.CreatePlanRequest) (sub2api.PlanRef, error) { f.createPlanCalls++ return sub2api.PlanRef{ID: "plan_1", Name: "p"}, nil } func (f *fakeHostAdapter) DeletePlan(_ context.Context, planID string) error { f.deletedResources = append(f.deletedResources, "plan:"+planID) return nil } func (f *fakeHostAdapter) CreateAccount(context.Context, sub2api.CreateAccountRequest) (sub2api.AccountRef, error) { return sub2api.AccountRef{}, errors.New("unused") } func (f *fakeHostAdapter) BatchCreateAccounts(_ context.Context, req sub2api.BatchCreateAccountsRequest) ([]sub2api.AccountRef, error) { f.batchCreateReq = req if f.batchCreateErr != nil { return nil, f.batchCreateErr } return f.batchAccounts, nil } func (f *fakeHostAdapter) DeleteAccount(_ context.Context, accountID string) error { f.deletedResources = append(f.deletedResources, "account:"+accountID) return nil } func (f *fakeHostAdapter) TestAccount(_ context.Context, accountID string) (sub2api.ProbeResult, error) { result, ok := f.testResults[accountID] if !ok { return sub2api.ProbeResult{}, fmt.Errorf("missing test result for %s", accountID) } return result, nil } func (f *fakeHostAdapter) GetAccountModels(_ context.Context, accountID string) ([]sub2api.AccountModel, error) { models, ok := f.models[accountID] if !ok { return nil, fmt.Errorf("missing models for %s", accountID) } return models, nil } func (f *fakeHostAdapter) EnsureSubscriptionAccess(_ context.Context, req sub2api.EnsureSubscriptionAccessRequest) (sub2api.SubscriptionAccessRef, error) { return sub2api.SubscriptionAccessRef{UserID: req.UserSelector, APIKey: "managed-subscription-key"}, nil } func (f *fakeHostAdapter) AssignSubscription(_ context.Context, req sub2api.AssignSubscriptionRequest) (sub2api.SubscriptionRef, error) { if f.assignErr != nil { return sub2api.SubscriptionRef{}, f.assignErr } f.assignedSubscriptions = append(f.assignedSubscriptions, req) return sub2api.SubscriptionRef{ID: "subscription_1"}, nil } func (f *fakeHostAdapter) CheckGatewayAccess(_ context.Context, req sub2api.GatewayAccessCheckRequest) (sub2api.GatewayAccessResult, error) { f.gatewayProbe = req if f.gatewayErr != nil { return sub2api.GatewayAccessResult{}, f.gatewayErr } return f.gatewayResult, nil } func (f *fakeHostAdapter) ListManagedResources(_ context.Context, req sub2api.ListManagedResourcesRequest) (sub2api.ManagedResourceSnapshot, error) { f.listManagedReq = req return sub2api.ManagedResourceSnapshot{ Groups: filterNamedResourcesByExactName(f.managedSnapshot.Groups, req.GroupName), Channels: filterNamedResourcesByExactName(f.managedSnapshot.Channels, req.ChannelName), Plans: filterNamedResourcesByExactName(f.managedSnapshot.Plans, req.PlanName), Accounts: filterNamedResourcesByPrefix(f.managedSnapshot.Accounts, req.AccountNamePrefix), }, nil } func filterNamedResourcesByExactName(resources []sub2api.NamedResource, expected string) []sub2api.NamedResource { expected = strings.TrimSpace(expected) if expected == "" { return nil } filtered := make([]sub2api.NamedResource, 0, len(resources)) for _, resource := range resources { if strings.TrimSpace(resource.Name) == expected { filtered = append(filtered, resource) } } return filtered } func filterNamedResourcesByPrefix(resources []sub2api.NamedResource, prefix string) []sub2api.NamedResource { prefix = strings.TrimSpace(prefix) if prefix == "" { return resources } filtered := make([]sub2api.NamedResource, 0, len(resources)) for _, resource := range resources { if strings.HasPrefix(strings.TrimSpace(resource.Name), prefix) { filtered = append(filtered, resource) } } return filtered }