package simwarning import ( "context" "encoding/json" "errors" "fmt" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcfg" ) func TestMonthlyExpiryRunPagesAndPartialFailures(t *testing.T) { firstPage := make([]monthlyExpiryCompany, monthlyExpiryPageSize) for i := range firstPage { firstPage[i] = monthlyExpiryCompany{TenantID: "000000", DeptID: int64(i + 1), LeaderID: int64(i + 1001)} } calls := make(map[string]int) pages := make([]int64, 0, 2) runner := monthlyExpiryRunner{ companies: func(_ context.Context, lastID int64) ([]monthlyExpiryCompany, error) { pages = append(pages, lastID) if lastID == 0 { return firstPage, nil } if lastID == int64(monthlyExpiryPageSize) { return []monthlyExpiryCompany{{TenantID: "000000", DeptID: 101, LeaderID: 1101}}, nil } return nil, fmt.Errorf("unexpected cursor %d", lastID) }, summaries: func(_ context.Context, company monthlyExpiryCompany, begin, end time.Time) ([]monthlyExpirySummary, error) { if begin.Format("2006-01-02") != "2026-12-01" || end.Format("2006-01-02") != "2027-02-01" { t.Errorf("wrong query window %s..%s", begin, end) } if company.DeptID == 2 { return nil, errors.New("count failed") } return []monthlyExpirySummary{{Month: "2026-12", Count: 3, FirstDate: "2026-12-01", LastDate: "2026-12-31"}, {Month: "2027-01", Count: 6, FirstDate: "2027-01-31", LastDate: "2027-01-31"}}, nil }, leader: func(_ context.Context, company monthlyExpiryCompany) (*monthlyExpiryLeader, error) { if company.DeptID == 3 { return nil, nil } return &monthlyExpiryLeader{UserID: company.LeaderID, Phone: "13800000000"}, nil }, deliver: func(_ context.Context, notice monthlyExpiryNotice, channel string) (bool, error) { calls[notice.Summary.Month+"/"+channel]++ if notice.Company.DeptID == 1 && channel == "sms" { return false, errors.New("sms failed") } if notice.Company.DeptID == 1 && notice.Summary.Month == "2027-01" && channel == "platform" { return false, nil } if notice.Company.DeptID == 4 && notice.Summary.Month == "2026-12" && channel == "platform" { return true, errors.New("dedupe mark failed after delivery") } return true, nil }, } stats, err := runner.run(context.Background(), time.Date(2026, 12, 15, 9, 0, 0, 0, monthlyExpiryLocation)) if err != nil { t.Fatal(err) } if stats.Companies != 101 || stats.Groups != 198 || stats.Sent != 393 || stats.Duplicate != 1 || stats.Errors != 5 { t.Fatalf("unexpected stats: %+v", stats) } if len(pages) != 2 || pages[0] != 0 || pages[1] != 100 { t.Fatalf("wrong page cursors: %v", pages) } if calls["2026-12/platform"] != 99 || calls["2027-01/platform"] != 99 { t.Fatalf("wrong delivered group counts: %+v", calls) } } func TestMonthlyExpiryNoticeContentAndRecipient(t *testing.T) { base := monthlyExpiryNotice{ Company: monthlyExpiryCompany{TenantID: "000000", DeptID: 117, LeaderID: 12}, Leader: monthlyExpiryLeader{UserID: 12}, RunTime: time.Date(2026, 10, 15, 9, 0, 0, 0, monthlyExpiryLocation), Summary: monthlyExpirySummary{Month: "2026-10", Count: 7, FirstDate: "2026-10-21", LastDate: "2026-10-31"}, } current := base.content() for _, text := range []string{"【7】张", "2026-10-21至2026-10-31", "本月25日前", "本月底卡片将执行停机"} { if !strings.Contains(current, text) { t.Errorf("current notice missing %q: %s", text, current) } } param := monthlyExpiryMessageParam(base) if *param.MessageType != 1 || *param.SendUserType != 1 || *param.MessageTitle != "到期提醒" || !strings.Contains(param.UserIds.String(), "12") { t.Fatalf("wrong targeted system message: %+v", param) } base.Summary = monthlyExpirySummary{Month: "2026-11", Count: 9, FirstDate: "2026-11-30", LastDate: "2026-11-30"} if got := base.content(); !strings.Contains(got, "次月25日前") || strings.Contains(got, "本月底卡片") { t.Fatalf("next month notice incorrect: %s", got) } if base.expiryDateText() != "2026-11-30" { t.Errorf("single-day range not collapsed: %s", base.expiryDateText()) } if key := monthlyExpirySentKey(base, "sms"); !strings.Contains(key, ":000000:117:2026-11:sms") { t.Errorf("missing dedupe dimensions: %s", key) } } func TestMonthlyExpirySMSParamsRequireConfirmedTemplate(t *testing.T) { for _, invalid := range []string{"", "invalid", "{}", `{"count":7}`} { if _, err := renderMonthlyExpirySMSParams(invalid, 3, "2026-11-30"); err == nil { t.Errorf("unconfirmed/invalid template accepted: %q", invalid) } } content, err := renderMonthlyExpirySMSParams(`{"actualCount":"{count}","actualExpire":"{expireDate}"}`, 5, "2026-11-01至2026-11-30") if err != nil { t.Fatal(err) } var params map[string]string if err := json.Unmarshal([]byte(content), ¶ms); err != nil || params["actualCount"] != "5" || params["actualExpire"] != "2026-11-01至2026-11-30" { t.Fatalf("incorrect template params: %s, %v", content, err) } } func TestMonthlyExpirySMSLocalGateway(t *testing.T) { // 所有请求只发到本机测试服务;清除环境覆盖,绝不读取真实网关配置。 for _, name := range []string{"IOTLCM_SMS_GATEWAY_ENABLED", "IOTLCM_SMS_GATEWAY_URL", "IOTLCM_SMS_GATEWAY_GRPC_ADDR", "IOTLCM_SMS_GATEWAY_TIMEOUT", "IOTLCM_SMS_GATEWAY_ROUTER_ID"} { t.Setenv(name, "") } requests := make([]map[string]map[string]string, 0, 3) fail := false server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var body map[string]map[string]string if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Errorf("decode request: %v", err) } requests = append(requests, body) if fail { _, _ = w.Write([]byte(`{"code":502,"message":"supplier failure"}`)) return } _, _ = w.Write([]byte(`{"code":0,"message":"OK"}`)) })) defer server.Close() adapter, err := gcfg.NewAdapterContent() if err != nil { t.Fatal(err) } original := g.Cfg().GetAdapter() g.Cfg().SetAdapter(adapter) t.Cleanup(func() { g.Cfg().SetAdapter(original) }) configure := func(enabled bool, template string) { t.Helper() data, err := json.Marshal(g.Map{ "sms": g.Map{"gateway": g.Map{"enabled": enabled, "url": server.URL, "timeout": "1s"}}, "warning": g.Map{"expire": g.Map{"monthly": g.Map{ "currentMonthParamsTemplate": template, "nextMonthParamsTemplate": template, }}}, }) if err != nil { t.Fatal(err) } if err := adapter.SetContent(string(data)); err != nil { t.Fatal(err) } } configure(true, `{"card_num":"{count}","expire_date":"{expireDate}"}`) notice := monthlyExpiryNotice{ RunTime: time.Date(2026, 10, 15, 9, 0, 0, 0, monthlyExpiryLocation), Leader: monthlyExpiryLeader{UserID: 12, Phone: "13800000000"}, Summary: monthlyExpirySummary{Month: "2026-10", Count: 7, FirstDate: "2026-10-31", LastDate: "2026-10-31"}, } if err := sendMonthlyExpirySMS(context.Background(), notice); err != nil { t.Fatal(err) } notice.Summary = monthlyExpirySummary{Month: "2026-11", Count: 9, FirstDate: "2026-11-01", LastDate: "2026-11-30"} if err := sendMonthlyExpirySMS(context.Background(), notice); err != nil { t.Fatal(err) } if len(requests) != 2 || requests[0]["config"]["templateId"] != "SMS_512596191" || requests[1]["config"]["templateId"] != "SMS_512646156" { t.Fatalf("incorrect gateway templates: %+v", requests) } for _, request := range requests { if request["config"]["signName"] != "示例企业" { t.Fatalf("incorrect gateway sign: %+v", request) } } var params map[string]string if err := json.Unmarshal([]byte(requests[1]["params"]["msgContent"]), ¶ms); err != nil || params["card_num"] != "9" || params["expire_date"] != "2026-11-01至2026-11-30" { t.Fatalf("incorrect gateway params: %+v, %v", params, err) } fail = true if err := sendMonthlyExpirySMS(context.Background(), notice); err == nil { t.Fatal("gateway business failure must propagate") } configure(true, "") if err := sendMonthlyExpirySMS(context.Background(), notice); err == nil || len(requests) != 3 { t.Fatal("unconfirmed template must block before calling gateway") } configure(false, `{"card_num":"{count}","expire_date":"{expireDate}"}`) if err := sendMonthlyExpirySMS(context.Background(), notice); err == nil || len(requests) != 3 { t.Fatal("disabled gateway must not count as successful delivery") } } func TestMonthlyExpiryCancellationStopsBeforeDelivery(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() runner := monthlyExpiryRunner{ companies: func(context.Context, int64) ([]monthlyExpiryCompany, error) { t.Fatal("canceled run should not query") return nil, nil }, } _, err := runner.run(ctx, time.Now()) if !errors.Is(err, context.Canceled) { t.Fatalf("expected cancellation, got %v", err) } }