package gateway // 本文件锁定切片 B1 对**文档端点**的那一半改动:`.pdf` 进白名单,而类型判定是 // (扩展名, 字节嗅探) 的**成对**合取 —— 真 PDF 能上传、能被读端点按 PDF 下发; // 而 HTML/纯文本改名成 `.pdf`、PDF 改名成 `.txt` 一律 415。 // // 端点与错误码的其余口径(去重、鉴权、路径穿越、限额、元数据)在 documents_test.go, // 本文件只补 `.pdf` 这一条通路,避免把那边的用例文件撑过 500 行。 import ( "bytes" "fmt" "net/http" "strings" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/auth" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/documents" ) // minimalPDF 拼一份**合法的最简 PDF**(单页 + 一行文本),供 `.pdf` 白名单用例 // 使用。它与 `internal/runtime/documents` 的测试夹具同构(那边有完整版,覆盖多页/ // 无文本层/加密/断 xref);这里只需要「字节嗅探认得出 application/pdf」这一点。 func minimalPDF(text string) []byte { content := "BT /F1 12 Tf 72 720 Td (" + text + ") Tj ET" objs := []string{ "<< /Type /Catalog /Pages 2 0 R >>", "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>", "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", fmt.Sprintf("<< /Length %d >>\nstream\n%s\nendstream", len(content), content), } var buf bytes.Buffer buf.WriteString("%PDF-1.4\n") offsets := make([]int, 0, len(objs)) for i, body := range objs { offsets = append(offsets, buf.Len()) fmt.Fprintf(&buf, "%d 0 obj\n%s\nendobj\n", i+1, body) } xref := buf.Len() fmt.Fprintf(&buf, "xref\n0 %d\n0000000000 65535 f \n", len(objs)+1) for _, off := range offsets { fmt.Fprintf(&buf, "%010d 00000 n \n", off) } fmt.Fprintf(&buf, "trailer\n<< /Size %d /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF\n", len(objs)+1, xref) return buf.Bytes() } // TestDocumentUploadAcceptsRealPDF 覆盖验收第 4 条(切片 B1,**存储端行为变更**): // `.pdf` 进白名单后**真 PDF** 能上传(201,mime = `application/pdf`,落盘名以 // `.pdf` 结尾)并被读端点按 `application/pdf` 下发(仍 attachment + nosniff, // 浏览器不可内联);而 **HTML/纯文本改名成 `.pdf`、PDF 改名成 `.txt`** 一律 415 // —— 判定是 (扩展名, 字节嗅探) 的**成对**合取,改扩展名换不了类型。 func TestDocumentUploadAcceptsRealPDF(t *testing.T) { t.Parallel() env := newDocumentsEnv(t, 0) tok := env.token(t, auth.RoleOwner) pdfBytes := minimalPDF("hello pdf") resp, body := postDocument(t, env, "report.pdf", pdfBytes, "text/plain", tok) if resp.StatusCode != http.StatusCreated { t.Fatalf("真 PDF 上传: status = %d, want 201 (body=%s)", resp.StatusCode, body) } got := decodeDocumentEnvelope(t, body) wantName, wantSHA := docHashNameOf(t, "report.pdf", pdfBytes) if got.Data.Name != wantName || got.Data.MIME != documents.PDFMIME || got.Data.SHA256 != wantSHA { t.Fatalf("真 PDF 上传: (name,mime,sha) = (%q,%q,%q), want (%q,%q,%q)", got.Data.Name, got.Data.MIME, got.Data.SHA256, wantName, documents.PDFMIME, wantSHA) } rresp, rbody := getDocument(t, env, documentFilePrefix+got.Data.Name, tok) if rresp.StatusCode != http.StatusOK { t.Fatalf("读真 PDF: status = %d, want 200", rresp.StatusCode) } if ct := rresp.Header.Get("Content-Type"); ct != documents.PDFMIME { t.Fatalf("读真 PDF: Content-Type = %q, want %q(成对裁决在读侧同一份)", ct, documents.PDFMIME) } if d := rresp.Header.Get("Content-Disposition"); !strings.HasPrefix(d, "attachment") || rresp.Header.Get("X-Content-Type-Options") != "nosniff" { t.Fatalf("读真 PDF: 响应头 = (%q,%q), want attachment 前缀 + nosniff(永不可内联)", d, rresp.Header.Get("X-Content-Type-Options")) } if !bytes.Equal(rbody, pdfBytes) { t.Fatal("读真 PDF: 下发字节与上传内容不一致") } for _, tc := range []struct { label string filename string content []byte }{ {"HTML 改名成 .pdf", "lie.pdf", []byte("hi")}, {"纯文本改名成 .pdf", "plain.pdf", []byte("这只是一段普通文本\n")}, {"PDF 改名成 .txt", "lie.txt", pdfBytes}, } { resp, body := postDocument(t, env, tc.filename, tc.content, "text/plain", tok) if resp.StatusCode != http.StatusUnsupportedMediaType { t.Fatalf("%s: status = %d, want 415 (body=%s)", tc.label, resp.StatusCode, body) } if code := decodeDocumentEnvelope(t, body).Error.Code; code != documentUnsupportedCode { t.Fatalf("%s: error.code = %q, want %q", tc.label, code, documentUnsupportedCode) } } }