diff --git a/tests/mailu/ops.py b/tests/mailu/ops.py index f93ab08..3476b9c 100644 --- a/tests/mailu/ops.py +++ b/tests/mailu/ops.py @@ -1,10 +1,12 @@ -"""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).""" +"""mailu — pre-op seed hooks. Creates a test mailbox AND injects a test message to prove +backup→restore data integrity on BOTH the admin sqlite /data volume and the imap mail /mail +volume (P4 coverage, phase-mailu, ADV-mailu-01 fix).""" from __future__ import annotations import os import sys +import time sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")) from harness import lifecycle # noqa: E402 @@ -14,16 +16,48 @@ import _mailu # noqa: E402 _CI_LOCALPART = "citest" _CI_PASSWORD = "CcCi-BackupTest1!Aa" +_CI_MAIL_SUBJECT = "ccci-backup-probe" def pre_backup(ctx): - _mailu.ensure_domain(ctx.domain, ctx.domain) - _mailu.create_user(ctx.domain, _CI_LOCALPART, ctx.domain, _CI_PASSWORD) + mail_domain = ctx.domain + _mailu.ensure_domain(ctx.domain, mail_domain) + _mailu.create_user(ctx.domain, _CI_LOCALPART, mail_domain, _CI_PASSWORD) + + # Inject a test message so /mail (Maildir) is also covered by the backup/restore cycle. + # Uses in-container sendmail (same path as test_mail_flow.py: no greylist, no TLS needed). + email = f"{_CI_LOCALPART}@{mail_domain}" + sender = f"admin@{mail_domain}" + msg = ( + f"From: {sender}\\n" + f"To: {email}\\n" + f"Subject: {_CI_MAIL_SUBJECT}\\n" + f"\\nbody {_CI_MAIL_SUBJECT}\\n" + ) + lifecycle.exec_in_app( + ctx.domain, ["sh", "-c", f"printf '{msg}' | sendmail -f {sender} {email}"], service="smtp" + ) + + # Wait for dovecot to deliver the message (poll doveadm, ≤60s) + deadline = time.time() + 60 + while time.time() < deadline: + out = lifecycle.exec_in_app( + ctx.domain, + ["sh", "-c", f"doveadm search -u '{email}' mailbox INBOX header subject '{_CI_MAIL_SUBJECT}'"], + service="imap", + ) + if out.strip(): + return + time.sleep(3) + raise RuntimeError( + f"pre_backup: test message {_CI_MAIL_SUBJECT!r} not delivered to {email} within 60s" + ) 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.) + mail_domain = ctx.domain + + # 1. Delete the user from sqlite (/data) — wipes the account record lifecycle.exec_in_app( ctx.domain, [ @@ -34,3 +68,10 @@ def pre_restore(ctx): ], service="admin", ) + + # 2. Wipe the user's Maildir (/mail) — wipes the mail data so restore is observable + lifecycle.exec_in_app( + ctx.domain, + ["sh", "-c", f"rm -rf /mail/{mail_domain}/{_CI_LOCALPART}"], + service="imap", + ) diff --git a/tests/mailu/test_backup.py b/tests/mailu/test_backup.py index 2f2a7e3..c8ffab8 100644 --- a/tests/mailu/test_backup.py +++ b/tests/mailu/test_backup.py @@ -1,8 +1,9 @@ -"""mailu — BACKUP overlay: assert the seeded mailbox is present at backup time. +"""mailu — BACKUP overlay: assert both the seeded mailbox (sqlite /data) and the injected +mail message (imap /mail) are 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).""" +ops.pre_backup created citest@ AND injected a message with subject ccci-backup-probe; +this overlay asserts both are present at the moment the backup snapshot is taken. +The backup→restore divergence is in ops.pre_restore (deletes user + wipes maildir).""" from __future__ import annotations @@ -12,7 +13,11 @@ import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "functional")) import _mailu # noqa: E402 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")) +from harness import lifecycle # noqa: E402 + _CI_LOCALPART = "citest" +_CI_MAIL_SUBJECT = "ccci-backup-probe" def test_backup_captures_mailbox(live_app): @@ -23,3 +28,16 @@ def test_backup_captures_mailbox(live_app): f"seeded mailbox {email!r} not found in config-export at backup time; " f"users present: {emails}" ) + + +def test_backup_captures_mail_message(live_app): + email = f"{_CI_LOCALPART}@{live_app}" + out = lifecycle.exec_in_app( + live_app, + ["sh", "-c", f"doveadm search -u '{email}' mailbox INBOX header subject '{_CI_MAIL_SUBJECT}'"], + service="imap", + ) + assert out.strip(), ( + f"test message {_CI_MAIL_SUBJECT!r} not found in INBOX for {email!r} at backup time; " + f"the /mail Maildir was not seeded as expected" + ) diff --git a/tests/mailu/test_restore.py b/tests/mailu/test_restore.py index 1dd6cdf..f304f3b 100644 --- a/tests/mailu/test_restore.py +++ b/tests/mailu/test_restore.py @@ -1,8 +1,9 @@ -"""mailu — RESTORE overlay: assert the seeded mailbox is back after restore. +"""mailu — RESTORE overlay: assert both the seeded mailbox (sqlite /data) and the injected +mail message (imap /mail) are 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).""" +ops.pre_restore deleted the user from sqlite AND wiped the maildir to simulate full data +loss on both volumes; this overlay asserts that abra app restore brings back both the +account record (from /data) and the stored message (from /mail).""" from __future__ import annotations @@ -12,7 +13,11 @@ import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "functional")) import _mailu # noqa: E402 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")) +from harness import lifecycle # noqa: E402 + _CI_LOCALPART = "citest" +_CI_MAIL_SUBJECT = "ccci-backup-probe" def test_restore_returns_mailbox(live_app): @@ -21,6 +26,19 @@ def test_restore_returns_mailbox(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"the /data (sqlite) volume was not restored. " f"users present: {emails}" ) + + +def test_restore_returns_mail_message(live_app): + email = f"{_CI_LOCALPART}@{live_app}" + out = lifecycle.exec_in_app( + live_app, + ["sh", "-c", f"doveadm search -u '{email}' mailbox INBOX header subject '{_CI_MAIL_SUBJECT}'"], + service="imap", + ) + assert out.strip(), ( + f"test message {_CI_MAIL_SUBJECT!r} not found in INBOX for {email!r} after restore; " + f"the /mail (Maildir) volume was not restored from the backup snapshot" + )