Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/Release/DiviFeatureGateTest.php
T

347 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
use Brain\Monkey\Functions;
use LIT\MCP\Abilities\Ability;
use LIT\MCP\Abilities\DeclaresRequirements;
use LIT\MCP\Integrations\Divi\DiviIntegration;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\DiviFeatureGate;
use LIT\MCP\Integrations\Divi\Release\FeatureSets;
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
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\DiviFeatureFixtures;
use LIT\MCP\Tests\Support\FakeChangeLog;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use WP_Error;
final class DiviFeatureGateTest extends TestCase
{
/**
* @param list<DiviFeature> $features What diviFeatures() returns for any input.
* @return Ability&RequiresDiviFeatures
*/
private function tool(string $name, bool $readonly, array $features): Ability
{
$ability = Mockery::mock(Ability::class, RequiresDiviFeatures::class);
$ability->allows('name')->andReturn($name);
$ability->allows('annotations')->andReturn(['readonly' => $readonly]);
$ability->allows('diviFeatures')->andReturn($features);
return $ability;
}
private function gate(?FakeChangeLog $log = null): DiviFeatureGate
{
return new DiviFeatureGate($log);
}
public function testReleasedDiviRefusesNothingAndAddsNothing(): void
{
$gate = $this->gate(FakeChangeLog::forbidden());
$write = $this->tool('lit/divi-insert', false, [DiviFeature::PageWrite, DiviFeature::Catalog]);
$read = $this->tool('lit/divi-outline', true, [DiviFeature::PageRead]);
self::assertNull($gate->refusal($write, []));
self::assertNull($gate->refusal($read, []));
self::assertSame(['ok' => true], $gate->annotate($read, ['ok' => true]));
}
public function testAReadingToolIsRefusedWhenOneOfItsFeaturesIsLocked(): void
{
DiviFeatureFixtures::lock(DiviFeature::DesignRead);
$read = $this->tool('lit/divi-outline', true, [DiviFeature::PageRead, DiviFeature::DesignRead]);
// A refused read changes nothing: no change log entry (the fake allows none).
$error = $this->gate(FakeChangeLog::forbidden())->refusal($read, ['post_id' => 7]);
$payload = self::assertToolError($error, 'divi_untested');
self::assertSame(423, $error->get_error_data()['status']);
self::assertSame(['design-read'], array_column($payload['features'], 'id'));
}
public function testAWritingToolIsRefusedWhenItsWriteFeatureIsLockedAsIncompatible(): void
{
DiviFeatureFixtures::lock(DiviFeature::PageWrite, ReleaseState::Incompatible);
$log = new FakeChangeLog();
$write = $this->tool('lit/divi-insert', false, [DiviFeature::PageRead, DiviFeature::PageWrite]);
$payload = self::assertToolError($this->gate($log)->refusal($write, []), 'divi_incompatible');
self::assertSame('incompatible', $payload['features'][0]['state']);
self::assertSame(
'Abgelehnt: Divi 5.3.0 – Seiten schreiben inkompatibel.',
$log->records[0]['summary']
);
}
public function testAWritingToolIsRefusedWhenOnlyADeclaredReadFeatureIsLocked(): void
{
$write = $this->tool('lit/divi-insert', false, [
DiviFeature::PageRead,
DiviFeature::PageWrite,
DiviFeature::Catalog,
DiviFeature::DesignRead,
]);
foreach ([DiviFeature::Catalog, DiviFeature::DesignRead, DiviFeature::PageRead] as $readFeature) {
DiviFeatureFixtures::lock($readFeature);
$payload = self::assertToolError(
$this->gate(new FakeChangeLog())->refusal($write, []),
'divi_untested'
);
self::assertSame([$readFeature->value], array_column($payload['features'], 'id'));
}
DiviFeatureFixtures::lock(DiviFeature::LibraryRead);
self::assertNull(
$this->gate(FakeChangeLog::forbidden())->refusal($write, []),
'an unrelated locked read feature does not refuse'
);
}
public function testAToolThatDoesNotNeedTheLockedFeatureRuns(): void
{
DiviFeatureFixtures::useStates([
'preset-write' => ReleaseState::Incompatible,
'library-write' => ReleaseState::Untested,
'tb-write' => ReleaseState::Incompatible,
]);
$gate = $this->gate(FakeChangeLog::forbidden());
$insert = $this->tool('lit/divi-insert', false, [
DiviFeature::PageRead,
DiviFeature::PageWrite,
DiviFeature::Catalog,
DiviFeature::DesignRead,
DiviFeature::DynamicSources,
]);
self::assertNull($gate->refusal($insert, []), 'a write tool runs while only other write features are locked');
self::assertNull($gate->refusal($this->tool('lit/divi-catalog', true, [DiviFeature::Catalog]), []));
self::assertNull($gate->refusal($this->tool('lit/divi-design-snapshots', true, []), []), 'declares nothing');
}
public function testTheErrorNamesOnlyTheUnusableFeaturesOfTheCallEachOnce(): void
{
DiviFeatureFixtures::useStates([
'page-write' => ReleaseState::Untested,
'roles' => ReleaseState::Untested,
'tb-write' => ReleaseState::Incompatible,
]);
$tool = $this->tool('lit/divi-insert', false, [
DiviFeature::PageWrite,
DiviFeature::Roles,
DiviFeature::PageWrite,
DiviFeature::Catalog,
]);
$payload = self::assertToolError($this->gate(new FakeChangeLog())->refusal($tool, []), 'divi_untested');
self::assertSame(['page-write', 'roles'], array_column($payload['features'], 'id'));
self::assertSame(
['Seiten schreiben', 'Divi-Rollen'],
array_column($payload['features'], 'label')
);
self::assertSame('untested', $payload['features'][0]['state']);
self::assertIsArray($payload['features'][0]['problems']);
self::assertStringContainsString('Seiten schreiben', $payload['hint']);
self::assertStringContainsString('Divi-Rollen', $payload['hint']);
self::assertStringContainsString('weiter', $payload['hint'], 'other Divi tools keep working');
}
public function testAnIncompatibleFeatureAmongUntestedOnesMakesTheCodeIncompatible(): void
{
DiviFeatureFixtures::useStates([
'page-write' => ReleaseState::Untested,
'catalog' => ReleaseState::Incompatible,
]);
$tool = $this->tool('lit/divi-update', false, [DiviFeature::PageWrite, DiviFeature::Catalog]);
self::assertToolError($this->gate(new FakeChangeLog())->refusal($tool, []), 'divi_incompatible');
}
public function testTheToolIsAskedForTheInputOfTheCall(): void
{
DiviFeatureFixtures::lock(DiviFeature::LibraryWrite);
$tool = Mockery::mock(Ability::class, RequiresDiviFeatures::class);
$tool->allows('name')->andReturn('lit/divi-insert');
$tool->allows('annotations')->andReturn(['readonly' => false]);
$tool->allows('diviFeatures')->with(['post_id' => 1])->andReturn([DiviFeature::PageWrite]);
$tool->allows('diviFeatures')->with(['post_id' => 2])
->andReturn([DiviFeature::PageWrite, DiviFeature::LibraryWrite]);
$gate = $this->gate(new FakeChangeLog());
self::assertNull($gate->refusal($tool, ['post_id' => 1]));
self::assertToolError($gate->refusal($tool, ['post_id' => 2]), 'divi_untested');
}
public function testAToolWithoutDeclarationIsNeverTouchedEvenWithEverythingLocked(): void
{
DiviFeatureFixtures::useStates(array_fill_keys(
array_map(static fn (DiviFeature $f): string => $f->value, DiviFeature::cases()),
ReleaseState::Incompatible
));
$plain = Mockery::mock(Ability::class);
$plain->allows('name')->andReturn('lit/content-update');
self::assertNull($this->gate(FakeChangeLog::forbidden())->refusal($plain, ['post_id' => 1]));
}
private function contentCreate(?ContentComposer $composer): ContentCreateAbility
{
return new ContentCreateAbility(
Mockery::mock(ContentSchema::class),
Mockery::mock(PostWriter::class),
FakeChangeLog::forbidden(),
$composer
);
}
private function composerNeeding(string ...$ids): ContentComposer
{
$composer = Mockery::mock(ContentComposer::class);
$composer->allows('available')->andReturn(true);
$composer->allows('requirements')->andReturn(array_values($ids));
return $composer;
}
public function testContentCreateWithoutDiviIsUntouchedWhateverIsLocked(): void
{
DiviFeatureFixtures::lock(DiviFeature::PageWrite, ReleaseState::Incompatible);
$tool = $this->contentCreate($this->composerNeeding('page-write'));
self::assertInstanceOf(DeclaresRequirements::class, $tool);
self::assertNull(
$this->gate(FakeChangeLog::forbidden())->refusal($tool, ['post_type' => 'page', 'title' => 'Start'])
);
}
public function testContentCreateWithDiviIsRefusedWhenAFeatureOfTheComposerIsLocked(): void
{
DiviFeatureFixtures::lock(DiviFeature::PageWrite);
$log = new FakeChangeLog();
$tool = $this->contentCreate($this->composerNeeding('page-read', 'page-write', 'catalog'));
$input = ['post_type' => 'page', 'title' => 'Start', 'divi' => [['type' => 'section']]];
$payload = self::assertToolError($this->gate($log)->refusal($tool, $input), 'divi_untested');
self::assertSame(['page-write'], array_column($payload['features'], 'id'));
self::assertSame('lit/content-create', $log->records[0]['ability']);
}
public function testContentCreateWithDiviOnASiteWithoutDiviIsLeftToTheToolItself(): void
{
DiviFeatureFixtures::lock(DiviFeature::PageWrite, ReleaseState::Incompatible);
$composer = Mockery::mock(ContentComposer::class);
$composer->allows('available')->andReturn(false);
$composer->shouldNotReceive('requirements');
$tool = $this->contentCreate($composer);
self::assertNull(
$this->gate(FakeChangeLog::forbidden())->refusal($tool, ['post_type' => 'page', 'divi' => [[]]]),
'no divi_incompatible and no refused entry: execute() answers "Divi 5 ist nicht aktiv"'
);
}
public function testContentCreateWithDiviRunsWhenOnlyOtherFeaturesAreLocked(): void
{
DiviFeatureFixtures::useStates([
'preset-write' => ReleaseState::Untested,
'tb-write' => ReleaseState::Untested,
]);
$tool = $this->contentCreate($this->composerNeeding('page-read', 'page-write', 'catalog'));
self::assertNull(
$this->gate(FakeChangeLog::forbidden())->refusal($tool, ['post_type' => 'page', 'divi' => [[]]])
);
}
public function testAnUnknownRequirementOfTheCoreIsIgnored(): void
{
DiviFeatureFixtures::lock(DiviFeature::PageWrite);
$tool = $this->contentCreate($this->composerNeeding('something-else'));
self::assertNull(
$this->gate(FakeChangeLog::forbidden())->refusal($tool, ['post_type' => 'page', 'divi' => [[]]])
);
}
public function testAnnotateNeverChangesAResult(): void
{
DiviFeatureFixtures::lock(DiviFeature::PageWrite);
$gate = $this->gate();
$read = $this->tool('lit/divi-outline', true, [DiviFeature::PageRead]);
$error = new WP_Error('lit_not_found', 'x');
self::assertSame(['post_id' => 7], $gate->annotate($read, ['post_id' => 7]), 'E2: no notice any more');
self::assertSame(['warnings' => ['alt']], $gate->annotate($read, ['warnings' => ['alt']]));
self::assertSame([1, 2], $gate->annotate($read, [1, 2]));
self::assertSame($error, $gate->annotate($read, $error));
}
public function testDiviToolsKeepTheOptionalWarningsPropertyInTheirOutputSchema(): void
{
$gate = $this->gate();
$read = $this->tool('lit/divi-outline', true, []);
$properties = ['post_id' => ['type' => 'integer']];
$schema = $gate->outputSchema($read, ['type' => 'object', 'properties' => $properties]);
$warnings = ['type' => 'array', 'items' => ['type' => 'string']];
self::assertSame($warnings, $schema['properties']['warnings'] ?? null);
self::assertSame($properties['post_id'], $schema['properties']['post_id']);
$own = ['type' => 'object', 'properties' => ['warnings' => ['type' => 'array']]];
self::assertSame($own, $gate->outputSchema($read, $own), 'an existing warnings property stays');
self::assertNull($gate->outputSchema($read, null));
self::assertSame(['type' => 'array'], $gate->outputSchema($read, ['type' => 'array']));
}
public function testToolsOfTheCoreKeepTheirOutputSchema(): void
{
$gate = $this->gate();
$create = $this->contentCreate(null);
self::assertSame(['type' => 'object'], $gate->outputSchema($create, ['type' => 'object']));
self::assertSame(
['type' => 'object'],
$gate->outputSchema(Mockery::mock(Ability::class), ['type' => 'object'])
);
}
/**
* The real tools of the three Divi groups: locking one capability refuses exactly the tools that declare it
* (reading and writing alike) and no other.
*/
public function testLockingOneFeatureRefusesExactlyTheRealToolsThatDeclareIt(): void
{
Functions\when('__')->returnArg();
Functions\when('get_post_type')->justReturn(false);
Functions\when('current_user_can')->justReturn(true);
$divi = new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden());
$tools = [...$divi->abilities(), ...$divi->library()->abilities(), ...$divi->themeBuilder()->abilities()];
self::assertNotEmpty($tools);
foreach (DiviFeature::cases() as $feature) {
DiviFeatureFixtures::lock($feature);
$gate = $this->gate(FakeChangeLog::any());
foreach ($tools as $tool) {
$declares = in_array($feature, FeatureSets::declaredBy($tool, []), true);
self::assertSame(
$declares,
$gate->refusal($tool, []) instanceof WP_Error,
$tool->name() . ' / ' . $feature->value
);
}
}
}
}