package web import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "ymhut-box/server/unified-management/internal/config" ) func TestSetupRouterServesBuiltAssetsAndBlocksBusinessAPI(t *testing.T) { root := t.TempDir() setupDist := filepath.Join(root, "setup") if err := os.MkdirAll(filepath.Join(setupDist, "assets"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(setupDist, "index.html"), []byte(``), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(setupDist, "assets", "setup.css"), []byte(`body{}`), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(setupDist, "assets", "setup.js"), []byte(`console.log("setup")`), 0o644); err != nil { t.Fatal(err) } handler := NewSetupRouter(setupConfig(root, setupDist)) for _, item := range []struct { path string want int typ string }{ {"/setup", http.StatusOK, "text/html"}, {"/setup/assets/setup.css", http.StatusOK, "text/css"}, {"/setup/assets/setup.js", http.StatusOK, "javascript"}, {"/api/client/bootstrap", http.StatusServiceUnavailable, "application/json"}, } { req := httptest.NewRequest(http.MethodGet, item.path, nil) res := httptest.NewRecorder() handler.ServeHTTP(res, req) if res.Code != item.want { t.Fatalf("%s returned %d: %s", item.path, res.Code, res.Body.String()) } if got := res.Header().Get("Content-Type"); !strings.Contains(got, item.typ) { t.Fatalf("%s content-type = %q, want %q", item.path, got, item.typ) } } } func TestSetupSQLiteCompleteCreatesConfigAndDefaultAdmin(t *testing.T) { root := t.TempDir() handler := NewSetupRouter(setupConfig(root, filepath.Join(root, "missing-setup-dist"))) body := bytes.NewBufferString(`{"provider":"sqlite","baseUrl":"https://update.ymhut.cn","sqlitePath":"storage/unified.sqlite"}`) req := httptest.NewRequest(http.MethodPost, "/api/setup/complete", body) res := httptest.NewRecorder() handler.ServeHTTP(res, req) if res.Code != http.StatusOK { t.Fatalf("complete returned %d: %s", res.Code, res.Body.String()) } data, err := os.ReadFile(filepath.Join(root, "config.json")) if err != nil { t.Fatal(err) } var cfg config.Config if err := json.Unmarshal(data, &cfg); err != nil { t.Fatal(err) } if !cfg.Initialized || cfg.Database.Provider != "sqlite" { t.Fatalf("unexpected config: %#v", cfg) } if _, err := os.Stat(filepath.Join(root, "storage", "unified.sqlite")); err != nil { t.Fatal(err) } } func TestSetupSQLiteIgnoresStructuredMySQLDefaults(t *testing.T) { root := t.TempDir() handler := NewSetupRouter(setupConfig(root, filepath.Join(root, "missing-setup-dist"))) body := bytes.NewBufferString(`{"provider":"sqlite","baseUrl":"https://update.ymhut.cn","sqlitePath":"storage/unified.sqlite","mysql":{"host":"127.0.0.1","port":3306,"database":"ymhut_unified","username":"","password":"","charset":"utf8mb4","parseTime":true,"tls":"false"}}`) req := httptest.NewRequest(http.MethodPost, "/api/setup/database/test", body) res := httptest.NewRecorder() handler.ServeHTTP(res, req) if res.Code != http.StatusOK { t.Fatalf("sqlite test returned %d: %s", res.Code, res.Body.String()) } if strings.Contains(res.Body.String(), "mysql username is required") { t.Fatalf("sqlite test should ignore structured mysql defaults: %s", res.Body.String()) } } func TestSetupStructuredMySQLValidationReturnsFailureWithoutSaving(t *testing.T) { root := t.TempDir() handler := NewSetupRouter(setupConfig(root, filepath.Join(root, "missing-setup-dist"))) body := bytes.NewBufferString(`{"provider":"mysql","mysql":{"host":"127.0.0.1","port":1,"database":"ymhut","username":"root","password":"secret","charset":"utf8mb4","parseTime":true,"tls":"false"}}`) req := httptest.NewRequest(http.MethodPost, "/api/setup/database/test", body) res := httptest.NewRecorder() handler.ServeHTTP(res, req) if res.Code != http.StatusBadGateway { t.Fatalf("mysql test returned %d, want 502: %s", res.Code, res.Body.String()) } if _, err := os.Stat(filepath.Join(root, "config.json")); !os.IsNotExist(err) { t.Fatalf("config should not be written on failed test: %v", err) } if strings.Contains(res.Body.String(), "secret") { t.Fatalf("response leaked password: %s", res.Body.String()) } } func setupConfig(root, setupDist string) *config.Config { return &config.Config{ BaseDir: root, ConfigPath: filepath.Join(root, "config.json"), BaseURL: "https://update.ymhut.cn", StorageDir: filepath.Join(root, "storage"), SetupWebDir: setupDist, Database: config.DatabaseConfig{ Provider: "sqlite", SQLitePath: filepath.Join(root, "storage", "unified.sqlite"), HealthIntervalSec: 30, MaxOpenConns: 1, MaxIdleConns: 1, }, } }