packages/requirements/tests/Unit/Source/DownloadTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Source;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\DataProvider;
9use PHPUnit\Framework\Attributes\Small;
10use PHPUnit\Framework\TestCase;
11use Requirements\Source\Download;
12use RuntimeException;
13use Symfony\Component\HttpClient\MockHttpClient;
14use Symfony\Component\HttpClient\Response\MockResponse;
15use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
16use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
17
18#[CoversClass(Download::class)]
19#[Small]
20final class DownloadTest extends TestCase
21{
22    /**
23     * @throws ExceptionInterface
24     */
25    public function testGetReturnsTheBody(): void
26    {
27        $download = new Download(new MockHttpClient(new MockResponse(['Names shall ', 'start with a letter.'])));
28
29        self::assertSame('Names shall start with a letter.', $download->get('https://example.org/manual'));
30    }
31
32    /**
33     * @throws ExceptionInterface
34     */
35    public function testGetSendsABoundedRequest(): void
36    {
37        $response = new MockResponse('Source text.');
38        (new Download(new MockHttpClient($response)))->get('https://example.org/manual');
39        $options = $response->getRequestOptions();
40
41        self::assertSame('GET', $response->getRequestMethod());
42        self::assertSame('https://example.org/manual', $response->getRequestUrl());
43        self::assertEquals(20, $options['timeout']);
44        self::assertEquals(20, $options['max_duration']);
45        self::assertSame(5, $options['max_redirects']);
46        self::assertFalse($options['buffer']);
47        self::assertIsArray($options['headers']);
48        self::assertContains('User-Agent: requirements/1', $options['headers']);
49    }
50
51    /**
52     * @throws ExceptionInterface
53     */
54    #[DataProvider('providerGetSuccessfulStatus')]
55    public function testGetAcceptsSuccessfulStatus(int $status): void
56    {
57        self::assertSame('Source text.', (new Download(new MockHttpClient(new MockResponse('Source text.', ['http_code' => $status]))))->get('https://example.org/manual'));
58    }
59
60    /**
61     * @return array<string, array{int}>
62     */
63    public static function providerGetSuccessfulStatus(): array
64    {
65        return [
66            'ok' => [200],
67            'non-authoritative' => [203],
68            'last success' => [299],
69        ];
70    }
71
72    /**
73     * @throws ExceptionInterface
74     */
75    #[DataProvider('providerGetErrorStatus')]
76    public function testGetRejectsOtherStatus(int $status): void
77    {
78        $this->expectException(RuntimeException::class);
79        $this->expectExceptionMessage("HTTP $status");
80        (new Download(new MockHttpClient(new MockResponse('Names shall start with a letter.', ['http_code' => $status]))))->get('https://example.org/manual');
81    }
82
83    /**
84     * @return array<string, array{int}>
85     */
86    public static function providerGetErrorStatus(): array
87    {
88        return [
89            'redirect' => [300],
90            'not modified' => [304],
91            'not found' => [404],
92            'server error' => [500],
93        ];
94    }
95
96    /**
97     * @throws ExceptionInterface
98     */
99    public function testGetAcceptsTheSizeLimit(): void
100    {
101        self::assertSame(16777216, strlen((new Download(new MockHttpClient(new MockResponse(str_repeat('x', 16777216)))))->get('https://example.org/manual')));
102    }
103
104    /**
105     * @throws ExceptionInterface
106     */
107    public function testGetRejectsBodiesOverTheSizeLimit(): void
108    {
109        $this->expectException(RuntimeException::class);
110        $this->expectExceptionMessage('16 MiB size limit exceeded.');
111        (new Download(new MockHttpClient(new MockResponse([str_repeat('x', 16777216), 'x']))))->get('https://example.org/manual');
112    }
113
114    /**
115     * @throws ExceptionInterface
116     */
117    public function testGetPropagatesTransportFailures(): void
118    {
119        $this->expectException(TransportExceptionInterface::class);
120        (new Download(new MockHttpClient(new MockResponse('', ['error' => 'Connection refused.']))))->get('https://example.org/manual');
121    }
122
123    /**
124     * @throws ExceptionInterface
125     */
126    public function testGetUsesADefaultClient(): void
127    {
128        $this->expectException(TransportExceptionInterface::class);
129        (new Download())->get('http://127.0.0.1:1/manual');
130    }
131}
132