Apply review polish to the digest send path (post-v0.1.0:76)

Non-blocking items from the v76 reviewer pass. No redeploy needed — the box runs
v76 and its happy path is unaffected; these ride the next build:

- digest_mailer.send_digest: when Gmail is enabled but no sender resolves
  (CRM_DIGEST_SENDER unset and no admin email), raise NoTransport so the caller
  returns a clear 400 instead of a generic 502.
- gmail_send.send_via_gmail: wrap OSError/URLError (timeout/DNS) as a RuntimeError
  ("Gmail API unreachable: ...") to match the HTTPError handling; include the
  sender in the HTTPError message for debuggability.
- credentials.py: correct the now-stale GMAIL_COMPOSE_SCOPE comment (the digest
  mailer sends with this scope; only outreach drafts never send).
- test_gmail_send.py: add the HTTPError->RuntimeError branch, default_sender DB
  fallback (+None case + env override), and the send_digest SMTP-tag path.

19/19 backend tests green.
This commit is contained in:
Keysat
2026-06-15 20:37:49 -05:00
parent 47dfd110a0
commit fee037a630
4 changed files with 78 additions and 3 deletions
+64
View File
@@ -5,9 +5,11 @@ No network: the credential provider and urllib.request.urlopen are monkeypatched
Run directly or via backend/run_tests.py.
"""
import base64
import io
import json
import os
import sys
import urllib.error
import urllib.request
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
@@ -153,6 +155,68 @@ def main():
gmail_send.gmail_available = orig_avail
smtp_send.smtp_configured = orig_smtp_cfg
# 7. HTTPError from Gmail is wrapped as RuntimeError (not raised raw)
orig_build = gmail_send._creds.build_provider
gmail_send._creds.build_provider = lambda factory: _Provider()
orig_open2 = urllib.request.urlopen
def raise_http(req, timeout=None):
raise urllib.error.HTTPError(req.full_url, 403, "Forbidden", {},
io.BytesIO(b'{"error":"denied"}'))
urllib.request.urlopen = raise_http
try:
try:
gmail_send.send_via_gmail("g@x", ["a@x"], "s", "b"); ok = False
except RuntimeError as e:
ok = "403" in str(e)
except urllib.error.HTTPError:
ok = False # should have been wrapped, not raised raw
finally:
urllib.request.urlopen = orig_open2
gmail_send._creds.build_provider = orig_build
check(ok, "Gmail HTTPError wrapped as RuntimeError carrying the status")
# 8. default_sender: env wins; else DB first-admin; else None
class _FakeCur:
def __init__(self, row):
self._row = row
def fetchone(self):
return self._row
class _Conn:
def __init__(self, row):
self._row = row
def execute(self, *a, **k):
return _FakeCur(self._row)
os.environ.pop("CRM_DIGEST_SENDER", None)
check(digest_mailer.default_sender(_Conn({"email": "first@ten31.xyz"})) == "first@ten31.xyz",
"default_sender falls back to first active admin")
check(digest_mailer.default_sender(_Conn(None)) is None,
"default_sender returns None when no admin has an email")
os.environ["CRM_DIGEST_SENDER"] = "env@ten31.xyz"
check(digest_mailer.default_sender(_Conn({"email": "first@ten31.xyz"})) == "env@ten31.xyz",
"CRM_DIGEST_SENDER overrides the DB lookup")
os.environ.pop("CRM_DIGEST_SENDER", None)
# 9. send_digest tags transport 'smtp' on the fallback path
orig_avail2 = gmail_send.gmail_available
orig_smtp_cfg2 = smtp_send.smtp_configured
orig_smtp_send = smtp_send.send_email
try:
gmail_send.gmail_available = lambda: False
smtp_send.smtp_configured = lambda: True
smtp_send.send_email = lambda to, subj, body, **k: {"sent_to": to, "from": "f@x"}
res = digest_mailer.send_digest(None, ["a@x"], "S", "B")
check(res["transport"] == "smtp", "send_digest tags transport smtp on fallback")
finally:
gmail_send.gmail_available = orig_avail2
smtp_send.smtp_configured = orig_smtp_cfg2
smtp_send.send_email = orig_smtp_send
print()
if FAILS:
print(f"FAILED ({len(FAILS)}):")