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>
266 lines
11 KiB
PHP
266 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
|
|
|
use Brain\Monkey\Functions;
|
|
use LIT\MCP\Compatibility\ReleaseState;
|
|
use LIT\MCP\Integrations\Divi\Design\FontInstaller;
|
|
use LIT\MCP\Compatibility\Call;
|
|
use LIT\MCP\Compatibility\ConstantRequirement;
|
|
use LIT\MCP\Integrations\Divi\Release\DiviAdapters;
|
|
use LIT\MCP\Integrations\Divi\Release\DiviAreas;
|
|
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\Tests\Support\ReleaseMatrices;
|
|
use LIT\MCP\Tests\Unit\TestCase;
|
|
use Mockery;
|
|
use RuntimeException;
|
|
use WP_Error;
|
|
|
|
/**
|
|
* Divi's FontsUtility and HooksRegistration are replaced by Mockery aliases; the tests that need them run in their
|
|
* own process (an alias defines the class for the rest of the process).
|
|
*/
|
|
final class FontInstallerTest extends TestCase
|
|
{
|
|
private const FONTS = 'ET\\Builder\\VisualBuilder\\Fonts\\FontsUtility';
|
|
private const HOOKS = 'ET\\Builder\\VisualBuilder\\Hooks\\HooksRegistration';
|
|
private const UNCHECKED = ['ext' => false, 'type' => false, 'proper_filename' => false];
|
|
|
|
/** @var list<string> */
|
|
private array $files = [];
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
Functions\when('get_option')->justReturn([]);
|
|
Functions\when('sanitize_file_name')->alias(static fn (string $name): string => str_replace(' ', '-', $name));
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
foreach ($this->files as $file) {
|
|
if (is_file($file)) {
|
|
unlink($file);
|
|
}
|
|
}
|
|
parent::tearDown();
|
|
}
|
|
|
|
private function tempFile(string $bytes): string
|
|
{
|
|
$file = (string) tempnam(sys_get_temp_dir(), 'lit-font');
|
|
file_put_contents($file, $bytes);
|
|
$this->files[] = $file;
|
|
|
|
return $file;
|
|
}
|
|
|
|
public function testIsARegisteredDesignAdapterForFontUpload(): void
|
|
{
|
|
$manifest = FontInstaller::manifest();
|
|
|
|
self::assertContains(FontInstaller::class, DiviAdapters::CLASSES);
|
|
self::assertSame(DiviAreas::DESIGN, $manifest->area);
|
|
self::assertSame(
|
|
['FontsUtility::mime_types_font()', 'HooksRegistration::check_filetype_and_ext_font()',
|
|
'FontsUtility::font_add()', 'FontsUtility::font_remove()'],
|
|
array_map(static fn (Call $call): string => $call->label(), $manifest->calls)
|
|
);
|
|
self::assertSame([0, 3, 4, 1], array_map(static fn (Call $call): ?int => $call->args, $manifest->calls));
|
|
self::assertSame(
|
|
[self::FONTS . '::FONT_NAME_MAX_LENGTH'],
|
|
array_map(static fn (ConstantRequirement $constant): string => $constant->name, $manifest->constants)
|
|
);
|
|
self::assertCount(1, $manifest->mirrors);
|
|
self::assertSame('font-install', $manifest->mirrors[0]->id);
|
|
self::assertSame(
|
|
'includes/builder-5/server/VisualBuilder/AiAgent/AiAgentImageUtility.php',
|
|
$manifest->mirrors[0]->file
|
|
);
|
|
self::assertSame('AiAgentImageUtility::install_to_custom_fonts()', $manifest->mirrors[0]->label());
|
|
self::assertSame([DiviFeature::FontUpload], $manifest->features);
|
|
self::assertSame(['et_uploaded_fonts'], $manifest->keys);
|
|
}
|
|
|
|
public function testInstallNeedsAReleasedDivi(): void
|
|
{
|
|
ReleaseGate::useResolver(
|
|
static fn (): ReleaseStatus => ReleaseMatrices::uniform(ReleaseState::Untested, '5.16.0')
|
|
);
|
|
|
|
$this->expectException(DiviLocked::class);
|
|
|
|
(new FontInstaller())->install('Inter', ['woff2' => '/tmp/x'], 'all', 'sans-serif');
|
|
}
|
|
|
|
/** @runInSeparateProcess */
|
|
public function testFormatsAreThePluginsFourAsFarAsDiviKnowsThem(): void
|
|
{
|
|
Mockery::mock('alias:' . self::FONTS)->allows('mime_types_font')->andReturn([
|
|
'otf' => ['font/otf'],
|
|
'ttf' => ['font/ttf'],
|
|
'woff' => ['font/woff'],
|
|
'eot' => ['application/vnd.ms-fontobject'],
|
|
]);
|
|
|
|
self::assertSame(['woff', 'ttf', 'otf'], (new FontInstaller())->formats(), 'no woff2 in Divi, never eot');
|
|
}
|
|
|
|
/** @runInSeparateProcess */
|
|
public function testTheNameLimitIsDivis(): void
|
|
{
|
|
Mockery::getConfiguration()->setConstantsMap([self::FONTS => ['FONT_NAME_MAX_LENGTH' => 80]]);
|
|
Mockery::mock('alias:' . self::FONTS);
|
|
|
|
self::assertSame(80, (new FontInstaller())->nameMaxLength());
|
|
}
|
|
|
|
/** @runInSeparateProcess */
|
|
public function testAcceptsAsksDivisContentCheckAndWantsTheGivenExtensionBack(): void
|
|
{
|
|
$file = $this->tempFile('wOF2DATA');
|
|
$hooks = Mockery::mock('alias:' . self::HOOKS);
|
|
$hooks->expects('check_filetype_and_ext_font')->with(self::UNCHECKED, $file, 'Inter.woff2')
|
|
->andReturn(['ext' => 'woff2', 'type' => 'font/woff2', 'proper_filename' => 'Inter.woff2']);
|
|
$hooks->expects('check_filetype_and_ext_font')->with(self::UNCHECKED, $file, 'Inter.ttf')
|
|
->andReturn(['ext' => 'woff2', 'type' => 'font/woff2', 'proper_filename' => 'Inter.ttf']);
|
|
$hooks->expects('check_filetype_and_ext_font')->with(self::UNCHECKED, $file, 'Inter.otf')
|
|
->andReturn(self::UNCHECKED);
|
|
|
|
$installer = new FontInstaller();
|
|
|
|
self::assertTrue($installer->accepts($file, 'Inter.woff2'));
|
|
self::assertFalse($installer->accepts($file, 'Inter.ttf'), 'Divi reports another extension');
|
|
self::assertFalse($installer->accepts($file, 'Inter.otf'), 'Divi refuses the content');
|
|
}
|
|
|
|
/** @runInSeparateProcess */
|
|
public function testInstallHandsTheTempFilesToFontAddLikeDivisAiAgent(): void
|
|
{
|
|
$woff2 = $this->tempFile('wOF2DATA');
|
|
$ttf = $this->tempFile("\x00\x01\x00\x00TTF");
|
|
$hooks = Mockery::mock('alias:' . self::HOOKS);
|
|
$hooks->allows('check_filetype_and_ext_font')->andReturnUsing(
|
|
static fn (array $unchecked, string $file, string $name): array => [
|
|
'ext' => pathinfo($name, PATHINFO_EXTENSION),
|
|
'type' => 'font/' . pathinfo($name, PATHINFO_EXTENSION),
|
|
'proper_filename' => $name,
|
|
]
|
|
);
|
|
Mockery::mock('alias:' . self::FONTS)->expects('font_add')->with(
|
|
[
|
|
'woff2' => ['name' => 'Inter-Display.woff2', 'type' => 'font/woff2', 'tmp_name' => $woff2,
|
|
'error' => 0, 'size' => 8],
|
|
'ttf' => ['name' => 'Inter-Display.ttf', 'type' => 'font/ttf', 'tmp_name' => $ttf, 'error' => 0,
|
|
'size' => 7],
|
|
],
|
|
'Inter Display',
|
|
['font_weights' => '400,700', 'generic_family' => 'serif'],
|
|
['action' => 'wp_handle_upload_rest']
|
|
)->andReturn([
|
|
'uploaded_font' => 'Inter Display',
|
|
'updated_fonts' => [
|
|
'Andere' => ['font_file' => ['otf' => '/up/et-fonts/Andere.otf']],
|
|
'Inter Display' => ['font_file' => [
|
|
'woff2' => '/up/et-fonts/Inter-Display.woff2',
|
|
'ttf' => '/up/et-fonts/Inter-Display.ttf',
|
|
]],
|
|
],
|
|
]);
|
|
|
|
$installed = (new FontInstaller())->install(
|
|
'Inter Display',
|
|
['woff2' => $woff2, 'ttf' => $ttf],
|
|
'400,700',
|
|
'serif'
|
|
);
|
|
|
|
self::assertSame(['woff2', 'ttf'], $installed);
|
|
}
|
|
|
|
/** @runInSeparateProcess */
|
|
public function testAPartialSuccessListsOnlyTheFormatsDiviStored(): void
|
|
{
|
|
$woff2 = $this->tempFile('wOF2DATA');
|
|
$ttf = $this->tempFile("\x00\x01\x00\x00TTF");
|
|
Mockery::mock('alias:' . self::HOOKS)->allows('check_filetype_and_ext_font')->andReturnUsing(
|
|
static fn (array $unchecked, string $file, string $name): array => [
|
|
'ext' => pathinfo($name, PATHINFO_EXTENSION),
|
|
'type' => 'font/x',
|
|
'proper_filename' => $name,
|
|
]
|
|
);
|
|
Mockery::mock('alias:' . self::FONTS)->allows('font_add')->andReturn([
|
|
'uploaded_font' => 'Inter',
|
|
'updated_fonts' => ['Inter' => ['font_file' => ['woff2' => '/up/et-fonts/Inter.woff2']]],
|
|
]);
|
|
|
|
self::assertSame(
|
|
['woff2'],
|
|
(new FontInstaller())->install('Inter', ['woff2' => $woff2, 'ttf' => $ttf], 'all', 'sans-serif')
|
|
);
|
|
}
|
|
|
|
/** @runInSeparateProcess */
|
|
public function testACorruptFontListStopsTheInstallBeforeFontAdd(): void
|
|
{
|
|
$woff2 = $this->tempFile('wOF2DATA');
|
|
Functions\when('get_option')->justReturn('kaputt');
|
|
Mockery::mock('alias:' . self::HOOKS)->allows('check_filetype_and_ext_font')
|
|
->andReturn(['ext' => 'woff2', 'type' => 'font/woff2', 'proper_filename' => 'Inter.woff2']);
|
|
Mockery::mock('alias:' . self::FONTS)->expects('font_add')->never();
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
$this->expectExceptionMessage('et_uploaded_fonts');
|
|
|
|
(new FontInstaller())->install('Inter', ['woff2' => $woff2], 'all', 'sans-serif');
|
|
}
|
|
|
|
/** @runInSeparateProcess */
|
|
public function testAFileDivisTypeCheckRefusesStopsTheInstallBeforeFontAdd(): void
|
|
{
|
|
$woff2 = $this->tempFile('wOF2DATA');
|
|
Mockery::mock('alias:' . self::HOOKS)->allows('check_filetype_and_ext_font')->andReturn(self::UNCHECKED);
|
|
Mockery::mock('alias:' . self::FONTS)->expects('font_add')->never();
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
$this->expectExceptionMessage("font file woff2 refused by Divi's type check");
|
|
|
|
(new FontInstaller())->install('Inter', ['woff2' => $woff2], 'all', 'sans-serif');
|
|
}
|
|
|
|
/** @runInSeparateProcess */
|
|
public function testARefusedFontAddIsARuntimeException(): void
|
|
{
|
|
$woff2 = $this->tempFile('wOF2DATA');
|
|
Mockery::mock('alias:' . self::HOOKS)->allows('check_filetype_and_ext_font')
|
|
->andReturn(['ext' => 'woff2', 'type' => 'font/woff2', 'proper_filename' => 'Inter.woff2']);
|
|
Mockery::mock('alias:' . self::FONTS)->allows('font_add')
|
|
->andReturn(new WP_Error('upload_error_handler', 'Sorry, you are not allowed to upload this file type.'));
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
$this->expectExceptionMessage('font_add refused: Sorry, you are not allowed to upload this file type.');
|
|
|
|
(new FontInstaller())->install('Inter', ['woff2' => $woff2], 'all', 'sans-serif');
|
|
}
|
|
|
|
/** @runInSeparateProcess */
|
|
public function testRemoveDeletesTheFontAndReportsARefusal(): void
|
|
{
|
|
$fonts = Mockery::mock('alias:' . self::FONTS);
|
|
$fonts->expects('font_remove')->with('Inter')->andReturn(['updated_fonts' => []]);
|
|
$fonts->expects('font_remove')->with('Fehlt')->andReturn(new WP_Error('font_not_exist', 'Font Does not Exist.'));
|
|
|
|
(new FontInstaller())->remove('Inter');
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
$this->expectExceptionMessage('font_remove refused: Font Does not Exist.');
|
|
(new FontInstaller())->remove('Fehlt');
|
|
}
|
|
}
|