feat(zli): add config list/show/get/set/reset and isolate deprecated syntax (#4037)

* feat(zli): add config list/show/get/set/reset and isolate deprecated syntax

Introduce first-class subcommands for listing profiles, showing a profile,
getting and setting keys, and resetting optional keys (alongside existing add/remove).
The parent command now resolves ~/.zot via zliUserConfigPath(),
documents that profile names must not clash with subcommand names,
and states that positional/--list/--reset usage is deprecated and will be removed soon.

Legacy behavior is delegated to config_cmd_deprecated.go with stderr warnings for old flags and positional get/set.
Examples and inline help point users at the new commands.
FormatNames/FormatListedVars comments reference config list/show.

Tests are split so config_cmd_test.go exercises the supported subcommands
while config_cmd_deprecated_test.go retains coverage for the deprecated
paths under renamed TestConfigCmdDeprecated* entries.

Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>

* test: stabilize retention check tests

See https://github.com/project-zot/zot/actions/runs/25361779632/job/74362802944?pr=4037

Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>

---------

Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
This commit is contained in:
Andrei Aaron
2026-05-08 20:19:26 +03:00
committed by GitHub
parent b89d10834c
commit c7ddbe2e36
7 changed files with 1108 additions and 267 deletions
+2 -2
View File
@@ -217,7 +217,7 @@ func (f *ZliConfigFile) RemoveEntry(configName string) error {
return zerr.ErrConfigNotFound
}
// FormatNames renders name and URL columns for `zli config --list`.
// FormatNames renders name and URL columns for `zli config list`.
func (f *ZliConfigFile) FormatNames() (string, error) {
var builder strings.Builder
@@ -317,7 +317,7 @@ func (c *ZliConfig) ResetVar(key string) error {
return nil
}
// FormatListedVars renders lines for `zli config <name> --list`.
// FormatListedVars renders lines for `zli config show <name>`.
func (c *ZliConfig) FormatListedVars() string {
var builder strings.Builder
+215 -65
View File
@@ -6,6 +6,9 @@ import (
"fmt"
"os"
"path/filepath"
"slices"
"sort"
"strings"
"github.com/spf13/cobra"
@@ -18,80 +21,89 @@ func NewConfigCommand() *cobra.Command {
var isReset bool
configCmd := &cobra.Command{
Use: "config <config-name> [variable] [value]",
Use: "config",
Example: examples,
Short: "Configure zot registry parameters for CLI",
Long: `Configure zot registry parameters for CLI`,
Args: cobra.ArbitraryArgs,
RunE: func(cmd *cobra.Command, args []string) error {
home, err := os.UserHomeDir()
configPath, err := zliUserConfigPath()
if err != nil {
return err
}
configPath := filepath.Join(home, ".zot")
switch len(args) {
case noArgs:
if isListing { // zli config -l
res, err := getConfigNames(configPath)
if err != nil {
return err
}
fmt.Fprint(cmd.OutOrStdout(), res)
return nil
}
return zerr.ErrInvalidArgs
case oneArg:
// zli config <name> -l
if isListing {
res, err := getAllConfig(configPath, args[0])
if err != nil {
return err
}
fmt.Fprint(cmd.OutOrStdout(), res)
return nil
}
return zerr.ErrInvalidArgs
case twoArgs:
if isReset { // zli config <name> <key> --reset
return resetConfigValue(configPath, args[0], args[1])
}
// zli config <name> <key>
res, err := getConfigValue(configPath, args[0], args[1])
if err != nil {
return err
}
fmt.Fprintln(cmd.OutOrStdout(), res)
case threeArgs:
// zli config <name> <key> <value>
if err := setConfigValue(configPath, args[0], args[1], args[2]); err != nil {
return err
}
default:
return zerr.ErrInvalidArgs
}
return nil
return runLegacyConfig(cmd, args, configPath, isListing, isReset)
},
}
configCmd.Flags().BoolVarP(&isListing, "list", "l", false, "List configurations")
configCmd.Flags().BoolVar(&isReset, "reset", false, "Reset a variable value")
configCmd.Flags().BoolVarP(&isListing, "list", "l", false,
"[deprecated: use \"config list\" or \"config show <name>\"] List configurations")
configCmd.Flags().BoolVar(&isReset, "reset", false,
"[deprecated: use \"config reset\"] Reset a variable value")
configCmd.SetUsageTemplate(configCmd.UsageTemplate() + supportedOptions)
configCmd.AddCommand(NewConfigAddCommand())
configCmd.AddCommand(NewConfigRemoveCommand())
configCmd.AddCommand(NewConfigListCommand())
configCmd.AddCommand(NewConfigShowCommand())
configCmd.AddCommand(NewConfigGetCommand())
configCmd.AddCommand(NewConfigSetCommand())
configCmd.AddCommand(NewConfigResetCommand())
// Build this from actual subcommands to avoid drift.
reserved := strings.Join(reservedProfileNames(configCmd), ", ")
configCmd.Long = fmt.Sprintf(`Configure zot registry parameters for CLI.
Use the list, show, get, set, and reset subcommands for inspecting and editing profiles.
Profile names must not collide with subcommand names (%s).
Older positional syntax on this command is deprecated and will soon be removed.`, reserved)
return configCmd
}
func zliUserConfigPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, ".zot"), nil
}
// validateProfileNameForCreation prevents creating profiles that shadow subcommand names.
// We intentionally allow interacting with pre-existing profiles that collide with subcommand names
// so users can migrate/rename/remove them without editing ~/.zot by hand.
func validateProfileNameForCreation(configCmd *cobra.Command, name string) error {
if slices.Contains(reservedProfileNames(configCmd), name) {
return fmt.Errorf("%w: %q", zerr.ErrReservedConfigName, name)
}
return nil
}
func reservedProfileNames(configCmd *cobra.Command) []string {
seen := make(map[string]struct{})
for _, sub := range configCmd.Commands() {
name := sub.Name()
if name == "" {
continue
}
seen[name] = struct{}{}
}
reserved := make([]string, 0, len(seen))
for name := range seen {
reserved = append(reserved, name)
}
sort.Strings(reserved)
return reserved
}
func NewConfigAddCommand() *cobra.Command {
configAddCmd := &cobra.Command{
Use: "add <config-name> <url>",
@@ -100,13 +112,20 @@ func NewConfigAddCommand() *cobra.Command {
Long: "Add configuration for a zot registry",
Args: cobra.ExactArgs(twoArgs),
RunE: func(cmd *cobra.Command, args []string) error {
home, err := os.UserHomeDir()
configPath, err := zliUserConfigPath()
if err != nil {
return err
}
configPath := filepath.Join(home, ".zot")
// zli config add <config-name> <url>
configRoot := cmd.Parent()
if configRoot == nil {
configRoot = cmd
}
if err := validateProfileNameForCreation(configRoot, args[0]); err != nil {
return err
}
err = addConfig(configPath, args[0], args[1])
if err != nil {
return err
@@ -130,13 +149,11 @@ func NewConfigRemoveCommand() *cobra.Command {
Long: "Remove configuration for a zot registry",
Args: cobra.ExactArgs(oneArg),
RunE: func(cmd *cobra.Command, args []string) error {
home, err := os.UserHomeDir()
configPath, err := zliUserConfigPath()
if err != nil {
return err
}
configPath := filepath.Join(home, ".zot")
// zli config remove <config-name>
err = removeConfig(configPath, args[0])
if err != nil {
return err
@@ -152,6 +169,137 @@ func NewConfigRemoveCommand() *cobra.Command {
return configRemoveCmd
}
func NewConfigListCommand() *cobra.Command {
listCmd := &cobra.Command{
Use: "list",
Example: " zli config list",
Short: "List all configuration profile names",
Long: "Print every configured CLI profile name (and URLs where applicable).",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
configPath, err := zliUserConfigPath()
if err != nil {
return err
}
res, err := getConfigNames(configPath)
if err != nil {
return err
}
fmt.Fprint(cmd.OutOrStdout(), res)
return nil
},
}
listCmd.SetUsageTemplate(listCmd.UsageTemplate())
return listCmd
}
func NewConfigShowCommand() *cobra.Command {
showCmd := &cobra.Command{
Use: "show <name>",
Example: " zli config show main",
Short: "Show all variables for one profile",
Long: "Print every variable set for the named CLI profile.",
Args: cobra.ExactArgs(oneArg),
RunE: func(cmd *cobra.Command, args []string) error {
configPath, err := zliUserConfigPath()
if err != nil {
return err
}
res, err := getAllConfig(configPath, args[0])
if err != nil {
return err
}
fmt.Fprint(cmd.OutOrStdout(), res)
return nil
},
}
showCmd.SetUsageTemplate(showCmd.UsageTemplate())
return showCmd
}
func NewConfigGetCommand() *cobra.Command {
getCmd := &cobra.Command{
Use: "get <name> <key>",
Example: " zli config get main url",
Short: "Print one configuration variable",
Long: "Print the value of a single key for the named profile.",
Args: cobra.ExactArgs(twoArgs),
RunE: func(cmd *cobra.Command, args []string) error {
configPath, err := zliUserConfigPath()
if err != nil {
return err
}
res, err := getConfigValue(configPath, args[0], args[1])
if err != nil {
return err
}
fmt.Fprintln(cmd.OutOrStdout(), res)
return nil
},
}
getCmd.SetUsageTemplate(getCmd.UsageTemplate())
return getCmd
}
func NewConfigSetCommand() *cobra.Command {
setCmd := &cobra.Command{
Use: "set <name> <key> <value>",
Example: " zli config set main showspinner false",
Short: "Set a configuration variable",
Long: "Set a single key for the named profile and persist ~/.zot.",
Args: cobra.ExactArgs(threeArgs),
RunE: func(cmd *cobra.Command, args []string) error {
configPath, err := zliUserConfigPath()
if err != nil {
return err
}
return setConfigValue(configPath, args[0], args[1], args[2])
},
}
setCmd.SetUsageTemplate(setCmd.UsageTemplate())
return setCmd
}
func NewConfigResetCommand() *cobra.Command {
resetCmd := &cobra.Command{
Use: "reset <name> <key>",
Example: " zli config reset main showspinner",
Short: "Reset a configuration variable to its default",
Long: "Remove a non-default key from the named profile (URL and profile name cannot be reset).",
Args: cobra.ExactArgs(twoArgs),
RunE: func(cmd *cobra.Command, args []string) error {
configPath, err := zliUserConfigPath()
if err != nil {
return err
}
return resetConfigValue(configPath, args[0], args[1])
},
}
resetCmd.SetUsageTemplate(resetCmd.UsageTemplate())
return resetCmd
}
func getConfigNames(configPath string) (string, error) {
cfg, err := ReadZliConfigFile(configPath)
if err != nil {
@@ -289,9 +437,11 @@ func getAllConfig(configPath, configName string) (string, error) {
const (
examples = ` zli config add main https://zot-foo.com:8080
zli config --list
zli config main url
zli config main --list
zli config list
zli config show main
zli config get main url
zli config set main showspinner false
zli config reset main showspinner
zli config remove main`
supportedOptions = `
+89
View File
@@ -0,0 +1,89 @@
//go:build search
package client
import (
"fmt"
"io"
"github.com/spf13/cobra"
zerr "zotregistry.dev/zot/v2/errors"
)
// runLegacyConfig handles deprecated positional syntax and --list/--reset on the parent command.
// Prefer subcommands (list, show, get, set, reset); this file emits deprecation warnings to stderr.
func runLegacyConfig(cmd *cobra.Command, args []string, configPath string, isListing, isReset bool) error {
switch len(args) {
case noArgs:
if isListing { // zli config -l
warnLegacyDeprecatedInvocation(cmd.ErrOrStderr(), "`zli config --list`", "`zli config list`")
res, err := getConfigNames(configPath)
if err != nil {
return err
}
fmt.Fprint(cmd.OutOrStdout(), res)
return nil
}
return zerr.ErrInvalidArgs
case oneArg:
// zli config <name> -l
if isListing {
warnLegacyDeprecatedInvocation(cmd.ErrOrStderr(), "`zli config <name> --list`", "`zli config show <name>`")
res, err := getAllConfig(configPath, args[0])
if err != nil {
return err
}
fmt.Fprint(cmd.OutOrStdout(), res)
return nil
}
return zerr.ErrInvalidArgs
case twoArgs:
if isReset { // zli config <name> <key> --reset
warnLegacyDeprecatedInvocation(
cmd.ErrOrStderr(),
"`zli config <name> <key> --reset`",
"`zli config reset <name> <key>`",
)
return resetConfigValue(configPath, args[0], args[1])
}
warnLegacyDeprecatedInvocation(cmd.ErrOrStderr(), "`zli config <name> <key>`", "`zli config get <name> <key>`")
res, err := getConfigValue(configPath, args[0], args[1])
if err != nil {
return err
}
fmt.Fprintln(cmd.OutOrStdout(), res)
case threeArgs:
warnLegacyDeprecatedInvocation(
cmd.ErrOrStderr(),
"`zli config <name> <key> <value>`",
"`zli config set <name> <key> <value>`",
)
if err := setConfigValue(configPath, args[0], args[1], args[2]); err != nil {
return err
}
default:
return zerr.ErrInvalidArgs
}
return nil
}
func warnLegacyDeprecatedInvocation(w io.Writer, invoked, replacement string) {
fmt.Fprintf(w, "Warning: deprecated invocation %s; use %s instead.\n", invoked, replacement)
}
@@ -0,0 +1,542 @@
//go:build search
// Deprecated parent `config` invocation (--list, positional args, --reset). Prefer tests in config_cmd_test.go.
package client_test
import (
"bytes"
"errors"
"os"
"regexp"
"strings"
"testing"
. "github.com/smartystreets/goconvey/convey"
zerr "zotregistry.dev/zot/v2/errors"
"zotregistry.dev/zot/v2/pkg/cli/client"
)
func TestConfigCmdDeprecatedBasics(t *testing.T) {
Convey("Test config help", t, func() {
args := []string{"--help"}
_ = makeConfigFile(t, "showspinner = false")
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldBeNil)
So(buff.String(), ShouldContainSubstring, "Usage")
Convey("with the shorthand", func() {
args[0] = "-h"
_ = makeConfigFile(t, "showspinner = false")
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(buff.String(), ShouldContainSubstring, "Usage")
So(err, ShouldBeNil)
})
})
Convey("Test config no args", t, func() {
args := []string{}
_ = makeConfigFile(t, "showspinner = false")
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(buff.String(), ShouldContainSubstring, "Usage")
So(err, ShouldNotBeNil)
})
}
func TestConfigCmdDeprecatedMain(t *testing.T) {
Convey("Test add config", t, func() {
args := []string{"add", "configtest1", "https://test-url.com"}
configPath := makeConfigFile(t, "")
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
_ = cmd.Execute()
actual, err := os.ReadFile(configPath)
if err != nil {
panic(err)
}
actualStr := string(actual)
So(actualStr, ShouldContainSubstring, "configtest1")
So(actualStr, ShouldContainSubstring, "https://test-url.com")
})
Convey("Test error on home directory", t, func() {
args := []string{"add", "configtest1", "https://test-url.com"}
_ = makeConfigFile(t, "")
t.Setenv("HOME", "nonExistentDirectory")
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("Test error on home directory at new add config", t, func() {
args := []string{"configtest1", "https://test-url.com"}
_ = makeConfigFile(t, "")
t.Setenv("HOME", "nonExistentDirectory")
cmd := client.NewConfigAddCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("Test list config with invalid format", t, func() {
args := []string{"--list"}
_ = makeConfigFile(t, `{"configs":{"_name":"configtest","url":"https://test-url.com","showspinner":false}}`)
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(errors.Is(err, zerr.ErrCliBadConfig), ShouldBeTrue)
})
Convey("Test add config with invalid URL", t, func() {
args := []string{"add", "configtest1", "test..com"}
_ = makeConfigFile(t, "")
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
So(strings.Contains(err.Error(), zerr.ErrInvalidURL.Error()), ShouldBeTrue)
})
Convey("Test remove config entry successfully", t, func() {
args := []string{"remove", "configtest"}
configPath := makeConfigFile(t,
`{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldBeNil)
actual, err := os.ReadFile(configPath)
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
actualString := space.ReplaceAllString(string(actual), " ")
So(actualString, ShouldEqual, `{ "configs": [] }`)
})
Convey("Test remove missing config entry", t, func() {
args := []string{"remove", "configtest"}
_ = makeConfigFile(t, `{"configs":[]}`)
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
So(buff.String(), ShouldContainSubstring, "does not exist")
})
Convey("Test remove bad config file content", t, func() {
args := []string{"remove", "configtest"}
_ = makeConfigFile(t, `{"asdf":[]`)
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
So(errors.Is(err, zerr.ErrCliBadConfig), ShouldBeTrue)
})
Convey("Test remove bad config file entry", t, func() {
args := []string{"remove", "configtest"}
_ = makeConfigFile(t, `{"configs":[asdad]`)
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
So(buff.String(), ShouldContainSubstring, zerr.ErrCliBadConfig.Error())
})
Convey("Test remove config bad permissions", t, func() {
args := []string{"remove", "configtest"}
configPath := makeConfigFile(t,
`{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
defer func() {
_ = os.Chmod(configPath, 0o600)
}()
err := os.Chmod(configPath, 0o400) // Read-only, so we fail only on updating the file, not reading
So(err, ShouldBeNil)
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
So(buff.String(), ShouldContainSubstring, "permission denied")
})
Convey("Test fetch all config", t, func() {
args := []string{"--list"}
_ = makeConfigFile(t, `{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
cmd := client.NewConfigCommand()
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs(args)
err := cmd.Execute()
So(outBuff.String(), ShouldContainSubstring, "https://test-url.com")
So(errBuff.String(), ShouldContainSubstring, "`zli config list`")
So(err, ShouldBeNil)
Convey("with the shorthand", func() {
args := []string{"-l"}
_ = makeConfigFile(t, `{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
cmd := client.NewConfigCommand()
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldBeNil)
So(outBuff.String(), ShouldContainSubstring, "https://test-url.com")
So(errBuff.String(), ShouldContainSubstring, "`zli config list`")
})
Convey("From empty file", func() {
args := []string{"-l"}
_ = makeConfigFile(t, ``)
cmd := client.NewConfigCommand()
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldBeNil)
So(strings.TrimSpace(outBuff.String()), ShouldEqual, "")
So(errBuff.String(), ShouldContainSubstring, "`zli config list`")
})
})
Convey("Test fetch a config", t, func() {
args := []string{"configtest", "--list"}
_ = makeConfigFile(t, `{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
cmd := client.NewConfigCommand()
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldBeNil)
So(outBuff.String(), ShouldContainSubstring, "url = https://test-url.com")
So(outBuff.String(), ShouldContainSubstring, "showspinner = false")
So(errBuff.String(), ShouldContainSubstring, "`zli config show <name>`")
Convey("with the shorthand", func() {
args := []string{"configtest", "-l"}
_ = makeConfigFile(t, `{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
cmd := client.NewConfigCommand()
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldBeNil)
So(outBuff.String(), ShouldContainSubstring, "url = https://test-url.com")
So(outBuff.String(), ShouldContainSubstring, "showspinner = false")
So(errBuff.String(), ShouldContainSubstring, "`zli config show <name>`")
})
Convey("From empty file", func() {
args := []string{"configtest", "-l"}
_ = makeConfigFile(t, ``)
cmd := client.NewConfigCommand()
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldBeNil)
So(strings.TrimSpace(outBuff.String()), ShouldEqual, "")
So(errBuff.String(), ShouldContainSubstring, "`zli config show <name>`")
})
})
Convey("Test fetch a config val", t, func() {
args := []string{"configtest", "url"}
_ = makeConfigFile(t, `{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
cmd := client.NewConfigCommand()
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldBeNil)
So(outBuff.String(), ShouldEqual, "https://test-url.com\n")
So(errBuff.String(), ShouldContainSubstring, "deprecated invocation")
Convey("From empty file", func() {
args := []string{"configtest", "url"}
_ = makeConfigFile(t, ``)
cmd := client.NewConfigCommand()
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
combined := errBuff.String() + outBuff.String()
So(combined, ShouldContainSubstring, "does not exist")
})
})
Convey("Test add a config val", t, func() {
args := []string{"configtest", "showspinner", "false"}
configPath := makeConfigFile(t, `{"configs":[{"_name":"configtest","url":"https://test-url.com"}]}`)
cmd := client.NewConfigCommand()
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldBeNil)
actual, err := os.ReadFile(configPath)
if err != nil {
panic(err)
}
actualStr := string(actual)
So(actualStr, ShouldContainSubstring, "https://test-url.com")
So(actualStr, ShouldContainSubstring, `"showspinner": false`)
So(outBuff.String(), ShouldEqual, "")
So(errBuff.String(), ShouldContainSubstring, "deprecated invocation")
Convey("To an empty file", func() {
args := []string{"configtest", "showspinner", "false"}
_ = makeConfigFile(t, ``)
cmd := client.NewConfigCommand()
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
combined := errBuff.String() + outBuff.String()
So(combined, ShouldContainSubstring, "does not exist")
})
})
Convey("Test overwrite a config", t, func() {
args := []string{"configtest", "url", "https://new-url.com"}
configPath := makeConfigFile(t,
`{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
cmd := client.NewConfigCommand()
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldBeNil)
actual, err := os.ReadFile(configPath)
if err != nil {
panic(err)
}
actualStr := string(actual)
So(actualStr, ShouldContainSubstring, `https://new-url.com`)
So(actualStr, ShouldContainSubstring, `"showspinner": false`)
So(actualStr, ShouldNotContainSubstring, `https://test-url.com`)
So(outBuff.String(), ShouldEqual, "")
So(errBuff.String(), ShouldContainSubstring, "deprecated invocation")
})
Convey("Test reset a config val", t, func() {
args := []string{"configtest", "showspinner", "--reset"}
configPath := makeConfigFile(t,
`{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
cmd := client.NewConfigCommand()
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldBeNil)
actual, err := os.ReadFile(configPath)
if err != nil {
panic(err)
}
actualStr := string(actual)
So(actualStr, ShouldNotContainSubstring, "showspinner")
So(actualStr, ShouldContainSubstring, `"url": "https://test-url.com"`)
So(outBuff.String(), ShouldEqual, "")
So(errBuff.String(), ShouldContainSubstring, "`zli config reset <name> <key>`")
})
Convey("Test reset a url", t, func() {
args := []string{"configtest", "url", "--reset"}
_ = makeConfigFile(t, `{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
cmd := client.NewConfigCommand()
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
combined := errBuff.String() + outBuff.String()
So(combined, ShouldContainSubstring, "cannot reset")
So(combined, ShouldContainSubstring, "`zli config reset <name> <key>`")
})
Convey("Test add a config with an existing saved name", t, func() {
args := []string{"add", "configtest", "https://test-url.com/new"}
_ = makeConfigFile(t, `{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
So(buff.String(), ShouldContainSubstring, "cli config name already added")
})
Convey("Test deprecated config invalid args (too many args)", t, func() {
args := []string{"configtest", "url", "x", "y"}
_ = makeConfigFile(t, `{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
cmd := client.NewConfigCommand()
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
So(errors.Is(err, zerr.ErrInvalidArgs), ShouldBeTrue)
})
}
+205 -200
View File
@@ -88,6 +88,63 @@ func TestConfigCmdMain(t *testing.T) {
So(actualStr, ShouldContainSubstring, "https://test-url.com")
})
Convey("Test add config rejects reserved names", t, func() {
args := []string{"add", "list", "https://test-url.com"}
_ = makeConfigFile(t, "")
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
So(errors.Is(err, zerr.ErrReservedConfigName), ShouldBeTrue)
So(err.Error(), ShouldContainSubstring, `"list"`)
})
Convey("Test reserved-named profiles are still accessible for migration", t, func() {
_ = makeConfigFile(t,
`{"configs":[{"_name":"list","url":"https://test-url.com","showspinner":false}]}`)
Convey("show", func() {
cmd := client.NewConfigCommand()
out := bytes.NewBufferString("")
errOut := bytes.NewBufferString("")
cmd.SetOut(out)
cmd.SetErr(errOut)
cmd.SetArgs([]string{"show", "list"})
err := cmd.Execute()
So(err, ShouldBeNil)
So(out.String(), ShouldContainSubstring, "https://test-url.com")
})
Convey("get", func() {
cmd := client.NewConfigCommand()
out := bytes.NewBufferString("")
errOut := bytes.NewBufferString("")
cmd.SetOut(out)
cmd.SetErr(errOut)
cmd.SetArgs([]string{"get", "list", "url"})
err := cmd.Execute()
So(err, ShouldBeNil)
So(out.String(), ShouldContainSubstring, "https://test-url.com")
})
Convey("remove", func() {
cmd := client.NewConfigCommand()
out := bytes.NewBufferString("")
errOut := bytes.NewBufferString("")
cmd.SetOut(out)
cmd.SetErr(errOut)
cmd.SetArgs([]string{"remove", "list"})
err := cmd.Execute()
So(err, ShouldBeNil)
})
})
Convey("Test error on home directory", t, func() {
args := []string{"add", "configtest1", "https://test-url.com"}
@@ -105,7 +162,7 @@ func TestConfigCmdMain(t *testing.T) {
})
Convey("Test error on home directory at new add config", t, func() {
args := []string{"add", "configtest1", "https://test-url.com"}
args := []string{"configtest1", "https://test-url.com"}
_ = makeConfigFile(t, "")
@@ -120,8 +177,8 @@ func TestConfigCmdMain(t *testing.T) {
So(err, ShouldNotBeNil)
})
Convey("Test add config with invalid format", t, func() {
args := []string{"--list"}
Convey("Test list config with invalid format", t, func() {
args := []string{"list"}
_ = makeConfigFile(t, `{"configs":{"_name":"configtest","url":"https://test-url.com","showspinner":false}}`)
@@ -236,246 +293,194 @@ func TestConfigCmdMain(t *testing.T) {
So(buff.String(), ShouldContainSubstring, "permission denied")
})
Convey("Test fetch all config", t, func() {
args := []string{"--list"}
_ = makeConfigFile(t, `{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(buff.String(), ShouldContainSubstring, "https://test-url.com")
So(err, ShouldBeNil)
Convey("with the shorthand", func() {
args := []string{"-l"}
Convey("Test config list", t, func() {
Convey("prints profile names and URLs", func() {
_ = makeConfigFile(t, `{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldBeNil)
So(buff.String(), ShouldContainSubstring, "https://test-url.com")
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs([]string{"list"})
So(cmd.Execute(), ShouldBeNil)
So(outBuff.String(), ShouldContainSubstring, "https://test-url.com")
So(errBuff.String(), ShouldEqual, "")
})
Convey("From empty file", func() {
args := []string{"-l"}
Convey("from empty config file", func() {
_ = makeConfigFile(t, ``)
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldBeNil)
So(strings.TrimSpace(buff.String()), ShouldEqual, "")
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs([]string{"list"})
So(cmd.Execute(), ShouldBeNil)
So(strings.TrimSpace(outBuff.String()), ShouldEqual, "")
So(errBuff.String(), ShouldEqual, "")
})
})
Convey("Test fetch a config", t, func() {
args := []string{"configtest", "--list"}
_ = makeConfigFile(t, `{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldBeNil)
So(buff.String(), ShouldContainSubstring, "url = https://test-url.com")
So(buff.String(), ShouldContainSubstring, "showspinner = false")
Convey("with the shorthand", func() {
args := []string{"configtest", "-l"}
Convey("Test config show", t, func() {
Convey("prints variables for the profile", func() {
_ = makeConfigFile(t, `{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs([]string{"show", "configtest"})
So(cmd.Execute(), ShouldBeNil)
So(outBuff.String(), ShouldContainSubstring, "url = https://test-url.com")
So(outBuff.String(), ShouldContainSubstring, "showspinner = false")
So(errBuff.String(), ShouldEqual, "")
})
err := cmd.Execute()
Convey("from empty config file", func() {
_ = makeConfigFile(t, ``)
cmd := client.NewConfigCommand()
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs([]string{"show", "configtest"})
So(cmd.Execute(), ShouldBeNil)
So(strings.TrimSpace(outBuff.String()), ShouldEqual, "")
So(errBuff.String(), ShouldEqual, "")
})
})
Convey("Test config get", t, func() {
Convey("prints one key", func() {
_ = makeConfigFile(t, `{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
cmd := client.NewConfigCommand()
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs([]string{"get", "configtest", "url"})
So(cmd.Execute(), ShouldBeNil)
So(outBuff.String(), ShouldEqual, "https://test-url.com\n")
So(errBuff.String(), ShouldEqual, "")
})
Convey("from empty config file", func() {
_ = makeConfigFile(t, ``)
cmd := client.NewConfigCommand()
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs([]string{"get", "configtest", "url"})
So(cmd.Execute(), ShouldNotBeNil)
combined := errBuff.String() + outBuff.String()
So(combined, ShouldContainSubstring, "does not exist")
})
})
Convey("Test config set", t, func() {
Convey("adds a variable", func() {
configPath := makeConfigFile(t, `{"configs":[{"_name":"configtest","url":"https://test-url.com"}]}`)
cmd := client.NewConfigCommand()
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs([]string{"set", "configtest", "showspinner", "false"})
So(cmd.Execute(), ShouldBeNil)
So(outBuff.String(), ShouldEqual, "")
So(errBuff.String(), ShouldEqual, "")
actual, err := os.ReadFile(configPath)
So(err, ShouldBeNil)
So(buff.String(), ShouldContainSubstring, "url = https://test-url.com")
So(buff.String(), ShouldContainSubstring, "showspinner = false")
actualStr := string(actual)
So(actualStr, ShouldContainSubstring, "https://test-url.com")
So(actualStr, ShouldContainSubstring, `"showspinner": false`)
})
Convey("From empty file", func() {
args := []string{"configtest", "-l"}
Convey("to an empty config file", func() {
_ = makeConfigFile(t, ``)
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs([]string{"set", "configtest", "showspinner", "false"})
So(cmd.Execute(), ShouldNotBeNil)
err := cmd.Execute()
So(err, ShouldBeNil)
So(strings.TrimSpace(buff.String()), ShouldEqual, "")
combined := errBuff.String() + outBuff.String()
So(combined, ShouldContainSubstring, "does not exist")
})
})
Convey("Test fetch a config val", t, func() {
args := []string{"configtest", "url"}
_ = makeConfigFile(t, `{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldBeNil)
So(buff.String(), ShouldEqual, "https://test-url.com\n")
Convey("From empty file", func() {
args := []string{"configtest", "url"}
_ = makeConfigFile(t, ``)
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
So(buff.String(), ShouldContainSubstring, "does not exist")
})
})
Convey("Test add a config val", t, func() {
args := []string{"configtest", "showspinner", "false"}
configPath := makeConfigFile(t, `{"configs":[{"_name":"configtest","url":"https://test-url.com"}]}`)
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldBeNil)
actual, err := os.ReadFile(configPath)
if err != nil {
panic(err)
}
actualStr := string(actual)
So(actualStr, ShouldContainSubstring, "https://test-url.com")
So(actualStr, ShouldContainSubstring, `"showspinner": false`)
So(buff.String(), ShouldEqual, "")
Convey("To an empty file", func() {
args := []string{"configtest", "showspinner", "false"}
_ = makeConfigFile(t, ``)
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
So(buff.String(), ShouldContainSubstring, "does not exist")
})
})
Convey("Test overwrite a config", t, func() {
args := []string{"configtest", "url", "https://new-url.com"}
Convey("Test config set overwrites URL", t, func() {
configPath := makeConfigFile(t,
`{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldBeNil)
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs([]string{"set", "configtest", "url", "https://new-url.com"})
So(cmd.Execute(), ShouldBeNil)
So(outBuff.String(), ShouldEqual, "")
So(errBuff.String(), ShouldEqual, "")
actual, err := os.ReadFile(configPath)
if err != nil {
panic(err)
}
So(err, ShouldBeNil)
actualStr := string(actual)
So(actualStr, ShouldContainSubstring, `https://new-url.com`)
So(actualStr, ShouldContainSubstring, `"showspinner": false`)
So(actualStr, ShouldNotContainSubstring, `https://test-url.com`)
So(buff.String(), ShouldEqual, "")
})
Convey("Test reset a config val", t, func() {
args := []string{"configtest", "showspinner", "--reset"}
Convey("Test config reset", t, func() {
Convey("clears an optional variable", func() {
configPath := makeConfigFile(t,
`{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
configPath := makeConfigFile(t,
`{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
cmd := client.NewConfigCommand()
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs([]string{"reset", "configtest", "showspinner"})
So(cmd.Execute(), ShouldBeNil)
So(outBuff.String(), ShouldEqual, "")
So(errBuff.String(), ShouldEqual, "")
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldBeNil)
actual, err := os.ReadFile(configPath)
So(err, ShouldBeNil)
actualStr := string(actual)
So(actualStr, ShouldNotContainSubstring, "showspinner")
So(actualStr, ShouldContainSubstring, `"url": "https://test-url.com"`)
})
actual, err := os.ReadFile(configPath)
if err != nil {
panic(err)
}
actualStr := string(actual)
So(actualStr, ShouldNotContainSubstring, "showspinner")
So(actualStr, ShouldContainSubstring, `"url": "https://test-url.com"`)
So(buff.String(), ShouldEqual, "")
})
Convey("rejects resetting url", func() {
_ = makeConfigFile(t, `{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
Convey("Test reset a url", t, func() {
args := []string{"configtest", "url", "--reset"}
cmd := client.NewConfigCommand()
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs([]string{"reset", "configtest", "url"})
_ = makeConfigFile(t, `{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
So(cmd.Execute(), ShouldNotBeNil)
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
So(buff.String(), ShouldContainSubstring, "cannot reset")
combined := errBuff.String() + outBuff.String()
So(combined, ShouldContainSubstring, "cannot reset")
})
})
Convey("Test add a config with an existing saved name", t, func() {