import io import json import secrets import zipfile from xml.etree import ElementTree from application_harness import Application checks=[] def check(condition,message,details=b''): if not condition:raise AssertionError(message+': '+details[:700].decode(errors='replace')) checks.append(message) def expect(response,status,message): check(response[0]==status,message,response[1]);return response[1] app=Application() try: app.login() csrf=app.token expect(app.post('Dropdowns.addDropdowntype',step='add',dropdowntype_name='Unsafe',dropdowntype_title='Unsafe',dropdowntype_table='*function',dropdowntype_column='getSettings'),422,'Callback metadata is rejected on write') expect(app.post('Dropdowns.addDropdowntype',step='add',dropdowntype_name='Colors',dropdowntype_title='Colors',dropdowntype_table='*none',dropdowntype_column=''),200,'Safe dropdown type can be created') kind=app.fixture('inspect')['dropdowntypes'][0]['dropdowntype_serial'] expect(app.post('Dropdowns.addDropdown',step='add',dropdowntype_serial=kind,dropdown_value='Green'),200,'Dropdown value can be created') first=app.fixture('inspect')['dropdowns'][0]['dropdown_serial'] expect(app.post('Dropdowns.updateDropdownDefault',dropdown_serial=first,dropdown_dropdowntype=kind,dropdown_checked='0, dropdown_value=123'),422,'SQL payload is rejected as an invalid boolean') check(app.fixture('inspect')['dropdowns'][0]['dropdown_value']=='Green','Rejected SQL input leaves data unchanged') expect(app.post('Dropdowns.updateDropdownDefault',dropdown_serial=first,dropdown_dropdowntype=kind,dropdown_checked='true'),409,'Inactive dropdown cannot become default') expect(app.post('Dropdowns.updateDropdownActive',dropdown_serial=first,dropdown_checked='true'),200,'Dropdown can be activated') expect(app.post('Dropdowns.updateDropdownDefault',dropdown_serial=first,dropdown_dropdowntype=kind,dropdown_checked='true'),200,'Active dropdown can become default') expect(app.post('Dropdowns.deleteDropdown',dropdown_serial=first),409,'Default dropdown cannot be deleted') expect(app.post('Dropdowns.deleteDropdowntype',dropdowntype_serial=kind),409,'Type with child dropdowns cannot be deleted') expect(app.post('Dropdowns.updateDropdownDefault',dropdown_serial=first,dropdown_dropdowntype=99999,dropdown_checked='true'),404,'Wrong dropdown type is rejected') expect(app.post('Dropdowns.addDropdown',step='add',dropdowntype_serial=kind,dropdown_value='Blue'),200,'Second dropdown can be created') second=app.fixture('inspect')['dropdowns'][-1]['dropdown_serial'] app.post('Dropdowns.updateDropdownActive',dropdown_serial=second,dropdown_checked='true') expect(app.post('Dropdowns.updateDropdownDefault',dropdown_serial=second,dropdown_dropdowntype=kind,dropdown_checked='true'),200,'Default can be switched atomically') check(sum(int(row['dropdown_default']) for row in app.fixture('inspect')['dropdowns'])==1,'Only one dropdown default remains') expect(app.post('Dropdowns.deleteDropdown',dropdown_serial=first),200,'Unused nondefault dropdown can be deleted') unsafe=app.fixture('unsafeRule')['dropdown'] body=expect(app.post('Dropdowns.dropdownActive',dropdown_serial=unsafe),422,'Legacy callback also fails closed when already stored') check(b'DatabasePassword' not in body and b'SmtpPassword' not in body,'Callback failure does not return settings') for name in ['Ready','Waiting']: expect(app.post('Statuses.addStatus',step='add',status_name=name,status_classes='text-primary',status_active='true',status_default='true'),200,'Status '+name+' can be saved') state=app.fixture('inspect') check(sum(int(row['status_default']) for row in state['statuses'])==1,'Only one default status remains') default=next(row['status_serial'] for row in state['statuses'] if row['status_default']) expect(app.post('Statuses.deleteStatus',status_serial=default),409,'Default status cannot be deleted') expect(app.post('Statuses.statusActive',status_serial=default),200,'Status usage check no longer queries workorders') expect(app.post('Statuses.addStatus',step='add',status_name='Broken',status_active='false',status_default='true'),422,'Inactive default status is rejected') admin=state['users'][0] expect(app.post('Users.deleteUser',user_serial=admin['user_serial']),409,'Administrator cannot delete own account') expect(app.post('Users.updateActiveStatus',user_serial=admin['user_serial'],user_active='false'),409,'Last active administrator cannot be disabled') expect(app.post('Users.editUser',step='update',user_serial=admin['user_serial'],user_name=admin['user_name'],user_first_name='Test',user_last_name='Administrator',user_email=admin['user_email'],user_role='User',user_active='true'),409,'Last administrator cannot be demoted') details={'step':'add','user_name':'member_one','user_first_name':'First','user_last_name':'Member','user_email':'member@example.test','user_role':'User'} body=expect(app.post('Users.addUser',**details),200,'Account creation survives invitation delivery failure') check(bool(json.loads(body)['warning']),'Invitation failure has an explicit warning and resend path') expect(app.post('Users.addUser',**details),409,'Retry cannot silently create a duplicate account') member=next(row for row in app.fixture('inspect')['users'] if row['user_name']=='member_one') expect(app.post('Notes.addNote',step='add',note_type='Other',note_reference=member['user_serial'],note_text='bad'),422,'Note type is validated during save') expect(app.post('Notes.addNote',step='add',note_type='User',note_reference=999999,note_text='orphan'),409,'Orphan user note is rejected') expect(app.post('Notes.addNote',step='add',note_type='User',note_reference=member['user_serial'],note_text=''),200,'Valid user note is saved') expect(app.post('Users.deleteUser',user_serial=member['user_serial']),409,'User with note history cannot be deleted') body=expect(app.request(data={'action':'Users.userSummary','user_serial':member['user_serial'],'csrf_token':csrf}),200,'User summary no longer depends on subscriptions') check(b'<script>text only</script>' in body,'Notes are escaped in the summary') expect(app.post('Users.userActive',user_serial=member['user_serial']),200,'User usage check no longer queries requests') expect(app.post('Roster.workspace',step='club',club_serial=0,club_name='North Woods'),200,'First club can be created') club_a=app.fixture('inspect')['clubs'][0]['club_serial'] expect(app.post('Roster.workspace',step='club',club_serial=0,club_name='South Woods'),200,'Second club can be created') club_b=app.fixture('inspect')['clubs'][-1]['club_serial'] for club in [club_a,club_b]: expect(app.post('Roster.workspace',step='season',club_serial=club,season_name='2026 Season',starts_on='2026-01-01',ends_on='2026-12-31'),200,'Club season can be created') expect(app.post('Roster.workspace',step='season',club_serial=club_a,season_name='Invalid',starts_on='2026-12-31',ends_on='2026-01-01'),422,'Reversed season dates are rejected') expect(app.post('Roster.workspace',step='member',club_serial=club_a,identifier='member_one',membership_status='active',joined_on='2026-01-01'),409,'Unverified accounts cannot receive active memberships') app.fixture('verifyEmail','member_one') expect(app.post('Roster.workspace',step='member',club_serial=club_a,identifier='member_one',membership_status='active',joined_on='2026-01-01'),200,'Account can be assigned to a club') expect(app.post('Roster.workspace',step='member',club_serial=club_a,identifier='member_one',membership_status='active',joined_on='2026-01-01'),409,'Duplicate club membership is rejected') state=app.fixture('inspect');membership=state['club_memberships'][0]['membership_serial'];season_a=state['club_seasons'][0]['season_serial'];season_b=state['club_seasons'][1]['season_serial'] expect(app.post('Roster.workspace',step='charge',club_serial=club_a,membership_serial=membership,season_serial=season_b,amount='100.00'),409,'Cross-club season cannot be used for dues') expect(app.post('Roster.workspace',step='charge',club_serial=club_a,membership_serial=membership,season_serial=season_a,amount='100.001'),422,'Money rejects fractional cents') expect(app.post('Roster.workspace',step='charge',club_serial=club_a,membership_serial=membership,season_serial=season_a,amount='100.00'),200,'Season dues can be recorded') expect(app.post('Roster.workspace',step='charge',club_serial=club_a,membership_serial=membership,season_serial=season_a,amount='100.00'),409,'Duplicate season dues are rejected') due=app.fixture('inspect')['club_dues'][0]['due_serial'];key=secrets.token_hex(32) payment=dict(step='payment',club_serial=club_a,due_serial=due,amount='3.14',paid_on='2026-01-02',request_key=key) expect(app.post('Roster.workspace',**payment),200,'Received payment can be recorded') expect(app.post('Roster.workspace',**payment),200,'Repeated payment submission is idempotent') check(len(app.fixture('inspect')['club_payments'])==1,'Payment retry does not double-charge the ledger') expect(app.post('Roster.workspace',**{**payment,'amount':'100.00','request_key':secrets.token_hex(32)}),409,'Payment above balance is rejected') for action,signature in [('csv',b'Club,Username'),('excel',b'PK'),('pdf',b'%PDF')]: body=expect(app.request(data={'action':'RosterReports.'+action,'club_serial':club_a,'csrf_token':csrf}),200,action+' report downloads successfully') check(body.startswith(signature),action+' report has the correct file format') if action=='excel': with zipfile.ZipFile(io.BytesIO(body)) as archive: root=ElementTree.fromstring(archive.read('xl/worksheets/sheet1.xml')) check(not root.findall('.//{http://schemas.openxmlformats.org/spreadsheetml/2006/main}f'),'Spreadsheet cells do not contain formulas') body=expect(app.post('Users.editUser',step='update',user_serial=member['user_serial'],user_name='member_one',user_first_name='=HYPERLINK("unsafe")',user_last_name='Updated',user_email='member@example.test',user_role='User',user_active='true'),200,'Administrator can edit first and last names') body=app.request(data={'action':'RosterReports.csv','club_serial':club_a,'csrf_token':csrf})[1] check(b"'=HYPERLINK" in body,'CSV formula text is neutralized') # Set a known member password in this disposable fixture via the real reset flow. # Fixture token generation is intentionally scoped to httpuser, so create that # account through signup and assign it separately for isolation tests. app.logout();_,signup,_=app.request('/?Xaction=signup') expect(app.request(data={'action':'SignUp.createAccount','csrf_token':app.csrf(signup),'user_first_name':'Regular','user_last_name':'Member','user_email':'http@example.test','user_name':'httpuser','user_password':'HTTP long password!','user_password_confirm':'HTTP long password!'}),200,'Regular member account can sign up') app.login();csrf=app.token app.fixture('verifyEmail','httpuser') app.post('Roster.workspace',step='member',club_serial=club_a,identifier='httpuser',membership_status='active',joined_on='2026-01-01') app.logout();app.login('http@example.test','HTTP long password!') body=expect(app.request('/?Xaction=Roster.workspace&club_serial='+str(club_a)),200,'Member can view assigned club') check(b'South Woods' not in body,'Other clubs are absent from member navigation') check(b'$100.00' not in body,'Member cannot view another member dues') expect(app.request('/?Xaction=Roster.workspace&club_serial='+str(club_b)),403,'Member cannot access another club by changing its ID') expect(app.post('Roster.workspace',step='club',club_serial=0,club_name='Unauthorized'),403,'Member cannot create clubs') expect(app.post('RosterReports.csv',club_serial=club_a),403,'Member cannot download administrator exports') expect(app.post('Users.deleteUser',user_serial=member['user_serial']),403,'Member cannot delete accounts') # Keep that member session alive while a separate administrator session changes # membership/club status; access must be rechecked on each request. member_client=app.client import urllib.request,http.cookiejar app.client=urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar())) app.login();csrf=app.token;admin_client=app.client http_membership=next(row['membership_serial'] for row in app.fixture('inspect')['club_memberships'] if row['user_serial']==next(user['user_serial'] for user in app.fixture('inspect')['users'] if user['user_name']=='httpuser')) expect(app.post('Roster.workspace',step='member',club_serial=club_a,membership_serial=http_membership,membership_status='inactive',joined_on='2026-01-01'),200,'Membership can be made inactive while preserving history') app.client=member_client expect(app.request('/?Xaction=Roster.workspace&club_serial='+str(club_a)),403,'Inactive membership immediately loses club access in an existing session') app.client=admin_client app.post('Roster.workspace',step='member',club_serial=club_a,membership_serial=http_membership,membership_status='active',joined_on='2026-01-01') expect(app.post('Roster.workspace',step='club',club_serial=club_a,club_name='North Woods',club_status='archived'),200,'Club can be archived without deleting its history') app.client=member_client expect(app.request('/?Xaction=Roster.workspace&club_serial='+str(club_a)),403,'Archived club is inaccessible to ordinary members') app.client=admin_client expect(app.post('Roster.workspace',step='club',club_serial=club_a,club_name='North Woods',club_status='active'),200,'Archived club can be reactivated') check(len(app.fixture('inspect')['club_payments'])==1,'Archiving and reactivation preserve the payment ledger') body=expect(app.request(data={'action':'Journal.viewJournal','csrf_token':csrf}),200,'Journal page loads after authentication events') check(b'logged in' in body,'Authentication writes journal entries') for action in ['Users.setupUsers','Dropdowns.setupDropdowntypes','Statuses.setupStatuses','Alerts.setupAlerts','Popovers.setupPopovers','Journal.viewJournal']: expect(app.request(data={'action':action,'pagination':'last','csrf_token':csrf}),200,action+' handles last page without SQL errors') body=expect(app.post('getZipcode',zip_code='12345'),200,'ZIP lookup uses a valid route and JSON response') json.loads(body) app.fixture('paging') from lxml import html as parser import re response=app.request(data={'action':'Popovers.setupPopovers','pagination':'last','csrf_token':csrf}) body=expect(response,200,'Last-page query works with 41 real records') text=re.sub(r'\s+',' ',parser.fromstring(body).text_content()) check('Showing 41 to 41 of 41' in text,'Last page starts at record 41 rather than record 22') body=app.request(data={'action':'Users.setupUsers','csrf_token':csrf})[1] text=re.sub(r'\s+',' ',parser.fromstring(body).text_content()) check('Showing 1 to 20 of 44' in text,'User pagination is independent of popover pagination') body=app.request(data={'action':'Users.setupUsers','find_user':'Family41','csrf_token':csrf})[1] check(b'Family41' in body and b'paging40@example.test' not in body,'Users can be searched by last name') body=app.request(data={'action':'Popovers.setupPopovers','pagination':'first','popovers_per_page':'40','csrf_token':csrf})[1] text=re.sub(r'\s+',' ',parser.fromstring(body).text_content()) check('Showing 1 to 40 of 41' in text,'Per-page selection is respected') expect(app.post('Journal.clearJournal'),404,'Journal-clearing endpoint is no longer routable') expect(app.post('Roster.workspace',step='adjust',club_serial=club_a,due_serial=due,corrected_amount='90.00',reason='HTTP correction',request_key=secrets.token_hex(32)),200,'Dues corrections work through the protected HTTP route') expect(app.post('Roster.workspace',step='payment',club_serial=club_a,due_serial=due,amount='1.00',paid_on='2026-01-02',request_key=secrets.token_hex(32),payment_method='check',payment_reference='=SUM(1,1)'),200,'Payment references and methods are saved through HTTP') new_payment=app.fixture('inspect')['club_payments'][-1]['payment_serial'] expect(app.post('Roster.workspace',step='reverse',club_serial=club_a,payment_serial=new_payment,reason='HTTP reversal',request_key=secrets.token_hex(32)),200,'Payment reversals work through the protected HTTP route') body=expect(app.request(data={'action':'RosterReports.ledger','club_serial':club_a,'csrf_token':csrf}),200,'Financial history exports through HTTP') check(b"'=SUM(1,1)" in body,'Financial CSV neutralizes formula text in payment references') check(b'HTTP correction' in body and b'HTTP reversal' in body,'Financial CSV includes correction and reversal reasons') body=expect(app.request('/?Xaction=verifyemail'),200,'Public email verification page renders') expect(app.request(data={'action':'verifyemail','step':'confirm','token':'0'*64,'user_password':'wrong'}),403,'Email verification rejects a missing CSRF token') headers=app.request()[2] check("script-src 'self'" in headers['Content-Security-Policy'],'Application sends a CSP without inline-script permission') print('\n'.join('PASS: '+message for message in checks)) print(f'{len(checks)} project HTTP checks passed.') finally:app.close()