simCardStatistics_test.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. package simCardQueue
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "fwh-toolbox-server/domain"
  8. "io"
  9. "net/http"
  10. "strings"
  11. "testing"
  12. "github.com/hibiken/asynq"
  13. )
  14. type roundTripFunc func(*http.Request) (*http.Response, error)
  15. func (fn roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return fn(req) }
  16. func TestAccumulateSimCardStatistics(t *testing.T) {
  17. stats := domain.SimCardDailyStatistics{Date: "2026-09-24"}
  18. zero := 0.0
  19. previous := 200.0 * 1024
  20. // Thresholds are strict; cards without a previous snapshot count only toward totals.
  21. accumulateSimCardStatistics(&stats, 200*1024, &zero)
  22. accumulateSimCardStatistics(&stats, 1024*1024, &previous)
  23. accumulateSimCardStatistics(&stats, 2*1024*1024, nil)
  24. accumulateSimCardStatistics(&stats, 3*1024*1024, &zero)
  25. if stats.CardCount != 4 || stats.TotalOver200MB != 3 || stats.TotalOver1GB != 2 || stats.TotalOver2GB != 1 {
  26. t.Fatalf("unexpected total statistics: %+v", stats)
  27. }
  28. if stats.MaxTotalKB != 3*1024*1024 || stats.IncrementCardCount != 3 {
  29. t.Fatalf("unexpected maxima or comparison count: %+v", stats)
  30. }
  31. if stats.IncrementOver20MB != 3 || stats.IncrementOver50MB != 3 || stats.IncrementOver200MB != 2 {
  32. t.Fatalf("unexpected increment statistics: %+v", stats)
  33. }
  34. if stats.MaxIncrementKB == nil || *stats.MaxIncrementKB != 3*1024*1024 {
  35. t.Fatalf("unexpected maximum increment: %+v", stats.MaxIncrementKB)
  36. }
  37. if !strings.Contains(formatSimCardStatistics(stats), "最大增量:3072.00 MB") {
  38. t.Fatal("report does not show maximum increment")
  39. }
  40. }
  41. func TestPostSimCardStatistics(t *testing.T) {
  42. client := &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
  43. if r.Method != http.MethodPost || r.Header.Get("Content-Type") != "application/json" {
  44. t.Errorf("unexpected request: %s %s", r.Method, r.Header.Get("Content-Type"))
  45. }
  46. if r.URL.Query().Get("timestamp") == "" || r.URL.Query().Get("sign") == "" || r.URL.Query().Get("access_token") != "test" {
  47. t.Errorf("signature or access token missing: %s", r.URL.RawQuery)
  48. }
  49. var message struct {
  50. MsgType string `json:"msgtype"`
  51. Markdown struct {
  52. Text string `json:"text"`
  53. } `json:"markdown"`
  54. }
  55. if err := json.NewDecoder(r.Body).Decode(&message); err != nil {
  56. t.Error(err)
  57. }
  58. if message.MsgType != "markdown" || !strings.Contains(message.Markdown.Text, "2026-09-24") {
  59. t.Errorf("unexpected message: %+v", message)
  60. }
  61. return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"errcode":0,"errmsg":"ok"}`))}, nil
  62. })}
  63. report := simCardDailyReport{Stats: domain.SimCardDailyStatistics{Date: "2026-09-24"}}
  64. if err := postSimCardStatistics(context.Background(), client, "https://oapi.dingtalk.com/robot/send?access_token=test", "secret", report); err != nil {
  65. t.Fatal(err)
  66. }
  67. }
  68. func TestPostSimCardStatisticsRejectsBotError(t *testing.T) {
  69. client := &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
  70. return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"errcode":310000,"errmsg":"rejected"}`))}, nil
  71. })}
  72. if err := postSimCardStatistics(context.Background(), client, "https://oapi.dingtalk.com/robot/send", "", simCardDailyReport{}); err == nil {
  73. t.Fatal("DingTalk error response must be retried")
  74. }
  75. }
  76. func TestUnconfiguredWebhookSkipsStatistics(t *testing.T) {
  77. processor := &SimCardProcessor{}
  78. if err := processor.enqueueSimCardReportIfComplete("2026-09-24"); err != nil {
  79. t.Fatal(err)
  80. }
  81. if err := processor.handleSimCardDailyReport(context.Background(), asynq.NewTask(taskTypeSimCardDailyReport, []byte("2026-09-24"))); err != nil {
  82. t.Fatal(err)
  83. }
  84. }
  85. func TestAccumulateSimCardStatisticsMissingPrevious(t *testing.T) {
  86. stats := domain.SimCardDailyStatistics{}
  87. accumulateSimCardStatistics(&stats, 300*1024, nil)
  88. if stats.IncrementCardCount != 0 || stats.MaxIncrementKB != nil {
  89. t.Fatalf("missing previous day must not create an increment: %+v", stats)
  90. }
  91. }
  92. func TestAccumulateSimCardStatisticsCounterReset(t *testing.T) {
  93. stats := domain.SimCardDailyStatistics{}
  94. previous := 2.0 * 1024 * 1024
  95. accumulateSimCardStatistics(&stats, 60*1024, &previous)
  96. if stats.MaxIncrementKB == nil || *stats.MaxIncrementKB != 60*1024 || stats.IncrementOver50MB != 1 {
  97. t.Fatalf("reset counter should use current accumulated usage: %+v", stats)
  98. }
  99. }
  100. func TestAccumulateSimCardStatisticsReturnsIncrement(t *testing.T) {
  101. stats := domain.SimCardDailyStatistics{}
  102. previous := 10.0 * 1024
  103. if inc, ok := accumulateSimCardStatistics(&stats, 150*1024, &previous); !ok || inc != 140*1024 {
  104. t.Fatalf("unexpected increment: %v %v", inc, ok)
  105. }
  106. if _, ok := accumulateSimCardStatistics(&stats, 150*1024, nil); ok {
  107. t.Fatal("missing previous day must not report an increment")
  108. }
  109. }
  110. func strPtr(s string) *string { return &s }
  111. func TestPickSimCardSite(t *testing.T) {
  112. if pickSimCardSite(nil) != nil {
  113. t.Fatal("no candidates must pick nothing")
  114. }
  115. sites := []domain.SiteInfoDomain{
  116. {Sn: "OFF-OLD", Status: 0, OffLineTime: strPtr("2026-10-01 00:00:00")},
  117. {Sn: "ON-OLD", Status: 1, OnLineTime: strPtr("2026-09-01 00:00:00")},
  118. {Sn: "OFF-NEW", Status: 0, OffLineTime: strPtr("2026-11-01 00:00:00")},
  119. {Sn: "ON-NEW", Status: 1, OnLineTime: strPtr("2026-10-15 00:00:00")},
  120. }
  121. if got := pickSimCardSite(sites); got.Sn != "ON-NEW" {
  122. t.Fatalf("online site with the latest online time expected, got %s", got.Sn)
  123. }
  124. offline := []domain.SiteInfoDomain{sites[0], {Sn: "OFF-NIL", Status: 0}, sites[2]}
  125. if got := pickSimCardSite(offline); got.Sn != "OFF-NEW" {
  126. t.Fatalf("offline site closest to now expected, got %s", got.Sn)
  127. }
  128. }
  129. func TestBuildSimCardDailyReport(t *testing.T) {
  130. var heavy []simCardHeavyUsage
  131. for i := 0; i < simCardHeavyUsageMaxRows+5; i++ {
  132. heavy = append(heavy, simCardHeavyUsage{ICCID: fmt.Sprintf("89860000000000%05d", i), IncrementKB: float64(101+i) * 1024})
  133. }
  134. top := heavy[len(heavy)-1].ICCID
  135. unbound := heavy[len(heavy)-2].ICCID
  136. var asked []string
  137. report := buildSimCardDailyReport(domain.SimCardDailyStatistics{Date: "2026-11-03"}, heavy,
  138. func(ids []string) ([]domain.SiteInfoDomain, error) {
  139. asked = ids
  140. return []domain.SiteInfoDomain{
  141. {Sn: "SN-OFF", ICCID: top + "1", Version: "V1", Status: 0, OffLineTime: strPtr("2026-11-01 00:00:00")},
  142. {Sn: "SN-ON", ICCID: top + "1", Version: "V2", Status: 1},
  143. {Sn: "SN-OTHER", ICCID: "0000000000000000000", Version: "V3", Status: 1},
  144. }, nil
  145. })
  146. if report.HeavyCount != simCardHeavyUsageMaxRows+5 || len(report.Heavy) != simCardHeavyUsageMaxRows || len(asked) != simCardHeavyUsageMaxRows {
  147. t.Fatalf("list must be truncated before lookup: count=%d rows=%d asked=%d", report.HeavyCount, len(report.Heavy), len(asked))
  148. }
  149. if report.Heavy[0].ICCID != top || report.Heavy[0].SN != "SN-ON" || report.Heavy[0].Version != "V2" {
  150. t.Fatalf("largest usage must come first with the online site: %+v", report.Heavy[0])
  151. }
  152. if report.Heavy[1].ICCID != unbound || report.Heavy[1].SN != "" {
  153. t.Fatalf("unbound card must keep empty SN: %+v", report.Heavy[1])
  154. }
  155. text := formatSimCardDailyReport(report)
  156. for _, want := range []string{
  157. fmt.Sprintf("日用量超过 100 MB 的站点(%d 张卡", simCardHeavyUsageMaxRows+5),
  158. "- SN-ON · 135.00 MB · V2",
  159. "- ICCID " + unbound + "(未绑定站点) · 134.00 MB · -",
  160. "- 其余 5 张省略",
  161. } {
  162. if !strings.Contains(text, want) {
  163. t.Errorf("report missing %q:\n%s", want, text)
  164. }
  165. }
  166. if strings.Contains(text, "**") {
  167. t.Error("usage under 500 MB must not be bold")
  168. }
  169. }
  170. func TestFormatSimCardDailyReportBoldAndLookupFailure(t *testing.T) {
  171. heavy := []simCardHeavyUsage{
  172. {ICCID: "8986000000000000001", IncrementKB: 500 * 1024},
  173. {ICCID: "8986000000000000002", IncrementKB: 600 * 1024},
  174. }
  175. report := buildSimCardDailyReport(domain.SimCardDailyStatistics{}, heavy,
  176. func([]string) ([]domain.SiteInfoDomain, error) { return nil, errors.New("timeout") })
  177. text := formatSimCardDailyReport(report)
  178. for _, want := range []string{
  179. "站点信息查询失败",
  180. "- **ICCID 8986000000000000002 · 600.00 MB · -**",
  181. "- ICCID 8986000000000000001 · 500.00 MB · -",
  182. "总流量(",
  183. } {
  184. if !strings.Contains(text, want) {
  185. t.Errorf("report missing %q:\n%s", want, text)
  186. }
  187. }
  188. if strings.Contains(text, "未绑定站点") {
  189. t.Error("lookup failure must not mark cards as unbound")
  190. }
  191. }
  192. func TestFormatSimCardDailyReportEmpty(t *testing.T) {
  193. text := formatSimCardDailyReport(buildSimCardDailyReport(domain.SimCardDailyStatistics{}, nil,
  194. func([]string) ([]domain.SiteInfoDomain, error) { t.Fatal("no lookup expected"); return nil, nil }))
  195. if !strings.Contains(text, "(0 张卡") || !strings.HasSuffix(text, "- 无") {
  196. t.Fatalf("empty list must say none:\n%s", text)
  197. }
  198. }