Files
3x-ui/internal/web/service/calendar_weekly_bound_test.go
T
MHSanaei 351a01ff0c fix(clients): bound the auto-renew interval at a century
An interval renewal turned the client's reset days into milliseconds
with no bound, so a value that wraps int64 (a node reports it unchecked)
sent the catch-up loop stepping through time for seconds per client
while the traffic writer held the database. Intervals above 36500 days
no longer renew, and every client write refuses them.
2026-10-31 14:32:31 +01:00

97 lines
3.7 KiB
Go

package service
import (
"encoding/binary"
"testing"
"time"
"github.com/mhsanaei/3x-ui/v3/internal/xray"
)
func TestWeeklyRenewalSearchFailsClosed(t *testing.T) {
// Keep a background clock reader so -race catches global timezone writes.
started, stop, done := make(chan struct{}), make(chan struct{}), make(chan struct{})
go func() {
defer close(done)
_ = time.Now()
close(started)
for {
select {
case <-stop:
return
default:
_ = time.Now()
}
}
}()
<-started
t.Cleanup(func() { close(stop); <-done })
// Fault injection: a valid TZif with twelve absent Sundays, not a claim
// about real IANA zones. Each following Monday repeats, so no Sunday returns.
const transitions = 24
data := make([]byte, 44+transitions*5+2*6+2)
copy(data, "TZif")
binary.BigEndian.PutUint32(data[32:36], transitions)
binary.BigEndian.PutUint32(data[36:40], 2)
binary.BigEndian.PutUint32(data[40:44], 2)
from := time.Date(2026, time.March, 1, 0, 0, 0, 0, time.UTC)
for week := range 12 {
at := from.AddDate(0, 0, (week+1)*7).Unix()
for day := range 2 {
index := week*2 + day
binary.BigEndian.PutUint32(data[44+index*4:48+index*4], uint32(at+int64(day)*86400))
data[44+transitions*4+index] = byte(1 - day)
}
}
binary.BigEndian.PutUint32(data[44+transitions*5+6:48+transitions*5+6], 86400)
data[len(data)-2] = 'X'
loc, err := time.LoadLocationFromTZData("Test/SkippedSundays", data)
if err != nil {
t.Fatal(err)
}
if got := nextWeeklyRenewal(from, 7, loc); !got.Equal(from) {
t.Fatalf("exhausted weekly search advanced to %s, want unchanged %s", got.Format(time.RFC3339), from.Format(time.RFC3339))
}
traffic := &xray.ClientTraffic{ExpiryTime: from.UnixMilli(), ResetWeekday: 7, ResetMax: 4, ResetCount: 2, Enable: false, Up: 111, Down: 222}
before := *traffic
expiry, renewals := catchUpClientRenewal(traffic, from.AddDate(0, 0, 28).UnixMilli(), loc)
if expiry != before.ExpiryTime || renewals != 0 || *traffic != before {
t.Fatalf("exhausted search changed billing/traffic: expiry=%d renewals=%d traffic=%+v", expiry, renewals, traffic)
}
for _, tt := range []struct {
name string
now int64
}{
{"initial suggestion exhausted", from.UnixMilli()},
{"catch-up exhausted with allowances left", from.AddDate(0, 3, 0).UnixMilli()},
} {
t.Run(tt.name, func(t *testing.T) {
preview, err := previewClientRenewal(ClientRenewalPreviewRequest{
ExpiryTime: before.ExpiryTime, ResetWeekday: 7, ResetMax: 4, ResetCount: 2,
}, tt.now, loc)
if err == nil || err.Error() != "calendar renewal could not find a future expiry\n" || preview != nil {
t.Fatalf("failed search preview/error = %+v/%v, want nil/calendar search error", preview, err)
}
})
}
}
// A node reports the renewal interval unchecked; one that wraps int64 when turned into
// milliseconds made the catch-up loop step through time for seconds per client.
func TestCatchUpClientRenewalRefusesAnOversizedInterval(t *testing.T) {
now := time.Now().UnixMilli()
for _, reset := range []int{106751991167, maxClientResetDays + 1} {
traffic := &xray.ClientTraffic{Email: "c", Reset: reset, ExpiryTime: now - 86400000}
start := time.Now()
expiry, renewals := catchUpClientRenewal(traffic, now, time.UTC)
if expiry != traffic.ExpiryTime || renewals != 0 || time.Since(start) > 100*time.Millisecond {
t.Fatalf("reset %d: expiry %d, renewals %d after %v; want no renewal", reset, expiry, renewals, time.Since(start))
}
}
traffic := &xray.ClientTraffic{Email: "c", Reset: maxClientResetDays, ExpiryTime: now - 86400000}
if _, renewals := catchUpClientRenewal(traffic, now, time.UTC); renewals != 1 {
t.Fatalf("a %d-day interval renewed %d times, want once", maxClientResetDays, renewals)
}
}