Files
cf7-spam-to-telegram/tests/ClassifierTest.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

175 lines
6.4 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\Classifier;
use PHPUnit\Framework\TestCase;
final class ClassifierTest extends TestCase
{
public function test_real_client_inquiry_is_green(): void
{
$fields = [
'your-name' => 'Анна Петровна',
'your-tel' => '+7 903 123-45-67',
'your-email' => 'anna@mail.ru',
'your-message' => 'Нужна сиделка для бабушки после инсульта, пожалуйста перезвоните.',
];
$r = Classifier::classify($fields, ['reason' => 'Akismet']);
self::assertSame('client', $r['label']);
self::assertGreaterThanOrEqual(3, $r['score']);
}
public function test_seo_spam_with_url_is_red(): void
{
$fields = [
'your-name' => 'Melissa Johnson',
'your-email' => 'promo@example.com',
'your-message' => 'Hi, we boost SEO positions, visit http://cheap-seo.net',
];
$r = Classifier::classify($fields, ['reason' => 'Akismet']);
self::assertSame('spam', $r['label']);
self::assertLessThanOrEqual(-2, $r['score']);
}
public function test_honeypot_forces_spam_even_with_phone(): void
{
$fields = [
'your-name' => 'Анна',
'your-tel' => '+79031234567',
'your-message' => 'сиделка для мамы',
];
$r = Classifier::classify($fields, ['reason' => 'Honeypot']);
self::assertSame('spam', $r['label']);
}
public function test_empty_fields_are_unclear(): void
{
$fields = [
'your-email' => 'a@b.com',
];
$r = Classifier::classify($fields, ['reason' => 'CF7 rules']);
self::assertSame('unclear', $r['label']);
}
public function test_multiple_keywords_capped_at_minus_six(): void
{
$fields = [
'your-message' => 'SEO, SEO, SEO, backlinks, crypto, casino, займ, bitcoin',
];
$r = Classifier::classify($fields, ['reason' => 'Akismet']);
// 8 hits × -2 would be -16; cap is -6
self::assertGreaterThanOrEqual(-10, $r['score']); // keyword part >= -6, plus -3 for URL? none here
}
public function test_ru_domain_email_adds_one(): void
{
$fields = [
'your-name' => 'Иван',
'your-email' => 'ivan@yandex.ru',
'your-message' => 'Здравствуйте, интересует услуга сиделки',
];
$r = Classifier::classify($fields, ['reason' => 'Akismet']);
$hasRuMail = false;
foreach ($r['reasons'] as $reason) {
if (strpos($reason['text'], 'ру-домен') !== false) {
$hasRuMail = true;
}
}
self::assertTrue($hasRuMail);
}
public function test_reasons_list_contains_sign_and_weight(): void
{
$fields = [
'your-tel' => '+79031234567',
];
$r = Classifier::classify($fields, ['reason' => 'Akismet']);
self::assertNotEmpty($r['reasons']);
self::assertArrayHasKey('sign', $r['reasons'][0]);
self::assertArrayHasKey('weight', $r['reasons'][0]);
self::assertArrayHasKey('text', $r['reasons'][0]);
}
public function test_latin_only_name_penalty(): void
{
$fields = [
'your-name' => 'John Smith',
'your-message' => 'please help me with website ranking',
];
$r = Classifier::classify($fields, ['reason' => 'Akismet']);
$hasLatinPenalty = false;
foreach ($r['reasons'] as $reason) {
if ($reason['sign'] === '-' && strpos($reason['text'], 'латиница') !== false) {
$hasLatinPenalty = true;
}
}
self::assertTrue($hasLatinPenalty);
}
public function test_url_in_any_field_triggers_minus_three(): void
{
$fields = [
'your-name' => 'Ольга',
'your-message' => 'посмотрите https://мойсайт.рф',
];
$r = Classifier::classify($fields, ['reason' => 'Akismet']);
$hasUrl = false;
foreach ($r['reasons'] as $reason) {
if (strpos($reason['text'], 'URL') !== false) {
$hasUrl = true;
self::assertSame('-', $reason['sign']);
self::assertSame(3, $reason['weight']);
}
}
self::assertTrue($hasUrl);
}
public function test_thresholds_client_edge(): void
{
// Score exactly 3 → client
$fields = ['your-tel' => '+79031234567'];
$r = Classifier::classify($fields, ['reason' => 'Akismet']);
self::assertSame(3, $r['score']);
self::assertSame('client', $r['label']);
}
public function test_thresholds_unclear_between_neg_one_and_two(): void
{
$fields = ['your-name' => 'Олег', 'your-message' => 'короткий'];
$r = Classifier::classify($fields, ['reason' => 'Akismet']);
self::assertGreaterThan(-2, $r['score']);
self::assertLessThan(3, $r['score']);
self::assertSame('unclear', $r['label']);
}
public function test_keyword_matching_case_insensitive(): void
{
$fields = ['your-message' => 'Продаём ВИАГРА с доставкой'];
$r = Classifier::classify($fields, ['reason' => 'Akismet']);
$matched = false;
foreach ($r['reasons'] as $reason) {
if (strpos($reason['text'], 'виагра') !== false) $matched = true;
}
self::assertTrue($matched);
}
public function test_detects_cyrillic_name_under_custom_field_key(): void
{
// Real form on washanyanya.ru uses keys like 'text-211' / 'text-212'
// instead of 'your-name' / 'your-tel'. Classifier must still award
// +1 for a cyrillic name via the value-based heuristic.
$fields = [
'text-211' => 'Александр',
'text-212' => '+79776277470',
];
$r = Classifier::classify($fields, ['reason' => 'Akismet']);
$has_name = false;
foreach ($r['reasons'] as $reason) {
if (strpos($reason['text'], 'кириллическое имя') !== false) $has_name = true;
}
self::assertTrue($has_name, 'expected +1 кириллическое имя via value heuristic');
}
}