- Paste-to-embed: pasting an image into the markdown editor uploads it and
inserts  at the cursor. Unlike gallery attachments these aren't
tied to a journal_entry (the entry may not exist yet while composing), so
they're stored per-user under app/media/journal/{user_id}/pasted/ with no
DB row, served through an ownership-scoped route, and never cleaned up
automatically when an entry is deleted -- an accepted tradeoff at this
app's personal scale.
- The markdown sanitizer was stripping all <img> tags (not on the bleach
allowlist), which would have silently deleted every pasted image on save;
added img/src/alt/title while keeping event-handler attributes blocked.
- Cap embedded image width in both the editor pane and the rendered preview
so a large pasted photo can't overflow its card.
- Fix real data loss risk found while testing this: docker-compose.yml had
no volume for app/media, so every container recreate during a deploy wiped
uploaded photos, and deploy_sftp.py was syncing app/media/ (runtime user
data, not source) into the remote build context. Added the volume mount
and excluded media/ from the sync script. Recovered and relocated the
real attachments that had already landed in the wrong place on the NAS
during earlier deploys this session.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
54 lines
2.2 KiB
Python
54 lines
2.2 KiB
Python
from app.markdown_utils import render_markdown
|
|
|
|
|
|
def test_bold_and_heading_render_as_html():
|
|
html = render_markdown("# 제목\n\n**굵게** 쓴 문장입니다")
|
|
assert "<h1>제목</h1>" in html
|
|
assert "<strong>굵게</strong>" in html
|
|
|
|
|
|
def test_single_newline_becomes_line_break():
|
|
html = render_markdown("첫째 줄\n둘째 줄")
|
|
assert "<br" in html
|
|
|
|
|
|
def test_list_renders_as_html_list():
|
|
html = render_markdown("- 하나\n- 둘")
|
|
assert "<ul>" in html
|
|
assert "<li>하나</li>" in html
|
|
|
|
|
|
def test_dash_and_star_bullets_render_identically():
|
|
# 마크다운 글머리 기호는 -/*/+ 전부 동일하게 처리돼야 한다 — 에디터 쪽 기본 기호(unorderedListStyle)만
|
|
# "-"로 바뀌었을 뿐, 렌더링 결과는 어떤 기호를 써도 같아야 한다.
|
|
assert render_markdown("- 하나\n- 둘") == render_markdown("* 하나\n* 둘") == render_markdown("+ 하나\n+ 둘")
|
|
|
|
|
|
def test_script_tag_is_stripped_not_executed():
|
|
html = render_markdown('<script>alert("xss")</script>본문')
|
|
assert "<script" not in html
|
|
assert "alert" not in html or "<script" not in html # 태그는 지워지고 텍스트만 남아야 함
|
|
|
|
|
|
def test_javascript_href_is_neutralized():
|
|
html = render_markdown('[click me](javascript:alert(1))')
|
|
assert "javascript:" not in html
|
|
|
|
|
|
def test_onerror_attribute_is_stripped_even_though_img_is_allowed():
|
|
html = render_markdown('<img src="x.png" onerror="alert(1)">본문')
|
|
assert "onerror" not in html
|
|
assert '<img src="x.png">' in html # img 자체는 허용되지만 onerror 같은 이벤트 속성은 지워져야 함
|
|
|
|
|
|
def test_allowed_link_href_is_preserved():
|
|
html = render_markdown("[내 블로그](https://example.com)")
|
|
assert 'href="https://example.com"' in html
|
|
|
|
|
|
def test_pasted_image_markdown_renders_with_relative_src():
|
|
# 붙여넣은 이미지는 절대 URL이 아니라 /api/journal/pasted-media/... 같은 상대 경로로 참조된다 —
|
|
# bleach가 스킴 없는 상대 경로도 그대로 통과시키는지 확인.
|
|
html = render_markdown("")
|
|
assert 'src="/api/journal/pasted-media/abc123.png"' in html
|