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
This commit is contained in:
337
tests/integration/test_create_exchange.py
Normal file
337
tests/integration/test_create_exchange.py
Normal file
@@ -0,0 +1,337 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user