Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 26 additions & 11 deletions backend/app/services/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,33 @@ def _send_weekly_digests() -> None:

sent = 0
for sub in subs:
stats = compute_subscriber_stats(db, sub.email)
if not stats:
log.debug("No stats for %s, skipping", sub.email)
try:
stats = compute_subscriber_stats(db, sub.email)
if not stats:
log.debug("No stats for %s, skipping", sub.email)
continue

ok = send_digest(stats, sub.unsubscribe_token)
if ok:
sub.last_sent_at = datetime.now(UTC)
sent += 1
else:
log.warning("Failed to deliver digest to %s", sub.email)

except Exception:
log.exception(
"Error processing weekly digest for subscriber %s",
sub.email,
)
continue
ok = send_digest(stats, sub.unsubscribe_token)
if ok:
sub.last_sent_at = datetime.now(UTC)
sent += 1
else:
log.warning("Failed to deliver digest to %s", sub.email)

db.commit()

try:
db.commit()
except Exception:
db.rollback()
log.exception("Failed to commit weekly digest updates")
return

log.info("Weekly digest sent to %d/%d subscribers", sent, len(subs))
except Exception:
log.exception("Error in weekly digest job")
Expand Down
56 changes: 56 additions & 0 deletions backend/tests/test_digest.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from app.main import app as fastapi_app
from app.models import DigestSubscription
from app.services import email_service
from app.services import scheduler
from sqlalchemy.pool import StaticPool

TEST_ENGINE = create_engine(
Expand Down Expand Up @@ -252,6 +253,8 @@ def test_unsubscribe_url_handles_base_url_slashes(
assert "//subscribe" not in unsubscribe_url




def test_unsubscribe_url_encodes_query_parameters(monkeypatch):
monkeypatch.setattr(
email_service.settings, "digest_base_url", "https://qyverixai.onrender.com"
Expand All @@ -266,3 +269,56 @@ def test_unsubscribe_url_encodes_query_parameters(monkeypatch):
assert parsed.path == "/subscribe/unsubscribe"
assert query["email"] == ["digest.user+weekly@example.com"]
assert query["token"] == ["token/value+with symbols"]

def test_scheduler_continues_after_subscriber_exception(monkeypatch):
class FakeSubscription:
def __init__(self, email):
self.email = email
self.unsubscribe_token = "token"
self.last_sent_at = None

subscribers = [
FakeSubscription("fail@example.com"),
FakeSubscription("success@example.com"),
]

class FakeQuery:
def filter(self, *_args, **_kwargs):
return self

def all(self):
return subscribers

class FakeDB:
def query(self, *_args, **_kwargs):
return FakeQuery()

def commit(self):
pass

def close(self):
pass

monkeypatch.setattr(scheduler.settings, "digest_enabled", True)
monkeypatch.setattr(scheduler, "SessionLocal", lambda: FakeDB())

def fake_compute(_db, email):
if email == "fail@example.com":
raise RuntimeError("Simulated failure")
return {"email": email}

sent = []

monkeypatch.setattr(scheduler, "compute_subscriber_stats", fake_compute)
monkeypatch.setattr(
scheduler,
"send_digest",
lambda stats, token: sent.append(stats["email"]) or True,
)

scheduler._send_weekly_digests()

assert sent == ["success@example.com"]



Loading