diff --git a/wordpress-dev/bin/run-email-attendees-tests.sh b/wordpress-dev/bin/run-email-attendees-tests.sh new file mode 100755 index 00000000..076dd949 --- /dev/null +++ b/wordpress-dev/bin/run-email-attendees-tests.sh @@ -0,0 +1,125 @@ +#!/bin/bash + +# Email Attendees Feature Tests Runner +# This script: +# 1. Transfers test files to the staging server +# 2. Runs the tests on the staging server +# 3. Returns the results + +# Colors +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Configuration +STAGING_HOST="146.190.76.204" +STAGING_USER="roodev" +REMOTE_PLUGIN_PATH="/home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/plugins/hvac-community-events" +LOCAL_PATH="$(pwd)" + +# Print header +echo -e "${GREEN}=== Running Email Attendees Tests on Staging ===${NC}" +echo -e "Remote host: ${STAGING_HOST}" +echo -e "Remote user: ${STAGING_USER}" +echo -e "Plugin path: ${REMOTE_PLUGIN_PATH}" +echo -e "=======================================" + +# Check if test directory exists on staging +ssh ${STAGING_USER}@${STAGING_HOST} "if [ ! -d ${REMOTE_PLUGIN_PATH}/tests ]; then mkdir -p ${REMOTE_PLUGIN_PATH}/tests; fi" +ssh ${STAGING_USER}@${STAGING_HOST} "if [ ! -d ${REMOTE_PLUGIN_PATH}/tests/unit ]; then mkdir -p ${REMOTE_PLUGIN_PATH}/tests/unit; fi" +ssh ${STAGING_USER}@${STAGING_HOST} "if [ ! -d ${REMOTE_PLUGIN_PATH}/tests/integration ]; then mkdir -p ${REMOTE_PLUGIN_PATH}/tests/integration; fi" + +# Transfer test files +echo -e "Transferring test files to staging server..." +scp "${LOCAL_PATH}/wordpress/wp-content/plugins/hvac-community-events/tests/unit/test-email-attendees-data.php" ${STAGING_USER}@${STAGING_HOST}:${REMOTE_PLUGIN_PATH}/tests/unit/ +scp "${LOCAL_PATH}/wordpress/wp-content/plugins/hvac-community-events/tests/integration/test-email-attendees-integration.php" ${STAGING_USER}@${STAGING_HOST}:${REMOTE_PLUGIN_PATH}/tests/integration/ + +# Create bootstrap file if it doesn't exist +cat > /tmp/bootstrap-staging.php << 'EOF' + /tmp/wp-tests-config-staging.php << 'EOF' + { + let eventId: string; + let eventTitle: string = 'Test Event for Email Attendees'; + + test.beforeAll(async ({ browser }) => { + // Create a test event with attendees + const context = await browser.newContext(); + const page = await context.newPage(); + + await loginAsTrainer(page); + + // Create a test event + eventId = await createTestEvent(page, { + title: eventTitle, + description: 'This is a test event for the email attendees functionality', + ticketType: 'General Admission', + price: '50.00' + }); + + // Add test attendees - In a real environment, you'd use the Tribe Tickets API + // For testing, we'd either mock this or use a separate helper to purchase tickets + + await context.close(); + }); + + test('can access Email Attendees page from Event Summary', async ({ page }) => { + // Login as trainer + await loginAsTrainer(page); + + // Navigate to event summary + await page.goto(`/event-summary/?event_id=${eventId}`); + await expect(page).toHaveTitle(new RegExp(eventTitle)); + + // Check that the Email Attendees button exists + const emailAttendeesButton = page.locator('a:text("Email Attendees")'); + await expect(emailAttendeesButton).toBeVisible(); + + // Click Email Attendees button + await emailAttendeesButton.click(); + + // Verify we're on the Email Attendees page + await expect(page).toHaveURL(new RegExp(`/email-attendees/\\?event_id=${eventId}`)); + await expect(page.locator('h1:text("Email Attendees")')).toBeVisible(); + await expect(page.locator(`h2:text("${eventTitle}")`)).toBeVisible(); + }); + + test('email form has all required elements', async ({ page }) => { + // Login and go to Email Attendees page + await loginAsTrainer(page); + await page.goto(`/email-attendees/?event_id=${eventId}`); + + // Check for required form elements + await expect(page.locator('#email_subject')).toBeVisible(); + await expect(page.locator('#email_cc')).toBeVisible(); + + // The rich text editor might be in an iframe, so check for either + const hasEditor = await page.locator('.wp-editor-area, #email_message').count() > 0; + expect(hasEditor).toBeTruthy(); + + // Check for recipients section + await expect(page.locator('h3:text("Recipients")')).toBeVisible(); + + // Check for Send Email button + await expect(page.locator('button[name="hvac_send_email"]')).toBeVisible(); + }); + + test('can filter attendees by ticket type', async ({ page }) => { + // Login and go to Email Attendees page + await loginAsTrainer(page); + await page.goto(`/email-attendees/?event_id=${eventId}`); + + // Check if there's a ticket type filter (may not be visible if only one ticket type) + const hasTicketTypeFilter = await page.locator('#ticket_type_filter').count() > 0; + + if (hasTicketTypeFilter) { + // Select a ticket type + await page.selectOption('#ticket_type_filter', 'General Admission'); + + // Wait for page to refresh + await page.waitForLoadState('networkidle'); + + // Verify URL contains the ticket type parameter + await expect(page).toHaveURL(new RegExp('ticket_type=General\\+Admission')); + + // Verify attendees are filtered + const attendeeItems = page.locator('.hvac-attendee-item'); + await expect(attendeeItems).toContainText(['General Admission']); + } + }); + + test('can select all attendees', async ({ page }) => { + // Login and go to Email Attendees page + await loginAsTrainer(page); + await page.goto(`/email-attendees/?event_id=${eventId}`); + + // Get initial count of checked boxes + const initialCheckedCount = await page.locator('.hvac-attendee-checkbox:checked').count(); + expect(initialCheckedCount).toBe(0); + + // Click "Select All" checkbox + await page.locator('#select_all_attendees').click(); + + // Verify all checkboxes are now checked + const attendeeCheckboxes = page.locator('.hvac-attendee-checkbox'); + const attendeeCount = await attendeeCheckboxes.count(); + const checkedCount = await page.locator('.hvac-attendee-checkbox:checked').count(); + + expect(checkedCount).toBe(attendeeCount); + }); + + test('shows validation error when form is incomplete', async ({ page }) => { + // Login and go to Email Attendees page + await loginAsTrainer(page); + await page.goto(`/email-attendees/?event_id=${eventId}`); + + // Submit form without filling required fields + await page.locator('button[name="hvac_send_email"]').click(); + + // Verify error message is shown + await expect(page.locator('.hvac-email-error')).toBeVisible(); + await expect(page.locator('.hvac-email-error')).toContainText('fill in all required fields'); + }); + + test('can send email to attendees', async ({ page }) => { + // Login and go to Email Attendees page + await loginAsTrainer(page); + await page.goto(`/email-attendees/?event_id=${eventId}`); + + // Fill out the form + await page.fill('#email_subject', 'Test Email Subject'); + + // Fill the message (handling both regular textarea and TinyMCE) + if (await page.locator('#email_message').count() > 0) { + await page.fill('#email_message', 'This is a test email message.'); + } else { + // For TinyMCE, we need to use the iframe + const frame = page.frameLocator('.wp-editor-container iframe'); + await frame.locator('body').fill('This is a test email message.'); + } + + // Select recipients (first attendee) + await page.locator('.hvac-attendee-checkbox').first().check(); + + // Submit form + await page.locator('button[name="hvac_send_email"]').click(); + + // Verify success message + await expect(page.locator('.hvac-email-sent')).toBeVisible(); + await expect(page.locator('.hvac-email-sent')).toContainText('Email successfully sent'); + }); +}); \ No newline at end of file diff --git a/wordpress-dev/tests/e2e/utils/event-helpers.ts b/wordpress-dev/tests/e2e/utils/event-helpers.ts new file mode 100644 index 00000000..7807b250 --- /dev/null +++ b/wordpress-dev/tests/e2e/utils/event-helpers.ts @@ -0,0 +1,89 @@ +import { Page } from '@playwright/test'; +import { DashboardPage } from '../pages/DashboardPage'; +import { CreateEventPage } from '../pages/CreateEventPage'; + +interface EventData { + title: string; + description?: string; + startDate?: string; + endDate?: string; + startTime?: string; + endTime?: string; + ticketType?: string; + price?: string; + venue?: string; + organizer?: string; +} + +/** + * Helper function to create a test event + * @param page Playwright Page object + * @param eventData Event data to create + * @returns Event ID of the created event + */ +export async function createTestEvent(page: Page, eventData: EventData): Promise { + // Navigate to dashboard + const dashboardPage = new DashboardPage(page); + await dashboardPage.navigate(); + + // Click create event button + await dashboardPage.clickCreateEvent(); + + // Fill event form + const createEventPage = new CreateEventPage(page); + await createEventPage.fillEventTitle(eventData.title); + + if (eventData.description) { + await createEventPage.fillEventDescription(eventData.description); + } + + // Set dates and times if provided + if (eventData.startDate) { + await createEventPage.setStartDate(eventData.startDate); + } + + if (eventData.endDate) { + await createEventPage.setEndDate(eventData.endDate); + } + + if (eventData.startTime) { + await createEventPage.setStartTime(eventData.startTime); + } + + if (eventData.endTime) { + await createEventPage.setEndTime(eventData.endTime); + } + + // Add ticket if price is provided + if (eventData.ticketType && eventData.price) { + await createEventPage.addTicket(eventData.ticketType, eventData.price); + } + + // Set venue if provided + if (eventData.venue) { + await createEventPage.setVenue(eventData.venue); + } + + // Set organizer if provided + if (eventData.organizer) { + await createEventPage.setOrganizer(eventData.organizer); + } + + // Submit the form + const eventId = await createEventPage.submitForm(); + + return eventId; +} + +/** + * Helper function to get event ID from URL + * @param url Event URL + * @returns Event ID extracted from URL + */ +export function extractEventId(url: string): string { + const match = url.match(/event_id=(\d+)/); + if (match && match[1]) { + return match[1]; + } + return ''; +} \ No newline at end of file diff --git a/wordpress-dev/tests/e2e/utils/login-helpers.ts b/wordpress-dev/tests/e2e/utils/login-helpers.ts new file mode 100644 index 00000000..e41e2fbc --- /dev/null +++ b/wordpress-dev/tests/e2e/utils/login-helpers.ts @@ -0,0 +1,34 @@ +import { Page } from '@playwright/test'; +import { LoginPage } from '../pages/LoginPage'; + +/** + * Helper function to login as a trainer + * @param page Playwright Page object + */ +export async function loginAsTrainer(page: Page): Promise { + const loginPage = new LoginPage(page); + await loginPage.navigate(); + await loginPage.login('trainer'); +} + +/** + * Helper function to login as an admin trainer + * @param page Playwright Page object + */ +export async function loginAsAdminTrainer(page: Page): Promise { + const loginPage = new LoginPage(page); + await loginPage.navigate(); + await loginPage.login('adminTrainer'); +} + +/** + * Helper function to login with custom credentials + * @param page Playwright Page object + * @param username Username to login with + * @param password Password to login with + */ +export async function loginWithCredentials(page: Page, username: string, password: string): Promise { + const loginPage = new LoginPage(page); + await loginPage.navigate(); + await loginPage.loginWithCredentials(username, password); +} \ No newline at end of file diff --git a/wordpress-dev/tests/manual/email-attendees-test-plan.md b/wordpress-dev/tests/manual/email-attendees-test-plan.md new file mode 100644 index 00000000..0e0402f1 --- /dev/null +++ b/wordpress-dev/tests/manual/email-attendees-test-plan.md @@ -0,0 +1,88 @@ +# Email Attendees Feature - Manual Test Plan + +This document outlines a manual testing plan for the Email Attendees functionality in the HVAC Community Events plugin. + +## Prerequisites + +- A WordPress site with The Events Calendar and Event Tickets plugins installed and activated +- The HVAC Community Events plugin installed and activated +- A user with the `hvac_trainer` role +- At least one event created by the trainer +- At least one attendee registered for the event + +## Test Scenarios + +### 1. Navigation and Access Control + +| # | Test Case | Expected Result | Pass/Fail | +|---|-----------|-----------------|-----------| +| 1.1 | As a logged-in trainer, navigate to Event Summary page of your event | Event Summary page loads with event details and Email Attendees button is visible | | +| 1.2 | Click on Email Attendees button | Redirects to Email Attendees page with the same event ID | | +| 1.3 | Try to access Email Attendees page directly when logged out | Redirects to login page | | +| 1.4 | Try to access Email Attendees page for an event you don't own | Access denied message appears | | + +### 2. UI Elements + +| # | Test Case | Expected Result | Pass/Fail | +|---|-----------|-----------------|-----------| +| 2.1 | Load Email Attendees page for an event with attendees | Page shows event title, navigation buttons, email form, and list of attendees | | +| 2.2 | Verify form elements | Subject field, CC field, rich text editor, and recipient list are present | | +| 2.3 | Check navigation buttons | View Event Summary and Return to Dashboard buttons are functional | | +| 2.4 | Verify attendee list | List shows attendee names, emails, and ticket types | | +| 2.5 | Check for Select All functionality | Select All checkbox selects/deselects all attendees | | + +### 3. Filtering + +| # | Test Case | Expected Result | Pass/Fail | +|---|-----------|-----------------|-----------| +| 3.1 | With multiple ticket types, check if filter exists | Ticket type filter dropdown is visible with all ticket types | | +| 3.2 | Select a specific ticket type | Page reloads with only attendees of that ticket type | | +| 3.3 | Select "All Tickets" option | Page shows all attendees regardless of ticket type | | + +### 4. Form Validation + +| # | Test Case | Expected Result | Pass/Fail | +|---|-----------|-----------------|-----------| +| 4.1 | Submit form without subject | Error message indicates subject is required | | +| 4.2 | Submit form without message | Error message indicates message is required | | +| 4.3 | Submit form without selecting recipients | Error message indicates at least one recipient is required | | +| 4.4 | Enter invalid email in CC field | Error message indicates invalid email format | | + +### 5. Email Sending + +| # | Test Case | Expected Result | Pass/Fail | +|---|-----------|-----------------|-----------| +| 5.1 | Fill all required fields and submit | Success message indicates email was sent | | +| 5.2 | Check recipient receives email | Email is received with correct subject and content | | +| 5.3 | Send email with CC | CC recipient also receives the email | | +| 5.4 | Verify personalization | Email begins with attendee's name if available | | +| 5.5 | Check email subject | Subject contains event title and custom subject text | | + +### 6. Edge Cases + +| # | Test Case | Expected Result | Pass/Fail | +|---|-----------|-----------------|-----------| +| 6.1 | Access page for an event with no attendees | Message indicates no attendees available | | +| 6.2 | Try to email non-existent event | Redirects to dashboard with error message | | +| 6.3 | Test with a large number of attendees | List correctly paginates or scrolls, Select All works correctly | | +| 6.4 | Test with special characters in email fields | Special characters are properly escaped | | + +## Test Results + +**Tester:** ______________________ + +**Date:** ________________________ + +**Environment:** _________________ + +**Overall Result:** _______________ + +## Notes + +- Record any bugs, issues, or unexpected behavior in this section +- Include browser and device information for any UI or display issues +- Document any performance concerns or usability issues + +## Recommendations + +- Add any recommendations for future improvements or feature enhancements here \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/hvac-email-attendees.css b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/hvac-email-attendees.css new file mode 100644 index 00000000..ab3b86ed --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/hvac-email-attendees.css @@ -0,0 +1,136 @@ +/** + * Styles for the Email Attendees page + */ + +.hvac-email-attendees-wrapper { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +.hvac-email-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.hvac-email-title h1 { + margin: 0 0 10px 0; +} + +.hvac-email-navigation { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.hvac-email-form { + margin-top: 20px; +} + +.hvac-email-info { + background-color: #f9f9f9; + padding: 15px; + border-radius: 5px; + margin-bottom: 20px; +} + +.hvac-email-form-row { + margin-bottom: 15px; +} + +.hvac-email-form-row label { + display: block; + margin-bottom: 5px; + font-weight: 600; +} + +.hvac-email-form-row input[type="text"], +.hvac-email-form-row textarea { + width: 100%; + padding: 8px; + border-radius: 4px; + border: 1px solid #ddd; +} + +.hvac-email-recipients { + margin-top: 20px; + border: 1px solid #ddd; + padding: 15px; + border-radius: 5px; +} + +.hvac-email-filter { + margin-bottom: 15px; + display: flex; + gap: 15px; + align-items: center; + flex-wrap: wrap; +} + +.hvac-attendee-list { + max-height: 300px; + overflow-y: auto; + border: 1px solid #eee; + padding: 10px; +} + +.hvac-attendee-item { + margin-bottom: 10px; + padding: 5px; + background-color: #f9f9f9; + border-radius: 3px; +} + +.hvac-attendee-checkbox { + margin-right: 10px; +} + +.hvac-email-sent { + background-color: #d4edda; + color: #155724; + padding: 15px; + border-radius: 5px; + margin-bottom: 20px; +} + +.hvac-email-error { + background-color: #f8d7da; + color: #721c24; + padding: 15px; + border-radius: 5px; + margin-bottom: 20px; +} + +.hvac-count-badge { + background-color: #007cba; + color: white; + padding: 2px 8px; + border-radius: 10px; + font-size: 0.8em; + margin-left: 5px; +} + +.hvac-select-all-container { + margin-bottom: 10px; +} + +/* Responsive styles */ +@media (max-width: 768px) { + .hvac-email-header { + flex-direction: column; + align-items: flex-start; + } + + .hvac-email-navigation { + margin-top: 15px; + margin-bottom: 15px; + } + + .hvac-email-filter { + flex-direction: column; + align-items: flex-start; + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php index 6eb839eb..c5bd08d8 100644 --- a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php @@ -63,6 +63,10 @@ function hvac_ce_create_required_pages() { 'title' => 'Event Summary', 'content' => '[hvac_event_summary]', ], + 'email-attendees' => [ // Add email attendees page + 'title' => 'Email Attendees', + 'content' => '[hvac_email_attendees]', + ], // REMOVED: 'submit-event' page creation. Will link to default TEC CE page. // 'submit-event' => [ // 'title' => 'Submit Event', @@ -231,6 +235,22 @@ function hvac_ce_enqueue_event_summary_styles() { } add_action( 'wp_enqueue_scripts', 'hvac_ce_enqueue_event_summary_styles' ); +/** + * Enqueue styles specifically for the HVAC Email Attendees page. + */ +function hvac_ce_enqueue_email_attendees_styles() { + // Check if we are on the email attendees page + if ( is_page( 'email-attendees' ) ) { + wp_enqueue_style( + 'hvac-email-attendees-style', + HVAC_CE_PLUGIN_URL . 'assets/css/hvac-email-attendees.css', + [], // No dependencies for now + HVAC_CE_VERSION + ); + } +} +add_action( 'wp_enqueue_scripts', 'hvac_ce_enqueue_email_attendees_styles' ); + // Include the main plugin class diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-community-events.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-community-events.php index 165803f6..f2cec300 100644 --- a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-community-events.php +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-community-events.php @@ -110,6 +110,9 @@ class HVAC_Community_Events { // Add authentication check for event summary page add_action('template_redirect', array($this, 'check_event_summary_auth')); + + // Add authentication check for email attendees page + add_action('template_redirect', array($this, 'check_email_attendees_auth')); } // End init_hooks /** @@ -123,6 +126,18 @@ class HVAC_Community_Events { exit; } } + + /** + * Check authentication for email attendees page + */ + public function check_email_attendees_auth() { + // Check if we're on the email-attendees page + if (is_page('email-attendees') && !is_user_logged_in()) { + // Redirect to login page + wp_redirect(home_url('/community-login/?redirect_to=' . urlencode($_SERVER['REQUEST_URI']))); + exit; + } + } /** * Plugin activation (Should be called statically or from the main plugin file context) @@ -235,6 +250,9 @@ class HVAC_Community_Events { // Add edit profile shortcode add_shortcode('hvac_edit_profile', array('HVAC_Registration', 'render_edit_profile_form')); + // Add email attendees shortcode + add_shortcode('hvac_email_attendees', array($this, 'render_email_attendees')); + // Remove the event form shortcode as we're using TEC's shortcode instead // add_shortcode('hvac_event_form', array('HVAC_Community_Event_Handler', 'render_event_form')); @@ -302,6 +320,40 @@ class HVAC_Community_Events { include HVAC_CE_PLUGIN_DIR . 'templates/template-trainer-profile.php'; return ob_get_clean(); } + + /** + * Render email attendees content + */ + public function render_email_attendees() { + // Check if user is logged in + if (!is_user_logged_in()) { + return '

