# Conflicts:
#	migrations/versions/20260519_0021_portfolio_tags_data_migration.py
#	migrations/versions/20260520_0022_service_token_unique_index_fix.py
This commit is contained in:
2026-05-21 13:28:56 +00:00
5 changed files with 37 additions and 7 deletions
+1
View File
@@ -51,6 +51,7 @@ class Settings(BaseSettings):
loki_port: int = Field(default=3100, validation_alias="LOKI_PORT") loki_port: int = Field(default=3100, validation_alias="LOKI_PORT")
loki_enabled: bool = Field(default=False, validation_alias="LOKI_ENABLED") loki_enabled: bool = Field(default=False, validation_alias="LOKI_ENABLED")
credential_encryption_key: str = Field(default="", validation_alias="CREDENTIAL_ENCRYPTION_KEY") credential_encryption_key: str = Field(default="", validation_alias="CREDENTIAL_ENCRYPTION_KEY")
root_path: str = Field(default="", validation_alias="PORTFOLIOHUB_ROOT_PATH")
model_config = SettingsConfigDict( model_config = SettingsConfigDict(
env_prefix="PORTFOLIOHUB_", env_prefix="PORTFOLIOHUB_",
+1
View File
@@ -63,6 +63,7 @@ def create_app(
version=settings.app_version, version=settings.app_version,
docs_url="/docs", docs_url="/docs",
redoc_url="/redoc", redoc_url="/redoc",
root_path=settings.root_path,
lifespan=lifespan, lifespan=lifespan,
) )
@@ -11,7 +11,6 @@ before running this migration in a live environment.
from __future__ import annotations from __future__ import annotations
import os import os
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op from alembic import op
from sqlalchemy import inspect, text 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: 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( 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}, {"uid": admin_user_id},
) )
count = result.scalar() user_id = result.scalar()
if not count: if not user_id:
raise ValueError( raise ValueError(
f"Migration admin user not found in users table: {admin_user_id}. " 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 # 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) GROUP BY LOWER(tag_value)
ON CONFLICT (name) DO NOTHING ON CONFLICT (name) DO NOTHING
"""), """),
{"admin_id": admin_user_id}, {"admin_id": user_id},
) )
# Fill portfolios.tag_ids from the migrated tags # Fill portfolios.tag_ids from the migrated tags
@@ -25,6 +25,7 @@ def upgrade() -> None:
bind = op.get_bind() bind = op.get_bind()
inspector = inspect(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"): if _index_exists(inspector, "auth_tokens", "idx_auth_tokens_service_name_unique"):
op.execute("DROP INDEX IF EXISTS idx_auth_tokens_service_name_unique") op.execute("DROP INDEX IF EXISTS idx_auth_tokens_service_name_unique")
+24
View File
@@ -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"