193 lines
7.2 KiB
Go
193 lines
7.2 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-host/internal/bootstrap"
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
// Argument handling gets tests for the reason `mesh-host` records: the standard library stops
|
|
// parsing at the first non-flag argument, so a flag sitting after one is silently dropped and the
|
|
// command exits zero having ignored what it was asked. Here that would mean `--dry-run` ignored on
|
|
// a program whose whole job is to change a machine.
|
|
|
|
func TestBootstrapIsWhatItDoesWithNoCommand(t *testing.T) {
|
|
// Running the installer with nothing but flags must install, not print usage: the command is
|
|
// the reason the binary exists, and making somebody type its name twice buys nothing.
|
|
command, opts, _, err := parseArgs([]string{"--dry-run"})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if command != "bootstrap" {
|
|
t.Errorf("command = %q, want bootstrap", command)
|
|
}
|
|
if !opts.DryRun {
|
|
t.Error("--dry-run before any subcommand was ignored")
|
|
}
|
|
}
|
|
|
|
func TestFlagsAreReadWhereverTheySit(t *testing.T) {
|
|
for _, args := range [][]string{
|
|
{"bootstrap", "--dry-run", "--bundle", "s.lock", "--json"},
|
|
{"bootstrap", "--json", "--bundle=s.lock", "--dry-run"},
|
|
{"--bundle", "s.lock", "--dry-run", "--json"},
|
|
} {
|
|
_, opts, jsonOut, err := parseArgs(args)
|
|
if err != nil {
|
|
t.Errorf("%v: unexpected error: %v", args, err)
|
|
continue
|
|
}
|
|
if !opts.DryRun || !jsonOut || opts.Template != "s.lock" {
|
|
t.Errorf("%v parsed as dry-run=%v json=%v bundle=%q",
|
|
args, opts.DryRun, jsonOut, opts.Template)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAMistypedFlagIsRefusedNotIgnored(t *testing.T) {
|
|
// Asymmetric cost: an error is a moment's annoyance, and a silently dropped --dry-run is a
|
|
// machine changed by somebody who asked for it not to be.
|
|
if _, _, _, err := parseArgs([]string{"bootstrap", "--dry-runn"}); err == nil {
|
|
t.Fatal("a mistyped flag was accepted")
|
|
}
|
|
}
|
|
|
|
func TestAnUnexpectedArgumentIsRefused(t *testing.T) {
|
|
if _, _, _, err := parseArgs([]string{"bootstrap", "foundation.lock"}); err == nil {
|
|
t.Fatal("a stray argument was ignored rather than refused — the bundle is --bundle")
|
|
}
|
|
}
|
|
|
|
func TestTheDefaultsAreTheDocumentedOnes(t *testing.T) {
|
|
// The usage text is a promise. A default that drifts from what is printed is a small lie that
|
|
// costs somebody an afternoon in front of a machine that will not come up.
|
|
_, opts, jsonOut, err := parseArgs(nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if opts.Template != defaultTemplate {
|
|
t.Errorf("default bundle is %q; the usage text says %q", opts.Template, defaultTemplate)
|
|
}
|
|
if opts.Out != defaultOut {
|
|
t.Errorf("default out is %q; the usage text says %q", opts.Out, defaultOut)
|
|
}
|
|
if opts.State != store.DefaultPath {
|
|
t.Errorf("default state is %q; the host's own default is %q", opts.State, store.DefaultPath)
|
|
}
|
|
if opts.Timeout != 30*time.Second {
|
|
t.Errorf("default timeout is %s; the usage text says 30s", opts.Timeout)
|
|
}
|
|
if opts.Wait != 3*time.Minute {
|
|
t.Errorf("default wait is %s; the usage text says 3m", opts.Wait)
|
|
}
|
|
if opts.DryRun || jsonOut || opts.System != "" {
|
|
t.Error("something is on by default that the usage text describes as a flag")
|
|
}
|
|
}
|
|
|
|
// Every flag the usage text promises must exist, and every flag that exists must be in the usage
|
|
// text. The two drifting apart is how a program acquires a feature nobody can find and a
|
|
// documented option that does nothing.
|
|
func TestTheUsageTextAndTheFlagsAgree(t *testing.T) {
|
|
var opts bootstrap.Options
|
|
var jsonOut bool
|
|
set := newFlagSet(&opts, &jsonOut)
|
|
|
|
declared := map[string]bool{}
|
|
set.VisitAll(func(f *flag.Flag) { declared[f.Name] = true })
|
|
|
|
for name := range declared {
|
|
if !strings.Contains(usage, "--"+name) {
|
|
t.Errorf("--%s exists and the usage text does not mention it", name)
|
|
}
|
|
}
|
|
for _, promised := range []string{
|
|
"bundle", "out", "state", "system", "timeout", "wait", "dry-run", "json",
|
|
"catalog", "node", "registry", "host", "host-service", "host-in-background",
|
|
"tools-source", "tools-ref", "catalog-source", "catalog-ref",
|
|
"private-network", "endpoint", "site", "packet-filter", "extras",
|
|
} {
|
|
if !declared[promised] {
|
|
t.Errorf("the usage text promises --%s and no such flag exists", promised)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The pivot's defaults are the documented ones too, and the one that has no default is the one
|
|
// that decides whether the pivot happens at all.
|
|
func TestThePivotsDefaultsAreTheDocumentedOnes(t *testing.T) {
|
|
_, opts, _, err := parseArgs(nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if opts.Registry != defaultRegistry {
|
|
t.Errorf("default registry is %q; the usage text says %q", opts.Registry, defaultRegistry)
|
|
}
|
|
if opts.Host != defaultHost {
|
|
t.Errorf("default host binary is %q; the usage text says %q", opts.Host, defaultHost)
|
|
}
|
|
if opts.HostService != defaultService {
|
|
t.Errorf("default host service is %q; the usage text says %q",
|
|
opts.HostService, defaultService)
|
|
}
|
|
if opts.HostInBackground {
|
|
t.Error("the host is started unsupervised by default, and no real machine should")
|
|
}
|
|
// **No default, deliberately.** A catalogue this installer went looking for on its own would
|
|
// be a checkout somebody else made, at whatever commit they left it on — and it decides which
|
|
// image the mesh's control plane is pinned to for ever after.
|
|
if opts.Catalogue != "" {
|
|
t.Errorf("--catalog defaults to %q; without one the installer stops at the foundation",
|
|
opts.Catalogue)
|
|
}
|
|
// The machine's own name, because that is what a person already calls it.
|
|
if opts.Node == "" {
|
|
t.Error("no default node name; this machine can say what it is called")
|
|
}
|
|
}
|
|
|
|
// --node overrides the machine's own name rather than being ignored because a default was already
|
|
// computed. The name is what the mesh's records, tokens, assignments and pushes all name.
|
|
func TestTheNodeNameCanBeSaid(t *testing.T) {
|
|
_, opts, _, err := parseArgs([]string{"--node", "anchor", "--catalog", "/somewhere"})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if opts.Node != "anchor" {
|
|
t.Errorf("--node anchor parsed as %q", opts.Node)
|
|
}
|
|
if opts.Catalogue != "/somewhere" {
|
|
t.Errorf("--catalog parsed as %q", opts.Catalogue)
|
|
}
|
|
}
|
|
|
|
// Defends novox/hq ADR 0100: the foundation's ports are inputs to genesis, and the registry's port
|
|
// and the address nodes pull from say one port.
|
|
func TestTheFoundationsPortsAreGiven(t *testing.T) {
|
|
_, opts, _, err := parseArgs([]string{"--store-port", "5433", "--hub-port", "51821", "--overlay-range", "10.77.0.0/16"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if opts.Ports.Store != 5433 || opts.Ports.Hub != 51821 || opts.Ports.Bus != 5671 || opts.OverlayRange != "10.77.0.0/16" {
|
|
t.Errorf("ports read as %+v, range %s", opts.Ports, opts.OverlayRange)
|
|
}
|
|
}
|
|
|
|
func TestTheRegistrysPortAndAddressAgree(t *testing.T) {
|
|
_, opts, _, err := parseArgs([]string{"--registry-port", "5100"})
|
|
if err != nil || opts.Registry != "127.0.0.1:5100" {
|
|
t.Errorf("--registry-port alone: %s %v", opts.Registry, err)
|
|
}
|
|
_, opts, _, err = parseArgs([]string{"--registry", "192.0.2.10:5100"})
|
|
if err != nil || opts.Ports.Registry != 5100 {
|
|
t.Errorf("--registry alone: %d %v", opts.Ports.Registry, err)
|
|
}
|
|
if _, _, _, err := parseArgs([]string{"--registry", "192.0.2.10:5000", "--registry-port", "5100"}); err == nil {
|
|
t.Error("two ports for one registry were accepted")
|
|
}
|
|
}
|