Ctrl K
ComunidadGuías…

cli/mock.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package cli

import (
	"context"
	"errors"
	"fmt"

	"github.com/spf13/cobra"
	"go.keploy.io/server/v3/config"
	mockSvc "go.keploy.io/server/v3/pkg/service/mock"
	"go.keploy.io/server/v3/utils"
	"go.uber.org/zap"
)

func init() {
	Register("mock", Mock)
}

// Mock is the `keploy mock` parent command: use Keploy as a framework-agnostic
// mocking layer for your OWN test runner (pytest, go test, jest/playwright,
// mobile UI tests). Record captures the outgoing dependency calls your tests
// make; replay serves them back so the suite runs hermetically.
//
// Enterprise re-registers "mock" to add cloud-registry subcommands
// (download/upload/patch); it composes these OSS record/replay builders under
// its own parent, so keep MockRecord/MockReplay exported and free of AddFlags
// calls (the parent's loop adds flags — double registration panics).
func Mock(ctx context.Context, logger *zap.Logger, _ *config.Config, serviceFactory ServiceFactory, cmdConfigurator CmdConfigurator) *cobra.Command {
	cmd := &cobra.Command{
		Use:   "mock",
		Short: "Use Keploy as a mocking framework for your own tests (record/replay dependency calls)",
		Long: `Record the real outgoing calls your test suite makes, then replay them so the
suite runs without the real dependencies — a language-agnostic VCR/WireMock at
the network layer.

Examples:
  # Record the dependency calls pytest makes into the "default" mock set
  keploy mock record -c "pytest"

  # Replay them (dependencies can be offline)
  keploy mock replay -c "pytest"

  # A named set, refreshed on a merge to main, appending any new calls
  keploy mock record -c "go test ./..." --name orders
  keploy mock replay -c "go test ./..." --name orders --on-miss record`,
	}

	cmd.AddCommand(MockRecord(ctx, logger, serviceFactory, cmdConfigurator))
	cmd.AddCommand(MockReplay(ctx, logger, serviceFactory, cmdConfigurator))
	for _, subCmd := range cmd.Commands() {
		if err := cmdConfigurator.AddFlags(subCmd); err != nil {
			utils.LogError(logger, err, "failed to add flags to command", zap.String("command", subCmd.Name()))
		}
	}
	return cmd
}

// MockRecord builds `keploy mock record`. Exported so enterprise can compose it.
// It does NOT call AddFlags — the parent's loop does.
func MockRecord(ctx context.Context, logger *zap.Logger, serviceFactory ServiceFactory, cmdConfigurator CmdConfigurator) *cobra.Command {
	return &cobra.Command{
		Use:     "record",
		Short:   "Record the outgoing dependency calls your test command makes",
		Example: `keploy mock record -c "pytest" --name default`,
		PreRunE: func(cmd *cobra.Command, _ []string) error {
			return cmdConfigurator.Validate(ctx, cmd)
		},
		SilenceUsage: true,
		RunE: func(cmd *cobra.Command, _ []string) error {
			svc, err := serviceFactory.GetService(ctx, "mock-record")
			if err != nil {
				utils.LogError(logger, err, "failed to get service")
				// Return the error, don't swallow it: main's exitCodeForCmdErr
				// turns a non-nil error into exit 1. Returning nil here reported
				// SUCCESS to the shell and to CI after a hard setup failure — so
				// `keploy mock record ... --name nope` printed four ERROR lines and
				// still exited 0. The wrapped runner's own exit code is a
				// separate channel (utils.ErrCode via propagateExit) and is
				// untouched by this.
				return err
			}
			m, ok := svc.(mockSvc.Service)
			if !ok {
				err := fmt.Errorf("service doesn't satisfy mock service interface")
				utils.LogError(logger, nil, err.Error())
				return err
			}
			// Ensure background goroutines (agent monitor, proxy streams) are
			// torn down when Record returns, unless the user already interrupted.
			defer func() {
				select {
				case <-ctx.Done():
				default:
					utils.ExecCancel()
				}
			}()
			if err := m.Record(ctx); err != nil {
				// A user interrupt is not a failure.
				if ctx.Err() == context.Canceled {
					return nil
				}
				if errors.Is(err, mockSvc.ErrRecordRefused) {
					return err
				}
				utils.LogError(logger, err, "failed to record mocks")
				// Record() returns nil after a merely-failing test command (it calls
				// propagateExit and lets utils.ErrCode mirror the runner), so a
				// non-nil error here is always a real Keploy failure and must not
				// exit 0.
				return err
			}
			return nil
		},
	}
}

// MockReplay builds `keploy mock replay`. Exported so enterprise can compose it.
// It does NOT call AddFlags — the parent's loop does.
func MockReplay(ctx context.Context, logger *zap.Logger, serviceFactory ServiceFactory, cmdConfigurator CmdConfigurator) *cobra.Command {
	return &cobra.Command{
		Use:     "replay",
		Short:   "Replay recorded mocks while your test command runs",
		Example: `keploy mock replay -c "pytest" --name default --on-miss fail`,
		PreRunE: func(cmd *cobra.Command, _ []string) error {
			return cmdConfigurator.Validate(ctx, cmd)
		},
		SilenceUsage: true,
		RunE: func(cmd *cobra.Command, _ []string) error {
			svc, err := serviceFactory.GetService(ctx, "mock-replay")
			if err != nil {
				utils.LogError(logger, err, "failed to get service")
				// Return the error, don't swallow it: main's exitCodeForCmdErr
				// turns a non-nil error into exit 1. Returning nil here reported
				// SUCCESS to the shell and to CI after a hard setup failure — so
				// `keploy mock replay ... --name nope` printed four ERROR lines and
				// still exited 0. The wrapped runner's own exit code is a
				// separate channel (utils.ErrCode via propagateExit) and is
				// untouched by this.
				return err
			}
			m, ok := svc.(mockSvc.Service)
			if !ok {
				err := fmt.Errorf("service doesn't satisfy mock service interface")
				utils.LogError(logger, nil, err.Error())
				return err
			}
			defer func() {
				select {
				case <-ctx.Done():
				default:
					utils.ExecCancel()
				}
			}()
			if err := m.Replay(ctx); err != nil {
				// A user interrupt is not keploy's failure, and no pass
				// either: it exits 128+N for the signal
				// (utils.InterruptExitCode) unless a code is already armed,
				// never the test command's 0. keploy's own Replay arms it and
				// returns nil; this arms it for a service that wraps Replay
				// (enterprise composes it) and fails on the cancel instead. A
				// wrapper that returns nil must arm it itself.
				if ctx.Err() == context.Canceled {
					if code := utils.InterruptExitCode(); code != 0 {
						utils.SetExitCodeOnce(code)
					}
					return nil
				}
				utils.LogError(logger, err, "failed to replay mocks")
				// Replay() returns nil after a merely-failing test command (it calls
				// propagateExit and lets utils.ErrCode mirror the runner), so a
				// non-nil error here is always a real Keploy failure and must not
				// exit 0.
				return err
			}
			return nil
		},
	}
}