Skip to content

Commit 166a66c

Browse files
committed
RemotePageFetcherTest
1 parent 25ef84a commit 166a66c

File tree

2 files changed

+213
-11
lines changed

2 files changed

+213
-11
lines changed

src/Domain/Common/RemotePageFetcher.php

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -102,18 +102,17 @@ private function fetchUrlDirect(string $url): string
102102

103103
private function expandURL(string $url): string
104104
{
105-
$url_append = $this->configProvider->getValue(ConfigOption::RemoteUrlAppend);
106-
$url_append = strip_tags($url_append);
107-
$url_append = preg_replace('/\W/', '', $url_append);
108-
if ($url_append) {
109-
if (strpos($url, '?')) {
110-
$url = $url.$url_append;
111-
} else {
112-
$url = $url.'?'.$url_append;
113-
}
105+
$append = $this->configProvider->getValue(ConfigOption::RemoteUrlAppend);
106+
$append = strip_tags($append);
107+
$append = trim($append);
108+
109+
if ($append === '') {
110+
return $url;
114111
}
115112

116-
return $url;
113+
$delimiter = (str_contains($url, '?')) ? '&' : '?';
114+
$append = ltrim($append, '?&');
115+
116+
return $url . $delimiter . $append;
117117
}
118118
}
119-
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Tests\Unit\Domain\Common;
6+
7+
use Doctrine\ORM\EntityManagerInterface;
8+
use PhpList\Core\Domain\Common\HtmlUrlRewriter;
9+
use PhpList\Core\Domain\Common\RemotePageFetcher;
10+
use PhpList\Core\Domain\Configuration\Model\ConfigOption;
11+
use PhpList\Core\Domain\Configuration\Model\UrlCache;
12+
use PhpList\Core\Domain\Configuration\Repository\UrlCacheRepository;
13+
use PhpList\Core\Domain\Configuration\Service\Manager\EventLogManager;
14+
use PhpList\Core\Domain\Configuration\Service\Provider\ConfigProvider;
15+
use PHPUnit\Framework\MockObject\MockObject;
16+
use PHPUnit\Framework\TestCase;
17+
use Psr\SimpleCache\CacheInterface;
18+
use Symfony\Contracts\HttpClient\HttpClientInterface;
19+
use Symfony\Contracts\HttpClient\ResponseInterface;
20+
21+
class RemotePageFetcherTest extends TestCase
22+
{
23+
private HttpClientInterface&MockObject $httpClient;
24+
private CacheInterface&MockObject $cache;
25+
private ConfigProvider&MockObject $configProvider;
26+
private UrlCacheRepository&MockObject $urlCacheRepository;
27+
private EventLogManager&MockObject $eventLogManager;
28+
private HtmlUrlRewriter&MockObject $htmlUrlRewriter;
29+
private EntityManagerInterface&MockObject $entityManager;
30+
31+
protected function setUp(): void
32+
{
33+
$this->httpClient = $this->createMock(HttpClientInterface::class);
34+
$this->cache = $this->createMock(CacheInterface::class);
35+
$this->configProvider = $this->createMock(ConfigProvider::class);
36+
$this->urlCacheRepository = $this->createMock(UrlCacheRepository::class);
37+
$this->eventLogManager = $this->createMock(EventLogManager::class);
38+
$this->htmlUrlRewriter = $this->createMock(HtmlUrlRewriter::class);
39+
$this->entityManager = $this->createMock(EntityManagerInterface::class);
40+
}
41+
42+
private function createFetcher(int $ttl = 300): RemotePageFetcher
43+
{
44+
return new RemotePageFetcher(
45+
httpClient: $this->httpClient,
46+
cache: $this->cache,
47+
configProvider: $this->configProvider,
48+
urlCacheRepository: $this->urlCacheRepository,
49+
eventLogManager: $this->eventLogManager,
50+
htmlUrlRewriter: $this->htmlUrlRewriter,
51+
entityManager: $this->entityManager,
52+
defaultTtl: $ttl,
53+
);
54+
}
55+
56+
public function testReturnsContentFromPsrCacheWhenFresh(): void
57+
{
58+
$url = 'https://example.com/page?x=1&y=2';
59+
$this->configProvider->method('getValue')->with(ConfigOption::RemoteUrlAppend)->willReturn('');
60+
61+
$cached = [
62+
'fetched' => time(),
63+
'content' => '<p>cached</p>',
64+
];
65+
$this->cache->method('get')->with(md5($url))->willReturn($cached);
66+
67+
$this->urlCacheRepository->expects($this->never())->method('findByUrlAndLastModified');
68+
$this->httpClient->expects($this->never())->method('request');
69+
70+
$fetcher = $this->createFetcher();
71+
$result = $fetcher($url, []);
72+
73+
$this->assertSame('<p>cached</p>', $result);
74+
}
75+
76+
public function testReturnsContentFromDbCacheWhenFresh(): void
77+
{
78+
$url = 'https://ex.org/page';
79+
$this->configProvider->method('getValue')->with(ConfigOption::RemoteUrlAppend)->willReturn('');
80+
81+
$this->cache->method('get')->with(md5($url))->willReturn(null);
82+
83+
$recent = (new UrlCache())
84+
->setUrl($url)
85+
->setLastModified(time())
86+
->setContent('<p>db</p>');
87+
88+
$this->urlCacheRepository
89+
->expects($this->once())
90+
->method('findByUrlAndLastModified')
91+
->with($url)
92+
->willReturn($recent);
93+
94+
$this->httpClient->expects($this->never())->method('request');
95+
96+
$fetcher = $this->createFetcher();
97+
$result = $fetcher($url, []);
98+
99+
$this->assertSame('<p>db</p>', $result);
100+
}
101+
102+
public function testFetchesAndCachesWhenNoFreshCache(): void
103+
{
104+
$url = 'https://ex.net/a.html';
105+
$this->configProvider->method('getValue')->with(ConfigOption::RemoteUrlAppend)->willReturn('');
106+
107+
$this->cache->method('get')->with(md5($url))->willReturn(null);
108+
109+
$this->urlCacheRepository
110+
->expects($this->atLeast(2))
111+
->method('findByUrlAndLastModified')
112+
->with($this->equalTo($url), $this->logicalOr($this->equalTo(0), $this->isType('int')))
113+
->willReturnOnConsecutiveCalls(null, null);
114+
115+
$this->urlCacheRepository->method('getByUrl')->with($url)->willReturn([]);
116+
117+
$response = $this->createMock(ResponseInterface::class);
118+
$response->method('getContent')->with(false)->willReturn('<h1>hello</h1>');
119+
$this->httpClient
120+
->expects($this->once())
121+
->method('request')
122+
->with('GET', $url, $this->arrayHasKey('timeout'))
123+
->willReturn($response);
124+
125+
$this->htmlUrlRewriter
126+
->expects($this->once())
127+
->method('addAbsoluteResources')
128+
->with('<h1>hello</h1>', $url)
129+
->willReturn('rewritten:<h1>hello</h1>');
130+
131+
$this->entityManager->expects($this->once())->method('persist')
132+
->with($this->isInstanceOf(UrlCache::class));
133+
134+
$this->cache->expects($this->once())->method('set')
135+
->with(md5($url), $this->callback(function ($v) {
136+
return is_array($v)
137+
&& isset($v['fetched'], $v['content'])
138+
&& $v['content'] === 'rewritten:<h1>hello</h1>'
139+
&& is_int($v['fetched']);
140+
}));
141+
142+
$this->eventLogManager->expects($this->atLeastOnce())->method('log');
143+
144+
$fetcher = $this->createFetcher();
145+
$result = $fetcher($url, []);
146+
147+
$this->assertSame('rewritten:<h1>hello</h1>', $result);
148+
}
149+
150+
public function testHttpFailureReturnsEmptyStringAndNoCacheSet(): void
151+
{
152+
$url = 'https://bad.example/x';
153+
$this->configProvider->method('getValue')->with(ConfigOption::RemoteUrlAppend)->willReturn('');
154+
$this->cache->method('get')->with(md5($url))->willReturn(null);
155+
156+
$this->urlCacheRepository->method('findByUrlAndLastModified')->willReturn(null);
157+
158+
$this->httpClient->method('request')->willThrowException(new \RuntimeException('fail'));
159+
160+
$this->cache->expects($this->never())->method('set');
161+
$this->entityManager->expects($this->never())->method('persist');
162+
$this->htmlUrlRewriter->expects($this->never())->method('addAbsoluteResources');
163+
164+
$fetcher = $this->createFetcher();
165+
$result = $fetcher($url, []);
166+
167+
$this->assertSame('', $result);
168+
}
169+
170+
public function testUrlExpansionAndPlaceholderSubstitution(): void
171+
{
172+
$baseUrl = 'https://site.tld/path';
173+
174+
$this->configProvider->method('getValue')->with(ConfigOption::RemoteUrlAppend)->willReturn('a=1&b=2');
175+
176+
$this->cache->method('get')->willReturn(null);
177+
178+
$this->urlCacheRepository->method('findByUrlAndLastModified')->willReturn(null);
179+
$this->urlCacheRepository->method('getByUrl')->willReturn([]);
180+
181+
// After expansion, the code appends sanitized string directly. Because the URL already
182+
// contains a '?', append will be concatenated without an extra separator.
183+
184+
// The invoke method replaces placeholders in URL prior to expansion.
185+
$urlWithPlaceholders = $baseUrl . '/[name]?q=[q]&amp;x=1';
186+
$userData = ['name' => 'John Doe', 'q' => 'a&b', 'password' => 'secret'];
187+
188+
$response = $this->createMock(ResponseInterface::class);
189+
$response->method('getContent')->with(false)->willReturn('ok');
190+
$this->httpClient
191+
->expects($this->once())
192+
->method('request')
193+
->with($this->equalTo('GET'), $this->isType('string'), $this->arrayHasKey('timeout'))
194+
->willReturn($response);
195+
196+
$this->htmlUrlRewriter->method('addAbsoluteResources')->willReturnCallback(fn(string $html) => $html);
197+
198+
$fetcher = $this->createFetcher();
199+
$result = $fetcher($urlWithPlaceholders, $userData);
200+
201+
$this->assertSame('ok', $result);
202+
}
203+
}

0 commit comments

Comments
 (0)