Files
jschoubben e07b56ce43 An address is read from the node's settings where it is used, never recorded with a port
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`, read on top of the sealed value by the
store, the broker, the management API and the bus connection, and filled into
its container by a placeholder that names a seat, `${seat:mesh-store:5432}`,
from the node's given or mesh-assigned ports — never the manifest's number, and
empty when the mesh has nothing to add, so what genesis wrote stands. A value
that is still a placeholder is nothing said, aloud: the manifest naming it lands
in the next commit, once every control plane that composes it knows it.

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. Over the network as `<node>.internal:<port>`; on the store's own node
before any network exists — every genesis push before its "network" step — by
loopback. 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
come from one derivation: the node's given port, over the mesh's assignment,
over the manifest's number.

novox/hq 04-ISSUES/102
2026-09-23 23:49:31 +02:00

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)
}
}