2017-07-08 01:06:38 +00:00
< ? php
2017-11-17 01:23:18 +00:00
/** @ license MIT
* Copyright 2017 J . King , Dustin Wilson et al .
* See LICENSE and AUTHORS files for details */
2017-07-08 01:06:38 +00:00
declare ( strict_types = 1 );
2021-04-14 15:17:01 +00:00
2017-07-08 01:06:38 +00:00
namespace JKingWeb\Arsse\Test ;
2017-08-29 14:50:31 +00:00
2021-02-27 20:24:02 +00:00
use Eloquent\Phony\Mock\Handle\InstanceHandle ;
use Eloquent\Phony\Phpunit\Phony ;
2020-01-24 20:54:08 +00:00
use GuzzleHttp\Exception\GuzzleException ;
use GuzzleHttp\Exception\RequestException ;
2017-07-08 01:06:38 +00:00
use JKingWeb\Arsse\Exception ;
2017-07-17 11:47:57 +00:00
use JKingWeb\Arsse\Arsse ;
2018-01-09 17:31:40 +00:00
use JKingWeb\Arsse\Conf ;
2019-06-21 22:52:27 +00:00
use JKingWeb\Arsse\Db\Driver ;
use JKingWeb\Arsse\Db\Result ;
2021-02-07 04:51:23 +00:00
use JKingWeb\Arsse\Factory ;
2017-07-17 11:47:57 +00:00
use JKingWeb\Arsse\Misc\Date ;
2019-06-21 22:52:27 +00:00
use JKingWeb\Arsse\Misc\ValueInfo ;
2019-09-25 22:30:53 +00:00
use JKingWeb\Arsse\Misc\URL ;
2022-08-06 02:08:36 +00:00
use JKingWeb\Arsse\Misc\HTTP ;
2018-01-11 16:09:25 +00:00
use Psr\Http\Message\MessageInterface ;
use Psr\Http\Message\RequestInterface ;
use Psr\Http\Message\ServerRequestInterface ;
2018-01-04 04:13:08 +00:00
use Psr\Http\Message\ResponseInterface ;
2022-08-06 20:03:50 +00:00
use GuzzleHttp\Psr7\ServerRequest ;
2017-07-08 01:06:38 +00:00
2017-07-20 22:36:03 +00:00
/** @coversNothing */
2017-07-08 01:06:38 +00:00
abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
2019-10-16 18:42:43 +00:00
use \DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts ;
2021-02-27 20:24:02 +00:00
protected $objMock ;
protected $confMock ;
protected $langMock ;
protected $dbMock ;
protected $userMock ;
2018-01-06 17:02:45 +00:00
2021-02-27 20:24:02 +00:00
public function setUp () : void {
2018-11-23 15:01:17 +00:00
self :: clearData ();
2021-02-27 20:24:02 +00:00
// create the object factory as a mock
$this -> objMock = Arsse :: $obj = $this -> mock ( Factory :: class );
$this -> objMock -> get -> does ( function ( string $class ) {
return new $class ;
});
2018-01-06 17:02:45 +00:00
}
2020-01-20 18:34:03 +00:00
public static function clearData ( bool $loadLang = true ) : void {
2018-11-06 17:32:28 +00:00
date_default_timezone_set ( " America/Toronto " );
$r = new \ReflectionClass ( \JKingWeb\Arsse\Arsse :: class );
$props = array_keys ( $r -> getStaticProperties ());
foreach ( $props as $prop ) {
Arsse :: $$prop = null ;
}
if ( $loadLang ) {
Arsse :: $lang = new \JKingWeb\Arsse\Lang ();
}
}
2020-01-20 18:34:03 +00:00
public static function setConf ( array $conf = [], bool $force = true ) : void {
2018-11-17 02:32:27 +00:00
$defaults = [
2019-06-22 14:29:26 +00:00
'dbSQLite3File' => " :memory: " ,
'dbSQLite3Timeout' => 0 ,
2021-04-14 15:17:01 +00:00
'dbPostgreSQLHost' => $_ENV [ 'ARSSE_TEST_PGSQL_HOST' ] ? : " " ,
'dbPostgreSQLPort' => $_ENV [ 'ARSSE_TEST_PGSQL_PORT' ] ? : 5432 ,
'dbPostgreSQLUser' => $_ENV [ 'ARSSE_TEST_PGSQL_USER' ] ? : " arsse_test " ,
'dbPostgreSQLPass' => $_ENV [ 'ARSSE_TEST_PGSQL_PASS' ] ? : " arsse_test " ,
'dbPostgreSQLDb' => $_ENV [ 'ARSSE_TEST_PGSQL_DB' ] ? : " arsse_test " ,
2019-06-22 14:29:26 +00:00
'dbPostgreSQLSchema' => $_ENV [ 'ARSSE_TEST_PGSQL_SCHEMA' ] ? : " arsse_test " ,
2021-04-14 15:17:01 +00:00
'dbMySQLHost' => $_ENV [ 'ARSSE_TEST_MYSQL_HOST' ] ? : " localhost " ,
'dbMySQLPort' => $_ENV [ 'ARSSE_TEST_MYSQL_PORT' ] ? : 3306 ,
'dbMySQLUser' => $_ENV [ 'ARSSE_TEST_MYSQL_USER' ] ? : " arsse_test " ,
'dbMySQLPass' => $_ENV [ 'ARSSE_TEST_MYSQL_PASS' ] ? : " arsse_test " ,
'dbMySQLDb' => $_ENV [ 'ARSSE_TEST_MYSQL_DB' ] ? : " arsse_test " ,
2018-11-17 02:32:27 +00:00
];
2019-01-21 03:40:49 +00:00
Arsse :: $conf = (( $force ? null : Arsse :: $conf ) ? ? ( new Conf )) -> import ( $defaults ) -> import ( $conf );
2018-01-09 17:31:40 +00:00
}
2024-12-15 21:31:57 +00:00
protected function serverRequest ( string $method , string $url , string $urlPrefix , array $headers = [], array $vars = [], $body = null , string $type = " " , $params = [], ? string $user = null ) : ServerRequestInterface {
2019-09-25 22:30:53 +00:00
$server = [
'REQUEST_METHOD' => $method ,
2020-03-01 20:16:50 +00:00
'REQUEST_URI' => $url ,
2019-09-25 22:30:53 +00:00
];
if ( strlen ( $type )) {
$server [ 'HTTP_CONTENT_TYPE' ] = $type ;
}
if ( isset ( $params )) {
if ( is_array ( $params )) {
$params = implode ( " & " , array_map ( function ( $v , $k ) {
2019-10-25 19:16:35 +00:00
return rawurlencode ( $k ) . ( isset ( $v ) ? " = " . rawurlencode ( $v ) : " " );
2019-09-25 22:30:53 +00:00
}, $params , array_keys ( $params )));
}
$url = URL :: queryAppend ( $url , ( string ) $params );
2021-02-10 16:24:01 +00:00
$params = null ;
2019-09-25 22:30:53 +00:00
}
$q = parse_url ( $url , \PHP_URL_QUERY );
if ( strlen ( $q ? ? " " )) {
parse_str ( $q , $params );
} else {
$params = [];
}
$parsedBody = null ;
if ( isset ( $body )) {
if ( is_string ( $body ) && in_array ( strtolower ( $type ), [ " " , " application/x-www-form-urlencoded " ])) {
parse_str ( $body , $parsedBody );
} elseif ( ! is_string ( $body ) && in_array ( strtolower ( $type ), [ " application/json " , " text/json " ])) {
2019-10-25 19:16:35 +00:00
$body = json_encode ( $body , \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE );
2019-09-25 22:30:53 +00:00
} elseif ( ! is_string ( $body ) && in_array ( strtolower ( $type ), [ " " , " application/x-www-form-urlencoded " ])) {
$parsedBody = $body ;
$body = http_build_query ( $body , " a " , " & " );
}
}
$server = array_merge ( $server , $vars );
2022-08-06 20:03:50 +00:00
$req = new ServerRequest ( $method , $url , $headers , $body , " 1.1 " , $server );
2022-08-06 20:16:18 +00:00
$req = $req -> withParsedBody ( $parsedBody ) -> withQueryParams ( $params );
2019-09-25 22:30:53 +00:00
if ( isset ( $user )) {
if ( strlen ( $user )) {
$req = $req -> withAttribute ( " authenticated " , true ) -> withAttribute ( " authenticatedUser " , $user );
} else {
$req = $req -> withAttribute ( " authenticationFailed " , true );
}
}
2020-03-01 20:16:50 +00:00
if ( strlen ( $type ) && strlen ( $body ? ? " " )) {
2019-09-25 22:30:53 +00:00
$req = $req -> withHeader ( " Content-Type " , $type );
}
foreach ( $headers as $key => $value ) {
if ( ! is_null ( $value )) {
$req = $req -> withHeader ( $key , $value );
} else {
$req = $req -> withoutHeader ( $key );
}
}
$target = substr ( URL :: normalize ( $url ), strlen ( $urlPrefix ));
$req = $req -> withRequestTarget ( $target );
if ( strlen ( $body ? ? " " )) {
$p = $req -> getBody ();
$p -> write ( $body );
$req = $req -> withBody ( $p );
}
return $req ;
}
2021-03-01 23:20:50 +00:00
public static function assertMatchesRegularExpression ( string $pattern , string $string , string $message = '' ) : void {
if ( method_exists ( parent :: class , " assertMatchesRegularExpression " )) {
parent :: assertMatchesRegularExpression ( $pattern , $string , $message );
} else {
parent :: assertRegExp ( $pattern , $string , $message );
}
}
2021-06-25 15:08:56 +00:00
public static function assertFileDoesNotExist ( string $filename , string $message = '' ) : void {
if ( method_exists ( parent :: class , " assertFileDoesNotExist " )) {
parent :: assertFileDoesNotExist ( $filename , $message );
} else {
parent :: assertFileNotExists ( $filename , $message );
}
}
2020-01-20 18:34:03 +00:00
public function assertException ( $msg = " " , string $prefix = " " , string $type = " Exception " ) : void {
2017-08-29 14:50:31 +00:00
if ( func_num_args ()) {
2019-03-21 02:24:35 +00:00
if ( $msg instanceof \JKingWeb\Arsse\AbstractException ) {
$this -> expectException ( get_class ( $msg ));
$this -> expectExceptionCode ( $msg -> getCode ());
2017-07-08 01:06:38 +00:00
} else {
2020-03-01 20:16:50 +00:00
$class = \JKingWeb\Arsse\NS_BASE . ( $prefix !== " " ? str_replace ( " / " , " \\ " , $prefix ) . " \\ " : " " ) . $type ;
$msgID = ( $prefix !== " " ? $prefix . " / " : " " ) . $type . " . $msg " ;
2019-03-21 02:24:35 +00:00
if ( array_key_exists ( $msgID , Exception :: CODES )) {
$code = Exception :: CODES [ $msgID ];
} else {
$code = 0 ;
}
$this -> expectException ( $class );
$this -> expectExceptionCode ( $code );
2017-07-08 01:06:38 +00:00
}
} else {
// expecting a standard PHP exception
2019-01-21 03:40:49 +00:00
$this -> expectException ( \Throwable :: class );
2017-07-08 01:06:38 +00:00
}
}
2020-01-20 18:34:03 +00:00
protected function assertMessage ( MessageInterface $exp , MessageInterface $act , string $text = '' ) : void {
2018-01-11 16:09:25 +00:00
if ( $exp instanceof ResponseInterface ) {
$this -> assertInstanceOf ( ResponseInterface :: class , $act , $text );
2020-12-14 03:10:34 +00:00
$this -> assertSame ( $exp -> getStatusCode (), $act -> getStatusCode (), $text );
2018-01-11 16:09:25 +00:00
} elseif ( $exp instanceof RequestInterface ) {
if ( $exp instanceof ServerRequestInterface ) {
$this -> assertInstanceOf ( ServerRequestInterface :: class , $act , $text );
$this -> assertEquals ( $exp -> getAttributes (), $act -> getAttributes (), $text );
}
$this -> assertInstanceOf ( RequestInterface :: class , $act , $text );
2018-01-11 20:48:29 +00:00
$this -> assertSame ( $exp -> getMethod (), $act -> getMethod (), $text );
2018-01-11 16:09:25 +00:00
$this -> assertSame ( $exp -> getRequestTarget (), $act -> getRequestTarget (), $text );
}
2022-08-06 02:08:36 +00:00
if ( $exp instanceof ResponseInterface && HTTP :: matchType ( $exp , " application/json " , " text/json " , " +json " )) {
2022-08-06 20:03:50 +00:00
$expBody = @ json_decode (( string ) $exp -> getBody (), true );
$actBody = @ json_decode (( string ) $act -> getBody (), true );
2022-08-06 02:08:36 +00:00
$this -> assertSame ( \JSON_ERROR_NONE , json_last_error (), " Response body is not valid JSON " );
$this -> assertEquals ( $expBody , $actBody , $text );
$this -> assertSame ( $expBody , $actBody , $text );
} elseif ( $exp instanceof ResponseInterface && HTTP :: matchType ( $exp , " application/xml " , " text/xml " , " +xml " )) {
2019-07-24 13:10:13 +00:00
$this -> assertXmlStringEqualsXmlString (( string ) $exp -> getBody (), ( string ) $act -> getBody (), $text );
2018-01-06 17:02:45 +00:00
} else {
2020-12-14 03:10:34 +00:00
$this -> assertSame (( string ) $exp -> getBody (), ( string ) $act -> getBody (), $text );
2018-01-04 04:13:08 +00:00
}
2018-01-05 04:08:53 +00:00
$this -> assertEquals ( $exp -> getHeaders (), $act -> getHeaders (), $text );
2018-01-04 04:13:08 +00:00
}
2022-08-06 20:03:50 +00:00
protected function extractMessageJson ( MessageInterface $msg ) {
if ( HTTP :: matchType ( $msg , " application/json " , " text/json " , " +json " )) {
$json = @ json_decode (( string ) $msg -> getBody (), true );
if ( json_last_error () === \JSON_ERROR_NONE ) {
return $json ;
}
}
return null ;
}
2020-01-20 18:34:03 +00:00
public function assertTime ( $exp , $test , string $msg = '' ) : void {
2018-11-06 17:32:28 +00:00
$test = $this -> approximateTime ( $exp , $test );
2020-03-01 20:16:50 +00:00
$exp = Date :: transform ( $exp , " iso8601 " );
2018-11-06 17:32:28 +00:00
$test = Date :: transform ( $test , " iso8601 " );
$this -> assertSame ( $exp , $test , $msg );
}
2017-12-08 21:00:23 +00:00
public function approximateTime ( $exp , $act ) {
if ( is_null ( $act )) {
return null ;
2017-12-20 00:08:08 +00:00
} elseif ( is_null ( $exp )) {
return $act ;
2017-12-08 21:00:23 +00:00
}
$target = Date :: normalize ( $exp ) -> getTimeStamp ();
$value = Date :: normalize ( $act ) -> getTimeStamp ();
if ( $value >= ( $target - 1 ) && $value <= ( $target + 1 )) {
// if the actual time is off by no more than one second, it's acceptable
return $exp ;
} else {
return $act ;
}
}
2018-11-08 19:50:58 +00:00
public function stringify ( $value ) {
if ( ! is_array ( $value )) {
return $value ;
}
foreach ( $value as $k => $v ) {
if ( is_array ( $v )) {
2021-03-02 04:27:58 +00:00
$value [ $k ] = $this -> stringify ( $v );
2018-11-08 19:50:58 +00:00
} elseif ( is_int ( $v ) || is_float ( $v )) {
$value [ $k ] = ( string ) $v ;
}
}
return $value ;
}
2019-06-21 22:52:27 +00:00
public function primeDatabase ( Driver $drv , array $data ) : bool {
$tr = $drv -> begin ();
foreach ( $data as $table => $info ) {
$cols = array_map ( function ( $v ) {
return '"' . str_replace ( '"' , '""' , $v ) . '"' ;
}, array_keys ( $info [ 'columns' ]));
$cols = implode ( " , " , $cols );
$bindings = array_values ( $info [ 'columns' ]);
$params = implode ( " , " , array_fill ( 0 , sizeof ( $info [ 'columns' ]), " ? " ));
$s = $drv -> prepareArray ( " INSERT INTO $table ( $cols ) values( $params ) " , $bindings );
foreach ( $info [ 'rows' ] as $row ) {
$s -> runArray ( $row );
}
}
$tr -> commit ();
$this -> primed = true ;
return true ;
}
public function compareExpectations ( Driver $drv , array $expected ) : bool {
foreach ( $expected as $table => $info ) {
$cols = array_map ( function ( $v ) {
return '"' . str_replace ( '"' , '""' , $v ) . '"' ;
}, array_keys ( $info [ 'columns' ]));
$cols = implode ( " , " , $cols );
$types = $info [ 'columns' ];
$data = $drv -> prepare ( " SELECT $cols from $table " ) -> run () -> getAll ();
$cols = array_keys ( $info [ 'columns' ]);
foreach ( $info [ 'rows' ] as $index => $row ) {
2019-07-25 17:14:29 +00:00
$this -> assertCount ( sizeof ( $cols ), $row , " The number of columns in array index $index of expectations for table $table does not match its definition " );
2019-06-21 22:52:27 +00:00
$row = array_combine ( $cols , $row );
foreach ( $data as $index => $test ) {
foreach ( $test as $col => $value ) {
switch ( $types [ $col ]) {
case " datetime " :
$test [ $col ] = $this -> approximateTime ( $row [ $col ], $value );
break ;
case " int " :
$test [ $col ] = ValueInfo :: normalize ( $value , ValueInfo :: T_INT | ValueInfo :: M_DROP | valueInfo :: M_NULL );
break ;
case " float " :
$test [ $col ] = ValueInfo :: normalize ( $value , ValueInfo :: T_FLOAT | ValueInfo :: M_DROP | valueInfo :: M_NULL );
break ;
case " bool " :
$test [ $col ] = ( int ) ValueInfo :: normalize ( $value , ValueInfo :: T_BOOL | ValueInfo :: M_DROP | valueInfo :: M_NULL );
break ;
}
}
2020-03-01 20:16:50 +00:00
if ( $row === $test ) {
2019-06-21 22:52:27 +00:00
$data [ $index ] = $test ;
break ;
}
}
2019-07-25 17:14:29 +00:00
$this -> assertContains ( $row , $data , " Actual Table $table does not contain record at expected array index $index " );
2019-06-21 22:52:27 +00:00
$found = array_search ( $row , $data , true );
unset ( $data [ $found ]);
}
2019-07-25 17:14:29 +00:00
$this -> assertSame ([], $data , " Actual table $table contains extra rows not in expectations " );
2019-06-21 22:52:27 +00:00
}
return true ;
}
2019-07-05 23:01:34 +00:00
public function primeExpectations ( array $source , array $tableSpecs ) : array {
2019-06-21 22:52:27 +00:00
$out = [];
foreach ( $tableSpecs as $table => $columns ) {
// make sure the source has the table we want
$this -> assertArrayHasKey ( $table , $source , " Source for expectations does not contain requested table $table . " );
$out [ $table ] = [
'columns' => [],
'rows' => array_fill ( 0 , sizeof ( $source [ $table ][ 'rows' ]), []),
];
// make sure the source has all the columns we want for the table
$cols = array_flip ( $columns );
$cols = array_intersect_key ( $cols , $source [ $table ][ 'columns' ]);
$this -> assertSame ( array_keys ( $cols ), $columns , " Source for table $table does not contain all requested columns " );
// get a map of source value offsets and keys
$targets = array_flip ( array_keys ( $source [ $table ][ 'columns' ]));
foreach ( $cols as $key => $order ) {
// fill the column-spec
$out [ $table ][ 'columns' ][ $key ] = $source [ $table ][ 'columns' ][ $key ];
foreach ( $source [ $table ][ 'rows' ] as $index => $row ) {
// fill each row column-wise with re-ordered values
$out [ $table ][ 'rows' ][ $index ][ $order ] = $row [ $targets [ $key ]];
}
}
}
return $out ;
}
2020-01-20 18:34:03 +00:00
public function assertResult ( array $expected , Result $data ) : void {
2021-03-02 16:04:21 +00:00
$data = $data -> getAll ();
2021-03-02 04:27:58 +00:00
// stringify our expectations if necessary
if ( static :: $stringOutput ? ? false ) {
$expected = $this -> stringify ( $expected );
2021-03-02 16:04:21 +00:00
// MySQL is extra-special and mixes strings and integers, so we cast the data, too
if (( static :: $implementation ? ? " " ) === " MySQL " ) {
$data = $this -> stringify ( $data );
}
2021-03-02 04:27:58 +00:00
}
2019-06-21 22:52:27 +00:00
$this -> assertCount ( sizeof ( $expected ), $data , " Number of result rows ( " . sizeof ( $data ) . " ) differs from number of expected rows ( " . sizeof ( $expected ) . " ) " );
if ( sizeof ( $expected )) {
// make sure the expectations are consistent
foreach ( $expected as $exp ) {
if ( ! isset ( $keys )) {
$keys = $exp ;
continue ;
}
$this -> assertSame ( array_keys ( $keys ), array_keys ( $exp ), " Result set expectations are irregular " );
}
// filter the result set to contain just the desired keys (we don't care if the result has extra keys)
$rows = [];
2021-03-02 04:27:58 +00:00
$keys = array_keys ( $keys );
2019-06-21 22:52:27 +00:00
foreach ( $data as $row ) {
2021-03-02 04:27:58 +00:00
$r = [];
foreach ( $keys as $k ) {
if ( array_key_exists ( $k , $row )) {
$r [ $k ] = $row [ $k ];
}
}
$rows [] = $r ;
2019-06-21 22:52:27 +00:00
}
// compare the result set to the expectations
foreach ( $rows as $row ) {
2021-03-02 04:27:58 +00:00
$this -> assertContains ( $row , $expected , " Result set contains unexpected record. \n " . var_export ( $expected , true ));
2019-06-21 22:52:27 +00:00
$found = array_search ( $row , $expected );
unset ( $expected [ $found ]);
}
$this -> assertArraySubset ( $expected , [], false , " Expectations not in result set. " );
}
}
2020-01-24 20:54:08 +00:00
/** Guzzle's exception classes require some fairly complicated construction; this abstracts it all away so that only message and code need be supplied */
protected function mockGuzzleException ( string $class , ? string $message = null , ? int $code = null , ? \Throwable $e = null ) : GuzzleException {
if ( is_a ( $class , RequestException :: class , true )) {
2021-02-27 20:24:02 +00:00
$req = $this -> mock ( RequestInterface :: class );
$res = $this -> mock ( ResponseInterface :: class );
$res -> getStatusCode -> returns ( $code ? ? 0 );
return new $class ( $message ? ? " " , $req -> get (), $res -> get (), $e );
2020-01-24 20:54:08 +00:00
} else {
return new $class ( $message ? ? " " , $code ? ? 0 , $e );
}
}
2021-02-27 20:24:02 +00:00
protected function mock ( string $class ) : InstanceHandle {
return Phony :: mock ( $class );
}
protected function partialMock ( string $class , ... $argument ) : InstanceHandle {
return Phony :: partialMock ( $class , $argument );
}
2017-08-29 14:50:31 +00:00
}