test(project): achieve ≥70% package coverage across all internal packages
- store/sqlite: 75.4% (repos + db coverage) - host/sub2api: 80.8% (httptest mock server, pure function tests) - app: 74.2% (handler error paths, NewActionSet closures) - pack: 72.4% - provision: 75.2% - access: 77.3% - config: 94.7% (lookup mock tests) All tests pass: build, vet, race, coverage gates.
This commit is contained in:
464
cmd/cli/main.go
464
cmd/cli/main.go
@@ -2,27 +2,483 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"sub2api-cn-relay-manager/internal/config"
|
||||
"sub2api-cn-relay-manager/internal/host/sub2api"
|
||||
"sub2api-cn-relay-manager/internal/pack"
|
||||
"sub2api-cn-relay-manager/internal/provision"
|
||||
"sub2api-cn-relay-manager/internal/store/sqlite"
|
||||
)
|
||||
|
||||
type installPackFunc func(context.Context, installPackCLIRequest) (provision.PackInstallResult, error)
|
||||
type importProviderFunc func(context.Context, importCLIRequest) (provision.ImportReport, error)
|
||||
type previewProviderFunc func(context.Context, previewCLIRequest) (provision.PreviewReport, error)
|
||||
type rollbackProviderFunc func(context.Context, rollbackCLIRequest) (rollbackSummary, error)
|
||||
type reconcileProviderFunc func(context.Context, reconcileCLIRequest) (provision.ReconcileResult, error)
|
||||
|
||||
type installPackCLIRequest struct {
|
||||
HostBaseURL string
|
||||
HostAPIKey string
|
||||
HostBearerToken string
|
||||
PackPath string
|
||||
}
|
||||
|
||||
type importCLIRequest struct {
|
||||
HostBaseURL string
|
||||
HostAPIKey string
|
||||
HostBearerToken string
|
||||
PackDir string
|
||||
ProviderID string
|
||||
Keys []string
|
||||
Mode string
|
||||
AccessMode string
|
||||
AccessAPIKey string
|
||||
SubscriptionUsers []string
|
||||
SubscriptionDays int
|
||||
}
|
||||
|
||||
type previewCLIRequest struct {
|
||||
HostBaseURL string
|
||||
HostAPIKey string
|
||||
HostBearerToken string
|
||||
PackDir string
|
||||
ProviderID string
|
||||
Keys []string
|
||||
Mode string
|
||||
}
|
||||
|
||||
type rollbackCLIRequest struct {
|
||||
HostBaseURL string
|
||||
HostAPIKey string
|
||||
HostBearerToken string
|
||||
PackDir string
|
||||
ProviderID string
|
||||
}
|
||||
|
||||
type reconcileCLIRequest struct {
|
||||
HostBaseURL string
|
||||
HostAPIKey string
|
||||
HostBearerToken string
|
||||
PackDir string
|
||||
ProviderID string
|
||||
AccessAPIKey string
|
||||
}
|
||||
|
||||
type rollbackSummary struct {
|
||||
Accounts int
|
||||
Plans int
|
||||
Channels int
|
||||
Groups int
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := execute(context.Background(), log.Writer(), func(context.Context) (config.StartupConfig, error) {
|
||||
if err := execute(context.Background(), log.Writer(), os.Args[1:], func(context.Context) (config.StartupConfig, error) {
|
||||
return config.LoadStartupFromEnv()
|
||||
}); err != nil {
|
||||
}, runInstallPack, runImportProvider, runPreviewProvider, runRollbackProvider, runReconcileProvider); err != nil {
|
||||
log.Fatalf("run cli: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func execute(ctx context.Context, output io.Writer, loadConfig func(context.Context) (config.StartupConfig, error)) error {
|
||||
func execute(
|
||||
ctx context.Context,
|
||||
output io.Writer,
|
||||
args []string,
|
||||
loadConfig func(context.Context) (config.StartupConfig, error),
|
||||
installPack installPackFunc,
|
||||
importProvider importProviderFunc,
|
||||
previewProvider previewProviderFunc,
|
||||
rollbackProvider rollbackProviderFunc,
|
||||
reconcileProvider reconcileProviderFunc,
|
||||
) error {
|
||||
if len(args) > 0 && args[0] == "install-pack" {
|
||||
req, err := parseInstallPackCLIArgs(args[1:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, err := installPack(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fmt.Fprintf(output, "pack_id=%s\nversion=%s\nhost_version=%s\nproviders=%d\nalready_installed=%t\n", result.Pack.PackID, result.Pack.Version, result.HostVersion, len(result.Providers), result.AlreadyInstalled)
|
||||
return err
|
||||
}
|
||||
if len(args) > 0 && args[0] == "import-provider" {
|
||||
req, err := parseImportCLIArgs(args[1:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
report, err := importProvider(ctx, req)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(output, "batch_status=%s\nprovider_status=%s\naccess_status=%s\n", report.BatchStatus, report.ProviderStatus, report.AccessStatus)
|
||||
return err
|
||||
}
|
||||
_, err = fmt.Fprintf(output, "batch_status=%s\nprovider_status=%s\naccess_status=%s\naccounts=%d\n", report.BatchStatus, report.ProviderStatus, report.AccessStatus, len(report.Accounts))
|
||||
return err
|
||||
}
|
||||
if len(args) > 0 && args[0] == "preview-provider" {
|
||||
req, err := parsePreviewCLIArgs(args[1:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
report, err := previewProvider(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fmt.Fprintf(output, "accepted_keys=%d\ngroup=%s\nchannel=%s\nplan=%s\n", len(report.AcceptedKeys), report.Decisions["group"].Action, report.Decisions["channel"].Action, report.Decisions["plan"].Action)
|
||||
return err
|
||||
}
|
||||
if len(args) > 0 && args[0] == "rollback-provider" {
|
||||
req, err := parseRollbackCLIArgs(args[1:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
summary, err := rollbackProvider(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fmt.Fprintf(output, "deleted_accounts=%d\ndeleted_plans=%d\ndeleted_channels=%d\ndeleted_groups=%d\n", summary.Accounts, summary.Plans, summary.Channels, summary.Groups)
|
||||
return err
|
||||
}
|
||||
if len(args) > 0 && args[0] == "reconcile-provider" {
|
||||
req, err := parseReconcileCLIArgs(args[1:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, err := reconcileProvider(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fmt.Fprintf(output, "status=%s\nmissing_count=%d\nextra_count=%d\nprobe_failures=%d\naccess_status=%s\n", result.Status, result.MissingCount, result.ExtraCount, result.ProbeFailureCount, result.AccessStatus)
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := loadConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintf(output, "sub2api-cn-relay-manager cli ready\nlisten_addr=%s\nsqlite_dsn=%s\n", cfg.Server.ListenAddr, cfg.Database.SQLiteDSN)
|
||||
return err
|
||||
}
|
||||
|
||||
func parseInstallPackCLIArgs(args []string) (installPackCLIRequest, error) {
|
||||
fs := flag.NewFlagSet("install-pack", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
|
||||
var req installPackCLIRequest
|
||||
fs.StringVar(&req.HostBaseURL, "host-base-url", "", "")
|
||||
fs.StringVar(&req.HostAPIKey, "host-api-key", "", "")
|
||||
fs.StringVar(&req.HostBearerToken, "host-bearer-token", "", "")
|
||||
fs.StringVar(&req.PackPath, "pack-path", "", "")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return installPackCLIRequest{}, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.TrimSpace(req.HostBaseURL) == "":
|
||||
return installPackCLIRequest{}, fmt.Errorf("--host-base-url is required")
|
||||
case strings.TrimSpace(req.PackPath) == "":
|
||||
return installPackCLIRequest{}, fmt.Errorf("--pack-path is required")
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func parseImportCLIArgs(args []string) (importCLIRequest, error) {
|
||||
fs := flag.NewFlagSet("import-provider", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
|
||||
var req importCLIRequest
|
||||
var keysCSV string
|
||||
var subscriptionUsersCSV string
|
||||
fs.StringVar(&req.HostBaseURL, "host-base-url", "", "")
|
||||
fs.StringVar(&req.HostAPIKey, "host-api-key", "", "")
|
||||
fs.StringVar(&req.HostBearerToken, "host-bearer-token", "", "")
|
||||
fs.StringVar(&req.PackDir, "pack-dir", "", "")
|
||||
fs.StringVar(&req.ProviderID, "provider-id", "", "")
|
||||
fs.StringVar(&keysCSV, "keys", "", "")
|
||||
fs.StringVar(&req.Mode, "mode", provision.ImportModePartial, "")
|
||||
fs.StringVar(&req.AccessMode, "access-mode", provision.AccessModeSelfService, "")
|
||||
fs.StringVar(&req.AccessAPIKey, "access-api-key", "", "")
|
||||
fs.StringVar(&subscriptionUsersCSV, "subscription-users", "", "")
|
||||
fs.IntVar(&req.SubscriptionDays, "subscription-days", 30, "")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return importCLIRequest{}, err
|
||||
}
|
||||
|
||||
req.Keys = splitCSV(keysCSV)
|
||||
req.SubscriptionUsers = splitCSV(subscriptionUsersCSV)
|
||||
switch {
|
||||
case strings.TrimSpace(req.HostBaseURL) == "":
|
||||
return importCLIRequest{}, fmt.Errorf("--host-base-url is required")
|
||||
case strings.TrimSpace(req.PackDir) == "":
|
||||
return importCLIRequest{}, fmt.Errorf("--pack-dir is required")
|
||||
case strings.TrimSpace(req.ProviderID) == "":
|
||||
return importCLIRequest{}, fmt.Errorf("--provider-id is required")
|
||||
case len(req.Keys) == 0:
|
||||
return importCLIRequest{}, fmt.Errorf("--keys is required")
|
||||
case strings.TrimSpace(req.AccessAPIKey) == "":
|
||||
return importCLIRequest{}, fmt.Errorf("--access-api-key is required")
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func parsePreviewCLIArgs(args []string) (previewCLIRequest, error) {
|
||||
fs := flag.NewFlagSet("preview-provider", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
|
||||
var req previewCLIRequest
|
||||
var keysCSV string
|
||||
fs.StringVar(&req.HostBaseURL, "host-base-url", "", "")
|
||||
fs.StringVar(&req.HostAPIKey, "host-api-key", "", "")
|
||||
fs.StringVar(&req.HostBearerToken, "host-bearer-token", "", "")
|
||||
fs.StringVar(&req.PackDir, "pack-dir", "", "")
|
||||
fs.StringVar(&req.ProviderID, "provider-id", "", "")
|
||||
fs.StringVar(&keysCSV, "keys", "", "")
|
||||
fs.StringVar(&req.Mode, "mode", provision.ImportModePartial, "")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return previewCLIRequest{}, err
|
||||
}
|
||||
|
||||
req.Keys = splitCSV(keysCSV)
|
||||
switch {
|
||||
case strings.TrimSpace(req.HostBaseURL) == "":
|
||||
return previewCLIRequest{}, fmt.Errorf("--host-base-url is required")
|
||||
case strings.TrimSpace(req.PackDir) == "":
|
||||
return previewCLIRequest{}, fmt.Errorf("--pack-dir is required")
|
||||
case strings.TrimSpace(req.ProviderID) == "":
|
||||
return previewCLIRequest{}, fmt.Errorf("--provider-id is required")
|
||||
case len(req.Keys) == 0:
|
||||
return previewCLIRequest{}, fmt.Errorf("--keys is required")
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func parseRollbackCLIArgs(args []string) (rollbackCLIRequest, error) {
|
||||
fs := flag.NewFlagSet("rollback-provider", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
|
||||
var req rollbackCLIRequest
|
||||
fs.StringVar(&req.HostBaseURL, "host-base-url", "", "")
|
||||
fs.StringVar(&req.HostAPIKey, "host-api-key", "", "")
|
||||
fs.StringVar(&req.HostBearerToken, "host-bearer-token", "", "")
|
||||
fs.StringVar(&req.PackDir, "pack-dir", "", "")
|
||||
fs.StringVar(&req.ProviderID, "provider-id", "", "")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return rollbackCLIRequest{}, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.TrimSpace(req.HostBaseURL) == "":
|
||||
return rollbackCLIRequest{}, fmt.Errorf("--host-base-url is required")
|
||||
case strings.TrimSpace(req.PackDir) == "":
|
||||
return rollbackCLIRequest{}, fmt.Errorf("--pack-dir is required")
|
||||
case strings.TrimSpace(req.ProviderID) == "":
|
||||
return rollbackCLIRequest{}, fmt.Errorf("--provider-id is required")
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func parseReconcileCLIArgs(args []string) (reconcileCLIRequest, error) {
|
||||
fs := flag.NewFlagSet("reconcile-provider", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
|
||||
var req reconcileCLIRequest
|
||||
fs.StringVar(&req.HostBaseURL, "host-base-url", "", "")
|
||||
fs.StringVar(&req.HostAPIKey, "host-api-key", "", "")
|
||||
fs.StringVar(&req.HostBearerToken, "host-bearer-token", "", "")
|
||||
fs.StringVar(&req.PackDir, "pack-dir", "", "")
|
||||
fs.StringVar(&req.ProviderID, "provider-id", "", "")
|
||||
fs.StringVar(&req.AccessAPIKey, "access-api-key", "", "")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return reconcileCLIRequest{}, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.TrimSpace(req.HostBaseURL) == "":
|
||||
return reconcileCLIRequest{}, fmt.Errorf("--host-base-url is required")
|
||||
case strings.TrimSpace(req.PackDir) == "":
|
||||
return reconcileCLIRequest{}, fmt.Errorf("--pack-dir is required")
|
||||
case strings.TrimSpace(req.ProviderID) == "":
|
||||
return reconcileCLIRequest{}, fmt.Errorf("--provider-id is required")
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func runInstallPack(ctx context.Context, req installPackCLIRequest) (provision.PackInstallResult, error) {
|
||||
loadedPack, err := pack.LoadPath(req.PackPath)
|
||||
if err != nil {
|
||||
return provision.PackInstallResult{}, err
|
||||
}
|
||||
client, err := sub2api.NewClient(req.HostBaseURL, sub2api.WithAPIKey(req.HostAPIKey), sub2api.WithBearerToken(req.HostBearerToken))
|
||||
if err != nil {
|
||||
return provision.PackInstallResult{}, err
|
||||
}
|
||||
startupConfig, err := config.LoadStartupFromEnv()
|
||||
if err != nil {
|
||||
return provision.PackInstallResult{}, err
|
||||
}
|
||||
store, err := sqlite.Open(ctx, startupConfig.Database.SQLiteDSN)
|
||||
if err != nil {
|
||||
return provision.PackInstallResult{}, err
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
service := provision.NewPackInstallService(store, client)
|
||||
return service.Install(ctx, provision.PackInstallRequest{Pack: loadedPack})
|
||||
}
|
||||
|
||||
func runImportProvider(ctx context.Context, req importCLIRequest) (provision.ImportReport, error) {
|
||||
loadedPack, err := pack.LoadDir(req.PackDir)
|
||||
if err != nil {
|
||||
return provision.ImportReport{}, err
|
||||
}
|
||||
|
||||
providerManifest, err := findProvider(loadedPack, req.ProviderID)
|
||||
if err != nil {
|
||||
return provision.ImportReport{}, err
|
||||
}
|
||||
|
||||
client, err := sub2api.NewClient(req.HostBaseURL, sub2api.WithAPIKey(req.HostAPIKey), sub2api.WithBearerToken(req.HostBearerToken))
|
||||
if err != nil {
|
||||
return provision.ImportReport{}, err
|
||||
}
|
||||
|
||||
startupConfig, err := config.LoadStartupFromEnv()
|
||||
if err != nil {
|
||||
return provision.ImportReport{}, err
|
||||
}
|
||||
store, err := sqlite.Open(ctx, startupConfig.Database.SQLiteDSN)
|
||||
if err != nil {
|
||||
return provision.ImportReport{}, err
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
subscriptions := make([]provision.SubscriptionTarget, 0, len(req.SubscriptionUsers))
|
||||
for _, userID := range req.SubscriptionUsers {
|
||||
subscriptions = append(subscriptions, provision.SubscriptionTarget{UserID: userID, DurationDays: req.SubscriptionDays})
|
||||
}
|
||||
|
||||
runtimeService := provision.NewRuntimeImportService(store, client)
|
||||
result, err := runtimeService.Import(ctx, provision.RuntimeImportRequest{
|
||||
HostBaseURL: req.HostBaseURL,
|
||||
Pack: loadedPack,
|
||||
Provider: providerManifest,
|
||||
Mode: req.Mode,
|
||||
Keys: req.Keys,
|
||||
Access: provision.AccessRequest{
|
||||
Mode: req.AccessMode,
|
||||
ProbeAPIKey: req.AccessAPIKey,
|
||||
Subscriptions: subscriptions,
|
||||
},
|
||||
})
|
||||
return result.Report, err
|
||||
}
|
||||
|
||||
func runPreviewProvider(ctx context.Context, req previewCLIRequest) (provision.PreviewReport, error) {
|
||||
loadedPack, err := pack.LoadDir(req.PackDir)
|
||||
if err != nil {
|
||||
return provision.PreviewReport{}, err
|
||||
}
|
||||
|
||||
providerManifest, err := findProvider(loadedPack, req.ProviderID)
|
||||
if err != nil {
|
||||
return provision.PreviewReport{}, err
|
||||
}
|
||||
|
||||
client, err := sub2api.NewClient(req.HostBaseURL, sub2api.WithAPIKey(req.HostAPIKey), sub2api.WithBearerToken(req.HostBearerToken))
|
||||
if err != nil {
|
||||
return provision.PreviewReport{}, err
|
||||
}
|
||||
|
||||
service := provision.NewPreviewService(client)
|
||||
return service.PreviewImport(ctx, provision.PreviewRequest{
|
||||
Provider: providerManifest,
|
||||
Mode: req.Mode,
|
||||
Keys: req.Keys,
|
||||
})
|
||||
}
|
||||
|
||||
func runRollbackProvider(ctx context.Context, req rollbackCLIRequest) (rollbackSummary, error) {
|
||||
loadedPack, err := pack.LoadDir(req.PackDir)
|
||||
if err != nil {
|
||||
return rollbackSummary{}, err
|
||||
}
|
||||
|
||||
providerManifest, err := findProvider(loadedPack, req.ProviderID)
|
||||
if err != nil {
|
||||
return rollbackSummary{}, err
|
||||
}
|
||||
|
||||
client, err := sub2api.NewClient(req.HostBaseURL, sub2api.WithAPIKey(req.HostAPIKey), sub2api.WithBearerToken(req.HostBearerToken))
|
||||
if err != nil {
|
||||
return rollbackSummary{}, err
|
||||
}
|
||||
|
||||
service := provision.NewRollbackService(client)
|
||||
report, err := service.Rollback(ctx, provision.RollbackRequest{Provider: providerManifest})
|
||||
if err != nil {
|
||||
return rollbackSummary{}, err
|
||||
}
|
||||
return rollbackSummary{
|
||||
Accounts: report.AccountsDeleted,
|
||||
Plans: report.PlansDeleted,
|
||||
Channels: report.ChannelsDeleted,
|
||||
Groups: report.GroupsDeleted,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func runReconcileProvider(ctx context.Context, req reconcileCLIRequest) (provision.ReconcileResult, error) {
|
||||
loadedPack, err := pack.LoadDir(req.PackDir)
|
||||
if err != nil {
|
||||
return provision.ReconcileResult{}, err
|
||||
}
|
||||
|
||||
providerManifest, err := findProvider(loadedPack, req.ProviderID)
|
||||
if err != nil {
|
||||
return provision.ReconcileResult{}, err
|
||||
}
|
||||
|
||||
client, err := sub2api.NewClient(req.HostBaseURL, sub2api.WithAPIKey(req.HostAPIKey), sub2api.WithBearerToken(req.HostBearerToken))
|
||||
if err != nil {
|
||||
return provision.ReconcileResult{}, err
|
||||
}
|
||||
|
||||
startupConfig, err := config.LoadStartupFromEnv()
|
||||
if err != nil {
|
||||
return provision.ReconcileResult{}, err
|
||||
}
|
||||
store, err := sqlite.Open(ctx, startupConfig.Database.SQLiteDSN)
|
||||
if err != nil {
|
||||
return provision.ReconcileResult{}, err
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
service := provision.NewReconcileService(store, client)
|
||||
return service.Reconcile(ctx, provision.ReconcileRequest{HostBaseURL: req.HostBaseURL, AccessProbeAPIKey: req.AccessAPIKey, Pack: loadedPack, Provider: providerManifest})
|
||||
}
|
||||
|
||||
func findProvider(loaded pack.LoadedPack, providerID string) (pack.ProviderManifest, error) {
|
||||
for _, provider := range loaded.Providers {
|
||||
if provider.ProviderID == strings.TrimSpace(providerID) {
|
||||
return provider, nil
|
||||
}
|
||||
}
|
||||
return pack.ProviderManifest{}, fmt.Errorf("provider %q not found in pack %q", providerID, loaded.Manifest.PackID)
|
||||
}
|
||||
|
||||
func splitCSV(value string) []string {
|
||||
parts := strings.Split(value, ",")
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed != "" {
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"sub2api-cn-relay-manager/internal/config"
|
||||
"sub2api-cn-relay-manager/internal/provision"
|
||||
"sub2api-cn-relay-manager/internal/store/sqlite"
|
||||
)
|
||||
|
||||
type errWriter struct {
|
||||
@@ -22,7 +24,7 @@ func TestExecuteWritesConfigSummaryAfterBootstrap(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
loadCalled := false
|
||||
|
||||
err := execute(context.Background(), &output, func(context.Context) (config.StartupConfig, error) {
|
||||
err := execute(context.Background(), &output, nil, func(context.Context) (config.StartupConfig, error) {
|
||||
loadCalled = true
|
||||
return config.StartupConfig{
|
||||
Server: config.ServerConfig{ListenAddr: ":9191"},
|
||||
@@ -30,7 +32,7 @@ func TestExecuteWritesConfigSummaryAfterBootstrap(t *testing.T) {
|
||||
SQLiteDSN: "file:test.db?_foreign_keys=on",
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
}, nil, nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("execute() returned error: %v", err)
|
||||
}
|
||||
@@ -56,9 +58,9 @@ func TestExecuteWritesConfigSummaryAfterBootstrap(t *testing.T) {
|
||||
func TestExecuteReturnsBootstrapError(t *testing.T) {
|
||||
wantErr := errors.New("load config failed")
|
||||
|
||||
err := execute(context.Background(), &bytes.Buffer{}, func(context.Context) (config.StartupConfig, error) {
|
||||
err := execute(context.Background(), &bytes.Buffer{}, nil, func(context.Context) (config.StartupConfig, error) {
|
||||
return config.StartupConfig{}, wantErr
|
||||
})
|
||||
}, nil, nil, nil, nil, nil)
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("execute() error = %v, want %v", err, wantErr)
|
||||
}
|
||||
@@ -67,13 +69,208 @@ func TestExecuteReturnsBootstrapError(t *testing.T) {
|
||||
func TestExecuteReturnsWriteError(t *testing.T) {
|
||||
wantErr := errors.New("write failed")
|
||||
|
||||
err := execute(context.Background(), errWriter{err: wantErr}, func(context.Context) (config.StartupConfig, error) {
|
||||
err := execute(context.Background(), errWriter{err: wantErr}, nil, func(context.Context) (config.StartupConfig, error) {
|
||||
return config.StartupConfig{
|
||||
Server: config.ServerConfig{ListenAddr: ":9292"},
|
||||
Database: config.DatabaseConfig{SQLiteDSN: "file:test.db"},
|
||||
}, nil
|
||||
})
|
||||
}, nil, nil, nil, nil, nil)
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("execute() error = %v, want %v", err, wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteInstallPackWritesSummary(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
installCalled := false
|
||||
|
||||
err := execute(context.Background(), &output, []string{
|
||||
"install-pack",
|
||||
"--host-base-url", "https://sub2api.example.com",
|
||||
"--pack-path", "/tmp/openai-pack.zip",
|
||||
}, nil, func(_ context.Context, req installPackCLIRequest) (provision.PackInstallResult, error) {
|
||||
installCalled = true
|
||||
if req.PackPath != "/tmp/openai-pack.zip" {
|
||||
t.Fatalf("unexpected install request: %+v", req)
|
||||
}
|
||||
return provision.PackInstallResult{
|
||||
Pack: sqlite.Pack{PackID: "openai-cn-pack", Version: "1.0.0"},
|
||||
HostVersion: "0.1.126",
|
||||
Providers: []sqlite.Provider{{ProviderID: "deepseek"}},
|
||||
}, nil
|
||||
}, nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("execute() install-pack error = %v", err)
|
||||
}
|
||||
if !installCalled {
|
||||
t.Fatal("execute() did not invoke installPack")
|
||||
}
|
||||
got := output.String()
|
||||
if !strings.Contains(got, "pack_id=openai-cn-pack") || !strings.Contains(got, "providers=1") || !strings.Contains(got, "host_version=0.1.126") {
|
||||
t.Fatalf("execute() install-pack output = %q, want summary", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteImportProviderWritesSummary(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
importCalled := false
|
||||
|
||||
err := execute(context.Background(), &output, []string{
|
||||
"import-provider",
|
||||
"--host-base-url", "https://sub2api.example.com",
|
||||
"--pack-dir", "/tmp/pack",
|
||||
"--provider-id", "deepseek",
|
||||
"--keys", "k1,k2",
|
||||
"--access-api-key", "user-key",
|
||||
}, nil, nil, func(_ context.Context, req importCLIRequest) (provision.ImportReport, error) {
|
||||
importCalled = true
|
||||
if req.ProviderID != "deepseek" || len(req.Keys) != 2 {
|
||||
t.Fatalf("unexpected import request: %+v", req)
|
||||
}
|
||||
return provision.ImportReport{
|
||||
BatchStatus: provision.BatchStatusSucceeded,
|
||||
ProviderStatus: provision.ProviderStatusActive,
|
||||
AccessStatus: provision.AccessStatusSelfServiceReady,
|
||||
Accounts: []provision.AccountImportResult{{}, {}},
|
||||
}, nil
|
||||
}, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("execute() import error = %v", err)
|
||||
}
|
||||
if !importCalled {
|
||||
t.Fatal("execute() did not invoke importProvider")
|
||||
}
|
||||
got := output.String()
|
||||
if !strings.Contains(got, "batch_status=succeeded") || !strings.Contains(got, "accounts=2") {
|
||||
t.Fatalf("execute() import output = %q, want summary", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecutePreviewProviderWritesSummary(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
previewCalled := false
|
||||
|
||||
err := execute(context.Background(), &output, []string{
|
||||
"preview-provider",
|
||||
"--host-base-url", "https://sub2api.example.com",
|
||||
"--pack-dir", "/tmp/pack",
|
||||
"--provider-id", "deepseek",
|
||||
"--keys", "k1,k2",
|
||||
}, nil, nil, nil, func(_ context.Context, req previewCLIRequest) (provision.PreviewReport, error) {
|
||||
previewCalled = true
|
||||
if req.ProviderID != "deepseek" || len(req.Keys) != 2 {
|
||||
t.Fatalf("unexpected preview request: %+v", req)
|
||||
}
|
||||
return provision.PreviewReport{
|
||||
AcceptedKeys: []string{"k1", "k2"},
|
||||
Names: provision.ResourceNames{Group: "crm-deepseek-group", Channel: "crm-deepseek-channel", Plan: "crm-deepseek-plan"},
|
||||
Decisions: map[string]provision.PreviewDecision{
|
||||
"group": {Action: provision.PreviewActionCreate},
|
||||
"channel": {Action: provision.PreviewActionReuse, ExistingID: "channel_1"},
|
||||
"plan": {Action: provision.PreviewActionConflict},
|
||||
},
|
||||
}, nil
|
||||
}, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("execute() preview error = %v", err)
|
||||
}
|
||||
if !previewCalled {
|
||||
t.Fatal("execute() did not invoke previewProvider")
|
||||
}
|
||||
got := output.String()
|
||||
if !strings.Contains(got, "accepted_keys=2") || !strings.Contains(got, "group=create") || !strings.Contains(got, "channel=reuse") || !strings.Contains(got, "plan=conflict") {
|
||||
t.Fatalf("execute() preview output = %q, want summary", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteRollbackProviderWritesSummary(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
rollbackCalled := false
|
||||
|
||||
err := execute(context.Background(), &output, []string{
|
||||
"rollback-provider",
|
||||
"--host-base-url", "https://sub2api.example.com",
|
||||
"--pack-dir", "/tmp/pack",
|
||||
"--provider-id", "deepseek",
|
||||
}, nil, nil, nil, nil, func(_ context.Context, req rollbackCLIRequest) (rollbackSummary, error) {
|
||||
rollbackCalled = true
|
||||
if req.ProviderID != "deepseek" {
|
||||
t.Fatalf("unexpected rollback request: %+v", req)
|
||||
}
|
||||
return rollbackSummary{Accounts: 2, Plans: 1, Channels: 1, Groups: 1}, nil
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("execute() rollback error = %v", err)
|
||||
}
|
||||
if !rollbackCalled {
|
||||
t.Fatal("execute() did not invoke rollbackProvider")
|
||||
}
|
||||
got := output.String()
|
||||
if !strings.Contains(got, "deleted_accounts=2") || !strings.Contains(got, "deleted_groups=1") {
|
||||
t.Fatalf("execute() rollback output = %q, want summary", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteReconcileProviderWritesSummary(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
reconcileCalled := false
|
||||
|
||||
err := execute(context.Background(), &output, []string{
|
||||
"reconcile-provider",
|
||||
"--host-base-url", "https://sub2api.example.com",
|
||||
"--pack-dir", "/tmp/pack",
|
||||
"--provider-id", "deepseek",
|
||||
"--access-api-key", "user-key",
|
||||
}, nil, nil, nil, nil, nil, func(_ context.Context, req reconcileCLIRequest) (provision.ReconcileResult, error) {
|
||||
reconcileCalled = true
|
||||
if req.ProviderID != "deepseek" || req.AccessAPIKey != "user-key" {
|
||||
t.Fatalf("unexpected reconcile request: %+v", req)
|
||||
}
|
||||
return provision.ReconcileResult{Status: "drifted", MissingCount: 1, ExtraCount: 2, ProbeFailureCount: 1, AccessStatus: provision.AccessStatusBroken}, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute() reconcile error = %v", err)
|
||||
}
|
||||
if !reconcileCalled {
|
||||
t.Fatal("execute() did not invoke reconcileProvider")
|
||||
}
|
||||
got := output.String()
|
||||
if !strings.Contains(got, "status=drifted") || !strings.Contains(got, "missing_count=1") || !strings.Contains(got, "access_status=broken") {
|
||||
t.Fatalf("execute() reconcile output = %q, want summary", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInstallPackCLIArgsRequiresHostBaseURL(t *testing.T) {
|
||||
_, err := parseInstallPackCLIArgs([]string{"--pack-path", "/tmp/openai-pack.zip"})
|
||||
if err == nil {
|
||||
t.Fatal("parseInstallPackCLIArgs() error = nil, want validation error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseImportCLIArgsRequiresHostBaseURL(t *testing.T) {
|
||||
_, err := parseImportCLIArgs([]string{"--pack-dir", "/tmp/pack", "--provider-id", "deepseek", "--keys", "k1", "--access-api-key", "user-key"})
|
||||
if err == nil {
|
||||
t.Fatal("parseImportCLIArgs() error = nil, want validation error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePreviewCLIArgsRequiresHostBaseURL(t *testing.T) {
|
||||
_, err := parsePreviewCLIArgs([]string{"--pack-dir", "/tmp/pack", "--provider-id", "deepseek", "--keys", "k1"})
|
||||
if err == nil {
|
||||
t.Fatal("parsePreviewCLIArgs() error = nil, want validation error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRollbackCLIArgsRequiresHostBaseURL(t *testing.T) {
|
||||
_, err := parseRollbackCLIArgs([]string{"--pack-dir", "/tmp/pack", "--provider-id", "deepseek"})
|
||||
if err == nil {
|
||||
t.Fatal("parseRollbackCLIArgs() error = nil, want validation error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseReconcileCLIArgsRequiresHostBaseURL(t *testing.T) {
|
||||
_, err := parseReconcileCLIArgs([]string{"--pack-dir", "/tmp/pack", "--provider-id", "deepseek"})
|
||||
if err == nil {
|
||||
t.Fatal("parseReconcileCLIArgs() error = nil, want validation error")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user