"""FlowDeck — v5.5.0 "Embeds & Média riche" test suite. Covers the five roadmap points: 1. Universal embed block → app/services/embeds.py + /board/api/embed/resolve 2. Bookmark cards → app/services/og_fetcher.py + /board/api/og/metadata 3. Image lightbox → editor + public page markup 4. Inline previews → PDF / video / audio rendering 5. Cover & page icon → /board/api/pages/{id}/cover|icon + public page """ from __future__ import annotations import asyncio import json import secrets from pathlib import Path import httpx import pytest # ── Helpers ────────────────────────────────────────────────────────────── def _login(client): from app.auth.session import SessionManager from app.db import get_conn with get_conn() as conn: login = f"testv55_{secrets.token_hex(4)}" conn.execute( "INSERT INTO users (login, full_name, email, password_hash, is_admin) " "VALUES (?, 'Test V55', ?, '', 1)", (login, f"{login}@test.com"), ) uid = conn.execute("SELECT id FROM users WHERE login=?", (login,)).fetchone()["id"] conn.commit() session = SessionManager.create_session({"id": uid, "login": login, "is_admin": 1}) r = client.get("/api/csrf-token", cookies={"flowdeck_session": session}) return session, r.json()["csrf_token"] def _workspace(client, cookie, name="V55 WS"): r = client.post("/workspace", json={"name": name}, cookies={"flowdeck_session": cookie}) assert r.status_code == 200 ws = r.json() client.cookies.set("flowdeck_workspace", str(ws["id"])) return ws["id"] def _page(client, cookie, csrf, title="V55 Page"): r = client.post( f"/board/api/pages?title={title}§ion=Private&parent_id=0", headers={"X-CSRF-Token": csrf}, cookies={"flowdeck_session": cookie}, ) assert r.status_code == 200 return r.json()["id"] def _save_blocks(client, cookie, csrf, page_id, blocks, title="V55 Page"): r = client.post( f"/board/api/pages/{page_id}/blocks", json={"title": title, "blocks": blocks}, headers={"X-CSRF-Token": csrf, "Content-Type": "application/json"}, cookies={"flowdeck_session": cookie}, ) assert r.status_code == 200 return r.json() def _cookie_headers(cookie, csrf=None): h = {} if csrf: h["X-CSRF-Token"] = csrf return h # ══════════════════════════════════════════════════════════════════════════ # 1. Universal embed # ══════════════════════════════════════════════════════════════════════════ class TestEmbedService: def test_youtube_variants(self): from app.services.embeds import embed_src assert embed_src("https://www.youtube.com/watch?v=dQw4w9WgXcQ") == ( "https://www.youtube.com/embed/dQw4w9WgXcQ" ) assert embed_src("https://youtu.be/dQw4w9WgXcQ") == ( "https://www.youtube.com/embed/dQw4w9WgXcQ" ) assert embed_src("https://www.youtube.com/shorts/abcdef123") == ( "https://www.youtube.com/embed/abcdef123" ) assert "start=42" in embed_src("https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=42") @pytest.mark.parametrize( "url,needle", [ ("https://vimeo.com/123456789", "player.vimeo.com/video/123456789"), ("https://www.figma.com/file/abc123/Doc", "figma.com/embed"), ("https://www.google.com/maps/place/Eiffel", "maps.google.com/maps?q="), ("https://docs.google.com/document/d/ABC123/edit", "docs.google.com/document/d/ABC123/preview"), ("https://docs.google.com/spreadsheets/d/SHEET1/edit", "docs.google.com/spreadsheets/d/SHEET1/preview"), ("https://www.loom.com/share/" + "a" * 32, "loom.com/embed/" + "a" * 32), ("https://codepen.io/user/pen/abc123", "codepen.io/user/embed/abc123"), ("https://miro.com/app/board/uXjV=", "miro.com/app/live-embed/uXjV="), ("https://open.spotify.com/track/abc123", "open.spotify.com/embed/track/abc123"), ("https://soundcloud.com/artist/track", "w.soundcloud.com/player/"), ("https://www.twitch.tv/somestreamer", "player.twitch.tv/?channel=somestreamer"), ("https://twitter.com/user/status/123456", "Tweet.html?id=123456"), ("https://x.com/user/status/123456", "Tweet.html?id=123456"), ("https://www.pinterest.com/pin/12345/", "pinterest.com/pin/embed"), ("https://example.com/report.docx", "view.officeapps.live.com/op/embed.aspx"), ], ) def test_providers(self, url, needle): from app.services.embeds import embed_src out = embed_src(url) assert out and needle in out def test_twitch_parent_is_used(self): from app.services.embeds import embed_src out = embed_src("https://www.twitch.tv/chan", parent="flowdeck.example.com") assert out.endswith("parent=flowdeck.example.com") def test_unknown_http_falls_back_to_iframe(self): from app.services.embeds import embed_src assert embed_src("https://example.com/page") == "https://example.com/page" def test_bare_domain_is_upgraded_to_https(self): from app.services.embeds import embed_src assert embed_src("example.com/page") == "https://example.com/page" def test_non_http_is_rejected(self): from app.services.embeds import embed_src assert embed_src("mailto:hi@example.com") is None assert embed_src("") is None def test_inline_kind(self): from app.services.embeds import inline_kind assert inline_kind("https://ex.com/a.png") == "image" assert inline_kind("https://ex.com/a.pdf") == "pdf" assert inline_kind("https://ex.com/a.mp4") == "video" assert inline_kind("https://ex.com/a.mp3") == "audio" assert inline_kind("https://ex.com/page") == "iframe" assert inline_kind("nonsense") is None def test_resolve_embed(self): from app.services.embeds import resolve_embed out = resolve_embed("https://www.youtube.com/watch?v=dQw4w9WgXcQ") assert out["provider"] == "youtube" assert out["kind"] == "iframe" assert out["src"] == "https://www.youtube.com/embed/dQw4w9WgXcQ" def test_embed_html_is_responsive(self): from app.services.embeds import embed_html html = embed_html("https://ex.com/embed", height=360) assert " Fallback title hi""" class TestOGParser: def test_parse_og_handles_attribute_order(self): from app.services.og_fetcher import parse_og data = parse_og(_OG_HTML, "https://flowdeck.example.com/page") assert data["title"] == "FlowDeck — Notion clone" assert data["description"] == "Plan, write and ship." assert data["site_name"] == "FlowDeck" assert data["image"] == "https://flowdeck.example.com/static/preview.png" assert data["favicon"] == "https://flowdeck.example.com/favicon.ico" def test_parse_og_title_fallback(self): from app.services.og_fetcher import parse_og data = parse_og("Plain title", "https://ex.com") assert data["title"] == "Plain title" def test_fetch_og_metadata_success(self): from app.services.og_fetcher import fetch_og_metadata def handler(request): return httpx.Response(200, headers={"content-type": "text/html"}, text=_OG_HTML) transport = httpx.MockTransport(handler) data = asyncio.run(fetch_og_metadata("https://example.com/page", transport=transport)) assert data["title"] == "FlowDeck — Notion clone" assert data["site_name"] == "FlowDeck" def test_fetch_og_metadata_non_html(self): from app.services.og_fetcher import fetch_og_metadata def handler(request): return httpx.Response(200, headers={"content-type": "application/pdf"}, content=b"%PDF") data = asyncio.run( fetch_og_metadata("https://example.com/file.pdf", transport=httpx.MockTransport(handler)) ) assert data["title"] == "example.com" def test_fetch_og_metadata_network_error_is_safe(self): from app.services.og_fetcher import fetch_og_metadata def handler(request): raise httpx.ConnectError("boom") data = asyncio.run( fetch_og_metadata("https://example.com/unreachable", transport=httpx.MockTransport(handler)) ) assert data["url"].startswith("https://example.com/unreachable") assert data["title"] == "example.com" class TestOGMetadataEndpoint: def test_endpoint_returns_payload(self, client, monkeypatch): cookie, csrf = _login(client) async def fake_fetch(url, timeout=6.0, transport=None): return { "url": url, "title": "Mocked title", "description": "Mocked description", "image": "https://ex.com/img.png", "site_name": "Ex", "favicon": "", } monkeypatch.setattr("app.services.og_fetcher.fetch_og_metadata", fake_fetch) r = client.post( "/board/api/og/metadata", json={"url": "https://ex.com"}, headers={"Content-Type": "application/json", **_cookie_headers(cookie, csrf)}, cookies={"flowdeck_session": cookie}, ) assert r.status_code == 200 data = r.json() assert data["ok"] is True assert data["title"] == "Mocked title" class TestBookmarkPersistence: def test_bookmark_roundtrip(self, client): cookie, csrf = _login(client) _workspace(client, cookie) page = _page(client, cookie, csrf, "Bookmark") blocks = [{ "type": "bookmark", "url": "https://example.com", "title": "Example", "description": "A test site", "image": "https://example.com/og.png", "site_name": "Example", }] _save_blocks(client, cookie, csrf, page, blocks, "Bookmark") saved = json.loads( client.get(f"/board/api/pages/{page}", cookies={"flowdeck_session": cookie}).json()["content"] ) assert saved[0]["type"] == "bookmark" assert saved[0]["image"] == "https://example.com/og.png" # ══════════════════════════════════════════════════════════════════════════ # 3. Image lightbox # ══════════════════════════════════════════════════════════════════════════ class TestLightbox: def test_editor_lightbox_supports_keyboard_navigation(self): tpl = Path("static/js/page_editor_scripts.js").read_text(encoding="utf-8") assert "_openLightbox" in tpl assert "fd-lightbox-nav" in tpl assert "ArrowRight" in tpl and "ArrowLeft" in tpl assert "Escape" in tpl def test_public_page_lightbox_markup(self): tpl = Path("app/templates/public_page.html").read_text(encoding="utf-8") assert "fd-lightbox" in tpl assert "data-full" in tpl assert "ArrowRight" in tpl and "ArrowLeft" in tpl def test_public_image_has_zoom_cursor(self): from app.routers.dashboard import _render_blocks_public html = _render_blocks_public([{"type": "image", "src": "https://ex.com/a.png", "alt": "A"}]) assert 'data-full="https://ex.com/a.png"' in html assert "cursor:zoom-in" in html # ══════════════════════════════════════════════════════════════════════════ # 4. Inline previews (PDF / video / audio) # ══════════════════════════════════════════════════════════════════════════ class TestInlinePreviews: def test_editor_renders_video_audio_pdf(self): tpl = Path("static/js/page_editor_scripts.js").read_text(encoding="utf-8") assert "