Files
WP-LIT-MCP/tests/Unit/Integrations/Divi/Release/ReleaseResolverTest.php
T

321 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
use Brain\Monkey\Filters;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\ManifestCheck;
use LIT\MCP\Integrations\Divi\Release\Problem;
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
use LIT\MCP\Integrations\Divi\Release\ReleaseStore;
use LIT\MCP\Plugin;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
final class ReleaseResolverTest extends TestCase
{
private const UNKNOWN = '5.99.0';
/** @var array<string, mixed> */
private array $transients = [];
private int $mirrorRuns = 0;
private int $codeRuns = 0;
protected function setUp(): void
{
parent::setUp();
$this->transients = [];
$this->mirrorRuns = 0;
$this->codeRuns = 0;
Functions\when('get_transient')->alias(fn (string $key): mixed => $this->transients[$key] ?? false);
Functions\when('set_transient')->alias(function (string $key, mixed $value): bool {
$this->transients[$key] = $value;
return true;
});
}
/**
* @param list<Problem> $code
* @param list<Problem> $mirrors
* @param array<string, mixed>|null $entry
*/
private function resolver(
string $version,
array $code = [],
array $mirrors = [],
?array $entry = null
): ReleaseResolver {
$check = Mockery::mock(ManifestCheck::class);
$check->allows('codeProblems')->andReturnUsing(function () use ($code): array {
++$this->codeRuns;
return $code;
});
$check->allows('mirrorProblems')->andReturnUsing(function () use ($mirrors): array {
++$this->mirrorRuns;
return $mirrors;
});
$store = Mockery::mock(ReleaseStore::class);
$store->allows('find')->with($version, Plugin::VERSION)->andReturn($entry);
return new ReleaseResolver(
$check,
$store,
static fn (): ?string => $version,
static fn (string $v): string => 'key-' . $v
);
}
/** @param list<DiviFeature> $features */
private static function problem(string $kind, array $features, string $label = 'Seiten: et_x()'): Problem
{
return new Problem($label, $features, $kind);
}
/** @return array<string, string> Feature value => state, for every feature. */
private static function states(ReleaseStatus $status): array
{
$states = [];
foreach (DiviFeature::cases() as $feature) {
$states[$feature->value] = $status->matrix->of($feature)->state->value;
}
return $states;
}
/**
* @param array<string, string> $except Feature value => state.
* @return array<string, string>
*/
private static function allBut(string $state, array $except = []): array
{
$states = [];
foreach (DiviFeature::cases() as $feature) {
$states[$feature->value] = $except[$feature->value] ?? $state;
}
return $states;
}
public function testATestedVersionWithoutProblemsIsReleasedFromCode(): void
{
$status = $this->resolver('5.2.1')->resolve();
self::assertSame(ReleaseState::Released, $status->state);
self::assertSame('code', $status->source);
self::assertSame(self::allBut('released'), self::states($status));
self::assertSame('code', $status->matrix->of(DiviFeature::PageWrite)->source);
}
public function testAReleaseCoversChangedSignaturesAndMissingReferences(): void
{
$status = $this->resolver('5.2.1', [
self::problem(Problem::SIGNATURE, [DiviFeature::PageWrite]),
self::problem(Problem::REFERENCE_MISSING, [DiviFeature::Catalog]),
])->resolve();
self::assertSame(self::allBut('released'), self::states($status));
self::assertSame([], $status->matrix->unusable());
self::assertSame([], $status->problems);
}
public function testAHardProblemLocksItsFeaturesEvenForATestedVersion(): void
{
$status = $this->resolver('5.2.1', [
self::problem(Problem::CALL, [DiviFeature::PageWrite], 'Seiten: et_x()'),
self::problem(Problem::CONSTANT, [DiviFeature::Catalog], 'Katalog: Konstante X'),
])->resolve();
self::assertSame(
self::allBut('released', ['page-write' => 'incompatible', 'catalog' => 'incompatible']),
self::states($status)
);
self::assertSame(['Seiten: et_x()'], $status->matrix->of(DiviFeature::PageWrite)->problems);
self::assertSame(ReleaseState::Incompatible, $status->state);
self::assertSame(['Seiten: et_x()', 'Katalog: Konstante X'], $status->problems);
self::assertFalse($status->matrix->of(DiviFeature::PageWrite)->usable());
}
public function testAProblemWithoutFeaturesLocksEveryFeature(): void
{
$status = $this->resolver('5.2.1', [self::problem(Problem::CALL, [])])->resolve();
self::assertSame(self::allBut('incompatible'), self::states($status));
}
/** Review focus 2: a changed mirror locks exactly its features – released version or automatic release. */
public function testAChangedMirrorLocksExactlyItsFeatures(): void
{
$mirror = self::problem(
Problem::MIRROR,
[DiviFeature::ThemeBuilderWrite, DiviFeature::LibraryWrite],
'Theme Builder: Nachbau von y() – z'
);
$expected = ['tb-write' => 'incompatible', 'library-write' => 'incompatible'];
self::assertSame(
self::allBut('released', $expected),
self::states($this->resolver('5.2.1', mirrors: [$mirror])->resolve())
);
$automatic = $this->resolver(self::UNKNOWN, mirrors: [$mirror])->resolve();
self::assertSame(self::allBut('released', $expected), self::states($automatic));
self::assertSame(
['Theme Builder: Nachbau von y() – z'],
$automatic->matrix->of(DiviFeature::ThemeBuilderWrite)->problems
);
}
public function testAnUnknownVersionWithoutProblemsIsReleasedAutomatically(): void
{
$status = $this->resolver(self::UNKNOWN)->resolve();
self::assertSame(self::allBut('released'), self::states($status));
self::assertSame('auto', $status->source);
self::assertSame('auto', $status->matrix->of(DiviFeature::PresetWrite)->source);
self::assertSame([], $status->matrix->unusable());
}
/** Review focus 3: the automatic release needs every signature unchanged. */
public function testAChangedSignatureMakesItsFeatureIncompatibleUnderAutomaticRelease(): void
{
$status = $this->resolver(self::UNKNOWN, [
self::problem(Problem::SIGNATURE, [DiviFeature::PresetWrite], 'Designsystem: x() – Signatur geändert'),
])->resolve();
self::assertSame(self::allBut('released', ['preset-write' => 'incompatible']), self::states($status));
self::assertSame(
['Designsystem: x() – Signatur geändert'],
$status->matrix->of(DiviFeature::PresetWrite)->problems
);
self::assertFalse($status->matrix->of(DiviFeature::PresetWrite)->usable());
}
public function testAMissingReferenceMakesItsFeatureUntestedUnderAutomaticRelease(): void
{
$status = $this->resolver(self::UNKNOWN, [
self::problem(Problem::REFERENCE_MISSING, [DiviFeature::Roles], 'Seiten: r() – Referenz fehlt'),
self::problem(Problem::REFERENCE_MISSING, [DiviFeature::PageRead]),
self::problem(Problem::SIGNATURE, [DiviFeature::PageRead]),
])->resolve();
self::assertSame(
self::allBut('released', ['roles' => 'untested', 'page-read' => 'incompatible']),
self::states($status)
);
self::assertSame(
['Seiten: r() – Referenz fehlt'],
$status->matrix->of(DiviFeature::Roles)->problems
);
}
/** Review focus 1: unknown version with the automatic release switched off. */
public function testWithoutAutomaticReleaseAnUnknownVersionIsUntestedUnlessSomethingHardFailed(): void
{
Filters\expectApplied(ReleaseResolver::AUTO_RELEASE_FILTER)->once()->with(true)->andReturn(false);
$status = $this->resolver(self::UNKNOWN, [
self::problem(Problem::SIGNATURE, [DiviFeature::PresetWrite]),
self::problem(Problem::CALL, [DiviFeature::Catalog]),
])->resolve();
self::assertSame(self::allBut('untested', ['catalog' => 'incompatible']), self::states($status));
self::assertNull($status->matrix->of(DiviFeature::PageRead)->source);
}
public function testOnlyALiteralTrueSwitchesTheAutomaticReleaseOn(): void
{
Filters\expectApplied(ReleaseResolver::AUTO_RELEASE_FILTER)->once()->andReturn(1);
self::assertSame(self::allBut('untested'), self::states($this->resolver(self::UNKNOWN)->resolve()));
}
public function testADatabaseReleaseReleasesEveryFeatureWithoutHardProblems(): void
{
$entry = ['user_id' => 1, 'released_at' => 'd'];
$status = $this->resolver(self::UNKNOWN, [
self::problem(Problem::SIGNATURE, [DiviFeature::PageWrite]),
self::problem(Problem::REFERENCE_MISSING, [DiviFeature::Catalog]),
], [self::problem(Problem::MIRROR, [DiviFeature::LibraryWrite])], $entry)->resolve();
self::assertSame(self::allBut('released', ['library-write' => 'incompatible']), self::states($status));
self::assertSame('database', $status->matrix->of(DiviFeature::PageWrite)->source);
self::assertSame($entry, $status->release);
}
public function testWithoutDiviNothingIsReleasedAutomatically(): void
{
$status = $this->resolver('')->resolve();
self::assertSame(ReleaseState::Untested, $status->state);
self::assertSame(self::allBut('untested'), self::states($status));
}
public function testCodeProblemsRunOnEveryResolveMirrorProblemsOncePerStateKey(): void
{
$mirrors = [self::problem(Problem::MIRROR, [DiviFeature::ThemeBuilderWrite], 'Theme Builder: Nachbau von y()')];
$first = $this->resolver('5.2.1', mirrors: $mirrors)->resolve();
$second = $this->resolver('5.2.1', mirrors: $mirrors)->resolve();
self::assertSame(2, $this->codeRuns, 'code and signature checks are live');
self::assertSame(1, $this->mirrorRuns, 'the second resolve reads the transient');
self::assertSame(self::states($first), self::states($second));
self::assertSame($first->problems, $second->problems);
self::assertSame(
['format' => 2, 'key' => 'key-5.2.1', 'problems' => [
['label' => 'Theme Builder: Nachbau von y()', 'kind' => 'mirror', 'features' => ['tb-write']],
]],
$this->transients[ReleaseResolver::CACHE]
);
$this->resolver('5.2.2', mirrors: $mirrors)->resolve();
self::assertSame(2, $this->mirrorRuns, 'another state key checks again');
}
/** Review focus 1: an old or broken transient is never trusted. */
public function testAnOldOrBrokenCacheEntryIsAMiss(): void
{
$mirrors = [self::problem(Problem::MIRROR, [DiviFeature::PageWrite])];
$entries = [
'format 1' => ['key' => 'key-5.2.1', 'problems' => []],
'other format' => ['format' => 3, 'key' => 'key-5.2.1', 'problems' => []],
'unknown feature' => ['format' => 2, 'key' => 'key-5.2.1', 'problems' => [
['label' => 'x', 'kind' => 'mirror', 'features' => ['page-delete']],
]],
'no list' => ['format' => 2, 'key' => 'key-5.2.1', 'problems' => 'none'],
'garbage' => 'x',
];
foreach ($entries as $case => $entry) {
$this->transients[ReleaseResolver::CACHE] = $entry;
$runs = $this->mirrorRuns;
$status = $this->resolver('5.2.1', mirrors: $mirrors)->resolve();
self::assertSame($runs + 1, $this->mirrorRuns, $case);
self::assertFalse($status->matrix->of(DiviFeature::PageWrite)->usable(), $case);
}
}
public function testTheCacheHoldsProblemsSoAReleaseCountsAtOnce(): void
{
Filters\expectApplied(ReleaseResolver::AUTO_RELEASE_FILTER)->andReturn(false);
$before = $this->resolver(self::UNKNOWN)->resolve();
$after = $this->resolver(self::UNKNOWN, entry: ['user_id' => 1, 'released_at' => 'd'])->resolve();
self::assertSame(ReleaseState::Untested, $before->state);
self::assertSame(ReleaseState::Released, $after->state);
self::assertSame(1, $this->mirrorRuns);
}
}