1
0
mirror of https://github.com/chylex/Nextcloud-News.git synced 2024-10-17 08:42:48 +02:00
Nextcloud-News/tests/Unit/Fetcher/FeedFetcherTest.php
Benjamin Brahmer b1476e958a If timestamp is null use timestamp "one year ago"
if timestamp is not set during creation of a feed use date one year ago
code fixes and linting fixes.

Co-authored-by: Sean Molenaar <SMillerDev@users.noreply.github.com>
Signed-off-by: Benjamin Brahmer <info@b-brahmer.de>
2023-03-23 12:12:06 +01:00

724 lines
22 KiB
PHP

<?php
/**
* Nextcloud - News
*
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*
* @author Alessandro Cosentino <cosenal@gmail.com>
* @author Bernhard Posselt <dev@bernhard-posselt.com>
* @copyright 2012 Alessandro Cosentino
* @copyright 2012-2014 Bernhard Posselt
*/
namespace OCA\News\Tests\Unit\Fetcher;
use DateTime;
use Favicon\Favicon;
use FeedIo\Adapter\ResponseInterface;
use FeedIo\Feed\Item\Author;
use FeedIo\Feed\Item\MediaInterface;
use FeedIo\Feed\Node\Category;
use FeedIo\Feed\ItemInterface;
use FeedIo\FeedInterface;
use FeedIo\FeedIo;
use FeedIo\Reader\Result;
use OC\L10N\L10N;
use \OCA\News\Db\Feed;
use \OCA\News\Db\Item;
use OCA\News\Scraper\Scraper;
use OCA\News\Fetcher\FeedFetcher;
use GuzzleHttp\Client;
use OCA\News\Utility\Time;
use OCP\IL10N;
use OCP\ITempManager;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
/**
* Class FeedFetcherTest
*
* @package OCA\News\Tests\Unit\Fetcher
*/
class FeedFetcherTest extends TestCase
{
/**
* The class to test
*
* @var MockObject|FeedFetcher
*/
private $fetcher;
/**
* Feed reader
*
* @var MockObject|FeedIo
*/
private $reader;
/**
* Feed reader result
*
* @var MockObject|Result
*/
private $result;
/**
* Feed reader result object
*
* @var MockObject|ResponseInterface
*/
private $response;
/**
* @var MockObject|Favicon
*/
private $favicon;
/**
* @var MockObject|L10N
*/
private $l10n;
/**
* @var MockObject|ITempManager
*/
private $ITempManager;
/**
* @var MockObject|ItemInterface
*/
private $item_mock;
/**
* @var MockObject|FeedInterface
*/
private $feed_mock;
/**
* @var MockObject|LoggerInterface
*/
private $logger;
/**
* @var MockObject|Scraper
*/
private $scraper;
/**
* @var MockObject|Client
*/
private $client;
//metadata
/**
* @var integer
*/
private $time;
/**
* @var string
*/
private $encoding;
/**
* @var string
*/
private $url;
// items
private $permalink;
private $title;
private $guid;
private $guid_hash;
private $pub;
private $updated;
private $body;
private $parsed_body;
/**
* @var Author
*/
private $author;
private $enclosure;
private $rtl;
private $language;
// feed
private $feed_title;
private $feed_link;
private $feed_image;
private $web_favicon;
private $modified;
private $location;
protected function setUp(): void
{
$this->l10n = $this->getMockBuilder(IL10N::class)
->disableOriginalConstructor()
->getMock();
$this->ITempManager = $this->getMockBuilder(ITempManager::class)
->disableOriginalConstructor()
->getMock();
$this->reader = $this->getMockBuilder(FeedIo::class)
->disableOriginalConstructor()
->getMock();
$this->favicon = $this->getMockBuilder(Favicon::class)
->disableOriginalConstructor()
->getMock();
$this->result = $this->getMockBuilder(Result::class)
->disableOriginalConstructor()
->getMock();
$this->response = $this->getMockBuilder(ResponseInterface::class)
->disableOriginalConstructor()
->getMock();
$this->item_mock = $this->getMockBuilder(ItemInterface::class)
->disableOriginalConstructor()
->getMock();
$this->feed_mock = $this->getMockBuilder(FeedInterface::class)
->disableOriginalConstructor()
->getMock();
$this->time = 2323;
$timeFactory = $this->getMockBuilder(Time::class)
->disableOriginalConstructor()
->getMock();
$timeFactory->expects($this->any())
->method('getTime')
->will($this->returnValue($this->time));
$this->logger = $this->getMockBuilder(LoggerInterface::class)
->getMock();
$this->scraper = $this->getMockBuilder(Scraper::class)
->disableOriginalConstructor()
->getMock();
$this->client = $this->getMockBuilder(Client::class)
->disableOriginalConstructor()
->getMock();
$this->fetcher = new FeedFetcher(
$this->reader,
$this->favicon,
$this->scraper,
$this->l10n,
$this->ITempManager,
$timeFactory,
$this->logger,
$this->client
);
$this->url = 'http://tests/';
$this->permalink = 'http://permalink';
$this->title = 'my&amp;lt;&apos; title';
$this->guid = 'hey guid here';
$this->guid_hash = 'df9a5f84e44bfe38cf44f6070d5b0250';
$this->body
= '<![CDATA[let the bodies hit the floor <a href="test">test</a>]]>';
$this->parsed_body
= 'let the bodies hit the floor <a href="test">test</a>';
$this->pub = 23111;
$this->updated = 23444;
$this->author = new Author();
$this->author->setName('&lt;boogieman');
$this->enclosure = 'http://enclosure.you';
$category = new Category();
$category->setLabel('food');
$this->categories = new \ArrayIterator([$category]);
$this->categoriesJson = json_encode(['food']);
$this->feed_title = '&lt;a&gt;&amp;its a&lt;/a&gt; title';
$this->feed_link = 'http://tests/';
$this->feed_image = '/an/image';
$this->web_favicon = 'http://anon.google.com';
$this->modified = new DateTime('@3');
$this->encoding = 'UTF-8';
$this->language = 'de-DE';
$this->rtl = false;
}
/**
* Test if the fetcher can handle a URL.
*/
public function testCanHandle()
{
$url = 'google.de';
$this->assertTrue($this->fetcher->canHandle($url));
}
/**
* Test if empty is logged when the feed remain the same.
*/
public function testFetchIfNoModifiedExists()
{
$this->setUpReader($this->url, true);
$item = $this->createItem();
$feed = $this->createFeed();
$this->mockIterator($this->feed_mock, [$this->item_mock]);
$result = $this->fetcher->fetch($this->url, '0', false, null, null);
$this->assertEquals([$feed, [$item]], $result);
}
/**
* Return body options
*
* @return array
*/
public function feedBodyProvider()
{
return [
[
'<![CDATA[let the bodies hit the floor <a href="test">test</a>]]>',
'let the bodies hit the floor <a href="test">test</a>'
],
[
'let the bodies hit the floor <a href="test">test</a>',
'let the bodies hit the floor <a href="test">test</a>'
],
[
'let the bodies hit the floor "test" test',
'let the bodies hit the floor "test" test'
],
[
'<img src="https://imgs.xkcd.com/google_trends_maps.png" title="It\'s early 2020. The entire country is gripped with Marco Rubio" />',
'<img src="https://imgs.xkcd.com/google_trends_maps.png" title="It\'s early 2020. The entire country is gripped with Marco Rubio" />'
],
];
}
/**
* Test if body is set correctly.
*
* @dataProvider feedBodyProvider
*
* @param string $body The body before parsing.
* @param string $parsed_body The body after parsing.
*/
public function testFetchWithFeedContent(string $body, string $parsed_body)
{
$bodyBackup = $this->body;
$parsedBackup = $this->parsed_body;
$this->body = $body;
$this->parsed_body = $parsed_body;
$this->setUpReader($this->url, true);
$item = $this->createItem();
$feed = $this->createFeed();
$this->mockIterator($this->feed_mock, [$this->item_mock]);
$result = $this->fetcher->fetch($this->url, '0', false, null, null);
$this->assertEquals([$feed, [$item]], $result);
$this->body = $bodyBackup;
$this->parsed_body = $parsedBackup;
}
/**
* Test if the fetch function fetches a feed.
*/
public function testFetch()
{
$this->setUpReader($this->url);
$item = $this->createItem();
$feed = $this->createFeed();
$this->mockIterator($this->feed_mock, [$this->item_mock]);
$result = $this->fetcher->fetch($this->url, false, null, null, null);
$this->assertEquals([$feed, [$item]], $result);
}
/**
* Test if fetching a feed with authentication works.
*/
public function testFetchAccount()
{
$this->setUpReader('http://account%40email.com:F9sEU%2ARt%25%3AKFK8HMHT%26@tests/');
$item = $this->createItem();
$feed = $this->createFeed('de-DE', 'http://account%40email.com:F9sEU%2ARt%25%3AKFK8HMHT%26@tests/');
$this->mockIterator($this->feed_mock, [$this->item_mock]);
$result = $this->fetcher->fetch(
$this->url,
false,
'account@email.com',
'F9sEU*Rt%:KFK8HMHT&',
$this->modified->format(DateTime::RSS)
);
$this->assertEquals([$feed, [$item]], $result);
}
/**
* Test if fetching a feed with an audio item works.
*/
public function testAudioEnclosure()
{
$this->setUpReader($this->url);
$item = $this->createItem('audio/ogg');
$feed = $this->createFeed();
$this->mockIterator($this->feed_mock, [$this->item_mock]);
$result = $this->fetcher->fetch($this->url, false, null, null, null);
$this->assertEquals([$feed, [$item]], $result);
}
/**
* Test if fetching a feed with a video item works.
*/
public function testVideoEnclosure()
{
$this->setUpReader($this->url);
$item = $this->createItem('video/ogg');
$feed = $this->createFeed();
$this->mockIterator($this->feed_mock, [$this->item_mock]);
$result = $this->fetcher->fetch($this->url, false, null, null, null);
$this->assertEquals([$feed, [$item]], $result);
}
/**
* Test if fetching a feed with a favicon works.
*/
public function testFavicon()
{
$this->setUpReader($this->url);
$feed = $this->createFeed('de-DE');
$item = $this->createItem();
$this->mockIterator($this->feed_mock, [$this->item_mock]);
$result = $this->fetcher->fetch($this->url, false, null, null, null);
$this->assertEquals([$feed, [$item]], $result);
}
/**
* Test if fetching a feed with a non-western language works.
*/
public function testRtl()
{
$this->setUpReader($this->url);
$this->createFeed('he-IL');
$this->createItem();
$this->mockIterator($this->feed_mock, [$this->item_mock]);
list($_, $items) = $this->fetcher->fetch($this->url, false, null, null, null);
$this->assertTrue($items[0]->getRtl());
}
/**
* Test if fetching a feed with a RSS pubdate works and sets the property.
*/
public function testRssPubDate()
{
$this->setUpReader($this->url);
$this->createFeed('he-IL');
$this->createItem();
$this->item_mock->expects($this->exactly(3))
->method('getValue')
->will(
$this->returnValueMap(
[
['pubDate', '2018-03-27T19:50:29Z'],
['published', null],
]
)
);
$this->mockIterator($this->feed_mock, [$this->item_mock]);
list($feed, $items) = $this->fetcher->fetch($this->url, false, null, null, null);
$this->assertSame($items[0]->getPubDate(), 1522180229);
}
/**
* Test if fetching a feed with a Atom pubdate works and sets the property.
*/
public function testAtomPubDate()
{
$this->setUpReader($this->url);
$this->createFeed('he-IL');
$this->createItem();
$this->item_mock->expects($this->exactly(4))
->method('getValue')
->will(
$this->returnValueMap(
[
['pubDate', null],
['published', '2018-02-27T19:50:29Z'],
]
)
);
$this->mockIterator($this->feed_mock, [$this->item_mock]);
list($feed, $items) = $this->fetcher->fetch($this->url, false, null, null, null);
$this->assertSame($items[0]->getPubDate(), 1519761029);
}
/**
* Test if the fetch function fetches a feed that specifies a guid.
*/
public function testFetchWithGuid()
{
$this->setUpReader($this->url);
$this->createItem();
$feed = $this->createFeed();
$this->mockIterator($this->feed_mock, [$this->item_mock]);
$result = $this->fetcher->fetch($this->url, false, null, null. null, null);
//Explicitly assert GUID value
$this->assertEquals(2, count($result));
$this->assertEquals(1, count($result[1]));
$resultItem = $result[1][0];
$this->assertEquals($this->guid, $resultItem->getGuid());
}
/**
* Test if the fetch function fetches a feed that does not specify a guid.
*/
public function testFetchWithoutGuid()
{
$this->setUpReader($this->url);
$this->guid = null;
$this->createItem();
$feed = $this->createFeed();
$this->mockIterator($this->feed_mock, [$this->item_mock]);
$result = $this->fetcher->fetch($this->url, false, null, null, null);
//Explicitly assert GUID value
$this->assertEquals(2, count($result));
$this->assertEquals(1, count($result[1]));
$resultItem = $result[1][0];
$this->assertEquals($this->permalink, $resultItem->getGuid());
}
/**
* Mock an iteration option on an existing mock
*
* @param object $iteratorMock The mock to enhance
* @param array $items The items to make available
*
* @return mixed
*/
private function mockIterator(object $iteratorMock, array $items)
{
$iteratorData = new \stdClass();
$iteratorData->array = $items;
$iteratorData->position = 0;
$iteratorMock->expects($this->any())
->method('rewind')
->will(
$this->returnCallback(
function () use ($iteratorData) {
$iteratorData->position = 0;
}
)
);
$iteratorMock->expects($this->any())
->method('current')
->will(
$this->returnCallback(
function () use ($iteratorData) {
return $iteratorData->array[$iteratorData->position];
}
)
);
$iteratorMock->expects($this->any())
->method('key')
->will(
$this->returnCallback(
function () use ($iteratorData) {
return $iteratorData->position;
}
)
);
$iteratorMock->expects($this->any())
->method('next')
->will(
$this->returnCallback(
function () use ($iteratorData) {
$iteratorData->position++;
}
)
);
$iteratorMock->expects($this->any())
->method('valid')
->will(
$this->returnCallback(
function () use ($iteratorData) {
return isset($iteratorData->array[$iteratorData->position]);
}
)
);
$iteratorMock->expects($this->any())
->method('count')
->will(
$this->returnCallback(
function () use ($iteratorData) {
return sizeof($iteratorData->array);
}
)
);
return $iteratorMock;
}
/**
* Set up a FeedIO mock instance
*
* @param string $url URL that will be read.
* @param string|null $modifiedDate Date of last fetch
* @param bool $modified If the feed will be modified
*/
private function setUpReader(string $url = '', bool $modified = true)
{
$this->reader->expects($this->once())
->method('read')
->with($url)
->will($this->returnValue($this->result));
$this->location = $url;
if (!$modified) {
$this->result->expects($this->never())
->method('getUrl');
} else {
$this->result->expects($this->once())
->method('getUrl')
->will($this->returnValue($this->location));
$this->result->expects($this->once())
->method('getFeed')
->will($this->returnValue($this->feed_mock));
}
}
/**
* Create an item mock.
*
* @param string|null $enclosureType Media type.
*
* @return Item
*/
private function createItem($enclosureType = null)
{
$this->item_mock->expects($this->exactly(1))
->method('getLink')
->will($this->returnValue($this->permalink));
$this->item_mock->expects($this->exactly(1))
->method('getTitle')
->will($this->returnValue($this->title));
$this->item_mock->expects($this->exactly(1))
->method('getPublicId')
->will($this->returnValue($this->guid));
$this->item_mock->expects($this->exactly(1))
->method('getContent')
->will($this->returnValue($this->body));
$this->item_mock->expects($this->exactly(1))
->method('getLastModified')
->will($this->returnValue($this->modified));
$this->item_mock->expects($this->exactly(1))
->method('getAuthor')
->will($this->returnValue($this->author));
$this->item_mock->expects($this->exactly(1))
->method('getCategories')
->will($this->returnValue($this->categories));
$item = new Item();
$item->setUnread(true)
->setUrl($this->permalink)
->setTitle('my<\' title')
->setGuidHash($this->guid_hash)
->setBody($this->parsed_body)
->setRtl(false)
->setLastModified(3)
->setPubDate(3)
->setAuthor(html_entity_decode($this->author->getName()))
->setCategoriesJson($this->categoriesJson);
// some tests deliberately omit this, so leave default value if the guid is to be ignored
if ($this->guid !== null) {
$item->setGuid($this->guid);
}
if ($enclosureType === 'audio/ogg' || $enclosureType === 'video/ogg') {
$media = $this->getMockbuilder(MediaInterface::class)->getMock();
$media->expects($this->once())
->method('getType')
->will($this->returnValue('sounds'));
$media2 = $this->getMockbuilder(MediaInterface::class)->getMock();
$media2->expects($this->exactly(2))
->method('getType')
->will($this->returnValue($enclosureType));
$media2->expects($this->once())
->method('getUrl')
->will($this->returnValue($this->enclosure));
$this->item_mock->expects($this->exactly(1))
->method('hasMedia')
->will($this->returnValue(true));
$this->item_mock->expects($this->exactly(1))
->method('getMedias')
->will($this->returnValue([$media, $media2]));
$item->setEnclosureMime($enclosureType);
$item->setEnclosureLink($this->enclosure);
}
$item->generateSearchIndex();
return $item;
}
/**
* Create a mock feed.
*
* @param string $lang Feed language.
* @param bool $favicon Fetch favicon.
* @param string|null $url Feed URL.
*
* @return Feed
*/
private function createFeed($lang = 'de-DE', $url = null)
{
$url = $url ?? $this->url;
$this->feed_mock->expects($this->exactly(3))
->method('getTitle')
->will($this->returnValue($this->feed_title));
$this->feed_mock->expects($this->exactly(1))
->method('getLink')
->will($this->returnValue($this->feed_link));
$this->feed_mock->expects($this->exactly(2))
->method('getLastModified')
->will($this->returnValue($this->modified));
$this->feed_mock->expects($this->exactly(1))
->method('getLanguage')
->will($this->returnValue($lang));
$feed = new Feed();
$feed->setTitle('&its a title');
$feed->setLink($this->feed_link);
$feed->setLocation($this->location);
$feed->setUrl($url);
$feed->setHttpLastModified((new DateTime('@3'))->format(DateTime::RSS));
$feed->setAdded($this->time);
$feed->setFaviconLink('http://anon.google.com');
$this->favicon->expects($this->exactly(1))
->method('get')
->with($this->equalTo($url))
->will($this->returnValue($this->web_favicon));
return $feed;
}
}