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