Merge branch 'main' of https://github.com/fourblocks2020/portfolio-hub
# 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:
@@ -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_",
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
Reference in New Issue
Block a user