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:
parent
676ca1e926
commit
f5fa349815
8 changed files with 128 additions and 0 deletions
3
requirements-dev.txt
Normal file
3
requirements-dev.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
pytest==8.4.2
|
||||||
|
ruff==0.14.0
|
||||||
|
build==1.3.0
|
||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
13
tests/fixtures/backup/conversations.json
vendored
Normal file
13
tests/fixtures/backup/conversations.json
vendored
Normal 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)) и картинка )\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": []}
|
||||||
|
]
|
||||||
1
tests/fixtures/backup/projects/p-1.json
vendored
Normal file
1
tests/fixtures/backup/projects/p-1.json
vendored
Normal 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
1
tests/fixtures/backup/users.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
[{"uuid": "u-1", "full_name": "Тестовый Пользователь", "email_address": "user@example.com"}]
|
||||||
1
tests/fixtures/mapping.json
vendored
Normal file
1
tests/fixtures/mapping.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"c-1": "p-1"}
|
||||||
55
tests/test_build.py
Normal file
55
tests/test_build.py
Normal 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
54
tests/test_markdown.py
Normal 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("&", "&") in md.render(f"[ссылка]({url})")
|
||||||
|
|
||||||
|
|
||||||
|
def test_image_rejects_javascript_and_svg_data():
|
||||||
|
assert 'src="#"' in md.render(")")
|
||||||
|
assert 'src="#"' in md.render("")
|
||||||
|
assert 'src="https://example.com/a.png"' in md.render("")
|
||||||
|
|
||||||
|
|
||||||
|
def test_raw_html_is_escaped():
|
||||||
|
html = md.render("<script>alert(3)</script>")
|
||||||
|
assert "<script>" not in html
|
||||||
|
assert "<script>" 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 "<x>" 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
|
||||||
Loading…
Add table
Reference in a new issue