package export import ( "bytes" "context" "encoding/csv" "strings" "testing" "time" "bookra/apps/backend/internal/db" "bookra/apps/backend/internal/domain" ) var demoPrincipal = domain.Principal{Subject: "demo-owner", Role: "owner"} const demoTenantID = "5d6b3551-0a3e-4b86-bdf0-e9df20a47148" func readCSV(t *testing.T, buf *bytes.Buffer) [][]string { t.Helper() records, err := csv.NewReader(strings.NewReader(strings.TrimPrefix(buf.String(), "\ufeff"))).ReadAll() if err != nil { t.Fatalf("parse csv: %v\n%s", err, buf.String()) } return records } func TestWriteBookingsCSVResolvesLabelsAndEscapes(t *testing.T) { repo := db.NewMemoryRepository() serviceID := "d5d76a61-3d49-467c-8dd4-bf61ee754e39" // "Signature treatment" staffID := "6936c444-c7d0-4a7d-b596-a9b72d2f4fc0" // "Demo Stylist" locationID := "659f1cc0-a850-46d6-b3b8-cb15d55d8daf" _, err := repo.CreateBooking(context.Background(), db.CreateBookingParams{ TenantID: demoTenantID, ServiceID: &serviceID, StaffID: &staffID, LocationID: &locationID, CustomerName: "Novák, Jiří", CustomerEmail: "jiri@example.com", CustomerPhone: "+420111222333", StartsAt: time.Now().Add(24 * time.Hour), EndsAt: time.Now().Add(25 * time.Hour), Status: "confirmed", Reference: "BK-CSV-1", ManageToken: "tok", Notes: "note with \"quotes\"", }) if err != nil { t.Fatalf("create booking: %v", err) } var buf bytes.Buffer svc := NewService(repo) from := time.Now().Add(-time.Hour) to := time.Now().Add(72 * time.Hour) if err := svc.WriteBookingsCSV(context.Background(), demoPrincipal, &buf, from, to); err != nil { t.Fatalf("write csv: %v", err) } if !bytes.HasPrefix(buf.Bytes(), utf8BOM) { t.Fatal("expected UTF-8 BOM for Excel compatibility") } records := readCSV(t, &buf) if len(records) != 2 { t.Fatalf("expected header + 1 row, got %d rows", len(records)) } row := records[1] if row[0] != "BK-CSV-1" || row[1] != "confirmed" { t.Fatalf("unexpected reference/status: %v", row[:2]) } if row[2] != "Novák, Jiří" { t.Fatalf("expected escaped customer name, got %q", row[2]) } if row[5] != "Signature treatment" || row[6] != "Demo Stylist" || row[7] != "Main Studio" { t.Fatalf("expected resolved labels, got service=%q staff=%q location=%q", row[5], row[6], row[7]) } if row[11] != `note with "quotes"` { t.Fatalf("expected escaped notes, got %q", row[11]) } } func TestWriteBookingsCSVRangeExcludesOutOfWindow(t *testing.T) { repo := db.NewMemoryRepository() _, err := repo.CreateBooking(context.Background(), db.CreateBookingParams{ TenantID: demoTenantID, CustomerName: "Far Future", CustomerEmail: "far@example.com", StartsAt: time.Now().Add(200 * 24 * time.Hour), EndsAt: time.Now().Add(200*24*time.Hour + time.Hour), Status: "confirmed", Reference: "BK-CSV-FAR", ManageToken: "tok", }) if err != nil { t.Fatalf("create booking: %v", err) } var buf bytes.Buffer svc := NewService(repo) from := time.Now().Add(-time.Hour) to := time.Now().Add(24 * time.Hour) // far-future booking outside if err := svc.WriteBookingsCSV(context.Background(), demoPrincipal, &buf, from, to); err != nil { t.Fatalf("write csv: %v", err) } for _, record := range readCSV(t, &buf) { if record[0] == "BK-CSV-FAR" { t.Fatal("expected range to exclude far-future booking") } } } func TestWriteCustomersCSVPagesAndStrips(t *testing.T) { repo := db.NewMemoryRepository() var buf bytes.Buffer svc := NewService(repo) if err := svc.WriteCustomersCSV(context.Background(), demoPrincipal, &buf); err != nil { t.Fatalf("write csv: %v", err) } records := readCSV(t, &buf) if len(records) < 2 { t.Fatalf("expected header + seeded customers, got %d rows", len(records)) } if records[0][0] != "name" || records[0][4] != "created_at" { t.Fatalf("unexpected header: %v", records[0]) } found := false for _, record := range records[1:] { if record[0] == "Alice Johnson" && record[1] == "alice@example.com" { found = true } } if !found { t.Fatal("expected seeded customer Alice Johnson in export") } } func TestExportRequiresMembership(t *testing.T) { repo := db.NewMemoryRepository() svc := NewService(repo) var buf bytes.Buffer err := svc.WriteCustomersCSV(context.Background(), domain.Principal{Subject: "nobody"}, &buf) if err != ErrTenantMembership { t.Fatalf("expected ErrTenantMembership, got %v", err) } if buf.Len() != 0 { t.Fatal("expected no output on membership failure") } }