From 5ad4f012b12e4fdf1019cfbce4b0d82d88df9416 Mon Sep 17 00:00:00 2001 From: Rafael Cenzano <32753063+RafaelCenzano@users.noreply.github.com> Date: Fri, 22 May 2026 20:30:17 -0400 Subject: [PATCH 01/14] fix: update to use postgres for testing --- .github/workflows/pytest.yml | 4 ++-- config.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index aef9641a..250208e1 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -50,12 +50,12 @@ jobs: - name: Set up the Database env: - DATABASE_URL: postgresql+psycopg2://postgres:postgres@localhost:5432/labconnect + DB: postgresql+psycopg2://postgres:postgres@localhost:5432/labconnect run: | python db_init.py create - name: Running pytest env: - DATABASE_URL: postgresql+psycopg2://postgres:postgres@localhost:5432/labconnect + DB: postgresql+psycopg2://postgres:postgres@localhost:5432/labconnect run: | python -m pytest tests/ diff --git a/config.py b/config.py index d973c9c5..5e401a1e 100644 --- a/config.py +++ b/config.py @@ -26,7 +26,8 @@ class Config: SENTRY_PROFILES_SAMPLE_RATE = float(getenv("SENTRY_PROFILES_SAMPLE_RATE", 1.0)) SQLALCHEMY_DATABASE_URI = getenv( - "DB", "postgresql+psycopg2://postgres:root@localhost/labconnect" + "DATABASE_URL", + getenv("DB", "postgresql+psycopg2://postgres:root@localhost/labconnect"), ) JWT_SECRET_KEY = getenv("JWT_SECRET_KEY", "jwt-secret") From 74bfd6208373864a37babd1d3a59ae21335498d0 Mon Sep 17 00:00:00 2001 From: Rafael Cenzano <32753063+RafaelCenzano@users.noreply.github.com> Date: Fri, 22 May 2026 20:31:29 -0400 Subject: [PATCH 02/14] chore: cleanup inline bool value --- labconnect/__init__.py | 5 +++-- labconnect/main/opportunity_routes.py | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/labconnect/__init__.py b/labconnect/__init__.py index 2e477e95..e3db148b 100644 --- a/labconnect/__init__.py +++ b/labconnect/__init__.py @@ -87,8 +87,9 @@ def initialize_extensions(app) -> None: app.logger.info("Extensions initialized.") with app.app_context(): - db.create_all() - app.logger.info("Database tables created.") + if not app.config.get("TESTING"): + db.create_all() + app.logger.info("Database tables created.") @app.after_request def refresh_expiring_jwts(response): diff --git a/labconnect/main/opportunity_routes.py b/labconnect/main/opportunity_routes.py index 4a6fcd79..6a4750ac 100644 --- a/labconnect/main/opportunity_routes.py +++ b/labconnect/main/opportunity_routes.py @@ -42,9 +42,9 @@ 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": ( + opportunity.semester.value if opportunity.semester is not None else None + ), "year": opportunity.year, "active": bool(opportunity.active), } From 5619ba1c5cc8ec0491824faafa59b8fe66ae79f4 Mon Sep 17 00:00:00 2001 From: Rafael Cenzano <32753063+RafaelCenzano@users.noreply.github.com> Date: Fri, 22 May 2026 20:34:01 -0400 Subject: [PATCH 03/14] chore: formatting --- labconnect/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/labconnect/serializers.py b/labconnect/serializers.py index f4081da6..f340bac1 100644 --- a/labconnect/serializers.py +++ b/labconnect/serializers.py @@ -3,7 +3,7 @@ def serialize_course(course: Courses) -> dict: - course = {'code': course.code, 'name': course.name} + course = {"code": course.code, "name": course.name} return course From 141235f0f219cf49424e094d93cab3fb65fe4d57 Mon Sep 17 00:00:00 2001 From: Rafael Cenzano <32753063+RafaelCenzano@users.noreply.github.com> Date: Fri, 22 May 2026 20:43:24 -0400 Subject: [PATCH 04/14] fix: update db uri --- config.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/config.py b/config.py index 5e401a1e..ce4a952f 100644 --- a/config.py +++ b/config.py @@ -3,9 +3,15 @@ from os import getenv, path from dotenv import load_dotenv +from sqlalchemy.pool import StaticPool basedir = path.abspath(path.dirname(__file__)) +_test_db_uri = getenv( + "DATABASE_URL", + getenv("DB", "postgresql+psycopg2://postgres:root@localhost/labconnect"), +) + class Config: load_dotenv() @@ -25,10 +31,7 @@ class Config: SENTRY_TRACES_SAMPLE_RATE = float(getenv("SENTRY_TRACES_SAMPLE_RATE", 1.0)) SENTRY_PROFILES_SAMPLE_RATE = float(getenv("SENTRY_PROFILES_SAMPLE_RATE", 1.0)) - SQLALCHEMY_DATABASE_URI = getenv( - "DATABASE_URL", - getenv("DB", "postgresql+psycopg2://postgres:root@localhost/labconnect"), - ) + SQLALCHEMY_DATABASE_URI = _test_db_uri JWT_SECRET_KEY = getenv("JWT_SECRET_KEY", "jwt-secret") JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1) @@ -48,6 +51,12 @@ class TestingConfig(Config): DEBUG = True JWT_COOKIE_SECURE = False + if _test_db_uri.startswith("sqlite"): + SQLALCHEMY_ENGINE_OPTIONS = { + "connect_args": {"check_same_thread": False}, + "poolclass": StaticPool, + } + class ProductionConfig(Config): TESTING = False From 738e74b1a0bcb79c05e82e46f0a8a9cdf02ea9c2 Mon Sep 17 00:00:00 2001 From: Rafael Cenzano <32753063+RafaelCenzano@users.noreply.github.com> Date: Fri, 22 May 2026 20:43:46 -0400 Subject: [PATCH 05/14] refactor: move seed data from dbinit and for tests to seed.py --- db_init.py | 396 +---------------------------------------------- tests/seed.py | 420 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 423 insertions(+), 393 deletions(-) create mode 100644 tests/seed.py diff --git a/db_init.py b/db_init.py index 0acbfea0..80664418 100644 --- a/db_init.py +++ b/db_init.py @@ -193,399 +193,9 @@ def main() -> None: with app.app_context(): db.create_all() - rpi_schools_rows = ( - ("School of Science", "the coolest of them all"), - ("School of Engineering", "also pretty cool"), - ) - - for row_tuple in rpi_schools_rows: - row = RPISchools() - row.name = row_tuple[0] - row.description = row_tuple[1] - - db.session.add(row) - db.session.commit() - - rpi_departments_rows = ( - ("Computer Science", "DS is rough", "School of Science", "CSCI"), - ("Biology", "life science", "School of Science", "BIOL"), - ( - "Materials Engineering", - "also pretty cool", - "School of Engineering", - "MTLE", - ), - ( - "Environmental Engineering", - "water stuff", - "School of Engineering", - "ENVE", - ), - ("Math", "quick maths", "School of Science", "MATH"), - ( - "Mechanical, Aerospace, and Nuclear Engineering", - "space, the final frontier", - "School of Engineering", - "MANE", - ), - ) - - for row_tuple in rpi_departments_rows: - row = RPIDepartments() - row.name = row_tuple[0] - row.description = row_tuple[1] - row.school_id = row_tuple[2] - row.id = row_tuple[3] - row.image = "https://cdn-icons-png.flaticon.com/512/5310/5310672.png" - row.website = "https://www.rpi.edu" - - db.session.add(row) - db.session.commit() - - class_years_rows = (2025, 2026, 2027, 2028, 2029, 2030, 2031) - - for row_item in class_years_rows: - row = ClassYears() - row.class_year = row_item - row.active = True - - db.session.add(row) - db.session.commit() - - lab_manager_rows = ( - ("led", "Duy", "Le", "CSCI", "database database database"), - ( - "turner", - "Wes", - "Turner", - "CSCI", - "open source stuff is cool", - ), - ( - "kuzmin", - "Konstantine", - "Kuzmin", - "CSCI", - "java, psoft, etc.", - ), - ("goldd", "David", "Goldschmidt", "CSCI", "VIM master"), - ("rami", "Rami", "Rami", "MTLE", "cubes are cool"), - ("holm", "Mark", "Holmes", "MATH", "all about that math"), - ("test", "RCOS", "RCOS", "CSCI", "first test"), - ("test2", "RCOS", "RCOS", "CSCI", "Second test"), - ("test3", "RCOS", "RCOS", "CSCI", "Third test"), - ) - - raf_test_user = ( - "cenzar", - "Rafael", - "Cenzano", - "Raf", - 2025, - "CSCI", - "labconnect is the best RCOS project", - "https://rafael.sirv.com/Images/rafael.jpeg?thumbnail=350&format=webp&q=90", - "https://rafaelcenzano.com", - ) - - lab_manager = LabManager() - lab_manager.department_id = raf_test_user[5] - - db.session.add(lab_manager) - db.session.commit() - - user = User() - user.id = raf_test_user[0] - user.email = raf_test_user[0] + "@rpi.edu" - user.first_name = raf_test_user[1] - user.last_name = raf_test_user[2] - user.preferred_name = raf_test_user[3] - user.class_year = raf_test_user[4] - user.lab_manager_id = lab_manager.id - user.description = raf_test_user[6] - user.profile_picture = raf_test_user[7] - user.website = raf_test_user[8] - - db.session.add(user) - db.session.commit() - - for row_tuple in lab_manager_rows: - lab_manager = LabManager() - lab_manager.department_id = row_tuple[3] - - db.session.add(lab_manager) - db.session.commit() - - user = User() - user.id = row_tuple[0] - user.email = row_tuple[0] + "@rpi.edu" - user.first_name = row_tuple[1] - user.last_name = row_tuple[2] - user.lab_manager_id = lab_manager.id - user.description = row_tuple[4] - user.profile_picture = ( - "https://www.svgrepo.com/show/206842/professor.svg" - ) - - db.session.add(user) - db.session.commit() - - opportunities_rows = ( - ( - "Automated Cooling System", - "Energy efficient AC system", - "Thermodynamics", - 15.0, - False, - False, - False, - True, - SemesterEnum.SPRING, - 2025, - date.today(), - True, - datetime.now(), - LocationEnum.REMOTE, - ), - ( - "Iphone 15 durability test", - "Scratching the Iphone, drop testing etc.", - "Experienced in getting angry and throwing temper tantrum", - None, - True, - True, - True, - True, - SemesterEnum.SPRING, - 2025, - date.today(), - True, - datetime.now(), - LocationEnum.LALLY, - ), - ( - "Checking out cubes", - "Material Sciences", - "Experienced in materials.", - None, - True, - True, - True, - True, - SemesterEnum.FALL, - 2025, - date.today(), - True, - datetime.now(), - LocationEnum.MRC, - ), - ( - "Test the water", - "Testing the quality of water in Troy pipes", - "Understanding of lead poisioning", - None, - False, - False, - True, - True, - SemesterEnum.SUMMER, - 2025, - date.today(), - True, - datetime.now(), - LocationEnum.JEC, - ), - ( - "Data Science Research", - "Work with a team of researchers to analyze large datasets and " - "extract meaningful insights.", - "Python, Machine Learning, Data Analysis", - 20.0, - True, - False, - True, - False, - SemesterEnum.FALL, - 2025, - "2025-10-31", - True, - "2025-10-10T10:30:00", - LocationEnum.JROWL, - ), - ) - - for row_tuple in opportunities_rows: - row = Opportunities() - row.name = row_tuple[0] - row.description = row_tuple[1] - row.recommended_experience = row_tuple[2] - row.pay = row_tuple[3] - row.one_credit = row_tuple[4] - row.two_credits = row_tuple[5] - row.three_credits = row_tuple[6] - row.four_credits = row_tuple[7] - row.semester = row_tuple[8] - row.year = row_tuple[9] - row.application_due = row_tuple[10] - row.active = row_tuple[11] - row.last_updated = row_tuple[12] - row.location = row_tuple[13] - - db.session.add(row) - db.session.commit() - - courses_rows = ( - ("CSCI2300", "Introduction to Algorithms"), - ("CSCI4430", "Programming Languages"), - ("CSCI2961", "Rensselaer Center for Open Source"), - ("CSCI4390", "Data Mining"), - ) - - for row_tuple in courses_rows: - row = Courses() - row.code = row_tuple[0] - row.name = row_tuple[1] - - db.session.add(row) - db.session.commit() - - majors_rows = ( - ("CSCI", "Computer Science"), - ("ECSE", "Electrical, Computer, and Systems Engineering"), - ("BIOL", "Biological Science"), - ("MATH", "Mathematics"), - ("COGS", "Cognitive Science"), - ("PHYS", "Physics"), - ) - - for row_tuple in majors_rows: - row = Majors() - row.code = row_tuple[0] - row.name = row_tuple[1] - - db.session.add(row) - db.session.commit() - - # https://www.geeksforgeeks.org/datetime-timezone-in-sqlalchemy/ - # https://www.tutorialspoint.com/handling-timezone-in-python - - leads_rows = ( - (2, 1), - (1, 1), - (2, 2), - (1, 3), - (4, 4), - (8, 5), - ) - - for r in leads_rows: - row = Leads() - row.lab_manager_id = r[0] - row.opportunity_id = r[1] - - db.session.add(row) - db.session.commit() - - recommends_courses_rows = ( - (1, "CSCI4430"), - (1, "CSCI2961"), - (2, "CSCI4390"), - ) - - for r in recommends_courses_rows: - row = RecommendsCourses() - row.opportunity_id = r[0] - row.course_code = r[1] - - db.session.add(row) - db.session.commit() - - recommends_majors_rows = ((1, "CSCI"), (1, "PHYS"), (2, "BIOL")) - - for r in recommends_majors_rows: - row = RecommendsMajors() - row.opportunity_id = r[0] - row.major_code = r[1] - - db.session.add(row) - db.session.commit() - - recommends_class_years_rows = ((3, 2025), (2, 2025), (2, 2026), (1, 2027)) - - for r in recommends_class_years_rows: - row = RecommendsClassYears() - row.opportunity_id = r[0] - row.class_year = r[1] - - db.session.add(row) - db.session.commit() - - user_majors = ( - ("cenzar", "MATH"), - ("cenzar", "CSCI"), - ("test", "CSCI"), - ) - - for r in user_majors: - row = UserMajors() - row.user_id = r[0] - row.major_code = r[1] - - db.session.add(row) - db.session.commit() - - for r in user_majors: - row = UserDepartments() - row.user_id = r[0] - row.department_id = r[1] - - db.session.add(row) - db.session.commit() - - user_courses = ( - ("cenzar", "CSCI2300", False), - ("cenzar", "CSCI4430", True), - ("test", "CSCI2300", False), - ) - - for r in user_courses: - row = UserCourses() - row.user_id = r[0] - row.course_code = r[1] - row.in_progress = r[2] - - db.session.add(row) - db.session.commit() - - participates_rows = ( - ("cenzar", 1), - ("cenzar", 2), - ("test", 3), - ("test", 4), - ) - - for r in participates_rows: - row = Participates() - row.user_id = r[0] - row.opportunity_id = r[1] - - db.session.add(row) - db.session.commit() - - user_saved_opportunities_rows = ( - ("cenzar", 2), - ("cenzar", 3), - ("test", 3), - ("test", 1), - ) - - for r in user_saved_opportunities_rows: - row = UserSavedOpportunities() - row.user_id = r[0] - row.opportunity_id = r[1] - - db.session.add(row) - db.session.commit() + from tests.seed import seed_development_data + + seed_development_data() tables = [ ClassYears, diff --git a/tests/seed.py b/tests/seed.py new file mode 100644 index 00000000..0f50acb7 --- /dev/null +++ b/tests/seed.py @@ -0,0 +1,420 @@ +"""Seed database with development and test fixture data.""" + +from datetime import date, datetime + +from labconnect import db +from labconnect.helpers import LocationEnum, SemesterEnum +from labconnect.models import ( + ClassYears, + Courses, + LabManager, + Leads, + Majors, + Opportunities, + Participates, + RecommendsClassYears, + RecommendsCourses, + RecommendsMajors, + RPIDepartments, + RPISchools, + User, + UserCourses, + UserDepartments, + UserMajors, + UserSavedOpportunities, +) + + +def seed_development_data() -> None: + """Insert canonical dev/test rows. Caller must run inside app context.""" + rpi_schools_rows = ( + ("School of Science", "the coolest of them all"), + ("School of Engineering", "also pretty cool"), + ) + + for row_tuple in rpi_schools_rows: + row = RPISchools() + row.name = row_tuple[0] + row.description = row_tuple[1] + + db.session.add(row) + db.session.commit() + + rpi_departments_rows = ( + ("Computer Science", "DS is rough", "School of Science", "CSCI"), + ("Biology", "life science", "School of Science", "BIOL"), + ( + "Materials Engineering", + "also pretty cool", + "School of Engineering", + "MTLE", + ), + ( + "Environmental Engineering", + "water stuff", + "School of Engineering", + "ENVE", + ), + ("Math", "quick maths", "School of Science", "MATH"), + ( + "Mechanical, Aerospace, and Nuclear Engineering", + "space, the final frontier", + "School of Engineering", + "MANE", + ), + ) + + for row_tuple in rpi_departments_rows: + row = RPIDepartments() + row.name = row_tuple[0] + row.description = row_tuple[1] + row.school_id = row_tuple[2] + row.id = row_tuple[3] + row.image = "https://cdn-icons-png.flaticon.com/512/5310/5310672.png" + row.website = "https://www.rpi.edu" + + db.session.add(row) + db.session.commit() + + class_years_rows = (2025, 2026, 2027, 2028, 2029, 2030, 2031) + + for row_item in class_years_rows: + row = ClassYears() + row.class_year = row_item + row.active = True + + db.session.add(row) + db.session.commit() + + lab_manager_rows = ( + ("led", "Duy", "Le", "CSCI", "database database database"), + ( + "turner", + "Wes", + "Turner", + "CSCI", + "open source stuff is cool", + ), + ( + "kuzmin", + "Konstantine", + "Kuzmin", + "CSCI", + "java, psoft, etc.", + ), + ("goldd", "David", "Goldschmidt", "CSCI", "VIM master"), + ("rami", "Rami", "Rami", "MTLE", "cubes are cool"), + ("holm", "Mark", "Holmes", "MATH", "all about that math"), + ("test", "RCOS", "RCOS", "CSCI", "first test"), + ("test2", "RCOS", "RCOS", "CSCI", "Second test"), + ("test3", "RCOS", "RCOS", "CSCI", "Third test"), + ) + + raf_test_user = ( + "cenzar", + "Rafael", + "Cenzano", + "Raf", + 2025, + "CSCI", + "labconnect is the best RCOS project", + "https://rafael.sirv.com/Images/rafael.jpeg?thumbnail=350&format=webp&q=90", + "https://rafaelcenzano.com", + ) + + lab_manager = LabManager() + lab_manager.department_id = raf_test_user[5] + + db.session.add(lab_manager) + db.session.commit() + + user = User() + user.id = raf_test_user[0] + user.email = raf_test_user[0] + "@rpi.edu" + user.first_name = raf_test_user[1] + user.last_name = raf_test_user[2] + user.preferred_name = raf_test_user[3] + user.class_year = raf_test_user[4] + user.lab_manager_id = lab_manager.id + user.description = raf_test_user[6] + user.profile_picture = raf_test_user[7] + user.website = raf_test_user[8] + + db.session.add(user) + db.session.commit() + + for row_tuple in lab_manager_rows: + lab_manager = LabManager() + lab_manager.department_id = row_tuple[3] + + db.session.add(lab_manager) + db.session.commit() + + user = User() + user.id = row_tuple[0] + user.email = row_tuple[0] + "@rpi.edu" + user.first_name = row_tuple[1] + user.last_name = row_tuple[2] + user.lab_manager_id = lab_manager.id + user.description = row_tuple[4] + user.profile_picture = "https://www.svgrepo.com/show/206842/professor.svg" + + db.session.add(user) + db.session.commit() + + opportunities_rows = ( + ( + "Automated Cooling System", + "Energy efficient AC system", + "Thermodynamics", + 15.0, + False, + False, + False, + True, + SemesterEnum.SPRING, + 2025, + date.today(), + True, + datetime.now(), + LocationEnum.REMOTE, + ), + ( + "Iphone 15 durability test", + "Scratching the Iphone, drop testing etc.", + "Experienced in getting angry and throwing temper tantrum", + None, + True, + True, + True, + True, + SemesterEnum.SPRING, + 2025, + date.today(), + True, + datetime.now(), + LocationEnum.LALLY, + ), + ( + "Checking out cubes", + "Material Sciences", + "Experienced in materials.", + None, + True, + True, + True, + True, + SemesterEnum.FALL, + 2025, + date.today(), + True, + datetime.now(), + LocationEnum.MRC, + ), + ( + "Test the water", + "Testing the quality of water in Troy pipes", + "Understanding of lead poisioning", + None, + False, + False, + True, + True, + SemesterEnum.SUMMER, + 2025, + date.today(), + True, + datetime.now(), + LocationEnum.JEC, + ), + ( + "Data Science Research", + "Work with a team of researchers to analyze large datasets and " + "extract meaningful insights.", + "Python, Machine Learning, Data Analysis", + 20.0, + True, + False, + True, + False, + SemesterEnum.FALL, + 2025, + date(2025, 10, 31), + True, + datetime(2025, 10, 10, 10, 30, 0), + LocationEnum.JROWL, + ), + ) + + for row_tuple in opportunities_rows: + row = Opportunities() + row.name = row_tuple[0] + row.description = row_tuple[1] + row.recommended_experience = row_tuple[2] + row.pay = row_tuple[3] + row.one_credit = row_tuple[4] + row.two_credits = row_tuple[5] + row.three_credits = row_tuple[6] + row.four_credits = row_tuple[7] + row.semester = row_tuple[8] + row.year = row_tuple[9] + row.application_due = row_tuple[10] + row.active = row_tuple[11] + row.last_updated = row_tuple[12] + row.location = row_tuple[13] + + db.session.add(row) + db.session.commit() + + courses_rows = ( + ("CSCI2300", "Introduction to Algorithms"), + ("CSCI4430", "Programming Languages"), + ("CSCI2961", "Rensselaer Center for Open Source"), + ("CSCI4390", "Data Mining"), + ) + + for row_tuple in courses_rows: + row = Courses() + row.code = row_tuple[0] + row.name = row_tuple[1] + + db.session.add(row) + db.session.commit() + + majors_rows = ( + ("CSCI", "Computer Science"), + ("ECSE", "Electrical, Computer, and Systems Engineering"), + ("BIOL", "Biological Science"), + ("MATH", "Mathematics"), + ("COGS", "Cognitive Science"), + ("PHYS", "Physics"), + ) + + for row_tuple in majors_rows: + row = Majors() + row.code = row_tuple[0] + row.name = row_tuple[1] + + db.session.add(row) + db.session.commit() + + # https://www.geeksforgeeks.org/datetime-timezone-in-sqlalchemy/ + # https://www.tutorialspoint.com/handling-timezone-in-python + + leads_rows = ( + (2, 1), + (1, 1), + (2, 2), + (1, 3), + (4, 4), + (8, 5), + ) + + for r in leads_rows: + row = Leads() + row.lab_manager_id = r[0] + row.opportunity_id = r[1] + + db.session.add(row) + db.session.commit() + + recommends_courses_rows = ( + (1, "CSCI4430"), + (1, "CSCI2961"), + (2, "CSCI4390"), + ) + + for r in recommends_courses_rows: + row = RecommendsCourses() + row.opportunity_id = r[0] + row.course_code = r[1] + + db.session.add(row) + db.session.commit() + + recommends_majors_rows = ((1, "CSCI"), (1, "PHYS"), (2, "BIOL")) + + for r in recommends_majors_rows: + row = RecommendsMajors() + row.opportunity_id = r[0] + row.major_code = r[1] + + db.session.add(row) + db.session.commit() + + recommends_class_years_rows = ((3, 2025), (2, 2025), (2, 2026), (1, 2027)) + + for r in recommends_class_years_rows: + row = RecommendsClassYears() + row.opportunity_id = r[0] + row.class_year = r[1] + + db.session.add(row) + db.session.commit() + + user_majors = ( + ("cenzar", "MATH"), + ("cenzar", "CSCI"), + ("test", "CSCI"), + ) + + for r in user_majors: + row = UserMajors() + row.user_id = r[0] + row.major_code = r[1] + + db.session.add(row) + db.session.commit() + + for r in user_majors: + row = UserDepartments() + row.user_id = r[0] + row.department_id = r[1] + + db.session.add(row) + db.session.commit() + + user_courses = ( + ("cenzar", "CSCI2300", False), + ("cenzar", "CSCI4430", True), + ("test", "CSCI2300", False), + ) + + for r in user_courses: + row = UserCourses() + row.user_id = r[0] + row.course_code = r[1] + row.in_progress = r[2] + + db.session.add(row) + db.session.commit() + + participates_rows = ( + ("cenzar", 1), + ("cenzar", 2), + ("test", 3), + ("test", 4), + ) + + for r in participates_rows: + row = Participates() + row.user_id = r[0] + row.opportunity_id = r[1] + + db.session.add(row) + db.session.commit() + + user_saved_opportunities_rows = ( + ("cenzar", 2), + ("cenzar", 3), + ("test", 3), + ("test", 1), + ) + + for r in user_saved_opportunities_rows: + row = UserSavedOpportunities() + row.user_id = r[0] + row.opportunity_id = r[1] + + db.session.add(row) + db.session.commit() From 8660f6b18afa822758ef989197bdd274316ca2f4 Mon Sep 17 00:00:00 2001 From: Rafael Cenzano <32753063+RafaelCenzano@users.noreply.github.com> Date: Fri, 22 May 2026 20:43:55 -0400 Subject: [PATCH 06/14] feat: add helper functions for tests --- tests/helpers.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/helpers.py diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 00000000..8ce5041d --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,40 @@ +"""Shared helpers for parametrized route tests.""" + +from typing import Any + + +def assert_list_field_values(items: list[dict], field: str, values: list[Any]) -> None: + for item in items: + assert item[field] in values + + +def assert_nested_field_values( + items: list[dict], field: str, subfield: str, values: list[Any] +) -> None: + for item in items: + assert item[subfield] in values + + +def apply_response_checks( + json_data: dict | list, checks: list[dict] | None, *, is_list: bool = False +) -> None: + if not checks: + return + + for check in checks: + field = check["field"] + + if "subfields" in check: + if is_list: + raise ValueError("subfield checks require a dict response") + nested_items = json_data[field] + for item in nested_items: + for subfield_check in check["subfields"]: + assert item[subfield_check["subfield"]] in subfield_check["values"] + continue + + values = check["values"] + if is_list: + assert_list_field_values(json_data, field, values) + else: + assert json_data[field] in values From 13f6f095e4ea7045fa7c9c8a19222fdb7590c7ab Mon Sep 17 00:00:00 2001 From: Rafael Cenzano <32753063+RafaelCenzano@users.noreply.github.com> Date: Fri, 22 May 2026 20:44:07 -0400 Subject: [PATCH 07/14] fix: conftest setup --- tests/conftest.py | 139 +++++++++++++++++++++++----------------------- 1 file changed, 69 insertions(+), 70 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index eeeae20c..46884793 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,92 +1,91 @@ +import os + import pytest +from flask_jwt_extended import create_access_token +from sqlalchemy import Text, event -from labconnect import create_app +# Use SQLite for local pytest unless CI/dev sets a Postgres DB URL. +os.environ.setdefault("DB", "sqlite:///:memory:") -# -------- -# Fixtures -# https://gitlab.com/patkennedy79/flask_user_management_example/-/blob/main/tests/conftest.py -# -------- +from labconnect import create_app, db +from labconnect.models import Opportunities, User, update_search_vector -# @pytest.fixture(scope='module') -# def new_user(): -# user = User('patkennedy79@gmail.com', 'FlaskIsAwesome') -# return user +from tests.seed import seed_development_data -@pytest.fixture(scope="module") -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, - '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: - # Establish an application context - with flask_app.app_context(): - yield testing_client # this is where the testing happens! +def _use_sqlite() -> bool: + uri = os.environ.get("DB", "") + return uri.startswith("sqlite") + +def _configure_sqlite_models() -> None: + if Opportunities.__table__ is not None: + Opportunities.__table__.c.search_vector.type = Text() + Opportunities.__table_args__ = () + for listener in (update_search_vector,): + for hook in ("before_insert", "before_update"): + try: + event.remove(Opportunities, hook, listener) + except Exception: + pass -# @pytest.fixture(scope="module") -# def init_database(test_client): -# # Create the database and the database table -# db.create_all() -# # Insert user data -# default_user = User( -# email="patkennedy79@gmail.com", password_plaintext="FlaskIsAwesome" -# ) -# second_user = User( -# email="patrick@yahoo.com", password_plaintext="FlaskIsTheBest987" -# ) -# db.session.add(default_user) -# db.session.add(second_user) +if _use_sqlite(): + _configure_sqlite_models() -# # Commit the changes for the users -# db.session.commit() -# # Insert book data -# book1 = Book("Malibu Rising", "Taylor Jenkins Reid", "5", default_user.id) -# book2 = Book("Carrie Soto is Back", "Taylor Jenkins Reid", "4", default_user.id) -# book3 = Book("Book Lovers", "Emily Henry", "3", default_user.id) -# db.session.add(book1) -# db.session.add(book2) -# db.session.add(book3) +requires_postgres = pytest.mark.skipif( + _use_sqlite(), reason="This test requires PostgreSQL-specific SQL" +) -# # Commit the changes for the books -# db.session.commit() -# yield # this is where the testing happens! +@pytest.fixture(scope="session") +def test_client(): + flask_app = create_app() + flask_app.config.update( + { + "TESTING": True, + "DEBUG": True, + "SQLALCHEMY_DATABASE_URI": os.environ["DB"], + "JWT_TOKEN_LOCATION": ["headers"], + "JWT_COOKIE_CSRF_PROTECT": False, + "JWT_COOKIE_SECURE": False, + } + ) + + with flask_app.test_client() as testing_client: + with flask_app.app_context(): + db.drop_all() + db.create_all() + seed_development_data() + yield testing_client + with flask_app.app_context(): + db.session.remove() + db.engine.dispose() -# db.drop_all() +@pytest.fixture +def auth_headers(test_client): + """Bearer token for a seeded or custom user.""" -# @pytest.fixture(scope="function") -# def log_in_default_user(test_client): -# test_client.post( -# "/login", data={ -# "email": "patkennedy79@gmail.com", -# "password": "FlaskIsAwesome" -# } -# ) + def _headers(email: str = "test@rpi.edu"): + with test_client.application.app_context(): + token = create_access_token(identity=email) + return {"Authorization": f"Bearer {token}"} -# yield # this is where the testing happens! + return _headers -# test_client.get("/logout") +@pytest.fixture +def login_as_test_user(test_client, auth_headers): + """Authorization headers for test@rpi.edu via the dev login shortcut.""" -# @pytest.fixture(scope="function") -# def log_in_second_user(test_client): -# test_client.post( -# "login", data={"email": "patrick@yahoo.com", "password": "FlaskIsTheBest987"} -# ) + response = test_client.get("/login") + assert response.status_code == 302 + redirect_url = response.headers["Location"] + code = redirect_url.split("code=")[1].split("&")[0] -# yield # this is where the testing happens! + token_response = test_client.post("/token", json={"code": code}) + assert token_response.status_code == 200 -# # Log out the user -# test_client.get("/logout") + return auth_headers("test@rpi.edu") From 4d78b052fff2087b5b8fd3902395ebf977daf0f8 Mon Sep 17 00:00:00 2001 From: Rafael Cenzano <32753063+RafaelCenzano@users.noreply.github.com> Date: Fri, 22 May 2026 20:44:28 -0400 Subject: [PATCH 08/14] test: fix tests --- tests/test_authentication.py | 2 +- tests/test_departments.py | 186 ++++++++------- tests/test_general.py | 134 +++-------- tests/test_lab_manager.py | 138 +++++------ tests/test_majors.py | 102 +++----- tests/test_manager_promotion.py | 299 ++++++++++++------------ tests/test_opportunities_filtering.py | 115 ++++----- tests/test_opportunity.py | 206 ++++++++-------- tests/test_profile_routes.py | 97 ++++---- tests/test_single_opportunity_routes.py | 17 +- tests/test_user.py | 156 ++++--------- 11 files changed, 634 insertions(+), 818 deletions(-) diff --git a/tests/test_authentication.py b/tests/test_authentication.py index c1e141ae..bbf6c80a 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -159,4 +159,4 @@ # response = test_client.post("/login") -# assert response.status_code == 400 \ No newline at end of file +# assert response.status_code == 400 diff --git a/tests/test_departments.py b/tests/test_departments.py index 3818e524..c0844631 100644 --- a/tests/test_departments.py +++ b/tests/test_departments.py @@ -6,74 +6,61 @@ from flask import json from flask.testing import FlaskClient +from tests.helpers import apply_response_checks + +ALL_DEPARTMENTS_CHECKS = [ + { + "field": "name", + "values": [ + "Computer Science", + "Biology", + "Materials Engineering", + "Environmental Engineering", + "Math", + "Mechanical, Aerospace, and Nuclear Engineering", + ], + }, + { + "field": "description", + "values": [ + "DS is rough", + "life science", + "also pretty cool", + "water stuff", + "quick maths", + "space, the final frontier", + ], + }, + { + "field": "school_id", + "values": [ + "School of Science", + "School of Engineering", + ], + }, + { + "field": "id", + "values": ["CSCI", "BIOL", "MTLE", "ENVE", "MATH", "MANE"], + }, + { + "field": "image", + "values": ["https://cdn-icons-png.flaticon.com/512/5310/5310672.png"], + }, + {"field": "website", "values": ["https://www.rpi.edu"]}, +] + @pytest.mark.parametrize( "endpoint, request_json, expected_status, expected_response_checks", [ - ( - "/departments", - None, - 200, - [ - { - "field": "name", - "values": [ - "Computer Science", - "Biology", - "Materials Engineering", - "Environmental Engineering", - "Math", - "Aerospace Engineering", - "Aeronautical Engineering", - "Mechanical, Aerospace, and Nuclear Engineering" - ], - }, - { - "field": "description", - "values": [ - "DS is rough", - "life science", - "also pretty cool", - "water stuff", - "quick maths", - "space, the final frontier", - "flying, need for speed", - ], - }, - { - "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", - ], - }, - { - "field": "id", - "values": ["CSCI", "BIOL", "MTLE", "MATH", "ENVE", "MANE"], - }, - { - "field": "image", - "values": [ - "https://cdn-icons-png.flaticon.com/512/5310/5310672.png" - ] - * 7, - }, - {"field": "website", "values": ["https://www.rpi.edu"] * 7}, - ], - ), + ("/departments", None, 200, ALL_DEPARTMENTS_CHECKS), ( "/departments/CSCI", None, 200, [ {"field": "name", "values": ["Computer Science"]}, - {"field": "description", "values": ["DS"]}, - {"field": "school_id", "values": ["School of Science"]}, + {"field": "description", "values": ["DS is rough"]}, {"field": "id", "values": ["CSCI"]}, { "field": "image", @@ -83,41 +70,62 @@ }, {"field": "website", "values": ["https://www.rpi.edu"]}, { - "field": "professors", + "field": "staff", "subfields": [ { "subfield": "name", "values": [ "Duy Le", - "Rafael", - "Turner", - "Kuzmin", - "Goldschmidt", + "Raf Cenzano", + "Wes Turner", + "Konstantine Kuzmin", + "David Goldschmidt", + "RCOS RCOS", ], }, { - "subfield": "rcs_id", - "values": ["led", "cenzar", "turner", "kuzmin", "goldd"], - }, - ], - }, - { - "field": "opportunities", - "subfields": [ - {"subfield": "id", "values": [1, 2]}, - { - "subfield": "name", + "subfield": "id", "values": [ - "Automated Cooling System", - "Iphone 15 durability test", + "led", + "cenzar", + "turner", + "kuzmin", + "goldd", + "test", + "test2", + "test3", ], }, ], }, ], ), - ("/department", None, 400, None), - ("/department", {"wrong": "wrong"}, 400, None), + ( + "/departments/BIOL", + None, + 200, + [ + {"field": "name", "values": ["Biology"]}, + {"field": "id", "values": ["BIOL"]}, + ], + ), + ( + "/departments/MATH", + None, + 200, + [ + {"field": "name", "values": ["Math"]}, + {"field": "id", "values": ["MATH"]}, + ], + ), + ( + "/departments/UNKNOWN", + None, + 404, + None, + ), + ("/department", None, 404, None), + ("/department", {"wrong": "wrong"}, 404, None), ], ) def test_department_routes( @@ -127,11 +135,6 @@ def test_department_routes( expected_status, expected_response_checks, ) -> None: - """ - GIVEN a Flask application configured for testing - WHEN various '/departments' or '/department' routes are requested (GET) - THEN check that the response status and data are as expected - """ response = ( test_client.get(endpoint, json=request_json) if request_json @@ -139,16 +142,9 @@ def test_department_routes( ) assert response.status_code == expected_status - if expected_response_checks: - json_data = json.loads(response.data) + if expected_response_checks is None: + return - for check in expected_response_checks: - if "subfields" not in check: - for item in json_data: - assert item[check["field"]] in check["values"] - else: - for item in json_data.get(check["field"], []): - for subfield_check in check["subfields"]: - assert ( - item[subfield_check["subfield"]] in subfield_check["values"] - ) + json_data = json.loads(response.data) + is_list = endpoint == "/departments" + apply_response_checks(json_data, expected_response_checks, is_list=is_list) diff --git a/tests/test_general.py b/tests/test_general.py index 680ce754..d1e149f2 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -5,125 +5,55 @@ 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: - """ - GIVEN a Flask application configured for testing - WHEN the '/' page is requested (GET) - THEN check that the response is valid - """ - response = test_client.get("/") - - assert response.status_code == 200 - assert {"Hello": "There"} == json.loads(response.data) @pytest.mark.parametrize( - "input_id, expected_profile", + "route, expected_body", [ - ( - 1, - { - "id": "cenzar", - "first_name": "Rafael", - "opportunities": ["opportunity1"], - # Replace with expected opportunities data - }, - ) + ("/", {"Hello": "There"}), ], ) -def test_profile_page(test_client: FlaskClient, input_id, expected_profile) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/profile/' page is requested (GET) - THEN check that the response is valid - """ - # 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}'} - ) - +def test_static_routes( + test_client: FlaskClient, route: str, expected_body: dict +) -> None: + response = test_client.get(route) assert response.status_code == 200 - - json_data = json.loads(response.data) - assert json_data["id"] == expected_profile["id"] - assert json_data["first_name"] == expected_profile["first_name"] - assert json_data["opportunities"] != [] + assert json.loads(response.data) == expected_body @pytest.mark.parametrize( - "expected_schools", + "expected_years", [ - ( - ( - "School of Science", - "School of Engineering", - ), - ( - "the coolest of them all", - "also pretty cool", - ), - ) + ([2025, 2026, 2027, 2028, 2029, 2030, 2031],), ], ) -def test_schools_route(test_client: FlaskClient, expected_schools) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/schools' page is requested (GET) - THEN check that the response is valid - """ - response = test_client.get("/schools") - - assert response.status_code == 200 - - json_data = json.loads(response.data) - - for school in json_data: - assert school["name"] in expected_schools[0] - assert school["description"] in expected_schools[1] - - -def test_years_route(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/years' page is requested (GET) - THEN check that the response is valid - """ +def test_years_route(test_client: FlaskClient, expected_years) -> None: response = test_client.get("/years") - assert response.status_code == 200 - assert [2025, 2026, 2027, 2028, 2029, 2030, 2031] == json.loads(response.data) - + assert json.loads(response.data) == list(expected_years[0]) -def test_professor_profile(test_client: FlaskClient) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/getProfessorProfile/' page is requested (GET) - THEN check that the response is valid - """ - response = test_client.get("/staff/cenzar") +@pytest.mark.parametrize( + "rcs_id, expected_name, expected_department", + [ + ("cenzar", "Raf Cenzano", "Computer Science"), + ("led", "Duy Le", "Computer Science"), + ("rami", "Rami Rami", "Materials Engineering"), + ], +) +def test_staff_profile( + test_client: FlaskClient, + auth_headers, + rcs_id: str, + expected_name: str, + expected_department: str, +) -> None: + response = test_client.get( + f"/staff/{rcs_id}", + headers=auth_headers("test@rpi.edu"), + ) assert response.status_code == 200 - # Load the response data as JSON data = json.loads(response.data) - - assert data["first_name"] == "Rafael" - assert data["last_name"] == "Cenzano" - assert data["preferred_name"] == "Raf" - assert data["email"] == "cenzar@rpi.edu" - assert data["department_id"] == "Computer Science" - assert data["id"] == "cenzar" - assert "phone_number" in data - assert "website" in data + assert data["name"] == expected_name + assert data["department"] == expected_department diff --git a/tests/test_lab_manager.py b/tests/test_lab_manager.py index c053c643..493958d1 100644 --- a/tests/test_lab_manager.py +++ b/tests/test_lab_manager.py @@ -1,101 +1,85 @@ """ -Test lab manager routes with parameterization +Test staff / lab manager routes with parameterization """ +import json + import pytest -from flask import json from flask.testing import FlaskClient @pytest.mark.parametrize( - "input_json, expected_status, expected_response", + "rcs_id, expected_titles", [ ( - {"rcs_id": "cenzar"}, - 200, - { - "website": None, - "rcs_id": "cenzar", - "name": "Rafael", - "alt_email": None, - "phone_number": None, - "email": None, - "description": None, - }, + "cenzar", + {"Automated Cooling System", "Checking out cubes"}, + ), + ( + "led", + {"Automated Cooling System", "Iphone 15 durability test"}, + ), + ( + "turner", + set(), ), - (None, 400, None), # No input JSON case - ({"wrong": "wrong"}, 400, None), # Incorrect JSON structure case ], ) -def test_lab_manager_route( - test_client: FlaskClient, input_json, expected_status, expected_response +def test_staff_opportunity_cards( + test_client: FlaskClient, rcs_id: str, expected_titles: set[str] ) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/lab_manager' page is requested (GET) with different JSON inputs - THEN check that the response matches the expected outcome - """ - response = test_client.get("/lab_manager", json=input_json) - assert response.status_code == expected_status + response = test_client.get(f"/staff/opportunities/{rcs_id}") + assert response.status_code == 200 - if expected_response: - json_data = json.loads(response.data) - assert json_data == expected_response + titles = {card["title"] for card in json.loads(response.data)} + assert titles == expected_titles @pytest.mark.parametrize( - "input_json, expected_status", + "rcs_id", + ["missing-user", "notfound99", "xyz"], +) +def test_staff_opportunity_cards_empty(test_client: FlaskClient, rcs_id: str) -> None: + response = test_client.get(f"/staff/opportunities/{rcs_id}") + assert response.status_code == 200 + assert json.loads(response.data) == [] + + +@pytest.mark.parametrize( + "rcs_id, expected_name, expected_department", [ - ({"rcs_id": "cenzar"}, 200), - (None, 400), # No input JSON case - ({"wrong": "wrong"}, 400), # Incorrect JSON structure case + ("cenzar", "Raf Cenzano", "Computer Science"), + ("led", "Duy Le", "Computer Science"), + ("holm", "Mark Holmes", "Math"), ], ) -def test_lab_manager_opportunity_cards( - test_client: FlaskClient, input_json, expected_status +def test_staff_profile( + test_client: FlaskClient, + auth_headers, + rcs_id: str, + expected_name: str, + expected_department: str, ) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/lab_manager/opportunities' page is requested (GET) with different inputs - THEN check that the response matches the expected status code - """ - response = test_client.get("/lab_manager/opportunities", json=input_json) - assert response.status_code == expected_status + response = test_client.get( + f"/staff/{rcs_id}", + headers=auth_headers("test@rpi.edu"), + ) + assert response.status_code == 200 + + data = json.loads(response.data) + assert data["name"] == expected_name + assert data["department"] == expected_department - if input_json == {"rcs_id": "cenzar"} and expected_status == 200: - json_data = json.loads(response.data) - lab_manager_opportunities_data = [ - { - "name": "Automated Cooling System", - "description": "Energy efficient AC system", - "recommended_experience": "Thermodynamics", - "pay": 15.0, - "semester": "Spring", - "year": 2024, - "active": True, - }, - { - "name": "Iphone 15 durability test", - "description": "Scratching the Iphone, drop testing etc.", - "recommended_experience": "Experienced in getting angry and throwing" - " temper tantrum", - "pay": None, - "semester": "Spring", - "year": 2024, - "active": True, - }, - ] - for i, item in enumerate(json_data["cenzar"]): - assert item["name"] == lab_manager_opportunities_data[i]["name"] - assert ( - item["description"] == lab_manager_opportunities_data[i]["description"] - ) - assert ( - item["recommended_experience"] - == lab_manager_opportunities_data[i]["recommended_experience"] - ) - assert item["pay"] == lab_manager_opportunities_data[i]["pay"] - assert item["semester"] == lab_manager_opportunities_data[i]["semester"] - assert item["year"] == lab_manager_opportunities_data[i]["year"] - assert item["active"] == lab_manager_opportunities_data[i]["active"] +@pytest.mark.parametrize( + "rcs_id", + ["does-not-exist"], +) +def test_staff_profile_not_found( + test_client: FlaskClient, auth_headers, rcs_id: str +) -> None: + response = test_client.get( + f"/staff/{rcs_id}", + headers=auth_headers("test@rpi.edu"), + ) + assert response.status_code == 404 diff --git a/tests/test_majors.py b/tests/test_majors.py index 96eeca9e..216ddfb3 100644 --- a/tests/test_majors.py +++ b/tests/test_majors.py @@ -8,101 +8,67 @@ @pytest.mark.parametrize( - "expected_majors", + "expected_codes, expected_names", [ ( - ("CSCI", "ECSE", "BIOL", "MATH", "COGS"), + ("BIOL", "COGS", "CSCI", "ECSE", "MATH", "PHYS"), ( + "Biological Science", + "Cognitive Science", "Computer Science", "Electrical, Computer, and Systems Engineering", - "Biological Science", "Mathematics", - "Cognitive Science", + "Physics", ), - ) + ), ], ) -def test_majors_route(test_client: FlaskClient, expected_majors) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/majors' page is requested (GET) - THEN check that the response is valid - """ +def test_majors_route(test_client: FlaskClient, expected_codes, expected_names) -> None: response = test_client.get("/majors") - assert response.status_code == 200 json_data = json.loads(response.data) + assert len(json_data) == len(expected_codes) - for major in json_data: - assert major["code"] in expected_majors[0] - assert major["name"] in expected_majors[1] + codes = {major["code"] for major in json_data} + names = {major["name"] for major in json_data} + assert codes == set(expected_codes) + assert names == set(expected_names) @pytest.mark.parametrize( - "input_data, expected_majors", + "major_code, major_name", [ - ( - {"input": "computer"}, - ( - ("CSCI", "ECSE"), - ( - "Computer Science", - "Electrical, Computer, and Systems Engineering", - ), - ), - ), + ("CSCI", "Computer Science"), + ("ECSE", "Electrical, Computer, and Systems Engineering"), + ("BIOL", "Biological Science"), + ("MATH", "Mathematics"), + ("COGS", "Cognitive Science"), + ("PHYS", "Physics"), ], ) -def test_majors_route_with_input_name( - test_client: FlaskClient, input_data, expected_majors +def test_majors_route_each_major( + test_client: FlaskClient, major_code: str, major_name: str ) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/majors' page is requested (GET) - THEN check that the response is valid - """ - response = test_client.get("/majors", json=input_data) - + response = test_client.get("/majors") assert response.status_code == 200 - json_data = json.loads(response.data) - - for i, major in enumerate(json_data): - assert major["code"] == expected_majors[0][i] - assert major["name"] == expected_majors[1][i] + match = next( + (major for major in json.loads(response.data) if major["code"] == major_code), + None, + ) + assert match is not None + assert match["name"] == major_name @pytest.mark.parametrize( - "input_data, expected_majors", + "invalid_method, expected_status", [ - ( - {"input": "cs"}, - ( - ("CSCI", "ECSE", "MATH"), - ( - "Computer Science", - "Electrical, Computer, and Systems Engineering", - "Mathematics", - ), - ), - ), + ("post", 405), ], ) -def test_majors_route_with_input_code( - test_client: FlaskClient, input_data, expected_majors +def test_majors_route_invalid_method( + test_client: FlaskClient, invalid_method: str, expected_status: int ) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/majors' page is requested (GET) - THEN check that the response is valid - """ - response = test_client.get("/majors", json=input_data) - - assert response.status_code == 200 - - json_data = json.loads(response.data) - - for i, major in enumerate(json_data): - assert major["code"] == expected_majors[0][i] - assert major["name"] == expected_majors[1][i] + response = getattr(test_client, invalid_method)("/majors") + assert response.status_code == expected_status diff --git a/tests/test_manager_promotion.py b/tests/test_manager_promotion.py index 62771d4e..13b44924 100644 --- a/tests/test_manager_promotion.py +++ b/tests/test_manager_promotion.py @@ -6,225 +6,230 @@ @pytest.fixture -def setup_database(test_client): - """Set up and tear down database for each test""" - # rollback database for upcoming test +def setup_users(test_client): + """Set up promotion test users without clearing seeded development data.""" 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 - } + existing = db.session.execute( + db.select(User).where(User.email == "superadmin@example.com") + ).scalar_one_or_none() + if existing: + yield { + "super_admin": existing, + "regular_user": db.session.execute( + db.select(User).where(User.email == "regular@example.com") + ).scalar_one(), + "regular_user2": db.session.execute( + db.select(User).where(User.email == "regular2@example.com") + ).scalar_one(), + "non_admin": db.session.execute( + db.select(User).where(User.email == "nonadmin@example.com") + ).scalar_one(), + } + else: + 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) + + 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) + + 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) + + 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) - + + def _create_token(user): + with test_client.application.app_context(): + return create_access_token(identity=user.email) + 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) - + access_token = create_access_token_for_user(users["super_admin"]) + # 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() + 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) - + access_token = create_access_token_for_user(users["super_admin"]) + # 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() + 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): + +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) - + access_token = create_access_token_for_user(users["super_admin"]) + response = test_client.patch( f"/users/{users['regular_user'].email}/permissions", headers={"Authorization": f"Bearer {access_token}"}, - content_type='application/json' + 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): +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) - - - + access_token = create_access_token_for_user(users["non_admin"]) + response = test_client.patch( f"/users/{users['regular_user'].email}/permissions", headers={"Authorization": f"Bearer {access_token}"}, - json={"change_status": True} + 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): +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" + 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) - + + access_token = create_access_token_for_user(user_no_perms) + response = test_client.patch( f"/users/{users['regular_user'].email}/permissions", headers={"Authorization": f"Bearer {access_token}"}, - json={"change_status": True} + 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): +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) - + access_token = create_access_token_for_user(users["super_admin"]) + response = test_client.patch( "/users/nonexistent@example.com/permissions", headers={"Authorization": f"Bearer {access_token}"}, - json={"change_status": True} + json={"change_status": True}, ) - + assert response.status_code == 500 assert response.json["msg"] == "No user matches RCS ID" @@ -232,13 +237,13 @@ def test_promote_user_target_not_found(test_client, setup_users, 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') - + test_client.delete_cookie("access_token") + response = test_client.patch( f"/users/{users['regular_user'].email}/permissions", - json={"change_status": True} + json={"change_status": True}, ) - - assert response.status_code == 401 \ No newline at end of file + + assert response.status_code == 401 diff --git a/tests/test_opportunities_filtering.py b/tests/test_opportunities_filtering.py index 1c067014..b6936c29 100644 --- a/tests/test_opportunities_filtering.py +++ b/tests/test_opportunities_filtering.py @@ -2,104 +2,111 @@ Test opportunity filtering routes """ +import json + import pytest -from flask import json from flask.testing import FlaskClient +from tests.conftest import requires_postgres + +@requires_postgres @pytest.mark.parametrize( - "filters, expected_opportunities", + "query_string, expected_opportunities", [ + ("hourlypay=14.9", ["Automated Cooling System"]), + ("majors=BIOL", ["Iphone 15 durability test"]), ( - [{"field": "pay", "value": {"min": 14.9, "max": 21}}], - ["Automated Cooling System"], + "majors=CSCI,BIOL", + ["Automated Cooling System", "Iphone 15 durability test"], ), ( - [{"field": "departments", "value": ["Material Science"]}], - ["Checking out cubes"], - ), - ( - [ - { - "field": "departments", - "value": ["Computer Science", "Material Science"], - } - ], - [ - "Iphone 15 durability test", - "Checking out cubes", - "Automated Cooling System", - ], + "credits=1", + ["Iphone 15 durability test", "Checking out cubes"], ), ( - [{"field": "majors", "value": ["BIOL"]}], + "credits=2,4", [ "Iphone 15 durability test", "Checking out cubes", "Automated Cooling System", + "Test the water", ], ), + ("years=2025", ["Iphone 15 durability test", "Checking out cubes"]), ( - [{"field": "majors", "value": ["CSCI", "BIOL"]}], + "years=2025,2027", [ "Iphone 15 durability test", "Checking out cubes", "Automated Cooling System", ], ), + ("location=Remote", ["Automated Cooling System"]), ( - [{"field": "credits", "value": [1]}], - ["Iphone 15 durability test", "Checking out cubes"], - ), - ( - [{"field": "credits", "value": [2, 4]}], + "location=In-Person", [ "Iphone 15 durability test", "Checking out cubes", - "Automated Cooling System", "Test the water", + "Data Science Research", ], ), - ([{"field": "class_year", "value": [2025]}], ["Iphone 15 durability test"]), ( - [{"field": "class_year", "value": [2025, 2027]}], + "location=In-Person&departments=CSCI", ["Iphone 15 durability test", "Automated Cooling System"], ), - ([{"field": "location", "value": "Remote"}], ["Automated Cooling System"]), ( - [{"field": "location", "value": "In-Person"}], - ["Iphone 15 durability test", "Checking out cubes", "Test the water"], - ), - ( - [ - {"field": "location", "value": "In-Person"}, - {"field": "departments", "value": ["Computer Science"]}, - ], - ["Iphone 15 durability test"], + "credits=2,4&departments=CSCI", + ["Iphone 15 durability test", "Automated Cooling System"], ), + ("departments=MTLE", ["Test the water"]), ( + "departments=CSCI,MTLE", [ - {"field": "credits", "value": [2, 4]}, - {"field": "departments", "value": ["Computer Science"]}, + "Automated Cooling System", + "Iphone 15 durability test", + "Test the water", ], - ["Iphone 15 durability test", "Automated Cooling System"], ), ], ) def test_opportunity_filter( - test_client: FlaskClient, filters, expected_opportunities + test_client: FlaskClient, + auth_headers, + query_string, + expected_opportunities, ) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/opportunity/filter' page is requested (GET) - THEN check that the response is valid - """ - json_data = {"filters": filters} - response = test_client.get("/opportunity/filter", json=json_data) - + response = test_client.get( + f"/opportunity/filter?{query_string}", + headers=auth_headers("test@rpi.edu"), + ) assert response.status_code == 200 - json_data = json.loads(response.data) + names = {item["name"] for item in json.loads(response.data)} + for expected in expected_opportunities: + assert expected in names + + +@requires_postgres +@pytest.mark.parametrize( + "query_string, expected_status", + [ + ("years=not-a-year", 400), + ("credits=9", 400), + ("unknown=value", 400), + ], +) +def test_opportunity_filter_invalid_params( + test_client: FlaskClient, auth_headers, query_string, expected_status +) -> None: + response = test_client.get( + f"/opportunity/filter?{query_string}", + headers=auth_headers("test@rpi.edu"), + ) + assert response.status_code == expected_status + - for data in json_data: - assert data["name"] in expected_opportunities +def test_opportunity_filter_requires_auth(test_client: FlaskClient) -> None: + response = test_client.get("/opportunity/filter") + assert response.status_code == 401 diff --git a/tests/test_opportunity.py b/tests/test_opportunity.py index dcc567b4..d038a9cb 100644 --- a/tests/test_opportunity.py +++ b/tests/test_opportunity.py @@ -3,14 +3,12 @@ import pytest from flask.testing import FlaskClient +from tests.conftest import requires_postgres -def test_get_opportunity_parametrized(test_client: FlaskClient): - """ - GIVEN a Flask application configured for testing - WHEN the '/opportunity' page is requested (GET) with different IDs - THEN check that the responses are valid - """ - test_cases = [ + +@pytest.mark.parametrize( + "opportunity_id, expected_data", + [ ( 1, { @@ -23,7 +21,7 @@ def test_get_opportunity_parametrized(test_client: FlaskClient): "three_credits": False, "four_credits": True, "semester": "Spring", - "year": 2024, + "year": 2025, "active": True, }, ), @@ -32,126 +30,136 @@ def test_get_opportunity_parametrized(test_client: FlaskClient): { "name": "Iphone 15 durability test", "description": "Scratching the Iphone, drop testing etc.", - "recommended_experience": "Experienced in getting angry and throwing" - " temper tantrum", + "recommended_experience": ( + "Experienced in getting angry and throwing temper tantrum" + ), "pay": None, "one_credit": True, "two_credits": True, "three_credits": True, "four_credits": True, "semester": "Spring", - "year": 2024, + "year": 2025, + "active": True, + }, + ), + ( + 3, + { + "name": "Checking out cubes", + "semester": "Fall", + "year": 2025, + "active": True, + }, + ), + ( + 4, + { + "name": "Test the water", + "semester": "Summer", + "year": 2025, "active": True, }, ), - ] + ], +) +def test_get_opportunity_parametrized( + test_client: FlaskClient, opportunity_id, expected_data +): + response = test_client.get("/opportunity", json={"id": opportunity_id}) + assert response.status_code == 200 - for opportunity_id, expected_data in test_cases: - response = test_client.get("/opportunity", json={"id": opportunity_id}) - assert response.status_code == 200 + json_data = json.loads(response.data) + for key, value in expected_data.items(): + assert json_data[key] == value - json_data = json.loads(response.data) - for key, value in expected_data.items(): - assert json_data[key] == value +@pytest.mark.parametrize( + "path_id, expected_name", + [ + (1, "Automated Cooling System"), + (2, "Iphone 15 durability test"), + (3, "Checking out cubes"), + (4, "Test the water"), + ], +) +def test_get_opportunity_by_path( + test_client: FlaskClient, path_id: int, expected_name: str +): + response = test_client.get(f"/opportunity/{path_id}") + assert response.status_code == 200 + data = json.loads(response.data) + assert data["id"] == path_id + assert data["name"] == expected_name -def test_get_opportunity_no_json(test_client: FlaskClient): - """ - GIVEN a Flask application configured for testing - WHEN the '/opportunity' page is requested (GET) without JSON payload - THEN check that the response is 400 - """ - response = test_client.get("/opportunity") - assert response.status_code == 400 +@pytest.mark.parametrize( + "request_kwargs, expected_status", + [ + ({}, (400, 415)), + ({"json": {"wrong": "wrong"}}, (400,)), + ({"json": {"id": "not-an-int"}}, (400,)), + ], +) +def test_get_opportunity_errors( + test_client: FlaskClient, request_kwargs, expected_status +): + response = test_client.get("/opportunity", **request_kwargs) + if isinstance(expected_status, tuple): + assert response.status_code in expected_status + else: + assert response.status_code == expected_status -def test_opportunity_incorrect_json(test_client: FlaskClient): - """ - GIVEN a Flask application configured for testing - WHEN the '/opportunity' page is requested (GET) with incorrect JSON - THEN check that the response is 400 - """ - response = test_client.get("/opportunity", json={"wrong": "wrong"}) - assert response.status_code == 400 + +@pytest.mark.parametrize( + "missing_id", + [999999], +) +def test_get_opportunity_not_found(test_client: FlaskClient, missing_id: int): + response = test_client.get("/opportunity", json={"id": missing_id}) + assert response.status_code == 404 +@requires_postgres @pytest.mark.parametrize( - "endpoint, expected_keys", + "opp_id, expected_name, expected_department", [ - ( - "/getOpportunityMeta/1", - [ - "name", - "description", - "recommended_experience", - "pay", - "credits", - "semester", - "year", - "application_due", - "active", - "courses", - "majors", - "years", - ], - ), - ( - "/getOpportunity/2", - [ - "id", - "name", - "description", - "recommended_experience", - "author", - "department", - "aboutSection", - ], - ), + (1, "Automated Cooling System", "CSCI"), + (2, "Iphone 15 durability test", "CSCI"), ], ) -def test_opportunity_meta_parametrized( - test_client: FlaskClient, endpoint, expected_keys +def test_get_opportunity_detail( + test_client: FlaskClient, opp_id: int, expected_name: str, expected_department: str ): - """ - GIVEN a Flask application configured for testing - WHEN specific opportunity endpoints are requested - THEN check that the response contains the expected keys - """ - response = test_client.get(endpoint, content_type="application/json") + response = test_client.get(f"/getOpportunity/{opp_id}") assert response.status_code == 200 - data = json.loads(response.data) - if "data" in data: - data = data["data"] - - for key in expected_keys: - if isinstance(data, list): - for item in data: - assert key in item - else: - assert key in data + opp = json.loads(response.data)["data"] + assert opp["name"] == expected_name + assert opp["department"] == expected_department + assert "authors" in opp + assert "recommended_class_years" in opp @pytest.mark.parametrize( - "endpoint", + "endpoint, expected_min_cards", [ - "/getOpportunityByProfessor/led", - "/getProfessorOpportunityCards/led", - "/getProfileOpportunities/led", + ("/staff/opportunities/led", 2), + ("/profile/opportunities/cenzar", 2), + ("/profile/opportunities/test", 2), ], ) -def test_professor_related_opportunities(test_client: FlaskClient, endpoint): - """ - GIVEN a Flask application configured for testing - WHEN professor-related endpoints are requested - THEN check that the response contains expected keys in each card - """ - response = test_client.get(endpoint, content_type="application/json") +def test_opportunity_card_routes( + test_client: FlaskClient, endpoint, expected_min_cards, auth_headers +): + headers = auth_headers("test@rpi.edu") + response = test_client.get(endpoint, headers=headers) assert response.status_code == 200 - data = json.loads(response.data)["data"] - for each_card in data: - assert "id" in each_card - assert "title" in each_card or "name" in each_card - assert "body" in each_card or "description" in each_card - assert "attributes" in each_card or "recommended_experience" in each_card + cards = json.loads(response.data) + assert len(cards) >= expected_min_cards + for card in cards: + assert "id" in card + assert "title" in card + assert "due" in card + assert "credits" in card diff --git a/tests/test_profile_routes.py b/tests/test_profile_routes.py index 644aaee9..7d092a8c 100644 --- a/tests/test_profile_routes.py +++ b/tests/test_profile_routes.py @@ -1,64 +1,55 @@ import json +import pytest 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 === - +@pytest.fixture(autouse=True) +def restore_test_user(test_client): + """Reset test@rpi.edu after profile write tests so other modules stay deterministic.""" + yield + with test_client.application.app_context(): + user = db.session.get(User, "test") + if user is None: + return + user.first_name = "RCOS" + user.last_name = "RCOS" + user.preferred_name = None + user.class_year = None + user.website = None + user.description = "first test" + db.session.execute( + db.delete(UserDepartments).where(UserDepartments.user_id == user.id) + ) + db.session.execute( + db.delete(UserMajors).where(UserMajors.user_id == user.id) + ) + db.session.add(UserDepartments(user_id=user.id, department_id="CSCI")) + db.session.add(UserMajors(user_id=user.id, major_code="CSCI")) + db.session.commit() -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") +def test_get_profile_success(test_client: FlaskClient, login_as_test_user): + response = test_client.get("/profile", headers=login_as_test_user) 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 data["first_name"] == "RCOS" + assert data["last_name"] == "RCOS" 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) - +def test_update_profile_success(test_client: FlaskClient, login_as_test_user): update_data = { "first_name": "UpdatedFirst", "last_name": "UpdatedLast", @@ -66,15 +57,16 @@ def test_update_profile_success(test_client: FlaskClient): "class_year": 2025, "website": "https://new.example.com", "description": "This is an updated description.", - "departments": ["CS"], + "departments": ["CSCI"], "majors": ["CSCI", "MATH"], } - response = test_client.put("/profile", json=update_data) + response = test_client.put( + "/profile", headers=login_as_test_user, 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() @@ -91,7 +83,7 @@ def test_update_profile_success(test_client: FlaskClient): .scalars() .all() ) - assert set(user_depts) == {"CS"} + assert set(user_depts) == {"CSCI"} user_majors = ( db.session.execute( @@ -103,19 +95,15 @@ def test_update_profile_success(test_client: FlaskClient): 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) - +def test_update_profile_partial(test_client: FlaskClient, login_as_test_user): update_data = { "website": "https://partial.update.com", "description": "Only this was updated.", } - response = test_client.put("/profile", json=update_data) + response = test_client.put( + "/profile", headers=login_as_test_user, json=update_data + ) assert response.status_code == 200 user = db.session.execute( @@ -123,15 +111,8 @@ def test_update_profile_partial(test_client: FlaskClient): ).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) + response = test_client.put("/profile", json={"first_name": "ShouldFail"}) assert response.status_code == 401 diff --git a/tests/test_single_opportunity_routes.py b/tests/test_single_opportunity_routes.py index 977e0ef4..598beaa5 100644 --- a/tests/test_single_opportunity_routes.py +++ b/tests/test_single_opportunity_routes.py @@ -3,6 +3,7 @@ def test_opportunity_to_dict_none(): from importlib import import_module + opp_mod = import_module("labconnect.main.opportunity_routes") assert opp_mod.opportunity_to_dict(None) == {} @@ -10,7 +11,7 @@ def test_opportunity_to_dict_none(): def test_opportunity_to_dict_populated(): # create a lightweight Opportunities instance (no DB persistence needed) from labconnect.models import Opportunities - + opp = Opportunities() opp.id = 123 opp.name = "Unit Test Opportunity" @@ -26,6 +27,7 @@ def test_opportunity_to_dict_populated(): opp.active = True from importlib import import_module + opp_mod = import_module("labconnect.main.opportunity_routes") out = opp_mod.opportunity_to_dict(opp) @@ -49,7 +51,7 @@ def test_get_single_opportunity_success_and_json_variant(test_client): # create and persist an opportunity to the test database from labconnect import db from labconnect.models import Opportunities - + opp = Opportunities() opp.name = "Endpoint Test Opportunity" opp.description = "Endpoint description" @@ -61,10 +63,15 @@ def test_get_single_opportunity_success_and_json_variant(test_client): opp.four_credits = False opp.semester = None opp.year = 2025 - opp.application_due = None + from datetime import date, datetime + + from labconnect.helpers import LocationEnum, SemesterEnum + + opp.application_due = date.today() opp.active = True - opp.last_updated = None - opp.location = None + opp.last_updated = datetime.now() + opp.location = LocationEnum.TBD + opp.semester = SemesterEnum.FALL db.session.add(opp) db.session.commit() diff --git a/tests/test_user.py b/tests/test_user.py index f5f68fe6..ea1518d6 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -1,159 +1,91 @@ """ -Test user routes +Test user/profile routes (replaces legacy /user JSON endpoints). """ +import json + import pytest -from flask import json from flask.testing import FlaskClient @pytest.mark.parametrize( - "input_data, expected_status, expected_output", + "email, expected_profile", [ ( - {"id": "1"}, - 200, + "cenzar@rpi.edu", { - "id": 1, + "id": "cenzar", "first_name": "Rafael", "preferred_name": "Raf", "last_name": "Cenzano", "email": "cenzar@rpi.edu", - "description": "labconnect is the best RCOS project", - "profile_picture": "https://rafael.sirv.com/Images/rafael.jpeg?thumbnail=350&format=webp&q=90", - "website": "https://rafaelcenzano.com", - "class_year": "2025", - "lab_manager_id": 1, - "departments": [ - {"user_id": 1, "department_id": "Computer Science"}, - {"user_id": 1, "department_id": "Math"}, - ], - "majors": [ - {"user_id": 1, "major_code": "CSCI"}, - {"user_id": 1, "major_code": "MATH"}, - ], - "courses": [ - {"in_progress": False, "user_id": 1, "course_code": "CSCI2300"}, - {"in_progress": True, "user_id": 1, "course_code": "CSCI4430"}, - ], + "class_year": 2025, + "majors": ["CSCI", "MATH"], + "departments": ["CSCI", "MATH"], }, ), ( - {"id": "2"}, - 200, + "test@rpi.edu", { - "id": 2, + "id": "test", "first_name": "RCOS", "preferred_name": None, "last_name": "RCOS", "email": "test@rpi.edu", - "description": None, - "profile_picture": "https://www.svgrepo.com/show/206842/professor.svg", - "website": None, - "class_year": None, - "lab_manager_id": None, - "departments": [{"user_id": 2, "department_id": "Computer Science"}], - "majors": [{"user_id": 2, "major_code": "CSCI"}], - "courses": [ - {"in_progress": False, "user_id": 2, "course_code": "CSCI2300"} - ], + "majors": ["CSCI"], + "departments": ["CSCI"], }, ), ], ) -def test_user_route( - test_client: FlaskClient, input_data, expected_status, expected_output +def test_profile_route( + test_client: FlaskClient, auth_headers, email, expected_profile ) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/user' page is requested (GET) with input data - THEN check that the response is valid and matches expected output - """ - response = test_client.get("/user", json=input_data) - assert response.status_code == expected_status - json_data = json.loads(response.data) - assert json_data == expected_output + response = test_client.get("/profile", headers=auth_headers(email)) + assert response.status_code == 200 + + data = json.loads(response.data) + for key, value in expected_profile.items(): + assert data[key] == value @pytest.mark.parametrize( - "input_data, expected_opportunities", + "rcs_id, expected_titles", [ ( - {"id": 1}, - [ - { - "name": "Automated Cooling System", - "description": "Energy efficient AC system", - "recommended_experience": "Thermodynamics", - "pay": 15.0, - "semester": "Spring", - "year": 2024, - "active": True, - }, - { - "name": "Iphone 15 durability test", - "description": "Scratching the Iphone, drop testing etc.", - "recommended_experience": "Experienced in getting angry and " - "throwing temper tantrum", - "pay": None, - "semester": "Spring", - "year": 2024, - "active": True, - }, - ], + "cenzar", + ["Automated Cooling System", "Iphone 15 durability test"], ), ( - {"id": 2}, - [ - { - "name": "Checking out cubes", - "description": "Material Sciences", - "recommended_experience": "Experienced in materials.", - "pay": None, - "semester": "Fall", - "year": 2024, - "active": True, - }, - { - "name": "Test the water", - "description": "Testing the quality of water in Troy pipes", - "recommended_experience": "Understanding of lead poisioning", - "pay": None, - "semester": "Summer", - "year": 2024, - "active": True, - }, - ], + "test", + ["Checking out cubes", "Test the water"], ), ], ) -def test_user_opportunity_cards( - test_client: FlaskClient, input_data, expected_opportunities +def test_profile_opportunity_cards( + test_client: FlaskClient, + auth_headers, + rcs_id: str, + expected_titles: list[str], ) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/user' page is requested (GET) with input data - THEN check that the opportunity cards in the response are valid - """ - response = test_client.get("/user", json=input_data) + response = test_client.get( + f"/profile/opportunities/{rcs_id}", + headers=auth_headers("test@rpi.edu"), + ) assert response.status_code == 200 - json_data = json.loads(response.data) - for i, item in enumerate(json_data["opportunities"]): - assert item == expected_opportunities[i] + titles = {card["title"] for card in json.loads(response.data)} + assert titles == set(expected_titles) @pytest.mark.parametrize( - "input_data, expected_status", - [(None, 400), ({"wrong": "wrong"}, 400), ({"id": "not found"}, 404)], + "endpoint, expected_status", + [ + ("/profile", 401), + ], ) -def test_user_route_edge_cases( - test_client: FlaskClient, input_data, expected_status +def test_profile_route_edge_cases( + test_client: FlaskClient, endpoint, expected_status ) -> None: - """ - GIVEN a Flask application configured for testing - WHEN the '/user' page is requested (GET) with various edge case inputs - THEN check that the response status code is as expected - """ - response = test_client.get("/user", json=input_data) + response = test_client.get(endpoint) assert response.status_code == expected_status From 9e821866016d84273963210bbf3e233a2b0d6876 Mon Sep 17 00:00:00 2001 From: Rafael Cenzano <32753063+RafaelCenzano@users.noreply.github.com> Date: Fri, 22 May 2026 20:48:44 -0400 Subject: [PATCH 09/14] chore: cleanup routes --- labconnect/main/auth_routes.py | 35 +++++++++++++++------------- labconnect/main/profile_routes.py | 38 ++++++++++++++++++------------- labconnect/main/routes.py | 11 ++++++--- 3 files changed, 49 insertions(+), 35 deletions(-) diff --git a/labconnect/main/auth_routes.py b/labconnect/main/auth_routes.py index 84d98876..ff702bc8 100644 --- a/labconnect/main/auth_routes.py +++ b/labconnect/main/auth_routes.py @@ -194,6 +194,7 @@ 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") @@ -204,35 +205,37 @@ def promoteUser(email: str) -> Response: 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() + promoter_email = get_jwt_identity() + promoter = db.session.query(User).filter_by(email=promoter_email).first() + if not promoter: + return make_response({"msg": "Missing permissions"}, 401) + + 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 + management_permissions = ( + db.session.query(ManagementPermissions).filter_by(user_id=manager.id).first() + ) if management_permissions is None: management_permissions = ManagementPermissions(user_id=manager.id, admin=True) db.session.add(management_permissions) - + elif management_permissions.admin: + management_permissions.admin = False + else: + management_permissions.admin = True + db.session.commit() - + return make_response({"msg": "User Lab Manager permissions changed!"}, 200) - @main_blueprint.get("/metadata/") diff --git a/labconnect/main/profile_routes.py b/labconnect/main/profile_routes.py index f9d9f1eb..aea72c46 100644 --- a/labconnect/main/profile_routes.py +++ b/labconnect/main/profile_routes.py @@ -85,31 +85,37 @@ def update_profile() -> Response: db.session.execute( db.delete(UserDepartments).where(UserDepartments.user_id == user.id) ) - + req_dept_ids = set(json_data["departments"]) - if req_dept_ids: # Only query if list is not empty - valid_dept_ids = db.session.execute( - db.select(RPIDepartments.id).where( - RPIDepartments.id.in_(req_dept_ids) + if req_dept_ids: # Only query if list is not empty + valid_dept_ids = ( + db.session.execute( + db.select(RPIDepartments.id).where( + RPIDepartments.id.in_(req_dept_ids) + ) ) - ).scalars().all() + .scalars() + .all() + ) - for dept_id in valid_dept_ids: # Add only the valid ones + for dept_id in valid_dept_ids: # Add only the valid ones 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.execute( - db.delete(UserMajors).where(UserMajors.user_id == user.id) - ) - + db.session.execute(db.delete(UserMajors).where(UserMajors.user_id == user.id)) + req_major_codes = set(json_data["majors"]) - if req_major_codes: # Only query if list is not empty - valid_major_codes = db.session.execute( - db.select(Majors.code).where(Majors.code.in_(req_major_codes)) - ).scalars().all() + if req_major_codes: # Only query if list is not empty + valid_major_codes = ( + db.session.execute( + db.select(Majors.code).where(Majors.code.in_(req_major_codes)) + ) + .scalars() + .all() + ) - for major_code in valid_major_codes: # Add only the valid ones + for major_code in valid_major_codes: # Add only the valid ones new_user_major = UserMajors(user_id=user.id, major_code=major_code) db.session.add(new_user_major) diff --git a/labconnect/main/routes.py b/labconnect/main/routes.py index df9454c3..35232039 100644 --- a/labconnect/main/routes.py +++ b/labconnect/main/routes.py @@ -1,7 +1,7 @@ from typing import NoReturn from flask import abort, request -from flask_jwt_extended import get_jwt_identity, jwt_required +from flask_jwt_extended import jwt_required from labconnect import db from labconnect.models import ( @@ -27,8 +27,13 @@ def index() -> dict[str, str]: @main_blueprint.get("/departments") def departmentCards(): data = db.session.execute( - db.select(RPIDepartments.name, RPIDepartments.school_id, RPIDepartments.id, - RPIDepartments.description, RPIDepartments.website) + db.select( + RPIDepartments.name, + RPIDepartments.school_id, + RPIDepartments.id, + RPIDepartments.description, + RPIDepartments.website, + ) ).all() results = [ { From 4483839354d7ab8875efc94d9871f2806366466d Mon Sep 17 00:00:00 2001 From: Rafael Cenzano <32753063+RafaelCenzano@users.noreply.github.com> Date: Fri, 22 May 2026 20:52:28 -0400 Subject: [PATCH 10/14] chore: fix linting --- db_init.py | 3 --- labconnect/main/routes.py | 2 +- tests/conftest.py | 3 +-- tests/test_profile_routes.py | 15 +++++---------- 4 files changed, 7 insertions(+), 16 deletions(-) diff --git a/db_init.py b/db_init.py index 80664418..26314801 100644 --- a/db_init.py +++ b/db_init.py @@ -8,17 +8,14 @@ import re import sys -from datetime import date, datetime import requests from labconnect import create_app, db -from labconnect.helpers import LocationEnum, SemesterEnum from labconnect.models import ( ClassYears, Codes, Courses, - LabManager, Leads, Majors, Opportunities, diff --git a/labconnect/main/routes.py b/labconnect/main/routes.py index 35232039..a9e73950 100644 --- a/labconnect/main/routes.py +++ b/labconnect/main/routes.py @@ -1,7 +1,7 @@ from typing import NoReturn from flask import abort, request -from flask_jwt_extended import jwt_required +from flask_jwt_extended import get_jwt_identity, jwt_required from labconnect import db from labconnect.models import ( diff --git a/tests/conftest.py b/tests/conftest.py index 46884793..4cfa8104 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,8 +8,7 @@ os.environ.setdefault("DB", "sqlite:///:memory:") from labconnect import create_app, db -from labconnect.models import Opportunities, User, update_search_vector - +from labconnect.models import Opportunities, update_search_vector from tests.seed import seed_development_data diff --git a/tests/test_profile_routes.py b/tests/test_profile_routes.py index 7d092a8c..3e2f4c81 100644 --- a/tests/test_profile_routes.py +++ b/tests/test_profile_routes.py @@ -9,7 +9,8 @@ @pytest.fixture(autouse=True) def restore_test_user(test_client): - """Reset test@rpi.edu after profile write tests so other modules stay deterministic.""" + """Reset test@rpi.edu after profile write tests so + other modules stay deterministic.""" yield with test_client.application.app_context(): user = db.session.get(User, "test") @@ -24,9 +25,7 @@ def restore_test_user(test_client): db.session.execute( db.delete(UserDepartments).where(UserDepartments.user_id == user.id) ) - db.session.execute( - db.delete(UserMajors).where(UserMajors.user_id == user.id) - ) + db.session.execute(db.delete(UserMajors).where(UserMajors.user_id == user.id)) db.session.add(UserDepartments(user_id=user.id, department_id="CSCI")) db.session.add(UserMajors(user_id=user.id, major_code="CSCI")) db.session.commit() @@ -61,9 +60,7 @@ def test_update_profile_success(test_client: FlaskClient, login_as_test_user): "majors": ["CSCI", "MATH"], } - response = test_client.put( - "/profile", headers=login_as_test_user, json=update_data - ) + response = test_client.put("/profile", headers=login_as_test_user, json=update_data) assert response.status_code == 200 assert "Profile updated successfully" in json.loads(response.data)["msg"] @@ -101,9 +98,7 @@ def test_update_profile_partial(test_client: FlaskClient, login_as_test_user): "description": "Only this was updated.", } - response = test_client.put( - "/profile", headers=login_as_test_user, json=update_data - ) + response = test_client.put("/profile", headers=login_as_test_user, json=update_data) assert response.status_code == 200 user = db.session.execute( From ed5b61d295e9f8e91b2f533a78b03770e955f4de Mon Sep 17 00:00:00 2001 From: Rafael Cenzano <32753063+RafaelCenzano@users.noreply.github.com> Date: Fri, 22 May 2026 21:20:54 -0400 Subject: [PATCH 11/14] chore: remove duplicate route --- labconnect/main/routes.py | 45 +-------------------------------------- 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/labconnect/main/routes.py b/labconnect/main/routes.py index a9e73950..b7201b13 100644 --- a/labconnect/main/routes.py +++ b/labconnect/main/routes.py @@ -1,7 +1,7 @@ from typing import NoReturn from flask import abort, request -from flask_jwt_extended import get_jwt_identity, jwt_required +from flask_jwt_extended import jwt_required from labconnect import db from labconnect.models import ( @@ -12,7 +12,6 @@ Opportunities, RPIDepartments, User, - UserDepartments, ) from labconnect.serializers import serialize_course @@ -98,48 +97,6 @@ def departmentDetails(department: str): return result -@main_blueprint.get("/profile") -@jwt_required() -def profile(): - user_id = get_jwt_identity() - - data = db.session.execute( - db.select( - User.preferred_name, - User.first_name, - User.last_name, - User.profile_picture, - RPIDepartments.name, - User.description, - User.website, - User.lab_manager_id, - User.id, - User.pronouns, - ) - .where(User.email == user_id) - .join(UserDepartments, UserDepartments.user_id == User.id) - .join(RPIDepartments, UserDepartments.department_id == RPIDepartments.id) - ).first() - - if not data: - return {"error": "profile not found"}, 404 - - # if data[7]: - # return {"lab_manager": True, "id": data[7]} - - result = { - "id": data[8], - "name": data[0] + " " + data[2] if data[0] else data[1] + " " + data[2], - "image": data[3], - "department": data[4], - "description": data[5], - "website": data[6], - "pronouns": data[9], - } - - return result - - @main_blueprint.get("/staff/") @jwt_required() def getProfessorProfile(id: str): From 368955961553184b74ca6a2f4f8d6e4c3e30392b Mon Sep 17 00:00:00 2001 From: Rafael Cenzano <32753063+RafaelCenzano@users.noreply.github.com> Date: Sat, 23 May 2026 00:04:09 -0400 Subject: [PATCH 12/14] fix: db queries --- labconnect/main/opportunity_routes.py | 7 +------ labconnect/main/profile_routes.py | 10 ++++++---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/labconnect/main/opportunity_routes.py b/labconnect/main/opportunity_routes.py index 6a4750ac..ba1a60c3 100644 --- a/labconnect/main/opportunity_routes.py +++ b/labconnect/main/opportunity_routes.py @@ -330,15 +330,10 @@ def filterOpportunities(): where_conditions.append(RecommendsMajors.major_code.in_(value)) # Departments filter - # not currently in use elif field == "departments": if not isinstance(value, list): abort(400) - query = ( - query.join(Leads, Opportunities.id == Leads.opportunity_id) - .join(LabManager, Leads.lab_manager_id == LabManager.id) - .where(LabManager.department_id.in_(value)) - ) + where_conditions.append(LabManager.department_id.in_(value)) # Pay filter elif field == "hourlypay": diff --git a/labconnect/main/profile_routes.py b/labconnect/main/profile_routes.py index aea72c46..3d652a7e 100644 --- a/labconnect/main/profile_routes.py +++ b/labconnect/main/profile_routes.py @@ -11,9 +11,9 @@ 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 - ) + db.select(UserDepartments.department_id) + .where(UserDepartments.user_id == user.id) + .order_by(UserDepartments.department_id) ) .scalars() .all() @@ -21,7 +21,9 @@ def user_to_dict(user: User) -> dict: user_majors = ( db.session.execute( - db.select(UserMajors.major_code).where(UserMajors.user_id == user.id) + db.select(UserMajors.major_code) + .where(UserMajors.user_id == user.id) + .order_by(UserMajors.major_code) ) .scalars() .all() From 4cec38a56a15211274af0fc665836754a856ad32 Mon Sep 17 00:00:00 2001 From: Rafael Cenzano <32753063+RafaelCenzano@users.noreply.github.com> Date: Sat, 23 May 2026 00:44:51 -0400 Subject: [PATCH 13/14] fix: test and seed data not matching --- tests/seed.py | 2 +- tests/test_opportunities_filtering.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/seed.py b/tests/seed.py index 0f50acb7..f4096dff 100644 --- a/tests/seed.py +++ b/tests/seed.py @@ -306,7 +306,7 @@ def seed_development_data() -> None: (1, 1), (2, 2), (1, 3), - (4, 4), + (6, 4), (8, 5), ) diff --git a/tests/test_opportunities_filtering.py b/tests/test_opportunities_filtering.py index b6936c29..3f67106a 100644 --- a/tests/test_opportunities_filtering.py +++ b/tests/test_opportunities_filtering.py @@ -54,7 +54,11 @@ ), ( "location=In-Person&departments=CSCI", - ["Iphone 15 durability test", "Automated Cooling System"], + [ + "Iphone 15 durability test", + "Checking out cubes", + "Data Science Research", + ], ), ( "credits=2,4&departments=CSCI", @@ -66,7 +70,9 @@ [ "Automated Cooling System", "Iphone 15 durability test", + "Checking out cubes", "Test the water", + "Data Science Research", ], ), ], From d98b6e0bd7e1eb422cd3fa9fc98123ebcea72918 Mon Sep 17 00:00:00 2001 From: Rafael Cenzano <32753063+RafaelCenzano@users.noreply.github.com> Date: Sat, 23 May 2026 00:55:48 -0400 Subject: [PATCH 14/14] deepsource: ignore tests --- .deepsource.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.deepsource.toml b/.deepsource.toml index 0ac63d33..1fd8e15c 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -1,7 +1,9 @@ version = 1 +exclude_patterns = ["tests/**", "db_init.py"] + [[analyzers]] name = "python" [analyzers.meta] - runtime_version = "3.x.x" \ No newline at end of file + runtime_version = "3.x.x"