123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232 | package render_test
import (
"encoding/json"
"fmt"
"os"
"strings"
"testing"
)
// These tests push a hostile commit, then undo it the two ways a
// maintainer would (a revert commit, or a force push back to before it),
// and check that the site holds nothing the hostile commit put there.
// hostile commits files that render, plus a file "x" beside a directory
// "x.html". The file's page is code/<repo>/x.html, but the directory's
// children's pages go in a directory of that same name (x.html/y is
// code/<repo>/x.html/y.html), so rendering fails partway, after some of
// its pages are written.
func (e *env) pushHostile() {
e.t.Helper()
for _, p := range []string{"a1", "m1", "z1"} {
e.write("evil/"+p+".txt", "evil\n")
}
e.write("x", "file\n")
e.write("x.html/y", "beside the file\n")
e.commit("hostile")
e.push(false)
_, err := e.site.RenderRepo(e.repo, false)
e.t.Logf("hostile render: %v", err)
if err == nil {
e.t.Fatal("rendering the colliding pages should fail")
}
}
func (e *env) mustBeClean() {
e.t.Helper()
for _, p := range []string{"evil/a1.txt", "evil/m1.txt", "evil/z1.txt", "x", "x.html/y"} {
e.mustNotExist(e.page(p), e.page(p)+".gz", e.raw(p))
}
e.mustNotExist(e.page("evil"), "code/"+e.repo+"/evil", "raw/"+e.repo+"/evil")
e.mustExist(e.page("README.md"), e.page("src/main.c"))
}
func TestRevertAfterFailedRender(t *testing.T) {
e := newEnv(t, "proj", "")
e.seed()
e.render()
e.pushHostile()
e.git(e.work, "revert", "--no-edit", "HEAD")
e.push(false)
e.render()
e.mustBeClean()
}
func TestRollbackAfterFailedRender(t *testing.T) {
e := newEnv(t, "proj", "")
e.seed()
e.render()
e.pushHostile()
e.git(e.work, "reset", "-q", "--hard", "HEAD~1")
e.push(true)
e.render()
e.mustBeClean()
}
// A render that dies outright (killed, a crash, a full disk) leaves the
// state of the render before it, marked unfinished, and pages of its own
// commit. The next render must clean those up as well.
func TestRevertAfterInterruptedRender(t *testing.T) {
e := newEnv(t, "proj", "")
e.seed()
e.render()
statePath := e.site.Lay.StatePath(e.repo)
before, err := e.site.LoadState(e.repo)
if err != nil {
t.Fatal(err)
}
e.write("evil.txt", "evil\n")
e.commit("hostile")
e.push(false)
e.render()
e.mustExist(e.page("evil.txt"), e.raw("evil.txt"))
// What the render would have left had it died before saving.
before.Unfinished = true
b, err := json.Marshal(before)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(statePath, b, 0o644); err != nil {
t.Fatal(err)
}
e.git(e.work, "revert", "--no-edit", "HEAD")
e.push(false)
e.render()
e.mustNotExist(e.page("evil.txt"), e.raw("evil.txt"))
if st, err := e.site.LoadState(e.repo); err != nil || st.Unfinished {
t.Errorf("state after a finished render: %+v, %v", st, err)
}
}
// A tag moved or deleted in the bare repository itself runs no hook, and
// pushers cannot delete tags at all, so the next render has to notice
// that a tag's tarball holds the wrong commit or names no tag.
func TestTagTarballsFollowTags(t *testing.T) {
e := newEnv(t, "proj", "")
e.seed()
e.write("evil.txt", "evil\n")
e.commit("hostile")
e.git(e.work, "tag", "v1")
e.git(e.work, "tag", "-a", "-m", "release", "v2")
e.push(false)
e.git(e.work, "push", "-q", "origin", "--tags")
e.render()
v1, v2 := "archive/proj/proj-v1.tar.gz", "archive/proj/proj-v2.tar.gz"
mustContain(t, "v1 tarball", gunzip(t, e.h(v1)), "evil.txt")
e.mustExist(v2)
// A write that died leaves a temporary file beside the tarballs.
tmp := "archive/proj/.proj-v3.tar.gz.tmp-123"
if err := os.WriteFile(e.h(tmp), []byte("evil"), 0o644); err != nil {
t.Fatal(err)
}
e.git(e.work, "revert", "--no-edit", "HEAD")
e.push(false)
bare := e.site.Lay.GitDir(e.repo)
e.git(bare, "tag", "-f", "v1", "main")
e.git(bare, "tag", "-d", "v2")
e.render()
mustNotContain(t, "v1 tarball", gunzip(t, e.h(v1)), "evil.txt")
e.mustNotExist(v2, tmp)
mustNotContain(t, "tag list", e.read("iframes/proj/downloads.html"), "proj-v2")
}
// A superseded tip tarball stays for archive_retention, for pages loaded
// before the push, unless a force push took its commit off the branch.
func TestRollbackRemovesTipTarball(t *testing.T) {
e := newEnv(t, "proj", "")
e.seed()
e.render()
e.write("evil.txt", "evil\n")
e.commit("hostile")
e.push(false)
e.render()
evil := "snapshot/proj/proj@" + strings.TrimSpace(e.git(e.work, "rev-parse", "--short=12", "HEAD")) + ".tar.gz"
e.mustExist(evil)
// A revert keeps the commit in the branch's history, so its tarball
// stays for now.
e.git(e.work, "revert", "--no-edit", "HEAD")
e.push(false)
e.render()
e.mustExist(evil)
e.git(e.work, "reset", "-q", "--hard", "HEAD~2")
e.push(true)
e.render()
e.mustNotExist(evil)
}
// A commit whose files add up to more than repo_max_bytes is not
// rendered: the site keeps the last render, untouched and not marked
// unfinished, and a tag of it gets no tarball.
func TestRepoMaxBytes(t *testing.T) {
e := newEnv(t, "proj", "repo_max_bytes: 4096\n")
e.seed()
e.render()
readme := e.read(e.page("README.md"))
e.write("big/a.txt", strings.Repeat("a", 3000))
e.write("big/b.txt", strings.Repeat("b", 3000))
e.commit("too big")
e.push(false)
e.git(e.work, "tag", "big")
e.git(e.work, "push", "-q", "origin", "big")
_, err := e.site.RenderRepo(e.repo, false)
if err == nil || !strings.Contains(err.Error(), "repo_max_bytes") {
t.Fatalf("rendering a tree over the cap: %v", err)
}
e.mustNotExist(e.page("big/a.txt"), e.raw("big/a.txt"), "archive/proj/proj-big.tar.gz")
if got := e.read(e.page("README.md")); got != readme {
t.Error("a refused render rewrote a page")
}
if st, err := e.site.LoadState(e.repo); err != nil || st.Unfinished {
t.Errorf("state after a refused render: %+v, %v", st, err)
}
// A tag event for it removes a tarball the tag's old target had.
if err := os.MkdirAll(e.h("archive/proj"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(e.h("archive/proj/proj-big.tar.gz"), []byte("old"), 0o644); err != nil {
t.Fatal(err)
}
head := strings.TrimSpace(e.git(e.work, "rev-parse", "HEAD"))
if err := e.site.HandleTag(e.repo, "big", head); err == nil {
t.Error("archiving a tag over the cap should fail")
}
e.mustNotExist("archive/proj/proj-big.tar.gz")
e.git(e.work, "revert", "--no-edit", "HEAD")
e.push(false)
e.render()
e.mustNotExist(e.page("big/a.txt"))
mustContain(t, "drawer", e.read(e.drawer()), `id="README.md"`)
}
// repo_max_files refuses a commit of too many files the same way.
func TestRepoMaxFiles(t *testing.T) {
e := newEnv(t, "proj", "repo_max_files: 15\n")
e.seed() // 12 files
e.render()
for i := range 5 {
e.write(fmt.Sprintf("many/%d", i), "")
}
e.commit("too many")
e.push(false)
_, err := e.site.RenderRepo(e.repo, false)
if err == nil || !strings.Contains(err.Error(), "repo_max_files") {
t.Fatalf("rendering a tree over the cap: %v", err)
}
e.mustNotExist(e.page("many/0"), e.raw("many/0"))
if st, err := e.site.LoadState(e.repo); err != nil || st.Unfinished {
t.Errorf("state after a refused render: %+v, %v", st, err)
}
e.rm("many/0")
e.rm("many/1")
e.commit("fewer")
e.push(false)
e.render()
e.mustExist(e.page("many/2"), e.raw("many/2"))
}
|