fix(mailu): extend backup/restore seed to cover /mail Maildir volume (ADV-mailu-01)
This commit is contained in:
@ -1,10 +1,12 @@
|
|||||||
"""mailu — pre-op seed hooks. Creates / deletes a test mailbox in the admin sqlite DB to prove
|
"""mailu — pre-op seed hooks. Creates a test mailbox AND injects a test message to prove
|
||||||
backup→restore data integrity on real mail data (P4 coverage, phase-mailu)."""
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
|
||||||
from harness import lifecycle # noqa: E402
|
from harness import lifecycle # noqa: E402
|
||||||
@ -14,16 +16,48 @@ import _mailu # noqa: E402
|
|||||||
|
|
||||||
_CI_LOCALPART = "citest"
|
_CI_LOCALPART = "citest"
|
||||||
_CI_PASSWORD = "CcCi-BackupTest1!Aa"
|
_CI_PASSWORD = "CcCi-BackupTest1!Aa"
|
||||||
|
_CI_MAIL_SUBJECT = "ccci-backup-probe"
|
||||||
|
|
||||||
|
|
||||||
def pre_backup(ctx):
|
def pre_backup(ctx):
|
||||||
_mailu.ensure_domain(ctx.domain, ctx.domain)
|
mail_domain = ctx.domain
|
||||||
_mailu.create_user(ctx.domain, _CI_LOCALPART, ctx.domain, _CI_PASSWORD)
|
_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):
|
def pre_restore(ctx):
|
||||||
# Delete the seeded user directly from sqlite to simulate data loss before restore.
|
mail_domain = ctx.domain
|
||||||
# (flask mailu has no user-delete subcommand in 2024.06.52; sqlite3 module is always available.)
|
|
||||||
|
# 1. Delete the user from sqlite (/data) — wipes the account record
|
||||||
lifecycle.exec_in_app(
|
lifecycle.exec_in_app(
|
||||||
ctx.domain,
|
ctx.domain,
|
||||||
[
|
[
|
||||||
@ -34,3 +68,10 @@ def pre_restore(ctx):
|
|||||||
],
|
],
|
||||||
service="admin",
|
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",
|
||||||
|
)
|
||||||
|
|||||||
@ -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@<domain> via the admin container; this overlay verifies the admin
|
ops.pre_backup created citest@<domain> AND injected a message with subject ccci-backup-probe;
|
||||||
sqlite DB contains that user at the moment the backup is taken. The backup→restore divergence
|
this overlay asserts both are present at the moment the backup snapshot is taken.
|
||||||
is in ops.pre_restore (which deletes the user before restore runs)."""
|
The backup→restore divergence is in ops.pre_restore (deletes user + wipes maildir)."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@ -12,7 +13,11 @@ import sys
|
|||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "functional"))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "functional"))
|
||||||
import _mailu # noqa: E402
|
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_LOCALPART = "citest"
|
||||||
|
_CI_MAIL_SUBJECT = "ccci-backup-probe"
|
||||||
|
|
||||||
|
|
||||||
def test_backup_captures_mailbox(live_app):
|
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"seeded mailbox {email!r} not found in config-export at backup time; "
|
||||||
f"users present: {emails}"
|
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"
|
||||||
|
)
|
||||||
|
|||||||
@ -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@<domain> from the admin sqlite to simulate data loss; this
|
ops.pre_restore deleted the user from sqlite AND wiped the maildir to simulate full data
|
||||||
overlay verifies that abra app restore brings the user back (the /data sqlite volume is
|
loss on both volumes; this overlay asserts that abra app restore brings back both the
|
||||||
restored from the backup taken by pre_backup, which contained the seeded user)."""
|
account record (from /data) and the stored message (from /mail)."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@ -12,7 +13,11 @@ import sys
|
|||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "functional"))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "functional"))
|
||||||
import _mailu # noqa: E402
|
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_LOCALPART = "citest"
|
||||||
|
_CI_MAIL_SUBJECT = "ccci-backup-probe"
|
||||||
|
|
||||||
|
|
||||||
def test_restore_returns_mailbox(live_app):
|
def test_restore_returns_mailbox(live_app):
|
||||||
@ -21,6 +26,19 @@ def test_restore_returns_mailbox(live_app):
|
|||||||
emails = _mailu.user_emails(cfg)
|
emails = _mailu.user_emails(cfg)
|
||||||
assert email in emails, (
|
assert email in emails, (
|
||||||
f"seeded mailbox {email!r} not found in config-export after restore; "
|
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}"
|
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"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user