Skip to content

Commit c3107d0

Browse files
committed
added PHPStan level 8 and Nette Tester tests
1 parent 8ad527d commit c3107d0

16 files changed

Lines changed: 398 additions & 7 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/vendor
22
/composer.lock
33
tests/lock
4+
tests/tmp

composer.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,22 @@
1414
"php": ">=8.2",
1515
"ext-simplexml": "*"
1616
},
17+
"require-dev": {
18+
"nette/tester": "^2.6",
19+
"phpstan/phpstan": "^2.1",
20+
"phpstan/extension-installer": "^1.4",
21+
"nette/phpstan-rules": "^1.0"
22+
},
1723
"autoload": {
1824
"classmap": ["src/"]
25+
},
26+
"scripts": {
27+
"phpstan": "phpstan analyse",
28+
"tester": "tester tests -s"
29+
},
30+
"config": {
31+
"allow-plugins": {
32+
"phpstan/extension-installer": true
33+
}
1934
}
2035
}

phpstan.neon

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
parameters:
2+
level: 8
3+
4+
paths:
5+
- src
6+
7+
ignoreErrors:
8+
# intentional: allows subclassing Feed
9+
-
10+
identifier: new.static
11+
path: src/Feed.php
12+
13+
# toArray() returns recursive mixed structure
14+
-
15+
identifier: missingType.iterableValue
16+
count: 1
17+
path: src/Feed.php
18+
19+
# runtime-safe: $arr[$tag] is always array when count > 1
20+
-
21+
identifier: offsetAssign.dimType
22+
count: 1
23+
path: src/Feed.php
24+
25+
# curl_setopt expects non-empty-string, but url and userAgent are always non-empty at runtime
26+
-
27+
identifier: argument.type
28+
count: 2
29+
path: src/Feed.php

src/Feed.php

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ private static function fromRss(SimpleXMLElement $xml): static
6161
// generate 'url' & 'timestamp' tags
6262
$item->url = (string) $item->link;
6363
if (isset($item->{'dc:date'})) {
64-
$item->timestamp = strtotime($item->{'dc:date'});
64+
$item->timestamp = strtotime((string) $item->{'dc:date'});
6565
} elseif (isset($item->pubDate)) {
66-
$item->timestamp = strtotime($item->pubDate);
66+
$item->timestamp = strtotime((string) $item->pubDate);
6767
}
6868
}
6969
$feed = new static;
@@ -74,16 +74,17 @@ private static function fromRss(SimpleXMLElement $xml): static
7474

7575
private static function fromAtom(SimpleXMLElement $xml): static
7676
{
77-
if (!in_array('http://www.w3.org/2005/Atom', $xml->getDocNamespaces(), strict: true)
78-
&& !in_array('http://purl.org/atom/ns#', $xml->getDocNamespaces(), strict: true)
77+
$ns = $xml->getDocNamespaces() ?: [];
78+
if (!in_array('http://www.w3.org/2005/Atom', $ns, strict: true)
79+
&& !in_array('http://purl.org/atom/ns#', $ns, strict: true)
7980
) {
8081
throw new FeedException('Invalid feed.');
8182
}
8283

8384
// generate 'url' & 'timestamp' tags
8485
foreach ($xml->entry as $entry) {
8586
$entry->url = (string) $entry->link['href'];
86-
$entry->timestamp = strtotime($entry->updated);
87+
$entry->timestamp = strtotime((string) $entry->updated);
8788
}
8889
$feed = new static;
8990
$feed->xml = $xml;
@@ -149,7 +150,7 @@ private static function loadXml(string $url, ?string $user, ?string $pass): Simp
149150
&& $data = @file_get_contents($cacheFile)
150151
) {
151152
// ok
152-
} elseif ($data = trim(self::httpRequest($url, $user, $pass))) {
153+
} elseif (($data = self::httpRequest($url, $user, $pass)) !== false && $data = trim($data)) {
153154
if (self::$cacheDir) {
154155
file_put_contents($cacheFile, $data);
155156
}
@@ -183,7 +184,7 @@ private static function httpRequest(string $url, ?string $user, ?string $pass):
183184
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, value: true); // sometime is useful :)
184185
}
185186
$result = curl_exec($curl);
186-
return curl_errno($curl) === 0 && curl_getinfo($curl, CURLINFO_HTTP_CODE) === 200
187+
return curl_errno($curl) === 0 && curl_getinfo($curl, CURLINFO_HTTP_CODE) === 200 && is_string($result)
187188
? $result
188189
: false;
189190

