*/ private array $saved = []; protected function setUp(): void { parent::setUp(); $this->target = LibraryFixtures::page(DiviFixtures::markup('synthetic-basic.html')); $this->source = LibraryFixtures::page( DiviFixtures::markup('stage3/page--globale-instanzen.html'), ['ID' => 8, 'post_title' => 'Quelle'] ); LibraryFixtures::given([$this->target, $this->source, LibraryFixtures::item(1055)]); } private function ability( bool $diviWritable = true, bool $unfiltered = false, ?LibraryAccess $access = null, bool $libraryInsert = true ): DiviNodeCopyAbility { $access ??= LibraryFixtures::access(); $loader = LibraryFixtures::loader($access); $failures = []; $usages = Mockery::mock(GlobalUsages::class); $usages->allows('of')->andReturn(['items' => [], 'count' => 0]); $globals = new GlobalEdits($access, $usages); return new DiviNodeCopyAbility( DiviWriterFixture::writer($this->saved, $failures, $unfiltered, $loader, $globals), $loader, static fn (): bool => $diviWritable, static fn (): bool => $libraryInsert ); } /** @return array */ private function input(array $extra = []): array { return array_merge([ 'post_id' => 7, 'tree_rev' => Rev::of((string) $this->target->post_content), 'source_post_id' => 8, 'source_tree_rev' => Rev::of((string) $this->source->post_content), 'source_id' => 's1', 'after' => 's0', ], $extra); } private function sourceBytes(string $id): string { return Serializer::block( NodeIndex::build((new Parser())->parse((string) $this->source->post_content))->find($id) ); } public function testCopiesANodeByteForByteBetweenPages(): void { $bytes = $this->sourceBytes('s1'); $result = $this->ability()->execute($this->input()); self::assertTrue($result['ok']); self::assertCount(1, $this->saved, 'only the target is written'); self::assertStringContainsString($bytes, $this->saved[0]); self::assertSame('s1', $result['changed'][0]['id']); self::assertFalse($this->ability()->isDirect()); } public function testLeavesTheRestOfTheTargetByteIdentical(): void { $original = (string) $this->target->post_content; $bytes = $this->sourceBytes('s1'); $this->ability()->execute($this->input()); self::assertSame($original, str_replace($bytes, '', $this->saved[0])); self::assertSame( DiviFixtures::markup('stage3/page--globale-instanzen.html'), (string) $this->source->post_content, 'the source is only read' ); } public function testGlobalInstancesStayLinked(): void { $result = $this->ability()->execute($this->input(['source_id' => 'g0'])); self::assertTrue($result['ok']); self::assertStringContainsString('' . '', ['ID' => 8] ); LibraryFixtures::given([$this->target, $this->source]); $result = $this->ability()->execute($this->input(['source_id' => 's0'])); self::assertToolError($result, 'validation_failed'); self::assertSame([], $this->saved); } public function testAnAdminMayCopyBuilderOnlyValues(): void { $this->source = LibraryFixtures::page( '' . '', ['ID' => 8] ); LibraryFixtures::given([$this->target, $this->source]); $result = $this->ability(unfiltered: true)->execute($this->input(['source_id' => 's0'])); self::assertTrue($result['ok']); self::assertStringContainsString('body{display:none}', $this->saved[0]); } public function testTheSourceNeedsItsOwnRights(): void { Functions\when('current_user_can')->alias( static fn (string $cap, mixed ...$args): bool => ! ($cap === 'edit_post' && ($args[0] ?? 0) === 8) ); self::assertToolError($this->ability()->permission($this->input()), 'forbidden'); self::assertToolError($this->ability()->execute($this->input()), 'forbidden'); self::assertSame([], $this->saved); } public function testFollowsLitDiviInsertExposure(): void { $ability = $this->ability(diviWritable: false); self::assertToolError($ability->permission($this->input()), 'unsupported'); self::assertToolError($ability->execute($this->input()), 'unsupported'); self::assertSame([], $this->saved); } public function testCopiesIntoALibraryEntry(): void { $item = LibraryFixtures::item(1055); $result = $this->ability()->execute($this->input([ 'post_id' => 1055, 'tree_rev' => Rev::of((string) $item->post_content), 'source_id' => 's0.r0.c0.m0', 'after' => null, 'parent' => 's0.r0.c0', ])); self::assertTrue($result['ok']); self::assertStringContainsString('Kontakt aufnehmen', $this->saved[0]); } public function testCopiesFromALibraryEntry(): void { $item = LibraryFixtures::item(1055); $result = $this->ability()->execute($this->input([ 'source_post_id' => 1055, 'source_tree_rev' => Rev::of((string) $item->post_content), 'source_id' => 's0', ])); self::assertTrue($result['ok']); self::assertStringContainsString('Library-Blurb', $this->saved[0]); } /** @return array */ private function librarySource(): array { return $this->input([ 'source_post_id' => 1055, 'source_tree_rev' => Rev::of((string) LibraryFixtures::item(1055)->post_content), 'source_id' => 's0', ]); } public function testALibrarySourceNeedsAddLibrary(): void { $ability = $this->ability(access: LibraryFixtures::access(['add_library'])); self::assertToolError($ability->permission($this->librarySource()), 'forbidden'); self::assertToolError($ability->execute($this->librarySource()), 'forbidden'); self::assertSame([], $this->saved); } public function testALibrarySourceNeedsAWritableLibraryGroup(): void { $ability = $this->ability(access: LibraryFixtures::access(writable: false)); self::assertToolError($ability->execute($this->librarySource()), 'forbidden'); self::assertSame([], $this->saved); } public function testALibrarySourceFollowsLibraryInsertExposure(): void { $ability = $this->ability(libraryInsert: false); self::assertToolError($ability->permission($this->librarySource()), 'unsupported'); self::assertToolError($ability->execute($this->librarySource()), 'unsupported'); self::assertSame([], $this->saved); } public function testPageSourcesNeedNeitherAddLibraryNorLibraryInsert(): void { $ability = $this->ability(access: LibraryFixtures::access(['add_library']), libraryInsert: false); self::assertTrue($ability->execute($this->input())['ok']); } public function testAHiddenLibraryIsNoSource(): void { $item = LibraryFixtures::item(1055); $access = new LibraryAccess( LibraryFixtures::store(), LibraryFixtures::roles(), static fn (): bool => true, static fn (): bool => false ); $result = $this->ability(access: $access)->execute($this->input([ 'source_post_id' => 1055, 'source_tree_rev' => Rev::of((string) $item->post_content), 'source_id' => 's0', ])); self::assertToolError($result, 'not_found'); self::assertSame([], $this->saved); } public function testNeverNestsAnInstanceInAGlobalEntry(): void { $item = LibraryFixtures::item(1058); LibraryFixtures::given([$this->target, $this->source, $item]); $result = $this->ability()->execute($this->input([ 'post_id' => 1058, 'tree_rev' => Rev::of((string) $item->post_content), 'source_id' => 's0.r0.c0.g0', 'after' => null, 'parent' => 's0.r0.c0', 'confirm' => true, ])); self::assertToolError($result, 'validation_failed'); self::assertStringContainsString('keine globalen Module', $result->get_error_message()); self::assertSame([], $this->saved); } public function testRefusesAPlaceThatBreaksTheStructure(): void { $result = $this->ability()->execute($this->input(['after' => null, 'parent' => 's0.r0.c0'])); self::assertToolError($result, 'validation_failed'); self::assertSame([], $this->saved); } public function testRefusesAMissingSourceId(): void { self::assertToolError($this->ability()->execute($this->input(['source_id' => null])), 'not_found'); self::assertSame([], $this->saved); } }