Files
ladill-meet/tests/Feature/MeetWebTest.php
T
isaaccladandCursor 712dc487c6
Deploy Ladill Meet / deploy (push) Successful in 34s
Fix green room Go live and Panels hidden for conference hosts.
Stop registerPresenter from demoting hosts to panelists, recognize room owners as managers, and show stage controls in the green room banner.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 22:54:15 +00:00

1071 lines
37 KiB
PHP

<?php
namespace Tests\Feature;
use App\Http\Middleware\EnsurePlatformSession;
use App\Models\Member;
use App\Models\Organization;
use App\Models\Recording;
use App\Models\Room;
use App\Models\Session;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class MeetWebTest extends TestCase
{
use RefreshDatabase;
protected User $user;
protected Organization $organization;
protected function setUp(): void
{
parent::setUp();
$this->withoutMiddleware(EnsurePlatformSession::class);
$this->user = User::create([
'public_id' => 'test-user-001',
'name' => 'Test User',
'email' => 'test@example.com',
]);
$this->organization = Organization::create([
'owner_ref' => $this->user->public_id,
'name' => 'Test Org',
'slug' => 'test-org',
'timezone' => 'UTC',
'settings' => ['onboarded' => true],
]);
Member::create([
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'user_ref' => $this->user->public_id,
'role' => 'owner',
]);
$base = rtrim((string) config('billing.api_url'), '/');
Http::fake([
$base.'/can-afford*' => Http::response(['affordable' => true]),
$base.'/debit' => Http::response(['ok' => true]),
$base.'/balance*' => Http::response(['balance_minor' => 100000]),
]);
}
public function test_health_endpoint(): void
{
$this->getJson('/api/health')
->assertOk()
->assertJson(['status' => 'ok', 'app' => 'meet']);
}
public function test_guest_redirected_from_dashboard(): void
{
$this->get('/dashboard')->assertRedirect();
}
public function test_guest_root_uses_silent_sso_not_interactive_auth(): void
{
$response = $this->get('/');
$response->assertRedirect(route('sso.connect'));
$this->assertStringNotContainsString('interactive=1', (string) $response->headers->get('Location'));
}
public function test_sso_callback_sends_guest_without_session_to_marketing_page(): void
{
$this->get('/sso/callback?error=login_required')
->assertRedirect(config('ladill.marketing_url'));
}
public function test_onboarded_user_can_view_dashboard(): void
{
$this->actingAs($this->user)
->get('/dashboard')
->assertOk();
}
public function test_user_can_create_instant_meeting(): void
{
$this->actingAs($this->user)
->get('/meetings/instant')
->assertRedirect();
$this->assertDatabaseHas('meet_rooms', [
'host_user_ref' => $this->user->public_id,
'type' => 'instant',
]);
}
public function test_instant_meeting_opens_room(): void
{
$response = $this->actingAs($this->user)
->get('/meetings/instant');
$response->assertRedirect();
$session = Session::where('owner_ref', $this->user->public_id)->latest()->first();
$this->assertNotNull($session);
$this->assertTrue($session->isLive());
$this->actingAs($this->user)
->get('/room/'.$session->uuid)
->assertOk()
->assertSee($session->room->title);
}
public function test_host_can_start_scheduled_meeting_into_room(): void
{
$room = $this->createRoom();
$this->actingAs($this->user)
->post('/meetings/'.$room->uuid.'/start')
->assertRedirect();
$session = $room->fresh()->activeSession();
$this->assertNotNull($session);
$this->actingAs($this->user)
->get('/room/'.$session->uuid)
->assertOk();
}
public function test_guest_cannot_join_before_host_starts(): void
{
$room = $this->createRoom([
'settings' => array_merge(config('meet.default_settings'), [
'join_before_host' => false,
]),
]);
$this->post('/r/'.$room->uuid.'/enter', [
'display_name' => 'Guest User',
])
->assertRedirect()
->assertSessionHasErrors('join');
}
public function test_guest_can_join_live_meeting_and_enter_room(): void
{
$room = $this->createRoom([
'settings' => array_merge(config('meet.default_settings'), [
'join_before_host' => true,
'waiting_room' => false,
]),
]);
$session = Session::create([
'owner_ref' => $this->user->public_id,
'room_id' => $room->id,
'media_room_name' => 'room-'.$room->uuid,
'status' => 'live',
'started_at' => now(),
]);
$room->update(['status' => 'live']);
$this->post('/r/'.$room->uuid.'/enter', [
'display_name' => 'Guest User',
])
->assertRedirect(route('meet.room', $session));
$this->get('/room/'.$session->uuid)
->assertOk()
->assertSee('data-display-name="Guest User"', false);
}
public function test_anonymous_guest_does_not_take_over_host_participant(): void
{
$room = $this->createRoom([
'settings' => array_merge(config('meet.default_settings'), [
'join_before_host' => true,
'waiting_room' => false,
]),
]);
$session = Session::create([
'owner_ref' => $this->user->public_id,
'room_id' => $room->id,
'media_room_name' => 'room-'.$room->uuid,
'status' => 'live',
'started_at' => now(),
]);
$room->update(['status' => 'live']);
$hostParticipant = \App\Models\Participant::create([
'owner_ref' => $this->user->public_id,
'session_id' => $session->id,
'user_ref' => $this->user->public_id,
'display_name' => $this->user->name,
'role' => 'host',
'status' => 'joined',
'joined_at' => now(),
]);
$this->post('/r/'.$room->uuid.'/enter', [
'display_name' => 'Guest User',
])->assertRedirect(route('meet.room', $session));
$hostParticipant->refresh();
$this->assertSame('host', $hostParticipant->role);
$this->assertSame($this->user->name, $hostParticipant->display_name);
$guest = \App\Models\Participant::query()
->where('session_id', $session->id)
->where('display_name', 'Guest User')
->first();
$this->assertNotNull($guest);
$this->assertSame('guest', $guest->role);
$this->assertNotSame($hostParticipant->uuid, $guest->uuid);
}
public function test_guest_is_placed_in_waiting_room_when_enabled(): void
{
$room = $this->createRoom([
'settings' => array_merge(config('meet.default_settings'), [
'join_before_host' => true,
'waiting_room' => true,
]),
]);
$session = Session::create([
'owner_ref' => $this->user->public_id,
'room_id' => $room->id,
'media_room_name' => 'room-'.$room->uuid,
'status' => 'live',
'started_at' => now(),
]);
$room->update(['status' => 'live']);
$this->post('/r/'.$room->uuid.'/enter', [
'display_name' => 'Waiting Guest',
])
->assertRedirect(route('meet.join.waiting', $room));
$this->assertDatabaseHas('meet_participants', [
'session_id' => $session->id,
'display_name' => 'Waiting Guest',
'status' => 'waiting',
]);
}
public function test_leave_redirects_to_feedback_page(): void
{
$room = $this->createRoom(['settings' => array_merge(config('meet.default_settings'), ['waiting_room' => false])]);
$session = Session::create([
'owner_ref' => $this->user->public_id,
'room_id' => $room->id,
'media_room_name' => 'room-'.$room->uuid,
'status' => 'live',
'started_at' => now(),
]);
$participant = \App\Models\Participant::create([
'owner_ref' => $this->user->public_id,
'session_id' => $session->id,
'display_name' => 'Guest',
'role' => 'guest',
'status' => 'joined',
'joined_at' => now(),
]);
$this->withSession(["meet.participant.{$session->uuid}" => $participant->uuid])
->post('/room/'.$session->uuid.'/leave')
->assertRedirect(route('meet.left', $session));
}
public function test_participant_redirected_to_ended_page_when_meeting_ended(): void
{
$room = $this->createRoom(['settings' => array_merge(config('meet.default_settings'), ['waiting_room' => false])]);
$session = Session::create([
'owner_ref' => $this->user->public_id,
'room_id' => $room->id,
'media_room_name' => 'room-'.$room->uuid,
'status' => 'ended',
'started_at' => now()->subHour(),
'ended_at' => now(),
]);
$room->update(['status' => 'ended']);
$participant = \App\Models\Participant::create([
'owner_ref' => $this->user->public_id,
'session_id' => $session->id,
'display_name' => 'Guest',
'role' => 'guest',
'status' => 'left',
'joined_at' => now()->subHour(),
'left_at' => now(),
]);
$this->withSession(["meet.participant.{$session->uuid}" => $participant->uuid])
->get('/room/'.$session->uuid)
->assertRedirect(route('meet.ended', $session));
}
public function test_poll_returns_ended_payload_when_meeting_ended(): void
{
$room = $this->createRoom();
$session = Session::create([
'owner_ref' => $this->user->public_id,
'room_id' => $room->id,
'media_room_name' => 'room-'.$room->uuid,
'status' => 'ended',
'started_at' => now()->subHour(),
'ended_at' => now(),
]);
$participant = \App\Models\Participant::create([
'owner_ref' => $this->user->public_id,
'session_id' => $session->id,
'display_name' => 'Guest',
'role' => 'guest',
'status' => 'left',
'joined_at' => now()->subHour(),
'left_at' => now(),
]);
$this->withSession(["meet.participant.{$session->uuid}" => $participant->uuid])
->getJson('/room/'.$session->uuid.'/poll')
->assertOk()
->assertJson([
'ended' => true,
'redirect' => route('meet.ended', $session),
]);
}
public function test_waiting_guest_redirected_when_host_ends_meeting(): void
{
$room = $this->createRoom(['settings' => array_merge(config('meet.default_settings'), ['waiting_room' => true])]);
$session = Session::create([
'owner_ref' => $this->user->public_id,
'room_id' => $room->id,
'media_room_name' => 'room-'.$room->uuid,
'status' => 'ended',
'started_at' => now()->subHour(),
'ended_at' => now(),
]);
$room->update(['status' => 'ended']);
$participant = \App\Models\Participant::create([
'owner_ref' => $this->user->public_id,
'session_id' => $session->id,
'display_name' => 'Waiting Guest',
'role' => 'guest',
'status' => 'left',
'joined_at' => now()->subHour(),
'left_at' => now(),
]);
$this->withSession(["meet.participant.{$session->uuid}" => $participant->uuid])
->getJson('/r/'.$room->uuid.'/waiting/status')
->assertOk()
->assertJson([
'ended' => true,
'redirect' => route('meet.ended', $session),
]);
}
public function test_authenticated_user_can_join_live_meeting_without_display_name(): void
{
$room = $this->createRoom();
$session = Session::create([
'owner_ref' => $this->user->public_id,
'room_id' => $room->id,
'media_room_name' => 'room-'.$room->uuid,
'status' => 'live',
'started_at' => now(),
]);
$room->update(['status' => 'live']);
$this->actingAs($this->user)
->post('/r/'.$room->uuid.'/enter')
->assertRedirect(route('meet.room', $session));
$this->actingAs($this->user)
->get('/room/'.$session->uuid)
->assertOk();
}
public function test_room_has_join_url(): void
{
$room = Room::create([
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'host_user_ref' => $this->user->public_id,
'title' => 'Test Meeting',
'type' => 'instant',
'status' => 'scheduled',
'scheduled_at' => now(),
'timezone' => 'UTC',
'settings' => config('meet.default_settings'),
]);
$this->assertStringContainsString('meet.ladill.com/r/'.$room->uuid, $room->joinUrl());
}
public function test_user_can_view_recordings_index(): void
{
$room = $this->createRoom();
$session = Session::create([
'owner_ref' => $this->user->public_id,
'room_id' => $room->id,
'media_room_name' => 'room-'.$room->uuid,
'status' => 'ended',
'started_at' => now()->subHour(),
'ended_at' => now(),
]);
Recording::create([
'owner_ref' => $this->user->public_id,
'session_id' => $session->id,
'status' => 'ready',
'storage_path' => 'recordings/test.mp4',
]);
$this->actingAs($this->user)
->get('/recordings')
->assertOk()
->assertSee('Recordings');
}
public function test_admin_can_update_security_policy(): void
{
$this->actingAs($this->user)
->put('/settings/security', [
'org_only' => '1',
'authenticated_only' => '1',
'allowed_domains' => 'example.com, ladill.com',
'recording_host_only' => '1',
'session_idle_minutes' => 90,
])
->assertRedirect(route('meet.settings.security'));
$this->organization->refresh();
$this->assertTrue($this->organization->securitySetting('org_only'));
$this->assertContains('example.com', $this->organization->securitySetting('allowed_domains'));
}
public function test_user_can_access_personal_room(): void
{
$this->actingAs($this->user)
->get('/meetings/personal')
->assertRedirect();
$this->assertDatabaseHas('meet_rooms', [
'host_user_ref' => $this->user->public_id,
'type' => 'personal',
]);
}
public function test_admin_can_view_reports(): void
{
$this->actingAs($this->user)
->get('/reports')
->assertOk()
->assertSee('Reports');
}
public function test_service_api_can_create_room(): void
{
\Illuminate\Support\Facades\Http::fake();
config(['meet.service_api_keys.care' => 'test-care-key']);
$this->postJson('/api/service/v1/rooms', [
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'host_user_ref' => $this->user->public_id,
'title' => 'Care consultation',
'source' => ['app' => 'care', 'entity_type' => 'appointment', 'entity_id' => 'appt-123'],
], ['Authorization' => 'Bearer test-care-key'])
->assertCreated()
->assertJsonPath('room.title', 'Care consultation');
$this->assertDatabaseHas('meet_rooms', [
'title' => 'Care consultation',
'host_user_ref' => $this->user->public_id,
]);
}
public function test_service_api_resolves_organization_from_owner_ref(): void
{
\Illuminate\Support\Facades\Http::fake();
config(['meet.service_api_keys.crm' => 'test-crm-key']);
$this->postJson('/api/service/v1/rooms', [
'owner_ref' => $this->user->public_id,
'host_user_ref' => $this->user->public_id,
'title' => 'CRM follow-up',
'scheduled_at' => now()->addDay()->toIso8601String(),
'source' => ['app' => 'crm', 'entity_type' => 'deal', 'entity_id' => '42'],
], ['Authorization' => 'Bearer test-crm-key'])
->assertCreated()
->assertJsonPath('room.organization_id', $this->organization->id);
}
public function test_service_api_can_create_town_hall_room(): void
{
\Illuminate\Support\Facades\Http::fake();
config(['meet.service_api_keys.events' => 'test-events-key']);
$this->postJson('/api/service/v1/rooms', [
'owner_ref' => $this->user->public_id,
'host_user_ref' => $this->user->public_id,
'title' => 'Opening keynote',
'type' => 'town_hall',
'scheduled_at' => now()->addDay()->toIso8601String(),
'duration_minutes' => 90,
'source' => ['app' => 'events', 'entity_type' => 'virtual_session', 'entity_id' => 'vs-1'],
], ['Authorization' => 'Bearer test-events-key'])
->assertCreated()
->assertJsonPath('room.type', 'town_hall');
$this->assertDatabaseHas('meet_rooms', ['type' => 'town_hall', 'title' => 'Opening keynote']);
}
public function test_service_api_upserts_room_by_source_entity_id(): void
{
\Illuminate\Support\Facades\Http::fake();
config(['meet.service_api_keys.events' => 'test-events-key']);
$payload = [
'owner_ref' => $this->user->public_id,
'host_user_ref' => $this->user->public_id,
'title' => 'Session A',
'type' => 'webinar',
'scheduled_at' => now()->addDay()->toIso8601String(),
'source' => ['app' => 'events', 'entity_type' => 'virtual_session', 'entity_id' => 'vs-dup'],
];
$this->postJson('/api/service/v1/rooms', $payload, ['Authorization' => 'Bearer test-events-key'])
->assertCreated();
$payload['title'] = 'Session A (updated)';
$this->postJson('/api/service/v1/rooms', $payload, ['Authorization' => 'Bearer test-events-key'])
->assertOk()
->assertJsonPath('room.title', 'Session A (updated)');
$this->assertSame(1, \App\Models\Room::where('source->entity_id', 'vs-dup')->count());
}
public function test_service_api_lookup_by_source(): void
{
\Illuminate\Support\Facades\Http::fake();
config(['meet.service_api_keys.events' => 'test-events-key']);
$this->postJson('/api/service/v1/rooms', [
'owner_ref' => $this->user->public_id,
'host_user_ref' => $this->user->public_id,
'title' => 'Lookup test',
'type' => 'town_hall',
'scheduled_at' => now()->addDay()->toIso8601String(),
'source' => ['app' => 'events', 'entity_type' => 'virtual_session', 'entity_id' => 'vs-lookup'],
], ['Authorization' => 'Bearer test-events-key'])->assertCreated();
$this->getJson('/api/service/v1/rooms/by-source?source_app=events&entity_id=vs-lookup&owner_ref='.$this->user->public_id, [
'Authorization' => 'Bearer test-events-key',
])
->assertOk()
->assertJsonPath('room.title', 'Lookup test');
}
public function test_scheduled_room_syncs_to_ladill_mail_calendar(): void
{
\Illuminate\Support\Facades\Http::fake([
'mail.ladill.com/*' => \Illuminate\Support\Facades\Http::response(['data' => ['id' => 99]], 201),
]);
config([
'meet.calendar.mail.api_url' => 'https://mail.ladill.com/api/service/v1',
'meet.calendar.mail.api_key' => 'mail-test-key',
]);
$this->actingAs($this->user)
->post('/meetings', [
'title' => 'Calendar sync test',
'scheduled_at' => now()->addDay()->format('Y-m-d H:i'),
'duration_minutes' => 30,
])
->assertRedirect();
$room = \App\Models\Room::where('title', 'Calendar sync test')->first();
$this->assertNotNull($room);
$this->assertSame('99', (string) $room->calendar_event_id);
\Illuminate\Support\Facades\Http::assertSent(function ($request) {
return str_contains($request->url(), '/calendar/events')
&& $request['mailbox_email'] === strtolower($this->user->email);
});
}
public function test_invitation_rsvp_accepts(): void
{
$room = $this->createRoom();
$invitation = \App\Models\Invitation::create([
'owner_ref' => $this->user->public_id,
'room_id' => $room->id,
'email' => 'guest@example.com',
'status' => 'pending',
'rsvp_token' => 'test-rsvp-token',
]);
$this->post('/invite/'.$invitation->uuid.'/rsvp?token=test-rsvp-token', [
'status' => 'accepted',
])->assertOk();
$this->assertDatabaseHas('meet_invitations', [
'id' => $invitation->id,
'status' => 'accepted',
]);
}
public function test_admin_can_add_webhook_endpoint(): void
{
$this->actingAs($this->user)
->post('/settings/webhooks', [
'url' => 'https://care.example.com/webhooks/meet',
'secret' => 'whsec_test',
'events' => ['meeting.started', 'meeting.ended'],
])
->assertRedirect(route('meet.settings.webhooks'));
$this->assertDatabaseHas('meet_webhook_endpoints', [
'organization_id' => $this->organization->id,
'url' => 'https://care.example.com/webhooks/meet',
]);
}
public function test_webinar_registration_flow(): void
{
$room = $this->createRoom([
'type' => 'webinar',
'settings' => array_merge(config('meet.default_settings'), ['webinar_auto_approve' => true]),
]);
$this->post('/w/'.$room->uuid.'/register', [
'email' => 'attendee@example.com',
'display_name' => 'Attendee One',
])->assertOk();
$this->assertDatabaseHas('meet_webinar_registrations', [
'room_id' => $room->id,
'email' => 'attendee@example.com',
'status' => 'approved',
]);
}
public function test_host_can_create_webinar_room(): void
{
$this->actingAs($this->user)
->post('/webinars', [
'title' => 'Product launch',
'scheduled_at' => now()->addDay()->format('Y-m-d\TH:i'),
'duration_minutes' => 60,
])
->assertRedirect();
$this->assertDatabaseHas('meet_rooms', [
'title' => 'Product launch',
'type' => 'webinar',
]);
}
public function test_host_can_enable_panel_discussions_on_webinar(): void
{
$this->actingAs($this->user)
->post('/webinars', [
'title' => 'Panel webinar',
'scheduled_at' => now()->addDay()->format('Y-m-d\TH:i'),
'duration_minutes' => 60,
'panel_discussions' => '1',
])
->assertRedirect();
$room = Room::where('title', 'Panel webinar')->firstOrFail();
$this->assertTrue($room->setting('panel_discussions'));
$this->assertSame('plenary', $room->setting('stage_layout'));
$this->actingAs($this->user)
->post(route('meet.webinars.settings.update', $room), [
'panel_discussions' => '0',
])
->assertRedirect(route('meet.webinars.show', $room));
$this->assertFalse($room->fresh()->setting('panel_discussions'));
}
public function test_user_can_create_channel_and_post_message(): void
{
$this->actingAs($this->user)
->post('/channels', [
'name' => 'General',
'visibility' => 'public',
])
->assertRedirect();
$channel = \App\Models\Channel::first();
$this->assertNotNull($channel);
$this->actingAs($this->user)
->post('/channels/'.$channel->uuid.'/messages', ['body' => 'Hello team'])
->assertRedirect();
$this->assertDatabaseHas('meet_channel_messages', [
'channel_id' => $channel->id,
'body' => 'Hello team',
]);
}
public function test_admin_can_view_usage_dashboard(): void
{
$this->actingAs($this->user)
->get('/admin/usage')
->assertOk()
->assertSee('Usage');
}
public function test_mail_calendar_can_be_connected(): void
{
config(['meet.calendar.mail.api_key' => 'test-mail-key']);
$this->actingAs($this->user)
->post('/settings/calendar/mail')
->assertRedirect(route('meet.settings.calendar'));
$this->assertDatabaseHas('meet_calendar_connections', [
'user_ref' => $this->user->public_id,
'provider' => 'mail',
]);
}
public function test_host_can_create_conference(): void
{
$this->actingAs($this->user)
->post('/conferences', [
'title' => 'All hands',
'scheduled_at' => now()->addDay()->format('Y-m-d\TH:i'),
'duration_minutes' => 90,
])
->assertRedirect();
$this->assertDatabaseHas('meet_rooms', [
'title' => 'All hands',
'type' => 'town_hall',
]);
}
public function test_green_room_register_presenter_keeps_host_role(): void
{
$room = Room::create([
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'host_user_ref' => $this->user->public_id,
'title' => 'NextGen Summit',
'type' => 'town_hall',
'status' => 'live',
'scheduled_at' => now()->addHour(),
'timezone' => 'UTC',
'settings' => config('meet.default_settings'),
]);
$session = Session::create([
'owner_ref' => $this->user->public_id,
'room_id' => $room->id,
'media_room_name' => 'meet-'.$room->uuid.'-green',
'status' => 'live',
'session_mode' => 'green_room',
'started_at' => now(),
]);
$participant = \App\Models\Participant::create([
'owner_ref' => $this->user->public_id,
'session_id' => $session->id,
'user_ref' => $this->user->public_id,
'display_name' => $this->user->name,
'role' => 'host',
'status' => 'joined',
'joined_at' => now(),
]);
app(\App\Services\Meet\ConferenceService::class)->registerPresenter($session, $this->user, 'host');
$this->assertSame('host', $participant->fresh()->role);
}
public function test_host_can_end_conference_and_view_details(): void
{
$room = Room::create([
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'host_user_ref' => $this->user->public_id,
'title' => 'NextGen Summit',
'type' => 'town_hall',
'status' => 'live',
'scheduled_at' => now()->addHour(),
'timezone' => 'UTC',
'settings' => array_merge(config('meet.default_settings'), [
'green_room' => true,
'stage_mode' => true,
'presenter_refs' => [],
]),
]);
$session = Session::create([
'owner_ref' => $this->user->public_id,
'room_id' => $room->id,
'media_room_name' => 'meet-'.$room->uuid,
'status' => 'live',
'session_mode' => 'live',
'started_at' => now(),
]);
$participant = \App\Models\Participant::create([
'owner_ref' => $this->user->public_id,
'session_id' => $session->id,
'user_ref' => $this->user->public_id,
'display_name' => $this->user->name,
'role' => 'host',
'status' => 'joined',
'joined_at' => now(),
]);
$this->actingAs($this->user)
->withSession(["meet.participant.{$session->uuid}" => $participant->uuid])
->post('/room/'.$session->uuid.'/end')
->assertRedirect(route('meet.conferences.show', $room))
->assertSessionHas('success', 'Conference ended.');
$this->actingAs($this->user)
->get(route('meet.conferences.show', $room))
->assertOk()
->assertSee('NextGen Summit');
}
public function test_host_can_add_speakers_after_conference_created(): void
{
$speaker = User::create([
'public_id' => 'speaker-user-001',
'name' => 'Keynote Speaker',
'email' => 'speaker@example.com',
]);
Member::create([
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'user_ref' => $speaker->public_id,
'role' => 'member',
]);
$room = Room::create([
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'host_user_ref' => $this->user->public_id,
'title' => 'Product launch',
'type' => 'town_hall',
'status' => 'scheduled',
'scheduled_at' => now()->addDay(),
'timezone' => 'UTC',
'settings' => array_merge(config('meet.default_settings'), [
'green_room' => true,
'stage_mode' => true,
'presenter_refs' => [],
]),
]);
$this->actingAs($this->user)
->post(route('meet.conferences.speakers.update', $room), [
'presenter_refs' => [$speaker->public_id],
'invite_emails' => 'guest-speaker@example.com',
])
->assertRedirect(route('meet.conferences.show', $room))
->assertSessionHas('success');
$room->refresh();
$this->assertContains($speaker->public_id, $room->setting('presenter_refs'));
$this->assertDatabaseHas('meet_invitations', [
'room_id' => $room->id,
'email' => 'guest-speaker@example.com',
'role' => 'panelist',
]);
}
public function test_host_can_update_conference_stage_layout(): void
{
$room = Room::create([
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'host_user_ref' => $this->user->public_id,
'title' => 'NextGen Summit',
'type' => 'town_hall',
'status' => 'live',
'scheduled_at' => now()->addHour(),
'timezone' => 'UTC',
'settings' => array_merge(config('meet.default_settings'), [
'panel_discussions' => true,
'stage_layout' => 'plenary',
'panels' => [],
]),
]);
$session = Session::create([
'owner_ref' => $this->user->public_id,
'room_id' => $room->id,
'media_room_name' => 'meet-'.$room->uuid,
'status' => 'live',
'session_mode' => 'green_room',
'started_at' => now(),
]);
$participant = \App\Models\Participant::create([
'owner_ref' => $this->user->public_id,
'session_id' => $session->id,
'user_ref' => $this->user->public_id,
'display_name' => $this->user->name,
'role' => 'host',
'status' => 'joined',
'joined_at' => now(),
]);
$this->actingAs($this->user)
->withSession(["meet.participant.{$session->uuid}" => $participant->uuid])
->postJson('/room/'.$session->uuid.'/stage', [
'stage_layout' => 'panel',
'panels' => [
['name' => 'Opening panel', 'speaker_refs' => [$this->user->public_id]],
],
'active_panel_id' => null,
])
->assertOk()
->assertJson([
'stage_layout' => 'panel',
'panel_discussions' => true,
]);
$room->refresh();
$this->assertSame('panel', $room->setting('stage_layout'));
$this->assertCount(1, $room->setting('panels'));
}
public function test_poll_includes_stage_config_for_conference(): void
{
$room = Room::create([
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'host_user_ref' => $this->user->public_id,
'title' => 'NextGen Summit',
'type' => 'town_hall',
'status' => 'live',
'scheduled_at' => now()->addHour(),
'timezone' => 'UTC',
'settings' => array_merge(config('meet.default_settings'), [
'panel_discussions' => true,
'stage_layout' => 'plenary',
]),
]);
$session = Session::create([
'owner_ref' => $this->user->public_id,
'room_id' => $room->id,
'media_room_name' => 'meet-'.$room->uuid,
'status' => 'live',
'session_mode' => 'live',
'started_at' => now(),
]);
$participant = \App\Models\Participant::create([
'owner_ref' => $this->user->public_id,
'session_id' => $session->id,
'user_ref' => $this->user->public_id,
'display_name' => $this->user->name,
'role' => 'host',
'status' => 'joined',
'joined_at' => now(),
]);
$this->withSession(["meet.participant.{$session->uuid}" => $participant->uuid])
->getJson('/room/'.$session->uuid.'/poll')
->assertOk()
->assertJson([
'uses_stage_layout' => true,
'stage_layout' => 'plenary',
'panel_discussions' => true,
]);
}
public function test_regular_meeting_rejects_stage_update(): void
{
$room = $this->createRoom();
$session = Session::create([
'owner_ref' => $this->user->public_id,
'room_id' => $room->id,
'media_room_name' => 'meet-'.$room->uuid,
'status' => 'live',
'started_at' => now(),
]);
$participant = \App\Models\Participant::create([
'owner_ref' => $this->user->public_id,
'session_id' => $session->id,
'user_ref' => $this->user->public_id,
'display_name' => $this->user->name,
'role' => 'host',
'status' => 'joined',
'joined_at' => now(),
]);
$this->actingAs($this->user)
->withSession(["meet.participant.{$session->uuid}" => $participant->uuid])
->postJson('/room/'.$session->uuid.'/stage', [
'stage_layout' => 'panel',
])
->assertStatus(422);
}
public function test_host_can_create_space_room(): void
{
$this->actingAs($this->user)
->post('/rooms', [
'title' => 'Leadership space',
'invite_only' => true,
])
->assertRedirect();
$this->assertDatabaseHas('meet_rooms', [
'title' => 'Leadership space',
'type' => 'space',
]);
}
public function test_afia_chat_returns_reply_when_ai_configured(): void
{
config([
'afia.api_key' => 'test-openai-key',
'afia.platform_api_key' => '',
]);
Http::fake([
'api.openai.com/*' => Http::response([
'choices' => [['message' => ['content' => 'Go to Meetings and click Schedule meeting.']]],
]),
]);
$this->createRoom(['title' => 'Upcoming sync']);
$this->actingAs($this->user)
->postJson('/ai/chat', ['message' => 'How do I schedule a meeting?'])
->assertOk()
->assertJson(['reply' => 'Go to Meetings and click Schedule meeting.']);
}
protected function createRoom(array $overrides = []): Room
{
return Room::create(array_merge([
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'host_user_ref' => $this->user->public_id,
'title' => 'Test Meeting',
'type' => 'scheduled',
'status' => 'scheduled',
'scheduled_at' => now()->addDay(),
'timezone' => 'UTC',
'settings' => config('meet.default_settings'),
], $overrides));
}
}