feat: complete production backend integration

This commit is contained in:
Marek Miklewicz
2026-06-02 20:51:57 +02:00
parent d3f2fd69db
commit 4b531a4c24
20 changed files with 853 additions and 40 deletions
@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
require_once PLUGIN_ROOT . '/exec/lib/Settings.php';
require_once PLUGIN_ROOT . '/exec/lib/RuleRepository.php';
require_once PLUGIN_ROOT . '/exec/lib/DirectAdminSyncRepository.php';
require_once PLUGIN_ROOT . '/exec/lib/MailboxDirectory.php';
require_once PLUGIN_ROOT . '/exec/lib/DirectAdminVacationApi.php';
require_once PLUGIN_ROOT . '/exec/lib/DirectAdminVacationSyncService.php';
test('directadmin vacation sync creates tracked entries for enabled rule mailboxes', function (): void {
$virtualRoot = TEST_TMP . '/da-sync-virtual';
test_write($virtualRoot . '/domainowners', "example.com: alice\n");
test_write($virtualRoot . '/example.com/passwd', "info:x:1000:12::/home/alice/imap/example.com/info:/bin/false\nsales:x:1000:12::/home/alice/imap/example.com/sales:/bin/false\n");
$repo = new RuleRepository(TEST_TMP . '/da-sync-data');
$rule = $repo->create('alice', [
'subject' => 'Urlop',
'body' => 'Body',
'start_value' => '2026-06-10|morning',
'end_value' => '2026-06-24|evening',
'start_ts' => 1,
'end_ts' => 2,
'enabled' => true,
'dynamic_scope' => true,
]);
$calls = [];
$api = new class($calls) extends DirectAdminVacationApi {
public function __construct(private array &$calls) {}
public function saveVacation(string $owner, string $email, array $rule, bool $exists): void
{
$this->calls[] = ['save', $owner, $email, $exists, $rule['subject']];
}
public function deleteVacation(string $owner, string $email): void
{
$this->calls[] = ['delete', $owner, $email];
}
};
$sync = new DirectAdminVacationSyncService(
$repo,
new DirectAdminSyncRepository(TEST_TMP . '/da-sync-data'),
new MailboxDirectory($virtualRoot),
$api
);
$sync->syncUser('alice');
assert_same([
['save', 'alice', 'info@example.com', false, 'Urlop'],
['save', 'alice', 'sales@example.com', false, 'Urlop'],
], $calls);
$tracked = (new DirectAdminSyncRepository(TEST_TMP . '/da-sync-data'))->entriesForRule('alice', (string)$rule['id']);
assert_same(['info@example.com', 'sales@example.com'], array_column($tracked, 'email'));
});
test('directadmin vacation sync deletes stale tracked entries after rule removal', function (): void {
$virtualRoot = TEST_TMP . '/da-sync-delete-virtual';
test_write($virtualRoot . '/domainowners', "example.com: alice\n");
test_write($virtualRoot . '/example.com/passwd', "info:x:1000:12::/home/alice/imap/example.com/info:/bin/false\n");
$dataDir = TEST_TMP . '/da-sync-delete-data';
$repo = new RuleRepository($dataDir);
$syncRepo = new DirectAdminSyncRepository($dataDir);
$rule = $repo->create('alice', [
'subject' => 'Urlop',
'body' => 'Body',
'start_value' => '2026-06-10|morning',
'end_value' => '2026-06-24|evening',
'start_ts' => 1,
'end_ts' => 2,
'enabled' => true,
'dynamic_scope' => true,
]);
$syncRepo->replaceRuleEntries('alice', (string)$rule['id'], [
['email' => 'info@example.com', 'domain' => 'example.com', 'mailbox' => 'info', 'api_key' => 'info@example.com'],
]);
$repo->delete('alice', (string)$rule['id']);
$calls = [];
$api = new class($calls) extends DirectAdminVacationApi {
public function __construct(private array &$calls) {}
public function saveVacation(string $owner, string $email, array $rule, bool $exists): void {}
public function deleteVacation(string $owner, string $email): void
{
$this->calls[] = ['delete', $owner, $email];
}
};
$sync = new DirectAdminVacationSyncService($repo, $syncRepo, new MailboxDirectory($virtualRoot), $api);
$sync->syncUser('alice');
assert_same([['delete', 'alice', 'info@example.com']], $calls);
assert_same([], $syncRepo->entriesForRule('alice', (string)$rule['id']));
});
+46
View File
@@ -96,3 +96,49 @@ test('exim worker suppresses automated messages and ignores unresolved recipient
));
assert_same(0, $sent);
});
test('exim worker respects static mailbox snapshot scope', function (): void {
$settingsPath = TEST_TMP . '/worker-snapshot-settings.conf';
test_write($settingsPath, "GLOBAL_AUTORESPONDER_BACKEND=exim\nGLOBAL_AUTORESPONDER_REPLY_EVERY_MESSAGE=true\n");
$settings = Settings::load($settingsPath);
$virtualRoot = TEST_TMP . '/worker-snapshot-virtual';
test_write($virtualRoot . '/domainowners', "example.com: alice\n");
test_write($virtualRoot . '/example.com/passwd', "info:x:1000:12::/home/alice/imap/example.com/info:/bin/false\nsales:x:1000:12::/home/alice/imap/example.com/sales:/bin/false\n");
$repo = new RuleRepository(TEST_TMP . '/worker-snapshot-data');
$repo->create('alice', [
'subject' => 'Urlop',
'body' => 'Body',
'start_ts' => 1,
'end_ts' => 2000,
'enabled' => true,
'dynamic_scope' => false,
'mailbox_snapshot' => [
['domain' => 'example.com', 'mailbox' => 'info', 'email' => 'info@example.com'],
],
]);
$sent = 0;
$worker = new GlobalAutoresponderWorker(
$settings,
$repo,
new MailboxDirectory($virtualRoot),
new RepeatState(TEST_TMP . '/worker-snapshot-state', 1000),
static function () use (&$sent): bool {
$sent++;
return true;
},
1000
);
assert_same(0, $worker->handle(
['SENDER_ADDRESS' => 'person@example.net', 'RECIPIENT' => 'sales@example.com'],
"Auto-Submitted: no\n\nBody"
));
assert_same(1, $worker->handle(
['SENDER_ADDRESS' => 'person@example.net', 'RECIPIENT' => 'info@example.com'],
"Auto-Submitted: no\n\nBody"
));
assert_same(1, $sent);
});
+36
View File
@@ -76,3 +76,39 @@ test('rule form uses native period selector in da mode', function (): void {
assert_contains('Rano', $html);
assert_false(strpos($html, 'type="time"') !== false);
});
test('mutating handlers synchronize active backend after storage changes', function (): void {
foreach (['create.php', 'update.php', 'delete.php', 'toggle.php'] as $handler) {
$source = file_get_contents(PLUGIN_ROOT . '/exec/handlers/' . $handler) ?: '';
assert_contains('syncActiveBackend', $source, $handler . ' must synchronize backend after mutation');
}
});
test('calendar picker is based on selected rule month and supports month navigation', function (): void {
$settingsPath = TEST_TMP . '/form-calendar.conf';
test_write($settingsPath, "DEFAULT_PLUGIN_LANGUAGE=pl\nGLOBAL_AUTORESPONDER_BACKEND=exim\n");
$settings = Settings::load($settingsPath);
$ctx = new AppContext(
new DirectAdminUser('alice', 'enhanced', 'pl', TEST_TMP . '/config'),
$settings,
new RuleRepository(TEST_TMP . '/data'),
new CsrfGuard('secret', 'alice', 'sid'),
Lang::load('pl', $settings),
new AuditLog(TEST_TMP . '/audit.log')
);
$html = render_rule_form($ctx, 'update', [
'id' => 'abc',
'subject' => 'Urlop',
'body' => 'Body',
'enabled' => true,
'start_value' => '2027-02-14T20:00',
'end_value' => '2027-03-01T09:30',
]);
assert_contains('data-calendar-picker', $html);
assert_contains('data-calendar-prev', $html);
assert_contains('data-calendar-next', $html);
assert_contains('Luty 2027', $html);
assert_false(strpos($html, 'Czerwiec 2026') !== false);
});
+37
View File
@@ -7,3 +7,40 @@ test('packing guidelines exclude docs and allow later manual icon', function ():
assert_contains('must not contain `docs/`', $packing);
assert_contains('may be added later at `src/images/user_icon.svg`', $packing);
});
test('plugin metadata runs user level as root on directadmin 1.689 plus', function (): void {
$conf = file_get_contents(PLUGIN_ROOT . '/plugin.conf') ?: '';
assert_contains('type=user', $conf);
assert_contains('user_run_as=root', $conf);
});
test('shipped plugin files do not contain placeholders', function (): void {
$bad = [];
$root = new RecursiveDirectoryIterator(PLUGIN_ROOT, FilesystemIterator::SKIP_DOTS);
$it = new RecursiveIteratorIterator($root);
foreach ($it as $file) {
if (!$file->isFile()) {
continue;
}
$path = $file->getPathname();
$relative = substr($path, strlen(PLUGIN_ROOT) + 1);
if (str_starts_with($relative, '.git/') || str_starts_with($relative, 'tests/')) {
continue;
}
$content = file_get_contents($path) ?: '';
if (preg_match('/placeholder|TODO|FIXME|not implemented|pending_exim_validation|stub/i', $content)) {
$bad[] = $relative;
}
}
assert_same([], $bad);
});
test('backend script protects custom exim config with rollback and exact section insertion checks', function (): void {
$script = file_get_contents(PLUGIN_ROOT . '/scripts/backend.sh') ?: '';
assert_contains('backup_custom_conf()', $script);
assert_contains('restore_custom_conf()', $script);
assert_contains("trap 'restore_custom_conf' EXIT", $script);
assert_contains('ROLLBACK_REBUILD=1', $script);
assert_contains('if ! awk -v section="$section" -v snippet="$snippet"', $script);
assert_contains('if (inserted != 1)', $script);
});