xref: /dokuwiki/_test/tests/Feed/FeedParserTest.php (revision 057ab5a057a76df652cfe4176512a550ca9937f6)
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