tests/Feed.caching.phpt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types=1);
2+
3+
use Tester\Assert;
4+
5+
require __DIR__ . '/bootstrap.php';
6+
7+
8+
test('feed is read from cache', function () {
9+
$dir = getTempDir();
10+
Feed::$cacheDir = $dir;
11+
Feed::$cacheExpire = '1 day';
12+
13+
// pre-populate cache for a fake URL
14+
$url = 'http://example.com/cached';
15+
$cacheFile = $dir . '/feed.' . md5(serialize([$url, null, null])) . '.xml';
16+
copy(__DIR__ . '/fixtures/rss.xml', $cacheFile);
17+
18+
$feed = Feed::loadRss($url);
19+
Assert::same('Test RSS Feed', (string) $feed->title);
20+
});
21+
22+
23+
test('expired cache is not used without HTTP fallback', function () {
24+
$dir = getTempDir();
25+
Feed::$cacheDir = $dir;
26+
Feed::$cacheExpire = 0;
27+
28+
$url = 'http://invalid.example.com/feed';
29+
$cacheFile = $dir . '/feed.' . md5(serialize([$url, null, null])) . '.xml';
30+
copy(__DIR__ . '/fixtures/rss.xml', $cacheFile);
31+
32+
// set mtime to the past so the cache is expired
33+
touch($cacheFile, time() - 3600);
34+
35+
// with expired cache and unreachable URL, it falls back to stale cache
36+
$feed = Feed::loadRss($url);
37+
Assert::same('Test RSS Feed', (string) $feed->title);
38+
});

tests/Feed.errors.phpt

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php declare(strict_types=1);
2+
3+
use Tester\Assert;
4+
5+
require __DIR__ . '/bootstrap.php';
6+
7+
8+
test('loadRss throws on Atom feed', function () {
9+
setupCache('http://example.com/atom', __DIR__ . '/fixtures/atom.xml');
10+
11+
Assert::exception(
12+
fn() => Feed::loadRss('http://example.com/atom'),
13+
FeedException::class,
14+
'Invalid feed.',
15+
);
16+
});
17+
18+
19+
test('loadAtom throws on RSS feed', function () {
20+
setupCache('http://example.com/rss', __DIR__ . '/fixtures/rss.xml');
21+
22+
Assert::exception(
23+
fn() => Feed::loadAtom('http://example.com/rss'),
24+
FeedException::class,
25+
'Invalid feed.',
26+
);
27+
});
28+
29+
30+
test('loadAtom throws on invalid XML', function () {
31+
setupCache('http://example.com/invalid', __DIR__ . '/fixtures/invalid.xml');
32+
33+
Assert::exception(
34+
fn() => Feed::loadAtom('http://example.com/invalid'),
35+
FeedException::class,
36+
'Invalid feed.',
37+
);
38+
});
39+
40+
41+
test('__set throws exception', function () {
42+
setupCache('http://example.com/rss', __DIR__ . '/fixtures/rss.xml');
43+
$feed = Feed::loadRss('http://example.com/rss');
44+
45+
Assert::exception(
46+
fn() => $feed->foo = 'bar',
47+
Exception::class,
48+
"Cannot assign to a read-only property 'foo'.",
49+
);
50+
});

tests/Feed.load.phpt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php declare(strict_types=1);
2+
3+
use Tester\Assert;
4+
5+
require __DIR__ . '/bootstrap.php';
6+
7+
8+
test('auto-detect RSS feed', function () {
9+
setupCache('http://example.com/rss', __DIR__ . '/fixtures/rss.xml');
10+
$feed = Feed::load('http://example.com/rss');
11+
12+
Assert::same('Test RSS Feed', (string) $feed->title);
13+
});
14+
15+
16+
test('auto-detect Atom feed', function () {
17+
setupCache('http://example.com/atom', __DIR__ . '/fixtures/atom.xml');
18+
$feed = Feed::load('http://example.com/atom');
19+
20+
Assert::same('Test Atom Feed', (string) $feed->title);
21+
});

