package web import ( "encoding/json" "errors" "net/http" "net/http/httptest" "strings" "testing" "time" "gitgud.io/mike/mpv-manager/pkg/constants" "gitgud.io/mike/mpv-manager/pkg/platform" "gitgud.io/mike/mpv-manager/pkg/version" ) func TestMutatingRefreshAndShortcutHandlersRejectGET(t *testing.T) { server := &Server{} for path, handler := range map[string]http.HandlerFunc{ "/api/apps/check-updates": server.handleCheckAppUpdatesAPI, "/api/apps/refresh-installed": server.handleRefreshInstalledAppsAPI, "/api/settings/shortcut": server.handleSettingsShortcutAPI, } { t.Run(path, func(t *testing.T) { response := httptest.NewRecorder() handler(response, httptest.NewRequest(http.MethodGet, path, nil)) if response.Code != http.StatusMethodNotAllowed { t.Fatalf("status = %d, want %d", response.Code, http.StatusMethodNotAllowed) } }) } } func TestInstallAndUpdateJobsRejectInvalidUIType(t *testing.T) { for name, handler := range map[string]func(*Server, http.ResponseWriter, *http.Request){ "install": (*Server).handleInstallJobAPI, "update": (*Server).handleAppUpdateJobAPI, } { t.Run(name, func(t *testing.T) { server := &Server{ platform: &platform.Platform{OSType: platform.Linux}, jobManager: NewJobManager(), } body := `{"method_id":"` + constants.MethodModernZ + `","ui_type":"../../invalid"}` response := httptest.NewRecorder() handler(server, response, httptest.NewRequest(http.MethodPost, "/api/"+name+"/job", strings.NewReader(body))) if response.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d: %s", response.Code, http.StatusBadRequest, response.Body.String()) } if jobs := server.jobManager.GetActiveJobs(); len(jobs) != 0 { t.Fatalf("invalid request created %d job(s)", len(jobs)) } }) } } func TestHandleModalAPI_Shutdown(t *testing.T) { server := &Server{} req := httptest.NewRequest("GET", "/api/modal?type=shutdown", nil) w := httptest.NewRecorder() server.handleModalAPI(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status OK, got %d", w.Code) } var config ModalConfig err := json.Unmarshal(w.Body.Bytes(), &config) if err != nil { t.Fatalf("Failed to parse JSON: %v", err) } if config.Title != "Close MPV.Rocks Installer" { t.Errorf("Expected title 'Close MPV.Rocks Installer', got '%s'", config.Title) } if config.Danger != true { t.Errorf("Expected danger to be true for shutdown") } } func TestHandleModalAPI_UpdateManager(t *testing.T) { server := &Server{} req := httptest.NewRequest("GET", "/api/modal?type=update-manager&version=1.2.3", nil) w := httptest.NewRecorder() server.handleModalAPI(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status OK, got %d", w.Code) } var config ModalConfig err := json.Unmarshal(w.Body.Bytes(), &config) if err != nil { t.Fatalf("Failed to parse JSON: %v", err) } if config.Title != "Update MPV Manager" { t.Errorf("Expected title 'Update MPV Manager', got '%s'", config.Title) } if config.Message != "Update MPV Manager to version 1.2.3?" { t.Errorf("Expected message 'Update MPV Manager to version 1.2.3?', got '%s'", config.Message) } if config.Danger != false { t.Errorf("Expected danger to be false for update manager") } } func TestHandleModalAPI_UpdateManagerSuccess(t *testing.T) { server := &Server{} req := httptest.NewRequest("GET", "/api/modal?type=update-manager-success", nil) w := httptest.NewRecorder() server.handleModalAPI(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status OK, got %d", w.Code) } var config ModalConfig err := json.Unmarshal(w.Body.Bytes(), &config) if err != nil { t.Fatalf("Failed to parse JSON: %v", err) } if config.Title != "Update Ready" { t.Errorf("Expected title 'Update Ready', got '%s'", config.Title) } if config.ConfirmText != "Close & Exit" { t.Errorf("Expected confirm text 'Close & Exit', got '%s'", config.ConfirmText) } if config.CancelText != "" { t.Errorf("Expected cancel text to be empty, got '%s'", config.CancelText) } if config.ConfirmURL != "/api/shutdown" { t.Errorf("Expected confirm URL '/api/shutdown', got '%s'", config.ConfirmURL) } if config.Success != true { t.Errorf("Expected success to be true for update manager success modal") } if config.Danger != false { t.Errorf("Expected danger to be false for update manager success modal") } } func TestHandleModalAPI_RemoveFileAssociations(t *testing.T) { server := &Server{} req := httptest.NewRequest("GET", "/api/modal?type=remove-file-associations", nil) w := httptest.NewRecorder() server.handleModalAPI(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status OK, got %d", w.Code) } var config ModalConfig err := json.Unmarshal(w.Body.Bytes(), &config) if err != nil { t.Fatalf("Failed to parse JSON: %v", err) } if config.Title != "Remove File Associations" { t.Errorf("Expected title 'Remove File Associations', got '%s'", config.Title) } if config.Danger != true { t.Errorf("Expected danger to be true for remove file associations") } } func TestHandleModalAPI_ResetConfig(t *testing.T) { server := &Server{} req := httptest.NewRequest("GET", "/api/modal?type=reset-config", nil) w := httptest.NewRecorder() server.handleModalAPI(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status OK, got %d", w.Code) } var config ModalConfig err := json.Unmarshal(w.Body.Bytes(), &config) if err != nil { t.Fatalf("Failed to parse JSON: %v", err) } if config.Title != "Reset MPV Config" { t.Errorf("Expected title 'Reset MPV Config', got '%s'", config.Title) } if config.Danger != true { t.Errorf("Expected danger to be true for reset config") } } func TestHandleModalAPI_RestoreConfig(t *testing.T) { // Use a valid backup path that matches the expected pattern: YYYY-MM-DD-HHMMSS_mpv.conf // Note: This test will fail with 400 because the path doesn't actually exist in the filesystem, // but that's expected behavior for the validation. We're testing that the API handles validation correctly. server := &Server{} backupPath := "2026-01-31-123456_mpv.conf" req := httptest.NewRequest("GET", "/api/modal?type=restore-config&backup_path="+backupPath, nil) w := httptest.NewRecorder() server.handleModalAPI(w, req) // The test now expects 400 because validation will fail (path doesn't exist on filesystem) // This is the correct behavior - we validate the path before proceeding if w.Code != http.StatusBadRequest { t.Errorf("Expected status Bad Request (400), got %d", w.Code) } } func TestHandleModalAPI_MissingType(t *testing.T) { server := &Server{} req := httptest.NewRequest("GET", "/api/modal", nil) w := httptest.NewRecorder() server.handleModalAPI(w, req) if w.Code != http.StatusBadRequest { t.Errorf("Expected status Bad Request, got %d", w.Code) } } func TestHandleModalAPI_UnknownType(t *testing.T) { server := &Server{} req := httptest.NewRequest("GET", "/api/modal?type=unknown-type", nil) w := httptest.NewRecorder() server.handleModalAPI(w, req) if w.Code != http.StatusBadRequest { t.Errorf("Expected status Bad Request, got %d", w.Code) } } func TestHandleModalAPI_MissingRestoreParams(t *testing.T) { server := &Server{} req := httptest.NewRequest("GET", "/api/modal?type=restore-config", nil) w := httptest.NewRecorder() server.handleModalAPI(w, req) if w.Code != http.StatusBadRequest { t.Errorf("Expected status Bad Request, got %d", w.Code) } } func TestModalConfig_JSONSerialization(t *testing.T) { config := ModalConfig{ Title: "Test Title", Message: "Test Message", ConfirmText: "Confirm", CancelText: "Cancel", ConfirmURL: "/api/test", ConfirmData: `{"key": "value"}`, Danger: true, } data, err := json.Marshal(config) if err != nil { t.Fatalf("Failed to marshal JSON: %v", err) } var decoded ModalConfig err = json.Unmarshal(data, &decoded) if err != nil { t.Fatalf("Failed to unmarshal JSON: %v", err) } if decoded.Title != config.Title { t.Errorf("Title mismatch: got %s, want %s", decoded.Title, config.Title) } if decoded.Danger != config.Danger { t.Errorf("Danger mismatch: got %v, want %v", decoded.Danger, config.Danger) } } // TestPOSTOnlyHandlers verifies that mutating endpoints reject GET with 405 func TestPOSTOnlyHandlers(t *testing.T) { server := &Server{} handlers := []struct { name string handler http.HandlerFunc }{ {"handleConfigResetAPI", server.handleConfigResetAPI}, {"handleLogsClearAPI", server.handleLogsClearAPI}, {"handleManagerUpdateAPI", server.handleManagerUpdateAPI}, {"handleSettingsResetAPI", server.handleSettingsResetAPI}, {"handleSettingsInstallPathAPI", server.handleSettingsInstallPathAPI}, {"handleShortcutsCreateAPI", server.handleShortcutsCreateAPI}, {"handleFileAssociationsSetupAPI", server.handleFileAssociationsSetupAPI}, {"handleFileAssociationsRemoveAPI", server.handleFileAssociationsRemoveAPI}, } for _, h := range handlers { t.Run(h.name, func(t *testing.T) { req := httptest.NewRequest("GET", "/api/test", nil) w := httptest.NewRecorder() h.handler(w, req) if w.Code != http.StatusMethodNotAllowed { t.Errorf("Expected status 405 for GET, got %d", w.Code) } if ct := w.Header().Get("Content-Type"); !strings.Contains(ct, "application/json") { t.Errorf("Expected JSON error response, got Content-Type %q", ct) } }) } } func TestHandleModalAPI_UpdateManagerSanitizesVersion(t *testing.T) { server := &Server{} req := httptest.NewRequest("GET", "/api/modal?type=update-manager&version=%3Cscript%3Ealert(1)%3C/script%3E", nil) w := httptest.NewRecorder() server.handleModalAPI(w, req) if w.Code != http.StatusOK { t.Fatalf("Expected status OK, got %d", w.Code) } var config ModalConfig if err := json.Unmarshal(w.Body.Bytes(), &config); err != nil { t.Fatalf("Failed to parse modal config JSON: %v", err) } // Invalid version strings fall back to "latest" if config.Message != "Update MPV Manager to version latest?" { t.Errorf("Expected sanitized version in message, got %q", config.Message) } } func TestHandleLanguagesSaveAPI_RejectsInjection(t *testing.T) { tests := []struct { name string body string }{ {"audio newline injection", `{"audio_languages":["en\nscript=x"],"subtitle_languages":[]}`}, {"subtitle newline injection", `{"audio_languages":[],"subtitle_languages":["en","es\nconfig=1"]}`}, {"audio carriage return", `{"audio_languages":["pt\rBR"]}`}, {"audio garbage", `{"audio_languages":["