172 lines
6.5 KiB
Go
172 lines
6.5 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// 1124/AC1: the manifest declares `repositories` under config as a trusted object value with an empty default, as
|
|
// it declares agent-read-teams (ADR 0365 §3, ADR 0348), renders it into a file of its own, and runs this program
|
|
// at the module's start and whenever that file changes, as a run-once process (1124/AC6: its failed exit is the
|
|
// node-engine's gate, ADR 0136) declared last, so that gate stops nothing of the module after it.
|
|
func TestManifestDeclaresTheValueAndItsTrigger(t *testing.T) {
|
|
raw, err := os.ReadFile("../../module.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m struct {
|
|
Config map[string]map[string]any `json:"config"`
|
|
Resources []map[string]any `json:"resources"`
|
|
Build struct {
|
|
Artifacts []map[string]any `json:"artifacts"`
|
|
} `json:"build"`
|
|
}
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
v := m.Config[ConfigKey]
|
|
if v == nil || v["type"] != "object" || v["trusted"] != true {
|
|
t.Fatalf("config %s = %v, want a trusted object", ConfigKey, v)
|
|
}
|
|
if d, ok := v["default"].(map[string]any); !ok || len(d) != 0 {
|
|
t.Fatalf("its default is %v, want {}", v["default"])
|
|
}
|
|
var file, process map[string]any
|
|
for _, r := range m.Resources {
|
|
if r["id"] == "repositories-file" {
|
|
file = r
|
|
}
|
|
}
|
|
if file == nil || file["type"] != "file" {
|
|
t.Fatalf("no file repositories-file")
|
|
}
|
|
// 1128/AC1: a plain template the provisioner client fills, so an approved value lands at once: a file the
|
|
// controller merges (merge, takes-config) takes a config change only on the node's next send (ADR 0348 §10).
|
|
if file["merge"] != nil || file["takes-config"] != nil {
|
|
t.Errorf("repositories-file is merged by the controller (merge %v, takes-config %v), want a plain template",
|
|
file["merge"], file["takes-config"])
|
|
}
|
|
text, err := os.ReadFile("../../" + file["template"].(string))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(text), "${config:"+ConfigKey+"}") {
|
|
t.Fatalf("the template %s does not say ${config:%s}: %s", file["template"], ConfigKey, text)
|
|
}
|
|
filled := filepath.Join(t.TempDir(), "repositories.json")
|
|
value := `{"mesh-delivery":{"private":false,"description":"d"}}`
|
|
if err := os.WriteFile(filled, []byte(strings.ReplaceAll(strings.ReplaceAll(string(text), "${config:"+ConfigKey+"}", value), "${config:keep-main-rules}", "false")), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v, err := ReadValue(filled); err != nil || len(v) != 1 || v["mesh-delivery"].Description != "d" {
|
|
t.Errorf("the filled template reads as %v, %v; want mesh-delivery", v, err)
|
|
}
|
|
if ReadKeepMainRules(filled) {
|
|
t.Error("the filled template with keep-main-rules false reads as true")
|
|
}
|
|
if k := m.Config["keep-main-rules"]; k == nil || k["type"] != "boolean" || k["default"] != false {
|
|
t.Errorf("config keep-main-rules = %v, want a boolean defaulting to false", k)
|
|
}
|
|
for _, r := range m.Resources {
|
|
if r["type"] == "process" && has(strs(r["restart-on"]), file["id"].(string)) {
|
|
process = r
|
|
}
|
|
}
|
|
if process == nil {
|
|
t.Fatalf("no process runs again when %s changes", file["id"])
|
|
}
|
|
if process["run-once"] != true {
|
|
t.Errorf("the process is not run-once: %v", process)
|
|
}
|
|
if last := m.Resources[len(m.Resources)-1]; last["id"] != process["id"] {
|
|
t.Errorf("the last resource is %v, want %v: a refused step gates whatever of the module comes after it", last["id"], process["id"])
|
|
}
|
|
if process["health"] != nil {
|
|
t.Errorf("the process declares a health check: %v", process["health"])
|
|
}
|
|
if strings.Join(strs(process["run"]), " ") != "./repositories sync" || process["artifact"] != "repositories" {
|
|
t.Errorf("the process runs %v from %v", process["run"], process["artifact"])
|
|
}
|
|
env, _ := process["env"].(map[string]any)
|
|
for _, k := range []string{"MESH_GITEA_URL", "MESH_GITEA_ADMIN_USER", "MESH_GITEA_ADMIN_PASSWORD_FILE", "MESH_REPOSITORIES_FILE", "MESH_REPOSITORIES_OUTCOME"} {
|
|
if env[k] == nil {
|
|
t.Errorf("the process is not given %s", k)
|
|
}
|
|
}
|
|
found := false
|
|
for _, a := range m.Build.Artifacts {
|
|
if a["name"] == "repositories" && a["from"] == "cmd/repositories" && a["binary"] == "repositories" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("no bundle builds cmd/repositories")
|
|
}
|
|
// Like agent-read-teams: the same kind of value, the same way of applying it.
|
|
teams := m.Config["agent-read-teams"]
|
|
if teams["trusted"] != v["trusted"] || teams["type"] != v["type"] {
|
|
t.Errorf("repositories is declared unlike agent-read-teams: %v vs %v", v, teams)
|
|
}
|
|
}
|
|
|
|
// 0534-2: the manifest declares main-mergers as a trusted list naming the operator's login by default, merges it into
|
|
// the module's runtime config file (with url, which the TypeScript part reads), and the repositories program is given
|
|
// that file and runs again when it changes.
|
|
func TestManifestDeclaresTheMainMergers(t *testing.T) {
|
|
raw, err := os.ReadFile("../../module.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m struct {
|
|
Config map[string]map[string]any `json:"config"`
|
|
Resources []map[string]any `json:"resources"`
|
|
}
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
v := m.Config[MergersKey]
|
|
if v == nil || v["type"] != "list" || v["trusted"] != true || strings.Join(strs(v["default"]), ",") != "jschoubben" {
|
|
t.Fatalf("config %s = %v, want a trusted list defaulting to the operator's login", MergersKey, v)
|
|
}
|
|
var cfg, process map[string]any
|
|
for _, r := range m.Resources {
|
|
if r["id"] == "runtime-config" {
|
|
cfg = r
|
|
}
|
|
if r["name"] == "gitea-repositories" {
|
|
process = r
|
|
}
|
|
}
|
|
if cfg == nil || !has(strs(cfg["takes-config"]), MergersKey) || cfg["merge"] != "json" {
|
|
t.Fatalf("runtime-config does not take %s: %v", MergersKey, cfg)
|
|
}
|
|
text, err := os.ReadFile("../../" + cfg["template"].(string))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
file := filepath.Join(t.TempDir(), "config.json")
|
|
_ = os.WriteFile(file, text, 0o600)
|
|
if got, err := ReadMergers(file); err != nil || strings.Join(got, ",") != "jschoubben" {
|
|
t.Errorf("the template's mergers read as %v, %v; want the default", got, err)
|
|
}
|
|
env, _ := process["env"].(map[string]any)
|
|
if env["MESH_GITEA_CONFIG_FILE"] != cfg["path"] {
|
|
t.Errorf("the repositories program reads %v, want %v", env["MESH_GITEA_CONFIG_FILE"], cfg["path"])
|
|
}
|
|
if !has(strs(process["restart-on"]), "runtime-config") {
|
|
t.Errorf("the repositories program does not run again when the mergers change: %v", process["restart-on"])
|
|
}
|
|
}
|
|
|
|
func has(list []string, want string) bool {
|
|
for _, s := range list {
|
|
if s == want {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|