package web import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "ymhut-box/server/unified-management/internal/auth" "ymhut-box/server/unified-management/internal/config" "ymhut-box/server/unified-management/internal/db" "ymhut-box/server/unified-management/internal/feedback" "ymhut-box/server/unified-management/internal/legacy" "ymhut-box/server/unified-management/internal/notices" "ymhut-box/server/unified-management/internal/releases" "ymhut-box/server/unified-management/internal/sources" ) func TestCompatibilityRoutes(t *testing.T) { handler, cleanup := testRouter(t) defer cleanup() for _, path := range []string{"/api/client/bootstrap", "/update-info.json", "/media-types.json", "/modules.json"} { req := httptest.NewRequest(http.MethodGet, path, nil) res := httptest.NewRecorder() handler.ServeHTTP(res, req) if res.Code != http.StatusOK { t.Fatalf("%s returned %d: %s", path, res.Code, res.Body.String()) } var payload map[string]any if err := json.Unmarshal(res.Body.Bytes(), &payload); err != nil { t.Fatalf("%s did not return JSON: %v", path, err) } } } func TestClientBootstrapAndEndpointsShape(t *testing.T) { handler, cleanup := testRouter(t) defer cleanup() for _, path := range []string{"/api/client/bootstrap", "/api/client/endpoints", "/api/client/sources", "/api/client/notices"} { req := httptest.NewRequest(http.MethodGet, path, nil) res := httptest.NewRecorder() handler.ServeHTTP(res, req) if res.Code != http.StatusOK { t.Fatalf("%s returned %d: %s", path, res.Code, res.Body.String()) } var payload map[string]any if err := json.Unmarshal(res.Body.Bytes(), &payload); err != nil { t.Fatal(err) } if path == "/api/client/sources" { if payload["categories"] == nil { t.Fatalf("%s missing categories: %#v", path, payload) } continue } if payload["ok"] != true { t.Fatalf("%s missing ok=true: %#v", path, payload) } } } func TestBuiltFrontendAssetsAreServed(t *testing.T) { handler, cleanup := testRouter(t) defer cleanup() for _, item := range []struct { Path string ContentTypes []string }{ {Path: "/assets/portal.css", ContentTypes: []string{"text/css"}}, {Path: "/assets/portal.js", ContentTypes: []string{"text/javascript", "application/javascript"}}, {Path: "/admin/assets/admin.css", ContentTypes: []string{"text/css"}}, {Path: "/admin/assets/admin.js", ContentTypes: []string{"text/javascript", "application/javascript"}}, } { req := httptest.NewRequest(http.MethodGet, item.Path, nil) res := httptest.NewRecorder() handler.ServeHTTP(res, req) if res.Code != http.StatusOK { t.Fatalf("%s returned %d: %s", item.Path, res.Code, res.Body.String()) } if got := res.Header().Get("Content-Type"); !containsAny(got, item.ContentTypes) { t.Fatalf("%s content type = %q, want one of %v", item.Path, got, item.ContentTypes) } } } func containsAny(value string, needles []string) bool { for _, needle := range needles { if strings.Contains(value, needle) { return true } } return false } func TestReleaseNoticesRoutes(t *testing.T) { handler, cleanup := testRouter(t) defer cleanup() for _, path := range []string{"/api/client/notices", "/api/client/notices/2.0.0"} { req := httptest.NewRequest(http.MethodGet, path, nil) res := httptest.NewRecorder() handler.ServeHTTP(res, req) if res.Code != http.StatusOK { t.Fatalf("%s returned %d: %s", path, res.Code, res.Body.String()) } } req := httptest.NewRequest(http.MethodGet, "/api/client/releases", nil) res := httptest.NewRecorder() handler.ServeHTTP(res, req) var payload map[string]any if err := json.Unmarshal(res.Body.Bytes(), &payload); err != nil { t.Fatal(err) } if payload["notices"] == nil { t.Fatalf("release manifest missing notices: %#v", payload) } } func TestAdminLegacyRequiresAuth(t *testing.T) { handler, cleanup := testRouter(t) defer cleanup() req := httptest.NewRequest(http.MethodPut, "/api/admin/legacy/media-types", bytes.NewBufferString(`{"raw":"{}"}`)) res := httptest.NewRecorder() handler.ServeHTTP(res, req) if res.Code != http.StatusUnauthorized { t.Fatalf("expected unauthorized, got %d", res.Code) } } func testRouter(t *testing.T) (http.Handler, func()) { t.Helper() root := t.TempDir() public := filepath.Join(root, "public") noticeDir := filepath.Join(root, "update-notice") if err := os.MkdirAll(filepath.Join(public, "downloads"), 0o755); err != nil { t.Fatal(err) } adminDist := filepath.Join(root, "admin") portalDist := filepath.Join(root, "portal") for _, dir := range []string{filepath.Join(adminDist, "assets"), filepath.Join(portalDist, "assets")} { if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatal(err) } } if err := os.WriteFile(filepath.Join(portalDist, "index.html"), []byte(``), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(portalDist, "assets", "portal.css"), []byte(`body{}`), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(portalDist, "assets", "portal.js"), []byte(`console.log("portal")`), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(adminDist, "index.html"), []byte(``), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(adminDist, "assets", "admin.css"), []byte(`body{}`), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(adminDist, "assets", "admin.js"), []byte(`console.log("admin")`), 0o644); err != nil { t.Fatal(err) } if err := os.MkdirAll(noticeDir, 0o755); err != nil { t.Fatal(err) } mustWriteJSON(t, filepath.Join(public, "update-info.json"), map[string]any{"app_version": "0.0.1"}) mustWriteJSON(t, filepath.Join(public, "tool-status.json"), map[string]any{"ok": true}) mustWriteJSON(t, filepath.Join(public, "modules.json"), map[string]any{"modules": []any{}}) mustWriteJSON(t, filepath.Join(public, "media-types.json"), map[string]any{ "categories": []map[string]any{{ "id": "image", "name": "image", "subcategories": []map[string]any{{"id": "demo", "name": "demo", "api_url": "https://example.com/demo"}}, }}, }) mustWriteJSON(t, filepath.Join(noticeDir, "total.json"), map[string]any{ "schema_version": 1, "latest_version": "2.0.0", "latest_notice_file": "2.0.0.json", "latest": map[string]any{"version": "2.0.0", "title": "YMhut Box 2.0.0", "release_notes": "Initial release"}, "versions": []map[string]any{{"version": "2.0.0", "notice_file": "2.0.0.json", "summary": "Initial release"}}, }) mustWriteJSON(t, filepath.Join(noticeDir, "2.0.0.json"), map[string]any{"app_version": "2.0.0", "title": "YMhut Box 2.0.0", "release_notes": "Initial release", "release_notes_md": "## Initial"}) cfg := &config.Config{ Listen: ":0", BaseURL: "https://update.ymhut.cn", StorageDir: filepath.Join(root, "storage"), UpdatePublicDir: public, UpdateNoticeDir: noticeDir, DownloadsDir: filepath.Join(public, "downloads"), AdminWebDir: adminDist, PortalWebDir: portalDist, SourceCheckSeconds: 3600, Database: config.DatabaseConfig{ Provider: "sqlite", SQLitePath: filepath.Join(root, "storage", "unified.sqlite"), FailoverEnabled: true, HotSyncEnabled: true, HealthIntervalSec: 3600, }, } store, err := db.Open(cfg) if err != nil { t.Fatal(err) } if err := store.EnsureDefaultAdmin(context.Background()); err != nil { t.Fatal(err) } sourceService := sources.NewService(cfg, store) if err := sourceService.ImportLegacyMediaTypesIfEmpty(context.Background()); err != nil { t.Fatal(err) } noticeService := notices.NewService(cfg, store) if err := noticeService.Import(context.Background()); err != nil { t.Fatal(err) } handler := NewRouter( cfg, store, auth.NewService(store), feedback.NewService(cfg, store), releases.NewService(cfg, store, noticeService), sourceService, legacy.NewService(cfg, store), noticeService, ) return handler, func() { _ = store.Close() } } func mustWriteJSON(t *testing.T, path string, payload any) { t.Helper() data, err := json.Marshal(payload) if err != nil { t.Fatal(err) } if err := os.WriteFile(path, data, 0o644); err != nil { t.Fatal(err) } }