wiki.techinc.nl/tests/api-testing/REST/Page.js
daniel 51036da29e Page endpoints should return 404 for bad titles
Asking a page endpoint for an invalid title should generate a 404
response, not a 403.

Change-Id: Ic0e9c5a99a6561cfd10dd3725a059cda476e6563
2023-11-02 10:18:14 +01:00

389 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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 );
}
} );
} );
} );