import json,secrets,urllib.request,urllib.parse,urllib.error from application_harness import Application app=Application();checks=0 def check(ok,message): global checks assert ok,message checks+=1;print('PASS:',message) def url(club,member):return '/main.php?Xaction=Roster.memberSummary&club_serial='+str(club)+'&membership_serial='+str(member) def save(step,club,member,**extra): data={'action':'Roster.memberSummary','csrf_token':app.token,'step':step,'club_serial':club,'membership_serial':member,**extra} request=urllib.request.Request(app.base+url(club,member),data=urllib.parse.urlencode(data).encode(),headers={'X-Requested-With':'XMLHttpRequest','X-HCR-Modal':'1'}) try: with app.client.open(request) as response:return response.status,json.loads(response.read()) except urllib.error.HTTPError as error: body=error.read() try:result=json.loads(body) except json.JSONDecodeError:result={'error':body.decode(errors='replace')} return error.code,result try: ids=app.fixture('tenantSeed');a=ids['clubA'];b=ids['clubB'];member=ids['clubA_membera'];neighbor=ids['clubA_shared'];foreign=ids['clubB_shared'] app.login() for club,name in [(a,'New season'),(a,'Other season'),(b,'Foreign season')]:app.post('Roster.workspace',step='season',club_serial=club,season_name=name,starts_on='2026-01-01',ends_on='2026-12-31') state=app.fixture('inspect');season=next(s['season_serial'] for s in state['club_seasons'] if s['season_name']=='New season');foreignseason=next(s['season_serial'] for s in state['club_seasons'] if s['season_name']=='Foreign season') app.post('Roster.workspace',step='charge',club_serial=a,membership_serial=neighbor,season_serial=season,amount='100.00') app.post('Roster.workspace',step='charge',club_serial=b,membership_serial=foreign,season_serial=foreignseason,amount='100.00') state=app.fixture('inspect');neighbor_due=state['club_dues'][0]['due_serial'];foreign_due=state['club_dues'][1]['due_serial'] app.logout();app.login('admina@example.test','Tenant test password 123!') _,body,_=app.request(url(a,member)) check(b'data-club-step="member"' in body and b'data-club-step="charge"' in body,'Club administrator gets member edit and dues dialogs') check(save('charge',a,member,csrf_token='invalid',season_serial=season,amount='100.00')[0]==403,'Member summary writes require a valid CSRF token') check(save('charge',a,member,membership_serial=neighbor,season_serial=season,amount='100.00')[0]==409,'Posted membership cannot differ from the selected summary') status,result=save('charge',a,member,season_serial=season,amount='100.00') check(status==200 and result['ok'] and 'Roster.memberSummary' in result['redirect'] and 'membership_serial='+str(member) in result['redirect'],'Dues save returns a same-member summary redirect') state=app.fixture('inspect');due=next(d['due_serial'] for d in state['club_dues'] if d['membership_serial']==member) check(save('charge',a,member,season_serial=season,amount='100.00')[0]==409,'Duplicate season dues remain rejected') check(save('charge',a,member,season_serial=foreignseason,amount='10.00')[0]==409,'Dues cannot use another club season') check(save('payment',a,member,due_serial=neighbor_due,amount='1.00',paid_on='2026-01-01',request_key=secrets.token_hex(32))[0]==404,'Payment cannot target a different member in the same club') check(save('payment',a,member,due_serial=foreign_due,amount='1.00',paid_on='2026-01-01',request_key=secrets.token_hex(32))[0]==404,'Payment cannot target another club dues') key=secrets.token_hex(32) check(save('payment',a,member,due_serial=due,amount='25.00',paid_on='2026-01-01',request_key=key,payment_method='cash',payment_reference='Member summary receipt')[0]==200,'Payment is recorded for the selected membership') check(save('payment',a,member,due_serial=due,amount='25.00',paid_on='2026-01-01',request_key=key,payment_method='cash',payment_reference='Member summary receipt')[0]==200 and len(app.fixture('inspect')['club_payments'])==1,'Payment retry remains idempotent') app.fixture('mfaTestAuditFailure','on') status,_=save('payment',a,member,due_serial=due,amount='1.00',paid_on='2026-01-01',request_key=secrets.token_hex(32)) app.fixture('mfaTestAuditFailure','off') check(status==500 and len(app.fixture('inspect')['club_payments'])==1,'Failed mandatory audit rolls back a summary payment completely') check(save('payment',a,member,due_serial=due,amount='76.00',paid_on='2026-01-01',request_key=secrets.token_hex(32))[0]==409,'Overpayments remain rejected') status,result=save('member',a,member,membership_status='active',membership_role='member',joined_on='2026-02-01') check(status==200 and next(m for m in app.fixture('inspect')['club_memberships'] if m['membership_serial']==member)['joined_on']=='2026-02-01','Membership information saves from the summary') check(save('member',a,member,membership_status='active',membership_role='administrator',joined_on='2026-02-01')[0]==403,'Club administrator cannot promote members to administrator') check(save('member',a,ids['clubA_admina2'],membership_status='inactive',membership_role='member',joined_on='2026-02-01')[0]==403,'Club administrator cannot edit protected administrator memberships') sid=next(c.value for h in app.client.handlers if hasattr(h,'cookiejar') for c in h.cookiejar if c.name=='HUNTINGCLUBROSTER');app.fixture('sessionAge',sid,'','','301') check(save('member',a,member,membership_status='active',membership_role='member',joined_on='2026-03-01')[0]==403,'Membership edits require recent MFA on the new endpoint') app.logout();app.login('membera@example.test','Tenant test password 123!') _,body,_=app.request(url(a,member));check(b'club-action-modal' not in body,'Ordinary member receives no action dialogs') check(save('payment',a,member,due_serial=due,amount='1.00',paid_on='2026-01-01',request_key=secrets.token_hex(32))[0]==403,'Ordinary member cannot forge a summary payment request') check(save('charge',a,member,season_serial=season,amount='1.00')[0]==403,'Ordinary member cannot forge a dues request') app.logout();app.login('adminb@example.test','Tenant test password 123!') check(save('member',a,member,membership_status='active',membership_role='member',joined_on='2026-03-01')[0]==403,'Other club administrator cannot modify the membership') app.logout();app.login();app.post('Roster.workspace',step='club',club_serial=a,club_name='Tenant Alpha',club_status='archived') _,body,_=app.request(url(a,member));check(b'club-action-modal' not in body,'Archived club summary exposes no write dialogs') check(save('charge',a,member,season_serial=season,amount='10.00')[0]==409,'Archived club rejects forged member summary writes') print(checks,'member action HTTP checks passed.') finally: app.fixture('mfaTestAuditFailure','off');app.close()