Files
mesh-delivery/modules/mesh-delivery-view/internal/view/serve_test.go
T
jschoubben 0de534c692 Names N3 (view): the status view names its own buckets from its credential's module
Its states answers, reach and waits (viewstate WaitsBucket, ReachBucket, ViewAnswersBucket), its
first views and its marks were named from the constant ViewModule; they are now
`<cred.Module>_<state>` (viewstate.BucketsOf), and the model is made for its module.

Builds: design 47 G A-296

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 19:14:55 +01:00

155 lines
4.7 KiB
Go

package view
import (
"bufio"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
)
func served(t *testing.T) (string, *Model) {
t.Helper()
m := New(viewstate.ViewModule)
fill(m, base(t)...)
fill(m, haveE(t, "a", "w", have(t, "b9", nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, ""), t0))
dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "s")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { os.RemoveAll(dir) })
path := filepath.Join(dir, "run", "cli.sock")
l, err := ListenSocket(path)
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() { ServeSocket(ctx, l, m); close(done) }()
t.Cleanup(func() { cancel(); l.Close(); <-done })
return path, m
}
func TestSocketAnswersRows(t *testing.T) { // 1134/AC6
path, m := served(t)
got, err := Ask(path)
if err != nil {
t.Fatal(err)
}
want := m.Snapshot()
b1, _ := json.Marshal(got)
b2, _ := json.Marshal(want)
if len(want.Rows) == 0 || string(b1) != string(b2) {
t.Fatalf("socket answered %s, want %s", b1, b2)
}
st, err := os.Stat(path)
if err != nil || st.Mode().Perm() != 0o666 {
t.Fatalf("socket mode = %v, %v; want 0666", st.Mode().Perm(), err)
}
dst, _ := os.Stat(filepath.Dir(path))
if dst.Mode().Perm() != 0o755 {
t.Fatalf("directory mode = %v, want 0755", dst.Mode().Perm())
}
}
func TestSocketRefusesAnyOtherAsk(t *testing.T) { // 1134/AC6
path, m := served(t)
before, _ := json.Marshal(m.Snapshot())
for _, ask := range []string{`{"ask":"start","module":"x"}`, `{"ask":"retry"}`, `{"ask":""}`, `not json`, `{}`} {
c, err := dial(path)
if err != nil {
t.Fatal(err)
}
c.Write([]byte(ask + "\n"))
line, err := bufio.NewReader(c).ReadString('\n')
c.Close()
if err != nil || !strings.Contains(line, `"error"`) || strings.Contains(line, `"rows"`) {
t.Errorf("ask %q answered %q, %v; want a refusal", ask, line, err)
}
}
after, _ := json.Marshal(m.Snapshot())
if string(before) != string(after) {
t.Fatal("a refused ask changed the rows")
}
}
func TestTableShowsEveryRow(t *testing.T) { // 1134/AC6
_, m := served(t)
var out strings.Builder
s := m.Snapshot()
Table(&out, s)
for _, r := range s.Rows {
if !strings.Contains(out.String(), r.Module) {
t.Fatalf("table lacks %s:\n%s", r.Module, out.String())
}
}
for _, w := range []string{"NODE", "MODULE", "ASSIGNED", "HAVE", "PHASE", "HEALTH", "SINCE", "WAITS", "no have yet", "left behind"} {
if !strings.Contains(out.String(), w) {
t.Errorf("table lacks %q:\n%s", w, out.String())
}
}
}
func TestWebIsTheSameRowsAndReadOnly(t *testing.T) { // 1134/AC6
_, m := served(t)
h := Web(m)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
if rec.Code != 200 || !strings.Contains(rec.Body.String(), "no have yet") || !strings.Contains(rec.Header().Get("Content-Type"), "text/html") {
t.Fatalf("web = %d %q", rec.Code, rec.Body.String())
}
rec = httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/", strings.NewReader("x")))
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("POST = %d, want 405", rec.Code)
}
}
func watchmanSnapshot(t *testing.T) *Model {
t.Helper()
m := New(viewstate.ViewModule)
fill(m, presenceE(t, "a", false, t0), stuckE(t, "node-engine_have", "a.x", "a report from a", t0, false),
Entry{Bucket: PresenceBucket, Key: "c", Value: []byte(`{"v":9}`), At: t0})
return m
}
func TestTableAndWebShowTheWatchmansLists(t *testing.T) { // 1168/AC4
m := watchmanSnapshot(t)
var out strings.Builder
Table(&out, m.Snapshot())
rec := httptest.NewRecorder()
Web(m).ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
for name, text := range map[string]string{"table": out.String(), "web": rec.Body.String()} {
for _, w := range []string{"OFFLINE", "STUCK", "UNREADABLE", "node-engine_have", "a.x", "a report from a", "offline", "c"} {
if !strings.Contains(text, w) {
t.Errorf("%s lacks %q:\n%s", name, w, text)
}
}
}
}
func TestSocketCarriesTheWatchmansLists(t *testing.T) { // 1168/AC4
m := watchmanSnapshot(t)
dir, _ := os.MkdirTemp(os.Getenv("TMPDIR"), "w")
t.Cleanup(func() { os.RemoveAll(dir) })
path := filepath.Join(dir, "cli.sock")
l, err := ListenSocket(path)
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
go ServeSocket(ctx, l, m)
got, err := Ask(path)
if err != nil || len(got.Offline) != 1 || len(got.Stuck) != 1 || len(got.Unreadable) != 1 {
t.Fatalf("socket = %+v, %v", got, err)
}
}