123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 | package render_test
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"code.bruce-hill.com/repo-renderer/internal/queue"
)
func TestIndexSections(t *testing.T) {
cfgWith := func(siteExtra string) string {
return `
site:
title: Bruce's code
tagline: Things I made
unlisted_title: Misc
` + siteExtra + `
sections:
- title: "[Tools](https://example.com/tools)"
description: Small *programs*, <b>raw</b>.
repos: [beta, alpha, missing]
`
}
e := newEnv(t, "alpha", cfgWith(""))
e.seed()
e.render()
for _, extra := range []string{"beta", "gamma"} {
other := &env{t: t, root: e.root, host: e.host, repo: extra, site: e.site, home: e.home}
bare := e.site.Lay.GitDir(extra)
other.git("", "init", "-q", "--bare", "-b", "main", bare)
if err := os.WriteFile(filepath.Join(bare, "description"), []byte("desc of "+extra+"\n"), 0o644); err != nil {
t.Fatal(err)
}
other.work = t.TempDir()
other.git(other.work, "init", "-q", "-b", "main")
other.git(other.work, "remote", "add", "origin", bare)
other.write("README.md", "# "+extra+"\n")
other.commit("init")
other.push(false)
if _, err := e.site.RenderRepo(extra, false); err != nil {
t.Fatal(err)
}
}
if err := e.site.RenderIndex(); err != nil {
t.Fatal(err)
}
idx := e.read("index.html")
mustContain(t, "index", idx, "Bruce's code", "Things I made",
`<h2><a href="https://example.com/tools">Tools</a></h2>`, "<h2>Misc</h2>",
"<p>Small <em>programs</em>, <b>raw</b>.</p>",
"desc of beta", "desc of gamma")
mustNotContain(t, "index", idx, `href="/code/missing"`)
// Repos within a section are alphabetical, not config order; unlisted
// come last.
if strings.Index(idx, `href="/code/beta"`) < strings.Index(idx, `href="/code/alpha"`) {
t.Error("section repos not sorted by name")
}
if strings.Index(idx, "<h2>Misc</h2>") < strings.Index(idx, "Tools") {
t.Error("unlisted section should come last")
}
e.configure(cfgWith(" unlisted: hide\n"))
if err := e.site.RenderIndex(); err != nil {
t.Fatal(err)
}
mustNotContain(t, "index", e.read("index.html"), `href="/code/gamma"`, "Misc")
}
// A custom favicon's URL is versioned by its content, not just its path:
// replacing the file in place must reach browsers that cached the old one.
func TestFaviconVersioned(t *testing.T) {
e := newEnv(t, "proj", "")
icon := filepath.Join(t.TempDir(), "icon.png")
versionWith := func(data string) string {
t.Helper()
if err := os.WriteFile(icon, []byte(data), 0o644); err != nil {
t.Fatal(err)
}
e.configure("site:\n favicon: " + icon + "\n")
return e.site.TmplHash
}
if a, b := versionWith("one"), versionWith("two"); a == b {
t.Errorf("hash %s did not change with the favicon's content", a)
}
e.seed()
e.render()
mustContain(t, "index", e.read("index.html"), `rel="icon" href="/static/favicon.png?v=`+e.site.TmplHash+`" type="image/png"`)
}
// With unlisted: private, a repository in no section is not on the site
// at all: rendering it removes what an earlier render left, every branch
// view, tag tarball and the clone export included, and a push publishes
// nothing of it.
func TestPrivateRepo(t *testing.T) {
repos := "repos:\n proj:\n branches: [dev]\n"
e := newEnv(t, "proj", "site:\n unlisted: show\n"+repos)
e.seed()
e.git(e.work, "branch", "dev")
e.git(e.work, "push", "-q", "origin", "dev")
e.git(e.work, "tag", "v1")
e.git(e.work, "push", "-q", "origin", "v1")
e.git("", "clone", "-q", "--bare", e.site.Lay.GitDir("proj"), e.site.Lay.GitDir("other"))
if err := e.site.RenderAll(false); err != nil {
t.Fatal(err)
}
gone := []string{"code/proj.html", "code/proj", "code/proj@dev.html", "code/proj@dev",
"raw/proj", "raw/proj@dev", "search/proj", "search/proj@dev", "iframes/proj", "iframes/proj@dev",
"types/proj", "archive/proj", "snapshot/proj", "snapshot/proj@dev", "git/proj.git", "sitemap/proj.xml"}
e.mustExist(gone...)
e.configure("site:\n unlisted: private\nsections:\n - title: Tools\n repos: [other]\n" + repos)
if err := e.site.RenderAll(false); err != nil {
t.Fatal(err)
}
e.mustNotExist(gone...)
e.mustExist("code/other.html", "git/other.git/HEAD", "sitemap/other.xml")
for _, key := range []string{"proj", "proj@dev"} {
if _, err := os.Stat(e.site.Lay.StatePath(key)); err == nil {
t.Errorf("state of %s kept", key)
}
}
mustNotContain(t, "index", e.read("index.html"), "/code/proj")
mustNotContain(t, "sitemap", e.read("sitemap.xml"), "proj.xml")
e.write("new.txt", "new\n")
e.commit("more")
e.push(false)
e.git(e.work, "tag", "v2")
e.git(e.work, "push", "-q", "origin", "v2")
e.kick(
queue.Job{Repo: "proj", Ref: "refs/heads/main", Old: zeroSHA, New: e.sha("main")},
queue.Job{Repo: "proj", Ref: "refs/tags/v2", Old: zeroSHA, New: e.sha("v2")},
)
e.mustNotExist(gone...)
if changed, err := e.site.RenderRepo("proj", true); err != nil || changed {
t.Errorf("RenderRepo of a private repository with nothing left = %v, %v", changed, err)
}
e.mustNotExist(gone...)
}
// With a private site, a private repository renders there, every link
// under /private/, its clone URL its private export, and nothing of it on
// the public site, sitemap included. Moving it between the sites, by a
// render or by a push of a tag alone, takes it off the one it left.
func TestPrivateSite(t *testing.T) {
e := newEnv(t, "proj", "")
e.configure(e.privateConfig("other"))
e.seed()
e.git(e.work, "tag", "v1")
e.git(e.work, "push", "-q", "origin", "v1")
if err := e.site.RenderAll(false); err != nil {
t.Fatal(err)
}
priv := func(rel string) string { return filepath.Join(e.site.Cfg.PrivateHtdocs, "private", rel) }
privExists := func(rel string) bool { _, err := os.Stat(priv(rel)); return err == nil }
for _, rel := range []string{"index.html", "code/proj.html", "code/proj/src/main.c.html", "raw/proj",
"search/proj/corpus.json", "archive/proj/proj-v1.tar.gz", "git/proj.git/HEAD"} {
if !privExists(rel) {
t.Errorf("private site has no %s", rel)
}
}
e.mustNotExist("code/proj.html", "code/proj", "raw/proj", "git/proj.git", "archive/proj", "sitemap/proj.xml", "private")
if privExists("sitemap/proj.xml") || privExists("sitemap.xml") || privExists("static") {
t.Error("private site has a sitemap or static files of its own")
}
page := mustReadFile(t, priv("code/proj.html"))
mustContain(t, "private page", page, `href="/private/code/proj/src"`, `action="/private/search/proj/"`,
`src="/private/iframes/proj/downloads.html"`, "https://"+e.host+"/private/git/proj.git",
`<meta name="robots" content="noindex">`, `href="/private/"`, `href="/static/style.css?v=`)
mustNotContain(t, "private page", page, `href="/code/proj`, `"/raw/proj`, `"/search/proj`)
mustContain(t, "private index", mustReadFile(t, priv("index.html")), `href="/private/code/proj"`)
mustNotContain(t, "public index", e.read("index.html"), "/code/proj", "/private")
mustNotContain(t, "sitemap", e.read("sitemap.xml"), "proj")
// The private export clones over dumb HTTP from where the private
// site serves it.
srv := httptest.NewServer(http.FileServer(http.Dir(e.site.Cfg.PrivateHtdocs)))
defer srv.Close()
dst := filepath.Join(t.TempDir(), "c")
e.git("", "clone", "-q", srv.URL+"/private/git/proj.git", dst)
if _, err := os.Stat(filepath.Join(dst, "src", "main.c")); err != nil {
t.Errorf("clone of the private export: %v", err)
}
// Public, by a render: off the private site.
e.configure(e.privateConfig("proj"))
if err := e.site.RenderAll(false); err != nil {
t.Fatal(err)
}
e.mustExist("code/proj.html", "git/proj.git/HEAD", "archive/proj/proj-v1.tar.gz")
for _, rel := range []string{"code/proj.html", "raw/proj", "git/proj.git", "archive/proj"} {
if privExists(rel) {
t.Errorf("private site keeps %s of a public repository", rel)
}
}
if _, err := os.Stat(filepath.Join(e.site.Cfg.StateDir, "private", "state", "proj")); err == nil {
t.Error("private site keeps the state of a public repository")
}
// Private again, by a push of a tag alone: the whole repository moves.
e.configure(e.privateConfig("other"))
e.git(e.work, "tag", "v2")
e.git(e.work, "push", "-q", "origin", "v2")
e.kick(queue.Job{Repo: "proj", Ref: "refs/tags/v2", Old: zeroSHA, New: e.sha("v2")})
e.mustNotExist("code/proj.html", "git/proj.git", "archive/proj", "sitemap/proj.xml")
for _, rel := range []string{"code/proj.html", "archive/proj/proj-v2.tar.gz", "git/proj.git/HEAD"} {
if !privExists(rel) {
t.Errorf("private site has no %s after the tag push", rel)
}
}
mustNotContain(t, "public index", e.read("index.html"), "/code/proj")
mustContain(t, "private index", mustReadFile(t, priv("index.html")), `href="/private/code/proj"`)
}
|