package simCardQueue import ( "context" "encoding/json" "errors" "fmt" "fwh-toolbox-server/domain" "io" "net/http" "strings" "testing" "github.com/hibiken/asynq" ) type roundTripFunc func(*http.Request) (*http.Response, error) func (fn roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return fn(req) } func TestAccumulateSimCardStatistics(t *testing.T) { stats := domain.SimCardDailyStatistics{Date: "2026-09-24"} zero := 0.0 previous := 200.0 * 1024 // Thresholds are strict; cards without a previous snapshot count only toward totals. accumulateSimCardStatistics(&stats, 200*1024, &zero) accumulateSimCardStatistics(&stats, 1024*1024, &previous) accumulateSimCardStatistics(&stats, 2*1024*1024, nil) accumulateSimCardStatistics(&stats, 3*1024*1024, &zero) if stats.CardCount != 4 || stats.TotalOver200MB != 3 || stats.TotalOver1GB != 2 || stats.TotalOver2GB != 1 { t.Fatalf("unexpected total statistics: %+v", stats) } if stats.MaxTotalKB != 3*1024*1024 || stats.IncrementCardCount != 3 { t.Fatalf("unexpected maxima or comparison count: %+v", stats) } if stats.IncrementOver20MB != 3 || stats.IncrementOver50MB != 3 || stats.IncrementOver200MB != 2 { t.Fatalf("unexpected increment statistics: %+v", stats) } if stats.MaxIncrementKB == nil || *stats.MaxIncrementKB != 3*1024*1024 { t.Fatalf("unexpected maximum increment: %+v", stats.MaxIncrementKB) } if !strings.Contains(formatSimCardStatistics(stats), "最大增量:3072.00 MB") { t.Fatal("report does not show maximum increment") } } func TestPostSimCardStatistics(t *testing.T) { client := &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) { if r.Method != http.MethodPost || r.Header.Get("Content-Type") != "application/json" { t.Errorf("unexpected request: %s %s", r.Method, r.Header.Get("Content-Type")) } if r.URL.Query().Get("timestamp") == "" || r.URL.Query().Get("sign") == "" || r.URL.Query().Get("access_token") != "test" { t.Errorf("signature or access token missing: %s", r.URL.RawQuery) } var message struct { MsgType string `json:"msgtype"` Markdown struct { Text string `json:"text"` } `json:"markdown"` } if err := json.NewDecoder(r.Body).Decode(&message); err != nil { t.Error(err) } if message.MsgType != "markdown" || !strings.Contains(message.Markdown.Text, "2026-09-24") { t.Errorf("unexpected message: %+v", message) } return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"errcode":0,"errmsg":"ok"}`))}, nil })} report := simCardDailyReport{Stats: domain.SimCardDailyStatistics{Date: "2026-09-24"}} if err := postSimCardStatistics(context.Background(), client, "https://oapi.dingtalk.com/robot/send?access_token=test", "secret", report); err != nil { t.Fatal(err) } } func TestPostSimCardStatisticsRejectsBotError(t *testing.T) { client := &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) { return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"errcode":310000,"errmsg":"rejected"}`))}, nil })} if err := postSimCardStatistics(context.Background(), client, "https://oapi.dingtalk.com/robot/send", "", simCardDailyReport{}); err == nil { t.Fatal("DingTalk error response must be retried") } } func TestUnconfiguredWebhookSkipsStatistics(t *testing.T) { processor := &SimCardProcessor{} if err := processor.enqueueSimCardReportIfComplete("2026-09-24"); err != nil { t.Fatal(err) } if err := processor.handleSimCardDailyReport(context.Background(), asynq.NewTask(taskTypeSimCardDailyReport, []byte("2026-09-24"))); err != nil { t.Fatal(err) } } func TestAccumulateSimCardStatisticsMissingPrevious(t *testing.T) { stats := domain.SimCardDailyStatistics{} accumulateSimCardStatistics(&stats, 300*1024, nil) if stats.IncrementCardCount != 0 || stats.MaxIncrementKB != nil { t.Fatalf("missing previous day must not create an increment: %+v", stats) } } func TestAccumulateSimCardStatisticsCounterReset(t *testing.T) { stats := domain.SimCardDailyStatistics{} previous := 2.0 * 1024 * 1024 accumulateSimCardStatistics(&stats, 60*1024, &previous) if stats.MaxIncrementKB == nil || *stats.MaxIncrementKB != 60*1024 || stats.IncrementOver50MB != 1 { t.Fatalf("reset counter should use current accumulated usage: %+v", stats) } } func TestAccumulateSimCardStatisticsReturnsIncrement(t *testing.T) { stats := domain.SimCardDailyStatistics{} previous := 10.0 * 1024 if inc, ok := accumulateSimCardStatistics(&stats, 150*1024, &previous); !ok || inc != 140*1024 { t.Fatalf("unexpected increment: %v %v", inc, ok) } if _, ok := accumulateSimCardStatistics(&stats, 150*1024, nil); ok { t.Fatal("missing previous day must not report an increment") } } func strPtr(s string) *string { return &s } func TestPickSimCardSite(t *testing.T) { if pickSimCardSite(nil) != nil { t.Fatal("no candidates must pick nothing") } sites := []domain.SiteInfoDomain{ {Sn: "OFF-OLD", Status: 0, OffLineTime: strPtr("2026-10-01 00:00:00")}, {Sn: "ON-OLD", Status: 1, OnLineTime: strPtr("2026-09-01 00:00:00")}, {Sn: "OFF-NEW", Status: 0, OffLineTime: strPtr("2026-11-01 00:00:00")}, {Sn: "ON-NEW", Status: 1, OnLineTime: strPtr("2026-10-15 00:00:00")}, } if got := pickSimCardSite(sites); got.Sn != "ON-NEW" { t.Fatalf("online site with the latest online time expected, got %s", got.Sn) } offline := []domain.SiteInfoDomain{sites[0], {Sn: "OFF-NIL", Status: 0}, sites[2]} if got := pickSimCardSite(offline); got.Sn != "OFF-NEW" { t.Fatalf("offline site closest to now expected, got %s", got.Sn) } } func TestBuildSimCardDailyReport(t *testing.T) { var heavy []simCardHeavyUsage for i := 0; i < simCardHeavyUsageMaxRows+5; i++ { heavy = append(heavy, simCardHeavyUsage{ICCID: fmt.Sprintf("89860000000000%05d", i), IncrementKB: float64(101+i) * 1024}) } top := heavy[len(heavy)-1].ICCID unbound := heavy[len(heavy)-2].ICCID var asked []string report := buildSimCardDailyReport(domain.SimCardDailyStatistics{Date: "2026-11-03"}, heavy, func(ids []string) ([]domain.SiteInfoDomain, error) { asked = ids return []domain.SiteInfoDomain{ {Sn: "SN-OFF", ICCID: top + "1", Version: "V1", Status: 0, OffLineTime: strPtr("2026-11-01 00:00:00")}, {Sn: "SN-ON", ICCID: top + "1", Version: "V2", Status: 1}, {Sn: "SN-OTHER", ICCID: "0000000000000000000", Version: "V3", Status: 1}, }, nil }) if report.HeavyCount != simCardHeavyUsageMaxRows+5 || len(report.Heavy) != simCardHeavyUsageMaxRows || len(asked) != simCardHeavyUsageMaxRows { t.Fatalf("list must be truncated before lookup: count=%d rows=%d asked=%d", report.HeavyCount, len(report.Heavy), len(asked)) } if report.Heavy[0].ICCID != top || report.Heavy[0].SN != "SN-ON" || report.Heavy[0].Version != "V2" { t.Fatalf("largest usage must come first with the online site: %+v", report.Heavy[0]) } if report.Heavy[1].ICCID != unbound || report.Heavy[1].SN != "" { t.Fatalf("unbound card must keep empty SN: %+v", report.Heavy[1]) } text := formatSimCardDailyReport(report) for _, want := range []string{ fmt.Sprintf("日用量超过 100 MB 的站点(%d 张卡", simCardHeavyUsageMaxRows+5), "- SN-ON · 135.00 MB · V2", "- ICCID " + unbound + "(未绑定站点) · 134.00 MB · -", "- 其余 5 张省略", } { if !strings.Contains(text, want) { t.Errorf("report missing %q:\n%s", want, text) } } if strings.Contains(text, "**") { t.Error("usage under 500 MB must not be bold") } } func TestFormatSimCardDailyReportBoldAndLookupFailure(t *testing.T) { heavy := []simCardHeavyUsage{ {ICCID: "8986000000000000001", IncrementKB: 500 * 1024}, {ICCID: "8986000000000000002", IncrementKB: 600 * 1024}, } report := buildSimCardDailyReport(domain.SimCardDailyStatistics{}, heavy, func([]string) ([]domain.SiteInfoDomain, error) { return nil, errors.New("timeout") }) text := formatSimCardDailyReport(report) for _, want := range []string{ "站点信息查询失败", "- **ICCID 8986000000000000002 · 600.00 MB · -**", "- ICCID 8986000000000000001 · 500.00 MB · -", "总流量(", } { if !strings.Contains(text, want) { t.Errorf("report missing %q:\n%s", want, text) } } if strings.Contains(text, "未绑定站点") { t.Error("lookup failure must not mark cards as unbound") } } func TestFormatSimCardDailyReportEmpty(t *testing.T) { text := formatSimCardDailyReport(buildSimCardDailyReport(domain.SimCardDailyStatistics{}, nil, func([]string) ([]domain.SiteInfoDomain, error) { t.Fatal("no lookup expected"); return nil, nil })) if !strings.Contains(text, "(0 张卡") || !strings.HasSuffix(text, "- 无") { t.Fatalf("empty list must say none:\n%s", text) } }