mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-30 21:56:41 +08:00
12d51d7195
Most tests opened a throwaway panel DB with database.InitDB, which runs the full AutoMigrate + seed on an empty file every time: ~230ms, and ~850ms under -race because GORM's reflection-heavy migration is what the detector slows most. internal/web/service does this in ~550 of its 830 tests, so the CI race job spent ~10 of its ~14.6 minutes re-migrating empty databases. internal/database/dbtest.InitDB migrates once per test process, then hands each test its own copy of that file (~130ms under -race) and registers the CloseDB cleanup. The copy then goes through InitDB like a panel restart, so every test still starts from the state a fresh install has. Tests that reopen an existing file, migrate a hand-built legacy DB or target Postgres keep calling database.InitDB. Locally under -race: internal/web/service 626s (last CI run) -> 114s, internal/sub 246s -> 35s.
167 lines
5.3 KiB
Go
167 lines
5.3 KiB
Go
package controller
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/gin-contrib/sessions"
|
|
"github.com/gin-contrib/sessions/cookie"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/op/go-logging"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/entity"
|
|
)
|
|
|
|
func newHostTestDB(t *testing.T) {
|
|
t.Helper()
|
|
xuilogger.InitLogger(logging.ERROR)
|
|
gin.SetMode(gin.TestMode)
|
|
dbDir := t.TempDir()
|
|
t.Setenv("XUI_DB_FOLDER", dbDir)
|
|
dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db"))
|
|
}
|
|
|
|
type hostEnvelope struct {
|
|
Success bool `json:"success"`
|
|
Msg string `json:"msg"`
|
|
Obj json.RawMessage `json:"obj"`
|
|
}
|
|
|
|
func doHostReq(t *testing.T, engine *gin.Engine, method, path string, body any) hostEnvelope {
|
|
t.Helper()
|
|
var rdr *bytes.Reader
|
|
if body != nil {
|
|
b, _ := json.Marshal(body)
|
|
rdr = bytes.NewReader(b)
|
|
} else {
|
|
rdr = bytes.NewReader(nil)
|
|
}
|
|
req := httptest.NewRequest(method, path, rdr)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
engine.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("%s %s: status %d, body=%s", method, path, w.Code, w.Body.String())
|
|
}
|
|
var env hostEnvelope
|
|
if err := json.Unmarshal(w.Body.Bytes(), &env); err != nil {
|
|
t.Fatalf("%s %s: decode envelope: %v body=%s", method, path, err, w.Body.String())
|
|
}
|
|
return env
|
|
}
|
|
|
|
func TestHostController_AddListGetDelete(t *testing.T) {
|
|
newHostTestDB(t)
|
|
engine := gin.New()
|
|
NewHostController(engine.Group("/panel/api/hosts"))
|
|
|
|
ib := &model.Inbound{Tag: "ctl", Enable: true, Port: 5443, Protocol: model.VLESS, Settings: `{"clients":[]}`}
|
|
if err := database.GetDB().Create(ib).Error; err != nil {
|
|
t.Fatalf("seed inbound: %v", err)
|
|
}
|
|
|
|
add := doHostReq(t, engine, http.MethodPost, "/panel/api/hosts/add", map[string]any{
|
|
"inboundIds": []int{ib.Id}, "remark": "h1", "hosts": []string{"h1.example.com"}, "port": 8443,
|
|
})
|
|
if !add.Success {
|
|
t.Fatalf("add not successful: %s", add.Msg)
|
|
}
|
|
var created []*model.Host
|
|
if err := json.Unmarshal(add.Obj, &created); err != nil {
|
|
t.Fatalf("decode created hosts: %v", err)
|
|
}
|
|
if len(created) != 1 || created[0].GroupId == "" || created[0].Remark != "h1" {
|
|
t.Fatalf("created hosts = %+v", created)
|
|
}
|
|
groupId := created[0].GroupId
|
|
|
|
list := doHostReq(t, engine, http.MethodGet, "/panel/api/hosts/list", nil)
|
|
var groups []entity.HostGroup
|
|
if err := json.Unmarshal(list.Obj, &groups); err != nil {
|
|
t.Fatalf("decode list: %v", err)
|
|
}
|
|
if len(groups) != 1 || groups[0].GroupId != groupId {
|
|
t.Fatalf("list = %+v, want one group groupId=%s", groups, groupId)
|
|
}
|
|
|
|
get := doHostReq(t, engine, http.MethodGet, "/panel/api/hosts/get/"+groupId, nil)
|
|
if !get.Success {
|
|
t.Fatalf("get not successful: %s", get.Msg)
|
|
}
|
|
|
|
update := doHostReq(t, engine, http.MethodPost, "/panel/api/hosts/update/"+groupId, map[string]any{
|
|
"inboundIds": []int{ib.Id}, "remark": "h1-updated", "hosts": []string{"h1.example.com"}, "port": 8443,
|
|
})
|
|
if !update.Success {
|
|
t.Fatalf("update not successful: %s", update.Msg)
|
|
}
|
|
get2 := doHostReq(t, engine, http.MethodGet, "/panel/api/hosts/get/"+groupId, nil)
|
|
var group2 entity.HostGroup
|
|
_ = json.Unmarshal(get2.Obj, &group2)
|
|
if group2.Remark != "h1-updated" {
|
|
t.Fatalf("update did not change remark: %s", group2.Remark)
|
|
}
|
|
|
|
setEn := doHostReq(t, engine, http.MethodPost, "/panel/api/hosts/bulk/setEnable", map[string]any{
|
|
"ids": []string{groupId}, "enable": false,
|
|
})
|
|
if !setEn.Success {
|
|
t.Fatalf("bulk/setEnable not successful: %s", setEn.Msg)
|
|
}
|
|
get3 := doHostReq(t, engine, http.MethodGet, "/panel/api/hosts/get/"+groupId, nil)
|
|
var group3 entity.HostGroup
|
|
_ = json.Unmarshal(get3.Obj, &group3)
|
|
if !group3.IsDisabled {
|
|
t.Fatalf("bulk/setEnable did not disable host group")
|
|
}
|
|
|
|
add2 := doHostReq(t, engine, http.MethodPost, "/panel/api/hosts/bulk/add", map[string]any{
|
|
"inboundIds": []int{ib.Id}, "remark": "h2", "hosts": []string{"h2.example.com"}, "port": 8443,
|
|
})
|
|
var created2 []*model.Host
|
|
_ = json.Unmarshal(add2.Obj, &created2)
|
|
groupId2 := created2[0].GroupId
|
|
|
|
bulkDel := doHostReq(t, engine, http.MethodPost, "/panel/api/hosts/bulk/del", map[string]any{
|
|
"ids": []string{groupId, groupId2},
|
|
})
|
|
if !bulkDel.Success {
|
|
t.Fatalf("bulk/del not successful: %s", bulkDel.Msg)
|
|
}
|
|
|
|
list2 := doHostReq(t, engine, http.MethodGet, "/panel/api/hosts/list", nil)
|
|
var groups2 []entity.HostGroup
|
|
_ = json.Unmarshal(list2.Obj, &groups2)
|
|
if len(groups2) != 0 {
|
|
t.Fatalf("after delete, list = %+v, want empty", groups2)
|
|
}
|
|
}
|
|
|
|
func TestHostController_AuthInherited(t *testing.T) {
|
|
newHostTestDB(t)
|
|
engine := gin.New()
|
|
store := cookie.NewStore([]byte("host-auth-test-secret"))
|
|
engine.Use(sessions.Sessions("3x-ui", store))
|
|
|
|
a := &APIController{}
|
|
api := engine.Group("/panel/api")
|
|
api.Use(a.checkAPIAuth)
|
|
NewHostController(api.Group("/hosts"))
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/panel/api/hosts/list", nil)
|
|
req.Header.Set("X-Requested-With", "XMLHttpRequest")
|
|
w := httptest.NewRecorder()
|
|
engine.ServeHTTP(w, req)
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Fatalf("unauthenticated hosts/list = %d, want 401 (auth inherited)", w.Code)
|
|
}
|
|
}
|