// SPDX-License-Identifier: AGPL-3.0-or-later package web import ( "io" "io/fs" "net/http/httptest" "path" "strings" "testing" "github.com/tenseleyFlow/shithub/internal/web/render" ) // TestProductionTemplatesParse runs the embedded production templates // through render.New so any undefined-template ref or unparseable file // fails CI rather than the binary's first request after deploy. // // The check exists in addition to the targeted unit tests in // internal/web/render/render_test.go: those cover the validator's // behaviour on synthetic FS trees; this one covers the actual files // under internal/web/templates/. func TestProductionTemplatesParse(t *testing.T) { t.Parallel() _, err := render.New(TemplatesFS(), render.Options{}) if err != nil { t.Fatalf("production templates failed to parse: %v", err) } } // TestProductionTemplatesPartialsTolerateEmptyData renders every page // in the production tree with an empty map[string]any and fails the // test only if an error originates in a shared partial (filename // starting with `_`, e.g. _nav.html / _layout.html). // // Why this matters: html/template parses fine even when a partial // references a field nothing populates. The error only fires at // execute time. A live site went 500 in May 2026 when _nav.html // started referencing .GlobalSearchQuery and the homepage's // helloData struct didn't carry it. // // We pass an empty map (not a typed struct) because: // 1. map[string]any tolerates missing keys (`with .X` evaluates nil). // 2. So any error that *does* fire from inside _nav.html or // _layout.html is necessarily a real defect: a partial referencing // a field with semantics that can't be nil (e.g. ranging over a // non-map type, or calling a method on a nil interface). // // Page-internal errors (from non-partial templates) are expected with // empty data and are filtered out — those are exercised by the // handler-level tests that pass realistic fixtures. For the typed- // struct regression specifically, see the hello handler test in // internal/web/handlers/. func TestProductionTemplatesPartialsTolerateEmptyData(t *testing.T) { t.Parallel() tmplFS := TemplatesFS() r, err := render.New(tmplFS, render.Options{}) if err != nil { t.Fatalf("render.New: %v", err) } pages, err := listPages(tmplFS) if err != nil { t.Fatalf("listPages: %v", err) } if len(pages) == 0 { t.Fatal("no pages discovered under templates/ — fixture broken") } req := httptest.NewRequest("GET", "/", nil) for _, name := range pages { t.Run(name, func(t *testing.T) { t.Parallel() rw := httptest.NewRecorder() err := r.RenderPage(rw, req, name, map[string]any{}) if err != nil && errorOriginatesInPartial(err) { t.Errorf("page %q: shared partial errored on empty data: %v", name, err) } _, _ = io.Copy(io.Discard, rw.Body) }) } } func TestOrgPagesRenderSingleSharedOrgNav(t *testing.T) { t.Parallel() r, err := render.New(TemplatesFS(), render.Options{}) if err != nil { t.Fatalf("render.New: %v", err) } data := map[string]any{ "Title": "Organization", "Org": map[string]any{"Slug": "gardesk", "DisplayName": "gardesk"}, "AvatarURL": "/avatars/gardesk", "ActiveOrgNav": "repositories", "RepoCount": 5, "FilteredCount": 5, "PageCount": 1, "MemberCount": 1, "IsOwner": true, "Form": map[string]any{ "DisplayName": "gardesk", "Description": "", "Website": "", "Location": "", "BillingEmail": "", "AllowMemberRepoCreate": true, }, } req := httptest.NewRequest("GET", "/", nil) for _, page := range []string{"orgs/repositories", "orgs/settings_profile"} { t.Run(page, func(t *testing.T) { t.Parallel() rw := httptest.NewRecorder() if err := r.RenderPage(rw, req, page, data); err != nil { t.Fatalf("RenderPage: %v", err) } if got := strings.Count(rw.Body.String(), `