Files
larksuite--cli/shortcuts/mail/large_attachment_test.go
wehub-resource-sync bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:22:54 +08:00

1259 lines
42 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package mail
import (
"encoding/base64"
"encoding/json"
"os"
"strings"
"testing"
"github.com/spf13/cobra"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/vfs/localfileio"
"github.com/larksuite/cli/shortcuts/common"
draftpkg "github.com/larksuite/cli/shortcuts/mail/draft"
"github.com/larksuite/cli/shortcuts/mail/emlbuilder"
)
func TestEstimateBase64EMLSize(t *testing.T) {
// 3 bytes raw → 4 bytes base64 + ~200 overhead
got := estimateBase64EMLSize(3)
if got != 4+base64MIMEOverhead {
t.Errorf("estimateBase64EMLSize(3) = %d, want %d", got, 4+base64MIMEOverhead)
}
// 0 bytes raw → just overhead
got = estimateBase64EMLSize(0)
if got != base64MIMEOverhead {
t.Errorf("estimateBase64EMLSize(0) = %d, want %d", got, base64MIMEOverhead)
}
}
func TestClassifyAttachments_AllFit(t *testing.T) {
files := []attachmentFile{
{Path: "a.txt", FileName: "a.txt", Size: 1024},
{Path: "b.txt", FileName: "b.txt", Size: 2048},
}
result := classifyAttachments(files, 0)
if len(result.Normal) != 2 {
t.Fatalf("expected 2 normal, got %d", len(result.Normal))
}
if len(result.Oversized) != 0 {
t.Fatalf("expected 0 oversized, got %d", len(result.Oversized))
}
}
func TestClassifyAttachments_Overflow(t *testing.T) {
// emlBaseSize = 24MB, first file 500KB fits, second 2MB overflows
emlBase := int64(24 * 1024 * 1024)
files := []attachmentFile{
{Path: "small.txt", FileName: "small.txt", Size: 500 * 1024}, // ~667KB base64, fits
{Path: "medium.txt", FileName: "medium.txt", Size: 2 * 1024 * 1024}, // ~2.67MB base64, overflows
}
result := classifyAttachments(files, emlBase)
if len(result.Normal) != 1 || result.Normal[0].FileName != "small.txt" {
t.Fatalf("expected 1 normal (small.txt), got %d: %+v", len(result.Normal), result.Normal)
}
if len(result.Oversized) != 1 || result.Oversized[0].FileName != "medium.txt" {
t.Fatalf("expected 1 oversized (medium.txt), got %d: %+v", len(result.Oversized), result.Oversized)
}
}
func TestClassifyAttachments_SubsequentAlsoOversized(t *testing.T) {
// Once overflow triggers, all subsequent files are oversized even if they'd individually fit.
emlBase := int64(24 * 1024 * 1024)
files := []attachmentFile{
{Path: "big.bin", FileName: "big.bin", Size: 2 * 1024 * 1024}, // overflows
{Path: "tiny.txt", FileName: "tiny.txt", Size: 100}, // would fit alone, but comes after overflow
}
result := classifyAttachments(files, emlBase)
if len(result.Normal) != 0 {
t.Fatalf("expected 0 normal, got %d", len(result.Normal))
}
if len(result.Oversized) != 2 {
t.Fatalf("expected 2 oversized, got %d", len(result.Oversized))
}
}
func TestClassifyAttachments_PreservesOrder(t *testing.T) {
files := []attachmentFile{
{Path: "c.txt", FileName: "c.txt", Size: 100},
{Path: "a.txt", FileName: "a.txt", Size: 200},
{Path: "b.txt", FileName: "b.txt", Size: 50},
}
result := classifyAttachments(files, 0)
if len(result.Normal) != 3 {
t.Fatalf("expected 3 normal, got %d", len(result.Normal))
}
// Order must match input
if result.Normal[0].FileName != "c.txt" || result.Normal[1].FileName != "a.txt" || result.Normal[2].FileName != "b.txt" {
t.Fatalf("order not preserved: %v", result.Normal)
}
}
func TestMaxLargeAttachmentSize(t *testing.T) {
// 3GB constant should match desktop client
expected := int64(3 * 1024 * 1024 * 1024)
if MaxLargeAttachmentSize != expected {
t.Errorf("MaxLargeAttachmentSize = %d, want %d (3 GB)", MaxLargeAttachmentSize, expected)
}
}
func TestBuildLargeAttachmentPreviewURL(t *testing.T) {
tests := []struct {
brand core.LarkBrand
token string
want string
}{
{core.BrandFeishu, "abc123", "https://www.feishu.cn/mail/page/attachment?token=abc123"},
{core.BrandLark, "xyz789", "https://www.larksuite.com/mail/page/attachment?token=xyz789"},
}
for _, tt := range tests {
got := buildLargeAttachmentPreviewURL(tt.brand, tt.token)
if got != tt.want {
t.Errorf("buildLargeAttachmentPreviewURL(%s, %s) = %q, want %q", tt.brand, tt.token, got, tt.want)
}
}
}
func TestBuildLargeAttachmentHTML(t *testing.T) {
results := []largeAttachmentResult{
{FileName: "report.pdf", FileSize: 50 * 1024 * 1024, FileToken: "tok_abc"},
{FileName: "data.zip", FileSize: 100 * 1024 * 1024, FileToken: "tok_xyz"},
}
html := buildLargeAttachmentHTML(core.BrandFeishu, "en_us", results)
// Check it contains the container ID prefix
if !strings.Contains(html, "large-file-area-") {
t.Error("missing container ID")
}
// Check file names are present
if !strings.Contains(html, "report.pdf") {
t.Error("missing filename report.pdf")
}
if !strings.Contains(html, "data.zip") {
t.Error("missing filename data.zip")
}
// Check tokens are embedded as data attributes
if !strings.Contains(html, `data-mail-token="tok_abc"`) {
t.Error("missing data-mail-token for tok_abc")
}
// Check download links
if !strings.Contains(html, "www.feishu.cn/mail/page/attachment?token=tok_abc") {
t.Error("missing download link for tok_abc")
}
if !strings.Contains(html, ">Download<") {
t.Error("missing English download text")
}
}
func TestBuildLargeAttachmentHTML_BrandAwareTitle(t *testing.T) {
results := []largeAttachmentResult{{FileName: "a.pdf", FileSize: 1024, FileToken: "tok"}}
cases := []struct {
brand core.LarkBrand
lang string
wantTitle string
}{
{core.BrandFeishu, "zh_cn", "来自飞书邮箱的超大附件"},
{core.BrandFeishu, "en_us", "Large file from Feishu Mail"},
{core.BrandLark, "zh_cn", "来自Lark邮箱的超大附件"},
{core.BrandLark, "en_us", "Large file from Lark Mail"},
}
for _, tc := range cases {
html := buildLargeAttachmentHTML(tc.brand, tc.lang, results)
if !strings.Contains(html, tc.wantTitle) {
t.Errorf("brand=%s lang=%s: missing title %q\nhtml: %s", tc.brand, tc.lang, tc.wantTitle, html)
}
}
}
func TestBrandDisplayName(t *testing.T) {
cases := []struct {
brand core.LarkBrand
lang string
want string
}{
{core.BrandFeishu, "zh_cn", "飞书"},
{core.BrandFeishu, "en_us", "Feishu"},
{core.BrandFeishu, "", "Feishu"},
{core.BrandLark, "zh_cn", "Lark"},
{core.BrandLark, "en_us", "Lark"},
}
for _, tc := range cases {
if got := brandDisplayName(tc.brand, tc.lang); got != tc.want {
t.Errorf("brandDisplayName(%s, %q) = %q, want %q", tc.brand, tc.lang, got, tc.want)
}
}
}
func TestBuildLargeAttachmentHTML_Empty(t *testing.T) {
html := buildLargeAttachmentHTML(core.BrandFeishu, "en_us", nil)
if html != "" {
t.Errorf("expected empty string for nil results, got %q", html)
}
}
func TestBuildLargeAttachmentHTML_EscapesSpecialChars(t *testing.T) {
results := []largeAttachmentResult{
{FileName: `file<script>alert("xss")</script>.txt`, FileSize: 100, FileToken: `tok"inject`},
}
html := buildLargeAttachmentHTML(core.BrandFeishu, "en_us", results)
if strings.Contains(html, "<script>") {
t.Error("HTML injection: <script> not escaped")
}
if !strings.Contains(html, "&lt;script&gt;") {
t.Error("expected escaped <script> tag")
}
if strings.Contains(html, `data-mail-token="tok"inject"`) {
t.Error("token attribute injection: quote not escaped")
}
if !strings.Contains(html, `data-mail-token="tok&quot;inject"`) {
t.Error("expected escaped quote in token attribute")
}
}
func TestInsertBeforeQuoteOrAppend_WithQuote(t *testing.T) {
body := `<p>Hello</p><div id="lark-mail-quote-cli123" class="history-quote-wrapper"><div>quoted content</div></div>`
block := `<div id="lark-mail-large-file-container">CARD</div>`
result := draftpkg.InsertBeforeQuoteOrAppend(body, block)
// Block should appear before the quote
cardIdx := strings.Index(result, "CARD")
quoteIdx := strings.Index(result, "lark-mail-quote-cli123")
if cardIdx < 0 || quoteIdx < 0 {
t.Fatalf("missing card or quote in result: %s", result)
}
if cardIdx > quoteIdx {
t.Errorf("card should be before quote, but card@%d > quote@%d", cardIdx, quoteIdx)
}
// Original body text should still be before the card
helloIdx := strings.Index(result, "Hello")
if helloIdx > cardIdx {
t.Errorf("body text should be before card, but hello@%d > card@%d", helloIdx, cardIdx)
}
}
func TestInsertBeforeQuoteOrAppend_NestedQuoteIDs(t *testing.T) {
// Simulate a reply to a multi-reply thread: the outermost wrapper has
// class="history-quote-wrapper" but the inner quoted content contains
// deeper lark-mail-quote IDs from the original thread.
body := `<p>My reply</p>` +
`<div class="history-quote-wrapper"><div data-html-block="quote">` +
`<div><div><div id="lark-mail-quote-aaa">` +
`previous reply` +
`<div id="lark-mail-quote-bbb">original message</div>` +
`</div></div></div></div></div>`
block := `<div id="large-file-area-123">CARD</div>`
result := draftpkg.InsertBeforeQuoteOrAppend(body, block)
cardIdx := strings.Index(result, "CARD")
wrapperIdx := strings.Index(result, "history-quote-wrapper")
replyIdx := strings.Index(result, "My reply")
if cardIdx < 0 || wrapperIdx < 0 {
t.Fatalf("missing card or wrapper in result: %s", result)
}
// Card should be BEFORE the wrapper, not inside it
if cardIdx > wrapperIdx {
t.Errorf("card should be before quote wrapper, but card@%d > wrapper@%d", cardIdx, wrapperIdx)
}
// Body text should be before the card
if replyIdx > cardIdx {
t.Errorf("body text should be before card, but reply@%d > card@%d", replyIdx, cardIdx)
}
}
func TestInsertBeforeQuoteOrAppend_NoQuote(t *testing.T) {
body := `<p>Hello world</p>`
block := `<div>CARD</div>`
result := draftpkg.InsertBeforeQuoteOrAppend(body, block)
if !strings.HasSuffix(result, block) {
t.Errorf("without quote, block should be appended to end, got: %s", result)
}
}
func TestInsertBeforeQuoteOrAppend_EmptyBody(t *testing.T) {
result := draftpkg.InsertBeforeQuoteOrAppend("", "<div>CARD</div>")
if result != "<div>CARD</div>" {
t.Errorf("empty body should just return block, got: %s", result)
}
}
// encodeServerHeader builds a base64-encoded X-Lark-Large-Attachment value.
func encodeServerHeader(entries []map[string]interface{}) string {
b, _ := json.Marshal(entries)
return base64.StdEncoding.EncodeToString(b)
}
func TestEnsureLargeAttachmentCards_InjectsMissingCards(t *testing.T) {
headerVal := encodeServerHeader([]map[string]interface{}{
{"file_key": "tok_aaa", "file_name": "report.pdf", "file_size": 50 * 1024 * 1024},
{"file_key": "tok_bbb", "file_name": "data.zip", "file_size": 100 * 1024 * 1024},
})
snapshot := &draftpkg.DraftSnapshot{
Headers: []draftpkg.Header{
{Name: draftpkg.ServerLargeAttachmentHeader, Value: headerVal},
},
Body: &draftpkg.Part{
MediaType: "text/html",
Body: []byte("<p>Hello</p>"),
},
}
rt := common.TestNewRuntimeContext(&cobra.Command{}, &core.CliConfig{Brand: core.BrandFeishu})
ensureLargeAttachmentCards(rt, snapshot)
html := string(snapshot.Body.Body)
if !strings.Contains(html, "report.pdf") {
t.Error("missing card for report.pdf")
}
if !strings.Contains(html, "data.zip") {
t.Error("missing card for data.zip")
}
if !strings.Contains(html, `data-mail-token="tok_aaa"`) {
t.Error("missing data-mail-token for tok_aaa")
}
if !strings.Contains(html, `data-mail-token="tok_bbb"`) {
t.Error("missing data-mail-token for tok_bbb")
}
// Original body should still be present.
if !strings.Contains(html, "<p>Hello</p>") {
t.Error("original body content lost")
}
}
func TestEnsureLargeAttachmentCards_NoDuplicateWhenCardExists(t *testing.T) {
headerVal := encodeServerHeader([]map[string]interface{}{
{"file_key": "tok_aaa", "file_name": "report.pdf", "file_size": 50 * 1024 * 1024},
})
existingCard := `<div id="large-file-area-123456789" style="border:1px solid #DEE0E3;">` +
`<div>Title</div>` +
`<div style="border-top:solid 1px #DEE0E3;" id="large-file-item">` +
`<div><div>report.pdf</div><div><span>50.0 MB</span></div></div>` +
`<a href="https://example.com" data-mail-token="tok_aaa">Download</a>` +
`</div></div>`
snapshot := &draftpkg.DraftSnapshot{
Headers: []draftpkg.Header{
{Name: draftpkg.ServerLargeAttachmentHeader, Value: headerVal},
},
Body: &draftpkg.Part{
MediaType: "text/html",
Body: []byte("<p>Hello</p>" + existingCard),
},
}
rt := common.TestNewRuntimeContext(&cobra.Command{}, &core.CliConfig{Brand: core.BrandFeishu})
originalHTML := string(snapshot.Body.Body)
ensureLargeAttachmentCards(rt, snapshot)
// HTML should remain unchanged — no duplicate card injected.
if string(snapshot.Body.Body) != originalHTML {
t.Errorf("HTML was modified when card already existed.\nbefore: %s\nafter: %s", originalHTML, string(snapshot.Body.Body))
}
}
func TestEnsureLargeAttachmentCards_PartialMissing(t *testing.T) {
headerVal := encodeServerHeader([]map[string]interface{}{
{"file_key": "tok_aaa", "file_name": "report.pdf", "file_size": 50 * 1024 * 1024},
{"file_key": "tok_bbb", "file_name": "data.zip", "file_size": 100 * 1024 * 1024},
})
existingCard := `<div id="large-file-area-123456789">` +
`<div>Title</div>` +
`<div id="large-file-item">` +
`<a data-mail-token="tok_aaa">Download</a>` +
`</div></div>`
snapshot := &draftpkg.DraftSnapshot{
Headers: []draftpkg.Header{
{Name: draftpkg.ServerLargeAttachmentHeader, Value: headerVal},
},
Body: &draftpkg.Part{
MediaType: "text/html",
Body: []byte("<p>Hello</p>" + existingCard),
},
}
rt := common.TestNewRuntimeContext(&cobra.Command{}, &core.CliConfig{Brand: core.BrandFeishu})
ensureLargeAttachmentCards(rt, snapshot)
html := string(snapshot.Body.Body)
// tok_bbb should be injected.
if !strings.Contains(html, `data-mail-token="tok_bbb"`) {
t.Error("missing card for tok_bbb")
}
if !strings.Contains(html, "data.zip") {
t.Error("missing filename data.zip in card")
}
// tok_aaa's existing card should remain (present exactly once).
count := strings.Count(html, `data-mail-token="tok_aaa"`)
if count != 1 {
t.Errorf("tok_aaa card count: got %d, want 1", count)
}
}
func TestEnsureLargeAttachmentCards_NoServerHeader(t *testing.T) {
// Only CLI-format header — no server-format metadata to reconstruct from.
cliVal := base64.StdEncoding.EncodeToString([]byte(`[{"id":"tok_aaa"}]`))
snapshot := &draftpkg.DraftSnapshot{
Headers: []draftpkg.Header{
{Name: draftpkg.LargeAttachmentIDsHeader, Value: cliVal},
},
Body: &draftpkg.Part{
MediaType: "text/html",
Body: []byte("<p>Hello</p>"),
},
}
rt := common.TestNewRuntimeContext(&cobra.Command{}, nil)
originalHTML := string(snapshot.Body.Body)
ensureLargeAttachmentCards(rt, snapshot)
if string(snapshot.Body.Body) != originalHTML {
t.Error("HTML should not be modified when only CLI-format header is present")
}
}
func TestEnsureLargeAttachmentCards_PlainTextBodyInjectsDownloadInfo(t *testing.T) {
headerVal := encodeServerHeader([]map[string]interface{}{
{"file_key": "tok_aaa", "file_name": "report.pdf", "file_size": 1024},
})
snapshot := &draftpkg.DraftSnapshot{
Headers: []draftpkg.Header{
{Name: draftpkg.ServerLargeAttachmentHeader, Value: headerVal},
},
Body: &draftpkg.Part{
MediaType: "text/plain",
Body: []byte("plain text body"),
},
}
rt := common.TestNewRuntimeContext(&cobra.Command{}, &core.CliConfig{Brand: core.BrandFeishu})
ensureLargeAttachmentCards(rt, snapshot)
body := string(snapshot.Body.Body)
if !strings.Contains(body, "plain text body") {
t.Error("original text should be preserved")
}
if !strings.Contains(body, "report.pdf") {
t.Error("plain text should contain filename")
}
if !strings.Contains(body, "tok_aaa") {
t.Error("plain text should contain download link with token")
}
if draftpkg.FindHTMLBodyPart(snapshot.Body) != nil {
t.Error("should not create an HTML part when text/plain body already exists")
}
}
func TestEnsureLargeAttachmentCards_PlainTextNoDuplicate(t *testing.T) {
headerVal := encodeServerHeader([]map[string]interface{}{
{"file_key": "tok_aaa", "file_name": "report.pdf", "file_size": 1024},
})
bodyWithToken := "plain text body\nDownload: https://www.feishu.cn/mail/page/attachment?token=tok_aaa\n"
snapshot := &draftpkg.DraftSnapshot{
Headers: []draftpkg.Header{
{Name: draftpkg.ServerLargeAttachmentHeader, Value: headerVal},
},
Body: &draftpkg.Part{
MediaType: "text/plain",
Body: []byte(bodyWithToken),
},
}
rt := common.TestNewRuntimeContext(&cobra.Command{}, &core.CliConfig{Brand: core.BrandFeishu})
ensureLargeAttachmentCards(rt, snapshot)
if string(snapshot.Body.Body) != bodyWithToken {
t.Error("body should not be modified when token already present")
}
}
func TestBuildLargeAttachmentPlainText(t *testing.T) {
results := []largeAttachmentResult{
{FileName: "report.pdf", FileSize: 26214400, FileToken: "tok_aaa"},
{FileName: "video.mp4", FileSize: 314572800, FileToken: "tok_bbb"},
}
text := buildLargeAttachmentPlainText(core.BrandFeishu, "zh_cn", results)
if !strings.Contains(text, "来自飞书邮箱的超大附件") {
t.Error("should contain Chinese title for Feishu brand")
}
if !strings.Contains(text, "report.pdf") {
t.Error("should contain first filename")
}
if !strings.Contains(text, "video.mp4") {
t.Error("should contain second filename")
}
if !strings.Contains(text, "25.0 MB") {
t.Error("should contain file size display")
}
if !strings.Contains(text, "tok_aaa") {
t.Error("should contain first token in URL")
}
if !strings.Contains(text, "tok_bbb") {
t.Error("should contain second token in URL")
}
if !strings.Contains(text, "下载:") {
t.Error("should contain Chinese download label")
}
textEN := buildLargeAttachmentPlainText(core.BrandLark, "en_us", results)
if !strings.Contains(textEN, "Large file from Lark Mail") {
t.Error("should contain English title for Lark brand")
}
if !strings.Contains(textEN, "Download:") {
t.Error("should contain English download label")
}
}
func TestInjectLargeAttachmentTextIntoSnapshot(t *testing.T) {
snapshot := &draftpkg.DraftSnapshot{
Body: &draftpkg.Part{
MediaType: "text/plain",
Body: []byte("hello"),
},
}
injectLargeAttachmentTextIntoSnapshot(snapshot, "\nattachment info\n")
got := string(snapshot.Body.Body)
if got != "hello\nattachment info\n" {
t.Errorf("got %q", got)
}
if !snapshot.Body.Dirty {
t.Error("should mark part as dirty")
}
}
func TestInjectLargeAttachmentTextIntoSnapshot_NilBody(t *testing.T) {
snapshot := &draftpkg.DraftSnapshot{}
injectLargeAttachmentTextIntoSnapshot(snapshot, "attachment info\n")
if snapshot.Body == nil {
t.Fatal("should create body")
}
if snapshot.Body.MediaType != "text/plain" {
t.Errorf("MediaType = %q, want text/plain", snapshot.Body.MediaType)
}
if string(snapshot.Body.Body) != "attachment info\n" {
t.Errorf("body = %q", string(snapshot.Body.Body))
}
}
func TestInjectLargeAttachmentTextIntoSnapshot_ExistingHTMLBody(t *testing.T) {
snapshot := &draftpkg.DraftSnapshot{
Body: &draftpkg.Part{
MediaType: "text/html",
Body: []byte("<p>hello</p>"),
},
}
injectLargeAttachmentTextIntoSnapshot(snapshot, "\nattachment info\n")
if string(snapshot.Body.Body) != "<p>hello</p>" {
t.Error("should not modify existing non-text body")
}
}
func TestBuildLargeAttachmentPlainText_Empty(t *testing.T) {
text := buildLargeAttachmentPlainText(core.BrandFeishu, "zh_cn", nil)
if text != "" {
t.Error("should return empty string for no results")
}
}
func TestStatAttachmentFiles_BlockedExtension(t *testing.T) {
chdirTemp(t)
fio := &localfileio.LocalFileIO{}
blocked := []string{"malware.exe", "script.js", "payload.ps1", "trojan.bat"}
for _, name := range blocked {
os.WriteFile(name, []byte("content"), 0o644)
}
for _, name := range blocked {
t.Run(name, func(t *testing.T) {
_, err := statAttachmentFiles(fio, []string{name})
if err == nil {
t.Fatalf("expected blocked extension error for %q", name)
}
if !strings.Contains(err.Error(), "not allowed") {
t.Fatalf("unexpected error: %v", err)
}
})
}
// Allowed extensions should pass.
allowed := []string{"report.pdf", "data.csv", "photo.png"}
for _, name := range allowed {
os.WriteFile(name, []byte("content"), 0o644)
}
for _, name := range allowed {
t.Run(name, func(t *testing.T) {
files, err := statAttachmentFiles(fio, []string{name})
if err != nil {
t.Fatalf("expected %q to be allowed, got: %v", name, err)
}
if len(files) != 1 || files[0].FileName != name {
t.Fatalf("unexpected result: %+v", files)
}
})
}
}
func TestInjectLargeAttachmentHTML_MergesIntoExistingContainer(t *testing.T) {
existingCard := `<div id="large-file-area-123456789" style="border: 1px solid #DEE0E3;">` +
`<div style="font-weight: 500;">来自飞书邮箱的超大附件</div>` +
`<div id="large-file-item"><a data-mail-token="tok_old">下载</a></div>` +
`</div>`
snapshot := &draftpkg.DraftSnapshot{
Body: &draftpkg.Part{
MediaType: "text/html",
Body: []byte("<p>Hello</p>" + existingCard),
},
}
newResults := []largeAttachmentResult{
{FileName: "new_file.txt", FileSize: 26214400, FileToken: "tok_new"},
}
injectLargeAttachmentHTMLIntoSnapshot(snapshot, core.BrandFeishu, "zh_cn", newResults)
html := string(snapshot.Body.Body)
// Should still have only one large-file-area container.
containerCount := strings.Count(html, "large-file-area-")
if containerCount != 1 {
t.Errorf("expected 1 container, got %d", containerCount)
}
// Old item should still be present.
if !strings.Contains(html, `data-mail-token="tok_old"`) {
t.Error("lost existing card tok_old")
}
// New item should be present inside the same container.
if !strings.Contains(html, `data-mail-token="tok_new"`) {
t.Error("missing new card tok_new")
}
if !strings.Contains(html, "new_file.txt") {
t.Error("missing filename new_file.txt")
}
// Original body content preserved.
if !strings.Contains(html, "<p>Hello</p>") {
t.Error("original body lost")
}
}
func TestInjectLargeAttachmentHTML_CreatesContainerWhenNoneExists(t *testing.T) {
snapshot := &draftpkg.DraftSnapshot{
Body: &draftpkg.Part{
MediaType: "text/html",
Body: []byte("<p>Hello</p>"),
},
}
results := []largeAttachmentResult{
{FileName: "file.txt", FileSize: 1024, FileToken: "tok_a"},
}
injectLargeAttachmentHTMLIntoSnapshot(snapshot, core.BrandFeishu, "zh_cn", results)
html := string(snapshot.Body.Body)
if !strings.Contains(html, "large-file-area-") {
t.Error("should create a new container")
}
if !strings.Contains(html, `data-mail-token="tok_a"`) {
t.Error("missing card for tok_a")
}
if !strings.Contains(html, "<p>Hello</p>") {
t.Error("original body lost")
}
}
func TestInjectLargeAttachmentHTML_TwoInjectionsProduceSingleContainer(t *testing.T) {
// Simulates the draft-edit flow: ensureLargeAttachmentCards injects
// the first batch, then preprocessLargeAttachmentsForDraftEdit injects
// newly uploaded attachments. Both should end up in one container.
snapshot := &draftpkg.DraftSnapshot{
Body: &draftpkg.Part{
MediaType: "text/html",
Body: []byte("<p>body</p>"),
},
}
brand := core.BrandFeishu
lang := "zh_cn"
// First injection (from ensureLargeAttachmentCards)
injectLargeAttachmentHTMLIntoSnapshot(snapshot, brand, lang, []largeAttachmentResult{
{FileName: "old.txt", FileSize: 27262976, FileToken: "tok_old"},
})
// Second injection (from preprocessLargeAttachmentsForDraftEdit)
injectLargeAttachmentHTMLIntoSnapshot(snapshot, brand, lang, []largeAttachmentResult{
{FileName: "new.txt", FileSize: 26214400, FileToken: "tok_new"},
})
html := string(snapshot.Body.Body)
containerCount := strings.Count(html, "large-file-area-")
if containerCount != 1 {
t.Errorf("expected 1 container after two injections, got %d\nhtml: %s", containerCount, html)
}
if !strings.Contains(html, `data-mail-token="tok_old"`) {
t.Error("missing first injection card tok_old")
}
if !strings.Contains(html, `data-mail-token="tok_new"`) {
t.Error("missing second injection card tok_new")
}
if !strings.Contains(html, "old.txt") {
t.Error("missing filename old.txt")
}
if !strings.Contains(html, "new.txt") {
t.Error("missing filename new.txt")
}
}
func TestFlattenSnapshotParts(t *testing.T) {
t.Run("nil root", func(t *testing.T) {
got := flattenSnapshotParts(nil)
if len(got) != 0 {
t.Errorf("expected nil, got %d parts", len(got))
}
})
t.Run("single part", func(t *testing.T) {
root := &draftpkg.Part{MediaType: "text/html", Body: []byte("hello")}
got := flattenSnapshotParts(root)
if len(got) != 1 {
t.Fatalf("expected 1 part, got %d", len(got))
}
if string(got[0].Body) != "hello" {
t.Errorf("got body %q", string(got[0].Body))
}
})
t.Run("nested multipart", func(t *testing.T) {
leaf1 := &draftpkg.Part{MediaType: "text/plain", Body: []byte("text")}
leaf2 := &draftpkg.Part{MediaType: "text/html", Body: []byte("<p>html</p>")}
leaf3 := &draftpkg.Part{MediaType: "image/png", Body: []byte("png-data")}
mid := &draftpkg.Part{MediaType: "multipart/alternative", Children: []*draftpkg.Part{leaf1, leaf2}}
root := &draftpkg.Part{MediaType: "multipart/mixed", Children: []*draftpkg.Part{mid, leaf3}}
got := flattenSnapshotParts(root)
if len(got) != 5 {
t.Fatalf("expected 5 parts (root+mid+3 leaves), got %d", len(got))
}
})
}
func TestSnapshotEMLBaseSize(t *testing.T) {
t.Run("nil body", func(t *testing.T) {
snapshot := &draftpkg.DraftSnapshot{}
got := snapshotEMLBaseSize(snapshot)
if got != 2048 {
t.Errorf("expected 2048 (header overhead only), got %d", got)
}
})
t.Run("single text part", func(t *testing.T) {
body := make([]byte, 300)
snapshot := &draftpkg.DraftSnapshot{
Body: &draftpkg.Part{MediaType: "text/plain", Body: body},
}
got := snapshotEMLBaseSize(snapshot)
expected := int64(2048) + estimateBase64EMLSize(300)
if got != expected {
t.Errorf("expected %d, got %d", expected, got)
}
})
t.Run("multipart with children", func(t *testing.T) {
textBody := make([]byte, 100)
htmlBody := make([]byte, 500)
snapshot := &draftpkg.DraftSnapshot{
Body: &draftpkg.Part{
MediaType: "multipart/alternative",
Children: []*draftpkg.Part{
{MediaType: "text/plain", Body: textBody},
{MediaType: "text/html", Body: htmlBody},
},
},
}
got := snapshotEMLBaseSize(snapshot)
expected := int64(2048) + estimateBase64EMLSize(0) + estimateBase64EMLSize(100) + estimateBase64EMLSize(500)
if got != expected {
t.Errorf("expected %d, got %d", expected, got)
}
})
}
func TestEstimateEMLBaseSize(t *testing.T) {
chdirTemp(t)
fio := &localfileio.LocalFileIO{}
t.Run("no inline files", func(t *testing.T) {
got := estimateEMLBaseSize(fio, 1000, nil, 0)
expected := int64(2048) + estimateBase64EMLSize(1000)
if got != expected {
t.Errorf("expected %d, got %d", expected, got)
}
})
t.Run("with inline files", func(t *testing.T) {
os.WriteFile("img.png", make([]byte, 5000), 0o644)
got := estimateEMLBaseSize(fio, 200, []string{"img.png"}, 0)
expected := int64(2048) + estimateBase64EMLSize(200) + estimateBase64EMLSize(5000)
if got != expected {
t.Errorf("expected %d, got %d", expected, got)
}
})
t.Run("with extra bytes", func(t *testing.T) {
got := estimateEMLBaseSize(fio, 100, nil, 3000)
expected := int64(2048) + estimateBase64EMLSize(100) + 3000
if got != expected {
t.Errorf("expected %d, got %d", expected, got)
}
})
t.Run("missing inline file ignored", func(t *testing.T) {
got := estimateEMLBaseSize(fio, 100, []string{"nonexistent.png"}, 0)
expected := int64(2048) + estimateBase64EMLSize(100)
if got != expected {
t.Errorf("expected %d (missing file should be skipped), got %d", expected, got)
}
})
}
func TestNormalizeLargeAttachmentHeader(t *testing.T) {
t.Run("no large attachment headers", func(t *testing.T) {
snapshot := &draftpkg.DraftSnapshot{
Headers: []draftpkg.Header{
{Name: "Subject", Value: "test"},
},
}
normalizeLargeAttachmentHeader(snapshot)
if len(snapshot.Headers) != 1 {
t.Errorf("headers should be unchanged, got %d", len(snapshot.Headers))
}
})
t.Run("server header converted to CLI format", func(t *testing.T) {
serverVal := encodeServerHeader([]map[string]interface{}{
{"file_key": "tok_a", "file_name": "a.pdf", "file_size": 1024},
})
snapshot := &draftpkg.DraftSnapshot{
Headers: []draftpkg.Header{
{Name: draftpkg.ServerLargeAttachmentHeader, Value: serverVal},
},
}
normalizeLargeAttachmentHeader(snapshot)
found := false
for _, h := range snapshot.Headers {
if h.Name == draftpkg.ServerLargeAttachmentHeader {
t.Error("server header should have been removed")
}
if h.Name == draftpkg.LargeAttachmentIDsHeader {
found = true
decoded, _ := base64.StdEncoding.DecodeString(h.Value)
var ids []largeAttID
json.Unmarshal(decoded, &ids)
if len(ids) != 1 || ids[0].ID != "tok_a" {
t.Errorf("expected [{id:tok_a}], got %+v", ids)
}
}
}
if !found {
t.Error("CLI-format header not created")
}
})
t.Run("CLI header takes precedence over server header", func(t *testing.T) {
serverVal := encodeServerHeader([]map[string]interface{}{
{"file_key": "tok_server"},
})
cliIDs, _ := json.Marshal([]largeAttID{{ID: "tok_cli"}})
cliVal := base64.StdEncoding.EncodeToString(cliIDs)
snapshot := &draftpkg.DraftSnapshot{
Headers: []draftpkg.Header{
{Name: draftpkg.LargeAttachmentIDsHeader, Value: cliVal},
{Name: draftpkg.ServerLargeAttachmentHeader, Value: serverVal},
},
}
normalizeLargeAttachmentHeader(snapshot)
for _, h := range snapshot.Headers {
if h.Name == draftpkg.ServerLargeAttachmentHeader {
t.Error("server header should have been removed")
}
if h.Name == draftpkg.LargeAttachmentIDsHeader {
if h.Value != cliVal {
t.Error("CLI header value should be preserved as-is")
}
}
}
})
t.Run("multiple server headers deduped", func(t *testing.T) {
val1 := encodeServerHeader([]map[string]interface{}{{"file_key": "tok_a"}})
val2 := encodeServerHeader([]map[string]interface{}{{"file_key": "tok_a"}, {"file_key": "tok_b"}})
snapshot := &draftpkg.DraftSnapshot{
Headers: []draftpkg.Header{
{Name: draftpkg.ServerLargeAttachmentHeader, Value: val1},
{Name: draftpkg.ServerLargeAttachmentHeader, Value: val2},
},
}
normalizeLargeAttachmentHeader(snapshot)
var cliHeader *draftpkg.Header
for i := range snapshot.Headers {
if snapshot.Headers[i].Name == draftpkg.LargeAttachmentIDsHeader {
cliHeader = &snapshot.Headers[i]
}
if snapshot.Headers[i].Name == draftpkg.ServerLargeAttachmentHeader {
t.Error("server header should have been removed")
}
}
if cliHeader == nil {
t.Fatal("CLI header not created")
}
decoded, _ := base64.StdEncoding.DecodeString(cliHeader.Value)
var ids []largeAttID
json.Unmarshal(decoded, &ids)
if len(ids) != 2 {
t.Errorf("expected 2 deduped tokens, got %d: %+v", len(ids), ids)
}
})
}
func TestInjectLargeAttachmentHTML_EmptyResults(t *testing.T) {
snapshot := &draftpkg.DraftSnapshot{
Body: &draftpkg.Part{MediaType: "text/html", Body: []byte("<p>hello</p>")},
}
original := string(snapshot.Body.Body)
injectLargeAttachmentHTMLIntoSnapshot(snapshot, core.BrandFeishu, "zh_cn", nil)
if string(snapshot.Body.Body) != original {
t.Error("empty results should not modify body")
}
}
func TestInjectLargeAttachmentHTML_NilBodyCreatesNew(t *testing.T) {
snapshot := &draftpkg.DraftSnapshot{}
results := []largeAttachmentResult{
{FileName: "file.txt", FileSize: 1024, FileToken: "tok_a"},
}
injectLargeAttachmentHTMLIntoSnapshot(snapshot, core.BrandFeishu, "zh_cn", results)
if snapshot.Body == nil {
t.Fatal("should create body part")
}
if snapshot.Body.MediaType != "text/html" {
t.Errorf("MediaType = %q, want text/html", snapshot.Body.MediaType)
}
if !strings.Contains(string(snapshot.Body.Body), "tok_a") {
t.Error("body should contain the token")
}
if !snapshot.Body.Dirty {
t.Error("should mark part as dirty")
}
}
func TestInjectLargeAttachmentHTML_SkipsWhenNonNilBodyButNoHTMLPart(t *testing.T) {
snapshot := &draftpkg.DraftSnapshot{
Body: &draftpkg.Part{MediaType: "text/plain", Body: []byte("text only")},
}
original := string(snapshot.Body.Body)
injectLargeAttachmentHTMLIntoSnapshot(snapshot, core.BrandFeishu, "zh_cn",
[]largeAttachmentResult{{FileName: "f.txt", FileSize: 100, FileToken: "tok"}})
if string(snapshot.Body.Body) != original {
t.Error("should not modify text/plain body when looking for HTML part")
}
}
func TestInjectLargeAttachmentText_EmptyNilBody(t *testing.T) {
snapshot := &draftpkg.DraftSnapshot{
Body: &draftpkg.Part{MediaType: "text/html", Body: []byte("<p>html</p>")},
}
original := string(snapshot.Body.Body)
injectLargeAttachmentTextIntoSnapshot(snapshot, "\nattachment\n")
if string(snapshot.Body.Body) != original {
t.Error("should not modify when text part not found but body exists")
}
}
func TestStatAttachmentFiles_EmptyAndWhitespace(t *testing.T) {
fio := &localfileio.LocalFileIO{}
files, err := statAttachmentFiles(fio, []string{"", " ", ""})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(files) != 0 {
t.Errorf("expected 0 files for empty/whitespace paths, got %d", len(files))
}
}
func TestStatAttachmentFiles_FileNotFound(t *testing.T) {
chdirTemp(t)
fio := &localfileio.LocalFileIO{}
_, err := statAttachmentFiles(fio, []string{"nonexistent.txt"})
if err == nil {
t.Fatal("expected error for missing file")
}
if !strings.Contains(err.Error(), "cannot read file") {
t.Errorf("unexpected error: %v", err)
}
}
func TestBuildLargeAttachmentItems(t *testing.T) {
t.Run("empty results", func(t *testing.T) {
got := buildLargeAttachmentItems(core.BrandFeishu, "en_us", nil)
if got != "" {
t.Errorf("expected empty string, got %q", got)
}
})
t.Run("Chinese download text", func(t *testing.T) {
results := []largeAttachmentResult{
{FileName: "doc.pdf", FileSize: 1024, FileToken: "tok1"},
}
got := buildLargeAttachmentItems(core.BrandFeishu, "zh_cn", results)
if !strings.Contains(got, "下载") {
t.Error("should contain Chinese download text")
}
if !strings.Contains(got, iconCDNCN) {
t.Error("should use CN icon CDN for Feishu brand")
}
})
t.Run("English with Lark brand uses EN CDN", func(t *testing.T) {
results := []largeAttachmentResult{
{FileName: "doc.pdf", FileSize: 1024, FileToken: "tok1"},
}
got := buildLargeAttachmentItems(core.BrandLark, "en_us", results)
if !strings.Contains(got, "Download") {
t.Error("should contain English download text")
}
if !strings.Contains(got, iconCDNEN) {
t.Error("should use EN icon CDN for Lark brand")
}
})
}
func TestClassifyAttachments_EmptyFiles(t *testing.T) {
result := classifyAttachments(nil, 0)
if len(result.Normal) != 0 || len(result.Oversized) != 0 {
t.Errorf("expected empty result for nil files, got normal=%d oversized=%d",
len(result.Normal), len(result.Oversized))
}
}
func TestProcessLargeAttachments_AttachmentCountLimit(t *testing.T) {
rt := common.TestNewRuntimeContext(&cobra.Command{}, nil)
bld := emlbuilder.New()
paths := make([]string, MaxAttachmentCount+1)
for i := range paths {
paths[i] = "file.txt"
}
_, err := processLargeAttachments(nil, rt, bld, "<p>body</p>", "", paths, 0, 0)
if err == nil || !strings.Contains(err.Error(), "exceeds the limit") {
t.Fatalf("expected count limit error, got %v", err)
}
}
func TestProcessLargeAttachments_ExtraAttachCountLimit(t *testing.T) {
rt := common.TestNewRuntimeContext(&cobra.Command{}, nil)
bld := emlbuilder.New()
_, err := processLargeAttachments(nil, rt, bld, "<p>body</p>", "", []string{"a.txt"}, 0, MaxAttachmentCount)
if err == nil || !strings.Contains(err.Error(), "exceeds the limit") {
t.Fatalf("expected count limit error, got %v", err)
}
}
func TestProcessLargeAttachments_FileStatError(t *testing.T) {
chdirTemp(t)
rt := common.TestNewRuntimeContext(&cobra.Command{}, nil)
bld := emlbuilder.New()
_, err := processLargeAttachments(nil, rt, bld, "<p>body</p>", "", []string{"nonexistent.pdf"}, 0, 0)
if err == nil || !strings.Contains(err.Error(), "stat") {
t.Fatalf("expected stat error, got %v", err)
}
}
func TestProcessLargeAttachments_AllFitNormal(t *testing.T) {
chdirTemp(t)
os.WriteFile("small.txt", make([]byte, 1024), 0o644)
rt := common.TestNewRuntimeContext(&cobra.Command{}, nil)
bld := emlbuilder.New().WithFileIO(rt.FileIO())
result, err := processLargeAttachments(nil, rt, bld, "<p>body</p>", "", []string{"small.txt"}, 0, 0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
_ = result
}
func TestProcessLargeAttachments_OversizedEmptyBody(t *testing.T) {
chdirTemp(t)
os.WriteFile("huge.zip", make([]byte, 100), 0o644)
rt := common.TestNewRuntimeContext(&cobra.Command{}, nil)
bld := emlbuilder.New().WithFileIO(rt.FileIO())
_, err := processLargeAttachments(nil, rt, bld, "", "", []string{"huge.zip"}, emlbuilder.MaxEMLSize, 0)
if err == nil || !strings.Contains(err.Error(), "require a body") {
t.Fatalf("expected empty body error, got %v", err)
}
}
func TestProcessLargeAttachments_OversizedNoIdentity(t *testing.T) {
chdirTemp(t)
os.WriteFile("huge.zip", make([]byte, 100), 0o644)
rt := common.TestNewRuntimeContext(&cobra.Command{}, nil)
bld := emlbuilder.New().WithFileIO(rt.FileIO())
_, err := processLargeAttachments(nil, rt, bld, "<p>body</p>", "", []string{"huge.zip"}, emlbuilder.MaxEMLSize, 0)
if err == nil || !strings.Contains(err.Error(), "user identity") {
t.Fatalf("expected user identity error, got %v", err)
}
}
func TestPreprocessLargeAttachments_NoAddAttachmentOps(t *testing.T) {
rt := common.TestNewRuntimeContext(&cobra.Command{}, nil)
snapshot := &draftpkg.DraftSnapshot{
Body: &draftpkg.Part{MediaType: "text/html", Body: []byte("<p>hello</p>")},
}
patch := draftpkg.Patch{
Ops: []draftpkg.PatchOp{
{Op: "set_subject", Value: "test"},
},
}
result, err := preprocessLargeAttachmentsForDraftEdit(nil, rt, snapshot, patch)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(result.Ops) != 1 || result.Ops[0].Op != "set_subject" {
t.Errorf("expected patch to pass through unchanged, got %+v", result.Ops)
}
}
func TestPreprocessLargeAttachments_AllFitReturnsUnchanged(t *testing.T) {
chdirTemp(t)
os.WriteFile("small.pdf", make([]byte, 1024), 0o644)
rt := common.TestNewRuntimeContext(&cobra.Command{}, nil)
snapshot := &draftpkg.DraftSnapshot{
Body: &draftpkg.Part{MediaType: "text/html", Body: []byte("<p>hello</p>")},
}
patch := draftpkg.Patch{
Ops: []draftpkg.PatchOp{
{Op: "add_attachment", Path: "small.pdf"},
{Op: "set_subject", Value: "test"},
},
}
result, err := preprocessLargeAttachmentsForDraftEdit(nil, rt, snapshot, patch)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(result.Ops) != 2 {
t.Errorf("expected 2 ops (all fit, nothing removed), got %d", len(result.Ops))
}
}
func TestPreprocessLargeAttachments_StatError(t *testing.T) {
chdirTemp(t)
rt := common.TestNewRuntimeContext(&cobra.Command{}, nil)
snapshot := &draftpkg.DraftSnapshot{
Body: &draftpkg.Part{MediaType: "text/html", Body: []byte("<p>hello</p>")},
}
patch := draftpkg.Patch{
Ops: []draftpkg.PatchOp{
{Op: "add_attachment", Path: "nonexistent.pdf"},
},
}
_, err := preprocessLargeAttachmentsForDraftEdit(nil, rt, snapshot, patch)
if err == nil || !strings.Contains(err.Error(), "stat") {
t.Fatalf("expected stat error, got %v", err)
}
}
func TestPreprocessLargeAttachments_OversizedEmptyBody(t *testing.T) {
chdirTemp(t)
// Create a file large enough that when base64-encoded it exceeds MaxEMLSize - header overhead
fileSize := emlbuilder.MaxEMLSize // guaranteed to overflow
os.WriteFile("big.zip", make([]byte, fileSize), 0o644)
rt := common.TestNewRuntimeContext(&cobra.Command{}, nil)
// Empty snapshot has no HTML or text body part
snapshot := &draftpkg.DraftSnapshot{}
patch := draftpkg.Patch{
Ops: []draftpkg.PatchOp{
{Op: "add_attachment", Path: "big.zip"},
},
}
_, err := preprocessLargeAttachmentsForDraftEdit(nil, rt, snapshot, patch)
if err == nil || !strings.Contains(err.Error(), "require a body") {
t.Fatalf("expected empty body error, got %v", err)
}
}
func TestPreprocessLargeAttachments_OversizedNoIdentity(t *testing.T) {
chdirTemp(t)
os.WriteFile("big.zip", make([]byte, 100), 0o644)
rt := common.TestNewRuntimeContext(&cobra.Command{}, nil)
// Create a snapshot whose EML base is already at the limit
snapshot := &draftpkg.DraftSnapshot{
Body: &draftpkg.Part{
MediaType: "multipart/mixed",
Children: []*draftpkg.Part{
{MediaType: "text/html", Body: make([]byte, emlbuilder.MaxEMLSize)},
},
},
}
patch := draftpkg.Patch{
Ops: []draftpkg.PatchOp{
{Op: "add_attachment", Path: "big.zip"},
},
}
_, err := preprocessLargeAttachmentsForDraftEdit(nil, rt, snapshot, patch)
if err == nil || !strings.Contains(err.Error(), "user identity") {
t.Fatalf("expected user identity error, got %v", err)
}
}
func TestPreprocessLargeAttachments_NormalizesHeaders(t *testing.T) {
rt := common.TestNewRuntimeContext(&cobra.Command{}, nil)
serverVal := encodeServerHeader([]map[string]interface{}{
{"file_key": "tok_old", "file_name": "old.pdf", "file_size": 1024},
})
snapshot := &draftpkg.DraftSnapshot{
Headers: []draftpkg.Header{
{Name: draftpkg.ServerLargeAttachmentHeader, Value: serverVal},
},
Body: &draftpkg.Part{MediaType: "text/html", Body: []byte("<p>hello</p>")},
}
patch := draftpkg.Patch{} // no add_attachment ops
_, err := preprocessLargeAttachmentsForDraftEdit(nil, rt, snapshot, patch)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// After processing, server header should be normalized to CLI format
for _, h := range snapshot.Headers {
if h.Name == draftpkg.ServerLargeAttachmentHeader {
t.Error("server header should have been normalized away")
}
}
found := false
for _, h := range snapshot.Headers {
if h.Name == draftpkg.LargeAttachmentIDsHeader {
found = true
}
}
if !found {
t.Error("CLI-format header should have been created")
}
}
func TestFileTypeIcon(t *testing.T) {
cases := []struct {
filename string
want string
}{
{"report.pdf", "icon_file_pdf.png"},
{"doc.docx", "icon_file_doc.png"},
{"slides.pptx", "icon_file_ppt.png"},
{"data.xlsx", "icon_file_excel.png"},
{"archive.zip", "icon_file_zip.png"},
{"archive.7z", "icon_file_zip.png"},
{"photo.png", "icon_file_image.png"},
{"photo.JPEG", "icon_file_image.png"},
{"video.mp4", "icon_file_video.png"},
{"song.mp3", "icon_file_audio.png"},
{"notes.txt", "icon_file_doc.png"},
{"mail.eml", "icon_file_eml.png"},
{"app.apk", "icon_file_android.png"},
{"design.psd", "icon_file_ps.png"},
{"logo.ai", "icon_file_ai.png"},
{"mockup.sketch", "icon_file_sketch.png"},
{"deck.key", "icon_file_keynote.png"},
{"budget.numbers", "icon_file_numbers.png"},
{"letter.pages", "icon_file_pages.png"},
{"random.xyz", "icon_file_unknow.png"},
{"noext", "icon_file_unknow.png"},
}
for _, tc := range cases {
t.Run(tc.filename, func(t *testing.T) {
got := fileTypeIcon(tc.filename)
if got != tc.want {
t.Errorf("fileTypeIcon(%q) = %q, want %q", tc.filename, got, tc.want)
}
})
}
}