Files

50 lines
5.0 KiB
Python
Raw Permalink Normal View History

import re
2026-10-21 20:41:57 -04:00
from lxml import html
from application_harness import Application
app=Application();checks=0
def check(ok,message):
global checks
assert ok,message
checks+=1;print('PASS:',message)
def summary(club,membership,extra=''):
return app.request('/main.php?Xaction=Roster.memberSummary&club_serial='+str(club)+'&membership_serial='+str(membership)+extra)
try:
ids=app.fixture('tenantSeed');app.fixture('memberSummarySeed',str(ids['clubA']),str(ids['clubB']),str(ids['clubA_membera']),str(ids['clubA_shared']),str(ids['clubB_shared']))
_,login,_=app.request()
app.request(data={'action':'SignIn.login','csrf_token':app.csrf(login),'user_email':'admin@example.test','user_password':'Admin test password 123!'})
status,body,_=summary(ids['clubA'],ids['clubA_membera'])
check(status==200 and b'<h1>Account security</h1>' in body and b'<h1>Tenant Membera</h1>' not in body,'Member summary cannot bypass mandatory administrator MFA')
check(app.request('/main.php?Xaction=Roster.memberSummary&club_serial='+str(ids['clubA'])+'&membership_serial='+str(ids['clubA_membera']),ajax=True)[0]==403,'AJAX member summary also requires completed administrator MFA')
app.login();before=app.fixture('fingerprints')
status,body,_=summary(ids['clubA'],ids['clubA_membera'])
check(status==200 and b'<h1>Tenant Membera</h1>' in body and b'membera@example.test' in body,'Super Administrator opens a club member summary')
check(b'$3,150.00' in body and b'$31.00' in body and b'$3,119.00' in body,'Totals include all dues corrections and exclude reversed payments across every page')
check(b'Own reversed receipt' in body and b'OWN-RECEIPT-32' in body,'Payment history preserves reversed receipts and their reasons')
check(b'&lt;script&gt;unsafe&lt;/script&gt;' in body and b'<script>unsafe</script>' not in body,'Correction reasons are escaped safely')
check(b'NEIGHBOR-PRIVATE' not in body and b'FOREIGN-PRIVATE' not in body,'Selected member summary excludes neighboring members and other clubs')
2026-10-21 20:41:57 -04:00
check(html.fromstring(body).xpath('//h2[normalize-space(.)="Payment history"]/following-sibling::div[1]//table')[0].text_content().count('OWN-RECEIPT-')==25 and b'31 records.' in body and b'32 records.' in body,'Member dues and payment histories are bounded and counted independently')
links=re.findall(rb'href="([^"]+)"',body)
check(any(b'payments_page=2' in link and b'membership_serial='+str(ids['clubA_membera']).encode() in link and b'Roster.memberSummary' in link for link in links),'History pager links retain both club and membership scope')
status,last,_=summary(ids['clubA'],ids['clubA_membera'],'&payments_page=9999&dues_page=9999')
2026-10-21 20:41:57 -04:00
check(status==200 and html.fromstring(last).xpath('//h2[normalize-space(.)="Payment history"]/following-sibling::div[1]//table')[0].text_content().count('OWN-RECEIPT-')==7 and b'Page 2 of 2' in last,'Final history pages are clamped and show only remaining records')
check(b'$3,150.00' in last and b'$31.00' in last and b'$3,119.00' in last,'Summary totals remain identical on later history pages')
check(summary(ids['clubB'],ids['clubA_membera'])[0]==404,'Membership IDs cannot be paired with another club')
check(summary(ids['clubA'],'invalid')[0]==422,'Invalid membership identifiers are rejected')
check(summary(ids['clubA'],ids['clubA_membera'],'&payments_page=0')[0]==422,'Invalid history pages are rejected')
check(app.fixture('fingerprints')==before,'Opening summaries and changing pages perform no database writes beyond authentication bookkeeping')
app.logout();app.login('admina@example.test','Tenant test password 123!')
check(summary(ids['clubA'],ids['clubA_membera'])[0]==200,'Club administrator can view a member in the administered club')
check(summary(ids['clubB'],ids['clubB_shared'])[0]==404,'Administrator cannot view another member where they are only an ordinary member')
app.logout();app.login('adminb@example.test','Tenant test password 123!')
check(summary(ids['clubA'],ids['clubA_membera'])[0]==403,'Other club administrator is denied access to the club summary')
app.logout();app.login('membera@example.test','Tenant test password 123!')
status,body,_=summary(ids['clubA'],ids['clubA_membera'])
check(status==200 and b'$3,119.00' in body and b'club-action-modal' not in body,'Member can read their own finances without administrator actions')
check(summary(ids['clubA'],ids['clubA_shared'])[0]==404,'Member cannot read another member summary in the same club')
check(summary(ids['clubB'],ids['clubB_shared'])[0]==403,'Member cannot read another club summary')
app.logout();app.login('shared@example.test','Tenant test password 123!')
_,a,_=summary(ids['clubA'],ids['clubA_shared']);_,b,_=summary(ids['clubB'],ids['clubB_shared'])
check(b'NEIGHBOR-PRIVATE' in a and b'FOREIGN-PRIVATE' not in a and b'FOREIGN-PRIVATE' in b and b'NEIGHBOR-PRIVATE' not in b,'A member of two clubs sees separate membership histories')
print(checks,'member summary HTTP checks passed.')
finally:app.close()