package httpapi import ( "context" "database/sql" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/wangjia/pangolin/server/internal/codes" "github.com/wangjia/pangolin/server/internal/config" "github.com/wangjia/pangolin/server/internal/routing" "github.com/wangjia/pangolin/server/internal/store" ) // openRoutingTestDB opens an in-memory SQLite DB with migrations applied, // mirroring internal/routing/store_sqlite_test.go's helper (no shared helper // exists in this package yet). func openRoutingTestDB(t *testing.T) *sql.DB { t.Helper() db, err := store.Open(&config.Config{Driver: "sqlite", DSN: ":memory:"}) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = db.Close() }) if err := store.MigrateUp(db, "sqlite"); err != nil { t.Fatal(err) } if err := store.ApplyCodesLibMigrations(context.Background(), db, "sqlite"); err != nil { t.Fatal(err) } return db } func seedRoutingUser(t *testing.T, db *sql.DB, id int64) { t.Helper() uuid := "u-routing" if _, err := db.Exec(`INSERT INTO users (id,uuid,email,pw_hash,dp_uuid,status,created_at) VALUES (?,?,?, 'x','dp-'||?, 'active', ?)`, id, uuid, uuid+"@x", uuid, time.Now().UTC()); err != nil { t.Fatal(err) } } // doAuthReq builds an httptest request with an authenticated context (numeric // user id injected under codes.CtxKeyUserID, matching auth.RequireAuth) and // invokes the handler directly (no router needed for a single route). func doAuthReq(t *testing.T, method, target string, body *strings.Reader, uid int64, h http.HandlerFunc) *httptest.ResponseRecorder { t.Helper() var req *http.Request if body == nil { req = httptest.NewRequest(method, target, nil) } else { req = httptest.NewRequest(method, target, body) } ctx := context.WithValue(req.Context(), codes.CtxKeyUserID, uid) req = req.WithContext(ctx) rr := httptest.NewRecorder() h(rr, req) return rr } func TestRoutingGetDefaultThenSave(t *testing.T) { db := openRoutingTestDB(t) seedRoutingUser(t, db, 7) api := NewRoutingAPI(routing.NewStore(db)) // GET 无档案 → 200 + Default rr := doAuthReq(t, http.MethodGet, "/v1/me/routing", nil, 7, api.GetProfile) if rr.Code != 200 { t.Fatalf("GET code %d", rr.Code) } var p routing.Profile if err := json.Unmarshal(rr.Body.Bytes(), &p); err != nil { t.Fatalf("unmarshal: %v", err) } if p.Mode != "rule" { t.Fatalf("default mode %s", p.Mode) } // POST 合法 → 200 body := `{"mode":"rule","builtin":{"china_direct":true,"lan_direct":true,"private_via_tunnel":true},"rules":[{"type":"domain_suffix","value":"x.com","action":"direct","enabled":true}],"final":"proxy"}` rr = doAuthReq(t, http.MethodPost, "/v1/me/routing", strings.NewReader(body), 7, api.SaveProfile) if rr.Code != 200 { t.Fatalf("POST code %d body %s", rr.Code, rr.Body) } // GET 后应能取回刚保存的档案 rr = doAuthReq(t, http.MethodGet, "/v1/me/routing", nil, 7, api.GetProfile) if rr.Code != 200 { t.Fatalf("GET-after-save code %d", rr.Code) } var p2 routing.Profile if err := json.Unmarshal(rr.Body.Bytes(), &p2); err != nil { t.Fatalf("unmarshal2: %v", err) } if len(p2.Rules) != 1 || p2.Rules[0].Value != "x.com" { t.Fatalf("saved profile not persisted: %+v", p2) } // POST 非法 → 400 + errors rr = doAuthReq(t, http.MethodPost, "/v1/me/routing", strings.NewReader(`{"mode":"x","final":"y","rules":[]}`), 7, api.SaveProfile) if rr.Code != 400 { t.Fatalf("bad POST code %d", rr.Code) } var errBody map[string]any if err := json.Unmarshal(rr.Body.Bytes(), &errBody); err != nil { t.Fatalf("unmarshal err body: %v", err) } if errBody["code"] != "routing_invalid" { t.Fatalf("bad POST body code = %v", errBody["code"]) } errs, ok := errBody["errors"].([]any) if !ok || len(errs) == 0 { t.Fatalf("expected non-empty errors, got %v", errBody["errors"]) } } func TestRoutingGetUnauthorized(t *testing.T) { db := openRoutingTestDB(t) api := NewRoutingAPI(routing.NewStore(db)) req := httptest.NewRequest(http.MethodGet, "/v1/me/routing", nil) rr := httptest.NewRecorder() api.GetProfile(rr, req) if rr.Code != http.StatusUnauthorized { t.Fatalf("expected 401, got %d", rr.Code) } }