alias(static fn (string $path): string => 'https://kunde.test/wp-admin/' . $path); } /** * @param array $templateFields * @param array $bodyFields */ private static function givenPosts(array $templateFields = [], array $bodyFields = []): void { LibraryFixtures::given([ ThemeBuilderFixtures::templatePost(1066, $templateFields), ThemeBuilderFixtures::templatePost(1065), ThemeBuilderFixtures::layout(1061), ThemeBuilderFixtures::layout(1062, $bodyFields), ThemeBuilderFixtures::layout(1063), ]); } private static function ability(DiviThemeBuilder $store, ?FakeChangeLog $log = null): TbActivateAbility { return new TbActivateAbility( ThemeBuilderFixtures::access([], true, $store), new TemplateConditions($store), $log ?? DiviWriterFixture::log() ); } /** @return array */ private static function input(Template $template, bool $enabled, bool $confirm = false): array { return array_filter([ 'template_id' => $template->id, 'template_rev' => $template->rev(), 'enabled' => $enabled, 'confirm' => $confirm ?: null, ], static fn (mixed $value): bool => $value !== null); } public function testActivationIsAlwaysADryRunFirst(): void { $template = ThemeBuilderFixtures::template(1066); $other = ThemeBuilderFixtures::template(1070, [ 'enabled' => true, 'title' => 'Seite 1053', 'use_on' => ['singular:post_type:page:id:1053'], ]); $pages = ThemeBuilderFixtures::template(1071, [ 'enabled' => true, 'title' => 'Alle Seiten', 'use_on' => ['singular:post_type:page:all'], ]); $store = ThemeBuilderFixtures::store([$template, $other, $pages]); $store->shouldReceive('setEnabled')->never(); $dry = self::ability($store, FakeChangeLog::forbidden())->execute(self::input($template, true)); self::assertToolError($dry, 'confirm_required'); self::assertStringContainsString('Aktivieren', self::clientError($dry)['message']); self::assertSame(['Bestimmte Seiten: 1053'], $dry->get_error_data()['use_on']); self::assertSame([], $dry->get_error_data()['exclude_from']); self::assertSame(1070, $dry->get_error_data()['overlaps'][0]['id']); self::assertSame(1071, $dry->get_error_data()['possible_overlaps'][0]['id']); self::assertSame(1062, $dry->get_error_data()['layouts']['body']['id']); } public function testConfirmedActivationSwitchesOnlyTheFlag(): void { $template = ThemeBuilderFixtures::template(1066); $store = ThemeBuilderFixtures::store([$template]); $store->shouldReceive('setEnabled')->once()->with(1066, true); $store->shouldReceive('clearCaches')->once(); $store->shouldReceive('setConditions')->never(); $store->shouldReceive('attach')->never(); $log = DiviWriterFixture::log(); $result = self::ability($store, $log)->execute(self::input($template, true, true)); self::assertTrue($result['ok']); self::assertTrue($result['enabled']); self::assertTrue($result['live']); self::assertStringContainsString('aktiviert', $result['summary']); self::assertStringContainsString('enabled: false', $result['summary'], 'the way back is named'); self::assertSame(4711, $result['log_id']); self::assertStringContainsString('aktiviert', $log->summaries()[0]); } public function testDeactivationAlsoNeedsConfirm(): void { $template = ThemeBuilderFixtures::template(1066, ['enabled' => true]); $store = ThemeBuilderFixtures::store([$template]); $store->shouldReceive('setEnabled')->once()->with(1066, false); $store->shouldReceive('clearCaches')->once(); $dry = self::ability($store)->execute(self::input($template, false)); self::assertToolError($dry, 'confirm_required'); self::assertStringContainsString('Deaktivieren', self::clientError($dry)['message']); self::assertSame([], $dry->get_error_data()['overlaps']); $result = self::ability($store)->execute(self::input($template, false, true)); self::assertTrue($result['ok']); self::assertFalse($result['enabled']); self::assertFalse($result['live']); } /** Ruling Q16: an attached, enabled template in the trash is still rendered – the off-switch reaches it. */ public function testDeactivatesALiveTemplateInTheTrash(): void { self::givenPosts(['post_status' => 'trash']); $trashed = ThemeBuilderFixtures::template(1066, ['enabled' => true, 'status' => 'trash']); $store = ThemeBuilderFixtures::store([$trashed]); $store->shouldReceive('setEnabled')->once()->with(1066, false); $store->shouldReceive('clearCaches')->once(); self::assertTrue(self::ability($store)->permission(self::input($trashed, false))); self::assertToolError(self::ability($store)->permission(self::input($trashed, true)), 'not_found'); self::assertToolError(self::ability($store)->execute(self::input($trashed, true, true)), 'not_found'); $dry = self::ability($store)->execute(self::input($trashed, false)); self::assertToolError($dry, 'confirm_required'); self::assertStringContainsString('Papierkorb', self::clientError($dry)['message']); self::assertTrue(self::ability($store)->execute(self::input($trashed, false, true))['ok']); } /** Ruling Q8/B10: enabling needs published layouts; switching off never does. */ public function testActivationNeedsPublishedLayoutsDeactivationDoesNot(): void { self::givenPosts([], ['post_status' => 'trash']); $off = ThemeBuilderFixtures::template(1066); $store = ThemeBuilderFixtures::store([$off]); $store->shouldReceive('setEnabled')->never(); $refused = self::ability($store, FakeChangeLog::forbidden())->execute(self::input($off, true)); self::assertToolError($refused, 'validation_failed'); self::assertSame(['layouts.body'], array_keys(self::errorFields($refused))); $on = ThemeBuilderFixtures::template(1066, ['enabled' => true]); $store = ThemeBuilderFixtures::store([$on]); $store->shouldReceive('setEnabled')->once()->with(1066, false); $store->shouldReceive('clearCaches'); self::assertTrue(self::ability($store)->execute(self::input($on, false, true))['ok']); } public function testRefusesTemplatesWithoutConditionsOrDetached(): void { $empty = ThemeBuilderFixtures::template(1066, ['use_on' => []]); $store = ThemeBuilderFixtures::store([$empty]); $store->shouldReceive('setEnabled')->never(); self::assertToolError(self::ability($store)->execute(self::input($empty, true, true)), 'validation_failed'); $detached = ThemeBuilderFixtures::template(1066, ['attached' => false]); $store = ThemeBuilderFixtures::store([], 0, [$detached]); $store->shouldReceive('setEnabled')->never(); $refused = self::ability($store)->execute(self::input($detached, true, true)); self::assertToolError($refused, 'validation_failed'); self::assertStringContainsString('reattach', (string) json_encode(self::errorFields($refused))); } public function testNoChangeWhenAlreadyInThatState(): void { $template = ThemeBuilderFixtures::template(1066); $store = ThemeBuilderFixtures::store([$template]); $store->shouldReceive('setEnabled')->never(); $result = self::ability($store, FakeChangeLog::forbidden())->execute(self::input($template, false)); self::assertStringContainsString('Keine Änderung', $result['summary']); self::assertTrue(self::ability($store)->annotations()['destructive']); self::assertFalse(self::ability($store)->isDirect()); } public function testMissingEnabledAndTheDefaultTemplateAreRefused(): void { $template = ThemeBuilderFixtures::template(1066); $store = ThemeBuilderFixtures::store([ThemeBuilderFixtures::template(1065), $template]); $store->shouldReceive('setEnabled')->never(); $input = self::input($template, true, true); unset($input['enabled']); self::assertToolError(self::ability($store)->execute($input), 'validation_failed'); self::assertToolError( self::ability($store)->execute(self::input(ThemeBuilderFixtures::template(1065), true, true)), 'unsupported' ); } public function testAFailedSwitchIsRolledBack(): void { $template = ThemeBuilderFixtures::template(1066); $store = ThemeBuilderFixtures::store([$template]); $calls = []; $store->shouldReceive('setEnabled')->andReturnUsing(static function (int $id, bool $on) use (&$calls): void { $calls[] = $on; if ($on) { throw new RuntimeException('_et_enabled not stored'); } }); $store->shouldReceive('clearCaches')->never(); try { self::ability($store, FakeChangeLog::forbidden())->execute(self::input($template, true, true)); self::fail('no WriteFailed'); } catch (WriteFailed $e) { self::assertTrue($e->rolledBack); } self::assertSame([true, false], $calls); } public function testAFailedRollbackIsAPartialWrite(): void { $template = ThemeBuilderFixtures::template(1066); $store = ThemeBuilderFixtures::store([$template]); $store->shouldReceive('setEnabled')->andThrow(new RuntimeException('database gone')); $log = FakeChangeLog::partialsOnly(); try { self::ability($store, $log)->execute(self::input($template, true, true)); self::fail('no WriteFailed'); } catch (WriteFailed $e) { self::assertFalse($e->rolledBack); } $log->assertPartials(['ability' => 'lit/tb-activate', 'object_type' => 'et_template', 'object_id' => '1066']); } }