A board reads through interfaces and holds nothing. Everything it needs is already answered — as text, for people, which is not something a page can read. `--json` rather than a serving API, because nothing needs one yet: whatever serves a board runs the command, and the constraint holds either way — the board never touches a context's store. An API is the larger thing and should wait until something asks for it. Both forms are gathered from the same reads before either says anything, so they answer the same questions rather than being two implementations that can drift. That was not true of the first version: the JSON printed after the text, because the branch was too late. Four properties, each asserted and each confirmed to fail when removed: - refused and failed stay distinct all the way out. They are fixed in different places, so one word for both sends half a page's readers to the wrong one — and how much DID apply is carried, since "three of eight" and "none of eight" are different machines - a machine that never spoke carries no time at all, rather than a zero one that any page would format as a date in 1970 - nothing is null. A page distinguishing "no machines are wrong" from "this field is missing" has to handle both, and null is the one that gets forgotten - no field is named like a secret. Everything here comes from records that hold no readable one, but a shape a page is built against is exactly where one would eventually be added for convenience
118 lines
3.7 KiB
Go
118 lines
3.7 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Where a builder publishes.
|
|
//
|
|
// Preferably from the mesh: a builder that is a module requires an artifact store, and the mesh
|
|
// writes it a binding saying which machine answers and on what port. Reading it means the address
|
|
// is not a setting somebody keeps in step by hand.
|
|
|
|
func binding(t *testing.T, body string) string {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), "artifact-store.json")
|
|
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func TestTheMeshSaysWhereToPublish(t *testing.T) {
|
|
t.Setenv("MESH_BINDING", binding(t, `{"binding":1,"provision":"artifact-store",
|
|
"from":"anchor","at":"anchor.internal","serves":{"port":5000,"scheme":"http"}}`))
|
|
where, err := whereToPublish()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if where != "anchor.internal:5000" {
|
|
t.Fatalf("got %q", where)
|
|
}
|
|
}
|
|
|
|
func TestABindingWithNoAddressIsRefused(t *testing.T) {
|
|
// The provider is not on the private network, so there is no name to reach it by. Falling
|
|
// back to anything would publish to a store on the wrong machine and be found out much later.
|
|
t.Setenv("MESH_BINDING", binding(t, `{"binding":1,"from":"anchor","serves":{"port":5000}}`))
|
|
_, err := whereToPublish()
|
|
if err == nil {
|
|
t.Fatal("a binding with nowhere to reach was accepted")
|
|
}
|
|
if !strings.Contains(err.Error(), "anchor") {
|
|
t.Fatalf("the failure does not name the machine: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestABindingWithNoPortIsRefused(t *testing.T) {
|
|
t.Setenv("MESH_BINDING", binding(t, `{"binding":1,"from":"a","at":"a.internal","serves":{}}`))
|
|
if _, err := whereToPublish(); err == nil {
|
|
t.Fatal("a binding saying nothing about a port was accepted")
|
|
}
|
|
}
|
|
|
|
func TestTheVariableStillWorksForABuilderRunByAPerson(t *testing.T) {
|
|
// Which is how this started and how it is still run while being developed.
|
|
t.Setenv("MESH_BINDING", "")
|
|
t.Setenv("MESH_REGISTRY", "127.0.0.1:5000")
|
|
where, err := whereToPublish()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if where != "127.0.0.1:5000" {
|
|
t.Fatalf("got %q", where)
|
|
}
|
|
}
|
|
|
|
func TestNeitherIsRefusedRatherThanGuessed(t *testing.T) {
|
|
t.Setenv("MESH_BINDING", "")
|
|
t.Setenv("MESH_REGISTRY", "")
|
|
if _, err := whereToPublish(); err == nil {
|
|
t.Fatal("a builder with nowhere to publish reported somewhere")
|
|
}
|
|
}
|
|
|
|
func TestTheCredentialComesFromAFileTheMeshSealed(t *testing.T) {
|
|
// A builder that is a module is given its credential the way every module is: sealed to the
|
|
// machine and written by the host. An environment variable instead would put the one copy
|
|
// that matters through a terminal and a process listing.
|
|
path := filepath.Join(t.TempDir(), "broker")
|
|
if err := os.WriteFile(path, []byte("amqps://a-builder:secret@broker.internal:5671/\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("MESH_BROKER_FILE", path)
|
|
t.Setenv("MESH_BROKER_AMQP", "amqp://should-not-be-used@nowhere/")
|
|
|
|
got, err := brokerFrom()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != "amqps://a-builder:secret@broker.internal:5671/" {
|
|
t.Fatalf("got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestAnEmptyCredentialFileIsRefused(t *testing.T) {
|
|
// Otherwise the builder connects as nobody and is refused, with the reason three layers away.
|
|
path := filepath.Join(t.TempDir(), "broker")
|
|
if err := os.WriteFile(path, []byte("\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("MESH_BROKER_FILE", path)
|
|
t.Setenv("MESH_BROKER_AMQP", "")
|
|
if _, err := brokerFrom(); err == nil {
|
|
t.Fatal("an empty credential was accepted")
|
|
}
|
|
}
|
|
|
|
func TestABuilderWithNoCredentialAtAllSaysSo(t *testing.T) {
|
|
t.Setenv("MESH_BROKER_FILE", "")
|
|
t.Setenv("MESH_BROKER_AMQP", "")
|
|
if _, err := brokerFrom(); err == nil {
|
|
t.Fatal("a builder with no broker reported one")
|
|
}
|
|
}
|