Asking a page endpoint for an invalid title should generate a 404 response, not a 403. Change-Id: Ic0e9c5a99a6561cfd10dd3725a059cda476e6563
389 lines
18 KiB
JavaScript
389 lines
18 KiB
JavaScript
'use strict';
|
||
|
||
const { action, assert, REST, utils } = require( 'api-testing' );
|
||
const url = require( 'url' );
|
||
|
||
// Parse a URL-ref, which may or may not contain a protocol and host.
|
||
// WHATWG URL currently doesn't support partial URLs, see https://github.com/whatwg/url/issues/531
|
||
function parseURL( ref ) {
|
||
const urlObj = new url.URL( ref, 'http://fake-host' );
|
||
const urlRec = {
|
||
protocol: urlObj.protocol,
|
||
host: urlObj.host,
|
||
hostname: urlObj.hostname,
|
||
port: urlObj.port,
|
||
pathname: urlObj.pathname,
|
||
search: urlObj.search,
|
||
hash: urlObj.hash
|
||
};
|
||
|
||
if ( urlRec.hostname === 'fake-host' ) {
|
||
urlRec.host = '';
|
||
urlRec.hostname = '';
|
||
urlRec.port = '';
|
||
}
|
||
|
||
return urlRec;
|
||
}
|
||
|
||
describe( 'Page Source', () => {
|
||
const page = utils.title( 'PageSource_' );
|
||
const pageWithSpaces = page.replace( '_', ' ' );
|
||
const variantPage = utils.title( 'PageSourceVariant' );
|
||
const fallbackVariantPage = 'MediaWiki:Tog-underline/kk-latn';
|
||
|
||
// Create a page (or "agepay") for the pig latin variant test.
|
||
const agepayHash = utils.title( '' ).replace( /\d/g, 'x' ).toLowerCase(); // only lower-case letters.
|
||
const agepay = 'Page' + agepayHash; // will not exist
|
||
const atinlayAgepay = 'Age' + agepayHash + 'pay'; // will exist
|
||
|
||
const redirectPage = utils.title( 'Redirect ' );
|
||
const redirectedPage = redirectPage.replace( 'Redirect', 'Redirected' );
|
||
|
||
const client = new REST();
|
||
const anon = action.getAnon();
|
||
let mindy;
|
||
const baseEditText = "''Edit 1'' and '''Edit 2'''";
|
||
|
||
before( async () => {
|
||
mindy = await action.mindy();
|
||
await anon.edit( page, { text: baseEditText } );
|
||
await anon.edit( atinlayAgepay, { text: baseEditText } );
|
||
|
||
// Setup page with redirects
|
||
await anon.edit( redirectPage, { text: `Original name is ${redirectPage}` } );
|
||
const token = await mindy.token();
|
||
await mindy.action( 'move', {
|
||
from: redirectPage,
|
||
to: redirectedPage,
|
||
token
|
||
}, true );
|
||
} );
|
||
|
||
describe( 'GET /page/{title}', () => {
|
||
it( 'Title normalization should return permanent redirect (301)', async () => {
|
||
const redirectDbKey = utils.dbkey( redirectPage );
|
||
const { status, text, headers } = await client.get( `/page/${redirectPage}`, { flavor: 'edit' } );
|
||
const { host, search, pathname } = parseURL( headers.location );
|
||
assert.include( search, 'flavor=edit' );
|
||
assert.deepEqual( host, '' );
|
||
assert.include( pathname, `/page/${redirectDbKey}` );
|
||
assert.deepEqual( status, 301, text );
|
||
} );
|
||
|
||
it( 'When a wiki redirect exists, it should be present in the body response', async () => {
|
||
const redirectPageDbkey = utils.dbkey( redirectPage );
|
||
const redirectedPageDbKey = utils.dbkey( redirectedPage );
|
||
const { status, body: { redirect_target }, text } = await client.get( `/page/${redirectPageDbkey}` );
|
||
assert.deepEqual( status, 200, text );
|
||
assert.match( redirect_target, new RegExp( `/page/${encodeURIComponent( redirectedPageDbKey )}$` ) );
|
||
} );
|
||
|
||
it( 'Should successfully return page source and metadata for Wikitext page', async () => {
|
||
const { status, body, text } = await client.get( `/page/${page}` );
|
||
assert.deepEqual( status, 200, text );
|
||
assert.containsAllKeys( body, [ 'latest', 'id', 'key', 'license', 'title', 'content_model', 'source' ] );
|
||
assert.nestedPropertyVal( body, 'content_model', 'wikitext' );
|
||
assert.nestedPropertyVal( body, 'title', pageWithSpaces );
|
||
assert.nestedPropertyVal( body, 'key', utils.dbkey( page ) );
|
||
assert.nestedPropertyVal( body, 'source', baseEditText );
|
||
} );
|
||
it( 'Should return 404 error for non-existent page', async () => {
|
||
const dummyPageTitle = utils.title( 'DummyPage_' );
|
||
const { status } = await client.get( `/page/${dummyPageTitle}` );
|
||
assert.deepEqual( status, 404 );
|
||
} );
|
||
it( 'Should return 404 error for invalid titles', async () => {
|
||
const badTitle = '::X::';
|
||
const { status } = await client.get( `/page/${badTitle}` );
|
||
assert.deepEqual( status, 404 );
|
||
} );
|
||
it( 'Should return 404 error for special pages', async () => {
|
||
const badTitle = 'Special:Blankpage';
|
||
const { status } = await client.get( `/page/${badTitle}` );
|
||
assert.deepEqual( status, 404 );
|
||
} );
|
||
it( 'Should have appropriate response headers', async () => {
|
||
const preEditResponse = await client.get( `/page/${page}` );
|
||
const preEditDate = new Date( preEditResponse.body.latest.timestamp );
|
||
const preEditEtag = preEditResponse.headers.etag;
|
||
|
||
await anon.edit( page, { text: "'''Edit 3'''" } );
|
||
const postEditResponse = await client.get( `/page/${page}` );
|
||
const postEditDate = new Date( postEditResponse.body.latest.timestamp );
|
||
const postEditHeaders = postEditResponse.headers;
|
||
const postEditEtag = postEditResponse.headers.etag;
|
||
|
||
assert.containsAllKeys( postEditHeaders, [ 'etag' ] );
|
||
assert.deepEqual( postEditHeaders[ 'last-modified' ], postEditDate.toGMTString() );
|
||
assert.match( postEditHeaders[ 'cache-control' ], /^max-age=\d/ );
|
||
assert.strictEqual( isNaN( preEditDate.getTime() ), false );
|
||
assert.strictEqual( isNaN( postEditDate.getTime() ), false );
|
||
assert.notEqual( preEditDate, postEditDate );
|
||
assert.notEqual( preEditEtag, postEditEtag );
|
||
} );
|
||
} );
|
||
|
||
describe( 'GET /page/{title}/bare', () => {
|
||
it( 'Title normalization should return permanent redirect (301)', async () => {
|
||
const { status, text, headers } = await client.get( `/page/${redirectPage}/bare`, { flavor: 'edit' } );
|
||
const { search } = parseURL( headers.location );
|
||
assert.include( search, 'flavor=edit' );
|
||
assert.deepEqual( status, 301, text );
|
||
} );
|
||
|
||
it( 'When a wiki redirect exists, it should be present in the body response', async () => {
|
||
const redirectPageDbkey = utils.dbkey( redirectPage );
|
||
const redirectedPageDbKey = utils.dbkey( redirectedPage );
|
||
const { status, body: { redirect_target }, text } = await client.get( `/page/${redirectPageDbkey}/bare` );
|
||
assert.deepEqual( status, 200, text );
|
||
assert.match( redirect_target, new RegExp( `/page/${encodeURIComponent( redirectedPageDbKey )}/bare$` ) );
|
||
} );
|
||
|
||
it( 'Should successfully return page bare', async () => {
|
||
const { status, body, text } = await client.get( `/page/${page}/bare` );
|
||
assert.deepEqual( status, 200, text );
|
||
assert.containsAllKeys( body, [ 'latest', 'id', 'key', 'license', 'title', 'content_model', 'html_url' ] );
|
||
assert.nestedPropertyVal( body, 'content_model', 'wikitext' );
|
||
assert.nestedPropertyVal( body, 'title', pageWithSpaces );
|
||
assert.nestedPropertyVal( body, 'key', utils.dbkey( page ) );
|
||
assert.match( body.html_url, new RegExp( `/page/${encodeURIComponent( pageWithSpaces )}/html$` ) );
|
||
} );
|
||
it( 'Should return 404 error for non-existent page, even if a variant exists', async () => {
|
||
const agepayDbkey = utils.dbkey( agepay );
|
||
const { status } = await client.get( `/page/${agepayDbkey}/bare` );
|
||
assert.deepEqual( status, 404 );
|
||
} );
|
||
it( 'Should have appropriate response headers', async () => {
|
||
const preEditResponse = await client.get( `/page/${page}/bare` );
|
||
const preEditDate = new Date( preEditResponse.body.latest.timestamp );
|
||
const preEditEtag = preEditResponse.headers.etag;
|
||
|
||
await anon.edit( page, { text: "'''Edit 4'''" } );
|
||
const postEditResponse = await client.get( `/page/${page}/bare` );
|
||
const postEditDate = new Date( postEditResponse.body.latest.timestamp );
|
||
const postEditHeaders = postEditResponse.headers;
|
||
const postEditEtag = postEditResponse.headers.etag;
|
||
|
||
assert.containsAllKeys( postEditHeaders, [ 'etag' ] );
|
||
assert.deepEqual( postEditHeaders[ 'last-modified' ], postEditDate.toGMTString() );
|
||
assert.match( postEditHeaders[ 'cache-control' ], /^max-age=\d/ );
|
||
assert.strictEqual( isNaN( preEditDate.getTime() ), false );
|
||
assert.strictEqual( isNaN( postEditDate.getTime() ), false );
|
||
assert.notEqual( preEditDate, postEditDate );
|
||
assert.notEqual( preEditEtag, postEditEtag );
|
||
} );
|
||
} );
|
||
|
||
describe( 'GET /page/{title}/html', () => {
|
||
it( 'Title normalization should return permanent redirect (301)', async () => {
|
||
const { status, text, headers } = await client.get( `/page/${redirectPage}/html`, { flavor: 'edit' } );
|
||
const { search } = parseURL( headers.location );
|
||
assert.include( search, 'flavor=edit' );
|
||
assert.deepEqual( status, 301, text );
|
||
} );
|
||
|
||
it( 'Wiki redirects should return temporary redirect (307)', async () => {
|
||
const redirectPageDbkey = utils.dbkey( redirectPage );
|
||
const redirectedPageDbkey = utils.dbkey( redirectedPage );
|
||
const { status, text, headers } = await client.get( `/page/${redirectPageDbkey}/html`, { flavor: 'edit' } );
|
||
const { host, pathname, search } = parseURL( headers.location );
|
||
assert.include( search, 'flavor=edit' );
|
||
assert.include( pathname, `/page/${redirectedPageDbkey}` );
|
||
assert.deepEqual( host, '' );
|
||
assert.deepEqual( status, 307, text );
|
||
} );
|
||
|
||
it( 'Variant redirects should return temporary redirect (307)', async () => {
|
||
const agepayDbkey = utils.dbkey( agepay );
|
||
const atinlayAgepayDbkey = utils.dbkey( atinlayAgepay );
|
||
const { status, text, headers } = await client.get( `/page/${agepayDbkey}/html` );
|
||
assert.deepEqual( status, 307, text );
|
||
assert.include( headers.location, atinlayAgepayDbkey );
|
||
} );
|
||
|
||
it( 'Bypass wiki redirects with query param redirect=no', async () => {
|
||
const redirectPageDbkey = utils.dbkey( redirectPage );
|
||
const { status, text } = await client.get( `/page/${redirectPageDbkey}/html`, { redirect: 'no' } );
|
||
assert.deepEqual( status, 200, text );
|
||
} );
|
||
|
||
it( 'Bypass variant redirects with query param redirect=no', async () => {
|
||
const agepayDbkey = utils.dbkey( agepay );
|
||
const { status } = await client.get( `/page/${agepayDbkey}/html`, { redirect: 'no' } );
|
||
assert.deepEqual( status, 404 );
|
||
} );
|
||
|
||
it( 'Should successfully return page HTML', async () => {
|
||
const { status, headers, text } = await client.get( `/page/${page}/html` );
|
||
assert.deepEqual( status, 200, text );
|
||
assert.match( headers[ 'content-type' ], /^text\/html/ );
|
||
assert.match( text, /<html\b/ );
|
||
assert.match( text, /Edit \w+<\/b>/ );
|
||
} );
|
||
it( 'Should successfully return page HTML for a system message', async () => {
|
||
const msg = 'MediaWiki:Newpage-desc';
|
||
const { status, headers, text } = await client.get( `/page/${msg}/html` );
|
||
assert.deepEqual( status, 200, text );
|
||
assert.match( headers[ 'content-type' ], /^text\/html/ );
|
||
assert.match( text, /<html\b/ );
|
||
assert.match( text, /Start a new page/ );
|
||
} );
|
||
it( 'Should return 404 error for non-existent page', async () => {
|
||
const dummyPageTitle = utils.title( 'DummyPage_' );
|
||
const { status } = await client.get( `/page/${dummyPageTitle}/html` );
|
||
assert.deepEqual( status, 404 );
|
||
} );
|
||
it( 'Should have appropriate response headers', async () => {
|
||
const preEditResponse = await client.get( `/page/${page}/html` );
|
||
const preEditDate = new Date( preEditResponse.headers[ 'last-modified' ] );
|
||
const preEditEtag = preEditResponse.headers.etag;
|
||
|
||
await anon.edit( page, { text: "'''Edit XYZ'''" } );
|
||
const postEditResponse = await client.get( `/page/${page}/html` );
|
||
const postEditDate = new Date( postEditResponse.headers[ 'last-modified' ] );
|
||
const postEditHeaders = postEditResponse.headers;
|
||
const postEditEtag = postEditResponse.headers.etag;
|
||
|
||
assert.containsAllKeys( postEditHeaders, [ 'etag', 'cache-control', 'last-modified' ] );
|
||
assert.match( postEditHeaders[ 'cache-control' ], /^max-age=\d/ );
|
||
assert.strictEqual( isNaN( preEditDate.getTime() ), false );
|
||
assert.strictEqual( isNaN( postEditDate.getTime() ), false );
|
||
assert.notEqual( preEditDate, postEditDate );
|
||
assert.notEqual( preEditEtag, postEditEtag );
|
||
assert.match( postEditHeaders.etag, /^".*"$/, 'ETag must be present and not marked weak' );
|
||
} );
|
||
it( 'Should perform variant conversion', async () => {
|
||
await anon.edit( variantPage, { text: '<p>test language conversion</p>' } );
|
||
const { headers, text } = await client.get( `/page/${variantPage}/html`, null, {
|
||
'accept-language': 'en-x-piglatin'
|
||
} );
|
||
|
||
assert.match( text, /esttay anguagelay onversioncay/ );
|
||
assert.match( headers.vary, /\bAccept-Language\b/i );
|
||
assert.match( headers[ 'content-language' ], /en-x-piglatin/i );
|
||
assert.match( headers.etag, /en-x-piglatin/i );
|
||
} );
|
||
it( 'Should perform fallback variant conversion', async () => {
|
||
await mindy.edit( fallbackVariantPage, { text: 'Siltemeniñ astın sız:' } );
|
||
const { headers, text } = await client.get( `/page/${encodeURIComponent( fallbackVariantPage )}/html`, null, {
|
||
'accept-language': 'kk-cyrl'
|
||
} );
|
||
|
||
assert.match( text, /Сілтеменің астын сыз:/ );
|
||
assert.match( headers.vary, /\bAccept-Language\b/i );
|
||
assert.match( headers[ 'content-language' ], /kk-cyrl/i );
|
||
assert.match( headers.etag, /kk-cyrl/i );
|
||
} );
|
||
} );
|
||
|
||
describe( 'GET /page/{title}/with_html', () => {
|
||
it( 'Title normalization should return permanent redirect (301)', async () => {
|
||
const { status, text, headers } = await client.get( `/page/${redirectPage}/with_html`, { flavor: 'edit' } );
|
||
const { search } = parseURL( headers.location );
|
||
assert.include( search, 'flavor=edit' );
|
||
assert.deepEqual( status, 301, text );
|
||
} );
|
||
|
||
it( 'Wiki redirects should return temporary redirect (307)', async () => {
|
||
const redirectPageDbkey = utils.dbkey( redirectPage );
|
||
const { status, text, headers } = await client.get( `/page/${redirectPageDbkey}/with_html`, { flavor: 'edit' } );
|
||
const { search } = parseURL( headers.location );
|
||
assert.include( search, 'flavor=edit' );
|
||
assert.deepEqual( status, 307, text );
|
||
} );
|
||
|
||
it( 'Bypass redirects with query param redirect=no', async () => {
|
||
const redirectPageDbkey = utils.dbkey( redirectPage );
|
||
const redirectedPageDbKey = utils.dbkey( redirectedPage );
|
||
const { status, body: { redirect_target }, text } = await client.get( `/page/${redirectPageDbkey}/with_html`, { redirect: 'no' } );
|
||
assert.match( redirect_target, new RegExp( `/page/${encodeURIComponent( redirectedPageDbKey )}/with_html` ) );
|
||
assert.deepEqual( status, 200, text );
|
||
} );
|
||
|
||
it( 'Should successfully return page HTML and metadata for Wikitext page', async () => {
|
||
const { status, body, text } = await client.get( `/page/${page}/with_html` );
|
||
assert.deepEqual( status, 200, text );
|
||
assert.containsAllKeys( body, [ 'latest', 'id', 'key', 'license', 'title', 'content_model', 'html' ] );
|
||
assert.nestedPropertyVal( body, 'content_model', 'wikitext' );
|
||
assert.nestedPropertyVal( body, 'title', pageWithSpaces );
|
||
assert.nestedPropertyVal( body, 'key', utils.dbkey( page ) );
|
||
assert.match( body.html, /<html\b/ );
|
||
assert.match( body.html, /Edit \w+<\/b>/ );
|
||
} );
|
||
it( 'Should successfully return page HTML and metadata for a system message', async () => {
|
||
const msg = 'MediaWiki:Newpage-desc';
|
||
const { status, body, text } = await client.get( `/page/${msg}/with_html` );
|
||
assert.deepEqual( status, 200, text );
|
||
assert.containsAllKeys( body, [ 'latest', 'id', 'key', 'license', 'title', 'content_model', 'html' ] );
|
||
assert.nestedPropertyVal( body, 'content_model', 'wikitext' );
|
||
assert.nestedPropertyVal( body, 'title', msg );
|
||
assert.nestedPropertyVal( body, 'key', utils.dbkey( msg ) );
|
||
assert.nestedPropertyVal( body, 'id', 0 );
|
||
assert.nestedPropertyVal( body.latest, 'id', 0 );
|
||
assert.match( body.html, /<html\b/ );
|
||
assert.match( body.html, /Start a new page/ );
|
||
} );
|
||
it( 'Should return 404 error for non-existent page', async () => {
|
||
const dummyPageTitle = utils.title( 'DummyPage_' );
|
||
const { status } = await client.get( `/page/${dummyPageTitle}/with_html` );
|
||
assert.deepEqual( status, 404 );
|
||
} );
|
||
it( 'Should have appropriate response headers', async () => {
|
||
const preEditResponse = await client.get( `/page/${page}/with_html` );
|
||
const preEditRevDate = new Date( preEditResponse.body.latest.timestamp );
|
||
const preEditEtag = preEditResponse.headers.etag;
|
||
|
||
await anon.edit( page, { text: "'''Edit ABCD'''" } );
|
||
const postEditResponse = await client.get( `/page/${page}/with_html` );
|
||
const postEditRevDate = new Date( postEditResponse.body.latest.timestamp );
|
||
const postEditHeaders = postEditResponse.headers;
|
||
const postEditEtag = postEditResponse.headers.etag;
|
||
|
||
assert.containsAllKeys( postEditHeaders, [ 'etag', 'last-modified' ] );
|
||
const postEditHeaderDate = new Date( postEditHeaders[ 'last-modified' ] );
|
||
|
||
// The last-modified date is the render timestamp, which may be newer than the revision
|
||
assert.strictEqual( postEditRevDate.valueOf() <= postEditHeaderDate.valueOf(), true );
|
||
assert.match( postEditHeaders[ 'cache-control' ], /^max-age=\d/ );
|
||
assert.strictEqual( isNaN( preEditRevDate.getTime() ), false );
|
||
assert.strictEqual( isNaN( postEditRevDate.getTime() ), false );
|
||
assert.notEqual( preEditRevDate, postEditRevDate );
|
||
assert.notEqual( preEditEtag, postEditEtag );
|
||
} );
|
||
it( 'Should perform variant conversion', async () => {
|
||
await anon.edit( variantPage, { text: '<p>test language conversion</p>' } );
|
||
const { headers, text } = await client.get( `/page/${variantPage}/html`, null, {
|
||
'accept-language': 'en-x-piglatin'
|
||
} );
|
||
|
||
assert.match( text, /esttay anguagelay onversioncay/ );
|
||
assert.match( headers.vary, /\bAccept-Language\b/i );
|
||
assert.match( headers.etag, /en-x-piglatin/i );
|
||
|
||
// Since with_html returns JSON, content language is not set
|
||
// but if its set, we expect it to be set correctly.
|
||
const contentLanguageHeader = headers[ 'content-language' ];
|
||
if ( contentLanguageHeader ) {
|
||
assert.match( headers[ 'content-language' ], /en-x-piglatin/i );
|
||
}
|
||
} );
|
||
it( 'Should perform fallback variant conversion', async () => {
|
||
await mindy.edit( fallbackVariantPage, { text: 'Siltemeniñ astın sız:' } );
|
||
const { headers, text } = await client.get( `/page/${encodeURIComponent( fallbackVariantPage )}/html`, null, {
|
||
'accept-language': 'kk-cyrl'
|
||
} );
|
||
|
||
assert.match( text, /Сілтеменің астын сыз:/ );
|
||
assert.match( headers.vary, /\bAccept-Language\b/i );
|
||
assert.match( headers.etag, /kk-cyrl/i );
|
||
|
||
// Since with_html returns JSON, content language is not set
|
||
// but if its set, we expect it to be set correctly.
|
||
const contentLanguageHeader = headers[ 'content-language' ];
|
||
if ( contentLanguageHeader ) {
|
||
assert.match( headers[ 'content-language' ], /kk-cyrl/i );
|
||
}
|
||
} );
|
||
} );
|
||
} );
|