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'