Files
bruno d58c321c07
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
feat: réactions dans le tiroir Comments (+ suppression) et refonte du menu de sélection (v7.67.0)
Tiroir Comments
- Section « Reactions » au-dessus des fils : émoji, compte (si >1) et texte
  ancré lu dans le DOM (reactionList() / _anchorText(), offsets source hors
  pastilles) ; l'état vide ne s'affiche plus s'il reste des réactions.
- Suppression : ✕ sur la ligne du tiroir (mes réactions) ET croix ::after au
  survol de la pastille sur l'émoji que j'ai posé (aucun nœud ajouté → offsets
  intacts). Deux entrées → removeReaction() → POST toggle → loadReactions().

Menu de sélection (maquette ASCII)
- Ligne 4 = rangée scannée : 💬 Comment (libellé) à gauche, 🙂+ réaction à
  droite (.ft-row-split) ; la réaction sort de la ligne 3.
- Entête Skills avec ≡ à droite (::after), liste Skills scrollable.
- ✨ Ask AI retiré de la toolbar : doublon exact de « Edit with AI » du pied.
- Écart volontaire à la maquette : le barré (T) de la ligne 2 est conservé.

Fix racine (flake de gate)
- _is_public_host attrape OSError (le garde SSRF renvoie un booléen, il ne
  lève plus) : un socket.timeout DNS ne bascule plus fetch_og_metadata dans
  son except Exception → repli silencieux → AssertionError.
- fixture stub_dns sur TestOGParser (même motif que test_agent_web_tools) :
  zéro vrai DNS dans les tests OG.

Tests: pytest tests/ -q -n auto → 1390 passed / 0 failed (flake corrigé) ;
ruff check app tests → OK ; e2e v764 (réactions dans le tiroir + suppression
A/B), v762 (nouvelle ligne 4), v766, v7641, v763 → 6 passed sur :8081
(image flowdeck:v7.67.0 rebuild + redéployée).
2026-10-08 14:25:48 -04:00

639 lines
27 KiB
Python

