131 lines
4.8 KiB
Go
131 lines
4.8 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// A module that must know its own address — a login redirect, a canonical URL, an issuer — had it
|
|
// written into its manifest as a literal (novox/hq 04-ISSUES/122): a domain in a definition, wrong on
|
|
// every other machine. It is told instead, from the same composition the provider receives.
|
|
|
|
// selfAware contributes a labelled route, binds the requirement, and writes its own name into a file.
|
|
func selfAware(label string) Manifest {
|
|
m := labelled("board", label, 8080)
|
|
m.Requires = []string{"reverse-proxy"}
|
|
m.Binds = map[string]string{"reverse-proxy": "/var/lib/board/route.json"}
|
|
m.Resources = []map[string]any{
|
|
{"id": "conf", "type": "file", "path": "/var/lib/board/app.conf",
|
|
"content": "root = https://${bound:reverse-proxy:name}/\ninternal = ${bound:reverse-proxy:internal-name}\n"},
|
|
}
|
|
return m
|
|
}
|
|
|
|
// servingProxy is proxy() as the catalogue's route providers are declared: the provision scoped to
|
|
// the mesh, serving nothing a consumer must know (route-adapter, route-proxy: `"serves": {"route": {}}`).
|
|
func servingProxy() Manifest {
|
|
p := proxy()
|
|
p.Provides = []Offer{{Name: "reverse-proxy", Scope: ScopeMesh}}
|
|
p.Serves = map[string]map[string]any{"reverse-proxy": {}}
|
|
return p
|
|
}
|
|
|
|
// nodeProxy is the same provider scoped to its node, whose answer on the same machine comes from
|
|
// `here` rather than from the mesh's needs — the other path a binding is written by.
|
|
func nodeProxy() Manifest {
|
|
p := proxy()
|
|
p.Serves = map[string]map[string]any{"reverse-proxy": {"scheme": "http"}}
|
|
return p
|
|
}
|
|
|
|
// onBoth is a node with a public domain and a private-network address, so both names compose.
|
|
func onBoth(domain string) Node {
|
|
n := withDomain(domain)
|
|
n.At = "anchor.internal"
|
|
return n
|
|
}
|
|
|
|
func fileAt(t *testing.T, out []map[string]any, path string) string {
|
|
t.Helper()
|
|
for _, r := range out {
|
|
if r["path"] == path {
|
|
return r["content"].(string)
|
|
}
|
|
}
|
|
t.Fatalf("nothing was declared at %s", path)
|
|
return ""
|
|
}
|
|
|
|
func TestAModuleIsToldTheNameItsProviderServes(t *testing.T) {
|
|
got, err := Resolve(shelf(servingProxy(), selfAware("git")), []string{"traefik", "board"},
|
|
onBoth("example.tld"), World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out := mustDeclare(t, got)
|
|
served := received(t, out)[0].Values
|
|
|
|
var binding map[string]any
|
|
if err := json.Unmarshal([]byte(fileAt(t, out, "/var/lib/board/route.json")), &binding); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if binding["name"] != served["name"] || binding["name"] != "git.example.tld" {
|
|
t.Fatalf("the module was told %v, the provider serves %v", binding["name"], served["name"])
|
|
}
|
|
if binding["internal-name"] != served["internal-name"] || binding["internal-name"] == nil {
|
|
t.Fatalf("internal name: module told %v, provider serves %v",
|
|
binding["internal-name"], served["internal-name"])
|
|
}
|
|
|
|
conf := fileAt(t, out, "/var/lib/board/app.conf")
|
|
want := "root = https://git.example.tld/\ninternal = " + served["internal-name"].(string) + "\n"
|
|
if conf != want {
|
|
t.Fatalf("the file was rendered as\n%s\nwant\n%s", conf, want)
|
|
}
|
|
}
|
|
|
|
func TestTheNameAModuleIsToldFollowsTheNodesDomain(t *testing.T) {
|
|
// The whole point: the same definition, two machines, two names — nothing edited.
|
|
for _, domain := range []string{"example.tld", "other.example"} {
|
|
got, err := Resolve(shelf(servingProxy(), selfAware("git")), []string{"traefik", "board"},
|
|
onBoth(domain), World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conf := fileAt(t, mustDeclare(t, got), "/var/lib/board/app.conf")
|
|
if !strings.HasPrefix(conf, "root = https://git."+domain+"/") {
|
|
t.Fatalf("on %s the module wrote %q", domain, conf)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAModuleWithNoPublicNameIsNotToldOne(t *testing.T) {
|
|
// No public domain on the node: nothing composed, so no `name` — and a file asking for one is
|
|
// refused rather than rendered with a placeholder or an empty host.
|
|
m := selfAware("git")
|
|
m.Resources[0]["content"] = "root = https://${bound:reverse-proxy:name}/\n"
|
|
got, err := Resolve(shelf(servingProxy(), m), []string{"traefik", "board"}, workstation(), World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := got.Declaration(Rendering{}); err == nil ||
|
|
!strings.Contains(err.Error(), `"name"`) {
|
|
t.Fatalf("a file asking for a name that was never composed was not refused: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAModuleIsToldItsNameByANodeScopedProviderToo(t *testing.T) {
|
|
m := selfAware("git")
|
|
m.Resources[0]["content"] = "root = https://${bound:reverse-proxy:name}/\n"
|
|
got, err := Resolve(shelf(nodeProxy(), m), []string{"board"},
|
|
withDomain("example.tld"), World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conf := fileAt(t, mustDeclare(t, got), "/var/lib/board/app.conf")
|
|
if !strings.HasPrefix(conf, "root = https://git.example.tld/") {
|
|
t.Fatalf("a same-machine, node-scoped answer did not tell the module its name: %q", conf)
|
|
}
|
|
}
|