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) <noreply@anthropic.com>
This commit is contained in:
2026-09-26 17:53:54 +02:00
co-authored by Claude Opus 5.5
parent 3d96a41366
commit 72eeaa46ad
34 changed files with 725 additions and 353 deletions
+1 -6
View File
@@ -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). */
+3 -7
View File
@@ -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);
}
/**
+181
View File
@@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Support;
use LIT\MCP\ChangeLog\ChangeLog;
use LogicException;
use PHPUnit\Framework\Assert;
use Throwable;
/**
* ChangeLog without a database. Allowed calls are kept in $records / $partials (arguments by
* name); record() returns increasing ids from $firstId, or throws $failure after noting the call.
*
* What a fake allows mirrors the Mockery mocks it replaces:
* - new FakeChangeLog(id) record() only (allows('record')->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<array{ability: string, object_type: string, object_id: string, summary: string,
* revision_id: ?int, snapshot_id: ?string, result: string}>
*/
public array $records = [];
/**
* @var list<array{ability: string, object_type: string, object_id: string, summary: string,
* revision_id: ?int}>
*/
public array $partials = [];
/** @var list<string> 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<string, mixed> ...$expected
*/
public function assertRecorded(array ...$expected): void
{
self::assertCalls('record()', $expected, $this->records);
}
/**
* Asserts the recordPartial() calls like assertRecorded().
*
* @param array<string, mixed> ...$expected
*/
public function assertPartials(array ...$expected): void
{
self::assertCalls('recordPartial()', $expected, $this->partials);
}
/**
* @param list<array<string, mixed>> $expected
* @param list<array<string, mixed>> $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<string> 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);
}
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Support;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use Mockery;
use Mockery\MockInterface;
/** ContentSchema stand-ins for ability tests. */
final class SchemaFixture
{
/**
* Every post type is managed. $divi answers diviEnabled(); null = the test does not expect
* that question (a call fails the test, as with a plain mock).
*
* @return ContentSchema&MockInterface
*/
public static function managed(?bool $divi = null): ContentSchema
{
$schema = Mockery::mock(ContentSchema::class);
$schema->allows('isManaged')->andReturn(true);
if ($divi !== null) {
$schema->allows('diviEnabled')->andReturn($divi);
}
return $schema;
}
}
@@ -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();
@@ -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);
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);
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();
}
}
@@ -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());
@@ -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());
@@ -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 <script>x</script>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
@@ -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);
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
@@ -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']);
@@ -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;
}
@@ -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]);
@@ -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());
@@ -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']));
}
@@ -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([]);
}
}
@@ -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 {
@@ -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());
}
@@ -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());
}
@@ -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);
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());
@@ -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']);
@@ -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);
@@ -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([]);
}
}
@@ -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);
}
@@ -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<string, mixed> $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()
);
}
}
@@ -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']);
@@ -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());
}
}
@@ -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']);
@@ -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());
@@ -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());
@@ -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']);
}
@@ -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(),
+104
View File
@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Support;
use LIT\MCP\Tests\Support\FakeChangeLog;
use LIT\MCP\Tests\Unit\TestCase;
use LogicException;
use PHPUnit\Framework\AssertionFailedError;
use RuntimeException;
final class FakeChangeLogTest extends TestCase
{
public function testRecordsCallsWithIncreasingIds(): void
{
$log = new FakeChangeLog(41);
self::assertSame(41, $log->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']);
}
}
+2
View File
@@ -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();
}
}