wiki.techinc.nl/tests/api-testing/REST/Page.js
Kosta Harlan e5ea321269
api-testing/REST/Page.js: Use authenticated user for edits
Why:

- There's nothing in these tests making assertions about anonymous
  editing behavior, and using the anon user is problematic with temp
  accounts enabled, because after the initial edit, the user is
  logged-in, and subsequent edits need a new token.

What:

- Switch the actor used in making edits for the fixtures in the tests to
  be a named user

Bug: T365647
Change-Id: If1d3cdc46c894242b2c335e3e8bf25063ea5d8b5
2024-07-10 23:06:32 +02:00

412 lines
19 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/sh-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();
let mindy;
const baseEditText = "''Edit 1'' and '''Edit 2'''";
before( async () => {
mindy = await action.mindy();
await mindy.edit( page, { text: baseEditText } );
await mindy.edit( atinlayAgepay, { text: baseEditText } );
// Setup page with redirects
await mindy.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, headers } =
await client.get( `/page/${ redirectPageDbkey }` );
assert.deepEqual( status, 200, text );
assert.match( headers[ 'content-type' ], /^application\/json/ );
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 mindy.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, headers } =
await client.get( `/page/${ redirectPageDbkey }/bare` );
assert.deepEqual( status, 200, text );
assert.match( headers[ 'content-type' ], /^application\/json/ );
assert.match( redirect_target, new RegExp( `/page/${ encodeURIComponent( redirectedPageDbKey ) }/bare$` ) );
} );
it( 'Should successfully return page bare', async () => {
const { status, body, text, headers } = await client.get( `/page/${ page }/bare` );
assert.deepEqual( status, 200, text );
assert.match( headers[ 'content-type' ], /^application\/json/ );
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 mindy.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, headers } = await client.get(
`/page/${ redirectPageDbkey }/html`,
{ redirect: 'no' }
);
assert.deepEqual( status, 200, text );
assert.match( headers[ 'content-type' ], /^text\/html/ );
} );
it( 'Bypass variant redirects with query param redirect=no', async () => {
const agepayDbkey = utils.dbkey( agepay );
const { status, headers } = await client.get(
`/page/${ agepayDbkey }/html`,
{ redirect: 'no' }
);
assert.deepEqual( status, 404 );
// rest-nonexistent-title error object returned instead of HTML
assert.match( headers[ 'content-type' ], /^application\/json/ );
} );
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 mindy.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 mindy.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[ 'content-type' ], /^text\/html/ );
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: 'Podvlačenje linkova:' } );
const { headers, text } = await client.get( `/page/${ encodeURIComponent( fallbackVariantPage ) }/html`, null, {
'accept-language': 'sh-cyrl'
} );
assert.match( text, /Подвлачење линкова:/ );
assert.match( headers[ 'content-type' ], /^text\/html/ );
assert.match( headers.vary, /\bAccept-Language\b/i );
assert.match( headers[ 'content-language' ], /sh-cyrl/i );
assert.match( headers.etag, /sh-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, headers } = 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 );
assert.match( headers[ 'content-type' ], /^application\/json/ );
} );
it( 'Should successfully return page HTML and metadata for Wikitext page', async () => {
const { status, body, text, headers } = await client.get( `/page/${ page }/with_html` );
assert.deepEqual( status, 200, text );
assert.match( headers[ 'content-type' ], /^application\/json/ );
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, headers } = await client.get( `/page/${ msg }/with_html` );
assert.deepEqual( status, 200, text );
assert.match( headers[ 'content-type' ], /^application\/json/ );
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 mindy.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 mindy.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[ 'content-type' ], /^text\/html/ );
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: 'Podvlačenje linkova:' } );
const { headers, text } = await client.get( `/page/${ encodeURIComponent( fallbackVariantPage ) }/html`, null, {
'accept-language': 'sh-cyrl'
} );
assert.match( text, /Подвлачење линкова:/ );
assert.match( headers[ 'content-type' ], /^text\/html/ );
assert.match( headers.vary, /\bAccept-Language\b/i );
assert.match( headers.etag, /sh-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' ], /sh-cyrl/i );
}
} );
} );
} );