"""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 ipaddress
import json
import secrets
import socket
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}&section=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:[email protected]") 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 "<iframe" in html
assert "height:360px" in html
class TestEmbedResolveEndpoint:
def test_resolve_endpoint(self, client):
cookie, csrf = _login(client)
r = client.post(
"/board/api/embed/resolve",
json={"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"},
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["src"] == "https://www.youtube.com/embed/dQw4w9WgXcQ"
assert data["provider"] == "youtube"
def test_resolve_requires_url(self, client):
cookie, csrf = _login(client)
r = client.post(
"/board/api/embed/resolve",
json={},
headers={"Content-Type": "application/json", **_cookie_headers(cookie, csrf)},
cookies={"flowdeck_session": cookie},
)
assert r.status_code == 400
# ══════════════════════════════════════════════════════════════════════════
# 2. Bookmark cards / Open Graph
# ══════════════════════════════════════════════════════════════════════════
_OG_HTML = """<!doctype html><html><head>
<title>Fallback title</title>
<meta content="FlowDeck — Notion clone" property="og:title">
<meta property="og:description" content="Plan, write and ship.">
<meta name="twitter:image" content="/static/preview.png">
<meta property="og:site_name" content="FlowDeck">
<link rel="shortcut icon" href="/favicon.ico">
</head><body>hi</body></html>"""
#: IP publique factice — TestOGParser ne doit dépendre ni du réseau HTTP
#: (MockTransport) ni du DNS : `_is_public_host` résout l'hôte pour décider si
#: l'IP est publique, et cette résolution échoue sous `pytest -n auto` (même
#: piège que test_agent_web_tools.stub_dns — d'où un flake récurrent ici).
PUBLIC_IP = "93.184.216.34"
class TestOGParser:
@pytest.fixture(autouse=True)
def stub_dns(self, monkeypatch):
real = socket.getaddrinfo
def fake_getaddrinfo(host, *args, **kwargs):
try:
ipaddress.ip_address(host)
except ValueError:
return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (PUBLIC_IP, 0))]
return real(host, *args, **kwargs)
monkeypatch.setattr(socket, "getaddrinfo", fake_getaddrinfo)
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("<html><head><title>Plain title</title></head></html>", "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 "<video controls" in tpl
assert "<audio controls" in tpl
assert "embed_type==='pdf'" in tpl
def test_public_renders_video_audio_pdf(self):
from app.routers.dashboard import _render_blocks_public
blocks = [
{"type": "video", "src": "https://ex.com/v.mp4"},
{"type": "audio", "src": "https://ex.com/a.mp3"},
{"type": "embed", "src": "https://ex.com/doc.pdf", "embed_type": "pdf"},
]
html = _render_blocks_public(blocks)
assert "<video" in html
assert "<audio" in html
assert "doc.pdf" in html and "<iframe" in html
def test_public_embed_uses_resolved_src(self):
from app.routers.dashboard import _render_blocks_public
html = _render_blocks_public([{
"type": "embed",
"src": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"embed_src": "https://www.youtube.com/embed/dQw4w9WgXcQ",
}])
assert "youtube.com/embed/dQw4w9WgXcQ" in html
def test_public_embed_resolves_when_no_cached_src(self):
from app.routers.dashboard import _render_blocks_public
html = _render_blocks_public([{
"type": "embed",
"src": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
}])
assert "youtube.com/embed/dQw4w9WgXcQ" in html
def test_embed_src_field_persists(self, client):
cookie, csrf = _login(client)
_workspace(client, cookie)
page = _page(client, cookie, csrf, "Embed persist")
blocks = [{
"type": "embed",
"src": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"embed_src": "https://www.youtube.com/embed/dQw4w9WgXcQ",
"embed_provider": "youtube",
"height": 480,
}]
_save_blocks(client, cookie, csrf, page, blocks, "Embed persist")
saved = json.loads(
client.get(f"/board/api/pages/{page}", cookies={"flowdeck_session": cookie}).json()["content"]
)
assert saved[0]["embed_src"] == "https://www.youtube.com/embed/dQw4w9WgXcQ"
assert saved[0]["embed_provider"] == "youtube"
# ══════════════════════════════════════════════════════════════════════════
# 5. Cover & page icon
# ══════════════════════════════════════════════════════════════════════════
class TestCoverIcon:
def test_set_and_remove_cover_url(self, client):
cookie, csrf = _login(client)
_workspace(client, cookie)
page = _page(client, cookie, csrf, "Cover")
r = client.post(
f"/board/api/pages/{page}/cover",
json={"cover_url": "https://example.com/cover.png"},
headers={"Content-Type": "application/json", **_cookie_headers(cookie, csrf)},
cookies={"flowdeck_session": cookie},
)
assert r.status_code == 200
assert r.json()["cover_url"] == "https://example.com/cover.png"
r2 = client.get(f"/board/api/pages/{page}", cookies={"flowdeck_session": cookie})
assert r2.json()["cover_url"] == "https://example.com/cover.png"
r3 = client.delete(
f"/board/api/pages/{page}/cover",
headers=_cookie_headers(cookie, csrf),
cookies={"flowdeck_session": cookie},
)
assert r3.status_code == 200
r4 = client.get(f"/board/api/pages/{page}", cookies={"flowdeck_session": cookie})
assert r4.json()["cover_url"] in ("", None)
def test_upload_cover_file(self, client, monkeypatch, tmp_path):
cookie, csrf = _login(client)
_workspace(client, cookie)
page = _page(client, cookie, csrf, "Cover upload")
monkeypatch.setenv("FLOWDECK_DATA_DIR", str(tmp_path))
png = b"\x89PNG\r\n\x1a\n" + b"\x00" * 32
r = client.post(
f"/board/api/pages/{page}/cover",
files={"file": ("banner.png", png, "image/png")},
headers=_cookie_headers(cookie, csrf),
cookies={"flowdeck_session": cookie},
)
assert r.status_code == 200
url = r.json()["cover_url"]
assert url.startswith("/api/files/")
assert url.endswith(".png")
assert (tmp_path / "uploads").exists()
def test_upload_rejects_non_image(self, client, monkeypatch, tmp_path):
cookie, csrf = _login(client)
_workspace(client, cookie)
page = _page(client, cookie, csrf, "Bad upload")
monkeypatch.setenv("FLOWDECK_DATA_DIR", str(tmp_path))
r = client.post(
f"/board/api/pages/{page}/cover",
files={"file": ("evil.sh", b"#!/bin/sh", "text/plain")},
headers=_cookie_headers(cookie, csrf),
cookies={"flowdeck_session": cookie},
)
assert r.status_code == 400
def test_set_page_icon(self, client):
cookie, csrf = _login(client)
_workspace(client, cookie)
page = _page(client, cookie, csrf, "Icon")
r = client.post(
f"/board/api/pages/{page}/icon",
json={"icon": "🚀"},
headers={"Content-Type": "application/json", **_cookie_headers(cookie, csrf)},
cookies={"flowdeck_session": cookie},
)
assert r.status_code == 200
assert r.json()["icon"] == "🚀"
r2 = client.get(f"/board/api/pages/{page}", cookies={"flowdeck_session": cookie})
assert r2.json()["page_icon"] == "🚀"
def test_icon_too_long_rejected(self, client):
cookie, csrf = _login(client)
_workspace(client, cookie)
page = _page(client, cookie, csrf, "Icon long")
r = client.post(
f"/board/api/pages/{page}/icon",
json={"icon": "x" * 600},
headers={"Content-Type": "application/json", **_cookie_headers(cookie, csrf)},
cookies={"flowdeck_session": cookie},
)
assert r.status_code == 400
def test_custom_emoji_url_accepted_as_icon(self, client):
cookie, csrf = _login(client)
_workspace(client, cookie)
page = _page(client, cookie, csrf, "Custom icon")
url = "/api/files/1/emoji_123_cat.png"
r = client.post(
f"/board/api/pages/{page}/icon",
json={"icon": url},
headers={"Content-Type": "application/json", **_cookie_headers(cookie, csrf)},
cookies={"flowdeck_session": cookie},
)
assert r.status_code == 200
r2 = client.get(f"/board/api/pages/{page}", cookies={"flowdeck_session": cookie})
assert r2.json()["page_icon"] == url
def test_public_page_renders_cover_and_icon(self, client):
cookie, csrf = _login(client)
_workspace(client, cookie)
page = _page(client, cookie, csrf, "Public")
_save_blocks(client, cookie, csrf, page, [{"type": "paragraph", "content": "Hi"}], "Public")
client.post(
f"/board/api/pages/{page}/cover",
json={"cover_url": "https://example.com/c.jpg"},
headers={"Content-Type": "application/json", **_cookie_headers(cookie, csrf)},
cookies={"flowdeck_session": cookie},
)
client.post(
f"/board/api/pages/{page}/icon",
json={"icon": "🌟"},
headers={"Content-Type": "application/json", **_cookie_headers(cookie, csrf)},
cookies={"flowdeck_session": cookie},
)
r = client.post(
f"/api/pages/{page}/publish",
headers=_cookie_headers(cookie, csrf),
cookies={"flowdeck_session": cookie},
)
assert r.status_code == 200
slug = r.json()["publish_slug"]
html = client.get(f"/p/{slug}").text
assert "c.jpg" in html
assert "🌟" in html
assert "fd-lightbox" in html
# ══════════════════════════════════════════════════════════════════════════
# Regression: the editor must call the board-owned endpoints (v5.5.0 bug)
# ══════════════════════════════════════════════════════════════════════════
class TestEditorEndpointPaths:
def test_editor_uses_board_prefixed_endpoints(self):
tpl = Path("static/js/page_editor_scripts.js").read_text(encoding="utf-8")
for endpoint in (
"/board/api/pages/'+this.pid+'/cover",
"/board/api/pages/'+this.pid+'/icon",
"/board/api/pages/'+this.pid+'/versions",
"/board/api/pages/'+this.pid+'/backlinks",
"/board/api/og/metadata",
"/board/api/embed/resolve",
):
assert endpoint in tpl, f"missing editor endpoint: {endpoint}"
# the old, broken paths must be gone
assert "fetch('/api/pages/'+this.pid+'/cover" not in tpl
assert "fetch('/api/pages/'+this.pid+'/icon" not in tpl
assert "fetch('/api/pages/'+this.pid+'/versions" not in tpl
assert "fetch('/api/pages/'+this.pid+'/backlinks" not in tpl
assert "fetch('/api/og/metadata" not in tpl
# ══════════════════════════════════════════════════════════════════════════
# v5.6.0: custom workspace emojis
# ══════════════════════════════════════════════════════════════════════════
class TestCustomEmojis:
def test_list_empty(self, client):
cookie, _ = _login(client)
_workspace(client, cookie)
r = client.get("/api/custom-emojis", cookies={"flowdeck_session": cookie})
assert r.status_code == 200
assert r.json()["emojis"] == []
def test_upload_and_delete(self, client):
cookie, csrf = _login(client)
_workspace(client, cookie)
png = b"\x89PNG\r\n\x1a\n" + b"0" * 16
r = client.post(
"/api/custom-emojis",
data={"name": "pushup"},
files={"file": ("pushup.png", png, "image/png")},
headers=_cookie_headers(cookie, csrf),
cookies={"flowdeck_session": cookie},
)
assert r.status_code == 200
emoji = r.json()["emoji"]
assert emoji["name"] == "pushup"
assert emoji["url"].startswith("/api/files/")
listed = client.get("/api/custom-emojis", cookies={"flowdeck_session": cookie}).json()["emojis"]
assert any(e["id"] == emoji["id"] for e in listed)
d = client.request(
"DELETE",
f"/api/custom-emojis/{emoji['id']}",
headers=_cookie_headers(cookie, csrf),
cookies={"flowdeck_session": cookie},
)
assert d.status_code == 200
after = client.get("/api/custom-emojis", cookies={"flowdeck_session": cookie}).json()["emojis"]
assert all(e["id"] != emoji["id"] for e in after)
def test_reject_non_image(self, client):
cookie, csrf = _login(client)
_workspace(client, cookie)
r = client.post(
"/api/custom-emojis",
data={"name": "bad"},
files={"file": ("evil.sh", b"#!/bin/sh", "text/plain")},
headers=_cookie_headers(cookie, csrf),
cookies={"flowdeck_session": cookie},
)
assert r.status_code == 400