From 2687fe360321e49adfc5f2bb843967faf74b308b Mon Sep 17 00:00:00 2001 From: Matthew Knight Date: Wed, 18 Feb 2026 17:37:31 -0800 Subject: [PATCH] Fix rendering, create render tests --- internal/templates/render_test.go | 297 ++++++++++++++++++++ web/templates/pages/admin/tickets/list.html | 6 +- web/templates/pages/tickets/list.html | 3 +- 3 files changed, 303 insertions(+), 3 deletions(-) create mode 100644 internal/templates/render_test.go diff --git a/internal/templates/render_test.go b/internal/templates/render_test.go new file mode 100644 index 0000000..4d9f39c --- /dev/null +++ b/internal/templates/render_test.go @@ -0,0 +1,297 @@ +package templates + +import ( + "bytes" + "os" + "path/filepath" + "testing" + "time" + + "github.com/google/uuid" + "github.com/mattnite/forgejo-tickets/internal/models" +) + +// TestAllTemplatesExecute parses every template and executes it with +// representative data wrapped in PageData, exactly as the real Renderer does. +// This catches errors like accessing a field on PageData that only exists in +// the Data map (e.g. $.ProductFilter instead of using a captured variable). +func TestAllTemplatesExecute(t *testing.T) { + now := time.Now() + dueDate := now.Add(24 * time.Hour) + + testUser := &models.User{ + ID: uuid.New(), + Email: "test@example.com", + Name: "Test User", + Role: "customer", + } + + adminUser := &models.User{ + ID: uuid.New(), + Email: "admin@example.com", + Name: "Admin", + Role: "admin", + } + + repo := models.Repo{ + ID: uuid.New(), + Name: "Test Product", + Slug: "test-product", + ForgejoOwner: "owner", + ForgejoRepo: "repo", + } + + ticketID := uuid.New() + + // Each entry maps a template name to data that exercises all template paths, + // including {{range}} loops that re-scope the dot. + cases := map[string]struct { + data interface{} + user *models.User + }{ + "home": {data: nil, user: nil}, + + "login": {data: map[string]interface{}{ + "GoogleEnabled": true, + "MicrosoftEnabled": false, + "AppleEnabled": false, + "Error": "", + "Email": "test@example.com", + }}, + + "register": {data: map[string]interface{}{ + "Name": "Test", + "Email": "test@example.com", + "Error": "", + }}, + + "forgot-password": {data: nil}, + + "reset-password": {data: map[string]interface{}{ + "Token": "abc123", + "Error": "", + }}, + + "change-password": {data: map[string]interface{}{ + "HasPassword": true, + "Error": "", + }, user: testUser}, + + "tickets/list": {data: map[string]interface{}{ + "Tickets": []map[string]interface{}{ + { + "ID": ticketID, + "Title": "Test Ticket", + "Status": "open", + "Priority": "high", + "Pinned": false, + "RepoName": "Test Product", + "DueDate": &dueDate, + "CreatedAt": now, + }, + }, + "StatusFilter": "open", + "PriorityFilter": "", + "ProductFilter": "Test Product", + "Products": []string{"Test Product", "Other Product"}, + "Page": 1, + "TotalPages": 2, + }, user: testUser}, + + "tickets/new": {data: map[string]interface{}{ + "Repos": []models.Repo{repo}, + "Error": "", + "Title": "", + "Description": "", + "RepoID": "", + }, user: testUser}, + + "tickets/detail": {data: map[string]interface{}{ + "Ticket": map[string]interface{}{ + "ID": ticketID, + "Title": "Test Ticket", + "Description": "A test description", + "Status": "open", + "Priority": "high", + "Pinned": false, + "Assignees": "Dev User", + "DueDate": &dueDate, + "Attachments": []map[string]interface{}{ + {"ID": int64(1), "Name": "file.txt", "Size": int64(1024)}, + }, + "CreatedAt": now, + }, + "Repo": repo, + "Timeline": []map[string]interface{}{ + { + "Type": "comment", + "Body": "A comment", + "AuthorName": "Dev", + "IsTeam": true, + "EventText": "", + "CreatedAt": now, + "Attachments": []map[string]interface{}{ + {"ID": int64(2), "Name": "img.png"}, + }, + "CommentID": int64(100), + }, + { + "Type": "status_change", + "Body": "", + "AuthorName": "Dev", + "IsTeam": true, + "EventText": "changed status to open", + "CreatedAt": now, + "Attachments": []map[string]interface{}{}, + "CommentID": int64(0), + }, + }, + "RelatedIssues": []map[string]interface{}{ + {"Number": int64(5), "Title": "Related", "IsVisible": true, "DisplayText": "Related", "TicketID": uuid.New().String()}, + }, + "Mentions": map[string]string{"dev": "Dev User"}, + }, user: testUser}, + + "admin/dashboard": {data: map[string]interface{}{ + "UserCount": int64(10), + "TotalTickets": int64(50), + "OpenTickets": int64(20), + "InProgressTickets": int64(15), + "ClosedTickets": int64(15), + }, user: adminUser}, + + "admin/users/list": {data: map[string]interface{}{ + "Users": []models.User{*adminUser}, + }, user: adminUser}, + + "admin/users/new": {data: map[string]interface{}{ + "Error": "", + "Name": "", + "Email": "", + }, user: adminUser}, + + "admin/users/pending": {data: map[string]interface{}{ + "Users": []models.User{{ + ID: uuid.New(), + Email: "pending@example.com", + Name: "Pending User", + Role: "customer", + }}, + }, user: adminUser}, + + "admin/users/detail": {data: map[string]interface{}{ + "User": *testUser, + "Tickets": []map[string]interface{}{}, + "AllRepos": []models.Repo{repo}, + "AssignedRepoIDs": map[string]bool{ + repo.ID.String(): true, + }, + }, user: adminUser}, + + "admin/repos/list": {data: map[string]interface{}{ + "Repos": []models.Repo{repo}, + "BaseURL": "https://example.com", + }, user: adminUser}, + + "admin/repos/new": {data: map[string]interface{}{ + "Error": "", + "Name": "", + "Slug": "", + "ForgejoOwner": "", + "ForgejoRepo": "", + }, user: adminUser}, + + "admin/repos/edit": {data: map[string]interface{}{ + "Repo": repo, + "BaseURL": "https://example.com", + "ForgejoURL": "https://forgejo.example.com", + "RepoPermErr": "", + }, user: adminUser}, + + "admin/tickets/list": {data: map[string]interface{}{ + "Tickets": []map[string]interface{}{ + { + "ID": ticketID, + "Title": "Test Ticket", + "Status": "open", + "Priority": "high", + "Pinned": false, + "RepoName": "Test Product", + "UserName": "Test User", + "DueDate": &dueDate, + "CreatedAt": now, + }, + }, + "StatusFilter": "", + "PriorityFilter": "", + "ProductFilter": "Test Product", + "ReporterFilter": "Test User", + "Products": []string{"Test Product", "Other Product"}, + "Reporters": []string{"Test User", "Other User"}, + "Page": 1, + "TotalPages": 1, + }, user: adminUser}, + + "admin/tickets/detail": {data: map[string]interface{}{ + "Ticket": map[string]interface{}{ + "ID": ticketID, + "Title": "Test Ticket", + "Description": "A test description", + "Status": "open", + "Priority": "high", + "Pinned": false, + "Assignees": "Dev User", + "DueDate": &dueDate, + "Attachments": []map[string]interface{}{}, + "ForgejoIssueNumber": int64(1), + "CreatedAt": now, + }, + "User": *testUser, + "Repo": repo, + "Timeline": []map[string]interface{}{ + { + "Type": "comment", + "Body": "A comment", + "AuthorName": "Dev", + "IsTeam": true, + "EventText": "", + "CreatedAt": now, + "Attachments": []map[string]interface{}{}, + "CommentID": int64(100), + }, + }, + "RelatedIssues": []map[string]interface{}{}, + "Mentions": map[string]string{}, + }, user: adminUser}, + } + + // NewRenderer expects web/templates relative to cwd; chdir to project root. + if err := os.Chdir(filepath.Join("..", "..")); err != nil { + t.Fatalf("chdir to project root: %v", err) + } + + r, err := NewRenderer() + if err != nil { + t.Fatalf("NewRenderer: %v", err) + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + pd := PageData{ + User: tc.user, + CSRFToken: "test-csrf-token", + Data: tc.data, + } + + tmpl, ok := r.templates[name] + if !ok { + t.Fatalf("template %q not found", name) + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, pd); err != nil { + t.Errorf("template %q execution failed: %v", name, err) + } + }) + } +} diff --git a/web/templates/pages/admin/tickets/list.html b/web/templates/pages/admin/tickets/list.html index 287de59..99ae20d 100644 --- a/web/templates/pages/admin/tickets/list.html +++ b/web/templates/pages/admin/tickets/list.html @@ -5,6 +5,8 @@ {{with .Data}} {{$filters := dict "status" .StatusFilter "priority" .PriorityFilter "product" .ProductFilter "reporter" .ReporterFilter}} +{{$productFilter := .ProductFilter}} +{{$reporterFilter := .ReporterFilter}}
Status @@ -25,7 +27,7 @@ Product All {{range .Products}} - {{.}} + {{.}} {{end}}
{{end}} @@ -34,7 +36,7 @@ Reporter All {{range .Reporters}} - {{.}} + {{.}} {{end}}
{{end}} diff --git a/web/templates/pages/tickets/list.html b/web/templates/pages/tickets/list.html index 3e145c0..a6fcba8 100644 --- a/web/templates/pages/tickets/list.html +++ b/web/templates/pages/tickets/list.html @@ -8,6 +8,7 @@ {{with .Data}} {{$filters := dict "status" .StatusFilter "priority" .PriorityFilter "product" .ProductFilter}} + {{$productFilter := .ProductFilter}}
Status @@ -28,7 +29,7 @@ Product All {{range .Products}} - {{.}} + {{.}} {{end}}
{{end}}