2026-04-20 17:43:55 -03:00
|
|
|
package server
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
2026-04-20 19:46:11 -03:00
|
|
|
"testing/fstest"
|
2026-04-20 17:43:55 -03:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestNewUIRendererLoadsEmbeddedAssets(t *testing.T) {
|
|
|
|
|
renderer := newUIRenderer()
|
|
|
|
|
|
|
|
|
|
if renderer == nil {
|
|
|
|
|
t.Fatalf("expected ui renderer instance")
|
|
|
|
|
}
|
|
|
|
|
if renderer.fsys == nil {
|
|
|
|
|
t.Fatalf("expected embedded UI filesystem to be available")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 19:46:11 -03:00
|
|
|
func TestNewUIRendererFromFSHandlesMissingSubdirectory(t *testing.T) {
|
|
|
|
|
renderer := newUIRendererFromFS(fstest.MapFS{}, "../ui-dist")
|
|
|
|
|
if renderer == nil {
|
|
|
|
|
t.Fatalf("expected renderer instance")
|
|
|
|
|
}
|
|
|
|
|
if renderer.fsys != nil {
|
|
|
|
|
t.Fatalf("expected missing subdirectory to produce empty renderer, got %#v", renderer.fsys)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 17:43:55 -03:00
|
|
|
func TestUIRendererServeIndexHandlesMissingAndEmbeddedAssets(t *testing.T) {
|
|
|
|
|
t.Run("missing assets", func(t *testing.T) {
|
|
|
|
|
renderer := &uiRenderer{}
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
|
|
|
res := httptest.NewRecorder()
|
|
|
|
|
|
|
|
|
|
err := renderer.ServeIndex(res, req)
|
|
|
|
|
if err == nil || !strings.Contains(err.Error(), "not available") {
|
|
|
|
|
t.Fatalf("expected missing UI asset error, got %v", err)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-20 19:46:11 -03:00
|
|
|
t.Run("missing index file", func(t *testing.T) {
|
|
|
|
|
renderer := &uiRenderer{fsys: fstest.MapFS{}}
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
|
|
|
res := httptest.NewRecorder()
|
|
|
|
|
|
|
|
|
|
err := renderer.ServeIndex(res, req)
|
|
|
|
|
if err == nil || !strings.Contains(err.Error(), "read index") {
|
|
|
|
|
t.Fatalf("expected index read error, got %v", err)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-20 17:43:55 -03:00
|
|
|
t.Run("embedded index", func(t *testing.T) {
|
|
|
|
|
renderer := newUIRenderer()
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
|
|
|
res := httptest.NewRecorder()
|
|
|
|
|
|
|
|
|
|
if err := renderer.ServeIndex(res, req); err != nil {
|
|
|
|
|
t.Fatalf("expected embedded index to render, got %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got := res.Header().Get("Content-Type"); got != "text/html; charset=utf-8" {
|
|
|
|
|
t.Fatalf("expected html content type, got %q", got)
|
|
|
|
|
}
|
|
|
|
|
if got := res.Header().Get("Cache-Control"); got != "no-store" {
|
|
|
|
|
t.Fatalf("expected no-store cache control, got %q", got)
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(res.Body.String(), "Soteria Backup Console") {
|
|
|
|
|
t.Fatalf("expected rendered index HTML, got %q", res.Body.String())
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUIRendererServeAssetRejectsInvalidPathsAndServesRealAssets(t *testing.T) {
|
|
|
|
|
renderer := newUIRenderer()
|
2026-04-20 19:46:11 -03:00
|
|
|
trailingSlashRenderer := &uiRenderer{fsys: fstest.MapFS{
|
|
|
|
|
"assets/index.js": &fstest.MapFile{Data: []byte("console.log('ok')")},
|
|
|
|
|
}}
|
|
|
|
|
missingPathRenderer := &uiRenderer{fsys: fstest.MapFS{}}
|
2026-04-20 17:43:55 -03:00
|
|
|
|
|
|
|
|
testCases := []struct {
|
|
|
|
|
name string
|
|
|
|
|
method string
|
|
|
|
|
path string
|
|
|
|
|
ok bool
|
2026-04-20 19:46:11 -03:00
|
|
|
use *uiRenderer
|
2026-04-20 17:43:55 -03:00
|
|
|
}{
|
2026-04-20 19:46:11 -03:00
|
|
|
{name: "missing filesystem", method: http.MethodGet, path: "/assets/index-C8vHBL9g.js", ok: false, use: &uiRenderer{}},
|
2026-04-20 17:43:55 -03:00
|
|
|
{name: "invalid method", method: http.MethodPost, path: "/assets/index-C8vHBL9g.js", ok: false},
|
|
|
|
|
{name: "empty path", method: http.MethodGet, path: "/", ok: false},
|
2026-04-20 19:46:11 -03:00
|
|
|
{name: "dot path", method: http.MethodGet, path: "/.", ok: false},
|
2026-04-20 17:43:55 -03:00
|
|
|
{name: "api path", method: http.MethodGet, path: "/v1/backup", ok: false},
|
|
|
|
|
{name: "parent traversal", method: http.MethodGet, path: "/../secret.txt", ok: false},
|
2026-04-20 19:46:11 -03:00
|
|
|
{name: "trailing slash", method: http.MethodGet, path: "/assets/", ok: false, use: trailingSlashRenderer},
|
|
|
|
|
{name: "missing file", method: http.MethodGet, path: "/assets/does-not-exist.js", ok: false, use: missingPathRenderer},
|
2026-04-20 17:43:55 -03:00
|
|
|
{name: "real asset", method: http.MethodGet, path: "/assets/index-C8vHBL9g.js", ok: true},
|
|
|
|
|
{name: "head asset", method: http.MethodHead, path: "/assets/index-B24a4-XK.css", ok: true},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tc := range testCases {
|
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
|
activeRenderer := renderer
|
2026-04-20 19:46:11 -03:00
|
|
|
if tc.use != nil {
|
|
|
|
|
activeRenderer = tc.use
|
2026-04-20 17:43:55 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(tc.method, tc.path, nil)
|
|
|
|
|
res := httptest.NewRecorder()
|
|
|
|
|
|
|
|
|
|
if got := activeRenderer.ServeAsset(res, req); got != tc.ok {
|
|
|
|
|
t.Fatalf("expected ServeAsset=%v, got %v", tc.ok, got)
|
|
|
|
|
}
|
|
|
|
|
if tc.ok && res.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("expected successful asset response, got %d", res.Code)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|