Merge "maintenance: Script to rename titles for Unicode uppercasing changes"
[lhc/web/wiklou.git] / tests / integration / includes / http / MWHttpRequestTestCase.php
1 <?php
2
3 use MediaWiki\Http\HttpRequestFactory;
4 use MediaWiki\MediaWikiServices;
5 use Wikimedia\TestingAccessWrapper;
6
7 abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase {
8 protected static $httpEngine;
9 protected $oldHttpEngine;
10
11 /** @var HttpRequestFactory */
12 private $factory;
13
14 public function setUp() {
15 parent::setUp();
16 $this->oldHttpEngine = Http::$httpEngine;
17 Http::$httpEngine = static::$httpEngine;
18
19 $this->factory = MediaWikiServices::getInstance()->getHttpRequestFactory();
20
21 try {
22 $request = $factory->create( 'null:' );
23 } catch ( RuntimeException $e ) {
24 $this->markTestSkipped( static::$httpEngine . ' engine not supported' );
25 }
26
27 if ( static::$httpEngine === 'php' ) {
28 $this->assertInstanceOf( PhpHttpRequest::class, $request );
29 } else {
30 $this->assertInstanceOf( CurlHttpRequest::class, $request );
31 }
32 }
33
34 public function tearDown() {
35 parent::tearDown();
36 Http::$httpEngine = $this->oldHttpEngine;
37 }
38
39 // --------------------
40
41 public function testIsRedirect() {
42 $request = $this->factory->create( 'http://httpbin.org/get' );
43 $status = $request->execute();
44 $this->assertTrue( $status->isGood() );
45 $this->assertFalse( $request->isRedirect() );
46
47 $request = $this->factory->create( 'http://httpbin.org/redirect/1' );
48 $status = $request->execute();
49 $this->assertTrue( $status->isGood() );
50 $this->assertTrue( $request->isRedirect() );
51 }
52
53 public function testgetFinalUrl() {
54 $request = $this->factory->create( 'http://httpbin.org/redirect/3' );
55 if ( !$request->canFollowRedirects() ) {
56 $this->markTestSkipped( 'cannot follow redirects' );
57 }
58 $status = $request->execute();
59 $this->assertTrue( $status->isGood() );
60 $this->assertNotSame( 'http://httpbin.org/get', $request->getFinalUrl() );
61
62 $request = $this->factory->create( 'http://httpbin.org/redirect/3', [ 'followRedirects'
63 => true ] );
64 $status = $request->execute();
65 $this->assertTrue( $status->isGood() );
66 $this->assertSame( 'http://httpbin.org/get', $request->getFinalUrl() );
67 $this->assertResponseFieldValue( 'url', 'http://httpbin.org/get', $request );
68
69 $request = $this->factory->create( 'http://httpbin.org/redirect/3', [ 'followRedirects'
70 => true ] );
71 $status = $request->execute();
72 $this->assertTrue( $status->isGood() );
73 $this->assertSame( 'http://httpbin.org/get', $request->getFinalUrl() );
74 $this->assertResponseFieldValue( 'url', 'http://httpbin.org/get', $request );
75
76 if ( static::$httpEngine === 'curl' ) {
77 $this->markTestIncomplete( 'maxRedirects seems to be ignored by CurlHttpRequest' );
78 return;
79 }
80
81 $request = $this->factory->create( 'http://httpbin.org/redirect/3', [ 'followRedirects'
82 => true, 'maxRedirects' => 1 ] );
83 $status = $request->execute();
84 $this->assertTrue( $status->isGood() );
85 $this->assertNotSame( 'http://httpbin.org/get', $request->getFinalUrl() );
86 }
87
88 public function testSetCookie() {
89 $request = $this->factory->create( 'http://httpbin.org/cookies' );
90 $request->setCookie( 'foo', 'bar' );
91 $request->setCookie( 'foo2', 'bar2', [ 'domain' => 'example.com' ] );
92 $status = $request->execute();
93 $this->assertTrue( $status->isGood() );
94 $this->assertResponseFieldValue( 'cookies', [ 'foo' => 'bar' ], $request );
95 }
96
97 public function testSetCookieJar() {
98 $request = $this->factory->create( 'http://httpbin.org/cookies' );
99 $cookieJar = new CookieJar();
100 $cookieJar->setCookie( 'foo', 'bar', [ 'domain' => 'httpbin.org' ] );
101 $cookieJar->setCookie( 'foo2', 'bar2', [ 'domain' => 'example.com' ] );
102 $request->setCookieJar( $cookieJar );
103 $status = $request->execute();
104 $this->assertTrue( $status->isGood() );
105 $this->assertResponseFieldValue( 'cookies', [ 'foo' => 'bar' ], $request );
106
107 $request = $this->factory->create( 'http://httpbin.org/cookies/set?foo=bar' );
108 $cookieJar = new CookieJar();
109 $request->setCookieJar( $cookieJar );
110 $status = $request->execute();
111 $this->assertTrue( $status->isGood() );
112 $this->assertHasCookie( 'foo', 'bar', $request->getCookieJar() );
113
114 $this->markTestIncomplete( 'CookieJar does not handle deletion' );
115
116 // $request = $this->factory->create( 'http://httpbin.org/cookies/delete?foo' );
117 // $cookieJar = new CookieJar();
118 // $cookieJar->setCookie( 'foo', 'bar', [ 'domain' => 'httpbin.org' ] );
119 // $cookieJar->setCookie( 'foo2', 'bar2', [ 'domain' => 'httpbin.org' ] );
120 // $request->setCookieJar( $cookieJar );
121 // $status = $request->execute();
122 // $this->assertTrue( $status->isGood() );
123 // $this->assertNotHasCookie( 'foo', $request->getCookieJar() );
124 // $this->assertHasCookie( 'foo2', 'bar2', $request->getCookieJar() );
125 }
126
127 public function testGetResponseHeaders() {
128 $request = $this->factory->create( 'http://httpbin.org/response-headers?Foo=bar' );
129 $status = $request->execute();
130 $this->assertTrue( $status->isGood() );
131 $headers = array_change_key_case( $request->getResponseHeaders(), CASE_LOWER );
132 $this->assertArrayHasKey( 'foo', $headers );
133 $this->assertSame( $request->getResponseHeader( 'Foo' ), 'bar' );
134 }
135
136 public function testSetHeader() {
137 $request = $this->factory->create( 'http://httpbin.org/headers' );
138 $request->setHeader( 'Foo', 'bar' );
139 $status = $request->execute();
140 $this->assertTrue( $status->isGood() );
141 $this->assertResponseFieldValue( [ 'headers', 'Foo' ], 'bar', $request );
142 }
143
144 public function testGetStatus() {
145 $request = $this->factory->create( 'http://httpbin.org/status/418' );
146 $status = $request->execute();
147 $this->assertFalse( $status->isOK() );
148 $this->assertSame( $request->getStatus(), 418 );
149 }
150
151 public function testSetUserAgent() {
152 $request = $this->factory->create( 'http://httpbin.org/user-agent' );
153 $request->setUserAgent( 'foo' );
154 $status = $request->execute();
155 $this->assertTrue( $status->isGood() );
156 $this->assertResponseFieldValue( 'user-agent', 'foo', $request );
157 }
158
159 public function testSetData() {
160 $request = $this->factory->create( 'http://httpbin.org/post', [ 'method' => 'POST' ] );
161 $request->setData( [ 'foo' => 'bar', 'foo2' => 'bar2' ] );
162 $status = $request->execute();
163 $this->assertTrue( $status->isGood() );
164 $this->assertResponseFieldValue( 'form', [ 'foo' => 'bar', 'foo2' => 'bar2' ], $request );
165 }
166
167 public function testSetCallback() {
168 if ( static::$httpEngine === 'php' ) {
169 $this->markTestIncomplete( 'PhpHttpRequest does not use setCallback()' );
170 return;
171 }
172
173 $request = $this->factory->create( 'http://httpbin.org/ip' );
174 $data = '';
175 $request->setCallback( function ( $fh, $content ) use ( &$data ) {
176 $data .= $content;
177 return strlen( $content );
178 } );
179 $status = $request->execute();
180 $this->assertTrue( $status->isGood() );
181 $data = json_decode( $data, true );
182 $this->assertInternalType( 'array', $data );
183 $this->assertArrayHasKey( 'origin', $data );
184 }
185
186 public function testBasicAuthentication() {
187 $request = $this->factory->create( 'http://httpbin.org/basic-auth/user/pass', [
188 'username' => 'user',
189 'password' => 'pass',
190 ] );
191 $status = $request->execute();
192 $this->assertTrue( $status->isGood() );
193 $this->assertResponseFieldValue( 'authenticated', true, $request );
194
195 $request = $this->factory->create( 'http://httpbin.org/basic-auth/user/pass', [
196 'username' => 'user',
197 'password' => 'wrongpass',
198 ] );
199 $status = $request->execute();
200 $this->assertFalse( $status->isOK() );
201 $this->assertSame( 401, $request->getStatus() );
202 }
203
204 public function testFactoryDefaults() {
205 $request = $this->factory->create( 'http://acme.test' );
206 $this->assertInstanceOf( MWHttpRequest::class, $request );
207 }
208
209 // --------------------
210
211 /**
212 * Verifies that the request was successful, returned valid JSON and the given field of that
213 * JSON data is as expected.
214 * @param string|string[] $key Path to the data in the response object
215 * @param mixed $expectedValue
216 * @param MWHttpRequest $response
217 */
218 protected function assertResponseFieldValue( $key, $expectedValue, MWHttpRequest $response ) {
219 $this->assertSame( 200, $response->getStatus(), 'response status is not 200' );
220 $data = json_decode( $response->getContent(), true );
221 $this->assertInternalType( 'array', $data, 'response is not JSON' );
222 $keyPath = '';
223 foreach ( (array)$key as $keySegment ) {
224 $keyPath .= ( $keyPath ? '.' : '' ) . $keySegment;
225 $this->assertArrayHasKey( $keySegment, $data, $keyPath . ' not found' );
226 $data = $data[$keySegment];
227 }
228 $this->assertSame( $expectedValue, $data );
229 }
230
231 /**
232 * Asserts that the cookie jar has the given cookie with the given value.
233 * @param string $expectedName Cookie name
234 * @param string $expectedValue Cookie value
235 * @param CookieJar $cookieJar
236 */
237 protected function assertHasCookie( $expectedName, $expectedValue, CookieJar $cookieJar ) {
238 $cookieJar = TestingAccessWrapper::newFromObject( $cookieJar );
239 $cookies = array_change_key_case( $cookieJar->cookie, CASE_LOWER );
240 $this->assertArrayHasKey( strtolower( $expectedName ), $cookies );
241 $cookie = TestingAccessWrapper::newFromObject(
242 $cookies[strtolower( $expectedName )] );
243 $this->assertSame( $expectedValue, $cookie->value );
244 }
245
246 /**
247 * Asserts that the cookie jar does not have the given cookie.
248 * @param string $name Cookie name
249 * @param CookieJar $cookieJar
250 */
251 protected function assertNotHasCookie( $name, CookieJar $cookieJar ) {
252 $cookieJar = TestingAccessWrapper::newFromObject( $cookieJar );
253 $this->assertArrayNotHasKey( strtolower( $name ),
254 array_change_key_case( $cookieJar->cookie, CASE_LOWER ) );
255 }
256
257 public static function provideRelativeRedirects() {
258 return [
259 [
260 'location' => [ 'http://newsite/file.ext', '/newfile.ext' ],
261 'final' => 'http://newsite/newfile.ext',
262 'Relative file path Location: interpreted as full URL'
263 ],
264 [
265 'location' => [ 'https://oldsite/file.ext' ],
266 'final' => 'https://oldsite/file.ext',
267 'Location to the HTTPS version of the site'
268 ],
269 [
270 'location' => [
271 '/anotherfile.ext',
272 'http://anotherfile/hoster.ext',
273 'https://anotherfile/hoster.ext'
274 ],
275 'final' => 'https://anotherfile/hoster.ext',
276 'Relative file path Location: should keep the latest host and scheme!'
277 ],
278 [
279 'location' => [ '/anotherfile.ext' ],
280 'final' => 'http://oldsite/anotherfile.ext',
281 'Relative Location without domain '
282 ],
283 [
284 'location' => null,
285 'final' => 'http://oldsite/file.ext',
286 'No Location (no redirect) '
287 ],
288 ];
289 }
290
291 /**
292 * @dataProvider provideRelativeRedirects
293 * @covers MWHttpRequest::getFinalUrl
294 */
295 public function testRelativeRedirections( $location, $final, $message = null ) {
296 $h = $this->factory->create( 'http://oldsite/file.ext', [], __METHOD__ );
297 $h = TestingAccessWrapper::newFromObject( $h );
298
299 // Forge a Location header
300 $h->respHeaders['location'] = $location;
301
302 // Verify it correctly fixes the Location
303 $this->assertEquals( $final, $h->getFinalUrl(), $message );
304 }
305
306 }