Files
ladill-events/tests/Feature/EventSpeakerInviteTest.php
T
isaaccladandCursor 1746874aaa
Deploy Ladill Events / deploy (push) Successful in 49s
Use noreply@ladill.com for Events mail like other Ladill apps.
Stop overriding the From display name with the event title; rely on MAIL_FROM_ADDRESS and MAIL_FROM_NAME only.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 17:20:45 +00:00

302 lines
11 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\QrCode;
use App\Models\User;
use App\Services\Events\EventSpeakerInviteService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
class EventSpeakerInviteTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->withoutMiddleware(\App\Http\Middleware\EnsurePlatformSession::class);
config([
'mail.from.address' => 'noreply@ladill.com',
'mail.from.name' => 'Ladill',
'events.service_api_keys.meet' => 'test-meet-key',
]);
Mail::fake();
Http::fake([
config('billing.api_url').'/balance*' => Http::response(['balance_minor' => 50000]),
config('billing.api_url').'/can-afford*' => Http::response(['affordable' => true]),
config('billing.api_url').'/debit' => Http::response(['success' => true]),
]);
}
public function test_speaker_portal_page_renders_for_valid_token(): void
{
$owner = User::factory()->create(['public_id' => 'usr_sp_portal']);
$event = QrCode::create([
'user_id' => $owner->id,
'short_code' => 'evt-sp-portal',
'type' => QrCode::TYPE_EVENT,
'label' => 'Speaker portal event',
'payload' => [
'content' => [
'name' => 'Speaker portal event',
'format' => 'virtual',
'virtual_sessions' => [[
'id' => 'vs-1',
'title' => 'Opening keynote',
'meet_room_type' => 'webinar',
'join_url' => 'https://meet.test/r/room-uuid',
]],
'speakers' => [[
'name' => 'Jane Speaker',
'email' => 'jane@example.com',
'role' => 'Keynote',
'bio' => '',
'invite_token' => 'speaker-token-abc',
]],
],
'style' => [],
],
'is_active' => true,
]);
$this->get(route('qr.public.speaker.portal', ['shortCode' => $event->short_code, 'token' => 'speaker-token-abc']))
->assertOk()
->assertSee('Jane Speaker', false)
->assertSee('Opening keynote', false)
->assertSee('speaker-token-abc', false);
}
public function test_manual_speaker_invite_sends_email_and_records_metadata(): void
{
$owner = User::factory()->create(['public_id' => 'usr_sp_manual']);
$event = QrCode::create([
'user_id' => $owner->id,
'short_code' => 'evt-sp-manual',
'type' => QrCode::TYPE_EVENT,
'label' => 'Manual invite event',
'payload' => [
'content' => [
'name' => 'Manual invite event',
'speakers' => [[
'name' => 'Alex Host',
'email' => 'alex@example.com',
'role' => 'Panelist',
'bio' => '',
]],
],
'style' => [],
],
'is_active' => true,
]);
$this->actingAs($owner)
->from(route('speakers.show', $event))
->post(route('speakers.invite', $event), ['email' => 'alex@example.com'])
->assertRedirect(route('speakers.show', $event))
->assertSessionHas('success');
Http::assertSent(fn ($request) => str_contains($request->url(), '/debit'));
$event->refresh();
$speaker = $event->content()['speakers'][0];
$this->assertNotEmpty($speaker['invite_token']);
$this->assertNotEmpty($speaker['invited_at']);
$this->assertSame(EventSpeakerInviteService::SOURCE_MANUAL, $speaker['invite_source']);
}
public function test_programme_save_invites_assigned_speaker_hosts(): void
{
$owner = User::factory()->create(['public_id' => 'usr_sp_prog']);
$programme = QrCode::create([
'user_id' => $owner->id,
'short_code' => 'itn-sp-prog',
'type' => QrCode::TYPE_ITINERARY,
'label' => 'Summit programme',
'payload' => [
'content' => [
'title' => 'Summit programme',
'days' => [[
'label' => 'Day 1',
'date' => '2026-07-01',
'items' => [[
'ref' => 'day0-item0',
'time' => '09:00',
'title' => 'Opening',
'description' => '',
'location' => '',
'host' => 'Pat Speaker',
'host_speaker_email' => 'pat@example.com',
]],
]],
],
'style' => [],
],
'is_active' => true,
]);
$event = QrCode::create([
'user_id' => $owner->id,
'short_code' => 'evt-sp-prog',
'type' => QrCode::TYPE_EVENT,
'label' => 'Summit',
'payload' => [
'content' => [
'name' => 'Summit',
'programme_qr_id' => $programme->id,
'speakers' => [[
'name' => 'Pat Speaker',
'email' => 'pat@example.com',
'role' => 'Host',
'bio' => '',
]],
],
'style' => [],
],
'is_active' => true,
]);
app(EventSpeakerInviteService::class)->inviteProgrammeHosts($programme, $owner);
Http::assertSent(fn ($request) => str_contains($request->url(), '/debit'));
$event->refresh();
$speaker = $event->content()['speakers'][0];
$this->assertSame(EventSpeakerInviteService::SOURCE_PROGRAMME, $speaker['invite_source']);
}
public function test_verify_speaker_api_returns_speaker_details(): void
{
$owner = User::factory()->create(['public_id' => 'usr_sp_api']);
$event = QrCode::create([
'user_id' => $owner->id,
'short_code' => 'evt-sp-api',
'type' => QrCode::TYPE_EVENT,
'label' => 'API event',
'payload' => [
'content' => [
'name' => 'API event',
'speakers' => [[
'name' => 'Sam Panel',
'email' => 'sam@example.com',
'role' => 'Panelist',
'bio' => '',
'invite_token' => 'verify-token-xyz',
]],
],
'style' => [],
],
'is_active' => true,
]);
$this->withToken('test-meet-key')
->postJson('/api/service/v1/events/'.$event->id.'/verify-speaker', [
'owner_ref' => $owner->public_id,
'speaker_token' => 'verify-token-xyz',
])->assertOk()
->assertJsonPath('speaker.email', 'sam@example.com')
->assertJsonPath('speaker.name', 'Sam Panel');
}
public function test_speaker_roster_requires_email(): void
{
$owner = User::factory()->create(['public_id' => 'usr_sp_req_email']);
$event = QrCode::create([
'user_id' => $owner->id,
'short_code' => 'evt-sp-email-req',
'type' => QrCode::TYPE_EVENT,
'label' => 'Email required',
'payload' => ['content' => ['name' => 'Email required'], 'style' => []],
'is_active' => true,
]);
$this->actingAs($owner)
->from(route('speakers.show', $event))
->put(route('speakers.update', $event), [
'speakers' => [
['name' => 'No Email Person', 'email' => '', 'role' => '', 'bio' => ''],
],
])
->assertSessionHasErrors('speakers.0.email');
}
public function test_manual_speaker_invite_reports_when_email_not_configured(): void
{
config(['mail.from.address' => '']);
$owner = User::factory()->create(['public_id' => 'usr_sp_no_smtp']);
$event = QrCode::create([
'user_id' => $owner->id,
'short_code' => 'evt-sp-no-smtp',
'type' => QrCode::TYPE_EVENT,
'label' => 'No SMTP event',
'payload' => [
'content' => [
'name' => 'No SMTP event',
'speakers' => [[
'name' => 'Alex Host',
'email' => 'alex@example.com',
'role' => 'Panelist',
'bio' => '',
]],
],
'style' => [],
],
'is_active' => true,
]);
$this->actingAs($owner)
->from(route('speakers.show', $event))
->post(route('speakers.invite', $event), ['email' => 'alex@example.com'])
->assertRedirect(route('speakers.show', $event))
->assertSessionHas('error');
}
public function test_manual_speaker_invite_can_be_resent_after_programme_invite(): void
{
$owner = User::factory()->create(['public_id' => 'usr_sp_resend']);
$event = QrCode::create([
'user_id' => $owner->id,
'short_code' => 'evt-sp-resend',
'type' => QrCode::TYPE_EVENT,
'label' => 'Resend event',
'payload' => [
'content' => [
'name' => 'Resend event',
'speakers' => [[
'name' => 'Pat Speaker',
'email' => 'pat@example.com',
'role' => 'Host',
'bio' => '',
'invite_token' => 'existing-token',
'invited_at' => now()->toIso8601String(),
'invite_source' => EventSpeakerInviteService::SOURCE_PROGRAMME,
]],
],
'style' => [],
],
'is_active' => true,
]);
$this->actingAs($owner)
->from(route('speakers.show', $event))
->post(route('speakers.invite', $event), ['email' => 'pat@example.com'])
->assertRedirect(route('speakers.show', $event))
->assertSessionHas('success');
Http::assertSent(fn ($request) => str_contains($request->url(), '/debit'));
}
}