1<?php 2 3/** 4 * @covers PageDataRequestHandler 5 * @group PageData 6 */ 7class PageDataRequestHandlerTest extends \MediaWikiLangTestCase { 8 9 /** 10 * @var Title 11 */ 12 private $interfaceTitle; 13 14 /** 15 * @var int 16 */ 17 private $obLevel; 18 19 protected function setUp(): void { 20 parent::setUp(); 21 22 $this->interfaceTitle = Title::newFromText( __CLASS__ ); 23 $this->obLevel = ob_get_level(); 24 25 $this->setMwGlobals( 'wgArticlePath', '/wiki/$1' ); 26 } 27 28 protected function tearDown(): void { 29 $obLevel = ob_get_level(); 30 31 while ( ob_get_level() > $this->obLevel ) { 32 ob_end_clean(); 33 } 34 35 if ( $obLevel !== $this->obLevel ) { 36 $this->fail( "Test changed output buffer level: was {$this->obLevel}" . 37 "before test, but $obLevel after test." 38 ); 39 } 40 41 parent::tearDown(); 42 } 43 44 /** 45 * @return PageDataRequestHandler 46 */ 47 protected function newHandler() { 48 return new PageDataRequestHandler(); 49 } 50 51 /** 52 * @param array $params 53 * @param string[] $headers 54 * 55 * @return OutputPage 56 */ 57 protected function makeOutputPage( array $params, array $headers ) { 58 // construct request 59 $request = new FauxRequest( $params ); 60 $request->response()->header( 'Status: 200 OK', true, 200 ); // init/reset 61 62 foreach ( $headers as $name => $value ) { 63 $request->setHeader( strtoupper( $name ), $value ); 64 } 65 66 // construct Context and OutputPage 67 $context = new DerivativeContext( RequestContext::getMain() ); 68 $context->setRequest( $request ); 69 70 $output = new OutputPage( $context ); 71 $output->setTitle( $this->interfaceTitle ); 72 $context->setOutput( $output ); 73 74 return $output; 75 } 76 77 public function handleRequestProvider() { 78 $cases = []; 79 80 $cases[] = [ '', [], [], 'Invalid title', 400 ]; 81 82 $cases[] = [ 83 '', 84 [ 'target' => 'Helsinki' ], 85 [], 86 '', 87 303, 88 [ 'Location' => '?title=Helsinki&action=raw' ] 89 ]; 90 91 $subpageCases = []; 92 foreach ( $cases as $c ) { 93 $case = $c; 94 $case[0] = 'main/'; 95 96 if ( isset( $case[1]['target'] ) ) { 97 $case[0] .= $case[1]['target']; 98 unset( $case[1]['target'] ); 99 } 100 101 $subpageCases[] = $case; 102 } 103 104 $cases = array_merge( $cases, $subpageCases ); 105 106 $cases[] = [ 107 '', 108 [ 'target' => 'Helsinki' ], 109 [ 'Accept' => 'text/HTML' ], 110 '', 111 303, 112 [ 'Location' => '/wiki/Helsinki' ] 113 ]; 114 115 $cases[] = [ 116 '', 117 [ 118 'target' => 'Helsinki', 119 'revision' => '4242', 120 ], 121 [ 'Accept' => 'text/HTML' ], 122 '', 123 303, 124 [ 'Location' => '?title=Helsinki&oldid=4242' ] 125 ]; 126 127 $cases[] = [ 128 '/Helsinki', 129 [], 130 [], 131 '', 132 303, 133 [ 'Location' => '?title=Helsinki&action=raw' ] 134 ]; 135 136 // #31: /Q5 with "Accept: text/foobar" triggers a 406 137 $cases[] = [ 138 'main/Helsinki', 139 [], 140 [ 'Accept' => 'text/foobar' ], 141 'No matching format found', 142 406, 143 ]; 144 145 $cases[] = [ 146 'no slash', 147 [], 148 [ 'Accept' => 'text/HTML' ], 149 'Invalid title', 150 400, 151 ]; 152 153 $cases[] = [ 154 'main', 155 [], 156 [ 'Accept' => 'text/HTML' ], 157 'Invalid title', 158 400, 159 ]; 160 161 $cases[] = [ 162 'xyz/Helsinki', 163 [], 164 [ 'Accept' => 'text/HTML' ], 165 'Invalid title', 166 400, 167 ]; 168 169 $cases[] = [ 170 'main/Helsinki', 171 [], 172 [ 'Accept' => 'text/HTML' ], 173 '', 174 303, 175 [ 'Location' => '/wiki/Helsinki' ] 176 ]; 177 178 $cases[] = [ 179 '/Helsinki', 180 [], 181 [ 'Accept' => 'text/HTML' ], 182 '', 183 303, 184 [ 'Location' => '/wiki/Helsinki' ] 185 ]; 186 187 $cases[] = [ 188 'main/AC/DC', 189 [], 190 [ 'Accept' => 'text/HTML' ], 191 '', 192 303, 193 [ 'Location' => '/wiki/AC/DC' ] 194 ]; 195 196 return $cases; 197 } 198 199 /** 200 * @dataProvider handleRequestProvider 201 * 202 * @param string $subpage The subpage to request (or '') 203 * @param array $params Request parameters 204 * @param array $headers Request headers 205 * @param string $expectedOutput 206 * @param int $expectedStatusCode Expected HTTP status code. 207 * @param string[] $expectedHeaders Expected HTTP response headers. 208 */ 209 public function testHandleRequest( 210 $subpage, 211 array $params, 212 array $headers, 213 $expectedOutput = '', 214 $expectedStatusCode = 200, 215 array $expectedHeaders = [] 216 ) { 217 $output = $this->makeOutputPage( $params, $headers ); 218 $request = $output->getRequest(); 219 220 /** @var FauxResponse $response */ 221 $response = $request->response(); 222 223 // construct handler 224 $handler = $this->newHandler(); 225 226 try { 227 ob_start(); 228 $handler->handleRequest( $subpage, $request, $output ); 229 230 if ( $output->getRedirect() !== '' ) { 231 // hack to apply redirect to web response 232 $output->output(); 233 } 234 235 $text = ob_get_clean(); 236 237 $this->assertEquals( $expectedStatusCode, $response->getStatusCode(), 'status code' ); 238 $this->assertSame( $expectedOutput, $text, 'output' ); 239 240 foreach ( $expectedHeaders as $name => $exp ) { 241 $value = $response->getHeader( $name ); 242 $this->assertNotNull( $value, "header: $name" ); 243 $this->assertIsString( $value, "header: $name" ); 244 $this->assertStringEndsWith( $exp, $value, "header: $name" ); 245 } 246 } catch ( HttpError $e ) { 247 ob_end_clean(); 248 $this->assertEquals( $expectedStatusCode, $e->getStatusCode(), 'status code' ); 249 $this->assertStringContainsString( $expectedOutput, $e->getHTML(), 'error output' ); 250 } 251 252 // We always set "Access-Control-Allow-Origin: *" 253 $this->assertSame( '*', $response->getHeader( 'Access-Control-Allow-Origin' ) ); 254 } 255 256 public function provideHttpContentNegotiation() { 257 $helsinki = Title::newFromText( 'Helsinki' ); 258 return [ 259 'Accept Header of HTML' => [ 260 $helsinki, 261 [ 'ACCEPT' => 'text/html' ], // headers 262 'Helsinki' 263 ], 264 'Accept Header without weights' => [ 265 $helsinki, 266 [ 'ACCEPT' => '*/*, text/html, text/x-wiki' ], 267 'Helsinki&action=raw' 268 ], 269 'Accept Header with weights' => [ 270 $helsinki, 271 [ 'ACCEPT' => 'text/*; q=0.5, text/json; q=0.7, application/rdf+xml; q=0.8' ], 272 'Helsinki&action=raw' 273 ], 274 'Accept Header accepting evertyhing and HTML' => [ 275 $helsinki, 276 [ 'ACCEPT' => 'text/html, */*' ], 277 'Helsinki&action=raw' 278 ], 279 'No Accept Header' => [ 280 $helsinki, 281 [], 282 'Helsinki&action=raw' 283 ], 284 ]; 285 } 286 287 /** 288 * @dataProvider provideHttpContentNegotiation 289 * 290 * @param Title $title 291 * @param array $headers Request headers 292 * @param string $expectedRedirectSuffix Expected suffix of the HTTP Location header. 293 */ 294 public function testHttpContentNegotiation( 295 Title $title, 296 array $headers, 297 $expectedRedirectSuffix 298 ) { 299 /** @var FauxResponse $response */ 300 $output = $this->makeOutputPage( [], $headers ); 301 $request = $output->getRequest(); 302 303 $handler = $this->newHandler(); 304 $handler->httpContentNegotiation( $request, $output, $title ); 305 306 $this->assertStringEndsWith( 307 $expectedRedirectSuffix, 308 $output->getRedirect(), 309 'redirect target' 310 ); 311 } 312} 313