import re from soapbox.app import create_app from soapbox.config import load_config from tests.conftest import csrf, login, png_bytes def _create_post(client, prefix=""): login(client, prefix=prefix) r = client.post(f"{prefix}/admin/new", data={"csrf": csrf(client)}) assert r.status_code == 302 post_id = r.headers["Location"].split("/admin/edit/")[1] return post_id def test_login_required(client): assert client.get("/admin/").status_code == 302 assert client.get("/admin/login").status_code == 200 assert client.get("/admin/static/admin.css").content_type.startswith("text/css") assert client.get("/admin/static/editor.js").status_code == 200 def test_full_flow_owner(client, cfg): post_id = _create_post(client) assert post_id == "_owner/nytt-innlegg" token = csrf(client) # Last opp et bilde r = client.post( f"/admin/upload/{post_id}", data={"file": (__import__("io").BytesIO(png_bytes()), "Mitt Bilde.PNG")}, headers={"X-CSRF": token}, content_type="multipart/form-data", ) assert r.status_code == 200, r.data assert r.json["filename"] == "mitt-bilde.png" # Forhåndsvisning peker bildet til admin-media r = client.post( f"/admin/preview/{post_id}", json={"body": "![](mitt-bilde.png) [x](/annet/)"}, headers={"X-CSRF": token}, ) assert f'src="/admin/media/{post_id}/mitt-bilde.png"' in r.text assert 'href="../annet/"' in r.text # Publiser: slug følger tittelen body = "Hei **verden**\n\n![](mitt-bilde.png)\n\n[hjem](/)" r = client.post( f"/admin/edit/{post_id}", data={"csrf": token, "title": "Grønt er skjønt", "body": body, "action": "publish"}, ) assert r.headers["Location"].endswith("/admin/edit/_owner/groent-er-skjoent") assert (cfg.content_dir / "posts/groent-er-skjoent/index.md").exists() assert (cfg.content_dir / "posts/groent-er-skjoent/mitt-bilde.png").exists() # Statisk output r = client.get("/groent-er-skjoent") assert r.status_code == 301 and r.headers["Location"].endswith("/groent-er-skjoent/") html = client.get("/groent-er-skjoent/").text assert "verden" in html assert 'href="../theme/style.css"' in html assert 'href="../"' in html # intern lenke omskrevet til relativ assert client.get("/groent-er-skjoent/mitt-bilde.png").status_code == 200 front = client.get("/").text assert "Grønt er skjønt" in front assert 'href="https://kode.naiv.no/olemd/soapbox"' in front # AGPL: lenke til kilden feed = client.get("/feed.xml").text assert "https://blog.example.no/groent-er-skjoent/" in feed # Etter publisering fryses slug selv om tittelen endres r = client.post( "/admin/edit/_owner/groent-er-skjoent", data={"csrf": token, "title": "Ny tittel", "body": body, "action": "save"}, ) assert r.headers["Location"].endswith("/admin/edit/_owner/groent-er-skjoent") # Git-historikk from soapbox.gitstore import log lines = log(cfg.content_dir) assert any("Publiserte: Grønt er skjønt" in line for line in lines) assert all("eier:" in line for line in lines) def test_guest_flow_and_isolation(client, cfg): post_id = _create_post(client) # eierens utkast client.post("/admin/logout", data={"csrf": csrf(client)}) login(client, "gjest1") token = csrf(client) # Gjest kan ikke røre eierens innlegg assert client.get(f"/admin/edit/{post_id}").status_code == 403 assert client.get("/admin/users").status_code == 403 r = client.post("/admin/new", data={"csrf": token}) gid = r.headers["Location"].split("/admin/edit/")[1] assert gid == "gjest1/nytt-innlegg" client.post( f"/admin/edit/{gid}", data={"csrf": token, "title": "Post 1", "body": "hei", "action": "publish"}, ) assert (cfg.content_dir / "guests/gjest1/post-1/index.md").exists() html = client.get("/gjest1/post-1/").text assert 'href="../../theme/style.css"' in html assert "Gjest En" in html assert "Post 1" in client.get("/gjest1/").text assert "Post 1" in client.get("/").text # gjesteinnlegg vises også på forsiden # eierens utkast er ikke publisert assert client.get("/nytt-innlegg/").status_code == 404 def test_login_next_no_open_redirect(client): for bad in ("//evil.com", "/\\evil.com", "https://evil.com"): r = client.post( "/admin/login", data={"username": "eier", "password": "hemmelig123", "next": bad} ) assert r.headers["Location"] == "/admin/", bad r = client.post( "/admin/login", data={"username": "eier", "password": "hemmelig123", "next": "/admin/users"} ) assert r.headers["Location"] == "/admin/users" def test_csrf_enforced(client): login(client) assert client.post("/admin/new", data={}).status_code == 403 def test_subpath_mount(tmp_path): cfg = load_config({"SOAPBOX_DATA_DIR": str(tmp_path / "d"), "SOAPBOX_BASE_PATH": "/blog/"}) assert cfg.base_path == "/blog" app = create_app(cfg) app.config["TESTING"] = True from soapbox import db conn = db.connect(cfg.db_path) db.create_user(conn, "eier", "hemmelig123", db.ROLE_OWNER) conn.close() client = app.test_client() assert client.get("/admin/login").status_code == 404 assert client.get("/blog/admin/login").status_code == 200 login(client, prefix="/blog") r = client.post("/blog/admin/new", data={"csrf": csrf(client, "/blog")}) assert r.headers["Location"].startswith("/blog/admin/edit/") html = client.get("/blog/").text assert 'href="theme/style.css"' in html assert re.search(r'href="admin/"', html) assert client.get("/blog/theme/style.css").status_code == 200 def test_no_inline_js_with_content(client, cfg): """Titler med anførselstegn skal ikke havne i en JS-kontekst (XSS).""" post_id = _create_post(client) token = csrf(client) title = 'Ola\'s "test" ' r = client.post( f"/admin/edit/{post_id}", data={"csrf": token, "title": title, "body": "x", "action": "save"}, ) html = client.get(r.headers["Location"]).text assert "onsubmit=" not in html and "onclick=" not in html assert "Ola's" in html def test_post_history(client, cfg): post_id = _create_post(client) token = csrf(client) r = client.post( f"/admin/edit/{post_id}", data={"csrf": token, "title": "Hist", "body": "første", "action": "publish"}, ) pid = r.headers["Location"].split("/admin/edit/")[1] client.post( f"/admin/edit/{pid}", data={"csrf": token, "title": "Hist", "body": "andre", "action": "save"}, ) html = client.get(f"/admin/history/{pid}").text assert "Publiserte: Hist" in html and "Lagret: Hist" in html sha = re.search(r"sha=([0-9a-f]{40})", html).group(1) diff = client.get(f"/admin/history/{pid}?sha={sha}").text assert '+andre' in diff and '-første' in diff assert client.get(f"/admin/history/{pid}?sha=deadbeef").status_code == 404 assert client.get(f"/admin/history/{pid}?sha=;rm").status_code == 404 # Gjest får ikke se eierens historikk client.post("/admin/logout", data={"csrf": token}) login(client, "gjest1") assert client.get(f"/admin/history/{pid}").status_code == 403