mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-25 22:24:04 +00:00
253 lines
6.4 KiB
Go
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)
|
|
}
|
|
}
|