2026-10-21 06:53:45 -04:00
import re
2026-10-21 20:41:57 -04:00
from lxml import html
2026-10-21 06:53:45 -04:00
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 '<script>unsafe</script>' 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' )
2026-10-21 06:53:45 -04:00
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' )
2026-10-21 06:53:45 -04:00
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 ()