| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114 |
- package service
- import (
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "net/http/httptest"
- "strconv"
- "strings"
- "sync"
- "testing"
- "github.com/mhsanaei/3x-ui/v3/internal/database"
- "github.com/mhsanaei/3x-ui/v3/internal/database/model"
- )
- // fakeNodeHTTP emulates a node panel over real HTTP, so the master's Remote
- // (tag cache, list fetch, per-op RPC, timeouts) runs for real, not a stub.
- type fakeNodeHTTP struct {
- srv *httptest.Server
- mu sync.Mutex
- tags map[string]int
- hits map[string]int
- // hold makes every non-list request block until the master gives up.
- hold bool
- release chan struct{}
- }
- func newFakeNodeHTTP(t *testing.T) *fakeNodeHTTP {
- t.Helper()
- f := &fakeNodeHTTP{tags: map[string]int{}, hits: map[string]int{}, release: make(chan struct{})}
- f.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- if strings.HasSuffix(r.URL.Path, "/inbounds/list") {
- f.mu.Lock()
- f.hits["list"]++
- type ent struct {
- Id int `json:"id"`
- Tag string `json:"tag"`
- }
- list := make([]ent, 0, len(f.tags))
- for tag, id := range f.tags {
- list = append(list, ent{Id: id, Tag: tag})
- }
- f.mu.Unlock()
- b, _ := json.Marshal(list)
- _, _ = w.Write([]byte(`{"success":true,"msg":"","obj":` + string(b) + `}`))
- return
- }
- f.mu.Lock()
- f.hits[r.URL.Path]++
- hold := f.hold
- f.mu.Unlock()
- if hold {
- // Drain first: the server only notices a client disconnect once the
- // body is consumed, and Close would otherwise wait on this forever.
- _, _ = io.Copy(io.Discard, r.Body)
- select {
- case <-r.Context().Done():
- case <-f.release:
- }
- return
- }
- _, _ = w.Write([]byte(`{"success":true,"msg":""}`))
- }))
- t.Cleanup(f.srv.Close)
- // Registered after Close, so it runs first and frees any held handler.
- t.Cleanup(func() { close(f.release) })
- return f
- }
- func (f *fakeNodeHTTP) setHold(v bool) {
- f.mu.Lock()
- defer f.mu.Unlock()
- f.hold = v
- }
- // hitCount is how many requests whose path contains pathPart reached the node.
- func (f *fakeNodeHTTP) hitCount(pathPart string) int {
- f.mu.Lock()
- defer f.mu.Unlock()
- n := 0
- for path, c := range f.hits {
- if strings.Contains(path, pathPart) {
- n += c
- }
- }
- return n
- }
- // realNodeInbound creates a Node row pointing at the fake server plus one
- // inbound on it, with NO runtime override so RuntimeFor builds a real Remote.
- func realNodeInbound(t *testing.T, f *fakeNodeHTTP, port int, clients []model.Client) *model.Inbound {
- t.Helper()
- hostPart, portStr, _ := strings.Cut(strings.TrimPrefix(f.srv.URL, "http://"), ":")
- srvPort, err := strconv.Atoi(portStr)
- if err != nil {
- t.Fatalf("parse fake node port: %v", err)
- }
- node := &model.Node{
- Name: fmt.Sprintf("%s-%d", t.Name(), port), Scheme: "http", Address: hostPart, Port: srvPort,
- BasePath: "/", ApiToken: "tok", Enable: true, Status: "online",
- AllowPrivateAddress: true, TlsVerifyMode: "verify",
- }
- if err := database.GetDB().Create(node).Error; err != nil {
- t.Fatalf("create node: %v", err)
- }
- ib := nodeInbound(t, node.Id, port, clients)
- f.mu.Lock()
- f.tags[ib.Tag] = 100 + port%100
- f.mu.Unlock()
- return ib
- }
|