allows('summary')->andReturnUsing(static fn (WP_Post $post): array => ['id' => $post->ID]); return new ContentQueryAbility($schema ?? Mockery::mock(ContentSchema::class), $presenter); } public function testBuildsTheQueryAndPaginates(): void { Functions\when('current_user_can')->justReturn(true); Functions\when('get_post_type_object')->justReturn((object) ['cap' => (object) []]); $schema = Mockery::mock(ContentSchema::class); $schema->allows('fields')->andReturn(['level' => new FieldDefinition('level', 'Level', 'string', 'pods', 'writable')]); $schema->allows('taxonomies')->andReturn([['name' => 'category', 'label' => 'Kategorien', 'hierarchical' => true]]); Functions\expect('get_posts')->once()->with(Mockery::on(static function (array $args): bool { return $args['post_type'] === 'lit_person' && $args['post_status'] === ['draft'] && $args['s'] === 'Anna' && $args['posts_per_page'] === 3 && $args['offset'] === 4 && $args['orderby'] === 'title' && $args['order'] === 'ASC' && $args['tax_query'] === [ 'relation' => 'OR', ['taxonomy' => 'category', 'field' => 'term_id', 'terms' => [4]], ['taxonomy' => 'category', 'field' => 'slug', 'terms' => ['news']], ] && $args['meta_query'] === [['key' => 'level', 'compare' => '=', 'value' => 'senior']] && $args['date_query'] === [['after' => '2026-01-01', 'inclusive' => true]]; }))->andReturn([new WP_Post(['ID' => 1]), new WP_Post(['ID' => 2]), new WP_Post(['ID' => 3])]); $result = $this->ability($schema)->execute([ 'post_type' => 'lit_person', 'status' => ['draft'], 'search' => 'Anna', 'taxonomy' => ['taxonomy' => 'category', 'terms' => [4, 'news']], 'field' => ['key' => 'level', 'value' => 'senior'], 'date_after' => '2026-01-01', 'orderby' => 'title', 'order' => 'asc', 'limit' => 2, 'cursor' => Cursor::encode(4), ]); self::assertSame([['id' => 1], ['id' => 2]], $result['items']); self::assertSame(6, Cursor::decode($result['next_cursor'])); } public function testExistsFilterHasNoValueAndLastPageHasNoCursor(): void { Functions\when('current_user_can')->justReturn(true); Functions\when('get_post_type_object')->justReturn((object) ['cap' => (object) []]); $schema = Mockery::mock(ContentSchema::class); $schema->allows('fields')->andReturn(['role' => new FieldDefinition('role', 'Funktion', 'string', 'pods', 'writable')]); Functions\expect('get_posts')->once()->with(Mockery::on( static fn (array $args): bool => $args['meta_query'] === [['key' => 'role', 'compare' => 'EXISTS']] && $args['post_status'] === ['publish', 'draft', 'pending', 'private', 'future'] ))->andReturn([new WP_Post(['ID' => 1])]); $result = $this->ability($schema)->execute(['post_type' => 'page', 'field' => ['key' => 'role', 'compare' => 'EXISTS']]); self::assertNull($result['next_cursor']); } public function testPostsTheUserCannotEditAreDropped(): void { Functions\when('current_user_can')->alias(static fn (string $cap, int $id = 0): bool => $id !== 2); Functions\when('get_post_type_object')->justReturn((object) ['cap' => (object) []]); Functions\when('get_posts')->justReturn([new WP_Post(['ID' => 1]), new WP_Post(['ID' => 2])]); $result = $this->ability()->execute(['post_type' => 'page']); self::assertSame([['id' => 1]], $result['items']); } public function testInvalidCursorIsRejected(): void { $result = $this->ability()->execute(['post_type' => 'page', 'cursor' => '???']); self::assertSame('lit_validation_failed', $result->get_error_code()); } public function testQueryIsRestrictedToOwnPostsWithoutEditOthersPosts(): void { Functions\when('get_post_type_object')->justReturn( (object) ['cap' => (object) ['edit_others_posts' => 'edit_others_posts']] ); Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap !== 'edit_others_posts'); Functions\when('get_current_user_id')->justReturn(7); Functions\expect('get_posts')->once()->with( Mockery::on(static fn (array $args): bool => ($args['author'] ?? null) === 7) )->andReturn([]); $result = $this->ability()->execute(['post_type' => 'page']); self::assertSame([], $result['items']); } public function testQueryIsNotRestrictedToAuthorWithEditOthersPosts(): void { Functions\when('get_post_type_object')->justReturn((object) ['cap' => (object) []]); Functions\when('current_user_can')->justReturn(true); Functions\expect('get_posts')->once()->with( Mockery::on(static fn (array $args): bool => ! array_key_exists('author', $args)) )->andReturn([]); $this->ability()->execute(['post_type' => 'page']); } public function testDisallowedStatusesAreRemovedFromTheQuery(): void { Functions\when('get_post_type_object')->justReturn((object) ['cap' => (object) [ 'edit_others_posts' => 'edit_others_posts', 'edit_private_posts' => 'edit_private_posts', 'edit_published_posts' => 'edit_published_posts', ]]); Functions\when('current_user_can')->alias( static fn (string $cap): bool => ! in_array($cap, ['edit_private_posts', 'edit_published_posts'], true) ); Functions\expect('get_posts')->once()->with( Mockery::on(static fn (array $args): bool => $args['post_status'] === ['draft', 'pending']) )->andReturn([]); $this->ability()->execute(['post_type' => 'page']); } public function testRequestingOnlyDisallowedStatusesReturnsEmptyWithoutQuerying(): void { Functions\when('get_post_type_object')->justReturn((object) ['cap' => (object) [ 'edit_others_posts' => 'edit_others_posts', 'edit_published_posts' => 'edit_published_posts', ]]); Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap === 'edit_others_posts'); Functions\expect('get_posts')->never(); $result = $this->ability()->execute(['post_type' => 'page', 'status' => ['publish']]); self::assertSame(['items' => [], 'next_cursor' => null], $result); } public function testRequestingOnlyPrivateWithoutRightsReturnsEmptyWithoutQuerying(): void { Functions\when('get_post_type_object')->justReturn((object) ['cap' => (object) [ 'edit_others_posts' => 'edit_others_posts', 'edit_private_posts' => 'edit_private_posts', ]]); Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap === 'edit_others_posts'); Functions\expect('get_posts')->never(); $result = $this->ability()->execute(['post_type' => 'page', 'status' => ['private']]); self::assertSame(['items' => [], 'next_cursor' => null], $result); } public function testOwnPrivatePostsStayVisibleWithoutEditPrivatePostsWhenAuthorRestricted(): void { Functions\when('get_post_type_object')->justReturn((object) ['cap' => (object) [ 'edit_others_posts' => 'edit_others_posts', 'edit_private_posts' => 'edit_private_posts', ]]); Functions\when('current_user_can')->justReturn(false); Functions\when('get_current_user_id')->justReturn(9); Functions\expect('get_posts')->once()->with(Mockery::on( static fn (array $args): bool => $args['post_status'] === ['private'] && ($args['author'] ?? null) === 9 ))->andReturn([]); $this->ability()->execute(['post_type' => 'page', 'status' => ['private']]); } public function testUnknownFieldKeyIsRejected(): void { $schema = Mockery::mock(ContentSchema::class); $schema->allows('fields')->andReturn(['role' => new FieldDefinition('role', 'Funktion', 'string', 'pods', 'writable')]); $result = $this->ability($schema)->execute(['post_type' => 'page', 'field' => ['key' => 'ghost']]); self::assertSame('lit_validation_failed', $result->get_error_code()); self::assertStringContainsString('role', $result->get_error_message()); } public function testHiddenFieldKeyIsRejectedLikeAnUnknownOne(): void { $schema = Mockery::mock(ContentSchema::class); $schema->allows('fields')->andReturn([ 'role' => new FieldDefinition('role', 'Funktion', 'string', 'pods', 'writable'), 'secret' => new FieldDefinition('secret', 'Secret', 'password', 'pods', 'writable'), '_internal' => new FieldDefinition('_internal', 'Internal', 'string', 'meta', 'readonly'), ]); $password = $this->ability($schema)->execute(['post_type' => 'page', 'field' => ['key' => 'secret']]); $protected = $this->ability($schema)->execute(['post_type' => 'page', 'field' => ['key' => '_internal']]); self::assertSame('lit_validation_failed', $password->get_error_code()); self::assertSame('Verfügbare Felder: role.', $password->get_error_data()['hint']); self::assertSame('lit_validation_failed', $protected->get_error_code()); self::assertSame('Verfügbare Felder: role.', $protected->get_error_data()['hint']); } public function testUnknownTaxonomyIsRejected(): void { $schema = Mockery::mock(ContentSchema::class); $schema->allows('taxonomies')->andReturn([['name' => 'category', 'label' => 'Kategorien', 'hierarchical' => true]]); $result = $this->ability($schema)->execute([ 'post_type' => 'page', 'taxonomy' => ['taxonomy' => 'ghost', 'terms' => [1]], ]); self::assertSame('lit_validation_failed', $result->get_error_code()); self::assertStringContainsString('category', $result->get_error_message()); } public function testPermissionNeedsAManagedTypeAndEditRights(): void { $schema = Mockery::mock(ContentSchema::class); $schema->allows('isManaged')->andReturn(false); $ability = new ContentQueryAbility($schema, Mockery::mock(PostPresenter::class)); self::assertSame('lit_not_found', $ability->permission(['post_type' => 'secret'])->get_error_code()); } public function testPermissionIsForbiddenWithoutEditRights(): void { Functions\when('get_post_type_object')->justReturn((object) ['cap' => (object) ['edit_posts' => 'edit_posts']]); Functions\when('current_user_can')->justReturn(false); $schema = Mockery::mock(ContentSchema::class); $schema->allows('isManaged')->andReturn(true); $ability = new ContentQueryAbility($schema, Mockery::mock(PostPresenter::class)); self::assertSame('lit_forbidden', $ability->permission(['post_type' => 'page'])->get_error_code()); } public function testInputSchemaLimitsSearchLengthAndTaxonomyTermsCount(): void { $schema = $this->ability()->inputSchema(); self::assertSame(200, $schema['properties']['search']['maxLength']); self::assertSame(50, $schema['properties']['taxonomy']['properties']['terms']['maxItems']); } }