eCourt-note/tests/utils/upload-name.test.ts

37 lines
1.9 KiB
TypeScript
Raw Permalink Normal View History

fix(upload): decode mojibake of Thai filenames อัปโหลดไฟล์ชื่อไทยแล้ว title ที่แสดงในรายการเพี้ยนเป็น latin1 mojibake ("คำร้องทดสอบอัปโหลด.pdf" → "คำรเน‰à¸­à¸‡…") — คำขอผู้ใช้ 2026-10-05 (1) สาเหตุ: browser ใส่ชื่อไฟล์ (UTF-8) ลง header Content-Disposition ของ multipart body ตรง ๆ แต่ undici ของ Node (ตัว parse request.formData() ที่ Next.js ใช้) ถอดรหัส header เป็น latin1 ทำให้ file.name ใน Server Action เพี้ยนก่อนถึงโค้ด — เนื้อไฟล์ ไม่เกี่ยว (ผ่าน arrayBuffer() ตรง ๆ) พิสูจน์ด้วย Playwright อัปโหลดจริงก่อนแก้ได้สตริง mojibake ตรงกับที่ผู้ใช้รายงาน (2) แก้: utils/upload-name.ts ใหม่ — decodeUploadFilename เข้ารหัส ชื่อกลับเป็น byte แล้วถอดด้วย UTF-8 ให้คืนชื่อเดิม พร้อม guard 2 ชั้นกันแตะชื่อที่ถูกต้องอยู่แล้ว: ไม่มีตัวอักษรช่วง U+0080–U+00FF เลย = ASCII ล้วน/ชื่อถูกอยู่แล้ว (ไทย ≥ U+0E00) ไม่แต้อยู่เดิม / ถอดแล้วได้ U+FFFD (ไบต์ไม่ประกอบ UTF-8 สมบูรณ์ = ชื่อเป็น latin1 จริง) เก็บของเดิม — uploadFile เรียกครั้งเดียว ที่จุดเข้า ก่อนใช้ทั้ง allowedExt และ sanitizeTitle (3) tests: tests/utils/upload-name.test.ts ใหม่ 5 เคส (ชื่อไทย mojibake → คืนเดิม / ไทยผสมตัวเลขวงเล็บ / ASCII ไม่เปลี่ยน / ชื่อถูกอยู่แล้วไม่ถูกแตะ / latin1 จริงเก็บของเดิม) (4) code review 2026-10-05: ยอมรับความเสี่ยง false positive กรณีชื่อ latin1 จริงที่ byte คู่กันบังเอิญประกอบเป็น UTF-8 ครบ ("¢.pdf") — แคบมากเพราะ browser ส่งชื่อเป็น UTF-8 เสมอ และ heuristic นี้ คือ workaround มาตรฐานของปัญหา undici (บีบ guard เพิ่มจะทำให้ ชื่อ latin1-accent ที่โดน mojibake จริง ๆ กลับไม่ถูกซ่อม) (5) docs sync: CLAUDE.md (tree entry) + rules/data-layer.md (กติกา uploadFile) ผ่าน tsc + vitest + ยืนยันใน browser ว่าการ์ดแสดงชื่อไทยตรงเป๊ะ Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-05 11:30:56 +07:00
import { Buffer } from "node:buffer";
import { describe, expect, it } from "vitest";
import { decodeUploadFilename } from "@/utils/upload-name";
/** จำลองสิ่งที่ undici ทำกับชื่อไฟล์: ถอดรหัส UTF-8 bytes เป็น latin1 (mojibake) */
function mojibake(name: string): string {
return Buffer.from(name, "utf8").toString("latin1");
}
describe("decodeUploadFilename", () => {
it("ชื่อไทยที่ undici ถอดเป็น latin1 → คืนชื่อเดิม", () => {
expect(decodeUploadFilename(mojibake("คำร้องทดสอบอัปโหลด.pdf"))).toBe(
"คำร้องทดสอบอัปโหลด.pdf",
);
});
it("ชื่อไทยผสมตัวเลข/วงเล็บ/ช่องว่าง", () => {
expect(decodeUploadFilename(mojibake("คดีดำที่ 1234 (สำเนา).pdf"))).toBe(
"คดีดำที่ 1234 (สำเนา).pdf",
);
});
it("ชื่ออังกฤษล้วนไม่เปลี่ยน (รอบ latin1⇄utf8 เป็น identity)", () => {
expect(decodeUploadFilename("scan-2026_final.PDF")).toBe("scan-2026_final.PDF");
});
it("ชื่อที่ส่งมาถูกต้องอยู่แล้ว (ไทย ≥ U+0E00 ไม่มีตัวช่วง latin1) ไม่ถูกแตะ", () => {
expect(decodeUploadFilename("คำร้อง.pdf")).toBe("คำร้อง.pdf");
});
it("ชื่อ latin1 จริง (byte เดี่ยวประกอบเป็น UTF-8 ไม่ได้) เก็บของเดิม", () => {
// "café" ที่ sender เข้ารหัส latin1 จริง: é = byte 0xE9 เดี่ยว → ถอด UTF-8 ได้ U+FFFD
expect(decodeUploadFilename("café.pdf")).toBe("café.pdf");
});
});