2020-11-23 14:31:50 +00:00
< ? php
/** @ license MIT
* Copyright 2017 J . King , Dustin Wilson et al .
* See LICENSE and AUTHORS files for details */
declare ( strict_types = 1 );
2020-12-01 17:08:45 +00:00
namespace JKingWeb\Arsse\TestCase\REST\Miniflux ;
2020-11-23 14:31:50 +00:00
use JKingWeb\Arsse\Arsse ;
2020-12-14 17:41:09 +00:00
use JKingWeb\Arsse\Context\Context ;
2020-11-23 14:31:50 +00:00
use JKingWeb\Arsse\User ;
use JKingWeb\Arsse\Database ;
use JKingWeb\Arsse\Db\Transaction ;
2020-11-30 15:52:32 +00:00
use JKingWeb\Arsse\Db\ExceptionInput ;
2020-12-10 04:39:29 +00:00
use JKingWeb\Arsse\Misc\Date ;
2020-11-23 14:31:50 +00:00
use JKingWeb\Arsse\REST\Miniflux\V1 ;
2020-11-30 15:52:32 +00:00
use JKingWeb\Arsse\REST\Miniflux\ErrorResponse ;
2020-12-10 04:39:29 +00:00
use JKingWeb\Arsse\User\ExceptionConflict ;
2020-11-23 14:31:50 +00:00
use Psr\Http\Message\ResponseInterface ;
2020-11-30 15:52:32 +00:00
use Laminas\Diactoros\Response\JsonResponse as Response ;
use Laminas\Diactoros\Response\EmptyResponse ;
2020-12-12 04:47:13 +00:00
use JKingWeb\Arsse\Test\Result ;
2020-11-23 14:31:50 +00:00
/** @covers \JKingWeb\Arsse\REST\Miniflux\V1<extended> */
class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
2020-12-11 04:19:26 +00:00
protected const NOW = " 2020-12-09T22:35:10.023419Z " ;
2020-11-23 14:31:50 +00:00
protected $h ;
protected $transaction ;
2020-11-30 15:52:32 +00:00
protected $token = " Tk2o9YubmZIL2fm2w8Z4KlDEQJz532fNSOcTG0s2_xc= " ;
2020-12-11 04:19:26 +00:00
protected $users = [
[
'id' => 1 ,
'username' => " john.doe@example.com " ,
'is_admin' => true ,
'theme' => " custom " ,
'language' => " fr_CA " ,
'timezone' => " Asia/Gaza " ,
'entry_sorting_direction' => " asc " ,
'entries_per_page' => 200 ,
'keyboard_shortcuts' => false ,
'show_reading_time' => false ,
'last_login_at' => self :: NOW ,
'entry_swipe' => false ,
'extra' => [
'custom_css' => " p { } " ,
],
],
[
'id' => 2 ,
'username' => " jane.doe@example.com " ,
'is_admin' => false ,
'theme' => " light_serif " ,
'language' => " en_US " ,
'timezone' => " UTC " ,
'entry_sorting_direction' => " desc " ,
'entries_per_page' => 100 ,
'keyboard_shortcuts' => true ,
'show_reading_time' => true ,
'last_login_at' => self :: NOW ,
'entry_swipe' => true ,
'extra' => [
'custom_css' => " " ,
],
]
];
2020-11-23 14:31:50 +00:00
2020-12-11 04:19:26 +00:00
protected function req ( string $method , string $target , $data = " " , array $headers = [], ? string $user = " john.doe@example.com " , bool $body = true ) : ResponseInterface {
2020-11-23 14:31:50 +00:00
$prefix = " /v1 " ;
$url = $prefix . $target ;
if ( $body ) {
$params = [];
} else {
$params = $data ;
$data = [];
}
2020-12-11 04:19:26 +00:00
$req = $this -> serverRequest ( $method , $url , $prefix , $headers , [], $data , " application/json " , $params , $user );
2020-11-23 14:31:50 +00:00
return $this -> h -> dispatch ( $req );
}
public function setUp () : void {
self :: clearData ();
self :: setConf ();
2020-12-12 04:47:13 +00:00
// create a mock user manager; we use a PHPUnitmock because Phake for reasons unknown is unable to mock the User class correctly, sometimes
Arsse :: $user = $this -> createMock ( User :: class );
Arsse :: $user -> method ( " propertiesGet " ) -> willReturn ([ 'num' => 42 , 'admin' => false , 'root_folder_name' => null ]);
2020-11-23 14:31:50 +00:00
// create a mock database interface
Arsse :: $db = \Phake :: mock ( Database :: class );
$this -> transaction = \Phake :: mock ( Transaction :: class );
\Phake :: when ( Arsse :: $db ) -> begin -> thenReturn ( $this -> transaction );
//initialize a handler
$this -> h = new V1 ();
}
public function tearDown () : void {
self :: clearData ();
}
protected function v ( $value ) {
return $value ;
}
2020-11-26 13:42:35 +00:00
/** @dataProvider provideAuthResponses */
2020-11-30 15:52:32 +00:00
public function testAuthenticateAUser ( $token , bool $auth , bool $success ) : void {
2020-12-01 16:06:29 +00:00
$exp = $success ? new EmptyResponse ( 404 ) : new ErrorResponse ( " 401 " , 401 );
2020-11-30 15:52:32 +00:00
$user = " john.doe@example.com " ;
if ( $token !== null ) {
$headers = [ 'X-Auth-Token' => $token ];
} else {
$headers = [];
}
Arsse :: $user -> id = null ;
\Phake :: when ( Arsse :: $db ) -> tokenLookup -> thenThrow ( new ExceptionInput ( " subjectMissing " ));
\Phake :: when ( Arsse :: $db ) -> tokenLookup ( " miniflux.login " , $this -> token ) -> thenReturn ([ 'user' => $user ]);
2020-12-11 04:19:26 +00:00
$this -> assertMessage ( $exp , $this -> req ( " GET " , " / " , " " , $headers , $auth ? " john.doe@example.com " : null ));
2020-12-01 16:06:29 +00:00
$this -> assertSame ( $success ? $user : null , Arsse :: $user -> id );
2020-11-30 15:52:32 +00:00
}
public function provideAuthResponses () : iterable {
return [
[ null , false , false ],
[ null , true , true ],
[ $this -> token , false , true ],
[[ $this -> token , " BOGUS " ], false , true ],
[ " " , true , true ],
[[ " " , " BOGUS " ], true , true ],
[ " NOT A TOKEN " , false , false ],
[ " NOT A TOKEN " , true , false ],
[[ " BOGUS " , $this -> token ], false , false ],
[[ " " , $this -> token ], false , false ],
];
2020-11-23 14:31:50 +00:00
}
/** @dataProvider provideInvalidPaths */
2020-12-01 16:06:29 +00:00
public function testRespondToInvalidPaths ( $path , $method , $code , $allow = null ) : void {
2020-11-23 14:31:50 +00:00
$exp = new EmptyResponse ( $code , $allow ? [ 'Allow' => $allow ] : []);
$this -> assertMessage ( $exp , $this -> req ( $method , $path ));
}
public function provideInvalidPaths () : array {
return [
[ " / " , " GET " , 404 ],
2020-12-01 17:08:45 +00:00
[ " / " , " OPTIONS " , 404 ],
2020-12-01 16:06:29 +00:00
[ " /me " , " POST " , 405 , " GET " ],
2020-12-01 17:08:45 +00:00
[ " /me/ " , " GET " , 404 ],
2020-11-23 14:31:50 +00:00
];
}
/** @dataProvider provideOptionsRequests */
2020-12-01 17:08:45 +00:00
public function testRespondToOptionsRequests ( string $url , string $allow , string $accept ) : void {
2020-11-23 14:31:50 +00:00
$exp = new EmptyResponse ( 204 , [
'Allow' => $allow ,
'Accept' => $accept ,
]);
$this -> assertMessage ( $exp , $this -> req ( " OPTIONS " , $url ));
}
public function provideOptionsRequests () : array {
return [
2020-12-01 17:08:45 +00:00
[ " /feeds " , " HEAD, GET, POST " , " application/json " ],
[ " /feeds/2112 " , " HEAD, GET, PUT, DELETE " , " application/json " ],
[ " /me " , " HEAD, GET " , " application/json " ],
[ " /users/someone " , " HEAD, GET " , " application/json " ],
[ " /import " , " POST " , " application/xml, text/xml, text/x-opml " ],
2020-11-23 14:31:50 +00:00
];
}
2020-12-02 23:00:27 +00:00
public function testRejectBadlyTypedData () : void {
2020-12-11 18:31:35 +00:00
$exp = new ErrorResponse ([ " InvalidInputType " , 'field' => " url " , 'expected' => " string " , 'actual' => " integer " ], 400 );
2020-12-02 23:00:27 +00:00
$this -> assertMessage ( $exp , $this -> req ( " POST " , " /discover " , [ 'url' => 2112 ]));
}
public function testDiscoverFeeds () : void {
$exp = new Response ([
[ 'title' => " Feed " , 'type' => " rss " , 'url' => " http://localhost:8000/Feed/Discovery/Feed " ],
[ 'title' => " Feed " , 'type' => " rss " , 'url' => " http://localhost:8000/Feed/Discovery/Missing " ],
]);
$this -> assertMessage ( $exp , $this -> req ( " POST " , " /discover " , [ 'url' => " http://localhost:8000/Feed/Discovery/Valid " ]));
$exp = new Response ([]);
$this -> assertMessage ( $exp , $this -> req ( " POST " , " /discover " , [ 'url' => " http://localhost:8000/Feed/Discovery/Invalid " ]));
2020-12-11 18:31:35 +00:00
$exp = new ErrorResponse ( " Fetch404 " , 500 );
2020-12-02 23:00:27 +00:00
$this -> assertMessage ( $exp , $this -> req ( " POST " , " /discover " , [ 'url' => " http://localhost:8000/Feed/Discovery/Missing " ]));
}
2020-12-10 04:39:29 +00:00
2020-12-11 04:19:26 +00:00
/** @dataProvider provideUserQueries */
public function testQueryUsers ( bool $admin , string $route , ResponseInterface $exp ) : void {
2020-12-10 04:39:29 +00:00
$u = [
[ 'num' => 1 , 'admin' => true , 'theme' => " custom " , 'lang' => " fr_CA " , 'tz' => " Asia/Gaza " , 'sort_asc' => true , 'page_size' => 200 , 'shortcuts' => false , 'reading_time' => false , 'swipe' => false , 'stylesheet' => " p { } " ],
[ 'num' => 2 , 'admin' => false , 'theme' => null , 'lang' => null , 'tz' => null , 'sort_asc' => null , 'page_size' => null , 'shortcuts' => null , 'reading_time' => null , 'swipe' => null , 'stylesheet' => null ],
new ExceptionConflict ( " doesNotExist " ),
];
2020-12-11 04:19:26 +00:00
$user = $admin ? " john.doe@example.com " : " jane.doe@example.com " ;
2020-12-10 04:39:29 +00:00
// FIXME: Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead
Arsse :: $user = $this -> createMock ( User :: class );
Arsse :: $user -> method ( " list " ) -> willReturn ([ " john.doe@example.com " , " jane.doe@example.com " , " admin@example.com " ]);
Arsse :: $user -> method ( " propertiesGet " ) -> willReturnCallback ( function ( string $user , bool $includeLerge = true ) use ( $u ) {
if ( $user === " john.doe@example.com " ) {
return $u [ 0 ];
} elseif ( $user === " jane.doe@example.com " ) {
return $u [ 1 ];
2020-12-11 01:08:00 +00:00
} else {
throw $u [ 2 ];
}
});
Arsse :: $user -> method ( " lookup " ) -> willReturnCallback ( function ( int $num ) use ( $u ) {
if ( $num === 1 ) {
return " john.doe@example.com " ;
} elseif ( $num === 2 ) {
return " jane.doe@example.com " ;
} else {
2020-12-10 04:39:29 +00:00
throw $u [ 2 ];
}
});
$this -> h = $this -> createPartialMock ( V1 :: class , [ " now " ]);
2020-12-11 04:19:26 +00:00
$this -> h -> method ( " now " ) -> willReturn ( Date :: normalize ( self :: NOW ));
$this -> assertMessage ( $exp , $this -> req ( " GET " , $route , " " , [], $user ));
}
public function provideUserQueries () : iterable {
self :: clearData ();
return [
[ true , " /users " , new Response ( $this -> users )],
[ true , " /me " , new Response ( $this -> users [ 0 ])],
[ true , " /users/john.doe@example.com " , new Response ( $this -> users [ 0 ])],
[ true , " /users/1 " , new Response ( $this -> users [ 0 ])],
[ true , " /users/jane.doe@example.com " , new Response ( $this -> users [ 1 ])],
[ true , " /users/2 " , new Response ( $this -> users [ 1 ])],
[ true , " /users/jack.doe@example.com " , new ErrorResponse ( " 404 " , 404 )],
[ true , " /users/47 " , new ErrorResponse ( " 404 " , 404 )],
[ false , " /users " , new ErrorResponse ( " 403 " , 403 )],
[ false , " /me " , new Response ( $this -> users [ 1 ])],
[ false , " /users/john.doe@example.com " , new ErrorResponse ( " 403 " , 403 )],
[ false , " /users/1 " , new ErrorResponse ( " 403 " , 403 )],
[ false , " /users/jane.doe@example.com " , new ErrorResponse ( " 403 " , 403 )],
[ false , " /users/2 " , new ErrorResponse ( " 403 " , 403 )],
[ false , " /users/jack.doe@example.com " , new ErrorResponse ( " 403 " , 403 )],
[ false , " /users/47 " , new ErrorResponse ( " 403 " , 403 )],
];
2020-12-10 04:39:29 +00:00
}
2020-12-12 04:47:13 +00:00
public function testListCategories () : void {
\Phake :: when ( Arsse :: $db ) -> folderList -> thenReturn ( new Result ( $this -> v ([
[ 'id' => 1 , 'name' => " Science " ],
[ 'id' => 20 , 'name' => " Technology " ],
])));
$exp = new Response ([
[ 'id' => 1 , 'title' => " All " , 'user_id' => 42 ],
[ 'id' => 2 , 'title' => " Science " , 'user_id' => 42 ],
[ 'id' => 21 , 'title' => " Technology " , 'user_id' => 42 ],
]);
$this -> assertMessage ( $exp , $this -> req ( " GET " , " /categories " ));
\Phake :: verify ( Arsse :: $db ) -> folderList ( " john.doe@example.com " , null , false );
// run test again with a renamed root folder
Arsse :: $user = $this -> createMock ( User :: class );
Arsse :: $user -> method ( " propertiesGet " ) -> willReturn ([ 'num' => 47 , 'admin' => false , 'root_folder_name' => " Uncategorized " ]);
$exp = new Response ([
[ 'id' => 1 , 'title' => " Uncategorized " , 'user_id' => 47 ],
[ 'id' => 2 , 'title' => " Science " , 'user_id' => 47 ],
[ 'id' => 21 , 'title' => " Technology " , 'user_id' => 47 ],
]);
$this -> assertMessage ( $exp , $this -> req ( " GET " , " /categories " ));
}
/** @dataProvider provideCategoryAdditions */
public function testAddACategory ( $title , ResponseInterface $exp ) : void {
if ( ! strlen (( string ) $title )) {
\Phake :: when ( Arsse :: $db ) -> folderAdd -> thenThrow ( new ExceptionInput ( " missing " ));
} elseif ( ! strlen ( trim (( string ) $title ))) {
\Phake :: when ( Arsse :: $db ) -> folderAdd -> thenThrow ( new ExceptionInput ( " whitespace " ));
} elseif ( $title === " Duplicate " ) {
\Phake :: when ( Arsse :: $db ) -> folderAdd -> thenThrow ( new ExceptionInput ( " constraintViolation " ));
} else {
\Phake :: when ( Arsse :: $db ) -> folderAdd -> thenReturn ( 2111 );
}
$this -> assertMessage ( $exp , $this -> req ( " POST " , " /categories " , [ 'title' => $title ]));
}
public function provideCategoryAdditions () : iterable {
return [
[ " New " , new Response ([ 'id' => 2112 , 'title' => " New " , 'user_id' => 42 ])],
[ " Duplicate " , new ErrorResponse ([ " DuplicateCategory " , 'title' => " Duplicate " ], 500 )],
[ " " , new ErrorResponse ([ " InvalidCategory " , 'title' => " " ], 500 )],
[ " " , new ErrorResponse ([ " InvalidCategory " , 'title' => " " ], 500 )],
[ false , new ErrorResponse ([ " InvalidInputType " , 'field' => " title " , 'actual' => " boolean " , 'expected' => " string " ], 400 )],
];
}
2020-12-13 17:56:57 +00:00
/** @dataProvider provideCategoryUpdates */
2020-12-14 03:10:34 +00:00
public function testRenameACategory ( int $id , $title , $out , ResponseInterface $exp ) : void {
2020-12-13 17:56:57 +00:00
Arsse :: $user -> method ( " propertiesSet " ) -> willReturn ([ 'root_folder_name' => $title ]);
2020-12-14 03:10:34 +00:00
if ( is_string ( $out )) {
\Phake :: when ( Arsse :: $db ) -> folderPropertiesSet -> thenThrow ( new ExceptionInput ( $out ));
2020-12-13 17:56:57 +00:00
} else {
2020-12-14 03:10:34 +00:00
\Phake :: when ( Arsse :: $db ) -> folderPropertiesSet -> thenReturn ( $out );
2020-12-13 17:56:57 +00:00
}
2020-12-14 03:10:34 +00:00
$times = ( int ) ( $id === 1 && is_string ( $title ) && strlen ( trim ( $title )));
Arsse :: $user -> expects ( $this -> exactly ( $times )) -> method ( " propertiesSet " ) -> with ( " john.doe@example.com " , [ 'root_folder_name' => $title ]);
2020-12-13 17:56:57 +00:00
$this -> assertMessage ( $exp , $this -> req ( " PUT " , " /categories/ $id " , [ 'title' => $title ]));
2020-12-14 03:10:34 +00:00
$times = ( int ) ( $id !== 1 && is_string ( $title ));
\Phake :: verify ( Arsse :: $db , \Phake :: times ( $times )) -> folderPropertiesSet ( " john.doe@example.com " , $id - 1 , [ 'name' => $title ]);
2020-12-13 17:56:57 +00:00
}
public function provideCategoryUpdates () : iterable {
return [
2020-12-14 03:10:34 +00:00
[ 3 , " New " , " subjectMissing " , new ErrorResponse ( " 404 " , 404 )],
[ 2 , " New " , true , new Response ([ 'id' => 2 , 'title' => " New " , 'user_id' => 42 ])],
[ 2 , " Duplicate " , " constraintViolation " , new ErrorResponse ([ " DuplicateCategory " , 'title' => " Duplicate " ], 500 )],
[ 2 , " " , " missing " , new ErrorResponse ([ " InvalidCategory " , 'title' => " " ], 500 )],
[ 2 , " " , " whitespace " , new ErrorResponse ([ " InvalidCategory " , 'title' => " " ], 500 )],
[ 2 , false , " subjectMissing " , new ErrorResponse ([ " InvalidInputType " , 'field' => " title " , 'actual' => " boolean " , 'expected' => " string " ], 400 )],
[ 1 , " New " , true , new Response ([ 'id' => 1 , 'title' => " New " , 'user_id' => 42 ])],
[ 1 , " Duplicate " , " constraintViolation " , new Response ([ 'id' => 1 , 'title' => " Duplicate " , 'user_id' => 42 ])], // This is allowed because the name of the root folder is only a duplicate in circumstances where it is used
[ 1 , " " , " missing " , new ErrorResponse ([ " InvalidCategory " , 'title' => " " ], 500 )],
[ 1 , " " , " whitespace " , new ErrorResponse ([ " InvalidCategory " , 'title' => " " ], 500 )],
[ 1 , false , false , new ErrorResponse ([ " InvalidInputType " , 'field' => " title " , 'actual' => " boolean " , 'expected' => " string " ], 400 )],
2020-12-13 17:56:57 +00:00
];
}
2020-12-14 03:10:34 +00:00
public function testDeleteARealCategory () : void {
\Phake :: when ( Arsse :: $db ) -> folderRemove -> thenReturn ( true ) -> thenThrow ( new ExceptionInput ( " subjectMissing " ));
$this -> assertMessage ( new EmptyResponse ( 204 ), $this -> req ( " DELETE " , " /categories/2112 " ));
\Phake :: verify ( Arsse :: $db ) -> folderRemove ( " john.doe@example.com " , 2111 );
$this -> assertMessage ( new ErrorResponse ( " 404 " , 404 ), $this -> req ( " DELETE " , " /categories/47 " ));
\Phake :: verify ( Arsse :: $db ) -> folderRemove ( " john.doe@example.com " , 46 );
}
public function testDeleteTheSpecialCategory () : void {
\Phake :: when ( Arsse :: $db ) -> subscriptionList -> thenReturn ( new Result ( $this -> v ([
[ 'id' => 1 ],
[ 'id' => 47 ],
[ 'id' => 2112 ],
])));
\Phake :: when ( Arsse :: $db ) -> subscriptionRemove -> thenReturn ( true );
$this -> assertMessage ( new EmptyResponse ( 204 ), $this -> req ( " DELETE " , " /categories/1 " ));
\Phake :: inOrder (
\Phake :: verify ( Arsse :: $db ) -> begin (),
\Phake :: verify ( Arsse :: $db ) -> subscriptionList ( " john.doe@example.com " , null , false ),
\Phake :: verify ( Arsse :: $db ) -> subscriptionRemove ( " john.doe@example.com " , 1 ),
\Phake :: verify ( Arsse :: $db ) -> subscriptionRemove ( " john.doe@example.com " , 47 ),
\Phake :: verify ( Arsse :: $db ) -> subscriptionRemove ( " john.doe@example.com " , 2112 ),
\Phake :: verify ( $this -> transaction ) -> commit ()
);
}
2020-12-14 17:41:09 +00:00
public function testMarkACategoryAsRead () : void {
\Phake :: when ( Arsse :: $db ) -> articleMark -> thenReturn ( 1 ) -> thenReturn ( 1 ) -> thenThrow ( new ExceptionInput ( " idMissing " ));
$this -> assertMessage ( new EmptyResponse ( 204 ), $this -> req ( " PUT " , " /categories/2/mark-all-as-read " ));
$this -> assertMessage ( new EmptyResponse ( 204 ), $this -> req ( " PUT " , " /categories/1/mark-all-as-read " ));
$this -> assertMessage ( new ErrorResponse ( " 404 " , 404 ), $this -> req ( " PUT " , " /categories/2112/mark-all-as-read " ));
\Phake :: inOrder (
\Phake :: verify ( Arsse :: $db ) -> articleMark ( " john.doe@example.com " , [ 'read' => true ], ( new Context ) -> folder ( 1 )),
\Phake :: verify ( Arsse :: $db ) -> articleMark ( " john.doe@example.com " , [ 'read' => true ], ( new Context ) -> folderShallow ( 0 )),
\Phake :: verify ( Arsse :: $db ) -> articleMark ( " john.doe@example.com " , [ 'read' => true ], ( new Context ) -> folder ( 2111 ))
);
}
2020-11-23 14:31:50 +00:00
}