fix(time): break_minutes DB-Default ergänzen + Retention-Tests korrigiert
Gleiches Muster wie hours_payouts.status (7166bbb): Model hatte nur
Python-default, kein server_default -> Migration 0044 zieht DB-Default
nach. Retention-Tests nutzten fehlenden Wert bei Raw-SQL-Insert und
fehlenden RLS-Bypass zwischen den Transaktionsgrenzen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gis16MnuwkYcivLrSxK1pD
This commit is contained in:
@@ -36,7 +36,7 @@ class TimeEntry(Base):
|
|||||||
date: Mapped[date] = mapped_column(Date, nullable=False, index=True)
|
date: Mapped[date] = mapped_column(Date, nullable=False, index=True)
|
||||||
start_time: Mapped[time] = mapped_column(Time(timezone=False), nullable=False)
|
start_time: Mapped[time] = mapped_column(Time(timezone=False), nullable=False)
|
||||||
end_time: Mapped[time | None] = mapped_column(Time(timezone=False))
|
end_time: Mapped[time | None] = mapped_column(Time(timezone=False))
|
||||||
break_minutes: Mapped[int] = mapped_column(Integer, default=0)
|
break_minutes: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
|
||||||
break_start: Mapped[time | None] = mapped_column(Time(timezone=False)) # Aktive Pause tracken
|
break_start: Mapped[time | None] = mapped_column(Time(timezone=False)) # Aktive Pause tracken
|
||||||
project_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True))
|
project_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True))
|
||||||
note: Mapped[str | None] = mapped_column(Text)
|
note: Mapped[str | None] = mapped_column(Text)
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
"""time_entries.break_minutes: DB-Default ergänzen (Model-Sync)
|
||||||
|
|
||||||
|
Revision ID: 0044
|
||||||
|
Revises: 0043
|
||||||
|
Create Date: 2026-08-27
|
||||||
|
|
||||||
|
Model hatte nur Python-seitigen default=0, kein server_default (wie zuvor
|
||||||
|
hours_payouts.status, siehe Commit 71e3e82). Column ist bereits NOT NULL,
|
||||||
|
hier nur DEFAULT nachgezogen für Konsistenz mit dem Model.
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
revision = "0044"
|
||||||
|
down_revision = "0043"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.execute(text("ALTER TABLE time_entries ALTER COLUMN break_minutes SET DEFAULT 0"))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.execute(text("ALTER TABLE time_entries ALTER COLUMN break_minutes DROP DEFAULT"))
|
||||||
@@ -21,7 +21,9 @@ async def retention_headers(client: AsyncClient):
|
|||||||
})
|
})
|
||||||
assert resp.status_code == 201, resp.text
|
assert resp.status_code == 201, resp.text
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
return {"Authorization": f"Bearer {data['access_token']}"}, data["user"]["company_id"]
|
headers = {"Authorization": f"Bearer {data['access_token']}"}
|
||||||
|
me = await client.get("/api/v1/users/me", headers=headers)
|
||||||
|
return headers, me.json()["company_id"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio(loop_scope="session")
|
@pytest.mark.asyncio(loop_scope="session")
|
||||||
@@ -35,20 +37,23 @@ async def test_purge_deletes_old_time_entries_respects_retention(
|
|||||||
old_date = date.today() - timedelta(days=11 * 365) # älter als Default 10 Jahre
|
old_date = date.today() - timedelta(days=11 * 365) # älter als Default 10 Jahre
|
||||||
recent_date = date.today() - timedelta(days=30)
|
recent_date = date.today() - timedelta(days=30)
|
||||||
|
|
||||||
|
await db_session.execute(text("SET LOCAL app.bypass_rls = 'on'"))
|
||||||
await db_session.execute(text(
|
await db_session.execute(text(
|
||||||
"INSERT INTO time_entries (id, user_id, date, start_time, end_time, status, source) "
|
"INSERT INTO time_entries (id, user_id, date, start_time, end_time, break_minutes, status, source) "
|
||||||
"VALUES (gen_random_uuid(), :uid, :d, '08:00', '16:00', 'approved', 'web')"
|
"VALUES (gen_random_uuid(), :uid, :d, '08:00', '16:00', 0, 'approved', 'web')"
|
||||||
), {"uid": user_id, "d": old_date})
|
), {"uid": user_id, "d": old_date})
|
||||||
await db_session.execute(text(
|
await db_session.execute(text(
|
||||||
"INSERT INTO time_entries (id, user_id, date, start_time, end_time, status, source) "
|
"INSERT INTO time_entries (id, user_id, date, start_time, end_time, break_minutes, status, source) "
|
||||||
"VALUES (gen_random_uuid(), :uid, :d, '08:00', '16:00', 'approved', 'web')"
|
"VALUES (gen_random_uuid(), :uid, :d, '08:00', '16:00', 0, 'approved', 'web')"
|
||||||
), {"uid": user_id, "d": recent_date})
|
), {"uid": user_id, "d": recent_date})
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
|
|
||||||
|
await db_session.execute(text("SET LOCAL app.bypass_rls = 'on'"))
|
||||||
result = await run_retention_purge(db_session, company_id=company_id)
|
result = await run_retention_purge(db_session, company_id=company_id)
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
assert result["time_entries"] == 1
|
assert result["time_entries"] == 1
|
||||||
|
|
||||||
|
await db_session.execute(text("SET LOCAL app.bypass_rls = 'on'"))
|
||||||
remaining = await db_session.execute(text(
|
remaining = await db_session.execute(text(
|
||||||
"SELECT date FROM time_entries WHERE user_id = :uid"
|
"SELECT date FROM time_entries WHERE user_id = :uid"
|
||||||
), {"uid": user_id})
|
), {"uid": user_id})
|
||||||
@@ -70,12 +75,14 @@ async def test_purge_configurable_retention_years(
|
|||||||
user_id = me.json()["id"]
|
user_id = me.json()["id"]
|
||||||
three_years_ago = date.today() - timedelta(days=3 * 365)
|
three_years_ago = date.today() - timedelta(days=3 * 365)
|
||||||
|
|
||||||
|
await db_session.execute(text("SET LOCAL app.bypass_rls = 'on'"))
|
||||||
await db_session.execute(text(
|
await db_session.execute(text(
|
||||||
"INSERT INTO time_entries (id, user_id, date, start_time, end_time, status, source) "
|
"INSERT INTO time_entries (id, user_id, date, start_time, end_time, break_minutes, status, source) "
|
||||||
"VALUES (gen_random_uuid(), :uid, :d, '08:00', '16:00', 'approved', 'web')"
|
"VALUES (gen_random_uuid(), :uid, :d, '08:00', '16:00', 0, 'approved', 'web')"
|
||||||
), {"uid": user_id, "d": three_years_ago})
|
), {"uid": user_id, "d": three_years_ago})
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
|
|
||||||
|
await db_session.execute(text("SET LOCAL app.bypass_rls = 'on'"))
|
||||||
result = await run_retention_purge(db_session, company_id=company_id)
|
result = await run_retention_purge(db_session, company_id=company_id)
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
assert result["time_entries"] >= 1
|
assert result["time_entries"] >= 1
|
||||||
@@ -92,6 +99,7 @@ async def test_purge_expired_sessions_and_audit_logs_global(
|
|||||||
expired = datetime.now(timezone.utc) - timedelta(days=1)
|
expired = datetime.now(timezone.utc) - timedelta(days=1)
|
||||||
old_audit = datetime.now(timezone.utc) - timedelta(days=4 * 365)
|
old_audit = datetime.now(timezone.utc) - timedelta(days=4 * 365)
|
||||||
|
|
||||||
|
await db_session.execute(text("SET LOCAL app.bypass_rls = 'on'"))
|
||||||
await db_session.execute(text(
|
await db_session.execute(text(
|
||||||
"INSERT INTO sessions (id, user_id, refresh_token_hash, expires_at) "
|
"INSERT INTO sessions (id, user_id, refresh_token_hash, expires_at) "
|
||||||
"VALUES (gen_random_uuid(), :uid, :h, :exp)"
|
"VALUES (gen_random_uuid(), :uid, :h, :exp)"
|
||||||
@@ -102,6 +110,7 @@ async def test_purge_expired_sessions_and_audit_logs_global(
|
|||||||
), {"cid": company_id, "uid": user_id, "ts": old_audit})
|
), {"cid": company_id, "uid": user_id, "ts": old_audit})
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
|
|
||||||
|
await db_session.execute(text("SET LOCAL app.bypass_rls = 'on'"))
|
||||||
result = await run_retention_purge(db_session, company_id=None)
|
result = await run_retention_purge(db_session, company_id=None)
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
assert result["sessions"] >= 1
|
assert result["sessions"] >= 1
|
||||||
|
|||||||
Reference in New Issue
Block a user