The demo writes a whole site root that exercises every feature: two languages with a fallback, a series with ordered chapters, a gallery, a figure, an include, extras, tags across sections, a slug with an alias, an undated page, a draft, a template override, static files and site.yaml. It generates its filler rather than copying stored files, because nothing in this repository is content (ADR-0011) — and that makes it a test of the engine rather than a fixture: anything khosra can do that the demo cannot express is a gap. Two things found by generating and then serving it, which is the whole point: `khosra check` reported the demo's own series as mixing ordered and unordered members. It was right — the chapter bodies *described* `order: 10` while the frontmatter never carried it. The checker caught its own author. And `/` was a **404**. ADR-0008 leaves the root engine-owned, which is right, but "engine-owned" was never given an answer, so a visitor to the site's own address got nothing. The root now lists every bundle, newest first, paginated like any other listing, and 404s only when nothing is published. A hand-written home page stays a separate decision, recorded as such. Verified end to end: 23 files written, 12 bundles, 12 derivatives, `check` clean, and every URL the demo promises answers — including the alias redirecting, the draft hidden, and the front page rendering through the site's *own* template override.
139 lines
4.8 KiB
Go
139 lines
4.8 KiB
Go
package scaffold
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"khosra/internal/content"
|
|
)
|
|
|
|
func TestNewWritesADirectoryBundleThatScansAsADraft(t *testing.T) {
|
|
dir := t.TempDir()
|
|
written, err := New(dir, "posts/hello-world", "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// A directory bundle, because only that shape can own local files (content-model.md).
|
|
if written != "content/posts/hello-world/index.en.md" {
|
|
t.Errorf("wrote %q, want a directory bundle", written)
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(dir, written))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Parsed by the engine's own parser, not by eye: the scaffold has to be something khosra can read.
|
|
b, err := content.Parse(strings.TrimPrefix(written, "content/"), data)
|
|
if err != nil {
|
|
t.Fatalf("the engine cannot parse its own scaffold: %v\n%s", err, data)
|
|
}
|
|
if b.Title != "Hello world" {
|
|
t.Errorf("title = %q, want one derived from the slug", b.Title)
|
|
}
|
|
if !b.Draft {
|
|
t.Error("a scaffold must be a draft: a tool that publishes when it runs publishes by accident")
|
|
}
|
|
if b.Date.IsZero() {
|
|
t.Error("no date was written")
|
|
}
|
|
if b.Key != "posts/hello-world" {
|
|
t.Errorf("key = %q", b.Key)
|
|
}
|
|
}
|
|
|
|
func TestNewHonoursLanguageAndTitle(t *testing.T) {
|
|
dir := t.TempDir()
|
|
written, err := New(dir, "pages/about", "bn", "পরিচিতি")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if written != "content/pages/about/index.bn.md" {
|
|
t.Errorf("wrote %q, want the Bengali variant", written)
|
|
}
|
|
data, _ := os.ReadFile(filepath.Join(dir, written))
|
|
if !strings.Contains(string(data), "title: পরিচিতি") {
|
|
t.Errorf("the given title should be used verbatim:\n%s", data)
|
|
}
|
|
}
|
|
|
|
func TestNewNeverOverwritesAndNeverEscapes(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if _, err := New(dir, "posts/twice", "", ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := New(dir, "posts/twice", "", ""); err == nil {
|
|
t.Error("a second run must refuse rather than replace someone's writing")
|
|
}
|
|
// os.Root refuses an escape, and the key is normalised and trimmed before it is used at all (ADR-0031).
|
|
for _, key := range []string{"../outside", "posts/../../outside", "", "/"} {
|
|
if _, err := New(dir, key, "", ""); err == nil {
|
|
t.Errorf("New(%q) should have failed", key)
|
|
}
|
|
}
|
|
if _, err := os.Stat(filepath.Join(filepath.Dir(dir), "outside")); err == nil {
|
|
t.Fatal("something was written outside the site root")
|
|
}
|
|
// The weaker check above is not enough: `../outside` does not escape the root, it lands *inside* it and
|
|
// outside content/, because path.Join collapses `..` before os.Root ever sees the name. So assert what
|
|
// should be true — content/ is the only thing this ever creates.
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, e := range entries {
|
|
if e.Name() != "content" {
|
|
t.Errorf("created %q at the site root; only content/ should ever appear", e.Name())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheDemoIsAWholeSiteThatTheEngineAccepts(t *testing.T) {
|
|
// The demo's real assertion is that khosra can serve what khosra wrote, so this checks the shape and leaves
|
|
// the serving to the web tests. Anything the engine can do that the demo cannot express is a gap.
|
|
dir := t.TempDir()
|
|
written, err := Demo(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(written) < 20 {
|
|
t.Errorf("wrote %d files, which is too few to exercise the engine", len(written))
|
|
}
|
|
// Every feature is represented, named by the file that carries it.
|
|
for _, want := range []string{
|
|
"site.yaml",
|
|
"content/posts/first-light/index.en.md", "content/posts/first-light/index.bn.md", // two languages
|
|
"content/posts/renamed-thing/index.en.md", // slug plus alias
|
|
"content/posts/unfinished.en.md", // a draft
|
|
"content/comics/the-long-monsoon/_index.en.md", // a series landing
|
|
"content/comics/the-long-monsoon/the-flood/water.jpg", // a figure's picture
|
|
"content/art/monsoon-studies/10-grey.jpg", // a gallery
|
|
"content/writing/notes-on-water/_method.md", // an include's fragment
|
|
"content/writing/notes-on-water/extras/research.md", // extras
|
|
"content/pages/about.en.md", // undated
|
|
"templates/list.html", "static/robots-note.txt",
|
|
} {
|
|
if !slices.Contains(written, want) {
|
|
t.Errorf("the demo does not cover %s", want)
|
|
}
|
|
}
|
|
|
|
// The bundles it wrote are bundles: parsed by the engine's own scanner, not by eye.
|
|
bundles, problems, err := content.ScanReport(os.DirFS(dir))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(problems) != 0 {
|
|
t.Errorf("the engine cannot read its own demo: %v", problems)
|
|
}
|
|
if len(bundles) < 10 {
|
|
t.Errorf("scanned %d bundles, want the whole demo", len(bundles))
|
|
}
|
|
|
|
// And it refuses to write over a site that already has content.
|
|
if _, err := Demo(dir); err == nil {
|
|
t.Error("a demo that overwrites somebody's site is worse than no demo")
|
|
}
|
|
}
|