package admin import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" "ds2api/internal/account" "ds2api/internal/config" ) func newAdminProxyTestHandler(t *testing.T, raw string) *Handler { t.Helper() t.Setenv("DS2API_CONFIG_JSON", raw) store := config.LoadStore() return &Handler{ Store: store, Pool: account.NewPool(store), } } func TestAddProxyPersistsNormalizedProxy(t *testing.T) { h := newAdminProxyTestHandler(t, `{"accounts":[]}`) r := chi.NewRouter() r.Post("/admin/proxies", h.addProxy) req := httptest.NewRequest(http.MethodPost, "/admin/proxies", bytes.NewBufferString(`{ "name":" HK Exit ", "type":" SOCKS5H ", "host":" 127.0.0.1 ", "port":1081, "username":" user ", "password":" pass " }`)) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String()) } proxies := h.Store.Snapshot().Proxies if len(proxies) != 1 { t.Fatalf("expected 1 proxy, got %d", len(proxies)) } if proxies[0].Name != "HK Exit" { t.Fatalf("unexpected proxy name: %#v", proxies[0]) } if proxies[0].Type != "socks5h" { t.Fatalf("unexpected proxy type: %#v", proxies[0]) } if proxies[0].Username != "user" || proxies[0].Password != "pass" { t.Fatalf("expected trimmed credentials, got %#v", proxies[0]) } if proxies[0].ID == "" { t.Fatalf("expected generated proxy id, got %#v", proxies[0]) } } func TestAddProxyDoesNotFailOnUnrelatedInvalidRuntimeConfig(t *testing.T) { router := newHTTPAdminHarness(t, `{ "keys":["k1"], "runtime":{ "account_max_inflight":8, "global_max_inflight":4 } }`, &testingDSMock{}) rec := httptest.NewRecorder() router.ServeHTTP(rec, adminReq(http.MethodPost, "/proxies", []byte(`{ "name":"HK Exit", "type":"socks5h", "host":"127.0.0.1", "port":1080 }`))) if rec.Code != http.StatusOK { t.Fatalf("expected add proxy success despite unrelated runtime issue, got %d body=%s", rec.Code, rec.Body.String()) } readRec := httptest.NewRecorder() router.ServeHTTP(readRec, adminReq(http.MethodGet, "/config", nil)) if readRec.Code != http.StatusOK { t.Fatalf("config read status=%d body=%s", readRec.Code, readRec.Body.String()) } var payload map[string]any if err := json.Unmarshal(readRec.Body.Bytes(), &payload); err != nil { t.Fatalf("decode config response: %v", err) } proxies, _ := payload["proxies"].([]any) if len(proxies) != 1 { t.Fatalf("expected proxy to be persisted, got %#v", payload["proxies"]) } } func TestDeleteProxyClearsAssignedAccountProxyID(t *testing.T) { h := newAdminProxyTestHandler(t, `{ "proxies":[{"id":"proxy-1","name":"Node 1","type":"socks5","host":"127.0.0.1","port":1080}], "accounts":[{"email":"u@example.com","password":"pwd","proxy_id":"proxy-1"}] }`) r := chi.NewRouter() r.Delete("/admin/proxies/{proxyID}", h.deleteProxy) req := httptest.NewRequest(http.MethodDelete, "/admin/proxies/proxy-1", nil) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String()) } snap := h.Store.Snapshot() if len(snap.Proxies) != 0 { t.Fatalf("expected proxy removed, got %#v", snap.Proxies) } if len(snap.Accounts) != 1 { t.Fatalf("expected account kept, got %#v", snap.Accounts) } if snap.Accounts[0].ProxyID != "" { t.Fatalf("expected proxy assignment cleared, got %#v", snap.Accounts[0]) } } func TestUpdateProxyResponseDoesNotExposeStoredPassword(t *testing.T) { h := newAdminProxyTestHandler(t, `{ "proxies":[{"id":"proxy-1","name":"Node 1","type":"socks5h","host":"127.0.0.1","port":1080,"username":"u","password":"secret"}] }`) r := chi.NewRouter() r.Put("/admin/proxies/{proxyID}", h.updateProxy) req := httptest.NewRequest(http.MethodPut, "/admin/proxies/proxy-1", bytes.NewBufferString(`{ "name":"Node 1", "type":"socks5h", "host":"127.0.0.2", "port":1081, "username":"u2" }`)) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String()) } var payload map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("decode response: %v", err) } proxy, _ := payload["proxy"].(map[string]any) if _, exists := proxy["password"]; exists { t.Fatalf("response should not expose password, got %#v", proxy) } if hasPassword, _ := proxy["has_password"].(bool); !hasPassword { t.Fatalf("expected has_password=true, got %#v", proxy) } } func TestUpdateAccountProxyAssignsProxyID(t *testing.T) { h := newAdminProxyTestHandler(t, `{ "proxies":[{"id":"proxy-1","name":"Node 1","type":"socks5h","host":"127.0.0.1","port":1080}], "accounts":[{"email":"u@example.com","password":"pwd"}] }`) r := chi.NewRouter() r.Put("/admin/accounts/{identifier}/proxy", h.updateAccountProxy) req := httptest.NewRequest(http.MethodPut, "/admin/accounts/u@example.com/proxy", bytes.NewBufferString(`{"proxy_id":"proxy-1"}`)) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String()) } acc, ok := h.Store.FindAccount("u@example.com") if !ok { t.Fatal("expected account") } if acc.ProxyID != "proxy-1" { t.Fatalf("expected proxy assigned, got %#v", acc) } } func TestTestProxyUsesStoredProxy(t *testing.T) { h := newAdminProxyTestHandler(t, `{ "proxies":[{"id":"proxy-1","name":"Node 1","type":"socks5h","host":"127.0.0.1","port":1080}] }`) original := proxyConnectivityTester defer func() { proxyConnectivityTester = original }() var got config.Proxy proxyConnectivityTester = func(_ context.Context, proxy config.Proxy) map[string]any { got = proxy return map[string]any{ "success": true, "proxy_id": proxy.ID, "proxy_type": proxy.Type, "response_time": 12, } } r := chi.NewRouter() r.Post("/admin/proxies/test", h.testProxy) req := httptest.NewRequest(http.MethodPost, "/admin/proxies/test", bytes.NewBufferString(`{"proxy_id":"proxy-1"}`)) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String()) } if got.ID != "proxy-1" || got.Type != "socks5h" { t.Fatalf("expected stored proxy passed to tester, got %#v", got) } var payload map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("decode response: %v", err) } if ok, _ := payload["success"].(bool); !ok { t.Fatalf("expected success payload, got %#v", payload) } }