package web import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "gitgud.io/mike/mpv-manager/pkg/config" "gitgud.io/mike/mpv-manager/pkg/modernzconf" ) // setupSandboxedConfigHome points HOME and XDG_CONFIG_HOME at temp dirs so // handlers resolving user config paths (modernz.conf, input.conf) operate // inside a sandbox instead of the real user configuration. // Returns the temp dir used as HOME. func setupSandboxedConfigHome(t *testing.T) string { t.Helper() tmp := t.TempDir() t.Cleanup(func() { _, _ = config.Reload() }) for _, key := range []string{"HOME", "USERPROFILE", "APPDATA", "LOCALAPPDATA", "XDG_CONFIG_HOME"} { t.Setenv(key, tmp) } if _, err := config.Reload(); err != nil { t.Fatal(err) } return tmp } // postJSON issues a POST request with a raw JSON body against a handler. func postJSON(t *testing.T, handler http.HandlerFunc, target, body string) *httptest.ResponseRecorder { t.Helper() req := httptest.NewRequest(http.MethodPost, target, strings.NewReader(body)) w := httptest.NewRecorder() handler(w, req) return w } // choiceModernZOption returns a known ModernZ option that has a discrete // choice list, along with a valid value and its default. func choiceModernZOption(t *testing.T) (key, validValue, defaultValue string) { t.Helper() for _, opt := range modernzconf.KnownOptions() { if len(opt.Choices) > 0 { return opt.Key, opt.Choices[0], opt.Default } } t.Skip("no choice-based ModernZ options defined") return "", "", "" } func TestHandleModernZConfigAPI_DefaultsWhenFileMissing(t *testing.T) { setupSandboxedConfigHome(t) server := &Server{} req := httptest.NewRequest(http.MethodGet, "/api/modernz", nil) w := httptest.NewRecorder() server.handleModernZConfigAPI(w, req) if w.Code != http.StatusOK { t.Fatalf("Expected status 200, got %d (body: %s)", w.Code, w.Body.String()) } var resp struct { Options []modernzconf.Option `json:"options"` Values map[string]string `json:"values"` IsSet map[string]bool `json:"is_set"` Path string `json:"path"` } if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("Failed to parse JSON: %v", err) } if len(resp.Options) == 0 { t.Fatal("Expected non-empty options list") } if resp.Path == "" { t.Error("Expected non-empty path") } for _, opt := range resp.Options { if got, ok := resp.Values[opt.Key]; !ok { t.Errorf("values map missing key %q", opt.Key) } else if got != opt.Default { t.Errorf("Expected default value %q for %q, got %q", opt.Default, opt.Key, got) } if resp.IsSet[opt.Key] { t.Errorf("Expected is_set=false for %q when config file is missing", opt.Key) } } } func TestHandleModernZConfigAPI_MethodNotAllowed(t *testing.T) { server := &Server{} w := postJSON(t, server.handleModernZConfigAPI, "/api/modernz", `{}`) if w.Code != http.StatusMethodNotAllowed { t.Errorf("Expected status 405, got %d", w.Code) } } func TestHandleModernZSetAPI(t *testing.T) { key, validValue, _ := choiceModernZOption(t) tests := []struct { name string body string wantStatus int }{ {name: "valid value", body: `{"key":"` + key + `","value":"` + validValue + `"}`, wantStatus: http.StatusOK}, {name: "invalid value", body: `{"key":"` + key + `","value":"not-a-valid-choice-12345"}`, wantStatus: http.StatusBadRequest}, {name: "unknown key", body: `{"key":"no_such_option_abc123","value":"1"}`, wantStatus: http.StatusBadRequest}, {name: "malformed body", body: `{"key":`, wantStatus: http.StatusBadRequest}, {name: "newline injection in value", body: `{"key":"` + key + `","value":"yes\nno=1"}`, wantStatus: http.StatusBadRequest}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { setupSandboxedConfigHome(t) server := &Server{} w := postJSON(t, server.handleModernZSetAPI, "/api/modernz/set", tt.body) if w.Code != tt.wantStatus { t.Fatalf("Expected status %d, got %d (body: %s)", tt.wantStatus, w.Code, w.Body.String()) } if tt.wantStatus != http.StatusOK { return } var resp struct { Success bool `json:"success"` } if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("Failed to parse JSON: %v", err) } if !resp.Success { t.Error("Expected success=true") } // Verify the value was persisted to disk confPath, err := modernzconf.GetModernZConfPath() if err != nil { t.Fatalf("Failed to get modernz.conf path: %v", err) } conf, err := modernzconf.ParseModernZConf(confPath) if err != nil { t.Fatalf("Failed to re-parse modernz.conf: %v", err) } if got := conf.GetValue(key); got != validValue { t.Errorf("Expected persisted value %q for %q, got %q", validValue, key, got) } }) } } func TestHandleModernZResetAPI(t *testing.T) { setupSandboxedConfigHome(t) server := &Server{} key, validValue, defaultValue := choiceModernZOption(t) // Set a non-default state first (skip if the only valid choice IS the default // and there is no alternative; reset semantics are still verified below). setValue := validValue w := postJSON(t, server.handleModernZSetAPI, "/api/modernz/set", `{"key":"`+key+`","value":"`+setValue+`"}`) if w.Code != http.StatusOK { t.Fatalf("Setup set failed with status %d (body: %s)", w.Code, w.Body.String()) } // Unknown key must be rejected w = postJSON(t, server.handleModernZResetAPI, "/api/modernz/reset", `{"key":"no_such_option_abc123"}`) if w.Code != http.StatusBadRequest { t.Errorf("Expected status 400 for unknown key, got %d (body: %s)", w.Code, w.Body.String()) } // Reset the known key w = postJSON(t, server.handleModernZResetAPI, "/api/modernz/reset", `{"key":"`+key+`"}`) if w.Code != http.StatusOK { t.Fatalf("Expected status 200, got %d (body: %s)", w.Code, w.Body.String()) } var resp struct { Success bool `json:"success"` } if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("Failed to parse JSON: %v", err) } if !resp.Success { t.Error("Expected success=true") } // After reset the option must be unset and back at its default confPath, err := modernzconf.GetModernZConfPath() if err != nil { t.Fatalf("Failed to get modernz.conf path: %v", err) } conf, err := modernzconf.ParseModernZConf(confPath) if err != nil { t.Fatalf("Failed to re-parse modernz.conf: %v", err) } if conf.IsSet(key) { t.Errorf("Expected %q to be unset after reset", key) } if got := conf.GetValue(key); got != defaultValue { t.Errorf("Expected default value %q after reset, got %q", defaultValue, got) } }