123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 | package config
import (
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)
// paths are the settings every config must have.
const paths = "git_dir: /srv/git\nhtdocs: /srv/www\nstate_dir: /srv/state\n"
func TestDefaultsAndParse(t *testing.T) {
cfg, err := Parse([]byte("host: code.example.com\n" + paths))
if err != nil {
t.Fatal(err)
}
if cfg.GitDir != "/srv/git" || cfg.Htdocs != "/srv/www" || cfg.ListingTruncate != 5 ||
cfg.Log != LogSyslog || len(cfg.LexerDirs) != 0 {
t.Errorf("defaults: %+v", cfg)
}
for _, key := range []string{"git_dir", "htdocs", "state_dir"} {
if _, err := Parse([]byte("host: h\n")); err == nil || !strings.Contains(err.Error(), key+" is required") {
t.Errorf("no %s: %v", key, err)
}
}
if got := cfg.CloneURL("foo"); got != "https://code.example.com/git/foo.git" {
t.Errorf("clone url: %s", got)
}
full := `
host: code.example.com
git_dir: /srv/git
htdocs: /srv/www/code
state_dir: /srv/state
lexer_dirs: [/srv/repo-render/lexers]
site:
title: Bruce's code
clone_url: "git://code.example.com/{repo}"
sections:
- name: tools
title: Tools
- name: tomo
title: "[Tomo](https://tomo.example.com)"
lexers:
tomo:
globs: ["*.tm"]
aliases: [tomo, tm]
repos:
bp:
branches: [dev]
`
cfg, err = Parse([]byte(full))
if err != nil {
t.Fatal(err)
}
if cfg.CloneURL("bp") != "git://code.example.com/bp" || cfg.SectionIndex("tomo") != 1 || cfg.SectionIndex("zzz") != -1 || cfg.SectionIndex("") != -1 {
t.Errorf("parsed: %+v", cfg)
}
if !cfg.Private("") || !cfg.Private("zzz") || cfg.Private("tomo") {
t.Error("exactly the repos in no section (or one the config hasn't) should be private")
}
if len(cfg.Repo("bp").Branches) != 1 || cfg.LexerNames()[0] != "tomo" || cfg.Htdocs != "/srv/www/code" || cfg.LexerDirs[0] != "/srv/repo-render/lexers" {
t.Errorf("parsed: %+v", cfg)
}
}
func TestValidation(t *testing.T) {
cases := map[string]string{
"": "host is required",
"# just a comment\n": "host is required",
"host: h\nbogus: 1": `unknown field "bogus"`,
"host: h\nsite:\n titel: x": `unknown field "titel"`,
"host: h\nhost: h2": `"host" already defined`,
"host: h\nsections:\n - name: a": "title is required",
"host: h\nsections:\n - title: a": "name is required",
"host: h\nsections:\n - {name: a b, title: a}": "name \"a b\" is not usable",
"host: h\nsections:\n - {name: a, title: a}\n - {name: a, title: b}": "name \"a\" is used twice",
"host: h\n" + paths + "private_htdocs: /srv/www/private": "must be outside htdocs",
"host: h\n" + paths + "private_htdocs: /srv": "must be outside htdocs",
"host: h\ngit_dir: /g\nstate_dir: /a\nhtdocs: /srv/a\nprivate_htdocs: /srv/a": "must be outside htdocs",
"host: h\ngit_dir: \"\"": "git_dir is required",
"host: ../x": "host must be a hostname",
"host: a b": "host must be a hostname",
"host: h\nrepos:\n r:\n bogus: 1": `unknown field "bogus"`,
}
for src, want := range cases {
_, err := Parse([]byte(src))
if err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("%q: got %v, want %q", src, err, want)
}
}
for _, ok := range []string{"a", "my-repo", "x.y_z", "A1"} {
if !ValidRepoName(ok) {
t.Errorf("ValidRepoName(%q) = false", ok)
}
}
for _, bad := range []string{"", ".", "..", ".hidden", "a/b", "a b", "repo.git", "-x"} {
if ValidRepoName(bad) {
t.Errorf("ValidRepoName(%q) = true", bad)
}
}
}
func TestBranchAllowlist(t *testing.T) {
good := []string{"dev", "my-name/feature-with-kebabs", "release/1.2", "v2_x"}
for _, b := range good {
if !ValidBranchName(b) {
t.Errorf("%q rejected", b)
}
}
bad := []string{"", "/x", "x/", "a//b", "a/../b", ".hidden", "a/.b", "x.lock", "a b", "a:b", "a~b", "feat*", "a@{b}", "a..b", "é", "HEAD"}
for _, b := range bad {
if ValidBranchName(b) {
t.Errorf("%q accepted", b)
}
}
if k := ViewKey("tomo", "my-name/feature-with-kebabs"); k != "tomo@my-name:feature-with-kebabs" {
t.Errorf("ViewKey = %q", k)
}
if ViewKey("tomo", "") != "tomo" {
t.Error("default view key must be the repo name")
}
if ViewKey("a", "x/y") == ViewKey("a", "x-y") {
t.Error("slugs of different branches collide")
}
for _, src := range []string{
"host: h\nrepos:\n r:\n branches: [a b]\n",
"host: h\nrepos:\n r:\n branches: [dev, dev]\n",
"host: h\nrepos:\n r:\n branch: ../../x\n",
"host: h\nrepos:\n r:\n branch: \"a\\nb\"\n",
} {
if _, err := Parse([]byte(src)); err == nil {
t.Errorf("accepted:\n%s", src)
}
}
cfg, err := Parse([]byte("host: h\n" + paths + "repos:\n r:\n branch: trunk/main\n branches: [dev, a/b]\n"))
if err != nil || len(cfg.Repo("r").Branches) != 2 || cfg.Repo("r").Branch != "trunk/main" {
t.Errorf("good branches: %v", err)
}
for tag, ok := range map[string]bool{"lua": true, "vibe coding": true, "": false, " lua": false, "a\tb": false} {
if ValidTag(tag) != ok {
t.Errorf("ValidTag(%q) = %v", tag, !ok)
}
}
}
// TestLoadRelativePaths checks that relative paths in a config file are
// relative to the file, so one directory can hold a whole site.
func TestLoadRelativePaths(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "repo-render.yaml")
src := "host: h\ngit_dir: git\nhtdocs: htdocs\nstate_dir: /abs/state\nlexer_dirs: [lexers]\nsite:\n favicon: icon.svg\n"
if err := os.WriteFile(p, []byte(src), 0o644); err != nil {
t.Fatal(err)
}
cfg, err := Load(p)
if err != nil {
t.Fatal(err)
}
for got, want := range map[string]string{
cfg.GitDir: filepath.Join(dir, "git"),
cfg.Htdocs: filepath.Join(dir, "htdocs"),
cfg.StateDir: "/abs/state",
cfg.LexerDirs[0]: filepath.Join(dir, "lexers"),
cfg.Site.Favicon: filepath.Join(dir, "icon.svg"),
cfg.Path: p,
} {
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}
}
func TestSiteURL(t *testing.T) {
cfg, err := Parse([]byte("host: localhost\n" + paths + "site:\n url: http://localhost:8080/\n"))
if err != nil {
t.Fatal(err)
}
if got := cfg.BaseURL(); got != "http://localhost:8080" {
t.Errorf("base url: %s", got)
}
if got := cfg.CloneURL("foo"); got != "http://localhost:8080/git/foo.git" {
t.Errorf("clone url: %s", got)
}
if _, err := Parse([]byte("host: localhost\nsite:\n url: localhost:8080\n")); err == nil {
t.Error("site.url without a scheme accepted")
}
}
// TestExampleConfig keeps examples/repo-render.yaml valid as shipped, and
// with every commented-out setting uncommented, so that the defaults and
// examples it shows are ones the renderer takes. A commented-out setting
// is a "#" followed by no space or by two or more; prose has one.
func TestExampleConfig(t *testing.T) {
src, err := os.ReadFile(filepath.Join("..", "..", "examples", "repo-render.yaml"))
if err != nil {
t.Fatal(err)
}
if _, err := Parse(src); err != nil {
t.Errorf("as shipped: %v", err)
}
setting := regexp.MustCompile(`(?m)^(\s*)#(\S| )`)
all := setting.ReplaceAll(src, []byte("$1$2"))
cfg, err := Parse(all)
if err != nil {
t.Fatalf("uncommented: %v\n%s", err, all)
}
if cfg.PrivateHtdocs == "" || len(cfg.Sections) != 2 || len(cfg.Lexers) != 3 || len(cfg.Repo("tomo").Branches) != 1 || cfg.Site.Footer == "" {
t.Errorf("uncommented, settings are missing: %+v", cfg)
}
d := Defaults()
if cfg.Style != d.Style || cfg.StyleLight != d.StyleLight || cfg.Site.Title != d.Site.Title {
t.Error("a default the example shows is not the default")
}
}
|