cli/config_defaults_test.go
Rama main · réplica de lectura del repositorio original.
package cli
import (
"bytes"
"context"
"os"
"path/filepath"
"strings"
"testing"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.keploy.io/server/v3/config"
"go.uber.org/zap"
yamlLib "gopkg.in/yaml.v3"
)
// `keploy config defaults` is where the settings a generated config leaves
// out have to remain readable. If it stops printing them, the short file just
// moves the question somewhere nobody can answer it.
func TestConfigDefaults_PrintsEverySetting(t *testing.T) {
cmd := configDefaults(zap.NewNop())
var out bytes.Buffer
cmd.SetOut(&out)
cmd.SetArgs(nil)
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
got := out.String()
// EVERY top-level block, derived from the defaults themselves rather
// than from a list six keys long: dropping the whole `mock:`, `contract:`
// or `async:` block passed that list without a murmur.
defaults, err := config.DefaultsYAML()
if err != nil {
t.Fatal(err)
}
var all map[string]interface{}
if err := yamlLib.Unmarshal([]byte(defaults), &all); err != nil {
t.Fatal(err)
}
if len(all) < 20 {
t.Fatalf("only %d top-level settings in the defaults; this test is no longer checking anything", len(all))
}
for key := range all {
if !strings.Contains(got, "\n"+key+":") && !strings.HasPrefix(got, key+":") {
t.Errorf("the defaults listing does not mention %q", key)
}
}
// ...and the documentation for the ones that need it.
for _, explained := range []string{"strictMockWindow", "upstreamTls", "mysqlPorts"} {
if !strings.Contains(got, explained) {
t.Errorf("the listing omits %q", explained)
}
}
if n := strings.Count(got, "#"); n < 30 {
t.Errorf("the listing carries only %d comment lines; the settings it lists are unexplained", n)
}
if !strings.HasPrefix(got, "# Generated by Keploy") {
t.Errorf("no version comment on the listing: %.40q", got)
}
// The header that tops a GENERATED file would be a lie here: this is the
// full document, not the part that differs from it.
if strings.Contains(got, "differ from Keploy's defaults") {
t.Error("the full listing claims to hold only what differs")
}
// `agent` is Keploy's to manage; printing it invites editing a block the
// generated file will never contain.
if strings.Contains(got, "\nagent:") {
t.Error("the listing offers the internally-managed agent block")
}
}
// It is a document a developer can start from: printing it and writing it
// must produce the same bytes, and what it produces must be what Keploy
// applies -- i.e. stripping it against the defaults leaves nothing.
func TestConfigDefaults_OutputFileMatchesStdout(t *testing.T) {
var out bytes.Buffer
printed := configDefaults(zap.NewNop())
printed.SetOut(&out)
if err := printed.Execute(); err != nil {
t.Fatal(err)
}
path := filepath.Join(t.TempDir(), "keploy.defaults.yml")
saved := configDefaults(zap.NewNop())
saved.SetOut(&bytes.Buffer{})
saved.SetArgs([]string{"--output", path})
if err := saved.Execute(); err != nil {
t.Fatal(err)
}
body, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
// The SETTINGS are the same; the saved copy carries one thing more. A
// document a repository may keep has to say what keeping it costs --
// freezing today's defaults there, which is the anti-pattern the whole
// minimal config exists to end, and the command's own help recommends
// `-o keploy.yml` as a starting point.
if !strings.Contains(string(body), "FREEZES") {
t.Errorf("the saved defaults do not say what keeping them costs:\n%s", body)
}
settings := func(doc string) string {
var kept []string
for _, line := range strings.Split(doc, "\n") {
if t := strings.TrimSpace(line); t != "" && !strings.HasPrefix(t, "#") {
kept = append(kept, line)
}
}
return strings.Join(kept, "\n")
}
if settings(string(body)) != settings(out.String()) {
t.Fatalf("-o wrote different SETTINGS from what it prints:\n--- file\n%s\n--- stdout\n%s", body, out.String())
}
// The real property, and the reason to print them at all: saving this
// document as keploy.yml must leave the effective config exactly as it
// would have been with no file. (Comparing StripDefaults(defaults,
// defaults) instead was a tautology -- the same function on both sides.)
dir := t.TempDir()
cfgPath := filepath.Join(dir, "keploy.yml")
if err := os.WriteFile(cfgPath, []byte(out.String()), 0644); err != nil {
t.Fatal(err)
}
v := viper.New()
v.SetConfigFile(cfgPath)
if err := v.ReadInConfig(); err != nil {
t.Fatalf("what this prints is not a config Keploy can read: %v", err)
}
fromFile := config.New()
if err := v.Unmarshal(fromFile); err != nil {
t.Fatal(err)
}
withFile, err := yamlLib.Marshal(fromFile)
if err != nil {
t.Fatal(err)
}
withoutFile, err := yamlLib.Marshal(config.New())
if err != nil {
t.Fatal(err)
}
if string(withFile) != string(withoutFile) {
t.Fatalf("saving the printed defaults changed the effective config:\n--- with the file\n%s\n--- with none\n%s", withFile, withoutFile)
}
}
// The obvious target is keploy.yml, and 134 lines of defaults written over a
// developer's own config would take their command with it -- silently, exit
// 0. `keploy config --generate` asks before it overwrites; this is pipeable,
// so it refuses and names the way past.
func TestConfigDefaults_RefusesToOverwriteWithoutForce(t *testing.T) {
path := filepath.Join(t.TempDir(), "keploy.yml")
mine := "command: npm test\ntest:\n delay: 42\n"
if err := os.WriteFile(path, []byte(mine), 0644); err != nil {
t.Fatal(err)
}
cmd := configDefaults(zap.NewNop())
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
cmd.SetArgs([]string{"-o", path})
err := cmd.Execute()
if err == nil {
t.Fatal("overwrote an existing config and reported success")
}
if !strings.Contains(err.Error(), "--force") {
t.Errorf("the refusal does not name the way past: %v", err)
}
body, readErr := os.ReadFile(path)
if readErr != nil {
t.Fatal(readErr)
}
if string(body) != mine {
t.Fatalf("the developer's config was modified:\n%s", body)
}
// ...and --force is the way past.
forced := configDefaults(zap.NewNop())
forced.SetOut(&bytes.Buffer{})
forced.SetArgs([]string{"-o", path, "--force"})
if err := forced.Execute(); err != nil {
t.Fatal(err)
}
body, readErr = os.ReadFile(path)
if readErr != nil {
t.Fatal(readErr)
}
if !strings.Contains(string(body), "proxyPort:") {
t.Fatalf("--force did not write the defaults:\n%s", body)
}
}
// A stray argument is a typo, not a request to print everything.
func TestConfigDefaults_RejectsStrayArguments(t *testing.T) {
cmd := configDefaults(zap.NewNop())
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
cmd.SetArgs([]string{"keploy.yml"})
if err := cmd.Execute(); err == nil {
t.Fatal("a stray argument was accepted")
}
}
// A failure has to be reported, not logged past: `keploy config defaults -o
// /nope/keploy.yml` that prints a success line and writes nothing is how a
// script silently ships no config at all.
func TestConfigDefaults_UnwritableOutputIsAnError(t *testing.T) {
cmd := configDefaults(zap.NewNop())
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
cmd.SetArgs([]string{"-o", filepath.Join(t.TempDir(), "no-such-dir", "keploy.yml")})
if err := cmd.Execute(); err == nil {
t.Fatal("writing into a directory that does not exist reported success")
}
}
// --- `keploy config --generate` -------------------------------------------
//
// Nothing constructed the `config` command itself, so its whole RunE -- the
// path the flag names, the refusal to clobber, the telemetry lookup -- had no
// coverage at all. Reverting either fix left the suite green.
type stubConfigurator struct{ path string }
func (s *stubConfigurator) AddFlags(cmd *cobra.Command) error {
// The same flags cli/provider registers for `config`. A stub that is
// missing one does not simplify the test, it tests a command that does
// not exist -- and the RunE now refuses outright when a flag it needs is
// not registered, rather than quietly reading it as false.
cmd.Flags().Bool("generate", false, "")
cmd.Flags().Bool("force", false, "")
cmd.Flags().StringP("path", "p", ".", "")
if s.path != "" {
return cmd.Flags().Set("path", s.path)
}
return nil
}
func (s *stubConfigurator) ValidateFlags(_ context.Context, _ *cobra.Command) error { return nil }
func (s *stubConfigurator) Validate(_ context.Context, _ *cobra.Command) error { return nil }
type stubFactory struct{ calls int }
func (f *stubFactory) GetService(_ context.Context, _ string) (interface{}, error) {
f.calls++
return nil, nil
}
func configCmd(t *testing.T, cfg *config.Config, path string) (*cobra.Command, *stubFactory) {
t.Helper()
f := &stubFactory{}
cmd := Config(context.Background(), zap.NewNop(), cfg, f, &stubConfigurator{path: path})
if cmd == nil {
t.Fatal("the config command could not be built")
}
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
return cmd, f
}
func TestConfigGenerate_WritesWhereThePathFlagSays(t *testing.T) {
// cfg.Path is permanently empty here: root.go allocates a second
// config.Config and hands THAT to every command constructor, while
// ValidateFlags writes into the one main.go made. Reading cfg.Path meant
// `--path ./svc` wrote keploy.yml into the current directory instead --
// a silent overwrite of a file the user did not name.
dir := t.TempDir()
svc := filepath.Join(dir, "svc")
if err := os.MkdirAll(svc, 0o755); err != nil {
t.Fatal(err)
}
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(dir); err != nil {
t.Fatal(err)
}
defer func() { _ = os.Chdir(cwd) }()
cmd, factory := configCmd(t, config.New(), svc)
cmd.SetArgs([]string{"--generate", "--path", svc})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(svc, "keploy.yml")); err != nil {
t.Fatalf("--path was ignored; nothing at %s: %v", svc, err)
}
if _, err := os.Stat(filepath.Join(dir, "keploy.yml")); err == nil {
t.Fatal("--path was ignored: the config landed in the current directory")
}
// Every other command pings telemetry through the service lookup, and
// `config --generate` is an onboarding step.
if factory.calls == 0 {
t.Error("config --generate reported nothing; it is the one command in the CLI that goes unseen")
}
}
func TestConfigGenerate_DoesNotWriteThroughADanglingSymlink(t *testing.T) {
// Stat follows the link, so a dangling keploy.yml read as "no file here":
// the command never asked, and wrote through the link to a path the user
// never named. The -o flag of `config defaults` was fixed for exactly
// this; the more dangerous caller was left alone.
//
// OUTSIDE the directory, which is what makes it dangerous. A dangling
// link pointing INSIDE is the committed-monorepo layout and is written
// through on purpose -- see the case below, and
// TestWriteMinimalConfig_DanglingLinkInsideIsWrittenThrough.
outer := t.TempDir()
dir := filepath.Join(outer, "project")
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
target := filepath.Join(outer, "somewhere-else.yml")
if err := os.Symlink(target, filepath.Join(dir, "keploy.yml")); err != nil {
t.Skipf("symlinks unavailable: %v", err)
}
// No --force and no terminal, deliberately: a symlink is refused for
// being a symlink, and must not be reported as "there is nobody here to
// confirm overwriting it" -- an answer that sends the user to a flag
// which would refuse for the same real reason anyway.
cmd, _ := configCmd(t, config.New(), dir)
cmd.SetArgs([]string{"--generate", "--path", dir})
// The ERROR as well as the absence: swallowing it would print "Config
// file generated successfully" over a file that was never written, and
// asserting only the absence cannot tell the two apart.
if err := cmd.Execute(); err == nil {
t.Fatal("config --generate reported success for a config it refused to write")
} else if !strings.Contains(err.Error(), "symbolic link") {
t.Errorf("the refusal does not say why: %v", err)
}
if _, err := os.Stat(target); err == nil {
t.Fatal("config --generate wrote through a dangling symlink to a file the user never named")
}
}
// ...and the layout that link style exists for: a keploy.yml committed as a
// link to the file the generator has not written yet. Refusing it took the
// generator away from those repositories, and the refusal said the link
// pointed outside the directory when it pointed squarely inside it.
func TestConfigGenerate_WritesThroughADanglingLinkInsideTheDirectory(t *testing.T) {
dir := t.TempDir()
if err := os.MkdirAll(filepath.Join(dir, "config"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.Symlink(filepath.Join("config", "keploy.yml"), filepath.Join(dir, "keploy.yml")); err != nil {
t.Skipf("symlinks unavailable: %v", err)
}
cmd, _ := configCmd(t, config.New(), dir)
cmd.SetArgs([]string{"--generate", "--path", dir})
if err := cmd.Execute(); err != nil {
t.Fatalf("a committed link to a not-yet-generated config was refused: %v", err)
}
if _, err := os.Stat(filepath.Join(dir, "config", "keploy.yml")); err != nil {
t.Fatalf("nothing was written through the link: %v", err)
}
}
// --force means "overwrite the file I named", not "write through a link to
// one I did not". The non-force branch refused a dangling link; --force wrote
// straight through it, outside the project, and reported success.
func TestConfigDefaults_ForceDoesNotWriteThroughASymlink(t *testing.T) {
dir := t.TempDir()
outside := filepath.Join(dir, "outside.yml")
link := filepath.Join(dir, "link.yml")
if err := os.Symlink(outside, link); err != nil {
t.Skipf("symlinks unavailable: %v", err)
}
cmd := configDefaults(zap.NewNop())
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
cmd.SetArgs([]string{"-o", link, "--force"})
if err := cmd.Execute(); err == nil {
t.Fatal("--force wrote through a symbolic link and reported success")
}
if _, err := os.Stat(outside); err == nil {
t.Fatal("--force created a file outside the path the user named")
}
}
// ...and it does not leave a file the way it found it, if that way was
// world-writable. os.WriteFile does not change an existing file's mode.
func TestConfigDefaults_ForceLeavesTheFileReadableOnly(t *testing.T) {
path := filepath.Join(t.TempDir(), "keploy.yml")
if err := os.WriteFile(path, []byte("command: npm test\n"), 0o777); err != nil {
t.Fatal(err)
}
// CHMOD, not the WriteFile mode: the umask strips exactly the bits this
// test is about, so the fixture was never world-writable.
if err := os.Chmod(path, 0o777); err != nil {
t.Fatal(err)
}
cmd := configDefaults(zap.NewNop())
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
cmd.SetArgs([]string{"-o", path, "--force"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
info, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if mode := info.Mode().Perm(); mode&0o022 != 0 {
t.Errorf("keploy.yml is still group/world-writable (%04o) after --force", mode)
}
}