Skip to content

Commit a140c2b

Browse files
committed
Add tests
1 parent 823e006 commit a140c2b

File tree

4 files changed

+534
-0
lines changed

4 files changed

+534
-0
lines changed

phpunit.xml.dist

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
colors="true"
88
bootstrap="vendor/autoload.php"
99
>
10+
<testsuites>
11+
<testsuite name="Unit">
12+
<directory>tests</directory>
13+
</testsuite>
14+
</testsuites>
1015
<php>
1116
<ini name="error_reporting" value="-1"/>
1217
<server name="KERNEL_CLASS" value="PhpList\Core\Core\ApplicationKernel"/>
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Tests\Unit\Domain\Common;
6+
7+
use PhpList\Core\Domain\Common\HtmlUrlRewriter;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class HtmlUrlRewriterTest extends TestCase
11+
{
12+
private HtmlUrlRewriter $rewriter;
13+
14+
protected function setUp(): void
15+
{
16+
$this->rewriter = new HtmlUrlRewriter();
17+
}
18+
19+
public function testAbsolutizesBasicAttributes(): void
20+
{
21+
$html = '<img src="images/pic.jpg">
22+
<a href="/contact">Contact</a>
23+
<form action="submit.php"></form>
24+
<table background="bg/pat.png"></table>';
25+
26+
$base = 'https://example.com/base/dir/page.html';
27+
$out = $this->rewriter->addAbsoluteResources($html, $base);
28+
29+
$this->assertStringContainsString('src="https://example.com/base/dir/images/pic.jpg"', $out);
30+
$this->assertStringContainsString('href="https://example.com/contact"', $out);
31+
$this->assertStringContainsString('action="https://example.com/base/dir/submit.php"', $out);
32+
$this->assertStringContainsString('background="https://example.com/base/dir/bg/pat.png"', $out);
33+
}
34+
35+
public function testLeavesAloneSpecialSchemesAnchorsPlaceholders(): void
36+
{
37+
$html = '<a href="#section">Jump</a>
38+
<a href="mailto:[email protected]">Mail</a>
39+
<a href="javascript:void(0)">JS</a>
40+
<img src="data:image/png;base64,AAAA">';
41+
42+
$base = 'https://example.com/base/index.html';
43+
$out = $this->rewriter->addAbsoluteResources($html, $base);
44+
45+
$this->assertStringContainsString('href="#section"', $out);
46+
$this->assertStringContainsString('href="mailto:[email protected]"', $out);
47+
$this->assertStringContainsString('href="javascript:void(0)"', $out);
48+
$this->assertStringContainsString('src="data:image/png;base64,AAAA"', $out);
49+
}
50+
51+
public function testProtocolRelativePreservesHostAndUsesBaseScheme(): void
52+
{
53+
$html = '<img src="//cdn.example.org/img.png"><link rel="stylesheet" href="//cdn.example.org/a.css">';
54+
$base = 'https://example.com/dir/';
55+
$out = $this->rewriter->addAbsoluteResources($html, $base);
56+
57+
$this->assertStringContainsString('src="https://cdn.example.org/img.png"', $out);
58+
$this->assertStringContainsString('href="https://cdn.example.org/a.css"', $out);
59+
}
60+
61+
public function testRewritesSrcsetCandidates(): void
62+
{
63+
$html = '<img srcset="/img/a.jpg 1x, /img/b.jpg 2x, https://other/x.jpg 3x">';
64+
$base = 'http://site.test/sub/path/';
65+
$out = $this->rewriter->addAbsoluteResources($html, $base);
66+
67+
$this->assertStringContainsString(
68+
'srcset="http://site.test/img/a.jpg 1x, http://site.test/img/b.jpg 2x, https://other/x.jpg 3x"',
69+
$out
70+
);
71+
}
72+
73+
public function testRewritesCssUrlsAndImportsIncludingStyleAttribute(): void
74+
{
75+
$html = '<style>
76+
body { background-image: url("../img/bg.png"); }
77+
@import url("/css/reset.css");
78+
@import "css/theme.css";
79+
</style>
80+
<div style="background: url(icons/ico.svg) no-repeat;">X</div>
81+
';
82+
83+
$base = 'https://ex.am/dir/level/page.html';
84+
$out = $this->rewriter->addAbsoluteResources($html, $base);
85+
86+
$this->assertMatchesRegularExpression(
87+
'~url\((["\']?)https://ex\.am/dir/img/bg\.png\1\)~',
88+
$out
89+
);
90+
91+
$this->assertMatchesRegularExpression(
92+
'~@import\s+(?:url\()?(["\']?)https://ex\.am/css/reset\.css\1\)?~',
93+
$out
94+
);
95+
96+
$this->assertMatchesRegularExpression(
97+
'~@import\s+(?:url\()?(["\']?)https://ex\.am/dir/level/css/theme\.css\1\)?~',
98+
$out
99+
);
100+
101+
$this->assertMatchesRegularExpression(
102+
'~url\((["\']?)https://ex\.am/dir/level/icons/ico\.svg\1\)~',
103+
$out
104+
);
105+
}
106+
107+
public function testAbsolutizeUrlDirectlyCoversDotSegmentsAndPort(): void
108+
{
109+
$base = 'http://example.com:8080/a/b/c/';
110+
111+
$this->assertSame(
112+
'http://example.com:8080/a/b/img.png',
113+
$this->rewriter->absolutizeUrl('../img.png', $base)
114+
);
115+
116+
$this->assertSame(
117+
'http://example.com:8080/a/b/c/d/e.png?x=1#top',
118+
$this->rewriter->absolutizeUrl('d/./e.png?x=1#top', $base)
119+
);
120+
}
121+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service;
6+
7+
use PhpList\Core\Domain\Common\Html2Text;
8+
use PhpList\Core\Domain\Configuration\Model\ConfigOption;
9+
use PhpList\Core\Domain\Configuration\Service\Provider\ConfigProvider;
10+
use PhpList\Core\Domain\Messaging\Model\Template;
11+
use PhpList\Core\Domain\Messaging\Repository\TemplateRepository;
12+
use PhpList\Core\Domain\Messaging\Service\Manager\TemplateImageManager;
13+
use PhpList\Core\Domain\Messaging\Service\SystemMailConstructor;
14+
use PHPUnit\Framework\MockObject\MockObject;
15+
use PHPUnit\Framework\TestCase;
16+
17+
class SystemMailConstructorTest extends TestCase
18+
{
19+
private Html2Text&MockObject $html2Text;
20+
private ConfigProvider&MockObject $configProvider;
21+
private TemplateRepository&MockObject $templateRepository;
22+
private TemplateImageManager&MockObject $templateImageManager;
23+
24+
protected function setUp(): void
25+
{
26+
$this->html2Text = $this->getMockBuilder(Html2Text::class)
27+
->disableOriginalConstructor()
28+
->onlyMethods(['__invoke'])
29+
->getMock();
30+
$this->configProvider = $this->createMock(ConfigProvider::class);
31+
$this->templateRepository = $this->createMock(TemplateRepository::class);
32+
$this->templateImageManager = $this->getMockBuilder(TemplateImageManager::class)
33+
->disableOriginalConstructor()
34+
->onlyMethods(['parseLogoPlaceholders'])
35+
->getMock();
36+
}
37+
38+
private function createConstructor(bool $poweredByPhplist = false): SystemMailConstructor
39+
{
40+
// Defaults needed by constructor
41+
$this->configProvider->method('getValue')->willReturnMap([
42+
[ConfigOption::PoweredByText, '<b>Powered</b> by phpList'],
43+
[ConfigOption::SystemMessageTemplate, null],
44+
]);
45+
46+
return new SystemMailConstructor(
47+
html2Text: $this->html2Text,
48+
configProvider: $this->configProvider,
49+
templateRepository: $this->templateRepository,
50+
templateImageManager: $this->templateImageManager,
51+
poweredByPhplist: $poweredByPhplist,
52+
);
53+
}
54+
55+
public function testPlainTextWithoutTemplateLinkifiedAndNl2br(): void
56+
{
57+
$constructor = $this->createConstructor();
58+
59+
// Html2Text is not used when source is plain text
60+
$this->html2Text->expects($this->never())->method('__invoke');
61+
62+
[$html, $text] = $constructor('Line1' . "\n" . 'Visit http://example.com', 'Subject');
63+
64+
$this->assertSame("Line1\nVisit http://example.com", $text);
65+
$this->assertStringContainsString('Line1<br', $html);
66+
$this->assertStringContainsString('<a href="http://example.com">http://example.com</a>', $html);
67+
}
68+
69+
public function testHtmlSourceWithoutTemplateUsesHtml2Text(): void
70+
{
71+
$constructor = $this->createConstructor();
72+
73+
$this->html2Text->expects($this->once())
74+
->method('__invoke')
75+
->with('<p><strong>Hello</strong></p>')
76+
->willReturn('Hello');
77+
78+
[$html, $text] = $constructor('<p><strong>Hello</strong></p>', 'Subject');
79+
80+
$this->assertSame('<p><strong>Hello</strong></p>', $html);
81+
$this->assertSame('Hello', $text);
82+
}
83+
84+
public function testTemplateWithSignaturePlaceholderUsesPoweredByImageWhenFlagFalse(): void
85+
{
86+
// Configure template usage
87+
$this->configProvider->method('getValue')->willReturnMap([
88+
[ConfigOption::PoweredByText, '<b>Powered</b>'],
89+
[ConfigOption::SystemMessageTemplate, '10'],
90+
[ConfigOption::PoweredByImage, '<img alt="" src="/assets/power-phplist.png" />'],
91+
]);
92+
93+
$template = new Template('sys-template');
94+
$template->setContent('<html><body>[SUBJECT]: [CONTENT] [SIGNATURE]</body></html>');
95+
$template->setText("SUBJ: [SUBJECT]\n[BODY]\n[CONTENT]\n[SIGNATURE]");
96+
97+
$this->templateRepository->method('findOneById')->with(10)->willReturn($template);
98+
99+
$this->templateImageManager->expects($this->once())
100+
->method('parseLogoPlaceholders')
101+
->with($this->callback(fn ($html) => is_string($html)))
102+
->willReturnArgument(0);
103+
104+
// Plain text input so Html2Text is called only for powered by text when building text part
105+
$this->html2Text->expects($this->once())
106+
->method('__invoke')
107+
->with('<b>Powered</b>')
108+
->willReturn('Powered');
109+
110+
$constructor = new SystemMailConstructor(
111+
html2Text: $this->html2Text,
112+
configProvider: $this->configProvider,
113+
templateRepository: $this->templateRepository,
114+
templateImageManager: $this->templateImageManager,
115+
poweredByPhplist: false,
116+
);
117+
118+
[$html, $text] = $constructor('Body', 'Subject');
119+
120+
// HTML should contain processed powered-by image (src rewritten to powerphplist.png) in place of [SIGNATURE]
121+
$this->assertStringContainsString('Subject: Body', $html);
122+
$this->assertStringContainsString('src="powerphplist.png"', $html);
123+
124+
// Text should include powered by text substituted into [SIGNATURE]
125+
$this->assertStringContainsString("SUBJ: Subject\n[BODY]\nBody\nPowered", $text);
126+
}
127+
128+
public function testTemplateWithoutSignatureAppendsPoweredByTextAndBeforeBodyEndWhenHtml(): void
129+
{
130+
// Configure template usage with poweredByPhplist=true (use text snippet instead of image)
131+
$this->configProvider->method('getValue')->willReturnMap([
132+
[ConfigOption::PoweredByText, '<i>PB</i>'],
133+
[ConfigOption::SystemMessageTemplate, '11'],
134+
]);
135+
136+
$template = new Template('sys-template');
137+
$template->setContent('<html><body>[CONTENT]</body></html>');
138+
$template->setText('[CONTENT]');
139+
$this->templateRepository->method('findOneById')->with(11)->willReturn($template);
140+
141+
$this->templateImageManager->method('parseLogoPlaceholders')->willReturnCallback(static fn ($h) => $h);
142+
143+
// Html2Text is called twice: once for the HTML message -> text, and once for powered-by text
144+
$this->html2Text->expects($this->exactly(2))
145+
->method('__invoke')
146+
->withConsecutive(
147+
['Hello <b>World</b>'],
148+
['<i>PB</i>']
149+
)
150+
->willReturnOnConsecutiveCalls('Hello World', 'PB');
151+
152+
$constructor = new SystemMailConstructor(
153+
html2Text: $this->html2Text,
154+
configProvider: $this->configProvider,
155+
templateRepository: $this->templateRepository,
156+
templateImageManager: $this->templateImageManager,
157+
poweredByPhplist: true,
158+
);
159+
160+
[$html, $text] = $constructor('Hello <b>World</b>', 'Sub');
161+
162+
// HTML path: since poweredByPhplist=true, raw PoweredByText should be inserted before </body>
163+
$this->assertStringContainsString('Hello <b>World</b>', $html);
164+
$this->assertMatchesRegularExpression('~<i>PB</i></body>\s*</html>$~', $html);
165+
166+
// TEXT path: PoweredByText (converted) appended with two newlines since no [SIGNATURE]
167+
$this->assertSame("Hello World\n\nPB", $text);
168+
}
169+
}

0 commit comments

Comments
 (0)