Please log in to email event attendees.

'; + } + + // Get event ID from URL parameter + $event_id = isset($_GET['event_id']) ? absint($_GET['event_id']) : 0; + + if ($event_id <= 0) { + return '
No event ID provided. Please access this page from your dashboard or event summary page.
'; + } + + // Check if the event exists and user has permission to view it + $event = get_post($event_id); + if (!$event || get_post_type($event) !== Tribe__Events__Main::POSTTYPE) { + return '
Event not found or invalid.
'; + } + + // Check if the current user has permission to view this event + // For now, we'll check if they're the post author or have edit_posts capability + if ($event->post_author != get_current_user_id() && !current_user_can('edit_posts')) { + return '
You do not have permission to email attendees for this event.
'; + } + + // Include the email attendees template + ob_start(); + include HVAC_CE_PLUGIN_DIR . 'templates/email-attendees/template-email-attendees.php'; + return ob_get_clean(); + } /** * Include custom templates for plugin pages @@ -347,6 +399,14 @@ class HVAC_Community_Events { } } + // Check for email-attendees page + if (is_page('email-attendees')) { + $custom_template = HVAC_CE_PLUGIN_DIR . 'templates/email-attendees/template-email-attendees.php'; + if (file_exists($custom_template)) { + return $custom_template; + } + } + // Check for edit-profile page if (is_page('edit-profile')) { $custom_template = HVAC_CE_PLUGIN_DIR . 'templates/template-edit-profile.php'; diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/community/class-email-attendees-data.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/community/class-email-attendees-data.php new file mode 100644 index 00000000..306b66f0 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/community/class-email-attendees-data.php @@ -0,0 +1,277 @@ +event_id = intval( $event_id ); + } + + /** + * Check if the event is valid. + * + * @return bool Whether the event exists and is valid. + */ + public function is_valid_event() { + if ( empty( $this->event_id ) ) { + return false; + } + + $event = get_post( $this->event_id ); + return ( $event && $event->post_type === 'tribe_events' ); + } + + /** + * Check if the current user can view and email attendees for this event. + * + * @return bool Whether the user can view and email attendees. + */ + public function user_can_email_attendees() { + if ( ! is_user_logged_in() ) { + return false; + } + + $event = get_post( $this->event_id ); + if ( ! $event ) { + return false; + } + + // Allow event author or admins with edit_posts capability + return ( get_current_user_id() === (int) $event->post_author || current_user_can( 'edit_posts' ) ); + } + + /** + * Get all attendees for the event. + * + * @return array Array of attendee data. + */ + public function get_attendees() { + if ( ! $this->is_valid_event() ) { + return array(); + } + + // Use The Events Calendar's function to get attendees + $attendees = tribe_tickets_get_attendees( $this->event_id ); + $processed_attendees = array(); + + if ( ! empty( $attendees ) ) { + foreach ( $attendees as $attendee ) { + $email = isset( $attendee['holder_email'] ) ? $attendee['holder_email'] : ''; + $name = isset( $attendee['holder_name'] ) ? $attendee['holder_name'] : ''; + $ticket_name = isset( $attendee['ticket_name'] ) ? $attendee['ticket_name'] : ''; + + // Only include attendees with valid emails + if ( ! empty( $email ) && is_email( $email ) ) { + $processed_attendees[] = array( + 'name' => $name, + 'email' => $email, + 'ticket_name' => $ticket_name, + 'attendee_id' => isset( $attendee['attendee_id'] ) ? $attendee['attendee_id'] : 0, + 'order_id' => isset( $attendee['order_id'] ) ? $attendee['order_id'] : 0, + ); + } + } + } + + return $processed_attendees; + } + + /** + * Get attendees filtered by ticket type. + * + * @param string $ticket_type The ticket type to filter by. + * @return array Filtered attendees. + */ + public function get_attendees_by_ticket_type( $ticket_type ) { + $attendees = $this->get_attendees(); + + if ( empty( $ticket_type ) ) { + return $attendees; + } + + return array_filter( $attendees, function( $attendee ) use ( $ticket_type ) { + return $attendee['ticket_name'] === $ticket_type; + }); + } + + /** + * Get all ticket types for the event. + * + * @return array Array of ticket types. + */ + public function get_ticket_types() { + $attendees = $this->get_attendees(); + $ticket_types = array(); + + foreach ( $attendees as $attendee ) { + if ( ! empty( $attendee['ticket_name'] ) && ! in_array( $attendee['ticket_name'], $ticket_types ) ) { + $ticket_types[] = $attendee['ticket_name']; + } + } + + return $ticket_types; + } + + /** + * Get the event details. + * + * @return array Event details. + */ + public function get_event_details() { + if ( ! $this->is_valid_event() ) { + return array(); + } + + $event = get_post( $this->event_id ); + + return array( + 'id' => $this->event_id, + 'title' => get_the_title( $event ), + 'start_date' => tribe_get_start_date( $event, false, 'F j, Y' ), + 'start_time' => tribe_get_start_date( $event, false, 'g:i a' ), + 'end_date' => tribe_get_end_date( $event, false, 'F j, Y' ), + 'end_time' => tribe_get_end_date( $event, false, 'g:i a' ), + ); + } + + /** + * Send email to attendees. + * + * @param array $recipients Array of recipient emails or attendee IDs. + * @param string $subject The email subject. + * @param string $message The email message. + * @param string $cc Optional CC email addresses. + * @return array Result with status and message. + */ + public function send_email( $recipients, $subject, $message, $cc = '' ) { + if ( empty( $recipients ) || empty( $subject ) || empty( $message ) ) { + return array( + 'success' => false, + 'message' => 'Missing required fields (recipients, subject, or message).', + ); + } + + if ( ! $this->is_valid_event() || ! $this->user_can_email_attendees() ) { + return array( + 'success' => false, + 'message' => 'You do not have permission to email attendees for this event.', + ); + } + + $headers = array('Content-Type: text/html; charset=UTF-8'); + $event_details = $this->get_event_details(); + $event_title = $event_details['title']; + + // Add CC if provided + if ( ! empty( $cc ) ) { + $cc_emails = explode( ',', $cc ); + foreach ( $cc_emails as $cc_email ) { + $cc_email = trim( $cc_email ); + if ( is_email( $cc_email ) ) { + $headers[] = 'Cc: ' . $cc_email; + } + } + } + + // Add sender information + $current_user = wp_get_current_user(); + $from_name = $current_user->display_name; + $from_email = $current_user->user_email; + $headers[] = 'From: ' . $from_name . ' <' . $from_email . '>'; + + // Process recipients + $all_attendees = $this->get_attendees(); + $attendee_emails = array(); + $sent_count = 0; + $error_count = 0; + + // Handle numeric IDs or email addresses + foreach ( $recipients as $recipient ) { + if ( is_numeric( $recipient ) ) { + // Find attendee by ID + foreach ( $all_attendees as $attendee ) { + if ( $attendee['attendee_id'] == $recipient ) { + $attendee_emails[$attendee['email']] = $attendee['name']; + break; + } + } + } elseif ( is_email( $recipient ) ) { + // Add directly if it's an email + $attendee_name = ''; + foreach ( $all_attendees as $attendee ) { + if ( $attendee['email'] === $recipient ) { + $attendee_name = $attendee['name']; + break; + } + } + $attendee_emails[$recipient] = $attendee_name; + } + } + + // Subject with event title + $email_subject = sprintf( '[%s] %s', $event_title, $subject ); + + // Send to each recipient individually for personalization + foreach ( $attendee_emails as $email => $name ) { + // Personalize message with attendee name if available + $personalized_message = $message; + if ( ! empty( $name ) ) { + $personalized_message = "Hello " . $name . ",\n\n" . $message; + } + + $mail_sent = wp_mail( $email, $email_subject, wpautop( $personalized_message ), $headers ); + + if ( $mail_sent ) { + $sent_count++; + } else { + $error_count++; + } + } + + // Return results + if ( $error_count > 0 ) { + return array( + 'success' => $sent_count > 0, + 'message' => sprintf( + 'Email sent to %d recipients. Failed to send to %d recipients.', + $sent_count, + $error_count + ), + ); + } + + return array( + 'success' => true, + 'message' => sprintf( 'Email successfully sent to %d recipients.', $sent_count ), + ); + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/email-attendees/template-email-attendees.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/email-attendees/template-email-attendees.php new file mode 100644 index 00000000..ea2ee274 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/email-attendees/template-email-attendees.php @@ -0,0 +1,336 @@ +is_valid_event() ) { + wp_redirect( site_url( '/hvac-dashboard/' ) ); + exit; +} + +if ( ! $email_data->user_can_email_attendees() ) { + wp_die( __( 'You do not have permission to email attendees for this event.', 'hvac-community-events' ) ); +} + +// Get event details and attendees +$event_details = $email_data->get_event_details(); +$attendees = $email_data->get_attendees(); +$ticket_types = $email_data->get_ticket_types(); + +// Handle form submission +$email_sent = false; +$email_error = ''; +$email_success = ''; + +if ( isset( $_POST['hvac_send_email'] ) && isset( $_POST['_wpnonce'] ) && wp_verify_nonce( $_POST['_wpnonce'], 'hvac_email_attendees_' . $event_id ) ) { + + $subject = isset( $_POST['email_subject'] ) ? sanitize_text_field( $_POST['email_subject'] ) : ''; + $message = isset( $_POST['email_message'] ) ? wp_kses_post( $_POST['email_message'] ) : ''; + $cc = isset( $_POST['email_cc'] ) ? sanitize_text_field( $_POST['email_cc'] ) : ''; + + // Get selected recipients + $recipients = array(); + if ( isset( $_POST['email_attendees'] ) && is_array( $_POST['email_attendees'] ) ) { + $recipients = array_map( 'sanitize_text_field', $_POST['email_attendees'] ); + } + + // Validate and send email + if ( empty( $subject ) || empty( $message ) || empty( $recipients ) ) { + $email_error = __( 'Please fill in all required fields (subject, message, and select at least one recipient).', 'hvac-community-events' ); + } else { + $result = $email_data->send_email( $recipients, $subject, $message, $cc ); + + if ( $result['success'] ) { + $email_sent = true; + $email_success = $result['message']; + } else { + $email_error = $result['message']; + } + } +} + +// Get filtered attendees if a ticket type is selected +$selected_ticket_type = isset( $_GET['ticket_type'] ) ? sanitize_text_field( $_GET['ticket_type'] ) : ''; +if ( ! empty( $selected_ticket_type ) ) { + $attendees = $email_data->get_attendees_by_ticket_type( $selected_ticket_type ); +} + +// Get the site title for the page title +$site_title = get_bloginfo( 'name' ); +?> + + +> + + + + <?php echo esc_html( $site_title ); ?> - <?php _e( 'Email Attendees', 'hvac-community-events' ); ?> + + + + +> + + + + + + + \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/event-summary/template-event-summary.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/event-summary/template-event-summary.php index 7c06352b..b5a9e459 100644 --- a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/event-summary/template-event-summary.php +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/event-summary/template-event-summary.php @@ -131,10 +131,9 @@ get_header(); // View public event page echo 'View Public Page'; - // Email attendees link (future feature) + // Email attendees link if ( current_user_can( 'edit_post', $event_id ) ) { - // TODO: Link to actual Email Attendees page when implemented (Phase 2) - $email_url = '#'; // Placeholder for now + $email_url = add_query_arg( 'event_id', $event_id, home_url( '/email-attendees/' ) ); echo 'Email Attendees'; } ?> diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/template-event-summary.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/template-event-summary.php index dcce773f..a8908ddf 100644 --- a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/template-event-summary.php +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/template-event-summary.php @@ -131,10 +131,9 @@ get_header(); // View public event page echo 'View Public Page'; - // Email attendees link (future feature) + // Email attendees link if ( current_user_can( 'edit_post', $event_id ) ) { - // TODO: Link to actual Email Attendees page when implemented (Phase 2) - $email_url = '#'; // Placeholder for now + $email_url = add_query_arg( 'event_id', $event_id, home_url( '/email-attendees/' ) ); echo 'Email Attendees'; } ?> diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/integration/test-email-attendees-integration.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/integration/test-email-attendees-integration.php new file mode 100644 index 00000000..1180b478 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/integration/test-email-attendees-integration.php @@ -0,0 +1,198 @@ +server = $wp_rest_server = new WP_REST_Server; + do_action('rest_api_init'); + + // Create a test user with trainer role + $this->user_id = $this->factory->user->create(array( + 'role' => 'hvac_trainer', + 'user_login' => 'test_trainer', + 'user_pass' => 'password', + 'user_email' => 'trainer@example.com', + 'display_name' => 'Test Trainer', + )); + + // Set current user for permission testing + wp_set_current_user($this->user_id); + + // Make sure the classes exist + require_once HVAC_CE_PLUGIN_DIR . 'includes/community/class-email-attendees-data.php'; + require_once HVAC_CE_PLUGIN_DIR . 'includes/community/class-event-summary-data.php'; + + // Create a test event + $event_data = array( + 'post_title' => 'Test Event for Email Attendees Integration', + 'post_content' => 'Test event integration test content', + 'post_status' => 'publish', + 'post_type' => Tribe__Events__Main::POSTTYPE, + 'post_author' => $this->user_id, + ); + $this->event_id = wp_insert_post($event_data); + + // Set up mock attendees if needed using Event Tickets functions + // This is more complex and would depend on the actual implementation + } + + /** + * Clean up test data after each test + */ + public function tearDown(): void { + // Delete test event + wp_delete_post($this->event_id, true); + + // Delete test user + wp_delete_user($this->user_id); + + parent::tearDown(); + } + + /** + * Test that the email attendees shortcode exists and renders + */ + public function test_email_attendees_shortcode_exists() { + // Verify the shortcode is registered + global $shortcode_tags; + $this->assertArrayHasKey('hvac_email_attendees', $shortcode_tags); + + // Test shortcode output with no parameters + $output = do_shortcode('[hvac_email_attendees]'); + $this->assertStringContainsString('Please log in to email event attendees', $output); + } + + /** + * Test the integration between Event Summary and Email Attendees + */ + public function test_event_summary_links_to_email_attendees() { + // Get event summary output + ob_start(); + include HVAC_CE_PLUGIN_DIR . 'templates/event-summary/template-event-summary.php'; + $output = ob_get_clean(); + + // Check that the email attendees link is included + $expected_url = add_query_arg('event_id', $this->event_id, home_url('/email-attendees/')); + $this->assertStringContainsString($expected_url, $output); + $this->assertStringContainsString('Email Attendees', $output); + } + + /** + * Test that the email attendees template loads correctly + */ + public function test_email_attendees_template_loads() { + // Set up $_GET parameter + $_GET['event_id'] = $this->event_id; + + // Capture template output + ob_start(); + include HVAC_CE_PLUGIN_DIR . 'templates/email-attendees/template-email-attendees.php'; + $output = ob_get_clean(); + + // Verify template elements + $this->assertStringContainsString('Email Attendees', $output); + $this->assertStringContainsString('Subject:', $output); + $this->assertStringContainsString('CC:', $output); + $this->assertStringContainsString('Message:', $output); + $this->assertStringContainsString('Recipients', $output); + $this->assertStringContainsString('Send Email', $output); + + // Unset $_GET parameter + unset($_GET['event_id']); + } + + /** + * Test the authentication check for email attendees page + */ + public function test_email_attendees_auth_check() { + // Create an instance of the main plugin class + $plugin = new HVAC_Community_Events(); + + // Log out to test the redirect + wp_set_current_user(0); + + // Mock WordPress is_page function + global $wp_query; + $wp_query->is_page = true; + $wp_query->queried_object = (object) array('post_name' => 'email-attendees'); + + // Capture redirect + ob_start(); + $plugin->check_email_attendees_auth(); + $output = ob_get_clean(); + + // Since we're not in a real page request, the function will try to redirect + // but won't exit, so we don't expect any output + $this->assertEmpty($output); + + // Reset current user + wp_set_current_user($this->user_id); + } + + /** + * Test form submission handling + */ + public function test_form_submission_handling() { + // This test is more complex as it involves form submission + // We would need to mock the $_POST data and possibly override wp_mail + + // Set up $_GET and $_POST data + $_GET['event_id'] = $this->event_id; + $_POST['hvac_send_email'] = 1; + $_POST['email_subject'] = 'Integration Test Subject'; + $_POST['email_message'] = 'Integration test message content'; + $_POST['email_attendees'] = array('test@example.com'); + + // Create nonce + $_POST['_wpnonce'] = wp_create_nonce('hvac_email_attendees_' . $this->event_id); + + // Override wp_mail for testing + add_filter('wp_mail', function($args) { + // Capture email arguments for validation + $this->assertEquals('Integration Test Subject', $args['subject']); + $this->assertEquals('test@example.com', $args['to']); + return false; // Prevent actual email sending + }); + + // Capture template output + ob_start(); + include HVAC_CE_PLUGIN_DIR . 'templates/email-attendees/template-email-attendees.php'; + $output = ob_get_clean(); + + // Verify success message appears in output + $this->assertStringContainsString('Email successfully sent', $output); + + // Clean up + unset($_GET['event_id']); + unset($_POST['hvac_send_email']); + unset($_POST['email_subject']); + unset($_POST['email_message']); + unset($_POST['email_attendees']); + unset($_POST['_wpnonce']); + remove_all_filters('wp_mail'); + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/unit/test-email-attendees-data.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/unit/test-email-attendees-data.php new file mode 100644 index 00000000..b7060978 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/tests/unit/test-email-attendees-data.php @@ -0,0 +1,345 @@ +user_id = $this->factory->user->create(array( + 'role' => 'hvac_trainer', + )); + + // Set current user for permission testing + wp_set_current_user($this->user_id); + + // Make sure the class exists + require_once HVAC_CE_PLUGIN_DIR . 'includes/community/class-email-attendees-data.php'; + + // Create a test event + $event_data = array( + 'post_title' => 'Test Event for Email Attendees', + 'post_content' => 'Test event content', + 'post_status' => 'publish', + 'post_type' => Tribe__Events__Main::POSTTYPE, + 'post_author' => $this->user_id, + ); + $this->event_id = wp_insert_post($event_data); + + // Mock tribe_tickets_get_attendees functionality if needed + // This would require more setup depending on how deeply you want to test + } + + /** + * Clean up test data after each test + */ + public function tearDown(): void { + // Delete test event + wp_delete_post($this->event_id, true); + + // Delete test user + wp_delete_user($this->user_id); + + parent::tearDown(); + } + + /** + * Test constructor + */ + public function test_constructor() { + $email_data = new HVAC_Email_Attendees_Data($this->event_id); + $this->assertInstanceOf('HVAC_Email_Attendees_Data', $email_data); + } + + /** + * Test is_valid_event method + */ + public function test_is_valid_event() { + $email_data = new HVAC_Email_Attendees_Data($this->event_id); + $this->assertTrue($email_data->is_valid_event()); + + // Test with invalid event ID + $email_data_invalid = new HVAC_Email_Attendees_Data(999999); + $this->assertFalse($email_data_invalid->is_valid_event()); + + // Test with empty event ID + $email_data_empty = new HVAC_Email_Attendees_Data(); + $this->assertFalse($email_data_empty->is_valid_event()); + } + + /** + * Test user_can_email_attendees method + */ + public function test_user_can_email_attendees() { + $email_data = new HVAC_Email_Attendees_Data($this->event_id); + + // Current user is event author, should return true + $this->assertTrue($email_data->user_can_email_attendees()); + + // Create another user who is not the event author + $another_user_id = $this->factory->user->create(array( + 'role' => 'hvac_trainer', + )); + wp_set_current_user($another_user_id); + + // Another user without edit_posts capability should return false + $this->assertFalse($email_data->user_can_email_attendees()); + + // Create admin user + $admin_user_id = $this->factory->user->create(array( + 'role' => 'administrator', + )); + wp_set_current_user($admin_user_id); + + // Admin with edit_posts capability should return true + $this->assertTrue($email_data->user_can_email_attendees()); + + // Reset to original user + wp_set_current_user($this->user_id); + } + + /** + * Test get_event_details method + */ + public function test_get_event_details() { + $email_data = new HVAC_Email_Attendees_Data($this->event_id); + $event_details = $email_data->get_event_details(); + + // Verify event details contain expected data + $this->assertIsArray($event_details); + $this->assertEquals($this->event_id, $event_details['id']); + $this->assertEquals('Test Event for Email Attendees', $event_details['title']); + } + + /** + * Test get_attendees and get_attendees_by_ticket_type methods with mocked data + */ + public function test_get_attendees_with_mock_data() { + // Create a test instance + $email_data = $this->getMockBuilder('HVAC_Email_Attendees_Data') + ->setConstructorArgs(array($this->event_id)) + ->setMethods(array('get_attendees')) + ->getMock(); + + // Mock attendee data + $mock_attendees = array( + array( + 'name' => 'Test Attendee 1', + 'email' => 'attendee1@example.com', + 'ticket_name' => 'General Admission', + 'attendee_id' => 101, + 'order_id' => 1001, + ), + array( + 'name' => 'Test Attendee 2', + 'email' => 'attendee2@example.com', + 'ticket_name' => 'VIP Pass', + 'attendee_id' => 102, + 'order_id' => 1002, + ), + array( + 'name' => 'Test Attendee 3', + 'email' => 'attendee3@example.com', + 'ticket_name' => 'General Admission', + 'attendee_id' => 103, + 'order_id' => 1003, + ), + ); + + // Configure mock to return test data + $email_data->expects($this->any()) + ->method('get_attendees') + ->will($this->returnValue($mock_attendees)); + + // Use reflection to access non-public methods for testing + $reflection = new ReflectionClass('HVAC_Email_Attendees_Data'); + $method = $reflection->getMethod('get_attendees_by_ticket_type'); + $method->setAccessible(true); + + // Test filtering by ticket type + $general_admission = $method->invokeArgs($email_data, array('General Admission')); + $this->assertCount(2, $general_admission); + $this->assertEquals('Test Attendee 1', $general_admission[0]['name']); + $this->assertEquals('Test Attendee 3', $general_admission[2]['name']); + + // Test filtering by another ticket type + $vip_pass = $method->invokeArgs($email_data, array('VIP Pass')); + $this->assertCount(1, $vip_pass); + $this->assertEquals('Test Attendee 2', $vip_pass[0]['name']); + + // Test getting all attendees (no filter) + $all_attendees = $method->invokeArgs($email_data, array('')); + $this->assertCount(3, $all_attendees); + } + + /** + * Test get_ticket_types method with mocked data + */ + public function test_get_ticket_types_with_mock_data() { + // Create a test instance + $email_data = $this->getMockBuilder('HVAC_Email_Attendees_Data') + ->setConstructorArgs(array($this->event_id)) + ->setMethods(array('get_attendees')) + ->getMock(); + + // Mock attendee data + $mock_attendees = array( + array( + 'name' => 'Test Attendee 1', + 'email' => 'attendee1@example.com', + 'ticket_name' => 'General Admission', + 'attendee_id' => 101, + 'order_id' => 1001, + ), + array( + 'name' => 'Test Attendee 2', + 'email' => 'attendee2@example.com', + 'ticket_name' => 'VIP Pass', + 'attendee_id' => 102, + 'order_id' => 1002, + ), + array( + 'name' => 'Test Attendee 3', + 'email' => 'attendee3@example.com', + 'ticket_name' => 'General Admission', + 'attendee_id' => 103, + 'order_id' => 1003, + ), + ); + + // Configure mock to return test data + $email_data->expects($this->any()) + ->method('get_attendees') + ->will($this->returnValue($mock_attendees)); + + // Use reflection to access the get_ticket_types method + $reflection = new ReflectionClass('HVAC_Email_Attendees_Data'); + $method = $reflection->getMethod('get_ticket_types'); + $method->setAccessible(true); + + // Test getting ticket types + $ticket_types = $method->invoke($email_data); + $this->assertIsArray($ticket_types); + $this->assertCount(2, $ticket_types); + $this->assertContains('General Admission', $ticket_types); + $this->assertContains('VIP Pass', $ticket_types); + } + + /** + * Test send_email method with mocked wp_mail + */ + public function test_send_email() { + global $wp_mail_called, $wp_mail_args; + + // Override wp_mail for testing + $wp_mail_called = 0; + $wp_mail_args = array(); + + function test_wp_mail($to, $subject, $message, $headers = '', $attachments = array()) { + global $wp_mail_called, $wp_mail_args; + $wp_mail_called++; + $wp_mail_args[] = array( + 'to' => $to, + 'subject' => $subject, + 'message' => $message, + 'headers' => $headers, + 'attachments' => $attachments, + ); + return true; + } + + // Replace WordPress's wp_mail with our test version + add_filter('wp_mail', 'test_wp_mail', 10, 5); + + // Create a test instance + $email_data = $this->getMockBuilder('HVAC_Email_Attendees_Data') + ->setConstructorArgs(array($this->event_id)) + ->setMethods(array('is_valid_event', 'user_can_email_attendees', 'get_attendees', 'get_event_details')) + ->getMock(); + + // Mock necessary methods + $email_data->expects($this->any()) + ->method('is_valid_event') + ->will($this->returnValue(true)); + + $email_data->expects($this->any()) + ->method('user_can_email_attendees') + ->will($this->returnValue(true)); + + $email_data->expects($this->any()) + ->method('get_event_details') + ->will($this->returnValue(array( + 'id' => $this->event_id, + 'title' => 'Test Event for Email Attendees', + 'start_date' => date('F j, Y'), + 'start_time' => '10:00 am', + 'end_date' => date('F j, Y'), + 'end_time' => '4:00 pm', + ))); + + $mock_attendees = array( + array( + 'name' => 'Test Attendee 1', + 'email' => 'attendee1@example.com', + 'ticket_name' => 'General Admission', + 'attendee_id' => 101, + 'order_id' => 1001, + ), + array( + 'name' => 'Test Attendee 2', + 'email' => 'attendee2@example.com', + 'ticket_name' => 'VIP Pass', + 'attendee_id' => 102, + 'order_id' => 1002, + ), + ); + + $email_data->expects($this->any()) + ->method('get_attendees') + ->will($this->returnValue($mock_attendees)); + + // Test sending email + $recipients = array('attendee1@example.com', 'attendee2@example.com'); + $subject = 'Test Email Subject'; + $message = 'Test email message body'; + $cc = 'cc@example.com'; + + $result = $email_data->send_email($recipients, $subject, $message, $cc); + + // Verify email was sent successfully + $this->assertTrue($result['success']); + $this->assertEquals(2, $wp_mail_called); // One email per recipient + + // Verify email content + $this->assertEquals('attendee1@example.com', $wp_mail_args[0]['to']); + $this->assertStringContains('[Test Event for Email Attendees]', $wp_mail_args[0]['subject']); + $this->assertStringContains('Test email message body', $wp_mail_args[0]['message']); + + // Verify CC header + foreach ($wp_mail_args as $args) { + $this->assertStringContains('Cc: cc@example.com', implode("\n", $args['headers'])); + } + + // Test with invalid parameters + $result = $email_data->send_email(array(), $subject, $message); + $this->assertFalse($result['success']); + + // Clean up + remove_filter('wp_mail', 'test_wp_mail'); + } +} \ No newline at end of file