Skip to content

Commit

Permalink
chore: pytests, codecov
Browse files Browse the repository at this point in the history
  • Loading branch information
Aleksandr Chikovani committed Dec 18, 2024
1 parent 51564e7 commit 93f48c1
Show file tree
Hide file tree
Showing 17 changed files with 854 additions and 30 deletions.
42 changes: 42 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Unit tests
on:
pull_request:
types:
- opened
- edited
- reopened
- synchronize
jobs:
python-tests:
name: Run python tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1
with:
python-version: 3.11
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install tox
tox -e py
- name: Build coverage file
run: |
tox -e py
pytest --cache-clear --junitxml=coverage.xml --cov-report=term-missing:skip-covered --cov=mlflow_oidc_auth > pytest-coverage.txt
- name: Override Coverage Source Path for Sonar
run: sed -i "s@<source>/home/runner/work/mlflow-oidc-auth/mlflow-oidc-auth</source>@<source>/github/workspace</source>@g" /home/runner/work/mlflow-oidc-auth/mlflow-oidc-auth/coverage.xml
- name: debug cov
run: |
pwd
ls -alh
head -n50 coverage.xml
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@e44258b109568baa0df60ed515909fc6c72cba92 # v2.3.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
# todo: remove GH App
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,4 @@ flask_session/
node_modules/
.angular/
mlflow_oidc_auth/ui
pytest-coverage.txt
38 changes: 27 additions & 11 deletions mlflow_oidc_auth/auth.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,38 @@
from typing import Union
from typing import Union, Optional

import requests
from authlib.integrations.flask_client import OAuth
from authlib.jose import jwt
from flask import Response, request
from werkzeug.datastructures import Authorization

from mlflow_oidc_auth.app import app
from mlflow_oidc_auth.config import config
from mlflow_oidc_auth.store import store
oauth = OAuth(app)

oauth.register(
name="oidc",
client_id=config.OIDC_CLIENT_ID,
client_secret=config.OIDC_CLIENT_SECRET,
server_metadata_url=config.OIDC_DISCOVERY_URL,
client_kwargs={"scope": config.OIDC_SCOPE},
)

_oauth_instance: Optional[OAuth] = None


def get_oauth_instance(app) -> OAuth:
# returns a singleton instance of OAuth
# to avoid circular imports
global _oauth_instance

if _oauth_instance is None:
_oauth_instance = OAuth(app)
_oauth_instance.register(
name="oidc",
client_id=config.OIDC_CLIENT_ID,
client_secret=config.OIDC_CLIENT_SECRET,
server_metadata_url=config.OIDC_DISCOVERY_URL,
client_kwargs={"scope": config.OIDC_SCOPE},
)
return _oauth_instance


def _get_oidc_jwks():
from mlflow_oidc_auth.app import cache
from mlflow_oidc_auth.app import cache, app

jwks = cache.get("jwks")
if jwks:
app.logger.debug("JWKS cache hit")
Expand All @@ -41,6 +53,8 @@ def validate_token(token):


def authenticate_request_basic_auth() -> Union[Authorization, Response]:
from mlflow_oidc_auth.app import app

username = request.authorization.username
password = request.authorization.password
app.logger.debug("Authenticating user %s", username)
Expand All @@ -53,6 +67,8 @@ def authenticate_request_basic_auth() -> Union[Authorization, Response]:


def authenticate_request_bearer_token() -> Union[Authorization, Response]:
from mlflow_oidc_auth.app import app

token = request.authorization.token
try:
user = validate_token(token)
Expand Down
2 changes: 2 additions & 0 deletions mlflow_oidc_auth/sqlalchemy_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ def delete_user(self, username: str):
with self.ManagedSessionMaker() as session:
user = self._get_user(session, username)
session.delete(user)
session.flush()