tests/Feed.loadAtom.phpt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types=1);
2+
3+
use Tester\Assert;
4+
5+
require __DIR__ . '/bootstrap.php';
6+
7+
8+
test('Atom feed properties', function () {
9+
setupCache('http://example.com/atom', __DIR__ . '/fixtures/atom.xml');
10+
$feed = Feed::loadAtom('http://example.com/atom');
11+
12+
Assert::same('Test Atom Feed', (string) $feed->title);
13+
});
14+
15+
16+
test('Atom feed entries', function () {
17+
setupCache('http://example.com/atom', __DIR__ . '/fixtures/atom.xml');
18+
$feed = Feed::loadAtom('http://example.com/atom');
19+
20+
$entries = [];
21+
foreach ($feed->entry as $entry) {
22+
$entries[] = $entry;
23+
}
24+
25+
Assert::count(2, $entries);
26+
Assert::same('First Entry', (string) $entries[0]->title);
27+
Assert::same('https://example.com/entry1', (string) $entries[0]->url);
28+
Assert::same('Content of first entry', (string) $entries[0]->content);
29+
});
30+
31+
32+
test('Atom feed updated to timestamp', function () {
33+
setupCache('http://example.com/atom', __DIR__ . '/fixtures/atom.xml');
34+
$feed = Feed::loadAtom('http://example.com/atom');
35+
36+
$entry = $feed->entry[0];
37+
Assert::same(strtotime('2024-01-01T12:00:00Z'), (int) $entry->timestamp);
38+
});

tests/Feed.loadRss.dc.phpt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php declare(strict_types=1);
2+
3+
use Tester\Assert;
4+
5+
require __DIR__ . '/bootstrap.php';
6+
7+
8+
test('RSS with Dublin Core dc:date to timestamp', function () {
9+
setupCache('http://example.com/dc', __DIR__ . '/fixtures/rss-dc.xml');
10+
$feed = Feed::loadRss('http://example.com/dc');
11+
12+
$item = $feed->item[0];
13+
Assert::same('DC Article', (string) $item->title);
14+
Assert::same(strtotime('2024-01-15T10:30:00+00:00'), (int) $item->timestamp);
15+
});
16+
17+
18+
test('RSS with Dublin Core namespaced tags', function () {
19+
setupCache('http://example.com/dc', __DIR__ . '/fixtures/rss-dc.xml');
20+
$feed = Feed::loadRss('http://example.com/dc');
21+
22+
$item = $feed->item[0];
23+
Assert::same('Test Author', (string) $item->{'dc:creator'});
24+
});
25+
26+
27+
test('RSS with content:encoded', function () {
28+
setupCache('http://example.com/dc', __DIR__ . '/fixtures/rss-dc.xml');
29+
$feed = Feed::loadRss('http://example.com/dc');
30+
31+
$item = $feed->item[0];
32+
Assert::same('<p>Full HTML content</p>', (string) $item->{'content:encoded'});
33+
});

tests/Feed.loadRss.phpt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php declare(strict_types=1);
2+
3+
use Tester\Assert;
4+
5+
require __DIR__ . '/bootstrap.php';
6+
7+
8+
test('RSS feed properties', function () {
9+
setupCache('http://example.com/rss', __DIR__ . '/fixtures/rss.xml');
10+
$feed = Feed::loadRss('http://example.com/rss');
11+
12+
Assert::same('Test RSS Feed', (string) $feed->title);
13+
Assert::same('A test RSS feed', (string) $feed->description);
14+
});
15+
16+
17+
test('RSS feed items', function () {
18+
setupCache('http://example.com/rss', __DIR__ . '/fixtures/rss.xml');
19+
$feed = Feed::loadRss('http://example.com/rss');
20+
21+
$items = [];
22+
foreach ($feed->item as $item) {
23+
$items[] = $item;
24+
}
25+
26+
Assert::count(2, $items);
27+
Assert::same('First Article', (string) $items[0]->title);
28+
Assert::same('https://example.com/first', (string) $items[0]->url);
29+
Assert::same('Description of first article', (string) $items[0]->description);
30+
});
31+
32+
33+
test('RSS feed pubDate to timestamp', function () {
34+
setupCache('http://example.com/rss', __DIR__ . '/fixtures/rss.xml');
35+
$feed = Feed::loadRss('http://example.com/rss');
36+
37+
$item = $feed->item[0];
38+
Assert::same(strtotime('Mon, 01 Jan 2024 12:00:00 +0000'), (int) $item->timestamp);
39+
});

0 commit comments

Comments
 (0)