From a496864762a5c7d89c0de4fac3a3be790b7cc12e Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Tue, 28 Oct 2025 16:29:39 +0100 Subject: [PATCH] hscontrol: add template HTML consistency test Add test to validate HTML template output consistency across all templates (OIDC callback, registration, Windows, Apple). Verifies all templates produce valid HTML5 with: - Proper DOCTYPE declaration - HTML5 lang attribute - UTF-8 charset - Viewport meta tag - Semantic HTML structure Ensures template refactoring maintains standards compliance. --- hscontrol/templates_consistency_test.go | 213 ++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 hscontrol/templates_consistency_test.go diff --git a/hscontrol/templates_consistency_test.go b/hscontrol/templates_consistency_test.go new file mode 100644 index 00000000..65d14346 --- /dev/null +++ b/hscontrol/templates_consistency_test.go @@ -0,0 +1,213 @@ +package hscontrol + +import ( + "strings" + "testing" + + "github.com/juanfont/headscale/hscontrol/templates" + "github.com/juanfont/headscale/hscontrol/types" + "github.com/stretchr/testify/assert" +) + +func TestTemplateHTMLConsistency(t *testing.T) { + // Test all templates produce consistent modern HTML + testCases := []struct { + name string + html string + }{ + { + name: "OIDC Callback", + html: templates.OIDCCallback("test@example.com", "Logged in").Render(), + }, + { + name: "Register Web", + html: templates.RegisterWeb(types.RegistrationID("test-key-123")).Render(), + }, + { + name: "Windows Config", + html: templates.Windows("https://example.com").Render(), + }, + { + name: "Apple Config", + html: templates.Apple("https://example.com").Render(), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Check DOCTYPE + assert.True(t, strings.HasPrefix(tc.html, ""), + "%s should start with ", tc.name) + + // Check HTML5 lang attribute + assert.Contains(t, tc.html, ``, + "%s should have html lang=\"en\"", tc.name) + + // Check UTF-8 charset + assert.Contains(t, tc.html, `charset="UTF-8"`, + "%s should have UTF-8 charset", tc.name) + + // Check viewport meta tag + assert.Contains(t, tc.html, `name="viewport"`, + "%s should have viewport meta tag", tc.name) + + // Check IE compatibility meta tag + assert.Contains(t, tc.html, `X-UA-Compatible`, + "%s should have X-UA-Compatible meta tag", tc.name) + + // Check closing tags + assert.Contains(t, tc.html, "", + "%s should have closing html tag", tc.name) + assert.Contains(t, tc.html, "", + "%s should have closing head tag", tc.name) + assert.Contains(t, tc.html, "", + "%s should have closing body tag", tc.name) + }) + } +} + +func TestTemplateModernHTMLFeatures(t *testing.T) { + testCases := []struct { + name string + html string + }{ + { + name: "OIDC Callback", + html: templates.OIDCCallback("test@example.com", "Logged in").Render(), + }, + { + name: "Register Web", + html: templates.RegisterWeb(types.RegistrationID("test-key-123")).Render(), + }, + { + name: "Windows Config", + html: templates.Windows("https://example.com").Render(), + }, + { + name: "Apple Config", + html: templates.Apple("https://example.com").Render(), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Check no deprecated tags + assert.NotContains(t, tc.html, " tag", tc.name) + assert.NotContains(t, tc.html, " tag", tc.name) + + // Check modern structure + assert.Contains(t, tc.html, "", + "%s should have section", tc.name) + assert.Contains(t, tc.html, " section", tc.name) + assert.Contains(t, tc.html, "", + "%s should have <title> tag", tc.name) + }) + } +} + +func TestTemplateExternalLinkSecurity(t *testing.T) { + // Test that all external links (http/https) have proper security attributes + testCases := []struct { + name string + html string + externalURLs []string // URLs that should have security attributes + }{ + { + name: "OIDC Callback", + html: templates.OIDCCallback("test@example.com", "Logged in").Render(), + externalURLs: []string{ + "https://github.com/juanfont/headscale/tree/main/docs", + "https://tailscale.com/kb/", + }, + }, + { + name: "Register Web", + html: templates.RegisterWeb(types.RegistrationID("test-key-123")).Render(), + externalURLs: []string{}, // No external links + }, + { + name: "Windows Config", + html: templates.Windows("https://example.com").Render(), + externalURLs: []string{ + "https://tailscale.com/download/windows", + }, + }, + { + name: "Apple Config", + html: templates.Apple("https://example.com").Render(), + externalURLs: []string{ + "https://apps.apple.com/app/tailscale/id1470499037", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for _, url := range tc.externalURLs { + // Find the link tag containing this URL + if !strings.Contains(tc.html, url) { + t.Errorf("%s should contain external link %s", tc.name, url) + continue + } + + // Check for rel="noreferrer noopener" + // We look for the pattern: href="URL"...rel="noreferrer noopener" + // The attributes might be in any order, so we check within a reasonable window + idx := strings.Index(tc.html, url) + if idx == -1 { + continue + } + + // Look for the closing > of the <a> tag (within 200 chars should be safe) + endIdx := strings.Index(tc.html[idx:idx+200], ">") + if endIdx == -1 { + endIdx = 200 + } + + linkTag := tc.html[idx : idx+endIdx] + + assert.Contains(t, linkTag, `rel="noreferrer noopener"`, + "%s external link %s should have rel=\"noreferrer noopener\"", tc.name, url) + assert.Contains(t, linkTag, `target="_blank"`, + "%s external link %s should have target=\"_blank\"", tc.name, url) + } + }) + } +} + +func TestTemplateAccessibilityAttributes(t *testing.T) { + // Test that all templates have proper accessibility attributes + testCases := []struct { + name string + html string + }{ + { + name: "OIDC Callback", + html: templates.OIDCCallback("test@example.com", "Logged in").Render(), + }, + { + name: "Register Web", + html: templates.RegisterWeb(types.RegistrationID("test-key-123")).Render(), + }, + { + name: "Windows Config", + html: templates.Windows("https://example.com").Render(), + }, + { + name: "Apple Config", + html: templates.Apple("https://example.com").Render(), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Check for translate="no" on body tag to prevent browser translation + // This is important for technical documentation with commands + assert.Contains(t, tc.html, `translate="no"`, + "%s should have translate=\"no\" attribute on body tag", tc.name) + }) + } +}