Skip to content

Commit 4428344

Browse files
[FIX] Mail: Refactor template handling for improved testing
1 parent 308c75f commit 4428344

File tree

7 files changed

+96
-47
lines changed

7 files changed

+96
-47
lines changed

components/ILIAS/Mail/classes/class.ilMailTemplatePlaceholderResolver.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
declare(strict_types=1);
2020

2121
use ILIAS\Mail\TemplateEngine\TemplateEngineInterface;
22+
use ILIAS\Mail\TemplateEngine\MailTemplateContextAdapter;
2223

2324
class ilMailTemplatePlaceholderResolver
2425
{
@@ -37,7 +38,7 @@ public function resolve(
3738
): string {
3839
return $this->template_engine->render(
3940
$message,
40-
new ilMailTemplateContextAdapter(
41+
new MailTemplateContextAdapter(
4142
[$context],
4243
$context_parameters,
4344
$this->template_engine,

components/ILIAS/Mail/classes/Mustache/class.ilMailTemplateContextAdapter.php renamed to components/ILIAS/Mail/src/TemplateEngine/MailTemplateContextAdapter.php

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818

1919
declare(strict_types=1);
2020

21-
use ILIAS\Mail\TemplateEngine\TemplateEngineInterface;
21+
namespace ILIAS\Mail\TemplateEngine;
22+
23+
use ilMailTemplateContext;
24+
use ilObjUser;
2225

2326
/**
2427
* This class forms an interface between the existing ILIAS mail contexts and the requirements of the template engine.
@@ -29,10 +32,10 @@
2932
* if the check is successful. This is done in this class using the two magic methods.
3033
* With the introduction of this interface, the ILIAS mail contexts do not have to be changed for the template engine.
3134
*/
32-
class ilMailTemplateContextAdapter
35+
class MailTemplateContextAdapter
3336
{
3437
public function __construct(
35-
/** @var ilMailTemplateContext[] $contexts */
38+
/** @var list<ilMailTemplateContext> $contexts */
3639
protected array $contexts,
3740
protected array $context_parameter,
3841
protected readonly TemplateEngineInterface $template_engine,
@@ -56,7 +59,7 @@ static function ($placeholder): string {
5659
},
5760
array_keys($context->getPlaceholders())
5861
);
59-
if (in_array($name, $possible_placeholder, true)) {
62+
if (\in_array($name, $possible_placeholder, true)) {
6063
return true;
6164
}
6265
}
@@ -68,7 +71,7 @@ public function __get(string $name): string
6871
{
6972
foreach ($this->contexts as $context) {
7073
$ret = $context->resolvePlaceholder($name, $this->context_parameter, $this->recipient);
71-
if (in_array($name, $context->getNestedPlaceholders(), true)) {
74+
if (\in_array($name, $context->getNestedPlaceholders(), true)) {
7275
$ret = $this->template_engine->render($ret, $this);
7376
}
7477
if ($ret !== '') {

components/ILIAS/Mail/src/TemplateEngine/Mustache/MustacheTemplateEngine.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,15 @@
2121
namespace ILIAS\Mail\TemplateEngine\Mustache;
2222

2323
use ILIAS\Mail\TemplateEngine\TemplateEngineInterface;
24-
use Mustache_Engine;
2524

2625
/**
2726
* Mustache implementation of the template engine interface.
28-
* Wraps Mustache_Engine to decouple Mail component from the concrete library.
27+
* Wraps \Mustache\Engine to decouple Mail component from the concrete library.
2928
*/
3029
class MustacheTemplateEngine implements TemplateEngineInterface
3130
{
3231
public function __construct(
33-
private readonly Mustache_Engine $engine
32+
private readonly \Mustache\Engine $engine
3433
) {
3534
}
3635

components/ILIAS/Mail/src/TemplateEngine/Mustache/MustacheTemplateEngineFactory.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,11 @@
2222

2323
use ILIAS\Mail\TemplateEngine\TemplateEngineFactoryInterface;
2424
use ILIAS\Mail\TemplateEngine\TemplateEngineInterface;
25-
use Mustache_Engine;
2625

2726
class MustacheTemplateEngineFactory implements TemplateEngineFactoryInterface
2827
{
2928
public function getBasicEngine(): TemplateEngineInterface
3029
{
31-
return new MustacheTemplateEngine(new Mustache_Engine());
30+
return new MustacheTemplateEngine(new \Mustache\Engine());
3231
}
3332
}

components/ILIAS/Mail/src/TemplateEngine/TemplateEngineInterface.php

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,36 @@
2222

2323
/**
2424
* Interface for template engine functionality used in Mail and related components.
25-
* Abstracts the concrete template engine implementation (e.g. Mustache) to enable
25+
* Abstracts the concrete template engine implementation (e.g., Mustache) to enable
2626
* dependency inversion and easier testing.
2727
*/
2828
interface TemplateEngineInterface
2929
{
3030
/**
3131
* Renders a template string with the given context.
3232
*
33-
* @param string $template The template string (e.g. Mustache syntax)
34-
* @param array<string, mixed>|object $context The context data for placeholder replacement
33+
* @param string $template The template string.
34+
* @param array<string, mixed>|object $context Data for the template. Either an
35+
* associative array of key-value pairs, or an object whose public
36+
* properties and methods the engine uses to resolve values. Exact
37+
* behavior depends on the engine implementation.
38+
*
39+
* Example context object with properties, a method, and a list for repeated
40+
* blocks:
41+
* <code>
42+
* $context = new class () {
43+
* public string $recipient = 'Jane';
44+
* public int $amount = 100;
45+
* public array $lines = [
46+
* ['title' => 'Item A', 'qty' => 2],
47+
* ['title' => 'Item B', 'qty' => 1],
48+
* ];
49+
* public function net_amount(): float
50+
* {
51+
* return $this->amount * 0.9;
52+
* }
53+
* };
54+
* </code>
3555
*/
3656
public function render(string $template, array|object $context): string;
3757
}

components/ILIAS/Mail/tests/ilMailTemplateContextTest.php

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@
1818

1919
declare(strict_types=1);
2020

21+
use ILIAS\Mail\TemplateEngine\MailTemplateContextAdapter;
2122
use PHPUnit\Framework\MockObject\MockBuilder;
2223
use PHPUnit\Framework\MockObject\MockObject;
2324
use OrgUnit\PublicApi\OrgUnitUserService;
2425
use OrgUnit\User\ilOrgUnitUser;
2526
use PHPUnit\Framework\Attributes\DataProvider;
26-
use ILIAS\Mail\TemplateEngine\Mustache\MustacheTemplateEngineFactory;
2727

2828
class ilMailTemplateContextTest extends ilMailBaseTestCase
2929
{
@@ -203,7 +203,7 @@ public function testGlobalPlaceholdersCanBeResolvedWithCorrespondingValues(
203203
$env_helper->expects($this->atLeastOnce())->method('getClientId')->willReturn('###phpunit_client###');
204204
$env_helper->expects($this->atLeastOnce())->method('getHttpPath')->willReturn('###http_ilias###');
205205
$lng_helper->expects($this->atLeastOnce())->method('getLanguageByIsoCode')->willReturn($lng);
206-
$lng_helper->expects($this->atLeastOnce())->method('getCurrentLanguage')->willReturn($lng);
206+
$lng_helper->method('getCurrentLanguage')->willReturn($lng);
207207

208208
$expected_ids_constraint = [];
209209
if ($ou_superiors !== []) {
@@ -232,31 +232,53 @@ static function (ilOrgUnitUser $user): \PHPUnit\Framework\Constraint\Traversable
232232
$lng_helper
233233
);
234234

235-
$template_engine = (new class () extends MustacheTemplateEngineFactory {})->getBasicEngine();
236-
$placeholder_resolver = new ilMailTemplatePlaceholderResolver($template_engine);
237-
238-
$message = implode('', [
239-
'{{MAIL_SALUTATION}}',
240-
'{{FIRST_NAME}}',
241-
'{{LAST_NAME}}',
242-
'{{LOGIN}}',
243-
'{{TITLE}}',
244-
'{{FIRSTNAME_LASTNAME_SUPERIOR}}',
245-
'{{ILIAS_URL}}',
246-
'{{INSTALLATION_NAME}}',
247-
]);
248-
$replace_message = $placeholder_resolver->resolve($context, $message, $user);
249-
250-
$this->assertStringContainsString('###Dr. Ing###', $replace_message);
251-
$this->assertStringContainsString('###phpunit###', $replace_message);
252-
$this->assertStringContainsString('###Unit###', $replace_message);
253-
$this->assertStringContainsString('###PHP###', $replace_message);
254-
$this->assertStringContainsString('###phpunit_client###', $replace_message);
255-
$this->assertStringContainsString('###http_ilias###', $replace_message);
256-
$this->assertStringContainsString('mail_salutation_' . $user->getGender(), $replace_message);
257-
258-
foreach ($first_and_last_names as $first_and_lastname) {
259-
$this->assertStringContainsString($first_and_lastname, $replace_message);
260-
}
235+
$mail_salutation_expected = $user->getGender() === ''
236+
? 'mail_salutation_n'
237+
: 'mail_salutation_' . $user->getGender();
238+
$expected_values = [
239+
'MAIL_SALUTATION' => $mail_salutation_expected,
240+
'FIRST_NAME' => '###PHP###',
241+
'LAST_NAME' => '###Unit###',
242+
'LOGIN' => '###phpunit###',
243+
'TITLE' => '###Dr. Ing###',
244+
'FIRSTNAME_LASTNAME_SUPERIOR' => implode(', ', $first_and_last_names),
245+
'ILIAS_URL' => '###http_ilias### ',
246+
'INSTALLATION_NAME' => '###phpunit_client###',
247+
];
248+
249+
$engine = new class ($this, $expected_values) implements \ILIAS\Mail\TemplateEngine\TemplateEngineInterface {
250+
public function __construct(
251+
private readonly \PHPUnit\Framework\TestCase $test_case,
252+
/** @var array<string, string> */
253+
private readonly array $expected_values
254+
) {
255+
}
256+
257+
public function render(string $template, array|object $context): string
258+
{
259+
$this->test_case->assertInstanceOf(MailTemplateContextAdapter::class, $context);
260+
if (!str_contains($template, '{{')) {
261+
return $template;
262+
}
263+
264+
foreach ($this->expected_values as $placeholder => $expected_value) {
265+
$this->test_case->assertSame(
266+
$expected_value,
267+
$context->{$placeholder},
268+
sprintf('Context value for placeholder "%s" does not match.', $placeholder)
269+
);
270+
}
271+
272+
return '';
273+
}
274+
};
275+
276+
$placeholder_resolver = new ilMailTemplatePlaceholderResolver($engine);
277+
278+
$message = implode('', array_map(
279+
static fn(string $key): string => '{{' . $key . '}}',
280+
array_keys($expected_values)
281+
));
282+
$placeholder_resolver->resolve($context, $message, $user);
261283
}
262284
}

components/ILIAS/Mail/tests/ilMailTest.php

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ public function getAddress(): ilMailAddress
182182
};
183183
});
184184

185-
$db = $this->getMockBuilder(ilDBInterface::class)->getMock();
185+
$db = $this->createMock(ilDBInterface::class);
186186
$next_id = 0;
187187
$db->method('nextId')->willReturnCallback(function () use (&$next_id): int {
188188
++$next_id;
@@ -201,7 +201,7 @@ public function getAddress(): ilMailAddress
201201
$mail_options = $this->getMockBuilder(ilMailOptions::class)->disableOriginalConstructor()->getMock();
202202
$mail_box = $this->getMockBuilder(ilMailbox::class)->disableOriginalConstructor()->getMock();
203203
$actor = $this->getMockBuilder(ilObjUser::class)->disableOriginalConstructor()->getMock();
204-
$template_engine_factory = $this->getMockBuilder(TemplateEngineFactoryInterface::class)->getMock();
204+
$template_engine_factory = $this->createMock(TemplateEngineFactoryInterface::class);
205205

206206
$mail_service = new ilMail(
207207
$sender_usr_id,
@@ -221,7 +221,12 @@ public function getAddress(): ilMailAddress
221221
4711,
222222
$actor,
223223
new ilMailTemplatePlaceholderResolver(
224-
(new class () extends MustacheTemplateEngineFactory {})->getBasicEngine()
224+
new class () implements \ILIAS\Mail\TemplateEngine\TemplateEngineInterface {
225+
public function render(string $template, object|array $context): string
226+
{
227+
return 'phpunit';
228+
}
229+
}
225230
)
226231
);
227232

@@ -660,7 +665,7 @@ private function createAndExpectDatabaseCall(int $some_mail_id, array $row_data)
660665

661666
private function create(int $ref_id = 234, int $usr_id = 123): ilMail
662667
{
663-
$refinery = $this->getMockBuilder(\ILIAS\Refinery\Factory::class)->disableOriginalConstructor()->getMock();
668+
$refinery = $this->getMockBuilder(Factory::class)->disableOriginalConstructor()->getMock();
664669
$this->setGlobalVariable('refinery', $refinery);
665670

666671
$instance = new ilMail(
@@ -669,7 +674,7 @@ private function create(int $ref_id = 234, int $usr_id = 123): ilMail
669674
($this->mock_parser_factory = $this->getMockBuilder(ilMailRfc822AddressParserFactory::class)->disableOriginalConstructor()->getMock()),
670675
$this->getMockBuilder(ilAppEventHandler::class)->disableOriginalConstructor()->getMock(),
671676
($this->mock_log = $this->getMockBuilder(ilLogger::class)->disableOriginalConstructor()->getMock()),
672-
($this->mock_database = $this->getMockBuilder(ilDBInterface::class)->disableOriginalConstructor()->getMock()),
677+
($this->mock_database = $this->createMock(ilDBInterface::class)),
673678
($this->mock_language = $this->getMockBuilder(ilLanguage::class)->disableOriginalConstructor()->getMock()),
674679
$this->getMockBuilder(ilFileDataMail::class)->disableOriginalConstructor()->getMock(),
675680
$this->getMockBuilder(ilMailOptions::class)->disableOriginalConstructor()->getMock(),

0 commit comments

Comments
 (0)