feat: implement reminder preferences and withdrawal (Stories 6.3, 6.2)
Implement Phase 3 participant self-management features: Story 6.3 - Reminder Preferences: - Add ReminderPreferenceForm to participant forms - Add update_preferences route for preference updates - Update dashboard template with reminder preference toggle - Participants can enable/disable reminder emails at any time Story 6.2 - Withdrawal from Exchange: - Add can_withdraw utility function for state validation - Create WithdrawalService to handle withdrawal process - Add WithdrawForm with explicit confirmation requirement - Add withdraw route with GET (confirmation) and POST (process) - Add withdrawal confirmation email template - Update dashboard to show withdraw link when allowed - Withdrawal only allowed before registration closes - Session cleared after withdrawal, user redirected to registration All acceptance criteria met for both stories. Test coverage: 90.02% (156 tests passing) Linting and type checking: passed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
67
tests/unit/test_withdrawal_service.py
Normal file
67
tests/unit/test_withdrawal_service.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""Unit tests for withdrawal service."""
|
||||
|
||||
from datetime import datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from src.services.withdrawal import WithdrawalError, withdraw_participant
|
||||
|
||||
|
||||
def test_withdraw_participant_success(participant_factory, db, app): # noqa: ARG001
|
||||
"""Test successful withdrawal."""
|
||||
with app.app_context():
|
||||
participant = participant_factory()
|
||||
|
||||
with patch("src.services.withdrawal.EmailService") as mock_email_service:
|
||||
withdraw_participant(participant)
|
||||
|
||||
assert participant.withdrawn_at is not None
|
||||
mock_email_service.return_value.send_withdrawal_confirmation.assert_called_once()
|
||||
|
||||
|
||||
def test_withdraw_participant_already_withdrawn(participant_factory, app):
|
||||
"""Test error when already withdrawn."""
|
||||
with app.app_context():
|
||||
participant = participant_factory(withdrawn_at=datetime.utcnow())
|
||||
|
||||
with pytest.raises(WithdrawalError, match="already withdrawn"):
|
||||
withdraw_participant(participant)
|
||||
|
||||
|
||||
def test_withdraw_participant_registration_closed(
|
||||
exchange_factory, participant_factory, app
|
||||
):
|
||||
"""Test error when registration is closed."""
|
||||
with app.app_context():
|
||||
exchange = exchange_factory(state="registration_closed")
|
||||
participant = participant_factory(exchange=exchange)
|
||||
|
||||
with pytest.raises(WithdrawalError, match="Registration has closed"):
|
||||
withdraw_participant(participant)
|
||||
|
||||
|
||||
def test_withdraw_participant_after_matching(
|
||||
exchange_factory, participant_factory, app
|
||||
):
|
||||
"""Test error when matching has occurred."""
|
||||
with app.app_context():
|
||||
exchange = exchange_factory(state="matched")
|
||||
participant = participant_factory(exchange=exchange)
|
||||
|
||||
with pytest.raises(WithdrawalError, match="Matching has already occurred"):
|
||||
withdraw_participant(participant)
|
||||
|
||||
|
||||
def test_withdraw_participant_sets_timestamp(participant_factory, db, app): # noqa: ARG001
|
||||
"""Test that withdrawal sets timestamp correctly."""
|
||||
with app.app_context():
|
||||
participant = participant_factory()
|
||||
|
||||
with patch("src.services.withdrawal.EmailService"):
|
||||
before = datetime.utcnow()
|
||||
withdraw_participant(participant)
|
||||
after = datetime.utcnow()
|
||||
|
||||
assert participant.withdrawn_at is not None
|
||||
assert before <= participant.withdrawn_at <= after
|
||||
Reference in New Issue
Block a user