From 72eeaa46adbb35e71ff92994392c61ef0e9f544b Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Sat, 26 Sep 2026 17:53:54 +0200 Subject: [PATCH] test: FakeChangeLog and SchemaFixture instead of repeated mocks (T2) - tests/Support/FakeChangeLog extends ChangeLog: records record()/recordPartial() calls, increasing ids, optional failure; new FakeChangeLog(id) / partialsOnly() / any(id) / forbidden() mirror what the replaced mocks allowed. assertRecorded() / assertPartials() check count and the named arguments. Calls a fake does not allow throw and are also verified in TestCase::tearDown (one assertion per test that uses a fake), so swallowed change-log calls still fail like a never() expectation. - All ChangeLog mocks of ability/integration tests replaced (the admin tests that stub query/purge/clear keep their mocks); expectException tests that checked the partial log now catch and assert. - SchemaFixture::managed(?divi) replaces the 21 isManaged-only ContentSchema mocks; DiviFixtures::schema() uses it. Tests 1397 -> 1404 (FakeChangeLogTest); no test has fewer assertions. Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/Support/DiviFixtures.php | 7 +- tests/Support/DiviWriterFixture.php | 10 +- tests/Support/FakeChangeLog.php | 181 ++++++++++++++++++ tests/Support/SchemaFixture.php | 30 +++ .../Integrations/Divi/DiviIntegrationTest.php | 6 +- .../Divi/Pages/DiviWriterTest.php | 52 +++-- .../Media/MediaDeleteAbilityTest.php | 45 +++-- .../Media/MediaIntegrationTest.php | 4 +- .../Media/MediaUpdateAbilityTest.php | 34 ++-- .../Media/MediaUploadAbilitiesTest.php | 39 ++-- .../Content/ContentCreateAbilityTest.php | 49 ++--- .../Content/ContentCreateDiviTest.php | 10 +- .../Content/ContentGetAbilityTest.php | 7 +- .../Content/ContentQueryAbilityTest.php | 4 +- .../Content/ContentTypesAbilityTest.php | 10 +- .../Content/ContentUpdateAbilityTest.php | 38 ++-- .../WordPress/Content/NewDraftTest.php | 15 +- .../Menus/MenuAssignLocationAbilityTest.php | 48 ++--- .../WordPress/Menus/MenuCreateAbilityTest.php | 20 +- .../Menus/MenuSetItemsAbilityTest.php | 56 +++--- .../WordPress/PostChangeResultTest.php | 19 +- .../Render/PageRenderSummaryAbilityTest.php | 5 +- .../Revisions/RevisionRestoreAbilityTest.php | 35 ++-- .../Revisions/RevisionsListAbilityTest.php | 6 +- .../Settings/WpSettingsAbilitiesTest.php | 42 ++-- .../Status/PostDuplicateAbilityTest.php | 22 ++- .../Status/PostPublishAbilityTest.php | 30 +-- .../Status/PostReplaceWithCopyAbilityTest.php | 52 ++--- .../Status/PostScheduleAbilityTest.php | 27 ++- .../WordPress/Status/PostTrashAbilityTest.php | 38 ++-- .../Status/PostUnpublishAbilityTest.php | 19 +- .../WordPress/WordPressIntegrationTest.php | 12 +- tests/Unit/Support/FakeChangeLogTest.php | 104 ++++++++++ tests/Unit/TestCase.php | 2 + 34 files changed, 725 insertions(+), 353 deletions(-) create mode 100644 tests/Support/FakeChangeLog.php create mode 100644 tests/Support/SchemaFixture.php create mode 100644 tests/Unit/Support/FakeChangeLogTest.php diff --git a/tests/Support/DiviFixtures.php b/tests/Support/DiviFixtures.php index 78f3d9e..f0ec80e 100644 --- a/tests/Support/DiviFixtures.php +++ b/tests/Support/DiviFixtures.php @@ -8,7 +8,6 @@ use Brain\Monkey\Functions; use LIT\MCP\Integrations\Divi\Design\DesignSystem; use LIT\MCP\Integrations\Divi\Schema\ModuleCatalog; use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; -use Mockery; use WP_Post; /** @@ -41,11 +40,7 @@ final class DiviFixtures public static function schema(bool $diviEnabled = true): ContentSchema { - $schema = Mockery::mock(ContentSchema::class); - $schema->allows('isManaged')->andReturn(true); - $schema->allows('diviEnabled')->andReturn($diviEnabled); - - return $schema; + return SchemaFixture::managed($diviEnabled); } /** Shapes as GlobalData / GlobalPreset return them (Divi 5.2.1, verified in DDEV). */ diff --git a/tests/Support/DiviWriterFixture.php b/tests/Support/DiviWriterFixture.php index 93292e1..e37e2ec 100644 --- a/tests/Support/DiviWriterFixture.php +++ b/tests/Support/DiviWriterFixture.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace LIT\MCP\Tests\Support; -use LIT\MCP\ChangeLog\ChangeLog; use LIT\MCP\Integrations\Divi\Design\DesignSystemReader; use LIT\MCP\Integrations\Divi\Pages\DiviComposition; use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader; @@ -83,13 +82,10 @@ final class DiviWriterFixture return $saver; } - /** A ChangeLog that accepts every record (log id 4711). */ - public static function log(): ChangeLog + /** A ChangeLog that accepts records (log ids from 4711). */ + public static function log(): FakeChangeLog { - $log = Mockery::mock(ChangeLog::class); - $log->allows('record')->andReturn(4711); - - return $log; + return new FakeChangeLog(4711); } /** diff --git a/tests/Support/FakeChangeLog.php b/tests/Support/FakeChangeLog.php new file mode 100644 index 0000000..c832ed3 --- /dev/null +++ b/tests/Support/FakeChangeLog.php @@ -0,0 +1,181 @@ +andReturn(id)) + * - FakeChangeLog::partialsOnly() recordPartial() only (expects('recordPartial')) + * - FakeChangeLog::any(id) both + * - FakeChangeLog::forbidden() nothing (a mock without expectations) + * A call that is not allowed throws – and, because the code under test often swallows + * change-log failures (FollowUp), it is also noted and fails the test in TestCase::tearDown() + * via verify(). + */ +class FakeChangeLog extends ChangeLog +{ + /** + * @var list + */ + public array $records = []; + + /** + * @var list + */ + public array $partials = []; + + /** @var list Calls that were not allowed, across all fakes of the running test. */ + private static array $unexpected = []; + + private static bool $used = false; + + public function __construct( + private int $firstId = 1, + private ?Throwable $failure = null, + private bool $allowsRecord = true, + private bool $allowsPartial = false + ) { + parent::__construct(); + self::$used = true; + } + + public static function forbidden(): self + { + return new self(1, null, false, false); + } + + public static function partialsOnly(): self + { + return new self(1, null, false, true); + } + + public static function any(int $firstId = 1): self + { + return new self($firstId, null, true, true); + } + + /** + * Fails the running test if any fake got a call it does not allow; resets for the next test. + * Counts as one assertion in tests that created a fake (like a Mockery never() expectation). + */ + public static function verify(): void + { + $unexpected = self::$unexpected; + $used = self::$used; + self::$unexpected = []; + self::$used = false; + if ($used || $unexpected !== []) { + Assert::assertSame([], $unexpected, 'Unexpected change log calls: ' . implode('; ', $unexpected)); + } + } + + public function record( + string $ability, + string $objectType, + string $objectId, + string $summary, + ?int $revisionId = null, + ?string $snapshotId = null, + string $result = 'ok' + ): ?int { + $this->refuseUnless($this->allowsRecord, sprintf('record(%s, %s, %s)', $ability, $objectType, $objectId)); + $this->records[] = [ + 'ability' => $ability, + 'object_type' => $objectType, + 'object_id' => $objectId, + 'summary' => $summary, + 'revision_id' => $revisionId, + 'snapshot_id' => $snapshotId, + 'result' => $result, + ]; + if ($this->failure !== null) { + throw $this->failure; + } + + return $this->firstId + count($this->records) - 1; + } + + public function recordPartial( + string $ability, + string $objectType, + string $objectId, + string $summary, + ?int $revisionId = null + ): void { + $this->refuseUnless( + $this->allowsPartial, + sprintf('recordPartial(%s, %s, %s)', $ability, $objectType, $objectId) + ); + $this->partials[] = [ + 'ability' => $ability, + 'object_type' => $objectType, + 'object_id' => $objectId, + 'summary' => $summary, + 'revision_id' => $revisionId, + ]; + } + + /** + * Asserts the record() calls: one $expected per call, in order; each names only the + * arguments it checks (ability, object_type, object_id, summary, revision_id, …). + * + * @param array ...$expected + */ + public function assertRecorded(array ...$expected): void + { + self::assertCalls('record()', $expected, $this->records); + } + + /** + * Asserts the recordPartial() calls like assertRecorded(). + * + * @param array ...$expected + */ + public function assertPartials(array ...$expected): void + { + self::assertCalls('recordPartial()', $expected, $this->partials); + } + + /** + * @param list> $expected + * @param list> $actual + */ + private static function assertCalls(string $method, array $expected, array $actual): void + { + Assert::assertCount(count($expected), $actual, 'Number of ChangeLog::' . $method . ' calls'); + foreach ($expected as $i => $call) { + $checked = []; + foreach (array_keys($call) as $key) { + Assert::assertArrayHasKey($key, $actual[$i], 'Unknown ChangeLog argument'); + $checked[$key] = $actual[$i][$key]; + } + Assert::assertSame($call, $checked, 'ChangeLog::' . $method . ' call ' . $i); + } + } + + /** @return list The summaries of record(), in order. */ + public function summaries(): array + { + return array_column($this->records, 'summary'); + } + + private function refuseUnless(bool $allowed, string $call): void + { + if (! $allowed) { + self::$unexpected[] = $call; + throw new LogicException('Unexpected ChangeLog::' . $call); + } + } +} diff --git a/tests/Support/SchemaFixture.php b/tests/Support/SchemaFixture.php new file mode 100644 index 0000000..c5eaebd --- /dev/null +++ b/tests/Support/SchemaFixture.php @@ -0,0 +1,30 @@ +allows('isManaged')->andReturn(true); + if ($divi !== null) { + $schema->allows('diviEnabled')->andReturn($divi); + } + + return $schema; + } +} diff --git a/tests/Unit/Integrations/Divi/DiviIntegrationTest.php b/tests/Unit/Integrations/Divi/DiviIntegrationTest.php index 004e343..97bdb75 100644 --- a/tests/Unit/Integrations/Divi/DiviIntegrationTest.php +++ b/tests/Unit/Integrations/Divi/DiviIntegrationTest.php @@ -5,10 +5,10 @@ declare(strict_types=1); namespace LIT\MCP\Tests\Unit\Integrations\Divi; use LIT\MCP\Abilities\Ability; -use LIT\MCP\ChangeLog\ChangeLog; use LIT\MCP\Integrations\Divi\DiviIntegration; use LIT\MCP\Integrations\Divi\Pages\DiviContentComposer; use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; +use LIT\MCP\Tests\Support\FakeChangeLog; use LIT\MCP\Tests\Unit\TestCase; use Mockery; @@ -30,7 +30,7 @@ final class DiviIntegrationTest extends TestCase public function testProvidesTheDiviAbilitiesOnlyWithDivi5(): void { - $integration = new DiviIntegration(Mockery::mock(ContentSchema::class), Mockery::mock(ChangeLog::class)); + $integration = new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden()); self::assertSame('divi', $integration->id()); self::assertFalse($integration->isAvailable(), 'no Divi classes in unit tests'); @@ -48,7 +48,7 @@ final class DiviIntegrationTest extends TestCase public function testGivesContentCreateTheDiviShortForm(): void { - $integration = new DiviIntegration(Mockery::mock(ContentSchema::class), Mockery::mock(ChangeLog::class)); + $integration = new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden()); $composer = $integration->contentComposer(); diff --git a/tests/Unit/Integrations/Divi/Pages/DiviWriterTest.php b/tests/Unit/Integrations/Divi/Pages/DiviWriterTest.php index 875fc8b..28b338c 100644 --- a/tests/Unit/Integrations/Divi/Pages/DiviWriterTest.php +++ b/tests/Unit/Integrations/Divi/Pages/DiviWriterTest.php @@ -19,6 +19,7 @@ use LIT\MCP\Integrations\Divi\Pages\DiviWriter; use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap; use LIT\MCP\Tests\Support\DiviFixtures; use LIT\MCP\Tests\Support\DiviWriterFixture; +use LIT\MCP\Tests\Support\FakeChangeLog; use LIT\MCP\Tests\Unit\TestCase; use Mockery; use WP_Post; @@ -50,7 +51,7 @@ final class DiviWriterTest extends TestCase { $content = DiviFixtures::markup('synthetic-basic.html'); DiviFixtures::givenPost($content); - $writer = $this->writer(Mockery::mock(DiviSaver::class), Mockery::mock(ChangeLog::class)); + $writer = $this->writer(Mockery::mock(DiviSaver::class), FakeChangeLog::forbidden()); self::assertInstanceOf(DiviPage::class, $writer->page(['post_id' => 7, 'tree_rev' => Rev::of($content)])); self::assertSame('lit_rev_mismatch', $writer->page(['post_id' => 7])->get_error_code()); @@ -66,7 +67,7 @@ final class DiviWriterTest extends TestCase $writer = new DiviWriter( new DiviPageLoader(DiviFixtures::schema(false)), Mockery::mock(DiviSaver::class), - Mockery::mock(ChangeLog::class), + FakeChangeLog::forbidden(), Mockery::mock(DesignSystemReader::class), Mockery::mock(DiviComposition::class) ); @@ -87,7 +88,7 @@ final class DiviWriterTest extends TestCase { $content = DiviFixtures::markup('synthetic-basic.html'); DiviFixtures::givenPost($content); - $writer = $this->writer(Mockery::mock(DiviSaver::class), Mockery::mock(ChangeLog::class)); + $writer = $this->writer(Mockery::mock(DiviSaver::class), FakeChangeLog::forbidden()); $page = $writer->page(['post_id' => 7, 'tree_rev' => Rev::of($content)]); $writer->composition($page)->placeholders(); @@ -101,7 +102,7 @@ final class DiviWriterTest extends TestCase DiviFixtures::givenPost($content); $saver = Mockery::mock(DiviSaver::class); $saver->expects('save')->never(); - $writer = $this->writer($saver, Mockery::mock(ChangeLog::class)); + $writer = $this->writer($saver, FakeChangeLog::forbidden()); $page = $writer->page(['post_id' => 7, 'tree_rev' => Rev::of($content)]); $result = $writer->save('lit/divi-update', $page, new DiviChange('nichts'), []); @@ -122,9 +123,7 @@ final class DiviWriterTest extends TestCase return ['revision_id' => 811, 'warnings' => ['vom Speichern']]; }); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->with('lit/divi-update', 'page', '7', 'Divi "Start": Button geändert', 811) - ->andReturn(55); + $log = new FakeChangeLog(55); $input = ['post_id' => 7, 'tree_rev' => Rev::of($content)]; Functions\expect('do_action')->once()->with('lit_mcp_after_save', 7, 'lit/divi-update', $input); Functions\when('get_preview_post_link')->justReturn('https://kunde.test/?page_id=7&preview=true'); @@ -158,6 +157,13 @@ final class DiviWriterTest extends TestCase self::assertContains('eigene Warnung', $result['warnings']); self::assertContains('vom Speichern', $result['warnings']); self::assertSame(Rev::ofPost(DiviFixtures::givenPost($stored)), $result['rev']); + $log->assertRecorded([ + 'ability' => 'lit/divi-update', + 'object_type' => 'page', + 'object_id' => '7', + 'summary' => 'Divi "Start": Button geändert', + 'revision_id' => 811, + ]); } public function testFailedRollbackIsLoggedAsPartial(): void @@ -166,14 +172,24 @@ final class DiviWriterTest extends TestCase DiviFixtures::givenPost($content); $saver = Mockery::mock(DiviSaver::class); $saver->allows('save')->andThrow(WriteFailed::partial(DiviSaver::REASON, postId: 7, revisionId: 811)); - $log = Mockery::mock(ChangeLog::class); - $log->expects('recordPartial')->with('lit/divi-delete', 'page', '7', 'Divi "Start": Änderung nur teilweise zurückgenommen', 811); + $log = FakeChangeLog::partialsOnly(); $writer = $this->writer($saver, $log); $page = $writer->page(['post_id' => 7, 'tree_rev' => Rev::of($content)]); $page->document->children()[0]->removeChild($page->index->find('s1')); - $this->expectException(WriteFailed::class); - $writer->save('lit/divi-delete', $page, new DiviChange('s1 gelöscht'), []); + try { + $writer->save('lit/divi-delete', $page, new DiviChange('s1 gelöscht'), []); + self::fail('WriteFailed expected'); + } catch (WriteFailed $e) { + self::assertFalse($e->rolledBack); + } + $log->assertPartials([ + 'ability' => 'lit/divi-delete', + 'object_type' => 'page', + 'object_id' => '7', + 'summary' => 'Divi "Start": Änderung nur teilweise zurückgenommen', + 'revision_id' => 811, + ]); } public function testCleanFailureIsNotLogged(): void @@ -182,14 +198,18 @@ final class DiviWriterTest extends TestCase DiviFixtures::givenPost($content); $saver = Mockery::mock(DiviSaver::class); $saver->allows('save')->andThrow(WriteFailed::rolledBack(DiviSaver::REASON, postId: 7, revisionId: 811)); - $log = Mockery::mock(ChangeLog::class); - $log->expects('recordPartial')->never(); - $log->expects('record')->never(); + $log = FakeChangeLog::forbidden(); $writer = $this->writer($saver, $log); $page = $writer->page(['post_id' => 7, 'tree_rev' => Rev::of($content)]); $page->document->children()[0]->removeChild($page->index->find('s1')); - $this->expectException(WriteFailed::class); - $writer->save('lit/divi-delete', $page, new DiviChange('s1 gelöscht'), []); + try { + $writer->save('lit/divi-delete', $page, new DiviChange('s1 gelöscht'), []); + self::fail('WriteFailed expected'); + } catch (WriteFailed $e) { + self::assertTrue($e->rolledBack); + } + $log->assertRecorded(); + $log->assertPartials(); } } diff --git a/tests/Unit/Integrations/Media/MediaDeleteAbilityTest.php b/tests/Unit/Integrations/Media/MediaDeleteAbilityTest.php index 42eb80c..8917ae3 100644 --- a/tests/Unit/Integrations/Media/MediaDeleteAbilityTest.php +++ b/tests/Unit/Integrations/Media/MediaDeleteAbilityTest.php @@ -8,6 +8,7 @@ use Brain\Monkey\Functions; use LIT\MCP\ChangeLog\ChangeLog; use LIT\MCP\Integrations\Media\MediaDeleteAbility; use LIT\MCP\Integrations\Media\MediaUsageFinder; +use LIT\MCP\Tests\Support\FakeChangeLog; use LIT\MCP\Tests\Unit\TestCase; use Mockery; use RuntimeException; @@ -38,7 +39,7 @@ final class MediaDeleteAbilityTest extends TestCase { Functions\expect('wp_delete_attachment')->never(); - $result = $this->ability(self::USAGE, Mockery::mock(ChangeLog::class))->execute(['id' => 33]); + $result = $this->ability(self::USAGE, FakeChangeLog::forbidden())->execute(['id' => 33]); self::assertSame('lit_confirm_required', $result->get_error_code()); self::assertStringContainsString('endgültig', $result->get_error_message()); @@ -48,7 +49,7 @@ final class MediaDeleteAbilityTest extends TestCase public function testConfirmationMentionsTheTrashWhenMediaTrashIsOn(): void { - $result = $this->ability([], Mockery::mock(ChangeLog::class), true)->execute(['id' => 33, 'confirm' => false]); + $result = $this->ability([], FakeChangeLog::forbidden(), true)->execute(['id' => 33, 'confirm' => false]); self::assertSame('lit_confirm_required', $result->get_error_code()); self::assertStringContainsString('Papierkorb', $result->get_error_message()); @@ -58,7 +59,7 @@ final class MediaDeleteAbilityTest extends TestCase { Functions\expect('wp_delete_attachment')->never(); - $result = $this->ability(self::USAGE, Mockery::mock(ChangeLog::class))->execute(['id' => 33, 'confirm' => true]); + $result = $this->ability(self::USAGE, FakeChangeLog::forbidden())->execute(['id' => 33, 'confirm' => true]); self::assertSame('lit_in_use', $result->get_error_code()); self::assertSame(self::USAGE, $result->get_error_data()['usages']); @@ -68,7 +69,7 @@ final class MediaDeleteAbilityTest extends TestCase { Functions\expect('wp_delete_attachment')->never(); - $result = $this->ability(self::POSSIBLE, Mockery::mock(ChangeLog::class))->execute(['id' => 33, 'confirm' => true]); + $result = $this->ability(self::POSSIBLE, FakeChangeLog::forbidden())->execute(['id' => 33, 'confirm' => true]); self::assertSame('lit_in_use', $result->get_error_code()); } @@ -76,25 +77,35 @@ final class MediaDeleteAbilityTest extends TestCase public function testForceDeletesUsedMedia(): void { Functions\expect('wp_delete_attachment')->once()->with(33, false)->andReturn(new WP_Post(['ID' => 33])); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->with('lit/media-delete', 'attachment', '33', 'Medium "Foto" gelöscht')->andReturn(50); + $log = new FakeChangeLog(50); $result = $this->ability(self::USAGE, $log)->execute(['id' => 33, 'confirm' => true, 'force' => true]); self::assertSame(['id' => '33', 'type' => 'attachment', 'action' => 'deleted', 'label' => 'Foto'], $result['changed'][0]); self::assertSame(50, $result['log_id']); self::assertSame(self::USAGE, $result['usages']); + $log->assertRecorded([ + 'ability' => 'lit/media-delete', + 'object_type' => 'attachment', + 'object_id' => '33', + 'summary' => 'Medium "Foto" gelöscht', + ]); } public function testUnusedMediaGoesToTheTrashWhenMediaTrashIsOn(): void { Functions\expect('wp_delete_attachment')->once()->with(33, false)->andReturn(new WP_Post(['ID' => 33])); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->with('lit/media-delete', 'attachment', '33', 'Medium "Foto" in den Papierkorb gelegt')->andReturn(51); + $log = new FakeChangeLog(51); $result = $this->ability([], $log, true)->execute(['id' => 33, 'confirm' => true]); self::assertSame('trashed', $result['changed'][0]['action']); + $log->assertRecorded([ + 'ability' => 'lit/media-delete', + 'object_type' => 'attachment', + 'object_id' => '33', + 'summary' => 'Medium "Foto" in den Papierkorb gelegt', + ]); } public function testRefusedDeleteWithTheAttachmentIntactIsAWriteFailure(): void @@ -102,7 +113,7 @@ final class MediaDeleteAbilityTest extends TestCase Functions\when('get_post_meta')->justReturn('2026/09/foto.jpg'); Functions\expect('wp_delete_attachment')->once()->andReturn(false); - $result = $this->ability([], Mockery::mock(ChangeLog::class))->execute(['id' => 33, 'confirm' => true]); + $result = $this->ability([], FakeChangeLog::forbidden())->execute(['id' => 33, 'confirm' => true]); self::assertSame('lit_write_failed', $result->get_error_code()); } @@ -118,13 +129,13 @@ final class MediaDeleteAbilityTest extends TestCase return null; }); - $log = Mockery::mock(ChangeLog::class); - $log->expects('recordPartial')->once()->with('lit/media-delete', 'attachment', '33', Mockery::type('string')); + $log = FakeChangeLog::partialsOnly(); $result = $this->ability([], $log)->execute(['id' => 33, 'confirm' => true]); self::assertSame('lit_partial_write', $result->get_error_code()); self::assertSame(33, $result->get_error_data()['attachment_id']); + $log->assertPartials(['ability' => 'lit/media-delete', 'object_type' => 'attachment', 'object_id' => '33']); } public function testADatabaseErrorInTheUsageCheckRefusesWithoutForce(): void @@ -138,7 +149,7 @@ final class MediaDeleteAbilityTest extends TestCase $db->allows('get_results')->andReturn(null); $ability = new MediaDeleteAbility( new MediaUsageFinder(static fn (): array => [], $db), - Mockery::mock(ChangeLog::class), + FakeChangeLog::forbidden(), static fn (): bool => false ); @@ -158,8 +169,7 @@ final class MediaDeleteAbilityTest extends TestCase public function testChangeLogFailureIsOnlyAWarning(): void { Functions\expect('wp_delete_attachment')->once()->andReturn(new WP_Post(['ID' => 33])); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->andThrow(new RuntimeException('db down')); + $log = new FakeChangeLog(1, new RuntimeException('db down')); $file = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log'); $previous = ini_set('error_log', $file); @@ -173,6 +183,7 @@ final class MediaDeleteAbilityTest extends TestCase self::assertSame('deleted', $result['changed'][0]['action']); self::assertNull($result['log_id']); self::assertCount(1, $result['warnings']); + $log->assertRecorded([]); } public function testTrashedAttachmentIsRefused(): void @@ -180,7 +191,7 @@ final class MediaDeleteAbilityTest extends TestCase Functions\when('get_post')->justReturn(new WP_Post(['ID' => 33, 'post_type' => 'attachment', 'post_status' => 'trash'])); Functions\expect('wp_delete_attachment')->never(); - $ability = $this->ability([], Mockery::mock(ChangeLog::class)); + $ability = $this->ability([], FakeChangeLog::forbidden()); self::assertSame('lit_validation_failed', $ability->permission(['id' => 33])->get_error_code()); self::assertSame('lit_validation_failed', $ability->execute(['id' => 33, 'confirm' => true])->get_error_code()); @@ -190,14 +201,14 @@ final class MediaDeleteAbilityTest extends TestCase { Functions\when('current_user_can')->alias(static fn (string $cap, int $id): bool => ! ($cap === 'delete_post' && $id === 33)); - $result = $this->ability([], Mockery::mock(ChangeLog::class))->permission(['id' => 33]); + $result = $this->ability([], FakeChangeLog::forbidden())->permission(['id' => 33]); self::assertSame('lit_forbidden', $result->get_error_code()); } public function testIsDestructiveAndDescribesWhatTheUsageCheckMisses(): void { - $ability = $this->ability([], Mockery::mock(ChangeLog::class)); + $ability = $this->ability([], FakeChangeLog::forbidden()); self::assertSame(['readonly' => false, 'destructive' => true, 'idempotent' => false], $ability->annotations()); self::assertFalse($ability->isDirect()); diff --git a/tests/Unit/Integrations/Media/MediaIntegrationTest.php b/tests/Unit/Integrations/Media/MediaIntegrationTest.php index 8fe60c4..4a4ed2a 100644 --- a/tests/Unit/Integrations/Media/MediaIntegrationTest.php +++ b/tests/Unit/Integrations/Media/MediaIntegrationTest.php @@ -5,10 +5,10 @@ declare(strict_types=1); namespace LIT\MCP\Tests\Unit\Integrations\Media; use LIT\MCP\Abilities\Ability; -use LIT\MCP\ChangeLog\ChangeLog; use LIT\MCP\Integrations\Media\MediaIntegration; use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; use LIT\MCP\Limits; +use LIT\MCP\Tests\Support\FakeChangeLog; use LIT\MCP\Tests\Unit\TestCase; use Mockery; @@ -28,7 +28,7 @@ final class MediaIntegrationTest extends TestCase public function testProvidesTheMediaAbilities(): void { - $integration = new MediaIntegration(Mockery::mock(ContentSchema::class), Mockery::mock(ChangeLog::class), new Limits()); + $integration = new MediaIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden(), new Limits()); self::assertSame('media', $integration->id()); self::assertTrue($integration->isAvailable()); diff --git a/tests/Unit/Integrations/Media/MediaUpdateAbilityTest.php b/tests/Unit/Integrations/Media/MediaUpdateAbilityTest.php index b472903..0895521 100644 --- a/tests/Unit/Integrations/Media/MediaUpdateAbilityTest.php +++ b/tests/Unit/Integrations/Media/MediaUpdateAbilityTest.php @@ -8,8 +8,8 @@ use Brain\Monkey\Functions; use LIT\MCP\Abilities\WriteFailed; use LIT\MCP\ChangeLog\ChangeLog; use LIT\MCP\Integrations\Media\MediaUpdateAbility; +use LIT\MCP\Tests\Support\FakeChangeLog; use LIT\MCP\Tests\Unit\TestCase; -use Mockery; use RuntimeException; use WP_Error; use WP_Post; @@ -53,25 +53,15 @@ final class MediaUpdateAbilityTest extends TestCase }); } - private function log(int ...$ids): ChangeLog + /** For paths that must not log. */ + private function log(): ChangeLog { - $log = Mockery::mock(ChangeLog::class); - if ($ids !== []) { - $log->expects('record')->times(count($ids))->andReturn(...$ids); - } - - return $log; + return FakeChangeLog::forbidden(); } public function testUpdatesEveryItemAndReportsPerItem(): void { - $log = Mockery::mock(ChangeLog::class); - $log->expects('record') - ->with('lit/media-update', 'attachment', '33', 'Metadaten von "Foto" geändert: alt, caption') - ->andReturn(41); - $log->expects('record') - ->with('lit/media-update', 'attachment', '34', 'Metadaten von "Logo" geändert: title') - ->andReturn(42); + $log = new FakeChangeLog(41); $result = (new MediaUpdateAbility($log))->execute(['items' => [ ['id' => 33, 'alt' => 'Team vor dem Haus', 'caption' => 'Unser Team'], @@ -84,6 +74,12 @@ final class MediaUpdateAbilityTest extends TestCase self::assertSame(42, $result['results'][1]['log_id']); self::assertSame(['id' => '34', 'type' => 'attachment', 'action' => 'updated', 'label' => 'Firmenlogo'], $result['changed'][1]); self::assertSame('2 von 2 Medien aktualisiert.', $result['summary']); + $log->assertRecorded( + ['ability' => 'lit/media-update', 'object_type' => 'attachment', 'object_id' => '33', + 'summary' => 'Metadaten von "Foto" geändert: alt, caption'], + ['ability' => 'lit/media-update', 'object_type' => 'attachment', 'object_id' => '34', + 'summary' => 'Metadaten von "Logo" geändert: title'] + ); } public function testAnInvalidItemRefusesTheWholeBatchAndNamesEveryFailure(): void @@ -226,8 +222,7 @@ final class MediaUpdateAbilityTest extends TestCase return true; }); - $log = Mockery::mock(ChangeLog::class); - $log->expects('recordPartial')->once()->with('lit/media-update', 'attachment', '33', Mockery::type('string')); + $log = FakeChangeLog::partialsOnly(); try { (new MediaUpdateAbility($log))->execute(['items' => [ @@ -241,12 +236,12 @@ final class MediaUpdateAbilityTest extends TestCase self::assertSame([33], $error->get_error_data()['changed_ids']); self::assertStringContainsString('33', $error->get_error_message()); } + $log->assertPartials(['ability' => 'lit/media-update', 'object_type' => 'attachment', 'object_id' => '33']); } public function testChangeLogFailureIsOnlyAWarning(): void { - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->andThrow(new RuntimeException('db down')); + $log = new FakeChangeLog(1, new RuntimeException('db down')); // error_log() is a PHP internal and cannot be stubbed; redirect it to a temp file instead. $file = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log'); $previous = ini_set('error_log', $file); @@ -260,6 +255,7 @@ final class MediaUpdateAbilityTest extends TestCase self::assertTrue($result['results'][0]['ok']); self::assertNull($result['results'][0]['log_id']); self::assertCount(1, $result['warnings']); + $log->assertRecorded([]); } public function testIsIdempotentIndirectAndNotDestructive(): void diff --git a/tests/Unit/Integrations/Media/MediaUploadAbilitiesTest.php b/tests/Unit/Integrations/Media/MediaUploadAbilitiesTest.php index 427b899..938c8fe 100644 --- a/tests/Unit/Integrations/Media/MediaUploadAbilitiesTest.php +++ b/tests/Unit/Integrations/Media/MediaUploadAbilitiesTest.php @@ -13,8 +13,9 @@ use LIT\MCP\Integrations\Media\MediaPresenter; use LIT\MCP\Integrations\Media\MediaUploadAbility; use LIT\MCP\Integrations\Media\MediaUploadFromUrlAbility; use LIT\MCP\Integrations\Media\MediaUploads; -use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; use LIT\MCP\Limits; +use LIT\MCP\Tests\Support\FakeChangeLog; +use LIT\MCP\Tests\Support\SchemaFixture; use LIT\MCP\Tests\Unit\TestCase; use Mockery; use RuntimeException; @@ -26,10 +27,9 @@ final class MediaUploadAbilitiesTest extends TestCase { $presenter = Mockery::mock(MediaPresenter::class); $presenter->allows('summary')->andReturn(['id' => 55, 'title' => 'Logo']); - $schema = Mockery::mock(ContentSchema::class); - $schema->allows('isManaged')->andReturn(true); + $schema = SchemaFixture::managed(); - return new MediaUploads($presenter, $log ?? Mockery::mock(ChangeLog::class), $schema); + return new MediaUploads($presenter, $log ?? FakeChangeLog::forbidden(), $schema); } private function savedAttachment(): void @@ -43,8 +43,7 @@ final class MediaUploadAbilitiesTest extends TestCase $this->savedAttachment(); $importer = Mockery::mock(MediaImporter::class); $importer->expects('fromUrl')->with('https://example.com/logo.png', null, ['alt' => 'Logo'])->andReturn(55); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->with('lit/media-upload-from-url', 'attachment', '55', Mockery::type('string'))->andReturn(3); + $log = new FakeChangeLog(3); $result = (new MediaUploadFromUrlAbility($importer, $this->uploads($log))) ->execute(['url' => 'https://example.com/logo.png', 'alt' => 'Logo']); @@ -54,6 +53,11 @@ final class MediaUploadAbilitiesTest extends TestCase self::assertSame(3, $result['log_id']); self::assertSame('https://kunde.de/wp-admin/post.php?post=55&action=edit', $result['edit_url']); self::assertSame([], $result['warnings']); + $log->assertRecorded([ + 'ability' => 'lit/media-upload-from-url', + 'object_type' => 'attachment', + 'object_id' => '55', + ]); } public function testBase64UploadPassesFilenameDataAndMeta(): void @@ -61,14 +65,14 @@ final class MediaUploadAbilitiesTest extends TestCase $this->savedAttachment(); $importer = Mockery::mock(MediaImporter::class); $importer->expects('fromBase64')->with('AAAA', 'x.png', ['title' => 'Logo', 'post_id' => 5])->andReturn(55); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->with('lit/media-upload', 'attachment', '55', Mockery::type('string'))->andReturn(4); + $log = new FakeChangeLog(4); $result = (new MediaUploadAbility($importer, $this->uploads($log), new Limits())) ->execute(['filename' => 'x.png', 'data' => 'AAAA', 'title' => 'Logo', 'post_id' => 5]); self::assertTrue($result['ok']); self::assertSame(4, $result['log_id']); + $log->assertRecorded(['ability' => 'lit/media-upload', 'object_type' => 'attachment', 'object_id' => '55']); } public function testAFailingChangeLogIsOnlyAWarning(): void @@ -76,8 +80,7 @@ final class MediaUploadAbilitiesTest extends TestCase $this->savedAttachment(); $importer = Mockery::mock(MediaImporter::class); $importer->allows('fromUrl')->andReturn(55); - $log = Mockery::mock(ChangeLog::class); - $log->allows('record')->andThrow(new RuntimeException('db down')); + $log = new FakeChangeLog(1, new RuntimeException('db down')); // error_log() is a PHP internal and cannot be stubbed; redirect it to a temp file instead. $file = (string) tempnam(sys_get_temp_dir(), 'lit-log'); $previous = ini_set('error_log', $file); @@ -111,11 +114,19 @@ final class MediaUploadAbilitiesTest extends TestCase $failure = WriteFailed::partial('Alt-Text fehlt.', postId: 55); $importer = Mockery::mock(MediaImporter::class); $importer->allows('fromUrl')->andThrow($failure); - $log = Mockery::mock(ChangeLog::class); - $log->expects('recordPartial')->with('lit/media-upload-from-url', 'attachment', '55', Mockery::type('string')); + $log = FakeChangeLog::partialsOnly(); - $this->expectExceptionObject($failure); - (new MediaUploadFromUrlAbility($importer, $this->uploads($log)))->execute(['url' => 'https://example.com/a.png']); + try { + (new MediaUploadFromUrlAbility($importer, $this->uploads($log)))->execute(['url' => 'https://example.com/a.png']); + self::fail('WriteFailed expected'); + } catch (WriteFailed $e) { + self::assertSame($failure, $e); + } + $log->assertPartials([ + 'ability' => 'lit/media-upload-from-url', + 'object_type' => 'attachment', + 'object_id' => '55', + ]); } public function testPermissionNeedsUploadRightsAndEditRightsOnTheParent(): void diff --git a/tests/Unit/Integrations/WordPress/Content/ContentCreateAbilityTest.php b/tests/Unit/Integrations/WordPress/Content/ContentCreateAbilityTest.php index f43a175..512a8d5 100644 --- a/tests/Unit/Integrations/WordPress/Content/ContentCreateAbilityTest.php +++ b/tests/Unit/Integrations/WordPress/Content/ContentCreateAbilityTest.php @@ -13,6 +13,8 @@ use LIT\MCP\ChangeLog\ChangeLog; use LIT\MCP\Integrations\WordPress\Content\ContentCreateAbility; use LIT\MCP\Integrations\WordPress\Content\PostWriter; use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; +use LIT\MCP\Tests\Support\FakeChangeLog; +use LIT\MCP\Tests\Support\SchemaFixture; use LIT\MCP\Tests\Unit\TestCase; use Mockery; use Mockery\MockInterface; @@ -27,8 +29,7 @@ final class ContentCreateAbilityTest extends TestCase { Functions\when('get_post_type_object')->justReturn((object) ['cap' => (object) ['create_posts' => 'edit_pages']]); Functions\when('current_user_can')->justReturn(true); - $schema = Mockery::mock(ContentSchema::class); - $schema->allows('isManaged')->andReturn(true); + $schema = SchemaFixture::managed(); return $schema; } @@ -72,7 +73,7 @@ final class ContentCreateAbilityTest extends TestCase $writer->allows('prepare')->andReturn(Errors::invalidInput('ungültig', ['fields.role' => 'Pflichtfeld fehlt.'])); Functions\expect('wp_insert_post')->never(); - $result = (new ContentCreateAbility($this->schema(), $writer, Mockery::mock(ChangeLog::class))) + $result = (new ContentCreateAbility($this->schema(), $writer, FakeChangeLog::forbidden())) ->execute(['post_type' => 'lit_person', 'title' => 'Anna']); self::assertSame('lit_validation_failed', $result->get_error_code()); @@ -97,8 +98,7 @@ final class ContentCreateAbilityTest extends TestCase ], true)->andReturn(42); Actions\expectDone('lit_mcp_after_save')->once()->with(42, 'lit/content-create', $input); $post = $this->stubResultLinks(); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->with('lit/content-create', 'lit_person', '42', 'Entwurf "Anna" angelegt', null)->andReturn(7); + $log = new FakeChangeLog(7); $result = (new ContentCreateAbility($schema, $writer, $log))->execute($input); @@ -108,6 +108,13 @@ final class ContentCreateAbilityTest extends TestCase self::assertSame(['w'], $result['warnings']); self::assertStringContainsString('Entwurf', $result['summary']); self::assertSame(Rev::ofPost($post), $result['rev']); + $log->assertRecorded([ + 'ability' => 'lit/content-create', + 'object_type' => 'lit_person', + 'object_id' => '42', + 'summary' => 'Entwurf "Anna" angelegt', + 'revision_id' => null, + ]); } public function testFailingExtrasDeleteCreatedTermsAndTheNewDraft(): void @@ -118,8 +125,7 @@ final class ContentCreateAbilityTest extends TestCase Functions\when('wp_insert_post')->justReturn(42); Functions\expect('wp_delete_post')->once()->with(42, true)->andReturn(new WP_Post(['ID' => 42])); Actions\expectDone('lit_mcp_after_save')->never(); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->never(); + $log = FakeChangeLog::forbidden(); $failure = $this->execute($writer, $log); @@ -139,8 +145,7 @@ final class ContentCreateAbilityTest extends TestCase Functions\when('get_current_user_id')->justReturn(3); Functions\when('wp_insert_post')->justReturn(42); Functions\expect('wp_delete_post')->once()->with(42, true)->andReturn(new WP_Post(['ID' => 42])); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->never(); + $log = FakeChangeLog::forbidden(); $result = (new ContentCreateAbility($this->schema(), $writer, $log))->execute(self::INPUT); @@ -157,8 +162,7 @@ final class ContentCreateAbilityTest extends TestCase Functions\when('get_current_user_id')->justReturn(3); Functions\when('wp_insert_post')->justReturn(42); Functions\when('wp_delete_post')->justReturn(false); - $log = Mockery::mock(ChangeLog::class); - $log->expects('recordPartial'); + $log = FakeChangeLog::partialsOnly(); try { (new ContentCreateAbility($this->schema(), $writer, $log))->execute(self::INPUT); @@ -167,6 +171,7 @@ final class ContentCreateAbilityTest extends TestCase self::assertFalse($e->rolledBack); self::assertSame(42, $e->postId); } + $log->assertPartials([]); } public function testADraftThatCannotBeDeletedIsAPartialWriteAndLogged(): void @@ -176,16 +181,14 @@ final class ContentCreateAbilityTest extends TestCase $writer->expects('deleteCreatedTerms'); Functions\when('wp_insert_post')->justReturn(42); Functions\when('wp_delete_post')->justReturn(false); - $log = Mockery::mock(ChangeLog::class); - $log->expects('recordPartial') - ->with('lit/content-create', 'lit_person', '42', Mockery::type('string')) - ->andReturn(9); + $log = FakeChangeLog::partialsOnly(); $failure = $this->execute($writer, $log); self::assertFalse($failure->rolledBack); self::assertSame(42, $failure->postId); self::assertStringContainsString('Eintrag 42', $failure->toError()->get_error_message()); + $log->assertPartials(['ability' => 'lit/content-create', 'object_type' => 'lit_person', 'object_id' => '42']); } public function testLeftoverCreatedTermsMakeItAPartialWrite(): void @@ -195,13 +198,13 @@ final class ContentCreateAbilityTest extends TestCase $writer->expects('deleteCreatedTerms')->andThrow(new RuntimeException('term 15 bleibt')); Functions\when('wp_insert_post')->justReturn(42); Functions\when('wp_delete_post')->justReturn(new WP_Post(['ID' => 42])); - $log = Mockery::mock(ChangeLog::class); - $log->expects('recordPartial'); + $log = FakeChangeLog::partialsOnly(); $failure = $this->execute($writer, $log); self::assertFalse($failure->rolledBack); self::assertStringContainsString('term 15 bleibt', $failure->getMessage()); + $log->assertPartials([]); } public function testAnInsertThatThrowsIsAPartialWriteWithoutId(): void @@ -210,16 +213,14 @@ final class ContentCreateAbilityTest extends TestCase Functions\when('wp_insert_post')->alias(static function (): never { throw new RuntimeException('save_post hook kaputt'); }); - $log = Mockery::mock(ChangeLog::class); - $log->expects('recordPartial') - ->with('lit/content-create', 'lit_person', '', Mockery::type('string')) - ->andReturn(9); + $log = FakeChangeLog::partialsOnly(); $failure = $this->execute($writer, $log); self::assertFalse($failure->rolledBack); self::assertNull($failure->postId); self::assertSame('lit_partial_write', $failure->toError()->get_error_code()); + $log->assertPartials(['ability' => 'lit/content-create', 'object_type' => 'lit_person', 'object_id' => '']); } public function testFailuresAfterTheWriteOnlyAddWarnings(): void @@ -231,8 +232,7 @@ final class ContentCreateAbilityTest extends TestCase Actions\expectDone('lit_mcp_after_save')->once()->whenHappen(static function (): void { throw new RuntimeException('listener kaputt'); }); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->andThrow(new RuntimeException('db weg')); + $log = new FakeChangeLog(1, new RuntimeException('db weg')); $file = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log'); $previous = ini_set('error_log', $file); @@ -248,12 +248,13 @@ final class ContentCreateAbilityTest extends TestCase self::assertStringContainsString('listener kaputt', (string) file_get_contents($file)); self::assertStringContainsString('db weg', (string) file_get_contents($file)); unlink($file); + $log->assertRecorded([]); } public function testStatusIsNotAnInputAndPermissionNeedsCreateRights(): void { $schema = $this->schema(); - $ability = new ContentCreateAbility($schema, Mockery::mock(PostWriter::class), Mockery::mock(ChangeLog::class)); + $ability = new ContentCreateAbility($schema, Mockery::mock(PostWriter::class), FakeChangeLog::forbidden()); self::assertArrayNotHasKey('status', $ability->inputSchema()['properties']); self::assertFalse($ability->inputSchema()['additionalProperties']); diff --git a/tests/Unit/Integrations/WordPress/Content/ContentCreateDiviTest.php b/tests/Unit/Integrations/WordPress/Content/ContentCreateDiviTest.php index a797d1b..de1e24d 100644 --- a/tests/Unit/Integrations/WordPress/Content/ContentCreateDiviTest.php +++ b/tests/Unit/Integrations/WordPress/Content/ContentCreateDiviTest.php @@ -16,6 +16,8 @@ use LIT\MCP\Integrations\WordPress\Content\ContentCreateAbility; use LIT\MCP\Integrations\WordPress\Content\PostWriter; use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; use LIT\MCP\Tests\Support\DiviWriterFixture; +use LIT\MCP\Tests\Support\FakeChangeLog; +use LIT\MCP\Tests\Support\SchemaFixture; use LIT\MCP\Tests\Unit\TestCase; use Mockery; use Mockery\MockInterface; @@ -34,9 +36,7 @@ final class ContentCreateDiviTest extends TestCase Functions\when('get_post_type_object')->justReturn((object) ['cap' => (object) ['create_posts' => 'edit_pages']]); Functions\when('current_user_can')->justReturn(true); Functions\when('get_current_user_id')->justReturn(3); - $schema = Mockery::mock(ContentSchema::class); - $schema->allows('isManaged')->andReturn(true); - $schema->allows('diviEnabled')->andReturn(true); + $schema = SchemaFixture::managed(true); return $schema; } @@ -66,9 +66,7 @@ final class ContentCreateDiviTest extends TestCase private function log(): ChangeLog { - $log = Mockery::mock(ChangeLog::class); - $log->allows('record')->andReturn(9); - $log->allows('recordPartial'); + $log = FakeChangeLog::any(9); return $log; } diff --git a/tests/Unit/Integrations/WordPress/Content/ContentGetAbilityTest.php b/tests/Unit/Integrations/WordPress/Content/ContentGetAbilityTest.php index 1f37160..0d1fea8 100644 --- a/tests/Unit/Integrations/WordPress/Content/ContentGetAbilityTest.php +++ b/tests/Unit/Integrations/WordPress/Content/ContentGetAbilityTest.php @@ -8,6 +8,7 @@ use Brain\Monkey\Functions; use LIT\MCP\Integrations\WordPress\Content\ContentGetAbility; use LIT\MCP\Integrations\WordPress\Content\PostPresenter; use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; +use LIT\MCP\Tests\Support\SchemaFixture; use LIT\MCP\Tests\Unit\TestCase; use Mockery; use WP_Post; @@ -19,8 +20,7 @@ final class ContentGetAbilityTest extends TestCase $post = new WP_Post(['ID' => 5, 'post_type' => 'page']); Functions\when('get_post')->justReturn($post); Functions\when('current_user_can')->justReturn(true); - $schema = Mockery::mock(ContentSchema::class); - $schema->allows('isManaged')->andReturn(true); + $schema = SchemaFixture::managed(); $presenter = Mockery::mock(PostPresenter::class); $presenter->expects('full')->with($post)->andReturn(['id' => 5]); $ability = new ContentGetAbility($schema, $presenter); @@ -34,8 +34,7 @@ final class ContentGetAbilityTest extends TestCase $post = new WP_Post(['ID' => 5, 'post_type' => 'page', 'post_status' => 'trash']); Functions\when('get_post')->justReturn($post); Functions\when('current_user_can')->justReturn(true); - $schema = Mockery::mock(ContentSchema::class); - $schema->allows('isManaged')->andReturn(true); + $schema = SchemaFixture::managed(); $presenter = Mockery::mock(PostPresenter::class); $presenter->expects('full')->with($post)->andReturn(['id' => 5]); diff --git a/tests/Unit/Integrations/WordPress/Content/ContentQueryAbilityTest.php b/tests/Unit/Integrations/WordPress/Content/ContentQueryAbilityTest.php index e610bf2..50bbf4e 100644 --- a/tests/Unit/Integrations/WordPress/Content/ContentQueryAbilityTest.php +++ b/tests/Unit/Integrations/WordPress/Content/ContentQueryAbilityTest.php @@ -10,6 +10,7 @@ use LIT\MCP\Integrations\WordPress\Content\ContentQueryAbility; use LIT\MCP\Integrations\WordPress\Content\PostPresenter; use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition; +use LIT\MCP\Tests\Support\SchemaFixture; use LIT\MCP\Tests\Unit\TestCase; use Mockery; use WP_Post; @@ -241,8 +242,7 @@ final class ContentQueryAbilityTest extends TestCase { Functions\when('get_post_type_object')->justReturn((object) ['cap' => (object) ['edit_posts' => 'edit_posts']]); Functions\when('current_user_can')->justReturn(false); - $schema = Mockery::mock(ContentSchema::class); - $schema->allows('isManaged')->andReturn(true); + $schema = SchemaFixture::managed(); $ability = new ContentQueryAbility($schema, Mockery::mock(PostPresenter::class)); self::assertSame('lit_forbidden', $ability->permission(['post_type' => 'page'])->get_error_code()); diff --git a/tests/Unit/Integrations/WordPress/Content/ContentTypesAbilityTest.php b/tests/Unit/Integrations/WordPress/Content/ContentTypesAbilityTest.php index 6edf341..38e4067 100644 --- a/tests/Unit/Integrations/WordPress/Content/ContentTypesAbilityTest.php +++ b/tests/Unit/Integrations/WordPress/Content/ContentTypesAbilityTest.php @@ -7,6 +7,7 @@ namespace LIT\MCP\Tests\Unit\Integrations\WordPress\Content; use Brain\Monkey\Functions; use LIT\MCP\Integrations\WordPress\Content\ContentTypesAbility; use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; +use LIT\MCP\Tests\Support\SchemaFixture; use LIT\MCP\Tests\Unit\TestCase; use Mockery; use WP_Error; @@ -89,8 +90,7 @@ final class ContentTypesAbilityTest extends TestCase { Functions\when('get_post_type_object')->justReturn((object) ['cap' => (object) ['edit_posts' => 'edit_pages']]); Functions\when('current_user_can')->justReturn(false); - $schema = Mockery::mock(ContentSchema::class); - $schema->allows('isManaged')->andReturn(true); + $schema = SchemaFixture::managed(); $result = (new ContentTypesAbility($schema))->execute(['post_type' => 'page']); @@ -110,8 +110,7 @@ final class ContentTypesAbilityTest extends TestCase { Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap === 'edit_posts'); Functions\when('get_post_type_object')->justReturn((object) ['cap' => (object) ['edit_posts' => 'edit_pages']]); - $schema = Mockery::mock(ContentSchema::class); - $schema->allows('isManaged')->andReturn(true); + $schema = SchemaFixture::managed(); $result = (new ContentTypesAbility($schema))->permission(['post_type' => 'lit_person']); @@ -123,8 +122,7 @@ final class ContentTypesAbilityTest extends TestCase { Functions\when('current_user_can')->justReturn(true); Functions\when('get_post_type_object')->justReturn((object) ['cap' => (object) ['edit_posts' => 'edit_posts']]); - $schema = Mockery::mock(ContentSchema::class); - $schema->allows('isManaged')->andReturn(true); + $schema = SchemaFixture::managed(); self::assertTrue((new ContentTypesAbility($schema))->permission(['post_type' => 'page'])); } diff --git a/tests/Unit/Integrations/WordPress/Content/ContentUpdateAbilityTest.php b/tests/Unit/Integrations/WordPress/Content/ContentUpdateAbilityTest.php index dfd1033..4ab5b12 100644 --- a/tests/Unit/Integrations/WordPress/Content/ContentUpdateAbilityTest.php +++ b/tests/Unit/Integrations/WordPress/Content/ContentUpdateAbilityTest.php @@ -13,6 +13,8 @@ use LIT\MCP\Integrations\WordPress\Content\ContentUpdateAbility; use LIT\MCP\Integrations\WordPress\Content\PostPresenter; use LIT\MCP\Integrations\WordPress\Content\PostWriter; use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; +use LIT\MCP\Tests\Support\FakeChangeLog; +use LIT\MCP\Tests\Support\SchemaFixture; use LIT\MCP\Tests\Unit\TestCase; use Mockery; use RuntimeException; @@ -39,15 +41,14 @@ final class ContentUpdateAbilityTest extends TestCase private function schema(): ContentSchema { - $schema = Mockery::mock(ContentSchema::class); - $schema->allows('isManaged')->andReturn(true); + $schema = SchemaFixture::managed(); return $schema; } private function ability(PostWriter $writer, ?ChangeLog $log = null): ContentUpdateAbility { - return new ContentUpdateAbility($this->schema(), $writer, $log ?? Mockery::mock(ChangeLog::class)); + return new ContentUpdateAbility($this->schema(), $writer, $log ?? FakeChangeLog::forbidden()); } public function testRevIsRequired(): void @@ -155,8 +156,7 @@ final class ContentUpdateAbilityTest extends TestCase Actions\expectDone('lit_mcp_after_save')->once()->with(5, 'lit/content-update', $input); Functions\when('get_preview_post_link')->justReturn('https://kunde.de/team/'); Functions\when('get_edit_post_link')->justReturn('https://kunde.de/wp-admin/post.php?post=5&action=edit'); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->with('lit/content-update', 'page', '5', Mockery::type('string'), 811)->andReturn(4711); + $log = new FakeChangeLog(4711); $result = $this->ability($writer, $log)->execute($input); @@ -166,6 +166,12 @@ final class ContentUpdateAbilityTest extends TestCase self::assertSame('updated', $result['changed'][0]['action']); self::assertSame(Rev::ofPost($this->post), $result['rev']); self::assertStringContainsString('Status unverändert: publish', $result['summary']); + $log->assertRecorded([ + 'ability' => 'lit/content-update', + 'object_type' => 'page', + 'object_id' => '5', + 'revision_id' => 811, + ]); } /** @@ -197,8 +203,7 @@ final class ContentUpdateAbilityTest extends TestCase private function executeExpectingFailure(PostWriter $writer, array $input, ?ChangeLog $log = null): WriteFailed { if ($log === null) { - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->never(); + $log = FakeChangeLog::forbidden(); } try { @@ -298,14 +303,14 @@ final class ContentUpdateAbilityTest extends TestCase }); Functions\when('wp_revisions_enabled')->justReturn(true); Functions\expect('wp_restore_post_revision')->never(); - $log = Mockery::mock(ChangeLog::class); - $log->expects('recordPartial'); + $log = FakeChangeLog::partialsOnly(); $failure = $this->executeExpectingFailure($writer, $input, $log); self::assertFalse($failure->rolledBack); self::assertCount(1, $updates); self::assertStringContainsString('publish', $failure->getMessage()); + $log->assertPartials([]); } public function testAnIncompleteRollbackIsAPartialWriteAndLogged(): void @@ -314,10 +319,7 @@ final class ContentUpdateAbilityTest extends TestCase $updates = []; $this->captureUpdates($updates); Functions\when('wp_revisions_enabled')->justReturn(false); - $log = Mockery::mock(ChangeLog::class); - $log->expects('recordPartial') - ->with('lit/content-update', 'page', '5', Mockery::type('string'), 811) - ->andReturn(12); + $log = FakeChangeLog::partialsOnly(); $failure = $this->executeExpectingFailure($writer, $input, $log); @@ -327,6 +329,12 @@ final class ContentUpdateAbilityTest extends TestCase self::assertSame('lit_partial_write', $error->get_error_code()); self::assertSame(5, $error->get_error_data()['post_id']); self::assertSame(811, $error->get_error_data()['revision_id']); + $log->assertPartials([ + 'ability' => 'lit/content-update', + 'object_type' => 'page', + 'object_id' => '5', + 'revision_id' => 811, + ]); } public function testFailuresAfterTheWriteOnlyAddWarnings(): void @@ -345,8 +353,7 @@ final class ContentUpdateAbilityTest extends TestCase Actions\expectDone('lit_mcp_after_save')->once()->whenHappen(static function (): void { throw new RuntimeException('listener kaputt'); }); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->andThrow(new RuntimeException('db weg')); + $log = new FakeChangeLog(1, new RuntimeException('db weg')); $file = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log'); $previous = ini_set('error_log', $file); @@ -361,5 +368,6 @@ final class ContentUpdateAbilityTest extends TestCase self::assertCount(2, $result['warnings']); self::assertStringContainsString('listener kaputt', (string) file_get_contents($file)); unlink($file); + $log->assertRecorded([]); } } diff --git a/tests/Unit/Integrations/WordPress/Content/NewDraftTest.php b/tests/Unit/Integrations/WordPress/Content/NewDraftTest.php index 00514aa..632e876 100644 --- a/tests/Unit/Integrations/WordPress/Content/NewDraftTest.php +++ b/tests/Unit/Integrations/WordPress/Content/NewDraftTest.php @@ -6,9 +6,9 @@ namespace LIT\MCP\Tests\Unit\Integrations\WordPress\Content; use Brain\Monkey\Functions; use LIT\MCP\Abilities\WriteFailed; -use LIT\MCP\ChangeLog\ChangeLog; use LIT\MCP\Integrations\WordPress\Content\NewDraft; use LIT\MCP\Integrations\WordPress\Content\PostWriter; +use LIT\MCP\Tests\Support\FakeChangeLog; use LIT\MCP\Tests\Unit\TestCase; use Mockery; use RuntimeException; @@ -22,9 +22,7 @@ final class NewDraftTest extends TestCase Functions\when('wp_delete_post')->alias(static function (): never { throw new RuntimeException('delete_post hook kaputt'); }); - $log = Mockery::mock(ChangeLog::class); - $log->expects('recordPartial') - ->with('lit/content-create', 'page', '42', 'Entwurf 42: Anlegen nur teilweise zurückgenommen'); + $log = FakeChangeLog::partialsOnly(); $draft = new NewDraft($writer, $log, 'lit/content-create', 'page'); try { @@ -40,6 +38,12 @@ final class NewDraftTest extends TestCase $e->getMessage() ); } + $log->assertPartials([ + 'ability' => 'lit/content-create', + 'object_type' => 'page', + 'object_id' => '42', + 'summary' => 'Entwurf 42: Anlegen nur teilweise zurückgenommen', + ]); } public function testACleanDiscardIsRolledBackWithTheGivenReason(): void @@ -47,8 +51,7 @@ final class NewDraftTest extends TestCase $writer = Mockery::mock(PostWriter::class); $writer->expects('deleteCreatedTerms'); Functions\expect('wp_delete_post')->once()->with(42, true)->andReturn(true); - $log = Mockery::mock(ChangeLog::class); - $log->expects('recordPartial')->never(); + $log = FakeChangeLog::forbidden(); $draft = new NewDraft($writer, $log, 'lit/content-create', 'page'); try { diff --git a/tests/Unit/Integrations/WordPress/Menus/MenuAssignLocationAbilityTest.php b/tests/Unit/Integrations/WordPress/Menus/MenuAssignLocationAbilityTest.php index 43fced5..a1b0fe6 100644 --- a/tests/Unit/Integrations/WordPress/Menus/MenuAssignLocationAbilityTest.php +++ b/tests/Unit/Integrations/WordPress/Menus/MenuAssignLocationAbilityTest.php @@ -5,10 +5,9 @@ declare(strict_types=1); namespace LIT\MCP\Tests\Unit\Integrations\WordPress\Menus; use Brain\Monkey\Functions; -use LIT\MCP\ChangeLog\ChangeLog; use LIT\MCP\Integrations\WordPress\Menus\MenuAssignLocationAbility; +use LIT\MCP\Tests\Support\FakeChangeLog; use LIT\MCP\Tests\Unit\TestCase; -use Mockery; final class MenuAssignLocationAbilityTest extends TestCase { @@ -29,28 +28,26 @@ final class MenuAssignLocationAbilityTest extends TestCase public function testAssignsAndReportsThePreviousMenu(): void { Functions\expect('set_theme_mod')->once()->with('nav_menu_locations', ['primary-menu' => 8]); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record') - ->with( - 'lit/menu-assign-location', - 'menu_location', - 'primary-menu', - 'Menü 8 der Position primary-menu zugewiesen' - ) - ->andReturn(6); + $log = new FakeChangeLog(6); $result = (new MenuAssignLocationAbility($log))->execute(['location' => 'primary-menu', 'menu_id' => 8]); self::assertSame(3, $result['previous_menu_id']); self::assertSame('assigned', $result['changed'][0]['action']); self::assertSame(6, $result['log_id']); + $log->assertRecorded([ + 'ability' => 'lit/menu-assign-location', + 'object_type' => 'menu_location', + 'object_id' => 'primary-menu', + 'summary' => 'Menü 8 der Position primary-menu zugewiesen', + ]); } public function testRemovingAnAssignmentNeedsConfirmation(): void { Functions\expect('set_theme_mod')->never(); - $result = (new MenuAssignLocationAbility(Mockery::mock(ChangeLog::class))) + $result = (new MenuAssignLocationAbility(FakeChangeLog::forbidden())) ->execute(['location' => 'primary-menu', 'menu_id' => 0]); self::assertSame('lit_confirm_required', $result->get_error_code()); @@ -63,15 +60,7 @@ final class MenuAssignLocationAbilityTest extends TestCase public function testZeroRemovesTheAssignmentAndNamesTheRemovedMenu(): void { Functions\expect('set_theme_mod')->once()->with('nav_menu_locations', []); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record') - ->with( - 'lit/menu-assign-location', - 'menu_location', - 'primary-menu', - 'Menü "Hauptmenü" (ID 3) von der Position primary-menu entfernt' - ) - ->andReturn(7); + $log = new FakeChangeLog(7); $result = (new MenuAssignLocationAbility($log)) ->execute(['location' => 'primary-menu', 'menu_id' => 0, 'confirm' => true]); @@ -80,14 +69,19 @@ final class MenuAssignLocationAbilityTest extends TestCase self::assertSame(3, $result['previous_menu_id']); self::assertStringContainsString('"Hauptmenü" (ID 3)', $result['summary']); self::assertSame(7, $result['log_id']); + $log->assertRecorded([ + 'ability' => 'lit/menu-assign-location', + 'object_type' => 'menu_location', + 'object_id' => 'primary-menu', + 'summary' => 'Menü "Hauptmenü" (ID 3) von der Position primary-menu entfernt', + ]); } public function testRemovingFromAnEmptyLocationNeedsNoConfirmation(): void { Functions\when('get_nav_menu_locations')->justReturn([]); Functions\expect('set_theme_mod')->once()->with('nav_menu_locations', []); - $log = Mockery::mock(ChangeLog::class); - $log->allows('record')->andReturn(8); + $log = new FakeChangeLog(8); $result = (new MenuAssignLocationAbility($log))->execute(['location' => 'primary-menu', 'menu_id' => 0]); @@ -97,7 +91,7 @@ final class MenuAssignLocationAbilityTest extends TestCase public function testDescribesTheConfirmationForRemoval(): void { - $ability = new MenuAssignLocationAbility(Mockery::mock(ChangeLog::class)); + $ability = new MenuAssignLocationAbility(FakeChangeLog::forbidden()); self::assertArrayHasKey('confirm', $ability->inputSchema()['properties']); self::assertStringContainsString('confirm: true', $ability->description()); @@ -107,7 +101,7 @@ final class MenuAssignLocationAbilityTest extends TestCase { Functions\expect('set_theme_mod')->never(); - $result = (new MenuAssignLocationAbility(Mockery::mock(ChangeLog::class))) + $result = (new MenuAssignLocationAbility(FakeChangeLog::forbidden())) ->execute(['location' => 'primary-menu', 'menu_id' => 99]); self::assertSame('lit_not_found', $result->get_error_code()); @@ -117,7 +111,7 @@ final class MenuAssignLocationAbilityTest extends TestCase { Functions\when('current_user_can')->justReturn(false); - $ability = new MenuAssignLocationAbility(Mockery::mock(ChangeLog::class)); + $ability = new MenuAssignLocationAbility(FakeChangeLog::forbidden()); $input = ['location' => 'primary-menu', 'menu_id' => 8]; self::assertSame('lit_forbidden', $ability->permission($input)->get_error_code()); @@ -125,7 +119,7 @@ final class MenuAssignLocationAbilityTest extends TestCase public function testIsDestructiveBecauseItReplacesTheMenuOfTheLocation(): void { - $ability = new MenuAssignLocationAbility(Mockery::mock(ChangeLog::class)); + $ability = new MenuAssignLocationAbility(FakeChangeLog::forbidden()); self::assertSame(['readonly' => false, 'destructive' => true, 'idempotent' => true], $ability->annotations()); } diff --git a/tests/Unit/Integrations/WordPress/Menus/MenuCreateAbilityTest.php b/tests/Unit/Integrations/WordPress/Menus/MenuCreateAbilityTest.php index df3782a..b75ab3c 100644 --- a/tests/Unit/Integrations/WordPress/Menus/MenuCreateAbilityTest.php +++ b/tests/Unit/Integrations/WordPress/Menus/MenuCreateAbilityTest.php @@ -5,10 +5,9 @@ declare(strict_types=1); namespace LIT\MCP\Tests\Unit\Integrations\WordPress\Menus; use Brain\Monkey\Functions; -use LIT\MCP\ChangeLog\ChangeLog; use LIT\MCP\Integrations\WordPress\Menus\MenuCreateAbility; +use LIT\MCP\Tests\Support\FakeChangeLog; use LIT\MCP\Tests\Unit\TestCase; -use Mockery; use WP_Error; final class MenuCreateAbilityTest extends TestCase @@ -18,7 +17,7 @@ final class MenuCreateAbilityTest extends TestCase Functions\when('get_registered_nav_menus')->justReturn(['primary-menu' => 'Hauptmenü']); Functions\expect('wp_create_nav_menu')->never(); - $ability = new MenuCreateAbility(Mockery::mock(ChangeLog::class)); + $ability = new MenuCreateAbility(FakeChangeLog::forbidden()); $result = $ability->execute(['name' => 'Neu', 'location' => 'nope']); self::assertSame('lit_validation_failed', $result->get_error_code()); @@ -31,7 +30,7 @@ final class MenuCreateAbilityTest extends TestCase new WP_Error('menu_exists', 'Der Menüname existiert bereits.') ); - $result = (new MenuCreateAbility(Mockery::mock(ChangeLog::class)))->execute(['name' => 'Hauptmenü']); + $result = (new MenuCreateAbility(FakeChangeLog::forbidden()))->execute(['name' => 'Hauptmenü']); self::assertSame('lit_validation_failed', $result->get_error_code()); } @@ -45,23 +44,26 @@ final class MenuCreateAbilityTest extends TestCase 'nav_menu_locations', ['footer-menu' => 2, 'primary-menu' => 8] ); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record') - ->with('lit/menu-create', 'nav_menu', '8', 'Menü "Neu" angelegt (Position primary-menu)') - ->andReturn(5); + $log = new FakeChangeLog(5); $result = (new MenuCreateAbility($log))->execute(['name' => 'Neu', 'location' => 'primary-menu']); self::assertSame(8, $result['menu_id']); self::assertSame('created', $result['changed'][0]['action']); self::assertSame(5, $result['log_id']); + $log->assertRecorded([ + 'ability' => 'lit/menu-create', + 'object_type' => 'nav_menu', + 'object_id' => '8', + 'summary' => 'Menü "Neu" angelegt (Position primary-menu)', + ]); } public function testPermissionNeedsThemeOptionRights(): void { Functions\when('current_user_can')->justReturn(false); - $ability = new MenuCreateAbility(Mockery::mock(ChangeLog::class)); + $ability = new MenuCreateAbility(FakeChangeLog::forbidden()); self::assertSame('lit_forbidden', $ability->permission(['name' => 'Neu'])->get_error_code()); } diff --git a/tests/Unit/Integrations/WordPress/Menus/MenuSetItemsAbilityTest.php b/tests/Unit/Integrations/WordPress/Menus/MenuSetItemsAbilityTest.php index db9d870..51a4798 100644 --- a/tests/Unit/Integrations/WordPress/Menus/MenuSetItemsAbilityTest.php +++ b/tests/Unit/Integrations/WordPress/Menus/MenuSetItemsAbilityTest.php @@ -7,11 +7,11 @@ namespace LIT\MCP\Tests\Unit\Integrations\WordPress\Menus; use Brain\Monkey\Functions; use LIT\MCP\Abilities\Rev; use LIT\MCP\Abilities\WriteFailed; -use LIT\MCP\ChangeLog\ChangeLog; use LIT\MCP\Integrations\WordPress\Menus\MenuOperations; use LIT\MCP\Integrations\WordPress\Menus\MenuSetItemsAbility; use LIT\MCP\Integrations\WordPress\Menus\MenuTree; use LIT\MCP\Integrations\WordPress\Menus\MenuWriter; +use LIT\MCP\Tests\Support\FakeChangeLog; use LIT\MCP\Tests\Unit\TestCase; use Mockery; use RuntimeException; @@ -37,7 +37,7 @@ final class MenuSetItemsAbilityTest extends TestCase { $writer = Mockery::mock(MenuWriter::class); $writer->expects('apply')->never(); - $ability = new MenuSetItemsAbility($writer, new MenuOperations(), Mockery::mock(ChangeLog::class)); + $ability = new MenuSetItemsAbility($writer, new MenuOperations(), FakeChangeLog::forbidden()); $stale = $ability->execute(['menu_id' => 3, 'menu_rev' => 'abcdefabcdef', 'items' => [['id' => 10]]]); $missing = $ability->execute(['menu_id' => 3, 'items' => [['id' => 10]]]); @@ -55,8 +55,7 @@ final class MenuSetItemsAbilityTest extends TestCase $writer = Mockery::mock(MenuWriter::class); $writer->allows('plan')->andReturn(['errors' => [], 'removals' => [], 'warnings' => []]); $writer->allows('apply')->andReturn(['created' => 0, 'updated' => 1, 'removed' => 0]); - $log = Mockery::mock(ChangeLog::class); - $log->allows('record')->andReturn(30); + $log = new FakeChangeLog(30); $result = (new MenuSetItemsAbility($writer, new MenuOperations(), $log))->execute( ['menu_id' => 3, 'menu_rev' => self::rev(), 'items' => [['id' => 10]]] @@ -67,7 +66,7 @@ final class MenuSetItemsAbilityTest extends TestCase public function testItemsAndOperationsAreMutuallyExclusive(): void { - $ability = new MenuSetItemsAbility(new MenuWriter(), new MenuOperations(), Mockery::mock(ChangeLog::class)); + $ability = new MenuSetItemsAbility(new MenuWriter(), new MenuOperations(), FakeChangeLog::forbidden()); self::assertSame('lit_validation_failed', $ability->execute(['menu_id' => 3, 'menu_rev' => self::rev()])->get_error_code()); self::assertSame( @@ -80,7 +79,7 @@ final class MenuSetItemsAbilityTest extends TestCase public function testUnknownMenuIsNotFound(): void { Functions\when('wp_get_nav_menu_object')->justReturn(false); - $ability = new MenuSetItemsAbility(new MenuWriter(), new MenuOperations(), Mockery::mock(ChangeLog::class)); + $ability = new MenuSetItemsAbility(new MenuWriter(), new MenuOperations(), FakeChangeLog::forbidden()); self::assertSame('lit_not_found', $ability->execute(['menu_id' => 9, 'items' => []])->get_error_code()); } @@ -89,7 +88,7 @@ final class MenuSetItemsAbilityTest extends TestCase { Functions\expect('wp_update_nav_menu_item')->never(); Functions\expect('wp_delete_post')->never(); - $ability = new MenuSetItemsAbility(new MenuWriter(), new MenuOperations(), Mockery::mock(ChangeLog::class)); + $ability = new MenuSetItemsAbility(new MenuWriter(), new MenuOperations(), FakeChangeLog::forbidden()); $result = $ability->execute(['menu_id' => 3, 'menu_rev' => self::rev(), 'items' => []]); @@ -102,7 +101,7 @@ final class MenuSetItemsAbilityTest extends TestCase Functions\when('wp_parse_url')->alias('parse_url'); Functions\expect('wp_update_nav_menu_item')->never(); Functions\expect('wp_delete_post')->never(); - $ability = new MenuSetItemsAbility(new MenuWriter(), new MenuOperations(), Mockery::mock(ChangeLog::class)); + $ability = new MenuSetItemsAbility(new MenuWriter(), new MenuOperations(), FakeChangeLog::forbidden()); $result = $ability->execute([ 'menu_id' => 3, 'menu_rev' => self::rev(), @@ -118,7 +117,7 @@ final class MenuSetItemsAbilityTest extends TestCase { $writer = Mockery::mock(MenuWriter::class); $writer->expects('apply')->never(); - $ability = new MenuSetItemsAbility($writer, new MenuOperations(), Mockery::mock(ChangeLog::class)); + $ability = new MenuSetItemsAbility($writer, new MenuOperations(), FakeChangeLog::forbidden()); $result = $ability->execute(['menu_id' => 3, 'menu_rev' => self::rev(), 'operations' => [['op' => 'move', 'id' => 99]]]); @@ -130,8 +129,7 @@ final class MenuSetItemsAbilityTest extends TestCase $writer = Mockery::mock(MenuWriter::class); $writer->allows('plan')->andReturn(['errors' => [], 'removals' => [], 'warnings' => []]); $writer->expects('apply')->andReturn(['created' => 1, 'updated' => 1, 'removed' => 0]); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->with('lit/menu-set-items', 'nav_menu', '3', Mockery::type('string'))->andReturn(21); + $log = new FakeChangeLog(21); $result = (new MenuSetItemsAbility($writer, new MenuOperations(), $log))->execute([ 'menu_id' => 3, 'menu_rev' => self::rev(), @@ -141,6 +139,7 @@ final class MenuSetItemsAbilityTest extends TestCase self::assertSame('Home', $result['previous_items'][0]['title']); self::assertSame(21, $result['log_id']); self::assertStringContainsString('1 neu, 1 geändert, 0 entfernt', $result['summary']); + $log->assertRecorded(['ability' => 'lit/menu-set-items', 'object_type' => 'nav_menu', 'object_id' => '3']); } public function testConfirmedRemovalsAreWrittenAndTheOldTreeIsReturned(): void @@ -151,8 +150,7 @@ final class MenuSetItemsAbilityTest extends TestCase ); $writer->expects('apply')->with(3, [], Mockery::type('array')) ->andReturn(['created' => 0, 'updated' => 0, 'removed' => 1]); - $log = Mockery::mock(ChangeLog::class); - $log->allows('record')->andReturn(22); + $log = new FakeChangeLog(22); $result = (new MenuSetItemsAbility($writer, new MenuOperations(), $log))->execute( ['menu_id' => 3, 'menu_rev' => self::rev(), 'items' => [], 'confirm' => true] @@ -169,8 +167,7 @@ final class MenuSetItemsAbilityTest extends TestCase Functions\when('wp_get_nav_menu_items')->justReturn($items); Functions\expect('wp_delete_post')->never(); Functions\expect('wp_update_nav_menu_item')->never(); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->never(); + $log = FakeChangeLog::forbidden(); $result = (new MenuSetItemsAbility(new MenuWriter(), new MenuOperations(), $log))->execute( ['menu_id' => 3, 'menu_rev' => self::rev(), 'items' => MenuTree::build($items)] @@ -187,7 +184,7 @@ final class MenuSetItemsAbilityTest extends TestCase $writer = Mockery::mock(MenuWriter::class); $writer->expects('plan')->with(Mockery::type('array'), Mockery::type('array'), true) ->andReturn(['errors' => ['items.0' => 'x'], 'removals' => [], 'warnings' => []]); - $ability = new MenuSetItemsAbility($writer, new MenuOperations(), Mockery::mock(ChangeLog::class)); + $ability = new MenuSetItemsAbility($writer, new MenuOperations(), FakeChangeLog::forbidden()); $result = $ability->execute( ['menu_id' => 3, 'menu_rev' => self::rev(), 'items' => [['type' => 'post', 'object_id' => 7]], 'allow_unpublished' => true] @@ -205,8 +202,7 @@ final class MenuSetItemsAbilityTest extends TestCase ); Functions\when('get_post_type')->justReturn('page'); Functions\expect('wp_update_nav_menu_item')->once()->andReturn(60); - $log = Mockery::mock(ChangeLog::class); - $log->allows('record')->andReturn(24); + $log = new FakeChangeLog(24); $ability = new MenuSetItemsAbility(new MenuWriter(), new MenuOperations(), $log); $input = ['menu_id' => 3, 'menu_rev' => self::rev(), 'operations' => [['op' => 'add', 'item' => ['type' => 'post', 'object_id' => 7]]]]; @@ -226,7 +222,7 @@ final class MenuSetItemsAbilityTest extends TestCase new \WP_Post(['ID' => 7, 'post_type' => 'page', 'post_status' => 'private', 'post_title' => 'Intern']) ); Functions\expect('wp_update_nav_menu_item')->never(); - $ability = new MenuSetItemsAbility(new MenuWriter(), new MenuOperations(), Mockery::mock(ChangeLog::class)); + $ability = new MenuSetItemsAbility(new MenuWriter(), new MenuOperations(), FakeChangeLog::forbidden()); $result = $ability->execute([ 'menu_id' => 3, 'menu_rev' => self::rev(), @@ -243,8 +239,7 @@ final class MenuSetItemsAbilityTest extends TestCase $writer = Mockery::mock(MenuWriter::class); $writer->allows('plan')->andReturn(['errors' => [], 'removals' => [], 'warnings' => []]); $writer->expects('apply')->andThrow(WriteFailed::rolledBack('Menü nicht gespeichert.')); - $log = Mockery::mock(ChangeLog::class); - $log->shouldNotReceive('recordPartial'); + $log = FakeChangeLog::forbidden(); $ability = new MenuSetItemsAbility($writer, new MenuOperations(), $log); $this->expectException(WriteFailed::class); @@ -256,12 +251,16 @@ final class MenuSetItemsAbilityTest extends TestCase $writer = Mockery::mock(MenuWriter::class); $writer->allows('plan')->andReturn(['errors' => [], 'removals' => [], 'warnings' => []]); $writer->expects('apply')->andThrow(WriteFailed::partial('Menü nicht gespeichert.')); - $log = Mockery::mock(ChangeLog::class); - $log->expects('recordPartial')->with('lit/menu-set-items', 'nav_menu', '3', Mockery::type('string')); + $log = FakeChangeLog::partialsOnly(); $ability = new MenuSetItemsAbility($writer, new MenuOperations(), $log); - $this->expectException(WriteFailed::class); - $ability->execute(['menu_id' => 3, 'menu_rev' => self::rev(), 'items' => [['id' => 10]]]); + try { + $ability->execute(['menu_id' => 3, 'menu_rev' => self::rev(), 'items' => [['id' => 10]]]); + self::fail('WriteFailed expected'); + } catch (WriteFailed $e) { + self::assertFalse($e->rolledBack); + } + $log->assertPartials(['ability' => 'lit/menu-set-items', 'object_type' => 'nav_menu', 'object_id' => '3']); } public function testAChangeLogFailureIsOnlyAWarning(): void @@ -269,8 +268,7 @@ final class MenuSetItemsAbilityTest extends TestCase $writer = Mockery::mock(MenuWriter::class); $writer->allows('plan')->andReturn(['errors' => [], 'removals' => [], 'warnings' => ['"X" ist nicht veröffentlicht']]); $writer->allows('apply')->andReturn(['created' => 0, 'updated' => 1, 'removed' => 0]); - $log = Mockery::mock(ChangeLog::class); - $log->allows('record')->andThrow(new RuntimeException('db down')); + $log = new FakeChangeLog(1, new RuntimeException('db down')); $file = (string) tempnam(sys_get_temp_dir(), 'lit'); $errorLog = ini_set('error_log', $file); @@ -290,14 +288,14 @@ final class MenuSetItemsAbilityTest extends TestCase public function testPermissionNeedsThemeOptionRights(): void { Functions\when('current_user_can')->justReturn(false); - $ability = new MenuSetItemsAbility(new MenuWriter(), new MenuOperations(), Mockery::mock(ChangeLog::class)); + $ability = new MenuSetItemsAbility(new MenuWriter(), new MenuOperations(), FakeChangeLog::forbidden()); self::assertSame('lit_forbidden', $ability->permission(['menu_id' => 3, 'menu_rev' => self::rev()])->get_error_code()); } public function testIsDestructiveAndDescribesWhenConfirmIsNeeded(): void { - $ability = new MenuSetItemsAbility(new MenuWriter(), new MenuOperations(), Mockery::mock(ChangeLog::class)); + $ability = new MenuSetItemsAbility(new MenuWriter(), new MenuOperations(), FakeChangeLog::forbidden()); self::assertSame(['readonly' => false, 'destructive' => true, 'idempotent' => false], $ability->annotations()); self::assertStringContainsString('confirm: true', $ability->description()); diff --git a/tests/Unit/Integrations/WordPress/PostChangeResultTest.php b/tests/Unit/Integrations/WordPress/PostChangeResultTest.php index f7463e1..4372fe0 100644 --- a/tests/Unit/Integrations/WordPress/PostChangeResultTest.php +++ b/tests/Unit/Integrations/WordPress/PostChangeResultTest.php @@ -7,10 +7,9 @@ namespace LIT\MCP\Tests\Unit\Integrations\WordPress; use Brain\Monkey\Actions; use Brain\Monkey\Functions; use LIT\MCP\Abilities\Rev; -use LIT\MCP\ChangeLog\ChangeLog; use LIT\MCP\Integrations\WordPress\PostChangeResult; +use LIT\MCP\Tests\Support\FakeChangeLog; use LIT\MCP\Tests\Unit\TestCase; -use Mockery; use RuntimeException; use WP_Post; @@ -38,8 +37,7 @@ final class PostChangeResultTest extends TestCase $post = new WP_Post(['ID' => 5, 'post_title' => 'Team']); Actions\expectDone('lit_mcp_after_save')->once()->with(5, 'lit/content-update', ['post_id' => 5]); Functions\when('get_post')->justReturn($post); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->with('lit/content-update', 'page', '5', 'geändert', 811)->andReturn(9); + $log = new FakeChangeLog(9); $change = new PostChangeResult('lit/content-update', $log, 5, 'die Änderung', ['vorher']); $change->afterSave(['post_id' => 5]); @@ -52,6 +50,13 @@ final class PostChangeResultTest extends TestCase self::assertSame(Rev::ofPost($post), $result['rev']); self::assertSame(9, $result['log_id']); self::assertSame(['vorher'], $result['warnings']); + $log->assertRecorded([ + 'ability' => 'lit/content-update', + 'object_type' => 'page', + 'object_id' => '5', + 'summary' => 'geändert', + 'revision_id' => 811, + ]); } public function testFailuresAfterTheWriteBecomeWarnings(): void @@ -60,8 +65,7 @@ final class PostChangeResultTest extends TestCase throw new RuntimeException('listener kaputt'); }); Functions\when('get_post')->justReturn(null); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->andThrow(new RuntimeException('log kaputt')); + $log = new FakeChangeLog(1, new RuntimeException('log kaputt')); $fallback = new WP_Post(['ID' => 5]); $change = new PostChangeResult('lit/post-publish', $log, 5); @@ -77,13 +81,14 @@ final class PostChangeResultTest extends TestCase 'Änderungsprotokoll konnte nicht geschrieben werden; die Änderung ist gespeichert.', ], $result['warnings']); self::assertStringContainsString('listener kaputt', (string) file_get_contents($this->errorLog)); + $log->assertRecorded([]); } public function testWithoutAReadablePostTheRevIsNull(): void { Functions\when('get_post')->justReturn(null); - $change = new PostChangeResult('lit/content-create', Mockery::mock(ChangeLog::class), 5, 'der Entwurf'); + $change = new PostChangeResult('lit/content-create', FakeChangeLog::forbidden(), 5, 'der Entwurf'); self::assertNull($change->reread()); self::assertNull($change->result('ok', null)->toArray()['rev']); diff --git a/tests/Unit/Integrations/WordPress/Render/PageRenderSummaryAbilityTest.php b/tests/Unit/Integrations/WordPress/Render/PageRenderSummaryAbilityTest.php index 8621c4d..62d4d8b 100644 --- a/tests/Unit/Integrations/WordPress/Render/PageRenderSummaryAbilityTest.php +++ b/tests/Unit/Integrations/WordPress/Render/PageRenderSummaryAbilityTest.php @@ -8,7 +8,7 @@ use Brain\Monkey\Functions; use LIT\MCP\Integrations\WordPress\Render\PageRenderer; use LIT\MCP\Integrations\WordPress\Render\PageRenderSummaryAbility; use LIT\MCP\Integrations\WordPress\Render\RenderSummary; -use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; +use LIT\MCP\Tests\Support\SchemaFixture; use LIT\MCP\Tests\Unit\TestCase; use Mockery; use WP_Post; @@ -123,8 +123,7 @@ final class PageRenderSummaryAbilityTest extends TestCase Functions\when('get_post')->justReturn($post); Functions\when('current_user_can')->justReturn(true); Functions\when('get_permalink')->justReturn('https://k.test/start/'); - $schema = Mockery::mock(ContentSchema::class); - $schema->allows('isManaged')->andReturn(true); + $schema = SchemaFixture::managed(); $renderer = Mockery::mock(PageRenderer::class); $renderer->expects('render')->with($post)->andReturn(self::HTML); $ability = new PageRenderSummaryAbility($schema, $renderer); diff --git a/tests/Unit/Integrations/WordPress/Revisions/RevisionRestoreAbilityTest.php b/tests/Unit/Integrations/WordPress/Revisions/RevisionRestoreAbilityTest.php index 7da15f0..9a6e110 100644 --- a/tests/Unit/Integrations/WordPress/Revisions/RevisionRestoreAbilityTest.php +++ b/tests/Unit/Integrations/WordPress/Revisions/RevisionRestoreAbilityTest.php @@ -9,9 +9,9 @@ use Brain\Monkey\Functions; use LIT\MCP\Abilities\Rev; use LIT\MCP\ChangeLog\ChangeLog; use LIT\MCP\Integrations\WordPress\Revisions\RevisionRestoreAbility; -use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; +use LIT\MCP\Tests\Support\FakeChangeLog; +use LIT\MCP\Tests\Support\SchemaFixture; use LIT\MCP\Tests\Unit\TestCase; -use Mockery; use RuntimeException; use WP_Post; @@ -38,8 +38,7 @@ final class RevisionRestoreAbilityTest extends TestCase private function ability(ChangeLog $log): RevisionRestoreAbility { - $schema = Mockery::mock(ContentSchema::class); - $schema->allows('isManaged')->andReturn(true); + $schema = SchemaFixture::managed(); return new RevisionRestoreAbility($schema, $log); } @@ -51,14 +50,14 @@ final class RevisionRestoreAbilityTest extends TestCase public function testNonRevisionIdIsNotFound(): void { - self::assertSame('lit_not_found', $this->ability(Mockery::mock(ChangeLog::class))->permission(['revision_id' => 5])->get_error_code()); + self::assertSame('lit_not_found', $this->ability(FakeChangeLog::forbidden())->permission(['revision_id' => 5])->get_error_code()); } public function testRequiresConfirmation(): void { Functions\expect('wp_restore_post_revision')->never(); - $result = $this->ability(Mockery::mock(ChangeLog::class))->execute(['revision_id' => 810]); + $result = $this->ability(FakeChangeLog::forbidden())->execute(['revision_id' => 810]); self::assertSame('lit_confirm_required', $result->get_error_code()); } @@ -69,7 +68,7 @@ final class RevisionRestoreAbilityTest extends TestCase ? new WP_Post(['ID' => 810, 'post_type' => 'revision', 'post_parent' => 0]) : null); - $error = $this->ability(Mockery::mock(ChangeLog::class)) + $error = $this->ability(FakeChangeLog::forbidden()) ->permission(['revision_id' => 810, 'rev' => 'irrelevant', 'confirm' => true]); self::assertSame('lit_validation_failed', $error->get_error_code()); @@ -85,7 +84,7 @@ final class RevisionRestoreAbilityTest extends TestCase }); Functions\expect('wp_restore_post_revision')->never(); - $error = $this->ability(Mockery::mock(ChangeLog::class))->execute(['revision_id' => 810, 'rev' => 'x', 'confirm' => true]); + $error = $this->ability(FakeChangeLog::forbidden())->execute(['revision_id' => 810, 'rev' => 'x', 'confirm' => true]); self::assertSame('lit_validation_failed', $error->get_error_code()); self::assertArrayHasKey('revision_id', $error->get_error_data()['fields']); @@ -105,7 +104,7 @@ final class RevisionRestoreAbilityTest extends TestCase Functions\expect('wp_save_post_revision')->never(); Functions\expect('wp_restore_post_revision')->never(); - $error = $this->ability(Mockery::mock(ChangeLog::class))->execute([ + $error = $this->ability(FakeChangeLog::forbidden())->execute([ 'revision_id' => 810, 'rev' => Rev::ofPost(new WP_Post($current)), 'confirm' => true, @@ -119,7 +118,7 @@ final class RevisionRestoreAbilityTest extends TestCase { Functions\expect('wp_restore_post_revision')->never(); - $result = $this->ability(Mockery::mock(ChangeLog::class)) + $result = $this->ability(FakeChangeLog::forbidden()) ->execute(['revision_id' => 810, 'confirm' => true, 'rev' => 'stale']); self::assertSame('lit_rev_mismatch', $result->get_error_code()); @@ -139,8 +138,7 @@ final class RevisionRestoreAbilityTest extends TestCase }); Functions\when('get_preview_post_link')->justReturn('https://kunde.de/team/'); Functions\when('get_edit_post_link')->justReturn('https://kunde.de/wp-admin/post.php?post=5&action=edit'); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->with('lit/revision-restore', 'page', '5', Mockery::type('string'), 813)->andReturn(12); + $log = new FakeChangeLog(12); $result = $this->ability($log)->execute([ 'revision_id' => 810, @@ -154,6 +152,12 @@ final class RevisionRestoreAbilityTest extends TestCase self::assertStringContainsString('2026-09-20 10:00:00', $result['summary']); self::assertStringContainsString('Status unverändert: publish', $result['summary']); self::assertSame([], $result['warnings']); + $log->assertRecorded([ + 'ability' => 'lit/revision-restore', + 'object_type' => 'page', + 'object_id' => '5', + 'revision_id' => 813, + ]); } public function testRestoreFailureIsWriteFailed(): void @@ -161,8 +165,7 @@ final class RevisionRestoreAbilityTest extends TestCase Functions\when('wp_save_post_revision')->justReturn(813); Functions\when('wp_get_post_revisions')->justReturn([813 => new WP_Post(['ID' => 813])]); Functions\when('wp_restore_post_revision')->justReturn(false); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->never(); + $log = FakeChangeLog::forbidden(); $result = $this->ability($log)->execute([ 'revision_id' => 810, @@ -188,8 +191,7 @@ final class RevisionRestoreAbilityTest extends TestCase Actions\expectDone('lit_mcp_after_save')->once()->whenHappen(static function (): void { throw new RuntimeException('listener kaputt'); }); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->andThrow(new RuntimeException('db weg')); + $log = new FakeChangeLog(1, new RuntimeException('db weg')); $file = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log'); $previous = ini_set('error_log', $file); @@ -208,5 +210,6 @@ final class RevisionRestoreAbilityTest extends TestCase self::assertCount(2, $result['warnings']); self::assertStringContainsString('listener kaputt', (string) file_get_contents($file)); unlink($file); + $log->assertRecorded([]); } } diff --git a/tests/Unit/Integrations/WordPress/Revisions/RevisionsListAbilityTest.php b/tests/Unit/Integrations/WordPress/Revisions/RevisionsListAbilityTest.php index 7bdafdc..8474782 100644 --- a/tests/Unit/Integrations/WordPress/Revisions/RevisionsListAbilityTest.php +++ b/tests/Unit/Integrations/WordPress/Revisions/RevisionsListAbilityTest.php @@ -8,9 +8,8 @@ use Brain\Monkey\Functions; use LIT\MCP\Abilities\Cursor; use LIT\MCP\Integrations\WordPress\Content\PostPresenter; use LIT\MCP\Integrations\WordPress\Revisions\RevisionsListAbility; -use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; +use LIT\MCP\Tests\Support\SchemaFixture; use LIT\MCP\Tests\Unit\TestCase; -use Mockery; use WP_Post; final class RevisionsListAbilityTest extends TestCase @@ -27,8 +26,7 @@ final class RevisionsListAbilityTest extends TestCase private function ability(): RevisionsListAbility { - $schema = Mockery::mock(ContentSchema::class); - $schema->allows('isManaged')->andReturn(true); + $schema = SchemaFixture::managed(); return new RevisionsListAbility($schema); } diff --git a/tests/Unit/Integrations/WordPress/Settings/WpSettingsAbilitiesTest.php b/tests/Unit/Integrations/WordPress/Settings/WpSettingsAbilitiesTest.php index f55ca31..01382d0 100644 --- a/tests/Unit/Integrations/WordPress/Settings/WpSettingsAbilitiesTest.php +++ b/tests/Unit/Integrations/WordPress/Settings/WpSettingsAbilitiesTest.php @@ -6,10 +6,10 @@ namespace LIT\MCP\Tests\Unit\Integrations\WordPress\Settings; use Brain\Monkey\Functions; use LIT\MCP\Abilities\WriteFailed; -use LIT\MCP\ChangeLog\ChangeLog; use LIT\MCP\Integrations\WordPress\Settings\SettingsWhitelist; use LIT\MCP\Integrations\WordPress\Settings\WpSettingsGetAbility; use LIT\MCP\Integrations\WordPress\Settings\WpSettingsUpdateAbility; +use LIT\MCP\Tests\Support\FakeChangeLog; use LIT\MCP\Tests\Unit\TestCase; use Mockery; use RuntimeException; @@ -53,8 +53,7 @@ final class WpSettingsAbilitiesTest extends TestCase { $options = ['blogname' => 'Alt', 'blogdescription' => 'Praxis']; $this->optionStore($options); - $log = Mockery::mock(ChangeLog::class); - $log->allows('record')->andReturn(40); + $log = new FakeChangeLog(40); $ability = new WpSettingsUpdateAbility(new SettingsWhitelist(), $log); $input = ['settings' => ['blogname' => 'Müller & Söhne', 'blogdescription' => 'Zahnärzte']]; @@ -81,8 +80,7 @@ final class WpSettingsAbilitiesTest extends TestCase { $options = ['blogname' => 'Alt']; $this->optionStore($options); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->andThrow(new RuntimeException('log kaputt')); + $log = new FakeChangeLog(1, new RuntimeException('log kaputt')); $file = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log'); $previous = ini_set('error_log', $file); @@ -96,6 +94,7 @@ final class WpSettingsAbilitiesTest extends TestCase self::assertTrue($result['ok']); self::assertNull($result['log_id']); self::assertCount(1, $result['warnings']); + $log->assertRecorded([]); } protected function tearDown(): void @@ -118,7 +117,7 @@ final class WpSettingsAbilitiesTest extends TestCase { Functions\expect('update_option')->never(); - $result = (new WpSettingsUpdateAbility(new SettingsWhitelist(), Mockery::mock(ChangeLog::class))) + $result = (new WpSettingsUpdateAbility(new SettingsWhitelist(), FakeChangeLog::forbidden())) ->execute(['settings' => ['blogname' => 'Neu', 'admin_email' => 'x@y.de']]); self::assertSame('lit_validation_failed', $result->get_error_code()); @@ -129,7 +128,7 @@ final class WpSettingsAbilitiesTest extends TestCase { Functions\expect('update_option')->never(); - $result = (new WpSettingsUpdateAbility(new SettingsWhitelist(), Mockery::mock(ChangeLog::class))) + $result = (new WpSettingsUpdateAbility(new SettingsWhitelist(), FakeChangeLog::forbidden())) ->execute(['settings' => ['template' => 'twentyten']]); self::assertSame('lit_validation_failed', $result->get_error_code()); @@ -162,8 +161,7 @@ final class WpSettingsAbilitiesTest extends TestCase }); $GLOBALS['wp_rewrite'] = $rewrite; Functions\expect('flush_rewrite_rules')->once(); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->with('lit/wp-settings-update', 'option', 'blogname,permalink_structure', Mockery::type('string'))->andReturn(30); + $log = new FakeChangeLog(30); $result = (new WpSettingsUpdateAbility(new SettingsWhitelist(), $log))->execute(['settings' => [ 'blogname' => 'Neu', @@ -174,6 +172,11 @@ final class WpSettingsAbilitiesTest extends TestCase self::assertSame(['blogname', 'permalink_structure'], array_column($result['changed'], 'id')); self::assertSame(['blogname' => 'Alt', 'blogdescription' => 'Gleich', 'permalink_structure' => ''], $result['previous']); self::assertSame(30, $result['log_id']); + $log->assertRecorded([ + 'ability' => 'lit/wp-settings-update', + 'object_type' => 'option', + 'object_id' => 'blogname,permalink_structure', + ]); } public function testDoesNotTreatAnAlreadyStoredValueAsAWriteFailure(): void @@ -190,8 +193,7 @@ final class WpSettingsAbilitiesTest extends TestCase 'WPLANG' => '', ][$key]); Functions\expect('update_option')->once()->with('blogname', 'Neu')->andReturn(true); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->andReturn(31); + $log = new FakeChangeLog(31); $result = (new WpSettingsUpdateAbility(new SettingsWhitelist(), $log))->execute(['settings' => [ 'blogname' => 'Neu', @@ -200,6 +202,7 @@ final class WpSettingsAbilitiesTest extends TestCase self::assertSame(['blogname'], array_column($result['changed'], 'id')); self::assertSame(31, $result['log_id']); + $log->assertRecorded([]); } public function testRestoresPreviousValuesWhenAWriteFailsPartway(): void @@ -211,9 +214,7 @@ final class WpSettingsAbilitiesTest extends TestCase Functions\expect('update_option')->once()->with('blogname', 'Neu')->andReturn(true); Functions\expect('update_option')->once()->with('blogdescription', 'Auch neu')->andReturn(false); Functions\expect('update_option')->once()->with('blogname', 'Alt')->andReturn(true); - $log = Mockery::mock(ChangeLog::class); - $log->shouldNotReceive('record'); - $log->shouldNotReceive('recordPartial'); + $log = FakeChangeLog::forbidden(); $error = $this->failure(new WpSettingsUpdateAbility(new SettingsWhitelist(), $log), [ 'blogname' => 'Neu', @@ -221,6 +222,8 @@ final class WpSettingsAbilitiesTest extends TestCase ]); self::assertSame('lit_write_failed', $error->get_error_code()); + $log->assertRecorded(); + $log->assertPartials(); } public function testReportsPartialWriteWhenRestoringAlsoFails(): void @@ -242,9 +245,7 @@ final class WpSettingsAbilitiesTest extends TestCase }); Functions\expect('update_option')->once()->with('blogdescription', 'Auch neu')->andReturn(false); Functions\expect('update_option')->once()->with('blogname', 'Alt')->andReturn(false); - $log = Mockery::mock(ChangeLog::class); - $log->shouldNotReceive('record'); - $log->expects('recordPartial')->with('lit/wp-settings-update', 'option', 'blogname', Mockery::type('string')); + $log = FakeChangeLog::partialsOnly(); $error = $this->failure(new WpSettingsUpdateAbility(new SettingsWhitelist(), $log), [ 'blogname' => 'Neu', @@ -254,6 +255,11 @@ final class WpSettingsAbilitiesTest extends TestCase self::assertSame('lit_partial_write', $error->get_error_code()); self::assertStringContainsString('blogname', $error->get_error_message()); self::assertStringContainsString('lit/wp-settings-get', $error->get_error_data()['hint']); + $log->assertPartials([ + 'ability' => 'lit/wp-settings-update', + 'object_type' => 'option', + 'object_id' => 'blogname', + ]); } /** @param array $settings */ @@ -275,7 +281,7 @@ final class WpSettingsAbilitiesTest extends TestCase self::assertSame('lit_forbidden', (new WpSettingsGetAbility(new SettingsWhitelist()))->permission([])->get_error_code()); self::assertSame( 'lit_forbidden', - (new WpSettingsUpdateAbility(new SettingsWhitelist(), Mockery::mock(ChangeLog::class)))->permission([])->get_error_code() + (new WpSettingsUpdateAbility(new SettingsWhitelist(), FakeChangeLog::forbidden()))->permission([])->get_error_code() ); } } diff --git a/tests/Unit/Integrations/WordPress/Status/PostDuplicateAbilityTest.php b/tests/Unit/Integrations/WordPress/Status/PostDuplicateAbilityTest.php index 7585c8a..4a6ee0f 100644 --- a/tests/Unit/Integrations/WordPress/Status/PostDuplicateAbilityTest.php +++ b/tests/Unit/Integrations/WordPress/Status/PostDuplicateAbilityTest.php @@ -8,9 +8,10 @@ use Brain\Monkey\Actions; use Brain\Monkey\Functions; use LIT\MCP\Abilities\Rev; use LIT\MCP\ChangeLog\ChangeLog; -use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; use LIT\MCP\Integrations\WordPress\Status\PostCloner; use LIT\MCP\Integrations\WordPress\Status\PostDuplicateAbility; +use LIT\MCP\Tests\Support\FakeChangeLog; +use LIT\MCP\Tests\Support\SchemaFixture; use LIT\MCP\Tests\Unit\TestCase; use Mockery; use WP_Post; @@ -34,8 +35,7 @@ final class PostDuplicateAbilityTest extends TestCase private function ability(PostCloner $cloner, ChangeLog $log): PostDuplicateAbility { - $schema = Mockery::mock(ContentSchema::class); - $schema->allows('isManaged')->andReturn(true); + $schema = SchemaFixture::managed(); return new PostDuplicateAbility($schema, $cloner, $log); } @@ -45,8 +45,7 @@ final class PostDuplicateAbilityTest extends TestCase $cloner = Mockery::mock(PostCloner::class); $cloner->expects('duplicate')->with($this->original)->andReturn(77); Actions\expectDone('lit_mcp_after_save')->once()->with(77, 'lit/post-duplicate', ['post_id' => 5]); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->with('lit/post-duplicate', 'page', '77', 'Kopie von "Team" (ID 5) angelegt', null)->andReturn(8); + $log = new FakeChangeLog(8); $result = $this->ability($cloner, $log)->execute(['post_id' => 5]); @@ -55,6 +54,13 @@ final class PostDuplicateAbilityTest extends TestCase self::assertSame(8, $result['log_id']); self::assertSame(Rev::ofPost($this->copy), $result['rev']); self::assertSame([], $result['warnings']); + $log->assertRecorded([ + 'ability' => 'lit/post-duplicate', + 'object_type' => 'page', + 'object_id' => '77', + 'summary' => 'Kopie von "Team" (ID 5) angelegt', + 'revision_id' => null, + ]); } public function testNeedsTheRightToCreateEntriesOfThatType(): void @@ -63,7 +69,7 @@ final class PostDuplicateAbilityTest extends TestCase $cloner = Mockery::mock(PostCloner::class); $cloner->expects('duplicate')->never(); - $result = $this->ability($cloner, Mockery::mock(ChangeLog::class))->execute(['post_id' => 5]); + $result = $this->ability($cloner, FakeChangeLog::forbidden())->execute(['post_id' => 5]); self::assertSame('lit_forbidden', $result->get_error_code()); } @@ -74,14 +80,14 @@ final class PostDuplicateAbilityTest extends TestCase $cloner = Mockery::mock(PostCloner::class); $cloner->expects('duplicate')->never(); - $result = $this->ability($cloner, Mockery::mock(ChangeLog::class))->execute(['post_id' => 5]); + $result = $this->ability($cloner, FakeChangeLog::forbidden())->execute(['post_id' => 5]); self::assertSame('lit_validation_failed', $result->get_error_code()); } public function testIsIndirectAndNotIdempotent(): void { - $ability = $this->ability(Mockery::mock(PostCloner::class), Mockery::mock(ChangeLog::class)); + $ability = $this->ability(Mockery::mock(PostCloner::class), FakeChangeLog::forbidden()); self::assertFalse($ability->isDirect()); self::assertFalse($ability->annotations()['idempotent']); diff --git a/tests/Unit/Integrations/WordPress/Status/PostPublishAbilityTest.php b/tests/Unit/Integrations/WordPress/Status/PostPublishAbilityTest.php index f59eeac..024ae10 100644 --- a/tests/Unit/Integrations/WordPress/Status/PostPublishAbilityTest.php +++ b/tests/Unit/Integrations/WordPress/Status/PostPublishAbilityTest.php @@ -7,10 +7,10 @@ namespace LIT\MCP\Tests\Unit\Integrations\WordPress\Status; use Brain\Monkey\Functions; use LIT\MCP\Abilities\WriteFailed; use LIT\MCP\ChangeLog\ChangeLog; -use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; use LIT\MCP\Integrations\WordPress\Status\PostPublishAbility; +use LIT\MCP\Tests\Support\FakeChangeLog; +use LIT\MCP\Tests\Support\SchemaFixture; use LIT\MCP\Tests\Unit\TestCase; -use Mockery; use RuntimeException; use WP_Post; @@ -18,8 +18,7 @@ final class PostPublishAbilityTest extends TestCase { private function ability(ChangeLog $log): PostPublishAbility { - $schema = Mockery::mock(ContentSchema::class); - $schema->allows('isManaged')->andReturn(true); + $schema = SchemaFixture::managed(); return new PostPublishAbility($schema, $log); } @@ -37,14 +36,20 @@ final class PostPublishAbilityTest extends TestCase $this->stubPost(['post_status' => 'draft', 'post_date_gmt' => '0000-00-00 00:00:00']); Functions\expect('wp_update_post')->once()->with(['ID' => 5, 'post_status' => 'publish'], true)->andReturn(5); Functions\when('get_post_status')->justReturn('publish'); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->with('lit/post-publish', 'page', '5', '"Team" veröffentlicht', null)->andReturn(9); + $log = new FakeChangeLog(9); $result = $this->ability($log)->execute(['post_id' => 5]); self::assertSame('published', $result['changed'][0]['action']); self::assertSame(9, $result['log_id']); self::assertSame('https://kunde.de/team/', $result['preview_url']); + $log->assertRecorded([ + 'ability' => 'lit/post-publish', + 'object_type' => 'page', + 'object_id' => '5', + 'summary' => '"Team" veröffentlicht', + 'revision_id' => null, + ]); } public function testScheduledPostIsPublishedNowInsteadOfStayingFuture(): void @@ -59,8 +64,7 @@ final class PostPublishAbilityTest extends TestCase 'edit_date' => true, ], true)->andReturn(5); Functions\when('get_post_status')->justReturn('publish'); - $log = Mockery::mock(ChangeLog::class); - $log->allows('record')->andReturn(1); + $log = new FakeChangeLog(1); $this->ability($log)->execute(['post_id' => 5]); } @@ -70,7 +74,7 @@ final class PostPublishAbilityTest extends TestCase $this->stubPost(['post_status' => 'publish']); Functions\expect('wp_update_post')->never(); - $result = $this->ability(Mockery::mock(ChangeLog::class))->execute(['post_id' => 5]); + $result = $this->ability(FakeChangeLog::forbidden())->execute(['post_id' => 5]); self::assertSame([], $result['changed']); self::assertStringContainsString('bereits veröffentlicht', $result['summary']); @@ -82,8 +86,7 @@ final class PostPublishAbilityTest extends TestCase Functions\when('wp_update_post')->alias(static function (): void { throw new RuntimeException('save_post-Hook kaputt'); }); - $log = Mockery::mock(ChangeLog::class); - $log->expects('recordPartial')->with('lit/post-publish', 'page', '5', Mockery::type('string')); + $log = FakeChangeLog::partialsOnly(); try { $this->ability($log)->execute(['post_id' => 5]); @@ -94,6 +97,7 @@ final class PostPublishAbilityTest extends TestCase self::assertStringContainsString('save_post-Hook kaputt', $e->getMessage()); self::assertSame('lit_partial_write', $e->toError()->get_error_code()); } + $log->assertPartials(['ability' => 'lit/post-publish', 'object_type' => 'page', 'object_id' => '5']); } public function testATrashedPostIsRefused(): void @@ -101,7 +105,7 @@ final class PostPublishAbilityTest extends TestCase $this->stubPost(['post_status' => 'trash']); Functions\expect('wp_update_post')->never(); - $result = $this->ability(Mockery::mock(ChangeLog::class))->execute(['post_id' => 5]); + $result = $this->ability(FakeChangeLog::forbidden())->execute(['post_id' => 5]); self::assertSame('lit_validation_failed', $result->get_error_code()); } @@ -111,6 +115,6 @@ final class PostPublishAbilityTest extends TestCase $this->stubPost(['post_status' => 'draft']); Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap !== 'publish_post'); - self::assertSame('lit_forbidden', $this->ability(Mockery::mock(ChangeLog::class))->permission(['post_id' => 5])->get_error_code()); + self::assertSame('lit_forbidden', $this->ability(FakeChangeLog::forbidden())->permission(['post_id' => 5])->get_error_code()); } } diff --git a/tests/Unit/Integrations/WordPress/Status/PostReplaceWithCopyAbilityTest.php b/tests/Unit/Integrations/WordPress/Status/PostReplaceWithCopyAbilityTest.php index e7d9310..cd84fe7 100644 --- a/tests/Unit/Integrations/WordPress/Status/PostReplaceWithCopyAbilityTest.php +++ b/tests/Unit/Integrations/WordPress/Status/PostReplaceWithCopyAbilityTest.php @@ -8,10 +8,11 @@ use Brain\Monkey\Functions; use LIT\MCP\Abilities\Rev; use LIT\MCP\Abilities\WriteFailed; use LIT\MCP\ChangeLog\ChangeLog; -use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; use LIT\MCP\Integrations\WordPress\Status\PostCloner; use LIT\MCP\Integrations\WordPress\Status\PostReplaceWithCopyAbility; use LIT\MCP\Integrations\WordPress\Status\ReplaceFailed; +use LIT\MCP\Tests\Support\FakeChangeLog; +use LIT\MCP\Tests\Support\SchemaFixture; use LIT\MCP\Tests\Unit\TestCase; use Mockery; use RuntimeException; @@ -39,8 +40,7 @@ final class PostReplaceWithCopyAbilityTest extends TestCase private function ability(PostCloner $cloner, ChangeLog $log, bool $trash = true): PostReplaceWithCopyAbility { - $schema = Mockery::mock(ContentSchema::class); - $schema->allows('isManaged')->andReturn(true); + $schema = SchemaFixture::managed(); return new PostReplaceWithCopyAbility($schema, $cloner, $log, static fn (): bool => $trash); } @@ -65,7 +65,7 @@ final class PostReplaceWithCopyAbilityTest extends TestCase $cloner = Mockery::mock(PostCloner::class); $cloner->expects('replace')->never(); - $result = $this->ability($cloner, Mockery::mock(ChangeLog::class)) + $result = $this->ability($cloner, FakeChangeLog::forbidden()) ->execute(['original_id' => 5, 'copy_id' => 77, 'rev' => Rev::ofPost($this->original)]); self::assertSame('lit_confirm_required', $result->get_error_code()); @@ -77,7 +77,7 @@ final class PostReplaceWithCopyAbilityTest extends TestCase $cloner = Mockery::mock(PostCloner::class); $cloner->expects('replace')->never(); - $result = $this->ability($cloner, Mockery::mock(ChangeLog::class))->execute($this->input()); + $result = $this->ability($cloner, FakeChangeLog::forbidden())->execute($this->input()); self::assertSame('lit_validation_failed', $result->get_error_code()); } @@ -101,7 +101,7 @@ final class PostReplaceWithCopyAbilityTest extends TestCase $cloner->expects('backup')->never(); $cloner->expects('replace')->never(); - $result = $this->ability($cloner, Mockery::mock(ChangeLog::class))->execute($this->input()); + $result = $this->ability($cloner, FakeChangeLog::forbidden())->execute($this->input()); self::assertSame('lit_validation_failed', $result->get_error_code()); } @@ -114,7 +114,7 @@ final class PostReplaceWithCopyAbilityTest extends TestCase $cloner->expects('backup')->never(); $cloner->expects('replace')->never(); - $result = $this->ability($cloner, Mockery::mock(ChangeLog::class))->execute($this->input()); + $result = $this->ability($cloner, FakeChangeLog::forbidden())->execute($this->input()); self::assertSame('lit_validation_failed', $result->get_error_code()); self::assertArrayHasKey('original_id', $result->get_error_data()['fields']); @@ -129,8 +129,7 @@ final class PostReplaceWithCopyAbilityTest extends TestCase $cloner->allows('backup')->andReturn(88); $cloner->expects('replace'); Functions\when('wp_trash_post')->justReturn(new WP_Post(['ID' => 77])); - $log = Mockery::mock(ChangeLog::class); - $log->allows('record')->andReturn(11); + $log = new FakeChangeLog(11); self::assertTrue($this->ability($cloner, $log)->execute($this->input())['ok']); } @@ -141,7 +140,7 @@ final class PostReplaceWithCopyAbilityTest extends TestCase $cloner->expects('replace')->never(); Functions\expect('wp_save_post_revision')->never(); - $result = $this->ability($cloner, Mockery::mock(ChangeLog::class))->execute($this->input(['rev' => 'veraltet'])); + $result = $this->ability($cloner, FakeChangeLog::forbidden())->execute($this->input(['rev' => 'veraltet'])); self::assertSame('lit_rev_mismatch', $result->get_error_code()); } @@ -151,7 +150,7 @@ final class PostReplaceWithCopyAbilityTest extends TestCase $cloner = Mockery::mock(PostCloner::class); $cloner->expects('replace')->never(); - $result = $this->ability($cloner, Mockery::mock(ChangeLog::class), false)->execute($this->input()); + $result = $this->ability($cloner, FakeChangeLog::forbidden(), false)->execute($this->input()); self::assertSame('lit_unsupported', $result->get_error_code()); } @@ -181,8 +180,7 @@ final class PostReplaceWithCopyAbilityTest extends TestCase $order[] = 'trash'; return new WP_Post(['ID' => 77]); }); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->with('lit/post-replace-with-copy', 'page', '5', Mockery::type('string'), 900)->andReturn(11); + $log = new FakeChangeLog(11); $result = $this->ability($cloner, $log)->execute($this->input()); @@ -194,6 +192,12 @@ final class PostReplaceWithCopyAbilityTest extends TestCase self::assertSame(Rev::ofPost($this->original), $result['rev']); self::assertSame(['replaced', 'trashed'], array_column($result['changed'], 'action')); self::assertSame([], $result['warnings']); + $log->assertRecorded([ + 'ability' => 'lit/post-replace-with-copy', + 'object_type' => 'page', + 'object_id' => '5', + 'revision_id' => 900, + ]); } public function testAFailedTrashOfTheCopyIsAWarningOnly(): void @@ -204,8 +208,7 @@ final class PostReplaceWithCopyAbilityTest extends TestCase $cloner->allows('backup')->andReturn(88); $cloner->expects('replace'); Functions\when('wp_trash_post')->justReturn(false); - $log = Mockery::mock(ChangeLog::class); - $log->allows('record')->andReturn(11); + $log = new FakeChangeLog(11); $result = $this->ability($cloner, $log)->execute($this->input()); @@ -225,7 +228,7 @@ final class PostReplaceWithCopyAbilityTest extends TestCase Functions\expect('wp_trash_post')->never(); try { - $this->ability($cloner, Mockery::mock(ChangeLog::class))->execute($this->input()); + $this->ability($cloner, FakeChangeLog::forbidden())->execute($this->input()); self::fail('WriteFailed expected'); } catch (WriteFailed $e) { self::assertTrue($e->rolledBack); @@ -246,10 +249,7 @@ final class PostReplaceWithCopyAbilityTest extends TestCase $cloner->expects('replace')->andThrow(new RuntimeException('hook kaputt')); $cloner->expects('restore')->with($this->original, self::SNAPSHOT, PostCloner::PARTS)->andThrow(new RuntimeException('Restore incomplete: terms')); Functions\expect('wp_trash_post')->never(); - $log = Mockery::mock(ChangeLog::class); - $log->expects('recordPartial') - ->with('lit/post-replace-with-copy', 'page', '5', Mockery::type('string'), 900) - ->andReturn(12); + $log = FakeChangeLog::partialsOnly(); try { $this->ability($cloner, $log)->execute($this->input()); @@ -264,6 +264,12 @@ final class PostReplaceWithCopyAbilityTest extends TestCase self::assertStringContainsString('Sicherung', $error->get_error_message()); self::assertStringContainsString('88', $error->get_error_message()); } + $log->assertPartials([ + 'ability' => 'lit/post-replace-with-copy', + 'object_type' => 'page', + 'object_id' => '5', + 'revision_id' => 900, + ]); } public function testNeedsTheRightToCreateEntriesForTheBackup(): void @@ -272,7 +278,7 @@ final class PostReplaceWithCopyAbilityTest extends TestCase $cloner = Mockery::mock(PostCloner::class); $cloner->expects('backup')->never(); $cloner->expects('replace')->never(); - $ability = $this->ability($cloner, Mockery::mock(ChangeLog::class)); + $ability = $this->ability($cloner, FakeChangeLog::forbidden()); self::assertSame('lit_forbidden', $ability->permission($this->input())->get_error_code()); self::assertSame('lit_forbidden', $ability->execute($this->input())->get_error_code()); @@ -289,7 +295,7 @@ final class PostReplaceWithCopyAbilityTest extends TestCase Functions\expect('wp_trash_post')->never(); try { - $this->ability($cloner, Mockery::mock(ChangeLog::class))->execute($this->input()); + $this->ability($cloner, FakeChangeLog::forbidden())->execute($this->input()); self::fail('WriteFailed expected'); } catch (WriteFailed $e) { self::assertSame('lit_write_failed', $e->toError()->get_error_code()); @@ -298,7 +304,7 @@ final class PostReplaceWithCopyAbilityTest extends TestCase public function testRequiresRevAndIsDestructive(): void { - $ability = $this->ability(Mockery::mock(PostCloner::class), Mockery::mock(ChangeLog::class)); + $ability = $this->ability(Mockery::mock(PostCloner::class), FakeChangeLog::forbidden()); self::assertContains('rev', $ability->inputSchema()['required']); self::assertContains('backup_id', $ability->outputSchema()['required']); diff --git a/tests/Unit/Integrations/WordPress/Status/PostScheduleAbilityTest.php b/tests/Unit/Integrations/WordPress/Status/PostScheduleAbilityTest.php index 628d71e..12fa5a3 100644 --- a/tests/Unit/Integrations/WordPress/Status/PostScheduleAbilityTest.php +++ b/tests/Unit/Integrations/WordPress/Status/PostScheduleAbilityTest.php @@ -8,10 +8,10 @@ use Brain\Monkey\Functions; use DateTimeZone; use LIT\MCP\Abilities\WriteFailed; use LIT\MCP\ChangeLog\ChangeLog; -use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; use LIT\MCP\Integrations\WordPress\Status\PostScheduleAbility; +use LIT\MCP\Tests\Support\FakeChangeLog; +use LIT\MCP\Tests\Support\SchemaFixture; use LIT\MCP\Tests\Unit\TestCase; -use Mockery; use RuntimeException; use WP_Post; @@ -29,8 +29,7 @@ final class PostScheduleAbilityTest extends TestCase private function ability(ChangeLog $log): PostScheduleAbility { - $schema = Mockery::mock(ContentSchema::class); - $schema->allows('isManaged')->andReturn(true); + $schema = SchemaFixture::managed(); return new PostScheduleAbility($schema, $log); } @@ -45,13 +44,13 @@ final class PostScheduleAbilityTest extends TestCase 'edit_date' => true, ], true)->andReturn(5); Functions\when('get_post_status')->justReturn('future'); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->andReturn(4); + $log = new FakeChangeLog(4); $result = $this->ability($log)->execute(['post_id' => 5, 'date' => '2099-06-01 09:30']); self::assertSame('scheduled', $result['changed'][0]['action']); self::assertStringContainsString('2099-06-01 09:30', $result['summary']); + $log->assertRecorded([]); } public function testWarnsThatAPrivatePostBecomesPublic(): void @@ -59,8 +58,7 @@ final class PostScheduleAbilityTest extends TestCase Functions\when('get_post')->justReturn(new WP_Post(['ID' => 5, 'post_type' => 'post', 'post_title' => 'Intern', 'post_status' => 'private'])); Functions\when('wp_update_post')->justReturn(5); Functions\when('get_post_status')->justReturn('future'); - $log = Mockery::mock(ChangeLog::class); - $log->allows('record')->andReturn(4); + $log = new FakeChangeLog(4); $result = $this->ability($log)->execute(['post_id' => 5, 'date' => '2099-06-01 09:30']); @@ -73,8 +71,7 @@ final class PostScheduleAbilityTest extends TestCase { Functions\when('wp_update_post')->justReturn(5); Functions\when('get_post_status')->justReturn('future'); - $log = Mockery::mock(ChangeLog::class); - $log->allows('record')->andReturn(4); + $log = new FakeChangeLog(4); $result = $this->ability($log)->execute(['post_id' => 5, 'date' => '2099-06-01 09:30']); @@ -86,8 +83,7 @@ final class PostScheduleAbilityTest extends TestCase Functions\when('wp_update_post')->alias(static function (): void { throw new RuntimeException('save_post-Hook kaputt'); }); - $log = Mockery::mock(ChangeLog::class); - $log->expects('recordPartial')->with('lit/post-schedule', 'post', '5', Mockery::type('string')); + $log = FakeChangeLog::partialsOnly(); try { $this->ability($log)->execute(['post_id' => 5, 'date' => '2099-06-01 09:30']); @@ -98,6 +94,7 @@ final class PostScheduleAbilityTest extends TestCase self::assertStringContainsString('save_post-Hook kaputt', $e->getMessage()); self::assertSame('lit_partial_write', $e->toError()->get_error_code()); } + $log->assertPartials(['ability' => 'lit/post-schedule', 'object_type' => 'post', 'object_id' => '5']); } public function testATrashedPostIsRefused(): void @@ -105,7 +102,7 @@ final class PostScheduleAbilityTest extends TestCase Functions\when('get_post')->justReturn(new WP_Post(['ID' => 5, 'post_type' => 'post', 'post_status' => 'trash'])); Functions\expect('wp_update_post')->never(); - $result = $this->ability(Mockery::mock(ChangeLog::class))->execute(['post_id' => 5, 'date' => '2099-06-01 09:30']); + $result = $this->ability(FakeChangeLog::forbidden())->execute(['post_id' => 5, 'date' => '2099-06-01 09:30']); self::assertSame('lit_validation_failed', $result->get_error_code()); } @@ -113,7 +110,7 @@ final class PostScheduleAbilityTest extends TestCase public function testPastAndImpossibleDatesAreRejected(): void { Functions\expect('wp_update_post')->never(); - $ability = $this->ability(Mockery::mock(ChangeLog::class)); + $ability = $this->ability(FakeChangeLog::forbidden()); $past = $ability->execute(['post_id' => 5, 'date' => '2020-01-01 10:00']); $impossible = $ability->execute(['post_id' => 5, 'date' => '2099-13-45 10:00']); @@ -131,7 +128,7 @@ final class PostScheduleAbilityTest extends TestCase ->setTimezone(new DateTimeZone('Europe/Berlin')); Functions\when('get_post_status')->justReturn('future'); - $result = $this->ability(Mockery::mock(ChangeLog::class)) + $result = $this->ability(FakeChangeLog::forbidden()) ->execute(['post_id' => 5, 'date' => $soon->format('Y-m-d H:i')]); self::assertSame('lit_validation_failed', $result->get_error_code()); diff --git a/tests/Unit/Integrations/WordPress/Status/PostTrashAbilityTest.php b/tests/Unit/Integrations/WordPress/Status/PostTrashAbilityTest.php index 8563a08..df680e4 100644 --- a/tests/Unit/Integrations/WordPress/Status/PostTrashAbilityTest.php +++ b/tests/Unit/Integrations/WordPress/Status/PostTrashAbilityTest.php @@ -7,10 +7,10 @@ namespace LIT\MCP\Tests\Unit\Integrations\WordPress\Status; use Brain\Monkey\Functions; use LIT\MCP\Abilities\WriteFailed; use LIT\MCP\ChangeLog\ChangeLog; -use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; use LIT\MCP\Integrations\WordPress\Status\PostTrashAbility; +use LIT\MCP\Tests\Support\FakeChangeLog; +use LIT\MCP\Tests\Support\SchemaFixture; use LIT\MCP\Tests\Unit\TestCase; -use Mockery; use RuntimeException; use WP_Post; @@ -33,8 +33,7 @@ final class PostTrashAbilityTest extends TestCase { Functions\when('get_post')->justReturn(new WP_Post(['ID' => 5, 'post_type' => 'page', 'post_title' => 'Alt', 'post_status' => 'publish'])); Functions\when('current_user_can')->justReturn(true); - $schema = Mockery::mock(ContentSchema::class); - $schema->allows('isManaged')->andReturn(true); + $schema = SchemaFixture::managed(); return new PostTrashAbility($schema, $log); } @@ -43,7 +42,7 @@ final class PostTrashAbilityTest extends TestCase { Functions\expect('wp_trash_post')->never(); - $result = $this->ability(Mockery::mock(ChangeLog::class))->execute(['post_id' => 5]); + $result = $this->ability(FakeChangeLog::forbidden())->execute(['post_id' => 5]); self::assertSame('lit_confirm_required', $result->get_error_code()); } @@ -52,13 +51,19 @@ final class PostTrashAbilityTest extends TestCase { Functions\expect('wp_trash_post')->once()->with(5)->andReturn(new WP_Post(['ID' => 5])); Functions\expect('wp_delete_post')->never(); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->with('lit/post-trash', 'page', '5', '"Alt" in den Papierkorb gelegt', null)->andReturn(2); + $log = new FakeChangeLog(2); $result = $this->ability($log)->execute(['post_id' => 5, 'confirm' => true]); self::assertSame('trashed', $result['changed'][0]['action']); self::assertStringContainsString('Papierkorb', $result['summary']); + $log->assertRecorded([ + 'ability' => 'lit/post-trash', + 'object_type' => 'page', + 'object_id' => '5', + 'summary' => '"Alt" in den Papierkorb gelegt', + 'revision_id' => null, + ]); } public function testRefusesWhenTheTrashIsDisabledInsteadOfDeleting(): void @@ -66,9 +71,8 @@ final class PostTrashAbilityTest extends TestCase Functions\when('get_post')->justReturn(new WP_Post(['ID' => 5, 'post_type' => 'page', 'post_status' => 'publish'])); Functions\when('current_user_can')->justReturn(true); Functions\expect('wp_trash_post')->never(); - $schema = Mockery::mock(ContentSchema::class); - $schema->allows('isManaged')->andReturn(true); - $ability = new PostTrashAbility($schema, Mockery::mock(ChangeLog::class), static fn (): bool => false); + $schema = SchemaFixture::managed(); + $ability = new PostTrashAbility($schema, FakeChangeLog::forbidden(), static fn (): bool => false); $result = $ability->execute(['post_id' => 5, 'confirm' => true]); @@ -80,8 +84,7 @@ final class PostTrashAbilityTest extends TestCase Functions\when('wp_trash_post')->alias(static function (): void { throw new RuntimeException('save_post-Hook kaputt'); }); - $log = Mockery::mock(ChangeLog::class); - $log->expects('recordPartial')->with('lit/post-trash', 'page', '5', Mockery::type('string')); + $log = FakeChangeLog::partialsOnly(); try { $this->ability($log)->execute(['post_id' => 5, 'confirm' => true]); @@ -92,11 +95,12 @@ final class PostTrashAbilityTest extends TestCase self::assertStringContainsString('save_post-Hook kaputt', $e->getMessage()); self::assertSame('lit_partial_write', $e->toError()->get_error_code()); } + $log->assertPartials(['ability' => 'lit/post-trash', 'object_type' => 'page', 'object_id' => '5']); } public function testAnAlreadyTrashedPostIsANoOp(): void { - $ability = $this->ability(Mockery::mock(ChangeLog::class)); + $ability = $this->ability(FakeChangeLog::forbidden()); Functions\when('get_post')->justReturn(new WP_Post(['ID' => 5, 'post_type' => 'page', 'post_title' => 'Alt', 'post_status' => 'trash'])); Functions\expect('wp_trash_post')->never(); @@ -110,8 +114,7 @@ final class PostTrashAbilityTest extends TestCase { $this->options(['show_on_front' => 'page', 'page_on_front' => '5', 'page_for_posts' => '9']); Functions\when('wp_trash_post')->justReturn(new WP_Post(['ID' => 5])); - $log = Mockery::mock(ChangeLog::class); - $log->allows('record')->andReturn(3); + $log = new FakeChangeLog(3); $ability = $this->ability($log); $confirm = $ability->execute(['post_id' => 5]); @@ -141,8 +144,7 @@ final class PostTrashAbilityTest extends TestCase : [] ); Functions\when('wp_trash_post')->justReturn(new WP_Post(['ID' => 5])); - $log = Mockery::mock(ChangeLog::class); - $log->allows('record')->andReturn(3); + $log = new FakeChangeLog(3); $result = $this->ability($log)->execute(['post_id' => 5, 'confirm' => true]); @@ -153,7 +155,7 @@ final class PostTrashAbilityTest extends TestCase public function testIsDestructiveAndIndirect(): void { - $ability = $this->ability(Mockery::mock(ChangeLog::class)); + $ability = $this->ability(FakeChangeLog::forbidden()); self::assertTrue($ability->annotations()['destructive']); self::assertFalse($ability->isDirect()); diff --git a/tests/Unit/Integrations/WordPress/Status/PostUnpublishAbilityTest.php b/tests/Unit/Integrations/WordPress/Status/PostUnpublishAbilityTest.php index 7a23f68..11eee39 100644 --- a/tests/Unit/Integrations/WordPress/Status/PostUnpublishAbilityTest.php +++ b/tests/Unit/Integrations/WordPress/Status/PostUnpublishAbilityTest.php @@ -7,10 +7,10 @@ namespace LIT\MCP\Tests\Unit\Integrations\WordPress\Status; use Brain\Monkey\Functions; use LIT\MCP\Abilities\WriteFailed; use LIT\MCP\ChangeLog\ChangeLog; -use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; use LIT\MCP\Integrations\WordPress\Status\PostUnpublishAbility; +use LIT\MCP\Tests\Support\FakeChangeLog; +use LIT\MCP\Tests\Support\SchemaFixture; use LIT\MCP\Tests\Unit\TestCase; -use Mockery; use RuntimeException; use WP_Post; @@ -22,8 +22,7 @@ final class PostUnpublishAbilityTest extends TestCase Functions\when('current_user_can')->justReturn(true); Functions\when('get_preview_post_link')->justReturn('https://kunde.de/?p=5&preview=true'); Functions\when('get_edit_post_link')->justReturn('https://kunde.de/wp-admin/post.php?post=5&action=edit'); - $schema = Mockery::mock(ContentSchema::class); - $schema->allows('isManaged')->andReturn(true); + $schema = SchemaFixture::managed(); return new PostUnpublishAbility($schema, $log); } @@ -31,12 +30,12 @@ final class PostUnpublishAbilityTest extends TestCase public function testPublishedPostBecomesDraft(): void { Functions\expect('wp_update_post')->once()->with(['ID' => 5, 'post_status' => 'draft'], true)->andReturn(5); - $log = Mockery::mock(ChangeLog::class); - $log->expects('record')->andReturn(3); + $log = new FakeChangeLog(3); $result = $this->ability($log, 'publish')->execute(['post_id' => 5]); self::assertSame('unpublished', $result['changed'][0]['action']); + $log->assertRecorded([]); } public function testAHookThrowingDuringTheWriteIsAPartialWrite(): void @@ -44,8 +43,7 @@ final class PostUnpublishAbilityTest extends TestCase Functions\when('wp_update_post')->alias(static function (): void { throw new RuntimeException('save_post-Hook kaputt'); }); - $log = Mockery::mock(ChangeLog::class); - $log->expects('recordPartial')->with('lit/post-unpublish', 'page', '5', Mockery::type('string')); + $log = FakeChangeLog::partialsOnly(); try { $this->ability($log, 'publish')->execute(['post_id' => 5]); @@ -56,13 +54,14 @@ final class PostUnpublishAbilityTest extends TestCase self::assertStringContainsString('save_post-Hook kaputt', $e->getMessage()); self::assertSame('lit_partial_write', $e->toError()->get_error_code()); } + $log->assertPartials(['ability' => 'lit/post-unpublish', 'object_type' => 'page', 'object_id' => '5']); } public function testATrashedPostIsRefusedInsteadOfBecomingADraft(): void { Functions\expect('wp_update_post')->never(); - $result = $this->ability(Mockery::mock(ChangeLog::class), 'trash')->execute(['post_id' => 5]); + $result = $this->ability(FakeChangeLog::forbidden(), 'trash')->execute(['post_id' => 5]); self::assertSame('lit_validation_failed', $result->get_error_code()); } @@ -71,7 +70,7 @@ final class PostUnpublishAbilityTest extends TestCase { Functions\expect('wp_update_post')->never(); - $result = $this->ability(Mockery::mock(ChangeLog::class), 'draft')->execute(['post_id' => 5]); + $result = $this->ability(FakeChangeLog::forbidden(), 'draft')->execute(['post_id' => 5]); self::assertSame([], $result['changed']); } diff --git a/tests/Unit/Integrations/WordPress/WordPressIntegrationTest.php b/tests/Unit/Integrations/WordPress/WordPressIntegrationTest.php index 946f087..ccdd205 100644 --- a/tests/Unit/Integrations/WordPress/WordPressIntegrationTest.php +++ b/tests/Unit/Integrations/WordPress/WordPressIntegrationTest.php @@ -6,10 +6,11 @@ namespace LIT\MCP\Tests\Unit\Integrations\WordPress; use LIT\MCP\Abilities\Ability; use Brain\Monkey\Functions; -use LIT\MCP\ChangeLog\ChangeLog; use LIT\MCP\Integrations\WordPress\Content\ContentComposer; use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; use LIT\MCP\Integrations\WordPress\WordPressIntegration; +use LIT\MCP\Tests\Support\FakeChangeLog; +use LIT\MCP\Tests\Support\SchemaFixture; use LIT\MCP\Tests\Unit\TestCase; use Mockery; @@ -42,7 +43,7 @@ final class WordPressIntegrationTest extends TestCase public function testProvidesTheWordPressAbilitiesInOrder(): void { - $integration = new WordPressIntegration(Mockery::mock(ContentSchema::class), Mockery::mock(ChangeLog::class)); + $integration = new WordPressIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden()); self::assertSame('wordpress', $integration->id()); self::assertTrue($integration->isAvailable()); @@ -54,7 +55,7 @@ final class WordPressIntegrationTest extends TestCase public function testEveryAbilityHasAnObjectInputSchemaThatRejectsUnknownKeys(): void { - $integration = new WordPressIntegration(Mockery::mock(ContentSchema::class), Mockery::mock(ChangeLog::class)); + $integration = new WordPressIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden()); foreach ($integration->abilities() as $ability) { $schema = $ability->inputSchema(); @@ -68,11 +69,10 @@ final class WordPressIntegrationTest extends TestCase { Functions\when('get_post_type_object')->justReturn((object) ['cap' => (object) ['create_posts' => 'edit_pages']]); Functions\when('current_user_can')->justReturn(true); - $schema = Mockery::mock(ContentSchema::class); - $schema->allows('isManaged')->andReturn(true); + $schema = SchemaFixture::managed(); $composer = Mockery::mock(ContentComposer::class); $composer->expects('available')->andReturn(false); - $integration = new WordPressIntegration($schema, Mockery::mock(ChangeLog::class), $composer); + $integration = new WordPressIntegration($schema, FakeChangeLog::forbidden(), $composer); $create = array_values(array_filter( $integration->abilities(), diff --git a/tests/Unit/Support/FakeChangeLogTest.php b/tests/Unit/Support/FakeChangeLogTest.php new file mode 100644 index 0000000..b47ea88 --- /dev/null +++ b/tests/Unit/Support/FakeChangeLogTest.php @@ -0,0 +1,104 @@ +record('lit/a', 'page', '5', 'eins', 811)); + self::assertSame(42, $log->record('lit/a', 'page', '6', 'zwei')); + $log->assertRecorded( + ['object_id' => '5', 'summary' => 'eins', 'revision_id' => 811, 'result' => 'ok'], + ['object_id' => '6', 'revision_id' => null] + ); + self::assertSame(['eins', 'zwei'], $log->summaries()); + } + + public function testAFailureIsThrownAfterTheCallIsNoted(): void + { + $log = new FakeChangeLog(1, new RuntimeException('db weg')); + + try { + $log->record('lit/a', 'page', '5', 'x'); + self::fail('RuntimeException expected'); + } catch (RuntimeException $e) { + self::assertSame('db weg', $e->getMessage()); + } + $log->assertRecorded(['ability' => 'lit/a']); + } + + public function testAssertRecordedChecksTheNumberOfCalls(): void + { + $log = new FakeChangeLog(); + $log->record('lit/a', 'page', '5', 'x'); + + $this->expectException(AssertionFailedError::class); + $log->assertRecorded([], []); + } + + public function testAssertPartialsChecksTheNamedArguments(): void + { + $log = FakeChangeLog::partialsOnly(); + $log->recordPartial('lit/a', 'page', '5', 'teilweise', 811); + $log->assertPartials(['ability' => 'lit/a', 'revision_id' => 811]); + + $this->expectException(AssertionFailedError::class); + $log->assertPartials(['object_id' => '6']); + } + + public function testACallTheFakeDoesNotAllowThrowsAndFailsTheTestEvenWhenSwallowed(): void + { + $log = FakeChangeLog::forbidden(); + + try { + $log->record('lit/a', 'page', '5', 'x'); + self::fail('LogicException expected'); + } catch (LogicException $e) { + self::assertStringContainsString('record(lit/a, page, 5)', $e->getMessage()); + } + + try { + FakeChangeLog::verify(); + self::fail('verify() must fail'); + } catch (AssertionFailedError $e) { + self::assertStringContainsString('Unexpected change log calls', $e->getMessage()); + } + FakeChangeLog::verify(); + } + + public function testRecordOnlyFakesRefusePartialsAndPartialsOnlyFakesRefuseRecords(): void + { + foreach ([[new FakeChangeLog(), 'recordPartial'], [FakeChangeLog::partialsOnly(), 'record']] as [$log, $method]) { + try { + $log->$method('lit/a', 'page', '5', 'x'); + self::fail($method . ' must be refused'); + } catch (LogicException $e) { + self::assertStringContainsString($method, $e->getMessage()); + } + } + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Unexpected change log calls: recordPartial(lit/a, page, 5); record(lit/a, page, 5)'); + FakeChangeLog::verify(); + } + + public function testAnyAllowsBoth(): void + { + $log = FakeChangeLog::any(9); + + self::assertSame(9, $log->record('lit/a', 'page', '5', 'x')); + $log->recordPartial('lit/a', 'page', '5', 'y'); + $log->assertPartials(['summary' => 'y']); + } +} diff --git a/tests/Unit/TestCase.php b/tests/Unit/TestCase.php index 28ea60c..3636e88 100644 --- a/tests/Unit/TestCase.php +++ b/tests/Unit/TestCase.php @@ -6,6 +6,7 @@ namespace LIT\MCP\Tests\Unit; use Brain\Monkey; use Brain\Monkey\Functions; +use LIT\MCP\Tests\Support\FakeChangeLog; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; use PHPUnit\Framework\TestCase as BaseTestCase; @@ -35,5 +36,6 @@ abstract class TestCase extends BaseTestCase { Monkey\tearDown(); parent::tearDown(); + FakeChangeLog::verify(); } }