feat: attach photos from Search preview replies (Closes #955)
This commit is contained in:
1 parent
053f32c428
commit
d3a57f222e
10 files changed
+320
-4
No files matched your search
@@ -325,6 +325,70 @@ async def test_global_search_preview_reply_rejects_a_mismatched_target_before_co
|
||||
assert comments == []
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_global_search_preview_attachment_is_idempotent_for_exact_visible_target(monkeypatch):
|
||||
main._idempotency_ledger.clear()
|
||||
previews = []
|
||||
uploads = []
|
||||
|
||||
async def preview(repository, kind, number):
|
||||
previews.append((repository, kind, number))
|
||||
return {"repository": repository, "kind": kind, "number": number, "commentable": True}
|
||||
|
||||
async def upload(repository, number, filename, content_type, content):
|
||||
uploads.append((repository, number, filename, content_type, content))
|
||||
return {"name": filename, "url": "https://forge.example/evidence/photo.png", "size": len(content)}
|
||||
|
||||
monkeypatch.setattr(main.gitea_proxy, "work_preview", preview)
|
||||
monkeypatch.setattr(main.gitea_proxy, "upload_preview_attachment", upload)
|
||||
transport = httpx.ASGITransport(app=main.app)
|
||||
headers = {"Idempotency-Key": "search-photo-pull-42"}
|
||||
image = b"\x89PNG\r\n\x1a\nsafe-pixels"
|
||||
files = {"file": ("photo.png", image, "image/png")}
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
first = await client.post(
|
||||
"/api/v1/repos/stackchain/api/issues/42/preview/attachments?kind=pull",
|
||||
files=files,
|
||||
headers=headers,
|
||||
)
|
||||
replay = await client.post(
|
||||
"/api/v1/repos/stackchain/api/issues/42/preview/attachments?kind=pull",
|
||||
files=files,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
assert first.status_code == replay.status_code == 201
|
||||
assert replay.json() == first.json()
|
||||
assert first.json()["markdown"] == ""
|
||||
assert previews == [("stackchain/api", "pull", 42)]
|
||||
assert uploads == [("stackchain/api", 42, "photo.png", "image/png", image)]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_global_search_preview_attachment_rejects_kind_mismatch_before_upload(monkeypatch):
|
||||
main._idempotency_ledger.clear()
|
||||
uploads = []
|
||||
|
||||
async def preview(repository, kind, number):
|
||||
return {"repository": repository, "kind": "issue", "number": number}
|
||||
|
||||
async def upload(*args):
|
||||
uploads.append(args)
|
||||
|
||||
monkeypatch.setattr(main.gitea_proxy, "work_preview", preview)
|
||||
monkeypatch.setattr(main.gitea_proxy, "upload_preview_attachment", upload)
|
||||
transport = httpx.ASGITransport(app=main.app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
response = await client.post(
|
||||
"/api/v1/repos/stackchain/api/issues/42/preview/attachments?kind=pull",
|
||||
files={"file": ("photo.png", b"\x89PNG\r\n\x1a\nsafe-pixels", "image/png")},
|
||||
headers={"Idempotency-Key": "search-photo-mismatch-42"},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert uploads == []
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_global_search_queries_issues_and_pulls_and_skips_unsafe_results():
|
||||
requests = []
|
||||
|
||||
@@ -113,6 +113,60 @@ def test_mobile_conversation_composers_accept_five_ordered_photos():
|
||||
assert "maxFiles: 5" in mount.group(1), controller
|
||||
|
||||
|
||||
def test_mobile_search_preview_exposes_and_mounts_photo_evidence_controls():
|
||||
html = INDEX.read_text()
|
||||
dashboard = DASHBOARD.read_text()
|
||||
css = CSS.read_text()
|
||||
|
||||
assert 'for="take-search-reply-photo">Take photo</label>' in html
|
||||
assert 'id="take-search-reply-photo" type="file" accept="image/*" capture="environment"' in html
|
||||
assert 'for="search-reply-attachment">Choose existing</label>' in html
|
||||
assert 'id="search-reply-attachment" type="file" accept="image/png,image/jpeg,image/webp" multiple' in html
|
||||
mount = re.search(
|
||||
r"const searchReplyAttachmentController = issueAttachment\.mount\(\{(.*?)\n \}\);",
|
||||
dashboard,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert mount
|
||||
assert "maxFiles: 5" in mount.group(1)
|
||||
assert "'/preview/attachments?kind='" in mount.group(1)
|
||||
assert ".search-preview-reply .conversation-photo-actions" in css
|
||||
|
||||
|
||||
def test_search_preview_can_send_photo_only_and_advances_after_confirmed_comment():
|
||||
search_preview = Path(__file__).parents[1] / "frontend" / "search-preview.js"
|
||||
script = f"""
|
||||
const create = require({json.dumps(str(search_preview))});
|
||||
const posted=[]; const prepared=[]; let cleared=0; let navigated=[]; let attached=true;
|
||||
const item={{kind:'issue',repository:'stackchain/api',number:42}};
|
||||
const next={{kind:'pull',repository:'stackchain/web',number:9}};
|
||||
const api=create({{
|
||||
fetchJson:async value=>({{...value,commentable:true}}),
|
||||
postReply:async (target,body,key)=>{{posted.push([target.number,body,key]);return {{id:91,body}};}},
|
||||
prepareReply:async (target,body)=>{{prepared.push([target.number,body]);return '';}},
|
||||
hasAttachments:()=>attached,
|
||||
clearAttachments:()=>{{cleared += 1;attached=false;}},
|
||||
storage:{{getItem:()=>'',setItem:()=>{{}},removeItem:()=>{{}}}},
|
||||
createOperationId:()=> 'reply-photo-42',
|
||||
getSession:()=>({{items:[item,next],more:false}}),
|
||||
onNavigate:value=>navigated.push(value.number),
|
||||
onState:()=>{{}},
|
||||
}});
|
||||
(async()=>{{
|
||||
await api.open(item);
|
||||
await api.reply({{advance:true}});
|
||||
process.stdout.write(JSON.stringify({{posted,prepared,cleared,navigated}}));
|
||||
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
||||
"""
|
||||
output = json.loads(run_node(script))
|
||||
assert output == {
|
||||
"posted": [[42, "", "reply-photo-42"]],
|
||||
"prepared": [[42, ""]],
|
||||
"cleared": 1,
|
||||
"navigated": [9],
|
||||
}
|
||||
|
||||
|
||||
def test_mobile_conversation_composers_review_reorder_remove_and_caption_every_photo():
|
||||
html = INDEX.read_text()
|
||||
dashboard = DASHBOARD.read_text()
|
||||
|
||||
Reference in new issue
Block a user