package walletnotice import ( "context" "database/sql" "encoding/json" "fmt" "os" "strings" "testing" "time" _ "github.com/go-sql-driver/mysql" ) func TestMySQLRepositoryProcessesRealWalletOutbox(t *testing.T) { dsn := strings.TrimSpace(os.Getenv("NOTICE_REAL_MYSQL_DSN")) if dsn == "" { t.Skip("set NOTICE_REAL_MYSQL_DSN to run real MySQL notice validation") } walletDatabase := strings.TrimSpace(os.Getenv("NOTICE_REAL_WALLET_DATABASE")) if walletDatabase == "" { walletDatabase = "hyapp_wallet" } db, err := sql.Open("mysql", dsn) if err != nil { t.Fatalf("open mysql: %v", err) } defer db.Close() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := db.PingContext(ctx); err != nil { t.Fatalf("ping mysql: %v", err) } repository, err := NewMySQLRepository(db, walletDatabase) if err != nil { t.Fatalf("NewMySQLRepository: %v", err) } nowMs := time.Now().UnixMilli() suffix := fmt.Sprintf("%d", time.Now().UnixNano()) appCode := "notice_real" userID := int64(990000000) eventID := "notice_real_evt_" + suffix transactionID := "notice_real_tx_" + suffix commandID := "notice_real_cmd_" + suffix payload := map[string]any{ "transaction_id": transactionID, "command_id": commandID, "user_id": userID, "asset_type": "COIN", "available_delta": int64(188), "frozen_delta": int64(0), "available_after": int64(99887766), "frozen_after": int64(0), "balance_version": int64(66), "metadata": map[string]any{ "validation": "notice-service-real-mysql", }, "created_at_ms": nowMs, } payloadJSON, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal payload: %v", err) } if _, err := db.ExecContext(ctx, fmt.Sprintf(` INSERT INTO %s.wallet_outbox ( app_code, event_id, event_type, transaction_id, command_id, user_id, asset_type, available_delta, frozen_delta, payload, status, retry_count, last_error, created_at_ms, updated_at_ms ) VALUES (?, ?, 'WalletBalanceChanged', ?, ?, ?, 'COIN', 188, 0, ?, 'pending', 0, '', ?, ?)`, quoteDB(walletDatabase), ), appCode, eventID, transactionID, commandID, userID, string(payloadJSON), nowMs, nowMs); err != nil { t.Fatalf("insert wallet_outbox: %v", err) } if !keepRealRows() { t.Cleanup(func() { _, _ = db.ExecContext(context.Background(), `DELETE FROM notice_delivery_events WHERE app_code = ? AND source_event_id = ?`, appCode, eventID) _, _ = db.ExecContext(context.Background(), fmt.Sprintf(`DELETE FROM %s.wallet_outbox WHERE app_code = ? AND event_id = ?`, quoteDB(walletDatabase)), appCode, eventID) }) } publisher := &fakePublisher{} service := New(Config{NodeID: "notice-real-test"}, repository, publisher) processed, err := service.ProcessWalletBalanceNotices(ctx, WalletNoticeWorkerOptions{ WorkerID: "notice-real-test-worker", BatchSize: 10, LockTTL: 30 * time.Second, PublishTimeout: time.Second, MaxRetryCount: 3, }) if err != nil { t.Fatalf("ProcessWalletBalanceNotices: %v", err) } if processed != 1 || len(publisher.messages) != 1 { t.Fatalf("unexpected process result: processed=%d messages=%d", processed, len(publisher.messages)) } var noticeStatus string var deliveredAtMS int64 var storedPayload string if err := db.QueryRowContext(ctx, ` SELECT status, delivered_at_ms, COALESCE(CAST(payload_json AS CHAR), '{}') FROM notice_delivery_events WHERE source_name = 'wallet_outbox' AND app_code = ? AND source_event_id = ? AND channel = 'tencent_im_c2c'`, appCode, eventID, ).Scan(¬iceStatus, &deliveredAtMS, &storedPayload); err != nil { t.Fatalf("query notice_delivery_events: %v", err) } if noticeStatus != "delivered" || deliveredAtMS <= 0 { t.Fatalf("unexpected delivery marker: status=%s delivered_at_ms=%d payload=%s", noticeStatus, deliveredAtMS, storedPayload) } var noticePayload map[string]any if err := json.Unmarshal([]byte(storedPayload), ¬icePayload); err != nil { t.Fatalf("decode stored notice payload: %v", err) } if noticePayload["event_id"] != eventID || noticePayload["asset_type"] != "COIN" || noticePayload["balance_version"].(float64) != 66 { t.Fatalf("unexpected stored notice payload: %+v", noticePayload) } t.Logf("real notice validation event_id=%s status=%s delivered_at_ms=%d", eventID, noticeStatus, deliveredAtMS) } func keepRealRows() bool { value := strings.ToLower(strings.TrimSpace(os.Getenv("NOTICE_REAL_MYSQL_KEEP_ROWS"))) return value == "1" || value == "true" || value == "yes" } func quoteDB(value string) string { return "`" + strings.ReplaceAll(value, "`", "``") + "`" }