From 0d8715c6f9d2edfd3d8dfae4b75147245cff1ba7 Mon Sep 17 00:00:00 2001 From: Leon Glaz Date: Fri, 17 Oct 2025 03:25:43 +0000 Subject: [PATCH 1/3] Add extracurricular activities and signup validation to the API --- src/app.py | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index 4ebb1d9..c9ab86c 100644 --- a/src/app.py +++ b/src/app.py @@ -38,6 +38,45 @@ "schedule": "Mondays, Wednesdays, Fridays, 2:00 PM - 3:00 PM", "max_participants": 30, "participants": ["john@mergington.edu", "olivia@mergington.edu"] + }, + # Sports related activities + "Soccer Team": { + "description": "Join the school soccer team and compete in local leagues", + "schedule": "Wednesdays and Fridays, 4:00 PM - 5:30 PM", + "max_participants": 18, + "participants": ["lucas@mergington.edu", "mia@mergington.edu"] + }, + "Basketball Club": { + "description": "Practice basketball skills and play friendly matches", + "schedule": "Tuesdays, 5:00 PM - 6:30 PM", + "max_participants": 15, + "participants": ["liam@mergington.edu", "ava@mergington.edu"] + }, + # Artistic activities + "Art Club": { + "description": "Explore painting, drawing, and other visual arts", + "schedule": "Thursdays, 3:30 PM - 5:00 PM", + "max_participants": 16, + "participants": ["charlotte@mergington.edu", "amelia@mergington.edu"] + }, + "Drama Society": { + "description": "Participate in theater productions and acting workshops", + "schedule": "Mondays, 4:00 PM - 5:30 PM", + "max_participants": 20, + "participants": ["jack@mergington.edu", "ella@mergington.edu"] + }, + # Intellectual activities + "Math Olympiad": { + "description": "Prepare for math competitions and solve challenging problems", + "schedule": "Wednesdays, 3:30 PM - 4:30 PM", + "max_participants": 10, + "participants": ["noah@mergington.edu", "isabella@mergington.edu"] + }, + "Science Club": { + "description": "Conduct experiments and explore scientific concepts", + "schedule": "Fridays, 2:00 PM - 3:30 PM", + "max_participants": 14, + "participants": ["benjamin@mergington.edu", "grace@mergington.edu"] } } @@ -61,7 +100,9 @@ def signup_for_activity(activity_name: str, email: str): # Get the specific activity activity = activities[activity_name] - + # Validate student is not already signed up + if email in activity["participants"]: + raise HTTPException(status_code=400, detail="Student already signed up for this activity") # Add student activity["participants"].append(email) return {"message": f"Signed up {email} for {activity_name}"} From a12a7df24a556febfffc9bc0ded0728180ccc08a Mon Sep 17 00:00:00 2001 From: Leon Glaz Date: Fri, 17 Oct 2025 03:40:37 +0000 Subject: [PATCH 2/3] Add participant validation and display in signup process --- src/app.py | 6 ++++++ src/static/app.js | 19 ++++++++++++++++--- src/static/styles.css | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/app.py b/src/app.py index c9ab86c..fec7534 100644 --- a/src/app.py +++ b/src/app.py @@ -100,9 +100,15 @@ def signup_for_activity(activity_name: str, email: str): # Get the specific activity activity = activities[activity_name] + + # Validate activity is not at capacity + if len(activity["participants"]) >= activity["max_participants"]: + raise HTTPException(status_code=400, detail="Activity is at full capacity") + # Validate student is not already signed up if email in activity["participants"]: raise HTTPException(status_code=400, detail="Student already signed up for this activity") + # Add student activity["participants"].append(email) return {"message": f"Signed up {email} for {activity_name}"} diff --git a/src/static/app.js b/src/static/app.js index dcc1e38..13d4c65 100644 --- a/src/static/app.js +++ b/src/static/app.js @@ -20,11 +20,19 @@ document.addEventListener("DOMContentLoaded", () => { const spotsLeft = details.max_participants - details.participants.length; + const participantsList = details.participants.length > 0 + ? `` + : '

No participants yet

'; + activityCard.innerHTML = `

${name}

-

${details.description}

+

Description: ${details.description}

Schedule: ${details.schedule}

-

Availability: ${spotsLeft} spots left

+

