a70785beca
- Add DEFAULT_THRESHOLDS['drop'] = -5 and DEFAULT_HARD_DROP_RULES const
- Add evaluate_hard_drop(): fires when no positive signals + short latin name OR ≥3 placeholder fields
- Wire into classify(): drop overrides all other labels; synthetic reason explains trigger
- Add drop_reason_label() and 'drop' => '⛔ авто-дроп' to MessageFormatter::LABEL_MAP
- Update ClassifierTest/MessageFormatterTest to extend Cf7stgTestCase (filter isolation)
- Adjust pre-existing assertions that now correctly yield 'drop' instead of 'spam'
205 lines
9.1 KiB
PHP
205 lines
9.1 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
namespace Cf7stg\Tests;
|
||
|
||
use Cf7stg\MessageFormatter;
|
||
use Cf7stg\Classifier;
|
||
|
||
final class MessageFormatterTest extends Cf7stgTestCase
|
||
{
|
||
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('<script>', $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
|
||
{
|
||
// score = SEO(-2) + URL(-3) + latin_only(-1) = -6 ≤ drop threshold(-5) → label=drop
|
||
// Fields with no valid email and name 'Melissa' (7 chars, not short latin) → score-based drop.
|
||
$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_spam_label_emoji_pure_spam(): void
|
||
{
|
||
// Score -3 (URL) + -2 (SEO keyword) = -5, but no drop because cyrillic name protects
|
||
// from hard-rule, and score -5 is NOT ≤ drop threshold -5 (it equals it, so it IS drop).
|
||
// Use URL(-3) only → score=-3, between spam(-2) and drop(-5) → spam label.
|
||
$fields = [
|
||
'your-name' => 'Nikolay',
|
||
'your-email' => 'nick@example.com',
|
||
'your-message' => 'visit http://promo.net for deals',
|
||
];
|
||
$classification = Classifier::classify($fields, ['reason' => 'Akismet']);
|
||
// valid email prevents hard-rule; score = URL(-3) + latin_only(-1) = -4 → spam
|
||
$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']);
|
||
}
|
||
}
|