diff --git a/tests/integration/test_registration_link.py b/tests/integration/test_registration_link.py new file mode 100644 index 0000000..3754931 --- /dev/null +++ b/tests/integration/test_registration_link.py @@ -0,0 +1,338 @@ +"""Integration tests for Story 2.6: Generate Registration Link.""" + +from datetime import datetime, timedelta + +from src.models import Exchange + + +class TestGenerateRegistrationLink: + """Test cases for registration link generation (Story 2.6).""" + + def test_registration_link_generated_for_exchange(self, client, db, admin): # noqa: ARG002 + """Test that unique link is generated for each exchange. + + Acceptance Criteria: + - Unique link generated for each exchange + """ + # Login first + client.post( + "/admin/login", + data={ + "email": "admin@example.com", + "password": "testpassword123", + }, + follow_redirects=True, + ) + + # Create two exchanges + future_close_date = datetime.utcnow() + timedelta(days=7) + future_exchange_date = datetime.utcnow() + timedelta(days=14) + + exchange1 = Exchange( + slug=Exchange.generate_slug(), + name="Exchange 1", + budget="$20-30", + max_participants=10, + registration_close_date=future_close_date, + exchange_date=future_exchange_date, + timezone="America/New_York", + state=Exchange.STATE_DRAFT, + ) + + exchange2 = Exchange( + slug=Exchange.generate_slug(), + name="Exchange 2", + budget="$20-30", + max_participants=10, + registration_close_date=future_close_date, + exchange_date=future_exchange_date, + timezone="America/New_York", + state=Exchange.STATE_DRAFT, + ) + + db.session.add(exchange1) + db.session.add(exchange2) + db.session.commit() + + # View first exchange details + response1 = client.get(f"/admin/exchange/{exchange1.id}") + assert response1.status_code == 200 + + # View second exchange details + response2 = client.get(f"/admin/exchange/{exchange2.id}") + assert response2.status_code == 200 + + # Verify different slugs in registration links + assert exchange1.slug.encode() in response1.data + assert exchange2.slug.encode() in response2.data + assert exchange1.slug != exchange2.slug + + def test_registration_link_is_copyable(self, client, db, admin): # noqa: ARG002 + """Test that link is copyable to clipboard. + + Acceptance Criteria: + - Link is copyable to clipboard + """ + # Login first + client.post( + "/admin/login", + data={ + "email": "admin@example.com", + "password": "testpassword123", + }, + follow_redirects=True, + ) + + # Create an exchange + future_close_date = datetime.utcnow() + timedelta(days=7) + future_exchange_date = datetime.utcnow() + timedelta(days=14) + + exchange = Exchange( + slug=Exchange.generate_slug(), + name="Test Exchange", + budget="$20-30", + max_participants=10, + registration_close_date=future_close_date, + exchange_date=future_exchange_date, + timezone="America/New_York", + state=Exchange.STATE_DRAFT, + ) + + db.session.add(exchange) + db.session.commit() + + # View exchange details + response = client.get(f"/admin/exchange/{exchange.id}") + assert response.status_code == 200 + + # Verify copy button or mechanism exists + assert b"copy" in response.data.lower() or b"clipboard" in response.data.lower() + + def test_registration_link_displayed_in_detail_view(self, client, db, admin): # noqa: ARG002 + """Test that link is displayed when exchange is in appropriate state. + + Acceptance Criteria: + - Link is displayed when exchange is in appropriate state + """ + # 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) + future_exchange_date = datetime.utcnow() + timedelta(days=14) + + exchange = Exchange( + slug=Exchange.generate_slug(), + name="Test Exchange", + budget="$20-30", + max_participants=10, + registration_close_date=future_close_date, + exchange_date=future_exchange_date, + timezone="America/New_York", + state=Exchange.STATE_DRAFT, + ) + + db.session.add(exchange) + db.session.commit() + + # View exchange details + response = client.get(f"/admin/exchange/{exchange.id}") + assert response.status_code == 200 + + # Verify registration link section exists + assert ( + b"Registration Link" in response.data + or b"registration link" in response.data.lower() + ) + + def test_registration_link_leads_to_correct_page(self, client, db, admin): # noqa: ARG002 + """Test that link leads to registration page for that specific exchange. + + Acceptance Criteria: + - Link leads to registration page for that specific exchange + """ + # 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) + future_exchange_date = datetime.utcnow() + timedelta(days=14) + + exchange = Exchange( + slug=Exchange.generate_slug(), + name="Test Exchange", + budget="$20-30", + max_participants=10, + registration_close_date=future_close_date, + exchange_date=future_exchange_date, + timezone="America/New_York", + state=Exchange.STATE_DRAFT, + ) + + db.session.add(exchange) + db.session.commit() + + # View exchange details + response = client.get(f"/admin/exchange/{exchange.id}") + assert response.status_code == 200 + + # Verify the link format contains the slug + expected_link_part = f"/exchange/{exchange.slug}/register" + assert expected_link_part.encode() in response.data + + def test_registration_link_uses_slug_not_id(self, client, db, admin): # noqa: ARG002 + """Test that registration link uses slug instead of numeric ID. + + Acceptance Criteria: + - Unique link generated for each exchange + - Link uses slug (not ID) for security + """ + # 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) + future_exchange_date = datetime.utcnow() + timedelta(days=14) + + exchange = Exchange( + slug=Exchange.generate_slug(), + name="Test Exchange", + budget="$20-30", + max_participants=10, + registration_close_date=future_close_date, + exchange_date=future_exchange_date, + timezone="America/New_York", + state=Exchange.STATE_DRAFT, + ) + + db.session.add(exchange) + db.session.commit() + + # View exchange details + response = client.get(f"/admin/exchange/{exchange.id}") + assert response.status_code == 200 + + # Verify slug is in the link + assert exchange.slug.encode() in response.data + + # Verify numeric ID is NOT in the registration link part + # (It will be in admin URLs, but not in the public registration link) + response_text = response.data.decode() + # Extract registration link section + if "Registration Link" in response_text: + # Find the registration link + assert f"/exchange/{exchange.slug}/register" in response_text + # Should NOT contain /exchange/{id}/register + assert f"/exchange/{exchange.id}/register" not in response_text + + def test_registration_link_contains_full_url(self, client, db, admin): # noqa: ARG002 + """Test that registration link is a complete URL. + + Acceptance Criteria: + - Link is copyable (implies full URL) + """ + # 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) + future_exchange_date = datetime.utcnow() + timedelta(days=14) + + exchange = Exchange( + slug=Exchange.generate_slug(), + name="Test Exchange", + budget="$20-30", + max_participants=10, + registration_close_date=future_close_date, + exchange_date=future_exchange_date, + timezone="America/New_York", + state=Exchange.STATE_DRAFT, + ) + + db.session.add(exchange) + db.session.commit() + + # View exchange details + response = client.get(f"/admin/exchange/{exchange.id}") + assert response.status_code == 200 + + # Verify full URL is displayed (starts with http:// or https://) + assert ( + b"http://" in response.data or b"https://" in response.data + ), "Registration link should be a full URL" + + def test_registration_link_available_in_all_states(self, client, db, admin): # noqa: ARG002 + """Test that registration link is available regardless of exchange state. + + Acceptance Criteria: + - Link is displayed when exchange is in appropriate state + - Note: Based on design, link is always shown but may have different behavior + """ + # Login first + client.post( + "/admin/login", + data={ + "email": "admin@example.com", + "password": "testpassword123", + }, + follow_redirects=True, + ) + + future_close_date = datetime.utcnow() + timedelta(days=7) + future_exchange_date = datetime.utcnow() + timedelta(days=14) + + # Test in different states + for state in [ + Exchange.STATE_DRAFT, + Exchange.STATE_REGISTRATION_OPEN, + ]: + exchange = Exchange( + slug=Exchange.generate_slug(), + name=f"Exchange in {state}", + budget="$20-30", + max_participants=10, + registration_close_date=future_close_date, + exchange_date=future_exchange_date, + timezone="America/New_York", + state=state, + ) + + db.session.add(exchange) + db.session.commit() + + # View exchange details + response = client.get(f"/admin/exchange/{exchange.id}") + assert response.status_code == 200 + + # Verify registration link is displayed + assert exchange.slug.encode() in response.data + assert b"/exchange/" in response.data + assert b"/register" in response.data