232 lines
6.9 KiB
Go
232 lines
6.9 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The module's manifest, read the way the catalogue reads it, for the manifest tests. The same in
|
|
// every desktop module that carries it.
|
|
|
|
type manifest struct {
|
|
Module string `json:"module"`
|
|
Version string `json:"version"`
|
|
Capabilities []string `json:"capabilities"`
|
|
Requires []string `json:"requires"`
|
|
Claims []claim `json:"claims"`
|
|
Seats []any `json:"seats"`
|
|
Tools []string `json:"tools"`
|
|
Config map[string]config `json:"config"`
|
|
Environment *environment `json:"environment"`
|
|
Shell []shellCode `json:"shell"`
|
|
Resources []map[string]any `json:"resources"`
|
|
Build struct {
|
|
Artifacts []map[string]any `json:"artifacts"`
|
|
} `json:"build"`
|
|
}
|
|
|
|
// config is one config value as the definition declares it (novox/hq ADR 0348 §4).
|
|
type config struct {
|
|
Type string `json:"type"`
|
|
Description string `json:"description"`
|
|
Enum []any `json:"enum"`
|
|
Minimum *float64 `json:"minimum"`
|
|
Maximum *float64 `json:"maximum"`
|
|
Pattern string `json:"pattern"`
|
|
Trusted bool `json:"trusted"`
|
|
Predefined string `json:"predefined"`
|
|
Default any `json:"default"`
|
|
Required bool `json:"required"`
|
|
Runtime bool `json:"runtime"`
|
|
}
|
|
|
|
type claim struct {
|
|
Name string `json:"name"`
|
|
Scope string `json:"scope"`
|
|
Serves []string `json:"serves"`
|
|
}
|
|
|
|
type environment struct {
|
|
Variables map[string]string `json:"variables"`
|
|
Path []map[string]any `json:"path"`
|
|
}
|
|
|
|
type shellCode struct {
|
|
For string `json:"for"`
|
|
Slot string `json:"slot"`
|
|
Code string `json:"code"`
|
|
}
|
|
|
|
func readManifest(t *testing.T) manifest {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dec := json.NewDecoder(strings.NewReader(string(raw)))
|
|
dec.DisallowUnknownFields()
|
|
var m manifest
|
|
if err := dec.Decode(&m); err != nil {
|
|
t.Fatalf("module.json: %v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (m manifest) resource(t *testing.T, id string) map[string]any {
|
|
t.Helper()
|
|
for _, r := range m.Resources {
|
|
if r["id"] == id {
|
|
return r
|
|
}
|
|
}
|
|
t.Fatalf("no resource %q", id)
|
|
return nil
|
|
}
|
|
|
|
func (m manifest) packages() (present, absent []string) {
|
|
for _, r := range m.Resources {
|
|
if r["type"] == "package" {
|
|
if r["absent"] == true {
|
|
absent = append(absent, r["package"].(string))
|
|
} else {
|
|
present = append(present, r["package"].(string))
|
|
}
|
|
}
|
|
}
|
|
return present, absent
|
|
}
|
|
|
|
// fromTemplate checks that a file resource takes its text from its template file in the module's
|
|
// repository (novox/hq ADR 0349 §2): it names the template, carries no text of its own beside it, and the
|
|
// template is there. The readable file in the repository is the one place the machine's text is written.
|
|
func (m manifest) fromTemplate(t *testing.T, id, template string) {
|
|
t.Helper()
|
|
r := m.resource(t, id)
|
|
if r["type"] != "file" {
|
|
t.Fatalf("%s is a %v, not a file", id, r["type"])
|
|
}
|
|
if r["template"] != template {
|
|
t.Fatalf("resource %s does not take its text from %s: template %v", id, template, r["template"])
|
|
}
|
|
if _, inline := r["content"]; inline {
|
|
t.Fatalf("resource %s keeps its text inline beside its template: a file's text is in one place", id)
|
|
}
|
|
if _, err := os.Stat(filepath.Join("..", "..", template)); err != nil {
|
|
t.Fatalf("resource %s names %s: %v", id, template, err)
|
|
}
|
|
if r["owner"] != "${machine:account}" && !strings.HasPrefix(r["path"].(string), "/etc/") {
|
|
t.Fatalf("%s under the home is the account's", id)
|
|
}
|
|
}
|
|
|
|
// everyFileIsATemplate checks that no file resource carries its text inline, and that the build packs
|
|
// files/ into the one archive the controller reads the templates back from (novox/hq ADR 0349 §2).
|
|
func (m manifest) everyFileIsATemplate(t *testing.T) {
|
|
t.Helper()
|
|
for _, r := range m.Resources {
|
|
if _, inline := r["content"]; inline {
|
|
t.Errorf("resource %v keeps its text inline: keep it in files/ and name it as the template", r["id"])
|
|
}
|
|
}
|
|
packs := 0
|
|
for _, a := range m.Build.Artifacts {
|
|
if a["kind"] == "archive" && a["from"] == "files" {
|
|
packs++
|
|
if a["name"] != "files" {
|
|
t.Errorf("the templates archive is named %v, not files", a["name"])
|
|
}
|
|
}
|
|
}
|
|
if packs != 1 {
|
|
t.Errorf("the build packs files/ into %d archives, not one", packs)
|
|
}
|
|
}
|
|
|
|
// text is the text a file resource places on the machine: its template file's.
|
|
func (m manifest) text(t *testing.T, id string) string {
|
|
t.Helper()
|
|
name, _ := m.resource(t, id)["template"].(string)
|
|
if name == "" {
|
|
t.Fatalf("resource %s names no template", id)
|
|
}
|
|
raw, err := os.ReadFile(filepath.Join("..", "..", name))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return string(raw)
|
|
}
|
|
|
|
// checkTheToolsAgree checks that the manifest lists the module's own tools exactly, that the bundle
|
|
// serves each seat verb the claims promise as <seat>.<verb>, and that the Go bundle is declared.
|
|
func checkTheToolsAgree(t *testing.T, m manifest) {
|
|
t.Helper()
|
|
own, seat := map[string]bool{}, map[string]bool{}
|
|
for _, tool := range tools() {
|
|
if strings.Contains(tool.Name, ".") {
|
|
seat[tool.Name] = true
|
|
} else {
|
|
own[tool.Name] = true
|
|
}
|
|
if strings.TrimSpace(tool.Description) == "" {
|
|
t.Errorf("%s has no description", tool.Name)
|
|
}
|
|
}
|
|
listed := map[string]bool{}
|
|
for _, name := range m.Tools {
|
|
listed[name] = true
|
|
if !own[name] {
|
|
t.Errorf("module.json lists %s, which the bundle does not serve", name)
|
|
}
|
|
}
|
|
for name := range own {
|
|
if !listed[name] {
|
|
t.Errorf("the bundle serves %s, which module.json does not list", name)
|
|
}
|
|
if !strings.HasPrefix(name, strings.ReplaceAll(m.Module, "-", "_")+"_") {
|
|
t.Errorf("%s is not prefixed with the module's name", name)
|
|
}
|
|
}
|
|
promised := map[string]bool{}
|
|
for _, c := range m.Claims {
|
|
for _, verb := range c.Serves {
|
|
promised[c.Name+"."+verb] = true
|
|
if !seat[c.Name+"."+verb] {
|
|
t.Errorf("the claim on %s promises %s, which the bundle does not serve", c.Name, verb)
|
|
}
|
|
}
|
|
}
|
|
for name := range seat {
|
|
if !promised[name] {
|
|
t.Errorf("the bundle serves %s, which no claim promises", name)
|
|
}
|
|
}
|
|
var bundle map[string]any
|
|
for _, a := range m.Build.Artifacts {
|
|
if a["kind"] == "bundle" {
|
|
bundle = a
|
|
}
|
|
}
|
|
if bundle == nil || bundle["language"] != "go" || bundle["system"] != "arch" ||
|
|
bundle["from"] != "cmd/"+m.Module+"-tools" || bundle["binary"] != m.Module+"-tools" {
|
|
t.Errorf("the Go tools bundle: %v", bundle)
|
|
}
|
|
}
|
|
|
|
// checkNoSecretsOrInstallationNames refuses what a catalogue manifest must never carry.
|
|
func checkNoSecretsOrInstallationNames(t *testing.T) {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s := strings.ToLower(string(raw))
|
|
for _, never := range []string{"/home/", "jochen", "g14", "shanks", "novox.be", "api_key", ".hal/", "greenclip daemon"} {
|
|
if strings.Contains(s, never) {
|
|
t.Errorf("module.json names %q", never)
|
|
}
|
|
}
|
|
}
|