Yurik has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/198667

Change subject: API Unittests and bugfixes
......................................................................

API Unittests and bugfixes

Implemented tons of unit tests for all 3 current gather APIs,
and discovered a number of nasty bugs in the process.
Also, unified and simplified editlist security to lock it down.

Change-Id: I3e08d17bf0d83f0d813eccee2d27feeccd357a9b
---
M includes/api/ApiEditList.php
M includes/api/ApiQueryListPages.php
M includes/api/ApiQueryLists.php
D tests/phpunit/api/ApiQueryLists.php
A tests/phpunit/api/GatherTests.php
5 files changed, 946 insertions(+), 584 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Gather 
refs/changes/67/198667/1

diff --git a/includes/api/ApiEditList.php b/includes/api/ApiEditList.php
index 6b62e90..86b3975 100644
--- a/includes/api/ApiEditList.php
+++ b/includes/api/ApiEditList.php
@@ -57,67 +57,15 @@
 
                if ( $params['label'] !== null ) {
                        $params['label'] = trim( $params['label'] );
-                       if ( $params['label'] === '' ) {
-                               $this->dieUsage( 'If given, label must not be 
empty', 'badlabel' );
-                       }
                }
+               $this->checkPermissions( $params );
 
-               $user = $this->getUser(); // TBD: We might want to allow other 
users with getWatchlistUser()
-
-               if ( !$user->isLoggedIn() ) {
-                       $this->dieUsage( 'You must be logged-in to have a 
list', 'notloggedin' );
-               }
-               if ( !$user->isAllowed( 'editmywatchlist' ) ) {
-                       $this->dieUsage( 'You don\'t have permission to edit 
your list',
-                               'permissiondenied' );
-               }
-
-               $pageSet = $this->getPageSet();
                $p = $this->getModulePrefix();
-
+               $user = $this->getUser(); // TBD: We might want to allow other 
users with getWatchlistUser()
                $isDeletingList = $params['deletelist'];
                $listId = $params['id'];
                $isNew = $listId === null;
                $isWatchlist = $listId === 0;
-
-               // Validate 'deletelist' parameters
-               if ( $isDeletingList ) {
-
-                       // ID == 0 is a watchlist
-                       if ( $isWatchlist ) {
-                               $this->dieUsage( "List #0 (watchlist) may not 
be deleted", 'badid' );
-                       }
-                       if ( $isNew ) {
-                               $this->dieUsage(
-                                       "List must be identified with {$p}id 
when {$p}deletelist is used", 'invalidparammix'
-                               );
-                       }
-
-                       // For deletelist, disallow all parameters except those 
unset
-                       $tmp = $params + $pageSet->extractRequestParams();
-                       unset( $tmp['deletelist'] );
-                       unset( $tmp['id'] );
-                       unset( $tmp['token'] );
-                       $extraParams =
-                               array_keys( array_filter( $tmp, function ( $x ) 
{
-                                       return $x !== null && $x !== false;
-                               } ) );
-                       if ( $extraParams ) {
-                               $this->dieUsage( "The parameter {$p}deletelist 
must not be used with " .
-                                       implode( ", ", $extraParams ), 
'invalidparammix' );
-                       }
-               } elseif ( $isNew ) {
-                       if ( $params['remove'] ) {
-                               $this->dieUsage( "List must be identified with 
{$p}id when {$p}remove is used",
-                                       'invalidparammix' );
-                       }
-                       if ( !$params['label'] ) {
-                               $this->dieUsage( "List {$p}label must be given 
for new lists", 'invalidparammix' );
-                       }
-               }
-               if ( $isWatchlist && $params['label'] ) {
-                       $this->dieUsage( "List {$p}label may not be set for the 
id==0", 'invalidparammix' );
-               }
 
                $dbw = wfGetDB( DB_MASTER, 'api' );
 
@@ -126,25 +74,22 @@
                        $listId = $this->createRow( $dbw, $user, $params, 
$isWatchlist );
                } else {
                        // Find existing list
-                       $row = $this->getListRow( $dbw, array( 'gl_id' => 
$listId ) );
+                       $row = $this->getListRow( $params, $dbw, array( 'gl_id' 
=> $listId ) );
                        if ( $row === false ) {
                                // No database record with the given ID
                                $this->dieUsage( "List {$p}id was not found", 
'badid' );
                        }
+                       $isWatchlist = $row->gl_label === '';
                        if ( !$isDeletingList ) {
+                               // ACTION: update list
                                $this->updateListDb( $dbw, $params, $row );
                        } else {
-                               // Check again - we didn't know it was a 
watchlist until DB query
-                               if ( $row->gl_label === '' ) {
-                                       $this->dieUsage( "Watchlist may not be 
deleted", 'badid' );
-                               }
-                               if ( strval( $row->gl_user ) !== strval( 
$user->getId() ) ) {
-                                       $this->dieUsage( "List {$p}id does not 
belong to current user", 'permissiondenied' );
-                               }
-                               // ACTION: deleting list
-                               $dbw->delete( 'gather_list', array( 'gl_id' => 
$row->gl_id ), __METHOD__ );
+                               // ACTION: delete list (items + list itself)
+                               $dbw->delete( 'gather_list_item', array( 
'gli_gl_id' => $listId ), __METHOD__ );
+                               $dbw->delete( 'gather_list', array( 'gl_id' => 
$listId ), __METHOD__ );
                                $this->getResult()->addValue( null, 
$this->getModuleName(), array(
                                        'status' => 'deleted',
+                                       'id' => $listId,
                                ) );
                        }
                }
@@ -152,6 +97,82 @@
                if ( !$isDeletingList ) {
                        // Add the titles to the list (or subscribe with the 
legacy watchlist)
                        $this->processTitles( $params, $user, $listId, $dbw, 
$isWatchlist );
+               }
+       }
+
+       /**
+        * This method should be called twice - once before accessing DB, and 
once when db row is found
+        * @param array $params
+        * @param stdClass $row
+        * @throws \UsageException
+        */
+       private function checkPermissions( array $params, $row = null ) {
+
+               if ( $row ) {
+                       $isNew = false;
+                       $isWatchlist = $row->gl_label === '';
+               } else {
+                       $isNew = $params['id'] === null;
+                       $isWatchlist = $params['id'] === 0;
+               }
+
+               $user = $this->getUser(); // TBD: We might want to allow other 
users with getWatchlistUser()
+               $p = $this->getModulePrefix();
+               $deleteList = $params['deletelist'];
+
+               if ( !$user->isLoggedIn() ) {
+                       $this->dieUsage( 'You must be logged-in to edit a 
list', 'notloggedin' );
+               } elseif ( !$user->isAllowed( 'editmywatchlist' ) ) {
+                       $this->dieUsage( 'You don\'t have permission to edit 
your list', 'permissiondenied' );
+               } elseif ( $params['label'] === '' ) {
+                       $this->dieUsage( 'If given, label must not be empty', 
'badlabel' );
+               }
+
+               if ( $isWatchlist ) {
+                       if ( $params['label'] !== null ) {
+                               $this->dieUsage( "{$p}label cannot be set for a 
watchlist", 'invalidparammix' );
+                       } elseif ( $deleteList ) {
+                               $this->dieUsage( "List #0 (watchlist) may not 
be deleted", 'badid' );
+                       } elseif ( $params['perm'] === 'public' ) {
+                               // Per team discussion, introducing artificial 
limitation for now
+                               // until we establish that making watchlist 
public would cause no harm.
+                               // This check can be deleted at any time since 
all other API code supports it.
+                               $this->dieUsage( 'Making watchlist public is 
not supported for security reasons',
+                                       'publicwatchlist' );
+                       }
+               }
+               if ( $isNew ) {
+                       if ( $deleteList ) {
+                               // This is more for safety - it shouldn't be 
possible to delete a list by label
+                               $this->dieUsage( "List must be identified with 
{$p}id when {$p}deletelist is used",
+                                       'invalidparammix' );
+                       } elseif ( $params['remove'] ) {
+                               $this->dieUsage( "List must be identified with 
{$p}id when {$p}remove is used",
+                                       'invalidparammix' );
+                       } elseif ( $params['label'] === null ) {
+                               $this->dieUsage( "List {$p}label must be given 
for new lists", 'invalidparammix' );
+                       }
+               }
+               if ( $deleteList && !$row ) {
+                       // For deletelist, disallow all parameters except those 
unset
+                       // Minor optimization - don't validate on the second 
pass
+                       $tmp = $params + 
$this->getPageSet()->extractRequestParams();
+                       unset( $tmp['deletelist'] );
+                       unset( $tmp['id'] );
+                       unset( $tmp['token'] );
+                       $extraParams = array_keys( array_filter( $tmp, function 
( $x ) {
+                               return $x !== null && $x !== false;
+                       } ) );
+                       if ( $extraParams ) {
+                               $this->dieUsage( "The parameter {$p}deletelist 
must not be used with " .
+                                                                implode( ", ", 
$extraParams ), 'invalidparammix' );
+                       }
+               }
+               if ( $row ) {
+                       if ( strval( $row->gl_user ) !== strval( $user->getId() 
) ) {
+                               $this->dieUsage( "List {$p}id does not belong 
to the current user",
+                                       'permissiondenied' );
+                       }
                }
        }
 
@@ -220,22 +241,12 @@
         * Given an info object, update it with arguments from params, and 
return JSON str if changed
         * @param stdClass $v
         * @param Array $params
-        * @param bool $isWatchlist
         * @return string JSON encoded info object in case it changed, or NULL 
if update is not needed
         * @throws \UsageException
         */
-       private function updateInfo( stdClass $v, array $params, $isWatchlist ) 
{
+       private function updateInfo( stdClass $v, array $params ) {
                $updated = false;
 
-               if ( $isWatchlist && $params['perm'] === 'public' ) {
-                       // Per team discussion, introducing artificial 
limitation for now
-                       // until we establish that making watchlist public 
would cause no harm.
-                       // This check can be deleted at any time since all 
other API code supports it.
-                       $this->dieUsage( 'Making watchlist public is not 
supported for security reasons',
-                               'publicwatchlist' );
-               }
-
-               //
                // Set default
                if ( !property_exists( $v, 'description' ) ) {
                        $v->description = '';
@@ -244,7 +255,6 @@
                        $v->image = '';
                }
 
-               //
                // Update from api parameters
                if ( $params['description'] !== null && $v->description !== 
$params['description'] ) {
                        $v->description = $params['description'];
@@ -310,22 +320,22 @@
 
                if ( $id === 0 ) {
                        // List already exists, update instead, or might not 
need it
-                       $row = $this->getListRow( $dbw, array(
+                       $row = $this->getListRow( $params, $dbw, array(
                                'gl_user' => $user->getId(), 'gl_label' => 
$label
                        ) );
-                       if ( $row === false ) {
-                               if ( $createRow ) {
-                                       // If creation failed, the second query 
should have succeeded
-                                       $this->dieDebug( "List was not found", 
'badid' );
-                               }
+                       if ( $row !== false ) {
+                               $id = $row->gl_id;
+                               $isWatchlist = $row->gl_label === '';
+                               $this->updateListDb( $dbw, $params, $row );
+                       } elseif ( $createRow ) {
+                               // If creation failed, the second query should 
have succeeded
+                               $this->dieDebug( "List was not found", 'badid' 
);
+                       } else {
+                               // Watchlist, no changes
                                $this->getResult()->addValue( null, 
$this->getModuleName(), array(
                                        'status' => 'nochanges',
                                        'id' => 0,
                                ) );
-                       } else {
-                               $id = $row->gl_id;
-                               $isWatchlist = $row->gl_label === '';
-                               $this->updateListDb( $dbw, $params, $row );
                        }
                } else {
                        $this->getResult()->addValue( null, 
$this->getModuleName(), array(
@@ -345,19 +355,19 @@
         */
        private function updateListDb( DatabaseBase $dbw, array $params, $row ) 
{
                $update = array();
+               $info = self::parseListInfo( $row->gl_info, $row->gl_id, true );
+               $info = $this->updateInfo( $info, $params, $row->gl_label === 
'' );
+               if ( $info ) {
+                       $update['gl_info'] = $info;
+               }
                if ( $params['label'] !== null && $row->gl_label !== 
$params['label'] ) {
                        $update['gl_label'] = $params['label'];
                }
                if ( $params['perm'] !== null ) {
-                       $perm = $params['perm'] === 'public' ? 1 : 0;
-                       if ( $row->gl_perm !== $perm ) {
+                       $perm = $params['perm'] === 'public' ? '1' : '0';
+                       if ( strval( $row->gl_perm ) !== $perm ) {
                                $update['gl_perm'] = $perm;
                        }
-               }
-               $info = self::parseListInfo( $row->gl_info, $row->gl_id, true );
-               $json = $this->updateInfo( $info, $params, $row->gl_label === 
'' );
-               if ( $json ) {
-                       $update['gl_info'] = $json;
                }
                if ( $update ) {
                        // ACTION: update list record
@@ -460,7 +470,7 @@
                                $dbw->delete( 'gather_list_item', array(
                                        'gli_gl_id' => $listId,
                                        $set
-                               ) );
+                               ), __METHOD__ );
                        }
                }
 
@@ -523,14 +533,20 @@
        }
 
        /**
+        * Get DB row and if found, validate it against user parameters
+        * @param array $params
         * @param DatabaseBase $dbw
         * @param array $conds
         * @return bool|stdClass
         */
-       private function getListRow( DatabaseBase $dbw, $conds ) {
-               return $dbw->selectRow( 'gather_list',
+       private function getListRow( array $params, DatabaseBase $dbw, array 
$conds ) {
+               $row = $dbw->selectRow( 'gather_list',
                        array( 'gl_id', 'gl_user', 'gl_label', 'gl_perm', 
'gl_info' ),
                        $conds,
                        __METHOD__ );
+               if ( $row ) {
+                       $this->checkPermissions( $params, $row );
+               }
+               return $row;
        }
 }
diff --git a/includes/api/ApiQueryListPages.php 
b/includes/api/ApiQueryListPages.php
index dca0811..5f919a2 100644
--- a/includes/api/ApiQueryListPages.php
+++ b/includes/api/ApiQueryListPages.php
@@ -79,7 +79,7 @@
                        // Id was given, this could be public or private list, 
legacy watchlist or regular
                        // Allow access to any public list/watchlist, and to 
private with proper owner/self
                        $db = $this->getDB();
-                       $listRow = $db->selectRow( 'gather_list', array( 
'gl_label', 'gl_user' ),
+                       $listRow = $db->selectRow( 'gather_list', array( 
'gl_label', 'gl_user', 'gl_perm' ),
                                array( 'gl_id' => $params['id'] ), __METHOD__ );
                        if ( $listRow === false ) {
                                $this->dieUsage( "List does not exist", 'badid' 
);
@@ -91,17 +91,18 @@
                                // but that might be unexpected behavior
                                $user = $this->getWatchlistUser( $params );
                                if ( strval( $user->getId() ) !== 
$listRow->gl_user ) {
-                                       $this->dieUsage( 'The owner supplied 
does not match the list\'s owner', 'permissiondenied' );
+                                       $this->dieUsage( 'The owner supplied 
does not match the list\'s owner',
+                                               'permissiondenied' );
                                }
                                $showPrivate = true;
                        } else {
                                $user = $this->getUser();
-                               $showPrivate = $user->isLoggedIn() && strval( 
$user->getId() ) === $listRow->gl_user &&
-                                       $user->isAllowed( 'viewmywatchlist' );
+                               $showPrivate = $user->isLoggedIn() && strval( 
$user->getId() ) === $listRow->gl_user
+                                       && $user->isAllowed( 'viewmywatchlist' 
);
                        }
 
                        // Check if this is a public list (if required)
-                       if ( !$showPrivate && $listRow->perm !== 1 ) {
+                       if ( !$showPrivate && strval( $listRow->gl_perm ) !== 
'1' ) {
                                $this->dieUsage( "You have no rights to see 
this list", 'badid' );
                        }
 
diff --git a/includes/api/ApiQueryLists.php b/includes/api/ApiQueryLists.php
index 0528d8f..0072754 100644
--- a/includes/api/ApiQueryLists.php
+++ b/includes/api/ApiQueryLists.php
@@ -235,7 +235,7 @@
                                }
                        }
                        if ( $fld_public ) {
-                               $data['public'] = $row->gl_perm === 1;
+                               $data['public'] = strval( $row->gl_perm ) === 
'1';
                        }
                        if ( $useInfo ) {
                                $info = ApiEditList::parseListInfo( 
$row->gl_info, $row->gl_id, false );
diff --git a/tests/phpunit/api/ApiQueryLists.php 
b/tests/phpunit/api/ApiQueryLists.php
deleted file mode 100644
index bc7512c..0000000
--- a/tests/phpunit/api/ApiQueryLists.php
+++ /dev/null
@@ -1,482 +0,0 @@
-<?php
-/**
- *
- * Created on Feb 6, 2013
- *
- * Copyright © 2013 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-/**
- * These tests validate basic functionality of the api query module
- *
- * @group API
- * @group Database
- * @group medium
- * @covers ApiQuery
- */
-class ApiQueryLists extends ApiQueryTestBase {
-       protected $exceptionFromAddDBData;
-
-       /** @var TestUser[] */
-       private static $wlUsers = null;
-
-       /**
-        * Create a set of pages. These must not change, otherwise the tests 
might give wrong results.
-        * @see MediaWikiTestCase::addDBData()
-        */
-       public function addDBData() {
-               try {
-                       if ( !Title::newFromText( 'Gather-All' )->exists() ) {
-                               $this->editPage( 'Gather-ListA', 'a' );
-                               $this->editPage( 'Gather-ListB', 'b' );
-                               $this->editPage( 'Gather-ListAB', 'ab' );
-                               $this->editPage( 'Gather-ListW', 'w' );
-                               $this->editPage( 'Gather-ListWA', 'wa' );
-                               $this->editPage( 'Gather-ListWAB', 'wab' );
-                       }
-               } catch ( Exception $e ) {
-                       $this->exceptionFromAddDBData = $e;
-               }
-       }
-
-       protected function setUp() {
-               parent::setUp();
-               if ( !self::$wlUsers ) {
-                       foreach ( array(
-                                                 'GatherML',
-                                                 'GatherML2',
-                                                 'GatherWML',
-                                                 'GatherWML2',
-                                                 'GatherWlOnly',
-                                                 'GatherWlOnly2',
-                                         ) as $name ) {
-                               self::$wlUsers[$name] = new TestUser( $name );
-                       }
-               }
-               self::$users = array_merge( self::$users, self::$wlUsers );
-       }
-
-       public function testAnonymous() {
-               $a = User::newFromId( 0 );
-
-               $this->assertUsage( 'an-0', '{ "list": "lists" }', $a );
-               $this->assertUsage( 'an-1', '{ "list": "lists", "lstids": 0 }', 
$a );
-       }
-
-       public function testMultipleLists() {
-               $this->allTests( false );
-       }
-
-       public function testMultipleListsWithWatchlist() {
-               $this->allTests( true );
-       }
-
-       private function allTests( $createWatchlist ) {
-               $p = $createWatchlist ? 'Test With watchlist: #' : 'Test 
without watchlist: #';
-               $n = $createWatchlist ? 'GatherWML' : 'GatherML';
-               $n2 = $createWatchlist ? 'GatherWML2' : 'GatherML2';
-
-               $a = User::newFromId( 0 ); // Anonymous user
-               $u = self::$users[$n]->getUser(); // User for this test
-               $u2 = self::$users[$n2]->getUser(); // Second user for this test
-
-               $token = $this->getToken( $u );
-
-               if ( $createWatchlist ) {
-                       // Create watchlist row
-                       $res = $this->updateList( "$p a1", 
'{"id":0,"description":"x"}', $u, $token );
-                       $wlId = $this->getVal( "$p a1", '"id"', $res );
-               } else {
-                       $wlId = 0;
-               }
-
-               // Add pages to various lists
-               $res = $this->updateList( "$p a1",
-                       
'{"id":0,"titles":"Gather-ListW|Gather-ListWA|Gather-ListWAB"}', $u, $token );
-               $this->getVal( "$p a1", '"status"', $res, 'nochanges' );
-               $this->getVal( "$p a1", '"pages",0,"added"', $res, '' );
-               $this->getVal( "$p a1", '"pages",1,"added"', $res, '' );
-               $this->getVal( "$p a1", '"pages",2,"added"', $res, '' );
-               $this->getVal( "$p a1", '"id"', $res, $wlId );
-
-               $res = $this->updateList( "$p a2",
-                       
'{"label":"A","titles":"Gather-ListWA|Gather-ListWAB"}', $u, $token );
-               $this->getVal( "$p a2", '"status"', $res, 'created' );
-               $this->getVal( "$p a2", '"pages",0,"added"', $res, '' );
-               $this->getVal( "$p a2", '"pages",1,"added"', $res, '' );
-               $idA = $this->getVal( "$p a2", '"id"', $res );
-
-               $res = $this->updateList( "$p a3",
-                       '{"label":"B", "perm":"public", 
"titles":"Gather-ListWAB"}', $u, $token );
-               $this->getVal( "$p a3", '"status"', $res, 'created' );
-               $this->getVal( "$p a3", '"pages",0,"added"', $res, '' );
-               $idB = $this->getVal( "$p a3", '"id"', $res );
-
-               $res = $this->callApi( "$p b1", '{ "list": "lists" }', $u );
-               $this->assertListNoId( "$p b1", $res, $wlId
-                       ? '[{"watchlist":true,"label":"Watchlist"}, 
{"label":"A"}, {"label":"B"}]'
-                       : '[{"id":0, "watchlist":true,"label":"Watchlist"}, 
{"label":"A"}, {"label":"B"}]' );
-               $this->getVal( "$p b1", '"query","lists",1,"id"', $res, $idA );
-               $this->getVal( "$p b1", '"query","lists",2,"id"', $res, $idB );
-
-               //
-               // Continuation
-               $request = $this->toApiParams( "$p c1", '{ "list": "lists", 
"lstlimit": 1 }' );
-
-               $res = $this->callApi( "$p c1", $request, $u );
-               $this->assertListsEquals( "$p c1a", $res,
-                       '[{"id":' . $wlId . ', 
"watchlist":true,"label":"Watchlist"}]' );
-               $continue = $this->getVal( "$p c1b", '"continue"', $res );
-
-               $res = $this->callApi( "$p c2", array_merge( $continue, 
$request ), $u );
-               $this->assertListNoId( "$p c2a", $res, '[{"label":"A"}]' );
-               $continue = $this->getVal( "$p c2b", '"continue"', $res );
-
-               $res = $this->callApi( "$p c3", array_merge( $continue, 
$request ), $u );
-               $this->assertListNoId( "$p c3a", $res, '[{"label":"B"}]' );
-               $this->assertArrayNotHasKey( 'continue', $res, "$p c3c" );
-
-               //
-               // ids=A
-               $res = $this->callApi( "$p d1", '{ "list": "lists", "lstids":' 
. $idA . ' }', $u );
-               $this->assertListNoId( "$p d1", $res, '[{"label":"A"}]' );
-
-               // ids=A as anon user
-               $res = $this->callApi( "$p d2", '{ "list": "lists", "lstids":' 
. $idA . ' }', $a );
-               $this->assertListNoId( "$p d2", $res, '[]' );
-
-               // ids=A as another user
-               $res = $this->callApi( "$p d3", '{ "list": "lists", "lstids":' 
. $idA . ' }', $u2 );
-               $this->assertListNoId( "$p d3", $res, '[]' );
-
-               //
-               // ids=B
-               $res = $this->callApi( "$p e1", '{ "list": "lists", "lstids":' 
. $idB . ' }', $u );
-               $this->assertListNoId( "$p e1", $res, '[{"label":"B"}]' );
-
-               // ids=B as anon user
-               $res = $this->callApi( "$p e2", '{ "list": "lists", "lstids":' 
. $idB . ' }', $a );
-               $this->assertListNoId( "$p e2", $res, '[{"label":"B"}]' );
-
-               // ids=B as another user
-               $res = $this->callApi( "$p e3", '{ "list": "lists", "lstids":' 
. $idB . ' }', $u2 );
-               $this->assertListNoId( "$p e3", $res, '[{"label":"B"}]' );
-
-               //
-               // Use owner param
-               // user: get all with owner=user
-               $res = $this->callApi( "$p i0", '{ "list": "lists", "lstowner": 
"' . $n . '" }', $u );
-               $this->assertListsEquals( "$p i0", $res,
-                       '[{"id":' . $wlId . ', 
"watchlist":true,"label":"Watchlist"}, ' .
-                       '{"id":' . $idA . ', "label":"A"},' . '{"id":' . $idB . 
', "label":"B"}]' );
-
-               // user: get by idA with owner=user
-               $res = $this->callApi( "$p i0a", '{ "list": "lists", 
"lstowner": "' . $n .
-                       '", "lstids": ' . $idA . ' }', $u );
-               $this->assertListsEquals( "$p i0a", $res, '[{"id":' . $idA . ', 
"label":"A"}]' );
-
-               // anon: get all with owner=user
-               $res = $this->callApi( "$p i1", '{ "list": "lists", "lstowner": 
"' . $n .
-                                                                          '" 
}', $a );
-               $this->assertListNoId( "$p i1", $res, '[{"label":"B"}]' );
-
-               // anon: get by idA with owner=user
-               $res = $this->callApi( "$p i2", '{ "list": "lists", "lstowner": 
"' . $n .
-                                                                          '", 
"lstids": ' . $idA . ' }', $a );
-               $this->assertListNoId( "$p i2", $res, '[]' );
-
-               // anon: get by idB with owner=user
-               $res = $this->callApi( "$p i3", '{ "list": "lists", "lstowner": 
"' . $n .
-                                                                          '", 
"lstids": ' . $idB . ' }', $a );
-               $this->assertListNoId( "$p i3", $res, '[{"label":"B"}]' );
-
-               // user2: get all with owner=user
-               $res = $this->callApi( "$p i4", '{ "list": "lists", "lstowner": 
"' . $n . '" }', $u2 );
-               $this->assertListNoId( "$p i4", $res, '[{"label":"B"}]' );
-
-               // user2: get by idA with owner=user
-               $res = $this->callApi( "$p i5", '{ "list": "lists", "lstowner": 
"' . $n .
-                                                                          '", 
"lstids": ' . $idA . ' }', $u2 );
-               $this->assertListNoId( "$p i5", $res, '[]' );
-
-               // user2: get by idB with owner=user
-               $res = $this->callApi( "$p i5", '{ "list": "lists", "lstowner": 
"' . $n .
-                                                                          '", 
"lstids": ' . $idB . ' }', $u2 );
-               $this->assertListNoId( "$p i5", $res, '[{"label":"B"}]' );
-       }
-
-       public function testWatchlistOnly() {
-               $u = self::$users['GatherWlOnly']->getUser(); // User for this 
test
-               $n = $u->getName(); // Name of the user for this test
-               $a = User::newFromId( 0 ); // Anonymous user
-               $u2 = self::$users['GatherWlOnly2']->getUser(); // Second user 
for this test
-
-               $token = $this->getToken( $u );
-               $wlOnly = '[{"id":0, "watchlist":true, "label":"Watchlist"}]';
-
-               //
-               // Validate empty watchlist / lists
-               $res = $this->callApi( 'nc-a0', '{ "list": "lists" }', $u );
-               $this->assertListNoId( 'nc-a0', $res, $wlOnly );
-
-               $res = $this->callApi( 'nc-a1', '{ "list": "lists", "lstids": 0 
}', $u );
-               $this->assertListNoId( 'nc-a1', $res, $wlOnly );
-
-               $res = $this->callApi( 'nc-a2', '{ "list": "lists", "lstlimit": 
1 }', $u );
-               $this->assertListNoId( 'nc-a2', $res, $wlOnly );
-
-               $res = $this->callApi( 'nc-a3',
-                       '{ "list": "lists", "lstprop": 
"label|description|public|count" }', $u );
-               $this->assertListNoId( 'nc-a3', $res,
-                       
'[{"id":0,"watchlist":true,"count":0,"label":"Watchlist","description":"","public":false}]'
-               );
-               $res = $this->callApi( 'nc-a4', '{ "list": "lists", "lsttitle": 
"Missing" }', $u );
-               $this->assertListNoId( 'nc-a4', $res,
-                       
'[{"id":0,"watchlist":true,"label":"Watchlist","title":false}]' );
-
-               //
-               // Add page to watchlist
-               $this->legacyAddToWatchlist( 'nc-b0', 'Gather-ListW', $u, 
$token );
-               $res = $this->callApi( 'nc-b0', '{ "list": "lists", "lstprop": 
"count" }', $u );
-               $this->assertListNoId( 'nc-b0', $res, '[{"id": 0, 
"watchlist":true, "count": 1}]' );
-
-               $res = $this->callApi( 'nc-b1', '{ "list": "lists", "lsttitle": 
"Gather-ListW" }', $u );
-               $this->assertListNoId( 'nc-b1', $res,
-                       
'[{"id":0,"watchlist":true,"label":"Watchlist","title":true}]' );
-
-               //
-               // Re-add the same page, using action=editlist & id=0
-               $res = $this->updateList( 'nc-c0', 
'{"id":0,"titles":"Gather-ListW"}', $u, $token );
-               $this->getVal( "nc-c0", '"status"', $res, 'nochanges' );
-               $this->getVal( "nc-c0", '"id"', $res, 0 );
-               $this->getVal( "nc-c0", '"pages",0,"added"', $res, '' );
-
-               $res = $this->callApi( 'nc-c0a', '{ "list": "lists" }', $u );
-               $this->assertListNoId( 'nc-c0a', $res, $wlOnly );
-
-               $res = $this->callApi( 'nc-c1', '{ "list": "lists", "lstids": 0 
}', $u );
-               $this->assertListNoId( 'nc-c1', $res, $wlOnly );
-
-               $res = $this->callApi( 'nc-c3', '{ "list": "lists", "lstprop": 
"count" }', $u );
-               $this->assertListNoId( 'nc-c3', $res, '[{"id":0, 
"watchlist":true, "count": 1}]' );
-
-               $res = $this->callApi( 'nc-c4', '{ "list": "lists", "lsttitle": 
"Gather-ListW" }', $u );
-               $this->assertListNoId( 'nc-c4', $res,
-                       
'[{"id":0,"watchlist":true,"label":"Watchlist","title":true}]' );
-
-               $res = $this->callApi( 'nc-c5',
-                       '{ "list": "lists", "lstids": 0, "lsttitle": 
"Gather-ListW" }', $u );
-               $this->assertListNoId( 'nc-c5', $res,
-                       
'[{"id":0,"watchlist":true,"label":"Watchlist","title":true}]' );
-
-               //
-               // What can others see from this user
-               $res = $this->callApi( 'nc-e0', '{ "list": "lists", "lstowner": 
"' . $n . '" }', $a );
-               $this->assertListNoId( 'nc-e0', $res, '[]' );
-
-               $res = $this->callApi( 'nc-e1', '{ "list": "lists", "lstowner": 
"' . $n .
-                       '", "lstids": 0 }', $a );
-               $this->assertListNoId( 'nc-e1', $res, '[]' );
-
-               $res = $this->callApi( 'nc-e2', '{ "list": "lists", "lstowner": 
"' . $n . '" }', $u2 );
-               $this->assertListNoId( 'nc-e2', $res, '[]' );
-
-               $res =  $this->callApi( 'nc-e3',
-                       '{ "list": "lists", "lstowner": "' . $n . '", "lstids": 
0 }', $u2 );
-               $this->assertListNoId( 'nc-e3', $res, '[]' );
-
-               //
-               // Create watchlist list DB record
-               $res = $this->updateList( 'nc-f0', '{ "id":0, 
"description":"aa" }', $u, $token );
-               $this->getVal( 'nc-f0', '"status"', $res, 'created' );
-               $id = $this->getVal( 'nc-f0', '"id"', $res );
-               $this->assertNotEquals( 0, $id );
-
-               $wlOnly = array( array( 'id' => $id, 'watchlist' => true, 
'label' => 'Watchlist' ) );
-
-               $res = $this->callApi( 'nc-f2', '{ "list": "lists" }', $u );
-               $this->assertListsEquals( 'nc-f2', $res, $wlOnly );
-
-               $res = $this->callApi( 'nc-f3', '{ "list": "lists", "lstids": 0 
}', $u );
-               $this->assertListsEquals( 'nc-f3', $res, $wlOnly );
-
-               $res = $this->callApi( 'nc-f4',
-                       '{ "list": "lists", "lstprop": 
"label|description|public|count" }', $u );
-               $this->assertListsEquals( 'nc-f4', $res,
-                       '[{"id":' . $id .
-                       
',"watchlist":true,"count":1,"label":"Watchlist","description":"aa","public":false}]'
 );
-
-               $res = $this->callApi( 'nc-f5', '{ "list": "lists", "lsttitle": 
"Gather-ListW" }', $u );
-               $this->assertListsEquals( 'nc-f5', $res, '[{"id":' . $id .
-                       ',"watchlist":true,"label":"Watchlist","title":true}]' 
);
-
-               //
-               // Others still can't see the watchlist
-               $res = $this->callApi( 'nc-g0', '{ "list": "lists", "lstowner": 
"' . $n . '" }', $a );
-               $this->assertListNoId( 'nc-g0', $res, '[]' );
-
-               $res = $this->callApi( 'nc-g1', '{ "list": "lists", "lstowner": 
"' . $n .
-                       '", "lstids": 0 }', $a );
-               $this->assertListNoId( 'nc-g1', $res, '[]' );
-
-               $res = $this->callApi( 'nc-g2', '{ "list": "lists", "lstids": ' 
. $id . ' }', $a );
-               $this->assertListNoId( 'nc-g2', $res, '[]' );
-
-               $res = $this->callApi( 'nc-g3', '{ "list": "lists", "lstowner": 
"' . $n . '", "lstids": ' .
-                               $id . ' }', $a );
-               $this->assertListNoId( 'nc-g3', $res, '[]' );
-
-               $res = $this->callApi( 'nc-h0', '{ "list": "lists", "lstowner": 
"' . $n . '" }', $u2 );
-               $this->assertListNoId( 'nc-h0', $res, '[]' );
-
-               $res = $this->callApi( 'nc-h1', '{ "list": "lists", "lstowner": 
"' . $n .
-                       '", "lstids": 0 }', $u2 );
-               $this->assertListNoId( 'nc-h1', $res, '[]' );
-
-               $res = $this->callApi( 'nc-h2', '{ "list": "lists", "lstids": ' 
. $id . ' }', $u2 );
-               $this->assertListNoId( 'nc-h2', $res, '[]' );
-
-               $res = $this->callApi( 'nc-h3', '{ "list": "lists", "lstowner": 
"' . $n . '", "lstids": ' .
-                               $id . ' }', $u2 );
-               $this->assertListNoId( 'nc-h3', $res, '[]' );
-
-               //
-               // Watchlist editing assertions
-               $this->assertUsage( 'nc-i0', '{ "action": "editlist", "id":0, 
"label":"bb" }', $u );
-               $this->assertUsage( 'nc-i1', '{ "action": "editlist", "id":' . 
$id . ', "label":"bb" }', $u );
-       }
-
-       private function assertListNoId( $message, $actual, $expected ) {
-               $this->assertListsEquals( $message, $actual, $expected, true );
-       }
-
-       private function assertListsEquals( $message, $actual, $expected, 
$removeIds = false ) {
-               $actual = $this->getVal( $message, '"query", "lists"', $actual 
);
-               $expected = $this->toArray( $message, $expected );
-               if ( $removeIds ) {
-                       $actual = self::removeIds( $actual );
-               }
-               $this->assertArrayEquals( $expected, $actual, true, true, 
$message );
-       }
-
-       private function updateList( $message, $params, $user, $token ) {
-               $params = $this->toArray( $message, $params );
-               if ( !isset( $params['action'] ) ) {
-                       $params['action'] = 'editlist';
-               }
-               if ( !isset( $params['token'] ) ) {
-                       $params['token'] = $token;
-               }
-               $res = $this->callApi( $message, $params, $user );
-               return $this->getVal( $message, array( $params['action'] ), 
$res );
-
-       }
-
-       private function legacyAddToWatchlist( $message, $titles, $user, $token 
) {
-               $params = array(
-                       'action' => 'watch',
-                       'titles' => $titles,
-                       'token' => $token,
-               );
-               $res = $this->callApi( $message, $params, $user );
-               $this->getVal( $message, '"watch", 0, "watched"', $res );
-       }
-
-       private function getToken( User $user ) {
-               $message = 'token-' . $user->getName();
-               $res = $this->callApi( $message, array(
-                       'meta' => 'tokens',
-                       'type' => 'watch',
-               ), $user );
-               return $this->getVal( $message, '"query", "tokens", 
"watchtoken"', $res );
-       }
-
-       private function assertUsage( $message, $params, User $user = null ) {
-               try {
-                       $params = $this->toApiParams( $message, $params );
-                       $result = $this->callApi( $message, $params, $user );
-                       $params = $this->toStr( $params );
-                       $this->fail( "No UsageException for $params, 
received:\n" .
-                               $this->toStr( $result[0], true ) );
-               } catch ( UsageException $e ) {
-                       $this->assertTrue( true );
-               }
-       }
-
-       private function callApi( $message, $params, User $user = null ) {
-               $params = $this->toApiParams( $message, $params );
-               $res = $this->doApiRequest( $params, null, false, $user );
-               return $res[0];
-       }
-
-       private function toApiParams( $message, $params ) {
-               $params = $this->toArray( $message, $params );
-               if ( !isset( $params['action'] ) ) {
-                       $params['action'] = 'query';
-               }
-               if ( $params['action'] === 'query' && !isset( 
$params['continue'] ) ) {
-                       $params['continue'] = '';
-               }
-               return $params;
-       }
-
-       private function toArray( $message, $params ) {
-               if ( is_string( $params ) && $params ) {
-                       $p = $params[0] !== '[' && $params[0] !== '{' ? 
"[$params]" : $params;
-                       $st = FormatJson::parse( $p, FormatJson::FORCE_ASSOC );
-                       $this->assertTrue( $st->isOK(), 'invalid JSON value ' . 
$params, $message );
-                       $params = $st->getValue();
-               }
-               return $params;
-       }
-
-       private function toStr( $params, $pretty = false ) {
-               if ( is_string( $params ) ) {
-                       return $params;
-               }
-               return FormatJson::encode( $params, $pretty, FormatJson::ALL_OK 
);
-       }
-
-       private static function removeIds( $arr ) {
-               foreach ( $arr as &$v ) {
-                       if ( array_key_exists( 'id', $v ) && $v['id'] !== 0 ) {
-                               unset( $v['id'] );
-                       }
-               }
-               return $arr;
-       }
-
-       private function getVal( $message, $path, $result, $expectedValue = 
null ) {
-               $path = $this->toArray( $message, $path );
-               $res = $result;
-               foreach ( $path as $p ) {
-                       if ( !array_key_exists( $p, $res ) ) {
-                               $path = $this->toStr( $path );
-                               $this->fail( "$message: Request has no key $p 
of $path in result\n" .
-                                                        $this->toStr( $result, 
true ) );
-                       }
-                       $res = $res[$p];
-               }
-               if ( $expectedValue !== null ) {
-                       $this->assertEquals( $expectedValue, $res, $message );
-               }
-               return $res;
-       }
-}
diff --git a/tests/phpunit/api/GatherTests.php 
b/tests/phpunit/api/GatherTests.php
new file mode 100644
index 0000000..995e82d
--- /dev/null
+++ b/tests/phpunit/api/GatherTests.php
@@ -0,0 +1,827 @@
+<?php
+/**
+ * Copyright © 2015 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * These tests validate Gather API functionality
+ *
+ * @group API
+ * @group Database
+ * @group medium
+ * @covers ApiQuery
+ */
+class GatherTests extends ApiTestCase {
+       protected $exceptionFromAddDBData;
+
+       /** @var TestUser[] */
+       private static $wlUsers = null;
+
+       /**
+        * Create a set of pages. These must not change, otherwise the tests 
might give wrong results.
+        * @see MediaWikiTestCase::addDBData()
+        */
+       public function addDBData() {
+               try {
+                       if ( !Title::newFromText( 'Gather-All' )->exists() ) {
+                               $this->editPage( 'Gather-ListA', 'a' );
+                               $this->editPage( 'Gather-ListB', 'b' );
+                               $this->editPage( 'Gather-ListAB', 'ab' );
+                               $this->editPage( 'Gather-ListW', 'w' );
+                               $this->editPage( 'Gather-ListWA', 'wa' );
+                               $this->editPage( 'Gather-ListWAB', 'wab' );
+                       }
+               } catch ( Exception $e ) {
+                       $this->exceptionFromAddDBData = $e;
+               }
+       }
+
+       protected function setUp() {
+               parent::setUp();
+               if ( !self::$wlUsers ) {
+                       foreach ( array(
+                               'GatherML',
+                               'GatherML2',
+                               'GatherWML',
+                               'GatherWML2',
+                               'GatherWlOnly',
+                               'GatherWlOnly2',
+                               'GatherE1',
+                               'GatherE2',
+                       ) as $name ) {
+                               self::$wlUsers[$name] = new TestUser( $name );
+                       }
+               }
+               self::$users = array_merge( self::$users, self::$wlUsers );
+       }
+
+       public function testListEditAndPages() {
+               $n = 'GatherE1';
+               $n2 = 'GatherE2';
+               $nS = 'sysop';
+               $pageW = array( 'ns' => 0, 'title' => 'Gather-ListW' );
+               $pageTW = array( 'ns' => 1, 'title' => 'Talk:Gather-ListW' );
+               $pageWA = array( 'ns' => 0, 'title' => 'Gather-ListWA' );
+               $pageTWA = array( 'ns' => 1, 'title' => 'Talk:Gather-ListWA' );
+               $pageWAB = array( 'ns' => 0, 'title' => 'Gather-ListWAB' );
+               $pageTWAB = array( 'ns' => 1, 'title' => 'Talk:Gather-ListWAB' 
);
+               $pageAB = array( 'ns' => 0, 'title' => 'Gather-ListAB' );
+               $pageTAB = array( 'ns' => 1, 'title' => 'Talk:Gather-ListAB' );
+               $pageB = array( 'ns' => 0, 'title' => 'Gather-ListB' );
+               $pageTB = array( 'ns' => 1, 'title' => 'Talk:Gather-ListB' );
+
+               $usr = self::$users[$n]->getUser();
+               $usr2 = self::$users[$n2]->getUser();
+               $usrS = self::$users[$nS]->getUser();
+               $usrA = User::newFromId( 0 ); // Anonymous user
+
+               $token = $this->getToken( $usr );
+               $token2 = $this->getToken( $usr2 );
+               $tokenS = $this->getToken( $usrS );
+               $tokenA = $this->getToken( $usrA );
+
+               // Make sure there are no watchlists yet for these users 
(starting from clean slate)
+               foreach ( array( $usr, $usr2 ) as $user ) {
+                       $res = $this->getLists( 'ed-a0', $user, '{}' );
+                       $this->assertListsEquals( 'ed-a0', $res,
+                               '[{"id":0, "watchlist":true, 
"label":"Watchlist"}]' );
+                       $this->assertPages( 'ed-a1', $user, null, array(), 
array() );
+               }
+
+               $this->badUsePage( 'ed-a2', $usr, '"lspid": 9999999' );
+               $this->badUsePage( 'ed-a3', $usrA, '"lspid": 9999999' );
+               $this->badUsePage( 'ed-a4', $usrA, '{}' );
+
+               // General use
+               $this->badUseEdit( 'ed-b1', $usr, $token, '{}' );
+               $this->badUseEdit( 'ed-b2', $usr, $token, '"label": ""' );
+               // TODO/BUG/SECURITY - Token of one user should not be accepted 
for another user
+               // $this->badUseEdit( 'ed-b3', $u, $token2, '"label": "x"' );
+               $this->badUseEdit( 'ed-b4', $usr, $tokenA, '"label": "x"' );
+               $this->badUseEdit( 'ed-b5', $usr, false, '"label": "x"' );
+               $this->badUseEdit( 'ed-b6', $usrA, $tokenA, '"label": "x"' );
+               $this->badUseEdit( 'ed-b7', $usrA, false, '"label": "x"' );
+
+               // watchlist should not be modifiable this way
+               $idWL = '"id":0';
+               $this->badUseEdit( 'ed-ba1', $usr, false, $idWL . ', 
"description": "x"' );
+               $this->badUseEdit( 'ed-ba2', $usrA, false, $idWL . ', 
"description": "x"' );
+               $this->badUseEdit( 'ed-ba3', $usr, $token, $idWL . ', "label": 
"x"' );
+               $this->badUseEdit( 'ed-ba3a', $usr, $token, $idWL . ', "label": 
""' );
+               $this->badUseEdit( 'ed-ba4', $usrA, $tokenA, $idWL . ', 
"label": "x"' );
+               $this->badUseEdit( 'ed-ba5', $usr, $tokenA, $idWL . ', 
"description": "x"' );
+               // Test #ba6 is ok for ID=0, but not OK for non-zero (#b6)
+               $this->badUseEdit( 'ed-ba7', $usr, $token, $idWL . ', 
"deletelist": 1' );
+               $this->badUseEdit( 'ed-ba8', $usr, $token, $idWL . ', "perm": 
"public"' );
+
+               $expListsW = array( 'id' => 0, 'watchlist' => true, 'label' => 
'Watchlist' );
+               $expListsW2 = array_merge( $expListsW, array(
+                       'public' => false,
+                       'description' => '',
+                       'image' => false,
+                       'count' => 0,
+               ) );
+               $this->assertOneList( 'ed-bb1', $usr, 0, $expListsW, 
$expListsW2 );
+
+               //
+               // Add one page to the non-created watchlist
+               $res = $this->editList( 'ed-c1', $usr, $token, $idWL . ', 
"titles": "Gather-ListW"' );
+               $this->getVal( 'ed-c1', '"id"', $res, 0 );
+               $this->getVal( 'ed-c1', '"status"', $res, 'nochanges' );
+               $this->getVal( 'ed-c1', '"pages", 0, "title"', $res, 
'Gather-ListW' );
+               $this->getVal( 'ed-c1', '"pages", 0, "added"', $res, '' );
+
+               $expListsW2['count'] = 1;
+               $expPagesW = array( $pageW, $pageTW );
+
+               $this->assertPages( 'ed-c2', $usr, null, $expPagesW );
+               $this->assertPages( 'ed-c3', $usr, 0, $expPagesW );
+               $this->assertOneList( 'ed-c4', $usr, 0, $expListsW, $expListsW2 
);
+
+               //
+               // Create Watchlist row
+               $res = $this->editList( 'ed-d1', $usr, $token, '"id":0, 
"description": "x"' );
+               $id0 = $this->getVal( 'ed-d1', '"id"', $res );
+               $idWL = '"id":' . $id0;
+               $expListsW['id'] = $id0;
+               $expListsW2['id'] = $id0;
+               $expListsW2['description'] = 'x';
+
+               $this->assertPages( 'ed-d2', $usr, 0, $expPagesW );
+               $this->assertPages( 'ed-d3', $usr, $id0, $expPagesW );
+               $this->assertOneList( 'ed-d4', $usr, 0, $expListsW, $expListsW2 
);
+               $this->assertOneList( 'ed-d5', $usr, $id0, $expListsW, 
$expListsW2 );
+               $this->assertOneList( 'ed-d6', $usrA, $id0, null );
+               $this->assertOneList( 'ed-d7', $usr2, $id0, null );
+               $this->assertOneList( 'ed-d7a', $usrS, $id0, null );
+               $this->badUsePage( 'ed-d8', $usrA, '"lspid": ' . $id0 );
+               $this->badUsePage( 'ed-d9', $usr2, '"lspid": ' . $id0 );
+               $this->badUsePage( 'ed-d10', $usrS, '"lspid": ' . $id0 );
+
+               // watchlist should not be modifiable this way
+               $this->badUseEdit( 'ed-da1', $usr, false, $idWL . ', 
"description": "x"' );
+               $this->badUseEdit( 'ed-da2', $usrA, false, $idWL . ', 
"description": "x"' );
+               $this->badUseEdit( 'ed-da3', $usr, $token, $idWL . ', "label": 
"x"' );
+               $this->badUseEdit( 'ed-da4', $usr, $token, $idWL . ', "label": 
""' );
+               $this->badUseEdit( 'ed-da5', $usrA, $tokenA, $idWL . ', 
"label": "x"' );
+               $this->badUseEdit( 'ed-da6', $usr, $tokenA, $idWL . ', 
"description": "x"' );
+               $this->badUseEdit( 'ed-da7', $usr2, $token2, $idWL . ', 
"description": "x"' );
+               $this->badUseEdit( 'ed-da7a', $usrS, $tokenS, $idWL . ', 
"description": "x"' );
+               $this->badUseEdit( 'ed-da8', $usr, $token, $idWL . ', 
"deletelist": 1' );
+               $this->badUseEdit( 'ed-da9', $usr, $token, $idWL . ', "perm": 
"public"' );
+
+               //
+               // Add Gather-ListWA to the created watchlist
+               $res = $this->editList( 'ed-e1', $usr, $token, $idWL . ', 
"titles": "Gather-ListWA"' );
+               $this->getVal( 'ed-e1', '"id"', $res, $id0 );
+               $this->getVal( 'ed-e1', '"status"', $res, 'nochanges' );
+               $this->getVal( 'ed-e1', '"pages", 0, "title"', $res, 
'Gather-ListWA' );
+               $this->getVal( 'ed-e1', '"pages", 0, "added"', $res, '' );
+
+               $expListsW2['count'] = 2;
+               $expPagesW = array( $pageW, $pageWA, $pageTW, $pageTWA );
+
+               $this->assertPages( 'ed-e2', $usr, null, $expPagesW );
+               $this->assertPages( 'ed-e3', $usr, 0, $expPagesW );
+               $this->assertPages( 'ed-e4', $usr, $id0, $expPagesW );
+               $this->assertOneList( 'ed-e5', $usr, 0, $expListsW, $expListsW2 
);
+               $this->assertOneList( 'ed-e6', $usr, $id0, $expListsW, 
$expListsW2 );
+
+               //
+               // Add Gather-ListWAB to the created watchlist with ID=0 and 
description change
+               $res = $this->editList( 'ed-f1', $usr, $token,
+                       '"id":0, "description":"y", "titles": "Gather-ListWAB"' 
);
+               $this->getVal( 'ed-f1', '"id"', $res, $id0 );
+               $this->getVal( 'ed-f1', '"status"', $res, 'updated' );
+               $this->getVal( 'ed-f1', '"pages", 0, "title"', $res, 
'Gather-ListWAB' );
+               $this->getVal( 'ed-f1', '"pages", 0, "added"', $res, '' );
+
+               $expListsW2['count'] = 3;
+               $expListsW2['description'] = 'y';
+               $expPagesW = array( $pageW, $pageWA, $pageWAB, $pageTW, 
$pageTWA, $pageTWAB );
+
+               $this->assertPages( 'ed-f2', $usr, null, $expPagesW );
+               $this->assertPages( 'ed-f3', $usr, 0, $expPagesW );
+               $this->assertPages( 'ed-f4', $usr, $id0, $expPagesW );
+               $this->assertOneList( 'ed-f5', $usr, 0, $expListsW, $expListsW2 
);
+               $this->assertOneList( 'ed-f6', $usr, $id0, $expListsW, 
$expListsW2 );
+
+               //
+               // Create new list A
+               $res = $this->editList( 'ed-i1', $usr, $token, '"label": "A"' );
+               $this->getVal( 'ed-i1', '"status"', $res, 'created' );
+               $idA = $this->getVal( 'ed-i1', '"id"', $res );
+               $idAs = '"id":' . $idA;
+               $expListsA = array( 'id' => $idA, 'label' => 'A' );
+               $expListsA2 = array_merge( $expListsA, array(
+                       'public' => false,
+                       'description' => '',
+                       'image' => false,
+                       'count' => 0,
+               ) );
+               $expPagesA = array();
+
+               $this->assertPages( 'ed-i2', $usr, $idA, $expPagesA );
+               $this->assertOneList( 'ed-i3', $usr, $idA, $expListsA, 
$expListsA2 );
+               $this->assertOneList( 'ed-i4', $usrA, $idA, null );
+               $this->assertOneList( 'ed-i5', $usr2, $idA, null );
+               $this->assertOneList( 'ed-i6', $usrS, $idA, null );
+
+               $this->badUsePage( 'ed-ia1', $usrA, '"lspid": ' . $idA );
+               $this->badUsePage( 'ed-ia2', $usr2, '"lspid": ' . $idA );
+               $this->badUsePage( 'ed-ia2a', $usrS, '"lspid": ' . $idA );
+               $this->badUseEdit( 'ed-ia3', $usr, false, $idAs . ', 
"description": "x"' );
+               $this->badUseEdit( 'ed-ia4', $usrA, false, $idAs . ', 
"description": "x"' );
+               $this->badUseEdit( 'ed-ia5', $usr, $token, $idAs . ', "label": 
""' );
+               $this->badUseEdit( 'ed-ia6', $usrA, $tokenA, $idAs . ', 
"label": "x"' );
+               $this->badUseEdit( 'ed-ia7', $usr, $tokenA, $idAs . ', 
"description": "x"' );
+               $this->badUseEdit( 'ed-ia8', $usr2, $token2, $idAs . ', 
"description": "x"' );
+               $this->badUseEdit( 'ed-ia9', $usrS, $tokenS, $idAs . ', 
"description": "x"' );
+
+               //
+               // Rename list A to 'a'
+               $res = $this->editList( 'ed-j1', $usr, $token, $idAs . ', 
"label": "a"' );
+               $this->getVal( 'ed-j1', '"status"', $res, 'updated' );
+               $this->getVal( 'ed-j1', '"id"', $res, $idA );
+               $expListsA['label'] = 'a';
+               $expListsA2['label'] = 'a';
+
+               $this->assertPages( 'ed-j2', $usr, $idA, $expPagesA );
+               $this->assertOneList( 'ed-j3', $usr, $idA, $expListsA, 
$expListsA2 );
+               $this->assertOneList( 'ed-j4', $usrA, $id0, null );
+               $this->assertOneList( 'ed-j5', $usr2, $id0, null );
+               $this->assertOneList( 'ed-j6', $usrS, $id0, null );
+
+               $this->badUsePage( 'ed-ja1', $usrA, '"lspid": ' . $idA );
+               $this->badUsePage( 'ed-ja2', $usr2, '"lspid": ' . $idA );
+               $this->badUsePage( 'ed-ja2a', $usrS, '"lspid": ' . $idA );
+               $this->badUseEdit( 'ed-ja3', $usr, false, $idAs . ', 
"description": "x"' );
+               $this->badUseEdit( 'ed-ja4', $usrA, false, $idAs . ', 
"description": "x"' );
+               $this->badUseEdit( 'ed-ja5', $usr, $token, $idAs . ', "label": 
""' );
+               $this->badUseEdit( 'ed-ja6', $usrA, $tokenA, $idAs . ', 
"label": "x"' );
+               $this->badUseEdit( 'ed-ja7', $usr, $tokenA, $idAs . ', 
"description": "x"' );
+               $this->badUseEdit( 'ed-ja8', $usr2, $token2, $idAs . ', 
"description": "x"' );
+               $this->badUseEdit( 'ed-ja8a', $usrS, $tokenS, $idAs . ', 
"description": "x"' );
+               $this->badUseEdit( 'ed-ja9', $usrA, $token, $idAs . ', 
"deletelist": 1' );
+               $this->badUseEdit( 'ed-ja10', $usr2, $token, $idAs . ', 
"deletelist": 1' );
+               $this->badUseEdit( 'ed-ja11', $usrS, $token, $idAs . ', 
"deletelist": 1' );
+
+               //
+               // Make list a public
+               $res = $this->editList( 'ed-k1', $usr, $token, '"label": "a", 
"perm": "public"' );
+               $this->getVal( 'ed-k1', '"status"', $res, 'updated' );
+               $this->getVal( 'ed-k1', '"id"', $res, $idA );
+               $expListsA2['public'] = true;
+
+               $this->assertPages( 'ed-k2', $usr, $idA, $expPagesA );
+               $this->assertPages( 'ed-k2a', $usr2, $idA, $expPagesA );
+               $this->assertPages( 'ed-k2b', $usrA, $idA, $expPagesA );
+               $this->assertPages( 'ed-k2c', $usrS, $idA, $expPagesA );
+               $this->assertOneList( 'ed-k3', $usr, $idA, $expListsA, 
$expListsA2 );
+               $this->assertOneList( 'ed-k4', $usrA, $idA, $expListsA, 
$expListsA2 );
+               $this->assertOneList( 'ed-k5', $usr2, $idA, $expListsA, 
$expListsA2 );
+               $this->assertOneList( 'ed-k6', $usrS, $idA, $expListsA, 
$expListsA2 );
+
+               $this->badUseEdit( 'ed-ka1', $usr, false, $idAs . ', 
"description": "x"' );
+               $this->badUseEdit( 'ed-ka2', $usrA, false, $idAs . ', 
"description": "x"' );
+               $this->badUseEdit( 'ed-ka3', $usr, $token, $idAs . ', "label": 
""' );
+               $this->badUseEdit( 'ed-ka4', $usrA, $tokenA, $idAs . ', 
"label": "x"' );
+               $this->badUseEdit( 'ed-ka5', $usr, $tokenA, $idAs . ', 
"description": "x"' );
+               $this->badUseEdit( 'ed-ka6', $usr2, $token2, $idAs . ', 
"description": "x"' );
+               $this->badUseEdit( 'ed-ka6', $usrS, $tokenS, $idAs . ', 
"description": "x"' );
+               $this->badUseEdit( 'ed-ka7', $usrA, $tokenA, $idAs . ', 
"deletelist": 1' );
+               $this->badUseEdit( 'ed-ka8', $usr2, $token2, $idAs . ', 
"deletelist": 1' );
+               $this->badUseEdit( 'ed-ka9', $usr2, $token2, $idAs . ', "perm": 
"public"' );
+               $this->badUseEdit( 'ed-ka10', $usr2, $token2, $idAs . ', 
"label": "xx"' );
+               $this->badUseEdit( 'ed-ka11', $usrS, $tokenS, $idAs . ', 
"deletelist": 1' );
+               $this->badUseEdit( 'ed-ka12', $usrS, $tokenS, $idAs . ', 
"perm": "public"' );
+               $this->badUseEdit( 'ed-ka13', $usrS, $tokenS, $idAs . ', 
"label": "xx"' );
+
+               //
+               // Delete list A
+               $res = $this->editList( 'ed-l1', $usr, $token, $idAs . ', 
"deletelist": 1' );
+               $this->getVal( 'ed-l1', '"status"', $res, 'deleted' );
+               $this->getVal( 'ed-l1', '"id"', $res, $idA );
+
+               $this->badUsePage( 'ed-l2', $usr, '"lspid": ' . $idA );
+               $this->badUsePage( 'ed-l3', $usr2, '"lspid": ' . $idA );
+               $this->badUsePage( 'ed-l3a', $usrS, '"lspid": ' . $idA );
+               $this->badUsePage( 'ed-l4', $usrA, '"lspid": ' . $idA );
+               $this->assertOneList( 'ed-l5', $usr, $idA, null );
+               $this->assertOneList( 'ed-l6', $usrA, $idA, null );
+               $this->assertOneList( 'ed-l7', $usr2, $idA, null );
+               $this->assertOneList( 'ed-l7a', $usrS, $idA, null );
+
+               $this->badUseEdit( 'ed-l8', $usr, $token, $idAs . ', "titles": 
"ABC"' );
+               $this->badUseEdit( 'ed-l9', $usr, $token, $idAs . ', "label": 
"x"' );
+               $this->badUseEdit( 'ed-l10', $usr2, $token2, $idAs . ', 
"label": "xx"' );
+               $this->badUseEdit( 'ed-l11', $usrS, $tokenS, $idAs . ', 
"label": "xx"' );
+
+               //
+               // Create public list B
+               $res = $this->editList( 'ed-n1', $usr, $token,
+                       '"label": "B", "perm":"public", ' .
+                       '"titles":"Gather-ListB|Gather-ListAB|Gather-ListWAB"' 
);
+               $this->getVal( 'ed-n1', '"status"', $res, 'created' );
+               $idB = $this->getVal( 'ed-n1', '"id"', $res );
+               $idBs = '"id":' . $idB;
+               $expListsB = array( 'id' => $idB, 'label' => 'B' );
+               $expListsB2 = array_merge( $expListsB, array(
+                       'public' => true,
+                       'description' => '',
+                       'image' => false,
+                       'count' => 3,
+               ) );
+               // Non-alphabetic order should be preserved
+               $expPagesB = array( $pageB, $pageAB, $pageWAB );
+
+               $this->assertPages( 'ed-n2', $usr, $idB, $expPagesB );
+               $this->assertPages( 'ed-n3', $usr2, $idB, $expPagesB );
+               $this->assertPages( 'ed-n3a', $usrS, $idB, $expPagesB );
+               $this->assertPages( 'ed-n4', $usrA, $idB, $expPagesB );
+               $this->assertOneList( 'ed-n5', $usr, $idB, $expListsB, 
$expListsB2 );
+               $this->assertOneList( 'ed-n6', $usrA, $idB, $expListsB, 
$expListsB2 );
+               $this->assertOneList( 'ed-n7', $usr2, $idB, $expListsB, 
$expListsB2 );
+               $this->assertOneList( 'ed-n8', $usrS, $idB, $expListsB, 
$expListsB2 );
+       }
+
+       public function testMultipleLists() {
+               $this->intTestMultipleLists( false );
+       }
+
+       public function testMultipleListsWithWatchlist() {
+               $this->intTestMultipleLists( true );
+       }
+
+       private function intTestMultipleLists( $createWatchlist ) {
+               $p = $createWatchlist ? 'Test With watchlist: #' : 'Test 
without watchlist: #';
+               $n = $createWatchlist ? 'GatherWML' : 'GatherML';
+               $n2 = $createWatchlist ? 'GatherWML2' : 'GatherML2';
+
+               $a = User::newFromId( 0 ); // Anonymous user
+               $u = self::$users[$n]->getUser(); // User for this test
+               $u2 = self::$users[$n2]->getUser(); // Second user for this test
+
+               $token = $this->getToken( $u );
+               $n = '"' . $n . '"';
+
+               // Anonymous tests
+               $this->badUseLists( "$p 0", $a, '{}' );
+               $this->badUseLists( "$p 0", $a, '"lstids": 0' );
+
+               if ( $createWatchlist ) {
+                       // Create watchlist row
+                       $res = $this->editList( "$p a0", $u, $token, 
'"id":0,"description":"x"' );
+                       $wlId = $this->getVal( "$p a0", '"id"', $res );
+               } else {
+                       $wlId = 0;
+               }
+
+               // Add pages to various lists
+               $res = $this->editList( "$p a1", $u, $token,
+                       
'"id":0,"titles":"Gather-ListW|Gather-ListWA|Gather-ListWAB"' );
+               $this->getVal( "$p a1", '"status"', $res, 'nochanges' );
+               $this->getVal( "$p a1", '"pages",0,"added"', $res, '' );
+               $this->getVal( "$p a1", '"pages",1,"added"', $res, '' );
+               $this->getVal( "$p a1", '"pages",2,"added"', $res, '' );
+               $this->getVal( "$p a1", '"id"', $res, $wlId );
+
+               $res = $this->editList( "$p a2", $u, $token,
+                       '"label":"A","titles":"Gather-ListWA|Gather-ListWAB"' );
+               $this->getVal( "$p a2", '"status"', $res, 'created' );
+               $this->getVal( "$p a2", '"pages",0,"added"', $res, '' );
+               $this->getVal( "$p a2", '"pages",1,"added"', $res, '' );
+               $idA = $this->getVal( "$p a2", '"id"', $res );
+
+               $res = $this->editList( "$p a3", $u, $token,
+                       '"label":"B", "perm":"public", 
"titles":"Gather-ListWAB"' );
+               $this->getVal( "$p a3", '"status"', $res, 'created' );
+               $this->getVal( "$p a3", '"pages",0,"added"', $res, '' );
+               $idB = $this->getVal( "$p a3", '"id"', $res );
+
+               $res = $this->getLists( "$p b1", $u, '{}' );
+               $this->assertListNoId( "$p b1", $res, $wlId
+                       ? '[{"watchlist":true,"label":"Watchlist"}, 
{"label":"A"}, {"label":"B"}]'
+                       : '[{"id":0, "watchlist":true,"label":"Watchlist"}, 
{"label":"A"}, {"label":"B"}]' );
+               $this->getVal( "$p b1", '"query","lists",1,"id"', $res, $idA );
+               $this->getVal( "$p b1", '"query","lists",2,"id"', $res, $idB );
+
+               //
+               // Continuation
+               $request = $this->toApiParams( "$p c1", 'lists', false, 
'"lstlimit": 1' );
+
+               $res = $this->getLists( "$p c1", $u, $request );
+               $this->assertListsEquals( "$p c1a", $res,
+                       '[{"id":' . $wlId . ', 
"watchlist":true,"label":"Watchlist"}]' );
+               $continue = $this->getVal( "$p c1b", '"continue"', $res );
+
+               $res = $this->getLists( "$p c2", $u, array_merge( $continue, 
$request ) );
+               $this->assertListNoId( "$p c2a", $res, '[{"label":"A"}]' );
+               $continue = $this->getVal( "$p c2b", '"continue"', $res );
+
+               $res = $this->getLists( "$p c3", $u, array_merge( $continue, 
$request ) );
+               $this->assertListNoId( "$p c3a", $res, '[{"label":"B"}]' );
+               $this->assertArrayNotHasKey( 'continue', $res, "$p c3c" );
+
+               //
+               // ids=A
+               $res = $this->getLists( "$p d1", $u, '"lstids":' . $idA );
+               $this->assertListNoId( "$p d1", $res, '[{"label":"A"}]' );
+
+               // ids=A as anon user
+               $res = $this->getLists( "$p d2", $a, '"lstids":' . $idA );
+               $this->assertListNoId( "$p d2", $res, '[]' );
+
+               // ids=A as another user
+               $res = $this->getLists( "$p d3", $u2, '"lstids":' . $idA );
+               $this->assertListNoId( "$p d3", $res, '[]' );
+
+               //
+               // ids=B
+               $res = $this->getLists( "$p e1", $u, '"lstids":' . $idB );
+               $this->assertListNoId( "$p e1", $res, '[{"label":"B"}]' );
+
+               // ids=B as anon user
+               $res = $this->getLists( "$p e2", $a, '"lstids":' . $idB );
+               $this->assertListNoId( "$p e2", $res, '[{"label":"B"}]' );
+
+               // ids=B as another user
+               $res = $this->getLists( "$p e3", $u2, '"lstids":' . $idB );
+               $this->assertListNoId( "$p e3", $res, '[{"label":"B"}]' );
+
+               //
+               // Use owner param
+               // user: get all with owner=user
+               $res = $this->getLists( "$p i0", $u, '"lstowner":' . $n );
+               $this->assertListsEquals( "$p i0", $res,
+                       '[{"id":' . $wlId . ', 
"watchlist":true,"label":"Watchlist"}, ' .
+                       '{"id":' . $idA . ', "label":"A"},' . '{"id":' . $idB . 
', "label":"B"}]' );
+
+               // user: get by idA with owner=user
+               $res = $this->getLists( "$p i0a", $u,
+                       '"lstowner": ' . $n . ', "lstids": ' . $idA );
+               $this->assertListsEquals( "$p i0a", $res, '[{"id":' . $idA . ', 
"label":"A"}]' );
+
+               // anon: get all with owner=user
+               $res = $this->getLists( "$p i1", $a, '"lstowner":' . $n );
+               $this->assertListNoId( "$p i1", $res, '[{"label":"B"}]' );
+
+               // anon: get by idA with owner=user
+               $res = $this->getLists( "$p i2", $a, '"lstowner": ' . $n . ', 
"lstids": ' . $idA );
+               $this->assertListNoId( "$p i2", $res, '[]' );
+
+               // anon: get by idB with owner=user
+               $res = $this->getLists( "$p i3", $a, '"lstowner": ' . $n . ', 
"lstids": ' . $idB );
+               $this->assertListNoId( "$p i3", $res, '[{"label":"B"}]' );
+
+               // user2: get all with owner=user
+               $res = $this->getLists( "$p i4", $u2, '"lstowner":' . $n );
+               $this->assertListNoId( "$p i4", $res, '[{"label":"B"}]' );
+
+               // user2: get by idA with owner=user
+               $res = $this->getLists( "$p i5", $u2, '"lstowner": ' . $n . ', 
"lstids": ' . $idA );
+               $this->assertListNoId( "$p i5", $res, '[]' );
+
+               // user2: get by idB with owner=user
+               $res = $this->getLists( "$p i5", $u2, '"lstowner": ' . $n . ', 
"lstids": ' . $idB );
+               $this->assertListNoId( "$p i5", $res, '[{"label":"B"}]' );
+       }
+
+       public function testWatchlistOnly() {
+               $u = self::$users['GatherWlOnly']->getUser(); // User for this 
test
+               $a = User::newFromId( 0 ); // Anonymous user
+               $u2 = self::$users['GatherWlOnly2']->getUser(); // Second user 
for this test
+
+               $token = $this->getToken( $u );
+               $wlOnly = '[{"id":0, "watchlist":true, "label":"Watchlist"}]';
+               $n = '"' . $u->getName() . '"'; // Name of the user for this 
test
+
+               //
+               // Validate empty watchlist / lists
+               $res = $this->getLists( 'nc-a0', $u, '{}' );
+               $this->assertListNoId( 'nc-a0', $res, $wlOnly );
+
+               $res = $this->getLists( 'nc-a1', $u, '"lstids": 0' );
+               $this->assertListNoId( 'nc-a1', $res, $wlOnly );
+
+               $res = $this->getLists( 'nc-a2', $u, '"lstlimit": 1' );
+               $this->assertListNoId( 'nc-a2', $res, $wlOnly );
+
+               $res = $this->getLists( 'nc-a3', $u, '"lstprop": 
"label|description|public|count"' );
+               $this->assertListNoId( 'nc-a3', $res,
+                       
'[{"id":0,"watchlist":true,"count":0,"label":"Watchlist","description":"","public":false}]'
+               );
+               $res = $this->getLists( 'nc-a4', $u, '"lsttitle": "Missing"' );
+               $this->assertListNoId( 'nc-a4', $res,
+                       
'[{"id":0,"watchlist":true,"label":"Watchlist","title":false}]' );
+
+               //
+               // Add page to watchlist
+               $this->legacyAddToWatchlist( 'nc-b0', $u, $token, 
'Gather-ListW' );
+               $res = $this->getLists( 'nc-b0', $u, '"lstprop": "count"' );
+               $this->assertListNoId( 'nc-b0', $res, '[{"id": 0, 
"watchlist":true, "count": 1}]' );
+
+               $res = $this->getLists( 'nc-b1', $u, '"lsttitle": 
"Gather-ListW"' );
+               $this->assertListNoId( 'nc-b1', $res,
+                       
'[{"id":0,"watchlist":true,"label":"Watchlist","title":true}]' );
+
+               //
+               // Re-add the same page, using action=editlist & id=0
+               $res = $this->editList( 'nc-c0', $u, $token, 
'"id":0,"titles":"Gather-ListW"' );
+               $this->getVal( "nc-c0", '"status"', $res, 'nochanges' );
+               $this->getVal( "nc-c0", '"id"', $res, 0 );
+               $this->getVal( "nc-c0", '"pages",0,"added"', $res, '' );
+
+               $res = $this->getLists( 'nc-c0a', $u, '{}' );
+               $this->assertListNoId( 'nc-c0a', $res, $wlOnly );
+
+               $res = $this->getLists( 'nc-c1', $u, '"lstids": 0' );
+               $this->assertListNoId( 'nc-c1', $res, $wlOnly );
+
+               $res = $this->getLists( 'nc-c3', $u, '"lstprop": "count"' );
+               $this->assertListNoId( 'nc-c3', $res, '[{"id":0, 
"watchlist":true, "count": 1}]' );
+
+               $res = $this->getLists( 'nc-c4', $u, '"lsttitle": 
"Gather-ListW"' );
+               $this->assertListNoId( 'nc-c4', $res,
+                       
'[{"id":0,"watchlist":true,"label":"Watchlist","title":true}]' );
+
+               $res = $this->getLists( 'nc-c5', $u, '"lstids": 0, "lsttitle": 
"Gather-ListW"' );
+               $this->assertListNoId( 'nc-c5', $res,
+                       
'[{"id":0,"watchlist":true,"label":"Watchlist","title":true}]' );
+
+               //
+               // What can others see from this user
+               $res = $this->getLists( 'nc-e0', $a, '"lstowner":' . $n );
+               $this->assertListNoId( 'nc-e0', $res, '[]' );
+
+               $res = $this->getLists( 'nc-e1', $a, '"lstowner": ' . $n . ', 
"lstids": 0' );
+               $this->assertListNoId( 'nc-e1', $res, '[]' );
+
+               $res = $this->getLists( 'nc-e2', $u2, '"lstowner":' . $n );
+               $this->assertListNoId( 'nc-e2', $res, '[]' );
+
+               $res =  $this->getLists( 'nc-e3', $u2, '"lstowner": ' . $n . ', 
"lstids": 0' );
+               $this->assertListNoId( 'nc-e3', $res, '[]' );
+
+               //
+               // Create watchlist list DB record
+               $res = $this->editList( 'nc-f0', $u, $token, '"id":0, 
"description":"aa"' );
+               $this->getVal( 'nc-f0', '"status"', $res, 'created' );
+               $id = $this->getVal( 'nc-f0', '"id"', $res );
+               $this->assertNotEquals( 0, $id );
+
+               $wlOnly = array( array( 'id' => $id, 'watchlist' => true, 
'label' => 'Watchlist' ) );
+
+               $res = $this->getLists( 'nc-f2', $u, '{}' );
+               $this->assertListsEquals( 'nc-f2', $res, $wlOnly );
+
+               $res = $this->getLists( 'nc-f3', $u, '"lstids": 0' );
+               $this->assertListsEquals( 'nc-f3', $res, $wlOnly );
+
+               $res = $this->getLists( 'nc-f4', $u, '"lstprop": 
"label|description|public|count"' );
+               $this->assertListsEquals( 'nc-f4', $res,
+                       '[{"id":' . $id .
+                       
',"watchlist":true,"count":1,"label":"Watchlist","description":"aa","public":false}]'
 );
+
+               $res = $this->getLists( 'nc-f5', $u, '"lsttitle": 
"Gather-ListW"' );
+               $this->assertListsEquals( 'nc-f5', $res, '[{"id":' . $id .
+                       ',"watchlist":true,"label":"Watchlist","title":true}]' 
);
+
+               //
+               // Others still can't see the watchlist
+               $res = $this->getLists( 'nc-g0', $a, '"lstowner":' . $n );
+               $this->assertListNoId( 'nc-g0', $res, '[]' );
+
+               $res = $this->getLists( 'nc-g1', $a, '"lstowner": ' . $n . ', 
"lstids": 0' );
+               $this->assertListNoId( 'nc-g1', $res, '[]' );
+
+               $res = $this->getLists( 'nc-g2', $a, '"lstids": ' . $id );
+               $this->assertListNoId( 'nc-g2', $res, '[]' );
+
+               $res = $this->getLists( 'nc-g3', $a, '"lstowner": ' . $n . ', 
"lstids": ' . $id );
+               $this->assertListNoId( 'nc-g3', $res, '[]' );
+
+               $res = $this->getLists( 'nc-h0', $u2, '"lstowner":' . $n );
+               $this->assertListNoId( 'nc-h0', $res, '[]' );
+
+               $res = $this->getLists( 'nc-h1', $u2, '"lstowner": ' . $n . ', 
"lstids": 0' );
+               $this->assertListNoId( 'nc-h1', $res, '[]' );
+
+               $res = $this->getLists( 'nc-h2', $u2, '"lstids": ' . $id );
+               $this->assertListNoId( 'nc-h2', $res, '[]' );
+
+               $res = $this->getLists( 'nc-h3', $u2, '"lstowner": ' . $n . ', 
"lstids": ' . $id );
+               $this->assertListNoId( 'nc-h3', $res, '[]' );
+
+               //
+               // Watchlist editing assertions
+               $this->badUseEdit( 'nc-i0', $u, false, '"id":0, "label":"bb"' );
+               $this->badUseEdit( 'nc-i1', $u, false, '"id":' . $id . ', 
"label":"bb"' );
+       }
+
+       private function assertListNoId( $message, $actual, $exp ) {
+               $this->assertListsEquals( $message, $actual, $exp, true );
+       }
+
+       private function assertListsEquals( $message, $actual, $exp, $removeIds 
= false ) {
+               $actual = $this->getVal( $message, '"query", "lists"', $actual 
);
+               $exp = $this->toArr( $message, $exp );
+               if ( $removeIds ) {
+                       $actual = self::removeIds( $actual );
+               }
+               $this->assertArrayEquals( $exp, $actual, true, true, $message );
+       }
+
+       private function legacyAddToWatchlist( $message, $user, $token, $titles 
) {
+               $params = array(
+                       'action' => 'watch',
+                       'titles' => $titles,
+                       'token' => $token,
+               );
+               $res = $this->getLists( $message, $user, $params );
+               $this->getVal( $message, '"watch", 0, "watched"', $res );
+       }
+
+       private function getToken( User $user ) {
+               $message = 'token-' . $user->getName();
+               $res = $this->doApiRequest2( $message, $user, array(
+                       'action' => 'query',
+                       'meta' => 'tokens',
+                       'type' => 'watch',
+               ) );
+               return $this->getVal( $message, '0, "query", "tokens", 
"watchtoken"', $res );
+       }
+
+       private function badUseLists( $message, User $user, $params ) {
+               $this->badUse( $message, $user, 'lists', false, $params );
+       }
+
+       private function badUsePage( $message, User $user, $params ) {
+               $this->badUse( $message, $user, 'listpages', false, $params );
+       }
+
+       private function badUseEdit( $message, User $user, $token, $params ) {
+               $this->badUse( $message, $user, 'editlist', $token, $params );
+       }
+
+       private function badUse( $message, User $user, $action, $token, $params 
) {
+               try {
+                       $params = $this->toApiParams( $message, $action, 
$token, $params );
+                       $result = $this->doApiRequest( $params, null, false, 
$user );
+                       $params = $this->toStr( $params );
+                       $this->fail( "$message: No UsageException for $params, 
received:\n" .
+                               $this->toStr( $result[0], true ) );
+               } catch ( UsageException $e ) {
+                       $this->assertTrue( true );
+               }
+       }
+
+       private function editList( $message, $user, $token, $params ) {
+               $params = $this->toApiParams( $message, 'editlist', $token, 
$params );
+               $res = $this->doApiRequest2( $message, $user, $params );
+               return $this->getVal( $message, array( $params['action'] ), 
$res[0] );
+       }
+
+       private function getLists( $message, User $user, $params ) {
+               $params = $this->toApiParams( $message, 'lists', false, $params 
);
+               $res = $this->doApiRequest2( $message, $user, $params );
+               return $res[0];
+       }
+
+       private function getPages( $message, User $user, $params ) {
+               $params = $this->toApiParams( $message, 'listpages', false, 
$params );
+               $res = $this->doApiRequest2( $message, $user, $params );
+               return $res[0];
+       }
+
+       private function doApiRequest2( $message, User $user, array $params ) {
+               try {
+                       return parent::doApiRequest( $params, null, false, 
$user );
+               } catch ( Exception $ex ) {
+                       echo "Failed API call $message\n";
+                       throw $ex;
+               }
+       }
+
+
+       private function toApiParams( $message, $default, $token, $params ) {
+               $params = $this->toArr( $message, $params, true );
+               if ( !isset( $params['action'] ) ) {
+                       $params['action'] = $default === 'editlist' ? $default 
: 'query';
+               }
+               if ( $params['action'] === 'query' ) {
+                       if ( !isset( $params['list'] ) ) {
+                               $params['list'] = $default;
+                       }
+                       if ( !isset( $params['continue'] ) ) {
+                               $params['continue'] = '';
+                       }
+               }
+               if ( $token && !isset( $params['token'] ) ) {
+                       $params['token'] = $token;
+               }
+               return $params;
+       }
+
+       private function toArr( $message, $params, $dictByDefault = false ) {
+               if ( is_string( $params ) && $params ) {
+                       $p = $params;
+                       if ( $p[0] !== '[' && $p[0] !== '{' ) {
+                               $p = $dictByDefault ? '{' . $params . '}' : 
"[$params]";
+                       }
+                       $st = FormatJson::parse( $p, FormatJson::FORCE_ASSOC );
+                       $this->assertTrue( $st->isOK(), "$message: invalid JSON 
value $params" );
+                       $params = $st->getValue();
+               }
+               return $params;
+       }
+
+       private function toStr( $params, $pretty = false ) {
+               if ( is_string( $params ) ) {
+                       return $params;
+               }
+               return FormatJson::encode( $params, $pretty, FormatJson::ALL_OK 
);
+       }
+
+       private static function removeIds( $arr ) {
+               foreach ( $arr as &$v ) {
+                       if ( array_key_exists( 'id', $v ) && $v['id'] !== 0 ) {
+                               unset( $v['id'] );
+                       }
+               }
+               return $arr;
+       }
+
+       private function getVal( $message, $path, $result, $expValue = null ) {
+               $path = $this->toArr( $message, $path );
+               $res = $result;
+               foreach ( $path as $p ) {
+                       if ( !array_key_exists( $p, $res ) ) {
+                               $path = $this->toStr( $path );
+                               $this->fail( "$message: Request has no key $p 
of $path in result\n" .
+                                       $this->toStr( $result, true ) );
+                       }
+                       $res = $res[$p];
+               }
+               if ( $expValue !== null ) {
+                       $this->assertEquals( $expValue, $res, $message );
+               }
+               return $res;
+       }
+
+       /**
+        * Debugging function to track the sate of the table during test 
development
+        * @param string $table
+        */
+       private function dumpTable( $table ) {
+               echo "\nTable dump $table:\n";
+               foreach ( $this->db->select( $table, '*' ) as $row ) {
+                       echo $this->toStr( $row ) . "\n";
+               }
+               echo "\nEnd of the table dump $table\n";
+       }
+
+       private function assertOneList( $message, $u, $id, $expected, 
$expectedProp = null ) {
+               $params = '"lstids":' . $id;
+               $res = $this->getLists( $message, $u, $params );
+               $lst = $this->getVal( $message, '"query", "lists"', $res );
+               if ( $expected === null ) {
+                       $this->assertCount( 0, $lst, $message );
+               } else {
+                       $this->assertCount( 1, $lst, $message );
+                       $this->assertEquals( $expected, $lst[0], $message );
+               }
+
+               if ( $expectedProp ) {
+                       $params .= ', 
"lstprop":"label|description|public|image|count"';
+                       $message .= '-p';
+                       $res = $this->getLists( $message, $u, $params );
+                       $lst = $this->getVal( $message, '"query", "lists"', 
$res );
+                       $this->assertCount( 1, $lst, $message );
+                       $this->assertEquals( $expectedProp, $lst[0], $message );
+               }
+       }
+
+       private function assertPages( $message, $u, $id, $expected ) {
+               $params = $id === null ? '{}' : '"lspid":' . $id;
+               $res = $this->getPages( $message, $u, $params );
+               $this->getVal( $message, '"listpages"', $res, $expected );
+       }
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/198667
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I3e08d17bf0d83f0d813eccee2d27feeccd357a9b
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Gather
Gerrit-Branch: master
Gerrit-Owner: Yurik <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to