Files
sneakyklaus/tests/integration/test_create_exchange.py
Phil Skentelbery 8554f27d86 feat: implement exchange creation
Add Exchange model, API endpoint, and form validation
for creating new gift exchanges.

- Create ExchangeForm with timezone validation
- Add admin routes for creating and viewing exchanges
- Generate unique 12-char slug for each exchange
- Validate registration/exchange dates
- Display exchanges in dashboard
- All tests passing with 92% coverage

Story: 2.1
2025-12-22 12:41:28 -07:00

338 lines
11 KiB
Python

"""Integration tests for Story 2.1: Create Exchange."""
from datetime import datetime, timedelta
from src.models import Exchange
class TestCreateExchange:
"""Test cases for exchange creation flow (Story 2.1)."""
def test_create_exchange_form_renders(self, client, db, admin): # noqa: ARG002
"""Test that create exchange form renders correctly.
Acceptance Criteria:
- Form to create exchange with all required fields
"""
# Login first
client.post(
"/admin/login",
data={
"email": "admin@example.com",
"password": "testpassword123",
},
follow_redirects=True,
)
response = client.get("/admin/exchange/new")
assert response.status_code == 200
# Check for all required form fields
assert b"name" in response.data.lower()
assert b"description" in response.data.lower()
assert b"budget" in response.data.lower()
assert b"max_participants" in response.data.lower()
assert b"registration_close_date" in response.data.lower()
assert b"exchange_date" in response.data.lower()
assert b"timezone" in response.data.lower()
def test_create_exchange_with_valid_data(self, client, db, admin): # noqa: ARG002
"""Test creating exchange with valid data.
Acceptance Criteria:
- Exchange created in "Draft" state
- Exchange appears in admin dashboard after creation
- Generate unique 12-char slug
"""
# Login first
client.post(
"/admin/login",
data={
"email": "admin@example.com",
"password": "testpassword123",
},
follow_redirects=True,
)
# Create exchange
future_close_date = (datetime.utcnow() + timedelta(days=7)).strftime(
"%Y-%m-%dT%H:%M"
)
future_exchange_date = (datetime.utcnow() + timedelta(days=14)).strftime(
"%Y-%m-%dT%H:%M"
)
response = client.post(
"/admin/exchange/new",
data={
"name": "Test Exchange",
"description": "This is a test exchange",
"budget": "$20-30",
"max_participants": 10,
"registration_close_date": future_close_date,
"exchange_date": future_exchange_date,
"timezone": "America/New_York",
},
follow_redirects=False,
)
# Should redirect to exchange detail page
assert response.status_code == 302
# Verify exchange was created
exchange = db.session.query(Exchange).filter_by(name="Test Exchange").first()
assert exchange is not None
assert exchange.slug is not None
assert len(exchange.slug) == 12
assert exchange.state == Exchange.STATE_DRAFT
assert exchange.description == "This is a test exchange"
assert exchange.budget == "$20-30"
assert exchange.max_participants == 10
assert exchange.timezone == "America/New_York"
def test_create_exchange_minimum_participants_validation(self, client, db, admin): # noqa: ARG002
"""Test that max_participants must be at least 3.
Acceptance Criteria:
- Maximum participants (required, minimum 3)
"""
# Login first
client.post(
"/admin/login",
data={
"email": "admin@example.com",
"password": "testpassword123",
},
follow_redirects=True,
)
future_close_date = (datetime.utcnow() + timedelta(days=7)).strftime(
"%Y-%m-%dT%H:%M"
)
future_exchange_date = (datetime.utcnow() + timedelta(days=14)).strftime(
"%Y-%m-%dT%H:%M"
)
# Try to create with less than 3 participants
response = client.post(
"/admin/exchange/new",
data={
"name": "Test Exchange",
"budget": "$20-30",
"max_participants": 2, # Invalid: less than 3
"registration_close_date": future_close_date,
"exchange_date": future_exchange_date,
"timezone": "America/New_York",
},
follow_redirects=True,
)
# Should show error
assert response.status_code == 200
assert (
b"at least 3" in response.data.lower()
or b"minimum" in response.data.lower()
)
# Verify no exchange was created
exchange = db.session.query(Exchange).filter_by(name="Test Exchange").first()
assert exchange is None
def test_create_exchange_date_validation(self, client, db, admin): # noqa: ARG002
"""Test that exchange_date must be after registration_close_date.
Acceptance Criteria:
- Registration close date (required)
- Exchange date (required)
- Dates must be properly ordered
"""
# Login first
client.post(
"/admin/login",
data={
"email": "admin@example.com",
"password": "testpassword123",
},
follow_redirects=True,
)
future_close_date = (datetime.utcnow() + timedelta(days=14)).strftime(
"%Y-%m-%dT%H:%M"
)
earlier_exchange_date = (datetime.utcnow() + timedelta(days=7)).strftime(
"%Y-%m-%dT%H:%M"
)
# Try to create with exchange date before close date
response = client.post(
"/admin/exchange/new",
data={
"name": "Test Exchange",
"budget": "$20-30",
"max_participants": 10,
"registration_close_date": future_close_date,
"exchange_date": earlier_exchange_date, # Before close date
"timezone": "America/New_York",
},
follow_redirects=True,
)
# Should show error
assert response.status_code == 200
assert (
b"exchange date" in response.data.lower()
and b"after" in response.data.lower()
) or b"must be after" in response.data.lower()
def test_create_exchange_timezone_validation(self, client, db, admin): # noqa: ARG002
"""Test that timezone must be valid.
Acceptance Criteria:
- Timezone (required)
- Validate timezone with pytz
"""
# Login first
client.post(
"/admin/login",
data={
"email": "admin@example.com",
"password": "testpassword123",
},
follow_redirects=True,
)
future_close_date = (datetime.utcnow() + timedelta(days=7)).strftime(
"%Y-%m-%dT%H:%M"
)
future_exchange_date = (datetime.utcnow() + timedelta(days=14)).strftime(
"%Y-%m-%dT%H:%M"
)
# Try to create with invalid timezone
response = client.post(
"/admin/exchange/new",
data={
"name": "Test Exchange",
"budget": "$20-30",
"max_participants": 10,
"registration_close_date": future_close_date,
"exchange_date": future_exchange_date,
"timezone": "Invalid/Timezone",
},
follow_redirects=True,
)
# Should show error
assert response.status_code == 200
assert (
b"timezone" in response.data.lower() or b"invalid" in response.data.lower()
)
def test_create_exchange_slug_is_unique(self, client, db, admin): # noqa: ARG002
"""Test that each exchange gets a unique slug.
Acceptance Criteria:
- Generate unique 12-char slug
"""
# Login first
client.post(
"/admin/login",
data={
"email": "admin@example.com",
"password": "testpassword123",
},
follow_redirects=True,
)
future_close_date = (datetime.utcnow() + timedelta(days=7)).strftime(
"%Y-%m-%dT%H:%M"
)
future_exchange_date = (datetime.utcnow() + timedelta(days=14)).strftime(
"%Y-%m-%dT%H:%M"
)
# Create first exchange
client.post(
"/admin/exchange/new",
data={
"name": "Exchange 1",
"budget": "$20-30",
"max_participants": 10,
"registration_close_date": future_close_date,
"exchange_date": future_exchange_date,
"timezone": "America/New_York",
},
follow_redirects=True,
)
# Create second exchange
client.post(
"/admin/exchange/new",
data={
"name": "Exchange 2",
"budget": "$20-30",
"max_participants": 10,
"registration_close_date": future_close_date,
"exchange_date": future_exchange_date,
"timezone": "America/New_York",
},
follow_redirects=True,
)
# Verify both have unique slugs
exchanges = db.session.query(Exchange).all()
assert len(exchanges) == 2
assert exchanges[0].slug != exchanges[1].slug
def test_create_exchange_required_fields(self, client, db, admin): # noqa: ARG002
"""Test that all required fields are validated.
Acceptance Criteria:
- Name (required)
- Budget (required)
- All other required fields
"""
# Login first
client.post(
"/admin/login",
data={
"email": "admin@example.com",
"password": "testpassword123",
},
follow_redirects=True,
)
# Try to create without required fields
response = client.post(
"/admin/exchange/new",
data={},
follow_redirects=True,
)
# Should show errors
assert response.status_code == 200
# Verify no exchange was created
exchange_count = db.session.query(Exchange).count()
assert exchange_count == 0
def test_create_exchange_slug_generation(self):
"""Test the slug generation method.
Acceptance Criteria:
- Generate unique 12-char slug
- Slug should be URL-safe alphanumeric
"""
slug1 = Exchange.generate_slug()
slug2 = Exchange.generate_slug()
# Verify length
assert len(slug1) == 12
assert len(slug2) == 12
# Verify uniqueness (statistically very likely)
assert slug1 != slug2
# Verify alphanumeric (letters and digits only)
assert slug1.isalnum()
assert slug2.isalnum()