package web import ( "bytes" "encoding/json" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "net/http" "net/http/httptest" "os" "path/filepath" "runtime" "strings" "testing" "gitgud.io/mike/mpv-manager/pkg/config" ) func TestHandleConfigApplyAPIValidatesAllFieldsBeforeWriting(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) t.Setenv("USERPROFILE", home) t.Setenv("APPDATA", home) t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config")) configPath := config.GetConfigPath() if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil { t.Fatal(err) } original := []byte("alang=de\nslang=de\n") if err := os.WriteFile(configPath, original, 0o644); err != nil { t.Fatal(err) } // Audio language is valid and appears first in the apply workflow. PNG // compression is intentionally invalid and appears much later. body := []byte(`{"audio_language":"en","screenshot_png_compression":"10"}`) req := httptest.NewRequest(http.MethodPost, "/api/config/apply", bytes.NewReader(body)) recorder := httptest.NewRecorder() server := &Server{} server.handleConfigApplyAPI(recorder, req) if recorder.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusBadRequest, recorder.Body.String()) } after, err := os.ReadFile(configPath) if err != nil { t.Fatal(err) } if !bytes.Equal(after, original) { t.Fatalf("invalid later field partially changed mpv.conf:\n%s", after) } } func TestConfigSettingsApplyRetainsScalarCommasAndProfileScope(t *testing.T) { setupSandboxedConfigHome(t) path := config.GetConfigPath() require.NoError(t, os.MkdirAll(filepath.Dir(path), 0755)) original := "hwdec=auto\nhwdec=nvdec-copy,auto\nalang=en,fr\nscreenshot-template=\"shot,%n\"\nscreenshot-directory=\"Pictures, shots\"\nscreenshot-avif-opts=crf=20,speed=6\n[special]\nhwdec=no\nscreenshot-avif-opts=crf=40,speed=8\n" require.NoError(t, os.WriteFile(path, []byte(original), 0600)) server := &Server{} settings := server.getConfigSettings() assert.Equal(t, "nvdec-copy,auto", settings.HardwareAccel) assert.Equal(t, []string{"en", "fr"}, settings.AudioLanguages) assert.Equal(t, "crf=20,speed=6", settings.ScreenshotAvifOpts) assert.Equal(t, "Pictures, shots", settings.ScreenshotDirectory) assert.Equal(t, "shot,%n", settings.ScreenshotTemplate) get := httptest.NewRecorder() server.handleConfigSettingsAPI(get, httptest.NewRequest(http.MethodGet, "/api/config/settings", nil)) var values map[string]string require.NoError(t, json.Unmarshal(get.Body.Bytes(), &values)) assert.Equal(t, settings.HardwareAccel, values["hardware_acceleration"]) // The form sends the displayed scalar fields back unchanged on Apply. body, err := json.Marshal(map[string]string{ "hardware_acceleration": settings.HardwareAccel, "screenshot_avif_opts": settings.ScreenshotAvifOpts, "screenshot_directory": settings.ScreenshotDirectory, "screenshot_template": settings.ScreenshotTemplate, }) require.NoError(t, err) result := httptest.NewRecorder() server.handleConfigApplyAPI(result, httptest.NewRequest(http.MethodPost, "/api/config/apply", bytes.NewReader(body))) require.Equal(t, http.StatusOK, result.Code, result.Body.String()) after := server.getConfigSettings() assert.Equal(t, settings.ScreenshotAvifOpts, after.ScreenshotAvifOpts) assert.Equal(t, settings.ScreenshotDirectory, after.ScreenshotDirectory) assert.Equal(t, settings.ScreenshotTemplate, after.ScreenshotTemplate) assert.Equal(t, settings.HardwareAccel, after.HardwareAccel) content, err := os.ReadFile(path) require.NoError(t, err) assert.Contains(t, string(content), "[special]\nhwdec=no\nscreenshot-avif-opts=crf=40,speed=8\n") if runtime.GOOS != "windows" { info, err := os.Stat(path) require.NoError(t, err) assert.Equal(t, os.FileMode(0600), info.Mode().Perm()) } } func TestConfigApplyDistinguishesClearFromOmittedFields(t *testing.T) { setupSandboxedConfigHome(t) path := config.GetConfigPath() require.NoError(t, os.MkdirAll(filepath.Dir(path), 0700)) original := "# preserved\nprofile=cinema\nvo=libmpv\nscale=ewa_lanczos\nscreenshot-directory=/tmp/old # directory note\nscreenshot-template=custom-%n\nscreenshot-avif-opts=crf=20,speed=6\n[cinema]\nscreenshot-directory=/tmp/profile\nscreenshot-template=profile-%n\n" require.NoError(t, os.WriteFile(path, []byte(original), 0600)) server := &Server{} result := httptest.NewRecorder() server.handleConfigApplyAPI(result, httptest.NewRequest(http.MethodPost, "/api/config/apply", strings.NewReader(`{"screenshot_directory":"","screenshot_template":"","screenshot_avif_opts":""}`))) require.Equal(t, http.StatusOK, result.Code, result.Body.String()) content, err := os.ReadFile(path) require.NoError(t, err) assert.Equal(t, "# preserved\nprofile=cinema\nvo=libmpv\nscale=ewa_lanczos\n# directory note\n[cinema]\nscreenshot-directory=/tmp/profile\nscreenshot-template=profile-%n\n", string(content)) // Omitted and JSON-null fields preserve their current state byte for byte. for _, body := range []string{`{}`, `{"profile":null,"video_output_driver":null}`} { result = httptest.NewRecorder() server.handleConfigApplyAPI(result, httptest.NewRequest(http.MethodPost, "/api/config/apply", strings.NewReader(body))) require.Equal(t, http.StatusOK, result.Code, result.Body.String()) after, err := os.ReadFile(path) require.NoError(t, err) assert.Equal(t, content, after) } } func TestConfigApplyExplicitEmptyLanguageListsRemainEmpty(t *testing.T) { setupSandboxedConfigHome(t) path := config.GetConfigPath() require.NoError(t, os.MkdirAll(filepath.Dir(path), 0700)) require.NoError(t, os.WriteFile(path, []byte("alang=en,fr\nslang=en\n"), 0600)) result := httptest.NewRecorder() (&Server{}).handleConfigApplyAPI(result, httptest.NewRequest(http.MethodPost, "/api/config/apply", strings.NewReader(`{"audio_language":"","subtitle_language":""}`))) require.Equal(t, http.StatusOK, result.Code, result.Body.String()) content, err := os.ReadFile(path) require.NoError(t, err) assert.Equal(t, "alang=\nslang=\n", string(content)) }