Files

232 lines
10 KiB
PHP
Raw Permalink 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\ThemeBuilder;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Divi\ThemeBuilder\DiviThemeBuilder;
use LIT\MCP\Integrations\Divi\ThemeBuilder\TemplateConditions;
use LIT\MCP\Tests\Support\ThemeBuilderFixtures;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use WP_Post;
use WP_Term;
final class TemplateConditionsTest extends TestCase
{
/** @var list<string> Capabilities the user lacks (read_post of 2202 is always missing). */
private array $denied = ['list_users'];
protected function setUp(): void
{
parent::setUp();
Functions\when('get_post')->alias(static fn (int $id): ?WP_Post => match ($id) {
1053 => new WP_Post(['ID' => 1053, 'post_type' => 'page', 'post_status' => 'draft']),
2201 => new WP_Post(['ID' => 2201, 'post_type' => 'lit_person', 'post_status' => 'publish']),
2202 => new WP_Post(['ID' => 2202, 'post_type' => 'page', 'post_status' => 'private']),
2203 => new WP_Post(['ID' => 2203, 'post_type' => 'page', 'post_status' => 'trash']),
7 => new WP_Post(['ID' => 7, 'post_type' => 'page', 'post_status' => 'publish']),
default => null,
});
Functions\when('current_user_can')->alias(
fn (string $cap, mixed ...$args): bool => ! in_array($cap, $this->denied, true)
&& ! ($cap === 'read_post' && ($args[0] ?? 0) === 2202)
);
Functions\when('get_term')->alias(
static fn (int $id, string $taxonomy): ?WP_Term => $id === 5 && $taxonomy === 'category'
? new WP_Term(['term_id' => 5, 'slug' => 'news', 'name' => 'News'])
: null
);
Functions\when('get_userdata')->justReturn(false);
Functions\when('wp_roles')->justReturn(new class {
public function is_role(string $role): bool
{
return $role === 'editor';
}
});
}
private static function conditions(): TemplateConditions
{
return new TemplateConditions(ThemeBuilderFixtures::store());
}
public function testAcceptsDivisConditionsAndReadableObjects(): void
{
$checked = self::conditions()->check([
'singular:post_type:lit_person:all',
'homepage',
'404',
'singular:post_type:page:id:1053',
'singular:post_type:lit_person:id:2201',
'archive:taxonomy:category:term:id:5',
'archive:user:role:editor',
'homepage',
], 'use_on');
self::assertSame([], $checked['errors']);
self::assertSame([
'singular:post_type:lit_person:all',
'homepage',
'404',
'singular:post_type:page:id:1053',
'singular:post_type:lit_person:id:2201',
'archive:taxonomy:category:term:id:5',
'archive:user:role:editor',
], $checked['ids']);
}
public function testRefusesUnknownUnreadableAndMalformedConditions(): void
{
$checked = self::conditions()->check([
'singular:post_type:page:all:x',
'singular:post_type:page:id:2202',
'singular:post_type:page:id:2203',
'singular:post_type:page:id:2201',
'singular:post_type:page:id:0123',
"homepage\n",
'archive:user:id:1',
'archive:taxonomy:category:term:id:6',
'archive:user:role:hacker',
'singular:post_type:page:id:',
42,
], 'use_on');
self::assertSame([], $checked['ids']);
self::assertSame(
array_map(static fn (int $i): string => 'use_on.' . $i, range(0, 10)),
array_keys($checked['errors'])
);
self::assertStringContainsString('unbekannte Bedingung', $checked['errors']['use_on.0']);
// Private and trashed look the same – no oracle over other users' entries.
self::assertSame(
$checked['errors']['use_on.1'],
str_replace('2203', '2202', $checked['errors']['use_on.2'])
);
self::assertStringContainsString('list_users', $checked['errors']['use_on.6']);
}
public function testListShapeAndSize(): void
{
self::assertSame(['ids' => [], 'errors' => []], self::conditions()->check(null, 'use_on'));
self::assertArrayHasKey('use_on', self::conditions()->check('homepage', 'use_on')['errors']);
self::assertArrayHasKey('use_on', self::conditions()->check(['a' => 'homepage'], 'use_on')['errors']);
self::assertArrayHasKey(
'exclude_from',
self::conditions()->check(array_fill(0, TemplateConditions::MAX + 1, 'homepage'), 'exclude_from')['errors']
);
self::assertSame(
[],
self::conditions()->check(array_fill(0, TemplateConditions::MAX, 'homepage'), 'exclude_from')['errors']
);
}
public function testLabelsAndTheCatalog(): void
{
self::assertSame('Alle Personen', self::conditions()->label('singular:post_type:lit_person:all'));
self::assertSame('Bestimmte Seiten: 1053', self::conditions()->label('singular:post_type:page:id:1053'));
self::assertSame('Unterseiten bestimmter Seiten: 7', self::conditions()->label('singular:post_type:page:children:id:7'));
self::assertSame('404-Seite', self::conditions()->label('404'));
self::assertSame('unbekannt:x', self::conditions()->label('unbekannt:x'));
self::assertSame('singular:post_type:page:id:', self::conditions()->label('singular:post_type:page:id:'));
$available = array_column(self::conditions()->available(), null, 'id');
self::assertNull($available['homepage']['needs']);
self::assertSame('post_type:page', $available['singular:post_type:page:id:']['needs']);
self::assertSame('taxonomy:category', $available['archive:taxonomy:category:term:id:']['needs']);
self::assertSame('user', $available['archive:user:id:']['needs']);
self::assertSame('user_role', $available['archive:user:role:']['needs']);
self::assertSame('404-Seite', $available['404']['label']);
self::assertSame(array_keys(ThemeBuilderFixtures::conditions()), array_keys($available));
}
/** Every ID Divi offers round-trips: exactly, or with an object the user may read. */
public function testEveryConditionDiviOffersRoundTrips(): void
{
$this->denied = [];
Functions\when('get_userdata')->alias(static fn (int $id): object|false => $id === 1 ? (object) ['ID' => 1] : false);
$objects = [
'post_type:page' => '1053',
'post_type:lit_person' => '2201',
'taxonomy:category' => '5',
'user' => '1',
'user_role' => 'editor',
];
$ids = [];
foreach (self::conditions()->available() as $condition) {
$ids[] = $condition['id'] . ($condition['needs'] === null ? '' : $objects[$condition['needs']]);
}
$checked = self::conditions()->check($ids, 'use_on');
self::assertSame([], $checked['errors']);
self::assertSame($ids, $checked['ids']);
}
/** Ruling Q10: authors only with list_users, and a missing user reads like a forbidden one. */
public function testAuthorConditionsNeedListUsers(): void
{
Functions\when('get_userdata')->alias(static fn (int $id): object|false => $id === 1 ? (object) ['ID' => 1] : false);
$forbidden = self::conditions()->check(['archive:user:id:1'], 'use_on')['errors'];
$this->denied = [];
$missing = self::conditions()->check(['archive:user:id:2'], 'use_on')['errors'];
$allowed = self::conditions()->check(['archive:user:id:1'], 'use_on');
self::assertSame($forbidden['use_on.0'], $missing['use_on.0']);
self::assertSame(['archive:user:id:1'], $allowed['ids']);
}
/** A term the user may not assign reads like a missing one; no term name or slug in any message. */
public function testTermsMustBeAssignable(): void
{
$this->denied = ['list_users', 'assign_term'];
$errors = self::conditions()->check(
['archive:taxonomy:category:term:id:5', 'archive:taxonomy:category:term:id:6'],
'use_on'
)['errors'];
self::assertSame($errors['use_on.0'], str_replace('6', '5', $errors['use_on.1']));
self::assertStringNotContainsString('News', implode(' ', $errors));
self::assertStringNotContainsString('news', implode(' ', $errors));
}
/** The object is the last segment as Divi reads it back; types a filter adds stay builder-only. */
public function testObjectIsTheLastSegmentAndForeignTypesStayInTheBuilder(): void
{
$store = Mockery::mock(DiviThemeBuilder::class);
$store->allows('flatConditions')->andReturn(ThemeBuilderFixtures::conditions() + [
'woo:shop:id:' => ['label' => 'Shop', 'type' => 'product_set', 'value' => 'x'],
]);
$conditions = new TemplateConditions($store);
$errors = $conditions->check([
'singular:post_type:page:id:7:1053',
'woo:shop:id:3',
'singular:post_type:page:id:1053 ',
'Homepage',
], 'exclude_from')['errors'];
self::assertSame(
['exclude_from.0', 'exclude_from.1', 'exclude_from.2', 'exclude_from.3'],
array_keys($errors)
);
self::assertStringContainsString('unbekannte Bedingung', $errors['exclude_from.0']);
self::assertStringContainsString('Divi Theme Builder', $errors['exclude_from.1']);
self::assertStringContainsString('unbekannte Bedingung', $errors['exclude_from.3']);
}
/** No existence oracle: a readable post of another type reads like a missing one. */
public function testWrongTypeReadsLikeMissing(): void
{
$errors = self::conditions()->check(
['singular:post_type:lit_person:id:1053', 'singular:post_type:lit_person:id:9999'],
'use_on'
)['errors'];
self::assertSame($errors['use_on.0'], str_replace('9999', '1053', $errors['use_on.1']));
}
}