mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-15 05:29:28 +00:00
fix(webui): version display, CSP guards, config traps, bucket-aligned stacking
Review fix round (4 Important + 2 folded): 1. Version display: OverviewView.vue was missing the muted version line the old vanilla-JS page rendered (types/api.ts already declared overview.version) - added. 2. CSP invariant now guarded twice: webui/generate.mjs's new assertNoInlineAssets fails the build if dist/index.html contains an inline <script> (no src= attribute) or any <style> tag - the exact condition the tightened admin CSP (script-src/style-src 'self', no 'unsafe-inline') depends on. admin_test.go's new TestAdminIndexHTML_NoInlineAssets re-asserts the identical invariant server-side, against the decoded adminIndexHTML admin.go actually serves, so a regression is caught even if admin_assets_gen.go were regenerated with a modified copy of generate.mjs. 3. webui/components.json's iconLibrary:"lucide"/font:"inter" fields cannot be fixed in place - checked shadcn-vue's own schema (PRESET_ICON_LIBRARIES/PRESET_FONTS in the installed package): no "none"/fontawesome/system choice exists for either, and JSON (unlike the tsconfig.*.json files touched in the previous commit) has no comment syntax to warn inline. Documented instead: webui/README.md gained a loud warning section naming the exact regression (already hit twice this branch - a re-added Google Fonts import, a re-added lucide icon import) and the two-step fix to apply after any future shadcn-vue add/init call. 4. Real-CSP verification: the browser pass earlier in this branch ran under vite preview, which sends no CSP header at all. New integration/integration_test.go: TestAdminAssetCSP fetches GET /admin against the real Traefik+Yaegi pipeline, extracts the asset URL from its <script src="..."> tag, fetches that asset, and asserts both responses carry the exact adminCSP string byte-for-byte. admin.go's serveAdminAsset now also sets Content-Security-Policy (previously JSON/page routes only) so this holds for a direct navigation to an asset URL, not just as a same-origin sub-resource of GET /admin; admin_test.go's TestAdminAsset_Serving updated to match. 5. (folded) UsageChart.vue stacked the "tokens" tab's two series (tokin/tokout - two separate HTTP requests, not one atomic call) by positional index. A window rollover landing between the two requests could leave one series one bucket ahead, mis-stacking that refresh. Now aligns both series by their bucket key (a sorted union of every active series' buckets - the fixed-width, zero-padded bucket strings sort chronologically by construction), with a missing bucket in one series reading as 0 rather than shifting the whole series. 6. (folded) AuthGate.vue now triggers an immediate dashboard.refresh() on successful key submit, instead of waiting up to 5s for the next poll tick - the dashboard no longer sits blank right after auth. Rebuilt via make admin-ui; admin_assets_gen.go regenerated.
This commit is contained in:
@@ -26,6 +26,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -946,6 +947,70 @@ func TestAudioTranscriptions(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// adminCSPHeader mirrors admin.go's adminCSP constant byte-for-byte. This
|
||||
// package is a separate Go module (integration/go.mod) and cannot import
|
||||
// the plugin's unexported const, so the value is duplicated by hand;
|
||||
// TestAdminAssetCSP below compares it against a real header the running
|
||||
// plugin sent, so a drift between the two shows up as a test failure
|
||||
// immediately, not as a silent divergence.
|
||||
const adminCSPHeader = "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'none'; form-action 'none'"
|
||||
|
||||
// adminAssetSrcRe extracts the `src` attribute of the first `<script>` tag
|
||||
// in the served /admin shell — the built Vue app's own entry-point asset
|
||||
// reference.
|
||||
var adminAssetSrcRe = regexp.MustCompile(`<script[^>]+src="([^"]+)"`)
|
||||
|
||||
// TestAdminAssetCSP proves the Content-Security-Policy header under the
|
||||
// REAL, shipped pipeline: a Vite build baked into the plugin binary by
|
||||
// webui/generate.mjs, served by a real Traefik v3.5 process interpreting
|
||||
// the plugin with Yaegi — not the in-process httptest handler
|
||||
// admin_test.go's unit tests use. It fetches GET /admin, extracts the
|
||||
// asset URL its own <script src="..."> tag names, fetches that asset, and
|
||||
// asserts both responses carry the exact adminCSP string. Combined with
|
||||
// generate.mjs's own build-time assertion and admin_test.go's
|
||||
// TestAdminIndexHTML_NoInlineAssets (both check the CSP invariant holds —
|
||||
// no inline script/style — this test checks the header itself is what
|
||||
// actually reaches the wire), the loop from "build" to "real HTTP
|
||||
// response" is closed.
|
||||
func TestAdminAssetCSP(t *testing.T) {
|
||||
pageResp, err := http.Get(traefik1URL + "/admin")
|
||||
if err != nil {
|
||||
t.Fatalf("GET /admin: %v", err)
|
||||
}
|
||||
defer func() { _ = pageResp.Body.Close() }()
|
||||
if pageResp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("GET /admin: status = %d, want 200", pageResp.StatusCode)
|
||||
}
|
||||
if got := pageResp.Header.Get("Content-Security-Policy"); got != adminCSPHeader {
|
||||
t.Errorf("GET /admin: Content-Security-Policy = %q, want %q", got, adminCSPHeader)
|
||||
}
|
||||
pageBody, err := io.ReadAll(pageResp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read /admin body: %v", err)
|
||||
}
|
||||
|
||||
match := adminAssetSrcRe.FindSubmatch(pageBody)
|
||||
if match == nil {
|
||||
t.Fatalf("GET /admin: body has no <script src=\"...\"> reference, body=%s", pageBody)
|
||||
}
|
||||
assetPath := string(match[1])
|
||||
if !strings.HasPrefix(assetPath, "/admin/assets/") {
|
||||
t.Fatalf("extracted asset src = %q, want a path under /admin/assets/", assetPath)
|
||||
}
|
||||
|
||||
assetResp, err := http.Get(traefik1URL + assetPath)
|
||||
if err != nil {
|
||||
t.Fatalf("GET %s: %v", assetPath, err)
|
||||
}
|
||||
defer func() { _ = assetResp.Body.Close() }()
|
||||
if assetResp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("GET %s: status = %d, want 200", assetPath, assetResp.StatusCode)
|
||||
}
|
||||
if got := assetResp.Header.Get("Content-Security-Policy"); got != adminCSPHeader {
|
||||
t.Errorf("GET %s: Content-Security-Policy = %q, want %q", assetPath, got, adminCSPHeader)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdminDashboard covers v0.2 integration coverage (spec §4): the
|
||||
// unauthenticated HTML shell carries no data, the two JSON routes gate
|
||||
// on admin vs. non-admin vs. unauthenticated, and GET /admin/api/usage
|
||||
|
||||
Reference in New Issue
Block a user