From 2328150cb9ef0f1e3ee6953e215247b2b117c57c Mon Sep 17 00:00:00 2001
From: Jaswanth Duddu <57510269+jaswanthDuddu@users.noreply.github.com>
Date: Mon, 29 Sep 2025 18:15:41 -0400
Subject: [PATCH 01/11] API Endpoint: Update opportunity_routes.py
new route that queries the database for all opportunities and returns them as a JSON list
---
labconnect/main/opportunity_routes.py | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/labconnect/main/opportunity_routes.py b/labconnect/main/opportunity_routes.py
index d38379f1..66ad912b 100644
--- a/labconnect/main/opportunity_routes.py
+++ b/labconnect/main/opportunity_routes.py
@@ -27,6 +27,30 @@
from . import main_blueprint
+@main_blueprint.route("/opportunities", methods=["GET"])
+def get_opportunities():
+ """
+ Gets all opportunities from the database.
+ """
+ # Query the database to get all opportunity records
+ opportunities = Opportunity.query.all()
+
+ # Create a list of dictionaries from the opportunity objects
+ opportunity_list = [
+ {
+ "id": op.id,
+ "name": op.name,
+ "description": op.description,
+ "professor_id": op.professor_id,
+ "posted_on": op.posted_on.isoformat() # format datetime for JSON
+ }
+ for op in opportunities
+ ]
+
+ # Return the list as a JSON response
+ return jsonify(opportunity_list), 200
+
+
@main_blueprint.get("/searchOpportunity/")
def searchOpportunity(query: str):
# Perform a search
From 474d3cffaed2435a319a1fce9b277eb2e6b3fd35 Mon Sep 17 00:00:00 2001
From: Jaswanth Duddu <57510269+jaswanthDuddu@users.noreply.github.com>
Date: Mon, 29 Sep 2025 18:19:24 -0400
Subject: [PATCH 02/11] API Endpoint: Update test_opportunity.py
test to ensure your new endpoint works correctly
---
tests/test_opportunity.py | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/tests/test_opportunity.py b/tests/test_opportunity.py
index dcc567b4..69c15fda 100644
--- a/tests/test_opportunity.py
+++ b/tests/test_opportunity.py
@@ -4,6 +4,21 @@
from flask.testing import FlaskClient
+
+def test_get_all_opportunities(client, auth):
+ """
+ Tests the GET /api/opportunities endpoint to ensure it returns all opportunities.
+ """
+ response = client.get("/api/opportunities")
+
+ assert response.status_code == 200 # request was successful (status code 200)
+
+ assert isinstance(response.json, list) # response is a list
+
+ # atleast 1 opportunity in data, not empty JSON return
+ assert len(response.json) > 0
+
+
def test_get_opportunity_parametrized(test_client: FlaskClient):
"""
GIVEN a Flask application configured for testing
From ce80f771a26c7278540da9f1f0fd8131eb24aa35 Mon Sep 17 00:00:00 2001
From: Jaswanth Duddu <57510269+jaswanthDuddu@users.noreply.github.com>
Date: Mon, 29 Sep 2025 18:55:01 -0400
Subject: [PATCH 03/11] Single Opportunity: Update opportunity_routes.py
creating a specific backend route to fetch the details of one opportunity by its ID
---
labconnect/main/opportunity_routes.py | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/labconnect/main/opportunity_routes.py b/labconnect/main/opportunity_routes.py
index 66ad912b..d87ec2fb 100644
--- a/labconnect/main/opportunity_routes.py
+++ b/labconnect/main/opportunity_routes.py
@@ -1,6 +1,6 @@
from datetime import datetime
-from flask import abort, request
+from flask import abort, request, jsonify
from flask_jwt_extended import get_jwt_identity, jwt_required
from sqlalchemy import case, func
@@ -51,6 +51,20 @@ def get_opportunities():
return jsonify(opportunity_list), 200
+@main_blueprint.get("/opportunity/")
+def get_single_opportunity(opportunity_id: int):
+ """
+ Retrieves the details of a single opportunity by its ID.
+ """
+ # Query the database for the opportunity with the given ID
+ opportunity = db.session.get(Opportunities, opportunity_id)
+ if opportunity is None:
+ abort(404, description="Opportunity not found")
+
+ # Serialize the opportunity data and return it as JSON
+ return jsonify(serialize_opportunity(opportunity))
+
+
@main_blueprint.get("/searchOpportunity/")
def searchOpportunity(query: str):
# Perform a search
From 0995fce3a6c2a9df0cdbd7c74bea55a21c1fb495 Mon Sep 17 00:00:00 2001
From: Jaswanth Duddu <57510269+jaswanthDuddu@users.noreply.github.com>
Date: Mon, 29 Sep 2025 18:59:11 -0400
Subject: [PATCH 04/11] Single Opportunity: Update test_opportunity.py
test verifies that the new endpoint correctly retrieves an opportunity or handle error
---
tests/test_opportunity.py | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)
diff --git a/tests/test_opportunity.py b/tests/test_opportunity.py
index 69c15fda..fbb80be7 100644
--- a/tests/test_opportunity.py
+++ b/tests/test_opportunity.py
@@ -19,6 +19,33 @@ def test_get_all_opportunities(client, auth):
assert len(response.json) > 0
+def test_get_single_opportunity_success(test_client: FlaskClient):
+ """
+ WHEN the '/opportunity/1' endpoint is requested (GET)
+ THEN check that the response is 200 and contains the correct opportunity data
+ """
+ response = test_client.get("/opportunity/1") # request to the endpoint
+
+ assert response.status_code == 200
+
+ data = json.loads(response.data) # Parse the JSON response
+
+ # correct opportunity return?
+ assert data["id"] == 1
+ assert data["name"] == "Automated Cooling System"
+ assert data["pay"] == 15.0
+
+
+def test_get_single_opportunity_not_found(test_client: FlaskClient):
+ """
+ WHEN the '/opportunity/999' endpoint is requested for a non-existent ID (GET)
+ THEN check that a 404 Not Found status code is returned
+ """
+ response = test_client.get("/opportunity/999") # request for an opportunity ID DNE
+
+ assert response.status_code == 404 # error check?
+
+
def test_get_opportunity_parametrized(test_client: FlaskClient):
"""
GIVEN a Flask application configured for testing
From 2a3cf8e4f926ed1fb771acaccbbeee233e841b68 Mon Sep 17 00:00:00 2001
From: Jaswanth Duddu <57510269+jaswanthDuddu@users.noreply.github.com>
Date: Fri, 3 Oct 2025 16:50:05 -0400
Subject: [PATCH 05/11] Update README.md
---
README.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/README.md b/README.md
index 60e550b7..0b93b07b 100644
--- a/README.md
+++ b/README.md
@@ -111,6 +111,10 @@ posted by professors, graduate students, or lab staff.
```bash
$ python3 -m pytest --cov
```
+ or individual tests
+ ```bash
+ $ python3 -m pytest -q tests/(file_name).py -q
+ ```
## Development
* Run flask with python directly
From 557cb0dca4796a0f21df3e12858ffab78af913b1 Mon Sep 17 00:00:00 2001
From: Jaswanth Duddu
Date: Fri, 10 Oct 2025 15:29:12 -0400
Subject: [PATCH 06/11] Fixes and Improvements for Single and All Opportunity
Endpoints Added opportunity_to_dict helper Fix two GET endpoints for better
error checks and improved data handling created corresponding tests in
test_single_opportunity_routes.py for a smooth experience
---
.gitignore | 7 +-
labconnect/main/opportunity_routes.py | 78 ++++++++++++--------
tests/test_opportunity.py | 43 -----------
tests/test_single_opportunity_routes.py | 94 +++++++++++++++++++++++++
4 files changed, 147 insertions(+), 75 deletions(-)
create mode 100644 tests/test_single_opportunity_routes.py
diff --git a/.gitignore b/.gitignore
index cb6c53bb..252b28bc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,4 +6,9 @@ env/
*.db
.coverage
*.vscode
-.env
\ No newline at end of file
+.env
+
+labconnect/bin
+labconnect/include
+labconnect/lib
+labconnect/pyvenv.cfg
\ No newline at end of file
diff --git a/labconnect/main/opportunity_routes.py b/labconnect/main/opportunity_routes.py
index d87ec2fb..c7c2e909 100644
--- a/labconnect/main/opportunity_routes.py
+++ b/labconnect/main/opportunity_routes.py
@@ -1,6 +1,6 @@
from datetime import datetime
-from flask import abort, request, jsonify
+from flask import abort, request
from flask_jwt_extended import get_jwt_identity, jwt_required
from sqlalchemy import case, func
@@ -27,42 +27,58 @@
from . import main_blueprint
-@main_blueprint.route("/opportunities", methods=["GET"])
-def get_opportunities():
- """
- Gets all opportunities from the database.
- """
- # Query the database to get all opportunity records
- opportunities = Opportunity.query.all()
-
- # Create a list of dictionaries from the opportunity objects
- opportunity_list = [
- {
- "id": op.id,
- "name": op.name,
- "description": op.description,
- "professor_id": op.professor_id,
- "posted_on": op.posted_on.isoformat() # format datetime for JSON
- }
- for op in opportunities
- ]
-
- # Return the list as a JSON response
- return jsonify(opportunity_list), 200
+def opportunity_to_dict(opportunity: Opportunities) -> dict:
+ """Return a plain dict representation of an Opportunities model instance."""
+ if opportunity is None:
+ return {}
+ return {
+ "id": opportunity.id,
+ "name": opportunity.name,
+ "description": opportunity.description,
+ "recommended_experience": opportunity.recommended_experience,
+ "pay": opportunity.pay,
+ "one_credit": bool(opportunity.one_credit),
+ "two_credits": bool(opportunity.two_credits),
+ "three_credits": bool(opportunity.three_credits),
+ "four_credits": bool(opportunity.four_credits),
+ "semester": str(opportunity.semester) if opportunity.semester is not None else None,
+ "year": opportunity.year,
+ "active": bool(opportunity.active),
+ }
+# Single opportunity endpoints used by the frontend/tests
@main_blueprint.get("/opportunity/")
def get_single_opportunity(opportunity_id: int):
+ """Return a single opportunity by id. Returns 404 if not found
"""
- Retrieves the details of a single opportunity by its ID.
- """
- # Query the database for the opportunity with the given ID
- opportunity = db.session.get(Opportunities, opportunity_id)
- if opportunity is None:
- abort(404, description="Opportunity not found")
+ opp = db.session.get(Opportunities, opportunity_id)
+ if not opp:
+ abort(404)
+
+ return opportunity_to_dict(opp)
+
+
+@main_blueprint.get("/opportunity")
+def get_opportunity_via_json():
+ """GET /opportunity expects a JSON payload with {"id": }."""
+ data = request.get_json()
+ if not data:
+ abort(400)
+
+ if "id" not in data:
+ abort(400)
+
+ try:
+ opp_id = int(data["id"])
+ except ValueError:
+ abort(400)
+
+ opp = db.session.get(Opportunities, opp_id)
+ if not opp:
+ abort(404)
- # Serialize the opportunity data and return it as JSON
- return jsonify(serialize_opportunity(opportunity))
+ return opportunity_to_dict(opp)
@main_blueprint.get("/searchOpportunity/")
diff --git a/tests/test_opportunity.py b/tests/test_opportunity.py
index fbb80be7..8a177813 100644
--- a/tests/test_opportunity.py
+++ b/tests/test_opportunity.py
@@ -3,49 +3,6 @@
import pytest
from flask.testing import FlaskClient
-
-
-def test_get_all_opportunities(client, auth):
- """
- Tests the GET /api/opportunities endpoint to ensure it returns all opportunities.
- """
- response = client.get("/api/opportunities")
-
- assert response.status_code == 200 # request was successful (status code 200)
-
- assert isinstance(response.json, list) # response is a list
-
- # atleast 1 opportunity in data, not empty JSON return
- assert len(response.json) > 0
-
-
-def test_get_single_opportunity_success(test_client: FlaskClient):
- """
- WHEN the '/opportunity/1' endpoint is requested (GET)
- THEN check that the response is 200 and contains the correct opportunity data
- """
- response = test_client.get("/opportunity/1") # request to the endpoint
-
- assert response.status_code == 200
-
- data = json.loads(response.data) # Parse the JSON response
-
- # correct opportunity return?
- assert data["id"] == 1
- assert data["name"] == "Automated Cooling System"
- assert data["pay"] == 15.0
-
-
-def test_get_single_opportunity_not_found(test_client: FlaskClient):
- """
- WHEN the '/opportunity/999' endpoint is requested for a non-existent ID (GET)
- THEN check that a 404 Not Found status code is returned
- """
- response = test_client.get("/opportunity/999") # request for an opportunity ID DNE
-
- assert response.status_code == 404 # error check?
-
-
def test_get_opportunity_parametrized(test_client: FlaskClient):
"""
GIVEN a Flask application configured for testing
diff --git a/tests/test_single_opportunity_routes.py b/tests/test_single_opportunity_routes.py
new file mode 100644
index 00000000..ca7922d1
--- /dev/null
+++ b/tests/test_single_opportunity_routes.py
@@ -0,0 +1,94 @@
+import pytest
+
+from labconnect import db
+from labconnect.models import Opportunities
+from labconnect.main.opportunity_routes import opportunity_to_dict
+
+
+def test_opportunity_to_dict_none():
+ assert opportunity_to_dict(None) == {}
+
+
+def test_opportunity_to_dict_populated():
+ # create a lightweight Opportunities instance (no DB persistence needed)
+ opp = Opportunities()
+ opp.id = 123
+ opp.name = "Unit Test Opportunity"
+ opp.description = "A test description"
+ opp.recommended_experience = "Testing"
+ opp.pay = 7.5
+ opp.one_credit = True
+ opp.two_credits = False
+ opp.three_credits = False
+ opp.four_credits = False
+ opp.semester = None
+ opp.year = 2025
+ opp.active = True
+
+ out = opportunity_to_dict(opp)
+
+ assert out["id"] == 123
+ assert out["name"] == "Unit Test Opportunity"
+ assert out["pay"] == 7.5
+ assert out["one_credit"] is True
+ assert out["two_credits"] is False
+ assert out["semester"] is None
+ assert out["year"] == 2025
+
+
+@pytest.mark.usefixtures("test_client")
+def test_get_single_opportunity_not_found(test_client):
+ response = test_client.get("/opportunity/999999")
+ assert response.status_code == 404
+
+
+@pytest.mark.usefixtures("test_client")
+def test_get_single_opportunity_success_and_json_variant(test_client):
+ # create and persist an opportunity to the test database
+ opp = Opportunities()
+ opp.name = "Endpoint Test Opportunity"
+ opp.description = "Endpoint description"
+ opp.recommended_experience = "None"
+ opp.pay = 12.0
+ opp.one_credit = False
+ opp.two_credits = True
+ opp.three_credits = False
+ opp.four_credits = False
+ opp.semester = None
+ opp.year = 2025
+ opp.application_due = None
+ opp.active = True
+ opp.last_updated = None
+ opp.location = None
+
+ db.session.add(opp)
+ db.session.commit()
+
+ # GET by URL id
+ resp = test_client.get(f"/opportunity/{opp.id}")
+ assert resp.status_code == 200
+ data = resp.get_json()
+ assert data["id"] == opp.id
+ assert data["name"] == "Endpoint Test Opportunity"
+ assert data["pay"] == 12.0
+
+ # GET via JSON body variant
+ resp2 = test_client.get("/opportunity", json={"id": opp.id})
+ assert resp2.status_code == 200
+ data2 = resp2.get_json()
+ assert data2["id"] == opp.id
+
+
+@pytest.mark.usefixtures("test_client")
+def test_get_opportunity_via_json_errors(test_client):
+ # No JSON -> 400
+ resp = test_client.get("/opportunity")
+ assert resp.status_code in (400, 415)
+
+ # Missing id key -> 400
+ resp2 = test_client.get("/opportunity", json={"wrong": "key"})
+ assert resp2.status_code == 400
+
+ # Non-integer id -> 400
+ resp3 = test_client.get("/opportunity", json={"id": "not-an-int"})
+ assert resp3.status_code == 400
From f9b2025946050f2a5e49f0f9c6399df5d9e424e0 Mon Sep 17 00:00:00 2001
From: Jaswanth Duddu
Date: Tue, 21 Oct 2025 14:09:17 -0400
Subject: [PATCH 07/11] User Profile Management GET /profile route to allow
users to fetch their own profile data. Added a 'user_to_dict' helper function
to consistently serialize user data.
---
labconnect/main/profile_routes.py | 42 +++++++++++++++++++++++++++++++
1 file changed, 42 insertions(+)
create mode 100644 labconnect/main/profile_routes.py
diff --git a/labconnect/main/profile_routes.py b/labconnect/main/profile_routes.py
new file mode 100644
index 00000000..48424ec3
--- /dev/null
+++ b/labconnect/main/profile_routes.py
@@ -0,0 +1,42 @@
+from flask import jsonify, request, Response, make_response
+from flask_jwt_extended import jwt_required, get_jwt_identity
+
+from labconnect import db
+from labconnect.models import User, UserDepartments, UserMajors, Departments, Majors
+from . import main_blueprint
+
+def user_to_dict(user: User) -> dict:
+ """ Helper function to serialize User object data. """
+ user_departments = db.session.execute(
+ db.select(UserDepartments.department_id).where(UserDepartments.user_id == user.id)
+ ).scalars().all()
+
+ user_majors = db.session.execute(
+ db.select(UserMajors.major_code).where(UserMajors.user_id == user.id)
+ ).scalars().all()
+
+ return {
+ "id": user.id,
+ "email": user.email,
+ "first_name": user.first_name,
+ "last_name": user.last_name,
+ "preferred_name": user.preferred_name,
+ "class_year": user.class_year,
+ "profile_picture": user.profile_picture,
+ "website": user.website,
+ "description": user.description,
+ "departments": user_departments,
+ "majors": user_majors,
+ }
+
+@main_blueprint.route("/profile", methods=["GET"])
+@jwt_required()
+def get_profile() -> Response:
+ """ GET /profile: current user profile """
+ user_email = get_jwt_identity()
+ user = db.session.execute(db.select(User).where(User.email == user_email)).scalar_one_or_none()
+
+ if not user:
+ return make_response(jsonify({"msg": "User not found"}), 404)
+
+ return jsonify(user_to_dict(user))
\ No newline at end of file
From 5e30938ed0dc8019f190395e9c6c2fc9d25a2475 Mon Sep 17 00:00:00 2001
From: Jaswanth Duddu
Date: Tue, 21 Oct 2025 14:14:15 -0400
Subject: [PATCH 08/11] User Profile Management Update PUT /profile route for
user to update their personal details.
---
labconnect/main/profile_routes.py | 28 +++++++++++++++++++++++++++-
1 file changed, 27 insertions(+), 1 deletion(-)
diff --git a/labconnect/main/profile_routes.py b/labconnect/main/profile_routes.py
index 48424ec3..81c52bd5 100644
--- a/labconnect/main/profile_routes.py
+++ b/labconnect/main/profile_routes.py
@@ -39,4 +39,30 @@ def get_profile() -> Response:
if not user:
return make_response(jsonify({"msg": "User not found"}), 404)
- return jsonify(user_to_dict(user))
\ No newline at end of file
+ return jsonify(user_to_dict(user))
+
+@main_blueprint.route("/profile", methods=["PUT"])
+@jwt_required()
+def update_profile() -> Response:
+ """ PUT /profile: Updates current user profile """
+ user_email = get_jwt_identity()
+ user = db.session.execute(db.select(User).where(User.email == user_email)).scalar_one_or_none()
+
+ if not user:
+ return make_response(jsonify({"msg": "User not found"}), 404)
+
+ json_data = request.get_json()
+ if not json_data:
+ return make_response(jsonify({"msg": "Missing JSON in request"}), 400)
+
+ # Update basic User fields
+ user.first_name = json_data.get("first_name", user.first_name)
+ user.last_name = json_data.get("last_name", user.last_name)
+ user.preferred_name = json_data.get("preferred_name", user.preferred_name)
+ user.class_year = json_data.get("class_year", user.class_year)
+ user.website = json_data.get("website", user.website)
+ user.description = json_data.get("description", user.description)
+
+ db.session.commit()
+
+ return jsonify({"msg": "Profile updated successfully"})
From 87ff46e07fb7aca1825af67f29276f73bbc59927 Mon Sep 17 00:00:00 2001
From: Jaswanth Duddu
Date: Tue, 21 Oct 2025 16:14:13 -0400
Subject: [PATCH 09/11] User Profile Management Update Majors, Departments PUT
/profile route for user to update their majors and department.
---
labconnect/main/profile_routes.py | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/labconnect/main/profile_routes.py b/labconnect/main/profile_routes.py
index 81c52bd5..6cc12043 100644
--- a/labconnect/main/profile_routes.py
+++ b/labconnect/main/profile_routes.py
@@ -2,7 +2,7 @@
from flask_jwt_extended import jwt_required, get_jwt_identity
from labconnect import db
-from labconnect.models import User, UserDepartments, UserMajors, Departments, Majors
+from labconnect.models import User, UserDepartments, UserMajors, RPIDepartments, Majors
from . import main_blueprint
def user_to_dict(user: User) -> dict:
@@ -62,6 +62,20 @@ def update_profile() -> Response:
user.class_year = json_data.get("class_year", user.class_year)
user.website = json_data.get("website", user.website)
user.description = json_data.get("description", user.description)
+
+ if "departments" in json_data:
+ db.session.query(UserDepartments).filter(UserDepartments.user_id == user.id).delete()
+ for dept_id in json_data["departments"]:
+ if db.session.get(RPIDepartments, dept_id): # Ensure department exists before adding
+ new_user_dept = UserDepartments(user_id=user.id, department_id=dept_id)
+ db.session.add(new_user_dept)
+
+ if "majors" in json_data:
+ db.session.query(UserMajors).filter(UserMajors.user_id == user.id).delete()
+ for major_code in json_data["majors"]:
+ if db.session.get(Majors, major_code): # Ensure major exists before adding
+ new_user_major = UserMajors(user_id=user.id, major_code=major_code)
+ db.session.add(new_user_major)
db.session.commit()
From 24b05902b5e867175ab75221398eaece53ce726c Mon Sep 17 00:00:00 2001
From: Jaswanth Duddu
Date: Tue, 21 Oct 2025 16:44:42 -0400
Subject: [PATCH 10/11] Tests: User Profile Management endpoints GET /profile
endpoint: both successful data retrieval and unauthorized access PUT /profile
endpoint: ensuring that full updates, partial updates, and unauthorized
attempts are handled correctly. ISSUE: Login helper function to streamline
authentication within the tests but the tests fail as user not in DB.
---
tests/test_profile_routes.py | 107 +++++++++++++++++++++++++++++++++++
1 file changed, 107 insertions(+)
create mode 100644 tests/test_profile_routes.py
diff --git a/tests/test_profile_routes.py b/tests/test_profile_routes.py
new file mode 100644
index 00000000..b35edb4d
--- /dev/null
+++ b/tests/test_profile_routes.py
@@ -0,0 +1,107 @@
+import json
+from flask.testing import FlaskClient
+from labconnect.models import User, UserDepartments, UserMajors
+from labconnect import db
+
+
+def login_as_student(test_client: FlaskClient):
+ """Helper function to log in a user and handle the auth flow."""
+ response = test_client.get("/login")
+ assert response.status_code == 302
+
+ redirect_url = response.headers['Location']
+ code = redirect_url.split('code=')[1]
+
+ token_response = test_client.post("/token", json={"code": code})
+ assert token_response.status_code == 200
+
+
+# === GET /profile Tests ===
+
+def test_get_profile_success(test_client: FlaskClient):
+ """
+ logged-in user: '/profile' endpoint is requested (GET) -> correct data and 200 status
+ """
+ login_as_student(test_client)
+
+ response = test_client.get("/profile")
+ data = json.loads(response.data)
+
+ assert response.status_code == 200
+ assert data["email"] == "test@rpi.edu"
+ assert data["first_name"] == "Test"
+ assert data["last_name"] == "User"
+ assert "departments" in data
+ assert "majors" in data
+
+def test_get_profile_unauthorized(test_client: FlaskClient):
+ """
+ no user is logged in: '/profile' endpoint is requested (GET) -> 401 Unauthorized status is returned.
+ """
+ test_client.get("/logout")
+ response = test_client.get("/profile")
+ assert response.status_code == 401
+
+
+# === PUT /profile Tests ===
+
+def test_update_profile_success(test_client: FlaskClient):
+ """
+ logged-in user: '/profile' endpoint is updated with new data (PUT) -> 200 status and database changed.
+ """
+ login_as_student(test_client)
+
+ update_data = {
+ "first_name": "UpdatedFirst",
+ "last_name": "UpdatedLast",
+ "preferred_name": "Pref",
+ "class_year": 2025,
+ "website": "https://new.example.com",
+ "description": "This is an updated description.",
+ "departments": ["CS"],
+ "majors": ["CSCI", "MATH"]
+ }
+
+ response = test_client.put("/profile", json=update_data)
+ assert response.status_code == 200
+ assert "Profile updated successfully" in json.loads(response.data)["msg"]
+
+ # Verify the changes in the database
+ user = db.session.execute(db.select(User).where(User.email == "test@rpi.edu")).scalar_one()
+ assert user.first_name == "UpdatedFirst"
+ assert user.website == "https://new.example.com"
+ assert user.class_year == 2025
+
+ user_depts = db.session.execute(db.select(UserDepartments.department_id).where(UserDepartments.user_id == user.id)).scalars().all()
+ assert set(user_depts) == {"CS"}
+
+ user_majors = db.session.execute(db.select(UserMajors.major_code).where(UserMajors.user_id == user.id)).scalars().all()
+ assert set(user_majors) == {"CSCI", "MATH"}
+
+def test_update_profile_partial(test_client: FlaskClient):
+ """
+ logged-in user: '/profile' endpoint is updated with partial data (PUT) -> check only provided fields updated.
+ """
+ login_as_student(test_client)
+
+ update_data = {
+ "website": "https://partial.update.com",
+ "description": "Only this was updated."
+ }
+
+ response = test_client.put("/profile", json=update_data)
+ assert response.status_code == 200
+
+ user = db.session.execute(db.select(User).where(User.email == "test@rpi.edu")).scalar_one()
+ assert user.website == "https://partial.update.com"
+ assert user.description == "Only this was updated."
+ assert user.last_name == "User"
+
+def test_update_profile_unauthorized(test_client: FlaskClient):
+ """
+ no user is logged in: '/profile' endpoint is sent a PUT request -> 401 Unauthorized status.
+ """
+ test_client.get("/logout")
+ update_data = {"first_name": "ShouldFail"}
+ response = test_client.put("/profile", json=update_data)
+ assert response.status_code == 401
\ No newline at end of file
From dcc07ff53e0eb4cf1ca89f0322217bd36b0b7332 Mon Sep 17 00:00:00 2001
From: Jaswanth Duddu
Date: Fri, 24 Oct 2025 16:29:58 -0400
Subject: [PATCH 11/11] Fixing Ruff Linter Errors
---
labconnect/main/opportunity_routes.py | 8 +-
labconnect/main/profile_routes.py | 186 ++++++++++--------
tests/test_opportunity.py | 1 +
tests/test_profile_routes.py | 244 +++++++++++++-----------
tests/test_single_opportunity_routes.py | 188 +++++++++---------
5 files changed, 341 insertions(+), 286 deletions(-)
diff --git a/labconnect/main/opportunity_routes.py b/labconnect/main/opportunity_routes.py
index a7b574b3..d2818f4c 100644
--- a/labconnect/main/opportunity_routes.py
+++ b/labconnect/main/opportunity_routes.py
@@ -42,16 +42,18 @@ def opportunity_to_dict(opportunity: Opportunities) -> dict:
"two_credits": bool(opportunity.two_credits),
"three_credits": bool(opportunity.three_credits),
"four_credits": bool(opportunity.four_credits),
- "semester": str(opportunity.semester) if opportunity.semester is not None else None,
+ "semester": str(opportunity.semester)
+ if opportunity.semester is not None
+ else None,
"year": opportunity.year,
"active": bool(opportunity.active),
}
+
# Single opportunity endpoints used by the frontend/tests
@main_blueprint.get("/opportunity/")
def get_single_opportunity(opportunity_id: int):
- """Return a single opportunity by id. Returns 404 if not found
- """
+ """Return a single opportunity by id. Returns 404 if not found"""
opp = db.session.get(Opportunities, opportunity_id)
if not opp:
abort(404)
diff --git a/labconnect/main/profile_routes.py b/labconnect/main/profile_routes.py
index 6cc12043..35820267 100644
--- a/labconnect/main/profile_routes.py
+++ b/labconnect/main/profile_routes.py
@@ -1,82 +1,104 @@
-from flask import jsonify, request, Response, make_response
-from flask_jwt_extended import jwt_required, get_jwt_identity
-
-from labconnect import db
-from labconnect.models import User, UserDepartments, UserMajors, RPIDepartments, Majors
-from . import main_blueprint
-
-def user_to_dict(user: User) -> dict:
- """ Helper function to serialize User object data. """
- user_departments = db.session.execute(
- db.select(UserDepartments.department_id).where(UserDepartments.user_id == user.id)
- ).scalars().all()
-
- user_majors = db.session.execute(
- db.select(UserMajors.major_code).where(UserMajors.user_id == user.id)
- ).scalars().all()
-
- return {
- "id": user.id,
- "email": user.email,
- "first_name": user.first_name,
- "last_name": user.last_name,
- "preferred_name": user.preferred_name,
- "class_year": user.class_year,
- "profile_picture": user.profile_picture,
- "website": user.website,
- "description": user.description,
- "departments": user_departments,
- "majors": user_majors,
- }
-
-@main_blueprint.route("/profile", methods=["GET"])
-@jwt_required()
-def get_profile() -> Response:
- """ GET /profile: current user profile """
- user_email = get_jwt_identity()
- user = db.session.execute(db.select(User).where(User.email == user_email)).scalar_one_or_none()
-
- if not user:
- return make_response(jsonify({"msg": "User not found"}), 404)
-
- return jsonify(user_to_dict(user))
-
-@main_blueprint.route("/profile", methods=["PUT"])
-@jwt_required()
-def update_profile() -> Response:
- """ PUT /profile: Updates current user profile """
- user_email = get_jwt_identity()
- user = db.session.execute(db.select(User).where(User.email == user_email)).scalar_one_or_none()
-
- if not user:
- return make_response(jsonify({"msg": "User not found"}), 404)
-
- json_data = request.get_json()
- if not json_data:
- return make_response(jsonify({"msg": "Missing JSON in request"}), 400)
-
- # Update basic User fields
- user.first_name = json_data.get("first_name", user.first_name)
- user.last_name = json_data.get("last_name", user.last_name)
- user.preferred_name = json_data.get("preferred_name", user.preferred_name)
- user.class_year = json_data.get("class_year", user.class_year)
- user.website = json_data.get("website", user.website)
- user.description = json_data.get("description", user.description)
-
- if "departments" in json_data:
- db.session.query(UserDepartments).filter(UserDepartments.user_id == user.id).delete()
- for dept_id in json_data["departments"]:
- if db.session.get(RPIDepartments, dept_id): # Ensure department exists before adding
- new_user_dept = UserDepartments(user_id=user.id, department_id=dept_id)
- db.session.add(new_user_dept)
-
- if "majors" in json_data:
- db.session.query(UserMajors).filter(UserMajors.user_id == user.id).delete()
- for major_code in json_data["majors"]:
- if db.session.get(Majors, major_code): # Ensure major exists before adding
- new_user_major = UserMajors(user_id=user.id, major_code=major_code)
- db.session.add(new_user_major)
-
- db.session.commit()
-
- return jsonify({"msg": "Profile updated successfully"})
+from flask import Response, jsonify, make_response, request
+from flask_jwt_extended import get_jwt_identity, jwt_required
+
+from labconnect import db
+from labconnect.models import Majors, RPIDepartments, User, UserDepartments, UserMajors
+
+from . import main_blueprint
+
+
+def user_to_dict(user: User) -> dict:
+ """Helper function to serialize User object data."""
+ user_departments = (
+ db.session.execute(
+ db.select(UserDepartments.department_id).where(
+ UserDepartments.user_id == user.id
+ )
+ )
+ .scalars()
+ .all()
+ )
+
+ user_majors = (
+ db.session.execute(
+ db.select(UserMajors.major_code).where(UserMajors.user_id == user.id)
+ )
+ .scalars()
+ .all()
+ )
+
+ return {
+ "id": user.id,
+ "email": user.email,
+ "first_name": user.first_name,
+ "last_name": user.last_name,
+ "preferred_name": user.preferred_name,
+ "class_year": user.class_year,
+ "profile_picture": user.profile_picture,
+ "website": user.website,
+ "description": user.description,
+ "departments": user_departments,
+ "majors": user_majors,
+ }
+
+
+@main_blueprint.route("/profile", methods=["GET"])
+@jwt_required()
+def get_profile() -> Response:
+ """GET /profile: current user profile"""
+ user_email = get_jwt_identity()
+ user = db.session.execute(
+ db.select(User).where(User.email == user_email)
+ ).scalar_one_or_none()
+
+ if not user:
+ return make_response(jsonify({"msg": "User not found"}), 404)
+
+ return jsonify(user_to_dict(user))
+
+
+@main_blueprint.route("/profile", methods=["PUT"])
+@jwt_required()
+def update_profile() -> Response:
+ """PUT /profile: Updates current user profile"""
+ user_email = get_jwt_identity()
+ user = db.session.execute(
+ db.select(User).where(User.email == user_email)
+ ).scalar_one_or_none()
+
+ if not user:
+ return make_response(jsonify({"msg": "User not found"}), 404)
+
+ json_data = request.get_json()
+ if not json_data:
+ return make_response(jsonify({"msg": "Missing JSON in request"}), 400)
+
+ # Update basic User fields
+ user.first_name = json_data.get("first_name", user.first_name)
+ user.last_name = json_data.get("last_name", user.last_name)
+ user.preferred_name = json_data.get("preferred_name", user.preferred_name)
+ user.class_year = json_data.get("class_year", user.class_year)
+ user.website = json_data.get("website", user.website)
+ user.description = json_data.get("description", user.description)
+
+ if "departments" in json_data:
+ db.session.query(UserDepartments).filter(
+ UserDepartments.user_id == user.id
+ ).delete()
+ for dept_id in json_data["departments"]:
+ if db.session.get(
+ RPIDepartments, dept_id
+ ): # Ensure department exists before adding
+ new_user_dept = UserDepartments(user_id=user.id, department_id=dept_id)
+ db.session.add(new_user_dept)
+
+ if "majors" in json_data:
+ db.session.query(UserMajors).filter(UserMajors.user_id == user.id).delete()
+ for major_code in json_data["majors"]:
+ if db.session.get(Majors, major_code): # Ensure major exists before adding
+ new_user_major = UserMajors(user_id=user.id, major_code=major_code)
+ db.session.add(new_user_major)
+
+ db.session.commit()
+
+ return jsonify({"msg": "Profile updated successfully"})
diff --git a/tests/test_opportunity.py b/tests/test_opportunity.py
index 8a177813..dcc567b4 100644
--- a/tests/test_opportunity.py
+++ b/tests/test_opportunity.py
@@ -3,6 +3,7 @@
import pytest
from flask.testing import FlaskClient
+
def test_get_opportunity_parametrized(test_client: FlaskClient):
"""
GIVEN a Flask application configured for testing
diff --git a/tests/test_profile_routes.py b/tests/test_profile_routes.py
index b35edb4d..644aaee9 100644
--- a/tests/test_profile_routes.py
+++ b/tests/test_profile_routes.py
@@ -1,107 +1,137 @@
-import json
-from flask.testing import FlaskClient
-from labconnect.models import User, UserDepartments, UserMajors
-from labconnect import db
-
-
-def login_as_student(test_client: FlaskClient):
- """Helper function to log in a user and handle the auth flow."""
- response = test_client.get("/login")
- assert response.status_code == 302
-
- redirect_url = response.headers['Location']
- code = redirect_url.split('code=')[1]
-
- token_response = test_client.post("/token", json={"code": code})
- assert token_response.status_code == 200
-
-
-# === GET /profile Tests ===
-
-def test_get_profile_success(test_client: FlaskClient):
- """
- logged-in user: '/profile' endpoint is requested (GET) -> correct data and 200 status
- """
- login_as_student(test_client)
-
- response = test_client.get("/profile")
- data = json.loads(response.data)
-
- assert response.status_code == 200
- assert data["email"] == "test@rpi.edu"
- assert data["first_name"] == "Test"
- assert data["last_name"] == "User"
- assert "departments" in data
- assert "majors" in data
-
-def test_get_profile_unauthorized(test_client: FlaskClient):
- """
- no user is logged in: '/profile' endpoint is requested (GET) -> 401 Unauthorized status is returned.
- """
- test_client.get("/logout")
- response = test_client.get("/profile")
- assert response.status_code == 401
-
-
-# === PUT /profile Tests ===
-
-def test_update_profile_success(test_client: FlaskClient):
- """
- logged-in user: '/profile' endpoint is updated with new data (PUT) -> 200 status and database changed.
- """
- login_as_student(test_client)
-
- update_data = {
- "first_name": "UpdatedFirst",
- "last_name": "UpdatedLast",
- "preferred_name": "Pref",
- "class_year": 2025,
- "website": "https://new.example.com",
- "description": "This is an updated description.",
- "departments": ["CS"],
- "majors": ["CSCI", "MATH"]
- }
-
- response = test_client.put("/profile", json=update_data)
- assert response.status_code == 200
- assert "Profile updated successfully" in json.loads(response.data)["msg"]
-
- # Verify the changes in the database
- user = db.session.execute(db.select(User).where(User.email == "test@rpi.edu")).scalar_one()
- assert user.first_name == "UpdatedFirst"
- assert user.website == "https://new.example.com"
- assert user.class_year == 2025
-
- user_depts = db.session.execute(db.select(UserDepartments.department_id).where(UserDepartments.user_id == user.id)).scalars().all()
- assert set(user_depts) == {"CS"}
-
- user_majors = db.session.execute(db.select(UserMajors.major_code).where(UserMajors.user_id == user.id)).scalars().all()
- assert set(user_majors) == {"CSCI", "MATH"}
-
-def test_update_profile_partial(test_client: FlaskClient):
- """
- logged-in user: '/profile' endpoint is updated with partial data (PUT) -> check only provided fields updated.
- """
- login_as_student(test_client)
-
- update_data = {
- "website": "https://partial.update.com",
- "description": "Only this was updated."
- }
-
- response = test_client.put("/profile", json=update_data)
- assert response.status_code == 200
-
- user = db.session.execute(db.select(User).where(User.email == "test@rpi.edu")).scalar_one()
- assert user.website == "https://partial.update.com"
- assert user.description == "Only this was updated."
- assert user.last_name == "User"
-
-def test_update_profile_unauthorized(test_client: FlaskClient):
- """
- no user is logged in: '/profile' endpoint is sent a PUT request -> 401 Unauthorized status.
- """
- test_client.get("/logout")
- update_data = {"first_name": "ShouldFail"}
- response = test_client.put("/profile", json=update_data)
- assert response.status_code == 401
\ No newline at end of file
+import json
+
+from flask.testing import FlaskClient
+
+from labconnect import db
+from labconnect.models import User, UserDepartments, UserMajors
+
+
+def login_as_student(test_client: FlaskClient):
+ """Helper function to log in a user and handle the auth flow."""
+ response = test_client.get("/login")
+ assert response.status_code == 302
+
+ redirect_url = response.headers["Location"]
+ code = redirect_url.split("code=")[1]
+
+ token_response = test_client.post("/token", json={"code": code})
+ assert token_response.status_code == 200
+
+
+# === GET /profile Tests ===
+
+
+def test_get_profile_success(test_client: FlaskClient):
+ """
+ logged-in user: '/profile' endpoint is requested (GET)
+ -> correct data and 200 status
+ """
+ login_as_student(test_client)
+
+ response = test_client.get("/profile")
+ data = json.loads(response.data)
+
+ assert response.status_code == 200
+ assert data["email"] == "test@rpi.edu"
+ assert data["first_name"] == "Test"
+ assert data["last_name"] == "User"
+ assert "departments" in data
+ assert "majors" in data
+
+
+def test_get_profile_unauthorized(test_client: FlaskClient):
+ """
+ no user is logged in: '/profile' endpoint is requested (GET)
+ -> 401 Unauthorized status is returned.
+ """
+ test_client.get("/logout")
+ response = test_client.get("/profile")
+ assert response.status_code == 401
+
+
+# === PUT /profile Tests ===
+
+
+def test_update_profile_success(test_client: FlaskClient):
+ """
+ logged-in user: '/profile' endpoint is updated with new data (PUT)
+ -> 200 status and database changed.
+ """
+ login_as_student(test_client)
+
+ update_data = {
+ "first_name": "UpdatedFirst",
+ "last_name": "UpdatedLast",
+ "preferred_name": "Pref",
+ "class_year": 2025,
+ "website": "https://new.example.com",
+ "description": "This is an updated description.",
+ "departments": ["CS"],
+ "majors": ["CSCI", "MATH"],
+ }
+
+ response = test_client.put("/profile", json=update_data)
+ assert response.status_code == 200
+ assert "Profile updated successfully" in json.loads(response.data)["msg"]
+
+ # Verify the changes in the database
+ user = db.session.execute(
+ db.select(User).where(User.email == "test@rpi.edu")
+ ).scalar_one()
+ assert user.first_name == "UpdatedFirst"
+ assert user.website == "https://new.example.com"
+ assert user.class_year == 2025
+
+ user_depts = (
+ db.session.execute(
+ db.select(UserDepartments.department_id).where(
+ UserDepartments.user_id == user.id
+ )
+ )
+ .scalars()
+ .all()
+ )
+ assert set(user_depts) == {"CS"}
+
+ user_majors = (
+ db.session.execute(
+ db.select(UserMajors.major_code).where(UserMajors.user_id == user.id)
+ )
+ .scalars()
+ .all()
+ )
+ assert set(user_majors) == {"CSCI", "MATH"}
+
+
+def test_update_profile_partial(test_client: FlaskClient):
+ """
+ logged-in user: '/profile' endpoint is updated with partial data (PUT)
+ -> check only provided fields updated.
+ """
+ login_as_student(test_client)
+
+ update_data = {
+ "website": "https://partial.update.com",
+ "description": "Only this was updated.",
+ }
+
+ response = test_client.put("/profile", json=update_data)
+ assert response.status_code == 200
+
+ user = db.session.execute(
+ db.select(User).where(User.email == "test@rpi.edu")
+ ).scalar_one()
+ assert user.website == "https://partial.update.com"
+ assert user.description == "Only this was updated."
+ assert user.last_name == "User"
+
+
+def test_update_profile_unauthorized(test_client: FlaskClient):
+ """
+ no user is logged in: '/profile' endpoint is sent a PUT request
+ -> 401 Unauthorized status.
+ """
+ test_client.get("/logout")
+ update_data = {"first_name": "ShouldFail"}
+ response = test_client.put("/profile", json=update_data)
+ assert response.status_code == 401
diff --git a/tests/test_single_opportunity_routes.py b/tests/test_single_opportunity_routes.py
index ca7922d1..e5fea110 100644
--- a/tests/test_single_opportunity_routes.py
+++ b/tests/test_single_opportunity_routes.py
@@ -1,94 +1,94 @@
-import pytest
-
-from labconnect import db
-from labconnect.models import Opportunities
-from labconnect.main.opportunity_routes import opportunity_to_dict
-
-
-def test_opportunity_to_dict_none():
- assert opportunity_to_dict(None) == {}
-
-
-def test_opportunity_to_dict_populated():
- # create a lightweight Opportunities instance (no DB persistence needed)
- opp = Opportunities()
- opp.id = 123
- opp.name = "Unit Test Opportunity"
- opp.description = "A test description"
- opp.recommended_experience = "Testing"
- opp.pay = 7.5
- opp.one_credit = True
- opp.two_credits = False
- opp.three_credits = False
- opp.four_credits = False
- opp.semester = None
- opp.year = 2025
- opp.active = True
-
- out = opportunity_to_dict(opp)
-
- assert out["id"] == 123
- assert out["name"] == "Unit Test Opportunity"
- assert out["pay"] == 7.5
- assert out["one_credit"] is True
- assert out["two_credits"] is False
- assert out["semester"] is None
- assert out["year"] == 2025
-
-
-@pytest.mark.usefixtures("test_client")
-def test_get_single_opportunity_not_found(test_client):
- response = test_client.get("/opportunity/999999")
- assert response.status_code == 404
-
-
-@pytest.mark.usefixtures("test_client")
-def test_get_single_opportunity_success_and_json_variant(test_client):
- # create and persist an opportunity to the test database
- opp = Opportunities()
- opp.name = "Endpoint Test Opportunity"
- opp.description = "Endpoint description"
- opp.recommended_experience = "None"
- opp.pay = 12.0
- opp.one_credit = False
- opp.two_credits = True
- opp.three_credits = False
- opp.four_credits = False
- opp.semester = None
- opp.year = 2025
- opp.application_due = None
- opp.active = True
- opp.last_updated = None
- opp.location = None
-
- db.session.add(opp)
- db.session.commit()
-
- # GET by URL id
- resp = test_client.get(f"/opportunity/{opp.id}")
- assert resp.status_code == 200
- data = resp.get_json()
- assert data["id"] == opp.id
- assert data["name"] == "Endpoint Test Opportunity"
- assert data["pay"] == 12.0
-
- # GET via JSON body variant
- resp2 = test_client.get("/opportunity", json={"id": opp.id})
- assert resp2.status_code == 200
- data2 = resp2.get_json()
- assert data2["id"] == opp.id
-
-
-@pytest.mark.usefixtures("test_client")
-def test_get_opportunity_via_json_errors(test_client):
- # No JSON -> 400
- resp = test_client.get("/opportunity")
- assert resp.status_code in (400, 415)
-
- # Missing id key -> 400
- resp2 = test_client.get("/opportunity", json={"wrong": "key"})
- assert resp2.status_code == 400
-
- # Non-integer id -> 400
- resp3 = test_client.get("/opportunity", json={"id": "not-an-int"})
- assert resp3.status_code == 400
+import pytest
+
+from labconnect import db
+from labconnect.main.opportunity_routes import opportunity_to_dict
+from labconnect.models import Opportunities
+
+
+def test_opportunity_to_dict_none():
+ assert opportunity_to_dict(None) == {}
+
+
+def test_opportunity_to_dict_populated():
+ # create a lightweight Opportunities instance (no DB persistence needed)
+ opp = Opportunities()
+ opp.id = 123
+ opp.name = "Unit Test Opportunity"
+ opp.description = "A test description"
+ opp.recommended_experience = "Testing"
+ opp.pay = 7.5
+ opp.one_credit = True
+ opp.two_credits = False
+ opp.three_credits = False
+ opp.four_credits = False
+ opp.semester = None
+ opp.year = 2025
+ opp.active = True
+
+ out = opportunity_to_dict(opp)
+
+ assert out["id"] == 123
+ assert out["name"] == "Unit Test Opportunity"
+ assert out["pay"] == 7.5
+ assert out["one_credit"] is True
+ assert out["two_credits"] is False
+ assert out["semester"] is None
+ assert out["year"] == 2025
+
+
+@pytest.mark.usefixtures("test_client")
+def test_get_single_opportunity_not_found(test_client):
+ response = test_client.get("/opportunity/999999")
+ assert response.status_code == 404
+
+
+@pytest.mark.usefixtures("test_client")
+def test_get_single_opportunity_success_and_json_variant(test_client):
+ # create and persist an opportunity to the test database
+ opp = Opportunities()
+ opp.name = "Endpoint Test Opportunity"
+ opp.description = "Endpoint description"
+ opp.recommended_experience = "None"
+ opp.pay = 12.0
+ opp.one_credit = False
+ opp.two_credits = True
+ opp.three_credits = False
+ opp.four_credits = False
+ opp.semester = None
+ opp.year = 2025
+ opp.application_due = None
+ opp.active = True
+ opp.last_updated = None
+ opp.location = None
+
+ db.session.add(opp)
+ db.session.commit()
+
+ # GET by URL id
+ resp = test_client.get(f"/opportunity/{opp.id}")
+ assert resp.status_code == 200
+ data = resp.get_json()
+ assert data["id"] == opp.id
+ assert data["name"] == "Endpoint Test Opportunity"
+ assert data["pay"] == 12.0
+
+ # GET via JSON body variant
+ resp2 = test_client.get("/opportunity", json={"id": opp.id})
+ assert resp2.status_code == 200
+ data2 = resp2.get_json()
+ assert data2["id"] == opp.id
+
+
+@pytest.mark.usefixtures("test_client")
+def test_get_opportunity_via_json_errors(test_client):
+ # No JSON -> 400
+ resp = test_client.get("/opportunity")
+ assert resp.status_code in (400, 415)
+
+ # Missing id key -> 400
+ resp2 = test_client.get("/opportunity", json={"wrong": "key"})
+ assert resp2.status_code == 400
+
+ # Non-integer id -> 400
+ resp3 = test_client.get("/opportunity", json={"id": "not-an-int"})
+ assert resp3.status_code == 400