diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml new file mode 100644 index 0000000..6c29cfa --- /dev/null +++ b/.github/workflows/smokeshow.yml @@ -0,0 +1,47 @@ +name: Generate Coverage + +on: + workflow_run: + workflows: [Test] + types: [completed] + +permissions: + statuses: write + +jobs: + Context: + runs-on: ubuntu-latest + + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + + Smokeshow: + if: ${{ github.event.workflow_run.event == 'push' && github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - run: pip install smokeshow + + - uses: dawidd6/action-download-artifact@v2.28.0 + with: + workflow: tests.yml + commit: ${{ github.event.workflow_run.head_sha }} + + - run: smokeshow upload coverage + env: + SMOKESHOW_GITHUB_STATUS_DESCRIPTION: Coverage {coverage-percentage} + SMOKESHOW_GITHUB_COVERAGE_THRESHOLD: 85 + SMOKESHOW_GITHUB_CONTEXT: Coverage + SMOKESHOW_GITHUB_PR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + SMOKESHOW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..8125a42 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,46 @@ +# test workflow from https://github.com/tiangolo/fastapi/blob/master/.github/workflows/test.yml +name: Test + +on: + push: + branches: + - main + pull_request: + types: + - opened + - synchronize + +jobs: + Test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11"] + fail-fast: true + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - uses: actions/cache@v3 + id: cache + with: + path: ${{ env.pythonLocation }} + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('requirements-tests.txt') }}-test-v06 + - name: Install Dependencies + if: steps.cache.outputs.cache-hit != 'true' + run: | + pip install -r requirements.txt + pip install -r requirements-tests.txt + - name: Test + run: python -m pytest -n auto tests --cov=. --cov-report=html:coverage --cov-fail-under=85 + - name: Store coverage files + uses: actions/upload-artifact@v3 + with: + name: coverage + path: coverage \ No newline at end of file diff --git a/README.md b/README.md index afcaa8b..8d7aa76 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # NTHU-Data-API -[![CodeFactor](https://www.codefactor.io/repository/github/nthu-sa/nthu-data-api/badge)](https://www.codefactor.io/repository/github/nthu-sa/nthu-data-api) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![CodeFactor](https://www.codefactor.io/repository/github/nthu-sa/nthu-data-api/badge)](https://www.codefactor.io/repository/github/nthu-sa/nthu-data-api) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Smokeshow coverage](https://coverage-badge.samuelcolvin.workers.dev/NTHU-SA/NTHU-Data-API.svg)](https://coverage-badge.samuelcolvin.workers.dev/redirect/NTHU-SA/NTHU-Data-API) ## Introduction This is a project for NTHU students to get data from NTHU website. diff --git a/requirements-tests.txt b/requirements-tests.txt new file mode 100644 index 0000000..df27660 --- /dev/null +++ b/requirements-tests.txt @@ -0,0 +1,5 @@ +# Tests +pytest==7.4.3 +pytest-xdist==3.4.0 +pytest-cov==4.1.0 +httpx==0.25.1 \ No newline at end of file diff --git a/src/api/models/buses.py b/src/api/models/buses.py index 57ceb2a..5177a43 100644 --- a/src/api/models/buses.py +++ b/src/api/models/buses.py @@ -306,13 +306,14 @@ def _find_stop_from_str(self, stop_str: str) -> Optional[Stop]: def _route_selector( self, dep_stop: str, line: str, from_gen_2: bool = False ) -> Route: - # 這裡不用 match 的原因是因為資料中有些會多空格 + # 清理資料,爬蟲抓下來的資料有些會多空格 + (dep_stop, line) = map(str.strip, [dep_stop, line]) # 下山 stops_lines_map = { - ("台積", "red", True): red_M5_M2, - ("台積", "red", False): red_M5_M1, - ("台積", "green", True): green_M5_M2, - ("台積", "green", False): green_M5_M1, + ("台積館", "red", True): red_M5_M2, + ("台積館", "red", False): red_M5_M1, + ("台積館", "green", True): green_M5_M2, + ("台積館", "green", False): green_M5_M1, ("校門", "red"): red_M1_M5, ("綜二", "red"): red_M2_M5, ("校門", "green"): green_M1_M5, diff --git a/src/api/routers/libraries.py b/src/api/routers/libraries.py index a15b9ed..f931b97 100644 --- a/src/api/routers/libraries.py +++ b/src/api/routers/libraries.py @@ -38,17 +38,17 @@ def get_library_rss_data( @router.get( - "/openinghours/{libaray_name}", response_model=schemas.resources.LibraryOpeningHour + "/openinghours/{library_name}", response_model=schemas.resources.LibraryOpeningHour ) def get_library_opening_hours( - libaray_name: schemas.resources.LibraryName = Path( + library_name: schemas.resources.LibraryName = Path( ..., description="圖書館代號:總圖(mainlib)、人社圖書館(hslib)、南大圖書館(nandalib)" ) ): """ 取得指定圖書館的開放時間。 """ - return library_scraper.get_opening_hours(libaray_name) + return library_scraper.get_opening_hours(library_name) @router.get("/goods", response_model=schemas.resources.LibraryNumberOfGoods) diff --git a/src/utils/cached_request.py b/src/utils/cached_request.py index 1c38b3e..0a1c1e2 100644 --- a/src/utils/cached_request.py +++ b/src/utils/cached_request.py @@ -68,7 +68,7 @@ def get(url: str, cache=True, update=False, auto_headers=True, **kwargs) -> str: else: headers = None url = validate_url(url) - response = requests.get(url, headers, **kwargs) + response = requests.get(url, headers, timeout=10, **kwargs) status_code = response.status_code if status_code != 200: raise HTTPException(status_code, f"Request error: {status_code}") @@ -86,7 +86,7 @@ def post(url: str, cache=True, update=False, **kwargs) -> str: del ttl_cache[url] if cache and url in ttl_cache: return ttl_cache[url] - response = requests.post(url, **kwargs) + response = requests.post(url, timeout=10, **kwargs) status_code = response.status_code if status_code != 200: raise HTTPException(status_code, f"Request error: {status_code}") diff --git a/tests/test_buses.py b/tests/test_buses.py new file mode 100644 index 0000000..c13a5ce --- /dev/null +++ b/tests/test_buses.py @@ -0,0 +1,48 @@ +import pytest +from fastapi.testclient import TestClient + +from src import app +from src.api import schemas + +client = TestClient(app) + + +@pytest.mark.parametrize( + "url, status_code", + [ + ("/buses/main", 200), + ("/buses/main/info/toward_main_gate", 200), + ("/buses/main/info/toward_tsmc_building", 200), + ("/buses/main/schedules/weekday/toward_main_gate", 200), + ("/buses/main/schedules/weekday/toward_tsmc_building", 200), + ("/buses/main/schedules/weekend/toward_main_gate", 200), + ("/buses/main/schedules/weekend/toward_tsmc_building", 200), + ("/buses/nanda", 200), + ("/buses/nanda/info/toward_main_campus", 200), + ("/buses/nanda/info/toward_south_campus", 200), + ("/buses/nanda/schedules/weekday/toward_main_campus", 200), + ("/buses/nanda/schedules/weekday/toward_south_campus", 200), + ("/buses/nanda/schedules/weekend/toward_main_campus", 200), + ("/buses/nanda/schedules/weekend/toward_south_campus", 200), + ], +) +def test_buses_endpoints(url, status_code): + response = client.get(url=url) + assert response.status_code == status_code + + +@pytest.mark.parametrize("stop_name", [_.value for _ in schemas.buses.StopsName]) +@pytest.mark.parametrize("bus_type", [_.value for _ in schemas.buses.BusType]) +@pytest.mark.parametrize("day", [_.value for _ in schemas.buses.BusDay]) +@pytest.mark.parametrize("direction", [_.value for _ in schemas.buses.BusDirection]) +def test_buses_stops(stop_name, bus_type, day, direction): + response = client.get(url=f"/buses/stops/{stop_name}/{bus_type}/{day}/{direction}") + assert response.status_code == 200 + + +@pytest.mark.parametrize("bus_type", [_.value for _ in schemas.buses.BusType]) +@pytest.mark.parametrize("day", [_.value for _ in schemas.buses.BusDay]) +@pytest.mark.parametrize("direction", [_.value for _ in schemas.buses.BusDirection]) +def test_buses_detailed(bus_type, day, direction): + response = client.get(url=f"/buses/detailed/{bus_type}/{day}/{direction}") + assert response.status_code == 200 diff --git a/tests/test_careers.py b/tests/test_careers.py new file mode 100644 index 0000000..db9d668 --- /dev/null +++ b/tests/test_careers.py @@ -0,0 +1,10 @@ +from fastapi.testclient import TestClient + +from src import app + +client = TestClient(app) + + +def test_careers(): + response = client.get(url="/resources/careers/bulletin/recruitment") + assert response.status_code == 200 diff --git a/tests/test_contacts.py b/tests/test_contacts.py new file mode 100644 index 0000000..8be561d --- /dev/null +++ b/tests/test_contacts.py @@ -0,0 +1,36 @@ +import pytest +from fastapi.testclient import TestClient + +from src import app + +client = TestClient(app) +id_list = [ + "7e00db83-b407-4320-af55-0a1b1f5734ad", + "8dfa4f30-2339-4ec2-aee9-0da58e78fdde", + "ed3ac738-8102-43fb-844c-3abbd5f493d8", + "4955571d-ec87-4c8f-bc39-c3b39142558c", +] +name_list = ["清華學院", "理學院", "主計"] + + +def test_contacts(): + response = client.get(url=f"/contacts") + assert response.status_code == 200 + + +@pytest.mark.parametrize("id", id_list) +def test_contacts_id(id): + response = client.get(url=f"/contacts/{id}") + assert response.status_code == 200 + + +@pytest.mark.parametrize("name", name_list) +def test_contacts_name(name): + response = client.get(url=f"/contacts/searches/{name}") + assert response.status_code == 200 + + +@pytest.mark.parametrize("name", name_list) +def test_contacts_name_post(name): + response = client.post(url="/contacts/searches/", json={"name": name}) + assert response.status_code == 200 diff --git a/tests/test_courses.py b/tests/test_courses.py new file mode 100644 index 0000000..485c7ec --- /dev/null +++ b/tests/test_courses.py @@ -0,0 +1,62 @@ +import pytest +from fastapi.testclient import TestClient + +from src import app +from src.api import schemas + +client = TestClient(app) + + +@pytest.mark.parametrize( + "url, status_code", + [ + ("/courses/", 200), + ("/courses/fields/info", 200), + ("/courses/lists/16weeks", 200), + ("/courses/lists/microcredits", 200), + ("/courses/lists/xclass", 200), + ], +) +def test_courses_endpoints(url, status_code): + response = client.get(url=url) + assert response.status_code == status_code + + +@pytest.mark.parametrize( + "field_name", [_.value for _ in schemas.courses.CourseFieldName] +) +def test_courses_fields(field_name): + response = client.get(url=f"/courses/fields/{field_name}") + assert response.status_code == 200 + + +@pytest.mark.parametrize( + "field_name", [_.value for _ in schemas.courses.CourseFieldName] +) +@pytest.mark.parametrize("value", ["testing"]) +def test_courses_fields(field_name, value): + response = client.get(url=f"/courses/fields/{field_name}/{value}") + assert response.status_code == 200 + + +@pytest.mark.parametrize( + "field_name", [_.value for _ in schemas.courses.CourseFieldName] +) +@pytest.mark.parametrize("value", ["testing"]) +def test_courses_search(field_name, value): + response = client.get(url=f"/courses/searches?field={field_name}&value={value}") + assert response.status_code == 200 + + +@pytest.mark.parametrize("path", ["id", "classroom", "time", "teacher"]) +@pytest.mark.parametrize("value", ["testing"]) +def test_courses_search_extension(path, value): + response = client.get(url=f"/courses/searches/{path}/{value}") + assert response.status_code == 404 + + +@pytest.mark.parametrize("path", ["credits"]) +@pytest.mark.parametrize("op", ["gt", "lt", "gte", "lte"]) +def test_courses_search_credits(path, op): + response = client.get(url=f"/courses/searches/{path}/3?op={op}") + assert response.status_code == 200 diff --git a/tests/test_default.py b/tests/test_default.py new file mode 100644 index 0000000..89aa05c --- /dev/null +++ b/tests/test_default.py @@ -0,0 +1,18 @@ +import pytest +from fastapi.testclient import TestClient + +from src import app + +client = TestClient(app) + + +@pytest.mark.parametrize( + "url, status_code", + [ + ("/", 200), + ("/ping", 200), + ], +) +def test_default_endpoints(url, status_code): + response = client.get(url=url) + assert response.status_code == status_code diff --git a/tests/test_dining.py b/tests/test_dining.py new file mode 100644 index 0000000..ba0b045 --- /dev/null +++ b/tests/test_dining.py @@ -0,0 +1,63 @@ +import pytest +from fastapi.testclient import TestClient + +from src import app +from src.api import schemas + +client = TestClient(app) +restaurant_name_list = [ + "麥當勞", + "紅燒如意坊", + "茗釀茶品", + "蘇記食堂", + "友記快餐館", + "蘇記牛肉麵", + "帕森義大利麵", + "顏記文昌雞", + "家味燒臘", + "喜番咖哩", + "牛肉先生", + "墨尼捲餅", + "珍御品粥麵館", +] + + +@pytest.mark.parametrize( + "url, status_code", + [ + ("/dining/", 200), + ("/dining/buildings", 200), + ("/dining/restaurants", 200), + ], +) +def test_dining_endpoints(url, status_code): + response = client.get(url=url) + assert response.status_code == status_code + + +@pytest.mark.parametrize( + "building_name", [_.value for _ in schemas.dining.DiningBuildingName] +) +def test_dining_buildings(building_name): + response = client.get(url=f"/dining/buildings/{building_name}") + assert response.status_code == 200 + + +@pytest.mark.parametrize("restaurant_name", restaurant_name_list) +def test_dining_restaurants(restaurant_name): + response = client.get(url=f"/dining/restaurants/{restaurant_name}") + assert response.status_code == 200 + + +@pytest.mark.parametrize( + "day_of_week", [_.value for _ in schemas.dining.DiningSceduleName] +) +def test_dining_schedules(day_of_week): + response = client.get(url=f"/dining/scedules/{day_of_week}") + assert response.status_code == 200 + + +@pytest.mark.parametrize("restaurant_name", restaurant_name_list) +def test_dining_searches_restaurants(restaurant_name): + response = client.get(url=f"/dining/searches/restaurants/{restaurant_name}") + assert response.status_code == 200 diff --git a/tests/test_energy.py b/tests/test_energy.py new file mode 100644 index 0000000..ba7b47f --- /dev/null +++ b/tests/test_energy.py @@ -0,0 +1,10 @@ +from fastapi.testclient import TestClient + +from src import app + +client = TestClient(app) + + +def test_energy(): + response = client.get(url="/energy/electricity_usage") + assert response.status_code == 200 diff --git a/tests/test_events.py b/tests/test_events.py new file mode 100644 index 0000000..618ca6a --- /dev/null +++ b/tests/test_events.py @@ -0,0 +1,24 @@ +import pytest +from fastapi.testclient import TestClient + +from src import app + +client = TestClient(app) + + +@pytest.mark.parametrize( + "url, status_code", + [ + ("/resources/events/libarys", 200), + ("/resources/events/goodjob", 200), + ("/resources/events/arts_center", 200), + ("/resources/events/global_affairs", 200), + ("/resources/events/health_center", 200), + ("/resources/events/bulletin/art_and_cultural", 200), + ("/resources/events/bulletin/academic", 200), + ("/resources/events/bulletin/academic", 200), + ], +) +def test_events_endpoints(url, status_code): + response = client.get(url=url) + assert response.status_code == status_code diff --git a/tests/test_libraries.py b/tests/test_libraries.py new file mode 100644 index 0000000..9c47bde --- /dev/null +++ b/tests/test_libraries.py @@ -0,0 +1,34 @@ +import pytest +from fastapi.testclient import TestClient + +from src import app +from src.api import schemas + +client = TestClient(app) + + +@pytest.mark.parametrize( + "url, status_code", + [ + ("/libraries/space", 200), + ("/libraries/lost_and_found", 200), + # ("/libraries/goods", 200), + ], +) +def test_libraries_endpoints(url, status_code): + response = client.get(url=url) + assert response.status_code == status_code + + +@pytest.mark.parametrize("rss", [_.value for _ in schemas.resources.LibraryRssType]) +def test_libraries_rss(rss): + response = client.get(url=f"/libraries/rss/{rss}") + assert response.status_code == 200 + + +@pytest.mark.parametrize( + "library_name", [_.value for _ in schemas.resources.LibraryName] +) +def test_libraries_openinghours(library_name): + response = client.get(url=f"/libraries/openinghours/{library_name}") + assert response.status_code == 200 diff --git a/tests/test_locations.py b/tests/test_locations.py new file mode 100644 index 0000000..30c5b5a --- /dev/null +++ b/tests/test_locations.py @@ -0,0 +1,35 @@ +import pytest +from fastapi.testclient import TestClient + +from src import app + +client = TestClient(app) +id_list = [ + "b876aa09-40a8-427b-8bc7-1933978690e2", + "6db26190-de4d-4d45-ae25-dccc5e47d795", + "66e128f8-f457-489e-bbc4-b631ddc5edef", +] +name_list = ["校門", "產業", "綜合", "台積", "台達"] + + +def test_locations(): + response = client.get(url="/locations") + assert response.status_code == 200 + + +@pytest.mark.parametrize("id", id_list) +def test_locations_id(id): + response = client.get(url=f"/locations/{id}") + assert response.status_code == 200 + + +@pytest.mark.parametrize("name", name_list) +def test_locations_name(name): + response = client.get(url=f"/locations/searches/{name}") + assert response.status_code == 200 + + +@pytest.mark.parametrize("name", name_list) +def test_locations_searches(name): + response = client.post(url="/locations/searches", json={"name": name}) + assert response.status_code == 200 diff --git a/tests/test_newsletter.py b/tests/test_newsletter.py new file mode 100644 index 0000000..2bb70af --- /dev/null +++ b/tests/test_newsletter.py @@ -0,0 +1,29 @@ +import pytest +from fastapi.testclient import TestClient + +from src import app +from src.api import schemas + +client = TestClient(app) +newsletter_link_list = [ + "https://newsletter.cc.nthu.edu.tw/nthu-list/index.php/zh/home-zh-tw/listid-44-" +] + + +# def test_newsletter(): +# response = client.get(url=f"/newsletter/") +# assert response.status_code == 200 + + +# @pytest.mark.parametrize( +# "newsletter_name", [_.value for _ in schemas.newsletter.NewsletterName] +# ) +# def test_newsletter_searches(newsletter_name): +# response = client.get(url=f"/newsletters/{newsletter_name}") +# assert response.status_code == 200 + + +# @pytest.mark.parametrize("newsletter_link", newsletter_link_list) +# def test_newsletter_paths_link(newsletter_link): +# response = client.get(url=f"/newsletters/paths/{newsletter_link}") +# assert response.status_code == 200 diff --git a/tests/test_phones.py b/tests/test_phones.py new file mode 100644 index 0000000..ab4b147 --- /dev/null +++ b/tests/test_phones.py @@ -0,0 +1,36 @@ +import pytest +from fastapi.testclient import TestClient + +from src import app + +client = TestClient(app) +id_list = [ + "7e00db83-b407-4320-af55-0a1b1f5734ad", + "8dfa4f30-2339-4ec2-aee9-0da58e78fdde", + "ed3ac738-8102-43fb-844c-3abbd5f493d8", + "4955571d-ec87-4c8f-bc39-c3b39142558c", +] +name_list = ["清華學院", "理學院", "主計"] + + +def test_phones(): + response = client.get(url=f"/phones") + assert response.status_code == 200 + + +@pytest.mark.parametrize("id", id_list) +def test_phones_id(id): + response = client.get(url=f"/phones/{id}") + assert response.status_code == 200 + + +@pytest.mark.parametrize("name", name_list) +def test_phones_name(name): + response = client.get(url=f"/phones/searches/{name}") + assert response.status_code == 200 + + +@pytest.mark.parametrize("name", name_list) +def test_phones_name_post(name): + response = client.post(url="/phones/searches/", json={"name": name}) + assert response.status_code == 200 diff --git a/tests/test_scrapers.py b/tests/test_scrapers.py new file mode 100644 index 0000000..52fdc91 --- /dev/null +++ b/tests/test_scrapers.py @@ -0,0 +1,13 @@ +import pytest +from fastapi.testclient import TestClient + +from src import app + +client = TestClient(app) +path_list = ["https://bulletin.site.nthu.edu.tw/p/403-1086-5081-1.php?Lang=zh-tw"] + + +@pytest.mark.parametrize("full_path", path_list) +def test_scrapers(full_path): + response = client.get(url=f"/scrapers/rpage/announcements/{full_path}") + assert response.status_code == 200