Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bfabric_rest_proxy/docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Fixed

- `/validate_token` now uses the server-configured `validation_bfabric_instance` instead of the client-provided instance.
- Handle empty list `[]` query parameter from R clients (converts to empty dict `{}`).

### Added
Expand Down
6 changes: 3 additions & 3 deletions bfabric_rest_proxy/src/bfabric_rest_proxy/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import fastapi
from bfabric.config.config_data import ConfigData
from bfabric.rest.token_data import get_token_data_async
from bfabric.rest.token_data import validate_token
from fastapi import Request
from fastapi.exceptions import RequestValidationError
from fastapi.params import Depends
Expand Down Expand Up @@ -128,13 +128,13 @@ class TokenParam(BaseModel):


@app.post("/validate_token")
async def validate_token(token_param: TokenParam, bfabric_instance: BfabricInstanceDep):
async def post_validate_token(token_param: TokenParam, settings: ServerSettingsDep):
"""Validates a token and returns the token data.

This endpoint is not really necessary since it proxies a REST endpoint, but is added here for consistency to avoid
shiny apps having to interface with two different APIs.
"""
token_data = await get_token_data_async(base_url=bfabric_instance, token=token_param.token, http_client=None)
token_data = await validate_token(token=token_param.token, settings=settings)
dump = token_data.model_dump(by_alias=True, mode="json")
dump["userWsPassword"] = token_data.user_ws_password.get_secret_value()
return dump
Expand Down
62 changes: 21 additions & 41 deletions bfabric_rest_proxy/tests/test_validate_token_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import pytest

from bfabric.errors import BfabricInstanceNotConfiguredError


class TestValidateTokenEndpoint:
"""Tests for the /validate_token endpoint."""
Expand All @@ -14,14 +16,13 @@ class TestValidateTokenEndpoint:
async def test_validate_token_success(self, client, mock_settings, mock_token_data, mocker):
"""Test successful token validation."""
mocker.patch(
"bfabric_rest_proxy.server.get_token_data_async",
"bfabric_rest_proxy.server.validate_token",
return_value=mock_token_data,
)

response = client.post(
"/validate_token",
json={"token": "valid_token_123"},
params={"bfabric_instance": "https://test.bfabric.example.com/"},
)

assert response.status_code == 200
Expand All @@ -39,67 +40,46 @@ async def test_validate_token_success(self, client, mock_settings, mock_token_da
assert data["environment"] == mock_token_data.environment

@pytest.mark.asyncio
async def test_validate_token_calls_async_function(self, client, mock_token_data, mocker):
"""Test that validate_token calls get_token_data_async with correct parameters."""
mock_get_token = mocker.patch(
"bfabric_rest_proxy.server.get_token_data_async",
async def test_validate_token_calls_with_settings(self, client, mock_settings, mock_token_data, mocker):
"""Test that validate_token is called with settings (not client-provided instance)."""
mock_validate = mocker.patch(
"bfabric_rest_proxy.server.validate_token",
return_value=mock_token_data,
)

client.post(
"/validate_token",
json={"token": "test_token"},
params={"bfabric_instance": "https://test.bfabric.example.com/"},
)

mock_get_token.assert_called_once()
mock_validate.assert_called_once()
call_kwargs = mock_validate.call_args[1]
assert call_kwargs["settings"] is mock_settings

@pytest.mark.asyncio
async def test_validate_token_expired_token(self, client, mocker):
async def test_validate_token_expired_token(self, client, mock_settings, mocker):
"""Test validation with invalid/expired token."""
mocker.patch(
"bfabric_rest_proxy.server.get_token_data_async",
"bfabric_rest_proxy.server.validate_token",
side_effect=Exception("Token validation failed"),
)

with pytest.raises(Exception, match="Token validation failed"):
client.post(
"/validate_token",
json={"token": "expired_token"},
params={"bfabric_instance": "https://test.bfabric.example.com/"},
)

@pytest.mark.asyncio
async def test_validate_token_missing_bfabric_instance(self, client, mock_settings, mock_token_data, mocker):
"""Test that missing bfabric_instance parameter uses default when available."""
mock_get_token = mocker.patch(
"bfabric_rest_proxy.server.get_token_data_async",
return_value=mock_token_data,
)

mock_settings.default_bfabric_instance = "https://test.bfabric.example.com/"

response = client.post(
"/validate_token",
json={"token": "test_token"},
)

assert response.status_code == 200
mock_get_token.assert_called_once()

@pytest.mark.asyncio
async def test_validate_token_uses_default_instance(self, client, mock_settings, mock_token_data, mocker):
"""Test that default instance is used when not specified."""
async def test_validate_token_unsupported_instance(self, client, mock_settings, mocker):
"""Test that tokens from unsupported instances are rejected."""
mocker.patch(
"bfabric_rest_proxy.server.get_token_data_async",
return_value=mock_token_data,
)

mock_settings.default_bfabric_instance = "https://test.bfabric.example.com/"

response = client.post(
"/validate_token",
json={"token": "test_token"},
"bfabric_rest_proxy.server.validate_token",
side_effect=BfabricInstanceNotConfiguredError("https://evil.example.com/"),
)

assert response.status_code == 200
with pytest.raises(BfabricInstanceNotConfiguredError):
client.post(
"/validate_token",
json={"token": "test_token"},
)
Loading