Capacity: ${details.participants.length}/${details.max_participants}

+
+
Current Participants:
+ ${participantsList} +
`; activitiesList.appendChild(activityCard); @@ -50,9 +58,13 @@ document.addEventListener("DOMContentLoaded", () => { try { const response = await fetch( - `/activities/${encodeURIComponent(activity)}/signup?email=${encodeURIComponent(email)}`, + `/activities/${encodeURIComponent(activity)}/signup`, { method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: `email=${encodeURIComponent(email)}`, } ); @@ -62,6 +74,7 @@ document.addEventListener("DOMContentLoaded", () => { messageDiv.textContent = result.message; messageDiv.className = "success"; signupForm.reset(); + fetchActivities(); // Refresh the activities list } else { messageDiv.textContent = result.detail || "An error occurred"; messageDiv.className = "error"; diff --git a/src/static/styles.css b/src/static/styles.css index a533b32..4f26ae9 100644 --- a/src/static/styles.css +++ b/src/static/styles.css @@ -74,6 +74,48 @@ section h3 { margin-bottom: 8px; } +.participants-section { + margin-top: 15px; + padding-top: 12px; + border-top: 1px solid #e0e0e0; +} + +.participants-section h5 { + margin-bottom: 8px; + color: #1a237e; + font-size: 14px; + font-weight: bold; +} + +.participants-list { + list-style: none; + padding-left: 0; + margin: 0; +} + +.participants-list li { + padding: 4px 0; + padding-left: 16px; + position: relative; + font-size: 14px; + color: #555; +} + +.participants-list li:before { + content: "•"; + color: #1a237e; + font-weight: bold; + position: absolute; + left: 0; +} + +.no-participants { + font-style: italic; + color: #888; + font-size: 14px; + margin: 0; +} + .form-group { margin-bottom: 15px; } From 58a185019997c09c10e08d490f5b831d7a60c2a8 Mon Sep 17 00:00:00 2001 From: Leon Glaz Date: Fri, 17 Oct 2025 03:56:51 +0000 Subject: [PATCH 3/3] Implement unregister functionality for activities and enhance participant management --- .coverage | Bin 0 -> 53248 bytes requirements.txt | 3 + src/app.py | 19 ++++ src/static/app.js | 72 +++++++++++-- src/static/styles.css | 42 +++++++- tests/README.md | 52 +++++++++ tests/__init__.py | 1 + tests/conftest.py | 85 +++++++++++++++ tests/test_api.py | 241 ++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 505 insertions(+), 10 deletions(-) create mode 100644 .coverage create mode 100644 tests/README.md create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_api.py diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..ed50fbe552c30fc58a5b60e45aedf561dc61eb80 GIT binary patch literal 53248 zcmeI)O=}xR7zglO$&n&iVpD<<#str$aZD^ZB8Soz+PbX|p@B4Q=%r9)v>M6VNW1dR zDz*!yC@BOA{RW}mBmEGCeuDJY@&Z4lPp_b zUcc%^O0R=;IkX$WN`;j?7g($D%a|&@G$-XW^)mEYM zU8PMF7rfSbasBQO#eC)LS^J)f>m);$Zi`d3p#$TpTxnB5ZV1K-;ns8Qo@h;h!1xEg-2A-EFM1KwWDRo*>oThuGnzTWN9>#c?yL_z$# zL5<(2(?jXUw@&9PtE=|C-MFJLPowEYdbCrR>NKOnGrA6+pWSbGluUOL9!1@*2-@+G z;oQPB4)x~AF|(Os$dCt`2$F;p=_~DR94y5kn%$r|-PhtCqZJJhnhVlo?Sb#|P|;im zH@K}r*-^FRteF;Gbw{GEQ!nHy+pDv>&ZrAC>Z9G_a5Oq)JB+r^8(;VYjG94qy8LCb-pQ~)G%&Jb3u2D~RP7m`7`O4CgJv1YN z=OwLgpjmkUvug)!VX5vq@iiAq`oYgUt#cIAKdtV%ZD^QQUv|Y1}<|?HR{w3K&06 z?J3oIoScUD=YiH9Evvk2>G#s3Vr@rio`v+9Xa+%7N#Cpul;et~YkFG5F&tf$J{*^! z54eoZ?X@_jbaCNn{ss-YbbQfHuP6OhvR?5rY)YlMp9d#j$4nxWQLNFR)HFn&Dl;uK zImH1ps zS>1qJt5N4iewTcjU&;+v?R@3pMSD0%2BVp08k!dMo=n3|o@S;x$x|oC{pA`>xb$jg zvY)Ws1P4DgSK!Ibv|!UHq^XkEI>mKz{}fB*y_009U<00Izz00bcLS_+gh*>g_%7ao_+*x8lk z%L*cB2tWV=5P$##AOHafKmY;|fB*zW0;TLZJN-34 zrkGtRB)4XBt!bzg6f_Z?NmGTFlbiYpzn2qXj6yortJ6W`x5x`f7|)h zq8l~{KmY;|fB*y_009U<00Izz00ibvV0&Tc6i)p9AK(8scMXexAOHafKmY;|fB*y_ z009U<00J*lAZur>vibb~iRC42tWV=5P$##AOHafKmY;|fB*#MMIf6k7tH7X zk1gl%yhMdKAOHafKmY;|fB*y_009U<00IzrVSy!kp-`x09^QZW&%Yl${PVE?+tqga z{AKg`|Doj^zHrIN2muH{00Izz00bZa0SG_<0uX?}{0c14F9hPx|E)9g8zdrw00bZa z0SG_<0uX=z1Rwwb2teTE0{r { const messageDiv = document.getElementById("message"); // Function to fetch activities from API - async function fetchActivities() { + async function fetchActivities(refreshDropdown = true) { try { const response = await fetch("/activities"); const activities = await response.json(); @@ -13,6 +13,11 @@ document.addEventListener("DOMContentLoaded", () => { // Clear loading message activitiesList.innerHTML = ""; + // Only clear and repopulate dropdown on initial load + if (refreshDropdown) { + activitySelect.innerHTML = ''; + } + // Populate activities list Object.entries(activities).forEach(([name, details]) => { const activityCard = document.createElement("div"); @@ -21,7 +26,14 @@ document.addEventListener("DOMContentLoaded", () => { const spotsLeft = details.max_participants - details.participants.length; const participantsList = details.participants.length > 0 - ? `
    ${details.participants.map(email => `
  • ${email}
  • `).join('')}
