Files
cf7-spam-to-telegram/tests/MessageFormatterTest.php
T
Vladimir Bryzgalov 8efb82ca43 fix: phone leak, field-by-value detection, strict Flamingo email fallback
Three pilot-surfaced issues in one pass:

1. PhoneParser regex used '\s' in the candidate character class, so a
   digit on a following flattened line bled into the phone. '+7977
   6277470\n1' (phone + trailing checkbox value) parsed as 12 digits,
   producing '+797762774701'. Replaced '\s' with a literal space so
   newlines/tabs stop the candidate.

2. Custom-themed forms don't use 'your-name' / 'your-email' keys
   (washanyanya.ru has text-211 / text-212 / name_user). Added
   FieldDetector::find_name + find_email as fallbacks. Classifier
   and MessageFormatter now call them if no standard key matches,
   so 'Александр' in text-211 is recognised as a cyrillic name and
   scored / displayed accordingly.

3. FlamingoHelper copied '_from_email' into 'your-email' without
   validating it. Flamingo derives that meta from whatever field it
   considers "first"; on phone-first forms it ends up holding the
   phone number. Now we only copy _from_email if it contains '@',
   and dropped the _from_name shortcut entirely — FieldDetector
   handles the name via the actual field values.

Tests: +1 FieldDetectorTest (13 cases); +2 PhoneParserTest cases for
newline/tab bleeds; +1 ClassifierTest case; +1 MessageFormatterTest
case for custom field keys. Full suite 53/53 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 00:22:39 +05:00

