Files
sibilla/tests/test_markdown.py
Sibilla 76bc667adf feat: v0.2.0 — API REST, TTS support, documentazione completa e UI mobile-first
✨ Nuove funzionalità:
- Nuovo endpoint API REST /api/v1/query (GET+POST) per consumo programmatico
- Campo speakable_text ottimizzato per sintesi vocale (Alexa, Google Home)
- Funzione strip_markdown() per conversione Markdown → plain text TTS
- Supporto CORS per accesso cross-origin
- UI responsive mobile-first con micro-interazioni e stati
- Nuovo pulsante 'Nuova ricerca' e metadata bar

📚 Documentazione completa:
- docs/ARCHITECTURE.md — architettura di sistema, stack, componenti
- docs/FLOW.md — diagrammi Mermaid della pipeline e flussi
- docs/API.md — documentazione endpoint REST con esempi curl
- docs/WIKI.md — FAQ, glossario, roadmap
- docs/DEPLOYMENT.md — guida deployment (Gunicorn, Nginx, Docker, systemd)
- docs/CONTRIBUTING.md — guida alla contribuzione

🔧 Migliorie tecniche:
- Pipeline di ricerca condivisa in _run_search_pipeline()
- Gestione errori Gemini migliorata con _clean_error()
- Sanitizzazione HTML con allowlist per prevenzione XSS
- .env.example completo con commenti

🧪 Test: 109/109 passati
2026-07-18 10:05:46 +00:00

265 lines
9.1 KiB
Python

