M1-T03: unify data layer, models, deps and routes onto single app DB
Collapse the three data layers into one. app/db.py now exposes a single Base, a cached engine bound to app_database_url with SQLite WAL enabled, and get_engine/get_session_local/reset_db_caches/get_db_session. Delete app/auth_db.py, app/poo_db.py and app/models/base.py. All models (auth, config, public_ip, location, poo) inherit the one Base and register on a single metadata. Dependencies converge to a single get_db; all routes use it. Also update the alembic env.py files (app/location/poo) and tests that imported the removed modules so the suite stays green, and drop the obsolete test_legacy_style_location_db test whose flow (app reading a separate location DB) no longer exists. Location/poo Alembic chains, adopt scripts and adoption tests remain for M1-T04; config fields remain for M1-T05. pytest 109 passed; ruff clean (pre-existing only); WAL verified; single Base.metadata holds all seven tables.
This commit is contained in:
+4
-2
@@ -3,11 +3,13 @@ from logging.config import fileConfig
|
|||||||
from alembic import context
|
from alembic import context
|
||||||
from sqlalchemy import engine_from_config, pool
|
from sqlalchemy import engine_from_config, pool
|
||||||
|
|
||||||
from app.auth_db import AuthBase
|
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
|
from app.db import Base
|
||||||
from app.models.config import AppConfigEntry # noqa: F401
|
from app.models.config import AppConfigEntry # noqa: F401
|
||||||
from app.models.auth import AuthSession, AuthUser # noqa: F401
|
from app.models.auth import AuthSession, AuthUser # noqa: F401
|
||||||
from app.models.public_ip import PublicIPHistory, PublicIPState # noqa: F401
|
from app.models.public_ip import PublicIPHistory, PublicIPState # noqa: F401
|
||||||
|
from app.models.location import Location # noqa: F401
|
||||||
|
from app.models.poo import PooRecord # noqa: F401
|
||||||
|
|
||||||
config = context.config
|
config = context.config
|
||||||
|
|
||||||
@@ -19,7 +21,7 @@ configured_url = config.get_main_option("sqlalchemy.url")
|
|||||||
if not configured_url or configured_url == "sqlite:///./data/app.db":
|
if not configured_url or configured_url == "sqlite:///./data/app.db":
|
||||||
config.set_main_option("sqlalchemy.url", settings.app_database_url)
|
config.set_main_option("sqlalchemy.url", settings.app_database_url)
|
||||||
|
|
||||||
target_metadata = AuthBase.metadata
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_offline() -> None:
|
def run_migrations_offline() -> None:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from sqlalchemy import engine_from_config, pool
|
|||||||
|
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
from app.models import Location # noqa: F401
|
from app.models import Location # noqa: F401
|
||||||
from app.models.base import Base
|
from app.db import Base
|
||||||
|
|
||||||
config = context.config
|
config = context.config
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@ from sqlalchemy import engine_from_config, pool
|
|||||||
|
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
from app.models.poo import PooRecord # noqa: F401
|
from app.models.poo import PooRecord # noqa: F401
|
||||||
from app.poo_db import PooBase
|
from app.db import Base
|
||||||
|
|
||||||
config = context.config
|
config = context.config
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ configured_url = config.get_main_option("sqlalchemy.url")
|
|||||||
if not configured_url or configured_url == "sqlite:///./data/pooRecorder.db":
|
if not configured_url or configured_url == "sqlite:///./data/pooRecorder.db":
|
||||||
config.set_main_option("sqlalchemy.url", settings.poo_database_url)
|
config.set_main_option("sqlalchemy.url", settings.poo_database_url)
|
||||||
|
|
||||||
target_metadata = PooBase.metadata
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_offline() -> None:
|
def run_migrations_offline() -> None:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from fastapi.templating import Jinja2Templates
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.config import Settings
|
from app.config import Settings
|
||||||
from app.dependencies import get_app_settings, get_auth_db, get_current_auth_session
|
from app.dependencies import get_app_settings, get_db, get_current_auth_session
|
||||||
from app.services.auth import (
|
from app.services.auth import (
|
||||||
AuthenticatedSession,
|
AuthenticatedSession,
|
||||||
authenticate_user,
|
authenticate_user,
|
||||||
@@ -57,7 +57,7 @@ def login_submit(
|
|||||||
username: str = Form(),
|
username: str = Form(),
|
||||||
password: str = Form(),
|
password: str = Form(),
|
||||||
csrf_token: str = Form(),
|
csrf_token: str = Form(),
|
||||||
session: Session = Depends(get_auth_db),
|
session: Session = Depends(get_db),
|
||||||
settings: Settings = Depends(get_app_settings),
|
settings: Settings = Depends(get_app_settings),
|
||||||
) -> Response:
|
) -> Response:
|
||||||
cookie_csrf_token = request.cookies.get(LOGIN_CSRF_COOKIE_NAME)
|
cookie_csrf_token = request.cookies.get(LOGIN_CSRF_COOKIE_NAME)
|
||||||
@@ -102,7 +102,7 @@ def change_password_submit(
|
|||||||
new_password: str = Form(),
|
new_password: str = Form(),
|
||||||
confirm_password: str = Form(),
|
confirm_password: str = Form(),
|
||||||
csrf_token: str = Form(),
|
csrf_token: str = Form(),
|
||||||
session: Session = Depends(get_auth_db),
|
session: Session = Depends(get_db),
|
||||||
settings: Settings = Depends(get_app_settings),
|
settings: Settings = Depends(get_app_settings),
|
||||||
current_auth: AuthenticatedSession | None = Depends(get_current_auth_session),
|
current_auth: AuthenticatedSession | None = Depends(get_current_auth_session),
|
||||||
) -> Response:
|
) -> Response:
|
||||||
@@ -151,7 +151,7 @@ def change_password_submit(
|
|||||||
def logout(
|
def logout(
|
||||||
request: Request,
|
request: Request,
|
||||||
csrf_token: str = Form(),
|
csrf_token: str = Form(),
|
||||||
session: Session = Depends(get_auth_db),
|
session: Session = Depends(get_db),
|
||||||
settings: Settings = Depends(get_app_settings),
|
settings: Settings = Depends(get_app_settings),
|
||||||
current_auth: AuthenticatedSession | None = Depends(get_current_auth_session),
|
current_auth: AuthenticatedSession | None = Depends(get_current_auth_session),
|
||||||
) -> RedirectResponse:
|
) -> RedirectResponse:
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from app.dependencies import (
|
|||||||
get_app_settings,
|
get_app_settings,
|
||||||
get_db,
|
get_db,
|
||||||
get_homeassistant_client,
|
get_homeassistant_client,
|
||||||
get_poo_db,
|
|
||||||
get_ticktick_client,
|
get_ticktick_client,
|
||||||
)
|
)
|
||||||
from app.integrations.homeassistant import (
|
from app.integrations.homeassistant import (
|
||||||
@@ -36,7 +35,6 @@ INTERNAL_SERVER_ERROR_MESSAGE = "internal server error"
|
|||||||
async def publish_from_homeassistant(
|
async def publish_from_homeassistant(
|
||||||
request: Request,
|
request: Request,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
poo_db: Session = Depends(get_poo_db),
|
|
||||||
settings: Settings = Depends(get_app_settings),
|
settings: Settings = Depends(get_app_settings),
|
||||||
homeassistant_client: HomeAssistantClient = Depends(get_homeassistant_client),
|
homeassistant_client: HomeAssistantClient = Depends(get_homeassistant_client),
|
||||||
ticktick_client: TickTickClient = Depends(get_ticktick_client),
|
ticktick_client: TickTickClient = Depends(get_ticktick_client),
|
||||||
@@ -49,7 +47,7 @@ async def publish_from_homeassistant(
|
|||||||
db,
|
db,
|
||||||
envelope,
|
envelope,
|
||||||
ticktick_client=ticktick_client,
|
ticktick_client=ticktick_client,
|
||||||
poo_session=poo_db,
|
poo_session=db,
|
||||||
settings=settings,
|
settings=settings,
|
||||||
homeassistant_client=homeassistant_client,
|
homeassistant_client=homeassistant_client,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse, Response
|
|||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
from app.config import Settings, get_settings
|
from app.config import Settings, get_settings
|
||||||
from app.dependencies import get_app_settings, get_auth_db, get_current_auth_session
|
from app.dependencies import get_app_settings, get_db, get_current_auth_session
|
||||||
from app.services.auth import AuthenticatedSession
|
from app.services.auth import AuthenticatedSession
|
||||||
from app.services.config_page import (
|
from app.services.config_page import (
|
||||||
ConfigSaveError,
|
ConfigSaveError,
|
||||||
@@ -100,7 +100,7 @@ def admin_redirect(
|
|||||||
@router.get("/config", response_class=HTMLResponse)
|
@router.get("/config", response_class=HTMLResponse)
|
||||||
def config_page(
|
def config_page(
|
||||||
request: Request,
|
request: Request,
|
||||||
auth_db_session: Session = Depends(get_auth_db),
|
auth_db_session: Session = Depends(get_db),
|
||||||
settings: Settings = Depends(get_app_settings),
|
settings: Settings = Depends(get_app_settings),
|
||||||
current_auth: AuthenticatedSession | None = Depends(get_current_auth_session),
|
current_auth: AuthenticatedSession | None = Depends(get_current_auth_session),
|
||||||
) -> Response:
|
) -> Response:
|
||||||
@@ -129,7 +129,7 @@ def config_page(
|
|||||||
@router.post("/config", response_class=HTMLResponse)
|
@router.post("/config", response_class=HTMLResponse)
|
||||||
async def config_submit(
|
async def config_submit(
|
||||||
request: Request,
|
request: Request,
|
||||||
auth_db_session: Session = Depends(get_auth_db),
|
auth_db_session: Session = Depends(get_db),
|
||||||
settings: Settings = Depends(get_app_settings),
|
settings: Settings = Depends(get_app_settings),
|
||||||
current_auth: AuthenticatedSession | None = Depends(get_current_auth_session),
|
current_auth: AuthenticatedSession | None = Depends(get_current_auth_session),
|
||||||
) -> Response:
|
) -> Response:
|
||||||
@@ -189,7 +189,7 @@ async def config_submit(
|
|||||||
@router.post("/config/smtp/test", response_class=HTMLResponse)
|
@router.post("/config/smtp/test", response_class=HTMLResponse)
|
||||||
async def smtp_test_submit(
|
async def smtp_test_submit(
|
||||||
request: Request,
|
request: Request,
|
||||||
auth_db_session: Session = Depends(get_auth_db),
|
auth_db_session: Session = Depends(get_db),
|
||||||
settings: Settings = Depends(get_app_settings),
|
settings: Settings = Depends(get_app_settings),
|
||||||
current_auth: AuthenticatedSession | None = Depends(get_current_auth_session),
|
current_auth: AuthenticatedSession | None = Depends(get_current_auth_session),
|
||||||
) -> Response:
|
) -> Response:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from pydantic import ValidationError
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.config import Settings
|
from app.config import Settings
|
||||||
from app.dependencies import get_app_settings, get_homeassistant_client, get_poo_db
|
from app.dependencies import get_app_settings, get_homeassistant_client, get_db
|
||||||
from app.integrations.homeassistant import HomeAssistantClient
|
from app.integrations.homeassistant import HomeAssistantClient
|
||||||
from app.schemas.poo import PooRecordRequest
|
from app.schemas.poo import PooRecordRequest
|
||||||
from app.services.poo import publish_latest_poo_status, record_poo
|
from app.services.poo import publish_latest_poo_status, record_poo
|
||||||
@@ -21,7 +21,7 @@ INTERNAL_SERVER_ERROR_MESSAGE = "internal server error"
|
|||||||
@router.post("/poo/record")
|
@router.post("/poo/record")
|
||||||
async def create_poo_record(
|
async def create_poo_record(
|
||||||
request: Request,
|
request: Request,
|
||||||
db: Session = Depends(get_poo_db),
|
db: Session = Depends(get_db),
|
||||||
settings: Settings = Depends(get_app_settings),
|
settings: Settings = Depends(get_app_settings),
|
||||||
homeassistant_client: HomeAssistantClient = Depends(get_homeassistant_client),
|
homeassistant_client: HomeAssistantClient = Depends(get_homeassistant_client),
|
||||||
) -> Response:
|
) -> Response:
|
||||||
@@ -56,7 +56,7 @@ async def create_poo_record(
|
|||||||
|
|
||||||
@router.get("/poo/latest")
|
@router.get("/poo/latest")
|
||||||
def notify_latest_poo(
|
def notify_latest_poo(
|
||||||
db: Session = Depends(get_poo_db),
|
db: Session = Depends(get_db),
|
||||||
settings: Settings = Depends(get_app_settings),
|
settings: Settings = Depends(get_app_settings),
|
||||||
homeassistant_client: HomeAssistantClient = Depends(get_homeassistant_client),
|
homeassistant_client: HomeAssistantClient = Depends(get_homeassistant_client),
|
||||||
) -> Response:
|
) -> Response:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.dependencies import get_auth_db, get_current_auth_session
|
from app.dependencies import get_db, get_current_auth_session
|
||||||
from app.schemas.public_ip import PublicIPCheckResponse
|
from app.schemas.public_ip import PublicIPCheckResponse
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
from app.services.auth import AuthenticatedSession
|
from app.services.auth import AuthenticatedSession
|
||||||
@@ -12,7 +12,7 @@ router = APIRouter(tags=["public-ip"])
|
|||||||
|
|
||||||
@router.get("/public-ip/check", response_model=PublicIPCheckResponse)
|
@router.get("/public-ip/check", response_model=PublicIPCheckResponse)
|
||||||
def run_public_ip_check(
|
def run_public_ip_check(
|
||||||
session: Session = Depends(get_auth_db),
|
session: Session = Depends(get_db),
|
||||||
current_auth: AuthenticatedSession | None = Depends(get_current_auth_session),
|
current_auth: AuthenticatedSession | None = Depends(get_current_auth_session),
|
||||||
) -> PublicIPCheckResponse:
|
) -> PublicIPCheckResponse:
|
||||||
if current_auth is None:
|
if current_auth is None:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from sqlalchemy.orm import Session
|
|||||||
from app.config import Settings
|
from app.config import Settings
|
||||||
from app.dependencies import (
|
from app.dependencies import (
|
||||||
get_app_settings,
|
get_app_settings,
|
||||||
get_auth_db,
|
get_db,
|
||||||
get_current_auth_session,
|
get_current_auth_session,
|
||||||
get_ticktick_client,
|
get_ticktick_client,
|
||||||
)
|
)
|
||||||
@@ -39,7 +39,7 @@ def start_ticktick_auth(
|
|||||||
@router.get("/ticktick/auth/code")
|
@router.get("/ticktick/auth/code")
|
||||||
def handle_ticktick_auth_code(
|
def handle_ticktick_auth_code(
|
||||||
request: Request,
|
request: Request,
|
||||||
auth_db_session: Session = Depends(get_auth_db),
|
auth_db_session: Session = Depends(get_db),
|
||||||
settings: Settings = Depends(get_app_settings),
|
settings: Settings = Depends(get_app_settings),
|
||||||
ticktick_client: TickTickClient = Depends(get_ticktick_client),
|
ticktick_client: TickTickClient = Depends(get_ticktick_client),
|
||||||
) -> Response:
|
) -> Response:
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
from collections.abc import Generator
|
|
||||||
from functools import lru_cache
|
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
|
||||||
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
|
|
||||||
|
|
||||||
from app.config import get_settings
|
|
||||||
|
|
||||||
|
|
||||||
class AuthBase(DeclarativeBase):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _build_connect_args(database_url: str) -> dict[str, object]:
|
|
||||||
connect_args: dict[str, object] = {}
|
|
||||||
if database_url.startswith("sqlite"):
|
|
||||||
connect_args["check_same_thread"] = False
|
|
||||||
return connect_args
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
|
||||||
def _get_auth_engine(database_url: str):
|
|
||||||
return create_engine(database_url, connect_args=_build_connect_args(database_url))
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
|
||||||
def _get_auth_session_local(database_url: str):
|
|
||||||
engine = _get_auth_engine(database_url)
|
|
||||||
return sessionmaker(bind=engine, autoflush=False, autocommit=False, class_=Session)
|
|
||||||
|
|
||||||
|
|
||||||
def get_auth_engine():
|
|
||||||
settings = get_settings()
|
|
||||||
return _get_auth_engine(settings.app_database_url)
|
|
||||||
|
|
||||||
|
|
||||||
def get_auth_session_local():
|
|
||||||
settings = get_settings()
|
|
||||||
return _get_auth_session_local(settings.app_database_url)
|
|
||||||
|
|
||||||
|
|
||||||
def reset_auth_db_caches() -> None:
|
|
||||||
_get_auth_session_local.cache_clear()
|
|
||||||
_get_auth_engine.cache_clear()
|
|
||||||
|
|
||||||
|
|
||||||
def get_auth_db_session() -> Generator[Session, None, None]:
|
|
||||||
session_local = get_auth_session_local()
|
|
||||||
session = session_local()
|
|
||||||
try:
|
|
||||||
yield session
|
|
||||||
finally:
|
|
||||||
session.close()
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine, event
|
||||||
|
from sqlalchemy.engine import Engine
|
||||||
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
|
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
|
||||||
|
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
@@ -10,18 +12,49 @@ class Base(DeclarativeBase):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
settings = get_settings()
|
def _build_connect_args(database_url: str) -> dict[str, object]:
|
||||||
|
connect_args: dict[str, object] = {}
|
||||||
|
if database_url.startswith("sqlite"):
|
||||||
|
connect_args["check_same_thread"] = False
|
||||||
|
return connect_args
|
||||||
|
|
||||||
connect_args: dict[str, object] = {}
|
|
||||||
if settings.location_database_url.startswith("sqlite"):
|
|
||||||
connect_args["check_same_thread"] = False
|
|
||||||
|
|
||||||
engine = create_engine(settings.location_database_url, connect_args=connect_args)
|
@lru_cache
|
||||||
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, class_=Session)
|
def _get_engine(database_url: str) -> Engine:
|
||||||
|
engine = create_engine(database_url, connect_args=_build_connect_args(database_url))
|
||||||
|
if database_url.startswith("sqlite"):
|
||||||
|
|
||||||
|
@event.listens_for(engine, "connect")
|
||||||
|
def _enable_sqlite_wal(dbapi_connection, _connection_record):
|
||||||
|
cursor = dbapi_connection.cursor()
|
||||||
|
cursor.execute("PRAGMA journal_mode=WAL")
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
return engine
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
|
def _get_session_local(database_url: str) -> sessionmaker:
|
||||||
|
engine = _get_engine(database_url)
|
||||||
|
return sessionmaker(bind=engine, autoflush=False, autocommit=False, class_=Session)
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine() -> Engine:
|
||||||
|
return _get_engine(get_settings().app_database_url)
|
||||||
|
|
||||||
|
|
||||||
|
def get_session_local() -> sessionmaker:
|
||||||
|
return _get_session_local(get_settings().app_database_url)
|
||||||
|
|
||||||
|
|
||||||
|
def reset_db_caches() -> None:
|
||||||
|
_get_session_local.cache_clear()
|
||||||
|
_get_engine.cache_clear()
|
||||||
|
|
||||||
|
|
||||||
def get_db_session() -> Generator[Session, None, None]:
|
def get_db_session() -> Generator[Session, None, None]:
|
||||||
session = SessionLocal()
|
session_local = get_session_local()
|
||||||
|
session = session_local()
|
||||||
try:
|
try:
|
||||||
yield session
|
yield session
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
+3
-13
@@ -3,30 +3,20 @@ from collections.abc import Generator
|
|||||||
from fastapi import Depends, Request
|
from fastapi import Depends, Request
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.auth_db import get_auth_db_session
|
|
||||||
from app.config import Settings, get_settings
|
from app.config import Settings, get_settings
|
||||||
from app.db import get_db_session
|
from app.db import get_db_session
|
||||||
from app.integrations.homeassistant import HomeAssistantClient
|
from app.integrations.homeassistant import HomeAssistantClient
|
||||||
from app.integrations.ticktick import TickTickClient
|
from app.integrations.ticktick import TickTickClient
|
||||||
from app.poo_db import get_poo_db_session
|
|
||||||
from app.services.auth import AuthenticatedSession, get_authenticated_session
|
from app.services.auth import AuthenticatedSession, get_authenticated_session
|
||||||
from app.services.config_page import build_runtime_settings
|
from app.services.config_page import build_runtime_settings
|
||||||
|
|
||||||
|
|
||||||
def get_auth_db() -> Generator[Session, None, None]:
|
|
||||||
yield from get_auth_db_session()
|
|
||||||
|
|
||||||
|
|
||||||
def get_app_settings(session: Session = Depends(get_auth_db)) -> Settings:
|
|
||||||
return build_runtime_settings(session, get_settings())
|
|
||||||
|
|
||||||
|
|
||||||
def get_db() -> Generator[Session, None, None]:
|
def get_db() -> Generator[Session, None, None]:
|
||||||
yield from get_db_session()
|
yield from get_db_session()
|
||||||
|
|
||||||
|
|
||||||
def get_poo_db() -> Generator[Session, None, None]:
|
def get_app_settings(session: Session = Depends(get_db)) -> Settings:
|
||||||
yield from get_poo_db_session()
|
return build_runtime_settings(session, get_settings())
|
||||||
|
|
||||||
|
|
||||||
def get_homeassistant_client(settings: Settings = Depends(get_app_settings)) -> HomeAssistantClient:
|
def get_homeassistant_client(settings: Settings = Depends(get_app_settings)) -> HomeAssistantClient:
|
||||||
@@ -39,7 +29,7 @@ def get_ticktick_client(settings: Settings = Depends(get_app_settings)) -> TickT
|
|||||||
|
|
||||||
def get_current_auth_session(
|
def get_current_auth_session(
|
||||||
request: Request,
|
request: Request,
|
||||||
session: Session = Depends(get_auth_db),
|
session: Session = Depends(get_db),
|
||||||
settings: Settings = Depends(get_app_settings),
|
settings: Settings = Depends(get_app_settings),
|
||||||
) -> AuthenticatedSession | None:
|
) -> AuthenticatedSession | None:
|
||||||
raw_token = request.cookies.get(settings.auth_session_cookie_name)
|
raw_token = request.cookies.get(settings.auth_session_cookie_name)
|
||||||
|
|||||||
+3
-3
@@ -10,7 +10,7 @@ from sqlalchemy.orm import Session
|
|||||||
from app import models # noqa: F401
|
from app import models # noqa: F401
|
||||||
from app.api.routes.auth import router as auth_router
|
from app.api.routes.auth import router as auth_router
|
||||||
from app.api.routes import pages, status
|
from app.api.routes import pages, status
|
||||||
import app.auth_db as auth_db
|
from app.db import get_session_local
|
||||||
from app.api.routes.homeassistant import router as homeassistant_router
|
from app.api.routes.homeassistant import router as homeassistant_router
|
||||||
from app.api.routes.location import router as location_router
|
from app.api.routes.location import router as location_router
|
||||||
from app.api.routes.poo import router as poo_router
|
from app.api.routes.poo import router as poo_router
|
||||||
@@ -26,7 +26,7 @@ from scripts.poo_db_adopt import PooDatabaseAdoptionError, validate_poo_runtime_
|
|||||||
|
|
||||||
|
|
||||||
def _run_scheduled_public_ip_check() -> None:
|
def _run_scheduled_public_ip_check() -> None:
|
||||||
session_local = auth_db.get_auth_session_local()
|
session_local = get_session_local()
|
||||||
session: Session = session_local()
|
session: Session = session_local()
|
||||||
try:
|
try:
|
||||||
check_public_ipv4_and_notify(session, bootstrap_settings=get_settings())
|
check_public_ipv4_and_notify(session, bootstrap_settings=get_settings())
|
||||||
@@ -35,7 +35,7 @@ def _run_scheduled_public_ip_check() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def ensure_auth_db_ready() -> None:
|
def ensure_auth_db_ready() -> None:
|
||||||
session_local = auth_db.get_auth_session_local()
|
session_local = get_session_local()
|
||||||
session: Session = session_local()
|
session: Session = session_local()
|
||||||
try:
|
try:
|
||||||
validate_app_runtime_db(get_settings().app_database_url)
|
validate_app_runtime_db(get_settings().app_database_url)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from app.models.auth import AuthSession, AuthUser
|
from app.models.auth import AuthSession, AuthUser
|
||||||
from app.models.config import AppConfigEntry
|
from app.models.config import AppConfigEntry
|
||||||
from app.models.location import Location
|
from app.models.location import Location
|
||||||
|
from app.models.poo import PooRecord
|
||||||
from app.models.public_ip import PublicIPHistory, PublicIPState
|
from app.models.public_ip import PublicIPHistory, PublicIPState
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -10,6 +11,7 @@ __all__ = [
|
|||||||
"AuthSession",
|
"AuthSession",
|
||||||
"AuthUser",
|
"AuthUser",
|
||||||
"Location",
|
"Location",
|
||||||
|
"PooRecord",
|
||||||
"PublicIPHistory",
|
"PublicIPHistory",
|
||||||
"PublicIPState",
|
"PublicIPState",
|
||||||
]
|
]
|
||||||
|
|||||||
+3
-3
@@ -3,10 +3,10 @@ from datetime import datetime
|
|||||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String
|
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from app.auth_db import AuthBase
|
from app.db import Base
|
||||||
|
|
||||||
|
|
||||||
class AuthUser(AuthBase):
|
class AuthUser(Base):
|
||||||
__tablename__ = "auth_users"
|
__tablename__ = "auth_users"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
@@ -19,7 +19,7 @@ class AuthUser(AuthBase):
|
|||||||
sessions: Mapped[list["AuthSession"]] = relationship(back_populates="user")
|
sessions: Mapped[list["AuthSession"]] = relationship(back_populates="user")
|
||||||
|
|
||||||
|
|
||||||
class AuthSession(AuthBase):
|
class AuthSession(Base):
|
||||||
__tablename__ = "auth_sessions"
|
__tablename__ = "auth_sessions"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
from app.db import Base
|
|
||||||
|
|
||||||
__all__ = ["Base"]
|
|
||||||
|
|
||||||
@@ -3,10 +3,10 @@ from datetime import datetime
|
|||||||
from sqlalchemy import DateTime, Integer, String
|
from sqlalchemy import DateTime, Integer, String
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from app.auth_db import AuthBase
|
from app.db import Base
|
||||||
|
|
||||||
|
|
||||||
class AppConfigEntry(AuthBase):
|
class AppConfigEntry(Base):
|
||||||
__tablename__ = "app_config"
|
__tablename__ = "app_config"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
|||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
from sqlalchemy import Float, String
|
from sqlalchemy import Float, String
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from app.poo_db import PooBase
|
from app.db import Base
|
||||||
|
|
||||||
|
|
||||||
class PooRecord(PooBase):
|
class PooRecord(Base):
|
||||||
__tablename__ = "poo_records"
|
__tablename__ = "poo_records"
|
||||||
|
|
||||||
timestamp: Mapped[str] = mapped_column(String, primary_key=True)
|
timestamp: Mapped[str] = mapped_column(String, primary_key=True)
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ from datetime import datetime
|
|||||||
from sqlalchemy import DateTime, Integer, String
|
from sqlalchemy import DateTime, Integer, String
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from app.auth_db import AuthBase
|
from app.db import Base
|
||||||
|
|
||||||
|
|
||||||
class PublicIPState(AuthBase):
|
class PublicIPState(Base):
|
||||||
__tablename__ = "public_ip_state"
|
__tablename__ = "public_ip_state"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
@@ -20,7 +20,7 @@ class PublicIPState(AuthBase):
|
|||||||
last_provider: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
last_provider: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||||
|
|
||||||
|
|
||||||
class PublicIPHistory(AuthBase):
|
class PublicIPHistory(Base):
|
||||||
__tablename__ = "public_ip_history"
|
__tablename__ = "public_ip_history"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
from collections.abc import Generator
|
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
|
||||||
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
|
|
||||||
|
|
||||||
from app.config import get_settings
|
|
||||||
|
|
||||||
|
|
||||||
class PooBase(DeclarativeBase):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
settings = get_settings()
|
|
||||||
|
|
||||||
connect_args: dict[str, object] = {}
|
|
||||||
if settings.poo_database_url.startswith("sqlite"):
|
|
||||||
connect_args["check_same_thread"] = False
|
|
||||||
|
|
||||||
poo_engine = create_engine(settings.poo_database_url, connect_args=connect_args)
|
|
||||||
PooSessionLocal = sessionmaker(bind=poo_engine, autoflush=False, autocommit=False, class_=Session)
|
|
||||||
|
|
||||||
|
|
||||||
def get_poo_db_session() -> Generator[Session, None, None]:
|
|
||||||
session = PooSessionLocal()
|
|
||||||
try:
|
|
||||||
yield session
|
|
||||||
finally:
|
|
||||||
session.close()
|
|
||||||
@@ -7,7 +7,7 @@ from typing import Any
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.auth_db import reset_auth_db_caches
|
from app.db import reset_db_caches
|
||||||
from app.config import Settings, get_settings
|
from app.config import Settings, get_settings
|
||||||
from app.models.config import AppConfigEntry
|
from app.models.config import AppConfigEntry
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ def sync_app_hostname_from_bootstrap(session: Session, bootstrap_settings: Setti
|
|||||||
current_values["APP_HOSTNAME"] = bootstrap_hostname
|
current_values["APP_HOSTNAME"] = bootstrap_hostname
|
||||||
_persist_config_values(session, current_values)
|
_persist_config_values(session, current_values)
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|
||||||
|
|
||||||
def build_runtime_settings(session: Session, bootstrap_settings: Settings) -> Settings:
|
def build_runtime_settings(session: Session, bootstrap_settings: Settings) -> Settings:
|
||||||
@@ -184,7 +184,7 @@ def save_config_updates(session: Session, form_data: dict[str, str], bootstrap_s
|
|||||||
_validate_config_values(merged_values, bootstrap_settings)
|
_validate_config_values(merged_values, bootstrap_settings)
|
||||||
_persist_config_values(session, merged_values)
|
_persist_config_values(session, merged_values)
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|
||||||
|
|
||||||
def save_config_value(
|
def save_config_value(
|
||||||
@@ -199,7 +199,7 @@ def save_config_value(
|
|||||||
_validate_config_values(current_values, bootstrap_settings)
|
_validate_config_values(current_values, bootstrap_settings)
|
||||||
_persist_config_values(session, current_values)
|
_persist_config_values(session, current_values)
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|
||||||
|
|
||||||
def is_ticktick_oauth_ready(settings: Settings) -> bool:
|
def is_ticktick_oauth_ready(settings: Settings) -> bool:
|
||||||
|
|||||||
+11
-39
@@ -5,10 +5,8 @@ from alembic import command
|
|||||||
from alembic.config import Config
|
from alembic.config import Config
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
|
|
||||||
from app.auth_db import reset_auth_db_caches
|
from app.db import reset_db_caches
|
||||||
import app.db as app_db
|
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
from app.main import create_app
|
from app.main import create_app
|
||||||
|
|
||||||
@@ -47,7 +45,7 @@ def test_database_urls(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
|||||||
monkeypatch.setenv("AUTH_BOOTSTRAP_PASSWORD", "test-password")
|
monkeypatch.setenv("AUTH_BOOTSTRAP_PASSWORD", "test-password")
|
||||||
monkeypatch.setenv("AUTH_COOKIE_SECURE_OVERRIDE", "false")
|
monkeypatch.setenv("AUTH_COOKIE_SECURE_OVERRIDE", "false")
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield {
|
yield {
|
||||||
@@ -60,7 +58,7 @@ def test_database_urls(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
|||||||
}
|
}
|
||||||
finally:
|
finally:
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -79,10 +77,10 @@ def ready_poo_database(test_database_urls):
|
|||||||
def auth_database(test_database_urls, monkeypatch: pytest.MonkeyPatch):
|
def auth_database(test_database_urls, monkeypatch: pytest.MonkeyPatch):
|
||||||
database_url = test_database_urls["app_url"]
|
database_url = test_database_urls["app_url"]
|
||||||
command.upgrade(_make_app_alembic_config(database_url), "head")
|
command.upgrade(_make_app_alembic_config(database_url), "head")
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|
||||||
yield test_database_urls
|
yield test_database_urls
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -97,46 +95,20 @@ def client(app):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def location_client(
|
def location_client(ready_location_database, ready_poo_database, auth_database):
|
||||||
ready_location_database,
|
app_url = auth_database["app_url"]
|
||||||
ready_poo_database,
|
engine = create_engine(app_url, connect_args={"check_same_thread": False})
|
||||||
auth_database,
|
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
|
||||||
):
|
|
||||||
database_url = ready_location_database["location_url"]
|
|
||||||
|
|
||||||
engine = create_engine(database_url, connect_args={"check_same_thread": False})
|
|
||||||
session_local = sessionmaker(bind=engine, autoflush=False, autocommit=False)
|
|
||||||
|
|
||||||
monkeypatch.setattr(app_db, "engine", engine)
|
|
||||||
monkeypatch.setattr(app_db, "SessionLocal", session_local)
|
|
||||||
|
|
||||||
fastapi_app = create_app()
|
fastapi_app = create_app()
|
||||||
with TestClient(fastapi_app) as client:
|
with TestClient(fastapi_app) as client:
|
||||||
yield client, engine
|
yield client, engine
|
||||||
|
|
||||||
engine.dispose()
|
engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def poo_client(
|
def poo_client(ready_location_database, ready_poo_database, auth_database):
|
||||||
ready_location_database,
|
app_url = auth_database["app_url"]
|
||||||
ready_poo_database,
|
engine = create_engine(app_url, connect_args={"check_same_thread": False})
|
||||||
auth_database,
|
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
|
||||||
):
|
|
||||||
database_url = ready_poo_database["poo_url"]
|
|
||||||
|
|
||||||
engine = create_engine(database_url, connect_args={"check_same_thread": False})
|
|
||||||
session_local = sessionmaker(bind=engine, autoflush=False, autocommit=False)
|
|
||||||
|
|
||||||
import app.poo_db as poo_db
|
|
||||||
|
|
||||||
monkeypatch.setattr(poo_db, "poo_engine", engine)
|
|
||||||
monkeypatch.setattr(poo_db, "PooSessionLocal", session_local)
|
|
||||||
|
|
||||||
fastapi_app = create_app()
|
fastapi_app = create_app()
|
||||||
with TestClient(fastapi_app) as client:
|
with TestClient(fastapi_app) as client:
|
||||||
yield client, engine
|
yield client, engine
|
||||||
|
|
||||||
engine.dispose()
|
engine.dispose()
|
||||||
|
|||||||
+13
-13
@@ -5,7 +5,7 @@ import pytest
|
|||||||
from alembic import command
|
from alembic import command
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from app.auth_db import reset_auth_db_caches
|
from app.db import reset_db_caches
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
from app.main import create_app
|
from app.main import create_app
|
||||||
from scripts.app_db_adopt import APP_BASELINE_REVISION, adopt_or_initialize_app_db
|
from scripts.app_db_adopt import APP_BASELINE_REVISION, adopt_or_initialize_app_db
|
||||||
@@ -49,7 +49,7 @@ def test_app_start_fails_when_app_db_missing(tmp_path, monkeypatch: pytest.Monke
|
|||||||
monkeypatch.setenv("LOCATION_DATABASE_URL", f"sqlite:///{location_database_path}")
|
monkeypatch.setenv("LOCATION_DATABASE_URL", f"sqlite:///{location_database_path}")
|
||||||
monkeypatch.setenv("POO_DATABASE_URL", f"sqlite:///{poo_database_path}")
|
monkeypatch.setenv("POO_DATABASE_URL", f"sqlite:///{poo_database_path}")
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
with pytest.raises(RuntimeError, match="Run 'python scripts/app_db_adopt.py' first"):
|
with pytest.raises(RuntimeError, match="Run 'python scripts/app_db_adopt.py' first"):
|
||||||
@@ -58,7 +58,7 @@ def test_app_start_fails_when_app_db_missing(tmp_path, monkeypatch: pytest.Monke
|
|||||||
assert not missing_app_path.exists()
|
assert not missing_app_path.exists()
|
||||||
|
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|
||||||
|
|
||||||
def test_app_db_adoption_initializes_new_database(tmp_path) -> None:
|
def test_app_db_adoption_initializes_new_database(tmp_path) -> None:
|
||||||
@@ -108,7 +108,7 @@ def test_app_start_seeds_missing_config_from_env_without_overwriting_existing_va
|
|||||||
monkeypatch.setenv("LOCATION_DATABASE_URL", f"sqlite:///{location_database_path}")
|
monkeypatch.setenv("LOCATION_DATABASE_URL", f"sqlite:///{location_database_path}")
|
||||||
monkeypatch.setenv("POO_DATABASE_URL", f"sqlite:///{poo_database_path}")
|
monkeypatch.setenv("POO_DATABASE_URL", f"sqlite:///{poo_database_path}")
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
anyio.run(_run_lifespan, app)
|
anyio.run(_run_lifespan, app)
|
||||||
@@ -124,7 +124,7 @@ def test_app_start_seeds_missing_config_from_env_without_overwriting_existing_va
|
|||||||
assert rows["AUTH_SESSION_COOKIE_NAME"] == "home_automation_session"
|
assert rows["AUTH_SESSION_COOKIE_NAME"] == "home_automation_session"
|
||||||
|
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|
||||||
|
|
||||||
def test_app_start_syncs_app_hostname_from_env_even_when_db_has_old_value(
|
def test_app_start_syncs_app_hostname_from_env_even_when_db_has_old_value(
|
||||||
@@ -152,7 +152,7 @@ def test_app_start_syncs_app_hostname_from_env_even_when_db_has_old_value(
|
|||||||
monkeypatch.setenv("LOCATION_DATABASE_URL", f"sqlite:///{location_database_path}")
|
monkeypatch.setenv("LOCATION_DATABASE_URL", f"sqlite:///{location_database_path}")
|
||||||
monkeypatch.setenv("POO_DATABASE_URL", f"sqlite:///{poo_database_path}")
|
monkeypatch.setenv("POO_DATABASE_URL", f"sqlite:///{poo_database_path}")
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
anyio.run(_run_lifespan, app)
|
anyio.run(_run_lifespan, app)
|
||||||
@@ -166,7 +166,7 @@ def test_app_start_syncs_app_hostname_from_env_even_when_db_has_old_value(
|
|||||||
assert rows["APP_HOSTNAME"] == "new.example.com"
|
assert rows["APP_HOSTNAME"] == "new.example.com"
|
||||||
|
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|
||||||
|
|
||||||
def test_app_start_fails_when_location_db_missing(
|
def test_app_start_fails_when_location_db_missing(
|
||||||
@@ -182,14 +182,14 @@ def test_app_start_fails_when_location_db_missing(
|
|||||||
monkeypatch.setenv("LOCATION_DATABASE_URL", f"sqlite:///{tmp_path / 'missing.db'}")
|
monkeypatch.setenv("LOCATION_DATABASE_URL", f"sqlite:///{tmp_path / 'missing.db'}")
|
||||||
monkeypatch.setenv("POO_DATABASE_URL", f"sqlite:///{poo_database_path}")
|
monkeypatch.setenv("POO_DATABASE_URL", f"sqlite:///{poo_database_path}")
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
with pytest.raises(RuntimeError, match="Run 'python scripts/location_db_adopt.py' first"):
|
with pytest.raises(RuntimeError, match="Run 'python scripts/location_db_adopt.py' first"):
|
||||||
anyio.run(_run_lifespan, app)
|
anyio.run(_run_lifespan, app)
|
||||||
|
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|
||||||
|
|
||||||
def test_app_start_fails_when_location_db_exists_but_is_not_adopted(
|
def test_app_start_fails_when_location_db_exists_but_is_not_adopted(
|
||||||
@@ -223,14 +223,14 @@ def test_app_start_fails_when_location_db_exists_but_is_not_adopted(
|
|||||||
monkeypatch.setenv("LOCATION_DATABASE_URL", f"sqlite:///{database_path}")
|
monkeypatch.setenv("LOCATION_DATABASE_URL", f"sqlite:///{database_path}")
|
||||||
monkeypatch.setenv("POO_DATABASE_URL", f"sqlite:///{poo_database_path}")
|
monkeypatch.setenv("POO_DATABASE_URL", f"sqlite:///{poo_database_path}")
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
with pytest.raises(RuntimeError, match="is not yet Alembic-managed"):
|
with pytest.raises(RuntimeError, match="is not yet Alembic-managed"):
|
||||||
anyio.run(_run_lifespan, app)
|
anyio.run(_run_lifespan, app)
|
||||||
|
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|
||||||
|
|
||||||
def test_app_start_fails_when_location_db_revision_mismatches(
|
def test_app_start_fails_when_location_db_revision_mismatches(
|
||||||
@@ -254,11 +254,11 @@ def test_app_start_fails_when_location_db_revision_mismatches(
|
|||||||
monkeypatch.setenv("LOCATION_DATABASE_URL", f"sqlite:///{database_path}")
|
monkeypatch.setenv("LOCATION_DATABASE_URL", f"sqlite:///{database_path}")
|
||||||
monkeypatch.setenv("POO_DATABASE_URL", f"sqlite:///{poo_database_path}")
|
monkeypatch.setenv("POO_DATABASE_URL", f"sqlite:///{poo_database_path}")
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
with pytest.raises(RuntimeError, match="Location DB revision mismatch"):
|
with pytest.raises(RuntimeError, match="Location DB revision mismatch"):
|
||||||
anyio.run(_run_lifespan, app)
|
anyio.run(_run_lifespan, app)
|
||||||
|
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from app.auth_db import reset_auth_db_caches
|
from app.db import reset_db_caches
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
from app.main import create_app
|
from app.main import create_app
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ def test_config_page_shows_ticktick_oauth_link_when_ticktick_is_configured(
|
|||||||
monkeypatch.setenv("TICKTICK_CLIENT_ID", "ticktick-client-id")
|
monkeypatch.setenv("TICKTICK_CLIENT_ID", "ticktick-client-id")
|
||||||
monkeypatch.setenv("TICKTICK_CLIENT_SECRET", "ticktick-client-secret")
|
monkeypatch.setenv("TICKTICK_CLIENT_SECRET", "ticktick-client-secret")
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|
||||||
with TestClient(create_app()) as client:
|
with TestClient(create_app()) as client:
|
||||||
login_page = client.get("/login")
|
login_page = client.get("/login")
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import pytest
|
|||||||
import yaml
|
import yaml
|
||||||
from alembic import command
|
from alembic import command
|
||||||
|
|
||||||
from app.auth_db import reset_auth_db_caches
|
from app.db import reset_db_caches
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
from app.main import create_app
|
from app.main import create_app
|
||||||
from scripts.app_db_adopt import APP_BASELINE_REVISION
|
from scripts.app_db_adopt import APP_BASELINE_REVISION
|
||||||
@@ -41,7 +41,7 @@ def _configure_database_env(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) ->
|
|||||||
monkeypatch.setenv("AUTH_BOOTSTRAP_PASSWORD", "test-password")
|
monkeypatch.setenv("AUTH_BOOTSTRAP_PASSWORD", "test-password")
|
||||||
monkeypatch.setenv("AUTH_COOKIE_SECURE_OVERRIDE", "false")
|
monkeypatch.setenv("AUTH_COOKIE_SECURE_OVERRIDE", "false")
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"app_path": app_path,
|
"app_path": app_path,
|
||||||
@@ -165,7 +165,7 @@ def test_migration_runner_initializes_and_is_idempotent(
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|
||||||
|
|
||||||
def test_migration_runner_adopts_legacy_sqlite_without_data_loss(
|
def test_migration_runner_adopts_legacy_sqlite_without_data_loss(
|
||||||
@@ -194,7 +194,7 @@ def test_migration_runner_adopts_legacy_sqlite_without_data_loss(
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|
||||||
|
|
||||||
def test_app_startup_still_fails_closed_without_running_adoption(
|
def test_app_startup_still_fails_closed_without_running_adoption(
|
||||||
@@ -212,4 +212,4 @@ def test_app_startup_still_fails_closed_without_running_adoption(
|
|||||||
assert not Path(missing_app_path).exists()
|
assert not Path(missing_app_path).exists()
|
||||||
|
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
import app.db as app_db
|
|
||||||
import app.poo_db as poo_db
|
|
||||||
from app.config import Settings, get_settings
|
from app.config import Settings, get_settings
|
||||||
from app.dependencies import get_app_settings, get_homeassistant_client
|
from app.dependencies import get_app_settings, get_homeassistant_client
|
||||||
from app.main import create_app
|
from app.main import create_app
|
||||||
@@ -161,42 +159,24 @@ def test_homeassistant_publish_poo_get_latest_publishes_latest_status(
|
|||||||
ready_location_database,
|
ready_location_database,
|
||||||
ready_poo_database,
|
ready_poo_database,
|
||||||
auth_database,
|
auth_database,
|
||||||
monkeypatch,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
location_engine = app_db.create_engine(
|
from fastapi.testclient import TestClient
|
||||||
ready_location_database["location_url"],
|
from sqlalchemy import create_engine
|
||||||
connect_args={"check_same_thread": False},
|
|
||||||
)
|
app_url = auth_database["app_url"]
|
||||||
location_session_local = app_db.sessionmaker(
|
engine = create_engine(app_url, connect_args={"check_same_thread": False})
|
||||||
bind=location_engine,
|
|
||||||
autoflush=False,
|
|
||||||
autocommit=False,
|
|
||||||
)
|
|
||||||
poo_engine = poo_db.create_engine(
|
|
||||||
ready_poo_database["poo_url"],
|
|
||||||
connect_args={"check_same_thread": False},
|
|
||||||
)
|
|
||||||
poo_session_local = poo_db.sessionmaker(
|
|
||||||
bind=poo_engine,
|
|
||||||
autoflush=False,
|
|
||||||
autocommit=False,
|
|
||||||
)
|
|
||||||
fake_ha = _FakeHomeAssistantClient()
|
fake_ha = _FakeHomeAssistantClient()
|
||||||
settings = Settings(
|
settings = Settings(
|
||||||
poo_sensor_entity_name="sensor.test_poo_status",
|
poo_sensor_entity_name="sensor.test_poo_status",
|
||||||
poo_sensor_friendly_name="Poo Status",
|
poo_sensor_friendly_name="Poo Status",
|
||||||
)
|
)
|
||||||
|
|
||||||
monkeypatch.setattr(app_db, "engine", location_engine)
|
|
||||||
monkeypatch.setattr(app_db, "SessionLocal", location_session_local)
|
|
||||||
monkeypatch.setattr(poo_db, "poo_engine", poo_engine)
|
|
||||||
monkeypatch.setattr(poo_db, "PooSessionLocal", poo_session_local)
|
|
||||||
|
|
||||||
test_app = create_app()
|
test_app = create_app()
|
||||||
test_app.dependency_overrides[get_homeassistant_client] = lambda: fake_ha
|
test_app.dependency_overrides[get_homeassistant_client] = lambda: fake_ha
|
||||||
test_app.dependency_overrides[get_app_settings] = lambda: settings
|
test_app.dependency_overrides[get_app_settings] = lambda: settings
|
||||||
|
|
||||||
with poo_engine.begin() as conn:
|
with engine.begin() as conn:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
text(
|
text(
|
||||||
"INSERT INTO poo_records (timestamp, status, latitude, longitude) "
|
"INSERT INTO poo_records (timestamp, status, latitude, longitude) "
|
||||||
@@ -211,8 +191,6 @@ def test_homeassistant_publish_poo_get_latest_publishes_latest_status(
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from fastapi.testclient import TestClient
|
|
||||||
|
|
||||||
with TestClient(test_app) as client:
|
with TestClient(test_app) as client:
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/homeassistant/publish",
|
"/homeassistant/publish",
|
||||||
@@ -233,52 +211,27 @@ def test_homeassistant_publish_poo_get_latest_publishes_latest_status(
|
|||||||
finally:
|
finally:
|
||||||
test_app.dependency_overrides.clear()
|
test_app.dependency_overrides.clear()
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
location_engine.dispose()
|
engine.dispose()
|
||||||
poo_engine.dispose()
|
|
||||||
|
|
||||||
|
|
||||||
def test_homeassistant_publish_returns_internal_error_for_unknown_poo_action(
|
def test_homeassistant_publish_returns_internal_error_for_unknown_poo_action(
|
||||||
ready_location_database,
|
ready_location_database,
|
||||||
ready_poo_database,
|
ready_poo_database,
|
||||||
auth_database,
|
auth_database,
|
||||||
monkeypatch,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
location_engine = app_db.create_engine(
|
from fastapi.testclient import TestClient
|
||||||
ready_location_database["location_url"],
|
|
||||||
connect_args={"check_same_thread": False},
|
|
||||||
)
|
|
||||||
location_session_local = app_db.sessionmaker(
|
|
||||||
bind=location_engine,
|
|
||||||
autoflush=False,
|
|
||||||
autocommit=False,
|
|
||||||
)
|
|
||||||
poo_engine = poo_db.create_engine(
|
|
||||||
ready_poo_database["poo_url"],
|
|
||||||
connect_args={"check_same_thread": False},
|
|
||||||
)
|
|
||||||
poo_session_local = poo_db.sessionmaker(
|
|
||||||
bind=poo_engine,
|
|
||||||
autoflush=False,
|
|
||||||
autocommit=False,
|
|
||||||
)
|
|
||||||
fake_ha = _FakeHomeAssistantClient()
|
fake_ha = _FakeHomeAssistantClient()
|
||||||
settings = Settings(
|
settings = Settings(
|
||||||
poo_sensor_entity_name="sensor.test_poo_status",
|
poo_sensor_entity_name="sensor.test_poo_status",
|
||||||
poo_sensor_friendly_name="Poo Status",
|
poo_sensor_friendly_name="Poo Status",
|
||||||
)
|
)
|
||||||
|
|
||||||
monkeypatch.setattr(app_db, "engine", location_engine)
|
|
||||||
monkeypatch.setattr(app_db, "SessionLocal", location_session_local)
|
|
||||||
monkeypatch.setattr(poo_db, "poo_engine", poo_engine)
|
|
||||||
monkeypatch.setattr(poo_db, "PooSessionLocal", poo_session_local)
|
|
||||||
|
|
||||||
test_app = create_app()
|
test_app = create_app()
|
||||||
test_app.dependency_overrides[get_homeassistant_client] = lambda: fake_ha
|
test_app.dependency_overrides[get_homeassistant_client] = lambda: fake_ha
|
||||||
test_app.dependency_overrides[get_app_settings] = lambda: settings
|
test_app.dependency_overrides[get_app_settings] = lambda: settings
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from fastapi.testclient import TestClient
|
|
||||||
|
|
||||||
with TestClient(test_app) as client:
|
with TestClient(test_app) as client:
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/homeassistant/publish",
|
"/homeassistant/publish",
|
||||||
@@ -295,8 +248,6 @@ def test_homeassistant_publish_returns_internal_error_for_unknown_poo_action(
|
|||||||
finally:
|
finally:
|
||||||
test_app.dependency_overrides.clear()
|
test_app.dependency_overrides.clear()
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
location_engine.dispose()
|
|
||||||
poo_engine.dispose()
|
|
||||||
|
|
||||||
|
|
||||||
def test_homeassistant_publish_returns_not_implemented_for_unknown_location_action(
|
def test_homeassistant_publish_returns_not_implemented_for_unknown_location_action(
|
||||||
|
|||||||
+1
-65
@@ -5,18 +5,14 @@ import sqlite3
|
|||||||
import pytest
|
import pytest
|
||||||
from alembic import command
|
from alembic import command
|
||||||
from alembic.config import Config
|
from alembic.config import Config
|
||||||
from sqlalchemy import create_engine, text
|
from sqlalchemy import text
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
|
|
||||||
import app.db as app_db
|
|
||||||
from app.main import create_app
|
|
||||||
from scripts.location_db_adopt import (
|
from scripts.location_db_adopt import (
|
||||||
EXPECTED_USER_VERSION,
|
EXPECTED_USER_VERSION,
|
||||||
LOCATION_BASELINE_REVISION,
|
LOCATION_BASELINE_REVISION,
|
||||||
LocationDatabaseAdoptionError,
|
LocationDatabaseAdoptionError,
|
||||||
adopt_or_initialize_location_db,
|
adopt_or_initialize_location_db,
|
||||||
)
|
)
|
||||||
from tests.conftest import _make_app_alembic_config, _make_poo_alembic_config
|
|
||||||
|
|
||||||
|
|
||||||
def _make_alembic_config(database_url: str) -> Config:
|
def _make_alembic_config(database_url: str) -> Config:
|
||||||
@@ -197,66 +193,6 @@ def test_location_record_endpoint_defaults_invalid_altitude_to_zero(location_cli
|
|||||||
assert row.altitude == pytest.approx(0.0)
|
assert row.altitude == pytest.approx(0.0)
|
||||||
|
|
||||||
|
|
||||||
def test_legacy_style_location_db_can_be_stamped_and_adopted(
|
|
||||||
test_database_urls, monkeypatch: pytest.MonkeyPatch
|
|
||||||
) -> None:
|
|
||||||
app_database_url = test_database_urls["app_url"]
|
|
||||||
database_path = test_database_urls["location_path"]
|
|
||||||
database_url = test_database_urls["location_url"]
|
|
||||||
poo_database_url = test_database_urls["poo_url"]
|
|
||||||
|
|
||||||
conn = sqlite3.connect(database_path)
|
|
||||||
conn.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE location (
|
|
||||||
person TEXT NOT NULL,
|
|
||||||
datetime TEXT NOT NULL,
|
|
||||||
latitude REAL NOT NULL,
|
|
||||||
longitude REAL NOT NULL,
|
|
||||||
altitude REAL,
|
|
||||||
PRIMARY KEY (person, datetime)
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
conn.execute("PRAGMA user_version = 2")
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
command.upgrade(_make_app_alembic_config(app_database_url), "head")
|
|
||||||
command.stamp(_make_alembic_config(database_url), LOCATION_BASELINE_REVISION)
|
|
||||||
command.upgrade(_make_poo_alembic_config(poo_database_url), "head")
|
|
||||||
|
|
||||||
engine = create_engine(database_url, connect_args={"check_same_thread": False})
|
|
||||||
session_local = sessionmaker(bind=engine, autoflush=False, autocommit=False)
|
|
||||||
monkeypatch.setattr(app_db, "engine", engine)
|
|
||||||
monkeypatch.setattr(app_db, "SessionLocal", session_local)
|
|
||||||
|
|
||||||
from fastapi.testclient import TestClient
|
|
||||||
|
|
||||||
fastapi_app = create_app()
|
|
||||||
with TestClient(fastapi_app) as client:
|
|
||||||
response = client.post(
|
|
||||||
"/location/record",
|
|
||||||
json={
|
|
||||||
"person": "legacy-user",
|
|
||||||
"latitude": "12.3",
|
|
||||||
"longitude": "45.6",
|
|
||||||
"altitude": "7.8",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
|
|
||||||
with engine.connect() as db_conn:
|
|
||||||
revision = db_conn.execute(text("SELECT version_num FROM alembic_version")).scalar_one()
|
|
||||||
row_count = db_conn.execute(text("SELECT COUNT(*) FROM location")).scalar_one()
|
|
||||||
|
|
||||||
assert revision == LOCATION_BASELINE_REVISION
|
|
||||||
assert row_count == 1
|
|
||||||
|
|
||||||
engine.dispose()
|
|
||||||
|
|
||||||
|
|
||||||
def test_location_db_adoption_initializes_new_db(tmp_path: Path) -> None:
|
def test_location_db_adoption_initializes_new_db(tmp_path: Path) -> None:
|
||||||
database_path = tmp_path / "new_location.db"
|
database_path = tmp_path / "new_location.db"
|
||||||
result = adopt_or_initialize_location_db(f"sqlite:///{database_path}")
|
result = adopt_or_initialize_location_db(f"sqlite:///{database_path}")
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from urllib.parse import parse_qs, urlparse
|
|||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from app.auth_db import reset_auth_db_caches
|
from app.db import reset_db_caches
|
||||||
from app.config import Settings, get_settings
|
from app.config import Settings, get_settings
|
||||||
from app.integrations.ticktick import (
|
from app.integrations.ticktick import (
|
||||||
AUTH_SCOPE,
|
AUTH_SCOPE,
|
||||||
@@ -221,7 +221,7 @@ def test_homeassistant_publish_creates_ticktick_action_task(
|
|||||||
monkeypatch.setenv("TICKTICK_TOKEN", "ticktick-access-token")
|
monkeypatch.setenv("TICKTICK_TOKEN", "ticktick-access-token")
|
||||||
monkeypatch.setenv("HOME_ASSISTANT_ACTION_TASK_PROJECT_ID", "project-123")
|
monkeypatch.setenv("HOME_ASSISTANT_ACTION_TASK_PROJECT_ID", "project-123")
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
|
|
||||||
captured = {"calls": []}
|
captured = {"calls": []}
|
||||||
|
|
||||||
@@ -265,7 +265,7 @@ def test_ticktick_auth_start_redirects_authenticated_user(
|
|||||||
monkeypatch.setenv("TICKTICK_CLIENT_ID", "ticktick-client-id")
|
monkeypatch.setenv("TICKTICK_CLIENT_ID", "ticktick-client-id")
|
||||||
monkeypatch.setenv("TICKTICK_CLIENT_SECRET", "ticktick-client-secret")
|
monkeypatch.setenv("TICKTICK_CLIENT_SECRET", "ticktick-client-secret")
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
monkeypatch.setattr("app.integrations.ticktick.secrets.token_hex", lambda _: "state-redirect")
|
monkeypatch.setattr("app.integrations.ticktick.secrets.token_hex", lambda _: "state-redirect")
|
||||||
|
|
||||||
with TestClient(create_app()) as client:
|
with TestClient(create_app()) as client:
|
||||||
@@ -301,7 +301,7 @@ def test_ticktick_auth_callback_persists_token(
|
|||||||
monkeypatch.setenv("TICKTICK_CLIENT_ID", "ticktick-client-id")
|
monkeypatch.setenv("TICKTICK_CLIENT_ID", "ticktick-client-id")
|
||||||
monkeypatch.setenv("TICKTICK_CLIENT_SECRET", "ticktick-client-secret")
|
monkeypatch.setenv("TICKTICK_CLIENT_SECRET", "ticktick-client-secret")
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
default_auth_state_store.pending_state = "callback-state"
|
default_auth_state_store.pending_state = "callback-state"
|
||||||
|
|
||||||
def fake_urlopen(req, timeout):
|
def fake_urlopen(req, timeout):
|
||||||
@@ -342,7 +342,7 @@ def test_ticktick_auth_callback_redirects_on_invalid_state(
|
|||||||
monkeypatch.setenv("TICKTICK_CLIENT_ID", "ticktick-client-id")
|
monkeypatch.setenv("TICKTICK_CLIENT_ID", "ticktick-client-id")
|
||||||
monkeypatch.setenv("TICKTICK_CLIENT_SECRET", "ticktick-client-secret")
|
monkeypatch.setenv("TICKTICK_CLIENT_SECRET", "ticktick-client-secret")
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
default_auth_state_store.pending_state = "expected-state"
|
default_auth_state_store.pending_state = "expected-state"
|
||||||
|
|
||||||
with TestClient(create_app()) as client:
|
with TestClient(create_app()) as client:
|
||||||
@@ -366,7 +366,7 @@ def test_ticktick_auth_callback_redirects_when_token_exchange_fails(
|
|||||||
monkeypatch.setenv("TICKTICK_CLIENT_ID", "ticktick-client-id")
|
monkeypatch.setenv("TICKTICK_CLIENT_ID", "ticktick-client-id")
|
||||||
monkeypatch.setenv("TICKTICK_CLIENT_SECRET", "ticktick-client-secret")
|
monkeypatch.setenv("TICKTICK_CLIENT_SECRET", "ticktick-client-secret")
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
reset_auth_db_caches()
|
reset_db_caches()
|
||||||
default_auth_state_store.pending_state = "callback-state"
|
default_auth_state_store.pending_state = "callback-state"
|
||||||
|
|
||||||
def fake_urlopen(req, timeout):
|
def fake_urlopen(req, timeout):
|
||||||
|
|||||||
Reference in New Issue
Block a user