"""信息安全自评估 — period math, cycle instantiation, aggregators, RBAC. Schema built via create_all (no migrations); each test is hermetic. The aggregators must run cleanly against the (near-)empty test DB — the whole point of the auto-fill is that it never crashes on a thin tenant. """ from __future__ import annotations import uuid from datetime import date import pytest from httpx import AsyncClient from app.domains.auditor import assessment_catalog as cat from app.domains.auditor import assessment_service as svc def test_period_parse_and_just_ended(): assert svc.parse_period_key("2026-H1") == (date(2026, 1, 1), date(2026, 6, 30)) assert svc.parse_period_key("2026-H2") == (date(2026, 7, 1), date(2026, 12, 31)) assert svc.period_just_ended(date(2026, 3, 1)) == "2025-H2" assert svc.period_just_ended(date(2026, 9, 1)) == "2026-H1" with pytest.raises(Exception): svc.parse_period_key("2026-Q1") @pytest.mark.asyncio async def test_create_cycle_instantiates_all_sections(db_session): cycle = await svc.create_cycle(db_session, period_key="2026-H1") await db_session.commit() detail = await svc.get_cycle_detail(db_session, cycle.id) assert detail["period_key"] == "2026-H1" assert len(detail["sections"]) == len(cat.SECTIONS) # every auto-source section got a summary without crashing on empty data auto = [s for s in detail["sections"] if s["auto_source"]] assert auto, "expected some auto-filled sections" assert all(s["auto_summary"] for s in auto) @pytest.mark.asyncio async def test_create_cycle_idempotent(db_session): await svc.create_cycle(db_session, period_key="2026-H2") await db_session.commit() with pytest.raises(Exception): await svc.create_cycle(db_session, period_key="2026-H2") @pytest.mark.asyncio async def test_finalize_locks_edits(db_session): cycle = await svc.create_cycle(db_session, period_key="2025-H1") await db_session.commit() detail = await svc.get_cycle_detail(db_session, cycle.id) editable = next(s for s in detail["sections"] if s["conclusion_applicable"]) await svc.finalize_cycle(db_session, cycle.id, finalized_by_person_id=None) await db_session.commit() with pytest.raises(Exception): await svc.update_section( db_session, uuid.UUID(editable["id"]), payload={"narrative": "x"}, ) @pytest.mark.asyncio async def test_prose_section_rejects_conclusion(db_session): cycle = await svc.create_cycle(db_session, period_key="2025-H2") await db_session.commit() detail = await svc.get_cycle_detail(db_session, cycle.id) prose = next(s for s in detail["sections"] if not s["conclusion_applicable"]) with pytest.raises(Exception): await svc.update_section( db_session, uuid.UUID(prose["id"]), payload={"conclusion": "compliant"}, ) @pytest.mark.asyncio async def test_measures_crud(db_session): m = await svc.create_measure( db_session, payload={"item": "邮箱密码策略", "status": "in_progress"}, ) await db_session.commit() rows = await svc.list_measures(db_session) assert len(rows) == 1 await svc.update_measure(db_session, m.id, payload={"status": "done"}) await db_session.commit() rows = await svc.list_measures(db_session) assert rows[0]["status"] == "done" with pytest.raises(Exception): await svc.create_measure(db_session, payload={"item": "x", "status": "bogus"}) @pytest.mark.asyncio async def test_http_rbac(client: AsyncClient, auditor_headers, employee_headers): ok = await client.get( "/api/v1/auditor/assessments/cycles", headers=auditor_headers, ) assert ok.status_code == 200, ok.text denied = await client.get( "/api/v1/auditor/assessments/cycles", headers=employee_headers, ) assert denied.status_code == 403 created = await client.post( "/api/v1/auditor/assessments/cycles", headers=auditor_headers, json={"period_key": "2026-H1"}, ) assert created.status_code == 200, created.text emp_create = await client.post( "/api/v1/auditor/assessments/cycles", headers=employee_headers, json={"period_key": "2026-H2"}, ) assert emp_create.status_code == 403