Three readers did not follow a moved foundation port (novox/hq 04-ISSUES/102),
and each took the control-node down in its own way: the control plane's own
store and broker connections, sealed at genesis with the port inside; and every
build the mesh ever recorded, kept as `<registry>:<port>/<module>/<artifact>@…`.
The control plane cannot open its own sealed connections to move a port, and it
cannot bind the store as a consumer would — a binding mints a credential. So its
settings get a third twin: `NAME_PORT`, composed into its container from the
node's settings by a placeholder that names a seat, `${seat:mesh-store:5432}`,
and read on top of the sealed value by the store, the broker, the management API
and the bus connection. The answer is empty when the mesh has nothing to add,
so what genesis wrote stands until the node says otherwise.
A build is now recorded by digest and path — `artifact-store://<module>/<artifact>@…`
— and the store's address is composed in where a reference is used: the
declaration, the trust file, the bases a build is handed, a replay to the
catalogue. A reference recorded before this, with an address, is re-routed the
same way when the mesh built it. The trust file and every provider's address
now come from one derivation, with the node's given port over the mesh's
assignment over the manifest's number.
novox/hq 04-ISSUES/102
268 lines
8.3 KiB
Go
268 lines
8.3 KiB
Go
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)
|
|
}
|
|
}
|
|
|
|
// The node moved the store, and the control plane's own connection follows (novox/hq 04-ISSUES/102).
|
|
//
|
|
// The connection string is what genesis wrote, port and all; the port twin is what the node's
|
|
// settings say now. The pool's configuration is the one place both meet.
|
|
func TestThePortTwinMovesTheStoresPort(t *testing.T) {
|
|
alone(t)
|
|
t.Setenv(PortVariable(example), "")
|
|
path := filepath.Join(t.TempDir(), "inventory")
|
|
if err := os.WriteFile(path, []byte(dsn+"\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv(FileVariable(example), path)
|
|
|
|
opened, err := Open(t.Context(), example)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := opened.Pool().Config().ConnConfig.Port; got != 5432 {
|
|
t.Fatalf("with nothing said, the store is on %d rather than what the file says", got)
|
|
}
|
|
opened.Close()
|
|
|
|
t.Setenv(PortVariable(example), "6852")
|
|
opened, err = Open(t.Context(), example)
|
|
if err != nil {
|
|
t.Fatalf("a moved port was refused: %v", err)
|
|
}
|
|
defer opened.Close()
|
|
if got := opened.Pool().Config().ConnConfig.Port; got != 6852 {
|
|
t.Fatalf("the store is on %d, and the node put it on 6852", got)
|
|
}
|
|
if got := opened.Pool().Config().ConnConfig.Password; got != "s3cret-in-here" {
|
|
t.Fatalf("moving the port changed the password to %q", got)
|
|
}
|
|
}
|
|
|
|
func TestAPortTwinThatIsNotAPortNamesItselfAndNotTheSecret(t *testing.T) {
|
|
alone(t)
|
|
t.Setenv(Variable(example), dsn)
|
|
t.Setenv(PortVariable(example), "six")
|
|
_, err := Open(t.Context(), example)
|
|
if err == nil {
|
|
t.Fatal("a port that is not a number was accepted")
|
|
}
|
|
if !strings.Contains(err.Error(), PortVariable(example)) {
|
|
t.Errorf("the error does not name the variable: %v", err)
|
|
}
|
|
if strings.Contains(err.Error(), "s3cret") {
|
|
t.Errorf("the error quotes the password: %v", err)
|
|
}
|
|
}
|