Skip to content

Commit bb72176

Browse files
GaryJonesclaude
andcommitted
test: add unit test suite with Brain Monkey
Adds unit tests for Ad_Code_Manager class methods that can be tested without WordPress, including validate_script_url() for URL whitelist validation and filter_output_tokens() for token replacement logic. Changes: - Add tests/Unit/ directory with TestCase base class - Add AdCodeManagerTest.php (10 tests) covering URL validation and token filtering - Add tests/bootstrap.php to support both unit and integration tests - Update phpunit.xml.dist to include Unit testsuite - Add test:unit composer script - Add phpunit ^9.6 to require-dev - Rename test-integration to test:integration for consistency 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 11b7327 commit bb72176

File tree

5 files changed

+255
-5
lines changed

5 files changed

+255
-5
lines changed

composer.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"automattic/vipwpcs": "^3",
2323
"php-parallel-lint/php-parallel-lint": "^1.0",
2424
"phpcompatibility/phpcompatibility-wp": "^2.1",
25+
"phpunit/phpunit": "^9.6",
2526
"yoast/wp-test-utils": "^1.2"
2627
},
2728
"config": {
@@ -49,8 +50,9 @@
4950
"lint-ci": [
5051
"@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . -e php --exclude vendor --exclude .git --checkstyle"
5152
],
52-
"test-integration": "wp-env run tests-cli --env-cwd=wp-content/plugins/ad-code-manager ./vendor/bin/phpunit --no-coverage --order-by=random",
53-
"test-integration-ms": "wp-env run tests-cli --env-cwd=wp-content/plugins/ad-code-manager /bin/bash -c 'WP_MULTISITE=1 ./vendor/bin/phpunit --no-coverage --order-by=random'"
53+
"test:unit": "@php ./vendor/bin/phpunit --testsuite Unit",
54+
"test:integration": "wp-env run tests-cli --env-cwd=wp-content/plugins/ad-code-manager ./vendor/bin/phpunit --testsuite integration --no-coverage --order-by=random",
55+
"test:integration-ms": "wp-env run tests-cli --env-cwd=wp-content/plugins/ad-code-manager /bin/bash -c 'WP_MULTISITE=1 ./vendor/bin/phpunit --testsuite integration --no-coverage --order-by=random'"
5456
},
5557
"scripts-descriptions": {
5658
"coverage": "Run tests with code coverage reporting",
@@ -60,7 +62,8 @@
6062
"i18n": "Generate a POT file for translation",
6163
"lint": "Run PHP linting",
6264
"lint-ci": "Run PHP linting and send results to stdout",
63-
"test-integration": "Run integration tests in wp-env",
64-
"test-integration-ms": "Run integration tests in multisite mode in wp-env"
65+
"test:unit": "Run unit tests (no WordPress required)",
66+
"test:integration": "Run integration tests in wp-env",
67+
"test:integration-ms": "Run integration tests in multisite mode in wp-env"
6568
}
6669
}

phpunit.xml.dist

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
33
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
4-
bootstrap="./tests/Integration/bootstrap.php"
4+
bootstrap="./tests/bootstrap.php"
55
cacheResultFile=".phpunit.cache/test-results"
66
executionOrder="depends,defects"
77
forceCoversAnnotation="false"
@@ -15,6 +15,9 @@
1515
testdox="true"
1616
verbose="true">
1717
<testsuites>
18+
<testsuite name="Unit">
19+
<directory suffix="Test.php">tests/Unit</directory>
20+
</testsuite>
1821
<testsuite name="integration">
1922
<directory>tests/Integration</directory>
2023
</testsuite>

