163 lines
6.0 KiB
Go
163 lines
6.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Issue 1482 (novox/hq ADR 0466 §3): the provider watches the build modules' state `keys` beside ssh-client's, and
|
|
// registers each builder entry read-only on nox-builder.
|
|
|
|
func keyJSON(node, account, kind, public string) []byte {
|
|
b, _ := json.Marshal(map[string]string{"node": node, "account": account, "kind": kind, "public": public,
|
|
"fingerprint": "SHA256:x", "at": "2026-10-20T10:00:00Z"})
|
|
return b
|
|
}
|
|
|
|
// watched is a team rig whose keys come through watches of the modules' states, as in the running provider.
|
|
type watched struct {
|
|
*teamRig
|
|
sources map[string]*fakeKeys
|
|
passes *int
|
|
logs *[]string
|
|
mu *sync.Mutex
|
|
}
|
|
|
|
func newWatched(t *testing.T) *watched {
|
|
r := newTeamRig(t)
|
|
r.svc.loaded, r.svc.loadedFrom = false, map[string]bool{}
|
|
r.svc.Pause = func(time.Duration) {}
|
|
n, logs, mu := 0, []string{}, &sync.Mutex{}
|
|
r.svc.OnKeys = func(string) { n++ }
|
|
r.svc.cfg.Log = func(f string, a ...any) { mu.Lock(); logs = append(logs, fmt.Sprintf(f, a...)); mu.Unlock() }
|
|
w := &watched{teamRig: r, sources: map[string]*fakeKeys{}, passes: &n, logs: &logs, mu: mu}
|
|
for _, module := range []string{"ssh-client", "mesh-build-go", "mesh-build-node"} {
|
|
w.sources[module] = &fakeKeys{}
|
|
if err := r.svc.WatchSource(context.Background(), module, w.sources[module]); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return w
|
|
}
|
|
|
|
func (w *watched) said() string {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
return strings.Join(*w.logs, "\n")
|
|
}
|
|
|
|
// 1482/AC1: a mesh-build-go.keys entry ace.mesh-build-go of kind builder: its public half is registered read-only on
|
|
// nox-builder titled mesh:ace.mesh-build-go.
|
|
func TestABuilderEntryOfABuildModulesStateIsRegisteredReadOnlyOnNoxBuilder(t *testing.T) {
|
|
w := newWatched(t)
|
|
w.bind("ace", "mesh-build-go")
|
|
w.sources["mesh-build-go"].send("ace.mesh-build-go", "put", keyJSON("ace", "mesh-build-go", "builder", pubGo+" mesh-build-go@ace (mesh)"), true)
|
|
w.pass()
|
|
keys := w.forge.keys["nox-builder"]
|
|
if len(keys) != 1 || keys[0].Title != "mesh:ace.mesh-build-go" || !keys[0].ReadOnly || keys[0].Key != pubGo+" mesh-build-go@ace (mesh)" {
|
|
t.Errorf("nox-builder's keys = %+v", keys)
|
|
}
|
|
if !w.svc.loadedFrom["mesh-build-go"] {
|
|
t.Errorf("the state was watched and its keys are not counted as read")
|
|
}
|
|
}
|
|
|
|
// 1482/AC2: that entry changed: the old key is removed and the new one registered; deleted: the key is removed and no key
|
|
// titled otherwise is touched.
|
|
func TestAChangedBuilderEntryReplacesItsKeyAndADeletedOneRemovesIt(t *testing.T) {
|
|
w := newWatched(t)
|
|
w.bind("ace", "mesh-build-go")
|
|
src := w.sources["mesh-build-go"]
|
|
src.send("ace.mesh-build-go", "put", keyJSON("ace", "mesh-build-go", "builder", pubGo), true)
|
|
w.pass()
|
|
w.forge.mu.Lock()
|
|
w.forge.keys["nox-builder"] = append(w.forge.keys["nox-builder"], forgeKey{ID: 99, Title: "my own", Key: pubOps})
|
|
w.forge.mu.Unlock()
|
|
|
|
before := *w.passes
|
|
src.send("ace.mesh-build-go", "put", keyJSON("ace", "mesh-build-go", "builder", pubNode), false)
|
|
if *w.passes != before+1 {
|
|
t.Errorf("a change asked for %d passes, want 1", *w.passes-before)
|
|
}
|
|
w.pass()
|
|
got := w.forge.keys["nox-builder"]
|
|
if len(got) != 2 || got[0].Title != "my own" || got[1].Title != "mesh:ace.mesh-build-go" || got[1].Key != pubNode || !got[1].ReadOnly {
|
|
t.Errorf("after the change: %+v", got)
|
|
}
|
|
|
|
src.send("ace.mesh-build-go", "delete", nil, false)
|
|
w.pass()
|
|
if got := strings.Join(w.forge.titles("nox-builder"), ","); got != "my own" {
|
|
t.Errorf("after the delete: %s, want only the operator's key", got)
|
|
}
|
|
}
|
|
|
|
// 1482/AC3: an entry of kind machine in a build module's state, or of kind builder in ssh-client's, is ignored and
|
|
// said in the provider's log.
|
|
func TestAKeyOfTheWrongKindForItsStateIsIgnoredAndSaid(t *testing.T) {
|
|
w := newWatched(t)
|
|
w.bind("ace", "mesh-build-go")
|
|
w.bind("g14", "ssh-client")
|
|
w.sources["mesh-build-go"].send("ace.mesh-build-go", "put", keyJSON("ace", "mesh-build-go", "machine", pubGo), true)
|
|
w.sources["ssh-client"].send("g14.nox", "put", keyJSON("g14", "nox", "builder", pubA), true)
|
|
w.pass()
|
|
if len(w.forge.users) != 0 || w.bus.has("ace.mesh-build-go.forge-account") || w.bus.has("g14.ssh-client.forge-account") {
|
|
t.Errorf("a key of the wrong kind was served: users %v", w.forge.users)
|
|
}
|
|
said := w.said()
|
|
for _, want := range []string{`ace.mesh-build-go in the state keys of mesh-build-go is of kind "machine", not "builder"`, `g14.nox in the state keys of ssh-client is of kind "builder", not "machine"`} {
|
|
if !strings.Contains(said, want) {
|
|
t.Errorf("the log lacks %q:\n%s", want, said)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A build module's state that never answers holds back its own entries, never a machine's.
|
|
func TestABuildModulesStateThatNeverAnswersHoldsBackOnlyItsOwnEntries(t *testing.T) {
|
|
w := newWatched(t)
|
|
w.svc.loadedFrom = map[string]bool{} // mesh-build-go's watch has not answered
|
|
w.bind("ace", "mesh-build-go")
|
|
w.bind("g14", "ssh-client")
|
|
w.sources["ssh-client"].send("g14.nox", "put", keyJSON("g14", "nox", "machine", pubA+" nox@g14 (mesh)"), true)
|
|
out := w.pass()
|
|
if !w.bus.has("g14.ssh-client.forge-account") {
|
|
t.Errorf("a machine's entry waited for a build module's state")
|
|
}
|
|
waits := 0
|
|
for _, o := range out {
|
|
if strings.Contains(o.Key, "mesh-build-go") && strings.Contains(o.Why, "keys of mesh-build-go are not read yet") {
|
|
waits++
|
|
}
|
|
}
|
|
if waits != 1 {
|
|
t.Errorf("outcomes = %+v", out)
|
|
}
|
|
}
|
|
|
|
// 1482/AC4 (the provider's half): the manifest reads the states of both build modules beside ssh-client's and the
|
|
// controller's assignments; the provider watches exactly the modules the code names as builders.
|
|
func TestTheManifestReadsTheBuildModulesKeys(t *testing.T) {
|
|
raw, err := os.ReadFile("../../module.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m struct {
|
|
Reads []string `json:"reads"`
|
|
}
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, state := range []string{"mesh-controller.assignments", "ssh-client.keys", "mesh-build-go.keys", "mesh-build-node.keys"} {
|
|
if !slices.Contains(m.Reads, state) {
|
|
t.Errorf("gitea's reads %v lack %q", m.Reads, state)
|
|
}
|
|
}
|
|
}
|