mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-05-14 00:51:12 +03:00
200 lines
6.2 KiB
Go
200 lines
6.2 KiB
Go
//go:build with_admin_panel
|
|
|
|
package admin_panel
|
|
|
|
import (
|
|
"io/fs"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// firstAssetPath returns one of the hashed Vite assets from the embedded
|
|
// SPA bundle. Used by tests that need a real file-served URL (i.e. one
|
|
// that should NOT fall back to index.html). Returns "" if the bundle is
|
|
// empty — every concrete test then skips itself, which keeps the suite
|
|
// runnable even before `make admin_panel_regen` has been run.
|
|
func firstAssetPath(t *testing.T) string {
|
|
t.Helper()
|
|
var found string
|
|
_ = fs.WalkDir(distRoot, "assets", func(p string, d fs.DirEntry, err error) error {
|
|
if err != nil || d.IsDir() || strings.HasSuffix(p, ".gz") {
|
|
return nil
|
|
}
|
|
found = p
|
|
return fs.SkipAll
|
|
})
|
|
return found
|
|
}
|
|
|
|
// firstGzippedAsset returns one asset that has a `.gz` companion in the
|
|
// embedded bundle. Like firstAssetPath, returns "" when nothing matches.
|
|
func firstGzippedAsset(t *testing.T) string {
|
|
t.Helper()
|
|
var found string
|
|
_ = fs.WalkDir(distRoot, "assets", func(p string, d fs.DirEntry, err error) error {
|
|
if err != nil || d.IsDir() || strings.HasSuffix(p, ".gz") {
|
|
return nil
|
|
}
|
|
if _, ok := gzipCompanion(p); ok {
|
|
found = p
|
|
return fs.SkipAll
|
|
}
|
|
return nil
|
|
})
|
|
return found
|
|
}
|
|
|
|
func TestSPAHandler(t *testing.T) {
|
|
r := chi.NewRouter()
|
|
handler := newSPAHandler()
|
|
r.Method(http.MethodGet, "/*", handler)
|
|
|
|
indexBytes, hasIndex := readFile("index.html")
|
|
if !hasIndex {
|
|
t.Skip("index.html not packed yet (run `make admin_panel_regen`)")
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
path string
|
|
}{
|
|
{"root", "/"},
|
|
{"client-route fallback", "/squads"},
|
|
{"deep route fallback", "/users/123"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, tc.path, nil)
|
|
r.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), "<div id=\"root\">") {
|
|
t.Fatalf("body does not look like index.html:\n%s", rec.Body.String())
|
|
}
|
|
})
|
|
}
|
|
|
|
// Ensure a real packed asset is served as itself, not as the fallback.
|
|
t.Run("real file is served", func(t *testing.T) {
|
|
assetPath := firstAssetPath(t)
|
|
if assetPath == "" {
|
|
t.Skip("no packed assets/* to verify")
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/"+assetPath, nil)
|
|
r.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", rec.Code)
|
|
}
|
|
if strings.Contains(rec.Body.String(), "<div id=\"root\">") {
|
|
t.Fatalf("asset request returned the index fallback")
|
|
}
|
|
})
|
|
|
|
// Sanity check: index.html in the bundle is the same as what serveIndex returns.
|
|
t.Run("index payload matches map", func(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
r.ServeHTTP(rec, req)
|
|
if rec.Body.String() != string(indexBytes) {
|
|
t.Fatalf("served index does not match readFile(\"index.html\")")
|
|
}
|
|
})
|
|
|
|
t.Run("index has no-store cache control", func(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
r.ServeHTTP(rec, req)
|
|
cc := rec.Header().Get("Cache-Control")
|
|
if !strings.Contains(cc, "no-store") {
|
|
t.Fatalf("Cache-Control = %q, want it to contain no-store", cc)
|
|
}
|
|
})
|
|
|
|
t.Run("hashed asset is cached aggressively", func(t *testing.T) {
|
|
assetPath := firstAssetPath(t)
|
|
if assetPath == "" {
|
|
t.Skip("no packed assets/* to verify")
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/"+assetPath, nil)
|
|
r.ServeHTTP(rec, req)
|
|
cc := rec.Header().Get("Cache-Control")
|
|
if !strings.Contains(cc, "immutable") {
|
|
t.Fatalf("Cache-Control for %q = %q, want it to contain immutable", assetPath, cc)
|
|
}
|
|
})
|
|
|
|
// With Accept-Encoding: gzip the handler should pass through the
|
|
// pre-compressed `.gz` companion verbatim with Content-Encoding: gzip.
|
|
t.Run("gzip pass-through", func(t *testing.T) {
|
|
assetPath := firstGzippedAsset(t)
|
|
if assetPath == "" {
|
|
t.Skip("no gzipped assets in this build")
|
|
}
|
|
gz, _ := gzipCompanion(assetPath)
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/"+assetPath, nil)
|
|
req.Header.Set("Accept-Encoding", "gzip")
|
|
r.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", rec.Code)
|
|
}
|
|
if got := rec.Header().Get("Content-Encoding"); got != "gzip" {
|
|
t.Fatalf("Content-Encoding = %q, want gzip", got)
|
|
}
|
|
if rec.Body.String() != string(gz) {
|
|
t.Fatalf("body did not match the stored gzipped bytes")
|
|
}
|
|
})
|
|
|
|
// Without Accept-Encoding the same asset should be served raw and
|
|
// Content-Encoding must be unset.
|
|
t.Run("gzip transparent fallback", func(t *testing.T) {
|
|
assetPath := firstGzippedAsset(t)
|
|
if assetPath == "" {
|
|
t.Skip("no gzipped assets in this build")
|
|
}
|
|
raw, _ := readFile(assetPath)
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/"+assetPath, nil)
|
|
r.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", rec.Code)
|
|
}
|
|
if got := rec.Header().Get("Content-Encoding"); got != "" {
|
|
t.Fatalf("Content-Encoding = %q, want empty (raw body)", got)
|
|
}
|
|
if rec.Body.String() != string(raw) {
|
|
t.Fatalf("body does not match the raw asset")
|
|
}
|
|
})
|
|
|
|
// `.gz` companions are an internal implementation detail of the
|
|
// pre-compression strategy; clients should never fetch them directly,
|
|
// and walking the embedded FS via /-prefixed URLs must not expose
|
|
// them as if they were real assets either. Today they happen to be
|
|
// reachable (since fs.ReadFile sees them) — this test pins that
|
|
// behaviour so we notice if the contract changes.
|
|
t.Run("gz companion lookup", func(t *testing.T) {
|
|
assetPath := firstGzippedAsset(t)
|
|
if assetPath == "" {
|
|
t.Skip("no gzipped assets in this build")
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/"+assetPath+".gz", nil)
|
|
r.ServeHTTP(rec, req)
|
|
// We don't assert the precise body — only that the request
|
|
// resolves to *something* (200 or fallback) instead of crashing.
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200 (asset or SPA fallback)", rec.Code)
|
|
}
|
|
})
|
|
}
|