test: force the concurrent-upload race deterministically with asyncio.Event
Plain asyncio.gather on one event loop could let the first upload commit before the second's pre-check, making the second a legitimate replace (2 successes, 1 live row) -- correct behavior that fails the '1 conflict' assertion. Same Event-synchronization pattern as the auto's HTTP race tests: hold the first past its pre-check until the second also pre-checks, so both read None before either writes and the partial-unique index decides deterministically. Code under test untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
fb2b8ce372
commit
39ba81efad
@@ -345,17 +345,57 @@ async def test_missing_api_key_is_401(db_session):
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_concurrent_uploads_for_same_product_branch_only_one_wins_the_other_gets_409(
|
async def test_concurrent_uploads_for_same_product_branch_only_one_wins_the_other_gets_409(
|
||||||
test_engine, db_session
|
test_engine, db_session, monkeypatch
|
||||||
):
|
):
|
||||||
"""Mesma prova do auto (`test_concurrent_uploads_for_same_branch_only_
|
"""Mesma prova do auto (`test_concurrent_uploads_for_same_branch_only_
|
||||||
one_wins_the_other_gets_409`): duas `AsyncSession` distintas contra o
|
one_wins_the_other_gets_409`): duas `AsyncSession` distintas contra o
|
||||||
MESMO `test_engine`, disparadas via `asyncio.gather` -- concorrência
|
MESMO `test_engine`, disparadas via `asyncio.gather` -- concorrência
|
||||||
REAL, não simulada."""
|
REAL, não simulada.
|
||||||
|
|
||||||
|
Sem sincronização explícita, as duas corrotinas rodam no MESMO event
|
||||||
|
loop e podem interleavear de um jeito que NÃO exercita a corrida real:
|
||||||
|
se a primeira `upload_certificate` COMMITA inteiro antes de a segunda
|
||||||
|
fazer o pre-check `_get_live_certificate_by_branch`, a segunda enxerga a
|
||||||
|
linha viva da primeira e faz um REPLACE LEGÍTIMO (soft-delete + insert)
|
||||||
|
-- 2 sucessos, 1 linha viva, comportamento CORRETO do serviço, mas que
|
||||||
|
quebraria a asserção abaixo (que exige exatamente 1 sucesso + 1
|
||||||
|
conflito). Mesma técnica de sincronização determinística de
|
||||||
|
`auto/backend/tests/modules/financeiro/test_pay_account_payable.py::
|
||||||
|
test_pay_concurrent_with_cancel_via_http_lock_serializes_the_race`:
|
||||||
|
monkeypatch no ponto de await entre o pre-check e o commit, com um
|
||||||
|
`asyncio.Event`, para FORÇAR a janela vulnerável -- as duas chamadas
|
||||||
|
fazem o pre-check (ambas leem `None`) ANTES de qualquer uma commitar,
|
||||||
|
e só depois disso o resultado passa a depender só do índice parcial
|
||||||
|
único do banco (determinístico: 1 vencedor, 1 `IntegrityError`
|
||||||
|
traduzido em `CertificateUploadConflictError`)."""
|
||||||
key = f"k-{uuid.uuid4().hex}"
|
key = f"k-{uuid.uuid4().hex}"
|
||||||
product = await create_product(db_session, name="auto", api_key=key)
|
product = await create_product(db_session, name="auto", api_key=key)
|
||||||
pfx_a = _build_test_pfx(cnpj="14200166000187", password="senha123", cn="A:14200166000187")
|
pfx_a = _build_test_pfx(cnpj="14200166000187", password="senha123", cn="A:14200166000187")
|
||||||
pfx_b = _build_test_pfx(cnpj="14200166000187", password="senha456", cn="B:14200166000187")
|
pfx_b = _build_test_pfx(cnpj="14200166000187", password="senha456", cn="B:14200166000187")
|
||||||
|
|
||||||
|
original_precheck = certificate_service._get_live_certificate_by_branch
|
||||||
|
precheck_done = asyncio.Event()
|
||||||
|
first_precheck_claimed = False
|
||||||
|
|
||||||
|
async def _precheck_forcing_both_before_any_commit(session, product_id, branch_ref):
|
||||||
|
nonlocal first_precheck_claimed
|
||||||
|
if not first_precheck_claimed:
|
||||||
|
first_precheck_claimed = True
|
||||||
|
result = await original_precheck(session, product_id, branch_ref)
|
||||||
|
precheck_done.set()
|
||||||
|
# Segura ESTA chamada (ainda antes do commit em upload_certificate)
|
||||||
|
# até depois que a outra também tenha feito seu pre-check --
|
||||||
|
# garante que as DUAS leem "nenhum certificado vivo" antes de
|
||||||
|
# qualquer uma escrever.
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
return result
|
||||||
|
await precheck_done.wait()
|
||||||
|
return await original_precheck(session, product_id, branch_ref)
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
certificate_service, "_get_live_certificate_by_branch", _precheck_forcing_both_before_any_commit
|
||||||
|
)
|
||||||
|
|
||||||
session_maker = async_sessionmaker(test_engine, expire_on_commit=False)
|
session_maker = async_sessionmaker(test_engine, expire_on_commit=False)
|
||||||
session_a = session_maker()
|
session_a = session_maker()
|
||||||
session_b = session_maker()
|
session_b = session_maker()
|
||||||
|
|||||||
Reference in New Issue
Block a user