mirror of
https://github.com/project-zot/zot.git
synced 2026-06-18 05:28:07 +08:00
fa92366009
feat(cli): add typed ~/.zot config layer and strict validation
Introduce pkg/cli/client/config.go with ZliConfigFile/ZliConfig and
ReadZliConfigFile, replacing the loose map[string]any load/save path in
config_cmd.go.
Parsing now rejects malformed JSON with ErrCliBadConfig and requires a
non-null configs array (ErrCliMissingConfigsField when wrapped). Each
profile must have non-empty _name and url.
Config commands delegate to typed helpers (Find, AddEntry, RemoveEntry,
GetVar/SetVar/ResetVar, FormatNames, WriteFile). Fresh or minimal files
still behave as empty via isFreshCliRead (ErrEmptyJSON or missing configs).
Tests: prefer t.Setenv("HOME", t.TempDir()) where CLI resolution uses --url
only; align CVE/client/search tests with mandatory profile URL and HOME
isolation.
Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
364 lines
9.0 KiB
Go
364 lines
9.0 KiB
Go
//go:build search
|
|
|
|
package client
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
zerr "zotregistry.dev/zot/v2/errors"
|
|
)
|
|
|
|
func writeTestZotFile(t *testing.T, dir, content string) string {
|
|
t.Helper()
|
|
|
|
path := filepath.Join(dir, ".zot")
|
|
require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
|
|
|
|
return path
|
|
}
|
|
|
|
// tempConfigPath returns ~/.zot path under an isolated temp dir; when writeFile is true, writes cfgContents first.
|
|
func tempConfigPath(t *testing.T, writeFile bool, cfgContents string) string {
|
|
t.Helper()
|
|
|
|
dir := t.TempDir()
|
|
if writeFile {
|
|
return writeTestZotFile(t, dir, cfgContents)
|
|
}
|
|
|
|
return filepath.Join(dir, ".zot")
|
|
}
|
|
|
|
func TestGetConfigValue(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
validProfile := `{"configs":[{"_name":"a","url":"https://example.com"}]}`
|
|
|
|
tests := []struct {
|
|
name string
|
|
cfgContents string // ignored when writeFile is false (missing ~/.zot until read)
|
|
writeFile bool
|
|
configName string
|
|
wantErrIs error
|
|
wantCliBadWrap bool // errors.Is(_, ErrCliBadConfig); implies !isConfigUnavailable
|
|
}{
|
|
{
|
|
name: "fresh_missing_file_ErrConfigNotFound",
|
|
writeFile: false,
|
|
configName: "any",
|
|
wantErrIs: zerr.ErrConfigNotFound,
|
|
},
|
|
{
|
|
name: "fresh_empty_configs_ErrConfigNotFound",
|
|
cfgContents: `{}`,
|
|
writeFile: true,
|
|
configName: "any",
|
|
wantErrIs: zerr.ErrConfigNotFound,
|
|
},
|
|
{
|
|
name: "read_invalid_JSON_ErrCliBadConfig",
|
|
cfgContents: `not-json`,
|
|
writeFile: true,
|
|
configName: "any",
|
|
wantCliBadWrap: true,
|
|
},
|
|
{
|
|
name: "profile_not_found_ErrConfigNotFound",
|
|
cfgContents: validProfile,
|
|
writeFile: true,
|
|
configName: "missing",
|
|
wantErrIs: zerr.ErrConfigNotFound,
|
|
},
|
|
}
|
|
|
|
for _, tableCase := range tests {
|
|
t.Run(tableCase.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfgPath := tempConfigPath(t, tableCase.writeFile, tableCase.cfgContents)
|
|
|
|
got, err := getConfigValue(cfgPath, tableCase.configName, URLFlag)
|
|
|
|
switch {
|
|
case tableCase.wantCliBadWrap:
|
|
require.Error(t, err)
|
|
require.False(t, isConfigUnavailable(err))
|
|
require.True(t, errors.Is(err, zerr.ErrCliBadConfig))
|
|
require.Equal(t, "", got)
|
|
case tableCase.wantErrIs != nil:
|
|
require.ErrorIs(t, err, tableCase.wantErrIs)
|
|
require.Equal(t, "", got)
|
|
default:
|
|
require.Failf(t, "table row must set wantErrIs or wantCliBadWrap: %s", tableCase.name)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
cfgPath := writeTestZotFile(t, dir, validProfile)
|
|
|
|
got, err := getConfigValue(cfgPath, "a", URLFlag)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "https://example.com", got)
|
|
})
|
|
}
|
|
|
|
func TestResetConfigValue(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
validProfile := `{"configs":[{"_name":"a","url":"https://example.com"}]}`
|
|
|
|
tests := []struct {
|
|
name string
|
|
cfgContents string
|
|
writeFile bool
|
|
configName string
|
|
key string
|
|
wantErrIs error
|
|
wantCliBadWrap bool
|
|
}{
|
|
{
|
|
name: "fresh_ErrConfigNotFound",
|
|
writeFile: false,
|
|
configName: "any",
|
|
key: showspinnerConfig,
|
|
wantErrIs: zerr.ErrConfigNotFound,
|
|
},
|
|
{
|
|
name: "cannot_reset_URL_before_read",
|
|
writeFile: false,
|
|
configName: "any",
|
|
key: URLFlag,
|
|
wantErrIs: zerr.ErrCannotResetConfigKey,
|
|
},
|
|
{
|
|
name: "read_invalid_JSON_ErrCliBadConfig",
|
|
cfgContents: `not-json`,
|
|
writeFile: true,
|
|
configName: "a",
|
|
key: showspinnerConfig,
|
|
wantCliBadWrap: true,
|
|
},
|
|
{
|
|
name: "profile_not_found_ErrConfigNotFound",
|
|
cfgContents: validProfile,
|
|
writeFile: true,
|
|
configName: "nobody",
|
|
key: showspinnerConfig,
|
|
wantErrIs: zerr.ErrConfigNotFound,
|
|
},
|
|
{
|
|
name: "ResetVar_illegal_key_ErrIllegalConfigKey",
|
|
cfgContents: validProfile,
|
|
writeFile: true,
|
|
configName: "a",
|
|
key: "bogus-key",
|
|
wantErrIs: zerr.ErrIllegalConfigKey,
|
|
},
|
|
}
|
|
|
|
for _, tableCase := range tests {
|
|
t.Run(tableCase.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfgPath := tempConfigPath(t, tableCase.writeFile, tableCase.cfgContents)
|
|
|
|
err := resetConfigValue(cfgPath, tableCase.configName, tableCase.key)
|
|
|
|
switch {
|
|
case tableCase.wantCliBadWrap:
|
|
require.Error(t, err)
|
|
require.False(t, isConfigUnavailable(err))
|
|
require.True(t, errors.Is(err, zerr.ErrCliBadConfig))
|
|
case tableCase.wantErrIs != nil:
|
|
require.ErrorIs(t, err, tableCase.wantErrIs)
|
|
default:
|
|
require.Failf(t, "incomplete table case: %s", tableCase.name)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("success_ResetVar_verify_tls_then_WriteFile", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
cfgPath := writeTestZotFile(t, dir,
|
|
`{"configs":[{"_name":"a","url":"https://example.com","verify-tls":false}]}`)
|
|
|
|
require.NoError(t, resetConfigValue(cfgPath, "a", verifyTLSConfig))
|
|
|
|
cfg, err := ReadZliConfigFile(cfgPath)
|
|
require.NoError(t, err)
|
|
require.Len(t, cfg.Configs, 1)
|
|
require.Nil(t, cfg.Configs[0].VerifyTLS)
|
|
})
|
|
}
|
|
|
|
func TestSetConfigValue(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
validProfile := `{"configs":[{"_name":"a","url":"https://example.com"}]}`
|
|
|
|
tests := []struct {
|
|
name string
|
|
cfgContents string
|
|
writeFile bool
|
|
configName string
|
|
key string
|
|
val string
|
|
wantErrIs error
|
|
wantCliBadWrap bool
|
|
}{
|
|
{
|
|
name: "fresh_ErrConfigNotFound",
|
|
writeFile: false,
|
|
configName: "any",
|
|
key: URLFlag,
|
|
val: "https://example.com",
|
|
wantErrIs: zerr.ErrConfigNotFound,
|
|
},
|
|
{
|
|
name: "read_invalid_JSON_ErrCliBadConfig",
|
|
cfgContents: `not-json`,
|
|
writeFile: true,
|
|
configName: "a",
|
|
key: URLFlag,
|
|
val: "https://example.com",
|
|
wantCliBadWrap: true,
|
|
},
|
|
{
|
|
name: "profile_not_found_ErrConfigNotFound",
|
|
cfgContents: validProfile,
|
|
writeFile: true,
|
|
configName: "nobody",
|
|
key: URLFlag,
|
|
val: "https://other.example",
|
|
wantErrIs: zerr.ErrConfigNotFound,
|
|
},
|
|
{
|
|
name: "SetVar_invalid_URL_ErrInvalidURL",
|
|
cfgContents: validProfile,
|
|
writeFile: true,
|
|
configName: "a",
|
|
key: URLFlag,
|
|
val: "not-a-valid-url",
|
|
wantErrIs: zerr.ErrInvalidURL,
|
|
},
|
|
{
|
|
name: "SetVar_illegal_key_ErrIllegalConfigKey",
|
|
cfgContents: validProfile,
|
|
writeFile: true,
|
|
configName: "a",
|
|
key: "bogus-key",
|
|
val: "x",
|
|
wantErrIs: zerr.ErrIllegalConfigKey,
|
|
},
|
|
{
|
|
name: "illegal_name_key_before_read_ErrIllegalConfigKey",
|
|
writeFile: false,
|
|
configName: "any",
|
|
key: nameKey,
|
|
val: "other",
|
|
wantErrIs: zerr.ErrIllegalConfigKey,
|
|
},
|
|
}
|
|
|
|
for _, tableCase := range tests {
|
|
t.Run(tableCase.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfgPath := tempConfigPath(t, tableCase.writeFile, tableCase.cfgContents)
|
|
|
|
err := setConfigValue(cfgPath, tableCase.configName, tableCase.key, tableCase.val)
|
|
|
|
switch {
|
|
case tableCase.wantCliBadWrap:
|
|
require.Error(t, err)
|
|
require.False(t, isConfigUnavailable(err))
|
|
require.True(t, errors.Is(err, zerr.ErrCliBadConfig))
|
|
case tableCase.wantErrIs != nil:
|
|
require.ErrorIs(t, err, tableCase.wantErrIs)
|
|
default:
|
|
require.Failf(t, "incomplete table case: %s", tableCase.name)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("success_SetVar_then_WriteFile", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
cfgPath := writeTestZotFile(t, dir, validProfile)
|
|
|
|
require.NoError(t, setConfigValue(cfgPath, "a", showspinnerConfig, "false"))
|
|
|
|
got, err := getConfigValue(cfgPath, "a", showspinnerConfig)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "false", got)
|
|
})
|
|
}
|
|
|
|
func TestConfigCmd_listFreshAndFindErrors(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
validProfile := `{"configs":[{"_name":"a","url":"https://example.com"}]}`
|
|
|
|
tests := []struct {
|
|
name string
|
|
writeFile bool
|
|
cfgContents string
|
|
runGetAll bool
|
|
configName string // only when runGetAll && wantErrIs != nil (Find miss)
|
|
wantOut string
|
|
wantErrIs error
|
|
}{
|
|
{
|
|
name: "getAllConfig_fresh_returns_empty",
|
|
runGetAll: true,
|
|
wantOut: "",
|
|
},
|
|
{
|
|
name: "getAllConfig_unknown_profile_ErrConfigNotFound",
|
|
writeFile: true,
|
|
cfgContents: validProfile,
|
|
runGetAll: true,
|
|
configName: "missing",
|
|
wantErrIs: zerr.ErrConfigNotFound,
|
|
},
|
|
{
|
|
name: "getConfigNames_fresh_returns_empty",
|
|
wantOut: "",
|
|
},
|
|
}
|
|
|
|
for _, tableCase := range tests {
|
|
t.Run(tableCase.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfgPath := tempConfigPath(t, tableCase.writeFile, tableCase.cfgContents)
|
|
|
|
switch {
|
|
case tableCase.runGetAll && tableCase.wantErrIs != nil:
|
|
_, err := getAllConfig(cfgPath, tableCase.configName)
|
|
require.ErrorIs(t, err, tableCase.wantErrIs)
|
|
case tableCase.runGetAll:
|
|
out, err := getAllConfig(cfgPath, "any")
|
|
require.NoError(t, err)
|
|
require.Equal(t, tableCase.wantOut, out)
|
|
default:
|
|
out, err := getConfigNames(cfgPath)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tableCase.wantOut, out)
|
|
}
|
|
})
|
|
}
|
|
}
|