Test the forge account provider's builder entries, and add the Go build module's manifest, ssh files and README (issues 1481, 1483), part 6 of 17

This commit is contained in:
2026-10-20 20:48:52 +00:00
parent fb52442b50
commit 298a0b8eda
7 changed files with 558 additions and 0 deletions
@@ -0,0 +1,356 @@
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"testing"
"git.novox.be/novox/mesh-sdk/go/provider"
"gitea/internal/forgehttp"
)
// Issue 1481 (novox/hq ADR 0466 §3): the build modules' keys on one forge user that reads and pushes nothing. The forge
// is faked with the teams, members and keys it keeps, so a team's shape and a key's right can be read back.
type forgeKey struct {
ID int64
Title string
Key string
ReadOnly bool
}
type teamForge struct {
mu sync.Mutex
users map[string]bool
teams map[string]map[string]any // by name: the shape last sent
ids map[string]int64
members map[string][]string // by team name
keys map[string][]forgeKey
next int64
}
func newTeamForge() *teamForge {
return &teamForge{users: map[string]bool{}, teams: map[string]map[string]any{}, ids: map[string]int64{},
members: map[string][]string{}, keys: map[string][]forgeKey{}, next: 10}
}
var (
teamRe = regexp.MustCompile(`^/api/v1/teams/(\d+)(?:/members/([^/]+))?$`)
userKeyRe = regexp.MustCompile(`^/api/v1/(?:admin/)?users/([^/]+)/keys(?:/(\d+))?$`)
)
func (f *teamForge) teamByID(id string) string {
for name, n := range f.ids {
if fmt.Sprint(n) == id {
return name
}
}
return ""
}
func (f *teamForge) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f.mu.Lock()
defer f.mu.Unlock()
body, _ := io.ReadAll(r.Body)
var in map[string]any
_ = json.Unmarshal(body, &in)
reply := func(code int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
if v != nil {
_ = json.NewEncoder(w).Encode(v)
}
}
p := r.URL.Path
if m := userKeyRe.FindStringSubmatch(p); m != nil {
switch r.Method {
case http.MethodGet:
out := []map[string]any{}
for _, k := range f.keys[m[1]] {
out = append(out, map[string]any{"id": k.ID, "title": k.Title, "key": k.Key})
}
reply(200, out)
case http.MethodPost:
f.next++
ro, _ := in["read_only"].(bool)
f.keys[m[1]] = append(f.keys[m[1]], forgeKey{ID: f.next, Title: in["title"].(string), Key: in["key"].(string), ReadOnly: ro})
reply(201, map[string]any{"id": f.next})
case http.MethodDelete:
var kept []forgeKey
for _, k := range f.keys[m[1]] {
if fmt.Sprint(k.ID) != m[2] {
kept = append(kept, k)
}
}
f.keys[m[1]] = kept
reply(204, nil)
}
return
}
if m := teamRe.FindStringSubmatch(p); m != nil {
name := f.teamByID(m[1])
switch {
case name == "":
reply(404, map[string]any{"message": "no team"})
case r.Method == http.MethodPatch && m[2] == "":
f.teams[name] = in
reply(200, map[string]any{})
case r.Method == http.MethodPut:
for _, u := range f.members[name] {
if u == m[2] {
reply(204, nil)
return
}
}
f.members[name] = append(f.members[name], m[2])
reply(204, nil)
default:
http.Error(w, "unexpected "+r.Method+" "+p, 400)
}
return
}
switch {
case r.Method == http.MethodGet && strings.HasPrefix(p, "/api/v1/users/"):
if !f.users[strings.TrimPrefix(p, "/api/v1/users/")] {
reply(404, map[string]any{"message": "no"})
return
}
reply(200, map[string]any{"active": true})
case r.Method == http.MethodPost && p == "/api/v1/admin/users":
f.users[in["username"].(string)] = true
reply(201, map[string]any{})
case p == "/api/v1/orgs/novox/teams" && r.Method == http.MethodGet:
out := []map[string]any{}
for name, id := range f.ids {
out = append(out, map[string]any{"id": id, "name": name})
}
reply(200, out)
case p == "/api/v1/orgs/novox/teams" && r.Method == http.MethodPost:
f.next++
name := in["name"].(string)
f.ids[name], f.teams[name] = f.next, in
reply(201, map[string]any{"id": f.next})
default:
http.Error(w, "unexpected "+r.Method+" "+p, 400)
}
}
func (f *teamForge) titles(user string) []string {
f.mu.Lock()
defer f.mu.Unlock()
out := []string{}
for _, k := range f.keys[user] {
out = append(out, k.Title)
}
return out
}
type teamRig struct {
t *testing.T
bus *fakeBus
forge *teamForge
svc *Service
runner *fakeRunner
}
func newTeamRig(t *testing.T) *teamRig {
t.Helper()
forge := newTeamForge()
srv := httptest.NewServer(forge)
t.Cleanup(srv.Close)
dir := t.TempDir()
pw, host, origins := filepath.Join(dir, "pw"), filepath.Join(dir, "host"), filepath.Join(dir, "origins")
for p, c := range map[string]string{pw: "x\n", host: "novox.internal\n", origins: "https://git.novox.be\n"} {
if err := os.WriteFile(p, []byte(c), 0o600); err != nil {
t.Fatal(err)
}
}
runner := &fakeRunner{out: scanned}
svc := NewService(Config{Node: "novox", HostFile: host, OriginsFile: origins, Port: 222,
Forge: forgehttp.Client{URL: srv.URL, User: "mesh-admin", PasswordFile: pw}, Runner: runner, Log: func(string, ...any) {}})
svc.loaded = true
svc.loadedFrom["mesh-build-go"], svc.loadedFrom["mesh-build-node"] = true, true
return &teamRig{t: t, bus: newBus(), forge: forge, svc: svc, runner: runner}
}
func (r *teamRig) pass() []provider.Outcome {
r.t.Helper()
out, err := r.svc.Provider(r.bus).Pass(context.Background())
if err != nil {
r.t.Fatal(err)
}
return out
}
// bind makes module on node bind gitea's forge-account in the assignments mirror.
func (r *teamRig) bind(node, module string) {
r.bus.set(provider.AssignmentsBucket, node+"."+module, []byte(fmt.Sprintf(
`{"node":%q,"module":%q,"provisioning":{"provisions":[{"provision":"forge-account","provider":"novox","provider-module":"gitea","as":"mesh_%s"}]}}`,
node, module, strings.ReplaceAll(node+"_"+module, "-", "_"))))
}
func (r *teamRig) unbind(node, module string) {
r.bus.mu.Lock()
delete(r.bus.buckets[provider.AssignmentsBucket], node+"."+module)
r.bus.mu.Unlock()
}
// key reports a key in module's state keys, of the kind and for the account given.
func (r *teamRig) key(module, node, account, kind, public string) {
b, _ := json.Marshal(map[string]string{"node": node, "account": account, "kind": kind, "public": public,
"fingerprint": "SHA256:x", "at": "2026-10-20T10:00:00Z"})
r.svc.SourceKeyChanged(module, node+"."+account, b)
}
const (
pubGo = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG"
pubNode = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN"
pubOps = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO"
)
// 1481/AC1: an entry from mesh-build-go on ace: the forge user nox-builder and the team builders are ensured, the team's
// units are code, read only, on every repository, and the entry's values name nox-builder. The key is registered
// read-only under its title.
func TestABuildModulesEntryIsServedAsNoxBuilderInAReadOnlyTeam(t *testing.T) {
r := newTeamRig(t)
r.bind("ace", "mesh-build-go")
r.key("mesh-build-go", "ace", "mesh-build-go", "builder", pubGo+" mesh-build-go@ace (mesh)")
r.pass()
if !r.forge.users["nox-builder"] || r.forge.users["nox-agent"] {
t.Errorf("users = %v, want nox-builder alone", r.forge.users)
}
shape := r.forge.teams["builders"]
if shape == nil {
t.Fatalf("no team builders: %v", r.forge.teams)
}
units, _ := json.Marshal(shape["units_map"])
if shape["permission"] != "read" || shape["includes_all_repositories"] != true || shape["can_create_org_repo"] != false || string(units) != `{"repo.code":"read"}` {
t.Errorf("the team's shape = %v (units %s), want code read on every repository and no other right", shape, units)
}
if got := strings.Join(r.forge.members["builders"], ","); got != "nox-builder" || len(r.forge.members["agents"]) != 0 {
t.Errorf("members = %v", r.forge.members)
}
keys := r.forge.keys["nox-builder"]
if len(keys) != 1 || keys[0].Title != "mesh:ace.mesh-build-go" || !keys[0].ReadOnly {
t.Errorf("nox-builder's keys = %+v, want one read-only key titled mesh:ace.mesh-build-go", keys)
}
e := r.bus.entry(t, "ace.mesh-build-go.forge-account")
if e.Values["user"] != "nox-builder" || e.Values["ssh_alias"] != "mesh-forge" || e.Values["host_key"] != "ssh-ed25519 AAAAC3HOSTKEY" {
t.Errorf("values = %v", e.Values)
}
}
// 1481/AC2: an entry from ssh-client is what it was: nox-agent, the agents team, a key that may write. (The existing
// tests of the provider pass unchanged.)
func TestAnSshClientEntryIsStillTheAgentsWithAKeyThatMayWrite(t *testing.T) {
r := newTeamRig(t)
// the agents team exists, as the forge has it
r.forge.ids["agents"], r.forge.teams["agents"] = 5, map[string]any{}
r.bind("g14", "ssh-client")
r.key("ssh-client", "g14", "nox", "machine", pubA+" nox@g14 (mesh)")
r.pass()
if e := r.bus.entry(t, "g14.ssh-client.forge-account"); e.Values["user"] != "nox-agent" {
t.Errorf("values = %v", e.Values)
}
keys := r.forge.keys["nox-agent"]
if len(keys) != 1 || keys[0].Title != "mesh:g14.nox" || keys[0].ReadOnly {
t.Errorf("nox-agent's keys = %+v, want one key that may write", keys)
}
if r.forge.teams["builders"] != nil || r.forge.users["nox-builder"] {
t.Errorf("a machine's entry made the builders' account or team")
}
if got := strings.Join(r.forge.members["agents"], ","); got != "nox-agent" {
t.Errorf("agents = %v", r.forge.members)
}
}
// 1481/AC3: a builder entry gone: only the keys titled mesh:<node>.mesh-build-go are removed; the user and the team
// stay, and so do the other build module's key on that machine and a key of the operator's.
func TestABuilderEntryGoneRemovesItsOwnKeyAndNothingElse(t *testing.T) {
r := newTeamRig(t)
r.bind("ace", "mesh-build-go")
r.bind("ace", "mesh-build-node")
r.key("mesh-build-go", "ace", "mesh-build-go", "builder", pubGo+" mesh-build-go@ace (mesh)")
r.key("mesh-build-node", "ace", "mesh-build-node", "builder", pubNode+" mesh-build-node@ace (mesh)")
r.pass()
r.forge.mu.Lock()
r.forge.keys["nox-builder"] = append(r.forge.keys["nox-builder"], forgeKey{ID: 99, Title: "my own", Key: pubOps})
r.forge.mu.Unlock()
if got := strings.Join(r.forge.titles("nox-builder"), ","); got != "mesh:ace.mesh-build-go,mesh:ace.mesh-build-node,my own" {
t.Fatalf("before: %s", got)
}
r.unbind("ace", "mesh-build-go")
r.pass()
if got := strings.Join(r.forge.titles("nox-builder"), ","); got != "mesh:ace.mesh-build-node,my own" {
t.Errorf("after: %s, want only the go module's key gone", got)
}
if !r.forge.users["nox-builder"] || r.forge.teams["builders"] == nil || len(r.forge.members["builders"]) != 1 {
t.Errorf("the user or the team went: users %v, members %v", r.forge.users, r.forge.members)
}
if r.bus.has("ace.mesh-build-go.forge-account") {
t.Errorf("the entry stayed")
}
}
// A build module and ssh-client on one machine each have their own entry, keys, user and team.
func TestABuildModuleAndSshClientOnOneMachineDoNotShareAnything(t *testing.T) {
r := newTeamRig(t)
r.forge.ids["agents"], r.forge.teams["agents"] = 5, map[string]any{}
r.bind("ace", "ssh-client")
r.bind("ace", "mesh-build-go")
r.key("ssh-client", "ace", "nox", "machine", pubA+" nox@ace (mesh)")
r.key("mesh-build-go", "ace", "mesh-build-go", "builder", pubGo+" mesh-build-go@ace (mesh)")
r.pass()
if got := strings.Join(r.forge.titles("nox-agent"), ","); got != "mesh:ace.nox" {
t.Errorf("nox-agent: %s", got)
}
if got := strings.Join(r.forge.titles("nox-builder"), ","); got != "mesh:ace.mesh-build-go" {
t.Errorf("nox-builder: %s", got)
}
// ssh-client's entry gone: nox-builder's key stays.
r.unbind("ace", "ssh-client")
r.pass()
if got := strings.Join(r.forge.titles("nox-builder"), ","); got != "mesh:ace.mesh-build-go" {
t.Errorf("nox-builder after ssh-client left: %s", got)
}
if got := strings.Join(r.forge.titles("nox-agent"), ","); got != "" {
t.Errorf("nox-agent after ssh-client left: %s", got)
}
}
// A build module's entry waits for its own module's builder key: a machine key of the same node, a key of the wrong
// kind in the build module's state, or the other build module's key is none.
func TestABuildModulesEntryWaitsForItsOwnBuilderKey(t *testing.T) {
r := newTeamRig(t)
r.bind("ace", "mesh-build-go")
r.key("ssh-client", "ace", "nox", "machine", pubA) // a machine's key
r.key("mesh-build-go", "ace", "mesh-build-go", "machine", pubGo) // the wrong kind in the build module's state
r.key("mesh-build-node", "ace", "mesh-build-node", "builder", pubNode) // the other build module's
out := r.pass()
if r.bus.has("ace.mesh-build-go.forge-account") || len(r.forge.users) != 0 || len(r.forge.teams) != 0 {
t.Fatalf("something was made without a builder key: users %v teams %v", r.forge.users, r.forge.teams)
}
if len(out) != 1 || !strings.Contains(out[0].Why, "mesh-build-go") || !strings.Contains(out[0].Why, "builder key") {
t.Errorf("the wait should name the module and the key: %+v", out)
}
}
func TestTheKindOfAModulesKeyAndItsForgeUser(t *testing.T) {
for module, want := range map[string]string{"mesh-build-go": "nox-builder", "mesh-build-node": "nox-builder", "ssh-client": "nox-agent", "anything-else": "nox-agent"} {
got, err := (&Service{}).valuesForUser("ace", module)
if err != nil || got != want {
t.Errorf("%s: %q, %v; want %q", module, got, err, want)
}
}
}