def create_experiment_permission(self, experiment_id: str, username: str, permission: str) -> ExperimentPermission:
_validate_permission(permission)
Expand Down Expand Up @@ -341,6 +342,7 @@ def delete_registered_model_permission(self, name: str, username: str):
with self.ManagedSessionMaker() as session:
perm = self._get_registered_model_permission(session, name, username)
session.delete(perm)
session.flush()

def list_experiment_permissions_for_experiment(self, experiment_id: str):
with self.ManagedSessionMaker() as session:
Expand Down
Empty file.
88 changes: 88 additions & 0 deletions mlflow_oidc_auth/tests/test_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from unittest.mock import patch, MagicMock
from mlflow_oidc_auth.auth import (
get_oauth_instance,
_get_oidc_jwks,
validate_token,
authenticate_request_basic_auth,
authenticate_request_bearer_token,
)


class TestAuth:
@patch("mlflow_oidc_auth.auth.OAuth")
@patch("mlflow_oidc_auth.auth.config")
def test_get_oauth_instance(self, mock_config, mock_oauth):
mock_app = MagicMock()
mock_oauth_instance = MagicMock()
mock_oauth.return_value = mock_oauth_instance

mock_config.OIDC_CLIENT_ID = "mock_client_id"
mock_config.OIDC_CLIENT_SECRET = "mock_client_secret"
mock_config.OIDC_DISCOVERY_URL = "mock_discovery_url"
mock_config.OIDC_SCOPE = "mock_scope"

result = get_oauth_instance(mock_app)

mock_oauth.assert_called_once_with(mock_app)
mock_oauth_instance.register.assert_called_once_with(
name="oidc",
client_id="mock_client_id",
client_secret="mock_client_secret",
server_metadata_url="mock_discovery_url",
client_kwargs={"scope": "mock_scope"},
)
assert result == mock_oauth_instance

@patch("mlflow_oidc_auth.auth._get_oidc_jwks")
@patch("mlflow_oidc_auth.auth.jwt.decode")
def test_validate_token(self, mock_jwt_decode, mock_get_oidc_jwks):
mock_jwks = {"keys": "mock_keys"}
mock_get_oidc_jwks.return_value = mock_jwks
mock_payload = MagicMock()
mock_jwt_decode.return_value = mock_payload

token = "mock_token"
result = validate_token(token)

mock_get_oidc_jwks.assert_called_once()
mock_jwt_decode.assert_called_once_with(token, mock_jwks)
mock_payload.validate.assert_called_once()
assert result == mock_payload

@patch("mlflow_oidc_auth.auth.store")
def test_authenticate_request_basic_auth_uses_authenticate_user(self, mock_store):
mock_request = MagicMock()
mock_request.authorization.username = "mock_username"
mock_request.authorization.password = "mock_password"
mock_store.authenticate_user.return_value = True

with patch("mlflow_oidc_auth.auth.request", mock_request):
# for some reason decorator doesn't mock flask
result = authenticate_request_basic_auth()

mock_store.authenticate_user.assert_called_once_with("mock_username", "mock_password")
assert result == True

@patch("mlflow_oidc_auth.auth.validate_token")
def test_authenticate_request_bearer_token_uses_validate_token(self, mock_validate_token):
mock_request = MagicMock()
mock_request.authorization.token = "mock_token"
mock_validate_token.return_value = MagicMock()
with patch("mlflow_oidc_auth.auth.request", mock_request):
# for some reason decorator doesn't mock flask
result = authenticate_request_bearer_token()

mock_validate_token.assert_called_once_with("mock_token")
assert result == True

@patch("mlflow_oidc_auth.auth.validate_token")
def test_authenticate_request_bearer_token_exception_returns_false(self, mock_validate_token):
mock_request = MagicMock()
mock_request.authorization.token = "mock_token"
mock_validate_token.side_effect = Exception()
with patch("mlflow_oidc_auth.auth.request", mock_request):
# for some reason decorator doesn't mock flask
result = authenticate_request_bearer_token()

mock_validate_token.assert_called_once_with("mock_token")
assert result == False
Loading

0 comments on commit 93f48c1

Please sign in to comment.