feat: implement Story 4.2 - New Participant Registration
Implement participant registration with the following features: - POST handler for /exchange/<slug>/register - Create Participant record with lowercased email - Generate magic token and send confirmation email - Redirect to success page after registration - Rate limiting: 10 registrations per hour per IP - Validation for exchange state (must be registration_open) - Form validation for required fields (name, email) - Email format validation - Optional fields support (gift_ideas, reminder_enabled) Also includes: - Registration success page template - 429 error handling template - Flash message support in base template - Test config update for email service dev mode - Comprehensive test suite with 8 tests All tests passing (86 total), 91% coverage maintained. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
318
tests/integration/test_story_4_2_new_participant_registration.py
Normal file
318
tests/integration/test_story_4_2_new_participant_registration.py
Normal file
@@ -0,0 +1,318 @@
|
||||
"""Tests for Story 4.2: New Participant Registration.
|
||||
|
||||
As a potential participant, I want to submit my registration information
|
||||
so that I can join the Secret Santa exchange.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from src.models.exchange import Exchange
|
||||
from src.models.participant import Participant
|
||||
|
||||
|
||||
class TestStory42NewParticipantRegistration:
|
||||
"""Test suite for Story 4.2: New Participant Registration."""
|
||||
|
||||
def test_successful_participant_registration(self, client, db):
|
||||
"""Test successful participant registration creates record and sends email."""
|
||||
# Create an exchange in registration_open state
|
||||
future_close_date = datetime.utcnow() + timedelta(days=7)
|
||||
future_exchange_date = datetime.utcnow() + timedelta(days=14)
|
||||
|
||||
exchange = Exchange(
|
||||
slug="winter2025",
|
||||
name="Office Secret Santa 2025",
|
||||
description="Annual office gift exchange",
|
||||
budget="$50",
|
||||
max_participants=10,
|
||||
registration_close_date=future_close_date,
|
||||
exchange_date=future_exchange_date,
|
||||
timezone="America/New_York",
|
||||
state=Exchange.STATE_REGISTRATION_OPEN,
|
||||
)
|
||||
db.session.add(exchange)
|
||||
db.session.commit()
|
||||
|
||||
# Submit registration form
|
||||
response = client.post(
|
||||
"/exchange/winter2025/register",
|
||||
data={
|
||||
"name": "John Doe",
|
||||
"email": "john.doe@example.com",
|
||||
"gift_ideas": "Books, gadgets",
|
||||
"reminder_enabled": True,
|
||||
},
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
# Should redirect to success page
|
||||
assert response.status_code == 302
|
||||
assert "/exchange/winter2025/register/success" in response.location
|
||||
|
||||
# Should create participant record
|
||||
participant = (
|
||||
db.session.query(Participant)
|
||||
.filter_by(email="john.doe@example.com", exchange_id=exchange.id)
|
||||
.first()
|
||||
)
|
||||
assert participant is not None
|
||||
assert participant.name == "John Doe"
|
||||
assert participant.email == "john.doe@example.com"
|
||||
assert participant.gift_ideas == "Books, gadgets"
|
||||
assert participant.reminder_enabled is True
|
||||
assert participant.withdrawn_at is None
|
||||
|
||||
def test_registration_lowercases_email(self, client, db):
|
||||
"""Test that email is stored in lowercase."""
|
||||
future_close_date = datetime.utcnow() + timedelta(days=7)
|
||||
future_exchange_date = datetime.utcnow() + timedelta(days=14)
|
||||
|
||||
exchange = Exchange(
|
||||
slug="test123",
|
||||
name="Test Exchange",
|
||||
budget="$30",
|
||||
max_participants=10,
|
||||
registration_close_date=future_close_date,
|
||||
exchange_date=future_exchange_date,
|
||||
timezone="America/New_York",
|
||||
state=Exchange.STATE_REGISTRATION_OPEN,
|
||||
)
|
||||
db.session.add(exchange)
|
||||
db.session.commit()
|
||||
|
||||
# Submit with uppercase email
|
||||
client.post(
|
||||
"/exchange/test123/register",
|
||||
data={
|
||||
"name": "Jane Smith",
|
||||
"email": "Jane.Smith@Example.COM",
|
||||
"gift_ideas": "",
|
||||
"reminder_enabled": False,
|
||||
},
|
||||
)
|
||||
|
||||
# Email should be lowercased
|
||||
participant = (
|
||||
db.session.query(Participant)
|
||||
.filter_by(email="jane.smith@example.com")
|
||||
.first()
|
||||
)
|
||||
assert participant is not None
|
||||
assert participant.email == "jane.smith@example.com"
|
||||
|
||||
def test_registration_fails_if_exchange_not_open(self, client, db):
|
||||
"""Test registration fails if exchange is not in registration_open state."""
|
||||
future_close_date = datetime.utcnow() + timedelta(days=7)
|
||||
future_exchange_date = datetime.utcnow() + timedelta(days=14)
|
||||
|
||||
exchange = Exchange(
|
||||
slug="closed123",
|
||||
name="Closed Exchange",
|
||||
budget="$30",
|
||||
max_participants=10,
|
||||
registration_close_date=future_close_date,
|
||||
exchange_date=future_exchange_date,
|
||||
timezone="America/New_York",
|
||||
state=Exchange.STATE_DRAFT, # Not open for registration
|
||||
)
|
||||
db.session.add(exchange)
|
||||
db.session.commit()
|
||||
|
||||
response = client.post(
|
||||
"/exchange/closed123/register",
|
||||
data={
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
},
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
# Should show error and not redirect
|
||||
assert response.status_code == 200
|
||||
assert b"Registration is not currently open" in response.data
|
||||
|
||||
# Should not create participant
|
||||
participant = (
|
||||
db.session.query(Participant).filter_by(email="john@example.com").first()
|
||||
)
|
||||
assert participant is None
|
||||
|
||||
def test_registration_validates_required_fields(self, client, db):
|
||||
"""Test that required fields are validated."""
|
||||
future_close_date = datetime.utcnow() + timedelta(days=7)
|
||||
future_exchange_date = datetime.utcnow() + timedelta(days=14)
|
||||
|
||||
exchange = Exchange(
|
||||
slug="test123",
|
||||
name="Test Exchange",
|
||||
budget="$30",
|
||||
max_participants=10,
|
||||
registration_close_date=future_close_date,
|
||||
exchange_date=future_exchange_date,
|
||||
timezone="America/New_York",
|
||||
state=Exchange.STATE_REGISTRATION_OPEN,
|
||||
)
|
||||
db.session.add(exchange)
|
||||
db.session.commit()
|
||||
|
||||
# Missing name
|
||||
response = client.post(
|
||||
"/exchange/test123/register",
|
||||
data={
|
||||
"email": "test@example.com",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert b"Name is required" in response.data
|
||||
|
||||
# Missing email
|
||||
response = client.post(
|
||||
"/exchange/test123/register",
|
||||
data={
|
||||
"name": "Test User",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert b"Email is required" in response.data
|
||||
|
||||
def test_registration_validates_email_format(self, client, db):
|
||||
"""Test that email format is validated."""
|
||||
future_close_date = datetime.utcnow() + timedelta(days=7)
|
||||
future_exchange_date = datetime.utcnow() + timedelta(days=14)
|
||||
|
||||
exchange = Exchange(
|
||||
slug="test123",
|
||||
name="Test Exchange",
|
||||
budget="$30",
|
||||
max_participants=10,
|
||||
registration_close_date=future_close_date,
|
||||
exchange_date=future_exchange_date,
|
||||
timezone="America/New_York",
|
||||
state=Exchange.STATE_REGISTRATION_OPEN,
|
||||
)
|
||||
db.session.add(exchange)
|
||||
db.session.commit()
|
||||
|
||||
response = client.post(
|
||||
"/exchange/test123/register",
|
||||
data={
|
||||
"name": "Test User",
|
||||
"email": "not-an-email",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert b"valid email address" in response.data
|
||||
|
||||
def test_registration_rate_limiting(self, client, db):
|
||||
"""Test rate limiting prevents spam registrations (10 per hour per IP)."""
|
||||
future_close_date = datetime.utcnow() + timedelta(days=7)
|
||||
future_exchange_date = datetime.utcnow() + timedelta(days=14)
|
||||
|
||||
exchange = Exchange(
|
||||
slug="test123",
|
||||
name="Test Exchange",
|
||||
budget="$30",
|
||||
max_participants=10,
|
||||
registration_close_date=future_close_date,
|
||||
exchange_date=future_exchange_date,
|
||||
timezone="America/New_York",
|
||||
state=Exchange.STATE_REGISTRATION_OPEN,
|
||||
)
|
||||
db.session.add(exchange)
|
||||
db.session.commit()
|
||||
|
||||
# Make 10 registration attempts
|
||||
for i in range(10):
|
||||
response = client.post(
|
||||
"/exchange/test123/register",
|
||||
data={
|
||||
"name": f"User {i}",
|
||||
"email": f"user{i}@example.com",
|
||||
},
|
||||
follow_redirects=False,
|
||||
)
|
||||
# First 10 should succeed
|
||||
assert response.status_code == 302
|
||||
|
||||
# 11th attempt should be rate limited
|
||||
response = client.post(
|
||||
"/exchange/test123/register",
|
||||
data={
|
||||
"name": "User 11",
|
||||
"email": "user11@example.com",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 429 # Too Many Requests
|
||||
assert b"Too many registration attempts" in response.data
|
||||
|
||||
def test_registration_success_page_displays(self, client, db):
|
||||
"""Test that success page displays after registration."""
|
||||
future_close_date = datetime.utcnow() + timedelta(days=7)
|
||||
future_exchange_date = datetime.utcnow() + timedelta(days=14)
|
||||
|
||||
exchange = Exchange(
|
||||
slug="test123",
|
||||
name="Test Exchange",
|
||||
budget="$30",
|
||||
max_participants=10,
|
||||
registration_close_date=future_close_date,
|
||||
exchange_date=future_exchange_date,
|
||||
timezone="America/New_York",
|
||||
state=Exchange.STATE_REGISTRATION_OPEN,
|
||||
)
|
||||
db.session.add(exchange)
|
||||
db.session.commit()
|
||||
|
||||
# Register
|
||||
client.post(
|
||||
"/exchange/test123/register",
|
||||
data={
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
},
|
||||
)
|
||||
|
||||
# Access success page
|
||||
response = client.get("/exchange/test123/register/success")
|
||||
assert response.status_code == 200
|
||||
assert (
|
||||
b"successfully registered" in response.data
|
||||
or b"Registration Complete" in response.data
|
||||
)
|
||||
assert b"Test Exchange" in response.data
|
||||
|
||||
def test_registration_handles_optional_fields(self, client, db):
|
||||
"""Test that optional fields (gift_ideas, reminder_enabled) work correctly."""
|
||||
future_close_date = datetime.utcnow() + timedelta(days=7)
|
||||
future_exchange_date = datetime.utcnow() + timedelta(days=14)
|
||||
|
||||
exchange = Exchange(
|
||||
slug="test123",
|
||||
name="Test Exchange",
|
||||
budget="$30",
|
||||
max_participants=10,
|
||||
registration_close_date=future_close_date,
|
||||
exchange_date=future_exchange_date,
|
||||
timezone="America/New_York",
|
||||
state=Exchange.STATE_REGISTRATION_OPEN,
|
||||
)
|
||||
db.session.add(exchange)
|
||||
db.session.commit()
|
||||
|
||||
# Register without gift ideas and unchecked reminder
|
||||
client.post(
|
||||
"/exchange/test123/register",
|
||||
data={
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
# gift_ideas omitted
|
||||
# reminder_enabled omitted (unchecked checkbox = False)
|
||||
},
|
||||
)
|
||||
|
||||
participant = (
|
||||
db.session.query(Participant).filter_by(email="john@example.com").first()
|
||||
)
|
||||
assert participant.gift_ideas is None or participant.gift_ideas == ""
|
||||
# Unchecked checkbox means False
|
||||
assert participant.reminder_enabled is False
|
||||
Reference in New Issue
Block a user