178 lines
5.6 KiB
Go
178 lines
5.6 KiB
Go
package manifest
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The controller's manifest reader is mesh-controller internal/catalogue (ParseManifest), an internal package of another
|
|
// repository, which this module cannot import. These tests therefore decode the manifest strictly into the fields its
|
|
// Manifest type names (DisallowUnknownFields, as its parseManifest does) and check the rules the issue names.
|
|
// **The merge gate's module check is what proves the controller takes this manifest (1135/AC6); these tests hold the
|
|
// rules so a unit run catches a refusal early.**
|
|
|
|
type state struct {
|
|
Name string `json:"name"`
|
|
History int `json:"history,omitempty"`
|
|
KeyTTL bool `json:"key-ttl,omitempty"`
|
|
}
|
|
|
|
type artifact struct {
|
|
Name string `json:"name"`
|
|
Kind string `json:"kind"`
|
|
Language string `json:"language"`
|
|
System string `json:"system"`
|
|
From string `json:"from"`
|
|
Binary string `json:"binary"`
|
|
Context *struct {
|
|
Repository string `json:"repository"`
|
|
} `json:"context"`
|
|
}
|
|
|
|
type manifest struct {
|
|
Module string `json:"module"`
|
|
Moved bool `json:"moved"`
|
|
Version string `json:"version"`
|
|
Slug string `json:"slug"`
|
|
Reads []string `json:"reads"`
|
|
State []state `json:"state"`
|
|
OwnSecrets map[string]string `json:"own-secrets"`
|
|
SecretsOwner string `json:"secrets-owner"`
|
|
Resources []map[string]any `json:"resources"`
|
|
Build struct {
|
|
Artifacts []artifact `json:"artifacts"`
|
|
} `json:"build"`
|
|
}
|
|
|
|
func load(t *testing.T) (manifest, map[string]json.RawMessage) {
|
|
t.Helper()
|
|
raw, err := os.ReadFile("module.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m manifest
|
|
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(&m); err != nil {
|
|
t.Fatalf("the controller's reader refuses a key this manifest has: %v", err)
|
|
}
|
|
var top map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &top); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return m, top
|
|
}
|
|
|
|
func TestManifestStateAndReads(t *testing.T) { // 1135/AC6
|
|
m, top := load(t)
|
|
if m.Module != "mesh-delivery-watchman" || m.Moved || m.Slug != "watchman" {
|
|
t.Errorf("module %q moved %v (it has no template, so it is not moved) slug %q", m.Module, m.Moved, m.Slug)
|
|
}
|
|
want := []state{{Name: "presence", History: 20}, {Name: "pulse", KeyTTL: true}, {Name: "stuck", History: 20}}
|
|
if !reflect.DeepEqual(m.State, want) {
|
|
t.Errorf("state = %+v, want %+v", m.State, want)
|
|
}
|
|
reads := slices.Clone(m.Reads)
|
|
slices.Sort(reads)
|
|
if !reflect.DeepEqual(reads, []string{"mesh-controller.checks-running"}) {
|
|
t.Errorf("reads = %v", reads)
|
|
}
|
|
for _, k := range []string{"emits", "context", "items", "consumes", "provides", "claims"} {
|
|
if _, ok := top[k]; ok {
|
|
t.Errorf("the manifest declares %q: the watchman has none", k)
|
|
}
|
|
}
|
|
if b, ok := m.OwnSecrets["broker"]; !ok || len(m.OwnSecrets) != 1 || b != "${dir:state}/broker" {
|
|
t.Errorf("own-secrets = %v, want exactly broker", m.OwnSecrets)
|
|
}
|
|
if m.SecretsOwner != "mesh-delivery-watchman" {
|
|
t.Errorf("secrets-owner = %q", m.SecretsOwner)
|
|
}
|
|
}
|
|
|
|
func TestManifestIsWholeInItsRepository(t *testing.T) { // 1135/AC6
|
|
m, _ := load(t)
|
|
if len(m.Build.Artifacts) != 1 {
|
|
t.Fatalf("artifacts = %+v, want the one bundle program", m.Build.Artifacts)
|
|
}
|
|
a := m.Build.Artifacts[0]
|
|
if a.Name != "program" || a.Kind != "bundle" || a.Language != "go" || a.System != "arch" || a.Context != nil {
|
|
t.Errorf("artifact = %+v", a)
|
|
}
|
|
if st, err := os.Stat(a.From); err != nil || !st.IsDir() {
|
|
t.Errorf("the artifact builds from %q, which is not a directory of this module", a.From)
|
|
}
|
|
if a.Binary != "mesh-delivery-watchman" || !strings.HasSuffix(a.From, a.Binary) {
|
|
t.Errorf("binary %q from %q", a.Binary, a.From)
|
|
}
|
|
}
|
|
|
|
func TestManifestResources(t *testing.T) { // 1135/AC6
|
|
m, _ := load(t)
|
|
byType := map[string][]map[string]any{}
|
|
for _, r := range m.Resources {
|
|
ty, _ := r["type"].(string)
|
|
byType[ty] = append(byType[ty], r)
|
|
}
|
|
for _, ty := range []string{"user", "directory", "process"} {
|
|
if len(byType[ty]) == 0 {
|
|
t.Fatalf("no %s resource", ty)
|
|
}
|
|
}
|
|
user := byType["user"][0]["name"]
|
|
if user != "mesh-delivery-watchman" {
|
|
t.Errorf("user = %v", user)
|
|
}
|
|
p := byType["process"][0]
|
|
if p["user"] != user || p["artifact"] != "program" {
|
|
t.Errorf("process = %v", p)
|
|
}
|
|
env, _ := p["env"].(map[string]any)
|
|
if env["MESH_BROKER_FILE"] != "${dir:state}/broker" {
|
|
t.Errorf("the process reads %v", env["MESH_BROKER_FILE"])
|
|
}
|
|
if h, _ := p["health"].(map[string]any); h["kind"] != "unit" {
|
|
t.Errorf("health = %v", p["health"])
|
|
}
|
|
d := byType["directory"][0]
|
|
if d["id"] != "state" || d["mode"] != "0700" || d["owner"] != user {
|
|
t.Errorf("state directory = %v", d)
|
|
}
|
|
}
|
|
|
|
func TestNoTickerTimerOrSleep(t *testing.T) { // 1135/AC7
|
|
forbidden := []string{"time.Tick", "time.NewTicker", "time.Sleep", "time.After", "time.NewTimer", "time.AfterFunc"}
|
|
var files int
|
|
for _, root := range []string{"cmd", "internal"} {
|
|
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil || d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
|
return err
|
|
}
|
|
files++
|
|
src, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, f := range forbidden {
|
|
if strings.Contains(string(src), f) {
|
|
t.Errorf("%s holds %s: the program holds no timer", path, f)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if files < 3 {
|
|
t.Fatalf("read %d source files, want the program's", files)
|
|
}
|
|
}
|