Files

253 lines
6.4 KiB
Go

package backup
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
func TestParseDSN_ValidFull(t *testing.T) {
creds, err := ParseDSN("postgres://myuser:s3cret@db.example.com:5433/mydb?sslmode=require")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
assertEqual(t, "host", "db.example.com", creds.Host)
assertEqual(t, "port", "5433", creds.Port)
assertEqual(t, "user", "myuser", creds.User)
assertEqual(t, "password", "s3cret", creds.Password)
assertEqual(t, "dbname", "mydb", creds.DBName)
assertEqual(t, "sslmode", "require", creds.SSLMode)
}
func TestParseDSN_Defaults(t *testing.T) {
creds, err := ParseDSN("postgres://admin@/testdb")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
assertEqual(t, "host", "localhost", creds.Host)
assertEqual(t, "port", "5432", creds.Port)
assertEqual(t, "user", "admin", creds.User)
assertEqual(t, "password", "", creds.Password)
assertEqual(t, "sslmode", "prefer", creds.SSLMode)
}
func TestParseDSN_InvalidScheme(t *testing.T) {
_, err := ParseDSN("mysql://user:pass@localhost/db")
if err == nil {
t.Fatal("expected error for mysql scheme")
}
if !strings.Contains(err.Error(), "unsupported scheme") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestParseDSN_MissingDBName(t *testing.T) {
_, err := ParseDSN("postgres://user:pass@localhost:5432/")
if err == nil {
t.Fatal("expected error for missing dbname")
}
}
func TestParseDSN_SpecialCharsInPassword(t *testing.T) {
// URL-encoded password: p@ss:w0rd! → p%40ss%3Aw0rd%21
creds, err := ParseDSN("postgres://user:p%40ss%3Aw0rd%21@localhost/db")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
assertEqual(t, "password", "p@ss:w0rd!", creds.Password)
}
func TestWritePgpass_FilePermissions(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Windows does not expose POSIX 0600 permissions reliably")
}
creds := &PGCredentials{
Host: "localhost", Port: "5432",
User: "testuser", Password: "testpass",
DBName: "testdb",
}
tempDir, pgpassPath, err := WritePgpass(creds)
if err != nil {
t.Fatalf("WritePgpass failed: %v", err)
}
defer os.RemoveAll(tempDir)
// Verify file exists
info, err := os.Stat(pgpassPath)
if err != nil {
t.Fatalf("pgpass file not found: %v", err)
}
// Verify 0600 permissions (owner read+write only)
perm := info.Mode().Perm()
if perm != 0600 {
t.Fatalf("expected 0600 permissions, got %04o", perm)
}
}
func TestWritePgpass_Content(t *testing.T) {
creds := &PGCredentials{
Host: "db.example.com", Port: "5433",
User: "myuser", Password: "my:pass\\word",
DBName: "mydb",
}
tempDir, pgpassPath, err := WritePgpass(creds)
if err != nil {
t.Fatalf("WritePgpass failed: %v", err)
}
defer os.RemoveAll(tempDir)
data, err := os.ReadFile(pgpassPath)
if err != nil {
t.Fatalf("read pgpass: %v", err)
}
content := string(data)
// Colons and backslashes must be escaped
expected := `db.example.com:5433:mydb:myuser:my\:pass\\word` + "\n"
if content != expected {
t.Fatalf("pgpass content mismatch:\n got: %q\n want: %q", content, expected)
}
}
func TestWritePgpass_TempDirCleanup(t *testing.T) {
creds := &PGCredentials{
Host: "localhost", Port: "5432",
User: "u", Password: "p", DBName: "db",
}
tempDir, _, err := WritePgpass(creds)
if err != nil {
t.Fatalf("WritePgpass failed: %v", err)
}
// Simulate cleanup
os.RemoveAll(tempDir)
// Verify directory is gone
if _, err := os.Stat(tempDir); !os.IsNotExist(err) {
t.Fatalf("temp dir still exists after RemoveAll: %s", tempDir)
}
}
func TestCleanEnv_NoSecretLeak(t *testing.T) {
// Set a fake secret in current env to verify it doesn't leak
t.Setenv("GOCLAW_POSTGRES_DSN", "postgres://user:SECRET@localhost/db")
t.Setenv("GOCLAW_ENCRYPTION_KEY", "super-secret-key")
t.Setenv("AWS_SECRET_ACCESS_KEY", "aws-secret")
env := CleanEnv("/tmp/test/.pgpass")
envStr := strings.Join(env, "\n")
// Must contain PGPASSFILE
if !strings.Contains(envStr, "PGPASSFILE=/tmp/test/.pgpass") {
t.Error("PGPASSFILE not set")
}
// Must NOT contain any secrets
for _, forbidden := range []string{"SECRET", "super-secret", "aws-secret", "GOCLAW_POSTGRES_DSN", "GOCLAW_ENCRYPTION_KEY", "AWS_SECRET"} {
if strings.Contains(envStr, forbidden) {
t.Errorf("secret leaked in clean env: found %q", forbidden)
}
}
// Must only have 4 entries
if len(env) != 4 {
t.Errorf("expected exactly 4 env vars, got %d: %v", len(env), env)
}
}
func TestCleanEnv_HasRequiredVars(t *testing.T) {
env := CleanEnv("/tmp/.pgpass")
required := map[string]bool{"PGPASSFILE": false, "PATH": false, "HOME": false, "LC_ALL": false}
for _, e := range env {
key := strings.SplitN(e, "=", 2)[0]
if _, ok := required[key]; ok {
required[key] = true
}
}
for k, found := range required {
if !found {
t.Errorf("missing required env var: %s", k)
}
}
}
func TestSanitizeDSN_StripsPassword(t *testing.T) {
cases := []struct {
input string
want string
}{
{
"postgres://user:s3cret@localhost:5432/db?sslmode=disable",
"postgres://user@localhost:5432/db?sslmode=disable",
},
{
"postgres://admin:p%40ss%3Aword@db.example.com/mydb",
"postgres://admin@db.example.com/mydb",
},
{
"postgres://nopass@localhost/db",
"postgres://nopass@localhost/db",
},
{
"not-a-url",
"***",
},
}
for _, tc := range cases {
got := SanitizeDSN(tc.input)
if got != tc.want {
t.Errorf("SanitizeDSN(%q) = %q, want %q", tc.input, got, tc.want)
}
}
}
func TestWritePgpass_PathIsolation(t *testing.T) {
// Create two concurrent pgpass files — verify they don't collide
creds1 := &PGCredentials{Host: "h1", Port: "5432", User: "u1", Password: "p1", DBName: "db1"}
creds2 := &PGCredentials{Host: "h2", Port: "5433", User: "u2", Password: "p2", DBName: "db2"}
dir1, path1, err := WritePgpass(creds1)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir1)
dir2, path2, err := WritePgpass(creds2)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir2)
// Different temp dirs
if dir1 == dir2 {
t.Error("concurrent WritePgpass created same temp dir")
}
// Different file paths
if path1 == path2 {
t.Error("concurrent WritePgpass created same pgpass path")
}
// Verify parent dirs are different
if filepath.Dir(path1) == filepath.Dir(path2) {
t.Error("pgpass files share parent directory")
}
}
func assertEqual(t *testing.T, field, want, got string) {
t.Helper()
if got != want {
t.Errorf("%s: got %q, want %q", field, got, want)
}
}