diff --git a/src/Abilities/WriteFailed.php b/src/Abilities/WriteFailed.php index 6f59ce8..023710d 100644 --- a/src/Abilities/WriteFailed.php +++ b/src/Abilities/WriteFailed.php @@ -32,7 +32,7 @@ final class WriteFailed extends RuntimeException public readonly ?int $postId, public readonly ?int $revisionId, ?Throwable $previous, - string $detail, + private string $detail, public readonly ?string $state, public readonly ?string $hint, public readonly array $details @@ -82,6 +82,28 @@ final class WriteFailed extends RuntimeException return new self($reason, false, $postId, $revisionId, $cause, $detail, $state, $hint, $details); } + /** + * The same failure with $details merged into its client details; a non-null $hint or $state + * replaces the current one (see rolledBack()/partial()). For callers that learn about + * something kept (e.g. a backup) only after the failure was thrown. + * + * @param array $details + */ + public function withDetails(array $details, ?string $hint = null, ?string $state = null): self + { + return new self( + $this->reason, + $this->rolledBack, + $this->postId, + $this->revisionId, + $this->getPrevious(), + $this->detail, + $state ?? $this->state, + $hint ?? $this->hint, + array_merge($this->details, $details) + ); + } + public function toError(): WP_Error { return $this->rolledBack diff --git a/src/Integrations/Divi/Library/GlobalEdits.php b/src/Integrations/Divi/Library/GlobalEdits.php index 5daa105..20b1739 100644 --- a/src/Integrations/Divi/Library/GlobalEdits.php +++ b/src/Integrations/Divi/Library/GlobalEdits.php @@ -27,7 +27,8 @@ defined('ABSPATH') || exit; * save scope is GlobalItem (site-wide cache) and, when revisions are off for the entry, a backup * copy is made first – the revision itself is DiviSaver's restore point, as for every page. * Confirm stays honour-based (ruling P9, like D10). A confirmed edit is refused while the user may - * not edit_post one of the published pages that show the entry (ruling P8, stricter than Divi). + * not edit_post one of the published, private or scheduled posts that show the entry (rulings + * P8/P8a, stricter than Divi); the dry run names them already (blocked_ids). * The backup (PostCloner, wp_insert_post with kses) is refused when it could not be faithful: for * users without unfiltered_html, and when the trash is disabled (ruling P9, S37). */ @@ -90,6 +91,7 @@ final class GlobalEdits $usages = $this->usages->of($id); $details = ['affected' => $usages['items'], 'affected_count' => $usages['count']]; + $blocked = $this->blocked($id); if (($input['confirm'] ?? false) !== true) { return Errors::confirmRequired( sprintf( @@ -97,13 +99,13 @@ final class GlobalEdits . 'veröffentlichte. Nichts wurde gespeichert (Probelauf).', $id, $usages['count'], - $usages['count'] === 1 ? 'Seite' : 'Seiten' - ), - $details + self::pages($usages['count']) + ) . self::blockedNote($blocked), + $details + self::blockedDetails($blocked) ); } - $refusal = $this->pagesRefusal($id) ?? $this->backupRefusal($item); + $refusal = $this->pagesRefusal($id, $blocked) ?? $this->backupRefusal($item); if ($refusal !== null) { return $refusal; } @@ -125,44 +127,105 @@ final class GlobalEdits return $error; } - $usages = $this->usages->of((int) $page->post->ID); + $id = (int) $page->post->ID; + $usages = $this->usages->of($id); + $blocked = $this->blocked($id); $data = $error->get_error_data(); $details = is_array($data) ? array_diff_key($data, ['status' => true, 'hint' => true]) : []; return Errors::confirmRequired( Errors::clientMessage($error) - . sprintf(' Globaler Bibliothekseintrag: wirkt auf %d Seiten (siehe affected).', $usages['count']), - array_merge($details, ['affected' => $usages['items'], 'affected_count' => $usages['count']]) + . sprintf( + ' Globaler Bibliothekseintrag: wirkt auf %d %s (siehe affected).', + $usages['count'], + self::pages($usages['count']) + ) + . self::blockedNote($blocked), + array_merge( + $details, + ['affected' => $usages['items'], 'affected_count' => $usages['count']], + self::blockedDetails($blocked) + ) ); } - /** Ruling P8: every published page that shows entry $id must be editable by the user. */ - private function pagesRefusal(int $id): ?WP_Error + /** + * Ruling P8a: the live posts (published, private, scheduled – they render now or will) that show + * entry $id and that the user may not edit_post. Drafts and pending posts never block. + * + * @return list + */ + private function blocked(int $id): array { - $blocked = array_values(array_filter( - $this->usages->publishedIds($id), - static fn (int $pageId): bool => ! current_user_can('edit_post', $pageId) + $live = $this->usages->liveIds($id); + if ($live === []) { + return []; + } + + _prime_post_caches($live, false, false); + + return array_values(array_filter( + $live, + static fn (int $postId): bool => ! current_user_can('edit_post', $postId) )); + } + + /** + * Ruling P8a: a confirmed edit needs edit_post on every live post that shows entry $id. + * + * @param list $blocked + */ + private function pagesRefusal(int $id, array $blocked): ?WP_Error + { if ($blocked === []) { return null; } - $listed = implode(', ', array_slice($blocked, 0, self::LISTED_IDS)); - $more = count($blocked) - self::LISTED_IDS; - return Errors::forbidden( sprintf( - 'Globaler Bibliothekseintrag %d erscheint auf veröffentlichten Einträgen, die dieses Konto nicht ' - . 'bearbeiten darf (IDs %s%s); nichts wurde gespeichert.', + 'Globaler Bibliothekseintrag %d erscheint auf veröffentlichten, privaten oder geplanten Einträgen, ' + . 'die dieses Konto nicht bearbeiten darf (IDs %s); nichts wurde gespeichert.', $id, - $listed, - $more > 0 ? sprintf(' und %d weitere', $more) : '' + self::listed($blocked) ), - 'Die Änderung von jemandem vornehmen lassen, der alle diese Seiten bearbeiten darf (z. B. Redakteur ' + 'Die Änderung von jemandem vornehmen lassen, der alle diese Einträge bearbeiten darf (z. B. Redakteur ' . 'oder Administrator).' ); } + /** @param list $blocked */ + private static function blockedNote(array $blocked): string + { + return $blocked === [] ? '' : sprintf( + ' Achtung: Die Bestätigung würde abgelehnt – veröffentlichte, private oder geplante Einträge, die dieses ' + . 'Konto nicht bearbeiten darf, zeigen den Eintrag (IDs %s, siehe blocked_ids).', + self::listed($blocked) + ); + } + + /** + * @param list $blocked + * @return array> + */ + private static function blockedDetails(array $blocked): array + { + return $blocked === [] ? [] : ['blocked_ids' => $blocked]; + } + + /** @param list $ids At most LISTED_IDS of them, then "und N weitere". */ + private static function listed(array $ids): string + { + $more = count($ids) - self::LISTED_IDS; + + return implode(', ', array_slice($ids, 0, self::LISTED_IDS)) + . ($more > 0 ? sprintf(' und %d weitere', $more) : ''); + } + + private static function pages(int $count): string + { + return $count === 1 ? 'Seite' : 'Seiten'; + } + /** Ruling P9: a backup (only made when revisions are off) must be faithful and must survive. */ private function backupRefusal(WP_Post $item): ?WP_Error { diff --git a/src/Integrations/Divi/Library/GlobalUsages.php b/src/Integrations/Divi/Library/GlobalUsages.php index 810fbe8..12342ff 100644 --- a/src/Integrations/Divi/Library/GlobalUsages.php +++ b/src/Integrations/Divi/Library/GlobalUsages.php @@ -15,8 +15,11 @@ class GlobalUsages { public const LIMIT = 50; - private const WHERE = "post_content LIKE %s AND post_type <> 'revision' " - . "AND post_status NOT IN ('trash', 'auto-draft', 'inherit')"; + /** Every post that exists for the user: listed in the dry run. */ + private const LISTED = "post_status NOT IN ('trash', 'auto-draft', 'inherit')"; + + /** Posts that render now or will on their own: published, private, scheduled (ruling P8a). */ + private const LIVE = "post_status IN ('publish', 'private', 'future')"; public function __construct(private ?\wpdb $db = null) { @@ -29,12 +32,12 @@ class GlobalUsages $like = self::like($db, $libraryId); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery $count = (int) $db->get_var( - $db->prepare('SELECT COUNT(*) FROM ' . $db->posts . ' WHERE ' . self::WHERE, $like) + $db->prepare('SELECT COUNT(*) FROM ' . $db->posts . ' WHERE ' . self::where(self::LISTED), $like) ); $rows = $db->get_results( $db->prepare( - 'SELECT ID, post_title, post_status, post_type FROM ' . $db->posts . ' WHERE ' . self::WHERE - . ' ORDER BY ID ASC LIMIT %d', + 'SELECT ID, post_title, post_status, post_type FROM ' . $db->posts . ' WHERE ' + . self::where(self::LISTED) . ' ORDER BY ID ASC LIMIT %d', $like, self::LIMIT ), @@ -57,18 +60,17 @@ class GlobalUsages } /** - * IDs of every published post that shows $libraryId – no limit (ruling P8 checks edit_post on - * each of them before a confirmed global edit). + * IDs of every live post (published, private, scheduled) that shows $libraryId – no limit + * (ruling P8a checks edit_post on each of them before a confirmed global edit). * * @return list */ - public function publishedIds(int $libraryId): array + public function liveIds(int $libraryId): array { $db = $this->db(); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery $ids = $db->get_col($db->prepare( - 'SELECT ID FROM ' . $db->posts . " WHERE post_content LIKE %s AND post_type <> 'revision' " - . "AND post_status = 'publish' ORDER BY ID ASC", + 'SELECT ID FROM ' . $db->posts . ' WHERE ' . self::where(self::LIVE) . ' ORDER BY ID ASC', self::like($db, $libraryId) )); // phpcs:enable @@ -76,13 +78,22 @@ class GlobalUsages return array_values(array_map('intval', is_array($ids) ? $ids : [])); } - private function db(): \wpdb + /** + * The one condition of both queries: posts (no revisions) whose content holds an instance + * (placeholder %s = like()), with the fixed status clause $status (a class constant). + */ + private static function where(string $status): string { - return $this->db ?? $GLOBALS['wpdb']; + return "post_content LIKE %s AND post_type <> 'revision' AND " . $status; } private static function like(\wpdb $db, int $libraryId): string { return '%' . $db->esc_like('"globalModule":"' . $libraryId . '"') . '%'; } + + private function db(): \wpdb + { + return $this->db ?? $GLOBALS['wpdb']; + } } diff --git a/src/Integrations/Divi/Pages/DiviWriter.php b/src/Integrations/Divi/Pages/DiviWriter.php index 5c0fda0..eeeecfa 100644 --- a/src/Integrations/Divi/Pages/DiviWriter.php +++ b/src/Integrations/Divi/Pages/DiviWriter.php @@ -179,7 +179,43 @@ final class DiviWriter return $prepared; } - return array_merge($this->save($ability, $page, $change, $input, $prepared['scope']), $prepared['details']); + try { + $saved = $this->save($ability, $page, $change, $input, $prepared['scope']); + } catch (WriteFailed $e) { + throw self::keepingBackup($e, $prepared['details']['backup_id'] ?? null); + } + + return array_merge($saved, $prepared['details']); + } + + /** + * Ruling P14: GlobalEdits made a backup (revisions off) before the save failed – the error names + * it, because it stays in the trash and is the only earlier state. + */ + private static function keepingBackup(WriteFailed $e, mixed $backupId): WriteFailed + { + if (! is_int($backupId)) { + return $e; + } + + $kept = sprintf( + 'Sicherung %d liegt im Papierkorb (vorheriger Stand des Bibliothekseintrags, in WordPress ' + . 'wiederherstellbar).', + $backupId + ); + if ($e->rolledBack) { + return $e->withDetails( + ['backup_id' => $backupId], + $kept . ' Den Aufruf erst nach Prüfung mit lit-divi-outline wiederholen.', + Errors::notSavedKeeping(sprintf('Sicherung %d im Papierkorb', $backupId)) + ); + } + + return $e->withDetails( + ['backup_id' => $backupId], + 'Den Aufruf nicht einfach wiederholen. ' . $kept . ' Zuerst den Stand mit lit-divi-outline prüfen und ' + . 'dem Nutzer Bescheid geben.' + ); } /** A tool's confirm_required on a global library entry, extended by the affected pages. */ diff --git a/tests/Unit/Abilities/WriteFailedTest.php b/tests/Unit/Abilities/WriteFailedTest.php index 3d14b42..cfffae5 100644 --- a/tests/Unit/Abilities/WriteFailedTest.php +++ b/tests/Unit/Abilities/WriteFailedTest.php @@ -96,4 +96,29 @@ final class WriteFailedTest extends TestCase self::assertSame([7, 811], [$partial->postId, $partial->revisionId]); self::assertArrayNotHasKey('post_id', $clean->toError()->get_error_data()); } + + /** Ruling P14: details (and hint/state) added after the fact, everything else kept. */ + public function testWithDetailsAddsDetailsAndKeepsTheRest(): void + { + $cause = new RuntimeException('hook'); + $failure = WriteFailed::rolledBack('x', $cause, 'intern', details: ['a' => 1], postId: 5, revisionId: 811); + + $extended = $failure->withDetails(['backup_id' => 9], 'Sicherung 9 liegt im Papierkorb.', 'Bleibt.'); + + self::assertNotSame($failure, $extended); + self::assertTrue($extended->rolledBack); + self::assertSame($cause, $extended->getPrevious()); + self::assertSame($failure->getMessage(), $extended->getMessage()); + self::assertSame([5, 811], [$extended->postId, $extended->revisionId]); + $data = $extended->toError()->get_error_data(); + self::assertSame(1, $data['a']); + self::assertSame(9, $data['backup_id']); + self::assertSame('Sicherung 9 liegt im Papierkorb.', $data['hint']); + self::assertStringContainsString('Bleibt.', $extended->toError()->get_error_message()); + + $partial = WriteFailed::partial('x', postId: 5)->withDetails(['backup_id' => 9]); + self::assertFalse($partial->rolledBack); + self::assertSame(9, $partial->toError()->get_error_data()['backup_id']); + self::assertSame(5, $partial->toError()->get_error_data()['post_id']); + } } diff --git a/tests/Unit/Integrations/Divi/Library/GlobalEditsTest.php b/tests/Unit/Integrations/Divi/Library/GlobalEditsTest.php index b158ee8..cf171d2 100644 --- a/tests/Unit/Integrations/Divi/Library/GlobalEditsTest.php +++ b/tests/Unit/Integrations/Divi/Library/GlobalEditsTest.php @@ -6,6 +6,7 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Library; use Brain\Monkey\Functions; use LIT\MCP\Abilities\Rev; +use LIT\MCP\Abilities\WriteFailed; use LIT\MCP\Integrations\Divi\Library\GlobalEdits; use LIT\MCP\Integrations\Divi\Library\GlobalUsages; use LIT\MCP\Integrations\Divi\Pages\DiviDeleteAbility; @@ -17,6 +18,7 @@ use LIT\MCP\Integrations\Divi\Pages\DiviWriter; use LIT\MCP\Integrations\Divi\Pages\SaveScope; use LIT\MCP\Tests\Support\DiviFixtures; use LIT\MCP\Tests\Support\DiviWriterFixture; +use LIT\MCP\Tests\Support\FakeChangeLog; use LIT\MCP\Tests\Support\LibraryFixtures; use LIT\MCP\Tests\Unit\TestCase; use Mockery; @@ -30,8 +32,14 @@ final class GlobalEditsTest extends TestCase private int $backups = 0; - /** @var list Published pages that show the global entry (ruling P8). */ - private array $published = [1053]; + /** Change log of the writer; null = DiviWriterFixture::log() (records only). */ + private ?FakeChangeLog $log = null; + + /** Thrown by the saver instead of saving, if set. */ + private ?\Throwable $failure = null; + + /** @var list Live (published, private, scheduled) posts that show the global entry (ruling P8a). */ + private array $live = [1053]; private bool $trash = true; @@ -39,6 +47,7 @@ final class GlobalEditsTest extends TestCase { parent::setUp(); Functions\when('wp_revisions_enabled')->justReturn(true); + Functions\when('_prime_post_caches')->justReturn(null); } private function usages(): GlobalUsages @@ -51,7 +60,7 @@ final class GlobalEditsTest extends TestCase ], 'count' => 2, ]); - $usages->allows('publishedIds')->andReturnUsing(fn (): array => $this->published); + $usages->allows('liveIds')->andReturnUsing(fn (): array => $this->live); return $usages; } @@ -61,6 +70,9 @@ final class GlobalEditsTest extends TestCase { $saver = Mockery::mock(DiviSaver::class); $saver->allows('save')->andReturnUsing(function (WP_Post $post, string $content, SaveScope $scope): array { + if ($this->failure !== null) { + throw $this->failure; + } $this->saved[] = [$content, $scope]; $post->post_content = $content; @@ -82,7 +94,7 @@ final class GlobalEditsTest extends TestCase return new DiviWriter( LibraryFixtures::loader($access), $saver, - DiviWriterFixture::log(), + $this->log ?? DiviWriterFixture::log(), $design, DiviWriterFixture::composition($design), $globals @@ -209,20 +221,138 @@ final class GlobalEditsTest extends TestCase public function testConfirmIsRefusedWhenAPublishedPageShowingTheEntryIsNotEditable(): void { LibraryFixtures::given([LibraryFixtures::item(1059)]); - $this->published = [1053, 1080]; + $this->live = [1053, 1080]; self::denyOn('edit_post', [1080]); $dryRun = (new DiviUpdateAbility($this->writer()))->execute(self::relabel(1059)); $result = (new DiviUpdateAbility($this->writer()))->execute(self::relabel(1059, true)); - self::assertToolError($dryRun, 'confirm_required', 'the dry run stays unchanged'); + self::assertToolError($dryRun, 'confirm_required', 'the dry run still lists the pages'); self::assertToolError($result, 'forbidden'); self::assertStringContainsString('1080', $result->get_error_message()); + self::assertStringContainsString('veröffentlichten, privaten oder geplanten', $result->get_error_message()); self::assertStringNotContainsString('1053', $result->get_error_message()); self::assertSame([], $this->saved); self::assertSame(0, $this->backups); } + /** + * Ruling P8a: private and scheduled posts render (or will) – each blocks when the user may not + * edit it; drafts are listed in the dry run but never block (they are not among liveIds()). + */ + public function testPrivateAndScheduledPagesBlockButDraftsDoNot(): void + { + LibraryFixtures::given([LibraryFixtures::item(1059)]); + foreach ([1081, 1082] as $private) { + $this->live = [1053, $private]; + self::denyOn('edit_post', [$private, 1070]); + + $result = (new DiviUpdateAbility($this->writer()))->execute(self::relabel(1059, true)); + + self::assertToolError($result, 'forbidden'); + self::assertStringContainsString((string) $private, $result->get_error_message()); + } + self::assertSame([], $this->saved); + + $this->live = [1053]; + $result = (new DiviUpdateAbility($this->writer()))->execute(self::relabel(1059, true)); + self::assertTrue($result['ok'], 'draft 1070 (not editable) does not block'); + } + + /** Ruling P14 (M-1): the dry run already says which posts would block the confirm. */ + public function testTheDryRunNamesThePostsThatWouldBlock(): void + { + LibraryFixtures::given([LibraryFixtures::item(1059)]); + $this->live = [1053, 1081]; + self::denyOn('edit_post', [1081]); + + $dryRun = (new DiviUpdateAbility($this->writer()))->execute(self::relabel(1059)); + + self::assertToolError($dryRun, 'confirm_required'); + self::assertSame([1081], $dryRun->get_error_data()['blocked_ids']); + self::assertStringContainsString('1081', $dryRun->get_error_message()); + self::assertStringContainsString('abgelehnt', $dryRun->get_error_message()); + } + + public function testADryRunWithoutBlockersHasNoBlockedIds(): void + { + LibraryFixtures::given([LibraryFixtures::item(1059)]); + + $dryRun = (new DiviUpdateAbility($this->writer()))->execute(self::relabel(1059)); + + self::assertArrayNotHasKey('blocked_ids', $dryRun->get_error_data()); + } + + /** Ruling P14 (I-2): a failed save after the backup names it. */ + public function testAFailedSaveAfterTheBackupNamesIt(): void + { + Functions\when('wp_revisions_enabled')->justReturn(false); + LibraryFixtures::given([LibraryFixtures::item(1059)]); + $this->failure = WriteFailed::rolledBack(DiviSaver::REASON, new \RuntimeException('hook'), postId: 1059); + + try { + (new DiviUpdateAbility($this->writer()))->execute(self::relabel(1059, true)); + self::fail('WriteFailed expected'); + } catch (WriteFailed $e) { + $error = $e->toError(); + } + + self::assertTrue($e->rolledBack); + self::assertSame(1071, $error->get_error_data()['backup_id']); + self::assertStringContainsString('Sicherung 1071 liegt im Papierkorb', $error->get_error_data()['hint']); + self::assertStringNotContainsString('Nichts wurde gespeichert', $error->get_error_message()); + self::assertSame(1, $this->backups); + } + + public function testAPartialSaveAfterTheBackupNamesIt(): void + { + Functions\when('wp_revisions_enabled')->justReturn(false); + LibraryFixtures::given([LibraryFixtures::item(1059)]); + $this->failure = WriteFailed::partial(DiviSaver::REASON, new \RuntimeException('hook'), postId: 1059); + $this->log = FakeChangeLog::partialsOnly(); + + try { + (new DiviUpdateAbility($this->writer()))->execute(self::relabel(1059, true)); + self::fail('WriteFailed expected'); + } catch (WriteFailed $e) { + $error = $e->toError(); + } + + self::assertToolError($error, 'partial_write'); + self::assertSame(1071, $error->get_error_data()['backup_id']); + self::assertSame(1059, $error->get_error_data()['post_id']); + self::assertStringContainsString('Sicherung 1071 liegt im Papierkorb', $error->get_error_data()['hint']); + self::assertCount(1, $this->log->partials); + } + + /** Ruling P14 (M-2): one page is "Seite". */ + public function testDeleteNoticeUsesTheSingularForOnePage(): void + { + LibraryFixtures::given([LibraryFixtures::item(1058)]); + $usages = Mockery::mock(GlobalUsages::class); + $usages->allows('of')->andReturn([ + 'items' => [['post_id' => 1053, 'title' => 'Globale Instanzen', 'status' => 'publish', 'type' => 'page']], + 'count' => 1, + ]); + $usages->allows('liveIds')->andReturn([1053]); + $access = LibraryFixtures::access(); + $design = DiviWriterFixture::designReader(); + $writer = new DiviWriter( + LibraryFixtures::loader($access), + Mockery::mock(DiviSaver::class), + DiviWriterFixture::log(), + $design, + DiviWriterFixture::composition($design), + new GlobalEdits($access, $usages) + ); + + $result = (new DiviDeleteAbility($writer))->execute( + ['post_id' => 1058, 'tree_rev' => self::rev(1058), 'ids' => ['s0.r0.c0.m1']] + ); + + self::assertStringContainsString('wirkt auf 1 Seite (siehe affected)', $result->get_error_message()); + } + public function testRefusesToEmptyOrDoubleTheRoot(): void { LibraryFixtures::given([LibraryFixtures::item(1058)]); diff --git a/tests/Unit/Integrations/Divi/Library/GlobalUsagesTest.php b/tests/Unit/Integrations/Divi/Library/GlobalUsagesTest.php index 7cd5d9f..0436fb8 100644 --- a/tests/Unit/Integrations/Divi/Library/GlobalUsagesTest.php +++ b/tests/Unit/Integrations/Divi/Library/GlobalUsagesTest.php @@ -45,8 +45,8 @@ final class GlobalUsagesTest extends TestCase self::assertStringContainsString('LIMIT 50', $queries[1]); } - /** Ruling P8: every published page showing the entry, beyond the listing limit. */ - public function testPublishedIdsAreUnlimited(): void + /** Ruling P8a: every live (published, private, scheduled) post showing the entry, beyond the listing limit. */ + public function testLiveIdsAreUnlimited(): void { $db = Mockery::mock(\wpdb::class)->makePartial(); $queries = []; @@ -56,9 +56,9 @@ final class GlobalUsagesTest extends TestCase return ['1053', '1080']; }); - self::assertSame([1053, 1080], (new GlobalUsages($db))->publishedIds(1059)); + self::assertSame([1053, 1080], (new GlobalUsages($db))->liveIds(1059)); self::assertStringContainsString("LIKE '%\"globalModule\":\"1059\"%'", $queries[0]); - self::assertStringContainsString("post_status = 'publish'", $queries[0]); + self::assertStringContainsString("post_status IN ('publish', 'private', 'future')", $queries[0]); self::assertStringContainsString("post_type <> 'revision'", $queries[0]); self::assertStringNotContainsString('LIMIT', $queries[0]); }