diff --git a/app/core/config.py b/app/core/config.py index b080ce4..f96cf0e 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -51,6 +51,7 @@ class Settings(BaseSettings): loki_port: int = Field(default=3100, validation_alias="LOKI_PORT") loki_enabled: bool = Field(default=False, validation_alias="LOKI_ENABLED") credential_encryption_key: str = Field(default="", validation_alias="CREDENTIAL_ENCRYPTION_KEY") + root_path: str = Field(default="", validation_alias="PORTFOLIOHUB_ROOT_PATH") model_config = SettingsConfigDict( env_prefix="PORTFOLIOHUB_", diff --git a/app/main.py b/app/main.py index 9033e08..4595c86 100644 --- a/app/main.py +++ b/app/main.py @@ -63,6 +63,7 @@ def create_app( version=settings.app_version, docs_url="/docs", redoc_url="/redoc", + root_path=settings.root_path, lifespan=lifespan, ) diff --git a/migrations/versions/20260519_0021_portfolio_tags_data_migration.py b/migrations/versions/20260519_0021_portfolio_tags_data_migration.py index 60a545f..16b04a1 100644 --- a/migrations/versions/20260519_0021_portfolio_tags_data_migration.py +++ b/migrations/versions/20260519_0021_portfolio_tags_data_migration.py @@ -11,7 +11,6 @@ before running this migration in a live environment. from __future__ import annotations import os - import sqlalchemy as sa from alembic import op from sqlalchemy import inspect, text @@ -33,16 +32,20 @@ def _table_exists(inspector: sa.engine.Inspector, table_name: str) -> bool: def run_migration(connection, admin_user_id: str) -> None: - """Core migration logic — extracted for testability.""" + """Core migration logic — extracted for testability. + + admin_user_id can be either a UUID (id) or username. + """ + # Try to find user by id (UUID) first, then by username result = connection.execute( - text("SELECT COUNT(*) FROM users WHERE id = :uid"), + text("SELECT id FROM users WHERE id = :uid OR username = :uid LIMIT 1"), {"uid": admin_user_id}, ) - count = result.scalar() - if not count: + user_id = result.scalar() + if not user_id: raise ValueError( f"Migration admin user not found in users table: {admin_user_id}. " - "Set MIGRATION_ADMIN_ID to a valid user ID before running." + "Set MIGRATION_ADMIN_ID to a valid user ID (UUID) or username before running." ) # Migrate distinct (lowercased) tag_values from portfolio_tags → tags master @@ -54,7 +57,7 @@ def run_migration(connection, admin_user_id: str) -> None: GROUP BY LOWER(tag_value) ON CONFLICT (name) DO NOTHING """), - {"admin_id": admin_user_id}, + {"admin_id": user_id}, ) # Fill portfolios.tag_ids from the migrated tags diff --git a/migrations/versions/20260520_0022_service_token_unique_index_fix.py b/migrations/versions/20260520_0022_service_token_unique_index_fix.py index 7ca8c6c..5a139fe 100644 --- a/migrations/versions/20260520_0022_service_token_unique_index_fix.py +++ b/migrations/versions/20260520_0022_service_token_unique_index_fix.py @@ -25,6 +25,7 @@ def upgrade() -> None: bind = op.get_bind() inspector = inspect(bind) + # Drop old index if it exists (non-partial index) if _index_exists(inspector, "auth_tokens", "idx_auth_tokens_service_name_unique"): op.execute("DROP INDEX IF EXISTS idx_auth_tokens_service_name_unique") diff --git a/tests/test_docs_root_path.py b/tests/test_docs_root_path.py new file mode 100644 index 0000000..d2da782 --- /dev/null +++ b/tests/test_docs_root_path.py @@ -0,0 +1,24 @@ +from fastapi.testclient import TestClient + +from app.main import create_app +from app.core.config import Settings + + +def test_docs_uses_root_path_for_openapi_url() -> None: + app = create_app(settings_factory=lambda: Settings(root_path="/portfolio-hub")) + + with TestClient(app) as client: + response = client.get("/docs") + + assert response.status_code == 200 + assert "url: '/portfolio-hub/openapi.json'" in response.text + + +def test_openapi_is_served_under_root_path() -> None: + app = create_app(settings_factory=lambda: Settings(root_path="/portfolio-hub")) + + with TestClient(app) as client: + response = client.get("/portfolio-hub/openapi.json") + + assert response.status_code == 200 + assert response.json()["info"]["title"] == "PortfolioHub"