package publish import ( "bytes" "io/fs" "os" "os/exec" "path/filepath" "strings" "testing" "git.coffeylabs.org/coffey-labs/hotdog-cms/internal/build" "git.coffeylabs.org/coffey-labs/hotdog-cms/starter" ) func starterSite(t *testing.T, dir string) { t.Helper() err := fs.WalkDir(starter.Files, "site", func(p string, d fs.DirEntry, err error) error { if err != nil { return err } target := filepath.Join(dir, strings.TrimPrefix(p, "site")) if d.IsDir() { return os.MkdirAll(target, 0o755) } data, _ := starter.Files.ReadFile(p) return os.WriteFile(target, []byte(strings.ReplaceAll(string(data), "{{SITE_NAME}}", "Test")), 0o644) }) if err != nil { t.Fatal(err) } } func built(t *testing.T) string { t.Helper() site := t.TempDir() starterSite(t, site) res, err := build.Run(build.Options{SiteDir: site}) if err != nil { t.Fatal(err) } return res.Out } func TestDirTarget(t *testing.T) { out := built(t) root := filepath.Join(t.TempDir(), "www") var log bytes.Buffer for i := 0; i < 2; i++ { // the second run replaces the first if err := Run("local", Target{Type: "dir", Path: root}, out, &log); err != nil { t.Fatal(err) } } if _, err := os.Stat(filepath.Join(root, "index.html")); err != nil { t.Fatal("index.html not published") } foreign := t.TempDir() os.WriteFile(filepath.Join(foreign, "keep.txt"), []byte("x"), 0o644) if err := Run("local", Target{Type: "dir", Path: foreign}, out, &log); err == nil { t.Fatal("replaced a folder hotdog-cms did not write") } } // fakeSSH puts an ssh on PATH that runs the remote command on this machine, // so the rsync target can be exercised without a server. func fakeSSH(t *testing.T) { t.Helper() bin := t.TempDir() script := `#!/bin/sh while [ $# -gt 0 ]; do case "$1" in -o|-p|-l) shift 2 ;; -*) shift ;; *) break ;; esac done shift # the host exec sh -c "$*" ` if err := os.WriteFile(filepath.Join(bin, "ssh"), []byte(script), 0o755); err != nil { t.Fatal(err) } t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH")) } func TestRsyncTarget(t *testing.T) { if _, err := exec.LookPath("rsync"); err != nil { t.Skip("rsync not installed") } fakeSSH(t) out := built(t) live := filepath.Join(t.TempDir(), "srv", "site") tg := Target{Type: "rsync", Host: "deploy@example.test", Path: live} var log bytes.Buffer for i := 0; i < 2; i++ { if err := Run("server", tg, out, &log); err != nil { t.Fatalf("run %d: %v\n%s", i, err, log.String()) } } if _, err := os.Stat(filepath.Join(live, "articles", "hello-world", "index.html")); err != nil { t.Fatal("article not published") } for _, leftover := range []string{live + ".hotdog-cms-staging", live + ".hotdog-cms-old"} { if _, err := os.Stat(leftover); err == nil { t.Errorf("%s left behind", leftover) } } foreign := filepath.Join(t.TempDir(), "srv", "other") os.MkdirAll(foreign, 0o755) os.WriteFile(filepath.Join(foreign, "keep.txt"), []byte("x"), 0o644) if err := Run("server", Target{Type: "rsync", Host: "h", Path: foreign}, out, &log); err == nil { t.Fatal("replaced a remote folder hotdog-cms did not write") } if _, err := os.Stat(filepath.Join(foreign, "keep.txt")); err != nil { t.Fatal("the remote folder's file is gone") } } func TestTargetValidation(t *testing.T) { bad := []Target{ {Type: "rsync", Host: "h", Path: "/"}, {Type: "rsync", Host: "h", Path: "/var"}, {Type: "rsync", Host: "h", Path: "relative/path"}, {Type: "rsync", Host: "h; rm -rf /", Path: "/srv/site"}, {Type: "container"}, {Type: "ftp", Path: "/x"}, } for _, tg := range bad { if err := tg.validate(); err == nil { t.Errorf("accepted %+v", tg) } } } func TestCommandAndContainerContext(t *testing.T) { out := built(t) var log bytes.Buffer if err := Run("hook", Target{Type: "command", Command: []string{"sh", "-c", `test -f "$HOTDOG_OUT/index.html" && test -f "$0/index.html"`, "{out}"}}, out, &log); err != nil { t.Fatalf("command target: %v", err) } ctx := filepath.Join(t.TempDir(), "ctx") if err := Run("image", Target{Type: "container", Context: ctx}, out, &log); err != nil { t.Fatal(err) } for _, f := range []string{"Containerfile", "Dockerfile", "nginx.conf", "site/index.html"} { if _, err := os.Stat(filepath.Join(ctx, f)); err != nil { t.Errorf("context is missing %s", f) } } } func TestPull(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git not installed") } repo := t.TempDir() starterSite(t, repo) git := func(args ...string) { t.Helper() cmd := exec.Command("git", append([]string{"-C", repo, "-c", "user.name=t", "-c", "user.email=t@example.test", "-c", "commit.gpgsign=false"}, args...)...) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("git %v: %v\n%s", args, err, out) } } git("init", "-q", "-b", "main") git("add", "-A") git("commit", "-q", "-m", "site") www := filepath.Join(t.TempDir(), "www") opt := PullOptions{Repo: repo, Out: www, Cache: filepath.Join(t.TempDir(), "clone"), Log: &bytes.Buffer{}} if err := Pull(opt); err != nil { t.Fatal(err) } about := filepath.Join(www, "about", "index.html") if b, _ := os.ReadFile(about); !strings.Contains(string(b), "