*/ private array $deleted = []; /** @var array What the fake Pods object holds. */ private array $stored = []; /** @var array|null The wp_insert_post arguments. */ private ?array $inserted = null; protected function setUp(): void { parent::setUp(); $this->deleted = []; $this->stored = []; $this->inserted = null; Functions\when('wp_insert_post')->alias(function (array $post): int { $this->inserted = $post; return 77; }); Functions\when('get_post_type')->justReturn('lit_person'); Functions\when('is_wp_error')->alias(static fn ($value): bool => $value instanceof WP_Error); Functions\when('wp_delete_post')->alias(function (int $id, bool $force): WP_Post { self::assertTrue($force, 'never to the trash'); $this->deleted[] = $id; return new WP_Post(['ID' => $id]); }); } /** @param array $args */ private function field( string $name, string $type, bool $required = false, array $args = [], bool $multiple = false ): object { $field = Mockery::mock(); $field->allows('get_name')->andReturn($name); $field->allows('get_label')->andReturn(ucfirst($name)); $field->allows('get_type')->andReturn($type); $field->allows('is_required')->andReturn($required); $field->allows('is_multi_value')->andReturn($multiple); $field->allows('get_arg')->andReturnUsing(static fn (string $key, $default = null) => $args[$key] ?? $default); return $field; } /** @param list $fields */ private function schema(array $fields, string $object = ''): object { $pod = Mockery::mock(); $pod->allows('get_type')->andReturn('post_type'); $pod->allows('get_arg')->andReturnUsing( static fn (string $key, $default = null) => $key === 'object' ? $object : $default ); $pod->allows('get_fields')->andReturn($fields); return $pod; } /** @param (Closure(array): mixed)|null $save */ private function entry(?Closure $save = null, ?string $readBack = null): object { $entry = Mockery::mock(); $entry->allows('save')->andReturnUsing(function (array $data) use ($save) { if ($save !== null) { return $save($data); } $this->stored = $data; return 77; }); $entry->allows('field')->andReturnUsing( fn (array $args) => $readBack ?? $this->stored[$args['name']] ?? '' ); return $entry; } /** @param list|null $fields */ private function probe(?array $fields = null, ?object $entry = null): PodsProbe { $pod = $this->schema($fields ?? [$this->field('role', 'text')]); $entry ??= $this->entry(); $source = new PodsFieldSource( static fn (): bool => true, static fn (string $name) => $name === 'lit_person' ? $pod : false, static fn () => $entry, static fn (object $field): ?bool => null ); return new PodsProbe( $source, static fn (string $name) => $name === 'lit_person' ? $pod : false, static fn (): array => ['post', 'lit_person'] ); } /** Captures what Log::failure writes to the PHP error log while $run runs. */ private function logged(Closure $run): string { $log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log'); $previous = ini_set('error_log', $log); try { $run(); return (string) file_get_contents($log); } finally { ini_set('error_log', (string) $previous); unlink($log); } } public function testGreenWhenTheWrittenValueIsReadBackAndTheEntryIsGone(): void { self::assertSame([], $this->probe()->run()); self::assertSame(['role' => PodsProbe::VALUE], $this->stored); self::assertSame([77], $this->deleted); } public function testCreatesAMarkedDraftWithThePrefixedTitleInThePodsPostType(): void { $this->probe()->run(); self::assertSame('lit_person', $this->inserted['post_type']); self::assertSame('draft', $this->inserted['post_status']); self::assertStringStartsWith(ThrowawayPost::TITLE, $this->inserted['post_title']); self::assertSame(['_lit_mcp_check_entry' => '1'], $this->inserted['meta_input']); } public function testADifferingValueIsAFixedTextAndTheValueGoesToTheLogOnly(): void { $problems = []; $logged = $this->logged(function () use (&$problems): void { $problems = $this->probe(null, $this->entry(null, 'etwas anderes'))->run(); }); self::assertSame(['Gelesener Wert weicht vom geschriebenen ab.'], $problems); self::assertStringContainsString('role', $logged); self::assertStringContainsString('etwas anderes', $logged); self::assertSame([77], $this->deleted); } public function testAnExceptionBecomesTheFixedTextAndALogLineAndTheEntryIsDeleted(): void { $entry = $this->entry(static function (): never { throw new RuntimeException('Pods kaputt'); }); $problems = []; $logged = $this->logged(function () use (&$problems, $entry): void { $problems = $this->probe(null, $entry)->run(); }); self::assertSame(['Pods-Probe abgebrochen (Details im PHP-Error-Log).'], $problems); self::assertStringContainsString('Pods kaputt', $logged); self::assertSame([77], $this->deleted); } public function testAFailingSchemaLookupIsTheFixedTextToo(): void { $broken = static fn () => throw new RuntimeException('Schema kaputt'); $probe = new PodsProbe( new PodsFieldSource(static fn (): bool => true, $broken), $broken, static fn (): array => ['lit_person'] ); $problems = []; $logged = $this->logged(function () use (&$problems, $probe): void { $problems = $probe->run(); }); self::assertSame(['Pods-Probe abgebrochen (Details im PHP-Error-Log).'], $problems); self::assertStringContainsString('Schema kaputt', $logged); self::assertNull($this->inserted); } public function testNothingToProbeWithoutAPodWithAPlainWritableTextField(): void { $fields = [$this->field('mail', 'email'), $this->field('intro', 'wysiwyg')]; self::assertNull($this->probe($fields)->run()); self::assertNull($this->probe([])->run()); self::assertNull($this->inserted, 'nothing is created without a target'); self::assertSame([], $this->deleted); } public function testARequiredTextFieldIsFineBecauseTheProbeWritesAValue(): void { self::assertSame([], $this->probe([$this->field('role', 'text', true)])->run()); self::assertSame(['role' => PodsProbe::VALUE], $this->stored); } public function testPicksThePlainTextFieldAmongOthers(): void { $this->probe([$this->field('mail', 'email'), $this->field('title2', 'text')])->run(); self::assertSame(['title2' => PodsProbe::VALUE], $this->stored); } public function testAnEntryThatCannotBeCreatedIsAFixedTextAndNothingIsDeleted(): void { Functions\when('wp_insert_post')->justReturn(new WP_Error('x', 'Datenbank voll')); $problems = []; $logged = $this->logged(function () use (&$problems): void { $problems = $this->probe()->run(); }); self::assertSame(['Der Probe-Eintrag konnte nicht angelegt werden (Details im PHP-Error-Log).'], $problems); self::assertStringContainsString('Datenbank voll', $logged); self::assertSame([], $this->deleted); } public function testAnEntryThatCannotBeDeletedIsReported(): void { Functions\when('wp_delete_post')->justReturn(false); $problems = $this->probe()->run(); self::assertCount(1, $problems); self::assertStringContainsString('77', $problems[0]); self::assertStringContainsString('gelöscht', $problems[0]); } public function testOrphansAreLookedForInEveryPostTypeWithThePodsMarker(): void { Functions\when('get_post_types')->justReturn(['post' => 'post', 'lit_person' => 'lit_person']); $query = null; Functions\when('get_posts')->alias(static function (array $args) use (&$query): array { $query = $args; return [ new WP_Post(['ID' => 5, 'post_title' => ThrowawayPost::TITLE . ' 2026-11-06 10:00']), new WP_Post(['ID' => 6, 'post_title' => 'Echte Person']), ]; }); self::assertSame([5], PodsProbe::removeOrphans()); self::assertSame([5], $this->deleted); self::assertSame('_lit_mcp_check_entry', $query['meta_key']); self::assertSame(['post', 'lit_person'], $query['post_type']); self::assertSame('post_modified_gmt', $query['date_query'][0]['column']); } /** @param array $pods */ private function probeOver(array $pods, ?callable $postTypes = null): PodsProbe { $load = static fn (string $name) => $pods[$name] ?? false; $source = new PodsFieldSource( static fn (): bool => true, $load, fn () => $this->entry(), static fn (object $field): ?bool => null ); return new PodsProbe($source, $load, $postTypes); } public function testPodsOwnPostTypesComeBeforeExtendedForeignOnesAndPostAndPage(): void { $fields = [$this->field('role', 'text')]; $pods = [ 'post' => $this->schema($fields, 'post'), 'event' => $this->schema($fields, 'event'), 'lit_person' => $this->schema($fields), ]; $this->probeOver($pods, static fn (): array => ['post', 'event', 'lit_person'])->run(); self::assertSame('lit_person', $this->inserted['post_type']); } public function testAnExtendedForeignTypeComesBeforePostAndPage(): void { $fields = [$this->field('role', 'text')]; $pods = ['post' => $this->schema($fields, 'post'), 'event' => $this->schema($fields, 'event')]; $this->probeOver($pods, static fn (): array => ['post', 'event'])->run(); self::assertSame('event', $this->inserted['post_type']); } public function testPostIsTheLastResort(): void { $pods = ['post' => $this->schema([$this->field('role', 'text')], 'post')]; $this->probeOver($pods, static fn (): array => ['page', 'post'])->run(); self::assertSame('post', $this->inserted['post_type']); } public function testALowerRankedTypeIsUsedWhenTheBetterOnesHaveNoTextField(): void { $pods = [ 'lit_person' => $this->schema([$this->field('mail', 'email')]), 'post' => $this->schema([$this->field('role', 'text')], 'post'), ]; $this->probeOver($pods, static fn (): array => ['lit_person', 'post'])->run(); self::assertSame('post', $this->inserted['post_type']); } public function testReadOnlyAndMultipleTextFieldsAreNotProbed(): void { $fields = [ $this->field('locked', 'text', false, ['read_only' => true]), $this->field('many', 'text', false, [], true), ]; self::assertNull($this->probe($fields)->run()); self::assertNull($this->inserted); $this->probe([...$fields, $this->field('role', 'text')])->run(); self::assertSame(['role' => PodsProbe::VALUE], $this->stored); } public function testTheDefaultPostTypesAreCustomTypesThenPostAndPage(): void { Functions\when('get_post_types')->justReturn(['lit_person' => 'lit_person', 'post' => 'post']); $asked = []; $probe = new PodsProbe( new PodsFieldSource(static fn (): bool => true, static fn () => false), static function (string $name) use (&$asked) { $asked[] = $name; return false; } ); self::assertNull($probe->run()); self::assertSame(['lit_person', 'post', 'page'], $asked); } }