test(stages): aware-даты этапов форм и проектов
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
bc89789026
commit
d73333705c
@ -326,3 +326,46 @@ def test_form3_phases_delete(client, admin_tokens):
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
|
||||
def _instant(value: str):
|
||||
from datetime import datetime
|
||||
|
||||
return datetime.fromisoformat(value)
|
||||
|
||||
|
||||
def test_form_phases_create_aware_dates_keep_instant(client, admin_tokens):
|
||||
body = {
|
||||
"sheet": "AHR",
|
||||
"phase_code": "tz_aware_phase",
|
||||
"role": "DFIP",
|
||||
"column_keys": ["plan.q1"],
|
||||
"opens_at": "2026-10-26T10:00:00+03:00",
|
||||
"closes_at": "2026-10-27T10:00:00+03:00",
|
||||
"direction": None,
|
||||
}
|
||||
headers = _auth_headers(admin_tokens)
|
||||
response = client.post("/api/v1/stages/form/4", json=body, headers=headers)
|
||||
try:
|
||||
assert response.status_code == 200, response.text
|
||||
result = response.json()["result"]
|
||||
assert _instant(result["opens_at"]) == _instant(body["opens_at"])
|
||||
assert _instant(result["closes_at"]) == _instant(body["closes_at"])
|
||||
|
||||
# GET -> PATCH тем же значением не меняет момент времени
|
||||
listed = client.get(
|
||||
"/api/v1/stages/form/4?sheet=AHR&phase_code=tz_aware_phase", headers=headers
|
||||
).json()["result"]
|
||||
assert len(listed) == 1
|
||||
patch = client.patch(
|
||||
"/api/v1/stages/form/4/AHR/tz_aware_phase",
|
||||
json={"opens_at": listed[0]["opens_at"], "closes_at": listed[0]["closes_at"]},
|
||||
headers=headers,
|
||||
)
|
||||
assert patch.status_code == 200, patch.text
|
||||
patched = patch.json()["result"]
|
||||
assert patched["opens_at"] == listed[0]["opens_at"]
|
||||
assert patched["closes_at"] == listed[0]["closes_at"]
|
||||
assert _instant(patched["opens_at"]) == _instant(body["opens_at"])
|
||||
finally:
|
||||
client.delete("/api/v1/stages/form/4/AHR/tz_aware_phase", headers=headers)
|
||||
|
||||
130
api/tests/unit/test_form_phase_service_tz.py
Normal file
130
api/tests/unit/test_form_phase_service_tz.py
Normal file
@ -0,0 +1,130 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from src.core.timezones import timezone_from_offset, to_org_local_naive
|
||||
from src.db.models.role import UserRoleEnum
|
||||
from src.domain.schemas import (
|
||||
Form3PhaseCreate,
|
||||
Form3PhaseUpdate,
|
||||
FormPhaseCreate,
|
||||
FormPhaseUpdate,
|
||||
)
|
||||
from src.services.form3_phase_service import Form3PhaseService
|
||||
from src.services.form_phase_service import FormPhaseService
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value, offset, expected",
|
||||
[
|
||||
(datetime(2026, 10, 26, 7, 0, tzinfo=timezone.utc), "+05", datetime(2026, 10, 26, 12, 0)),
|
||||
(
|
||||
datetime(2026, 10, 26, 10, 0, tzinfo=timezone(timedelta(hours=3))),
|
||||
"+05",
|
||||
datetime(2026, 10, 26, 12, 0),
|
||||
),
|
||||
(datetime(2026, 10, 26, 10, 0), "+05", datetime(2026, 10, 26, 10, 0)),
|
||||
(datetime(2026, 10, 26, 10, 0), None, datetime(2026, 10, 26, 10, 0)),
|
||||
(datetime(2026, 10, 26, 10, 0, tzinfo=timezone.utc), "-03", datetime(2026, 10, 26, 7, 0)),
|
||||
],
|
||||
)
|
||||
def test_to_org_local_naive(value, offset, expected):
|
||||
result = to_org_local_naive(value, offset)
|
||||
assert result == expected
|
||||
assert result.tzinfo is None
|
||||
|
||||
|
||||
def test_to_org_local_naive_keeps_instant():
|
||||
value = datetime(2026, 10, 26, 10, 0, tzinfo=timezone(timedelta(hours=3)))
|
||||
local = to_org_local_naive(value, "+05")
|
||||
assert local.replace(tzinfo=timezone_from_offset("+05")) == value
|
||||
|
||||
|
||||
def test_to_org_local_naive_aware_without_offset_is_422():
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
to_org_local_naive(datetime(2026, 10, 26, 10, 0, tzinfo=timezone.utc), None)
|
||||
assert exc.value.status_code == 422
|
||||
|
||||
|
||||
def _admin():
|
||||
return SimpleNamespace(role_id=UserRoleEnum.ADMIN)
|
||||
|
||||
|
||||
def _form_service():
|
||||
service = FormPhaseService(db=AsyncMock())
|
||||
service.fp_repo = AsyncMock()
|
||||
form = SimpleNamespace(
|
||||
id=1,
|
||||
form_type=SimpleNamespace(section_list=["plan"], sheet_list_with_directions=None),
|
||||
)
|
||||
return service, form
|
||||
|
||||
|
||||
MSK = timezone(timedelta(hours=3))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_form_phase_create_normalizes_to_org_local():
|
||||
service, form = _form_service()
|
||||
body = FormPhaseCreate(
|
||||
sheet="AHR", phase_code="p", role="DFIP", column_keys=["plan.q1"],
|
||||
opens_at=datetime(2026, 10, 26, 10, 0, tzinfo=MSK),
|
||||
closes_at=datetime(2026, 10, 27, 10, 0),
|
||||
)
|
||||
await service.create(body=body, user=_admin(), budget_form=form, utc_offset="+05")
|
||||
kwargs = service.fp_repo.create.await_args.kwargs
|
||||
assert kwargs["opens_at"] == datetime(2026, 10, 26, 12, 0)
|
||||
assert kwargs["opens_at"].tzinfo is None
|
||||
assert kwargs["closes_at"] == datetime(2026, 10, 27, 10, 0)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_form_phase_update_normalizes_only_given_fields():
|
||||
service, form = _form_service()
|
||||
body = FormPhaseUpdate(opens_at=datetime(2026, 10, 26, 10, 0, tzinfo=MSK))
|
||||
await service.update(
|
||||
sheet="AHR", direction=None, phase_code="p", body=body,
|
||||
user=_admin(), budget_form=form, utc_offset="+05",
|
||||
)
|
||||
data = service.fp_repo.update.await_args.kwargs["data"]
|
||||
assert data == {"opens_at": datetime(2026, 10, 26, 12, 0)}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_form_phase_create_aware_without_offset_is_422():
|
||||
service, form = _form_service()
|
||||
body = FormPhaseCreate(
|
||||
sheet="AHR", phase_code="p", role="DFIP", column_keys=["plan.q1"],
|
||||
opens_at=datetime(2026, 10, 26, 10, 0, tzinfo=MSK),
|
||||
closes_at=datetime(2026, 10, 27, 10, 0),
|
||||
)
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await service.create(body=body, user=_admin(), budget_form=form, utc_offset=None)
|
||||
assert exc.value.status_code == 422
|
||||
service.fp_repo.create.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_form3_phase_create_and_update_normalize():
|
||||
service = Form3PhaseService(db=AsyncMock())
|
||||
service.f3p_repo = AsyncMock()
|
||||
body = Form3PhaseCreate(
|
||||
report_type="PLAN", year=2026, phase_code="p", role="DFIP", column_keys=[],
|
||||
opens_at=datetime(2026, 10, 26, 10, 0, tzinfo=MSK),
|
||||
closes_at=datetime(2026, 10, 27, 10, 0, tzinfo=MSK),
|
||||
)
|
||||
await service.create(report_id=1, body=body, user=_admin(), utc_offset="+05")
|
||||
kwargs = service.f3p_repo.create.await_args.kwargs
|
||||
assert kwargs["opens_at"] == datetime(2026, 10, 26, 12, 0)
|
||||
assert kwargs["closes_at"] == datetime(2026, 10, 27, 12, 0)
|
||||
|
||||
await service.update(
|
||||
report_id=1, phase_code="p",
|
||||
body=Form3PhaseUpdate(closes_at=datetime(2026, 10, 27, 10, 0, tzinfo=MSK)),
|
||||
user=_admin(), utc_offset="+05",
|
||||
)
|
||||
data = service.f3p_repo.update.await_args.kwargs["data"]
|
||||
assert data == {"closes_at": datetime(2026, 10, 27, 12, 0)}
|
||||
Loading…
x
Reference in New Issue
Block a user