justReturn( (object) ['name' => 'page', 'cap' => (object) ['edit_posts' => 'edit_pages']] ); Functions\when('current_user_can')->justReturn(true); } private function schema(): ContentSchema { $schema = Mockery::mock(ContentSchema::class); $schema->allows('isManaged')->andReturn(true); $schema->allows('fields')->with('page')->andReturn([ 'telefon' => new FieldDefinition('telefon', 'Telefon', 'string', 'meta', FieldDefinition::WRITABLE), 'notiz' => new FieldDefinition('notiz', 'Notiz', 'text', 'pods', FieldDefinition::WRITABLE), 'foto' => new FieldDefinition('foto', 'Foto', 'file', 'pods', FieldDefinition::WRITABLE), 'projekte' => new FieldDefinition('projekte', 'Projekte', 'relation', 'pods', FieldDefinition::WRITABLE), '_intern' => new FieldDefinition('_intern', '_intern', 'string', 'meta', FieldDefinition::WRITABLE), 'geheim' => new FieldDefinition('geheim', 'Geheim', 'string', 'pods', FieldDefinition::HIDDEN), 'gefunden' => new FieldDefinition('gefunden', 'gefunden', 'unknown', 'sample', FieldDefinition::UNKNOWN), 'aktiv' => new FieldDefinition('aktiv', 'Aktiv', 'boolean', 'pods', FieldDefinition::WRITABLE), 'kennwort' => new FieldDefinition('kennwort', 'Kennwort', 'password', 'pods', FieldDefinition::WRITABLE), ]); return $schema; } /** * @param list $deniedRoles * @param array|null $registry */ private function ability( array $deniedRoles = [], ?string $storage = 'meta', ?array $registry = null ): DiviDynamicSourcesAbility { $registry ??= DiviFixtures::dynamicContentRegistry(); return new DiviDynamicSourcesAbility( $this->schema(), new DynamicContentMap(static fn (int $id): array => $registry), LibraryFixtures::roles($deniedRoles), static fn (string $postType): int => 831, static fn (string $postType): ?string => $storage ); } public function testListsDiviSourcesWithTheirPlaceholders(): void { $result = $this->ability()->execute(['post_type' => 'page']); $sources = array_column($result['sources'], null, 'name'); self::assertSame(831, $result['sample_post_id']); self::assertSame('{{post:title}}', $sources['post_title']['placeholder']); self::assertSame('{{link:}}', $sources['post_link_url_page']['placeholder']); self::assertNull($sources['site_title']['placeholder']); self::assertArrayNotHasKey('loop_post_title', $sources); self::assertEquals(['post_id' => 'select_post'], (array) $sources['post_link_url_page']['settings']); self::assertFalse($this->ability()->isDirect()); self::assertTrue($this->ability()->annotations()['readonly']); self::assertSame('lit/divi-dynamic-sources', $this->ability()->name()); } /** Review M-9: {{link:}} refuses attachments (status inherit), so their link sources get no hint. */ public function testNoLinkHintForAttachmentsOrInternalTypes(): void { $registry = DiviFixtures::dynamicContentRegistry(); foreach (['post_link_url_attachment', 'post_link_url__et_pb_speculation'] as $name) { $registry[$name] = $registry['post_link_url_page']; $registry[$name]['id'] = $name; } $sources = array_column($this->ability([], 'meta', $registry)->execute(['post_type' => 'page'])['sources'], null, 'name'); self::assertSame('{{link:}}', $sources['post_link_url_page']['placeholder']); self::assertNull($sources['post_link_url_attachment']['placeholder']); self::assertNull($sources['post_link_url__et_pb_speculation']['placeholder']); } public function testOffersTextLikeFieldsAndNamesTheOthers(): void { $result = $this->ability()->execute(['post_type' => 'page']); self::assertSame(['telefon', 'notiz'], array_column($result['fields'], 'key')); self::assertSame('{{field:notiz}}', $result['fields'][1]['placeholder']); $omitted = array_column($result['omitted'], 'reason', 'key'); self::assertSame(['foto', 'projekte', '_intern', 'gefunden', 'aktiv', 'kennwort'], array_keys($omitted)); self::assertStringContainsString('nur IDs', $omitted['foto']); self::assertStringContainsString('nur IDs', $omitted['projekte']); self::assertStringContainsString('„_“', $omitted['_intern']); // Ruling P9: unknown field types are not offered. self::assertStringContainsString('unbekannt', $omitted['gefunden']); self::assertStringContainsString('nicht textartig', $omitted['aktiv']); self::assertStringContainsString('nicht textartig', $omitted['kennwort']); } public function testProtectedAndHiddenFieldsLeakNothing(): void { $result = $this->ability()->execute(['post_type' => 'page']); $json = (string) json_encode($result); // Hidden fields are neither offered nor named (security audit M4). self::assertStringNotContainsString('geheim', $json); self::assertStringNotContainsString('Geheim', $json); // The protected key is named (so the client knows why) but never offered with a placeholder. self::assertStringNotContainsString('{{field:_intern}}', $json); self::assertSame(['key', 'label', 'reason'], array_keys($result['omitted'][2])); } public function testSourcesNeverNameProtectedMetaOptions(): void { $registry = DiviFixtures::dynamicContentRegistry() + [ 'custom_meta__edit_lock' => ['label' => '_edit_lock', 'group' => 'Custom', 'fields' => []], ]; $result = $this->ability([], 'meta', $registry)->execute(['post_type' => 'page']); self::assertStringNotContainsString('_edit_lock', (string) json_encode($result)); } public function testPodsTableStorageIsNotReadableByDivi(): void { $result = $this->ability([], 'table')->execute(['post_type' => 'page']); self::assertSame(['telefon'], array_column($result['fields'], 'key')); self::assertStringContainsString('Tabellen', array_column($result['omitted'], 'reason', 'key')['notiz']); } public function testFieldListNeedsTheDiviRole(): void { $result = $this->ability(['read_dynamic_content_custom_fields'])->execute(['post_type' => 'page']); self::assertSame([], $result['fields']); self::assertSame([], $result['omitted']); self::assertStringContainsString('Rollen-Editor', implode(' ', $result['notes'])); } public function testWithoutDivisRegistryThePlaceholderSourcesAreShown(): void { $result = $this->ability([], 'meta', [])->execute(['post_type' => 'page']); self::assertContains('post_title', array_column($result['sources'], 'name')); self::assertStringContainsString('nicht verfügbar', implode(' ', $result['notes'])); } public function testUnknownPostTypeIsNotFound(): void { Functions\when('get_post_type_object')->justReturn(null); self::assertToolError($this->ability()->permission(['post_type' => 'nix']), 'not_found'); self::assertToolError($this->ability()->execute(['post_type' => 'nix']), 'not_found'); } public function testNeedsTheTypesEditCapability(): void { Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap !== 'edit_pages'); self::assertToolError($this->ability()->permission(['post_type' => 'page']), 'forbidden'); } }