Requirement, Call, ConstantRequirement, Mirror, Problem, FeatureStatus, CompatibilityMatrix, Manifest, ManifestCheck sowie Fingerprints und LineDiff liegen jetzt im divi-freien Kern. Neu: GlobalRequirement, CompatibilityMatrix::fromProblems(), ManifestCheck-Option signatures, DiviManifestCheck::create() und DiviAreas. Divi-Verhalten unverändert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
164 lines
7.3 KiB
PHP
164 lines
7.3 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
|
||
|
||
use Brain\Monkey\Functions;
|
||
use LIT\MCP\Compatibility\ReleaseState;
|
||
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
|
||
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
|
||
use LIT\MCP\Integrations\Divi\Library\LibraryItemSpec;
|
||
use LIT\MCP\Integrations\Divi\Pages\DiviPages;
|
||
use LIT\MCP\Integrations\Divi\Pages\DiviSaver;
|
||
use LIT\MCP\Integrations\Divi\Pages\SaveScope;
|
||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
|
||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
|
||
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
|
||
use LIT\MCP\Integrations\WordPress\Content\NewDraft;
|
||
use LIT\MCP\Tests\Support\FakeChangeLog;
|
||
use LIT\MCP\Tests\Support\ReleaseMatrices;
|
||
use LIT\MCP\Tests\Unit\TestCase;
|
||
use RuntimeException;
|
||
use Throwable;
|
||
use WP_Post;
|
||
|
||
/**
|
||
* Spec §5.5, D5: every writing adapter method refuses before its first write exactly when a feature it needs is
|
||
* locked – and passes the gate when only other features are locked. (DesignChange: DesignChangeTest.)
|
||
*/
|
||
final class ReleaseGuardsTest extends TestCase
|
||
{
|
||
private const WRITES = ['wp_update_post', 'wp_insert_post', 'update_post_meta', 'add_post_meta', 'delete_post_meta',
|
||
'update_option', 'delete_option', 'wp_set_object_terms'];
|
||
|
||
protected function setUp(): void
|
||
{
|
||
parent::setUp();
|
||
// Nothing may be written while locked: every write function WordPress offers fails the test.
|
||
foreach (self::WRITES as $write) {
|
||
Functions\when($write)->alias(static function () use ($write): never {
|
||
self::fail($write . ' ran although Divi is locked');
|
||
});
|
||
}
|
||
Functions\when('current_user_can')->justReturn(true);
|
||
}
|
||
|
||
/** @return iterable<string, array{callable(): mixed, list<DiviFeature>}> */
|
||
public static function writes(): iterable
|
||
{
|
||
$post = new WP_Post(['ID' => 7, 'post_content' => '', 'post_type' => 'page']);
|
||
$spec = new LibraryItemSpec('Hero', 'section', false, 'regular', null, 'page');
|
||
$page = [DiviFeature::PageWrite];
|
||
$tb = [DiviFeature::ThemeBuilderWrite];
|
||
|
||
yield 'save' => [static fn () => (new DiviSaver())->save($post, '<!-- wp:divi/section /-->'), $page];
|
||
yield 'save library item' => [
|
||
static fn () => (new DiviSaver())->save($post, 'x', SaveScope::LibraryItem),
|
||
[DiviFeature::PageWrite, DiviFeature::LibraryWrite],
|
||
];
|
||
yield 'save global item' => [
|
||
static fn () => (new DiviSaver())->save($post, 'x', SaveScope::GlobalItem),
|
||
[DiviFeature::PageWrite, DiviFeature::LibraryWrite],
|
||
];
|
||
yield 'save tb layout' => [
|
||
static fn () => (new DiviSaver())->save($post, 'x', SaveScope::ThemeBuilderLayout),
|
||
[DiviFeature::PageWrite, DiviFeature::ThemeBuilderWrite],
|
||
];
|
||
yield 'saveNew' => [static fn () => (new DiviSaver())->saveNew(7, '<!-- wp:divi/section /-->'), $page];
|
||
yield 'saveNew library item' => [
|
||
static fn () => (new DiviSaver())->saveNew(7, 'x', SaveScope::LibraryItem),
|
||
[DiviFeature::PageWrite, DiviFeature::LibraryWrite],
|
||
];
|
||
yield 'pages setFlags' => [static fn () => DiviPages::setFlags(7), $page];
|
||
yield 'library create' => [static fn () => (new DiviLibraryStore())->create(
|
||
$spec,
|
||
new NewDraft(null, FakeChangeLog::forbidden(), 'lit/library-save', 'et_pb_layout', 'Bibliothekseintrag')
|
||
), [DiviFeature::LibraryWrite]];
|
||
yield 'tb liveId create' => [static fn () => (new DiviThemeBuilder())->liveId(true), $tb];
|
||
yield 'tb insertLayout' => [static fn () => (new DiviThemeBuilder())->insertLayout('header', 'Kopf'), $tb];
|
||
yield 'tb storeTemplate' => [static fn () => (new DiviThemeBuilder())->storeTemplate(3, [
|
||
'title' => 'V', 'layouts' => [], 'use_on' => [], 'exclude_from' => [],
|
||
]), $tb];
|
||
yield 'tb attach' => [static fn () => (new DiviThemeBuilder())->attach(3, 9), $tb];
|
||
yield 'tb detach' => [static fn () => (new DiviThemeBuilder())->detach(3, 9), $tb];
|
||
yield 'tb setConditions' => [static fn () => (new DiviThemeBuilder())->setConditions(9, ['singular'], []), $tb];
|
||
yield 'tb setEnabled' => [static fn () => (new DiviThemeBuilder())->setEnabled(9, true), $tb];
|
||
yield 'tb unmarkUnused' => [static fn () => (new DiviThemeBuilder())->unmarkUnused(9), $tb];
|
||
yield 'design savePresets' => [
|
||
static fn () => (new DesignSystemWriter())->savePresets(['module' => [], 'group' => []], false),
|
||
[DiviFeature::PresetWrite],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* @dataProvider writes
|
||
* @param list<DiviFeature> $needs
|
||
*/
|
||
public function testRefusesBeforeTheFirstWriteWhenAFeatureItNeedsIsLocked(callable $write, array $needs): void
|
||
{
|
||
foreach ([ReleaseState::Untested, ReleaseState::Incompatible] as $state) {
|
||
foreach ($needs as $feature) {
|
||
ReleaseGate::useResolver(static fn (): ReleaseStatus => ReleaseMatrices::status([$feature], $state));
|
||
|
||
try {
|
||
$write();
|
||
self::fail(sprintf('%s %s must refuse', $feature->value, $state->value));
|
||
} catch (DiviLocked $locked) {
|
||
self::assertSame([$feature], $locked->features);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @dataProvider writes
|
||
* @param list<DiviFeature> $needs
|
||
*/
|
||
public function testPassesTheGateWhenOnlyOtherFeaturesAreLocked(callable $write, array $needs): void
|
||
{
|
||
$others = array_values(array_filter(
|
||
DiviFeature::cases(),
|
||
static fn (DiviFeature $feature): bool => ! in_array($feature, $needs, true)
|
||
));
|
||
ReleaseGate::useResolver(
|
||
static fn (): ReleaseStatus => ReleaseMatrices::status($others, ReleaseState::Incompatible)
|
||
);
|
||
// Past the gate the method may write or fail on the missing Divi – anything but the lock.
|
||
foreach (self::WRITES as $name) {
|
||
Functions\when($name)->alias(static function () use ($name): never {
|
||
throw new RuntimeException($name . ' reached');
|
||
});
|
||
}
|
||
|
||
try {
|
||
$write();
|
||
} catch (DiviLocked $locked) {
|
||
self::fail('refused although only other features are locked: ' . $locked->getMessage());
|
||
} catch (Throwable) {
|
||
// Reached the code behind the gate.
|
||
}
|
||
self::addToAssertionCount(1);
|
||
}
|
||
|
||
public function testTheCheckPageMayBeWrittenWhileProbing(): void
|
||
{
|
||
ReleaseGate::useResolver(
|
||
static fn (): ReleaseStatus => ReleaseMatrices::uniform(ReleaseState::Untested, '5.3.0')
|
||
);
|
||
Functions\when('wp_update_post')->justReturn(41);
|
||
Functions\when('update_post_meta')->justReturn(true);
|
||
Functions\when('get_post_meta')->alias(
|
||
static fn (int $id, string $key): string => $key === '_et_pb_show_page_creation' ? 'off' : 'on'
|
||
);
|
||
Functions\when('get_post')->justReturn(new WP_Post(['ID' => 41, 'post_content' => 'x']));
|
||
Functions\when('has_filter')->justReturn(false);
|
||
|
||
$warnings = ReleaseGate::whileProbing(41, static fn () => (new DiviSaver())->saveNew(41, 'x'));
|
||
|
||
self::assertSame([], $warnings);
|
||
}
|
||
}
|