Skip to content

Commit

Permalink
Add Mantid versions endpoint (#421)
Browse files Browse the repository at this point in the history
* Add mantid version endpoint

* Move utility function out of router.py

* Add endpoint tests

* Formatting and linting commit

* Add unit tests

* Remove unused lines

* Use correct token

* Silence ruff

* Formatting and linting commit

* Add github package token

* Use user_token

* Add patch to tests

---------

Co-authored-by: github-actions <[email protected]>
  • Loading branch information
Dagonite and github-actions authored Nov 28, 2024
1 parent c5befd2 commit b51b336
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 8 deletions.
13 changes: 7 additions & 6 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ on: push

permissions:
contents: read
packages: read

jobs:
pytest:
Expand All @@ -14,7 +15,7 @@ jobs:
- name: Set up python
uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v4.5.0
with:
python-version: '3.12'
python-version: "3.12"

- name: Set up cache for Python dependencies
uses: actions/cache@v4
Expand All @@ -36,7 +37,6 @@ jobs:
with:
token: ${{ secrets.CODECOV_TOKEN }}


integration:
runs-on: ubuntu-latest

Expand All @@ -58,7 +58,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1
with:
python-version: '3.12'
python-version: "3.12"

- name: Set up cache for Python dependencies
uses: actions/cache@v4
Expand All @@ -82,7 +82,7 @@ jobs:
- name: Upload coverage
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
token: ${{ secrets.CODECOV_TOKEN }}
e2e:
runs-on: ubuntu-latest

Expand Down Expand Up @@ -112,7 +112,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1
with:
python-version: '3.12'
python-version: "3.12"

- name: Set up cache for Python dependencies
uses: actions/cache@v4
Expand All @@ -130,6 +130,7 @@ jobs:
- name: Run tests (excluding job maker)
env:
DATABASE_URL: postgresql://postgres:password@localhost:5432/test_db
GITHUB_PACKAGE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
pytest test/e2e --random-order --random-order-bucket=global --cov --cov-report=xml --ignore=test/e2e/test_job_maker.py
Expand All @@ -142,4 +143,4 @@ jobs:
- name: Upload coverage
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
token: ${{ secrets.CODECOV_TOKEN }}
19 changes: 19 additions & 0 deletions fia_api/core/utility.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
"""Collection of utility functions"""

import functools
import os
from collections.abc import Callable
from http import HTTPStatus
from pathlib import Path
from typing import Any, TypeVar, cast

import requests
from fastapi import HTTPException

from fia_api.core.exceptions import UnsafePathError

FuncT = TypeVar("FuncT", bound=Callable[[str], Any])

GITHUB_PACKAGE_TOKEN = os.environ.get("GITHUB_PACKAGE_TOKEN", "shh")


def forbid_path_characters(func: FuncT) -> FuncT:
"""Decorator that prevents path characters {/, ., \\} from a functions args by raising UnsafePathError"""
Expand Down Expand Up @@ -59,3 +63,18 @@ def safe_check_filepath(filepath: Path, base_path: Path) -> None:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Invalid path being accessed and file not found."
) from err


