PluginProbe ʕ •ᴥ•ʔ
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More / 2.9.0
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More v2.9.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 / Unit / Smash1631MultiLanguageBulkTest.php
reviews-feed / tests / Unit Last commit date
Doubles 1 month ago Providers 1 month ago BulkRearmOnGrowthTest.php 1 month ago BulkReviewsUpdateStuckStateTest.php 1 month ago ClearCacheRelayResetTest.php 1 month ago DeleteSourceRelayFailureTest.php 1 month ago ErrorHandlerFalsyOptionTest.php 1 month ago FeedCacheUpdateServiceTest.php 1 month ago FeedMalformedPayloadTest.php 1 month ago ForceKeylessRefetchTest.php 1 month ago LicenseDeactivateStaleStateTest.php 1 month ago MediaFinderMemoTest.php 1 month ago MultiSourceAggregationTest.php 1 month ago ReconcileMigratedLicenseRoutineTest.php 1 month ago ReconcileRemovalTest.php 1 month ago RegisterWebsiteRoutineTest.php 1 month ago RemoteRequestMemoTest.php 1 month ago ReviewAlertHeaderTotalsTest.php 1 month ago ReviewAlertPageTargetingTest.php 1 month ago ReviewAlertStarFillTest.php 1 month ago ShortcodeNeutralizationTest.php 1 month ago SiteMigrationRecoveryTest.php 1 month ago Smash1583HeaderParityTest.php 1 month ago Smash1631MultiLanguageBulkTest.php 1 month ago Smash1631UpdateSingleLangScopeTest.php 1 month ago Smash1706TripAdvisorPlaceIdTest.php 1 month ago Smash782BookingHeaderRatingTest.php 1 month ago Smash782CountryFlagEmojiTest.php 1 month ago Smash782ExternalRefreshCronTest.php 1 month ago Smash782ExtrasTemplateTest.php 1 month ago Smash782ReviewAlertProviderDataTest.php 1 month ago SourceIdLookupTest.php 1 month ago WpmlGetCurrentLanguageTest.php 1 month ago WpmlLanguageMappingTest.php 1 month ago
Smash1631MultiLanguageBulkTest.php
517 lines
1 <?php
2
3 namespace SmashBalloon\Reviews\Tests\Unit;
4
5 use PHPUnit\Framework\TestCase;
6 use SmashBalloon\Reviews\Pro\Services\BulkUpdate\Bulk_Reviews_Update;
7 use SmashBalloon\Reviews\Pro\Helpers\SBR_WPML;
8
9 /**
10 * SMASH-1631 Option B — full multilingual bulk history.
11 *
12 * The background bulk cron has no front-end request, so "Automatically by WPML"
13 * collapses to the WPML default language (e.g. English). Every secondary language
14 * (es-419, ...) was left with only the first on-demand page translated. The fix:
15 * the bulk job fetches EVERY active WPML language (config-based, available in cron)
16 * and tags each batch with its own language, so each language gets a complete set.
17 *
18 * @group bulk-history
19 * @group customer-bug-2026-07-02
20 */
21 class Smash1631MultiLanguageBulkTest extends TestCase
22 {
23 protected function setUp(): void
24 {
25 parent::setUp();
26 global $wp_options_mock, $wp_filter_mock;
27 $wp_options_mock = [];
28 $wp_filter_mock = [];
29 }
30
31 protected function tearDown(): void
32 {
33 global $wp_filter_mock, $sitepress;
34 $wp_filter_mock = [];
35 $sitepress = null;
36 parent::tearDown();
37 }
38
39 /**
40 * Pure mapper: WPML's active-languages array -> deduped Google codes.
41 * es-mx and es-ar both collapse to es-419; unmappable codes are dropped.
42 */
43 public function test_map_active_languages_to_google(): void
44 {
45 $active = [
46 'en' => ['code' => 'en', 'language_code' => 'en'],
47 'es-mx' => ['code' => 'es-mx', 'language_code' => 'es-mx'],
48 'es-ar' => ['code' => 'es-ar', 'language_code' => 'es-ar'],
49 'pt-br' => ['code' => 'pt-br', 'language_code' => 'pt-br'],
50 'zz' => ['code' => 'zz', 'language_code' => 'zz'], // unmappable -> dropped
51 ];
52
53 $codes = SBR_WPML::map_active_languages_to_google($active);
54
55 $this->assertContains('en', $codes);
56 $this->assertContains('es-419', $codes);
57 $this->assertContains('pt-BR', $codes);
58 $this->assertNotContains('zz', $codes);
59 // es-mx + es-ar dedupe to a single es-419
60 $this->assertSame(1, count(array_keys($codes, 'es-419')), 'es-419 must be deduped to one entry.');
61 }
62
63 public function test_map_active_languages_handles_non_array(): void
64 {
65 $this->assertSame([], SBR_WPML::map_active_languages_to_google(null));
66 $this->assertSame([], SBR_WPML::map_active_languages_to_google('en'));
67 }
68
69 /**
70 * In WP-Cron the 'wpml_active_languages' filter can return empty — WPML's own
71 * docs say it "can only work when the global $wp_query object has been
72 * instantiated ... after the 'wp' action", which cron never fires. When it's
73 * empty, get_active_google_languages() must fall back to the SitePress object's
74 * get_active_languages() (a direct icl_languages read that works headless) so
75 * the bulk job still fetches every configured language instead of collapsing to
76 * a single English pass. SMASH-1631.
77 */
78 public function test_get_active_google_languages_falls_back_to_sitepress_when_filter_empty(): void
79 {
80 if (! defined('ICL_SITEPRESS_VERSION')) {
81 define('ICL_SITEPRESS_VERSION', '4.9.5');
82 }
83 global $wp_filter_mock, $sitepress;
84 // Simulate cron: the $wp_query-dependent filter yields nothing.
85 $wp_filter_mock['wpml_active_languages'] = [];
86 // SitePress::get_active_languages() shape: keyed by code, each entry an
87 // array carrying at least 'code' (matches the real icl_languages SELECT).
88 $sitepress = new class {
89 public function get_active_languages()
90 {
91 return [
92 'en' => ['code' => 'en', 'english_name' => 'English', 'active' => 1],
93 'es-mx' => ['code' => 'es-mx', 'english_name' => 'Spanish (Mexico)', 'active' => 1],
94 ];
95 }
96 };
97
98 $codes = SBR_WPML::get_active_google_languages();
99
100 $this->assertContains('en', $codes, 'Default language must survive the SitePress fallback.');
101 $this->assertContains('es-419', $codes, 'es-mx must map to es-419 via the SitePress fallback in cron.');
102 }
103
104 /**
105 * When both the filter AND the SitePress object are unavailable, the helper
106 * returns [] — resolve_bulk_languages() then floors to a single no-language
107 * pass (original/English), so the bulk job never wedges. SMASH-1631.
108 */
109 public function test_get_active_google_languages_empty_when_no_source_available(): void
110 {
111 if (! defined('ICL_SITEPRESS_VERSION')) {
112 define('ICL_SITEPRESS_VERSION', '4.9.5');
113 }
114 global $wp_filter_mock, $sitepress;
115 $wp_filter_mock['wpml_active_languages'] = [];
116 $sitepress = null;
117
118 $this->assertSame([], SBR_WPML::get_active_google_languages());
119 }
120
121 /**
122 * Load More runs through admin-ajax, which often executes without the `wp`
123 * action — so the wpml_active_languages filter can be empty there too.
124 * switch_to_url_language() must fall back to SitePress like
125 * get_active_google_languages() does, or the Load More language regression
126 * persists (SMASH-1631, Copilot #493).
127 *
128 * @runInSeparateProcess
129 * @preserveGlobalState disabled
130 */
131 public function test_switch_to_url_language_falls_back_to_sitepress_when_filter_empty(): void
132 {
133 if (! defined('ICL_SITEPRESS_VERSION')) {
134 define('ICL_SITEPRESS_VERSION', '4.9.5');
135 }
136 global $wp_filter_mock, $sitepress;
137 $wp_filter_mock['wpml_active_languages'] = []; // admin-ajax: filter empty
138 $sitepress = new class {
139 public function get_active_languages()
140 {
141 return ['en' => ['code' => 'en'], 'es-mx' => ['code' => 'es-mx']];
142 }
143 };
144
145 $this->assertSame('es-mx', SBR_WPML::switch_to_url_language('https://x.com/es-mx/mujeres-reales/'));
146 }
147
148 /**
149 * Pure resolver matrix — the config cases that matter (global vs per-feed,
150 * wpml vs fixed, mixed). This is the heart of the config-agnostic fix.
151 */
152 public function test_resolve_bulk_languages_matrix(): void
153 {
154 $active = ['en', 'es-419'];
155 $sort = function ($a) {
156 sort($a);
157 return $a;
158 };
159
160 // global wpml, no per-feed -> all active
161 $this->assertSame(['en', 'es-419'], $sort(Bulk_Reviews_Update::resolve_bulk_languages('wpml', [], $active)));
162 // global empty, a feed set to wpml -> all active (THE per-feed fix)
163 $this->assertSame(['en', 'es-419'], $sort(Bulk_Reviews_Update::resolve_bulk_languages('', ['wpml'], $active)));
164 // global empty, a feed fixed to es-419 -> just es-419 (per-feed fixed fix)
165 $this->assertSame(['es-419'], Bulk_Reviews_Update::resolve_bulk_languages('', ['es-419'], $active));
166 // global fixed es-419 -> es-419
167 $this->assertSame(['es-419'], Bulk_Reviews_Update::resolve_bulk_languages('es-419', [], $active));
168 // global default + feed default -> a single no-language ('') fetch (untranslated)
169 $this->assertSame([''], Bulk_Reviews_Update::resolve_bulk_languages('default', ['default'], $active));
170 // source only in a fixed-'en' feed -> just en (global wpml does NOT apply — no feed inherits it)
171 $this->assertSame(['en'], Bulk_Reviews_Update::resolve_bulk_languages('wpml', ['en'], $active));
172 // dedupe across global + feed
173 $this->assertSame(['es-419'], Bulk_Reviews_Update::resolve_bulk_languages('es-419', ['es-419'], $active));
174 // SHARED SOURCE — no-language feed + es-419 feed: BOTH get extended (no-lang '' + es-419)
175 $this->assertSame(['es-419', ''], Bulk_Reviews_Update::resolve_bulk_languages('', ['default', 'es-419'], $active));
176 // feed on default INHERITS the global fixed language
177 $this->assertSame(['es-419'], Bulk_Reviews_Update::resolve_bulk_languages('es-419', ['default'], $active));
178 // feed on default inherits global wpml -> all active
179 $this->assertSame(['en', 'es-419'], $sort(Bulk_Reviews_Update::resolve_bulk_languages('wpml', ['default'], $active)));
180 // FLOOR: 'wpml' sentinel but WPML deactivated (no active languages) -> single no-language fetch
181 $this->assertSame([''], Bulk_Reviews_Update::resolve_bulk_languages('wpml', [], []));
182 $this->assertSame([''], Bulk_Reviews_Update::resolve_bulk_languages('', ['wpml'], []));
183 }
184
185 /**
186 * DISPLAY fix: Load More runs via admin-ajax (no language in the URL), so the
187 * handler maps the originating page URL (`location`) to a WPML language and
188 * switches to it. Pins the URL->language matching (subdir format).
189 *
190 * @runInSeparateProcess
191 * @preserveGlobalState disabled
192 */
193 public function test_switch_to_url_language_maps_page_url_to_language(): void
194 {
195 if (! defined('ICL_SITEPRESS_VERSION')) {
196 define('ICL_SITEPRESS_VERSION', '4.9.5');
197 }
198 global $wp_filter_mock;
199 $wp_filter_mock['wpml_active_languages'] = [
200 'en' => ['code' => 'en', 'language_code' => 'en'],
201 'es-mx' => ['code' => 'es-mx', 'language_code' => 'es-mx'],
202 ];
203
204 $this->assertSame('es-mx', SBR_WPML::switch_to_url_language('https://x.com/es-mx/mujeres-reales/'));
205 $this->assertSame('en', SBR_WPML::switch_to_url_language('https://x.com/en/reviews/'));
206 $this->assertNull(SBR_WPML::switch_to_url_language('https://x.com/fr/page/'), 'A non-active language must not match.');
207 $this->assertNull(SBR_WPML::switch_to_url_language('https://x.com/'), 'Default language (no prefix) leaves resolution alone.');
208 $this->assertNull(SBR_WPML::switch_to_url_language(''), 'Empty URL is a no-op.');
209 }
210
211 public function test_switch_to_url_language_noop_without_wpml(): void
212 {
213 // ICL_SITEPRESS_VERSION not defined in this (non-isolated) process.
214 $this->assertNull(SBR_WPML::switch_to_url_language('https://x.com/es-mx/page/'));
215 }
216
217 /**
218 * Non-WPML localization: a single explicit language passes through as one entry.
219 */
220 public function test_bulk_languages_single_for_explicit_localization(): void
221 {
222 $bulk = new Bulk_Reviews_Update();
223 $bulk->account_id = 'CHIJ_SINGLE';
224 $bulk->account_provider = 'google';
225 $bulk->settings = ['localization' => 'es-419'];
226
227 $this->assertSame(['es-419'], $bulk->bulk_languages());
228 }
229
230 /**
231 * THE FIX: with "Automatically by WPML" and WPML active, the bulk job fetches
232 * every active WPML language mapped to a Google code — not just the default.
233 *
234 * @runInSeparateProcess
235 * @preserveGlobalState disabled
236 */
237 public function test_bulk_languages_returns_all_active_wpml_languages(): void
238 {
239 if (! defined('ICL_SITEPRESS_VERSION')) {
240 define('ICL_SITEPRESS_VERSION', '4.9.5');
241 }
242 global $wp_filter_mock;
243 $wp_filter_mock['wpml_active_languages'] = [
244 'en' => ['code' => 'en', 'language_code' => 'en'],
245 'es-mx' => ['code' => 'es-mx', 'language_code' => 'es-mx'],
246 ];
247
248 $bulk = new Bulk_Reviews_Update();
249 $bulk->account_id = 'CHIJ_WPML';
250 $bulk->account_provider = 'google';
251 $bulk->settings = ['localization' => 'wpml'];
252
253 $langs = $bulk->bulk_languages();
254 sort($langs);
255 $this->assertSame(['en', 'es-419'], $langs, 'Bulk must fetch every active WPML language, mapped.');
256 }
257
258 /**
259 * THE PER-FEED FIX: global Language is default/empty, but a feed that uses this
260 * source is set to "Automatically by WPML" — the bulk must still fetch every
261 * active WPML language (the bulk otherwise only sees global settings).
262 *
263 * @runInSeparateProcess
264 * @preserveGlobalState disabled
265 */
266 public function test_bulk_languages_picks_up_per_feed_wpml_with_empty_global(): void
267 {
268 if (! defined('ICL_SITEPRESS_VERSION')) {
269 define('ICL_SITEPRESS_VERSION', '4.9.5');
270 }
271 global $wp_filter_mock, $wpdb;
272 $wp_filter_mock['wpml_active_languages'] = [
273 'en' => ['code' => 'en', 'language_code' => 'en'],
274 'es-mx' => ['code' => 'es-mx', 'language_code' => 'es-mx'],
275 ];
276 // A feed row whose sources include our source and whose language is wpml.
277 $wpdb = new class {
278 public $prefix = 'wp_';
279 public function esc_like($text)
280 {
281 return addcslashes((string) $text, '_%\\');
282 }
283 public function prepare($query, ...$args)
284 {
285 return $query;
286 }
287 public function get_results($sql, $output = null)
288 {
289 return [['settings' => '{"sources":["CHIJ_PERFEED"],"localization":"wpml"}']];
290 }
291 };
292
293 $bulk = new Bulk_Reviews_Update();
294 $bulk->account_id = 'CHIJ_PERFEED';
295 $bulk->account_provider = 'google';
296 $bulk->settings = ['localization' => 'default']; // global NOT wpml
297
298 $langs = $bulk->bulk_languages();
299 sort($langs);
300 $this->assertSame(['en', 'es-419'], $langs, 'Per-feed wpml with an empty global must still fetch all active languages.');
301 }
302
303 /**
304 * End-to-end of the loop: get_bulk_reviews issues one relay call per active
305 * language, each carrying that language — so es-419 reviews get fetched even
306 * though the cron default is English.
307 *
308 * @runInSeparateProcess
309 * @preserveGlobalState disabled
310 */
311 public function test_get_bulk_reviews_calls_relay_once_per_language(): void
312 {
313 if (! defined('ICL_SITEPRESS_VERSION')) {
314 define('ICL_SITEPRESS_VERSION', '4.9.5');
315 }
316 global $wp_filter_mock;
317 $wp_filter_mock['wpml_active_languages'] = [
318 'en' => ['code' => 'en', 'language_code' => 'en'],
319 'es-mx' => ['code' => 'es-mx', 'language_code' => 'es-mx'],
320 ];
321
322 $account_id = 'CHIJ_MULTILANG';
323 global $wp_options_mock;
324 $wp_options_mock['sbr_bulk_sources'] = [
325 $account_id => [
326 'account_id' => $account_id,
327 'provider' => 'google',
328 'retry' => false,
329 'is_done' => false,
330 'page' => 1,
331 ],
332 ];
333
334 $bulk = new Bulk_Reviews_Update();
335 $bulk->account_id = $account_id;
336 $bulk->account_provider = 'google';
337 $bulk->should_make_call();
338 $bulk->endpoint = 'reviews/google';
339 $bulk->settings = ['localization' => 'wpml'];
340 $bulk->provider = ['info' => '{"id":"' . $account_id . '"}'];
341
342 $relay = $this->makeCapturingRelay();
343 $bulk->relay = $relay;
344
345 $bulk->get_bulk_reviews();
346
347 $sent = array_map(function ($a) {
348 return $a['language'] ?? '(none)';
349 }, $relay->calls);
350 sort($sent);
351
352 $this->assertSame(['en', 'es-419'], $sent, 'One relay call per active WPML language, each carrying its own code — never the wpml sentinel.');
353 $this->assertNotContains('wpml', $sent);
354 }
355
356 /**
357 * SIDE-EFFECT GUARD (Copilot PR #493): when one language returns reviews for a
358 * page but another comes back empty (transient), the page must NOT advance —
359 * otherwise the empty language loses that page permanently. It retries instead.
360 */
361 public function test_partial_language_failure_retries_without_advancing_page(): void
362 {
363 $bulk = $this->makeBulkAtPage1('CHIJ_PARTIAL');
364
365 // 1 of 2 languages returned this page — transient failure for the other.
366 $bulk->advance_bulk_pagination(1, 2);
367
368 $state = $this->state('CHIJ_PARTIAL');
369 $this->assertSame(1, $state['page'], 'Partial page must NOT advance — the empty language would lose this page.');
370 $this->assertFalse($state['is_done'], 'Partial page must not finish the source.');
371 $this->assertTrue($state['retry'], 'Partial page must schedule a retry.');
372 }
373
374 /**
375 * After one retry, a still-partial page advances (bounded) so a persistently
376 * thin language can't wedge the source forever.
377 */
378 public function test_partial_after_retry_advances_bounded(): void
379 {
380 $bulk = $this->makeBulkAtPage1('CHIJ_PARTIAL2', ['retry' => true]);
381
382 $bulk->advance_bulk_pagination(1, 2);
383
384 $state = $this->state('CHIJ_PARTIAL2');
385 $this->assertSame(2, $state['page'], 'Still-partial after a retry advances to avoid a stuck source.');
386 $this->assertFalse($state['retry'], 'Retry flag cleared after the bounded advance.');
387 }
388
389 /**
390 * When every language returns the page, the source advances normally.
391 */
392 public function test_all_languages_returning_advances_page(): void
393 {
394 $bulk = $this->makeBulkAtPage1('CHIJ_FULLPAGE');
395
396 $bulk->advance_bulk_pagination(2, 2);
397
398 $this->assertSame(2, $this->state('CHIJ_FULLPAGE')['page'], 'A full page in every language advances to the next page.');
399 }
400
401 /**
402 * No language returned reviews — genuine end-of-history: retry once, then done.
403 */
404 public function test_no_reviews_ends_after_one_retry(): void
405 {
406 $bulk = $this->makeBulkAtPage1('CHIJ_EMPTY', ['retry' => true]);
407
408 $bulk->advance_bulk_pagination(0, 2);
409
410 $this->assertTrue($this->state('CHIJ_EMPTY')['is_done'], 'Empty page after a retry ends the source cleanly.');
411 }
412
413 /**
414 * First empty response (retry not yet used) reschedules with retry=true — it
415 * must NOT finish the source on the first empty page.
416 */
417 public function test_no_reviews_first_time_retries_not_done(): void
418 {
419 $bulk = $this->makeBulkAtPage1('CHIJ_EMPTY_FIRST'); // retry defaults to false
420
421 $bulk->advance_bulk_pagination(0, 2);
422
423 $state = $this->state('CHIJ_EMPTY_FIRST');
424 $this->assertTrue($state['retry'], 'First empty page flips retry -> true.');
425 $this->assertFalse($state['is_done'], 'First empty page must not finish the source.');
426 }
427
428 /**
429 * Non-Google providers (e.g. Yelp) never loop languages — bulk_languages() short
430 * -circuits to a single no-language fetch regardless of settings.
431 */
432 public function test_bulk_languages_non_google_is_single_no_language(): void
433 {
434 $bulk = new Bulk_Reviews_Update();
435 $bulk->account_provider = 'yelp';
436 $bulk->settings = ['localization' => 'wpml'];
437
438 $this->assertSame([''], $bulk->bulk_languages());
439 }
440
441 /**
442 * End-to-end BC: a feed fixed to an explicit code forwards exactly that code to
443 * the relay (one call), never the wpml sentinel.
444 */
445 public function test_get_bulk_reviews_forwards_fixed_language(): void
446 {
447 $account_id = 'CHIJ_FIXED';
448 global $wp_options_mock;
449 $wp_options_mock['sbr_bulk_sources'] = [
450 $account_id => ['account_id' => $account_id, 'provider' => 'google', 'retry' => false, 'is_done' => false, 'page' => 1],
451 ];
452
453 $bulk = new Bulk_Reviews_Update();
454 $bulk->account_id = $account_id;
455 $bulk->account_provider = 'google';
456 $bulk->should_make_call();
457 $bulk->endpoint = 'reviews/google';
458 $bulk->settings = ['localization' => 'es-419'];
459 $bulk->provider = ['info' => '{"id":"' . $account_id . '"}'];
460 $relay = $this->makeCapturingRelay();
461 $bulk->relay = $relay;
462
463 $bulk->get_bulk_reviews();
464
465 $sent = array_map(fn($a) => $a['language'] ?? '(none)', $relay->calls);
466 $this->assertSame(['es-419'], $sent, 'A fixed-language feed forwards exactly that code — one call.');
467 }
468
469 /**
470 * @param array<string,mixed> $overrides
471 */
472 private function makeBulkAtPage1(string $account_id, array $overrides = []): Bulk_Reviews_Update
473 {
474 global $wp_options_mock;
475 $wp_options_mock['sbr_bulk_sources'] = [
476 $account_id => array_merge(
477 ['account_id' => $account_id, 'provider' => 'google', 'retry' => false, 'is_done' => false, 'page' => 1],
478 $overrides
479 ),
480 ];
481 $bulk = new Bulk_Reviews_Update();
482 $bulk->account_id = $account_id;
483 $bulk->account_provider = 'google';
484 $bulk->should_make_call();
485 return $bulk;
486 }
487
488 /**
489 * @return array<string,mixed>
490 */
491 private function state(string $account_id): array
492 {
493 global $wp_options_mock;
494 return $wp_options_mock['sbr_bulk_sources'][$account_id] ?? [];
495 }
496
497 /**
498 * Records the args of every relay call and returns empty reviews.
499 *
500 * @return mixed Anonymous-class instance — wider than \stdClass so the typed
501 * Bulk_Reviews_Update::$relay property accepts it under phpstan.
502 */
503 private function makeCapturingRelay()
504 {
505 return new class {
506 /** @var array<int,array<string,mixed>> */
507 public $calls = [];
508 public function call($endpoint, $args, $method, $auth)
509 {
510 $this->calls[] = $args;
511 return ['data' => ['reviews' => []]];
512 }
513 };
514 }
515
516 }
517