194 lines
7.3 KiB
Go
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)
|
|
}
|
|
}
|