calendar_weekly_bound_test.go 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. package service
  2. import (
  3. "encoding/binary"
  4. "testing"
  5. "time"
  6. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  7. )
  8. func TestWeeklyRenewalSearchFailsClosed(t *testing.T) {
  9. // Keep a background clock reader so -race catches global timezone writes.
  10. started, stop, done := make(chan struct{}), make(chan struct{}), make(chan struct{})
  11. go func() {
  12. defer close(done)
  13. _ = time.Now()
  14. close(started)
  15. for {
  16. select {
  17. case <-stop:
  18. return
  19. default:
  20. _ = time.Now()
  21. }
  22. }
  23. }()
  24. <-started
  25. t.Cleanup(func() { close(stop); <-done })
  26. // Fault injection: a valid TZif with twelve absent Sundays, not a claim
  27. // about real IANA zones. Each following Monday repeats, so no Sunday returns.
  28. const transitions = 24
  29. data := make([]byte, 44+transitions*5+2*6+2)
  30. copy(data, "TZif")
  31. binary.BigEndian.PutUint32(data[32:36], transitions)
  32. binary.BigEndian.PutUint32(data[36:40], 2)
  33. binary.BigEndian.PutUint32(data[40:44], 2)
  34. from := time.Date(2026, time.March, 1, 0, 0, 0, 0, time.UTC)
  35. for week := range 12 {
  36. at := from.AddDate(0, 0, (week+1)*7).Unix()
  37. for day := range 2 {
  38. index := week*2 + day
  39. binary.BigEndian.PutUint32(data[44+index*4:48+index*4], uint32(at+int64(day)*86400))
  40. data[44+transitions*4+index] = byte(1 - day)
  41. }
  42. }
  43. binary.BigEndian.PutUint32(data[44+transitions*5+6:48+transitions*5+6], 86400)
  44. data[len(data)-2] = 'X'
  45. loc, err := time.LoadLocationFromTZData("Test/SkippedSundays", data)
  46. if err != nil {
  47. t.Fatal(err)
  48. }
  49. if got := nextWeeklyRenewal(from, 7, loc); !got.Equal(from) {
  50. t.Fatalf("exhausted weekly search advanced to %s, want unchanged %s", got.Format(time.RFC3339), from.Format(time.RFC3339))
  51. }
  52. traffic := &xray.ClientTraffic{ExpiryTime: from.UnixMilli(), ResetWeekday: 7, ResetMax: 4, ResetCount: 2, Enable: false, Up: 111, Down: 222}
  53. before := *traffic
  54. expiry, renewals := catchUpClientRenewal(traffic, from.AddDate(0, 0, 28).UnixMilli(), loc)
  55. if expiry != before.ExpiryTime || renewals != 0 || *traffic != before {
  56. t.Fatalf("exhausted search changed billing/traffic: expiry=%d renewals=%d traffic=%+v", expiry, renewals, traffic)
  57. }
  58. for _, tt := range []struct {
  59. name string
  60. now int64
  61. }{
  62. {"initial suggestion exhausted", from.UnixMilli()},
  63. {"catch-up exhausted with allowances left", from.AddDate(0, 3, 0).UnixMilli()},
  64. } {
  65. t.Run(tt.name, func(t *testing.T) {
  66. preview, err := previewClientRenewal(ClientRenewalPreviewRequest{
  67. ExpiryTime: before.ExpiryTime, ResetWeekday: 7, ResetMax: 4, ResetCount: 2,
  68. }, tt.now, loc)
  69. if err == nil || err.Error() != "calendar renewal could not find a future expiry\n" || preview != nil {
  70. t.Fatalf("failed search preview/error = %+v/%v, want nil/calendar search error", preview, err)
  71. }
  72. })
  73. }
  74. }