Deploy Ladill Events / deploy (push) Successful in 28s
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>
312 lines
11 KiB
PHP
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'));
|
|
}
|
|
}
|