package httpapi import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "path/filepath" "strconv" "strings" "testing" "webui4frpc/internal/process" "webui4frpc/internal/store" ) func newTestHandler(t *testing.T) (*Handler, *httptest.Server) { t.Helper() dir := t.TempDir() st, err := store.New(filepath.Join(dir, "test.db")) if err != nil { t.Fatal(err) } pm := process.NewManager(process.Options{ ConfigsDir: filepath.Join(dir, "configs"), LogsDir: filepath.Join(dir, "logs"), BinaryPath: func() string { return "" }, Render: func(string) ([]byte, error) { return []byte(`{}`), nil }, AutoRestart: func(string) bool { return false }, RestartInterval: func() int { return 5 }, }) h := &Handler{Store: st, Process: pm, WorkDir: dir, User: "admin", Password: "pw"} mux, err := NewServeMux(h) if err != nil { t.Fatal(err) } ts := httptest.NewServer(mux) t.Cleanup(ts.Close) t.Cleanup(func() { _ = st.Close() }) return h, ts } func TestAuthRequired(t *testing.T) { _, ts := newTestHandler(t) resp, err := http.Get(ts.URL + "/api/manager/status") if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("status = %d, want 401", resp.StatusCode) } } func TestCanvasRoundTrip(t *testing.T) { _, ts := newTestHandler(t) client := ts.Client() body := `{ "locals": [{"name":"web","ip":"127.0.0.1","port":8080,"protocol":"tcp"}], "remotes": [{"name":"srv-a","ip":"1.2.3.4","port":7000,"enabled":true}], "links": [{"local":"web","remote":"srv-a","remotePort":8080}] }` req, _ := http.NewRequest(http.MethodPut, ts.URL+"/api/manager/canvas", bytes.NewBufferString(body)) req.SetBasicAuth("admin", "pw") resp, err := client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("save canvas status = %d", resp.StatusCode) } var got canvasData if err := json.NewDecoder(resp.Body).Decode(&got); err != nil { t.Fatal(err) } if len(got.Locals) != 1 || got.Locals[0].Name != "web" { t.Fatalf("locals = %+v", got.Locals) } if len(got.Links) != 1 || got.Links[0].RemotePort != 8080 { t.Fatalf("links = %+v", got.Links) } // GET again to confirm persistence via store. req2, _ := http.NewRequest(http.MethodGet, ts.URL+"/api/manager/canvas", nil) req2.SetBasicAuth("admin", "pw") resp2, err2 := client.Do(req2) if err2 != nil { t.Fatal(err2) } defer resp2.Body.Close() var got2 canvasData _ = json.NewDecoder(resp2.Body).Decode(&got2) if len(got2.Remotes) != 1 || got2.Remotes[0].Name != "srv-a" { t.Fatalf("remotes after reload = %+v", got2.Remotes) } } func TestSettingsPutRoundTrip(t *testing.T) { _, ts := newTestHandler(t) client := ts.Client() // PUT updated settings body := `{"autoStartProfiles":false,"restartOnExit":false,"restartIntervalSeconds":12}` req, _ := http.NewRequest(http.MethodPut, ts.URL+"/api/manager/settings", bytes.NewBufferString(body)) req.SetBasicAuth("admin", "pw") resp, err := client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("PUT settings status = %d, want 200", resp.StatusCode) } // Confirm response body reflects saved values var saved store.Settings if err := json.NewDecoder(resp.Body).Decode(&saved); err != nil { t.Fatal(err) } if saved.AutoStartProfiles || saved.RestartOnExit || saved.RestartIntervalSeconds != 12 { t.Fatalf("saved settings = %+v", saved) } // GET again to confirm persistence req2, _ := http.NewRequest(http.MethodGet, ts.URL+"/api/manager/settings", nil) req2.SetBasicAuth("admin", "pw") resp2, err2 := client.Do(req2) if err2 != nil { t.Fatal(err2) } defer resp2.Body.Close() var got store.Settings _ = json.NewDecoder(resp2.Body).Decode(&got) if got.AutoStartProfiles || got.RestartOnExit || got.RestartIntervalSeconds != 12 { t.Fatalf("settings after reload = %+v", got) } } // TestFetchProxyStatesFromAdminAPI verifies the M3 status path: we mock the // frpc admin API (/api/status with Basic Auth) and confirm fetchProxyStates // parses per-proxy rows and connStateAdmin derives the right summary. func TestFetchProxyStatesFromAdminAPI(t *testing.T) { // Mock frpc admin server. admin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { u, p, ok := r.BasicAuth() if !ok || u != "admin" || p != "pw" { w.WriteHeader(http.StatusUnauthorized) return } if r.URL.Path != "/api/status" { http.NotFound(w, r) return } _ = json.NewEncoder(w).Encode(map[string][]proxyState{ "tcp": { {Name: "web1", Type: "tcp", Status: "running", LocalAddr: "127.0.0.1:8080", RemoteAddr: "1.2.3.4:8080"}, {Name: "web2", Type: "tcp", Status: "check failed", Err: "health check failed"}, }, }) })) defer admin.Close() u := strings.TrimPrefix(admin.URL, "http://") host, portStr, _ := strings.Cut(u, ":") port, _ := strconv.Atoi(portStr) states, err := fetchProxyStates(host, port, "admin", "pw") if err != nil { t.Fatalf("fetchProxyStates: %v", err) } if len(states) != 2 { t.Fatalf("states = %+v", states) } if states[0].Status != "running" || states[1].Status != "check failed" { t.Fatalf("unexpected states = %+v", states) } // connStateAdmin: one running -> connected. if got := connStateAdmin(states); got != "connected" { t.Fatalf("connStateAdmin = %q, want connected", got) } // All failed -> failed. if got := connStateAdmin([]proxyState{{Name: "a", Status: "check failed"}, {Name: "b", Status: "start error"}}); got != "failed" { t.Fatalf("connStateAdmin(all failed) = %q, want failed", got) } // wait start -> connecting. if got := connStateAdmin([]proxyState{{Name: "a", Status: "wait start"}}); got != "connecting" { t.Fatalf("connStateAdmin(wait start) = %q, want connecting", got) } }