` + ? `
${details.participants.map(email => ` +
+ ${email} + +
+ `).join('')}
` : '

No participants yet

'; activityCard.innerHTML = ` @@ -37,11 +49,13 @@ document.addEventListener("DOMContentLoaded", () => { activitiesList.appendChild(activityCard); - // Add option to select dropdown - const option = document.createElement("option"); - option.value = name; - option.textContent = name; - activitySelect.appendChild(option); + // Only add options to dropdown on initial load + if (refreshDropdown) { + const option = document.createElement("option"); + option.value = name; + option.textContent = name; + activitySelect.appendChild(option); + } }); } catch (error) { activitiesList.innerHTML = "

Failed to load activities. Please try again later.

"; @@ -74,7 +88,7 @@ document.addEventListener("DOMContentLoaded", () => { messageDiv.textContent = result.message; messageDiv.className = "success"; signupForm.reset(); - fetchActivities(); // Refresh the activities list + fetchActivities(false); // Refresh activities list but keep dropdown intact } else { messageDiv.textContent = result.detail || "An error occurred"; messageDiv.className = "error"; @@ -97,3 +111,45 @@ document.addEventListener("DOMContentLoaded", () => { // Initialize app fetchActivities(); }); + +// Function to unregister a participant from an activity +async function unregisterParticipant(activityName, email) { + const messageDiv = document.getElementById("message"); + + try { + const response = await fetch( + `/activities/${encodeURIComponent(activityName)}/unregister`, + { + method: "DELETE", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: `email=${encodeURIComponent(email)}`, + } + ); + + const result = await response.json(); + + if (response.ok) { + messageDiv.textContent = result.message; + messageDiv.className = "success"; + // Refresh the activities list to show updated participants + await fetchActivities(false); // Don't refresh dropdown, just update the display + } else { + messageDiv.textContent = result.detail || "An error occurred"; + messageDiv.className = "error"; + } + + messageDiv.classList.remove("hidden"); + + // Hide message after 5 seconds + setTimeout(() => { + messageDiv.classList.add("hidden"); + }, 5000); + } catch (error) { + messageDiv.textContent = "Failed to unregister participant. Please try again."; + messageDiv.className = "error"; + messageDiv.classList.remove("hidden"); + console.error("Error unregistering participant:", error); + } +} diff --git a/src/static/styles.css b/src/static/styles.css index 4f26ae9..4805c13 100644 --- a/src/static/styles.css +++ b/src/static/styles.css @@ -88,9 +88,47 @@ section h3 { } .participants-list { - list-style: none; - padding-left: 0; margin: 0; + padding: 0; +} + +.participant-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 0; + border-bottom: 1px solid #f0f0f0; + font-size: 14px; + color: #555; +} + +.participant-item:last-child { + border-bottom: none; +} + +.participant-email { + flex-grow: 1; +} + +.delete-btn { + background-color: #ff4444; + color: white; + border: none; + border-radius: 50%; + width: 20px; + height: 20px; + font-size: 12px; + cursor: pointer; + transition: background-color 0.2s; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + margin-left: 8px; +} + +.delete-btn:hover { + background-color: #cc0000; } .participants-list li { diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..2551d75 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,52 @@ +# Tests for Mergington High School Activities API + +This directory contains comprehensive tests for the FastAPI application. + +## Running Tests + +To run all tests: +```bash +pytest tests/ -v +``` + +To run tests with coverage: +```bash +pytest tests/ --cov=src --cov-report=term-missing +``` + +## Test Structure + +- `conftest.py` - Test configuration and fixtures +- `test_api.py` - API endpoint tests + +## Test Coverage + +The tests cover: + +### Root Endpoint +- Redirect functionality to static files + +### Activities Endpoint +- Retrieving all activities +- Data structure validation +- Activity details verification + +### Signup Endpoint +- Successful signups +- Duplicate signup prevention +- Capacity validation +- Non-existent activity handling +- Multiple activity signups + +### Unregister Endpoint +- Successful unregistration +- Non-participant unregistration +- Non-existent activity handling +- Complete signup/unregister workflow + +### Edge Cases +- Activity names with spaces +- Various email formats +- Case sensitivity testing + +All tests achieve 100% code coverage of the FastAPI application. \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..d5f1ea9 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Test package for Mergington High School Activities API \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..ddaa254 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,85 @@ +""" +Test configuration and fixtures for the Mergington High School Activities API tests. +""" + +import pytest +from fastapi.testclient import TestClient +from src.app import app, activities + + +@pytest.fixture +def client(): + """Create a test client for the FastAPI application.""" + return TestClient(app) + + +@pytest.fixture +def reset_activities(): + """Reset activities to initial state before each test.""" + # Store original activities + original_activities = { + "Chess Club": { + "description": "Learn strategies and compete in chess tournaments", + "schedule": "Fridays, 3:30 PM - 5:00 PM", + "max_participants": 12, + "participants": ["michael@mergington.edu", "daniel@mergington.edu"] + }, + "Programming Class": { + "description": "Learn programming fundamentals and build software projects", + "schedule": "Tuesdays and Thursdays, 3:30 PM - 4:30 PM", + "max_participants": 20, + "participants": ["emma@mergington.edu", "sophia@mergington.edu"] + }, + "Gym Class": { + "description": "Physical education and sports activities", + "schedule": "Mondays, Wednesdays, Fridays, 2:00 PM - 3:00 PM", + "max_participants": 30, + "participants": ["john@mergington.edu", "olivia@mergington.edu"] + }, + "Soccer Team": { + "description": "Join the school soccer team and compete in local leagues", + "schedule": "Wednesdays and Fridays, 4:00 PM - 5:30 PM", + "max_participants": 18, + "participants": ["lucas@mergington.edu", "mia@mergington.edu"] + }, + "Basketball Club": { + "description": "Practice basketball skills and play friendly matches", + "schedule": "Tuesdays, 5:00 PM - 6:30 PM", + "max_participants": 15, + "participants": ["liam@mergington.edu", "ava@mergington.edu"] + }, + "Art Club": { + "description": "Explore painting, drawing, and other visual arts", + "schedule": "Thursdays, 3:30 PM - 5:00 PM", + "max_participants": 16, + "participants": ["charlotte@mergington.edu", "amelia@mergington.edu"] + }, + "Drama Society": { + "description": "Participate in theater productions and acting workshops", + "schedule": "Mondays, 4:00 PM - 5:30 PM", + "max_participants": 20, + "participants": ["jack@mergington.edu", "ella@mergington.edu"] + }, + "Math Olympiad": { + "description": "Prepare for math competitions and solve challenging problems", + "schedule": "Wednesdays, 3:30 PM - 4:30 PM", + "max_participants": 10, + "participants": ["noah@mergington.edu", "isabella@mergington.edu"] + }, + "Science Club": { + "description": "Conduct experiments and explore scientific concepts", + "schedule": "Fridays, 2:00 PM - 3:30 PM", + "max_participants": 14, + "participants": ["benjamin@mergington.edu", "grace@mergington.edu"] + } + } + + # Reset activities to original state + activities.clear() + activities.update(original_activities) + + yield + + # Clean up after test + activities.clear() + activities.update(original_activities) \ No newline at end of file diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..d3ddce9 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,241 @@ +""" +Tests for the Mergington High School Activities API endpoints. +""" + +import pytest +from fastapi.testclient import TestClient + + +class TestRootEndpoint: + """Test the root endpoint.""" + + def test_root_redirects_to_static_index(self, client, reset_activities): + """Test that root endpoint redirects to static index.html.""" + response = client.get("/", follow_redirects=False) + assert response.status_code == 307 + assert response.headers["location"] == "/static/index.html" + + +class TestActivitiesEndpoint: + """Test the activities endpoints.""" + + def test_get_activities_returns_all_activities(self, client, reset_activities): + """Test that GET /activities returns all activities.""" + response = client.get("/activities") + assert response.status_code == 200 + + data = response.json() + assert isinstance(data, dict) + + # Check that all expected activities are present + expected_activities = [ + "Chess Club", "Programming Class", "Gym Class", "Soccer Team", + "Basketball Club", "Art Club", "Drama Society", "Math Olympiad", "Science Club" + ] + + for activity_name in expected_activities: + assert activity_name in data + activity = data[activity_name] + assert "description" in activity + assert "schedule" in activity + assert "max_participants" in activity + assert "participants" in activity + assert isinstance(activity["participants"], list) + assert isinstance(activity["max_participants"], int) + + def test_get_activities_structure(self, client, reset_activities): + """Test the structure of activity data.""" + response = client.get("/activities") + data = response.json() + + # Test Chess Club specifically + chess_club = data["Chess Club"] + assert chess_club["description"] == "Learn strategies and compete in chess tournaments" + assert chess_club["schedule"] == "Fridays, 3:30 PM - 5:00 PM" + assert chess_club["max_participants"] == 12 + assert "michael@mergington.edu" in chess_club["participants"] + assert "daniel@mergington.edu" in chess_club["participants"] + + +class TestSignupEndpoint: + """Test the signup functionality.""" + + def test_signup_for_existing_activity_success(self, client, reset_activities): + """Test successful signup for an existing activity.""" + response = client.post( + "/activities/Chess Club/signup?email=newstudent@mergington.edu" + ) + assert response.status_code == 200 + + data = response.json() + assert data["message"] == "Signed up newstudent@mergington.edu for Chess Club" + + # Verify the participant was added + activities_response = client.get("/activities") + activities_data = activities_response.json() + assert "newstudent@mergington.edu" in activities_data["Chess Club"]["participants"] + + def test_signup_for_nonexistent_activity(self, client, reset_activities): + """Test signup for a non-existent activity.""" + response = client.post( + "/activities/Nonexistent Club/signup?email=student@mergington.edu" + ) + assert response.status_code == 404 + + data = response.json() + assert data["detail"] == "Activity not found" + + def test_signup_duplicate_student(self, client, reset_activities): + """Test that a student cannot sign up twice for the same activity.""" + # Try to sign up a student who is already registered + response = client.post( + "/activities/Chess Club/signup?email=michael@mergington.edu" + ) + assert response.status_code == 400 + + data = response.json() + assert data["detail"] == "Student already signed up for this activity" + + def test_signup_at_capacity(self, client, reset_activities): + """Test signup when activity is at full capacity.""" + # Fill up Math Olympiad (max 10 participants, currently has 2) + for i in range(8): # Add 8 more to reach capacity + response = client.post( + f"/activities/Math Olympiad/signup?email=student{i}@mergington.edu" + ) + assert response.status_code == 200 + + # Now try to add one more (should fail) + response = client.post( + "/activities/Math Olympiad/signup?email=overflow@mergington.edu" + ) + assert response.status_code == 400 + + data = response.json() + assert data["detail"] == "Activity is at full capacity" + + def test_signup_multiple_activities(self, client, reset_activities): + """Test that a student can sign up for multiple different activities.""" + email = "multistudent@mergington.edu" + + # Sign up for Chess Club + response1 = client.post( + f"/activities/Chess Club/signup?email={email}" + ) + assert response1.status_code == 200 + + # Sign up for Programming Class + response2 = client.post( + f"/activities/Programming Class/signup?email={email}" + ) + assert response2.status_code == 200 + + # Verify student is in both activities + activities_response = client.get("/activities") + activities_data = activities_response.json() + assert email in activities_data["Chess Club"]["participants"] + assert email in activities_data["Programming Class"]["participants"] + + +class TestUnregisterEndpoint: + """Test the unregister functionality.""" + + def test_unregister_existing_participant(self, client, reset_activities): + """Test successful unregistration of an existing participant.""" + response = client.delete( + "/activities/Chess Club/unregister?email=michael@mergington.edu" + ) + assert response.status_code == 200 + + data = response.json() + assert data["message"] == "Unregistered michael@mergington.edu from Chess Club" + + # Verify the participant was removed + activities_response = client.get("/activities") + activities_data = activities_response.json() + assert "michael@mergington.edu" not in activities_data["Chess Club"]["participants"] + assert "daniel@mergington.edu" in activities_data["Chess Club"]["participants"] # Other participant should remain + + def test_unregister_from_nonexistent_activity(self, client, reset_activities): + """Test unregistration from a non-existent activity.""" + response = client.delete( + "/activities/Nonexistent Club/unregister?email=student@mergington.edu" + ) + assert response.status_code == 404 + + data = response.json() + assert data["detail"] == "Activity not found" + + def test_unregister_non_participant(self, client, reset_activities): + """Test unregistration of a student who is not registered.""" + response = client.delete( + "/activities/Chess Club/unregister?email=notregistered@mergington.edu" + ) + assert response.status_code == 400 + + data = response.json() + assert data["detail"] == "Student is not registered for this activity" + + def test_signup_and_unregister_workflow(self, client, reset_activities): + """Test the complete workflow of signing up and then unregistering.""" + email = "workflow@mergington.edu" + activity = "Art Club" + + # First, sign up + signup_response = client.post( + f"/activities/{activity}/signup?email={email}" + ) + assert signup_response.status_code == 200 + + # Verify signup worked + activities_response = client.get("/activities") + activities_data = activities_response.json() + assert email in activities_data[activity]["participants"] + + # Then, unregister + unregister_response = client.delete( + f"/activities/{activity}/unregister?email={email}" + ) + assert unregister_response.status_code == 200 + + # Verify unregistration worked + activities_response = client.get("/activities") + activities_data = activities_response.json() + assert email not in activities_data[activity]["participants"] + + +class TestEdgeCases: + """Test edge cases and error conditions.""" + + def test_activity_name_with_spaces_and_special_chars(self, client, reset_activities): + """Test handling of activity names with spaces.""" + # Chess Club has a space in the name + response = client.post( + "/activities/Chess Club/signup?email=test@mergington.edu" + ) + assert response.status_code == 200 + + def test_email_formats(self, client, reset_activities): + """Test various email formats.""" + valid_emails = [ + "test@mergington.edu", + "test.student@mergington.edu", + "test_student@mergington.edu", + "test-student@mergington.edu", + "test123@mergington.edu" + ] + + for i, email in enumerate(valid_emails): + response = client.post( + f"/activities/Programming Class/signup?email={email}" + ) + # Each should succeed as they're all different emails + assert response.status_code == 200, f"Failed for email: {email}" + + def test_case_sensitivity_activity_names(self, client, reset_activities): + """Test that activity names are case sensitive.""" + # This should fail because "chess club" != "Chess Club" + response = client.post( + "/activities/chess club/signup?email=test@mergington.edu" + ) + assert response.status_code == 404 \ No newline at end of file