From 4b5051f0039fa28fcb8d3bac565781aa97c19369 Mon Sep 17 00:00:00 2001 From: autonomic-bot Date: Thu, 11 Jun 2026 20:41:33 +0000 Subject: [PATCH] feat(mailu): add ops.py + backup/restore tests + update PARITY.md (P4 now covered via PR#3) --- tests/mailu/PARITY.md | 22 +++++++++++++++------- tests/mailu/ops.py | 36 ++++++++++++++++++++++++++++++++++++ tests/mailu/test_backup.py | 25 +++++++++++++++++++++++++ tests/mailu/test_restore.py | 26 ++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 tests/mailu/ops.py create mode 100644 tests/mailu/test_backup.py create mode 100644 tests/mailu/test_restore.py diff --git a/tests/mailu/PARITY.md b/tests/mailu/PARITY.md index 0e2aab4..dfb8a84 100644 --- a/tests/mailu/PARITY.md +++ b/tests/mailu/PARITY.md @@ -28,13 +28,21 @@ email stack: nginx front + admin + postfix/smtp + dovecot/imap + rspamd/antispam network IMAP-auth test was dropped: under notls dovecot disallows plaintext network auth, so a host-side login is not a meaningful signal here.) -## Backup data-integrity (P4) — N/A (recipe ships no backup config) -The upstream mailu recipe declares **no `backupbot.backup` label** on any service, so the cc-ci -backup/restore tiers cleanly SKIP (`backup_capable=False`). There is no recipe backup mechanism to -exercise — P4 is genuinely N/A for mailu as published, not a cut corner. The durable fix (if P4 -coverage is wanted) is a recipe-PR adding backupbot labels (mailu admin sqlite at /data + mail -volume), filed as a deferral mirroring the immich Q3.5 / Q3.2b pattern — see DEFERRED.md. Pending -Adversary §7.1 sign-off on the N/A. +## Backup data-integrity (P4) — COVERED (phase-mailu, 2026-06-11) + +P4 is now **earned** via recipe-mirror PR#3 (`add-backupbot-labels`) on +`git.autonomic.zone/recipe-maintainers/mailu`. That PR adds backupbot v2 labels to the `admin` +service (`/data` — sqlite DB) and `imap` service (`/mail` — Maildir), making `backup_capable=True` +at the PR head. + +cc-ci tests (all in `tests/mailu/`): +- `ops.py` — `pre_backup`: seeds `citest@` via `flask mailu user`; `pre_restore`: deletes + it via sqlite3 Python to simulate data loss. +- `test_backup.py` — asserts `citest@` is in `config-export` at backup time. +- `test_restore.py` — asserts `citest@` is back in `config-export` after restore. + +Auto-detected: `generic.backup_capable()` scans the compose.yml for `backupbot.backup.*true` and +returns `True` at the PR head — no `BACKUP_CAPABLE` override needed in `recipe_meta.py`. ## Browser flow (P6) Not added: mailu's user-facing UX (webmail/admin) is a standard web UI; the characteristic behaviour diff --git a/tests/mailu/ops.py b/tests/mailu/ops.py new file mode 100644 index 0000000..02e522d --- /dev/null +++ b/tests/mailu/ops.py @@ -0,0 +1,36 @@ +"""mailu — pre-op seed hooks. Creates / deletes a test mailbox in the admin sqlite DB to prove +backup→restore data integrity on real mail data (P4 coverage, phase-mailu).""" + +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")) +from harness import lifecycle # noqa: E402 + +sys.path.insert(0, os.path.dirname(__file__)) +import _mailu # noqa: E402 + +_CI_LOCALPART = "citest" +_CI_PASSWORD = "CcCi-BackupTest1!Aa" + + +def pre_backup(ctx): + _mailu.ensure_domain(ctx.domain, ctx.domain) + _mailu.create_user(ctx.domain, _CI_LOCALPART, ctx.domain, _CI_PASSWORD) + + +def pre_restore(ctx): + # Delete the seeded user directly from sqlite to simulate data loss before restore. + # (flask mailu has no user-delete subcommand in 2024.06.52; sqlite3 module is always available.) + lifecycle.exec_in_app( + ctx.domain, + [ + "python3", + "-c", + f"import sqlite3; db=sqlite3.connect('/data/main.db'); " + f"db.execute(\"DELETE FROM user WHERE localpart='{_CI_LOCALPART}'\"); db.commit()", + ], + service="admin", + ) diff --git a/tests/mailu/test_backup.py b/tests/mailu/test_backup.py new file mode 100644 index 0000000..db9be5a --- /dev/null +++ b/tests/mailu/test_backup.py @@ -0,0 +1,25 @@ +"""mailu — BACKUP overlay: assert the seeded mailbox is present at backup time. + +ops.pre_backup created citest@ via the admin container; this overlay verifies the admin +sqlite DB contains that user at the moment the backup is taken. The backup→restore divergence +is in ops.pre_restore (which deletes the user before restore runs).""" + +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(__file__)) +import _mailu # noqa: E402 + +_CI_LOCALPART = "citest" + + +def test_backup_captures_mailbox(live_app): + email = f"{_CI_LOCALPART}@{live_app}" + cfg = _mailu.config_export(live_app) + emails = _mailu.user_emails(cfg) + assert email in emails, ( + f"seeded mailbox {email!r} not found in config-export at backup time; " + f"users present: {emails}" + ) diff --git a/tests/mailu/test_restore.py b/tests/mailu/test_restore.py new file mode 100644 index 0000000..3c3695e --- /dev/null +++ b/tests/mailu/test_restore.py @@ -0,0 +1,26 @@ +"""mailu — RESTORE overlay: assert the seeded mailbox is back after restore. + +ops.pre_restore deleted citest@ from the admin sqlite to simulate data loss; this +overlay verifies that abra app restore brings the user back (the /data sqlite volume is +restored from the backup taken by pre_backup, which contained the seeded user).""" + +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(__file__)) +import _mailu # noqa: E402 + +_CI_LOCALPART = "citest" + + +def test_restore_returns_mailbox(live_app): + email = f"{_CI_LOCALPART}@{live_app}" + cfg = _mailu.config_export(live_app) + emails = _mailu.user_emails(cfg) + assert email in emails, ( + f"seeded mailbox {email!r} not found in config-export after restore; " + f"restore did not return the pre-mutation mail data. " + f"users present: {emails}" + )