CI / test (push) Successful in 2m13s
- Carte compositeur : en-tête + colonne d'arrivée, champ pleine largeur, bouton primaire ; neutralise le display:flex imposé par .board-toolbar aux formulaires. - 'Ajouter une description' devient un <details> natif : aucun JS requis, insensible au cache et aux erreurs de scripts (le bouton 0.31.0 pouvait rester inerte). - Assets du board versionnés (?v=APP_VERSION) contre les mélanges de cache après redéploiement ; Ctrl+Entrée. - Vérifié en Chromium réel sur le vrai board (captures contrôlées) ; 261 tests verts.
137 lines
4.7 KiB
Python
137 lines
4.7 KiB
Python
"""Palier 21 (0.31.0): description éditable, pièces jointes
|
|
d'issues, clic carte robuste (délégation côté JS)."""
|
|
|
|
from tests.conftest import csrf_from
|
|
|
|
|
|
def test_issue_update_title_and_body(client, fake_gitea):
|
|
token = csrf_from(client)
|
|
r = client.post(
|
|
"/api/issues/Projets/demo/1/update",
|
|
data={
|
|
"title": "Tâche un — précisée",
|
|
"body": "## Contexte\n\nLa description qui manquait.",
|
|
"csrf_token": token,
|
|
},
|
|
)
|
|
assert r.status_code == 200
|
|
payload = r.json()
|
|
assert payload["ok"] is True
|
|
assert payload["issue"]["body"].startswith("## Contexte")
|
|
issue = next(
|
|
i for i in fake_gitea.issues["Projets/demo"] if i["number"] == 1
|
|
)
|
|
assert issue["title"] == "Tâche un — précisée"
|
|
assert "description qui manquait" in issue["body"]
|
|
# Le journal consigne l'édition.
|
|
events = client.get("/api/events?repo=Projets/demo").json()["events"]
|
|
assert any(e["event_type"] == "edited" for e in events)
|
|
|
|
|
|
def test_issue_update_rejects_empty_title(client, fake_gitea):
|
|
token = csrf_from(client)
|
|
r = client.post(
|
|
"/api/issues/Projets/demo/1/update",
|
|
data={"title": " ", "body": "x", "csrf_token": token},
|
|
)
|
|
assert r.status_code == 400
|
|
|
|
|
|
def test_issue_detail_includes_assets_support(client, fake_gitea):
|
|
data = client.get("/api/issues/Projets/demo/1").json()
|
|
assert data["issue"]["attachments_supported"] is True
|
|
assert data["issue"]["assets"] == []
|
|
|
|
|
|
def test_asset_roundtrip(client, fake_gitea):
|
|
token = csrf_from(client)
|
|
r = client.post(
|
|
"/api/issues/Projets/demo/1/assets",
|
|
data={"csrf_token": token},
|
|
files={"attachment": ("notes.txt", b"bonjour kanbante", "text/plain")},
|
|
)
|
|
assert r.status_code == 201
|
|
asset = r.json()["asset"]
|
|
assert asset["name"] == "notes.txt"
|
|
assert asset["size"] == len(b"bonjour kanbante")
|
|
|
|
# Visible dans le détail.
|
|
data = client.get("/api/issues/Projets/demo/1").json()
|
|
assert [a["name"] for a in data["issue"]["assets"]] == ["notes.txt"]
|
|
|
|
# Téléchargement proxifié : contenu + disposition.
|
|
dl = client.get(
|
|
f"/api/issues/Projets/demo/1/assets/{asset['id']}/download"
|
|
)
|
|
assert dl.status_code == 200
|
|
assert dl.content == b"bonjour kanbante"
|
|
assert "notes.txt" in dl.headers["content-disposition"]
|
|
|
|
# Suppression.
|
|
token = csrf_from(client)
|
|
r = client.post(
|
|
f"/api/issues/Projets/demo/1/assets/{asset['id']}/delete",
|
|
data={"csrf_token": token},
|
|
)
|
|
assert r.status_code == 200
|
|
data = client.get("/api/issues/Projets/demo/1").json()
|
|
assert data["issue"]["assets"] == []
|
|
|
|
|
|
def test_asset_upload_rejects_empty_and_huge(client, fake_gitea):
|
|
token = csrf_from(client)
|
|
r = client.post(
|
|
"/api/issues/Projets/demo/1/assets",
|
|
data={"csrf_token": token},
|
|
files={"attachment": ("vide.txt", b"", "text/plain")},
|
|
)
|
|
assert r.status_code == 400
|
|
|
|
|
|
def test_board_page_has_editor_and_assets_ui(client, fake_gitea):
|
|
page = client.get("/board/Projets/demo")
|
|
assert page.status_code == 200
|
|
assert 'id="detail-edit-btn"' in page.text
|
|
assert 'id="detail-edit-form"' in page.text
|
|
assert 'id="detail-assets"' in page.text
|
|
assert 'id="asset-form"' in page.text
|
|
# Création avec description dépliable (0.31.1 : <details>
|
|
# natif plutôt qu'un bouton piloté en JS).
|
|
assert 'id="new-issue-body"' in page.text
|
|
assert 'id="new-issue-desc"' in page.text
|
|
assert 'class="composer"' in page.text
|
|
|
|
|
|
async def test_gitea_download_follows_browser_download_url():
|
|
"""Gitea renvoie la fiche JSON de l'asset, pas le binaire :
|
|
le téléchargement doit suivre browser_download_url (réel
|
|
1.27.3, attrapé au smoke de la 0.31.0)."""
|
|
import httpx
|
|
|
|
from app.gitea_client import GiteaClient
|
|
|
|
def handler(request):
|
|
url = str(request.url)
|
|
if url.endswith("/assets/42"):
|
|
return httpx.Response(200, json={
|
|
"id": 42, "name": "preuve.txt",
|
|
"browser_download_url": "https://gitea.test/attachments/uuid-1",
|
|
})
|
|
if url.endswith("/attachments/uuid-1"):
|
|
assert request.headers.get("authorization", "").startswith("token ")
|
|
return httpx.Response(
|
|
200, content=b"contenu reel",
|
|
headers={"content-type": "text/plain"},
|
|
)
|
|
return httpx.Response(404)
|
|
|
|
client = GiteaClient(
|
|
base_url="https://gitea.test", token="jeton" + "-factice",
|
|
cache_ttl=0, transport=httpx.MockTransport(handler),
|
|
)
|
|
content, content_type = await client.download_issue_asset(
|
|
"Projets", "demo", 1, 42
|
|
)
|
|
assert content == b"contenu reel"
|
|
assert content_type == "text/plain"
|