diff --git a/labconnect/main/auth_routes.py b/labconnect/main/auth_routes.py index 630b80b..84d9887 100644 --- a/labconnect/main/auth_routes.py +++ b/labconnect/main/auth_routes.py @@ -194,6 +194,46 @@ def registerUser() -> Response: db.session.commit() return make_response({"msg": "New user added"}) +# promotes/demotes User to a Lab Manager +# requires a super admin to promote +@main_blueprint.patch("/users//permissions") +@jwt_required() +def promoteUser(email: str) -> Response: + json_data = request.json + if not json_data or not json_data.get("change_status"): + abort(400) + + # if user accessing doesn't have the right perms then they can't assign perms + promoter_id = get_jwt_identity() + promoter_perms = db.session.query(ManagementPermissions).filter_by( + user_id=promoter_id + ).first() + if not promoter_perms or not promoter_perms.super_admin: + return make_response({"msg": "Missing permissions"}, 401) + + # look for the user that will be promoted + manager = db.session.query(User).filter_by(email=email).first() + if not manager: + return make_response({"msg": "No user matches RCS ID"}, 500) + + management_permissions = db.session.query(ManagementPermissions).filter_by( + user_id=manager.id + ).first() + + if management_permissions.admin: + management_permissions.admin = False + elif not management_permissions.admin: + management_permissions.admin = True + + if management_permissions is None: + management_permissions = ManagementPermissions(user_id=manager.id, admin=True) + db.session.add(management_permissions) + + db.session.commit() + + return make_response({"msg": "User Lab Manager permissions changed!"}, 200) + + @main_blueprint.get("/metadata/") def metadataRoute() -> Response: diff --git a/labconnect/main/routes.py b/labconnect/main/routes.py index 8160d65..df9454c 100644 --- a/labconnect/main/routes.py +++ b/labconnect/main/routes.py @@ -27,13 +27,16 @@ def index() -> dict[str, str]: @main_blueprint.get("/departments") def departmentCards(): data = db.session.execute( - db.select(RPIDepartments.name, RPIDepartments.school_id, RPIDepartments.id) + db.select(RPIDepartments.name, RPIDepartments.school_id, RPIDepartments.id, + RPIDepartments.description, RPIDepartments.website) ).all() results = [ { - "title": department.name, - "department_id": department.id, - "school": department.school_id, + "name": department.name, + "description": department.description, + "id": department.id, + "school_id": department.school_id, + "website": department.website, "image": "https://cdn-icons-png.flaticon.com/512/5310/5310672.png", } for department in data diff --git a/labconnect/serializers.py b/labconnect/serializers.py index 0a7b28a..f4081da 100644 --- a/labconnect/serializers.py +++ b/labconnect/serializers.py @@ -2,8 +2,9 @@ from labconnect.models import Courses, Opportunities -def serialize_course(course: Courses) -> str: - return f"{course.code} {course.name}" +def serialize_course(course: Courses) -> dict: + course = {'code': course.code, 'name': course.name} + return course def serialize_opportunity( diff --git a/tests/conftest.py b/tests/conftest.py index 6106dd4..eeeae20 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,7 +17,13 @@ def test_client(): # Set the Testing configuration prior to creating the Flask application flask_app = create_app() - flask_app.config.update({"TESTING": True, "DEBUG": True}) + flask_app.config.update({ + "TESTING": True, + "DEBUG": True, + 'JWT_TOKEN_LOCATION': ['cookies', 'headers'], + 'JWT_COOKIE_CSRF_PROTECT': True + }) + # Create a test client using the Flask application configured for testing with flask_app.test_client() as testing_client: diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 73610ce..c1e141a 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -4,6 +4,7 @@ # from flask import json # from flask.testing import FlaskClient +# import pytest # def test_login_route_one(test_client: FlaskClient) -> None: @@ -158,4 +159,4 @@ # response = test_client.post("/login") -# assert response.status_code == 400 +# assert response.status_code == 400 \ No newline at end of file diff --git a/tests/test_departments.py b/tests/test_departments.py index a8397be..3818e52 100644 --- a/tests/test_departments.py +++ b/tests/test_departments.py @@ -21,20 +21,21 @@ "Computer Science", "Biology", "Materials Engineering", - "Math", "Environmental Engineering", + "Math", "Aerospace Engineering", - "Areonautical Engineering", + "Aeronautical Engineering", + "Mechanical, Aerospace, and Nuclear Engineering" ], }, { "field": "description", "values": [ - "DS", - "life", + "DS is rough", + "life science", "also pretty cool", + "water stuff", "quick maths", - "water", "space, the final frontier", "flying, need for speed", ], @@ -42,18 +43,18 @@ { "field": "school_id", "values": [ - "School of science", - "School of science", - "School of engineering", - "School of science", - "School of engineering", - "School of engineering", - "School of engineering", + "School of Science", + "School of Science", + "School of Engineering", + "School of Science", + "School of Engineering", + "School of Engineering", + "School of Engineering", ], }, { "field": "id", - "values": ["CSCI", "BIOL", "MTLE", "MATH", "ENVI", "MANE", "MANE"], + "values": ["CSCI", "BIOL", "MTLE", "MATH", "ENVE", "MANE"], }, { "field": "image", @@ -62,12 +63,12 @@ ] * 7, }, - {"field": "webcite", "values": ["https://www.rpi.edu"] * 7}, + {"field": "website", "values": ["https://www.rpi.edu"] * 7}, ], ), ( - "/department", - {"department": "Computer Science"}, + "/departments/CSCI", + None, 200, [ {"field": "name", "values": ["Computer Science"]}, @@ -80,7 +81,7 @@ "https://cdn-icons-png.flaticon.com/512/5310/5310672.png" ], }, - {"field": "webcite", "values": ["https://www.rpi.edu"]}, + {"field": "website", "values": ["https://www.rpi.edu"]}, { "field": "professors", "subfields": [ diff --git a/tests/test_general.py b/tests/test_general.py index e712e64..680ce75 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -5,6 +5,7 @@ import pytest from flask import json from flask.testing import FlaskClient +from flask_jwt_extended import create_access_token def test_home_page(test_client: FlaskClient) -> None: @@ -19,25 +20,6 @@ def test_home_page(test_client: FlaskClient) -> None: assert {"Hello": "There"} == json.loads(response.data) -def test_discover_route(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/discover' page is requested (GET) - THEN check that the response is valid - """ - response = test_client.get("/discover") - - assert response.status_code == 200 - # Uncomment and modify the following line with expected response data - # data = json.loads(response.data.decode("utf-8")) - # assert data["data"][0] == { - # "title": "Nelson", - # "major": "CS", - # "attributes": ["Competitive Pay", "Four Credits", "Three Credits"], - # "pay": 9000.0, - # } - - @pytest.mark.parametrize( "input_id, expected_profile", [ @@ -46,7 +28,8 @@ def test_discover_route(test_client: FlaskClient) -> None: { "id": "cenzar", "first_name": "Rafael", - "opportunities": [...], # Replace with expected opportunities data + "opportunities": ["opportunity1"], + # Replace with expected opportunities data }, ) ], @@ -57,7 +40,19 @@ def test_profile_page(test_client: FlaskClient, input_id, expected_profile) -> N WHEN the '/profile/' page is requested (GET) THEN check that the response is valid """ - response = test_client.get("/profile", json={"id": input_id}) + # login_response = test_client.post("/login", + # json={"username": "test_user", "password": "password123"}) + # login_data = json.loads(login_response.data) + with test_client.application.app_context(): + access_token = create_access_token(identity='cenzar@rpi.edu') + + # response = test_client.get("/profile", json={"id": input_id}) + # Make the request with the JWT token + response = test_client.get( + "/profile", + json={"id": input_id}, + headers={'Authorization': f'Bearer {access_token}'} + ) assert response.status_code == 200 @@ -108,7 +103,7 @@ def test_years_route(test_client: FlaskClient) -> None: response = test_client.get("/years") assert response.status_code == 200 - assert [2024, 2025, 2026, 2027, 2028, 2029, 2030, 2031] == json.loads(response.data) + assert [2025, 2026, 2027, 2028, 2029, 2030, 2031] == json.loads(response.data) def test_professor_profile(test_client: FlaskClient) -> None: @@ -117,7 +112,7 @@ def test_professor_profile(test_client: FlaskClient) -> None: WHEN the '/getProfessorProfile/' page is requested (GET) THEN check that the response is valid """ - response = test_client.get("/getProfessorProfile/1") + response = test_client.get("/staff/cenzar") assert response.status_code == 200 diff --git a/tests/test_manager_promotion.py b/tests/test_manager_promotion.py new file mode 100644 index 0000000..62771d4 --- /dev/null +++ b/tests/test_manager_promotion.py @@ -0,0 +1,244 @@ +import pytest +from flask_jwt_extended import create_access_token + +from labconnect import db +from labconnect.models import ManagementPermissions, User + + +@pytest.fixture +def setup_database(test_client): + """Set up and tear down database for each test""" + # rollback database for upcoming test + db.session.rollback() + db.session.remove() + + # Clean up existing data + db.session.execute(db.text("TRUNCATE TABLE management_permissions CASCADE")) + db.session.execute(db.text("TRUNCATE TABLE \"user\" CASCADE")) + db.session.commit() + + yield + + +@pytest.fixture +def setup_users(test_client, setup_database): + """Set up test users and permissions""" + # add super admin user + super_admin = User( + id="superadm1", + email="superadmin@example.com", + first_name="Super", + last_name="Admin" + ) + db.session.add(super_admin) + db.session.commit() + + super_admin_perms = ManagementPermissions( + user_id=super_admin.id, + super_admin=True, + admin=False + ) + db.session.add(super_admin_perms) + + # add promotable user + regular_user = User( + id="regular01", + email="regular@example.com", + first_name="Regular", + last_name="User" + ) + db.session.add(regular_user) + db.session.commit() + + regular_user_perms = ManagementPermissions( + user_id=regular_user.id, + super_admin=False, + admin=False + ) + db.session.add(regular_user_perms) + + # add demotable user + regular_user2 = User( + id="regular02", + email="regular2@example.com", + first_name="Regular2", + last_name="User2" + ) + db.session.add(regular_user2) + db.session.commit() + + regular_user2_perms = ManagementPermissions( + user_id=regular_user2.id, + super_admin=False, + admin=True + ) + db.session.add(regular_user2_perms) + + # add non-super-admin user + non_admin = User( + id="nonadmin1", + email="nonadmin@example.com", + first_name="Non", + last_name="Admin" + ) + db.session.add(non_admin) + db.session.commit() + + non_admin_perms = ManagementPermissions( + user_id=non_admin.id, + super_admin=False, + admin=True + ) + db.session.add(non_admin_perms) + + db.session.commit() + + yield { + "super_admin": super_admin, + "regular_user": regular_user, + "regular_user2": regular_user2, + "non_admin": non_admin + } + + +@pytest.fixture +def create_access_token_for_user(test_client): + """Create a real JWT access token for testing""" + + def _create_token(user_id): + return create_access_token(identity=user_id) + + return _create_token + + +def test_promote_user_success(test_client, setup_users, create_access_token_for_user): + """Test successful user promotion by super admin""" + users = setup_users + access_token = create_access_token_for_user(users["super_admin"].id) + + # make the request with url to ensure cookies work + response = test_client.patch( + f"/users/{users['regular_user'].email}/permissions", + headers={"Authorization": f"Bearer {access_token}"}, + json={"change_status": True}, + ) + + assert response.status_code == 200 + assert response.json["msg"] == "User Lab Manager permissions changed!" + + # verify the user was actually promoted + promoted_perms = db.session.query(ManagementPermissions).filter_by( + user_id=users["regular_user"].id + ).first() + assert promoted_perms.admin is True + +def test_demote_user_success(test_client, setup_users, create_access_token_for_user): + """Test successful user demotion by super admin""" + users = setup_users + access_token = create_access_token_for_user(users["super_admin"].id) + + # demote user + response = test_client.patch( + f"/users/{users['regular_user2'].email}/permissions", + headers={"Authorization": f"Bearer {access_token}"}, + json={"change_status": True}, + ) + + assert response.status_code == 200 + assert response.json["msg"] == "User Lab Manager permissions changed!" + + # verify the user was actually promoted + demoted_perms = db.session.query(ManagementPermissions).filter_by( + user_id=users["regular_user2"].id + ).first() + assert demoted_perms.admin is False + +def test_promote_user_no_json_data(test_client, setup_users, + create_access_token_for_user): + """Test promotion fails when no JSON data is provided""" + users = setup_users + access_token = create_access_token_for_user(users["super_admin"].id) + + response = test_client.patch( + f"/users/{users['regular_user'].email}/permissions", + headers={"Authorization": f"Bearer {access_token}"}, + content_type='application/json' + ) + + assert response.status_code == 400 + + +def test_promote_user_no_super_admin_perms(test_client, setup_users, + create_access_token_for_user): + """Test promotion fails when promoter is not a super admin""" + users = setup_users + access_token = create_access_token_for_user(users["non_admin"].id) + + + + response = test_client.patch( + f"/users/{users['regular_user'].email}/permissions", + headers={"Authorization": f"Bearer {access_token}"}, + json={"change_status": True} + ) + + assert response.status_code == 401 + assert response.json["msg"] == "Missing permissions" + + +def test_promote_user_promoter_has_no_perms_record(test_client, setup_users, + create_access_token_for_user): + """Test promotion fails when promoter has no permissions record""" + users = setup_users + + # add user with no perms + user_no_perms = User( + id="noperms01", + email="noperms@example.com", + first_name="No", + last_name="Perms" + ) + db.session.add(user_no_perms) + db.session.commit() + + access_token = create_access_token_for_user(user_no_perms.id) + + response = test_client.patch( + f"/users/{users['regular_user'].email}/permissions", + headers={"Authorization": f"Bearer {access_token}"}, + json={"change_status": True} + ) + + assert response.status_code == 401 + assert response.json["msg"] == "Missing permissions" + + +def test_promote_user_target_not_found(test_client, setup_users, + create_access_token_for_user): + """Test promotion fails when target user doesn't exist""" + users = setup_users + access_token = create_access_token_for_user(users["super_admin"].id) + + response = test_client.patch( + "/users/nonexistent@example.com/permissions", + headers={"Authorization": f"Bearer {access_token}"}, + json={"change_status": True} + ) + + assert response.status_code == 500 + assert response.json["msg"] == "No user matches RCS ID" + + +def test_promote_user_no_jwt_token(test_client, setup_users): + """Test promotion fails when no JWT token is provided""" + users = setup_users + + # clear existing cookies + test_client.delete_cookie('access_token') + + response = test_client.patch( + f"/users/{users['regular_user'].email}/permissions", + json={"change_status": True} + ) + + assert response.status_code == 401 \ No newline at end of file