feat: content schema with registered and sampled meta fields
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\WordPress\Schema;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
|
||||
use LIT\MCP\Integrations\WordPress\Schema\FieldSource;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
|
||||
final class ContentSchemaTest extends TestCase
|
||||
{
|
||||
private function source(string $id, array $fields): FieldSource
|
||||
{
|
||||
$source = Mockery::mock(FieldSource::class);
|
||||
$source->allows('id')->andReturn($id);
|
||||
$source->allows('fieldsFor')->andReturn($fields);
|
||||
|
||||
return $source;
|
||||
}
|
||||
|
||||
private function stubPostTypes(): void
|
||||
{
|
||||
Functions\when('get_post_types')->justReturn([
|
||||
'post' => 'post',
|
||||
'page' => 'page',
|
||||
'attachment' => 'attachment',
|
||||
'et_pb_layout' => 'et_pb_layout',
|
||||
'lit_person' => 'lit_person',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testPostTypeNamesExcludeInternalTypesAndAreSorted(): void
|
||||
{
|
||||
$this->stubPostTypes();
|
||||
|
||||
$schema = new ContentSchema([]);
|
||||
|
||||
self::assertSame(['lit_person', 'page', 'post'], $schema->postTypeNames());
|
||||
self::assertTrue($schema->isManaged('page'));
|
||||
self::assertFalse($schema->isManaged('et_pb_layout'));
|
||||
}
|
||||
|
||||
public function testFieldsAreMergedByPriorityAndSorted(): void
|
||||
{
|
||||
$pods = $this->source('pods', [new FieldDefinition('role', 'Funktion', 'string', 'pods', 'writable', true)]);
|
||||
$meta = $this->source('sample', [
|
||||
new FieldDefinition('role', 'role', 'unknown', 'sample', 'unknown'),
|
||||
new FieldDefinition('city', 'city', 'unknown', 'sample', 'unknown'),
|
||||
]);
|
||||
$schema = new ContentSchema([$pods, $meta]);
|
||||
|
||||
$fields = $schema->fields('lit_person');
|
||||
|
||||
self::assertSame(['city', 'role'], array_keys($fields));
|
||||
self::assertSame('pods', $fields['role']->source);
|
||||
self::assertTrue($fields['role']->required);
|
||||
self::assertSame($pods, $schema->sourceOf($fields['role']));
|
||||
self::assertSame($meta, $schema->sourceOf($fields['city']));
|
||||
}
|
||||
|
||||
public function testDescribeIncludesTaxonomiesCountsDiviAndOptionalFields(): void
|
||||
{
|
||||
$this->stubPostTypes();
|
||||
Functions\when('get_post_type_object')->justReturn((object) [
|
||||
'hierarchical' => true,
|
||||
'labels' => (object) ['name' => 'Seiten', 'singular_name' => 'Seite'],
|
||||
]);
|
||||
Functions\when('get_all_post_type_supports')->justReturn(['title' => true, 'editor' => true]);
|
||||
Functions\when('wp_count_posts')->justReturn((object) ['publish' => 3, 'draft' => 2, 'trash' => 9]);
|
||||
Functions\when('get_object_taxonomies')->justReturn([
|
||||
(object) ['name' => 'category', 'show_ui' => true, 'hierarchical' => true, 'labels' => (object) ['name' => 'Kategorien']],
|
||||
(object) ['name' => 'post_format', 'show_ui' => false, 'hierarchical' => false, 'labels' => (object) ['name' => 'Formate']],
|
||||
]);
|
||||
$schema = new ContentSchema(
|
||||
[$this->source('meta', [new FieldDefinition('footnotes', 'footnotes', 'string', 'meta', 'writable')])],
|
||||
static fn (string $type): bool => $type === 'page'
|
||||
);
|
||||
|
||||
$withoutFields = $schema->describe('page', false);
|
||||
$withFields = $schema->describe('page', true);
|
||||
|
||||
self::assertSame('Seiten', $withoutFields['label']);
|
||||
self::assertSame('Seite', $withoutFields['singular_label']);
|
||||
self::assertTrue($withoutFields['hierarchical']);
|
||||
self::assertSame(['title', 'editor'], $withoutFields['supports']);
|
||||
self::assertTrue($withoutFields['divi']);
|
||||
self::assertSame(5, $withoutFields['count']);
|
||||
self::assertSame([['name' => 'category', 'label' => 'Kategorien', 'hierarchical' => true]], $withoutFields['taxonomies']);
|
||||
self::assertArrayNotHasKey('fields', $withoutFields);
|
||||
self::assertSame('footnotes', $withFields['fields'][0]['key']);
|
||||
self::assertNull($schema->describe('et_pb_layout', false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?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\RegisteredMetaSource;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
|
||||
final class RegisteredMetaSourceTest extends TestCase
|
||||
{
|
||||
/** @return array<string, array<string, mixed>> */
|
||||
private function fieldsByKey(): array
|
||||
{
|
||||
Functions\when('get_registered_meta_keys')->alias(static function (string $type, string $sub = ''): array {
|
||||
if ($sub === '') {
|
||||
return ['footnotes' => ['type' => 'string', 'single' => true, 'show_in_rest' => true]];
|
||||
}
|
||||
|
||||
return [
|
||||
'_internal' => ['type' => 'integer', 'single' => true, 'show_in_rest' => false],
|
||||
'tags_list' => ['type' => 'array', 'single' => false, 'description' => 'Liste'],
|
||||
];
|
||||
});
|
||||
Functions\when('is_protected_meta')->alias(static fn (string $key): bool => str_starts_with($key, '_'));
|
||||
|
||||
$fields = (new RegisteredMetaSource())->fieldsFor('event');
|
||||
|
||||
return array_column(array_map(static fn (FieldDefinition $f): array => $f->toArray(), $fields), null, 'key');
|
||||
}
|
||||
|
||||
public function testDescribesGlobalAndSubtypeMetaKeys(): void
|
||||
{
|
||||
$fields = $this->fieldsByKey();
|
||||
|
||||
self::assertSame(['footnotes', '_internal', 'tags_list'], array_keys($fields));
|
||||
self::assertSame('string', $fields['footnotes']['type']);
|
||||
self::assertSame(FieldDefinition::WRITABLE, $fields['footnotes']['status']);
|
||||
self::assertSame('meta', $fields['footnotes']['source']);
|
||||
self::assertFalse($fields['footnotes']['multiple']);
|
||||
}
|
||||
|
||||
public function testProtectedMetaHiddenFromRestIsReadonly(): void
|
||||
{
|
||||
$fields = $this->fieldsByKey();
|
||||
|
||||
self::assertSame(FieldDefinition::READONLY, $fields['_internal']['status']);
|
||||
self::assertSame('integer', $fields['_internal']['type']);
|
||||
}
|
||||
|
||||
public function testNonSingleMetaIsMultiple(): void
|
||||
{
|
||||
$fields = $this->fieldsByKey();
|
||||
|
||||
self::assertTrue($fields['tags_list']['multiple']);
|
||||
self::assertSame('Liste', $fields['tags_list']['description']);
|
||||
}
|
||||
|
||||
public function testReadUsesSingleOrMultipleMeta(): void
|
||||
{
|
||||
Functions\expect('get_post_meta')->once()->with(5, 'footnotes', true)->andReturn('x');
|
||||
Functions\expect('get_post_meta')->once()->with(5, 'tags_list', false)->andReturn(['a', 'b']);
|
||||
$source = new RegisteredMetaSource();
|
||||
|
||||
self::assertSame('x', $source->read(5, new FieldDefinition('footnotes', 'footnotes', 'string', 'meta', 'writable')));
|
||||
self::assertSame(
|
||||
['a', 'b'],
|
||||
$source->read(5, new FieldDefinition('tags_list', 'tags_list', 'array', 'meta', 'writable', false, true))
|
||||
);
|
||||
}
|
||||
|
||||
public function testWriteUpdatesReplacesListsAndDeletesNull(): void
|
||||
{
|
||||
$single = new FieldDefinition('footnotes', 'footnotes', 'string', 'meta', 'writable');
|
||||
$multi = new FieldDefinition('tags_list', 'tags_list', 'array', 'meta', 'writable', false, true);
|
||||
$gone = new FieldDefinition('old', 'old', 'string', 'meta', 'writable');
|
||||
Functions\expect('update_post_meta')->once()->with(5, 'footnotes', 'neu');
|
||||
Functions\expect('delete_post_meta')->once()->with(5, 'tags_list');
|
||||
Functions\expect('add_post_meta')->once()->with(5, 'tags_list', 'a');
|
||||
Functions\expect('add_post_meta')->once()->with(5, 'tags_list', 'b');
|
||||
Functions\expect('delete_post_meta')->once()->with(5, 'old');
|
||||
|
||||
(new RegisteredMetaSource())->write(
|
||||
5,
|
||||
'event',
|
||||
['footnotes' => 'neu', 'tags_list' => ['a', 'b'], 'old' => null],
|
||||
['footnotes' => $single, 'tags_list' => $multi, 'old' => $gone]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\WordPress\Schema;
|
||||
|
||||
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
|
||||
use LIT\MCP\Integrations\WordPress\Schema\SampledMetaSource;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
|
||||
final class SampledMetaSourceTest extends TestCase
|
||||
{
|
||||
public function testReturnsVisibleKeysOfRecentPostsAsUnknown(): void
|
||||
{
|
||||
$db = Mockery::mock(\wpdb::class)->makePartial();
|
||||
$db->expects('get_col')
|
||||
->with(Mockery::on(static fn (string $sql): bool => str_contains($sql, "post_type = 'lit_person'")
|
||||
&& str_contains($sql, 'LIMIT 50')))
|
||||
->andReturn(['_edit_lock', 'city', 'role']);
|
||||
|
||||
$fields = (new SampledMetaSource($db))->fieldsFor('lit_person');
|
||||
|
||||
self::assertSame(['city', 'role'], array_map(static fn (FieldDefinition $f): string => $f->key, $fields));
|
||||
self::assertSame(FieldDefinition::UNKNOWN, $fields[0]->status);
|
||||
self::assertSame('sample', $fields[0]->source);
|
||||
self::assertSame('unknown', $fields[0]->type);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user