feat: harden auth and study workflows
- replace plaintext login and unlock requests with RSA-OAEP/AES-GCM encrypted payloads - add login challenge replay protection, production RSA key validation, and auth tests - wire compose to environment-driven dev/prod settings without committing local secrets - update setup-config smoke scripts and Postman docs for encrypted login - add visit schedule migrations/tests and update study/subject setup workflows
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
from datetime import date
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.crud.subject import should_generate_visits_after_subject_update
|
||||
from app.crud.visit import build_visit_schedule_dates, sort_visits_for_display
|
||||
from app.schemas.study import StudyUpdate
|
||||
|
||||
|
||||
def test_build_visit_schedule_dates_uses_per_visit_windows():
|
||||
baseline_date = date(2026, 5, 8)
|
||||
|
||||
visits = build_visit_schedule_dates(
|
||||
[
|
||||
{
|
||||
"visit_code": "基线访视",
|
||||
"baseline_offset_days": 0,
|
||||
"window_before_days": 0,
|
||||
"window_after_days": 0,
|
||||
},
|
||||
{
|
||||
"visit_code": "V1",
|
||||
"baseline_offset_days": 7,
|
||||
"window_before_days": 2,
|
||||
"window_after_days": 2,
|
||||
},
|
||||
{
|
||||
"visit_code": "V2",
|
||||
"baseline_offset_days": 15,
|
||||
"window_before_days": 2,
|
||||
"window_after_days": 2,
|
||||
},
|
||||
{
|
||||
"visit_code": "V3",
|
||||
"baseline_offset_days": 23,
|
||||
"window_before_days": 3,
|
||||
"window_after_days": 3,
|
||||
},
|
||||
],
|
||||
baseline_date,
|
||||
)
|
||||
|
||||
assert [
|
||||
(visit["visit_code"], visit["baseline_offset_days"], visit["planned_date"], visit["window_start"], visit["window_end"])
|
||||
for visit in visits
|
||||
] == [
|
||||
("基线访视", 0, date(2026, 5, 8), date(2026, 5, 8), date(2026, 5, 8)),
|
||||
("V1", 7, date(2026, 5, 15), date(2026, 5, 13), date(2026, 5, 17)),
|
||||
("V2", 15, date(2026, 5, 23), date(2026, 5, 21), date(2026, 5, 25)),
|
||||
("V3", 23, date(2026, 5, 31), date(2026, 5, 28), date(2026, 6, 3)),
|
||||
]
|
||||
|
||||
|
||||
def test_build_visit_schedule_dates_keeps_visit_structure_without_baseline_date():
|
||||
visits = build_visit_schedule_dates(
|
||||
[
|
||||
{
|
||||
"visit_code": "基线访视",
|
||||
"baseline_offset_days": 0,
|
||||
"window_before_days": 0,
|
||||
"window_after_days": 0,
|
||||
},
|
||||
{
|
||||
"visit_code": "V1",
|
||||
"baseline_offset_days": 7,
|
||||
"window_before_days": 2,
|
||||
"window_after_days": 2,
|
||||
},
|
||||
],
|
||||
None,
|
||||
)
|
||||
|
||||
assert [
|
||||
(visit["visit_code"], visit["planned_date"], visit["window_start"], visit["window_end"])
|
||||
for visit in visits
|
||||
] == [
|
||||
("基线访视", None, None, None),
|
||||
("V1", None, None, None),
|
||||
]
|
||||
|
||||
|
||||
def test_study_update_allows_multiple_visits_with_same_baseline_offset():
|
||||
payload = StudyUpdate(
|
||||
visit_schedule=[
|
||||
{
|
||||
"visit_code": "筛选访视",
|
||||
"baseline_offset_days": 0,
|
||||
"window_before_days": 0,
|
||||
"window_after_days": 0,
|
||||
},
|
||||
{
|
||||
"visit_code": "基线访视",
|
||||
"baseline_offset_days": 0,
|
||||
"window_before_days": 0,
|
||||
"window_after_days": 0,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
assert [item.visit_code for item in payload.visit_schedule or []] == ["筛选访视", "基线访视"]
|
||||
|
||||
|
||||
def test_sort_visits_for_display_follows_configured_visit_order_without_dates():
|
||||
visits = [
|
||||
SimpleNamespace(visit_code="V1", planned_date=None),
|
||||
SimpleNamespace(visit_code="V2", planned_date=None),
|
||||
SimpleNamespace(visit_code="基线访视", planned_date=None),
|
||||
]
|
||||
visit_schedule = [
|
||||
{"visit_code": "基线访视"},
|
||||
{"visit_code": "V1"},
|
||||
{"visit_code": "V2"},
|
||||
]
|
||||
|
||||
assert [visit.visit_code for visit in sort_visits_for_display(visits, visit_schedule)] == ["基线访视", "V1", "V2"]
|
||||
|
||||
|
||||
def test_sort_visits_for_display_does_not_infer_business_order():
|
||||
visits = [
|
||||
SimpleNamespace(visit_code="基线访视", planned_date=None),
|
||||
SimpleNamespace(visit_code="V1", planned_date=None),
|
||||
SimpleNamespace(visit_code="V2", planned_date=None),
|
||||
]
|
||||
visit_schedule = [
|
||||
{"visit_code": "V1"},
|
||||
{"visit_code": "V2"},
|
||||
{"visit_code": "基线访视"},
|
||||
]
|
||||
|
||||
assert [visit.visit_code for visit in sort_visits_for_display(visits, visit_schedule)] == ["V1", "V2", "基线访视"]
|
||||
|
||||
|
||||
def test_should_generate_visits_when_baseline_date_is_set_or_changed():
|
||||
assert should_generate_visits_after_subject_update(
|
||||
previous_baseline_date=None,
|
||||
next_baseline_date=date(2026, 5, 8),
|
||||
)
|
||||
assert should_generate_visits_after_subject_update(
|
||||
previous_baseline_date=date(2026, 5, 8),
|
||||
next_baseline_date=date(2026, 5, 9),
|
||||
)
|
||||
assert not should_generate_visits_after_subject_update(
|
||||
previous_baseline_date=None,
|
||||
next_baseline_date=None,
|
||||
)
|
||||
Reference in New Issue
Block a user