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