package main import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" ) func post(t *testing.T, handler http.Handler, path, body, auth string) *httptest.ResponseRecorder { t.Helper() req := httptest.NewRequest(http.MethodPost, path, bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") if auth != "" { req.Header.Set("Authorization", "Bearer "+auth) } w := httptest.NewRecorder() handler.ServeHTTP(w, req) return w } // TestConnectReturnsValidConfig verifies that a well-formed request returns // a 200 with all four §3.1 top-level blocks present. func TestConnectReturnsValidConfig(t *testing.T) { h := makeHandler("test-token") w := post(t, h, "/v1/nodes/sg-1/connect", `{"device_id":"dev-001"}`, "test-token") if w.Code != http.StatusOK { t.Fatalf("want 200, got %d: %s", w.Code, w.Body.String()) } var out map[string]any if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil { t.Fatalf("response is not valid JSON: %v\nbody: %s", err, w.Body.String()) } for _, key := range []string{"inbounds", "outbounds", "route", "dns"} { if _, ok := out[key]; !ok { t.Errorf("§3.1 block missing: %q", key) } } // outbounds must include urltest group named "auto" outs, _ := out["outbounds"].([]any) found := false for _, o := range outs { m, _ := o.(map[string]any) if m["type"] == "urltest" && m["tag"] == "auto" { found = true } } if !found { t.Error("outbounds: missing urltest group 'auto'") } // inbound must have strict_route:true (Kill-switch) ins, _ := out["inbounds"].([]any) if len(ins) == 0 { t.Fatal("inbounds is empty") } tun, _ := ins[0].(map[string]any) if tun["strict_route"] != true { t.Error("inbounds[0].strict_route must be true (Kill-switch)") } } // TestConnectRequiresAuth verifies that a missing / wrong token yields 401. func TestConnectRequiresAuth(t *testing.T) { h := makeHandler("secret") cases := []struct{ name, token string }{ {"no-header", ""}, {"wrong-token", "wrong"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { w := post(t, h, "/v1/nodes/sg-1/connect", `{"device_id":"x"}`, c.token) if w.Code != http.StatusUnauthorized { t.Errorf("want 401, got %d", w.Code) } }) } } // TestConnectNoAuthCheck verifies that an empty token skips auth entirely. func TestConnectNoAuthCheck(t *testing.T) { h := makeHandler("") w := post(t, h, "/v1/nodes/sg-1/connect", `{"device_id":"x"}`, "") if w.Code != http.StatusOK { t.Errorf("want 200, got %d", w.Code) } } // TestConnectRequiresDeviceID verifies that missing device_id yields 400. func TestConnectRequiresDeviceID(t *testing.T) { h := makeHandler("") cases := []struct{ name, body string }{ {"empty-object", `{}`}, {"blank-id", `{"device_id":""}`}, {"not-json", `not-json`}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/v1/nodes/sg-1/connect", bytes.NewBufferString(c.body)) rw := httptest.NewRecorder() h.ServeHTTP(rw, req) if rw.Code != http.StatusBadRequest { t.Errorf("want 400, got %d", rw.Code) } }) } } // TestConnectMethodNotAllowed verifies that GET returns 405. func TestConnectMethodNotAllowed(t *testing.T) { h := makeHandler("") req := httptest.NewRequest(http.MethodGet, "/v1/nodes/sg-1/connect", nil) w := httptest.NewRecorder() h.ServeHTTP(w, req) if w.Code != http.StatusMethodNotAllowed { t.Errorf("want 405, got %d", w.Code) } } // TestConnectWrongPath verifies that unrelated paths return 404. func TestConnectWrongPath(t *testing.T) { h := makeHandler("") req := httptest.NewRequest(http.MethodPost, "/v1/nodes/sg-1/disconnect", nil) w := httptest.NewRecorder() h.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("want 404, got %d", w.Code) } }