PodsFieldSource maps hidden, read_only, admin_only and restrict_role/-capability like Pods' own view: hidden -> new status HIDDEN, read_only -> READONLY, restricted and not permitted -> READONLY with read_only_restricted, else HIDDEN. HIDDEN fields are not described, read out, queried or written (they look unknown) but stay in the schema for the media usage search; write() re-checks before Pods' save(). Required-but-restricted fields no longer block a create. Pods is not active on DDEV: covered by unit tests with a field-option fixture. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
141 lines
6.7 KiB
PHP
141 lines
6.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace LIT\MCP\Tests\Unit\Integrations\WordPress\Schema;
|
|
|
|
use Brain\Monkey\Functions;
|
|
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
|
|
use LIT\MCP\Integrations\WordPress\Schema\FieldValidator;
|
|
use LIT\MCP\Tests\Unit\TestCase;
|
|
|
|
final class FieldValidatorTest extends TestCase
|
|
{
|
|
/** @return array<string, FieldDefinition> */
|
|
private function definitions(): array
|
|
{
|
|
return [
|
|
'role' => new FieldDefinition('role', 'Funktion', 'string', 'pods', 'writable', true),
|
|
'level' => new FieldDefinition('level', 'Stufe', 'choice', 'pods', 'writable', false, false, [
|
|
['value' => 'junior', 'label' => 'Junior'],
|
|
['value' => 'senior', 'label' => 'Senior'],
|
|
]),
|
|
'mentor' => new FieldDefinition('mentor', 'Mentor', 'relation', 'pods', 'writable', false, false, null, [
|
|
'object_type' => 'post_type',
|
|
'name' => 'lit_person',
|
|
]),
|
|
'photos' => new FieldDefinition('photos', 'Fotos', 'file', 'pods', 'writable', false, true, null, [
|
|
'object_type' => 'media',
|
|
'name' => null,
|
|
]),
|
|
'start' => new FieldDefinition('start', 'Beginn', 'date', 'pods', 'writable'),
|
|
'secret' => new FieldDefinition('_secret', '_secret', 'string', 'meta', 'readonly'),
|
|
'city' => new FieldDefinition('city', 'city', 'unknown', 'sample', 'unknown'),
|
|
'pin' => new FieldDefinition('pin', 'PIN', 'password', 'pods', 'writable'),
|
|
'boss' => new FieldDefinition('boss', 'Boss', 'string', 'pods', FieldDefinition::HIDDEN),
|
|
];
|
|
}
|
|
|
|
public function testValidValuesPassUnchanged(): void
|
|
{
|
|
Functions\when('get_post_type')->alias(static fn (int $id) => [12 => 'lit_person', 3 => 'attachment'][$id] ?? false);
|
|
Functions\when('current_user_can')->justReturn(true);
|
|
$values = ['role' => 'Leitung', 'level' => 'senior', 'mentor' => 12, 'photos' => [3], 'start' => '2026-10-01'];
|
|
|
|
$result = (new FieldValidator())->validate($this->definitions(), $values, true);
|
|
|
|
self::assertSame([], $result['errors']);
|
|
self::assertSame($values, $result['values']);
|
|
}
|
|
|
|
public function testRequiredFieldMustBeSetOnCreateAndNotClearedOnUpdate(): void
|
|
{
|
|
$validator = new FieldValidator();
|
|
|
|
self::assertSame('Pflichtfeld fehlt.', $validator->validate($this->definitions(), [], true)['errors']['role']);
|
|
self::assertSame(
|
|
'Pflichtfeld darf nicht geleert werden.',
|
|
$validator->validate($this->definitions(), ['role' => ''], false)['errors']['role']
|
|
);
|
|
self::assertSame([], $validator->validate($this->definitions(), ['level' => 'junior'], false)['errors']);
|
|
}
|
|
|
|
public function testUnknownReadonlyAndInvalidValuesAreReported(): void
|
|
{
|
|
Functions\when('get_post_type')->alias(static fn (int $id) => [7 => 'page', 3 => 'attachment'][$id] ?? false);
|
|
Functions\when('current_user_can')->justReturn(true);
|
|
|
|
$errors = (new FieldValidator())->validate($this->definitions(), [
|
|
'role' => 'x',
|
|
'nope' => 1,
|
|
'secret' => 'x',
|
|
'level' => 'expert',
|
|
'mentor' => 7,
|
|
'photos' => 3,
|
|
'start' => '01.10.2026',
|
|
'boss' => 'x',
|
|
], false)['errors'];
|
|
|
|
self::assertSame($errors['nope'], $errors['boss'], 'a hidden field looks like an unknown one (M4)');
|
|
self::assertStringContainsString('lit-content-types', $errors['nope']);
|
|
self::assertSame('Feld ist schreibgeschützt.', $errors['secret']);
|
|
self::assertSame('Erlaubte Werte: junior, senior.', $errors['level']);
|
|
self::assertSame('Eintrag 7 ist kein Eintrag vom Typ lit_person.', $errors['mentor']);
|
|
self::assertSame('Liste erwartet.', $errors['photos']);
|
|
self::assertSame('Datum im Format JJJJ-MM-TT erwartet.', $errors['start']);
|
|
self::assertArrayNotHasKey('role', $errors);
|
|
}
|
|
|
|
public function testMissingRelatedPostAndNonMediaAreRejected(): void
|
|
{
|
|
Functions\when('get_post_type')->alias(static fn (int $id) => [5 => 'page'][$id] ?? false);
|
|
Functions\when('current_user_can')->justReturn(true);
|
|
|
|
$errors = (new FieldValidator())->validate($this->definitions(), ['mentor' => 99, 'photos' => [5]], false)['errors'];
|
|
|
|
self::assertSame('Eintrag 99 existiert nicht oder ist für dich nicht lesbar.', $errors['mentor']);
|
|
self::assertSame('5 ist kein für dich lesbares Medium der Mediathek.', $errors['photos']);
|
|
}
|
|
|
|
public function testUnreadableTargetsLookLikeMissingOnesAndRevealNoType(): void
|
|
{
|
|
Functions\when('get_post_type')->alias(static fn (int $id) => [8 => 'lit_secret', 4 => 'attachment'][$id] ?? false);
|
|
Functions\when('current_user_can')->alias(static fn (string $cap, int $id): bool => $cap === 'read_post' && ! in_array($id, [8, 4], true));
|
|
|
|
$errors = (new FieldValidator())->validate($this->definitions(), ['mentor' => 8, 'photos' => [4]], false)['errors'];
|
|
|
|
self::assertSame('Eintrag 8 existiert nicht oder ist für dich nicht lesbar.', $errors['mentor']);
|
|
self::assertStringNotContainsString('lit_secret', $errors['mentor']);
|
|
self::assertSame('4 ist kein für dich lesbares Medium der Mediathek.', $errors['photos']);
|
|
}
|
|
|
|
public function testPasswordFieldsAreValidatedAsStrings(): void
|
|
{
|
|
$validator = new FieldValidator();
|
|
|
|
self::assertSame([], $validator->validate($this->definitions(), ['pin' => '1234'], false)['errors']);
|
|
self::assertSame('Text erwartet.', $validator->validate($this->definitions(), ['pin' => 1234], false)['errors']['pin']);
|
|
}
|
|
|
|
public function testUnknownTypedFieldsAreAcceptedWithAWarningAndNullClears(): void
|
|
{
|
|
$result = (new FieldValidator())->validate($this->definitions(), ['city' => ['any' => 'thing'], 'level' => null], false);
|
|
|
|
self::assertSame([], $result['errors']);
|
|
self::assertSame(['city' => ['any' => 'thing'], 'level' => null], $result['values']);
|
|
self::assertCount(1, $result['warnings']);
|
|
self::assertStringContainsString('city', $result['warnings'][0]);
|
|
}
|
|
|
|
/** M4: a required field the user cannot write (restricted Pods field) cannot block a create. */
|
|
public function testRequiredButRestrictedFieldsAreNotDemanded(): void
|
|
{
|
|
$definitions = [
|
|
'boss' => new FieldDefinition('boss', 'Boss', 'string', 'pods', FieldDefinition::HIDDEN, true),
|
|
'fixed' => new FieldDefinition('fixed', 'Fest', 'string', 'pods', FieldDefinition::READONLY, true),
|
|
];
|
|
|
|
self::assertSame([], (new FieldValidator())->validate($definitions, [], true)['errors']);
|
|
}
|
|
}
|