Files
ladill-events/tests/Feature/EventSpeakerInviteTest.php
T
isaaccladandCursor 6e42cd1cb2
Deploy Ladill Events / deploy (push) Successful in 28s
Add per-customer SMS and Bird Integrations for event messaging.
Attendee and speaker comms use encrypted tenant relay credentials and skip platform-key fallback and Bird wallet double-debit.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-12 17:05:54 +00:00

312 lines
11 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\MessagingCredential;
use App\Models\QrCode;
use App\Models\User;
use App\Services\Events\EventSpeakerInviteService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class EventSpeakerInviteTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->withoutMiddleware(\App\Http\Middleware\EnsurePlatformSession::class);
config([
'events.service_api_keys.meet' => 'test-meet-key',
]);
Http::fake([
'*/api/smtp/send' => Http::response(['success' => true, 'message_id' => 'x']),
]);
}
private function seedBirdCredentials(User $owner): void
{
$plain = 'lsk_live_'.str_repeat('f', 32);
MessagingCredential::create([
'owner_ref' => $owner->public_id,
'bird_api_key_encrypted' => Crypt::encryptString($plain),
'bird_api_key_prefix' => substr($plain, 0, 16),
'bird_from_email' => 'events@example.test',
'bird_from_name' => 'Events',
'bird_status' => MessagingCredential::STATUS_VALID,
'bird_validated_at' => now(),
]);
}
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']);
$this->seedBirdCredentials($owner);
$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(), '/api/smtp/send'));
$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']);
$this->seedBirdCredentials($owner);
$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(), '/api/smtp/send'));
$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
{
$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']);
$this->seedBirdCredentials($owner);
$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(), '/api/smtp/send'));
}
}