267 lines
8.0 KiB
Go
267 lines
8.0 KiB
Go
package router
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"regexp"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"vctp/version"
|
|
)
|
|
|
|
var externalAssetRefPattern = regexp.MustCompile(`\b(?:src|href)=["']https?://`)
|
|
|
|
func TestHomePageUsesLocalVersionedAssets(t *testing.T) {
|
|
orig := version.Value
|
|
version.Value = "1.2.3"
|
|
defer func() { version.Value = orig }()
|
|
|
|
app := testRouter(t, testRouterSettings(t, false))
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
rr := httptest.NewRecorder()
|
|
app.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rr.Code)
|
|
}
|
|
body := rr.Body.String()
|
|
|
|
for _, want := range []string{
|
|
`href="/favicon.ico?v=1.2.3"`,
|
|
`href="/favicon-16x16.png?v=1.2.3"`,
|
|
`href="/favicon-32x32.png?v=1.2.3"`,
|
|
`src="/assets/js/htmx@v2.0.2.min.js"`,
|
|
`src="/assets/js/web3-charts.js?v=1.2.3"`,
|
|
`href="/assets/css/output@1.2.3.css"`,
|
|
`href="/assets/css/web3.css?v=1.2.3"`,
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected response body to contain %q", want)
|
|
}
|
|
}
|
|
|
|
if externalAssetRefPattern.MatchString(body) {
|
|
t.Fatalf("home page contains external asset URL: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestSwaggerUIUsesLocalAssetsOnly(t *testing.T) {
|
|
app := testRouter(t, testRouterSettings(t, false))
|
|
req := httptest.NewRequest(http.MethodGet, "/swagger/", nil)
|
|
rr := httptest.NewRecorder()
|
|
app.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rr.Code)
|
|
}
|
|
body := rr.Body.String()
|
|
|
|
for _, want := range []string{
|
|
`href="./swagger-ui.css"`,
|
|
`src="./swagger-ui-bundle.js"`,
|
|
`src="./swagger-ui-standalone-preset.js"`,
|
|
`src="./swagger-initializer.js"`,
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected swagger index to contain %q", want)
|
|
}
|
|
}
|
|
|
|
if externalAssetRefPattern.MatchString(body) {
|
|
t.Fatalf("swagger index contains external asset URL: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestStaticResourcesAreCacheableInReleaseMode(t *testing.T) {
|
|
orig := version.Value
|
|
version.Value = "1.2.3"
|
|
defer func() { version.Value = orig }()
|
|
|
|
app := testRouter(t, testRouterSettings(t, false))
|
|
|
|
tests := []struct {
|
|
path string
|
|
wantCacheControl string
|
|
}{
|
|
{path: "/assets/css/web3.css?v=1.2.3", wantCacheControl: "public, max-age=31536000, immutable"},
|
|
{path: "/assets/js/htmx@v2.0.2.min.js", wantCacheControl: "public, max-age=31536000, immutable"},
|
|
{path: "/favicon.ico?v=1.2.3", wantCacheControl: "public, max-age=31536000, immutable"},
|
|
{path: "/swagger/swagger-ui.css", wantCacheControl: "public, max-age=31536000"},
|
|
{path: "/swagger.json", wantCacheControl: "public, max-age=31536000"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.path, func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, tc.path, nil)
|
|
rr := httptest.NewRecorder()
|
|
app.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d for %s, got %d", http.StatusOK, tc.path, rr.Code)
|
|
}
|
|
|
|
if got := rr.Header().Get("Cache-Control"); got != tc.wantCacheControl {
|
|
t.Fatalf("unexpected Cache-Control for %s: got %q want %q", tc.path, got, tc.wantCacheControl)
|
|
}
|
|
if got := rr.Header().Get("Vary"); got != "Accept-Encoding" {
|
|
t.Fatalf("unexpected Vary for %s: %q", tc.path, got)
|
|
}
|
|
if rr.Header().Get("Expires") == "" {
|
|
t.Fatalf("expected Expires for %s", tc.path)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSwaggerJSONDefaultsToHTTPSWhenTLSEnabled(t *testing.T) {
|
|
cfg := testRouterSettings(t, false)
|
|
cfg.Values.Settings.BindDisableTLS = false
|
|
app := testRouter(t, cfg)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/swagger.json", nil)
|
|
rr := httptest.NewRecorder()
|
|
app.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rr.Code)
|
|
}
|
|
|
|
var spec struct {
|
|
Schemes []string `json:"schemes"`
|
|
}
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &spec); err != nil {
|
|
t.Fatalf("failed to decode swagger spec: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(spec.Schemes, []string{"https"}) {
|
|
t.Fatalf("unexpected schemes: got %v want %v", spec.Schemes, []string{"https"})
|
|
}
|
|
}
|
|
|
|
func TestSwaggerJSONDefaultsToHTTPWhenTLSDisabled(t *testing.T) {
|
|
cfg := testRouterSettings(t, false)
|
|
cfg.Values.Settings.BindDisableTLS = true
|
|
app := testRouter(t, cfg)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/swagger.json", nil)
|
|
rr := httptest.NewRecorder()
|
|
app.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rr.Code)
|
|
}
|
|
|
|
var spec struct {
|
|
Schemes []string `json:"schemes"`
|
|
}
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &spec); err != nil {
|
|
t.Fatalf("failed to decode swagger spec: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(spec.Schemes, []string{"http"}) {
|
|
t.Fatalf("unexpected schemes: got %v want %v", spec.Schemes, []string{"http"})
|
|
}
|
|
}
|
|
|
|
func TestSharedStylesExposeThemeTokensAndResponsiveAccessibilityRules(t *testing.T) {
|
|
app := testRouter(t, testRouterSettings(t, false))
|
|
req := httptest.NewRequest(http.MethodGet, "/assets/css/web3.css", nil)
|
|
rr := httptest.NewRecorder()
|
|
app.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rr.Code)
|
|
}
|
|
css := rr.Body.String()
|
|
|
|
assertContainsAll(t, css, []string{
|
|
":root {",
|
|
"--theme_text_primary:",
|
|
"--theme_accent_blue:",
|
|
"--theme_focus_outline:",
|
|
".web2-shell-wide {",
|
|
".web2-page-title {",
|
|
"font-size: clamp(",
|
|
".web2-table-shell {",
|
|
"overflow-x: auto;",
|
|
".web2-input:focus-visible {",
|
|
"a:focus-visible,",
|
|
"@media (max-width: 900px)",
|
|
".web2-actions .web2-button {",
|
|
"min-width: 520px;",
|
|
"@media (min-width: 1500px)",
|
|
"@media (min-width: 780px)",
|
|
"@media (min-width: 1024px)",
|
|
})
|
|
}
|
|
|
|
func TestDashboardAuthGuidanceMatchesRouteProtection(t *testing.T) {
|
|
app := testRouter(t, testRouterSettings(t, false))
|
|
|
|
homeReq := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
homeRR := httptest.NewRecorder()
|
|
app.ServeHTTP(homeRR, homeReq)
|
|
if homeRR.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, homeRR.Code)
|
|
}
|
|
homeBody := homeRR.Body.String()
|
|
assertContainsAll(t, homeBody, []string{
|
|
"POST /api/auth/login",
|
|
"Authorization: Bearer <token>",
|
|
"viewer",
|
|
"admin",
|
|
"UI pages and <code class=\"web2-code\">/metrics</code> remain public.",
|
|
})
|
|
|
|
for _, path := range []string{"/swagger/", "/metrics", "/vm/trace"} {
|
|
t.Run("public "+path, func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
rr := httptest.NewRecorder()
|
|
app.ServeHTTP(rr, req)
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d for %s, got %d", http.StatusOK, path, rr.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
protectedReq := httptest.NewRequest(http.MethodGet, "/api/report/snapshot", nil)
|
|
protectedRR := httptest.NewRecorder()
|
|
app.ServeHTTP(protectedRR, protectedReq)
|
|
if protectedRR.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected status %d for protected route, got %d", http.StatusUnauthorized, protectedRR.Code)
|
|
}
|
|
}
|
|
|
|
func TestVmTraceFormUsesLabelledInputsAndKeyboardFriendlyControls(t *testing.T) {
|
|
app := testRouter(t, testRouterSettings(t, false))
|
|
req := httptest.NewRequest(http.MethodGet, "/vm/trace", nil)
|
|
rr := httptest.NewRecorder()
|
|
app.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rr.Code)
|
|
}
|
|
body := rr.Body.String()
|
|
|
|
assertContainsAll(t, body, []string{
|
|
`<form method="get" action="/vm/trace" class="web2-form-grid">`,
|
|
`<label class="web2-label" for="vm_id">VM ID</label>`,
|
|
`<input class="web2-input" type="text" id="vm_id" name="vm_id"`,
|
|
`<label class="web2-label" for="vm_uuid">VM UUID</label>`,
|
|
`<input class="web2-input" type="text" id="vm_uuid" name="vm_uuid"`,
|
|
`<label class="web2-label" for="name">Name</label>`,
|
|
`<input class="web2-input" type="text" id="name" name="name"`,
|
|
`<button class="web3-button active" type="submit">Load VM Trace</button>`,
|
|
`<a class="web3-button" href="/vm/trace">Clear</a>`,
|
|
})
|
|
}
|
|
|
|
func assertContainsAll(t *testing.T, body string, snippets []string) {
|
|
t.Helper()
|
|
for _, snippet := range snippets {
|
|
if !strings.Contains(body, snippet) {
|
|
t.Fatalf("expected response body to contain %q", snippet)
|
|
}
|
|
}
|
|
}
|