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:
parent
83a949cfa0
commit
f4b2ff9d28
9 changed files with 104 additions and 35 deletions
|
|
@ -225,8 +225,8 @@ public sealed class DraftTests(IntegrationTestFixture fixture)
|
|||
public async Task Draft_OpenLink_BadToken_Returns404_AndNoBearerNeeded()
|
||||
{
|
||||
// /api/open ใช้ลายเซ็นใน path แทน Bearer ([AllowAnonymous] โดยเจตนา — Word/เบราว์เซอร์แนบ header ไม่ได้)
|
||||
// รูป URL: {BaseUrl}/api/open/{token}/{expiry}/d/{objectKey} — ไม่มี query string เพราะ Word 16.101+
|
||||
// บน Mac เมิน URL ที่มี query แบบเงียบๆ (v9.2) · token ผิดหรือหมดอายุ → 404 พร้อมข้อความบอกกดเปิดใหม่
|
||||
// รูป URL: {BaseUrl}/api/open/{token}/{expiry}/d/{templateId}/{stamp}/{fileName} — ไม่มี query string
|
||||
// เพราะ Word 16.101+ บน Mac เมิน URL ที่มี query แบบเงียบๆ (v9.2) · token ผิด/หมดอายุ → 404
|
||||
using var client = fixture.CreateClient();
|
||||
var (templateId, _) = await TestFlow.CreateConfiguredTemplateAsync(client, "openlink bad token");
|
||||
using var draft = await client.PostAsJsonAsync(
|
||||
|
|
@ -237,6 +237,13 @@ public sealed class DraftTests(IntegrationTestFixture fixture)
|
|||
Assert.DoesNotContain("?", link.Url); // ทุกอย่างอยู่ใน path — ไม่มี query string
|
||||
Assert.StartsWith("https://test-host/api/open/", link.Url); // ออกผ่าน BaseUrl (https) ของ backend ไม่ใช่ presigned MinIO
|
||||
|
||||
// ชื่อไฟล์ท้าย URL เป็นชื่อจริงตามหลักเดิม ({template}_{timestamp}.docx) ไม่ใช่คำว่า file —
|
||||
// Word ตั้งชื่อ copy ตาม URL ตอนเปิด และ browser ใช้ URL เป็น fallback ตอนดาวน์โหลด
|
||||
var tail = Uri.UnescapeDataString(new Uri(link.Url).AbsolutePath.Split('/')[^1]);
|
||||
Assert.StartsWith("openlink bad token", tail);
|
||||
Assert.EndsWith(".docx", tail);
|
||||
Assert.DoesNotContain("/file.docx", new Uri(link.Url).AbsolutePath);
|
||||
|
||||
// แทน token ใน path ด้วยขยะ (segment ที่ 3 หลัง /api/open/)
|
||||
var segments = new Uri(link.Url).AbsolutePath.Split('/');
|
||||
segments[3] = "garbage-token";
|
||||
|
|
@ -247,6 +254,54 @@ public sealed class DraftTests(IntegrationTestFixture fixture)
|
|||
Assert.Contains("หมดอายุ", body);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("stamp")] // แก้ stamp ของ object key (valid token) — resource เปลี่ยน ลายเซ็นต้องพัง
|
||||
[InlineData("templateId")] // แก้ templateId ใน path — เช่นกัน
|
||||
public async Task Draft_OpenLink_TamperedRouteSegments_Return404(string which)
|
||||
{
|
||||
using var client = fixture.CreateClient();
|
||||
var (templateId, _) = await TestFlow.CreateConfiguredTemplateAsync(client, $"openlink tamper {which}");
|
||||
using var draft = await client.PostAsJsonAsync(
|
||||
$"api/templates/{templateId}/generate-draft",
|
||||
new { values = TestFlow.DefaultValues }, IntegrationTestFixture.Json);
|
||||
var link = (await draft.Content.ReadFromJsonAsync<DraftLinkResult>(IntegrationTestFixture.Json))!;
|
||||
|
||||
// path: /api/open/{t}/{e}/d/{templateId}/{stamp}/{fileName}
|
||||
var segments = new Uri(link.Url).AbsolutePath.Split('/');
|
||||
switch (which)
|
||||
{
|
||||
case "stamp":
|
||||
segments[6] = "20990101000000_" + Guid.NewGuid().ToString("N");
|
||||
break;
|
||||
case "templateId":
|
||||
segments[5] = Guid.NewGuid().ToString();
|
||||
break;
|
||||
}
|
||||
using var anon = fixture.CreateAnonymousClient();
|
||||
using var response = await anon.GetAsync("/" + string.Join("/", segments.Skip(1)));
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Version_OpenLink_SwappedExtension_Returns404()
|
||||
{
|
||||
// นามสกุลท้าย URL ของ version เลือก format ซึ่งผูกกับลายเซ็น — เปลี่ยน .docx→.pdf ด้วย token เดิมต้อง 404
|
||||
// (draft ไม่มีเคสนี้: format เป็น docx เสมอและไม่มี pdf ให้สลับ)
|
||||
using var client = fixture.CreateClient();
|
||||
var documentId = Guid.NewGuid();
|
||||
using var upload = await client.PostAsync($"api/documents/{documentId}/versions",
|
||||
ApiExtensions.VersionForm(TestDocx.BuildPlainDocx(), "คำร้อง.docx", Guid.NewGuid().ToString()));
|
||||
var uploaded = (await upload.Content.ReadFromJsonAsync<UploadVersionResult>(IntegrationTestFixture.Json))!;
|
||||
var link = await client.GetFromJsonAsync<DownloadLinkResult>(
|
||||
$"api/documents/{documentId}/versions/{uploaded.VersionId}/download-link");
|
||||
|
||||
var segments = new Uri(link!.Url).AbsolutePath.Split('/'); // /api/open/{t}/{e}/v/{did}/{vid}/{fileName}
|
||||
segments[^1] = Uri.EscapeDataString("คำร้อง_v1.pdf"); // format signed เป็น docx
|
||||
using var anon = fixture.CreateAnonymousClient();
|
||||
using var response = await anon.GetAsync("/" + string.Join("/", segments.Skip(1)));
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Draft_LifecycleRule_Idempotent_AndSyncsRetentionDays()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue