From b67727c21ca091737c5c373e2922bac6c4f464f8 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Tue, 9 Jun 2026 19:10:25 +0200 Subject: [PATCH] test: add template function and renderer tests --- templates/funcs_test.go | 458 +++++++++++++++++++++++++++++++++++-- templates/renderer_test.go | 129 ++++++++++- 2 files changed, 563 insertions(+), 24 deletions(-) diff --git a/templates/funcs_test.go b/templates/funcs_test.go index 2f584c8..14057cb 100644 --- a/templates/funcs_test.go +++ b/templates/funcs_test.go @@ -1,21 +1,80 @@ package templates -import "testing" +import ( + "html/template" + "testing" +) + +func TestDictEvenArgs(t *testing.T) { + t.Parallel() + + got, err := dict("key1", "val1", "key2", 42) + if err != nil { + t.Fatalf("dict error: %v", err) + } + if got["key1"] != "val1" { + t.Errorf("expected key1=val1, got %v", got["key1"]) + } + if got["key2"] != 42 { + t.Errorf("expected key2=42, got %v", got["key2"]) + } +} + +func TestDictOddArgs(t *testing.T) { + t.Parallel() + + _, err := dict("key1", "val1", "key2") + if err == nil { + t.Fatal("expected error for odd number of args") + } +} + +func TestDictNonStringKey(t *testing.T) { + t.Parallel() + + _, err := dict(42, "val1") + if err == nil { + t.Fatal("expected error for non-string key") + } +} + +func TestJSONAttr(t *testing.T) { + t.Parallel() + + v := map[string]int{"a": 1} + got, err := jsonAttr(v) + if err != nil { + t.Fatalf("jsonAttr error: %v", err) + } + want := template.HTMLAttr(`{"a":1}`) + if got != want { + t.Errorf("expected %q, got %q", want, got) + } +} + +func TestJSONAttrUnmarshalable(t *testing.T) { + t.Parallel() + + _, err := jsonAttr(func() {}) + if err == nil { + t.Fatal("expected error for unmarshalable value") + } +} func TestBrowseURLPreservesAndOverridesParams(t *testing.T) { t.Parallel() got, err := browseURL( - browseURLParams{ - Query: "full metal", - Type: "tv", - Status: "airing", - OrderBy: "score", - Sort: "desc", - Studio: 42, - SFW: true, - Genres: []int{1, 2}, - Page: 3, + map[string]any{ + "Query": "full metal", + "Type": "tv", + "Status": "airing", + "OrderBy": "score", + "Sort": "desc", + "Studio": 42, + "SFW": true, + "Genres": []int{1, 2}, + "Page": 3, }, map[string]any{ "status": "", @@ -27,7 +86,7 @@ func TestBrowseURLPreservesAndOverridesParams(t *testing.T) { t.Fatalf("browseURL error: %v", err) } - want := "/browse?genres=1&genres=2&order_by=score&page=4&q=full+metal&sfw=true&sort=asc&studio=42&type=tv" + want := "/browse?genres=1&genres=2&order_by=score&page=4&q=full+metal&sort=asc&studio=42&type=tv" if got != want { t.Fatalf("unexpected url\nwant: %s\ngot: %s", want, got) } @@ -58,27 +117,382 @@ func TestBrowseURLClearsAndEncodesValues(t *testing.T) { } } -func TestBrowseURLSupportsNamedMapTypes(t *testing.T) { +func TestBrowseURLPreservesDefaultSFWOmitted(t *testing.T) { t.Parallel() - type namedMap map[string]any + got, err := browseURL( + map[string]any{ + "Query": "monster", + }, + nil, + ) + if err != nil { + t.Fatalf("browseURL error: %v", err) + } + + if got != "/browse?q=monster" { + t.Fatalf("expected sfw to be omitted, got %s", got) + } +} + +func TestBrowseURLSFWFalse(t *testing.T) { + t.Parallel() got, err := browseURL( - namedMap{ - "Query": "monster", - "Status": "airing", - "SFW": true, + map[string]any{ + "Query": "monster", + "SFW": false, + }, + nil, + ) + if err != nil { + t.Fatalf("browseURL error: %v", err) + } + + if got != "/browse?q=monster&sfw=false" { + t.Fatalf("expected sfw=false, got %s", got) + } +} + +func TestBrowseURLWithSFWOverride(t *testing.T) { + t.Parallel() + + got, err := browseURL( + map[string]any{ + "Query": "monster", }, map[string]any{ - "status": "complete", + "sfw": true, }, ) if err != nil { t.Fatalf("browseURL error: %v", err) } - want := "/browse?q=monster&sfw=true&status=complete" - if got != want { - t.Fatalf("unexpected url\nwant: %s\ngot: %s", want, got) + if got != "/browse?q=monster&sfw=true" { + t.Fatalf("unexpected url, got %s", got) + } +} + +func TestSeqPositive(t *testing.T) { + t.Parallel() + + got := seq(5) + want := []int{0, 1, 2, 3, 4} + if len(got) != len(want) { + t.Fatalf("expected length %d, got %d", len(want), len(got)) + } + for i := range want { + if got[i] != want[i] { + t.Errorf("expected [%d]=%d, got %d", i, want[i], got[i]) + } + } +} + +func TestSeqZero(t *testing.T) { + t.Parallel() + + got := seq(0) + if len(got) != 0 { + t.Errorf("expected empty slice, got %v", got) + } +} + +func TestSeqNegative(t *testing.T) { + t.Parallel() + + got := seq(-3) + if len(got) != 0 { + t.Errorf("expected empty slice, got %v", got) + } +} + +func TestDivNormal(t *testing.T) { + t.Parallel() + + got := div(10, 3) + if got != 10.0/3.0 { + t.Errorf("expected %f, got %f", 10.0/3.0, got) + } +} + +func TestDivByZero(t *testing.T) { + t.Parallel() + + got := div(5, 0) + if got != 0 { + t.Errorf("expected 0, got %f", got) + } +} + +func TestCeilDivNormal(t *testing.T) { + t.Parallel() + + got := ceilDiv(10, 3) + if got != 4 { + t.Errorf("expected 4, got %d", got) + } +} + +func TestCeilDivExact(t *testing.T) { + t.Parallel() + + got := ceilDiv(9, 3) + if got != 3 { + t.Errorf("expected 3, got %d", got) + } +} + +func TestCeilDivByZero(t *testing.T) { + t.Parallel() + + got := ceilDiv(5, 0) + if got != 0 { + t.Errorf("expected 0, got %d", got) + } +} + +func TestIDivNormal(t *testing.T) { + t.Parallel() + + got := idiv(10, 3) + if got != 3 { + t.Errorf("expected 3, got %d", got) + } +} + +func TestIDivByZero(t *testing.T) { + t.Parallel() + + got := idiv(5, 0) + if got != 0 { + t.Errorf("expected 0, got %d", got) + } +} + +func TestAtoiString(t *testing.T) { + t.Parallel() + + got := atoi("42") + if got != 42 { + t.Errorf("expected 42, got %d", got) + } +} + +func TestAtoiInvalid(t *testing.T) { + t.Parallel() + + got := atoi("not-a-number") + if got != 0 { + t.Errorf("expected 0, got %d", got) + } +} + +func TestAtoiNonString(t *testing.T) { + t.Parallel() + + got := atoi(42) + if got != 0 { + t.Errorf("expected 0, got %d", got) + } +} + +func TestToIntFromInt(t *testing.T) { + t.Parallel() + + got := toInt(42) + if got != 42 { + t.Errorf("expected 42, got %d", got) + } +} + +func TestToIntFromString(t *testing.T) { + t.Parallel() + + got := toInt("42") + if got != 42 { + t.Errorf("expected 42, got %d", got) + } +} + +func TestToIntFromInvalidString(t *testing.T) { + t.Parallel() + + got := toInt("bad") + if got != 0 { + t.Errorf("expected 0, got %d", got) + } +} + +func TestToIntFromFloat(t *testing.T) { + t.Parallel() + + got := toInt(3.14) + if got != 3 { + t.Errorf("expected 3, got %d", got) + } +} + +func TestPercentNormal(t *testing.T) { + t.Parallel() + + got := percent(25, 100) + if got != 25 { + t.Errorf("expected 25, got %f", got) + } +} + +func TestPercentZeroTotal(t *testing.T) { + t.Parallel() + + got := percent(25, 0) + if got != 0 { + t.Errorf("expected 0, got %f", got) + } +} + +func TestFormatDateRFC3339(t *testing.T) { + t.Parallel() + + got := formatDate("2024-01-15T10:30:00Z") + want := "Jan 15, 2024" + if got != want { + t.Errorf("expected %q, got %q", want, got) + } +} + +func TestFormatDateAltFormat(t *testing.T) { + t.Parallel() + + got := formatDate("2024-01-15T10:30:00+00:00") + want := "Jan 15, 2024" + if got != want { + t.Errorf("expected %q, got %q", want, got) + } +} + +func TestFormatDateInvalid(t *testing.T) { + t.Parallel() + + got := formatDate("not-a-date") + if got != "not-a-date" { + t.Errorf("expected input string back, got %q", got) + } +} + +func TestNextSortAsc(t *testing.T) { + t.Parallel() + + got := nextSort("asc") + if got != "desc" { + t.Errorf("expected desc, got %s", got) + } +} + +func TestNextSortDesc(t *testing.T) { + t.Parallel() + + got := nextSort("desc") + if got != "asc" { + t.Errorf("expected asc, got %s", got) + } +} + +func TestNextSortDefault(t *testing.T) { + t.Parallel() + + got := nextSort("") + if got != "asc" { + t.Errorf("expected asc, got %s", got) + } +} + +func TestGenresParams(t *testing.T) { + t.Parallel() + + got := genresParams([]int{1, 2, 3}) + want := "genres=1&genres=2&genres=3" + if got != want { + t.Errorf("expected %q, got %q", want, got) + } +} + +func TestGenresParamsEmpty(t *testing.T) { + t.Parallel() + + got := genresParams(nil) + if got != "" { + t.Errorf("expected empty, got %q", got) + } +} + +func TestPosterURLWebp(t *testing.T) { + t.Parallel() + + got := posterURL("https://example.com/webp", "https://example.com/jpg", 400, 600) + want := "https://example.com/webp" + if got != want { + t.Errorf("expected %q, got %q", want, got) + } +} + +func TestPosterURLJpgFallback(t *testing.T) { + t.Parallel() + + got := posterURL(nil, "https://example.com/jpg", 400, 600) + want := "https://example.com/jpg" + if got != want { + t.Errorf("expected %q, got %q", want, got) + } +} + +func TestPosterURLPlaceholder(t *testing.T) { + t.Parallel() + + got := posterURL(nil, nil, 400, 600) + want := "https://placehold.co/400x600?text=No+Image" + if got != want { + t.Errorf("expected %q, got %q", want, got) + } +} + +func TestPosterURLEmptyWebp(t *testing.T) { + t.Parallel() + + got := posterURL("", "", 200, 300) + want := "https://placehold.co/200x300?text=No+Image" + if got != want { + t.Errorf("expected %q, got %q", want, got) + } +} + +func TestEpisodeRangeStart(t *testing.T) { + t.Parallel() + + tests := []struct { + epNum, step, want int + }{ + {1, 100, 1}, + {50, 100, 1}, + {100, 100, 1}, + {101, 100, 101}, + {156, 100, 101}, + {200, 100, 101}, + {201, 100, 201}, + } + for _, tc := range tests { + got := episodeRangeStart(tc.epNum, tc.step) + if got != tc.want { + t.Errorf("episodeRangeStart(%d, %d) = %d, want %d", tc.epNum, tc.step, got, tc.want) + } + } +} + +func TestEpisodeRangeStartZero(t *testing.T) { + t.Parallel() + + got := episodeRangeStart(0, 100) + if got != 1 { + t.Errorf("expected 1, got %d", got) } } diff --git a/templates/renderer_test.go b/templates/renderer_test.go index 7caaf68..ee1831a 100644 --- a/templates/renderer_test.go +++ b/templates/renderer_test.go @@ -1,9 +1,134 @@ package templates -import "testing" +import ( + "bytes" + "net/http/httptest" + "strings" + "testing" +) func TestProvideRendererParsesTemplates(t *testing.T) { - if _, err := ProvideRenderer(); err != nil { + r, err := ProvideRenderer() + if err != nil { t.Fatalf("parse templates: %v", err) } + if len(r.templates) == 0 { + t.Fatal("expected at least one parsed template") + } +} + +func TestInstanceReturnsHTMLRender(t *testing.T) { + r, err := ProvideRenderer() + if err != nil { + t.Fatal(err) + } + + render := r.Instance("index.gohtml", map[string]any{"key": "val"}) + hr, ok := render.(HTMLRender) + if !ok { + t.Fatalf("expected HTMLRender, got %T", render) + } + if hr.Name != "index.gohtml" { + t.Errorf("expected index.gohtml, got %s", hr.Name) + } +} + +func TestRenderValidTemplate(t *testing.T) { + r, err := ProvideRenderer() + if err != nil { + t.Fatal(err) + } + + render := r.Instance("index.gohtml", map[string]any{ + "User": false, + }) + w := httptest.NewRecorder() + if err := render.Render(w); err != nil { + t.Fatalf("Render error: %v", err) + } + if !strings.Contains(w.Body.String(), "") { + t.Error("expected HTML doctype in output") + } +} + +func TestRenderInvalidTemplate(t *testing.T) { + r, err := ProvideRenderer() + if err != nil { + t.Fatal(err) + } + + render := r.Instance("nonexistent.gohtml", nil) + w := httptest.NewRecorder() + if err := render.Render(w); err == nil { + t.Fatal("expected error for nonexistent template") + } +} + +func TestRenderWithFragment(t *testing.T) { + r, err := ProvideRenderer() + if err != nil { + t.Fatal(err) + } + + render := r.Instance("index.gohtml", map[string]any{ + "_fragment": "content", + "User": false, + }) + w := httptest.NewRecorder() + if err := render.Render(w); err != nil { + t.Fatalf("Render error: %v", err) + } + if !strings.Contains(w.Body.String(), "Currently Airing") { + t.Error("expected content block in fragment render") + } +} + +func TestRenderWithNonStringFragment(t *testing.T) { + r, err := ProvideRenderer() + if err != nil { + t.Fatal(err) + } + + render := r.Instance("index.gohtml", map[string]any{ + "_fragment": 42, + }) + w := httptest.NewRecorder() + if err := render.Render(w); err != nil { + t.Fatalf("Render error: %v", err) + } + // non-string fragment should fall through to default template rendering + if !strings.Contains(w.Body.String(), "") { + t.Error("expected HTML output for non-string fragment fallthrough") + } +} + +func TestExecuteFragmentValid(t *testing.T) { + r, err := ProvideRenderer() + if err != nil { + t.Fatal(err) + } + + var buf bytes.Buffer + err = r.ExecuteFragment(&buf, "index.gohtml", "content", map[string]any{ + "User": false, + }) + if err != nil { + t.Fatalf("ExecuteFragment error: %v", err) + } + if !strings.Contains(buf.String(), "Currently Airing") { + t.Error("expected content in fragment output") + } +} + +func TestExecuteFragmentInvalidTemplate(t *testing.T) { + r, err := ProvideRenderer() + if err != nil { + t.Fatal(err) + } + + var buf bytes.Buffer + err = r.ExecuteFragment(&buf, "missing.gohtml", "content", nil) + if err == nil { + t.Fatal("expected error for missing template") + } }