def get_packages(org: str, image_name: str) -> Any:
"""Helper function for getting package versions from GitHub."""
response = requests.get(
f"https://api.github.com/orgs/{org}/packages/container/{image_name}/versions",
headers={"Authorization": f"Bearer {GITHUB_PACKAGE_TOKEN}"},
timeout=10,
)
if response.status_code != HTTPStatus.OK:
raise HTTPException(
status_code=response.status_code,
detail=f"GitHub API request failed with status code {response.status_code}: {response.text}",
)
return response.json()
17 changes: 16 additions & 1 deletion fia_api/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
get_job_by_instrument,
job_maker,
)
from fia_api.core.utility import safe_check_filepath
from fia_api.core.utility import get_packages, safe_check_filepath
from fia_api.scripts.acquisition import (
get_script_by_sha,
get_script_for_job,
Expand Down Expand Up @@ -349,3 +349,18 @@ async def upload_file_to_instrument_folder(instrument: str, filename: str, file:
await write_file_from_remote(file, file_directory)

return f"Successfully uploaded {filename}"


@ROUTER.get("/jobs/runners", tags=["jobs"])
async def get_mantid_runners(
credentials: Annotated[HTTPAuthorizationCredentials, Depends(jwt_security)],
) -> list[str]:
"""Return a list of Mantid versions if user is authenticated."""
user = get_user_from_token(credentials.credentials)

if user.role is None or user.user_number is None:
# Must be logged in to do this
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="User is not authorized to access this endpoint")

data = get_packages(org="fiaisis", image_name="mantid")
return [str(tag) for item in data for tag in item.get("metadata", {}).get("container", {}).get("tags", [])]
91 changes: 90 additions & 1 deletion test/core/test_utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@
Tests for utility functions
"""

from http import HTTPStatus
from pathlib import Path
from unittest.mock import patch

import pytest
from fastapi import HTTPException

from fia_api.core.exceptions import UnsafePathError
from fia_api.core.utility import filter_script_for_tokens, forbid_path_characters, safe_check_filepath
from fia_api.core.utility import (
GITHUB_PACKAGE_TOKEN,
filter_script_for_tokens,
forbid_path_characters,
get_packages,
safe_check_filepath,
)


def dummy_string_arg_function(arg: str) -> str:
Expand Down Expand Up @@ -123,3 +131,84 @@ def test_non_existing_folder_path(tmp_path):
safe_check_filepath(file_path, base_path)
assert exc_info.errisinstance(HTTPException)
assert "Invalid path being accessed and file not found" in exc_info.exconly()


def test_get_packages():
"""Test the get_packages() function for a successful API call."""
mock_response_data = [
{
"id": 294659748,
"metadata": {"package_type": "container", "container": {"tags": ["6.11.0"]}},
},
{
"id": 265303494,
"metadata": {"package_type": "container", "container": {"tags": ["6.10.0"]}},
},
{
"id": 220505057,
"metadata": {"package_type": "container", "container": {"tags": ["6.9.1"]}},
},
{
"id": 220504408,
"metadata": {"package_type": "container", "container": {"tags": ["6.9.0"]}},
},
{
"id": 220503717,
"metadata": {"package_type": "container", "container": {"tags": ["6.8.0"]}},
},
]

with patch("fia_api.core.utility.requests.get") as mock_get:
mock_get.return_value.status_code = HTTPStatus.OK
mock_get.return_value.json.return_value = mock_response_data

package_data = get_packages(org="fiaisis", image_name="mantid")
assert package_data == mock_response_data

# Verify the request was made with the correct URL and headers
mock_get.assert_called_once_with(
"https://api.github.com/orgs/fiaisis/packages/container/mantid/versions",
headers={"Authorization": f"Bearer {GITHUB_PACKAGE_TOKEN}"},
timeout=10,
)


def test_get_packages_error():
"Test the get_packages() function for an unsuccessful API call."
with patch("fia_api.core.utility.requests.get") as mock_get:
mock_get.return_value.status_code = HTTPStatus.NOT_FOUND

with pytest.raises(HTTPException) as excinfo:
get_packages(org="fiaisis", image_name="mantid")

assert excinfo.value.status_code == HTTPStatus.NOT_FOUND

# Verify the request was made with the correct URL and headers
mock_get.assert_called_once_with(
"https://api.github.com/orgs/fiaisis/packages/container/mantid/versions",
headers={"Authorization": f"Bearer {GITHUB_PACKAGE_TOKEN}"},
timeout=10,
)


def test_get_packages_forbidden_invalid_token():
"""
Test the get_packages() function for a forbidden API call caused by an invalid Bearer token.
"""
invalid_token = "invalid_token_value" # noqa: S105

with patch("fia_api.core.utility.requests.get") as mock_get:
mock_get.return_value.status_code = HTTPStatus.FORBIDDEN

with patch("fia_api.core.utility.GITHUB_PACKAGE_TOKEN", invalid_token):
with pytest.raises(HTTPException) as excinfo:
get_packages(org="fiaisis", image_name="mantid")

assert excinfo.value.status_code == HTTPStatus.FORBIDDEN

# Verify the request was made with the incorrect token
mock_get.assert_called_once_with(
"https://api.github.com/orgs/fiaisis/packages/container/mantid/versions",
headers={"Authorization": f"Bearer {invalid_token}"},
timeout=10,
)
27 changes: 27 additions & 0 deletions test/e2e/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,3 +524,30 @@ def test_put_instrument_specification_no_api_key():
client.put("/instrument/het/specification", json={"foo": "bar"})
response = client.get("/instrument/het/specification")
assert response.status_code == HTTPStatus.FORBIDDEN


@patch("fia_api.core.auth.tokens.requests.post")
def test_get_mantid_runners(mock_post):
"""Test endpoint contains all the Mantid runners."""
mock_post.return_value.status_code = HTTPStatus.OK
expected_runners = ["6.8.0", "6.9.0", "6.9.1", "6.10.0", "6.11.0"]
response = client.get("/jobs/runners", headers={"Authorization": f"Bearer {USER_TOKEN}"})
assert response.status_code == HTTPStatus.OK
for runner in expected_runners:
assert runner in response.json()


@patch("fia_api.core.auth.tokens.requests.post")
def test_get_mantid_runners_no_api_key(mock_post):
"""Test endpoint returns forbidden if no API key supplied."""
mock_post.return_value.status_code = HTTPStatus.FORBIDDEN
response = client.get("/jobs/runners")
assert response.status_code == HTTPStatus.FORBIDDEN


@patch("fia_api.core.auth.tokens.requests.post")
def test_get_mantid_runners_bad_jwt(mock_post):
"""Test endpoint returns forbidden if bad JWT supplied."""
mock_post.return_value.status_code = HTTPStatus.FORBIDDEN
response = client.get("/jobs/runners", headers={"Authorization": "foo"})
assert response.status_code == HTTPStatus.FORBIDDEN

0 comments on commit b51b336

Please sign in to comment.