1
0
Fork 0
mirror of https://github.com/EDeev/declaude.git synced 2026-10-07 20:49:57 +03:00

Тесты: защита ссылок, экранирование, рендер Markdown, сборка архива и командная строка

20 тестов на тестовом экспорте tests/fixtures (вымышленные чаты и проект).
This commit is contained in:
Egor Deev 2026-10-02 20:38:58 +00:00
parent 676ca1e926
commit f5fa349815
8 changed files with 128 additions and 0 deletions

3
requirements-dev.txt Normal file
View file

@ -0,0 +1,3 @@
pytest==8.4.2
ruff==0.14.0
build==1.3.0

0
tests/__init__.py Normal file
View file

View file

@ -0,0 +1,13 @@
[
{"uuid": "c-1", "name": "Нормализация до 3НФ", "created_at": "2026-02-03T09:00:00Z", "updated_at": "2026-02-03T09:30:00Z",
"chat_messages": [
{"uuid": "m-1", "sender": "human", "text": "Как привести таблицу к 3НФ?", "created_at": "2026-02-03T09:00:00Z", "content": [{"type": "text", "text": "Как привести таблицу к 3НФ?"}], "attachments": [], "files": []},
{"uuid": "m-2", "sender": "assistant", "text": "", "created_at": "2026-02-03T09:01:00Z", "content": [{"type": "text", "text": "## Шаги\n\n1. Убрать **повторяющиеся группы**\n2. Вынести *частичные* зависимости\n\n| Форма | Условие |\n|---|---|\n| 1НФ | атомарность |\n| 2НФ | нет частичных |\n\n```sql\nSELECT id, name FROM users WHERE id > 10;\n```\n\nСм. [документацию](https://www.postgresql.org/docs/) и `inline code`.\n\nОпасная ссылка: [клик](javascript:alert(1)) и картинка ![x](javascript:alert(2))\n\n<script>alert(3)</script>"}], "attachments": [], "files": []}
]},
{"uuid": "c-2", "name": "", "created_at": "2026-02-10T12:00:00Z", "updated_at": "2026-02-10T12:05:00Z",
"chat_messages": [
{"uuid": "m-3", "sender": "human", "text": "Напиши функцию на Python", "created_at": "2026-02-10T12:00:00Z", "content": [{"type": "text", "text": "Напиши функцию на Python"}], "attachments": [], "files": []},
{"uuid": "m-4", "sender": "assistant", "text": "", "created_at": "2026-02-10T12:01:00Z", "content": [{"type": "text", "text": "```python\ndef add(a, b):\n return a + b\n```"}], "attachments": [], "files": []}
]},
{"uuid": "c-3", "name": "Пустой чат", "created_at": "2026-02-11T12:00:00Z", "updated_at": "2026-02-11T12:00:00Z", "chat_messages": []}
]

View file

@ -0,0 +1 @@
{"uuid": "p-1", "name": "Курсовая по базам данных", "description": "Проектирование схемы и запросы", "created_at": "2026-02-01T10:00:00Z", "updated_at": "2026-03-01T10:00:00Z", "prompt_template": "Отвечай кратко", "docs": [{"filename": "schema.sql", "content": "CREATE TABLE users (id int);", "created_at": "2026-02-02T10:00:00Z"}]}

1
tests/fixtures/backup/users.json vendored Normal file
View file

@ -0,0 +1 @@
[{"uuid": "u-1", "full_name": "Тестовый Пользователь", "email_address": "user@example.com"}]

1
tests/fixtures/mapping.json vendored Normal file
View file

@ -0,0 +1 @@
{"c-1": "p-1"}

55
tests/test_build.py Normal file
View file

