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:
@@ -112,10 +112,7 @@ def register_error_handlers(app: Flask) -> None:
|
|||||||
@app.errorhandler(429)
|
@app.errorhandler(429)
|
||||||
def rate_limit_error(_error):
|
def rate_limit_error(_error):
|
||||||
"""Handle 429 Too Many Requests errors."""
|
"""Handle 429 Too Many Requests errors."""
|
||||||
from flask import flash, redirect, request
|
return render_template("errors/429.html"), 429
|
||||||
|
|
||||||
flash("Too many attempts. Please try again later.", "error")
|
|
||||||
return redirect(request.referrer or "/"), 429
|
|
||||||
|
|
||||||
|
|
||||||
def register_setup_check(app: Flask) -> None:
|
def register_setup_check(app: Flask) -> None:
|
||||||
@@ -140,6 +137,7 @@ def register_setup_check(app: Flask) -> None:
|
|||||||
"static",
|
"static",
|
||||||
"health",
|
"health",
|
||||||
"participant.register",
|
"participant.register",
|
||||||
|
"participant.register_success",
|
||||||
"participant.request_access",
|
"participant.request_access",
|
||||||
"participant.magic_login",
|
"participant.magic_login",
|
||||||
"participant.dashboard",
|
"participant.dashboard",
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ class TestConfig(Config):
|
|||||||
# Use a predictable secret key for tests
|
# Use a predictable secret key for tests
|
||||||
SECRET_KEY = "test-secret-key"
|
SECRET_KEY = "test-secret-key"
|
||||||
|
|
||||||
|
# Set FLASK_ENV to development to enable dev mode in EmailService
|
||||||
|
FLASK_ENV = "development"
|
||||||
|
|
||||||
|
|
||||||
# Configuration dictionary for easy access
|
# Configuration dictionary for easy access
|
||||||
config = {
|
config = {
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
"""Participant routes for Sneaky Klaus application."""
|
"""Participant routes for Sneaky Klaus application."""
|
||||||
|
|
||||||
from flask import Blueprint, abort, render_template
|
import hashlib
|
||||||
|
import secrets
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
|
from flask import Blueprint, abort, flash, redirect, render_template, request, url_for
|
||||||
|
|
||||||
from src.app import db
|
from src.app import db
|
||||||
from src.forms.participant import ParticipantRegistrationForm
|
from src.forms.participant import ParticipantRegistrationForm
|
||||||
from src.models.exchange import Exchange
|
from src.models.exchange import Exchange
|
||||||
|
from src.models.magic_token import MagicToken
|
||||||
|
from src.models.participant import Participant
|
||||||
|
from src.services.email import EmailService
|
||||||
|
from src.utils.rate_limit import check_rate_limit, increment_rate_limit
|
||||||
|
|
||||||
participant_bp = Blueprint("participant", __name__, url_prefix="")
|
participant_bp = Blueprint("participant", __name__, url_prefix="")
|
||||||
|
|
||||||
@@ -17,7 +25,7 @@ def register(slug: str):
|
|||||||
slug: Exchange registration slug.
|
slug: Exchange registration slug.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Rendered registration page template.
|
Rendered registration page template or redirect to success.
|
||||||
"""
|
"""
|
||||||
# Find the exchange by slug
|
# Find the exchange by slug
|
||||||
exchange = db.session.query(Exchange).filter_by(slug=slug).first()
|
exchange = db.session.query(Exchange).filter_by(slug=slug).first()
|
||||||
@@ -27,8 +35,126 @@ def register(slug: str):
|
|||||||
# Create the registration form
|
# Create the registration form
|
||||||
form = ParticipantRegistrationForm()
|
form = ParticipantRegistrationForm()
|
||||||
|
|
||||||
|
# Handle POST request
|
||||||
|
if form.validate_on_submit():
|
||||||
|
# Check if exchange is open for registration
|
||||||
|
if exchange.state != Exchange.STATE_REGISTRATION_OPEN:
|
||||||
|
flash("Registration is not currently open for this exchange.", "error")
|
||||||
|
return render_template(
|
||||||
|
"participant/register.html",
|
||||||
|
exchange=exchange,
|
||||||
|
form=form,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Rate limiting: 10 registrations per hour per IP
|
||||||
|
ip_address = request.remote_addr or "unknown"
|
||||||
|
rate_limit_key = f"register:{slug}:{ip_address}"
|
||||||
|
|
||||||
|
if check_rate_limit(rate_limit_key, max_attempts=10, window_minutes=60):
|
||||||
|
abort(429) # Too Many Requests
|
||||||
|
|
||||||
|
# Lowercase email for consistency
|
||||||
|
email = form.email.data.lower()
|
||||||
|
name = form.name.data
|
||||||
|
gift_ideas = form.gift_ideas.data or None
|
||||||
|
# Get reminder_enabled from form data, defaulting to True
|
||||||
|
# If checkbox not in POST data at all, it should default to True
|
||||||
|
# If explicitly unchecked, it will be False
|
||||||
|
reminder_enabled = form.reminder_enabled.data
|
||||||
|
|
||||||
|
# Create participant record
|
||||||
|
participant = Participant(
|
||||||
|
exchange_id=exchange.id,
|
||||||
|
name=name,
|
||||||
|
email=email,
|
||||||
|
gift_ideas=gift_ideas,
|
||||||
|
reminder_enabled=reminder_enabled,
|
||||||
|
)
|
||||||
|
db.session.add(participant)
|
||||||
|
db.session.flush() # Get participant ID
|
||||||
|
|
||||||
|
# Generate magic token
|
||||||
|
token = secrets.token_urlsafe(32)
|
||||||
|
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
||||||
|
|
||||||
|
magic_token = MagicToken(
|
||||||
|
token_hash=token_hash,
|
||||||
|
token_type="magic_link",
|
||||||
|
email=email,
|
||||||
|
participant_id=participant.id,
|
||||||
|
exchange_id=exchange.id,
|
||||||
|
expires_at=datetime.now(UTC) + timedelta(hours=1),
|
||||||
|
)
|
||||||
|
magic_token.validate()
|
||||||
|
db.session.add(magic_token)
|
||||||
|
|
||||||
|
# Commit before sending email
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Send registration confirmation email
|
||||||
|
magic_link_url = url_for(
|
||||||
|
"participant.magic_login",
|
||||||
|
token=token,
|
||||||
|
_external=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
email_service = EmailService()
|
||||||
|
try:
|
||||||
|
email_service.send_registration_confirmation(
|
||||||
|
to=email,
|
||||||
|
participant_name=name,
|
||||||
|
magic_link_url=magic_link_url,
|
||||||
|
exchange_name=exchange.name,
|
||||||
|
exchange_description=exchange.description,
|
||||||
|
budget_amount=float(exchange.budget.replace("$", "")),
|
||||||
|
gift_exchange_date=exchange.exchange_date.strftime("%Y-%m-%d"),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
# Log error but don't fail registration
|
||||||
|
# In production, we'd want proper logging
|
||||||
|
print(f"Failed to send confirmation email: {e}")
|
||||||
|
|
||||||
|
# Increment rate limit
|
||||||
|
increment_rate_limit(rate_limit_key, window_minutes=60)
|
||||||
|
|
||||||
|
return redirect(url_for("participant.register_success", slug=slug))
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"participant/register.html",
|
"participant/register.html",
|
||||||
exchange=exchange,
|
exchange=exchange,
|
||||||
form=form,
|
form=form,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@participant_bp.route("/exchange/<slug>/register/success")
|
||||||
|
def register_success(slug: str):
|
||||||
|
"""Registration success page.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
slug: Exchange registration slug.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Rendered success page template.
|
||||||
|
"""
|
||||||
|
exchange = db.session.query(Exchange).filter_by(slug=slug).first()
|
||||||
|
if not exchange:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"participant/register_success.html",
|
||||||
|
exchange=exchange,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@participant_bp.route("/auth/participant/magic/<token>")
|
||||||
|
def magic_login(token: str): # noqa: ARG001
|
||||||
|
"""Magic link login for participants.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token: Magic token from email link.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Redirect to participant dashboard.
|
||||||
|
"""
|
||||||
|
# Placeholder for Story 5.2
|
||||||
|
abort(404)
|
||||||
|
|||||||
12
src/templates/errors/429.html
Normal file
12
src/templates/errors/429.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{% extends "layouts/base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Too Many Requests - Sneaky Klaus{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article>
|
||||||
|
<header>
|
||||||
|
<h1>429 - Too Many Requests</h1>
|
||||||
|
</header>
|
||||||
|
<p>Too many registration attempts. Please try again later.</p>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
@@ -12,6 +12,15 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="container">
|
<main class="container">
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<article role="alert">
|
||||||
|
<p>{{ message }}</p>
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
33
src/templates/participant/register_success.html
Normal file
33
src/templates/participant/register_success.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{% extends "layouts/base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Registration Complete - {{ exchange.name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article>
|
||||||
|
<header>
|
||||||
|
<h1>Registration Complete!</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<p>You've successfully registered for <strong>{{ exchange.name }}</strong>.</p>
|
||||||
|
|
||||||
|
<p>Check your email for a confirmation message with a magic link to access your participant dashboard.</p>
|
||||||
|
|
||||||
|
<h3>What's Next?</h3>
|
||||||
|
<ul>
|
||||||
|
<li>You'll receive an email with a link to access your dashboard</li>
|
||||||
|
<li>After registration closes, you'll be assigned your Secret Santa recipient</li>
|
||||||
|
<li>You'll receive reminders about important dates</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Exchange Details</h3>
|
||||||
|
<dl>
|
||||||
|
<dt>Budget</dt>
|
||||||
|
<dd>{{ exchange.budget }}</dd>
|
||||||
|
|
||||||
|
<dt>Gift Exchange Date</dt>
|
||||||
|
<dd>{{ exchange.exchange_date.strftime('%Y-%m-%d') }}</dd>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
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
|
||||||
@@ -15,6 +15,7 @@ class TestEmailService:
|
|||||||
"""Test EmailService initialization with API key."""
|
"""Test EmailService initialization with API key."""
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
app.config["RESEND_API_KEY"] = "test-api-key"
|
app.config["RESEND_API_KEY"] = "test-api-key"
|
||||||
|
app.config["FLASK_ENV"] = "production"
|
||||||
service = EmailService()
|
service = EmailService()
|
||||||
assert service.api_key == "test-api-key"
|
assert service.api_key == "test-api-key"
|
||||||
assert service.dev_mode is False
|
assert service.dev_mode is False
|
||||||
|
|||||||
Reference in New Issue
Block a user