mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-31 04:12:13 +03:00
Frontend deps: @hookform/resolvers 5.4.3 -> 5.5.7, Storybook 10.5.4 -> 10.5.5 across the four packages we declare, globals 17.7.0 -> 17.8.0, and jsdom 29.1.1 -> 30.0.1. The jsdom major replaces its CSS and selector stack -- @asamuzakjp/css-color 5 -> 6, @asamuzakjp/dom-selector 7 -> 8, undici 7 -> 8, nwsapi and generational-cache folded into their parents, whatwg-url 17 nested underneath. Nothing in the Vitest suites reaches those directly and the whole frontend gate (typecheck, lint, tests, build, Storybook compile) is green. Panel frontend version to 0.6.0. Backend deps: mattn/go-sqlite3 1.14.48 -> 1.14.49 and valyala/fasthttp 1.72.0 -> 1.73.0, plus the golang.org/x/exp and genproto/googleapis/rpc indirect bumps that came with them. Go tests: modernize -fix output, covering range-over-int, sync.WaitGroup.Go in place of manual Add/Done pairs, maps.Copy, and Go 1.26 new(expr) for pointer-to-value in the forwarded-trust table. The storedAs helper is deleted instead of being left behind a //go:fix inline directive -- keeping it that way fails govet on the one call site the rewrite did not reach, and every caller now takes new(...) directly. Behaviour is unchanged. DnsTab: the hosts-sync effect tested dns while declaring dnsEnabled in its dependency array. Both carry the same truth value, so this is exhaustive-deps hygiene rather than a behaviour change.
181 lines
4.9 KiB
Go
181 lines
4.9 KiB
Go
package sub
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func resetSubscriptionCache(t *testing.T) {
|
|
t.Helper()
|
|
subscriptionCache.Lock()
|
|
previousEntries := subscriptionCache.m
|
|
previousInflight := subscriptionCache.inflight
|
|
subscriptionCache.m = make(map[string]subscriptionCacheEntry)
|
|
subscriptionCache.inflight = make(map[string]*subscriptionFetch)
|
|
subscriptionCache.Unlock()
|
|
t.Cleanup(func() {
|
|
subscriptionCache.Lock()
|
|
subscriptionCache.m = previousEntries
|
|
subscriptionCache.inflight = previousInflight
|
|
subscriptionCache.Unlock()
|
|
})
|
|
}
|
|
|
|
func TestFetchSubscriptionLinksSharesConcurrentRefresh(t *testing.T) {
|
|
resetSubscriptionCache(t)
|
|
var requests atomic.Int32
|
|
release := make(chan struct{})
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requests.Add(1)
|
|
<-release
|
|
_, _ = w.Write([]byte("vless://uuid@example.com:443"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
const callers = 16
|
|
results := make(chan []string, callers)
|
|
var wg sync.WaitGroup
|
|
for range callers {
|
|
wg.Go(func() {
|
|
results <- fetchSubscriptionLinks(srv.URL)
|
|
})
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
close(release)
|
|
wg.Wait()
|
|
close(results)
|
|
|
|
for links := range results {
|
|
if len(links) != 1 || links[0] != "vless://uuid@example.com:443" {
|
|
t.Fatalf("links = %#v", links)
|
|
}
|
|
}
|
|
if got := requests.Load(); got != 1 {
|
|
t.Fatalf("requests = %d, want 1", got)
|
|
}
|
|
}
|
|
|
|
func TestFetchSubscriptionLinksBoundsCacheSize(t *testing.T) {
|
|
resetSubscriptionCache(t)
|
|
var requests atomic.Int32
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requests.Add(1)
|
|
_, _ = w.Write([]byte("vless://uuid@example.com:443"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
for i := range subscriptionCacheCapacity + 1 {
|
|
links := fetchSubscriptionLinks(srv.URL + "?id=" + strconv.Itoa(i))
|
|
if len(links) != 1 {
|
|
t.Fatalf("links at %d = %#v", i, links)
|
|
}
|
|
}
|
|
|
|
subscriptionCache.Lock()
|
|
entries := len(subscriptionCache.m)
|
|
subscriptionCache.Unlock()
|
|
if entries != subscriptionCacheCapacity {
|
|
t.Fatalf("cache entries = %d, want %d", entries, subscriptionCacheCapacity)
|
|
}
|
|
if got := requests.Load(); got != subscriptionCacheCapacity+1 {
|
|
t.Fatalf("requests = %d, want %d", got, subscriptionCacheCapacity+1)
|
|
}
|
|
}
|
|
|
|
func TestFetchSubscriptionLinksSharesStaleResultAfterRefreshFailure(t *testing.T) {
|
|
resetSubscriptionCache(t)
|
|
stale := []string{"vless://stale@example.com:443"}
|
|
release := make(chan struct{})
|
|
var staleRequests atomic.Int32
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/stale" {
|
|
staleRequests.Add(1)
|
|
<-release
|
|
w.WriteHeader(http.StatusBadGateway)
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte("vless://fresh@example.com:443"))
|
|
}))
|
|
defer srv.Close()
|
|
staleURL := srv.URL + "/stale"
|
|
|
|
subscriptionCache.Lock()
|
|
subscriptionCache.m[staleURL] = subscriptionCacheEntry{
|
|
links: stale,
|
|
fetchedAt: time.Now().Add(-subscriptionCacheTTL),
|
|
}
|
|
for i := range subscriptionCacheCapacity - 1 {
|
|
subscriptionCache.m["cached-"+strconv.Itoa(i)] = subscriptionCacheEntry{fetchedAt: time.Now()}
|
|
}
|
|
subscriptionCache.Unlock()
|
|
|
|
const callers = 16
|
|
results := make(chan []string, callers)
|
|
var wg sync.WaitGroup
|
|
for range callers {
|
|
wg.Go(func() {
|
|
results <- fetchSubscriptionLinks(staleURL)
|
|
})
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
if links := fetchSubscriptionLinks(srv.URL + "/fresh"); len(links) != 1 || links[0] != "vless://fresh@example.com:443" {
|
|
t.Fatalf("fresh links = %#v", links)
|
|
}
|
|
close(release)
|
|
wg.Wait()
|
|
close(results)
|
|
|
|
for links := range results {
|
|
if len(links) != 1 || links[0] != stale[0] {
|
|
t.Fatalf("links = %#v, want %#v", links, stale)
|
|
}
|
|
}
|
|
if got := staleRequests.Load(); got != 1 {
|
|
t.Fatalf("requests = %d, want 1", got)
|
|
}
|
|
}
|
|
|
|
func TestDoFetchSubscriptionLinks_RejectsOversizedBody(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(strings.Repeat("a", subscriptionMaxBytes+1)))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
links, err := doFetchSubscriptionLinks(srv.URL)
|
|
if !errors.Is(err, errSubscriptionBodyTooLarge) {
|
|
t.Fatalf("err = %v, want errSubscriptionBodyTooLarge", err)
|
|
}
|
|
if links != nil {
|
|
t.Fatalf("links = %v, want nil", links)
|
|
}
|
|
}
|
|
|
|
func TestDoFetchSubscriptionLinks_AcceptsBodyAtLimit(t *testing.T) {
|
|
link := "vless://example"
|
|
body := link + "\n" + strings.Repeat("#", subscriptionMaxBytes-len(link)-1)
|
|
if len(body) != subscriptionMaxBytes {
|
|
t.Fatalf("fixture size = %d, want %d", len(body), subscriptionMaxBytes)
|
|
}
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(body))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
links, err := doFetchSubscriptionLinks(srv.URL)
|
|
if err != nil {
|
|
t.Fatalf("unexpected err: %v", err)
|
|
}
|
|
if len(links) != 1 || links[0] != link {
|
|
t.Fatalf("links = %v, want [%q]", links, link)
|
|
}
|
|
}
|