package support import ( "context" "errors" "fmt" "os" "strings" "testing" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) // TestSubmitCSATOwnershipAndOnce covers owner-only rating, eligibility, and one-rating rule. func TestSubmitCSATOwnershipAndOnce(t *testing.T) { dsn := strings.TrimSpace(os.Getenv("DATABASE_URL")) if dsn == "" { t.Skip("DATABASE_URL not set") } ctx := context.Background() pg, err := pgxpool.New(ctx, dsn) if err != nil { t.Fatalf("postgres: %v", err) } t.Cleanup(pg.Close) var hasTable bool if err := pg.QueryRow(ctx, ` SELECT EXISTS ( SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'support_csat_ratings' )`).Scan(&hasTable); err != nil { t.Fatalf("schema probe: %v", err) } if !hasTable { t.Skip("support_csat_ratings missing — run goose up for 029_support_desk") } companyID := uuid.New() ownerID := uuid.New() otherID := uuid.New() prefix := companyID.String()[:8] _, err = pg.Exec(ctx, `INSERT INTO companies (id, name, language) VALUES ($1, $2, 'en')`, companyID, "CSAT Co "+prefix) if err != nil { t.Fatalf("seed company: %v", err) } for _, u := range []struct { id uuid.UUID email string name string }{ {ownerID, fmt.Sprintf("csat-owner-%s@example.test", prefix), "Owner"}, {otherID, fmt.Sprintf("csat-other-%s@example.test", prefix), "Other"}, } { _, err = pg.Exec(ctx, ` INSERT INTO users (id, email, name, password_hash, must_set_password, is_platform_admin, is_active) VALUES ($1, $2, $3, 'x', false, false, true)`, u.id, u.email, u.name) if err != nil { t.Fatalf("seed user: %v", err) } _, err = pg.Exec(ctx, ` INSERT INTO memberships (company_id, user_id, role, status) VALUES ($1, $2, 'member', 'active')`, companyID, u.id) if err != nil { t.Fatalf("seed membership: %v", err) } } t.Cleanup(func() { cctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() _, _ = pg.Exec(cctx, `DELETE FROM support_csat_ratings WHERE company_id = $1`, companyID) _, _ = pg.Exec(cctx, `DELETE FROM support_notifications WHERE ticket_id IN (SELECT id FROM support_tickets WHERE company_id = $1)`, companyID) _, _ = pg.Exec(cctx, `DELETE FROM support_messages WHERE company_id = $1`, companyID) _, _ = pg.Exec(cctx, `DELETE FROM support_tickets WHERE company_id = $1`, companyID) _, _ = pg.Exec(cctx, `DELETE FROM memberships WHERE company_id = $1`, companyID) _, _ = pg.Exec(cctx, `DELETE FROM users WHERE id IN ($1, $2)`, ownerID, otherID) _, _ = pg.Exec(cctx, `DELETE FROM companies WHERE id = $1`, companyID) }) svc := NewService(pg) ticket, err := svc.Create(ctx, companyID, ownerID, CreateInput{ Subject: "CSAT probe", Category: "other", Priority: "normal", Body: "Need help rating", }) if err != nil { t.Fatalf("create: %v", err) } _, err = svc.SubmitCSAT(ctx, companyID, ownerID, ticket.ID, CSATInput{Score: 5, Comment: "secret PII should not log"}) if !errors.Is(err, ErrCSATNotEligible) { t.Fatalf("open ticket rate err=%v want ErrCSATNotEligible", err) } _, err = pg.Exec(ctx, ` UPDATE support_tickets SET status = 'resolved', resolved_at = now(), updated_at = now() WHERE id = $1`, ticket.ID) if err != nil { t.Fatalf("resolve: %v", err) } got, err := svc.GetForUser(ctx, companyID, ownerID, ticket.ID) if err != nil { t.Fatalf("get: %v", err) } if !got.CSATEligible || got.CSAT != nil { t.Fatalf("eligible=%v csat=%v", got.CSATEligible, got.CSAT) } _, err = svc.SubmitCSAT(ctx, companyID, otherID, ticket.ID, CSATInput{Score: 1}) if !errors.Is(err, ErrNotFound) { t.Fatalf("other rate err=%v want ErrNotFound", err) } rating, err := svc.SubmitCSAT(ctx, companyID, ownerID, ticket.ID, CSATInput{Score: 4, Comment: "good"}) if err != nil { t.Fatalf("owner rate: %v", err) } if rating.Score != 4 || rating.Comment != "good" { t.Fatalf("rating=%+v", rating) } _, err = svc.SubmitCSAT(ctx, companyID, ownerID, ticket.ID, CSATInput{Score: 5}) if !errors.Is(err, ErrAlreadyRated) { t.Fatalf("second rate err=%v want ErrAlreadyRated", err) } got, err = svc.GetForUser(ctx, companyID, ownerID, ticket.ID) if err != nil { t.Fatalf("get after: %v", err) } if got.CSATEligible || got.CSAT == nil || got.CSAT.Score != 4 { t.Fatalf("after rate eligible=%v csat=%v", got.CSATEligible, got.CSAT) } agg, err := svc.AggregateCSAT(ctx, nil, nil) if err != nil { t.Fatalf("aggregate: %v", err) } if agg.Total < 1 || agg.Distribution["4"] < 1 { t.Fatalf("aggregate=%+v", agg) } }