deploy_test.go
Rama main · réplica de lectura del repositorio original.
// SPDX-FileCopyrightText: 2026 Rashik Adhikari
// SPDX-License-Identifier: Apache-2.0
package main
import (
"fmt"
"net/netip"
"regexp"
"slices"
"strings"
"testing"
"github.com/quilzo/quilzo/internal/yamlite"
)
// The reference Docker Compose stack, checked against itself.
//
// deploy/compose.yaml was written, reviewed and never run as its own README
// says to run it. When it was, none of its three setup steps worked: each
// `docker compose run --rm admin ...` replaced the service's command, so the
// --root that named the store went with it, and Quilzo looked for a store in
// the image's read-only working directory. Without read_only it was worse:
// `init` said "created .quilzo" into the container's own layer, which --rm
// deleted. The public site then never started, because nothing had put a
// design in its templates volume.
//
// None of that needs Docker to see. It is the shape of three text files --
// compose.yaml, the Dockerfile and the README's steps -- and these tests
// read them.
// composeFile is deploy/compose.yaml as Compose reads it, with its anchors
// merged into the mappings that name them.
//
// yamlite refuses anchors and aliases, rightly for agent definitions, so the
// two forms this file uses -- `key: &name` on a top-level key and `<<: *name`
// -- are rewritten into plain keys first and merged here, the merged mapping's
// own keys winning as they do in Compose.
func composeFile(t *testing.T) map[string]any {
t.Helper()
src := read(t, "deploy/compose.yaml")
anchors := map[string]string{}
anchorDef := regexp.MustCompile(`(?m)^([A-Za-z0-9_.-]+):[ \t]*&([A-Za-z0-9_-]+)[ \t]*$`)
for _, m := range anchorDef.FindAllStringSubmatch(src, -1) {
anchors[m[2]] = m[1]
}
src = anchorDef.ReplaceAllString(src, "$1:")
src = regexp.MustCompile(`(?m)^([ \t]*)<<:[ \t]*\*([A-Za-z0-9_-]+)[ \t]*$`).
ReplaceAllString(src, "${1}x-merge-from: $2")
doc, err := yamlite.Parse([]byte(src))
if err != nil {
t.Fatalf("deploy/compose.yaml: %v", err)
}
top, ok := doc.(map[string]any)
if !ok {
t.Fatal("deploy/compose.yaml is not a mapping")
}
var merge func(v any)
merge = func(v any) {
switch x := v.(type) {
case map[string]any:
if name, ok := x["x-merge-from"].(string); ok {
from, _ := top[anchors[name]].(map[string]any)
if from == nil {
t.Fatalf("deploy/compose.yaml merges *%s, which names nothing", name)
}
for k, fv := range from {
if _, own := x[k]; !own {
x[k] = fv
}
}
delete(x, "x-merge-from")
}
for _, c := range x {
merge(c)
}
case []any:
for _, c := range x {
merge(c)
}
}
}
merge(top)
return top
}
func services(t *testing.T) map[string]map[string]any {
t.Helper()
raw, _ := composeFile(t)["services"].(map[string]any)
if len(raw) == 0 {
t.Fatal("deploy/compose.yaml has no services")
}
out := map[string]map[string]any{}
for name, v := range raw {
m, ok := v.(map[string]any)
if !ok {
t.Fatalf("service %s is not a mapping", name)
}
out[name] = m
}
return out
}
func strs(v any) []string {
var out []string
switch x := v.(type) {
case []any:
for _, e := range x {
out = append(out, fmt.Sprint(e))
}
case string:
out = append(out, x)
}
return out
}
// quilzoServices are the services that run this program's image.
func quilzoServices(t *testing.T) map[string]map[string]any {
t.Helper()
out := map[string]map[string]any{}
for name, s := range services(t) {
if img, _ := s["image"].(string); strings.Contains(img, "quilzo/quilzo") {
out[name] = s
}
}
if len(out) == 0 {
t.Fatal("no service in deploy/compose.yaml runs the Quilzo image")
}
return out
}
// mounts is a service's volumes, by where they are mounted: the volume's
// name and whether it is read-only.
func mounts(s map[string]any) map[string]struct {
Volume string
ReadOnly bool
} {
out := map[string]struct {
Volume string
ReadOnly bool
}{}
for _, v := range strs(s["volumes"]) {
parts := strings.Split(v, ":")
if len(parts) < 2 {
continue
}
out[parts[1]] = struct {
Volume string
ReadOnly bool
}{parts[0], len(parts) > 2 && strings.Contains(parts[2], "ro")}
}
return out
}
// entrypoint is the Dockerfile's ENTRYPOINT, in exec form.
func entrypoint(t *testing.T) []string {
t.Helper()
m := regexp.MustCompile(`(?m)^ENTRYPOINT\s+\[(.*)\]\s*$`).FindStringSubmatch(read(t, "Dockerfile"))
if m == nil {
t.Fatal("the Dockerfile has no ENTRYPOINT in exec form")
}
var out []string
for _, f := range strings.Split(m[1], ",") {
out = append(out, strings.Trim(strings.TrimSpace(f), `"`))
}
return out
}
// composeSteps is every `docker compose` command in the README's compose
// section, in the order a reader runs them.
func composeSteps(t *testing.T) []string {
t.Helper()
readme := read(t, "deploy/README.md")
var steps []string
in := false
for _, line := range strings.Split(readme, "\n") {
if strings.HasPrefix(line, "```") {
in = !in
continue
}
if !in {
continue
}
cmd := strings.TrimSpace(line)
if i := strings.Index(cmd, " #"); i >= 0 {
cmd = strings.TrimSpace(cmd[:i])
}
if strings.HasPrefix(cmd, "docker compose ") {
steps = append(steps, cmd)
}
}
if len(steps) == 0 {
t.Fatal("deploy/README.md shows no docker compose command")
}
return steps
}
// runArgs is what a `docker compose run --rm SERVICE ARGS...` step hands the
// service: its name and the arguments after it.
func runArgs(step string) (string, []string, bool) {
f := strings.Fields(step)
if len(f) < 4 || f[2] != "run" {
return "", nil, false
}
// The run flags that take the next word as their value.
valued := map[string]bool{"-e": true, "--env": true, "-v": true, "--volume": true,
"-w": true, "--workdir": true, "-u": true, "--user": true, "--name": true,
"--entrypoint": true, "-p": true, "--publish": true, "-l": true, "--label": true}
i := 3
for i < len(f) && strings.HasPrefix(f[i], "-") {
if valued[f[i]] {
i++
}
i++
}
if i >= len(f) {
return "", nil, false
}
return f[i], f[i+1:], true
}
// Finding STACK-F1. Every command the README runs reaches the store on its
// volume, however the command is given.
//
// `docker compose run --rm admin init` replaces the service's command, so a
// --root written in the command is lost on exactly the commands that set the
// store up. The image's entrypoint is the one part a command cannot replace,
// so that is where the store is named.
func TestComposeCommandsReachTheStoreOnItsVolume(t *testing.T) {
ep := entrypoint(t)
i := slices.Index(ep, "--root")
if i < 0 || i+1 >= len(ep) {
t.Fatalf("the image's entrypoint is %q: it does not name the store, so "+
"`docker compose run --rm admin init` looks for one in the working "+
"directory, which read_only makes read-only and --rm throws away", ep)
}
store := ep[i+1]
for name, s := range quilzoServices(t) {
if slices.Contains(strs(s["command"]), "--root") {
t.Errorf("%s's command names --root again; the entrypoint does, and a "+
"command given to `docker compose run` would replace this one", name)
}
if ro, _ := s["read_only"].(bool); !ro {
t.Errorf("%s is not read_only: a write anywhere but a volume would go "+
"into the container's own layer, and from there nowhere", name)
}
}
for _, name := range []string{"admin", "site"} {
s, ok := quilzoServices(t)[name]
if !ok {
t.Fatalf("there is no %s service running the Quilzo image", name)
}
m, ok := mounts(s)[store]
if !ok || m.Volume != "store" || m.ReadOnly {
t.Errorf("%s does not mount the store volume read-write at %s, where the "+
"entrypoint names it", name, store)
}
}
if vols, _ := composeFile(t)["volumes"].(map[string]any); vols == nil {
t.Error("deploy/compose.yaml declares no named volumes")
} else if _, ok := vols["store"]; !ok {
t.Error("deploy/compose.yaml does not declare the store volume")
}
}
// The README's steps name services the compose file has, and run in the
// directory it is in.
func TestComposeStepsNameItsServices(t *testing.T) {
svc := services(t)
for _, step := range composeSteps(t) {
if name, _, ok := runArgs(step); ok {
if _, found := svc[name]; !found {
t.Errorf("the README runs %q, and deploy/compose.yaml has no service %s", step, name)
}
}
}
if !strings.Contains(read(t, "deploy/README.md"), "cd deploy") {
t.Error("the README never says to run the steps in deploy/, where compose.yaml and .env are")
}
}
// The hardening the stack is designed around stays: no Quilzo container
// holds a capability, gains one, or runs as root.
func TestComposeKeepsItsHardening(t *testing.T) {
for name, s := range quilzoServices(t) {
if !slices.Contains(strs(s["cap_drop"]), "ALL") {
t.Errorf("%s does not drop every capability", name)
}
if !slices.Contains(strs(s["security_opt"]), "no-new-privileges:true") {
t.Errorf("%s can gain privileges", name)
}
if u, _ := s["user"].(string); u == "0" || strings.HasPrefix(u, "0:") || u == "root" {
t.Errorf("%s runs as root", name)
}
}
if !regexp.MustCompile(`(?m)^USER\s+nonroot`).MatchString(read(t, "Dockerfile")) {
t.Error("the image does not run as nonroot")
}
}
// workdir is the image's working directory, which a relative path in a
// command is read against.
func workdir(t *testing.T) string {
t.Helper()
m := regexp.MustCompile(`(?m)^WORKDIR\s+(\S+)\s*$`).FindAllStringSubmatch(read(t, "Dockerfile"), -1)
if len(m) == 0 {
t.Fatal("the Dockerfile sets no WORKDIR")
}
return m[len(m)-1][1]
}
// writableIn reports whether a service can write path: under a volume it
// mounts read-write, or the /tmp every Quilzo container has.
func writableIn(s map[string]any, path string) bool {
for at, m := range mounts(s) {
if !m.ReadOnly && (path == at || strings.HasPrefix(path, at+"/")) {
return true
}
}
return strings.HasPrefix(path, "/tmp/")
}
// flagValue is the value given to --name in args, or "".
func flagValue(args []string, name string) string {
for i, a := range args {
if a == "--"+name && i+1 < len(args) {
return args[i+1]
}
if v, ok := strings.CutPrefix(a, "--"+name+"="); ok {
return v
}
}
return ""
}
// Finding STACK-F2. Following the reference, the public site never started:
// nothing put a design in the templates volume, so `quilzo site` exited
// with "no layout in templates", Docker restarted it for ever and Caddy
// answered 502. A step before `up -d` writes one, where the site reads it.
func TestComposeSiteHasADesignBeforeItStarts(t *testing.T) {
steps := composeSteps(t)
up := slices.IndexFunc(steps, func(s string) bool { return strings.HasPrefix(s, "docker compose up") })
if up < 0 {
t.Fatal("the README never starts the stack")
}
svc := services(t)
wd := workdir(t)
designed := false
for _, step := range steps[:up] {
name, args, ok := runArgs(step)
if !ok || len(args) == 0 {
continue
}
isDemo := args[0] == "demo"
if !isDemo && !(len(args) > 1 && args[0] == "template" && args[1] == "use") {
continue
}
designed = true
// Where it writes, as the command reads its flags: templates
// relative to the working directory, and the demonstration's
// examples beside them unless told otherwise.
tpl := flagValue(args, "templates")
if !isDemo {
tpl = flagValue(args, "dir")
}
if tpl == "" {
tpl = "templates"
}
if !strings.HasPrefix(tpl, "/") {
tpl = wd + "/" + tpl
}
dirs := map[string]string{"templates": tpl}
if isDemo {
ex := flagValue(args, "examples")
if ex == "" {
ex = tpl[:strings.LastIndex(tpl, "/")] + "/examples"
}
dirs["examples"] = ex
}
for what, dir := range dirs {
if !writableIn(svc[name], dir) {
t.Errorf("%q writes its %s to %s, which %s cannot write: the read-only "+
"container refuses it", step, what, dir, name)
}
}
site, ok := svc["site"]
if !ok {
t.Fatal("there is no site service")
}
m, found := mounts(site)[tpl]
if !found || m.Volume != mounts(svc[name])[tpl].Volume {
t.Errorf("%q writes the design to %s, and the site does not read that volume there", step, tpl)
}
}
if !designed {
t.Error("nothing before `docker compose up` writes a design, so the site exits " +
"with \"no layout in templates\" and Caddy answers 502")
}
}
// cmsUID is the account every Quilzo container but the log writer runs as:
// distroless's nonroot.
const cmsUID = "65532"
// Finding STACK-F12. The reference ran no separate log writer, so the
// processes that publish content and face the internet wrote their own
// audit log, and anything that could run code as them could rewrite the
// record of it: `quilzo logd status` answered "no log writer", and the
// posture scan "The CMS writes its own audit log". The writer is its own
// service, as an account the CMS is not, holding the only writable mount of
// the log; the CMS reaches it over its socket.
func TestComposeKeepsTheAuditLogOutOfTheCMSsReach(t *testing.T) {
svc := quilzoServices(t)
var name string
for n, s := range svc {
if c := strs(s["command"]); len(c) > 0 && c[0] == "logd" {
name = n
}
}
if name == "" {
t.Fatal("no service runs `quilzo logd`, so the CMS writes its own audit log")
}
logd := svc[name]
dir := flagValue(strs(logd["command"]), "log-dir")
if dir == "" {
t.Fatalf("%s names no --log-dir", name)
}
user, _ := logd["user"].(string)
uid := strings.SplitN(user, ":", 2)[0]
if uid == "" || uid == "0" || uid == cmsUID {
t.Errorf("%s runs as %q: the writer has to be an account the CMS is not, or the CMS can open the log", name, user)
}
if got := flagValue(strs(logd["command"]), "allow-uid"); got != cmsUID {
t.Errorf("%s takes records from uid %q; the CMS is %s", name, got, cmsUID)
}
if m, ok := mounts(logd)[dir]; !ok || m.ReadOnly {
t.Errorf("%s does not mount %s read-write", name, dir)
}
if nm, _ := logd["network_mode"].(string); nm != "none" {
t.Errorf("%s has a network; it needs a socket and nothing else", name)
}
vol := mounts(logd)[dir].Volume
for n, s := range svc {
if n == name {
continue
}
m, ok := mounts(s)[dir]
if (n == "admin" || n == "site") && !ok {
t.Errorf("%s does not mount the log at %s, so it cannot reach the writer's socket", n, dir)
}
if ok && (m.Volume != vol || !m.ReadOnly) {
t.Errorf("%s mounts %s other than read-only, and could write the log itself", n, dir)
}
if ok && !slices.Contains(strs(s["depends_on"]), name) {
t.Errorf("%s starts without waiting for %s, whose socket its records go to", n, name)
}
}
// The volume is seeded from the image, ownership included, so the
// directory has to exist there owned by the writer.
if !regexp.MustCompile(`(?m)^COPY --from=build --chown=` + uid + `:\d+ \S+ ` + regexp.QuoteMeta(dir) + `\s*$`).
MatchString(read(t, "Dockerfile")) {
t.Errorf("the image has no %s owned by uid %s, so the volume is made owned by root and the writer cannot use it", dir, uid)
}
// The store has to say where the log is, as the step right after init,
// so everything after it is recorded by the writer.
steps := composeSteps(t)
at := -1
for i, step := range steps {
if _, args, ok := runArgs(step); ok && len(args) >= 3 && args[0] == "auditlog" && args[1] == "dir" && args[2] == dir {
at = i
}
}
if at < 1 {
t.Fatalf("the README never points the store at %s (quilzo auditlog dir %s)", dir, dir)
}
if _, args, _ := runArgs(steps[at-1]); len(args) != 1 || args[0] != "init" {
t.Errorf("the store is pointed at the writer's log after %q rather than right after init", steps[at-1])
}
}
// Finding STACK-F9. An image built as documented answered "quilzo dev": the
// Dockerfile takes the version as a build argument and passes it to the
// linker, and nothing that told people how to build one passed it. The
// reference can run an image built here, under a name of its own rather than
// the release's, and the README's build passes the version.
func TestComposeRunsAnImageThatKnowsItsVersion(t *testing.T) {
df := read(t, "Dockerfile")
stages := regexp.MustCompile(`(?m)^FROM\s`).Split(df, -1)
if len(stages) < 3 {
t.Fatal("the Dockerfile has no separate build stage")
}
build := stages[1]
arg := strings.Index(build, "ARG VERSION")
use := strings.Index(build, "-X main.version=$VERSION")
if arg < 0 || use < 0 || use < arg {
t.Error("the build stage does not declare ARG VERSION before passing it to -X main.version, " +
"and a build argument nothing declares is discarded without a word")
}
for name, s := range quilzoServices(t) {
img, _ := s["image"].(string)
if !strings.HasPrefix(img, "${QUILZO_IMAGE:-ghcr.io/quilzo/quilzo}:") || !strings.Contains(img, "${QUILZO_VERSION") {
t.Errorf("%s runs %q: an image built here can only be run under the release's own name", name, img)
}
}
readme := read(t, "deploy/README.md")
if !regexp.MustCompile(`docker build --build-arg VERSION=\S+ -t \S+`).MatchString(readme) {
t.Error("the README does not show how to build an image that knows its version")
}
for _, v := range []string{"QUILZO_IMAGE", "QUILZO_VERSION"} {
if !strings.Contains(read(t, "deploy/compose.env.example"), v+"=") {
t.Errorf("compose.env.example does not say %s can be set", v)
}
}
}
// networksOf is the networks a service is on, given as a list or as a
// mapping of each to its settings.
func networksOf(s map[string]any) []string {
if m, ok := s["networks"].(map[string]any); ok {
var out []string
for k := range m {
out = append(out, k)
}
slices.Sort(out)
return out
}
return strs(s["networks"])
}
// envDefaults is compose.env.example, the values a reader starts from.
func envDefaults(t *testing.T) map[string]string {
t.Helper()
out := map[string]string{}
for _, line := range strings.Split(read(t, "deploy/compose.env.example"), "\n") {
if k, v, ok := strings.Cut(strings.TrimSpace(line), "="); ok && !strings.HasPrefix(k, "#") {
out[k] = v
}
}
return out
}
// stepIndex is the first README step whose arguments, after the service,
// start with these words; -1 if none does.
func stepIndex(steps []string, words ...string) int {
for i, step := range steps {
if _, args, ok := runArgs(step); ok && len(args) >= len(words) && slices.Equal(args[:len(words)], words) {
return i
}
}
return -1
}
// Finding STACK-F10. `init --admin` printed http://localhost:8080/enrol/...
// whatever the admin was published on, because nothing had told the store
// where its admin is: with QUILZO_ADMIN_PORT=18092 the link pointed at
// nothing, and a passkey is made for the address it is made on. The admin
// is reached through a Caddy of its own, over HTTPS, on this machine's
// loopback and nowhere else; the README tells the store that address, from
// the same .env the port is published from, before the link is made.
func TestComposeEnrolmentLinkNamesThePublishedAdmin(t *testing.T) {
svc := services(t)
if ports := strs(svc["admin"]["ports"]); len(ports) > 0 {
t.Errorf("the admin publishes %v itself; it is reached through its own proxy", ports)
}
var proxy string
for name, s := range svc {
for _, v := range strs(s["volumes"]) {
if strings.HasPrefix(v, "./Caddyfile.admin:") {
proxy = name
}
}
}
if proxy == "" {
t.Fatal("no service serves the admin over HTTPS (./Caddyfile.admin)")
}
p := svc[proxy]
if nets := networksOf(p); !slices.Equal(nets, []string{"admin"}) {
t.Errorf("%s is on %v: the admin's proxy shares a network with the admin and nothing else", proxy, nets)
}
ports := strs(p["ports"])
if len(ports) == 0 {
t.Fatalf("%s publishes nothing", proxy)
}
for _, port := range ports {
if !strings.HasPrefix(port, "127.0.0.1:${QUILZO_ADMIN_PORT") {
t.Errorf("%s publishes %q: the admin is this machine's, on its loopback, at QUILZO_ADMIN_PORT", proxy, port)
}
}
// One capability at most, the one the image's caddy carries as a file
// capability and cannot be run without under no-new-privileges.
if !slices.Contains(strs(p["cap_drop"]), "ALL") ||
!slices.Contains(strs(p["security_opt"]), "no-new-privileges:true") ||
len(strs(p["cap_add"])) > 1 || (len(strs(p["cap_add"])) == 1 && strs(p["cap_add"])[0] != "NET_BIND_SERVICE") {
t.Errorf("%s holds more than caddy's one capability, or can gain more", proxy)
}
if ro, _ := p["read_only"].(bool); !ro {
t.Errorf("%s is not read-only", proxy)
}
caddyfile := read(t, "deploy/Caddyfile.admin")
for _, want := range []string{"tls internal", "reverse_proxy admin:8080"} {
if !strings.Contains(caddyfile, want) {
t.Errorf("Caddyfile.admin has no %q", want)
}
}
// The proxy the internet reaches, and the site behind it, still cannot
// reach the admin at all.
for _, name := range []string{"caddy", "site"} {
if slices.Contains(networksOf(svc[name]), "admin") {
t.Errorf("%s is on the admin's network", name)
}
}
readme := read(t, "deploy/README.md")
if !strings.Contains(readme, "set -a; . ./.env; set +a") {
t.Error("the README does not read .env into the shell, so its steps cannot name the addresses .env gives")
}
steps := composeSteps(t)
enrol := stepIndex(steps, "init", "--admin")
if enrol < 0 {
t.Fatal("the README makes no first administrator")
}
for _, set := range [][]string{
{"config", "set", "admin.base_url", `"https://localhost:$QUILZO_ADMIN_PORT"`},
{"config", "set", "admin.behind_tls_proxy", "true"},
} {
at := stepIndex(steps, set...)
if at < 0 {
t.Errorf("the README never runs %s", strings.Join(set, " "))
} else if at > enrol {
t.Errorf("%s comes after init --admin, whose link it decides", strings.Join(set, " "))
}
}
if envDefaults(t)["QUILZO_ADMIN_PORT"] == "" {
t.Error("compose.env.example does not set QUILZO_ADMIN_PORT")
}
}
// The first token, for the commands that come after the steps.
//
// The README gave it as something to run "once there is an administrator",
// and said an administrator makes the rest in the admin, which has no page
// for it. The command was the store's one exception, a token issued without
// one being shown, and the exception holds only while the store has no
// token at all: signing in with the passkey the enrolment link adds makes
// one. So by the time anybody followed the README to that command, it was
// refused, and nothing after the steps could be run. It is a step, after
// the first administrator is named and before the admin starts; every other
// token is issued with one.
func TestComposeIssuesTheFirstTokenBeforeAnybodySignsIn(t *testing.T) {
steps := composeSteps(t)
up := slices.IndexFunc(steps, func(s string) bool { return strings.HasPrefix(s, "docker compose up") })
enrol := stepIndex(steps, "init", "--admin")
if up < 0 || enrol < 0 {
t.Fatal("the README neither names a first administrator nor starts the stack")
}
_, enrolArgs, _ := runArgs(steps[enrol])
admin := flagValue(enrolArgs, "admin")
first := -1
for i, step := range steps {
_, args, ok := runArgs(step)
if !ok || len(args) < 2 || args[0] != "token" || args[1] != "issue" || strings.Contains(step, "QUILZO_TOKEN") {
continue
}
if i < enrol || i > up {
t.Errorf("%q issues a token without one where the store has nobody to give it to or has a token already", step)
}
if who := flagValue(args, "principal"); who != admin {
t.Errorf("%q issues the first token to %q, not to the first administrator, %s", step, who, admin)
}
first = i
}
if first < 0 {
t.Error("the README's steps issue no first token, so no command after them can be run")
}
// Nor anywhere else in it, in a sentence as much as in a block.
setup := map[string]bool{}
for _, s := range steps[:up] {
setup[s] = true
}
for _, m := range regexp.MustCompile("docker compose run[^`\n]*token issue[^`\n]*").FindAllString(read(t, "deploy/README.md"), -1) {
cmd := m
if i := strings.Index(cmd, " #"); i >= 0 {
cmd = cmd[:i]
}
cmd = strings.TrimSpace(cmd)
if !strings.Contains(cmd, "QUILZO_TOKEN") && !setup[cmd] {
t.Errorf("the README gives %q, which is refused once anybody has signed in", cmd)
}
}
}
// Finding STACK-F11. The site was given --base-url https://DOMAIN, right on
// 443 only: with Caddy published elsewhere, the sitemap and every canonical
// link named a port nothing listened on. The port Caddy is published on, the
// port it believes it serves (so its redirects and Alt-Svc name it too), and
// the site's address all come from the one variable; the store is told the
// same address for what the admin sends out, newsletters and share links.
func TestComposeSiteURLNamesThePublishedPort(t *testing.T) {
svc := services(t)
caddy := svc["caddy"]
const https = "${QUILZO_HTTPS_PORT:-443}"
want := map[string]bool{https + ":" + https: false, https + ":" + https + "/udp": false}
for _, p := range strs(caddy["ports"]) {
if _, ok := want[p]; ok {
want[p] = true
}
}
for p, seen := range want {
if !seen {
t.Errorf("caddy does not publish %q: the port inside and out must be one, or Caddy's "+
"redirects and Alt-Svc name a port nobody can reach", p)
}
}
env, _ := caddy["environment"].(map[string]any)
if _, ok := env["QUILZO_HTTPS_PORT"]; !ok {
t.Error("caddy is not told QUILZO_HTTPS_PORT")
}
cf := read(t, "deploy/Caddyfile.compose")
if !strings.Contains(cf, "\n{$QUILZO_DOMAIN}:{$QUILZO_HTTPS_PORT:443} {") {
t.Error("Caddyfile.compose does not serve the site on QUILZO_HTTPS_PORT")
}
if !strings.Contains(cf, "http_port {$QUILZO_HTTP_PORT:80}") {
t.Error("Caddyfile.compose does not listen for HTTP on QUILZO_HTTP_PORT")
}
// Caddy's redirect from HTTP leaves the site's port out when it is
// https_port, so https_port must not be set to it.
if regexp.MustCompile(`(?m)^\s*https_port\s`).MatchString(cf) {
t.Error("Caddyfile.compose sets https_port, so its redirect from HTTP drops the port")
}
base := flagValue(strs(svc["site"]["command"]), "base-url")
if base != "https://${QUILZO_DOMAIN:?set QUILZO_DOMAIN in deploy/.env}:"+https {
t.Errorf("the site's --base-url is %q, which does not name the port Caddy is published on", base)
}
steps := composeSteps(t)
at := stepIndex(steps, "config", "set", "site.base_url", `"https://$QUILZO_DOMAIN:$QUILZO_HTTPS_PORT"`)
if at < 0 {
t.Error("the README does not tell the store the site's address, so the demonstration " +
"sets it to http://127.0.0.1:8081 and every link the admin sends out names that")
} else if demo := stepIndex(steps, "demo"); demo >= 0 && demo < at {
t.Error("the site's address is set after the demonstration, which sets its own when there is none")
}
if envDefaults(t)["QUILZO_HTTPS_PORT"] != "443" {
t.Error("compose.env.example does not set QUILZO_HTTPS_PORT=443")
}
}
// The site believes X-Forwarded-For from one address, Caddy's, and the
// README tells the store that address. They were two copies of
// 172.31.250.10, and the edge network's range a third: on a machine where
// that range was taken -- by a second stack, as the test machine's was -- the
// stack could not be made at all, and moving it meant editing three places
// that had to agree. Now .env holds the range and the address once, and the
// default address is in the default range.
func TestComposeTrustsTheAddressCaddyHas(t *testing.T) {
top := composeFile(t)
nets, _ := top["networks"].(map[string]any)
edge, _ := nets["edge"].(map[string]any)
ipam, _ := edge["ipam"].(map[string]any)
cfg, _ := ipam["config"].([]any)
if len(cfg) != 1 {
t.Fatal("the edge network has no single address range")
}
subnet, _ := cfg[0].(map[string]any)["subnet"].(string)
if subnet != "${QUILZO_EDGE_SUBNET:-172.31.250.0/28}" {
t.Errorf("the edge network's range is %q, not QUILZO_EDGE_SUBNET", subnet)
}
caddyNets, _ := services(t)["caddy"]["networks"].(map[string]any)
onEdge, _ := caddyNets["edge"].(map[string]any)
if addr, _ := onEdge["ipv4_address"].(string); addr != "${QUILZO_PROXY_ADDRESS:-172.31.250.10}" {
t.Errorf("caddy's address on edge is %q, not QUILZO_PROXY_ADDRESS", addr)
}
if stepIndex(composeSteps(t), "config", "set", "network.trusted_proxies", `"$QUILZO_PROXY_ADDRESS"`) < 0 {
t.Error("the README does not trust QUILZO_PROXY_ADDRESS, the address .env gives Caddy")
}
env := envDefaults(t)
pfx, err := netip.ParsePrefix(env["QUILZO_EDGE_SUBNET"])
if err != nil {
t.Fatalf("compose.env.example's QUILZO_EDGE_SUBNET: %v", err)
}
addr, err := netip.ParseAddr(env["QUILZO_PROXY_ADDRESS"])
if err != nil || !pfx.Contains(addr) {
t.Errorf("compose.env.example gives Caddy %q, which is not in %s", env["QUILZO_PROXY_ADDRESS"], pfx)
}
if pfx.String() != "172.31.250.0/28" || addr.String() != "172.31.250.10" {
t.Errorf("compose.env.example's defaults (%s, %s) are not compose.yaml's", pfx, addr)
}
}