package store import ( "os" "path/filepath" "strings" "testing" "github.com/jackc/pgx/v5/pgxpool" ) // Where a context's connection settings come from, and what happens when that is unclear. // // No database is needed for any of this: pgxpool connects lazily, so `Open` succeeding proves that // a string was found and parsed, which is exactly what is under test here. The live tests next door // prove the rest. // example is a context name these tests can own outright. Short, and matching `contextName`, which // allows no dashes. const example = "example" // dsn is a connection string shaped like a real one, password and all — because the property most // of these tests assert is that this never appears in an error. const dsn = "postgres://postgres:s3cret-in-here@127.0.0.1:5432/example?sslmode=disable" // alone clears both variables for a context, so a test is not passing or failing on what the // machine running it happens to export. func alone(t *testing.T) { t.Helper() t.Setenv(Variable(example), "") t.Setenv(FileVariable(example), "") } func TestTheFileVariableIsTheVariablePlusFile(t *testing.T) { if got := FileVariable("inventory"); got != "MESH_STORE_INVENTORY_FILE" { t.Errorf("the file variable is %q", got) } } func TestTheConnectionMayComeFromAFile(t *testing.T) { alone(t) path := filepath.Join(t.TempDir(), "inventory") if err := os.WriteFile(path, []byte(dsn), 0o600); err != nil { t.Fatal(err) } t.Setenv(FileVariable(example), path) held, from, err := settingsFor(example) if err != nil { t.Fatalf("a file holding the settings was refused: %v", err) } if held != dsn { t.Errorf("the settings came back as %q", held) } if !strings.Contains(from, FileVariable(example)) || !strings.Contains(from, path) { t.Errorf("the source is reported as %q, which names neither the variable nor the file", from) } // And the whole way through, so this is not a test of a helper nobody calls. opened, err := Open(t.Context(), example) if err != nil { t.Fatalf("the store would not open from a file: %v", err) } opened.Close() } func TestATrailingNewlineInTheFileIsTolerated(t *testing.T) { alone(t) path := filepath.Join(t.TempDir(), "inventory") // What a person's editor writes, and what the host writes when it fills a ${secret:…} // placeholder into a file whose content ended in one. Untrimmed it is a control character // inside a URL, which is refused far from anything that could explain it. if err := os.WriteFile(path, []byte(dsn+"\n"), 0o600); err != nil { t.Fatal(err) } t.Setenv(FileVariable(example), path) held, _, err := settingsFor(example) if err != nil { t.Fatalf("a file ending in a newline was refused: %v", err) } if held != dsn { t.Errorf("the newline survived: %q", held) } if _, err := pgxpool.ParseConfig(held); err != nil { t.Errorf("what came out of the file does not parse: %v", err) } } func TestBothTheVariableAndTheFileIsRefused(t *testing.T) { alone(t) path := filepath.Join(t.TempDir(), "inventory") if err := os.WriteFile(path, []byte(dsn), 0o600); err != nil { t.Fatal(err) } t.Setenv(Variable(example), dsn) t.Setenv(FileVariable(example), path) _, _, err := settingsFor(example) if err == nil { t.Fatal("both were set and one of them was silently chosen") } for _, want := range []string{Variable(example), FileVariable(example)} { if !strings.Contains(err.Error(), want) { t.Errorf("the refusal does not name %s: %v", want, err) } } mustNotLeak(t, err) // Open refuses for the same reason rather than reaching a database. if _, err := Open(t.Context(), example); err == nil { t.Fatal("Open accepted both settings at once") } } func TestAFileThatCannotBeReadIsRefusedByPath(t *testing.T) { alone(t) path := filepath.Join(t.TempDir(), "never-written") t.Setenv(FileVariable(example), path) _, _, err := settingsFor(example) if err == nil { t.Fatal("a missing settings file was accepted") } if !strings.Contains(err.Error(), path) { t.Errorf("the refusal does not say which file: %v", err) } mustNotLeak(t, err) } func TestAnEmptyFileIsRefusedByPath(t *testing.T) { alone(t) path := filepath.Join(t.TempDir(), "inventory") // A placeholder that was never filled in looks exactly like this on the machine. if err := os.WriteFile(path, []byte("\n"), 0o600); err != nil { t.Fatal(err) } t.Setenv(FileVariable(example), path) _, _, err := settingsFor(example) if err == nil { t.Fatal("an empty settings file was accepted") } if !strings.Contains(err.Error(), path) { t.Errorf("the refusal does not say which file: %v", err) } } func TestNeitherIsRefusedNamingBoth(t *testing.T) { alone(t) _, _, err := settingsFor(example) if err == nil { t.Fatal("a context with no settings at all was accepted") } for _, want := range []string{Variable(example), FileVariable(example)} { if !strings.Contains(err.Error(), want) { t.Errorf("the refusal does not name %s: %v", want, err) } } } func TestTheEnvironmentVariableStillWorksUnchanged(t *testing.T) { alone(t) t.Setenv(Variable(example), dsn) held, from, err := settingsFor(example) if err != nil { t.Fatalf("the variable that has always worked was refused: %v", err) } if held != dsn { t.Errorf("the settings came back as %q", held) } if from != Variable(example) { t.Errorf("the source is reported as %q", from) } opened, err := Open(t.Context(), example) if err != nil { t.Fatalf("the store would not open from the variable: %v", err) } opened.Close() } // The no-leak property, at the one place a file makes it easy to lose: a value that will not parse // is usually a password with something wrong around it, and the error goes into a log. func TestASettingsFileThatCannotBeParsedIsNotQuotedBack(t *testing.T) { alone(t) path := filepath.Join(t.TempDir(), "inventory") const mangled = "postgres://postgres:s3cret-in-here@ 127.0.0.1:5432/example" if err := os.WriteFile(path, []byte(mangled), 0o600); err != nil { t.Fatal(err) } t.Setenv(FileVariable(example), path) opened, err := Open(t.Context(), example) if err == nil { opened.Close() t.Fatal("a mangled connection string was accepted") } if strings.Contains(err.Error(), "s3cret-in-here") { t.Fatalf("the password is in the error: %v", err) } // It still says enough to be actionable: which variable, and which file. if !strings.Contains(err.Error(), FileVariable(example)) || !strings.Contains(err.Error(), path) { t.Errorf("the refusal says neither where to look nor which file: %v", err) } } func mustNotLeak(t *testing.T, err error) { t.Helper() if strings.Contains(err.Error(), "s3cret-in-here") { t.Fatalf("the password is in the error: %v", err) } }