Skip to content

Commit 221ce14

Browse files
cevianclaude
andcommitted
Address PR review feedback for db save-password
- Add -p short flag for --password option - Use term.ReadPassword instead of bufio.Scanner for secure password input - Add TTY check before interactive prompt to fail gracefully in non-terminal environments - Scope err variable to conditional block for cleaner code - Output status messages to stderr instead of stdout for consistency - Make TTY check and password reading testable via function variables - Update tests to mock terminal behavior without requiring actual TTY - Move TIGER_NEW_PASSWORD documentation from global to save-password section 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 33e4f00 commit 221ce14

File tree

3 files changed

+59
-37
lines changed

3 files changed

+59
-37
lines changed

internal/tiger/cmd/db.go

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package cmd
22

33
import (
4-
"bufio"
54
"context"
65
"errors"
76
"fmt"
@@ -12,10 +11,12 @@ import (
1211
"github.com/jackc/pgx/v5"
1312
"github.com/jackc/pgx/v5/pgconn"
1413
"github.com/spf13/cobra"
14+
"golang.org/x/term"
1515

1616
"github.com/timescale/tiger-cli/internal/tiger/api"
1717
"github.com/timescale/tiger-cli/internal/tiger/config"
1818
"github.com/timescale/tiger-cli/internal/tiger/password"
19+
"github.com/timescale/tiger-cli/internal/tiger/util"
1920
)
2021

2122
var (
@@ -24,6 +25,16 @@ var (
2425

2526
// getServiceDetailsFunc can be overridden for testing
2627
getServiceDetailsFunc = getServiceDetails
28+
29+
// checkStdinIsTTY can be overridden for testing to bypass TTY detection
30+
checkStdinIsTTY = func() bool {
31+
return util.IsTerminal(os.Stdin)
32+
}
33+
34+
// readPasswordFromTerminal can be overridden for testing to inject password input
35+
readPasswordFromTerminal = func(fd int) ([]byte, error) {
36+
return term.ReadPassword(fd)
37+
}
2738
)
2839

2940
func buildDbConnectionStringCmd() *cobra.Command {
@@ -301,37 +312,37 @@ Examples:
301312
// Use environment variable
302313
passwordToSave = envPassword
303314
} else {
304-
// Interactive prompt
315+
// Interactive prompt - check if we're in a terminal
316+
if !checkStdinIsTTY() {
317+
return fmt.Errorf("TTY not detected - password required. Use --password flag or TIGER_NEW_PASSWORD environment variable")
318+
}
319+
305320
fmt.Fprint(cmd.OutOrStdout(), "Enter password: ")
306-
scanner := bufio.NewScanner(cmd.InOrStdin())
307-
if scanner.Scan() {
308-
passwordToSave = scanner.Text()
309-
} else {
310-
if err := scanner.Err(); err != nil {
311-
return fmt.Errorf("failed to read password: %w", err)
312-
}
313-
return fmt.Errorf("failed to read password: EOF")
321+
bytePassword, err := readPasswordFromTerminal(int(os.Stdin.Fd()))
322+
if err != nil {
323+
return fmt.Errorf("failed to read password: %w", err)
314324
}
325+
fmt.Fprintln(cmd.OutOrStdout()) // Print newline after hidden input
326+
passwordToSave = string(bytePassword)
315327
if passwordToSave == "" {
316328
return fmt.Errorf("password cannot be empty")
317329
}
318330
}
319331

320332
// Save password using configured storage
321333
storage := password.GetPasswordStorage()
322-
err = storage.Save(service, passwordToSave, dbSavePasswordRole)
323-
if err != nil {
334+
if err := storage.Save(service, passwordToSave, dbSavePasswordRole); err != nil {
324335
return fmt.Errorf("failed to save password: %w", err)
325336
}
326337

327-
fmt.Fprintf(cmd.OutOrStdout(), "Password saved successfully for service %s (role: %s)\n",
338+
fmt.Fprintf(cmd.ErrOrStderr(), "Password saved successfully for service %s (role: %s)\n",
328339
*service.ServiceId, dbSavePasswordRole)
329340
return nil
330341
},
331342
}
332343

333344
// Add flags for db save-password command
334-
cmd.Flags().StringVar(&dbSavePasswordValue, "password", "", "Password to save")
345+
cmd.Flags().StringVarP(&dbSavePasswordValue, "password", "p", "", "Password to save")
335346
cmd.Flags().StringVar(&dbSavePasswordRole, "role", "tsdbadmin", "Database role/username")
336347

337348
return cmd

internal/tiger/cmd/db_test.go

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,24 +1193,27 @@ func TestDBSavePassword_InteractivePrompt(t *testing.T) {
11931193

11941194
// Prepare the password input
11951195
testPassword := "interactive-password-999"
1196-
inputReader := strings.NewReader(testPassword + "\n")
11971196

1198-
// Build the command with custom stdin
1199-
testRoot := buildRootCmd()
1200-
buf := new(bytes.Buffer)
1201-
testRoot.SetOut(buf)
1202-
testRoot.SetErr(buf)
1203-
testRoot.SetIn(inputReader) // Use our reader as stdin
1204-
testRoot.SetArgs([]string{"db", "save-password"}) // No --password flag and no env var
1197+
// Mock TTY check to return true (simulate terminal)
1198+
originalCheckStdinIsTTY := checkStdinIsTTY
1199+
checkStdinIsTTY = func() bool {
1200+
return true
1201+
}
1202+
defer func() { checkStdinIsTTY = originalCheckStdinIsTTY }()
12051203

1206-
// Execute the command
1207-
err = testRoot.Execute()
1204+
// Mock password reading to return our test password
1205+
originalReadPasswordFromTerminal := readPasswordFromTerminal
1206+
readPasswordFromTerminal = func(fd int) ([]byte, error) {
1207+
return []byte(testPassword), nil
1208+
}
1209+
defer func() { readPasswordFromTerminal = originalReadPasswordFromTerminal }()
1210+
1211+
// Execute save-password without --password flag or env var
1212+
output, err := executeDBCommand("db", "save-password")
12081213
if err != nil {
12091214
t.Fatalf("Expected save-password to succeed with interactive input, got error: %v", err)
12101215
}
12111216

1212-
output := buf.String()
1213-
12141217
// Verify the prompt was shown
12151218
if !strings.Contains(output, "Enter password:") {
12161219
t.Errorf("Expected password prompt, got: %s", output)
@@ -1264,19 +1267,22 @@ func TestDBSavePassword_InteractivePromptEmpty(t *testing.T) {
12641267
// Make sure TIGER_NEW_PASSWORD is not set
12651268
os.Unsetenv("TIGER_NEW_PASSWORD")
12661269

1267-
// Prepare empty input (just a newline)
1268-
inputReader := strings.NewReader("\n")
1270+
// Mock TTY check to return true (simulate terminal)
1271+
originalCheckStdinIsTTY := checkStdinIsTTY
1272+
checkStdinIsTTY = func() bool {
1273+
return true
1274+
}
1275+
defer func() { checkStdinIsTTY = originalCheckStdinIsTTY }()
12691276

1270-
// Build the command with custom stdin
1271-
testRoot := buildRootCmd()
1272-
buf := new(bytes.Buffer)
1273-
testRoot.SetOut(buf)
1274-
testRoot.SetErr(buf)
1275-
testRoot.SetIn(inputReader) // Use our reader as stdin
1276-
testRoot.SetArgs([]string{"db", "save-password"}) // No --password flag or env var
1277+
// Mock password reading to return empty password
1278+
originalReadPasswordFromTerminal := readPasswordFromTerminal
1279+
readPasswordFromTerminal = func(fd int) ([]byte, error) {
1280+
return []byte(""), nil
1281+
}
1282+
defer func() { readPasswordFromTerminal = originalReadPasswordFromTerminal }()
12771283

12781284
// Execute the command
1279-
err = testRoot.Execute()
1285+
_, err = executeDBCommand("db", "save-password")
12801286
if err == nil {
12811287
t.Fatal("Expected error when user provides empty password interactively")
12821288
}

specs/spec.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ mv tiger /usr/local/bin/
3131
- `TIGER_CONFIG_DIR`: Configuration directory (default: ~/.config/tiger)
3232
- `TIGER_OUTPUT`: Default output format (json, yaml, table)
3333
- `TIGER_ANALYTICS`: Enable/disable usage analytics (true/false)
34-
- `TIGER_NEW_PASSWORD`: Password to use for save-password command (used when --password flag not provided)
3534

3635
### Configuration File
3736

@@ -430,6 +429,12 @@ The `connect` and `psql` commands automatically handle authentication using:
430429
2. `PGPASSWORD` environment variable
431430
3. Interactive password prompt (if neither above is available)
432431

432+
**Password Input for save-password:**
433+
The `save-password` command accepts passwords through three methods (in order of precedence):
434+
1. `--password` flag with explicit value (highest precedence)
435+
2. `TIGER_NEW_PASSWORD` environment variable
436+
3. Interactive prompt (if neither provided and stdin is a TTY)
437+
433438
**Advanced psql Usage:**
434439
The `connect` and `psql` commands support passing additional flags directly to the psql client using the `--` separator. Any flags after `--` are passed through to psql unchanged, allowing full access to psql's functionality while maintaining tiger's connection and authentication handling.
435440

0 commit comments

Comments
 (0)