wiki.techinc.nl/tests/api-testing/REST/Update.js
Chlod Alejandro e20ef88cb2
Add Content-Type header checks to REST API tests
Add extra checks on all REST API tests which return sucess data values.
This extra check is used to ensure that an accurate `Content-Type` is
being sent to the client, as an incorrect header can cause consumers
to misinterpret the data returned by the API (particularly JQuery, as
described in T352546).

The check was added only to endpoints which return success data.
Tests that check for 4xx status codes do not have the content type
check.

This is a follow-up to I381f33dd.

Bug: T352546
Change-Id: Ib9316b26035ed699f1e607f96e04da110c0edb32
2023-12-02 15:21:53 +08:00

362 lines
12 KiB
JavaScript

'use strict';
const { action, assert, REST, utils } = require( 'api-testing' );
describe( 'PUT /page/{title}', () => {
const client = new REST();
let mindy, anon, anonToken;
before( async () => {
mindy = await action.mindy();
// NOTE: this only works because the same token is shared by all anons.
// TODO: add support for login and tokens to RESTClient.
anon = action.getAnon();
anonToken = await anon.token();
} );
const checkEditResponse = function ( title, reqBody, body ) {
assert.containsAllKeys( body, [ 'title', 'key', 'source', 'latest', 'id', 'license', 'content_model' ] );
assert.containsAllKeys( body.latest, [ 'id', 'timestamp' ] );
assert.nestedPropertyVal( body, 'source', reqBody.source );
assert.nestedPropertyVal( body, 'title', title );
assert.nestedPropertyVal( body, 'key', utils.dbkey( title ) );
assert.isAbove( body.latest.id, 0 );
if ( reqBody.content_model ) {
assert.nestedPropertyVal( body, 'content_model', reqBody.content_model );
}
};
const checkSourceResponse = function ( title, reqBody, body ) {
if ( reqBody.content_model ) {
assert.nestedPropertyVal( body, 'content_model', reqBody.content_model );
}
assert.nestedPropertyVal( body, 'title', title );
assert.nestedPropertyVal( body, 'key', utils.dbkey( title ) );
assert.nestedPropertyVal( body, 'source', reqBody.source );
};
describe( 'successful operation', () => {
it( 'should create a page if it does not exist', async () => {
const title = utils.title( 'Edit Test ' );
const normalizedTitle = utils.dbkey( title );
const reqBody = {
token: anonToken,
source: 'Lörem Ipsüm',
comment: 'tästing'
};
const { status: editStatus, body: editBody, header: editHeader } =
await client.put( `/page/${ title }`, reqBody );
assert.equal( editStatus, 201 );
assert.match( editHeader[ 'content-type' ], /^application\/json/ );
checkEditResponse( title, reqBody, editBody );
const { status: sourceStatus, body: sourceBody, header: sourceHeader } =
await client.get( `/page/${ normalizedTitle }` );
assert.equal( sourceStatus, 200 );
assert.match( sourceHeader[ 'content-type' ], /^application\/json/ );
checkSourceResponse( title, reqBody, sourceBody );
} );
it( 'should create a page with specific content model', async () => {
const title = utils.title( 'Edit Test ' );
const normalizedTitle = utils.dbkey( title );
// TODO: test a content model different from the default.
// But that requires the chnagecontentmodel permission, which anons don't have.
const reqBody = {
token: anonToken,
source: 'Lörem Ipsüm',
comment: 'tästing',
content_model: 'wikitext'
};
const { status: editStatus, body: editBody, header: editHeader } =
await client.put( `/page/${ title }`, reqBody );
assert.equal( editStatus, 201 );
assert.match( editHeader[ 'content-type' ], /^application\/json/ );
checkEditResponse( title, reqBody, editBody );
const { status: sourceStatus, body: sourceBody, header: sourceHeader } =
await client.get( `/page/${ normalizedTitle }` );
assert.equal( sourceStatus, 200 );
assert.match( sourceHeader[ 'content-type' ], /^application\/json/ );
checkSourceResponse( title, reqBody, sourceBody );
} );
it( 'should update an existing page', async () => {
const title = utils.title( 'Edit Test ' );
const normalizedTitle = utils.dbkey( title );
// create
const firstRev = await mindy.edit( title, {} );
const reqBody = {
token: anonToken,
source: 'Lörem Ipsüm',
comment: 'tästing',
latest: { id: firstRev.newrevid }
};
const { status: editStatus, body: editBody, header: editHeader } =
await client.put( `/page/${ title }`, reqBody );
assert.equal( editStatus, 200 );
assert.match( editHeader[ 'content-type' ], /^application\/json/ );
checkEditResponse( title, reqBody, editBody );
assert.isAbove( editBody.latest.id, firstRev.newrevid );
const { status: sourceStatus, body: sourceBody, header: sourceHeader } =
await client.get( `/page/${ normalizedTitle }` );
assert.equal( sourceStatus, 200 );
assert.match( sourceHeader[ 'content-type' ], /^application\/json/ );
checkSourceResponse( title, reqBody, sourceBody );
} );
it( 'should handle null-edits (unchanged content) gracefully', async () => {
const title = utils.title( 'Edit Test ' );
// create
const firstRev = await mindy.edit( title, {} );
const reqBody = {
token: anonToken,
source: firstRev.param_text,
comment: 'nothing at all changed',
latest: { id: firstRev.newrevid }
};
const resp = await client.put( `/page/${ title }`, reqBody );
const { status: editStatus, body: editBody, header: editHeader } = resp;
assert.equal( editStatus, 200 );
assert.match( editHeader[ 'content-type' ], /^application\/json/ );
checkEditResponse( title, reqBody, editBody );
// No revision was created, new ID is the same as the old ID
assert.equal( editBody.latest.id, firstRev.newrevid );
} );
it( 'should automatically solve merge conflicts', async () => {
// XXX: this test may fail if the diff3 utility is not found on the web host
const title = utils.title( 'Edit Test ' );
// create
const firstRev = await mindy.edit( title, { text: 'first line\nlorem ipsum\nsecond line' } );
await mindy.edit( title, { text: 'FIRST LINE\nlorem ipsum\nsecond line' } );
const reqBody = {
token: anonToken,
source: 'first line\nlorem ipsum\nSECOND LINE',
comment: 'tästing',
content_model: 'wikitext',
latest: { id: firstRev.newrevid }
};
const { status: editStatus, body: editBody, header: editHeader } =
await client.put( `/page/${ title }`, reqBody );
assert.equal( editStatus, 200 );
assert.match( editHeader[ 'content-type' ], /^application\/json/ );
const expectedText = 'FIRST LINE\nlorem ipsum\nSECOND LINE';
assert.nestedPropertyVal( editBody, 'source', expectedText );
} );
} );
describe( 'request validation', () => {
const requiredProps = [ 'source', 'comment' ];
requiredProps.forEach( ( missingPropName ) => {
const title = utils.title( 'Edit Test ' );
const reqBody = {
token: anonToken,
source: 'Lörem Ipsüm',
comment: 'tästing',
content_model: 'wikitext'
};
it( `should fail when ${ missingPropName } is missing from the request body`, async () => {
const incompleteBody = { ...reqBody };
delete incompleteBody[ missingPropName ];
const { status: editStatus, body: editBody, header: editHeader } =
await client.put( `/page/${ title }`, incompleteBody );
assert.equal( editStatus, 400 );
assert.match( editHeader[ 'content-type' ], /^application\/json/ );
assert.nestedProperty( editBody, 'messageTranslations' );
} );
} );
it( 'should fail if no token is given', async () => {
const title = utils.title( 'Edit Test ' );
const reqBody = {
// no token
source: 'Lörem Ipsüm',
comment: 'tästing'
};
const { status: editStatus, body: editBody, header: editHeader } =
await client.put( `/page/${ title }`, reqBody );
assert.equal( editStatus, 400 );
assert.match( editHeader[ 'content-type' ], /^application\/json/ );
assert.nestedProperty( editBody, 'messageTranslations' );
} );
it( 'should fail if a bad token is given', async () => {
const title = utils.title( 'Edit Test ' );
const reqBody = {
token: 'BAD',
source: 'Lörem Ipsüm',
comment: 'tästing'
};
const { status: editStatus, body: editBody, header: editHeader } =
await client.put( `/page/${ title }`, reqBody );
assert.equal( editStatus, 403 );
assert.match( editHeader[ 'content-type' ], /^application\/json/ );
assert.nestedProperty( editBody, 'messageTranslations' );
} );
it( 'should fail if a bad content model is given', async () => {
const title = utils.title( 'Edit Test ' );
const reqBody = {
token: anonToken,
source: 'Lörem Ipsüm',
comment: 'tästing',
content_model: 'THIS DOES NOT EXIST!'
};
const { status: editStatus, body: editBody, header: editHeader } =
await client.put( `/page/${ title }`, reqBody );
assert.equal( editStatus, 400 );
assert.match( editHeader[ 'content-type' ], /^application\/json/ );
assert.nestedProperty( editBody, 'messageTranslations' );
} );
it( 'should fail if a bad title is given', async () => {
const title = '_|_'; // not a valid page title
const reqBody = {
source: 'Lörem Ipsüm',
comment: 'tästing',
content_model: 'wikitext'
};
const { status: editStatus, body: editBody, header: editHeader } =
await client.put( `/page/${ title }`, reqBody );
assert.equal( editStatus, 400 );
assert.match( editHeader[ 'content-type' ], /^application\/json/ );
assert.nestedProperty( editBody, 'messageTranslations' );
} );
it( 'should fail if no title is given', async () => {
const reqBody = {
source: 'Lörem Ipsüm',
comment: 'tästing',
content_model: 'wikitext'
};
const { status: editStatus, body: editBody, header: editHeader } =
await client.put( '/page/', reqBody );
assert.equal( editStatus, 400 );
assert.match( editHeader[ 'content-type' ], /^application\/json/ );
assert.nestedProperty( editBody, 'messageTranslations' );
} );
} );
describe( 'failures due to system state', () => {
it( 'should detect when the target page does not exist but revision ID was given', async () => {
const title = utils.title( 'Edit Test ' );
const reqBody = {
token: anonToken,
source: 'Lörem Ipsüm',
comment: 'tästing',
latest: { id: 1234 }
};
const { status: editStatus, body: editBody, header: editHeader } =
await client.put( `/page/${ title }`, reqBody );
assert.equal( editStatus, 404 );
assert.match( editHeader[ 'content-type' ], /^application\/json/ );
assert.nestedProperty( editBody, 'messageTranslations' );
} );
it( 'should detect a conflict if page exist but no revision ID was given', async () => {
const title = utils.title( 'Edit Test ' );
// create
await mindy.edit( title, {} );
const reqBody = {
token: anonToken,
source: 'Lörem Ipsüm',
comment: 'tästing'
// not 'latest' key, so page should be created
};
const { status: editStatus, body: editBody, header: editHeader } =
await client.put( `/page/${ title }`, reqBody );
assert.equal( editStatus, 409 );
assert.match( editHeader[ 'content-type' ], /^application\/json/ );
assert.nestedProperty( editBody, 'messageTranslations' );
} );
it( 'should detect a conflict when an old base revision ID is given and conflict resolution fails', async () => {
const title = utils.title( 'Edit Test ' );
// create
const firstRev = await mindy.edit( title, { text: 'initial text' } );
await mindy.edit( title, { text: 'updated text' } );
const reqBody = {
token: anonToken,
source: 'Lörem Ipsüm',
comment: 'tästing',
latest: { id: firstRev.newrevid }
};
const { status: editStatus, body: editBody, header: editHeader } =
await client.put( `/page/${ title }`, reqBody );
assert.equal( editStatus, 409 );
assert.match( editHeader[ 'content-type' ], /^application\/json/ );
assert.nestedProperty( editBody, 'messageTranslations' );
} );
} );
describe( 'permission checks', () => {
it( 'should fail when trying to edit a protected page without appropriate permissions', async () => {
const title = utils.title( 'Edit Test ' );
// create a protected page
const firstRev = await mindy.edit( title, {} );
await mindy.action( 'protect', {
title,
token: await mindy.token(),
protections: 'edit=sysop'
}, 'POST' );
const reqBody = {
token: anonToken,
source: 'Lörem Ipsüm',
comment: 'tästing',
latest: { id: firstRev.newrevid }
};
const { status: editStatus, body: editBody, header: editHeader } =
await client.put( `/page/${ title }`, reqBody );
assert.equal( editStatus, 403 );
assert.match( editHeader[ 'content-type' ], /^application\/json/ );
assert.nestedProperty( editBody, 'messageTranslations' );
} );
} );
} );