Files
huntingclubroster/tests/mfa_concurrency.py
T

35 lines
1.7 KiB
Python

import json,subprocess
from application_harness import Application,ROOT,PHP
app=Application();workers=[]
def race(operation,seed):
group=[]
for state in seed['states']:
p=subprocess.Popen(PHP+['tests/mfa_worker.php',operation],cwd=ROOT,env=app.env,stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True)
workers.append(p);group.append(p)
p.stdin.write(json.dumps({'state':state,'recovery':seed['recovery']})+'\n');p.stdin.flush()
for p in group:assert p.stdout.readline().strip()=='READY'
for p in group:p.stdin.write('go\n');p.stdin.flush()
result=[]
for p in group:
out,err=p.communicate(timeout=30)
assert p.returncode==0,err
result.append(json.loads(out))
return result
try:
seed=json.loads(subprocess.check_output(PHP+['tests/mfa_worker.php','seed'],cwd=ROOT,env=app.env,text=True))
before=app.fixture('mfaState','admin@example.test')
result=race('verify',seed)
after=app.fixture('mfaState','admin@example.test')
assert sorted(r['ok'] for r in result)==[False,True] and len(after['recovery'])==len(before['recovery'])-1,result
print('PASS: Concurrent recovery attempts consume a single-use code exactly once.')
result=race('replace',seed)
after=app.fixture('mfaState','admin@example.test')
assert sorted(r['ok'] for r in result)==[False,True],result
assert len(after['recovery'])==10 and after['factor']['secret_cipher']!=before['factor']['secret_cipher']
print('PASS: Competing authorized replacements commit exactly one complete factor swap.')
print('2 MFA concurrency checks passed.')
finally:
for p in workers:
if p.poll() is None:p.kill();p.wait()
app.close()