"""
Tests for feat #450: MEDIA: token inline rendering in web UI chat.
Covers:
1. /api/media endpoint: serves local image files by absolute path
2. /api/media endpoint: rejects paths outside allowed roots (path traversal)
3. /api/media endpoint: 404 for non-existent files
4. /api/media endpoint: auth gate when auth is enabled
5. renderMd() MEDIA: stash/restore logic (static JS analysis)
6. /api/media endpoint: integration test via live server (requires 8788)
"""
from __future__ import annotations
import json
import os
import pathlib
import tempfile
import unittest
import urllib.error
import urllib.request
from tests._pytest_port import BASE, TEST_STATE_DIR
REPO_ROOT = pathlib.Path(__file__).parent.parent
UI_JS = (REPO_ROOT / "static" / "ui.js").read_text(encoding="utf-8")
WORKSPACE_JS = (REPO_ROOT / "static" / "workspace.js").read_text(encoding="utf-8")
# ── Static analysis: renderMd MEDIA stash ────────────────────────────────────
class TestMediaRenderMdStash(unittest.TestCase):
"""Verify the MEDIA: stash/restore logic exists in ui.js."""
def test_media_stash_defined(self):
self.assertIn("media_stash", UI_JS,
"media_stash array must be defined in renderMd()")
def test_media_token_regex(self):
self.assertIn("MEDIA:", UI_JS,
"MEDIA: token regex must be present in renderMd()")
def test_media_restore_produces_img_tag(self):
self.assertIn("msg-media-img", UI_JS,
"restore pass must produce ")
def test_media_restore_produces_download_link(self):
self.assertIn("msg-media-link", UI_JS,
"restore pass must produce download link for non-image files")
def test_media_api_url_pattern(self):
self.assertIn("api/media?path=", UI_JS,
"renderMd must build api/media?path=... URL for local files")
def test_local_audio_video_media_tokens_request_inline_streaming(self):
self.assertIn("apiUrl+'&inline=1'", UI_JS,
"MEDIA: audio/video local paths must request inline streaming")
def test_media_stash_uses_null_byte_token(self):
self.assertIn("\\x00D", UI_JS,
"MEDIA stash must use null-byte token (\\x00D) to avoid conflicts")
def test_media_stash_runs_before_fence_stash(self):
media_pos = UI_JS.find("media_stash")
fence_pos = UI_JS.find("fence_stash")
self.assertGreater(fence_pos, media_pos,
"media_stash must be defined before fence_stash in renderMd()")
def test_image_extension_regex_covers_common_types(self):
# The JS source has these extensions in a regex like /\.png|jpg|.../i
# Check for the extension strings (without the dot, which may be escaped as \.)
for ext in ["png", "jpg", "jpeg", "gif", "webp"]:
self.assertIn(ext, UI_JS,
f"Image extension {ext} must be in the MEDIA img-check regex")
def test_http_url_media_rendered_as_img(self):
# renderMd should treat MEDIA:https://... as an
# In the JS source, the regex is /^https?:\/\//i (escaped)
self.assertTrue(
"https?:" in UI_JS or "http" in UI_JS,
"MEDIA: restore must handle HTTPS URLs",
)
def test_zoom_toggle_on_click(self):
# PR #1135: CSS class toggle replaced by proper lightbox overlay
self.assertIn("_openImgLightbox", UI_JS,
"Clicking the image must open lightbox overlay (_openImgLightbox)")
# ── Static analysis: CSS ──────────────────────────────────────────────────────
class TestMediaCSS(unittest.TestCase):
CSS = (REPO_ROOT / "static" / "style.css").read_text(encoding="utf-8")
def test_msg_media_img_class_defined(self):
self.assertIn(".msg-media-img", self.CSS)
def test_msg_media_img_max_width(self):
# PR #1135: resting thumbnail is 120x90px (fixed size); no max-width needed.
# Lightbox shows full-size. Check width is set instead.
idx = self.CSS.find(".msg-media-img{")
self.assertGreater(idx, 0)
rule = self.CSS[idx:idx+200]
self.assertIn("width:120px", rule, "Thumbnail must have fixed 120px width")
def test_msg_media_img_full_class_defined(self):
# PR #1135: .msg-media-img--full removed; lightbox replaces inline zoom.
self.assertIn(".img-lightbox", self.CSS,
"Full-size toggle class must exist for zoom-on-click")
def test_msg_media_link_class_defined(self):
self.assertIn(".msg-media-link", self.CSS,
"Download link style must be defined for non-image media")
class TestInlineAudioVideoEditor(unittest.TestCase):
"""Static checks for inline audio/video preview controls in chat and workspace."""
CSS = (REPO_ROOT / "static" / "style.css").read_text(encoding="utf-8")
WORKSPACE_JS = (REPO_ROOT / "static" / "workspace.js").read_text(encoding="utf-8")
INDEX_HTML = (REPO_ROOT / "static" / "index.html").read_text(encoding="utf-8")
def test_audio_and_video_extension_detection_exists(self):
self.assertIn("_AUDIO_EXTS", UI_JS)
self.assertIn("_VIDEO_EXTS", UI_JS)
for ext in ["mp3", "wav", "m4a", "mp4", "mov", "webm"]:
self.assertIn(ext, UI_JS)
def test_media_player_markup_has_native_controls(self):
self.assertIn("_mediaPlayerHtml", UI_JS)
self.assertIn("