v9.2.1: real filenames back in open-link URLs (was literal "file")

User report: downloaded/opened files named just "file" — v9.2 put a
literal file.docx/file.pdf at the end of open-link URLs (treated as
cosmetic since Content-Disposition carries the real name, but Word
names its URL-copy from the URL path and browsers fall back to the
URL filename)

- OpenLinkService.CreateLink takes fileName and appends it (URL-encoded,
  Thai-safe) — version: {base}_v{N}.docx/.pdf per FR-15, draft:
  {template}_{timestamp}.docx per FR-10 naming policy
- Draft route split into /d/{templateId}/{stamp}/{fileName} (rebuilds
  objectKey + HMAC-validates as before); version route unchanged shape
  with real name
- Tests: URL tails assert real names (คำร้อง_v1.pdf/_v1.docx, draft
  template name), no /file.docx; new tamper tests — modified stamp/
  templateId and swapped docx↔pdf extension (version route) → 404
- backend 89/89; QA reviewer PASS (1 round, minors 2 fixed)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Suphonchai Phoonsawat 2026-09-10 14:18:36 +07:00
parent 83a949cfa0
commit f4b2ff9d28
9 changed files with 104 additions and 35 deletions

View file

@ -33,6 +33,8 @@ public sealed class VersionPdfTests(IntegrationTestFixture fixture)
Assert.Equal("application/pdf", download.Content.Headers.ContentType!.MediaType);
Assert.Equal("คำร้อง.pdf", download.Content.Headers.ContentDisposition!.FileNameStar);
var link = await client.GetFromJsonAsync<DownloadLinkResult>($"api/documents/{id}/versions/{result.VersionId}/download-link?format=pdf");
// ชื่อไฟล์จริง ({base}_v{N}.pdf) อยู่ท้าย URL ด้วย (Word/browser ตั้งชื่อตาม URL ไม่ใช่แค่ disposition)
Assert.EndsWith("คำร้อง_v1.pdf", Uri.UnescapeDataString(new Uri(link!.Url).AbsolutePath.Split('/')[^1]));
using var anon = fixture.CreateAnonymousClient(); // /api/open ไม่ต้อง Bearer — ลิงก์ลายเซ็น (HMAC) แทน
using var signedDownload = await anon.GetAsync(new Uri(link!.Url).PathAndQuery);
Assert.Equal(HttpStatusCode.OK, signedDownload.StatusCode);
@ -40,6 +42,7 @@ public sealed class VersionPdfTests(IntegrationTestFixture fixture)
Assert.Equal("inline", signedDownload.Content.Headers.ContentDisposition!.DispositionType);
Assert.Equal("คำร้อง_v1.pdf", signedDownload.Content.Headers.ContentDisposition.FileNameStar);
var docxLink = await client.GetFromJsonAsync<DownloadLinkResult>($"api/documents/{id}/versions/{result.VersionId}/download-link");
Assert.EndsWith("คำร้อง_v1.docx", Uri.UnescapeDataString(new Uri(docxLink!.Url).AbsolutePath.Split('/')[^1]));
using var signedDocx = await anon.GetAsync(new Uri(docxLink!.Url).PathAndQuery);
Assert.Equal(HttpStatusCode.OK, signedDocx.StatusCode);
Assert.Equal("attachment", signedDocx.Content.Headers.ContentDisposition!.DispositionType);