Download
143 lines 4.6 KiBGoModified 2026-10-19 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
package render_test

import (
	"archive/tar"
	"compress/gzip"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"
)

func TestTags(t *testing.T) {
	e := newEnv(t, "proj", "")
	e.seed()
	e.render()
	e.git(e.work, "tag", "v1.0")
	e.git(e.work, "tag", "-a", "-m", "release", "rel/2.0")
	e.git(e.work, "push", "-q", "origin", "--tags")
	sha := strings.TrimSpace(e.git(e.work, "rev-parse", "v1.0"))
	if err := e.site.HandleTag("proj", "v1.0", sha); err != nil {
		t.Fatal(err)
	}
	if err := e.site.HandleTag("proj", "rel/2.0", sha); err != nil {
		t.Fatal(err)
	}
	e.mustExist("archive/proj/proj-v1.0.tar.gz", "archive/proj/proj-rel-2.0.tar.gz", "snapshot/proj/proj@"+sha[:12]+".tar.gz")

	// Tarball has the expected prefix.
	f, err := os.Open(e.h("archive/proj/proj-v1.0.tar.gz"))
	if err != nil {
		t.Fatal(err)
	}
	defer func() { _ = f.Close() }() // read only
	zr, err := gzip.NewReader(f)
	if err != nil {
		t.Fatal(err)
	}
	tr := tar.NewReader(zr)
	for {
		hdr, err := tr.Next()
		if err != nil {
			t.Fatal(err)
		}
		if hdr.Typeflag == tar.TypeXGlobalHeader {
			continue // git archive writes the commit id in a pax header first
		}
		if !strings.HasPrefix(hdr.Name, "proj-v1.0/") {
			t.Errorf("tar prefix: %q", hdr.Name)
		}
		break
	}

	// Every page's download menu frames the download list, so writing the
	// list is all a tag push needs; the pages stay as they are.
	root := e.read(e.page(""))
	if err := e.site.WriteDownloads("proj"); err != nil {
		t.Fatal(err)
	}
	mustContain(t, "tag list", e.read("iframes/proj/downloads.html"), `href="/archive/proj/proj-v1.0.tar.gz"`, `href="/archive/proj/proj-rel-2.0.tar.gz"`)
	for _, pg := range []string{e.page(""), e.page("src/main.c")} {
		mustContain(t, pg, e.read(pg), `src="/iframes/proj/downloads.html"`)
		mustNotContain(t, pg, e.read(pg), "proj-v1.0", "/archive/", "/snapshot/")
	}
	if e.read(e.page("")) != root {
		t.Error("writing the tag list rewrote a page")
	}

	// Deleting a tag removes its archive. Unsafe names are skipped.
	if err := e.site.HandleTag("proj", "v1.0", strings.Repeat("0", 40)); err != nil {
		t.Fatal(err)
	}
	e.mustNotExist("archive/proj/proj-v1.0.tar.gz")
	if err := e.site.WriteDownloads("proj"); err != nil {
		t.Fatal(err)
	}
	mustNotContain(t, "tag list", e.read("iframes/proj/downloads.html"), "proj-v1.0")
	if err := e.site.HandleTag("proj", "../evil", sha); err != nil {
		t.Fatal(err)
	}
	if _, err := os.Stat(filepath.Join(e.root, "htdocs", e.host, "archive", "proj", "proj-..-evil.tar.gz")); err == nil {
		t.Error("unsafe tag produced an archive")
	}

	// A render picks up archives for existing tags and lists them straight
	// away (a fresh deploy has no archives yet).
	if err := os.Remove(e.h("archive/proj/proj-rel-2.0.tar.gz")); err != nil {
		t.Fatal(err)
	}
	if _, err := e.site.RenderRepo("proj", false); err != nil {
		t.Fatal(err)
	}
	e.mustExist("archive/proj/proj-rel-2.0.tar.gz")
	mustContain(t, "tag list after render", e.read("iframes/proj/downloads.html"), `href="/archive/proj/proj-rel-2.0.tar.gz"`)
}

// The branch-tip tarball is named by its commit, so its URL can be cached
// for good. The download list links the current one and names its commit;
// the pages never do, so a push leaves untouched pages alone. A replaced
// tarball stays for archive_retention after the push that replaced it.
func TestTipArchive(t *testing.T) {
	e := newEnv(t, "proj", "")
	e.seed()
	e.render()
	tipOf := func() (sha, rel string) {
		sha = strings.TrimSpace(e.git(e.work, "rev-parse", "HEAD"))
		return sha, "snapshot/proj/proj@" + sha[:12] + ".tar.gz"
	}
	sha1, tip1 := tipOf()
	e.mustExist(tip1)
	mustContain(t, "download list", e.read("iframes/proj/downloads.html"), `href="/`+tip1+`"`, ">"+sha1[:12]+"<", ">initial<")
	untouched := e.read(e.page("src/main.c"))
	mustNotContain(t, "page", untouched, sha1[:12], "/snapshot/")
	if gunzip(t, e.h(tip1)) == "" {
		t.Error("empty tip tarball")
	}

	// Made long ago, but replaced only now: it stays.
	old := time.Now().Add(-2 * time.Hour)
	if err := os.Chtimes(e.h(tip1), old, old); err != nil {
		t.Fatal(err)
	}
	e.write("README.md", "# Hello again\n")
	e.commit("second")
	e.push(false)
	e.render()
	_, tip2 := tipOf()
	e.mustExist(tip1, tip2)
	list := e.read("iframes/proj/downloads.html")
	mustContain(t, "download list", list, `href="/`+tip2+`"`, ">second<")
	mustNotContain(t, "download list", list, tip1)
	if e.read(e.page("src/main.c")) != untouched {
		t.Error("a push rewrote a page it did not touch")
	}

	// Replaced more than archive_retention ago: the next render removes it.
	if err := os.Chtimes(e.h(tip1), old, old); err != nil {
		t.Fatal(err)
	}
	e.render()
	e.mustNotExist(tip1)
	e.mustExist(tip2)
}