Files
3x-ui/internal/web/controller/xray_setting_geodata_test.go
T
Kuzz007 2736f9beb3 feat(xray): autocomplete geosite/geoip categories in the routing rule editor
Writing a routing rule today means remembering exact geosite:/geoip:/
ext:file:code syntax by hand, with no way to discover what categories
actually exist in the .dat files sitting in the bin folder -- including
custom ones like geosite_roscom.dat added via the Geodata auto-update
feature. The Domain/IP fields in the rule editor now suggest categories
as you type (e.g. "you" -> "geosite:youtube"), built live from whatever
.dat files are actually on disk, while still accepting any free-typed
value exactly as before.

Backend: GET /panel/api/xray/getGeodataCategories scans the bin folder,
parses matched geosite*/geoip*.dat files via xray-core's own exported
protobuf types, and formats each category as the exact rule syntax
xray-core's parser accepts -- geosite:/geoip: for the default files,
ext:<file>:<code> for anything else (there's no shorthand for custom
files). Cached in memory keyed by each file's (name, size, modTime) so
a request-time scan is cheap until a file actually changes.

Frontend: the Domain/IP inputs become Select "tags" fields fed by a new
useGeodataCategories() query hook, with an explicit substring filter so
"you" matches "geosite:youtube" (not a prefix). The array<->CSV-string
adapter lives entirely at the FormField transform boundary, so the
underlying form schema and saved rule shape are unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 17:13:07 +03:00

65 lines
1.9 KiB
Go

package controller
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/xtls/xray-core/common/geodata"
"google.golang.org/protobuf/proto"
)
// TestGetGeodataCategoriesEndpoint exercises the real HTTP route (gin
// routing, controller wiring, JSON envelope) rather than just the
// underlying service function -- a genuine end-to-end check of
// GET /panel/api/xray/getGeodataCategories given no DB is available in
// this environment (go-sqlite3 needs cgo) to run the full panel binary.
func TestGetGeodataCategoriesEndpoint(t *testing.T) {
dir := t.TempDir()
t.Setenv("XUI_BIN_FOLDER", dir)
writeFixture(t, filepath.Join(dir, "geosite.dat"), &geodata.GeoSiteList{
Entry: []*geodata.GeoSite{{Code: "YOUTUBE"}},
})
writeFixture(t, filepath.Join(dir, "geosite_roscom.dat"), &geodata.GeoSiteList{
Entry: []*geodata.GeoSite{{Code: "SOME-CODE"}},
})
writeFixture(t, filepath.Join(dir, "geoip.dat"), &geodata.GeoIPList{
Entry: []*geodata.GeoIP{{Code: "PRIVATE"}},
})
gin.SetMode(gin.TestMode)
router := gin.New()
NewXraySettingController(router.Group("/panel/api"))
req := httptest.NewRequest(http.MethodGet, "/panel/api/xray/getGeodataCategories", nil)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
if resp.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body=%s", resp.Code, resp.Body.String())
}
body := resp.Body.String()
for _, want := range []string{`"success":true`, `"geosite:youtube"`, `"ext:geosite_roscom.dat:some-code"`, `"geoip:private"`} {
if !strings.Contains(body, want) {
t.Errorf("response body %s does not contain %s", body, want)
}
}
}
func writeFixture(t *testing.T, path string, msg proto.Message) {
t.Helper()
data, err := proto.Marshal(msg)
if err != nil {
t.Fatalf("marshal fixture %s: %v", path, err)
}
if err := os.WriteFile(path, data, 0o644); err != nil {
t.Fatalf("write fixture %s: %v", path, err)
}
}