Repository: defstudio/telegraph (php)

Issue:
Implement `poll` and `quiz` methods
implement https://core.telegram.org/bots/api#sendpoll

to allow:

```php
Telegraph::poll($question, [
    'answer 1',
    'answer 2',
    'answer 3',
], $isAnonymous)
```

```php
Telegraph::quiz($question, [
    'answer 1',
    'answer 2',
    'answer 3',
], $correctOptionId, $isAnonymous)
```

Pull request description:
[feat] `poll` and `quiz` methods
implement quiz and poll methods

closes #188 

Patch:
diff --git a/.github/workflows/x-ray.yml b/.github/workflows/x-ray.yml
index e4eb7183..62c3c8e3 100644
--- a/.github/workflows/x-ray.yml
+++ b/.github/workflows/x-ray.yml
@@ -22,5 +22,5 @@ jobs:
       - name: Install composer dependencies
         uses: ramsey/composer-install@v1
 
-      - name: Run PHPStan
+      - name: Run X-Ray
         run: ./vendor/bin/x-ray .
diff --git a/composer.json b/composer.json
index 2ae72762..d61bff1c 100644
--- a/composer.json
+++ b/composer.json
@@ -56,14 +56,15 @@
         "x-ray": "vendor/bin/x-ray .",
         "lint": "vendor/bin/php-cs-fixer fix -v",
         "test:lint": "vendor/bin/php-cs-fixer fix -v --dry-run",
-        "test:types": "vendor/bin/phpstan analyse --ansi",
+        "test:types": "vendor/bin/phpstan analyse --ansi --memory-limit=-1",
         "test:unit": "vendor/bin/pest --colors=always --exclude-group=sandbox",
         "test:sandbox": "vendor/bin/pest --colors=always --group=sandbox",
         "test": [
             "@test:lint",
             "@test:types",
             "@test:unit",
-            "@test:sandbox"
+            "@test:sandbox",
+            "x-ray"
         ],
         "update:snapshots": "vendor/bin/pest --colors=always -d --update-snapshots",
         "coverage": "@test:unit --coverage"
diff --git a/docs/content/en/features/telegram-api-calls.md b/docs/content/en/features/telegram-api-calls.md
index 27dd22b9..53403335 100644
--- a/docs/content/en/features/telegram-api-calls.md
+++ b/docs/content/en/features/telegram-api-calls.md
@@ -428,6 +428,34 @@ demote a user in a group, a supergroup or a channel from administrator status.
 Telegraph::demoteChatMember($userid)->send();
 ```
 
+## poll
+
+creates a native poll. For more info, see telegram [bot documentation](https://core.telegram.org/bots/api#sendpoll)
+
+```php
+Telegraph::poll("What's your favourite programming language?")
+    ->option('php')
+    ->option('typescript')
+    ->option('rust')
+    ->allowMultipeAnswers()
+    ->validUntil(now()->addMinutes(5))
+    ->send();
+```
+
+## quiz
+
+creates a quiz. For more info, see telegram [bot documentation](https://core.telegram.org/bots/api#sendpoll)
+
+```php
+Telegraph::quiz("What's your favourite programming language?")
+    ->option('php', correct: true)
+    ->option('typescript')
+    ->option('rust')
+    ->explanation('We all love php, right?')
+    ->validUntil(now()->addMinutes(5))
+    ->send();
+```
+
 ## dump
 
 print a `dump()` of the current api call status for testing purposes
diff --git a/docs/content/en/models/telegraph-chat.md b/docs/content/en/models/telegraph-chat.md
index 95b9db7b..41f7dc99 100644
--- a/docs/content/en/models/telegraph-chat.md
+++ b/docs/content/en/models/telegraph-chat.md
@@ -443,6 +443,40 @@ demote a user in a group, a supergroup or a channel from administrator status.
 use DefStudio\Telegraph\Models\TelegraphChat;
 
 /** @var TelegraphChat $telegraphChat */
-
 $telegraphChat->demoteMember($userid)->send();
 ```