"""Tests for sibilla.markdown — server-side Markdown → HTML conversion."""
from sibilla.markdown import render, strip_markdown
class TestBasicMarkdown:
def test_empty_returns_empty(self):
assert render("") == ""
assert render(None) == ""
def test_plain_text(self):
result = render("Ciao mondo")
assert "Ciao mondo" in result
assert result.startswith("<p>")
def test_bold(self):
result = render("Test **bold** text")
assert "<strong>bold</strong>" in result
def test_italic(self):
result = render("Test *italic* text")
assert "<em>italic</em>" in result
def test_bold_italic(self):
result = render("Test ***bold italic*** text")
assert "<strong>" in result
assert "<em>" in result
def test_inline_code(self):
result = render("Use `print()` function")
assert "<code>print()</code>" in result
def test_headers(self):
result = render("# H1\n## H2\n### H3")
assert "<h1>H1</h1>" in result
assert "<h2>H2</h2>" in result
assert "<h3>H3</h3>" in result
class TestLinks:
def test_basic_link(self):
result = render("[Google](https://google.com)")
assert '<a href="https://google.com">Google</a>' in result
def test_link_with_title(self):
result = render('[Home](https://example.com "Example Home")')
assert 'href="https://example.com"' in result
assert 'title="Example Home"' in result
def test_link_target_blank_is_preserved(self):
"""The markdown library doesn't add target, but if already in HTML..."""
# This tests that our sanitizer doesn't strip legitimate attrs
result = render("[link](https://a.com)")
assert "href" in result
class TestLists:
def test_unordered_list(self):
result = render("- Item 1\n- Item 2\n- Item 3")
assert "<ul>" in result
assert "<li>Item 1</li>" in result
assert "<li>Item 2</li>" in result
assert "<li>Item 3</li>" in result
def test_ordered_list(self):
result = render("1. First\n2. Second\n3. Third")
assert "<ol>" in result
assert "<li>First</li>" in result
assert "<li>Second</li>" in result
def test_nested_list(self):
result = render("- A\n - A1\n - A2\n- B")
assert result.count("<li>A</li>") == 1
assert result.count("<li>") >= 3 # A, A1, A2
class TestCodeBlocks:
def test_fenced_code_block(self):
result = render("```python\nprint('hello')\n```")
assert "<code>" in result or "<pre>" in result
def test_indented_code_block(self):
result = render(" code line 1\n code line 2")
assert "<code>" in result or "<pre>" in result
class TestBlockquotes:
def test_blockquote(self):
result = render("> This is a quote\n> Second line")
assert "<blockquote>" in result
assert "This is a quote" in result
class TestTables:
def test_table(self):
result = render("| A | B |\n|---|---|\n| 1 | 2 |")
assert "<table>" in result
assert "<th>A</th>" in result
assert "<td>1</td>" in result
class TestHorizontalRule:
def test_hr(self):
result = render("---")
assert "<hr" in result.lower()
class TestSanitization:
def test_script_tag_stripped(self):
"""<script> tags must be absent from rendered output."""
result = render("<script>alert('xss')</script>")
assert "<script>" not in result
assert "</script>" not in result
def test_event_handlers_stripped(self):
"""onerror/onclick handlers must be stripped even on allowed tags."""
result = render('<img src="x" onerror="alert(1)" />')
assert "onerror" not in result
assert 'src="x"' in result # legitimate attrs preserved
def test_javascript_url_stripped(self):
result = render("[click](javascript:alert(1))")
# The link text might appear, but the javascript: href must not
assert "javascript:" not in result.lower()
assert "click" in result
def test_regular_html_preserved_in_markdown(self):
"""Sanitization should NOT strip tags that come from markdown rendering."""
result = render("**bold** and *italic*")
assert "<strong>bold</strong>" in result
assert "<em>italic</em>" in result
class TestParagraphs:
def test_paragraph_wrapping(self):
result = render("Line 1.\n\nLine 2.")
assert result.count("<p>") >= 2
def test_single_line_break(self):
result = render("Line 1.\nLine 2.")
assert "Line 1." in result
assert "Line 2." in result
def test_multiple_paragraphs(self):
text = "Primo paragrafo.\n\nSecondo paragrafo.\n\nTerzo."
result = render(text)
assert "Primo paragrafo" in result
assert "Secondo paragrafo" in result
assert "Terzo" in result
# ── strip_markdown (TTS-friendly plain text) ──────────────────────
class TestStripMarkdown:
def test_empty_returns_empty(self):
assert strip_markdown("") == ""
assert strip_markdown(None) == ""
def test_plain_text_passes_through(self):
text = "Ciao mondo, oggi è una bella giornata."
assert strip_markdown(text) == text
def test_bold_double_asterisk(self):
result = strip_markdown("Questo è **importante** da sapere.")
assert result == "Questo è importante da sapere."
def test_bold_double_underscore(self):
result = strip_markdown("Questo è __importante__ da sapere.")
assert result == "Questo è importante da sapere."
def test_italic_single_asterisk(self):
result = strip_markdown("Una parola *italica* nel testo.")
assert result == "Una parola italica nel testo."
def test_italic_single_underscore(self):
result = strip_markdown("Una parola _italica_ nel testo.")
assert result == "Una parola italica nel testo."
def test_links_stripped(self):
result = strip_markdown(
"Visita [il nostro sito](https://example.com) per info."
)
assert result == "Visita il nostro sito per info."
def test_images_stripped(self):
result = strip_markdown(
"Ecco una foto: ![logo](https://a.com/logo.png)"
)
assert result == "Ecco una foto: logo"
def test_headings_stripped(self):
result = strip_markdown("# Titolo\n\nTesto del paragrafo.")
assert result == "Titolo\n\nTesto del paragrafo."
def test_code_blocks_stripped(self):
result = strip_markdown("```python\nprint('hello')\n```\n\nFine.")
assert "```" not in result
assert "print('hello')" in result
assert "Fine." in result
def test_inline_code_stripped(self):
result = strip_markdown("Usa il comando `pip install` per procedere.")
assert result == "Usa il comando pip install per procedere."
def test_blockquotes_stripped(self):
result = strip_markdown("> Questa è una citazione.\n> Seconda riga.")
assert "Questa è una citazione." in result
assert ">" not in result
def test_unordered_list_stripped(self):
result = strip_markdown("- Primo elemento\n- Secondo elemento\n- Terzo")
assert "Primo elemento" in result
assert "Secondo elemento" in result
assert "Terzo" in result
assert "-" not in result.split("Primo")[0]
def test_ordered_list_stripped(self):
result = strip_markdown("1. Primo\n2. Secondo\n3. Terzo")
assert "Primo" in result
assert "Secondo" in result
assert "Terzo" in result
def test_html_tags_stripped(self):
result = strip_markdown("Testo con <b>HTML</b> inline.")
assert result == "Testo con HTML inline."
def test_horizontal_rules_stripped(self):
result = strip_markdown("Prima sezione\n---\nSeconda sezione")
assert "---" not in result
assert "Prima sezione" in result
assert "Seconda sezione" in result
def test_multiple_blank_lines_collapsed(self):
result = strip_markdown("Riga 1\n\n\n\nRiga 2")
assert result == "Riga 1\n\nRiga 2"
def test_complex_mixed_markdown(self):
"""Realistic AI output with multiple markdown constructs."""
text = """## Risposta
**L'Odissea** è un poema epico attribuito a [Omero](https://it.wikipedia.org/wiki/Omero).
Ecco alcuni punti chiave:
- Composto intorno all'*VIII secolo a.C.*
- Narra il viaggio di **Ulisse**
- È diviso in `24 libri`
Per approfondire, vedi [Wikipedia](https://it.wikipedia.org)."""
result = strip_markdown(text)
# No markdown artifacts
assert "**" not in result
assert "*" not in result.split("VIII")[0] # no italic markers
assert "[" not in result
assert "](" not in result
assert "`" not in result
assert "##" not in result
assert "-" not in result.split("Composto")[0]
# Content preserved
assert "Odissea" in result
assert "Omero" in result
assert "VIII secolo a.C." in result
assert "Ulisse" in result
assert "24 libri" in result
assert "Wikipedia" in result