@ -1,3 +1,6 @@
import asyncio
import threading
import time
import unittest
import unittest
from datetime import datetime
from datetime import datetime
from types import SimpleNamespace
from types import SimpleNamespace
@ -6,6 +9,7 @@ from unittest.mock import patch
from fastapi import HTTPException
from fastapi import HTTPException
from sqlalchemy import create_engine
from sqlalchemy import create_engine
from sqlalchemy . orm import sessionmaker
from sqlalchemy . orm import sessionmaker
from sqlalchemy . pool import StaticPool
from app . db . mock_database import MockBase
from app . db . mock_database import MockBase
from app . db . mock_models import RentalContract , RentalFine , RentalPayment , RentalVehicle
from app . db . mock_models import RentalContract , RentalFine , RentalPayment , RentalVehicle
@ -62,6 +66,17 @@ class RentalServiceTests(unittest.IsolatedAsyncioTestCase):
self . addCleanup ( engine . dispose )
self . addCleanup ( engine . dispose )
return SessionLocal
return SessionLocal
def _build_threadsafe_session_local ( self ) :
engine = create_engine (
" sqlite:// " ,
connect_args = { " check_same_thread " : False } ,
poolclass = StaticPool ,
)
SessionLocal = sessionmaker ( autocommit = False , autoflush = False , bind = engine )
MockBase . metadata . create_all ( bind = engine )
self . addCleanup ( engine . dispose )
return SessionLocal
def _create_rental_vehicle (
def _create_rental_vehicle (
self ,
self ,
db ,
db ,
@ -256,6 +271,89 @@ class RentalServiceTests(unittest.IsolatedAsyncioTestCase):
self . assertEqual ( session . added , [ ] )
self . assertEqual ( session . added , [ ] )
self . assertTrue ( session . closed )
self . assertTrue ( session . closed )
async def test_abrir_locacao_aluguel_allows_single_success_under_race ( self ) :
SessionLocal = self . _build_threadsafe_session_local ( )
db = SessionLocal ( )
try :
vehicle = self . _create_rental_vehicle ( db , placa = " RAC1E01 " )
vehicle_id = vehicle . id
finally :
db . close ( )
attempts = 4
start_barrier = threading . Barrier ( attempts )
vehicle_lock = threading . Lock ( )
def _get_locked_vehicle ( db , * , rental_vehicle_id = None , placa = None ) :
acquired = vehicle_lock . acquire ( timeout = 2 )
if not acquired :
raise AssertionError ( " failed to acquire rental race lock " )
if not db . info . get ( " _test_rental_lock_wrapped " ) :
original_close = db . close
def close_with_lock_release ( ) :
try :
original_close ( )
finally :
held_lock = db . info . pop ( " _test_rental_vehicle_lock " , None )
if held_lock and held_lock . locked ( ) :
held_lock . release ( )
db . close = close_with_lock_release
db . info [ " _test_rental_lock_wrapped " ] = True
db . info [ " _test_rental_vehicle_lock " ] = vehicle_lock
time . sleep ( 0.05 )
return rental_service . _build_rental_vehicle_query (
db ,
rental_vehicle_id = rental_vehicle_id ,
placa = placa ,
) . first ( )
def _sync_open_rental ( ) :
start_barrier . wait ( timeout = 5 )
return asyncio . run (
rental_service . abrir_locacao_aluguel (
rental_vehicle_id = vehicle_id ,
data_inicio = " 17/03/2026 10:00 " ,
data_fim_prevista = " 20/03/2026 10:00 " ,
)
)
with patch . object ( rental_service , " SessionMockLocal " , SessionLocal ) , patch . object (
rental_service ,
" _get_rental_vehicle_for_update " ,
side_effect = _get_locked_vehicle ,
) :
results = await asyncio . gather (
* [ asyncio . to_thread ( _sync_open_rental ) for _ in range ( attempts ) ] ,
return_exceptions = True ,
)
successes = [ result for result in results if isinstance ( result , dict ) ]
conflicts = [
result
for result in results
if isinstance ( result , HTTPException )
and isinstance ( result . detail , dict )
and result . detail . get ( " code " ) == " rental_vehicle_unavailable "
]
unexpected = [ result for result in results if result not in successes and result not in conflicts ]
self . assertEqual ( len ( successes ) , 1 )
self . assertEqual ( len ( conflicts ) , attempts - 1 )
self . assertEqual ( unexpected , [ ] )
db = SessionLocal ( )
try :
contracts = db . query ( RentalContract ) . all ( )
stored_vehicle = db . query ( RentalVehicle ) . filter ( RentalVehicle . id == vehicle_id ) . one ( )
self . assertEqual ( len ( contracts ) , 1 )
self . assertEqual ( stored_vehicle . status , " alugado " )
finally :
db . close ( )
async def test_registrar_devolucao_aluguel_fecha_contrato_e_libera_veiculo ( self ) :
async def test_registrar_devolucao_aluguel_fecha_contrato_e_libera_veiculo ( self ) :
SessionLocal = self . _build_session_local ( )
SessionLocal = self . _build_session_local ( )
db = SessionLocal ( )
db = SessionLocal ( )