package migration import ( "bytes" "encoding/csv" "encoding/json" "strings" "testing" ) func TestJobAuditExportCSVFormat(t *testing.T) { var buf bytes.Buffer cw := csv.NewWriter(&buf) if err := cw.Write(jobAuditCSVHeaders); err != nil { t.Fatal(err) } row := JobAuditExportRow{ ItemID: "msg-fail", RelPath: "Inbox/foo.eml", Status: ItemStatusFailed, Error: "upload timeout", Service: "mail", Timestamp: "2026-06-13T12:00:00Z", } if err := writeJobAuditCSVRow(cw, row); err != nil { t.Fatal(err) } cw.Flush() if err := cw.Error(); err != nil { t.Fatal(err) } reader := csv.NewReader(strings.NewReader(buf.String())) records, err := reader.ReadAll() if err != nil { t.Fatal(err) } if len(records) != 2 { t.Fatalf("records = %d, want 2", len(records)) } if got := strings.Join(records[0], ","); got != "item_id,rel_path,status,error,service,timestamp" { t.Fatalf("headers = %q", got) } if records[1][0] != "msg-fail" || records[1][2] != ItemStatusFailed || records[1][3] != "upload timeout" { t.Fatalf("row = %#v", records[1]) } } func TestProjectAuditExportCSVFormat(t *testing.T) { var buf bytes.Buffer cw := csv.NewWriter(&buf) if err := cw.Write(projectAuditCSVHeaders); err != nil { t.Fatal(err) } if err := writeProjectAuditCSVRow(cw, JobAuditExportRow{ JobID: "job-1", ItemID: "file-1", Status: ItemStatusImported, Service: "drive", Timestamp: "2026-06-13T12:00:00Z", }); err != nil { t.Fatal(err) } cw.Flush() reader := csv.NewReader(strings.NewReader(buf.String())) records, err := reader.ReadAll() if err != nil { t.Fatal(err) } if records[0][0] != "job_id" || records[1][0] != "job-1" { t.Fatalf("records = %#v", records) } } func TestJobAuditExportNDJSONFormat(t *testing.T) { var buf bytes.Buffer enc := json.NewEncoder(&buf) if err := enc.Encode(JobAuditExportRow{ ItemID: "msg-skip", Status: ItemStatusSkipped, Error: "file too large", Service: "mail", Timestamp: "2026-06-13T12:00:00Z", }); err != nil { t.Fatal(err) } line := strings.TrimSpace(buf.String()) var decoded JobAuditExportRow if err := json.Unmarshal([]byte(line), &decoded); err != nil { t.Fatal(err) } if decoded.ItemID != "msg-skip" || decoded.Status != ItemStatusSkipped || decoded.Error != "file too large" { t.Fatalf("decoded = %#v", decoded) } } func TestJobAuditExportMeta(t *testing.T) { csvMeta := jobAuditExportMeta("csv", "01234567-abcd-efgh", false) if csvMeta.ContentType != "text/csv; charset=utf-8" { t.Fatalf("csv content type = %q", csvMeta.ContentType) } if !strings.HasSuffix(csvMeta.FileName, ".csv") { t.Fatalf("csv filename = %q", csvMeta.FileName) } ndMeta := jobAuditExportMeta("ndjson", "01234567-abcd-efgh", true) if ndMeta.ContentType != "application/x-ndjson; charset=utf-8" { t.Fatalf("ndjson content type = %q", ndMeta.ContentType) } if !strings.HasPrefix(ndMeta.FileName, "migration-project-audit-") { t.Fatalf("project filename = %q", ndMeta.FileName) } }