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