Files
khosra/internal/ext/scaffold/scaffold_test.go
T
bdeshi 771a491188 add khosra demo, and give the site a front page
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.
2026-08-01 02:23:37 +06:00

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")
}
}