tests/Unit/AdCodeManagerTest.php

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
<?php
2+
/**
3+
* Unit tests for the Ad_Code_Manager class.
4+
*
5+
* @package Automattic\AdCodeManager\Tests\Unit
6+
*/
7+
8+
declare( strict_types=1 );
9+
10+
namespace Automattic\AdCodeManager\Tests\Unit;
11+
12+
use Ad_Code_Manager;
13+
use Brain\Monkey\Functions;
14+
use stdClass;
15+
16+
/**
17+
* Test case for Ad_Code_Manager.
18+
*/
19+
class AdCodeManagerTest extends TestCase {
20+
21+
/**
22+
* The Ad_Code_Manager instance.
23+
*
24+
* @var Ad_Code_Manager
25+
*/
26+
private Ad_Code_Manager $ad_code_manager;
27+
28+
/**
29+
* Set up test fixtures.
30+
*/
31+
protected function setUp(): void {
32+
parent::setUp();
33+
34+
// Stub WordPress functions.
35+
Functions\stubs( [ '__' => null ] );
36+
37+
$this->ad_code_manager = new Ad_Code_Manager();
38+
39+
// Create a mock provider with whitelisted URLs.
40+
$this->ad_code_manager->current_provider = new stdClass();
41+
$this->ad_code_manager->current_provider->whitelisted_script_urls = [
42+
'example.com',
43+
'ads.google.com',
44+
'secure.pagead2.googlesyndication.com',
45+
];
46+
}
47+
48+
/**
49+
* Test validate_script_url with empty URL returns true.
50+
*/
51+
public function testValidateScriptUrlEmptyReturnsTrue(): void {
52+
$this->assertTrue( $this->ad_code_manager->validate_script_url( '' ) );
53+
}
54+
55+
/**
56+
* Test validate_script_url with exact domain match.
57+
*/
58+
public function testValidateScriptUrlExactDomainMatch(): void {
59+
Functions\when( 'wp_parse_url' )->alias( 'parse_url' );
60+
61+
$this->assertTrue(
62+
$this->ad_code_manager->validate_script_url( 'https://example.com/script.js' )
63+
);
64+
}
65+
66+
/**
67+
* Test validate_script_url with subdomain match.
68+
*/
69+
public function testValidateScriptUrlSubdomainMatch(): void {
70+
Functions\when( 'wp_parse_url' )->alias( 'parse_url' );
71+
72+
$this->assertTrue(
73+
$this->ad_code_manager->validate_script_url( 'https://cdn.example.com/ads/script.js' )
74+
);
75+
}
76+
77+
/**
78+
* Test validate_script_url with non-whitelisted domain.
79+
*/
80+
public function testValidateScriptUrlNonWhitelistedDomain(): void {
81+
Functions\when( 'wp_parse_url' )->alias( 'parse_url' );
82+
83+
$this->assertFalse(
84+
$this->ad_code_manager->validate_script_url( 'https://malicious-site.com/script.js' )
85+
);
86+
}
87+
88+
/**
89+
* Test validate_script_url prevents domain spoofing.
90+
*
91+
* Ensures that 'evilexample.com' doesn't match 'example.com'.
92+
*/
93+
public function testValidateScriptUrlPreventsDomainSpoofing(): void {
94+
Functions\when( 'wp_parse_url' )->alias( 'parse_url' );
95+
96+
$this->assertFalse(
97+
$this->ad_code_manager->validate_script_url( 'https://evilexample.com/script.js' )
98+
);
99+
}
100+
101+
/**
102+
* Test validate_script_url with Google Ads URL.
103+
*/
104+
public function testValidateScriptUrlGoogleAds(): void {
105+
Functions\when( 'wp_parse_url' )->alias( 'parse_url' );
106+
107+
$this->assertTrue(
108+
$this->ad_code_manager->validate_script_url( 'https://ads.google.com/ad-manager/tag.js' )
109+
);
110+
}
111+
112+
/**
113+
* Test validate_script_url with deep subdomain.
114+
*/
115+
public function testValidateScriptUrlDeepSubdomain(): void {
116+
Functions\when( 'wp_parse_url' )->alias( 'parse_url' );
117+
118+
$this->assertTrue(
119+
$this->ad_code_manager->validate_script_url( 'https://a.b.c.example.com/script.js' )
120+
);
121+
}
122+
123+
/**
124+
* Test filter_output_tokens adds URL vars as tokens.
125+
*/
126+
public function testFilterOutputTokensAddsUrlVars(): void {
127+
$code_to_display = [
128+
'url_vars' => [
129+
'site_id' => '12345',
130+
'zone' => 'header',
131+
'width' => '728',
132+
'height' => '90',
133+
],
134+
];
135+
136+
$output_tokens = $this->ad_code_manager->filter_output_tokens( [], 'test_tag', $code_to_display );
137+
138+
$this->assertArrayHasKey( '%site_id%', $output_tokens );
139+
$this->assertArrayHasKey( '%zone%', $output_tokens );
140+
$this->assertArrayHasKey( '%width%', $output_tokens );
141+
$this->assertArrayHasKey( '%height%', $output_tokens );
142+
$this->assertSame( '12345', $output_tokens['%site_id%'] );
143+
$this->assertSame( 'header', $output_tokens['%zone%'] );
144+
}
145+
146+
/**
147+
* Test filter_output_tokens returns original tokens when no URL vars.
148+
*/
149+
public function testFilterOutputTokensReturnsOriginalWhenNoUrlVars(): void {
150+
$code_to_display = [];
151+
$original_tokens = [ '%existing%' => 'value' ];
152+
153+
$output_tokens = $this->ad_code_manager->filter_output_tokens(
154+
$original_tokens,
155+
'test_tag',
156+
$code_to_display
157+
);
158+
159+
$this->assertSame( $original_tokens, $output_tokens );
160+
}
161+
162+
/**
163+
* Test filter_output_tokens preserves existing tokens.
164+
*/
165+
public function testFilterOutputTokensPreservesExistingTokens(): void {
166+
$code_to_display = [
167+
'url_vars' => [
168+
'new_var' => 'new_value',
169+
],
170+
];
171+
$original_tokens = [ '%existing%' => 'value' ];
172+
173+
$output_tokens = $this->ad_code_manager->filter_output_tokens(
174+
$original_tokens,
175+
'test_tag',
176+
$code_to_display
177+
);
178+
179+
$this->assertArrayHasKey( '%existing%', $output_tokens );
180+
$this->assertArrayHasKey( '%new_var%', $output_tokens );
181+
}
182+
}

