package wechat import ( "database/sql/driver" "encoding/json" "fmt" "io" "net/http" "os" "past/internal/store" "past/internal/testdb" "path/filepath" "strings" "testing" ) type subscribeTransport func(*http.Request) (*http.Response, error) func (f subscribeTransport) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } func TestSubscribeRecordsEveryOutcome(t *testing.T) { for _, tt := range []struct { name, openid, template, response, reason string subscribed, success bool code any requests int }{ {name: "success", openid: "recipient-openid", template: "template-id", subscribed: true, response: `{"errcode":0}`, success: true, code: int64(0), requests: 1}, {name: "quota exhausted", openid: "recipient-openid", template: "template-id", subscribed: true, response: `{"errcode":43101,"errmsg":"user refuse"}`, reason: "43101", code: int64(43101), requests: 1}, {name: "malformed response", openid: "recipient-openid", template: "template-id", subscribed: true, response: `not-json`, reason: "无效", requests: 1}, {name: "invalid errcode", openid: "recipient-openid", template: "template-id", subscribed: true, response: `{"errcode":"invalid"}`, reason: "无效", requests: 1}, {name: "missing errcode", openid: "recipient-openid", template: "template-id", subscribed: true, response: `{}`, reason: "errcode", requests: 1}, {name: "no authorization", openid: "recipient-openid", template: "template-id", reason: "未订阅"}, {name: "no openid", template: "template-id", reason: "openid"}, {name: "no template", openid: "recipient-openid", reason: "模板未配置"}, } { t.Run(tt.name, func(t *testing.T) { steps := []testdb.Step{{Match: "SELECT status FROM notify_log", Columns: []string{"status"}}, {Match: "SELECT openid FROM users", Args: []any{int64(2)}, Columns: []string{"openid"}, Rows: [][]driver.Value{{tt.openid}}}} if tt.openid != "" && tt.template != "" { auth := [][]driver.Value(nil) if tt.subscribed { auth = [][]driver.Value{{int64(1)}} } steps = append(steps, testdb.Step{Match: "FROM subscribe_templates", Args: []any{int64(2), "notify"}, Columns: []string{"id"}, Rows: auth}) } meta := []testdb.Step{{Match: "SELECT couple_id FROM users", Columns: []string{"couple_id"}, Rows: [][]driver.Value{{int64(5)}}}, {Match: "SELECT action_type FROM activity_log", Columns: []string{"action_type"}, Rows: [][]driver.Value{{"diary_create"}}}} steps = append(steps, meta...) steps = append(steps, testdb.Step{Match: "INSERT INTO subscribe_message_logs", Check: func(q string, args []any) error { v := testdb.InsertArgs(q, args) args = []any{v["activity_id"], v["couple_id"], v["user_id"], v["to_user_id"], v["to_openid"], v["action_type"], v["message_title"], v["message_content"], v["template_type"], v["template_id"], v["page"], v["status"], v["error_code"], v["http_status"], v["error_msg"], v["attempts"], v["duration_ms"], v["sent_at"]} if args[4] != tt.openid || args[5] != "diary_create" || args[6] != "new diary" { t.Errorf("wrong recipient/event: %v", args) return nil } if args[7] != strings.Repeat("爱", 20) { t.Errorf("log must record normalized content: %v", args[7]) } status := "failed" if tt.success { status = "success" } if args[11] != status || args[12] != tt.code || args[15] != int64(tt.requests) { t.Errorf("wrong result: %v", args) return nil } if !strings.Contains(fmt.Sprint(args[14]), tt.reason) { t.Errorf("missing failure reason: %v", args[14]) return nil } if tt.success && args[17] == nil { t.Error("missing sent_at") return nil } if !tt.success && args[17] != nil { t.Error("failed message has sent_at") return nil } return nil }}) steps = append(steps, meta...) steps = append(steps, testdb.Step{Match: "SELECT COUNT(*) as cnt FROM notify_log", Columns: []string{"cnt"}, Rows: [][]driver.Value{{int64(0)}}}, testdb.Step{Match: "INSERT INTO notify_log"}) db, _ := testdb.Open(t, steps) client := New("app", "secret", tt.template, t.TempDir(), store.WrapSQL(db)) os.MkdirAll(filepath.Dir(client.cacheFile()), 0755) os.WriteFile(client.cacheFile(), []byte("cached-token"), 0600) calls := 0 client.http.Transport = subscribeTransport(func(r *http.Request) (*http.Response, error) { calls++ var payload map[string]any json.NewDecoder(r.Body).Decode(&payload) if payload["touser"] != tt.openid { t.Fatal("wrong WeChat recipient") } if payload["data"].(map[string]any)["thing2"].(map[string]any)["value"] != strings.Repeat("爱", 20) { t.Fatal("overlong content sent to WeChat") } return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(tt.response))}, nil }) success := client.SendSubscribeMessage("2", "notify", map[string]string{"thing1": "new diary", "thing2": strings.Repeat("爱", 50)}, "", "10", "1") if success != tt.success || calls != tt.requests { t.Fatalf("success=%v requests=%d", success, calls) } }) } } func TestSubscribeWithoutActivityStillRecords(t *testing.T) { db, _ := testdb.Open(t, []testdb.Step{ {Match: "SELECT openid FROM users", Columns: []string{"openid"}, Rows: [][]driver.Value{{"recipient"}}}, {Match: "SELECT id FROM subscribe_templates", Columns: []string{"id"}}, {Match: "SELECT couple_id FROM users", Columns: []string{"couple_id"}, Rows: [][]driver.Value{{nil}}}, {Match: "INSERT INTO subscribe_message_logs", Check: func(q string, args []any) error { v := testdb.InsertArgs(q, args) if v["activity_id"] != nil || v["to_openid"] != "recipient" || v["status"] != "failed" { t.Error("missing zero-activity log") } return nil }}, }) client := New("app", "secret", "template", t.TempDir(), store.WrapSQL(db)) if client.SendSubscribeMessage("2", "notify", nil, "", "", "") { t.Fatal("unsubscribed send succeeded") } } func TestSuccessfulEventIsNotSentAgain(t *testing.T) { db, _ := testdb.Open(t, []testdb.Step{{Match: "SELECT status FROM notify_log", Columns: []string{"status"}, Rows: [][]driver.Value{{"success"}}}}) client := New("app", "secret", "template", t.TempDir(), store.WrapSQL(db)) client.http.Transport = subscribeTransport(func(*http.Request) (*http.Response, error) { t.Fatal("duplicate sent to WeChat"); return nil, nil }) if !client.SendSubscribeMessage("2", "notify", nil, "", "10", "1") { t.Fatal("completed message must be treated as success") } } func TestThingNormalizationDoesNotMutateInput(t *testing.T) { input := map[string]string{"thing1": strings.Repeat("爱💕", 30), "thing2": " ", "thing4": "行一\n行二", "date3": "2026-10-11"} output := normalizeSubscribeData(input) if len([]rune(output["thing1"])) != 20 || output["thing2"] == "" || output["thing4"] != "行一 行二" || output["date3"] != input["date3"] { t.Fatalf("wrong normalized fields: %v", output) } if output["thing1"] == input["thing1"] || input["thing2"] != " " { t.Fatal("input was mutated") } }