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
338 lines
11 KiB
Python
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()
|