package main import ( "bytes" "os" "path/filepath" "strings" "testing" ) // A file's text kept as a template file in its module's repository (novox/hq ADR 0349 ยง2, design 57 package T1, // issue 654): `module check` and the merge gate read it from the tree beside the manifest, so a change to a // template alone is judged as a change to its module. const templatedNotifier = `{"module":"notifier","version":"1", "build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}, "resources":[{"id":"conf","type":"file","path":"/etc/notifier/notifier.conf","mode":"0644","template":"files/notifier.conf"}]}` // inlineNotifier is the same module as the mesh holds it: built, its text inline. const inlineNotifier = `{"module":"notifier","version":"1", "resources":[{"id":"conf","type":"file","path":"/etc/notifier/notifier.conf","mode":"0644","content":"volume = 3\n"}]}` // aTemplatedModule writes a module's directory: its manifest and its files/. func aTemplatedModule(t *testing.T, dir, manifest string, files map[string]string) string { t.Helper() if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatal(err) } path := filepath.Join(dir, "module.json") if err := os.WriteFile(path, []byte(manifest), 0o644); err != nil { t.Fatal(err) } for name, text := range files { full := filepath.Join(dir, filepath.FromSlash(name)) if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(full, []byte(text), 0o644); err != nil { t.Fatal(err) } } return path } func TestModuleCheckReadsTemplatesFromTheTreeBesideTheManifest(t *testing.T) { good := aTemplatedModule(t, t.TempDir(), templatedNotifier, map[string]string{"files/notifier.conf": "volume = 3\n"}) var out bytes.Buffer if err := moduleCheck([]string{good}, &out); err != nil || !strings.Contains(out.String(), "notifier: ok") { t.Fatalf("a module with its text in files/ was refused: %v\n%s", err, out.String()) } // A change to the template alone, to text the controller refuses inline, is refused the same. bad := aTemplatedModule(t, t.TempDir(), templatedNotifier, map[string]string{"files/notifier.conf": "volume = 3\nstore = ${dir:nowhere}\n"}) out.Reset() if err := moduleCheck([]string{bad}, &out); err == nil || !strings.Contains(out.String(), "nowhere") { t.Fatalf("a template whose text is refused inline passed:\n%s", out.String()) } // A symlink under files/ that no template names: the build's packing refuses it, so the check does too. linked := t.TempDir() aTemplatedModule(t, linked, templatedNotifier, map[string]string{"files/notifier.conf": "volume = 3\n"}) if err := os.Symlink("/etc/hostname", filepath.Join(linked, "files", "elsewhere")); err != nil { t.Fatal(err) } out.Reset() if err := moduleCheck([]string{filepath.Join(linked, "module.json")}, &out); err == nil || !strings.Contains(out.String(), "files/elsewhere is a symlink") { t.Fatalf("a symlink under files/ passed the check:\n%s", out.String()) } // And a template the tree lacks, by name. missing := aTemplatedModule(t, t.TempDir(), templatedNotifier, nil) out.Reset() if err := moduleCheck([]string{missing}, &out); err == nil || !strings.Contains(out.String(), "files/notifier.conf") { t.Fatalf("a missing template was not refused by name:\n%s", out.String()) } } // templatedMesh is catalogueMesh with the notifier on the laptop, registered as built: its text inline. // aTemplatedTree is the catalogue's checkout with the notifier's text in files/. func aTemplatedTree(t *testing.T, manifests map[string]string, text string, has bool) string { t.Helper() tree := aTree(t, withEdit(manifests, "notifier", "")) files := map[string]string{} if has { files["files/notifier.conf"] = text } aTemplatedModule(t, filepath.Join(tree, "modules", "notifier"), templatedNotifier, files) return tree } const notifierTemplate = "modules/notifier/files/notifier.conf" // What the gate composes for the module in its template form is what the mesh composes for it inline. // A module whose files/ is unpacked onto the machine names its own templates directory (issue 731): `module check` // reads its templates from there and says of it exactly what it says of the same module with its text inline. func TestModuleCheckReadsTemplatesFromTheTemplatesDirectoryTheManifestNames(t *testing.T) { const inline = `{"module":"notifier","version":"1", "build":{"artifacts":[{"name":"scripts","kind":"archive","from":"files"}]}, "resources":[{"id":"scripts","type":"archive","path":"/usr/local/lib/notifier","artifact":"scripts"}, {"id":"conf","type":"file","path":"/etc/notifier/notifier.conf","mode":"0644","content":"volume = 3\n"}]}` const templated = `{"module":"notifier","version":"1","templates":"templates", "build":{"artifacts":[{"name":"scripts","kind":"archive","from":"files"}, {"name":"templates","kind":"archive","from":"templates"}]}, "resources":[{"id":"scripts","type":"archive","path":"/usr/local/lib/notifier","artifact":"scripts"}, {"id":"conf","type":"file","path":"/etc/notifier/notifier.conf","mode":"0644","template":"templates/notifier.conf"}]}` scripts := map[string]string{"files/bin/notify": "#!/bin/sh\n"} var want bytes.Buffer if err := moduleCheck([]string{aTemplatedModule(t, t.TempDir(), inline, scripts)}, &want); err != nil { t.Fatalf("the inline module was refused: %v\n%s", err, want.String()) } with := map[string]string{"files/bin/notify": "#!/bin/sh\n", "templates/notifier.conf": "volume = 3\n"} var got bytes.Buffer if err := moduleCheck([]string{aTemplatedModule(t, t.TempDir(), templated, with)}, &got); err != nil { t.Fatalf("a module with its text in templates/ was refused: %v\n%s", err, got.String()) } if got.String() != want.String() { t.Fatalf("module check says otherwise of the templated module:\ninline: %s\ntemplated: %s", want.String(), got.String()) } // Its text in files/ instead, where the machine unpacks it, is not where its manifest says its templates are. misplaced := strings.Replace(templated, `"templates/notifier.conf"`, `"files/notifier.conf"`, 1) got.Reset() if err := moduleCheck([]string{aTemplatedModule(t, t.TempDir(), misplaced, map[string]string{"files/notifier.conf": "volume = 3\n"})}, &got); err == nil || !strings.Contains(got.String(), "files/notifier.conf") { t.Fatalf("a template outside the templates directory the manifest names passed:\n%s", got.String()) } } // Issue 876: the gate judges a moved module that names its templates as registration reads it after a real build, // handed its templates; it refused every such module as "marked moved and names no template". This calls the gate's // own shelf (shelfWithChange), which needs no database.