mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-11-08 02:07:07 +00:00
193 lines
11 KiB
PHP
193 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Mailcow\Tests\Retention\Service;
|
|
|
|
use Mailcow\Core\Context;
|
|
use Mailcow\Core\UserError;
|
|
use Mailcow\Retention\Model\Rule;
|
|
use Mailcow\Retention\Repository\RuleRepository;
|
|
use Mailcow\Retention\Service\Planner;
|
|
use Mailcow\Retention\Service\RuleService;
|
|
use Mailcow\Tests\Support\FakeContext;
|
|
use PHPUnit\Framework\Attributes\DataProvider;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class RuleServiceTest extends TestCase
|
|
{
|
|
private RuleRepository&MockObject $repo;
|
|
/** @var Rule[] saved rules */
|
|
private array $saved = [];
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->repo = $this->createMock(RuleRepository::class);
|
|
$this->repo->method('isDomain')->willReturnCallback(fn(string $domain): bool => in_array($domain, ['firma.de', 'tochter.de'], true));
|
|
$this->repo->method('mailboxDomain')->willReturnCallback(fn(string $username): ?string =>
|
|
['azubi@firma.de' => 'firma.de', 'chef@firma.de' => 'firma.de', 'andere@tochter.de' => 'tochter.de'][$username] ?? null);
|
|
$this->repo->method('groupDomain')->willReturnCallback(fn(int $id): ?string => [7 => 'firma.de', 9 => 'tochter.de'][$id] ?? null);
|
|
$this->repo->method('groupsIn')->willReturn([['id' => 7, 'name' => 'Azubis', 'domain' => 'firma.de', 'address' => null]]);
|
|
$this->repo->method('save')->willReturnCallback(function (Rule $rule): int {
|
|
$rule->id ??= 1;
|
|
$this->saved[] = clone $rule;
|
|
return $rule->id;
|
|
});
|
|
}
|
|
|
|
private function service(?Context $context = null): RuleService
|
|
{
|
|
return new RuleService($this->repo, $context ?? FakeContext::admin());
|
|
}
|
|
|
|
private function stored(array $values = []): Rule
|
|
{
|
|
$rule = new Rule();
|
|
$rule->id = 3;
|
|
$rule->domain = 'firma.de';
|
|
$rule->name = 'Papierkorb';
|
|
$rule->folders = ['trash', 'folder:Newsletter'];
|
|
$rule->groups = [7];
|
|
foreach ($values as $property => $value) {
|
|
$rule->$property = $value;
|
|
}
|
|
return $rule;
|
|
}
|
|
|
|
private function expectUserError(string|array $msg, callable $call): void
|
|
{
|
|
try {
|
|
$call();
|
|
$this->fail('UserError ' . json_encode($msg) . ' expected');
|
|
} catch (UserError $e) {
|
|
$this->assertSame($msg, $e->msg());
|
|
}
|
|
}
|
|
|
|
public function testCreateNormalizesTheInput(): void
|
|
{
|
|
$rule = $this->service()->create([
|
|
'domain' => 'Firma.de', 'name' => 'Aufräumen', 'folders' => ['', 'trash', 'junk', 'trash'], 'own_folders' => "Projekte/*\n\n /Newsletter/ ",
|
|
'days' => '30', 'keep_flagged' => ['0', '1'], 'all_mailboxes' => ['0'], 'mailboxes' => ['', 'Azubi@firma.de'], 'groups' => ['7'],
|
|
]);
|
|
$this->assertSame(['domain' => 'firma.de', 'name' => 'Aufräumen', 'folders' => ['trash', 'junk', 'folder:Projekte/*', 'folder:Newsletter'],
|
|
'days' => 30, 'keep_flagged' => 1, 'mode' => 'simulate', 'all_mailboxes' => 0, 'active' => 1, 'mailboxes' => ['azubi@firma.de'], 'groups' => [7]],
|
|
array_intersect_key($rule->toArray(), array_flip(['domain', 'name', 'folders', 'days', 'keep_flagged', 'mode', 'all_mailboxes', 'active', 'mailboxes', 'groups'])));
|
|
}
|
|
|
|
public function testUpdateKeepsMissingValuesAndReplacesOwnFolders(): void
|
|
{
|
|
$this->repo->method('find')->willReturn($this->stored());
|
|
$this->assertSame(['trash', 'folder:Archiv/2020'], $this->service()->update(3, ['own_folders' => 'Archiv/2020'])->folders);
|
|
// the API names all folders in folders, the UI sends the own folders apart
|
|
$this->assertSame(['junk', 'folder:Alt'], $this->service()->update(3, ['folders' => ['junk', 'folder:Alt']])->folders);
|
|
$this->assertSame(['sent', 'folder:Neu'], $this->service()->update(3, ['folders' => ['sent'], 'own_folders' => 'Neu'])->folders);
|
|
// a list keeps commas in names, INBOX is written like Dovecot does
|
|
$this->assertSame(['sent', 'folder:Kunden, alt', 'folder:INBOX/Alt', 'folder:Inboxen'],
|
|
$this->service()->update(3, ['own_folders' => ['Kunden, alt', 'inbox/Alt', 'Inboxen']])->folders);
|
|
$this->assertSame('delete', $this->service()->update(3, ['mode' => 'delete'])->mode);
|
|
}
|
|
|
|
public static function invalidInput(): array
|
|
{
|
|
return [
|
|
'no folder' => [['folders' => '', 'own_folders' => ''], 'retention_folders_invalid'],
|
|
'unknown folder' => [['folders' => ['spam']], ['retention_folder_invalid', 'spam']],
|
|
'folder with control character' => [['own_folders' => "a\tb"], ['retention_folder_invalid', "folder:a\tb"]],
|
|
'folder list with a list' => [['own_folders' => [['a']]], ['retention_folder_invalid', '']],
|
|
'no days' => [['days' => '0'], ['retention_days_invalid', 36500]],
|
|
'too many days' => [['days' => '36501'], ['retention_days_invalid', 36500]],
|
|
'mode' => [['mode' => 'now'], 'retention_mode_invalid'],
|
|
'name' => [['name' => ''], 'retention_name_invalid'],
|
|
'mailbox of another domain' => [['mailboxes' => 'andere@tochter.de'], ['retention_member_invalid', 'andere@tochter.de']],
|
|
'group of another domain' => [['groups' => ['9']], ['retention_member_invalid', '9']],
|
|
'too many mailboxes' => [['mailboxes' => array_map(fn(int $i): string => "m$i@firma.de", range(1, 501))], ['retention_entries_exceeded', 500]],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('invalidInput')]
|
|
public function testInvalidInput(array $input, string|array $error): void
|
|
{
|
|
$this->expectUserError($error, fn() => $this->service()->create($input + ['domain' => 'firma.de', 'name' => 'Test', 'folders' => ['trash'], 'days' => 30]));
|
|
$this->assertSame([], $this->saved);
|
|
}
|
|
|
|
public function testPermissions(): void
|
|
{
|
|
$this->repo->method('find')->willReturn($this->stored());
|
|
$da = FakeContext::domainAdmin([], ['firma.de'], ['retention' => '1', 'mailbox_groups' => '1']);
|
|
$create = fn(string $domain): array => ['domain' => $domain, 'name' => 'Test', 'folders' => ['trash'], 'days' => 30];
|
|
|
|
$this->assertSame('Test', $this->service($da)->create($create('firma.de'))->name);
|
|
$this->assertNotNull($this->service($da)->get(3));
|
|
$this->expectUserError('access_denied', fn() => $this->service($da)->create($create('tochter.de')));
|
|
$this->assertNull($this->service(FakeContext::domainAdmin([], ['tochter.de'], ['retention' => '1']))->get(3));
|
|
$this->expectUserError('access_denied', fn() => $this->service(FakeContext::domainAdmin([], ['firma.de'], ['mail_policies' => '1']))->create($create('firma.de')));
|
|
$this->expectUserError('access_denied', fn() => $this->service(FakeContext::user('azubi@firma.de'))->update(3, []));
|
|
$readOnly = new Context('admin', 'API', [], false, 'ro');
|
|
$this->assertNotNull($this->service($readOnly)->get(3));
|
|
$this->expectUserError('access_denied', fn() => $this->service($readOnly)->delete(3));
|
|
}
|
|
|
|
public function testGroupsNeedTheGroupAcl(): void
|
|
{
|
|
$this->repo->method('find')->willReturn($this->stored());
|
|
$da = FakeContext::domainAdmin([], ['firma.de'], ['retention' => '1']);
|
|
|
|
$this->assertSame([], $this->service($da)->groupChoices(['firma.de']));
|
|
// the form sends no group field: the assignment stays; sending it back unchanged is accepted
|
|
$this->assertSame([7], $this->service($da)->update(3, ['days' => 60])->groups);
|
|
$this->assertSame([7], $this->service($da)->update(3, ['groups' => ['7']])->groups);
|
|
$this->expectUserError('access_denied', fn() => $this->service($da)->update(3, ['groups' => ['7', '8']]));
|
|
$this->assertCount(1, $this->service(FakeContext::domainAdmin([], ['firma.de'], ['retention' => '1', 'mailbox_groups' => '1']))->groupChoices(['firma.de']));
|
|
}
|
|
|
|
public function testEffectiveRulesOfAMailbox(): void
|
|
{
|
|
$rule = $this->stored(['folders' => ['trash'], 'days' => 30]);
|
|
$this->repo->method('candidates')->willReturn(['azubi@firma.de' => [['rule' => $rule, 'level' => Planner::LEVEL_DOMAIN]]]);
|
|
|
|
$this->assertSame('trash', $this->service(FakeContext::user('Azubi@firma.de'))->effective('azubi@firma.de')[0]['folder']);
|
|
// users only see their own mailbox
|
|
$this->expectUserError('access_denied', fn() => $this->service(FakeContext::user('chef@firma.de'))->effective('azubi@firma.de'));
|
|
$this->assertCount(2, $this->service()->effective('azubi@firma.de'));
|
|
// domain admins need the ACL as well
|
|
$this->expectUserError('access_denied', fn() => $this->service(FakeContext::domainAdmin(['azubi@firma.de'], ['firma.de'], []))->effective('azubi@firma.de'));
|
|
$this->assertCount(2, $this->service(FakeContext::domainAdmin(['azubi@firma.de'], ['firma.de'], ['retention' => '1']))->effective('azubi@firma.de'));
|
|
}
|
|
|
|
public function testThePlanComesInPages(): void
|
|
{
|
|
$rule = $this->stored(['folders' => ['all'], 'days' => 30]);
|
|
$this->repo->method('mailboxesWithRules')->willReturnCallback(fn(string $after, int $limit): array =>
|
|
array_slice(array_values(array_filter(['a@firma.de', 'b@firma.de', 'c@firma.de'], fn(string $u): bool => $u > $after)), 0, $limit));
|
|
$this->repo->method('candidates')->willReturnCallback(fn(array $usernames): array =>
|
|
array_fill_keys(array_diff($usernames, ['b@firma.de']), [['rule' => $rule, 'level' => Planner::LEVEL_DOMAIN]]));
|
|
|
|
$first = $this->service(Context::system())->plan('', 2);
|
|
// b has no rule of its own: not in the plan, but the page goes on after it
|
|
$this->assertSame(['a@firma.de'], array_keys($first['mailboxes']));
|
|
$this->assertSame('b@firma.de', $first['next']);
|
|
$last = $this->service(Context::system())->plan('b@firma.de', 2);
|
|
$this->assertSame(['c@firma.de'], array_keys($last['mailboxes']));
|
|
$this->assertNull($last['next']);
|
|
}
|
|
|
|
public function testReportDropsBrokenEntries(): void
|
|
{
|
|
$this->repo->expects($this->once())->method('addLog')->with(
|
|
[['username' => 'azubi@firma.de', 'rule_id' => 3, 'folder' => 'Trash', 'matched' => 2]],
|
|
[['rule_id' => 3, 'mailboxes' => 1]],
|
|
['mailboxes' => 1]
|
|
)->willReturn(3);
|
|
$this->assertSame(3, $this->service(Context::system())->report(
|
|
[['username' => 'azubi@firma.de', 'rule_id' => 3, 'folder' => 'Trash', 'matched' => 2], ['username' => ['x']], 'text'],
|
|
[['rule_id' => 3, 'mailboxes' => 1], ['rule_id' => 'x']],
|
|
['mailboxes' => 1]
|
|
));
|
|
// the runner sends parts of 200
|
|
$this->expectUserError(['retention_entries_exceeded', 1000], fn() => $this->service(Context::system())->report(array_fill(0, 1001, []), []));
|
|
}
|
|
}
|