package server import ( "net/http" "net/http/httptest" "strings" "testing" "testing/fstest" ) 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") } } 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) } } 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) } }) 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) } }) 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() trailingSlashRenderer := &uiRenderer{fsys: fstest.MapFS{ "assets/index.js": &fstest.MapFile{Data: []byte("console.log('ok')")}, }} missingPathRenderer := &uiRenderer{fsys: fstest.MapFS{}} testCases := []struct { name string method string path string ok bool use *uiRenderer }{ {name: "missing filesystem", method: http.MethodGet, path: "/assets/index-C8vHBL9g.js", ok: false, use: &uiRenderer{}}, {name: "invalid method", method: http.MethodPost, path: "/assets/index-C8vHBL9g.js", ok: false}, {name: "empty path", method: http.MethodGet, path: "/", ok: false}, {name: "dot path", method: http.MethodGet, path: "/.", ok: false}, {name: "api path", method: http.MethodGet, path: "/v1/backup", ok: false}, {name: "parent traversal", method: http.MethodGet, path: "/../secret.txt", ok: false}, {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}, {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 if tc.use != nil { activeRenderer = tc.use } 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) } }) } }