Files
privatae-bastion/client-go/pending_visible_test.go
T
Privatae 48899b41a9
ci / personal-data (push) Successful in 4s
ci / test (push) Successful in 4m58s
ci / fuzz (push) Successful in 3m33s
bastion 0.63.1
2026-11-03 22:56:18 +00:00

206 lines
7.2 KiB
Go

//go:build !windows
package main
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// anchoredWithPending is the S0 machine: anchored to a Host on this machine,
// with a remote Host that redeemed its code and waits.
func anchoredWithPending(t *testing.T) {
t.Helper()
localBox(t)
writeTCPPairing(t, tLocal, "127.0.0.1:8811", "")
if err := writeAnchor("the owner", tLocal); err != nil {
t.Fatal(err)
}
pd, _ := pairingsDir()
b, _ := json.Marshal(Config{HostName: tRemote, Transport: transportTCP, LaneAddr: "10.0.2.2:28811", WGPrivateKey: "k",
State: pairingPending, ListenAddr: "127.0.0.1:7879", Origin: pairingOriginCarried})
if err := os.WriteFile(filepath.Join(pd, tRemote+".json"), b, 0o600); err != nil {
t.Fatal(err)
}
}
// `pair` on an anchored machine says the code was used, that the Host waits
// for the anchor to admit it, and that the anchor asks the person.
//
// MUTATION: restore the 0.62 text ("a new host waits until the anchor host
// approves it"). RESULT: red.
func TestPairOnAnAnchoredMachineSaysWhoAdmitsTheHost(t *testing.T) {
anchoredWithPending(t)
cfg, all := pairingNamed(t, tRemote)
out := pairReportOf(cfg, all, false)
for _, want := range []string{
"Paired with " + tRemote + " — WAITING for " + tLocal + " to admit it.",
"The code was used",
tLocal + " asks you to approve " + tRemote,
"served at once, with no restart",
} {
if !strings.Contains(out, want) {
t.Errorf("pair does not say %q:\n%s", want, out)
}
}
if strings.Contains(out, "not answering") || strings.Contains(out, "privatae-bastion run") {
t.Errorf("pair names a dead end:\n%s", out)
}
}
// `pair` never tells the person to start a bastion that is running: the
// running one serves the new pairing by itself. With none running it names
// this machine's own start command.
//
// MUTATION: restore "Start (or restart) the bastion:\n privatae-bastion run".
// RESULT: red.
func TestPairNeverSaysRunWhenABastionServesTheMachine(t *testing.T) {
localBox(t)
writeTCPPairing(t, tLocal, "127.0.0.1:8811", "")
cfg, all := pairingNamed(t, tLocal)
prev := pairDaemonRunning
t.Cleanup(func() { pairDaemonRunning = prev })
pairDaemonRunning = func() bool { return true }
out := pairReportOf(cfg, all, false)
if strings.Contains(out, "privatae-bastion run") || !strings.Contains(out, "serves it within 15 seconds; nothing needs restarting") {
t.Errorf("with a bastion running:\n%s", out)
}
pairDaemonRunning = func() bool { return false }
out = pairReportOf(cfg, all, false)
if !strings.Contains(out, "Start the bastion so it serves it:\n "+runCommandHint()) {
t.Errorf("with none running:\n%s", out)
}
}
// `status` and `status --json` say a pending Host waits for the anchor to
// admit it, never that it is not answering, and the machine is RUNNING when
// every Host it may serve is served.
//
// MUTATION: count pending pairings as served in readStatus. RESULT: red
// (PARTLY RUNNING). Drop WaitingFor. RESULT: red.
func TestStatusSaysAPendingHostWaitsForTheAnchor(t *testing.T) {
anchoredWithPending(t)
s, err := readStatus(readOpts{})
if err != nil {
t.Fatal(err)
}
var pend *statusPairing
for i := range s.Pairings {
if s.Pairings[i].Host == tRemote {
pend = &s.Pairings[i]
}
}
if pend == nil || pend.WaitingFor != tLocal {
t.Fatalf("status --json does not say who admits it: %+v", s.Pairings)
}
raw, _ := json.Marshal(s)
if !strings.Contains(string(raw), `"waiting_for":"`+tLocal+`"`) {
t.Errorf("the JSON lacks waiting_for: %s", raw)
}
// As the daemon reads it, with the anchor's own lane answering.
s.Running = "yes"
for i := range s.Pairings {
if s.Pairings[i].Host == tLocal {
s.Pairings[i].Answering = true
}
}
var b strings.Builder
printStatus(&b, s)
out := b.String()
if !strings.HasPrefix(out, "RUNNING — serving 1 host(s)") {
t.Errorf("headline:\n%s", out)
}
if !strings.Contains(out, "Waiting for "+tLocal+" to admit it: "+tRemote) ||
!strings.Contains(out, "(waiting for "+tLocal+" to admit it)") {
t.Errorf("status does not say who admits it:\n%s", out)
}
if strings.Contains(out, "not answering") {
t.Errorf("a pending Host reads as not answering:\n%s", out)
}
}
// The console says the same, and the page shows it in place of "not answering".
//
// MUTATION: drop the WaitingFor branch in status.html. RESULT: red.
func TestTheConsoleSaysAPendingHostWaitsForTheAnchor(t *testing.T) {
anchoredWithPending(t)
s := collectUIStatus()
if strings.Contains(s.StateNote, "not answering") || !strings.Contains(s.StateNote, "wait(s) for "+tLocal+" to admit them") {
t.Errorf("state %q: %q", s.State, s.StateNote)
}
found := false
for _, p := range s.Pairings {
found = found || (p.Host == tRemote && p.WaitingFor == tLocal)
}
if !found {
t.Errorf("pairings %+v", s.Pairings)
}
page, err := os.ReadFile(filepath.Join("uiassets", "status.html"))
if err != nil {
t.Fatal(err)
}
i, j := strings.Index(string(page), "{{if .WaitingFor}}<span class=\"tag warn\">not served until"), strings.Index(string(page), "not answering</span>")
if i < 0 || j < 0 || i > j {
t.Error("the page shows a pending Host as not answering")
}
if line := trayStateLine(s); !strings.Contains(line, "waiting for "+tLocal) {
t.Errorf("tray: %q", line)
}
}
// The anchor's heartbeat carries each pending pairing with what the anchor
// Host needs for the approval prompt: who admits it, how it would run, where
// its Host is, and how it came.
//
// MUTATION: drop the pending fields in pairingsReport. RESULT: red.
func TestTheAnchorsBeatCarriesWhatItNeedsToAskThePerson(t *testing.T) {
anchoredWithPending(t)
var e *pairingEntry
for _, p := range pairingsReport() {
if p.HostName == tRemote {
p := p
e = &p
}
}
if e == nil || e.State != pairingPending || e.WaitingFor != tLocal || e.Transport != "tcp" || e.SameMachine ||
e.Origin != pairingOriginCarried || e.Handle != handleOf(tRemote) {
t.Fatalf("%+v", e)
}
for _, p := range pairingsReport() {
if p.HostName == tLocal && (p.WaitingFor != "" || p.Origin != "") {
t.Errorf("an active pairing carries the pending fields: %+v", p)
}
}
// The unapproved list keeps its four fields.
raw, _ := json.Marshal(unapprovedAttempts(time.Now().Add(time.Hour)))
if strings.Contains(string(raw), "waiting_for") {
t.Errorf("unapproved attempts grew fields: %s", raw)
}
}
// The installer's last words come from what the service holds: what it serves
// and what waits, before the "no Host is needed" line it said whatever the
// service served.
//
// MUTATION: drop the status --json read. RESULT: red.
func TestTheInstallerSaysWhatTheServiceServesAndWhatWaits(t *testing.T) {
raw, err := os.ReadFile(filepath.Join("..", "deploy", "install.ps1"))
if err != nil {
t.Fatal(err)
}
s := string(raw)
read := strings.Index(s, "$st = (& $exe status --json 2>$null | Out-String) | ConvertFrom-Json")
waits := strings.Index(s, "waits for $($p.waiting_for) to admit it")
none := strings.Index(s, "no Host is needed.")
if read < 0 || waits < 0 || none < 0 || !(read < waits && waits < none) {
t.Error("the installer's end text does not come from what the service serves")
}
if !strings.Contains(s, "if ($served -or $waits) {") {
t.Error("the 'no Host is needed' line is not kept for a service that serves none")
}
}