2015-12-24 14:24:18 +00:00
< ? php
2016-06-01 19:27:35 +00:00
namespace Tests\Wallabag\ImportBundle\Import ;
2015-12-24 14:24:18 +00:00
use Wallabag\UserBundle\Entity\User ;
2016-03-04 09:04:51 +00:00
use Wallabag\CoreBundle\Entity\Entry ;
2015-12-24 14:24:18 +00:00
use Wallabag\ImportBundle\Import\PocketImport ;
use GuzzleHttp\Client ;
use GuzzleHttp\Subscriber\Mock ;
use GuzzleHttp\Message\Response ;
use GuzzleHttp\Stream\Stream ;
2016-09-09 19:02:03 +00:00
use Wallabag\ImportBundle\Redis\Producer ;
2015-12-30 11:23:51 +00:00
use Monolog\Logger ;
use Monolog\Handler\TestHandler ;
2016-09-09 19:02:03 +00:00
use Simpleue\Queue\RedisQueue ;
use M6Web\Component\RedisMock\RedisMockFactory ;
2015-12-30 11:23:51 +00:00
2015-12-24 14:24:18 +00:00
class PocketImportTest extends \PHPUnit_Framework_TestCase
{
protected $token ;
protected $user ;
protected $em ;
2015-12-30 11:23:51 +00:00
protected $contentProxy ;
protected $logHandler ;
2015-12-24 14:24:18 +00:00
2016-09-03 15:36:57 +00:00
private function getPocketImport ( $consumerKey = 'ConsumerKey' )
2015-12-24 14:24:18 +00:00
{
$this -> user = new User ();
2015-12-30 11:23:51 +00:00
$this -> contentProxy = $this -> getMockBuilder ( 'Wallabag\CoreBundle\Helper\ContentProxy' )
-> disableOriginalConstructor ()
-> getMock ();
2015-12-24 14:24:18 +00:00
$this -> em = $this -> getMockBuilder ( 'Doctrine\ORM\EntityManager' )
-> disableOriginalConstructor ()
-> getMock ();
2016-01-21 07:53:09 +00:00
$config = $this -> getMockBuilder ( 'Craue\ConfigBundle\Util\Config' )
-> disableOriginalConstructor ()
-> getMock ();
$config -> expects ( $this -> any ())
-> method ( 'get' )
-> with ( 'pocket_consumer_key' )
-> willReturn ( $consumerKey );
2016-09-05 05:13:09 +00:00
$pocket = new PocketImport (
2015-12-24 14:24:18 +00:00
$this -> em ,
2015-12-30 11:23:51 +00:00
$this -> contentProxy ,
2016-09-03 15:36:57 +00:00
$config
2015-12-24 14:24:18 +00:00
);
2016-09-03 15:36:57 +00:00
$pocket -> setUser ( $this -> user );
2015-12-30 11:23:51 +00:00
$this -> logHandler = new TestHandler ();
2016-04-12 09:36:01 +00:00
$logger = new Logger ( 'test' , [ $this -> logHandler ]);
2015-12-30 11:23:51 +00:00
$pocket -> setLogger ( $logger );
return $pocket ;
2015-12-24 14:24:18 +00:00
}
public function testInit ()
{
$pocketImport = $this -> getPocketImport ();
$this -> assertEquals ( 'Pocket' , $pocketImport -> getName ());
2015-12-31 10:24:46 +00:00
$this -> assertNotEmpty ( $pocketImport -> getUrl ());
2016-03-09 07:59:08 +00:00
$this -> assertEquals ( 'import.pocket.description' , $pocketImport -> getDescription ());
2015-12-24 14:24:18 +00:00
}
public function testOAuthRequest ()
{
$client = new Client ();
$mock = new Mock ([
2015-12-30 11:23:51 +00:00
new Response ( 200 , [ 'Content-Type' => 'application/json' ], Stream :: factory ( json_encode ([ 'code' => 'wunderbar_code' ]))),
2015-12-24 14:24:18 +00:00
]);
$client -> getEmitter () -> attach ( $mock );
$pocketImport = $this -> getPocketImport ();
$pocketImport -> setClient ( $client );
2015-12-30 11:23:51 +00:00
$code = $pocketImport -> getRequestToken ( 'http://0.0.0.0/redirect' );
2015-12-24 14:24:18 +00:00
2015-12-30 11:23:51 +00:00
$this -> assertEquals ( 'wunderbar_code' , $code );
}
public function testOAuthRequestBadResponse ()
{
$client = new Client ();
$mock = new Mock ([
new Response ( 403 ),
]);
$client -> getEmitter () -> attach ( $mock );
$pocketImport = $this -> getPocketImport ();
$pocketImport -> setClient ( $client );
$code = $pocketImport -> getRequestToken ( 'http://0.0.0.0/redirect' );
$this -> assertFalse ( $code );
$records = $this -> logHandler -> getRecords ();
$this -> assertContains ( 'PocketImport: Failed to request token' , $records [ 0 ][ 'message' ]);
$this -> assertEquals ( 'ERROR' , $records [ 0 ][ 'level_name' ]);
2015-12-24 14:24:18 +00:00
}
public function testOAuthAuthorize ()
{
$client = new Client ();
$mock = new Mock ([
2015-12-30 11:23:51 +00:00
new Response ( 200 , [ 'Content-Type' => 'application/json' ], Stream :: factory ( json_encode ([ 'access_token' => 'wunderbar_token' ]))),
2015-12-24 14:24:18 +00:00
]);
$client -> getEmitter () -> attach ( $mock );
$pocketImport = $this -> getPocketImport ();
$pocketImport -> setClient ( $client );
2015-12-30 11:23:51 +00:00
$res = $pocketImport -> authorize ( 'wunderbar_code' );
2015-12-24 14:24:18 +00:00
2015-12-30 11:23:51 +00:00
$this -> assertTrue ( $res );
$this -> assertEquals ( 'wunderbar_token' , $pocketImport -> getAccessToken ());
2015-12-24 14:24:18 +00:00
}
2015-12-30 11:23:51 +00:00
public function testOAuthAuthorizeBadResponse ()
{
$client = new Client ();
$mock = new Mock ([
new Response ( 403 ),
]);
$client -> getEmitter () -> attach ( $mock );
$pocketImport = $this -> getPocketImport ();
$pocketImport -> setClient ( $client );
$res = $pocketImport -> authorize ( 'wunderbar_code' );
$this -> assertFalse ( $res );
$records = $this -> logHandler -> getRecords ();
$this -> assertContains ( 'PocketImport: Failed to authorize client' , $records [ 0 ][ 'message' ]);
$this -> assertEquals ( 'ERROR' , $records [ 0 ][ 'level_name' ]);
}
/**
* Will sample results from https :// getpocket . com / developer / docs / v3 / retrieve .
*/
2015-12-24 14:24:18 +00:00
public function testImport ()
{
$client = new Client ();
$mock = new Mock ([
2015-12-30 11:23:51 +00:00
new Response ( 200 , [ 'Content-Type' => 'application/json' ], Stream :: factory ( json_encode ([ 'access_token' => 'wunderbar_token' ]))),
new Response ( 200 , [ 'Content-Type' => 'application/json' ], Stream :: factory ( '
{
" status " : 1 ,
" list " : {
" 229279689 " : {
" item_id " : " 229279689 " ,
" resolved_id " : " 229279689 " ,
" given_url " : " http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview " ,
" given_title " : " The Massive Ryder Cup Preview - The Triangle Blog - Grantland " ,
" favorite " : " 1 " ,
" status " : " 1 " ,
2016-09-09 18:45:30 +00:00
" time_added " : " 1473020899 " ,
" time_updated " : " 1473020899 " ,
" time_read " : " 0 " ,
" time_favorited " : " 0 " ,
" sort_id " : 0 ,
2015-12-30 11:23:51 +00:00
" resolved_title " : " The Massive Ryder Cup Preview " ,
" resolved_url " : " http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview " ,
" excerpt " : " The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them. " ,
" is_article " : " 1 " ,
2016-09-09 18:45:30 +00:00
" is_index " : " 0 " ,
2015-12-30 11:23:51 +00:00
" has_video " : " 1 " ,
" has_image " : " 1 " ,
" word_count " : " 3197 " ,
" images " : {
" 1 " : {
" item_id " : " 229279689 " ,
" image_id " : " 1 " ,
" src " : " http://a.espncdn.com/combiner/i?img=/photo/2012/0927/grant_g_ryder_cr_640.jpg&w=640&h=360 " ,
" width " : " 0 " ,
" height " : " 0 " ,
" credit " : " Jamie Squire/Getty Images " ,
" caption " : " "
}
},
" videos " : {
" 1 " : {
" item_id " : " 229279689 " ,
" video_id " : " 1 " ,
" src " : " http://www.youtube.com/v/Er34PbFkVGk?version=3&hl=en_US&rel=0 " ,
" width " : " 420 " ,
" height " : " 315 " ,
" type " : " 1 " ,
" vid " : " Er34PbFkVGk "
}
},
" tags " : {
" grantland " : {
" item_id " : " 1147652870 " ,
" tag " : " grantland "
},
" Ryder Cup " : {
" item_id " : " 1147652870 " ,
" tag " : " Ryder Cup "
}
}
},
" 229279690 " : {
" item_id " : " 229279689 " ,
" resolved_id " : " 229279689 " ,
" given_url " : " http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview " ,
" given_title " : " The Massive Ryder Cup Preview - The Triangle Blog - Grantland " ,
" favorite " : " 1 " ,
" status " : " 1 " ,
2016-09-09 18:45:30 +00:00
" time_added " : " 1473020899 " ,
" time_updated " : " 1473020899 " ,
" time_read " : " 0 " ,
" time_favorited " : " 0 " ,
" sort_id " : 1 ,
2015-12-30 11:23:51 +00:00
" resolved_title " : " The Massive Ryder Cup Preview " ,
" resolved_url " : " http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview " ,
" excerpt " : " The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them. " ,
" is_article " : " 1 " ,
2016-09-09 18:45:30 +00:00
" is_index " : " 0 " ,
2015-12-30 11:23:51 +00:00
" has_video " : " 0 " ,
" has_image " : " 0 " ,
" word_count " : " 3197 "
}
}
}
' )),
]);
$client -> getEmitter () -> attach ( $mock );
$pocketImport = $this -> getPocketImport ();
$entryRepo = $this -> getMockBuilder ( 'Wallabag\CoreBundle\Repository\EntryRepository' )
-> disableOriginalConstructor ()
-> getMock ();
$entryRepo -> expects ( $this -> exactly ( 2 ))
2016-01-15 14:28:22 +00:00
-> method ( 'findByUrlAndUserId' )
2015-12-30 11:23:51 +00:00
-> will ( $this -> onConsecutiveCalls ( false , true ));
$this -> em
2016-02-19 13:22:20 +00:00
-> expects ( $this -> exactly ( 2 ))
2015-12-30 11:23:51 +00:00
-> method ( 'getRepository' )
2016-02-19 13:22:20 +00:00
-> willReturn ( $entryRepo );
2015-12-30 11:23:51 +00:00
2016-03-04 09:04:51 +00:00
$entry = new Entry ( $this -> user );
2015-12-30 11:23:51 +00:00
$this -> contentProxy
-> expects ( $this -> once ())
-> method ( 'updateEntry' )
-> willReturn ( $entry );
$pocketImport -> setClient ( $client );
$pocketImport -> authorize ( 'wunderbar_code' );
$res = $pocketImport -> import ();
$this -> assertTrue ( $res );
$this -> assertEquals ([ 'skipped' => 1 , 'imported' => 1 ], $pocketImport -> getSummary ());
}
2016-03-04 09:04:51 +00:00
/**
* Will sample results from https :// getpocket . com / developer / docs / v3 / retrieve .
*/
public function testImportAndMarkAllAsRead ()
{
$client = new Client ();
$mock = new Mock ([
new Response ( 200 , [ 'Content-Type' => 'application/json' ], Stream :: factory ( json_encode ([ 'access_token' => 'wunderbar_token' ]))),
new Response ( 200 , [ 'Content-Type' => 'application/json' ], Stream :: factory ( '
{
" status " : 1 ,
" list " : {
" 229279689 " : {
" item_id " : " 229279689 " ,
" resolved_id " : " 229279689 " ,
" given_url " : " http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview " ,
" given_title " : " The Massive Ryder Cup Preview - The Triangle Blog - Grantland " ,
" favorite " : " 1 " ,
" status " : " 1 " ,
2016-09-09 18:45:30 +00:00
" time_added " : " 1473020899 " ,
" time_updated " : " 1473020899 " ,
" time_read " : " 0 " ,
" time_favorited " : " 0 " ,
" sort_id " : 0 ,
2016-03-04 09:04:51 +00:00
" excerpt " : " The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them. " ,
" is_article " : " 1 " ,
" has_video " : " 1 " ,
" has_image " : " 1 " ,
" word_count " : " 3197 "
},
" 229279690 " : {
" item_id " : " 229279689 " ,
" resolved_id " : " 229279689 " ,
" given_url " : " http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview/2 " ,
" given_title " : " The Massive Ryder Cup Preview - The Triangle Blog - Grantland " ,
" favorite " : " 1 " ,
" status " : " 0 " ,
2016-09-09 18:45:30 +00:00
" time_added " : " 1473020899 " ,
" time_updated " : " 1473020899 " ,
" time_read " : " 0 " ,
" time_favorited " : " 0 " ,
" sort_id " : 1 ,
2016-03-04 09:04:51 +00:00
" excerpt " : " The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them. " ,
" is_article " : " 1 " ,
" has_video " : " 0 " ,
" has_image " : " 0 " ,
" word_count " : " 3197 "
}
}
}
' )),
]);
$client -> getEmitter () -> attach ( $mock );
$pocketImport = $this -> getPocketImport ();
$entryRepo = $this -> getMockBuilder ( 'Wallabag\CoreBundle\Repository\EntryRepository' )
-> disableOriginalConstructor ()
-> getMock ();
$entryRepo -> expects ( $this -> exactly ( 2 ))
-> method ( 'findByUrlAndUserId' )
-> will ( $this -> onConsecutiveCalls ( false , false ));
$this -> em
-> expects ( $this -> exactly ( 2 ))
-> method ( 'getRepository' )
-> willReturn ( $entryRepo );
// check that every entry persisted are archived
$this -> em
-> expects ( $this -> any ())
-> method ( 'persist' )
2016-03-08 14:22:35 +00:00
-> with ( $this -> callback ( function ( $persistedEntry ) {
2016-03-04 09:04:51 +00:00
return $persistedEntry -> isArchived ();
}));
$entry = new Entry ( $this -> user );
$this -> contentProxy
-> expects ( $this -> exactly ( 2 ))
-> method ( 'updateEntry' )
-> willReturn ( $entry );
$pocketImport -> setClient ( $client );
$pocketImport -> authorize ( 'wunderbar_code' );
$res = $pocketImport -> setMarkAsRead ( true ) -> import ();
$this -> assertTrue ( $res );
$this -> assertEquals ([ 'skipped' => 0 , 'imported' => 2 ], $pocketImport -> getSummary ());
}
2016-09-09 16:02:29 +00:00
/**
* Will sample results from https :// getpocket . com / developer / docs / v3 / retrieve .
*/
public function testImportWithRabbit ()
{
$client = new Client ();
$body = <<< 'JSON'
{
" item_id " : " 229279689 " ,
" resolved_id " : " 229279689 " ,
" given_url " : " http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview " ,
" given_title " : " The Massive Ryder Cup Preview - The Triangle Blog - Grantland " ,
" favorite " : " 1 " ,
" status " : " 1 " ,
2016-09-09 18:45:30 +00:00
" time_added " : " 1473020899 " ,
" time_updated " : " 1473020899 " ,
" time_read " : " 0 " ,
" time_favorited " : " 0 " ,
" sort_id " : 0 ,
2016-09-09 16:02:29 +00:00
" resolved_title " : " The Massive Ryder Cup Preview " ,
" resolved_url " : " http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview " ,
" excerpt " : " The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them. " ,
" is_article " : " 1 " ,
" has_video " : " 0 " ,
" has_image " : " 0 " ,
" word_count " : " 3197 "
}
JSON ;
$mock = new Mock ([
new Response ( 200 , [ 'Content-Type' => 'application/json' ], Stream :: factory ( json_encode ([ 'access_token' => 'wunderbar_token' ]))),
new Response ( 200 , [ 'Content-Type' => 'application/json' ], Stream :: factory ( '
{
" status " : 1 ,
" list " : {
" 229279690 " : '.$body.'
}
}
' )),
]);
$client -> getEmitter () -> attach ( $mock );
$pocketImport = $this -> getPocketImport ();
$entryRepo = $this -> getMockBuilder ( 'Wallabag\CoreBundle\Repository\EntryRepository' )
-> disableOriginalConstructor ()
-> getMock ();
$entryRepo -> expects ( $this -> never ())
-> method ( 'findByUrlAndUserId' );
$this -> em
-> expects ( $this -> never ())
-> method ( 'getRepository' );
$entry = new Entry ( $this -> user );
$this -> contentProxy
-> expects ( $this -> never ())
-> method ( 'updateEntry' );
$producer = $this -> getMockBuilder ( 'OldSound\RabbitMqBundle\RabbitMq\Producer' )
-> disableOriginalConstructor ()
-> getMock ();
$bodyAsArray = json_decode ( $body , true );
// because with just use `new User()` so it doesn't have an id
$bodyAsArray [ 'userId' ] = null ;
$producer
-> expects ( $this -> once ())
-> method ( 'publish' )
-> with ( json_encode ( $bodyAsArray ));
$pocketImport -> setClient ( $client );
2016-09-09 19:02:03 +00:00
$pocketImport -> setProducer ( $producer );
2016-09-09 16:02:29 +00:00
$pocketImport -> authorize ( 'wunderbar_code' );
$res = $pocketImport -> setMarkAsRead ( true ) -> import ();
$this -> assertTrue ( $res );
$this -> assertEquals ([ 'skipped' => 0 , 'imported' => 1 ], $pocketImport -> getSummary ());
}
2016-09-09 19:02:03 +00:00
/**
* Will sample results from https :// getpocket . com / developer / docs / v3 / retrieve .
*/
public function testImportWithRedis ()
{
$client = new Client ();
$body = <<< 'JSON'
{
" item_id " : " 229279689 " ,
" resolved_id " : " 229279689 " ,
" given_url " : " http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview " ,
" given_title " : " The Massive Ryder Cup Preview - The Triangle Blog - Grantland " ,
" favorite " : " 1 " ,
" status " : " 1 " ,
" time_added " : " 1473020899 " ,
" time_updated " : " 1473020899 " ,
" time_read " : " 0 " ,
" time_favorited " : " 0 " ,
" sort_id " : 0 ,
" resolved_title " : " The Massive Ryder Cup Preview " ,
" resolved_url " : " http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview " ,
" excerpt " : " The list of things I love about the Ryder Cup is so long that it could fill a (tedious) novel, and golf fans can probably guess most of them. " ,
" is_article " : " 1 " ,
" has_video " : " 0 " ,
" has_image " : " 0 " ,
" word_count " : " 3197 "
}
JSON ;
$mock = new Mock ([
new Response ( 200 , [ 'Content-Type' => 'application/json' ], Stream :: factory ( json_encode ([ 'access_token' => 'wunderbar_token' ]))),
new Response ( 200 , [ 'Content-Type' => 'application/json' ], Stream :: factory ( '
{
" status " : 1 ,
" list " : {
" 229279690 " : '.$body.'
}
}
' )),
]);
$client -> getEmitter () -> attach ( $mock );
$pocketImport = $this -> getPocketImport ();
$entryRepo = $this -> getMockBuilder ( 'Wallabag\CoreBundle\Repository\EntryRepository' )
-> disableOriginalConstructor ()
-> getMock ();
$entryRepo -> expects ( $this -> never ())
-> method ( 'findByUrlAndUserId' );
$this -> em
-> expects ( $this -> never ())
-> method ( 'getRepository' );
$entry = new Entry ( $this -> user );
$this -> contentProxy
-> expects ( $this -> never ())
-> method ( 'updateEntry' );
$factory = new RedisMockFactory ();
$redisMock = $factory -> getAdapter ( 'Predis\Client' , true );
$queue = new RedisQueue ( $redisMock , 'pocket' );
$producer = new Producer ( $queue );
$pocketImport -> setClient ( $client );
$pocketImport -> setProducer ( $producer );
$pocketImport -> authorize ( 'wunderbar_code' );
$res = $pocketImport -> setMarkAsRead ( true ) -> import ();
$this -> assertTrue ( $res );
$this -> assertEquals ([ 'skipped' => 0 , 'imported' => 1 ], $pocketImport -> getSummary ());
$this -> assertNotEmpty ( $redisMock -> lpop ( 'pocket' ));
}
2015-12-30 11:23:51 +00:00
public function testImportBadResponse ()
{
$client = new Client ();
$mock = new Mock ([
new Response ( 200 , [ 'Content-Type' => 'application/json' ], Stream :: factory ( json_encode ([ 'access_token' => 'wunderbar_token' ]))),
new Response ( 403 ),
2015-12-24 14:24:18 +00:00
]);
$client -> getEmitter () -> attach ( $mock );
$pocketImport = $this -> getPocketImport ();
$pocketImport -> setClient ( $client );
2015-12-30 11:23:51 +00:00
$pocketImport -> authorize ( 'wunderbar_code' );
$res = $pocketImport -> import ();
2015-12-24 14:24:18 +00:00
2015-12-30 11:23:51 +00:00
$this -> assertFalse ( $res );
2015-12-24 14:24:18 +00:00
2015-12-30 11:23:51 +00:00
$records = $this -> logHandler -> getRecords ();
$this -> assertContains ( 'PocketImport: Failed to import' , $records [ 0 ][ 'message' ]);
$this -> assertEquals ( 'ERROR' , $records [ 0 ][ 'level_name' ]);
2015-12-24 14:24:18 +00:00
}
2016-08-19 21:52:19 +00:00
public function testImportWithExceptionFromGraby ()
{
$client = new Client ();
$mock = new Mock ([
new Response ( 200 , [ 'Content-Type' => 'application/json' ], Stream :: factory ( json_encode ([ 'access_token' => 'wunderbar_token' ]))),
new Response ( 200 , [ 'Content-Type' => 'application/json' ], Stream :: factory ( '
{
" status " : 1 ,
" list " : {
" 229279689 " : {
" resolved_url " : " http://www.grantland.com/blog/the-triangle/post/_/id/38347/ryder-cup-preview "
}
}
}
' )),
]);
$client -> getEmitter () -> attach ( $mock );
$pocketImport = $this -> getPocketImport ();
$entryRepo = $this -> getMockBuilder ( 'Wallabag\CoreBundle\Repository\EntryRepository' )
-> disableOriginalConstructor ()
-> getMock ();
$entryRepo -> expects ( $this -> once ())
-> method ( 'findByUrlAndUserId' )
-> will ( $this -> onConsecutiveCalls ( false , true ));
$this -> em
-> expects ( $this -> once ())
-> method ( 'getRepository' )
-> willReturn ( $entryRepo );
$entry = new Entry ( $this -> user );
$this -> contentProxy
-> expects ( $this -> once ())
-> method ( 'updateEntry' )
-> will ( $this -> throwException ( new \Exception ()));
$pocketImport -> setClient ( $client );
$pocketImport -> authorize ( 'wunderbar_code' );
$res = $pocketImport -> import ();
$this -> assertTrue ( $res );
$this -> assertEquals ([ 'skipped' => 1 , 'imported' => 0 ], $pocketImport -> getSummary ());
}
2015-12-24 14:24:18 +00:00
}