Rework invite creation as guided flow
Magent CI/CD / verify (push) Canceled after 8m52s
Magent CI/CD / deploy-prod (push) Canceled after 0s
Magent CI/CD / deploy-beta (push) Canceled after 0s

This commit is contained in:
2026-09-01 21:53:27 +12:00
parent 3aac40ba0f
commit 2dbe11e6bc
4 changed files with 707 additions and 218 deletions
+15 -2
View File
@@ -122,6 +122,15 @@ def _require_recipient_email(value: object) -> str:
detail="recipient_email is required and must be a valid email address",
)
def _optional_recipient_email(value: object) -> Optional[str]:
if value is None or (isinstance(value, str) and not value.strip()):
return None
normalized = normalize_delivery_email(value)
if normalized:
return normalized
raise HTTPException(status_code=400, detail="recipient_email must be a valid email address")
SENSITIVE_KEYS = {
"magent_ssl_certificate_pem",
"magent_ssl_private_key_pem",
@@ -1927,8 +1936,10 @@ async def create_invite(payload: Dict[str, Any], current_user: Dict[str, Any] =
role = _normalize_role_or_none(payload.get("role"))
max_uses = _parse_optional_positive_int(payload.get("max_uses"), "max_uses")
expires_at = _parse_optional_expires_at(payload.get("expires_at"))
recipient_email = _require_recipient_email(payload.get("recipient_email"))
recipient_email = _optional_recipient_email(payload.get("recipient_email"))
send_email = bool(payload.get("send_email"))
if send_email and not recipient_email:
raise HTTPException(status_code=400, detail="recipient_email is required for email delivery")
delivery_message = _normalize_optional_text(payload.get("message"))
try:
invite = create_signup_invite(
@@ -1998,8 +2009,10 @@ async def edit_invite(invite_id: int, payload: Dict[str, Any]) -> Dict[str, Any]
role = _normalize_role_or_none(payload.get("role"))
max_uses = _parse_optional_positive_int(payload.get("max_uses"), "max_uses")
expires_at = _parse_optional_expires_at(payload.get("expires_at"))
recipient_email = _normalize_optional_text(payload.get("recipient_email"))
recipient_email = _optional_recipient_email(payload.get("recipient_email"))
send_email = bool(payload.get("send_email"))
if send_email and not recipient_email:
raise HTTPException(status_code=400, detail="recipient_email is required for email delivery")
delivery_message = _normalize_optional_text(payload.get("message"))
try:
invite = update_signup_invite(
+26
View File
@@ -1793,6 +1793,32 @@ class PortalMediaStatusTests(unittest.IsolatedAsyncioTestCase):
class InviteOperationalStateTests(TempDatabaseMixin, unittest.IsolatedAsyncioTestCase):
async def test_manual_invite_can_be_created_without_recipient_email(self) -> None:
payload = await admin_router.create_invite(
{
"label": "The neighbour",
"recipient_email": None,
"send_email": False,
"max_uses": 1,
},
{"username": "admin", "role": "admin"},
)
self.assertEqual(payload["status"], "ok")
self.assertEqual(payload["invite"]["label"], "The neighbour")
self.assertIsNone(payload["invite"]["recipient_email"])
self.assertTrue(payload["invite"]["enabled"])
async def test_email_delivery_still_requires_valid_recipient(self) -> None:
with self.assertRaises(HTTPException) as context:
await admin_router.create_invite(
{"label": "Family", "send_email": True},
{"username": "admin", "role": "admin"},
)
self.assertEqual(context.exception.status_code, 400)
self.assertIn("required for email delivery", str(context.exception.detail))
async def test_invite_list_reports_automatic_operational_states(self) -> None:
ready = db.create_signup_invite(code="READY", recipient_email="ready@example.com")
db.create_signup_invite(code="DISABLED", enabled=False, recipient_email="off@example.com")