From 6f8bcf927adf5113c08817e20a58f7ccadeaf524 Mon Sep 17 00:00:00 2001 From: Ritwik Gupta Date: Tue, 28 Jul 2026 22:06:25 -0700 Subject: [PATCH 1/2] Add live provider contract tests --- .github/workflows/live-tests.yml | 30 ++++++++++ live_tests/test_academic_providers.py | 44 ++++++++++++++ live_tests/test_campus_providers.py | 57 +++++++++++++++++++ live_tests/test_content_providers.py | 36 ++++++++++++ .../test_status_and_sports_providers.py | 23 ++++++++ pyproject.toml | 5 ++ 6 files changed, 195 insertions(+) create mode 100644 .github/workflows/live-tests.yml create mode 100644 live_tests/test_academic_providers.py create mode 100644 live_tests/test_campus_providers.py create mode 100644 live_tests/test_content_providers.py create mode 100644 live_tests/test_status_and_sports_providers.py diff --git a/.github/workflows/live-tests.yml b/.github/workflows/live-tests.yml new file mode 100644 index 0000000..d011641 --- /dev/null +++ b/.github/workflows/live-tests.yml @@ -0,0 +1,30 @@ +name: Run live provider tests + +on: + pull_request: + branches: [ "dev" ] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: live-tests-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + run-live-tests: + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - uses: actions/checkout@v4 + - name: Install uv and Python + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + python-version: "3.13" + enable-cache: true + - name: Install dependencies + run: uv sync --locked + - name: Test live provider contracts + run: uv run pytest -m live live_tests/ diff --git a/live_tests/test_academic_providers.py b/live_tests/test_academic_providers.py new file mode 100644 index 0000000..32e5ed1 --- /dev/null +++ b/live_tests/test_academic_providers.py @@ -0,0 +1,44 @@ +import pytest + +from pittapi.cal import CalendarClient, Event +from pittapi.course import CourseClient +from pittapi.textbook import TextbookClient + +pytestmark = pytest.mark.live + + +@pytest.mark.parametrize( + "method_name", + [ + "get_academic_calendar", + "get_grades_calendar", + "get_enrollment_calendar", + "get_course_calendar", + "get_graduation_calendar", + ], +) +def test_calendar_feed(method_name): + with CalendarClient() as calendar: + events = getattr(calendar, method_name)() + + assert isinstance(events, tuple) + assert all(isinstance(event, Event) for event in events) + + +def test_course_catalog(): + with CourseClient() as courses: + subject = courses.get_subject_courses("CS") + + assert subject.subject_code == "CS" + assert subject.courses + assert all(course.course_id for course in subject.courses) + + +def test_textbook_terms_and_subjects(): + with TextbookClient() as textbooks: + terms = textbooks.get_terms() + term = next(term for term in terms if term.inquiry_enabled) + textbooks.select_term(term) + textbooks.initialize_subjects() + + assert textbooks.subject_ids diff --git a/live_tests/test_campus_providers.py b/live_tests/test_campus_providers.py new file mode 100644 index 0000000..58e13ed --- /dev/null +++ b/live_tests/test_campus_providers.py @@ -0,0 +1,57 @@ +import pytest + +from pittapi.dining import DiningClient +from pittapi.gym import GymClient +from pittapi.lab import LabClient +from pittapi.laundry import LaundryClient +from pittapi.shuttle import ShuttleClient + +pytestmark = pytest.mark.live + + +def test_dining_locations(): + with DiningClient() as dining: + locations = dining.get_locations() + + assert locations + assert all(location.id and location.name for location in locations) + + +def test_gym_occupancy(): + with GymClient() as gyms: + occupancy = gyms.get_all_gyms_info() + + assert occupancy + assert all(gym.location_id and gym.facility_id for gym in occupancy) + assert all(gym.current_count >= 0 and gym.total_capacity >= 0 for gym in occupancy) + + +def test_lab_discovery_and_status(): + with LabClient() as labs: + locations = labs.get_locations() + status = labs.get_status(locations[0]) + + counted_computers = ( + status.available_computers + status.off_computers + status.in_use_computers + status.out_of_service_computers + ) + assert locations + assert status.total_computers == counted_computers + + +def test_laundry_discovery_and_machines(): + with LaundryClient() as laundry: + locations = laundry.get_locations() + machines = laundry.get_machine_statuses(locations[0]) + + assert locations + assert isinstance(machines, tuple) + + +def test_shuttle_configuration_and_routes(): + with ShuttleClient() as shuttle: + configuration = shuttle.get_configuration() + routes = shuttle.get_routes(configuration) + + assert configuration.api_key + assert routes + assert all(route.route_id for route in routes) diff --git a/live_tests/test_content_providers.py b/live_tests/test_content_providers.py new file mode 100644 index 0000000..f3c782d --- /dev/null +++ b/live_tests/test_content_providers.py @@ -0,0 +1,36 @@ +import pytest + +from pittapi.library import LibraryClient +from pittapi.news import NewsClient +from pittapi.people import PeopleClient + +pytestmark = pytest.mark.live + + +def test_library_catalog_and_hillman_reservations(): + with LibraryClient() as library: + documents = library.get_documents("computer science") + reservation_total = library.hillman_total_reserved() + + assert documents.num_results > 0 + assert documents.documents + assert reservation_total >= 0 + + +def test_pittwire_topics_and_articles(): + with NewsClient() as news: + topics = news.get_topics() + articles = news.get_articles_by_topic(topics[0], max_num_results=1) + + assert topics + assert articles + assert articles[0].url.startswith("https://www.pittwire.pitt.edu/") + + +def test_people_directory_service_account(): + with PeopleClient() as people: + matches = people.get_person("Technology Help Desk") + + assert matches + assert matches[0].name == "Help Desk, Technology" + assert any(field.name == "Email" for field in matches[0].fields) diff --git a/live_tests/test_status_and_sports_providers.py b/live_tests/test_status_and_sports_providers.py new file mode 100644 index 0000000..de0741d --- /dev/null +++ b/live_tests/test_status_and_sports_providers.py @@ -0,0 +1,23 @@ +import pytest + +from pittapi.sports import FOOTBALL_URL, MENS_BASKETBALL_URL, SportsClient +from pittapi.status import StatusClient + +pytestmark = pytest.mark.live + + +@pytest.mark.parametrize("url", [FOOTBALL_URL, MENS_BASKETBALL_URL]) +def test_espn_team_feed(url): + with SportsClient() as sports: + data = sports.get_team_data(url) + + assert data["team"]["id"] + assert data["team"]["displayName"] + + +def test_pitt_service_status(): + with StatusClient() as status: + summary = status.get_status() + + assert summary.components + assert all(component.name and component.status for component in summary.components) diff --git a/pyproject.toml b/pyproject.toml index 4c3fea7..50e6200 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,11 @@ dev = [ line-length = 127 target-version = ["py313"] +[tool.pytest.ini_options] +markers = [ + "live: makes requests to live provider services", +] + [tool.setuptools.packages.find] exclude = ["docs*", "tests*"] namespaces = false From 58440c7ba42b1b66e2cfef7fea371393f46e8963 Mon Sep 17 00:00:00 2001 From: Ritwik Gupta Date: Fri, 31 Jul 2026 02:26:58 -0700 Subject: [PATCH 2/2] xfail dining test because it doesn't like a GH runner calling it --- live_tests/test_campus_providers.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/live_tests/test_campus_providers.py b/live_tests/test_campus_providers.py index 58e13ed..f20b2f1 100644 --- a/live_tests/test_campus_providers.py +++ b/live_tests/test_campus_providers.py @@ -1,4 +1,5 @@ import pytest +import requests from pittapi.dining import DiningClient from pittapi.gym import GymClient @@ -10,8 +11,13 @@ def test_dining_locations(): - with DiningClient() as dining: - locations = dining.get_locations() + try: + with DiningClient() as dining: + locations = dining.get_locations() + except requests.HTTPError as error: + if error.response is not None and error.response.status_code == 403: + pytest.xfail("DineOnCampus blocks requests from some CI environments") + raise assert locations assert all(location.id and location.name for location in locations)