$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' => [[]]]) ); } /** The lock of IDs that are no Divi capability is CapabilityGate's (unknown ones fail closed there). */ public function testIdsThatAreNoDiviCapabilityAreIgnored(): void { DiviFeatureFixtures::useStates([ 'page-write' => ReleaseState::Incompatible, 'catalog' => ReleaseState::Incompatible, ]); $tool = $this->contentCreate($this->composerNeeding('pods-field-read', '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 ); } } } }