Files
mesh-catalog/modules/mesh-build-node/cmd/mesh-build-node/manifest_test.go
T

194 lines
7.3 KiB
Go

package main
import (
"encoding/json"
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// The catalogue's root and this module's directory.
const (
catalogueRoot = "../../../.."
moduleDir = "../.."
)
func readManifest(t *testing.T) catalogue.Manifest {
t.Helper()
m, err := catalogue.ReadTreeManifest(catalogueRoot, moduleDir)
if err != nil {
t.Fatalf("mesh-sdk's validator refuses module.json: %v", err)
}
return m
}
func resource(t *testing.T, m catalogue.Manifest, id string) map[string]any {
t.Helper()
for _, r := range m.Resources {
if r["id"] == id {
return r
}
}
t.Fatalf("module.json declares no resource %q", id)
return nil
}
// 1488/AC1: mesh-sdk's validator accepts the module (alone on a shelf too), and it runs as mesh-build-node with its
// home at mode 0700, requires user-namespaces and declares the four packages and carries toolchain/package-lock.json.
func TestManifestIsValidAndDeclaresTheAccountTheCapabilityAndThePackages(t *testing.T) {
m := readManifest(t)
if problems := catalogue.CatalogueProblems(catalogue.Shelf{m.Module: m}); len(problems) > 0 {
t.Errorf("the catalogue's rules refuse it: %v", problems)
}
if m.Module != "mesh-build-node" || m.RunsAs != "mesh-build-node" {
t.Errorf("module %q runs as %q, want mesh-build-node as itself", m.Module, m.RunsAs)
}
account := resource(t, m, "account")
if account["type"] != "user" || account["name"] != "mesh-build-node" || account["shell"] != "/usr/bin/nologin" || account["home"] != "/var/lib/mesh-build-node" {
t.Errorf("account = %v", account)
}
home := resource(t, m, "state")
if home["type"] != "directory" || home["mode"] != "0700" || home["owner"] != "mesh-build-node" || home["place"] != "." {
t.Errorf("home = %v, want its directory at mode 0700 owned by the account", home)
}
if !slices.Contains(m.Capabilities, "user-namespaces") {
t.Errorf("capabilities = %v, want user-namespaces", m.Capabilities)
}
packages := []string{}
for _, r := range m.Resources {
if r["type"] == "package" {
packages = append(packages, r["package"].(string))
}
}
slices.Sort(packages)
if want := []string{"git", "nodejs", "npm", "openssh"}; !slices.Equal(packages, want) {
t.Errorf("packages = %v, want %v", packages, want)
}
}
// The work and cache directories are outside the home (ADR 0466 §1), owned by the account.
func TestWorkAndCacheDirectoriesLieOutsideTheHome(t *testing.T) {
m := readManifest(t)
for _, id := range []string{"work", "cache"} {
r := resource(t, m, id)
path, _ := r["path"].(string)
if path == "" || path == "/var/lib/mesh-build-node" || len(path) > len("/var/lib/mesh-build-node") && path[:len("/var/lib/mesh-build-node/")] == "/var/lib/mesh-build-node/" {
t.Errorf("%s is at %q, inside or at the home", id, path)
}
if r["owner"] != "mesh-build-node" {
t.Errorf("%s is owned by %v", id, r["owner"])
}
}
}
// The states are the three the module keeps, no seat is claimed (ADR 0469 §6), and the health is the module's own
// tool, which the manifest also lists among its tools.
func TestManifestKeepsItsStatesClaimsNoSeatAndDeclaresItsHealthTool(t *testing.T) {
m := readManifest(t)
states := []string{}
for _, s := range m.State {
states = append(states, s.Name)
}
slices.Sort(states)
if want := []string{"keys", "steps", "toolchains"}; !slices.Equal(states, want) {
t.Errorf("state = %v, want %v", states, want)
}
if len(m.Claims) != 0 {
t.Errorf("claims = %v, want no seat", m.Claims)
}
if !slices.Contains(m.Tools, HealthTool) {
t.Errorf("tools = %v lack %s", m.Tools, HealthTool)
}
raw, _ := os.ReadFile(filepath.Join(moduleDir, "module.json"))
if !strings.Contains(string(raw), `"tool": "`+HealthTool+`"`) {
t.Errorf("runtime-health does not name %s", HealthTool)
}
}
// Every tool the manifest names is served, and every tool served is named.
func TestTheToolsServedAreTheToolsTheManifestNames(t *testing.T) {
m := readManifest(t)
served := []string{}
for _, tool := range (&Module{}).Tools((&Module{}).StepTools()) {
served = append(served, tool.Name)
}
slices.Sort(served)
named := slices.Clone(m.Tools)
slices.Sort(named)
if !slices.Equal(served, named) {
t.Errorf("served %v, manifest names %v", served, named)
}
}
// 1488/AC1: the module carries the committed lock beside its package.json, and the manifest unpacks them into the
// home's toolchain directory, where the program reads them.
func TestTheModuleCarriesItsToolchainLockAndTheManifestPlacesIt(t *testing.T) {
m := readManifest(t)
for _, name := range []string{"package.json", "package-lock.json"} {
if _, err := os.Stat(filepath.Join(moduleDir, "toolchain", name)); err != nil {
t.Errorf("the module carries no toolchain/%s: %v", name, err)
}
}
if !slices.Contains(m.Requires, "npm-package-registry") {
t.Errorf("requires = %v lack npm-package-registry, which the install resolves @novox from", m.Requires)
}
}
// 1488/AC1: the lock names the versions of the recipe: typescript 5.9.3, esbuild 0.25.12, @novox/mesh-sdk 0.1.14
// and nats, all by the lock's own hash (read from the file the module carries).
func TestTheLockNamesTheToolchainVersions(t *testing.T) {
raw, err := os.ReadFile(filepath.Join(moduleDir, "toolchain", "package-lock.json"))
if err != nil {
t.Fatal(err)
}
var lock struct {
Packages map[string]struct {
Version string `json:"version"`
} `json:"packages"`
}
if err := json.Unmarshal(raw, &lock); err != nil {
t.Fatal(err)
}
for pkg, want := range map[string]string{"typescript": "5.9.3", "esbuild": "0.25.12", "@novox/mesh-sdk": "0.1.14", "nats": "2.29.3", "@types/node": "22.20.1"} {
if got := lock.Packages["node_modules/"+pkg].Version; got != want {
t.Errorf("%s = %q in the lock, want %s", pkg, got, want)
}
}
}
// 1488 (the catalogue's manifest check): a manifest names no host, the mesh being name-agnostic. The tree-read
// manifest carries each template's text as the resource's content, and the toolchain's lock names registries, so it is
// shipped as an archive artifact instead, never as a template. No resource of the module, read as the mesh reads it,
// carries content naming a host, and the raw module.json names none either.
func TestNoResourceCarriesContentNamingAHost(t *testing.T) {
host := regexp.MustCompile(`(?i)://[a-z0-9]|\b[a-z0-9-]+(\.[a-z0-9-]+)*\.(com|org|net|be|io|dev)\b`)
m := readManifest(t)
for i, r := range m.Resources {
if c, _ := r["content"].(string); host.MatchString(c) {
t.Errorf("resources[%d] (%v) carries content naming %q", i, r["id"], host.FindString(c))
}
}
raw, _ := os.ReadFile(filepath.Join(moduleDir, "module.json"))
if h := host.FindString(string(raw)); h != "" {
t.Errorf("module.json names %q", h)
}
}
// 1488: the toolchain's package.json and lock are shipped in the archive artifact `toolchain` and unpacked into the
// home's toolchain directory, where the program reads them.
func TestTheToolchainFilesAreAnArchiveUnpackedIntoTheHome(t *testing.T) {
m := readManifest(t)
r := resource(t, m, "toolchain-dir")
if r["type"] != "archive" || r["path"] != "${dir:state}/toolchain" || r["owner"] != "mesh-build-node" {
t.Errorf("toolchain-dir = %v, want an archive unpacked at ${dir:state}/toolchain", r)
}
if _, err := os.Stat(filepath.Join(moduleDir, "toolchain", "package-lock.json")); err != nil {
t.Errorf("the module carries no toolchain/package-lock.json: %v", err)
}
}