2026-10-20 06:45:30 -04:00
import json
import secrets
import subprocess
from application_harness import Application , ROOT , PHP
app = Application (); workers = []
def race ( operation , records , extra = []):
group = []
for record in records :
process = subprocess . Popen ( PHP + [ 'tests/maintenance_worker.php' , operation , app . database , str ( record )] + list ( map ( str , extra )), cwd = ROOT , env = app . env , stdin = subprocess . PIPE , stdout = subprocess . PIPE , stderr = subprocess . PIPE , text = True )
workers . append ( process ); group . append ( process )
for process in group :
if process . stdout . readline () . strip () != 'READY' : raise AssertionError ( process . stderr . read ())
for process in group : process . stdin . write ( 'go \n ' ); process . stdin . flush ()
results = []
for process in group :
output , error = process . communicate ( timeout = 20 )
if process . returncode : raise AssertionError ( error + output )
results . append ( json . loads ( output ))
return results
try :
app . login ()
app . post ( 'Dropdowns.addDropdowntype' , step = 'add' , dropdowntype_name = 'Concurrent' , dropdowntype_title = 'Concurrent' , dropdowntype_table = '*none' , dropdowntype_column = '' )
kind = app . fixture ( 'inspect' )[ 'dropdowntypes' ][ 0 ][ 'dropdowntype_serial' ]
ids = []
for value in [ 'First' , 'Second' ]:
app . post ( 'Dropdowns.addDropdown' , step = 'add' , dropdowntype_serial = kind , dropdown_value = value )
ident = app . fixture ( 'inspect' )[ 'dropdowns' ][ - 1 ][ 'dropdown_serial' ]; ids . append ( ident ); app . post ( 'Dropdowns.updateDropdownActive' , dropdown_serial = ident , dropdown_checked = 'true' )
result = race ( 'default' , ids ,[ kind ])
assert all ( item [ 'ok' ] for item in result ), result
assert sum ( int ( row [ 'dropdown_default' ]) for row in app . fixture ( 'inspect' )[ 'dropdowns' ]) == 1
print ( 'PASS: Concurrent dropdown default changes preserve one default.' )
app . post ( 'Roster.workspace' , step = 'club' , club_serial = 0 , club_name = 'Concurrent ledger' )
club = app . fixture ( 'inspect' )[ 'clubs' ][ 0 ][ 'club_serial' ]
app . post ( 'Roster.workspace' , step = 'member' , club_serial = club , identifier = 'testadmin' , membership_status = 'active' , joined_on = '2026-01-01' )
app . post ( 'Roster.workspace' , step = 'season' , club_serial = club , season_name = 'Season' , starts_on = '2026-01-01' , ends_on = '2026-12-31' )
data = app . fixture ( 'inspect' ); membership = data [ 'club_memberships' ][ 0 ][ 'membership_serial' ]; season = data [ 'club_seasons' ][ 0 ][ 'season_serial' ]
app . post ( 'Roster.workspace' , step = 'charge' , club_serial = club , membership_serial = membership , season_serial = season , amount = '100.00' )
due = app . fixture ( 'inspect' )[ 'club_dues' ][ 0 ][ 'due_serial' ]
result = race ( 'payment' ,[ due , due ],[ club , '80.00' ])
assert sorted ( row [ 'ok' ] for row in result ) == [ False , True ], result
assert len ( app . fixture ( 'inspect' )[ 'club_payments' ]) == 1
print ( 'PASS: Concurrent payments cannot exceed the outstanding balance.' )
payment = app . fixture ( 'inspect' )[ 'club_payments' ][ 0 ][ 'payment_serial' ]
result = race ( 'reverse' ,[ payment , payment ],[ club , secrets . token_hex ( 32 )])
assert all ( row [ 'ok' ] for row in result ), result
assert len ( app . fixture ( 'inspect' )[ 'club_payment_reversals' ]) == 1
print ( 'PASS: Concurrent reversal retries create only one reversal.' )
result = race ( 'adjust' ,[ due , due ],[ club , secrets . token_hex ( 32 )])
assert all ( row [ 'ok' ] for row in result ), result
assert len ( app . fixture ( 'inspect' )[ 'club_due_adjustments' ]) == 1
print ( 'PASS: Concurrent dues correction retries create only one adjustment.' )
2026-10-21 06:53:45 -04:00
response = app . post ( 'Users.addUser' , step = 'add' , user_name = 'second_admin' , user_first_name = 'Second' , user_last_name = 'Administrator' , user_email = 'second@example.test' , user_role = 'Super System Administrator' )
2026-10-20 06:45:30 -04:00
assert response [ 0 ] == 200 , response [ 1 ]
2026-10-21 06:53:45 -04:00
ids = [ row [ 'user_serial' ] for row in app . fixture ( 'inspect' )[ 'users' ] if row [ 'user_role' ] == 'Super System Administrator' ]
2026-10-20 06:45:30 -04:00
result = race ( 'disable' , ids )
assert sorted ( item [ 'ok' ] for item in result ) == [ False , True ], result
2026-10-21 06:53:45 -04:00
assert next ( item for item in result if not item [ 'ok' ])[ 'status' ] in [ 403 , 409 ], result
assert sum ( row [ 'user_role' ] == 'Super System Administrator' and row [ 'user_status' ] == 'active' for row in app . fixture ( 'inspect' )[ 'users' ]) == 1
2026-10-20 06:45:30 -04:00
print ( 'PASS: Concurrent administrator changes cannot disable the final administrator.' )
2026-10-21 06:53:45 -04:00
tenant = app . fixture ( 'tenantSeed' )
invitation = app . fixture ( 'seedTenantInvitation' , str ( tenant [ 'clubA' ]), str ( tenant [ 'admina' ]), 'invitee@example.test' )[ 'invitation' ]
result = race ( 'accept' ,[ invitation , invitation ],[ tenant [ 'invitee' ]])
assert sorted ( item [ 'ok' ] for item in result ) == [ False , True ], result
assert sum ( row [ 'user_serial' ] == tenant [ 'invitee' ] and row [ 'club_serial' ] == tenant [ 'clubA' ] for row in app . fixture ( 'inspect' )[ 'club_memberships' ]) == 1
print ( 'PASS: Concurrent invitation acceptance grants exactly one membership.' )
# Restore the worker actor after the previous last-super race, for this isolated fixture only.
app . fixture ( 'restoreTestSuper' )
result = race ( 'membership' ,[ tenant [ 'clubA_admina' ], tenant [ 'clubA_admina2' ]],[ tenant [ 'clubA' ]])
assert sorted ( item [ 'ok' ] for item in result ) == [ False , True ], result
assert sum ( row [ 'club_serial' ] == tenant [ 'clubA' ] and row [ 'membership_role' ] == 'administrator' and row [ 'membership_status' ] == 'active' for row in app . fixture ( 'inspect' )[ 'club_memberships' ]) == 1
print ( 'PASS: Concurrent membership changes preserve the last club administrator.' )
print ( '7 concurrency scenarios passed.' )
2026-10-20 06:45:30 -04:00
finally :
for process in workers :
if process . poll () is None : process . terminate (); process . wait ( timeout = 10 )
app . close ()