import re from fastapi.testclient import TestClient from bs4 import BeautifulSoup import pytest from parser_lab.app import create_app from parser_lab.config import Settings from parser_lab.views import default_view @pytest.fixture def env(tmp_path): settings=Settings(tmp_path,'teacher','test-password','test-secret-long-enough-123456789',False,('http://testserver',)) app=create_app(settings) with TestClient(app) as client: yield app,client def login(client): page=client.get('/login') csrf=BeautifulSoup(page.text,'html.parser').select_one('[name=csrf]')['value'] return client.post('/login',data={'username':'teacher','password':'test-password','csrf':csrf},headers={'origin':'http://testserver'},follow_redirects=False) def test_auth_and_csrf(env): app,c=env assert c.get('/healthz').status_code==200 assert c.get('/admin',follow_redirects=False).status_code==303 assert c.post('/login',data={'username':'teacher','password':'test-password'}).status_code==403 assert login(c).status_code==303 assert c.get('/admin').status_code==200 assert c.post('/logout',headers={'origin':'https://bad.example'}).status_code==403 def test_admin_is_server_rendered_for_headless_browser(env): app,c=env login(c) page=c.get('/admin',headers={'user-agent':'Mozilla/5.0 HeadlessChrome/140.0.0.0 Safari/537.36'}) assert page.status_code==200 and 'Коллекции данных' in page.text assert '/rio/' not in page.text def test_logout_expiry_rate_limit_and_replacement_credentials(env): from parser_lab.auth import Auth, Unauthorized,RateLimited from dataclasses import replace app,c=env; auth=app.state.auth token=auth.login('teacher','test-password','one') assert auth.check(token)==token with app.state.repo.connect() as db: db.execute('UPDATE sessions SET expires=0') with pytest.raises(Unauthorized): auth.check(token) for _ in range(5): with pytest.raises(Unauthorized): auth.login('teacher','bad','two') with pytest.raises(RateLimited): auth.login('teacher','test-password','two') login(c); token=c.cookies['lab_session'] assert c.post('/logout',data={'csrf':auth.csrf(token)},headers={'origin':'http://testserver'},follow_redirects=False).status_code==303 with pytest.raises(Unauthorized): auth.check(token) token=auth.login('teacher','test-password','three') changed=Auth(replace(auth.settings,secret='changed'),app.state.repo) with pytest.raises(Unauthorized): changed.check(token) def test_oversized_upload_rejected_before_parsing(env): app,c=env;login(c) response=c.post('/api/admin/import',content=b'x',headers={'origin':'http://testserver','x-csrf-token':app.state.auth.csrf(c.cookies['lab_session']),'content-length':str(30*1024*1024)}) assert response.status_code==413 def test_html_snapshot_and_escaping(env): app,c=env; repo=app.state.repo d=repo.create('Исследование',b'[]','a.json',[{'text':'','secret':'HIDDEN'} for _ in range(51)]) d['views']={'table':default_view('table',['/text'])}; d=repo.save(d,1); p=repo.publish(d['id'],d['revision']) base=f"/html/{p['slug']}/v/1/table" page=c.get(base); soup=BeautifulSoup(page.text,'html.parser') assert page.status_code==200 and len(soup.select('tbody tr'))==25 assert soup.select_one('tbody td.field').text.strip()=='' assert not soup.select('script') and 'HIDDEN' not in page.text assert len(BeautifulSoup(c.get(base+'?page=3').text,'html.parser').select('tbody tr'))==1 assert c.get(base+'?page=0').status_code==400 assert c.get(base+'?page=no').status_code==400 assert c.get(base+'?page=4').status_code==404 assert 'HIDDEN' not in c.get(base+'/records/1').text assert c.get(f"/preview/{d['id']}/table",follow_redirects=False).status_code==303 repo.unpublish(p['slug'],1) assert c.get(base).status_code==404 assert c.get(f"/interactive/{p['slug']}/v/1/table").status_code==404 def test_preview_filters_match_publication(env): app,c=env;login(c);repo=app.state.repo d=repo.create('Filter',b'[]','a.json',[{'name':'Alice'},{'name':'Bob'}]) view=default_view('table',['/name']);view['search_pointer']='/name' d['views']={'table':view};d=repo.save(d,1) response=c.get(f"/preview/{d['id']}/table?q=Alice") assert BeautifulSoup(response.text,'html.parser').select_one('.field').text=='Alice' assert len(BeautifulSoup(response.text,'html.parser').select('.record'))==1 def test_all_pages_share_favicon(env): app,client=env static=client.get('/static/favicon.png') assert static.status_code==200 and static.content.startswith(b'\x89PNG\r\n\x1a\n') repo=app.state.repo d=repo.create('Icons',b'[]','a.json',[{'name':'Alice'}]) d['views']={'table':default_view('table',['/name'])};d=repo.save(d,1);repo.publish(d['id'],d['revision']) login(client) for path in ('/','/login','/admin',f"/html/{d['slug']}/v/1/table",f"/interactive/{d['slug']}/v/1/table"): page=BeautifulSoup(client.get(path).text,'html.parser') assert page.select_one('link[rel=icon]')['href']=='/static/favicon.png'