Deploy Ladill Meet / deploy (push) Successful in 33s
Let attendees request host-granted microphone access with LiveKit reconnect, participant panel controls, and toolbar UI that only shows mic when allowed. Fix Events create URL, surface API key setup guidance, and align the Webinars sidebar icon with other nav items using currentColor strokes. Co-authored-by: Cursor <cursoragent@cursor.com>
1533 lines
54 KiB
PHP
1533 lines
54 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_redirects_without_events_link(): void
|
|
{
|
|
$room = $this->createRoom(['type' => 'webinar']);
|
|
|
|
$this->post('/w/'.$room->uuid.'/register', [
|
|
'email' => 'attendee@example.com',
|
|
'display_name' => 'Attendee One',
|
|
])->assertRedirect(route('meet.webinars.show', $room));
|
|
}
|
|
|
|
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_go_live_auto_joins_live_session(): 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,
|
|
]),
|
|
]);
|
|
|
|
$greenSession = 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',
|
|
'presenter_refs' => [$this->user->public_id],
|
|
'started_at' => now(),
|
|
]);
|
|
|
|
$hostParticipant = \App\Models\Participant::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'session_id' => $greenSession->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.{$greenSession->uuid}" => $hostParticipant->uuid])
|
|
->postJson(route('meet.room.townhall.live', $greenSession))
|
|
->assertOk()
|
|
->assertJson(['mode' => 'live']);
|
|
|
|
$liveSession = Session::query()
|
|
->where('room_id', $room->id)
|
|
->where('session_mode', 'live')
|
|
->first();
|
|
|
|
$this->assertNotNull($liveSession);
|
|
$this->assertSame('ended', $greenSession->fresh()->status);
|
|
|
|
$this->actingAs($this->user)
|
|
->get(route('meet.room', $liveSession))
|
|
->assertOk();
|
|
|
|
$this->assertDatabaseHas('meet_participants', [
|
|
'session_id' => $liveSession->id,
|
|
'user_ref' => $this->user->public_id,
|
|
'role' => 'host',
|
|
'status' => 'joined',
|
|
]);
|
|
}
|
|
|
|
public function test_green_room_poll_redirects_presenters_to_live_session(): void
|
|
{
|
|
$room = Room::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'organization_id' => $this->organization->id,
|
|
'host_user_ref' => $this->user->public_id,
|
|
'title' => 'Panel prep',
|
|
'type' => 'town_hall',
|
|
'status' => 'live',
|
|
'scheduled_at' => now()->addHour(),
|
|
'timezone' => 'UTC',
|
|
'settings' => config('meet.default_settings'),
|
|
]);
|
|
|
|
$greenSession = 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',
|
|
'presenter_refs' => [$this->user->public_id],
|
|
'started_at' => now(),
|
|
]);
|
|
|
|
$hostParticipant = \App\Models\Participant::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'session_id' => $greenSession->id,
|
|
'user_ref' => $this->user->public_id,
|
|
'display_name' => $this->user->name,
|
|
'role' => 'host',
|
|
'status' => 'joined',
|
|
'joined_at' => now(),
|
|
]);
|
|
|
|
app(\App\Services\Meet\TownHallService::class)->goLive($greenSession, $this->user);
|
|
|
|
$liveSession = Session::query()
|
|
->where('room_id', $room->id)
|
|
->where('session_mode', 'live')
|
|
->firstOrFail();
|
|
|
|
$this->actingAs($this->user)
|
|
->withSession(["meet.participant.{$greenSession->uuid}" => $hostParticipant->uuid])
|
|
->getJson(route('meet.room.poll', $greenSession))
|
|
->assertOk()
|
|
->assertJson([
|
|
'ended' => true,
|
|
'transition' => 'go_live',
|
|
'redirect' => route('meet.room', $liveSession),
|
|
]);
|
|
}
|
|
|
|
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_breakouts_exclude_host_and_enable_meeting_mode_for_attendees(): void
|
|
{
|
|
config([
|
|
'meet.media.livekit.api_key' => 'APIxxxxxxxxxxxxxxxx',
|
|
'meet.media.livekit.api_secret' => 'secretsecretsecretsecretsecret12',
|
|
]);
|
|
|
|
$room = Room::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'organization_id' => $this->organization->id,
|
|
'host_user_ref' => $this->user->public_id,
|
|
'title' => '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,
|
|
'status' => 'live',
|
|
'session_mode' => 'live',
|
|
'started_at' => now(),
|
|
]);
|
|
|
|
$host = \App\Models\Participant::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'session_id' => $session->id,
|
|
'user_ref' => $this->user->public_id,
|
|
'display_name' => 'Host',
|
|
'role' => 'host',
|
|
'status' => 'joined',
|
|
'joined_at' => now(),
|
|
]);
|
|
|
|
$attendee = \App\Models\Participant::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'session_id' => $session->id,
|
|
'display_name' => 'Attendee',
|
|
'role' => 'attendee',
|
|
'status' => 'joined',
|
|
'joined_at' => now(),
|
|
]);
|
|
|
|
$sessions = app(\App\Services\Meet\SessionService::class);
|
|
$this->assertFalse($sessions->canParticipantPublish($attendee));
|
|
|
|
$this->actingAs($this->user)
|
|
->withSession(["meet.participant.{$session->uuid}" => $host->uuid])
|
|
->postJson('/room/'.$session->uuid.'/breakouts', ['count' => 2])
|
|
->assertOk();
|
|
|
|
$host->refresh();
|
|
$attendee->refresh();
|
|
|
|
$this->assertNull($host->breakout_room_id);
|
|
$this->assertNotNull($attendee->breakout_room_id);
|
|
$this->assertTrue($sessions->canParticipantPublish($attendee));
|
|
|
|
$this->actingAs($this->user)
|
|
->withSession(["meet.participant.{$session->uuid}" => $attendee->uuid])
|
|
->getJson('/room/'.$session->uuid.'/media-token')
|
|
->assertOk()
|
|
->assertJsonPath('in_breakout', true)
|
|
->assertJsonPath('can_publish', true)
|
|
->assertJsonPath('uses_stage_layout', false);
|
|
}
|
|
|
|
public function test_live_room_shows_programme_when_linked_from_events(): void
|
|
{
|
|
config([
|
|
'meet.media.livekit.api_key' => 'APIxxxxxxxxxxxxxxxx',
|
|
'meet.media.livekit.api_secret' => 'secretsecretsecretsecretsecret12',
|
|
]);
|
|
|
|
$programme = [
|
|
'title' => 'Summit programme',
|
|
'days' => [
|
|
[
|
|
'label' => 'Day 1',
|
|
'items' => [
|
|
['time' => '09:00', 'title' => 'Opening keynote', 'host' => 'Alex'],
|
|
],
|
|
],
|
|
],
|
|
];
|
|
|
|
$room = Room::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'organization_id' => $this->organization->id,
|
|
'host_user_ref' => $this->user->public_id,
|
|
'title' => 'Summit',
|
|
'type' => 'town_hall',
|
|
'status' => 'live',
|
|
'scheduled_at' => now()->addHour(),
|
|
'timezone' => 'UTC',
|
|
'settings' => array_merge(config('meet.default_settings'), [
|
|
'programme_snapshot' => $programme,
|
|
'linked_programme_items' => [
|
|
['time' => '09:00', 'title' => 'Opening keynote', 'host' => 'Alex'],
|
|
],
|
|
]),
|
|
]);
|
|
|
|
$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])
|
|
->get('/room/'.$session->uuid)
|
|
->assertOk()
|
|
->assertSee('Opening keynote', false)
|
|
->assertSee('Programme', false)
|
|
->assertSee('meet-icons/programme.svg', false)
|
|
->assertSee('View the conference lineup from Events', false);
|
|
}
|
|
|
|
public function test_live_room_always_lists_programme_in_more_menu_without_linked_data(): void
|
|
{
|
|
config([
|
|
'meet.media.livekit.api_key' => 'APIxxxxxxxxxxxxxxxx',
|
|
'meet.media.livekit.api_secret' => 'secretsecretsecretsecretsecret12',
|
|
]);
|
|
|
|
$room = Room::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'organization_id' => $this->organization->id,
|
|
'host_user_ref' => $this->user->public_id,
|
|
'title' => '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,
|
|
'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])
|
|
->get('/room/'.$session->uuid)
|
|
->assertOk()
|
|
->assertSee('Programme', false)
|
|
->assertSee('meet-icons/programme.svg', false)
|
|
->assertSee('View the conference lineup from Events', false)
|
|
->assertSee('No programme linked to this conference yet.', false);
|
|
}
|
|
|
|
public function test_webinar_live_room_lists_programme_in_more_menu(): void
|
|
{
|
|
config([
|
|
'meet.media.livekit.api_key' => 'APIxxxxxxxxxxxxxxxx',
|
|
'meet.media.livekit.api_secret' => 'secretsecretsecretsecretsecret12',
|
|
]);
|
|
|
|
$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' => 'webinar',
|
|
'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,
|
|
'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])
|
|
->get('/room/'.$session->uuid)
|
|
->assertOk()
|
|
->assertSee('View the webinar lineup from Events', false)
|
|
->assertSee('data-session-noun="webinar"', false);
|
|
}
|
|
|
|
public function test_webinar_attendee_can_request_and_receive_speak_access(): void
|
|
{
|
|
config([
|
|
'meet.media.livekit.api_key' => 'APIxxxxxxxxxxxxxxxx',
|
|
'meet.media.livekit.api_secret' => 'secretsecretsecretsecretsecret12',
|
|
]);
|
|
|
|
$room = Room::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'organization_id' => $this->organization->id,
|
|
'host_user_ref' => $this->user->public_id,
|
|
'title' => 'Town hall',
|
|
'type' => 'webinar',
|
|
'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,
|
|
'status' => 'live',
|
|
'session_mode' => 'live',
|
|
'started_at' => now(),
|
|
]);
|
|
|
|
$host = \App\Models\Participant::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'session_id' => $session->id,
|
|
'user_ref' => $this->user->public_id,
|
|
'display_name' => 'Host',
|
|
'role' => 'host',
|
|
'status' => 'joined',
|
|
'joined_at' => now(),
|
|
]);
|
|
|
|
$attendee = \App\Models\Participant::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'session_id' => $session->id,
|
|
'display_name' => 'Attendee',
|
|
'role' => 'attendee',
|
|
'status' => 'joined',
|
|
'joined_at' => now(),
|
|
]);
|
|
|
|
$sessions = app(\App\Services\Meet\SessionService::class);
|
|
$this->assertFalse($sessions->canParticipantPublish($attendee));
|
|
|
|
$this->withSession(["meet.participant.{$session->uuid}" => $attendee->uuid])
|
|
->get('/room/'.$session->uuid)
|
|
->assertOk()
|
|
->assertSee('Request to speak', false)
|
|
->assertSee('meet-icons/speak.svg', false)
|
|
->assertSee('data-uses-speak-access="1"', false);
|
|
|
|
$this->withSession(["meet.participant.{$session->uuid}" => $attendee->uuid])
|
|
->postJson(route('meet.room.speak.request', $session))
|
|
->assertOk()
|
|
->assertJsonPath('participant.speak_requested', true);
|
|
|
|
$attendee->refresh();
|
|
$this->assertTrue($attendee->speak_requested);
|
|
$this->assertFalse($attendee->speak_granted);
|
|
|
|
$this->actingAs($this->user)
|
|
->withSession(["meet.participant.{$session->uuid}" => $host->uuid])
|
|
->postJson(route('meet.room.speak.grant', [$session, $attendee]))
|
|
->assertOk()
|
|
->assertJsonPath('participant.speak_granted', true)
|
|
->assertJsonPath('participant.speak_requested', false);
|
|
|
|
$attendee->refresh();
|
|
$this->assertTrue($attendee->speak_granted);
|
|
$this->assertTrue($sessions->canParticipantPublish($attendee));
|
|
|
|
$mediaState = $sessions->mediaSessionState($attendee);
|
|
$this->assertTrue($mediaState['can_publish']);
|
|
$this->assertTrue($mediaState['audio_only_publish']);
|
|
$this->assertTrue($mediaState['speak_granted']);
|
|
|
|
$this->withSession(["meet.participant.{$session->uuid}" => $attendee->uuid])
|
|
->get('/room/'.$session->uuid)
|
|
->assertOk()
|
|
->assertSee('You can speak', false)
|
|
->assertSee('data-speak-granted="1"', false);
|
|
|
|
$this->actingAs($this->user)
|
|
->withSession(["meet.participant.{$session->uuid}" => $host->uuid])
|
|
->postJson(route('meet.room.speak.revoke', [$session, $attendee]))
|
|
->assertOk()
|
|
->assertJsonPath('participant.speak_granted', false);
|
|
|
|
$attendee->refresh();
|
|
$this->assertFalse($sessions->canParticipantPublish($attendee));
|
|
}
|
|
|
|
public function test_webinar_show_events_create_link_uses_events_create_path(): void
|
|
{
|
|
config([
|
|
'meet.events_app_url' => 'https://events.test',
|
|
'meet.events_api_url' => 'https://events.test/api/service/v1',
|
|
'meet.events_api_key' => '',
|
|
]);
|
|
|
|
$room = Room::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'organization_id' => $this->organization->id,
|
|
'host_user_ref' => $this->user->public_id,
|
|
'title' => 'Sales webinar',
|
|
'type' => 'webinar',
|
|
'status' => 'ended',
|
|
'scheduled_at' => now()->subDay(),
|
|
'timezone' => 'UTC',
|
|
'settings' => config('meet.default_settings'),
|
|
]);
|
|
|
|
$this->actingAs($this->user)
|
|
->get(route('meet.webinars.show', $room))
|
|
->assertOk()
|
|
->assertSee('https://events.test/events/create', false)
|
|
->assertSee('Events linking is not configured on this Meet instance.', false)
|
|
->assertDontSee('/qr-codes/create', false);
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|