182 lines
7.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace Cf7stg\Tests;
use Cf7stg\MessageFormatter;
use Cf7stg\Classifier;
use PHPUnit\Framework\TestCase;
final class MessageFormatterTest extends TestCase
{
public function test_full_client_payload(): void
{
$fields = [
'your-name' => 'Анна Петровна',
'your-tel' => '+79031234567',
'your-email' => 'anna@mail.ru',
'your-message' => 'Нужна сиделка для бабушки',
];
$classification = Classifier::classify($fields, ['reason' => 'Akismet']);
$result = MessageFormatter::build([
'fields' => $fields,
'classification' => $classification,
'site_title' => 'вашаняня.рф',
'form_title' => 'Обратная связь',
'submitted_at' => '17.04.2026 19:45',
'reason' => 'Akismet',
'ip' => '185.15.56.12',
'user_agent' => 'Mozilla/5.0',
'is_retro' => false,
]);
self::assertIsArray($result);
self::assertArrayHasKey('text', $result);
self::assertArrayHasKey('reply_markup', $result);
self::assertStringContainsString('🟢 похоже клиент', $result['text']);
self::assertStringContainsString('вашаняня.рф', $result['text']);
self::assertStringContainsString('Анна Петровна', $result['text']);
self::assertStringContainsString('+7 (903) 123-45-67', $result['text']);
self::assertStringContainsString('anna@mail.ru', $result['text']);
self::assertStringContainsString('Akismet', $result['text']);
}
public function test_reply_markup_is_null_because_tg_rejects_tel_urls(): void
{
// Telegram Bot API rejects tel: URLs in inline_keyboard with
// HTTP 400 "Wrong port number specified in the URL". We no longer
// emit any inline button; the phone number is rendered in the
// message body instead and mobile TG clients auto-linkify it.
$fields = ['your-tel' => '+79031234567', 'your-name' => 'Анна'];
$classification = Classifier::classify($fields, ['reason' => 'Akismet']);
$r = MessageFormatter::build([
'fields' => $fields, 'classification' => $classification,
'site_title' => 's.ru', 'form_title' => 'f',
'submitted_at' => '17.04.2026', 'reason' => 'Akismet',
'ip' => '', 'user_agent' => '', 'is_retro' => false,
]);
self::assertNull($r['reply_markup']);
// Phone must still be present in the text, formatted as Russian pretty.
self::assertStringContainsString('+7 (903) 123-45-67', $r['text']);
}
public function test_no_tel_button_when_no_phone(): void
{
$fields = ['your-email' => 'a@b.com'];
$classification = Classifier::classify($fields, ['reason' => 'Akismet']);
$r = MessageFormatter::build([
'fields' => $fields, 'classification' => $classification,
'site_title' => 's.ru', 'form_title' => 'f',
'submitted_at' => '17.04.2026', 'reason' => 'Akismet',
'ip' => '', 'user_agent' => '', 'is_retro' => false,
]);
self::assertNull($r['reply_markup']);
}
public function test_html_injection_in_name_is_escaped(): void
{
$fields = ['your-name' => '<script>alert(1)</script>', 'your-tel' => '+79031234567'];
$classification = Classifier::classify($fields, ['reason' => 'Akismet']);
$r = MessageFormatter::build([
'fields' => $fields, 'classification' => $classification,
'site_title' => 's.ru', 'form_title' => 'f',
'submitted_at' => '17.04.2026', 'reason' => 'Akismet',
'ip' => '', 'user_agent' => '', 'is_retro' => false,
]);
self::assertStringNotContainsString('<script>', $r['text']);
self::assertStringContainsString('&lt;script&gt;', $r['text']);
}
public function test_long_message_is_truncated(): void
{
$long = str_repeat('А', 5000);
$fields = ['your-message' => $long, 'your-tel' => '+79031234567'];
$classification = Classifier::classify($fields, ['reason' => 'Akismet']);
$r = MessageFormatter::build([
'fields' => $fields, 'classification' => $classification,
'site_title' => 's.ru', 'form_title' => 'f',
'submitted_at' => '17.04.2026', 'reason' => 'Akismet',
'ip' => '', 'user_agent' => '', 'is_retro' => false,
]);
self::assertLessThanOrEqual(4096, mb_strlen($r['text']));
self::assertStringContainsString('…[обрезано]', $r['text']);
}
public function test_retro_prefix_added(): void
{
$fields = ['your-tel' => '+79031234567'];
$classification = Classifier::classify($fields, ['reason' => 'Akismet']);
$r = MessageFormatter::build([
'fields' => $fields, 'classification' => $classification,
'site_title' => 's.ru', 'form_title' => 'f',
'submitted_at' => '17.04.2026', 'reason' => 'Akismet',
'ip' => '', 'user_agent' => '', 'is_retro' => true,
]);
self::assertStringContainsString('📂 Ретроспектива', $r['text']);
}
public function test_spam_label_emoji(): void
{
$fields = [
'your-name' => 'Melissa',
'your-message' => 'SEO promotion, visit http://spam.net',
];
$classification = Classifier::classify($fields, ['reason' => 'Akismet']);
$r = MessageFormatter::build([
'fields' => $fields, 'classification' => $classification,
'site_title' => 's.ru', 'form_title' => 'f',
'submitted_at' => '17.04.2026', 'reason' => 'Akismet',
'ip' => '', 'user_agent' => '', 'is_retro' => false,
]);
self::assertStringContainsString('🔴 похоже спам', $r['text']);
}
public function test_unclear_label_emoji(): void
{
$fields = ['your-name' => 'Олег', 'your-message' => 'Здравствуйте'];
$classification = Classifier::classify($fields, ['reason' => 'Akismet']);
$r = MessageFormatter::build([
'fields' => $fields, 'classification' => $classification,
'site_title' => 's.ru', 'form_title' => 'f',
'submitted_at' => '17.04.2026', 'reason' => 'Akismet',
'ip' => '', 'user_agent' => '', 'is_retro' => false,
]);
self::assertStringContainsString('🟡 неясно', $r['text']);
}
public function test_custom_form_field_keys_are_detected(): void
{
// washanyanya.ru-style form — fields are named text-NNN, name_user,
// etc. We must still pull out name + phone via value heuristics.
$fields = [
'text-211' => 'Александр',
'text-212' => '+79776277470',
'checkbox-382' => '1',
];
$classification = Classifier::classify($fields, ['reason' => 'Akismet']);
$r = MessageFormatter::build([
'fields' => $fields, 'classification' => $classification,
'site_title' => 's.ru', 'form_title' => 'f',
'submitted_at' => '18.04.2026', 'reason' => 'Akismet',
'ip' => '', 'user_agent' => '', 'is_retro' => false,
]);
// Name resolves to 'Александр' via FieldDetector
self::assertStringContainsString('Александр', $r['text']);
// Phone correctly parsed (11 digits, no trailing checkbox '1')
self::assertStringContainsString('+7 (977) 627-74-70', $r['text']);
// Email stays empty (no @-like value)
self::assertStringContainsString("📧 (не указан)", $r['text']);
// The checkbox value should still appear in 'Прочие поля'
self::assertStringContainsString('checkbox-382: 1', $r['text']);
}
}