PluginProbe ʕ •ᴥ•ʔ
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More / 2.10.0
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More v2.10.0
2.11.0 2.10.0 2.9.0 2.8.0 2.7.0 2.6.7 2.6.8 2.6.5 2.6.4 2.6.3 2.6.2 2.6.0 2.5.5 2.5.4 2.5.3 2.5.2 trunk 1.0 1.0.1 1.0.2 1.0.3 1.1 1.1.1 1.1.2 1.2.0 2.0 2.1.0 2.1.1 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.5.0 2.5.1
reviews-feed / tests / bootstrap.php
reviews-feed / tests Last commit date
Unit 3 weeks ago bootstrap.php 3 weeks ago phpunit.xml 3 weeks ago
bootstrap.php
600 lines
1 <?php
2
3 /**
4 * PHPUnit bootstrap file for sb-reviews plugin tests.
5 *
6 * These tests are designed to run without the full WordPress environment
7 * by mocking WordPress functions and focusing on unit-testable logic.
8 */
9
10 // Define WordPress stubs for functions used in tested code
11 if (!defined('ABSPATH')) {
12 define('ABSPATH', dirname(__DIR__) . '/');
13 }
14
15 if (!defined('SBR_RELAY_BASE_URL')) {
16 define('SBR_RELAY_BASE_URL', 'https://relay.smashballoon.com/api/v1.0/');
17 }
18
19 // Plugin constants required when tests `require` the full sbr-functions.php.
20 if (!defined('SBR_PLUGIN_BASENAME')) {
21 define('SBR_PLUGIN_BASENAME', 'reviews-feed-pro/sb-reviews-pro.php');
22 }
23 if (!defined('SBR_PLUGIN_URL')) {
24 define('SBR_PLUGIN_URL', 'https://example.test/wp-content/plugins/reviews-feed-pro/');
25 }
26 if (!defined('SBRVER')) {
27 define('SBRVER', '2.5.0-test');
28 }
29 // Pro-side constants — required for silent-reactivation Pro-path tests.
30 // Pro tests will have these defined; the Free-skip path is covered by the
31 // license_key==='' early return, since Free installs never populate a key.
32 if (!defined('SBR_PLUGIN_NAME')) {
33 define('SBR_PLUGIN_NAME', 'Reviews Feed Pro Test');
34 }
35 if (!defined('SBR_PRODUCT_ID')) {
36 define('SBR_PRODUCT_ID', 9999999);
37 }
38 // Feeds table name — mirrors the runtime define (plugin bootstrap.php) so tests
39 // exercising queries that use SBR_FEEDS_TABLE (e.g. feed_localizations_for_source)
40 // resolve the constant instead of erroring on an undefined constant.
41 if (!defined('SBR_FEEDS_TABLE')) {
42 define('SBR_FEEDS_TABLE', 'sbr_feeds');
43 }
44 // Reviews-posts table name — mirrors the runtime define (plugin bootstrap.php) so
45 // tests that load SinglePostCache (POSTS_TABLE_NAME = SBR_POSTS_TABLE class const)
46 // resolve the constant instead of erroring on class load.
47 if (!defined('SBR_POSTS_TABLE')) {
48 define('SBR_POSTS_TABLE', 'sbr_reviews_posts');
49 }
50
51 // Sources table name — mirrors the runtime define so tests exercising the
52 // external-provider refresh path (SBR_Sources::sources_by_providers) resolve
53 // the constant instead of erroring on an undefined constant.
54 if (!defined('SBR_SOURCES_TABLE')) {
55 define('SBR_SOURCES_TABLE', 'sbr_sources');
56 }
57
58 // Mock WordPress functions used in tested code
59 if (!function_exists('sanitize_text_field')) {
60 function sanitize_text_field($str)
61 {
62 return trim(strip_tags($str));
63 }
64 }
65
66 // SMASH-1795 — parse_single_review() and sbr_kses_review_text() now use these.
67 // Approximations of WordPress behaviour, enough for the unit contracts under test:
68 // tags stripped, newlines kept by the textarea variant, allowlist honoured by kses.
69 if (!function_exists('sanitize_textarea_field')) {
70 function sanitize_textarea_field($str)
71 {
72 // Like sanitize_text_field but newline-preserving.
73 return trim(strip_tags((string) $str));
74 }
75 }
76
77 if (!function_exists('sanitize_key')) {
78 function sanitize_key($key)
79 {
80 return preg_replace('/[^a-z0-9_\-]/', '', strtolower((string) $key));
81 }
82 }
83
84 // SMASH-1795 — sbr_kses_review_text() resolves an emoji alt through esc_html().
85 // These lived only inside individual test files, which made any class relying on
86 // them pass in a full run and fatal in isolation. They belong here.
87 if (!function_exists('esc_html')) {
88 function esc_html($text)
89 {
90 return htmlspecialchars((string) $text, ENT_QUOTES, 'UTF-8');
91 }
92 }
93
94 if (!function_exists('esc_attr')) {
95 function esc_attr($text)
96 {
97 return htmlspecialchars((string) $text, ENT_QUOTES, 'UTF-8');
98 }
99 }
100
101 if (!function_exists('esc_url_raw')) {
102 /**
103 * Mirrors the parts of WordPress's esc_url() that this ticket depends on, in the
104 * same ORDER — the order is what makes it safe, and a stub that skipped a step
105 * would give false coverage on exactly the attack class under test.
106 *
107 * Verified against a real WP install; all of these return '':
108 * javascript:alert(1) · data:text/html;base64,x · java<TAB>script:alert(1)
109 * jav&#x0A;ascript:alert(1) · jav&amp;#x0A;ascript:alert(1)
110 * and these round-trip: /wp-content/a.jpg · https://x.test/my%20photo.jpg
111 * while a scheme-less relative path gains a host: wp-content/a.jpg ->
112 * http://wp-content/a.jpg.
113 */
114 function esc_url_raw($url)
115 {
116 $url = str_replace(' ', '%20', ltrim((string) $url));
117 // WP strips every character outside this set BEFORE testing the protocol,
118 // which is what disarms `java<TAB>script:` and the entity-encoded forms.
119 $url = (string) preg_replace('|[^a-z0-9-~+_.?#=!&;,/:%@$\|*\'()\[\]\x80-\xff]|i', '', $url);
120 if ($url === '') {
121 return '';
122 }
123 if (stripos($url, 'mailto:') !== 0) {
124 $url = str_ireplace(array('%0d', '%0a'), '', $url);
125 }
126 $url = str_replace(';//', '://', $url);
127
128 if (strpos($url, ':') !== false) {
129 $scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME));
130 // A leading '//' is protocol-relative, not a scheme.
131 if (
132 strpos($url, '//') !== 0
133 && !in_array($scheme, array('http', 'https', 'mailto', 'tel'), true)
134 ) {
135 return '';
136 }
137 return $url;
138 }
139 if (!in_array($url[0], array('/', '#', '?'), true)) {
140 return 'http://' . $url;
141 }
142 return $url;
143 }
144 }
145
146 if (!function_exists('wp_kses')) {
147 /**
148 * strip_tags() alone is NOT a faithful enough stand-in: it keeps every attribute
149 * on an allowed tag, so `<span onmouseover=…>` would survive and a test asserting
150 * that attributes are dropped would pass against a broken implementation. Real
151 * wp_kses() drops any attribute not in the tag's allowlist, so the stub strips
152 * attributes too and only honours the ones explicitly permitted.
153 */
154 function wp_kses($string, $allowed_html = array())
155 {
156 // Real wp_kses() treats a STRING second argument as a CONTEXT NAME and resolves
157 // it through wp_kses_allowed_html() — 'post' yields $allowedposttags, which
158 // KEEPS <img class src alt>. Reproducing that here is what makes the
159 // non-array-filter-return test non-vacuous: a stub that quietly cast the string
160 // to an array would let the unguarded implementation pass. Verified against
161 // WordPress: wp_kses('<img class="emoji" src="x" alt="pwn">', 'post') returns
162 // the img intact.
163 if (is_string($allowed_html)) {
164 $allowed_html = $allowed_html === 'strip'
165 ? array()
166 : array('img' => array('class' => array(), 'src' => array(), 'alt' => array()),
167 'a' => array('href' => array()), 'em' => array(), 'strong' => array(), 'br' => array());
168 }
169 $allowed_html = (array) $allowed_html;
170 $allowed = array_keys($allowed_html);
171 $string = (string) $string;
172
173 if (empty($allowed)) {
174 return strip_tags($string);
175 }
176
177 $string = strip_tags($string, '<' . implode('><', $allowed) . '>');
178
179 // Drop attributes that the tag's own allowlist doesn't name.
180 return (string) preg_replace_callback(
181 '#<([a-zA-Z0-9]+)([^>]*)>#',
182 static function ($m) use ($allowed_html) {
183 $tag = strtolower($m[1]);
184 $attrs = isset($allowed_html[$tag]) ? (array) $allowed_html[$tag] : array();
185 if (empty($attrs)) {
186 // Preserve a self-closing marker (`<br />`) but nothing else.
187 return substr(rtrim($m[2]), -1) === '/' ? '<' . $tag . ' />' : '<' . $tag . '>';
188 }
189 $kept = '';
190 foreach (array_keys($attrs) as $name) {
191 if (preg_match('#\s' . preg_quote($name, '#') . '\s*=\s*("[^"]*"|\'[^\']*\'|\S+)#i', $m[2], $a)) {
192 $value = trim($a[1], '"\'');
193 // Real wp_kses() runs URL attributes through an allowed-protocol
194 // list, so `javascript:` / `data:` hrefs are dropped. Without this
195 // the stub would let an `a[href]` allowlist look safe when it isn't.
196 if (in_array($name, array('href', 'src', 'cite'), true)) {
197 $scheme = strtolower((string) parse_url($value, PHP_URL_SCHEME));
198 if ($scheme !== '' && !in_array($scheme, array('http', 'https', 'mailto', 'tel'), true)) {
199 continue;
200 }
201 }
202 $kept .= ' ' . $name . '=' . $a[1];
203 }
204 }
205 return '<' . $tag . $kept . '>';
206 },
207 $string
208 );
209 }
210 }
211
212
213 if (!function_exists('wp_strip_all_tags')) {
214 function wp_strip_all_tags($text, $remove_breaks = false)
215 {
216 $text = preg_replace('@<(script|style)[^>]*?>.*?</\\1>@si', '', (string) $text);
217 $text = strip_tags($text);
218 return $remove_breaks ? trim(preg_replace('/[\\r\\n\\t ]+/', ' ', $text)) : trim($text);
219 }
220 }
221
222 if (!function_exists('absint')) {
223 function absint($maybeint)
224 {
225 return abs((int) $maybeint);
226 }
227 }
228
229 if (!function_exists('get_option')) {
230 function get_option($option, $default = false)
231 {
232 global $wp_options_mock;
233 return $wp_options_mock[$option] ?? $default;
234 }
235 }
236
237 if (!function_exists('wp_parse_url')) {
238 function wp_parse_url($url, $component = -1)
239 {
240 return parse_url($url, $component);
241 }
242 }
243
244 if (!function_exists('wp_parse_args')) {
245 function wp_parse_args($args, $defaults = [])
246 {
247 if (is_object($args)) {
248 $args = get_object_vars($args);
249 } elseif (!is_array($args)) {
250 parse_str((string) $args, $args);
251 }
252 return array_merge($defaults, $args);
253 }
254 }
255
256 if (!function_exists('update_option')) {
257 // Core signature: update_option($option, $value, $autoload = null).
258 // $autoload is accepted for signature parity so other tests exercising code
259 // that passes it don't fail with "Too many arguments".
260 function update_option($option, $value, $autoload = null)
261 {
262 global $wp_options_mock;
263 if (!is_array($wp_options_mock)) {
264 $wp_options_mock = [];
265 }
266 $wp_options_mock[$option] = $value;
267 return true;
268 }
269 }
270
271 if (!function_exists('delete_option')) {
272 function delete_option($option)
273 {
274 global $wp_options_mock;
275 if (!is_array($wp_options_mock)) {
276 $wp_options_mock = [];
277 return true;
278 }
279 unset($wp_options_mock[$option]);
280 return true;
281 }
282 }
283
284 if (!function_exists('home_url')) {
285 // Core signature: home_url($path = '', $scheme = null).
286 function home_url($path = '', $scheme = null)
287 {
288 global $wp_home_url_mock;
289 $base = $wp_home_url_mock ?? 'https://example.test';
290 return rtrim($base, '/') . ($path === '' ? '' : '/' . ltrim($path, '/'));
291 }
292 }
293
294 if (!function_exists('get_bloginfo')) {
295 function get_bloginfo($show = '')
296 {
297 global $wp_bloginfo_mock;
298 if (is_array($wp_bloginfo_mock) && array_key_exists($show, $wp_bloginfo_mock)) {
299 return $wp_bloginfo_mock[$show];
300 }
301 return 'Test Site';
302 }
303 }
304
305 if (!function_exists('get_home_url')) {
306 // Core signature: get_home_url($blog_id = null, $path = '', $scheme = null).
307 // Accept and ignore the extra args so callers using the full form don't error.
308 function get_home_url($blog_id = null, $path = '', $scheme = null)
309 {
310 global $wp_home_url_mock;
311 return $wp_home_url_mock ?? '';
312 }
313 }
314
315 if (!function_exists('wp_json_encode')) {
316 function wp_json_encode($data, $options = 0, $depth = 512)
317 {
318 return json_encode($data, $options, $depth);
319 }
320 }
321
322 if (!function_exists('esc_sql')) {
323 function esc_sql($data)
324 {
325 return is_array($data) ? array_map('esc_sql', $data) : addslashes((string) $data);
326 }
327 }
328
329 if (!function_exists('trailingslashit')) {
330 function trailingslashit($string)
331 {
332 return rtrim($string, '/\\') . '/';
333 }
334 }
335
336 if (!function_exists('wp_upload_dir')) {
337 function wp_upload_dir($time = null, $create_dir = true, $refresh_cache = false)
338 {
339 return [
340 'path' => '/tmp/uploads',
341 'url' => 'https://example.test/wp-content/uploads',
342 'subdir' => '',
343 'basedir' => '/tmp/uploads',
344 'baseurl' => 'https://example.test/wp-content/uploads',
345 'error' => false,
346 ];
347 }
348 }
349
350 // Stubs that let tests `require_once 'class/sbr-functions.php'` without
351 // triggering WordPress-only bootstrap calls at the top level.
352 if (!function_exists('register_activation_hook')) {
353 function register_activation_hook($file, $callback)
354 {
355 // no-op for tests
356 }
357 }
358 if (!function_exists('add_action')) {
359 function add_action($hook, $callback, $priority = 10, $accepted_args = 1)
360 {
361 // no-op for tests
362 }
363 }
364 if (!function_exists('add_filter')) {
365 function add_filter($hook, $callback, $priority = 10, $accepted_args = 1)
366 {
367 // no-op for tests
368 }
369 }
370 if (!function_exists('apply_filters')) {
371 function apply_filters($hook, $value, ...$args)
372 {
373 // Tests can inject a return value per hook via $wp_filter_mock (e.g. to
374 // simulate WPML's wpml_active_languages / wpml_current_language). With no
375 // mock set the stub keeps its original passthrough behavior.
376 global $wp_filter_mock;
377 if (isset($wp_filter_mock[$hook])) {
378 return $wp_filter_mock[$hook];
379 }
380 return $value;
381 }
382 }
383 if (!function_exists('do_action')) {
384 function do_action($hook, ...$args)
385 {
386 // no-op for tests
387 }
388 }
389
390 // Transient stubs for silent-reactivation rate-limit + notice-payload tests.
391 // Stored in a dedicated global ($wp_transients_mock) so tests can manipulate
392 // them independently from $wp_options_mock.
393 if (!defined('HOUR_IN_SECONDS')) {
394 define('HOUR_IN_SECONDS', 3600);
395 }
396 if (!defined('DAY_IN_SECONDS')) {
397 define('DAY_IN_SECONDS', 86400);
398 }
399 if (!defined('WEEK_IN_SECONDS')) {
400 define('WEEK_IN_SECONDS', 604800);
401 }
402 if (!defined('HOUR_IN_SECONDS')) {
403 define('HOUR_IN_SECONDS', 3600);
404 }
405 if (!defined('MINUTE_IN_SECONDS')) {
406 define('MINUTE_IN_SECONDS', 60);
407 }
408 // `wpdb::get_results()` output_type constants — production code passes ARRAY_A.
409 if (!defined('ARRAY_A')) {
410 define('ARRAY_A', 'ARRAY_A');
411 }
412 if (!defined('ARRAY_N')) {
413 define('ARRAY_N', 'ARRAY_N');
414 }
415 if (!defined('OBJECT')) {
416 define('OBJECT', 'OBJECT');
417 }
418 if (!function_exists('get_transient')) {
419 function get_transient($key)
420 {
421 global $wp_transients_mock;
422 if (!is_array($wp_transients_mock) || !isset($wp_transients_mock[$key])) {
423 return false;
424 }
425 return $wp_transients_mock[$key];
426 }
427 }
428 if (!function_exists('set_transient')) {
429 function set_transient($key, $value, $ttl = 0)
430 {
431 global $wp_transients_mock;
432 if (!is_array($wp_transients_mock)) {
433 $wp_transients_mock = [];
434 }
435 $wp_transients_mock[$key] = $value;
436 return true;
437 }
438 }
439 if (!function_exists('delete_transient')) {
440 function delete_transient($key)
441 {
442 global $wp_transients_mock;
443 if (!is_array($wp_transients_mock)) {
444 return true;
445 }
446 unset($wp_transients_mock[$key]);
447 return true;
448 }
449 }
450
451 // Minimal $wpdb double so DB-touching helpers (e.g. clear_plugin_cache) no-op
452 // instead of fatalling in unit tests. Guarded so any test that installs its own
453 // $wpdb wins.
454 if (!isset($GLOBALS['wpdb'])) {
455 $GLOBALS['wpdb'] = new class {
456 public $prefix = 'wp_';
457 public function query($sql)
458 {
459 return 0;
460 }
461 public function get_results($sql, $output = null)
462 {
463 return [];
464 }
465 public function get_var($sql)
466 {
467 return null;
468 }
469 public function get_row($sql, $output = null)
470 {
471 return null;
472 }
473 public function prepare($query, ...$args)
474 {
475 return $query;
476 }
477 public function esc_like($text)
478 {
479 return addcslashes((string) $text, '_%\\');
480 }
481 };
482 }
483
484 // Stub is_plugin_active for provider-detection tests (EDD provider gate).
485 // Backed by $wp_active_plugins_mock so tests can flip plugin-presence per case
486 // without touching real wp-admin includes.
487 if (!function_exists('is_plugin_active')) {
488 function is_plugin_active($plugin_path)
489 {
490 global $wp_active_plugins_mock;
491 if (!is_array($wp_active_plugins_mock)) {
492 return false;
493 }
494 return in_array($plugin_path, $wp_active_plugins_mock, true);
495 }
496 }
497
498 // i18n stub used by translatable strings in tested code paths.
499 if (!function_exists('__')) {
500 function __($text, $domain = null)
501 {
502 return $text;
503 }
504 }
505
506 // WP HTTP helpers — never actually invoked in unit tests (SBRelay::call is
507 // mocked at the `onlyMethods(['call'])` level), but reverify_token_via_register
508 // has a `function_exists` defense-in-depth guard that bails early if these
509 // helpers aren't defined. Without these stubs the guard fires in tests and
510 // reverify never reaches the mocked `call()`.
511 if (!function_exists('wp_remote_post')) {
512 function wp_remote_post($url, $args = [])
513 {
514 return [];
515 }
516 }
517 if (!function_exists('wp_remote_get')) {
518 function wp_remote_get($url, $args = [])
519 {
520 return [];
521 }
522 }
523 if (!function_exists('is_wp_error')) {
524 function is_wp_error($thing)
525 {
526 return false;
527 }
528 }
529 if (!function_exists('wp_remote_retrieve_body')) {
530 function wp_remote_retrieve_body($response)
531 {
532 return '';
533 }
534 }
535
536 // Cron API stubs — namespace-fallback resolution requires these to live in
537 // the global namespace so callers in `SmashBalloon\Reviews\Pro\Services\BulkUpdate`
538 // (and elsewhere) can find them via PHP's fallback lookup.
539 if (!function_exists('wp_schedule_single_event')) {
540 function wp_schedule_single_event($timestamp, $hook, $args = [])
541 {
542 return true;
543 }
544 }
545
546 // Recurring-event recorder so scheduling logic (idempotency, recurrence) is
547 // unit-testable: wp_schedule_event() records into $wp_scheduled_events_mock and
548 // wp_next_scheduled() reads it back. Tests reset the global in setUp().
549 if (!function_exists('wp_schedule_event')) {
550 function wp_schedule_event($timestamp, $recurrence, $hook, $args = [], $wp_error = false)
551 {
552 global $wp_scheduled_events_mock;
553 if (!is_array($wp_scheduled_events_mock)) {
554 $wp_scheduled_events_mock = [];
555 }
556 $wp_scheduled_events_mock[$hook] = [
557 'timestamp' => $timestamp,
558 'recurrence' => $recurrence,
559 'args' => $args,
560 ];
561 return true;
562 }
563 }
564
565 if (!function_exists('wp_next_scheduled')) {
566 function wp_next_scheduled($hook, $args = [])
567 {
568 global $wp_scheduled_events_mock;
569 if (is_array($wp_scheduled_events_mock) && isset($wp_scheduled_events_mock[$hook])) {
570 return $wp_scheduled_events_mock[$hook]['timestamp'];
571 }
572 return false;
573 }
574 }
575
576 if (!function_exists('wp_clear_scheduled_hook')) {
577 function wp_clear_scheduled_hook($hook, $args = [], $wp_error = false)
578 {
579 global $wp_scheduled_events_mock;
580 if (is_array($wp_scheduled_events_mock)) {
581 unset($wp_scheduled_events_mock[$hook]);
582 }
583 return 0;
584 }
585 }
586
587 // Autoloader
588 require_once dirname(__DIR__) . '/vendor/autoload.php';
589
590 // Util::should_store_local_images() passes sbr_plugin_settings_defaults() as
591 // get_option()'s default, so it is evaluated eagerly even when a test has already
592 // mocked `sbr_settings`. Load the real helper rather than shadowing it — declaring a
593 // duplicate here fatals as soon as any test require_once's class/sbr-functions.php,
594 // which has no function_exists guard. Safe at this point: the add_action /
595 // register_activation_hook / add_filter stubs above absorb its top-level calls.
596 // (SMASH-1785)
597 if (!function_exists('sbr_plugin_settings_defaults')) {
598 require_once dirname(__DIR__) . '/class/sbr-functions.php';
599 }
600