1<?php 2 3namespace dokuwiki\test\Feed; 4 5use dokuwiki\Feed\FeedParser; 6use dokuwiki\Feed\FeedParserFile; 7use dokuwiki\HTTP\DokuHTTPClient; 8 9/** 10 * Tests for fetching and parsing remote feeds 11 */ 12class FeedParserTest extends \DokuWikiTest 13{ 14 protected const FEED = <<<XML 15<?xml version="1.0" encoding="UTF-8"?> 16<rss version="2.0"><channel> 17<title>Example Feed</title> 18<item><title>First Item</title><link>http://example.com/1</link><description>One</description></item> 19<item><title>Second Item</title><link>http://example.com/2</link><description>Two</description></item> 20</channel></rss> 21XML; 22 23 /** 24 * Builds a FeedParserFile that fetches through a mocked DokuHTTPClient. 25 * 26 * FeedParserFile creates its client in initHTTPClient() and maps the 27 * DokuHTTPClient fields onto the SimplePie response in its constructor, so we stub 28 * the factory method and then run the real constructor against the canned response. 29 * 30 * @return FeedParserFile 31 */ 32 protected function fetchedFile($status, $error, $body, $headers = ['content-type' => 'text/xml']) 33 { 34 $http = $this->createMock(DokuHTTPClient::class); 35 $http->method('sendRequest')->willReturnCallback( 36 function () use ($http, $status, $error, $body, $headers) { 37 $http->status = $status; 38 $http->error = $error; 39 $http->resp_body = $body; 40 $http->resp_headers = $headers; 41 return $status >= 200 && $status < 300; 42 } 43 ); 44 45 $file = $this->getMockBuilder(FeedParserFile::class) 46 ->onlyMethods(['initHTTPClient']) 47 ->disableOriginalConstructor() 48 ->getMock(); 49 $file->method('initHTTPClient')->willReturn($http); 50 $file->__construct('http://example.com/feed.xml'); 51 52 return $file; 53 } 54 55 /** 56 * On success DokuHTTPClient reports an empty string error and exposes the status 57 * separately. SimplePie's FileClient rejects any response whose error is non-null 58 * while the status code is zero, so the fetched file has to carry a real status code 59 * and a null error. Regression test for feed aggregation breaking after the 60 * SimplePie 1.9 upgrade. 61 */ 62 public function testSuccessfulResponseIsAcceptedBySimplePie() 63 { 64 $file = $this->fetchedFile(200, '', self::FEED); 65 66 $this->assertSame(200, $file->get_status_code()); 67 $this->assertNull($file->error); 68 $this->assertNotEmpty($file->get_body_content()); 69 } 70 71 /** 72 * DokuHTTPClient reports transport failures (no connection, SSL unsupported, timeout) 73 * as negative pseudo statuses while carrying the real message in its error field. 74 * SimplePie's FileClient only turns a file error into a thrown ClientException -- and 75 * thus surfaces that message -- when the status code is zero; any other value is 76 * reported as "unsupported status code" and the real message is lost. The pseudo 77 * statuses are therefore clamped to zero so the descriptive error reaches the output. 78 */ 79 public function testTransportFailureIsReportedWithStatusZero() 80 { 81 $file = $this->fetchedFile(-100, 'Could not connect to server', ''); 82 83 $this->assertSame(0, $file->get_status_code()); 84 $this->assertSame('Could not connect to server', $file->error); 85 } 86 87 /** 88 * DokuHTTPClient stores a header that occurred more than once as an array of 89 * values. Those must be flattened into SimplePie's list representation instead of 90 * being cast to the literal string "Array". 91 */ 92 public function testRepeatedHeadersAreFlattened() 93 { 94 $file = $this->fetchedFile(200, '', self::FEED, [ 95 'content-type' => 'text/xml', 96 'set-cookie' => ['a=1', 'b=2'], 97 ]); 98 99 $this->assertSame(['text/xml'], $file->get_header('content-type')); 100 $this->assertSame(['a=1', 'b=2'], $file->get_header('set-cookie')); 101 } 102 103 /** 104 * The fetched body is parsed into feed items 105 */ 106 public function testFeedItemsAreParsed() 107 { 108 $feed = new FeedParser(); 109 $feed->enable_order_by_date(false); 110 $feed->set_feed_url('http://example.com/feed.xml'); 111 $feed->set_file($this->fetchedFile(200, '', self::FEED)); 112 $rc = $feed->init(); 113 114 $this->assertTrue($rc); 115 $this->assertSame(2, $feed->get_item_quantity()); 116 $this->assertSame('First Item', $feed->get_item(0)->get_title()); 117 $this->assertSame('http://example.com/1', $feed->get_item(0)->get_permalink()); 118 $this->assertSame('Second Item', $feed->get_item(1)->get_title()); 119 } 120} 121