*/ private array $saved = []; protected function setUp(): void { parent::setUp(); $this->page = LibraryFixtures::page(DiviFixtures::markup('synthetic-basic.html')); LibraryFixtures::given(array_merge( [$this->page], array_map(static fn (int $id): WP_Post => LibraryFixtures::item($id), array_keys(LibraryFixtures::ITEMS)) )); } /** @param list $deniedRoles */ private function ability(array $deniedRoles = [], bool $diviWritable = true, bool $unfiltered = false): LibraryInsertAbility { $access = LibraryFixtures::access($deniedRoles); $failures = []; return new LibraryInsertAbility( DiviWriterFixture::writer($this->saved, $failures, $unfiltered, LibraryFixtures::loader($access)), $access, static fn (): bool => $diviWritable ); } /** * @param array $extra * @return array */ private function input(array $extra): array { return array_merge(['post_id' => 7, 'tree_rev' => Rev::of((string) $this->page->post_content)], $extra); } private static function fixtureInstance(int $id, string $end): string { $page = DiviFixtures::markup('stage3/page--globale-instanzen.html'); $start = (int) strpos($page, ''), $this->saved[0]); self::assertSame('g0', $result['changed'][0]['id']); self::assertStringContainsString('Bibliothekseintrag 1058', implode(' ', $result['warnings'])); } public function testInsertsAGlobalModuleIntoAColumn(): void { $result = $this->ability()->execute($this->input(['library_id' => 1059, 'parent' => 's0.r0.c1'])); self::assertTrue($result['ok']); self::assertStringContainsString(self::fixtureInstance(1059, '/-->'), $this->saved[0]); } public function testUntouchedContentKeepsItsBytes(): void { $before = (string) $this->page->post_content; $this->ability()->execute($this->input(['library_id' => 1059, 'parent' => 's0.r0.c1'])); $instance = self::fixtureInstance(1059, '/-->'); $at = (int) strpos($this->saved[0], $instance); self::assertSame($before, substr($this->saved[0], 0, $at) . substr($this->saved[0], $at + strlen($instance))); } public function testCopyInsertsTheRootsWithoutFrameAndCrlf(): void { $result = $this->ability()->execute($this->input(['library_id' => 1055])); $section = str_replace( ['', '', "\r\n"], ['', '', "\n"], DiviFixtures::markup('stage3/library--library-abschnitt.html') ); self::assertTrue($result['ok']); self::assertStringContainsString($section, $this->saved[0]); self::assertStringNotContainsString("\r", $this->saved[0]); } public function testLayoutCopyInsertsEverySection(): void { $result = $this->ability()->execute($this->input(['library_id' => 1054, 'mode' => 'copy'])); self::assertCount(3, $result['changed']); self::assertStringContainsString('Library-Blurb', $this->saved[0]); } /** F10: a copy of a global entry is deglobalized like Divi's /load – its root bytes, no globalParent. */ public function testCopyOfAGlobalEntryIsItsPlainRoot(): void { $result = $this->ability()->execute($this->input(['library_id' => 1058, 'mode' => 'copy'])); $section = str_replace( ['', '', "\r\n"], ['', '', "\n"], DiviFixtures::markup('stage3/library-global--globaler-abschnitt.html') ); self::assertTrue($result['ok']); self::assertStringContainsString($section, $this->saved[0]); self::assertStringNotContainsString('globalParent', $this->saved[0]); self::assertStringNotContainsString('global-layout', $this->saved[0]); self::assertSame([], $result['warnings']); } /** F10: nested instances in a (stored) global entry become plain nodes in the copy. */ public function testCopyDeglobalizesNestedInstances(): void { $instance = self::fixtureInstance(1059, '/-->'); $this->givenItem(1058, '' . '' . $instance . '' . ''); $result = $this->ability()->execute($this->input(['library_id' => 1058, 'mode' => 'copy'])); self::assertTrue($result['ok']); self::assertStringNotContainsString('globalModule', $this->saved[0]); self::assertStringContainsString(''); foreach ( [ '' . $instance . '', '' . '' . $instance . '' . '', '', '', ] as $content ) { $this->givenItem(1059, $content); $result = $this->ability([], true, true)->execute($this->input(['library_id' => 1059])); self::assertToolError($result, 'validation_failed'); self::assertStringContainsString('Verschachtelung', $result->get_error_message()); } self::assertSame([], $this->saved); } public function testGlobalModeNeedsAGlobalEntryAndUnknownEntriesAreNotFound(): void { self::assertToolError($this->ability()->execute($this->input(['library_id' => 1055, 'mode' => 'global'])), 'validation_failed'); self::assertToolError($this->ability()->execute($this->input(['library_id' => 9999])), 'not_found'); self::assertToolError($this->ability()->execute($this->input(['library_id' => 7])), 'not_found'); self::assertToolError($this->ability()->execute($this->input(['library_id' => 1055, 'mode' => 'link'])), 'validation_failed'); self::assertSame([], $this->saved); } public function testATrashedEntryIsNotFound(): void { $this->givenItem(1055, DiviFixtures::markup('stage3/library--library-abschnitt.html'), ['post_status' => 'trash']); self::assertToolError($this->ability()->execute($this->input(['library_id' => 1055])), 'not_found'); self::assertSame([], $this->saved); } /** Ruling P5: the entry is read through LibraryAccess – edit_post on it, not read_post. */ public function testNeedsEditPostOnTheEntry(): void { Functions\when('current_user_can')->alias( static fn (string $cap, mixed ...$args): bool => ! ($cap === 'edit_post' && (int) ($args[0] ?? 0) === 1058) ); $result = $this->ability()->execute($this->input(['library_id' => 1058])); self::assertToolError($result, 'forbidden'); self::assertStringNotContainsString('[Fixture]', $result->get_error_message()); self::assertSame([], $this->saved); } public function testNeedsAddLibraryAndAWritableDiviGroup(): void { self::assertToolError($this->ability(['add_library'])->permission($this->input(['library_id' => 1058])), 'forbidden'); self::assertToolError($this->ability(['add_library'])->execute($this->input(['library_id' => 1058])), 'forbidden'); self::assertToolError($this->ability([], false)->permission($this->input(['library_id' => 1058])), 'unsupported'); self::assertToolError($this->ability([], false)->execute($this->input(['library_id' => 1058])), 'unsupported'); self::assertTrue($this->ability()->permission($this->input(['library_id' => 1058]))); self::assertTrue($this->ability()->isDirect()); self::assertSame([], $this->saved); } public function testTheTargetKeepsTheTreeRevLockAndStructureRules(): void { self::assertToolError( $this->ability()->execute(['post_id' => 7, 'tree_rev' => 'alt', 'library_id' => 1058]), 'rev_mismatch' ); self::assertToolError( $this->ability()->execute($this->input(['library_id' => 1058, 'parent' => 's0.r0.c1'])), 'validation_failed' ); self::assertToolError( $this->ability()->execute($this->input(['library_id' => 1059, 'parent' => 's9'])), 'validation_failed' ); self::assertSame([], $this->saved); } public function testEditorCannotInsertAnItemWithCustomCss(): void { $custom = LibraryFixtures::item(1055, [ 'ID' => 1080, 'post_content' => '', ]); LibraryFixtures::given([$this->page, $custom]); $result = $this->ability()->execute($this->input(['library_id' => 1080])); self::assertToolError($result, 'validation_failed'); self::assertSame([], $this->saved); } public function testAdminMayInsertAnItemWithCustomCss(): void { $custom = LibraryFixtures::item(1055, [ 'ID' => 1080, 'post_content' => '', ]); LibraryFixtures::given([$this->page, $custom]); $result = $this->ability([], true, true)->execute($this->input(['library_id' => 1080])); self::assertTrue($result['ok']); self::assertStringContainsString('body{display:none}', $this->saved[0]); } }