gogcli/internal/cmd/backup_export_gmail_test.go
Peter Steinberger f26af3adba
feat(safety): add baked safety profiles (#536)
* feat(safety): add baked safety profiles

Co-authored-by: Drew Burchfield <1084679+drewburchfield@users.noreply.github.com>

* fix(safety): narrow readonly profile parent allows

* fix(safety): verify basename safe-build outputs

* fix(backup): promote Gmail checkpoints into final manifest

* docs(safety): explain baked safety profiles

* feat(safety): filter profiled help and schema

* fix(safety): avoid help filter shadow warnings

* fix(backup): make plaintext export resilient

* docs(changelog): mention safety help filtering

* fix(backup): satisfy export lint checks

---------

Co-authored-by: Drew Burchfield <1084679+drewburchfield@users.noreply.github.com>
2026-04-29 03:35:18 +01:00

196 lines
6.5 KiB
Go

package cmd
import (
"encoding/base64"
"os"
"path/filepath"
"strings"
"testing"
"github.com/steipete/gogcli/internal/backup"
)
func TestDecodeGmailRawAcceptsBase64URLVariants(t *testing.T) {
payload := []byte("Subject: Hello\r\n\r\nBody")
raw := base64.RawURLEncoding.EncodeToString(payload)
got, err := decodeGmailRaw(raw)
if err != nil {
t.Fatalf("decodeGmailRaw raw: %v", err)
}
if string(got) != string(payload) {
t.Fatalf("raw decoded = %q, want %q", got, payload)
}
padded := base64.URLEncoding.EncodeToString(payload)
got, err = decodeGmailRaw(padded)
if err != nil {
t.Fatalf("decodeGmailRaw padded: %v", err)
}
if string(got) != string(payload) {
t.Fatalf("padded decoded = %q, want %q", got, payload)
}
}
func TestExportGmailMessagesWritesReadableEMLAndIndex(t *testing.T) {
outDir := t.TempDir()
payload := []byte("Subject: Hello\r\nFrom: a@example.com\r\n\r\nBody")
message := gmailBackupMessage{
ID: "msg/one",
ThreadID: "thread-1",
InternalDate: mustUnixMilli(t, "2026-04-02T10:00:00Z"),
LabelIDs: []string{"INBOX"},
Raw: base64.RawURLEncoding.EncodeToString(payload),
}
shard, err := backup.NewJSONLShard("gmail", "messages", "acct/hash", "data/gmail/acct/messages/2026/04/part-0001.jsonl.gz.age", []gmailBackupMessage{message})
if err != nil {
t.Fatalf("NewJSONLShard: %v", err)
}
files, count, err := exportGmailMessages(outDir, shard, backupExportOptions{GmailFormat: "eml"})
if err != nil {
t.Fatalf("exportGmailMessages: %v", err)
}
if files != 2 || count != 1 {
t.Fatalf("files,count = %d,%d want 2,1", files, count)
}
emlRel := backupExportMessageEMLPath("acct_hash", message)
eml, err := os.ReadFile(filepath.Join(outDir, filepath.FromSlash(emlRel)))
if err != nil {
t.Fatalf("read eml: %v", err)
}
if string(eml) != string(payload) {
t.Fatalf("eml = %q, want %q", eml, payload)
}
index := readText(t, filepath.Join(outDir, "gmail", "acct_hash", "messages", "index.jsonl"))
if !strings.Contains(index, `"id":"msg/one"`) || !strings.Contains(index, `"eml":"`+emlRel+`"`) {
t.Fatalf("index missing expected fields: %s", index)
}
}
func TestExportGmailMessagesWritesMarkdownAndAttachments(t *testing.T) {
outDir := t.TempDir()
payload := strings.Join([]string{
"Subject: Report",
"From: Alice <alice@example.com>",
"To: Peter <peter@example.com>",
"Date: Thu, 02 Apr 2026 10:00:00 +0000",
"MIME-Version: 1.0",
`Content-Type: multipart/mixed; boundary="b1"`,
"",
"--b1",
"Content-Type: text/plain; charset=utf-8",
"",
"Body text.",
"--b1",
"Content-Type: application/pdf",
"Content-Transfer-Encoding: base64",
`Content-Disposition: attachment; filename="report.pdf"`,
"",
base64.StdEncoding.EncodeToString([]byte("pdf bytes")),
"--b1--",
"",
}, "\r\n")
message := gmailBackupMessage{
ID: "msg/one",
ThreadID: "thread-1",
InternalDate: mustUnixMilli(t, "2026-04-02T10:00:00Z"),
LabelIDs: []string{"INBOX"},
Raw: base64.RawURLEncoding.EncodeToString([]byte(payload)),
}
shard, err := backup.NewJSONLShard("gmail", "messages", "acct/hash", "data/gmail/acct/messages/2026/04/part-0001.jsonl.gz.age", []gmailBackupMessage{message})
if err != nil {
t.Fatalf("NewJSONLShard: %v", err)
}
files, count, err := exportGmailMessages(outDir, shard, backupExportOptions{GmailFormat: "markdown", GmailAttachments: "extract"})
if err != nil {
t.Fatalf("exportGmailMessages: %v", err)
}
if files != 3 || count != 1 {
t.Fatalf("files,count = %d,%d want 3,1", files, count)
}
messageDir := backupExportMessageDir("acct_hash", message, "Report")
mdRel := filepath.ToSlash(filepath.Join(messageDir, "message.md"))
md := readText(t, filepath.Join(outDir, filepath.FromSlash(mdRel)))
for _, want := range []string{
`subject: "Report"`,
"# Report",
"Body text.",
"- [report.pdf](attachments/report.pdf)",
} {
if !strings.Contains(md, want) {
t.Fatalf("markdown missing %q:\n%s", want, md)
}
}
attachment := readText(t, filepath.Join(outDir, filepath.FromSlash(filepath.Join(messageDir, "attachments", "report.pdf"))))
if attachment != "pdf bytes" {
t.Fatalf("attachment = %q", attachment)
}
index := readText(t, filepath.Join(outDir, "gmail", "acct_hash", "messages", "index.jsonl"))
if !strings.Contains(index, `"markdown":"`+mdRel+`"`) ||
!strings.Contains(index, `"attachments":["`+filepath.ToSlash(filepath.Join(messageDir, "attachments", "report.pdf"))+`"]`) ||
strings.Contains(index, `"eml"`) {
t.Fatalf("index missing expected markdown-only fields: %s", index)
}
}
func TestExportGmailMessagesWritesMarkdownFallbackForMalformedMIME(t *testing.T) {
outDir := t.TempDir()
payload := strings.Join([]string{
"Subject: Broken",
"From: Alice <alice@example.com>",
"MIME-Version: 1.0",
`Content-Type: multipart/mixed; boundary="b1"`,
"",
"--b1",
"Content-Type: text/plain; charset=utf-8",
"",
"incomplete body",
}, "\r\n")
message := gmailBackupMessage{
ID: "broken",
InternalDate: mustUnixMilli(t, "2026-04-02T10:00:00Z"),
Raw: base64.RawURLEncoding.EncodeToString([]byte(payload)),
}
shard, err := backup.NewJSONLShard("gmail", "messages", "acct/hash", "data/gmail/acct/messages/2026/04/part-0001.jsonl.gz.age", []gmailBackupMessage{message})
if err != nil {
t.Fatalf("NewJSONLShard: %v", err)
}
files, count, err := exportGmailMessages(outDir, shard, backupExportOptions{GmailFormat: "markdown", GmailAttachments: "extract"})
if err != nil {
t.Fatalf("exportGmailMessages: %v", err)
}
if files != 2 || count != 1 {
t.Fatalf("files,count = %d,%d want 2,1", files, count)
}
mdRel := filepath.ToSlash(filepath.Join(backupExportMessageDir("acct_hash", message, "Broken"), "message.md"))
md := readText(t, filepath.Join(outDir, filepath.FromSlash(mdRel)))
for _, want := range []string{
`subject: "Broken"`,
"parse_error:",
"MIME parse failed",
} {
if !strings.Contains(md, want) {
t.Fatalf("markdown missing %q:\n%s", want, md)
}
}
index := readText(t, filepath.Join(outDir, "gmail", "acct_hash", "messages", "index.jsonl"))
if !strings.Contains(index, `"markdown":"`+mdRel+`"`) {
t.Fatalf("index missing markdown fallback: %s", index)
}
}
func TestBackupEmailMarkdownBodyCleansHTMLFragments(t *testing.T) {
got := backupEmailMarkdownBody(backupEmail{TextBody: "<p>Hello&nbsp;<b>Peter</b></p>"})
if got != "Hello Peter" {
t.Fatalf("body = %q, want %q", got, "Hello Peter")
}
got = backupEmailMarkdownBody(backupEmail{HTMLBody: "<html><body><p>Hi<br>there</p></body></html>"})
if got != "Hi there" {
t.Fatalf("html body = %q, want %q", got, "Hi there")
}
}