wiki.techinc.nl/tests/api-testing/REST/Update.js
daniel d0f4e4514f REST: showcase usage of ArrayDef convenience functions
This demonstrates how to use ArrayDef::makeObjectSchema to define a
schema for a complex body field.

The "latest" field in the request body for page PUT requests identifies
the base revision. To allow the base revision info to be looped through
from an earlier GET request, it's nice to allow the "timestamp" field
to be present, even though we don't use it.

I changed the relevant part of Update.js to test that use case specifically.

Bug: T368131
Change-Id: I166396e0dbfc995e5346252ee438c9afe2c808e2
2024-07-25 16:54:18 +00:00

361 lines
12 KiB
JavaScript

'use strict';
const { action, assert, REST, utils } = require( 'api-testing' );
describe( 'PUT /page/{title}', () => {
let client, mindy, mindyToken;
before( async () => {
mindy = await action.mindy();
client = new REST( 'rest.php/v1', mindy );
mindyToken = await mindy.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: mindyToken,
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.
const reqBody = {
token: mindyToken,
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
await mindy.edit( normalizedTitle, {} );
const { body: newPage } = await client.get( `/page/${ normalizedTitle }/bare` );
const firstRev = newPage.latest;
const reqBody = {
token: mindyToken,
source: 'Lörem Ipsüm',
comment: 'tästing',
latest: firstRev // provide the base revision
};
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.id );
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: mindyToken,
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: mindyToken,
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: mindyToken,
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: mindyToken,
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: mindyToken,
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: mindyToken,
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: mindyToken,
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 anon = action.getAnon();
const reqBody = {
token: await anon.token(),
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' );
} );
} );
} );