mirror of
https://gitlab.archlinux.org/archlinux/aurweb.git
synced 2025-02-03 10:43:03 +01:00
155 lines
4.9 KiB
Python
155 lines
4.9 KiB
Python
import re
|
|
import urllib.parse
|
|
from http import HTTPStatus
|
|
|
|
import lxml.etree
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from aurweb import db
|
|
from aurweb.asgi import app
|
|
from aurweb.models.account_type import USER_ID
|
|
from aurweb.models.user import User
|
|
from aurweb.testing.requests import Request
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(db_test):
|
|
return
|
|
|
|
|
|
@pytest.fixture
|
|
def client() -> TestClient:
|
|
yield TestClient(app=app)
|
|
|
|
|
|
@pytest.fixture
|
|
def user() -> User:
|
|
with db.begin():
|
|
user = db.create(
|
|
User,
|
|
Username="test",
|
|
Email="test@example.org",
|
|
RealName="Test User",
|
|
Passwd="testPassword",
|
|
AccountTypeID=USER_ID,
|
|
)
|
|
yield user
|
|
|
|
|
|
def test_index(client: TestClient):
|
|
"""Test the index route at '/'."""
|
|
with client as req:
|
|
response = req.get("/")
|
|
assert response.status_code == int(HTTPStatus.OK)
|
|
|
|
|
|
def test_index_security_headers(client: TestClient):
|
|
"""Check for the existence of CSP, XCTO, XFO and RP security headers.
|
|
|
|
CSP: Content-Security-Policy
|
|
XCTO: X-Content-Type-Options
|
|
RP: Referrer-Policy
|
|
XFO: X-Frame-Options
|
|
"""
|
|
# Use `with` to trigger FastAPI app events.
|
|
with client as req:
|
|
response = req.get("/")
|
|
assert response.status_code == int(HTTPStatus.OK)
|
|
assert response.headers.get("Content-Security-Policy") is not None
|
|
assert response.headers.get("X-Content-Type-Options") == "nosniff"
|
|
assert response.headers.get("Referrer-Policy") == "same-origin"
|
|
assert response.headers.get("X-Frame-Options") == "SAMEORIGIN"
|
|
|
|
|
|
def test_favicon(client: TestClient):
|
|
"""Test the favicon route at '/favicon.ico'."""
|
|
with client as request:
|
|
response1 = request.get("/static/images/favicon.ico")
|
|
response2 = request.get("/favicon.ico")
|
|
assert response1.status_code == int(HTTPStatus.OK)
|
|
assert response1.content == response2.content
|
|
|
|
|
|
def test_language(client: TestClient):
|
|
"""Test the language post route as a guest user."""
|
|
post_data = {"set_lang": "de", "next": "/"}
|
|
with client as req:
|
|
response = req.post("/language", data=post_data)
|
|
assert response.status_code == int(HTTPStatus.SEE_OTHER)
|
|
|
|
|
|
def test_language_invalid_next(client: TestClient):
|
|
"""Test an invalid next route at '/language'."""
|
|
post_data = {"set_lang": "de", "next": "https://evil.net"}
|
|
with client as req:
|
|
response = req.post("/language", data=post_data)
|
|
assert response.status_code == int(HTTPStatus.BAD_REQUEST)
|
|
|
|
|
|
def test_user_language(client: TestClient, user: User):
|
|
"""Test the language post route as an authenticated user."""
|
|
post_data = {"set_lang": "de", "next": "/"}
|
|
|
|
sid = user.login(Request(), "testPassword")
|
|
assert sid is not None
|
|
|
|
with client as req:
|
|
response = req.post("/language", data=post_data, cookies={"AURSID": sid})
|
|
assert response.status_code == int(HTTPStatus.SEE_OTHER)
|
|
assert user.LangPreference == "de"
|
|
|
|
|
|
def test_language_query_params(client: TestClient):
|
|
"""Test the language post route with query params."""
|
|
next = urllib.parse.quote_plus("/")
|
|
post_data = {"set_lang": "de", "next": "/", "q": f"next={next}"}
|
|
q = post_data.get("q")
|
|
with client as req:
|
|
response = req.post("/language", data=post_data)
|
|
assert response.headers.get("location") == f"/?{q}"
|
|
assert response.status_code == int(HTTPStatus.SEE_OTHER)
|
|
|
|
|
|
def test_error_messages(client: TestClient):
|
|
with client as request:
|
|
response1 = request.get("/thisroutedoesnotexist")
|
|
response2 = request.get("/raisefivethree")
|
|
assert response1.status_code == int(HTTPStatus.NOT_FOUND)
|
|
assert response2.status_code == int(HTTPStatus.SERVICE_UNAVAILABLE)
|
|
|
|
|
|
def test_nonce_csp(client: TestClient):
|
|
with client as request:
|
|
response = request.get("/")
|
|
data = response.headers.get("Content-Security-Policy")
|
|
nonce = next(field for field in data.split("; ") if "nonce" in field)
|
|
match = re.match(r"^script-src .*'nonce-([a-fA-F0-9]{8})' .*$", nonce)
|
|
nonce = match.group(1)
|
|
assert nonce is not None and len(nonce) == 8
|
|
|
|
parser = lxml.etree.HTMLParser(recover=True)
|
|
root = lxml.etree.fromstring(response.text, parser=parser)
|
|
|
|
nonce_verified = False
|
|
scripts = root.xpath("//script")
|
|
for script in scripts:
|
|
if script.text is not None:
|
|
assert "nonce" in script.keys()
|
|
if not (nonce_verified := (script.get("nonce") == nonce)):
|
|
break
|
|
assert nonce_verified is True
|
|
|
|
|
|
def test_id_redirect(client: TestClient):
|
|
with client as request:
|
|
response = request.get(
|
|
"/",
|
|
params={
|
|
"id": "test", # This param will be rewritten into Location.
|
|
"key": "value", # Test that this param persists.
|
|
"key2": "value2", # And this one.
|
|
},
|
|
allow_redirects=False,
|
|
)
|
|
assert response.headers.get("location") == "/test?key=value&key2=value2"
|