tests/Unit/TestCase.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
/**
3+
* Base test case for unit tests.
4+
*
5+
* @package Automattic\AdCodeManager\Tests\Unit
6+
*/
7+
8+
declare( strict_types=1 );
9+
10+
namespace Automattic\AdCodeManager\Tests\Unit;
11+
12+
use Yoast\WPTestUtils\BrainMonkey\TestCase as BrainMonkeyTestCase;
13+
14+
/**
15+
* Base test case for unit tests.
16+
*/
17+
abstract class TestCase extends BrainMonkeyTestCase {
18+
// Extend as needed.
19+
}

tests/bootstrap.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
/**
3+
* PHPUnit bootstrap file for Ad Code Manager plugin tests.
4+
*
5+
* @package Automattic\AdCodeManager
6+
*/
7+
8+
declare( strict_types=1 );
9+
10+
namespace Automattic\AdCodeManager\Tests;
11+
12+
// Composer autoloader.
13+
require_once dirname( __DIR__ ) . '/vendor/autoload.php';
14+
15+
// Check for a `--testsuite Unit` arg when calling phpunit.
16+
$argv_local = $GLOBALS['argv'] ?? [];
17+
$key = (int) array_search( '--testsuite', $argv_local, true );
18+
$is_unit = false;
19+
20+
// Check for --testsuite Unit (two separate args).
21+
if ( $key && isset( $argv_local[ $key + 1 ] ) && 'Unit' === $argv_local[ $key + 1 ] ) {
22+
$is_unit = true;
23+
}
24+
25+
// Check for --testsuite=Unit (single arg with equals).
26+
foreach ( $argv_local as $arg ) {
27+
if ( '--testsuite=Unit' === $arg ) {
28+
$is_unit = true;
29+
break;
30+
}
31+
}
32+
33+
if ( $is_unit ) {
34+
// Unit tests use Brain Monkey - no WordPress loaded.
35+
// Load plugin classes that can be tested without WordPress.
36+
require_once dirname( __DIR__ ) . '/src/class-acm-provider.php';
37+
require_once dirname( __DIR__ ) . '/src/class-ad-code-manager.php';
38+
require_once __DIR__ . '/Unit/TestCase.php';
39+
return;
40+
}
41+
42+
// For integration tests, delegate to the Integration bootstrap.
43+
require_once __DIR__ . '/Integration/bootstrap.php';

0 commit comments

Comments
 (0)