diff --git a/tests/certificates/test_certificates.py b/tests/certificates/test_certificates.py index d7d0afb..de7505e 100644 --- a/tests/certificates/test_certificates.py +++ b/tests/certificates/test_certificates.py @@ -345,17 +345,57 @@ async def test_missing_api_key_is_401(db_session): @pytest.mark.asyncio 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_ one_wins_the_other_gets_409`): duas `AsyncSession` distintas contra o 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}" product = await create_product(db_session, name="auto", api_key=key) pfx_a = _build_test_pfx(cnpj="14200166000187", password="senha123", cn="A: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_a = session_maker() session_b = session_maker()