'pasha@gmail.com']; self::assertTrue(Classifier::has_valid_email_anywhere($fields)); } public function test_has_valid_email_anywhere_finds_in_array_value(): void { $fields = ['x' => ['noise', 'info@some-domain.com']]; self::assertTrue(Classifier::has_valid_email_anywhere($fields)); } public function test_has_valid_email_anywhere_returns_false_for_invalid(): void { $fields = ['email' => 'not-an-email', 'x' => 'AB']; self::assertFalse(Classifier::has_valid_email_anywhere($fields)); } public function test_has_valid_email_anywhere_returns_false_when_empty(): void { self::assertFalse(Classifier::has_valid_email_anywhere([])); } public function test_has_valid_email_anywhere_finds_email_embedded_in_text(): void { $fields = ['your-message' => 'reach me at user@domain.com thanks']; self::assertTrue(Classifier::has_valid_email_anywhere($fields)); } public function test_has_valid_email_anywhere_finds_second_email_when_first_invalid(): void { // First candidate is "alice@in" which is not a valid email; second is valid. // Without preg_match_all, the method would return false here. $fields = ['msg' => 'try alice@in or bob@example.com']; self::assertTrue(Classifier::has_valid_email_anywhere($fields)); } public function test_has_short_latin_name_true_for_two_letters(): void { $fields = ['your-name' => 'OB']; self::assertTrue(Classifier::has_short_latin_name($fields)); } public function test_has_short_latin_name_true_for_three_alnum(): void { $fields = ['name' => 'A1B']; self::assertTrue(Classifier::has_short_latin_name($fields)); } public function test_has_short_latin_name_false_for_long_name(): void { $fields = ['your-name' => 'Melissa']; self::assertFalse(Classifier::has_short_latin_name($fields)); } public function test_has_short_latin_name_false_for_cyrillic(): void { $fields = ['your-name' => 'Иван']; self::assertFalse(Classifier::has_short_latin_name($fields)); } public function test_has_short_latin_name_false_when_no_name_field(): void { // Только телефон, нет name-полей. FieldDetector::find_name пропустит +79991234567 (digits) $fields = ['tel-1' => '+79991234567']; self::assertFalse(Classifier::has_short_latin_name($fields)); } public function test_has_short_latin_name_picks_via_field_detector(): void { // text-211 не в списке известных name-ключей, FieldDetector найдёт 'XY' (no digits, len=2) $fields = ['text-211' => 'XY', 'text-212' => '+79991234567']; self::assertTrue(Classifier::has_short_latin_name($fields)); } public function test_has_short_latin_name_true_for_single_letter_boundary(): void { // Lower boundary of {1,3}: 1 char accepted $fields = ['your-name' => 'A']; self::assertTrue(Classifier::has_short_latin_name($fields)); } public function test_has_short_latin_name_false_for_four_letters_boundary(): void { // Upper boundary of {1,3}: 4 chars rejected $fields = ['your-name' => 'ABCD']; self::assertFalse(Classifier::has_short_latin_name($fields)); } public function test_has_short_latin_name_false_for_array_value_with_space_join(): void { // pick_field joins array values with a space → "OB MU" is 5 chars → no false positive. $fields = ['your-name' => ['OB', 'MU']]; self::assertFalse(Classifier::has_short_latin_name($fields)); } public function test_count_placeholder_fields_screenshot_1_exact_count(): void { $fields = require __DIR__ . '/fixtures/spam-screenshot-1.php'; // text-785: OB(2), text-787: 70(2), text-786: BP(2), text-788: ZZ(2), text-789: QV(2) → 5 placeholders. // text-790: 10 цифр — не placeholder. acceptance-data: служебное → исключено. self::assertSame(5, Classifier::count_placeholder_fields($fields)); } public function test_count_placeholder_fields_screenshot_2_exact_count(): void { $fields = require __DIR__ . '/fixtures/spam-screenshot-2.php'; // Same shape as screenshot-1: 5 placeholder fields. self::assertSame(5, Classifier::count_placeholder_fields($fields)); } public function test_count_placeholder_fields_screenshot_3_exact_count(): void { $fields = require __DIR__ . '/fixtures/spam-screenshot-3.php'; // text-347: MU(2) is the only placeholder; tel-185(10), text-subject (long), text-title (long) // are not. acceptance-data: служебное. self::assertSame(1, Classifier::count_placeholder_fields($fields)); } public function test_count_placeholder_fields_ignores_empty(): void { $fields = ['a' => 'AB', 'b' => '', 'c' => 'XY', 'd' => null, 'e' => 'CD']; self::assertSame(3, Classifier::count_placeholder_fields($fields)); } public function test_count_placeholder_fields_ignores_long_values(): void { $fields = ['a' => 'AB', 'b' => 'Hello world', 'c' => 'CD']; self::assertSame(2, Classifier::count_placeholder_fields($fields)); } public function test_count_placeholder_fields_ignores_service_keys(): void { $fields = [ 'acceptance-data' => '1', '_wpcf7' => 'AB', '_wpcf7_unit_tag' => 'XY', 'g-recaptcha-response' => 'CD', 'real' => 'EF', ]; self::assertSame(1, Classifier::count_placeholder_fields($fields)); } public function test_count_placeholder_fields_ignores_non_alnum(): void { // Кириллица или с пробелами не считается placeholder $fields = ['a' => 'Ия', 'b' => 'a b', 'c' => 'XY']; self::assertSame(1, Classifier::count_placeholder_fields($fields)); } public function test_screenshot_1_yields_drop(): void { $fields = require __DIR__ . '/fixtures/spam-screenshot-1.php'; $r = Classifier::classify($fields, ['reason' => 'Akismet']); self::assertSame('drop', $r['label']); } public function test_screenshot_2_yields_drop(): void { $fields = require __DIR__ . '/fixtures/spam-screenshot-2.php'; $r = Classifier::classify($fields, ['reason' => 'Akismet']); self::assertSame('drop', $r['label']); } public function test_screenshot_3_yields_drop(): void { $fields = require __DIR__ . '/fixtures/spam-screenshot-3.php'; $r = Classifier::classify($fields, ['reason' => 'Akismet']); self::assertSame('drop', $r['label']); } public function test_drop_skipped_when_cyrillic_name_present(): void { $fields = [ 'your-name' => 'Иван', 'text-1' => 'AB', 'text-2' => 'CD', 'text-3' => 'EF', ]; $r = Classifier::classify($fields, ['reason' => 'Akismet']); self::assertNotSame('drop', $r['label']); } public function test_drop_skipped_when_meaningful_text_present(): void { $fields = [ 'your-name' => 'AB', 'your-message' => 'Здравствуйте, нужна уборка квартиры в субботу с 10 утра', 'text-1' => 'AB', 'text-2' => 'CD', 'text-3' => 'EF', ]; $r = Classifier::classify($fields, ['reason' => 'Akismet']); self::assertNotSame('drop', $r['label']); } /** @dataProvider valid_email_provider */ public function test_drop_skipped_when_valid_email_present(string $email): void { $fields = [ 'your-name' => 'AB', 'your-email' => $email, 'text-1' => 'AB', 'text-2' => 'CD', 'text-3' => 'EF', ]; $r = Classifier::classify($fields, ['reason' => 'Akismet']); self::assertNotSame('drop', $r['label']); } public function valid_email_provider(): array { return [ 'gmail' => ['pasha@gmail.com'], 'doménный' => ['info@some-domain.com'], 'yandex' => ['name@yandex.ru'], ]; } public function test_drop_invalid_email_does_not_protect(): void { $fields = [ 'your-name' => 'OB', 'email' => 'not-an-email', 'text-1' => 'AB', 'text-2' => 'CD', 'text-3' => 'EF', ]; $r = Classifier::classify($fields, ['reason' => 'Akismet']); self::assertSame('drop', $r['label']); } public function test_drop_skipped_on_form_without_name_when_no_placeholder_rule(): void { // Форма «обратный звонок»: только телефон, имя нет, < 3 placeholder $fields = ['tel-1' => '+79991234567']; $r = Classifier::classify($fields, ['reason' => 'Akismet']); self::assertNotSame('drop', $r['label']); } public function test_drop_triggers_on_form_without_name_via_placeholder_rule(): void { $fields = ['a' => 'AB', 'b' => 'CD', 'c' => 'EF', 'd' => 'GH']; $r = Classifier::classify($fields, ['reason' => 'Akismet']); self::assertSame('drop', $r['label']); } public function test_score_drop_threshold_minus_5(): void { // keyword_cap (-6) + latin_only (-1) = -7, без позитивных → score-based drop // Имя длинное (Melissa Smith), сообщение latin_only → НЕ hard-rule, чисто по score. $fields = [ 'your-name' => 'Melissa Smith', 'your-message' => 'SEO, backlinks, crypto, casino, займ, bitcoin, viagra, porno', ]; $r = Classifier::classify($fields, ['reason' => 'Akismet']); self::assertLessThanOrEqual(-5, $r['score']); self::assertSame('drop', $r['label']); } public function test_filter_disables_short_latin_name_rule(): void { add_filter('cf7stg_hard_drop_rules', static function ($rules) { $rules['short_latin_name'] = false; return $rules; }); // Только короткое имя, < 3 placeholder, нет позитивных → правило отключено → не drop $fields = ['your-name' => 'OB', 'tel' => '+79991234567']; $r = Classifier::classify($fields, ['reason' => 'Akismet']); self::assertNotSame('drop', $r['label']); } public function test_filter_thresholds_can_change_drop_score(): void { add_filter('cf7stg_classifier_thresholds', static function ($t) { $t['drop'] = -3; return $t; }); // URL даёт -3, нет hard-rule (нет короткого латинского имени, нет 3+ placeholder), // но -3 ≤ -3 → drop по score-порогу. $fields = ['msg' => 'http://x.com']; $r = Classifier::classify($fields, ['reason' => 'Akismet']); self::assertSame('drop', $r['label']); } public function test_hard_drop_disabled_via_settings_skips_drop(): void { Settings::set_hard_drop_enabled(false); Settings::register_classifier_filters(); $fields = require __DIR__ . '/fixtures/spam-screenshot-1.php'; $r = Classifier::classify($fields, ['reason' => 'Akismet']); // With hard-rule disabled, screenshot-1 has no positive signals AND score isn't ≤ -5 // (placeholders by themselves give no score penalty), so label should NOT be 'drop'. self::assertNotSame('drop', $r['label']); } public function test_drop_score_threshold_from_settings_applied(): void { Settings::set_drop_score_threshold(-3); Settings::register_classifier_filters(); // URL gives -3 score, no positive signals, no hard-rule trigger. // With threshold lowered to -3, this should fire score-based drop. $fields = ['msg' => 'http://x.com']; $r = Classifier::classify($fields, ['reason' => 'Akismet']); self::assertSame('drop', $r['label']); } public function test_settings_threshold_can_be_overridden_by_user_filter(): void { Settings::set_drop_score_threshold(-3); Settings::register_classifier_filters(); // User filter at priority 10 should override Settings default at priority 5. add_filter('cf7stg_classifier_thresholds', static function ($t) { $t['drop'] = -10; return $t; }, 10); $fields = ['msg' => 'http://x.com']; // score = -3 $r = Classifier::classify($fields, ['reason' => 'Akismet']); // -3 is NOT ≤ -10, so no drop self::assertNotSame('drop', $r['label']); } public function test_filter_disables_placeholder_fields_rule(): void { add_filter('cf7stg_hard_drop_rules', static function ($rules) { $rules['placeholder_fields'] = false; return $rules; }); // Form WITHOUT name field — digit-only values are valid placeholders (^[A-Za-z0-9]{1,2}$) // but FieldDetector::find_name rejects them (has digits), so short_latin_name cannot fire. // With placeholder_fields rule also disabled → no hard-rule fires → no drop. $fields = ['a' => '11', 'b' => '22', 'c' => '33', 'd' => '44']; $r = Classifier::classify($fields, ['reason' => 'Akismet']); self::assertNotSame('drop', $r['label']); } }