mirror of
https://github.com/juanfont/headscale.git
synced 2026-04-17 06:19:51 +02:00
Generalise the registration pipeline to a more general auth pipeline supporting both node registrations and SSH check auth requests. Rename RegistrationID to AuthID, unexport AuthRequest fields, and introduce AuthVerdict to unify the auth finish API. Add the urlParam generic helper for extracting typed URL parameters from chi routes, used by the new auth request handler. Updates #1850
214 lines
5.8 KiB
Go
214 lines
5.8 KiB
Go
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.AuthID("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, "<!DOCTYPE html>"),
|
|
"%s should start with <!DOCTYPE html>", tc.name)
|
|
|
|
// Check HTML5 lang attribute
|
|
assert.Contains(t, tc.html, `<html lang="en">`,
|
|
"%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, "</html>",
|
|
"%s should have closing html tag", tc.name)
|
|
assert.Contains(t, tc.html, "</head>",
|
|
"%s should have closing head tag", tc.name)
|
|
assert.Contains(t, tc.html, "</body>",
|
|
"%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.AuthID("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, "<font",
|
|
"%s should not use deprecated <font> tag", tc.name)
|
|
assert.NotContains(t, tc.html, "<center",
|
|
"%s should not use deprecated <center> tag", tc.name)
|
|
|
|
// Check modern structure
|
|
assert.Contains(t, tc.html, "<head>",
|
|
"%s should have <head> section", tc.name)
|
|
assert.Contains(t, tc.html, "<body",
|
|
"%s should have <body> section", tc.name)
|
|
assert.Contains(t, tc.html, "<title>",
|
|
"%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://headscale.net/stable/",
|
|
"https://tailscale.com/kb/",
|
|
},
|
|
},
|
|
{
|
|
name: "Register Web",
|
|
html: templates.RegisterWeb(types.AuthID("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.AuthID("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)
|
|
})
|
|
}
|
|
}
|