+
+
+## `poll`
+
+creates a native poll. For more info, see telegram [bot documentation](https://core.telegram.org/bots/api#sendpoll)
+
+```php
+use DefStudio\Telegraph\Models\TelegraphChat;
+
+/** @var TelegraphChat $telegraphChat */
+$telegraphChat->poll("What's your favourite programming language?")
+    ->option('php')
+    ->option('typescript')
+    ->option('rust')
+    ->allowMultipeAnswers()
+    ->validUntil(now()->addMinutes(5))
+    ->send();
+```
+
+## `quiz`
+
+creates a quiz. For more info, see telegram [bot documentation](https://core.telegram.org/bots/api#sendpoll)
+
+```php
+use DefStudio\Telegraph\Models\TelegraphChat;
+
+/** @var TelegraphChat $telegraphChat */
+$telegraphChat->quiz("What's your favourite programming language?")
+    ->option('php', correct: true)
+    ->option('typescript')
+    ->option('rust')
+    ->explanation('We all love php, right?')
+    ->validUntil(now()->addMinutes(5))
+    ->send();
+```
diff --git a/phpstan.neon b/phpstan.neon
index 2a8d78f3..c9fd8198 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -3,6 +3,8 @@ parameters:
     level: max
     paths:
         - src
+    excludePaths:
+        - src/Support/Testing
 
     checkMissingIterableValueType: true
     checkGenericClassInNonGenericObjectType: false
diff --git a/src/Concerns/BuildsFromTelegraphClass.php b/src/Concerns/BuildsFromTelegraphClass.php
new file mode 100644
index 00000000..7ea7316a
--- /dev/null
+++ b/src/Concerns/BuildsFromTelegraphClass.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace DefStudio\Telegraph\Concerns;
+
+use DefStudio\Telegraph\Telegraph;
+use ReflectionClass;
+
+trait BuildsFromTelegraphClass
+{
+    public static function makeFrom(Telegraph $telegraph): self
+    {
+        $newInstance = new self();
+
+        $reflection = new ReflectionClass($telegraph);
+        $properties = $reflection->getProperties();
+
+        foreach ($properties as $property) {
+            $propertyName = $property->name;
+            $property->setAccessible(true);
+
+            if ($property->isInitialized($telegraph)) {
+                $newInstance->$propertyName = $property->getValue($telegraph);
+            }
+        }
+
+        return $newInstance;
+    }
+}
diff --git a/src/Concerns/CreatesScopedPayloads.php b/src/Concerns/CreatesScopedPayloads.php
new file mode 100644
index 00000000..08df0a4b
--- /dev/null
+++ b/src/Concerns/CreatesScopedPayloads.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace DefStudio\Telegraph\Concerns;
+
+use DefStudio\Telegraph\ScopedPayloads\TelegraphPollPayload;
+use DefStudio\Telegraph\ScopedPayloads\TelegraphQuizPayload;
+
+trait CreatesScopedPayloads
+{
+    public function poll(string $question): TelegraphPollPayload
+    {
+        $poolPayload = TelegraphPollPayload::makeFrom($this);
+
+        return $poolPayload->poll($question);
+    }
+
+    public function quiz(string $question): TelegraphQuizPayload
+    {
+        $quizPayload = TelegraphQuizPayload::makeFrom($this);
+
+        return $quizPayload->quiz($question);
+    }
+}
diff --git a/src/Concerns/FakesRequests.php b/src/Concerns/FakesRequests.php
new file mode 100644
index 00000000..3f4e1141
--- /dev/null
+++ b/src/Concerns/FakesRequests.php
@@ -0,0 +1,299 @@
+<?php
+
+/** @noinspection PhpPluralMixedCanBeReplacedWithArrayInspection */
+
+namespace DefStudio\Telegraph\Concerns;
+
+use DefStudio\Telegraph\Contracts\Downloadable;
+use DefStudio\Telegraph\Telegraph;
+use GuzzleHttp\Psr7\BufferStream;
+use Illuminate\Foundation\Bus\PendingDispatch;
+use Illuminate\Http\Client\Response;
+use Illuminate\Support\Arr;
+use Illuminate\Support\Facades\Queue;
+use Illuminate\Support\Str;
+use Illuminate\Support\Testing\Fakes\QueueFake;
+use PHPUnit\Framework\Assert;
+use Psr\Http\Message\MessageInterface;
+use Psr\Http\Message\StreamInterface;
+
+trait FakesRequests
+{
+    /** @var array<int, mixed[]> */
+    protected static array $sentMessages = [];
+
+    /** @var array<int, string> */
+    protected static array $downloadedFiles = [];
+
+    /** @var array<string, mixed[]> */
+    protected array $replies = [];
+
+    public static function reset(): void
+    {
+        self::$sentMessages = [];
+    }
+
+    protected function dispatchRequestToTelegram(string $queue = null): PendingDispatch
+    {
+        self::$sentMessages[] = $this->messageToArray();
+
+        if (!Queue::getFacadeRoot() instanceof QueueFake) {
+            Queue::fake();
+        }
+
+        return parent::dispatchRequestToTelegram($queue);
+    }
+
+    /**
+     * @return array<string, mixed>
+     */
+    protected function messageToArray(): array
+    {
+        return [
+            'url' => $this->getApiUrl(),
+            'endpoint' => $this->endpoint ?? null,
+            'data' => $this->prepareData(),
+            'files' => $this->files,
+            'bot_token' => $this->getBotIfAvailable()->token ?? null,
+            'chat_id' => $this->getChatIfAvailable()->id ?? null,
+            'message' => $this->data['text'] ?? null,
+            'parse_mode' => $this->data['parse_mode'] ?? null,
+        ];
+    }
+
+    protected function sendRequestToTelegram(): Response
+    {
+        self::$sentMessages[] = $this->messageToArray();
+
+        $messageClass = new class () implements MessageInterface {
+            public function __construct(private array|string $reply = [])
+            {
+            }
+
+            public function getStatusCode(): int
+            {
+                return 200;
+            }
+
+            public function getProtocolVersion(): string
+            {
+                return "";
+            }
+
+            public function withProtocolVersion($version): static
+            {
+                return $this;
+            }
+
+            public function getHeaders(): array
+            {
+                return [];
+            }
+
+            public function hasHeader($name): bool
+            {
+                return false;
+            }
+
+            public function getHeader($name): array
+            {
+                return [];
+            }
+
+            public function getHeaderLine($name): string
+            {
+                return "";
+            }
+
+            public function withHeader($name, $value): static
+            {
+                return $this;
+            }
+
+            public function withAddedHeader($name, $value): static
+            {
+                return $this;
+            }
+
+            public function withoutHeader($name): static
+            {
+                return $this;
+            }
+
+            public function getBody(): StreamInterface
+            {
+                $buffer = new BufferStream();
+
+                /** @phpstan-ignore-next-line */
+                $buffer->write(json_encode($this->reply));
+
+                return $buffer;
+            }
+
+            public function withBody(StreamInterface $body): static
+            {
+                return $this;
+            }
+        };
+
+        $response = $this->replies[$this->endpoint] ?? match ($this->endpoint) {
+            Telegraph::ENDPOINT_MESSAGE => [
+                'ok' => true,
+                'result' => [
+                    'message_id' => rand(1, 99999),
+                    'sender_chat' => [
+                        'id' => $this->getChatIfAvailable()?->chat_id ?? -rand(1, 99999),
+                        'title' => 'Test Chat',
+                        'type' => 'channel',
+                    ],
+                    'date' => now()->timestamp,
+                    'text' => $this->data['text'],
+                ],
+            ],
+            Telegraph::ENDPOINT_GET_BOT_INFO => [
+                'ok' => true,
+                'result' => [
+                    'id' => 42,
+                    'is_bot' => true,
+                    'first_name' => 'telegraph-test',
+                    'username' => 'test_bot',
+                    'can_join_groups' => true,
+                    'can_read_all_group_messages' => false,
+                    'supports_inline_queries' => false,
+                ],
+            ],
+            Telegraph::ENDPOINT_GET_CHAT_INFO => [
+                'ok' => true,
+                'result' => [
+                    'id' => 42,
+                    'type' => 'group',
+                    'title' => 'foo',
+                    'description' => 'bar',
+                    'has_private_forwards' => true,
+                    'join_by_request' => true,
+                    'has_protected_content' => true,
+                ],
+            ],
+            Telegraph::ENDPOINT_GET_CHAT_MEMBER_COUNT => [
+                'ok' => true,
+                'result' => 1,
+            ],
+            Telegraph::ENDPOINT_GET_BOT_UPDATES => [
+                'ok' => true,
+                'result' => [
+                    [
+                        'update_id' => 123456,
+                        'message' => [
+                            'message_id' => 42,
+                            'from' => [
+                                'id' => 444,
+                                'is_bot' => false,
+                                'first_name' => 'John',
+                                'last_name' => 'Smith',
+                                'username' => 'john_smith',
+                                'language_code' => 'en',
+                            ],
+                            'chat' => [
+                                'id' => 987654,
+                                'first_name' => 'John',
+                                'last_name' => 'Smith',
+                                'username' => 'john_smith',
+                                'type' => 'private',
+                            ],
+                            'date' => 1646516736,
+                            'text' => '/start',
+                            'entities' => [
+                                [
+                                    'offset' => 0,
+                                    'length' => 6,
+                                    'type' => 'bot_command',
+                                ],
+                            ],
+                        ],
+                    ],
+                    [
+                        'update_id' => 123457,
+                        'message' => [
+                            'message_id' => 99,
+                            'from' => [
+                                'id' => 8974,
+                                'is_bot' => true,
+                                'first_name' => 'Test Bot',
+                                'username' => 'test_bot',
+                                'can_join_groups' => true,
+                                'can_read_all_group_messages' => false,
+                                'supports_inline_queries' => false,
+                            ],
+                            'chat' => [
+                                'id' => -987455499,
+                                'title' => 'Bot Test Chat',
+                                'type' => 'group',
+                            ],
+                            'date' => 1646519736,
+                            'text' => 'Hello world!',
+                            'entities' => [],
+                        ],
+                    ],
+                ],
+            ],
+            default => [
+                'ok' => true,
+            ],
+        };
+
+        return new Response(new $messageClass($response));
+    }
+
+    public function store(Downloadable|string $downloadable, string $path, string $filename = null): string
+    {
+        $fileId = is_string($downloadable) ? $downloadable : $downloadable->id();
+
+        self::$downloadedFiles[] = $fileId;
+
+        return $path . "/" . ($filename ?? 'missing_name.jpg');
+    }
+
+    public function dumpSentData(): void
+    {
+        dump(self::$sentMessages);
+    }
+
+    /** @phpstan-ignore-next-line  */
+    public static function assertSentData(string $endpoint, array $data = null, bool $exact = true): void
+    {
+        $foundMessages = collect(self::$sentMessages);
+
+        $foundMessages = $foundMessages
+            ->filter(fn (array $message): bool => $message['endpoint'] == $endpoint)
+            ->filter(function (array $message) use ($data, $exact): bool {
+                foreach ($data ?? [] as $key => $value) {
+                    /** @var array<string, string> $data */
+                    $messageData = $message['data'];
+                    if (!Arr::has($messageData, $key)) {
+                        return false;
+                    }
+
+                    if ($exact) {
+                        if ($value != $messageData[$key]) {
+                            return false;
+                        }
+                    } else {
+                        if (!Str::of($messageData[$key])->contains($value)) {
+                            return false;
+                        }
+                    }
+                }
+
+                return true;
+            });
+
+
+        if ($data == null) {
+            $errorMessage = sprintf("Failed to assert that a request was sent to [%s] endpoint (sent %d requests so far)", $endpoint, count(self::$sentMessages));
+        } else {
+            $errorMessage = sprintf("Failed to assert that a request was sent to [%s] endpoint with the given data (sent %d requests so far)", $endpoint, count(self::$sentMessages));
+        }
+
+        Assert::assertNotEmpty($foundMessages->toArray(), $errorMessage);
+    }
+}
diff --git a/src/Concerns/HasBotsAndChats.php b/src/Concerns/HasBotsAndChats.php
index 295a3ba1..cb4b8138 100644
--- a/src/Concerns/HasBotsAndChats.php
+++ b/src/Concerns/HasBotsAndChats.php
@@ -26,9 +26,9 @@ use Illuminate\Support\Carbon;
  */
 trait HasBotsAndChats
 {
-    private TelegraphBot|null $bot;
+    protected TelegraphBot|null $bot;
 
-    private TelegraphChat|null $chat;
+    protected TelegraphChat|null $chat;
 
     public function bot(TelegraphBot $bot): Telegraph
     {
@@ -242,16 +242,6 @@ trait HasBotsAndChats
         return $telegraph;
     }
 
-    public function deleteChatPhoto(): Telegraph
-    {
-        $telegraph = clone $this;
-
-        $telegraph->endpoint = self::ENDPOINT_DELETE_CHAT_PHOTO;
-        $telegraph->data['chat_id'] = $telegraph->getChat()->chat_id;
-
-        return $telegraph;
-    }
-
     public function chatInfo(): Telegraph
     {
         $telegraph = clone $this;
diff --git a/src/Concerns/SendsPolls.php b/src/Concerns/SendsPolls.php
new file mode 100644
index 00000000..795baf49
--- /dev/null
+++ b/src/Concerns/SendsPolls.php
@@ -0,0 +1,60 @@
+<?php
+
+/** @noinspection PhpUnhandledExceptionInspection */
+
+namespace DefStudio\Telegraph\Concerns;
+
+use Carbon\CarbonInterface;
+use DefStudio\Telegraph\Exceptions\TelegraphPollException;
+use DefStudio\Telegraph\ScopedPayloads\TelegraphPollPayload;
+use DefStudio\Telegraph\ScopedPayloads\TelegraphQuizPayload;
+
+/**
+ * @mixin TelegraphPollPayload
+ * @mixin TelegraphQuizPayload
+ */
+trait SendsPolls
+{
+    public function option(string $option): static
+    {
+        $telegraph = clone $this;
+
+        /** @phpstan-ignore-next-line */
+        if (count($telegraph->data['options']) === 10) {
+            throw TelegraphPollException::tooManyOptions();
+        }
+
+        if (strlen($option) > 100) {
+            throw TelegraphPollException::optionMaxLengthExceeded($option);
+        }
+
+        /** @phpstan-ignore-next-line */
+        $telegraph->data['options'][] = $option;
+
+        return $telegraph;
+    }
+
+    public function disableAnonymous(): static
+    {
+        $telegraph = clone $this;
+        $telegraph->data['is_anonymous'] = false;
+
+        return $telegraph;
+    }
+
+    public function validUntil(CarbonInterface $endTime): self
+    {
+        if ($endTime->subSeconds(5)->isPast()) {
+            throw TelegraphPollException::durationTooShort($endTime);
+        }
+
+        if ($endTime->subSeconds(600)->isFuture()) {
+            throw TelegraphPollException::durationTooLong($endTime);
+        }
+
+        $telegraph = clone $this;
+        $telegraph->data['close_date'] = $endTime->timestamp;
+
+        return $telegraph;
+    }
+}
diff --git a/src/Exceptions/TelegraphPollException.php b/src/Exceptions/TelegraphPollException.php
new file mode 100644
index 00000000..53c5f3ce
--- /dev/null
+++ b/src/Exceptions/TelegraphPollException.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace DefStudio\Telegraph\Exceptions;
+
+use Carbon\CarbonInterface;
+
+class TelegraphPollException extends \Exception
+{
+    public static function tooManyOptions(): self
+    {
+        return new self("Maximum options count of 10 exceeded for a Telegram poll");
+    }
+
+    public static function optionMaxLengthExceeded(string $option): self
+    {
+        return new self("Max option length (100 chars) exceeded for option [$option]");
+    }
+
+    public static function explanationMaxLengthExceeded(): self
+    {
+        return new self("Max explanation length (200 chars) exceeded");
+    }
+
+    public static function onlyOneCorrectAnswerAllowed(string $chosenOption): self
+    {
+        return new self("A correct coption has already been chosen for the quiz: [$chosenOption]");
+    }
+
+    public static function durationTooShort(CarbonInterface $carbon): self
+    {
+        return new self("Poll/Quiz end time [$carbon] is too early, min duration is 5 seconds");
+    }
+
+    public static function durationTooLong(CarbonInterface $carbon): self
+    {
+        return new self("Poll/Quiz end time [$carbon] is too late, min duration is 600 seconds");
+    }
+}
diff --git a/src/Facades/Telegraph.php b/src/Facades/Telegraph.php
index 4a9b4c9a..0466188a 100644
--- a/src/Facades/Telegraph.php
+++ b/src/Facades/Telegraph.php
@@ -6,6 +6,8 @@ use DefStudio\Telegraph\Contracts\Downloadable;
 use DefStudio\Telegraph\Keyboard\Keyboard;
 use DefStudio\Telegraph\Models\TelegraphBot;
 use DefStudio\Telegraph\Models\TelegraphChat;
+use DefStudio\Telegraph\ScopedPayloads\TelegraphPollPayload;
+use DefStudio\Telegraph\ScopedPayloads\TelegraphQuizPayload;
 use DefStudio\Telegraph\Support\Testing\Fakes\TelegraphFake;
 use Illuminate\Support\Facades\Facade;
 
@@ -54,6 +56,8 @@ use Illuminate\Support\Facades\Facade;
  * @method static \DefStudio\Telegraph\Telegraph  restrictChatMember(string $userId, array $permissions)
  * @method static \DefStudio\Telegraph\Telegraph  promoteChatMember(string $userId, array $permissions)
  * @method static \DefStudio\Telegraph\Telegraph  demoteChatMember(string $userId)
+ * @method static TelegraphPollPayload poll(string $question)
+ * @method static TelegraphQuizPayload quiz(string $question)
  * @method static string store(Downloadable $attachment, string $path, string $filename = null)
  * @method static void  dumpSentData()
  * @method static void  assertSentData(string $endpoint, array $data = null, bool $exact = true)
diff --git a/src/Models/TelegraphChat.php b/src/Models/TelegraphChat.php
index 47556592..fcba40d6 100644
--- a/src/Models/TelegraphChat.php
+++ b/src/Models/TelegraphChat.php
@@ -10,6 +10,8 @@ use DefStudio\Telegraph\Database\Factories\TelegraphChatFactory;
 use DefStudio\Telegraph\Exceptions\TelegraphException;
 use DefStudio\Telegraph\Facades\Telegraph as TelegraphFacade;
 use DefStudio\Telegraph\Keyboard\Keyboard;
+use DefStudio\Telegraph\ScopedPayloads\TelegraphPollPayload;
+use DefStudio\Telegraph\ScopedPayloads\TelegraphQuizPayload;
 use DefStudio\Telegraph\Telegraph;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
@@ -196,11 +198,6 @@ class TelegraphChat extends Model
         return TelegraphFacade::chat($this)->setChatPhoto($path);
     }
 
-    public function deleteChatPhoto(): Telegraph
-    {
-        return TelegraphFacade::chat($this)->deleteChatPhoto();
-    }
-
     public function generatePrimaryInviteLink(): Telegraph
     {
         return TelegraphFacade::chat($this)->generateChatPrimaryInviteLink();
@@ -259,4 +256,14 @@ class TelegraphChat extends Model
     {
         return TelegraphFacade::chat($this)->demoteChatMember($userId);
     }
+
+    public function poll(string $question): TelegraphPollPayload
+    {
+        return TelegraphFacade::chat($this)->poll($question);
+    }
+
+    public function quiz(string $question): TelegraphQuizPayload
+    {
+        return TelegraphFacade::chat($this)->quiz($question);
+    }
 }
diff --git a/src/ScopedPayloads/TelegraphPollPayload.php b/src/ScopedPayloads/TelegraphPollPayload.php
new file mode 100644
index 00000000..c3ea53aa
--- /dev/null
+++ b/src/ScopedPayloads/TelegraphPollPayload.php
@@ -0,0 +1,35 @@
+<?php
+
+/** @noinspection PhpUnhandledExceptionInspection */
+
+namespace DefStudio\Telegraph\ScopedPayloads;
+
+use DefStudio\Telegraph\Concerns\BuildsFromTelegraphClass;
+use DefStudio\Telegraph\Concerns\SendsPolls;
+use DefStudio\Telegraph\Telegraph;
+
+class TelegraphPollPayload extends Telegraph
+{
+    use BuildsFromTelegraphClass;
+    use SendsPolls;
+
+    public function poll(string $question): static
+    {
+        $telegraph = clone $this;
+
+        $telegraph->endpoint = self::ENDPOINT_SEND_POLL;
+        $telegraph->data['chat_id'] = $telegraph->getChat()->chat_id;
+        $telegraph->data['options'] = [];
+        $telegraph->data['question'] = $question;
+
+        return $telegraph;
+    }
+
+    public function allowMultipleAnswers(): static
+    {
+        $telegraph = clone $this;
+        $telegraph->data['allows_multiple_answers'] = true;
+
+        return $telegraph;
+    }
+}
diff --git a/src/ScopedPayloads/TelegraphQuizPayload.php b/src/ScopedPayloads/TelegraphQuizPayload.php
new file mode 100644
index 00000000..1fbef3a9
--- /dev/null
+++ b/src/ScopedPayloads/TelegraphQuizPayload.php
@@ -0,0 +1,60 @@
+<?php
+
+/** @noinspection PhpUnhandledExceptionInspection */
+
+namespace DefStudio\Telegraph\ScopedPayloads;
+
+use DefStudio\Telegraph\Concerns\BuildsFromTelegraphClass;
+use DefStudio\Telegraph\Concerns\SendsPolls;
+use DefStudio\Telegraph\Exceptions\TelegraphPollException;
+use DefStudio\Telegraph\Telegraph;
+
+class TelegraphQuizPayload extends Telegraph
+{
+    use BuildsFromTelegraphClass;
+    use SendsPolls {
+        option as protected _createOption;
+    }
+
+    public function quiz(string $question): static
+    {
+        $telegraph = clone $this;
+
+        $telegraph->endpoint = self::ENDPOINT_SEND_POLL;
+        $telegraph->data['chat_id'] = $telegraph->getChat()->chat_id;
+        $telegraph->data['type'] = 'quiz';
+        $telegraph->data['options'] = [];
+        $telegraph->data['question'] = $question;
+
+        return $telegraph;
+    }
+
+    public function option(string $option, bool $correct = false): static
+    {
+        $telegraph = self::_createOption($option);
+
+        if ($correct) {
+            if (isset($telegraph->data['correct_option_id'])) {
+                /** @phpstan-ignore-next-line */
+                throw TelegraphPollException::onlyOneCorrectAnswerAllowed($telegraph->data['options'][$telegraph->data['correct_option_id']]);
+            }
+
+            /** @phpstan-ignore-next-line */
+            $telegraph->data['correct_option_id'] = count($telegraph->data['options']) - 1;
+        }
+
+        return $telegraph;
+    }
+
+    public function explanation(string $text): static
+    {
+        if (strlen($text) > 200) {
+            throw TelegraphPollException::explanationMaxLengthExceeded();
+        }
+
+        $telegraph = clone $this;
+        $telegraph->data['explanation'] = $text;
+
+        return $telegraph;
+    }
+}
diff --git a/src/Telegraph.php b/src/Telegraph.php
index 78ea7b2d..923a04d5 100755
--- a/src/Telegraph.php
+++ b/src/Telegraph.php
@@ -11,6 +11,7 @@ use DefStudio\Telegraph\Client\TelegraphResponse;
 use DefStudio\Telegraph\Concerns\AnswersInlineQueries;
 use DefStudio\Telegraph\Concerns\CallTraitsMethods;
 use DefStudio\Telegraph\Concerns\ComposesMessages;
+use DefStudio\Telegraph\Concerns\CreatesScopedPayloads;
 use DefStudio\Telegraph\Concerns\HasBotsAndChats;
 use DefStudio\Telegraph\Concerns\InteractsWithTelegram;
 use DefStudio\Telegraph\Concerns\InteractsWithWebhooks;
@@ -32,6 +33,7 @@ class Telegraph
     use SendsAttachments;
     use StoresFiles;
     use AnswersInlineQueries;
+    use CreatesScopedPayloads;
 
     public const MAX_DOCUMENT_SIZE_IN_MB = 50;
     public const MAX_PHOTO_SIZE_IN_MB = 10;
@@ -75,7 +77,6 @@ class Telegraph
     public const ENDPOINT_SET_CHAT_TITLE = 'setChatTitle';
     public const ENDPOINT_SET_CHAT_DESCRIPTION = 'setChatDescription';
     public const ENDPOINT_SET_CHAT_PHOTO = 'setChatPhoto';
-    public const ENDPOINT_DELETE_CHAT_PHOTO = 'deleteChatPhoto';
     public const ENDPOINT_EXPORT_CHAT_INVITE_LINK = 'exportChatInviteLink';
     public const ENDPOINT_CREATE_CHAT_INVITE_LINK = 'createChatInviteLink';
     public const ENDPOINT_EDIT_CHAT_INVITE_LINK = 'editChatInviteLink';
@@ -88,7 +89,7 @@ class Telegraph
     public const ENDPOINT_UNBAN_CHAT_MEMBER = 'unbanChatMember';
     public const ENDPOINT_RESTRICT_CHAT_MEMBER = 'restrictChatMember';
     public const ENDPOINT_PROMOTE_CHAT_MEMBER = 'promoteChatMember';
-
+    public const ENDPOINT_SEND_POLL = 'sendPoll';
 
 
     /** @var array<string, mixed> */
diff --git a/x-ray.yml b/x-ray.yml
index b7e96a25..42c2ac4a 100644
--- a/x-ray.yml
+++ b/x-ray.yml
@@ -13,6 +13,6 @@ paths:
     - 'build/*'
     - 'docs/*'
     - 'vendor/*'
-    - 'src/Support/Testing/Fakes/TelegraphFake.php'
+    - 'src/Concerns/FakesRequests.php'
     - 'src/Telegraph.php'
     - 'src/Client/TelegraphResponse.php'


Test patch:
diff --git a/src/Support/Testing/Fakes/TelegraphFake.php b/src/Support/Testing/Fakes/TelegraphFake.php
index a014e57d..f2ad577b 100644
--- a/src/Support/Testing/Fakes/TelegraphFake.php
+++ b/src/Support/Testing/Fakes/TelegraphFake.php
@@ -8,303 +8,46 @@
 
 namespace DefStudio\Telegraph\Support\Testing\Fakes;
 
-use DefStudio\Telegraph\Contracts\Downloadable;
+use DefStudio\Telegraph\Concerns\FakesRequests;
 use DefStudio\Telegraph\DTO\Attachment;
+use DefStudio\Telegraph\ScopedPayloads\TelegraphPollPayload;
+use DefStudio\Telegraph\ScopedPayloads\TelegraphQuizPayload;
 use DefStudio\Telegraph\Telegraph;
-use GuzzleHttp\Psr7\BufferStream;
-use Illuminate\Foundation\Bus\PendingDispatch;
-use Illuminate\Http\Client\Response;
 use Illuminate\Support\Arr;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Str;
-use Illuminate\Support\Testing\Fakes\QueueFake;
 use PHPUnit\Framework\Assert;
-use Psr\Http\Message\MessageInterface;
-use Psr\Http\Message\StreamInterface;
 
 class TelegraphFake extends Telegraph
 {
-    /** @var array<int, mixed[]> */
-    private static array $sentMessages = [];
-
-    /** @var array<int, string> */
-    private static array $downloadedFiles = [];
+    use FakesRequests;
 
     /**
      * @param array<string, array<mixed>> $replies
      */
-    public function __construct(private array $replies = [])
+    public function __construct(array $replies = [])
     {
         parent::__construct();
+        $this->replies = $replies;
     }
 
-    public static function reset(): void
-    {
-        self::$sentMessages = [];
-    }
-
-    protected function dispatchRequestToTelegram(string $queue = null): PendingDispatch
+    public function poll(string $question): TelegraphPollPayload
     {
-        self::$sentMessages[] = $this->messageToArray();
+        $fake = new TelegraphPollFake($this->replies);
+        $fake->endpoint = self::ENDPOINT_SEND_POLL;
+        $fake->data['options'] = [];
+        $fake->data['question'] = $question;
 
-        if (!Queue::getFacadeRoot() instanceof QueueFake) {
-            Queue::fake();
-        }
-
-        return parent::dispatchRequestToTelegram($queue);
+        return $fake;
     }
 
-    /**
-     * @return array<string, mixed>
-     */
-    protected function messageToArray(): array
+    public function quiz(string $question): TelegraphQuizPayload
     {
-        return [
-            'url' => $this->getApiUrl(),
-            'endpoint' => $this->endpoint ?? null,
-            'data' => $this->prepareData(),
-            'files' => $this->files,
-            'bot_token' => $this->getBotIfAvailable()->token ?? null,
-            'chat_id' => $this->getChatIfAvailable()->id ?? null,
-            'message' => $this->data['text'] ?? null,
-            'parse_mode' => $this->data['parse_mode'] ?? null,
-        ];
-    }
-
-    protected function sendRequestToTelegram(): Response
-    {
-        self::$sentMessages[] = $this->messageToArray();
-
-        $messageClass = new class () implements MessageInterface {
-            /**
-             * @param array<string, mixed> $reply
-             */
-            public function __construct(private array|string $reply = [])
-            {
-            }
-
-            public function getStatusCode(): int
-            {
-                return 200;
-            }
-
-            public function getProtocolVersion(): string
-            {
-                return "";
-            }
-
-            public function withProtocolVersion($version): static
-            {
-                return $this;
-            }
+        $fake = new TelegraphQuizFake($this->replies);
+        $fake->endpoint = self::ENDPOINT_SEND_POLL;
+        $fake->data['options'] = [];
+        $fake->data['question'] = $question;
+        $fake->data['type'] = 'quiz';
 
-            public function getHeaders(): array
-            {
-                return [];
-            }
-
-            public function hasHeader($name): bool
-            {
-                return false;
-            }
-
-            public function getHeader($name): array
-            {
-                return [];
-            }
-
-            public function getHeaderLine($name): string
-            {
-                return "";
-            }
-
-            public function withHeader($name, $value): static
-            {
-                return $this;
-            }
-
-            public function withAddedHeader($name, $value): static
-            {
-                return $this;
-            }
-
-            public function withoutHeader($name): static
-            {
-                return $this;
-            }
-
-            public function getBody(): StreamInterface
-            {
-                $buffer = new BufferStream();
-
-                /** @phpstan-ignore-next-line */
-                $buffer->write(json_encode($this->reply));
-
-                return $buffer;
-            }
-
-            public function withBody(StreamInterface $body): static
-            {
-                return $this;
-            }
-        };
-
-        $response = $this->replies[$this->endpoint] ?? match ($this->endpoint) {
-            Telegraph::ENDPOINT_MESSAGE => [
-                'ok' => true,
-                'result' => [
-                    'message_id' => rand(1, 99999),
-                    'sender_chat' => [
-                        'id' => $this->getChatIfAvailable()?->chat_id ?? -rand(1, 99999),
-                        'title' => 'Test Chat',
-                        'type' => 'channel',
-                    ],
-                    'date' => now()->timestamp,
-                    'text' => $this->data['text'],
-                ],
-            ],
-            Telegraph::ENDPOINT_GET_BOT_INFO => [
-                'ok' => true,
-                'result' => [
-                    'id' => 42,
-                    'is_bot' => true,
-                    'first_name' => 'telegraph-test',
-                    'username' => 'test_bot',
-                    'can_join_groups' => true,
-                    'can_read_all_group_messages' => false,
-                    'supports_inline_queries' => false,
-                ],
-            ],
-            Telegraph::ENDPOINT_GET_CHAT_INFO => [
-                'ok' => true,
-                'result' => [
-                    'id' => 42,
-                    'type' => 'group',
-                    'title' => 'foo',
-                    'description' => 'bar',
-                    'has_private_forwards' => true,
-                    'join_by_request' => true,
-                    'has_protected_content' => true,
-                ],
-            ],
-            Telegraph::ENDPOINT_GET_CHAT_MEMBER_COUNT => [
-                'ok' => true,
-                'result' => 1,
-            ],
-            Telegraph::ENDPOINT_GET_BOT_UPDATES => [
-                'ok' => true,
-                'result' => [
-                    [
-                        'update_id' => 123456,
-                        'message' => [
-                            'message_id' => 42,
-                            'from' => [
-                                'id' => 444,
-                                'is_bot' => false,
-                                'first_name' => 'John',
-                                'last_name' => 'Smith',
-                                'username' => 'john_smith',
-                                'language_code' => 'en',
-                            ],
-                            'chat' => [
-                                'id' => 987654,
-                                'first_name' => 'John',
-                                'last_name' => 'Smith',
-                                'username' => 'john_smith',
-                                'type' => 'private',
-                            ],
-                            'date' => 1646516736,
-                            'text' => '/start',
-                            'entities' => [
-                                [
-                                    'offset' => 0,
-                                    'length' => 6,
-                                    'type' => 'bot_command',
-                                ],
-                            ],
-                        ],
-                    ],
-                    [
-                        'update_id' => 123457,
-                        'message' => [
-                            'message_id' => 99,
-                            'from' => [
-                                'id' => 8974,
-                                'is_bot' => true,
-                                'first_name' => 'Test Bot',
-                                'username' => 'test_bot',
-                                'can_join_groups' => true,
-                                'can_read_all_group_messages' => false,
-                                'supports_inline_queries' => false,
-                            ],
-                            'chat' => [
-                                'id' => -987455499,
-                                'title' => 'Bot Test Chat',
-                                'type' => 'group',
-                            ],
-                            'date' => 1646519736,
-                            'text' => 'Hello world!',
-                            'entities' => [],
-                        ],
-                    ],
-                ],
-            ],
-            default => [
-                'ok' => true,
-            ],
-        };
-
-        return new Response(new $messageClass($response));
-    }
-
-    public function store(Downloadable|string $downloadable, string $path, string $filename = null): string
-    {
-        $fileId = is_string($downloadable) ? $downloadable : $downloadable->id();
-
-        self::$downloadedFiles[] = $fileId;
-
-        return $path . "/" . ($filename ?? 'missing_name.jpg');
-    }
-
-    /**
-     * @param array<string, string> $data
-     */
-    public function assertSentData(string $endpoint, array $data = [], bool $exact = true): void
-    {
-        $foundMessages = collect(self::$sentMessages);
-
-        $foundMessages = $foundMessages
-            ->filter(fn (array $message): bool => $message['endpoint'] == $endpoint)
-            ->filter(function (array $message) use ($data, $exact): bool {
-                foreach ($data as $key => $value) {
-                    /** @var array<string, string> $data */
-                    $data = $message['data'];
-                    if (!Arr::has($data, $key)) {
-                        return false;
-                    }
-
-                    if ($exact) {
-                        if ($value != $data[$key]) {
-                            return false;
-                        }
-                    } else {
-                        if (!Str::of($data[$key])->contains($value)) {
-                            return false;
-                        }
-                    }
-                }
-
-                return true;
-            });
-
-
-        if ($data == null) {
-            $errorMessage = sprintf("Failed to assert that a request was sent to [%s] endpoint (sent %d requests so far)", $endpoint, count(self::$sentMessages));
-        } else {
-            $errorMessage = sprintf("Failed to assert that a request was sent to [%s] endpoint with the given data (sent %d requests so far)", $endpoint, count(self::$sentMessages));
-        }
-
-        Assert::assertNotEmpty($foundMessages->toArray(), $errorMessage);
+        return $fake;
     }
 
     /**
@@ -388,9 +131,4 @@ class TelegraphFake extends Telegraph
             'text' => $message,
         ]);
     }
-
-    public function dumpSentData(): void
-    {
-        dump(self::$sentMessages);
-    }
 }
diff --git a/src/Support/Testing/Fakes/TelegraphPollFake.php b/src/Support/Testing/Fakes/TelegraphPollFake.php
new file mode 100644
index 00000000..f4929744
--- /dev/null
+++ b/src/Support/Testing/Fakes/TelegraphPollFake.php
@@ -0,0 +1,47 @@
+<?php
+
+/** @noinspection PhpUnhandledExceptionInspection */
+
+/** @noinspection PhpUnused */
+
+/** @noinspection PhpPropertyOnlyWrittenInspection */
+
+namespace DefStudio\Telegraph\Support\Testing\Fakes;
+
+use DefStudio\Telegraph\Concerns\FakesRequests;
+
+use DefStudio\Telegraph\ScopedPayloads\TelegraphPollPayload;
+use DefStudio\Telegraph\Telegraph;
+
+class TelegraphPollFake extends TelegraphPollPayload
+{
+    use FakesRequests;
+
+    /**
+     * @param array<string, array<mixed>> $replies
+     */
+    public function __construct(array $replies = [])
+    {
+        parent::__construct();
+        $this->replies = $replies;
+    }
+
+    /**
+     * @param array<int, string> $options
+     */
+    public static function assertSentPoll(string $question, array $options = []): void
+    {
+        if (empty($options)) {
+            self::assertSentData(Telegraph::ENDPOINT_SEND_POLL, [
+                'question' => $question,
+            ], false);
+
+            return;
+        }
+
+        self::assertSentData(Telegraph::ENDPOINT_SEND_POLL, [
+            'question' => $question,
+            'options' => ['bar!', 'baz!'],
+        ]);
+    }
+}
diff --git a/src/Support/Testing/Fakes/TelegraphQuizFake.php b/src/Support/Testing/Fakes/TelegraphQuizFake.php
new file mode 100644
index 00000000..586158fe
--- /dev/null
+++ b/src/Support/Testing/Fakes/TelegraphQuizFake.php
@@ -0,0 +1,46 @@
+<?php
+
+/** @noinspection PhpUnhandledExceptionInspection */
+
+/** @noinspection PhpUnused */
+
+/** @noinspection PhpPropertyOnlyWrittenInspection */
+
+namespace DefStudio\Telegraph\Support\Testing\Fakes;
+
+use DefStudio\Telegraph\Concerns\FakesRequests;
+
+use DefStudio\Telegraph\ScopedPayloads\TelegraphQuizPayload;
+use DefStudio\Telegraph\Telegraph;
+
+class TelegraphQuizFake extends TelegraphQuizPayload
+{
+    use FakesRequests;
+
+    /**
+     * @param array<string, array<mixed>> $replies
+     */
+    public function __construct(array $replies = [])
+    {
+        parent::__construct();
+        $this->replies = $replies;
+    }
+
+    /**
+     * @param array<int, string> $options
+     */
+    public static function assertSentQuiz(string $question, array $options = [], int $correct_index = null): void
+    {
+        $data = ['question' => $question, 'type' => 'quiz'];
+
+        if (!empty($options)) {
+            $data['options'] = $options;
+        }
+
+        if ($correct_index !== null) {
+            $data['correct_option_id'] = $correct_index;
+        }
+
+        self::assertSentData(Telegraph::ENDPOINT_SEND_POLL, $data);
+    }
+}
diff --git a/tests/Unit/Concerns/CreatesScopedPayloads.php b/tests/Unit/Concerns/CreatesScopedPayloads.php
new file mode 100644
index 00000000..cef2f7d1
--- /dev/null
+++ b/tests/Unit/Concerns/CreatesScopedPayloads.php
@@ -0,0 +1,22 @@
+<?php
+
+/** @noinspection PhpUnhandledExceptionInspection */
+
+
+use DefStudio\Telegraph\Facades\Telegraph;
+use DefStudio\Telegraph\ScopedPayloads\TelegraphPollPayload;
+use DefStudio\Telegraph\ScopedPayloads\TelegraphQuizPayload;
+
+it('can create a poll payload', function () {
+    $payload = Telegraph::chat(make_chat())->poll("foo");
+
+    expect($payload)
+        ->toBeInstanceOf(TelegraphPollPayload::class);
+});
+
+it('can create a quiz payload', function () {
+    $payload = Telegraph::chat(make_chat())->quiz("foo");
+
+    expect($payload)
+        ->toBeInstanceOf(TelegraphQuizPayload::class);
+});
diff --git a/tests/Unit/Concerns/HasBotsAndChatsTest.php b/tests/Unit/Concerns/HasBotsAndChatsTest.php
index 80d7e4f2..cd2b0837 100644
--- a/tests/Unit/Concerns/HasBotsAndChatsTest.php
+++ b/tests/Unit/Concerns/HasBotsAndChatsTest.php
@@ -130,12 +130,6 @@ test('photo is validated', function (string $path, bool $valid, string $exceptio
     ],
 ]);
 
-it('can delete chat photo', function () {
-    expect(function (\DefStudio\Telegraph\Telegraph $telegraph) {
-        return $telegraph->chat(make_chat())->deleteChatPhoto();
-    })->toMatchTelegramSnapshot();
-});
-
 it('can retrieve chat info', function () {
     expect(function (\DefStudio\Telegraph\Telegraph $telegraph) {
         return $telegraph->chat(make_chat())->chatInfo();
diff --git a/tests/Unit/Models/TelegraphChatTest.php b/tests/Unit/Models/TelegraphChatTest.php
index 0c75f842..e889cfc6 100644
--- a/tests/Unit/Models/TelegraphChatTest.php
+++ b/tests/Unit/Models/TelegraphChatTest.php
@@ -10,6 +10,8 @@ use DefStudio\Telegraph\Facades\Telegraph;
 use DefStudio\Telegraph\Keyboard\Button;
 use DefStudio\Telegraph\Keyboard\Keyboard;
 use DefStudio\Telegraph\Models\TelegraphBot;
+use DefStudio\Telegraph\Support\Testing\Fakes\TelegraphPollFake;
+use DefStudio\Telegraph\Support\Testing\Fakes\TelegraphQuizFake;
 use Illuminate\Support\Facades\Storage;
 
 use Illuminate\Support\Str;
@@ -252,15 +254,6 @@ it('can unpin all messages', function () {
     Telegraph::assertSentData(\DefStudio\Telegraph\Telegraph::ENDPOINT_UNPIN_ALL_MESSAGES);
 });
 
-it('can delete a chat photo', function () {
-    Telegraph::fake();
-    $chat = make_chat();
-
-    $chat->deleteChatPhoto()->send();
-
-    Telegraph::assertSentData(\DefStudio\Telegraph\Telegraph::ENDPOINT_DELETE_CHAT_PHOTO);
-});
-
 it('can leave a chat', function () {
     Telegraph::fake();
     $chat = make_chat();
@@ -450,3 +443,21 @@ it('can demote a chat member', function () {
 
     ], false);
 });
+
+it('can create a poll', function () {
+    Telegraph::fake();
+    $chat = make_chat();
+
+    $chat->poll('foo?')->option('bar!')->option('baz!')->send();
+
+    TelegraphPollFake::assertSentPoll('foo?', ['bar!', 'baz!']);
+});
+
+it('can create a quiz', function () {
+    Telegraph::fake();
+    $chat = make_chat();
+
+    $chat->quiz('foo?')->option('bar!')->option('baz!', true)->send();
+
+    TelegraphQuizFake::assertSentQuiz('foo?', ['bar!', 'baz!'], 1);
+});
diff --git a/tests/Unit/ScopedPayloads/TelegraphPollPayloadTest.php b/tests/Unit/ScopedPayloads/TelegraphPollPayloadTest.php
new file mode 100644
index 00000000..55a960ee
--- /dev/null
+++ b/tests/Unit/ScopedPayloads/TelegraphPollPayloadTest.php
@@ -0,0 +1,46 @@
+<?php
+
+/** @noinspection PhpUnhandledExceptionInspection */
+
+use DefStudio\Telegraph\Telegraph;
+use Illuminate\Support\Carbon;
+
+it('can create a poll', function () {
+    expect(
+        fn (Telegraph $telegraph) => $telegraph
+        ->poll('foo?')
+        ->option('bar')
+        ->option('baz')
+    )->toMatchTelegramSnapshot();
+});
+
+it('can allow multiple answers', function () {
+    expect(
+        fn (Telegraph $telegraph) => $telegraph
+        ->poll('foo?')
+        ->option('bar')
+        ->option('baz')
+        ->allowMultipleAnswers()
+    )->toMatchTelegramSnapshot();
+});
+
+it('can disable anonymous answers', function () {
+    expect(
+        fn (Telegraph $telegraph) => $telegraph
+        ->poll('foo?')
+        ->option('bar')
+        ->option('baz')
+        ->disableAnonymous()
+    )->toMatchTelegramSnapshot();
+});
+
+it('can set poll validity', function () {
+    \Spatie\PestPluginTestTime\testTime()->freeze(Carbon::make('2020-10-03'));
+    expect(
+        fn (Telegraph $telegraph) => $telegraph
+        ->poll('foo?')
+        ->option('bar')
+        ->option('baz')
+        ->validUntil(now()->addSeconds(30))
+    )->toMatchTelegramSnapshot();
+});
diff --git a/tests/Unit/ScopedPayloads/TelegraphQuizPayloadTest.php b/tests/Unit/ScopedPayloads/TelegraphQuizPayloadTest.php
new file mode 100644
index 00000000..535dcb8f
--- /dev/null
+++ b/tests/Unit/ScopedPayloads/TelegraphQuizPayloadTest.php
@@ -0,0 +1,48 @@
+<?php
+
+/** @noinspection PhpUnhandledExceptionInspection */
+
+use DefStudio\Telegraph\Telegraph;
+use Illuminate\Support\Carbon;
+
+it('can create a quiz', function () {
+    expect(
+        fn (Telegraph $telegraph) => $telegraph
+        ->quiz('foo?')
+        ->option('bar')
+        ->option('baz', true)
+        ->option('qux')
+    )->toMatchTelegramSnapshot();
+});
+
+it('can disable anonymous answers', function () {
+    expect(
+        fn (Telegraph $telegraph) => $telegraph
+        ->quiz('foo?')
+        ->option('bar')
+        ->option('baz')
+        ->disableAnonymous()
+    )->toMatchTelegramSnapshot();
+});
+
+it('can set quiz validity', function () {
+    \Spatie\PestPluginTestTime\testTime()->freeze(Carbon::make('2020-10-03'));
+    expect(
+        fn (Telegraph $telegraph) => $telegraph
+        ->quiz('foo?')
+        ->option('bar')
+        ->option('baz')
+        ->validUntil(now()->addSeconds(30))
+    )->toMatchTelegramSnapshot();
+});
+
+it('can set answer explanation', function () {
+    expect(
+        fn (Telegraph $telegraph) => $telegraph
+        ->quiz('foo?')
+        ->option('bar')
+        ->option('baz', true)
+        ->option('qux')
+        ->explanation("quux")
+    )->toMatchTelegramSnapshot();
+});
diff --git a/tests/Unit/Support/Testing/TelegraphFakeTest.php b/tests/Unit/Support/Testing/TelegraphFakeTest.php
index 19276c99..e5b284cc 100644
--- a/tests/Unit/Support/Testing/TelegraphFakeTest.php
+++ b/tests/Unit/Support/Testing/TelegraphFakeTest.php
@@ -34,7 +34,6 @@ it('fails if the given message is not sent', function () {
 
     Telegraph::assertSent('bar');
 })->throws(ExpectationFailedException::class, 'Failed to assert that a request was sent to [sendMessage] endpoint with the given data (sent 1 requests so far)');
-;
 
 it('asserts a partial message is sent', function () {
     $bot = make_bot();
diff --git a/tests/__snapshots__/TelegraphPollPayloadTest__it_can_allow_multiple_answers__1.yml b/tests/__snapshots__/TelegraphPollPayloadTest__it_can_allow_multiple_answers__1.yml
new file mode 100644
index 00000000..9ab7ca1c
--- /dev/null
+++ b/tests/__snapshots__/TelegraphPollPayloadTest__it_can_allow_multiple_answers__1.yml
@@ -0,0 +1,7 @@
+url: 'https://api.telegram.org/bot3f3814e1-5836-3d77-904e-60f64b15df36/sendPoll'
+payload:
+    chat_id: '-123456789'
+    options: [bar, baz]
+    question: 'foo?'
+    allows_multiple_answers: true
+files: {  }
diff --git a/tests/__snapshots__/HasBotsAndChatsTest__it_can_delete_chat_photo__1.yml b/tests/__snapshots__/TelegraphPollPayloadTest__it_can_create_a_poll__1.yml
similarity index 64%
rename from tests/__snapshots__/HasBotsAndChatsTest__it_can_delete_chat_photo__1.yml
rename to tests/__snapshots__/TelegraphPollPayloadTest__it_can_create_a_poll__1.yml
index 7d22f330..1022348a 100644
--- a/tests/__snapshots__/HasBotsAndChatsTest__it_can_delete_chat_photo__1.yml
+++ b/tests/__snapshots__/TelegraphPollPayloadTest__it_can_create_a_poll__1.yml
@@ -1,4 +1,6 @@
-url: 'https://api.telegram.org/bot3f3814e1-5836-3d77-904e-60f64b15df36/deleteChatPhoto'
+url: 'https://api.telegram.org/bot3f3814e1-5836-3d77-904e-60f64b15df36/sendPoll'
 payload:
     chat_id: '-123456789'
+    options: [bar, baz]
+    question: 'foo?'
 files: {  }
diff --git a/tests/__snapshots__/TelegraphPollPayloadTest__it_can_disable_anonymous_answers__1.yml b/tests/__snapshots__/TelegraphPollPayloadTest__it_can_disable_anonymous_answers__1.yml
new file mode 100644
index 00000000..40bc445b
--- /dev/null
+++ b/tests/__snapshots__/TelegraphPollPayloadTest__it_can_disable_anonymous_answers__1.yml
@@ -0,0 +1,7 @@
+url: 'https://api.telegram.org/bot3f3814e1-5836-3d77-904e-60f64b15df36/sendPoll'
+payload:
+    chat_id: '-123456789'
+    options: [bar, baz]
+    question: 'foo?'
+    is_anonymous: false
+files: {  }
diff --git a/tests/__snapshots__/TelegraphPollPayloadTest__it_can_set_poll_validity__1.yml b/tests/__snapshots__/TelegraphPollPayloadTest__it_can_set_poll_validity__1.yml
new file mode 100644
index 00000000..66e5ed68
--- /dev/null
+++ b/tests/__snapshots__/TelegraphPollPayloadTest__it_can_set_poll_validity__1.yml
@@ -0,0 +1,7 @@
+url: 'https://api.telegram.org/bot3f3814e1-5836-3d77-904e-60f64b15df36/sendPoll'
+payload:
+    chat_id: '-123456789'
+    options: [bar, baz]
+    question: 'foo?'
+    close_date: 1601682625
+files: {  }
diff --git a/tests/__snapshots__/TelegraphQuizPayloadTest__it_can_create_a_quiz__1.yml b/tests/__snapshots__/TelegraphQuizPayloadTest__it_can_create_a_quiz__1.yml
new file mode 100644
index 00000000..a468c193
--- /dev/null
+++ b/tests/__snapshots__/TelegraphQuizPayloadTest__it_can_create_a_quiz__1.yml
@@ -0,0 +1,8 @@
+url: 'https://api.telegram.org/bot3f3814e1-5836-3d77-904e-60f64b15df36/sendPoll'
+payload:
+    chat_id: '-123456789'
+    type: quiz
+    options: [bar, baz, qux]
+    question: 'foo?'
+    correct_option_id: 1
+files: {  }
diff --git a/tests/__snapshots__/TelegraphQuizPayloadTest__it_can_disable_anonymous_answers__1.yml b/tests/__snapshots__/TelegraphQuizPayloadTest__it_can_disable_anonymous_answers__1.yml
new file mode 100644
index 00000000..3c71b353
--- /dev/null
+++ b/tests/__snapshots__/TelegraphQuizPayloadTest__it_can_disable_anonymous_answers__1.yml
@@ -0,0 +1,8 @@
+url: 'https://api.telegram.org/bot3f3814e1-5836-3d77-904e-60f64b15df36/sendPoll'
+payload:
+    chat_id: '-123456789'
+    type: quiz
+    options: [bar, baz]
+    question: 'foo?'
+    is_anonymous: false
+files: {  }
diff --git a/tests/__snapshots__/TelegraphQuizPayloadTest__it_can_set_answer_explanation__1.yml b/tests/__snapshots__/TelegraphQuizPayloadTest__it_can_set_answer_explanation__1.yml
new file mode 100644
index 00000000..964280e4
--- /dev/null
+++ b/tests/__snapshots__/TelegraphQuizPayloadTest__it_can_set_answer_explanation__1.yml
@@ -0,0 +1,9 @@
+url: 'https://api.telegram.org/bot3f3814e1-5836-3d77-904e-60f64b15df36/sendPoll'
+payload:
+    chat_id: '-123456789'
+    type: quiz
+    options: [bar, baz, qux]
+    question: 'foo?'
+    correct_option_id: 1
+    explanation: quux
+files: {  }
diff --git a/tests/__snapshots__/TelegraphQuizPayloadTest__it_can_set_quiz_validity__1.yml b/tests/__snapshots__/TelegraphQuizPayloadTest__it_can_set_quiz_validity__1.yml
new file mode 100644
index 00000000..2acdd026
--- /dev/null
+++ b/tests/__snapshots__/TelegraphQuizPayloadTest__it_can_set_quiz_validity__1.yml
@@ -0,0 +1,8 @@
+url: 'https://api.telegram.org/bot3f3814e1-5836-3d77-904e-60f64b15df36/sendPoll'
+payload:
+    chat_id: '-123456789'
+    type: quiz
+    options: [bar, baz]
+    question: 'foo?'
+    close_date: 1601682625
+files: {  }
diff --git a/tests/__snapshots__/TeletraphPollPayloadTest__it_can_allow_multiple_answers__1.yml b/tests/__snapshots__/TeletraphPollPayloadTest__it_can_allow_multiple_answers__1.yml
new file mode 100644
index 00000000..9ab7ca1c
--- /dev/null
+++ b/tests/__snapshots__/TeletraphPollPayloadTest__it_can_allow_multiple_answers__1.yml
@@ -0,0 +1,7 @@
+url: 'https://api.telegram.org/bot3f3814e1-5836-3d77-904e-60f64b15df36/sendPoll'
+payload:
+    chat_id: '-123456789'
+    options: [bar, baz]
+    question: 'foo?'
+    allows_multiple_answers: true
+files: {  }
diff --git a/tests/__snapshots__/TeletraphPollPayloadTest__it_can_create_a_poll__1.yml b/tests/__snapshots__/TeletraphPollPayloadTest__it_can_create_a_poll__1.yml
new file mode 100644
index 00000000..1022348a
--- /dev/null
+++ b/tests/__snapshots__/TeletraphPollPayloadTest__it_can_create_a_poll__1.yml
@@ -0,0 +1,6 @@
+url: 'https://api.telegram.org/bot3f3814e1-5836-3d77-904e-60f64b15df36/sendPoll'
+payload:
+    chat_id: '-123456789'
+    options: [bar, baz]
+    question: 'foo?'
+files: {  }
diff --git a/tests/__snapshots__/TeletraphPollPayloadTest__it_can_disable_anonymous_answers__1.yml b/tests/__snapshots__/TeletraphPollPayloadTest__it_can_disable_anonymous_answers__1.yml
new file mode 100644
index 00000000..40bc445b
--- /dev/null
+++ b/tests/__snapshots__/TeletraphPollPayloadTest__it_can_disable_anonymous_answers__1.yml
@@ -0,0 +1,7 @@
+url: 'https://api.telegram.org/bot3f3814e1-5836-3d77-904e-60f64b15df36/sendPoll'
+payload:
+    chat_id: '-123456789'
+    options: [bar, baz]
+    question: 'foo?'
+    is_anonymous: false
+files: {  }
diff --git a/tests/__snapshots__/TeletraphPollPayloadTest__it_can_set_poll_validity__1.yml b/tests/__snapshots__/TeletraphPollPayloadTest__it_can_set_poll_validity__1.yml
new file mode 100644
index 00000000..66e5ed68
--- /dev/null
+++ b/tests/__snapshots__/TeletraphPollPayloadTest__it_can_set_poll_validity__1.yml
@@ -0,0 +1,7 @@
+url: 'https://api.telegram.org/bot3f3814e1-5836-3d77-904e-60f64b15df36/sendPoll'
+payload:
+    chat_id: '-123456789'
+    options: [bar, baz]
+    question: 'foo?'
+    close_date: 1601682625
+files: {  }
diff --git a/tests/__snapshots__/TeletraphQuizPayloadTest__it_can_create_a_quiz__1.yml b/tests/__snapshots__/TeletraphQuizPayloadTest__it_can_create_a_quiz__1.yml
new file mode 100644
index 00000000..a468c193
--- /dev/null
+++ b/tests/__snapshots__/TeletraphQuizPayloadTest__it_can_create_a_quiz__1.yml
@@ -0,0 +1,8 @@
+url: 'https://api.telegram.org/bot3f3814e1-5836-3d77-904e-60f64b15df36/sendPoll'
+payload:
+    chat_id: '-123456789'
+    type: quiz
+    options: [bar, baz, qux]
+    question: 'foo?'
+    correct_option_id: 1
+files: {  }
diff --git a/tests/__snapshots__/TeletraphQuizPayloadTest__it_can_disable_anonymous_answers__1.yml b/tests/__snapshots__/TeletraphQuizPayloadTest__it_can_disable_anonymous_answers__1.yml
new file mode 100644
index 00000000..3c71b353
--- /dev/null
+++ b/tests/__snapshots__/TeletraphQuizPayloadTest__it_can_disable_anonymous_answers__1.yml
@@ -0,0 +1,8 @@
+url: 'https://api.telegram.org/bot3f3814e1-5836-3d77-904e-60f64b15df36/sendPoll'
+payload:
+    chat_id: '-123456789'
+    type: quiz
+    options: [bar, baz]
+    question: 'foo?'
+    is_anonymous: false
+files: {  }
diff --git a/tests/__snapshots__/TeletraphQuizPayloadTest__it_can_set_quiz_validity__1.yml b/tests/__snapshots__/TeletraphQuizPayloadTest__it_can_set_quiz_validity__1.yml
new file mode 100644
index 00000000..2acdd026
--- /dev/null
+++ b/tests/__snapshots__/TeletraphQuizPayloadTest__it_can_set_quiz_validity__1.yml
@@ -0,0 +1,8 @@
+url: 'https://api.telegram.org/bot3f3814e1-5836-3d77-904e-60f64b15df36/sendPoll'
+payload:
+    chat_id: '-123456789'
+    type: quiz
+    options: [bar, baz]
+    question: 'foo?'
+    close_date: 1601682625
+files: {  }