Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions cmd/config/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ var configShowCmd = &cobra.Command{

apiKey := viper.GetString("seerr.api_key")
if apiKey != "" {
masked := apiKey
// Show only the last 4 characters so the key is identifiable without
// exposing the prefix, which is the more sensitive portion.
var masked string
if len(apiKey) > 4 {
masked = apiKey[:4] + "****" + apiKey[len(apiKey)-4:]
masked = "********" + apiKey[len(apiKey)-4:]
} else {
masked = "****"
}
Expand Down
97 changes: 97 additions & 0 deletions tests/config_show_mask_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package tests

import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"

"seerr-cli/cmd"

"github.com/spf13/viper"
)

// writeConfigFile writes minimal YAML to a temp file so config show can read it.
func writeConfigFile(t *testing.T, content string) string {
t.Helper()
dir := t.TempDir()
p := filepath.Join(dir, ".seerr-cli.yaml")
if err := os.WriteFile(p, []byte(content), 0o600); err != nil {
t.Fatalf("writeConfigFile: %v", err)
}
return p
}

func runConfigShow(t *testing.T, configPath string) string {
t.Helper()
viper.Reset()
b := new(bytes.Buffer)
cmd.RootCmd.SetOut(b)
cmd.RootCmd.SetErr(b)
cmd.RootCmd.SetArgs([]string{"config", "show", "--config", configPath})
if err := cmd.RootCmd.Execute(); err != nil {
t.Fatalf("config show: %v", err)
}
return b.String()
}

func TestConfigShowAPIKeyMasking(t *testing.T) {
tests := []struct {
name string
apiKey string
wantContain string
wantAbsent string
}{
{
name: "long key shows only last 4 chars",
apiKey: "abcdefghijklmnop",
wantContain: "********mnop",
wantAbsent: "abcd",
},
{
name: "exactly 5 char key shows only last 4",
apiKey: "hello",
wantContain: "****",
// "hell" must not appear
wantAbsent: "hell",
},
{
name: "4 char key is fully masked",
apiKey: "1234",
wantContain: "****",
wantAbsent: "1234",
},
{
name: "short key (2 chars) is fully masked",
apiKey: "ab",
wantContain: "****",
wantAbsent: "ab",
},
{
name: "key absent shows not-set label",
apiKey: "",
wantContain: "<not set>",
wantAbsent: "",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var yaml string
if tc.apiKey != "" {
yaml = "seerr:\n server: http://localhost:5055\n api_key: " + tc.apiKey + "\n"
} else {
yaml = "seerr:\n server: http://localhost:5055\n"
}
p := writeConfigFile(t, yaml)
out := runConfigShow(t, p)
if !strings.Contains(out, tc.wantContain) {
t.Errorf("expected output to contain %q, got: %s", tc.wantContain, out)
}
if tc.wantAbsent != "" && strings.Contains(out, tc.wantAbsent) {
t.Errorf("expected output NOT to contain %q, got: %s", tc.wantAbsent, out)
}
})
}
}
10 changes: 8 additions & 2 deletions tests/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,16 @@ func TestConfigCommands(t *testing.T) {
if !strings.Contains(out, "http://test-server:5055") {
t.Errorf("expected output to contain server URL, got: %s", out)
}
// Check for masked API key
if !strings.Contains(out, "test****2345") {
// Only the last 4 characters should be visible; the prefix is masked.
if !strings.Contains(out, "********2345") {
t.Errorf("expected output to contain masked API key, got: %s", out)
}
// The API Key line must not contain the plain-text prefix of the key.
for _, line := range strings.Split(out, "\n") {
if strings.HasPrefix(line, "API Key:") && strings.Contains(line, "test-api") {
t.Errorf("expected API key prefix to be masked in line: %s", line)
}
}
})

t.Run("config show empty", func(t *testing.T) {
Expand Down
Loading