@ -0,0 +1,55 @@
import json
import shutil
from pathlib import Path
import pytest
from declaude import DataLoader, MappingManager, SiteBuilder
from declaude.cli import main
FIXTURE = Path(__file__).parent / "fixtures" / "backup"
@pytest.fixture
def archive(tmp_path):
loader = DataLoader(FIXTURE)
loader.load()
mapping = MappingManager(tmp_path / "mapping.json")
mapping.load()
mapping.assign("c-1", "p-1")
SiteBuilder(tmp_path / "out", loader, mapping).build_all()
return tmp_path / "out"
def test_full_build_creates_all_pages(archive):
for rel in ("index.html", "all_conversations.html", "projects/p-1/index.html",
"conversations/c-1.html", "conversations/c-2.html", "conversations/c-3.html",
"assets/style.css", "assets/app.js"):
assert (archive / rel).is_file(), rel
def test_conversation_page_content(archive):
page = (archive / "conversations" / "c-1.html").read_text(encoding="utf-8")
assert "Нормализация до 3НФ" in page
assert "<table" in page
assert 'href="https://www.postgresql.org/docs/"' in page
assert "javascript:" not in page
assert "<script>alert" not in page
def test_project_page_lists_assigned_conversation(archive):
page = (archive / "projects" / "p-1" / "index.html").read_text(encoding="utf-8")
assert "Курсовая по базам данных" in page
assert "c-1.html" in page and "c-2.html" not in page
def test_cli_build_and_map(tmp_path, monkeypatch):
src = tmp_path / "backup"
shutil.copytree(FIXTURE, src)
out, mp = tmp_path / "out", tmp_path / "mapping.json"
monkeypatch.setattr("sys.argv", ["declaude", "--build", "--source", str(src), "--output", str(out), "--mapping", str(mp)])
main()
assert (out / "index.html").is_file()
monkeypatch.setattr("sys.argv", ["declaude", "--map", "c-2", "p-1", "--source", str(src), "--output", str(out), "--mapping", str(mp)])
main()
assert json.loads(mp.read_text(encoding="utf-8")).get("c-2") == "p-1"

54
tests/test_markdown.py Normal file
View file

@ -0,0 +1,54 @@
import pytest
from declaude import MarkdownRenderer
md = MarkdownRenderer()
@pytest.mark.parametrize("url", [
"javascript:alert(1)",
"JaVaScRiPt:alert(1)",
" javascript:alert(1)",
"java\tscript:alert(1)",
"data:text/html;base64,PHNjcmlwdD4=",
"vbscript:msgbox",
])
def test_dangerous_link_schemes_are_neutralized(url):
html = md.render(f"[клик]({url})")
assert 'href="#"' in html
assert "script:" not in html.lower().replace("#", "")
@pytest.mark.parametrize("url", ["https://example.com/a?b=1", "http://example.com", "mailto:me@example.com", "#anchor", "/local/page"])
def test_normal_links_are_kept(url):
assert f'href="{url}"'.replace("&", "&amp;") in md.render(f"[ссылка]({url})")
def test_image_rejects_javascript_and_svg_data():
assert 'src="#"' in md.render("![x](javascript:alert(2))")
assert 'src="#"' in md.render("![x](data:image/svg+xml;base64,PHN2Zz4=)")
assert 'src="https://example.com/a.png"' in md.render("![x](https://example.com/a.png)")
def test_raw_html_is_escaped():
html = md.render("<script>alert(3)</script>")
assert "<script>" not in html
assert "&lt;script&gt;" in html
def test_code_block_is_escaped_and_highlighted():
html = md.render("```python\nif a < b:\n return '<x>'\n```")
assert "<pre" in html and "&lt;x&gt;" in html
assert "<x>" not in html
def test_table_and_emphasis():
html = md.render("| A | B |\n|---|---|\n| **1** | *2* |")
assert "<table" in html
assert "<strong>1</strong>" in html and "<em>2</em>" in html
def test_headings_and_lists():
html = md.render("## Заголовок\n\n1. один\n2. два")
assert "<h2" in html and "Заголовок" in html
assert "<ol" in html and html.count("<li") == 2