Deploy Ladill Meet / deploy (push) Successful in 1m23s
Co-authored-by: Cursor <cursoragent@cursor.com>
2117 lines
75 KiB
PHP
2117 lines
75 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_room_for_invoice_app(): void
|
|
{
|
|
\Illuminate\Support\Facades\Http::fake();
|
|
config(['meet.service_api_keys.invoice' => 'test-invoice-key']);
|
|
|
|
$this->postJson('/api/service/v1/rooms', [
|
|
'owner_ref' => $this->user->public_id,
|
|
'host_user_ref' => $this->user->public_id,
|
|
'title' => 'Invoice review',
|
|
'scheduled_at' => now()->addDay()->toIso8601String(),
|
|
'source' => ['app' => 'invoice', 'entity_type' => 'invoice', 'entity_id' => '99'],
|
|
], ['Authorization' => 'Bearer test-invoice-key'])
|
|
->assertCreated()
|
|
->assertJsonPath('room.title', 'Invoice review');
|
|
}
|
|
|
|
public function test_service_api_can_create_room_for_sales_centre_app(): void
|
|
{
|
|
\Illuminate\Support\Facades\Http::fake();
|
|
config(['meet.service_api_keys.sales_centre' => 'test-sales-centre-key']);
|
|
|
|
$this->postJson('/api/service/v1/rooms', [
|
|
'owner_ref' => $this->user->public_id,
|
|
'host_user_ref' => $this->user->public_id,
|
|
'title' => 'Ladill demo with Prospect',
|
|
'type' => 'instant',
|
|
'source' => ['app' => 'sales_centre', 'entity_type' => 'sales_lead', 'entity_id' => '42'],
|
|
'invite_emails' => ['prospect@demo.test'],
|
|
], ['Authorization' => 'Bearer test-sales-centre-key'])
|
|
->assertCreated()
|
|
->assertJsonPath('room.title', 'Ladill demo with Prospect');
|
|
}
|
|
|
|
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_upserts_existing_room_by_meet_room_uuid(): void
|
|
{
|
|
\Illuminate\Support\Facades\Http::fake();
|
|
config(['meet.service_api_keys.events' => 'test-events-key']);
|
|
|
|
$room = \App\Models\Room::create([
|
|
'uuid' => 'prelinked-room-uuid',
|
|
'owner_ref' => $this->user->public_id,
|
|
'organization_id' => $this->organization->id,
|
|
'host_user_ref' => $this->user->public_id,
|
|
'title' => 'Prelinked town hall',
|
|
'type' => 'town_hall',
|
|
'status' => 'scheduled',
|
|
'scheduled_at' => now()->addDay(),
|
|
'timezone' => 'UTC',
|
|
'settings' => config('meet.default_settings'),
|
|
]);
|
|
|
|
$programme = [
|
|
'title' => 'Summit programme',
|
|
'days' => [
|
|
[
|
|
'label' => 'Day 1',
|
|
'items' => [
|
|
['time' => '09:00', 'title' => 'Opening keynote', 'host' => 'Alex'],
|
|
],
|
|
],
|
|
],
|
|
];
|
|
|
|
$this->postJson('/api/service/v1/rooms', [
|
|
'owner_ref' => $this->user->public_id,
|
|
'host_user_ref' => $this->user->public_id,
|
|
'meet_room_uuid' => $room->uuid,
|
|
'title' => 'Opening keynote',
|
|
'type' => 'town_hall',
|
|
'scheduled_at' => now()->addDay()->toIso8601String(),
|
|
'settings' => [
|
|
'programme_snapshot' => $programme,
|
|
'linked_programme_items' => [],
|
|
],
|
|
'source' => [
|
|
'app' => 'events',
|
|
'entity_type' => 'virtual_session',
|
|
'entity_id' => 'vs-prelinked',
|
|
'event_id' => '42',
|
|
],
|
|
], ['Authorization' => 'Bearer test-events-key'])
|
|
->assertOk()
|
|
->assertJsonPath('room.uuid', $room->uuid)
|
|
->assertJsonPath('room.settings.programme_snapshot.title', 'Summit programme');
|
|
|
|
$this->assertSame(1, \App\Models\Room::count());
|
|
$room->refresh();
|
|
$this->assertSame('Summit programme', $room->setting('programme_snapshot')['title'] ?? null);
|
|
$this->assertSame('vs-prelinked', $room->source['entity_id'] ?? null);
|
|
}
|
|
|
|
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_scheduled_room_uses_connected_mail_mailbox(): void
|
|
{
|
|
\Illuminate\Support\Facades\Http::fake([
|
|
'mail.ladill.com/*' => \Illuminate\Support\Facades\Http::response(['data' => ['id' => 42]], 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('/settings/calendar/mail', ['mailbox_email' => 'host@customdomain.com'])
|
|
->assertRedirect(route('meet.settings.calendar'));
|
|
|
|
$this->actingAs($this->user)
|
|
->post('/meetings', [
|
|
'title' => 'Custom mailbox sync',
|
|
'scheduled_at' => now()->addDay()->format('Y-m-d H:i'),
|
|
'duration_minutes' => 30,
|
|
])
|
|
->assertRedirect();
|
|
|
|
\Illuminate\Support\Facades\Http::assertSent(function ($request) {
|
|
return str_contains($request->url(), '/calendar/events')
|
|
&& $request['mailbox_email'] === 'host@customdomain.com';
|
|
});
|
|
}
|
|
|
|
public function test_events_sourced_room_does_not_sync_to_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',
|
|
]);
|
|
|
|
$room = $this->createRoom([
|
|
'scheduled_at' => now()->addDay(),
|
|
'source' => [
|
|
'app' => 'events',
|
|
'entity_type' => 'virtual_session',
|
|
'entity_id' => 'vs-1',
|
|
'event_id' => '42',
|
|
],
|
|
]);
|
|
|
|
app(\App\Services\Meet\CalendarService::class)->syncRoomCreated($room, $this->user);
|
|
|
|
\Illuminate\Support\Facades\Http::assertNothingSent();
|
|
$this->assertNull($room->fresh()->calendar_event_id);
|
|
}
|
|
|
|
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_host_can_spotlight_guest_panelist_by_participant_uuid(): void
|
|
{
|
|
$room = Room::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'organization_id' => $this->organization->id,
|
|
'host_user_ref' => $this->user->public_id,
|
|
'title' => 'Summit keynote',
|
|
'type' => 'webinar',
|
|
'status' => 'live',
|
|
'scheduled_at' => now()->addHour(),
|
|
'timezone' => 'UTC',
|
|
'settings' => array_merge(config('meet.default_settings'), [
|
|
'stage_layout' => 'plenary',
|
|
]),
|
|
]);
|
|
|
|
$session = Session::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'room_id' => $room->id,
|
|
'media_room_name' => 'meet-'.$room->uuid,
|
|
'status' => 'live',
|
|
'started_at' => now(),
|
|
]);
|
|
|
|
$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(),
|
|
]);
|
|
|
|
$guestSpeaker = \App\Models\Participant::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'session_id' => $session->id,
|
|
'display_name' => 'Guest Speaker',
|
|
'email' => 'speaker@example.com',
|
|
'role' => 'panelist',
|
|
'status' => 'joined',
|
|
'joined_at' => now(),
|
|
]);
|
|
|
|
$this->actingAs($this->user)
|
|
->withSession(["meet.participant.{$session->uuid}" => $hostParticipant->uuid])
|
|
->postJson('/room/'.$session->uuid.'/stage', [
|
|
'stage_layout' => 'plenary',
|
|
'spotlight_speaker_ref' => $guestSpeaker->uuid,
|
|
])
|
|
->assertOk()
|
|
->assertJson([
|
|
'stage_layout' => 'plenary',
|
|
'spotlight_speaker_ref' => $guestSpeaker->uuid,
|
|
]);
|
|
|
|
$room->refresh();
|
|
$this->assertSame($guestSpeaker->uuid, $room->setting('spotlight_speaker_ref'));
|
|
}
|
|
|
|
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',
|
|
])
|
|
->assertRedirect();
|
|
|
|
$room = Room::where('title', 'Leadership space')->firstOrFail();
|
|
|
|
$this->assertDatabaseHas('meet_rooms', [
|
|
'title' => 'Leadership space',
|
|
'type' => 'space',
|
|
]);
|
|
$this->assertFalse($room->setting('invite_only'));
|
|
}
|
|
|
|
public function test_space_create_sends_listener_email_invitations(): void
|
|
{
|
|
$this->actingAs($this->user)
|
|
->post('/rooms', [
|
|
'title' => 'Listener room',
|
|
'invite_emails' => 'listener@example.com, bad-email',
|
|
'invite_only' => true,
|
|
])
|
|
->assertSessionHasErrors('invite_emails');
|
|
|
|
$this->actingAs($this->user)
|
|
->post('/rooms', [
|
|
'title' => 'Listener room',
|
|
'invite_emails' => 'listener@example.com, guest@example.com',
|
|
'invite_only' => true,
|
|
])
|
|
->assertRedirect();
|
|
|
|
$room = Room::where('title', 'Listener room')->firstOrFail();
|
|
|
|
$this->assertDatabaseHas('meet_invitations', [
|
|
'room_id' => $room->id,
|
|
'email' => 'listener@example.com',
|
|
'role' => 'attendee',
|
|
'status' => 'pending',
|
|
]);
|
|
|
|
$this->assertDatabaseHas('meet_invitations', [
|
|
'room_id' => $room->id,
|
|
'email' => 'guest@example.com',
|
|
'role' => 'attendee',
|
|
]);
|
|
}
|
|
|
|
public function test_private_space_denies_uninvited_join(): void
|
|
{
|
|
$room = Room::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'organization_id' => $this->organization->id,
|
|
'host_user_ref' => $this->user->public_id,
|
|
'title' => 'Private space',
|
|
'type' => 'space',
|
|
'status' => 'scheduled',
|
|
'scheduled_at' => now()->addHour(),
|
|
'timezone' => 'UTC',
|
|
'settings' => array_merge(config('meet.default_settings'), [
|
|
'audio_only' => true,
|
|
'invite_only' => true,
|
|
]),
|
|
]);
|
|
|
|
$stranger = User::create([
|
|
'public_id' => 'stranger-001',
|
|
'name' => 'Stranger',
|
|
'email' => 'stranger@example.com',
|
|
]);
|
|
|
|
$this->actingAs($stranger)
|
|
->get('/r/'.$room->uuid)
|
|
->assertOk()
|
|
->assertSee('This room is private');
|
|
}
|
|
|
|
public function test_private_space_allows_invited_email_to_join(): void
|
|
{
|
|
$room = Room::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'organization_id' => $this->organization->id,
|
|
'host_user_ref' => $this->user->public_id,
|
|
'title' => 'Invited space',
|
|
'type' => 'space',
|
|
'status' => 'scheduled',
|
|
'scheduled_at' => now()->addHour(),
|
|
'timezone' => 'UTC',
|
|
'settings' => array_merge(config('meet.default_settings'), [
|
|
'audio_only' => true,
|
|
'invite_only' => true,
|
|
]),
|
|
]);
|
|
|
|
\App\Models\Invitation::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'room_id' => $room->id,
|
|
'email' => 'invited@example.com',
|
|
'role' => 'attendee',
|
|
'status' => 'pending',
|
|
'rsvp_token' => 'test-token',
|
|
]);
|
|
|
|
$invited = User::create([
|
|
'public_id' => 'invited-001',
|
|
'name' => 'Invited Listener',
|
|
'email' => 'invited@example.com',
|
|
]);
|
|
|
|
$this->actingAs($invited)
|
|
->get('/r/'.$room->uuid)
|
|
->assertOk()
|
|
->assertDontSee('This room is private');
|
|
}
|
|
|
|
public function test_space_host_cannot_create_breakouts(): void
|
|
{
|
|
$room = Room::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'organization_id' => $this->organization->id,
|
|
'host_user_ref' => $this->user->public_id,
|
|
'title' => 'Leadership space',
|
|
'type' => 'space',
|
|
'status' => 'live',
|
|
'scheduled_at' => now()->addHour(),
|
|
'timezone' => 'UTC',
|
|
'settings' => array_merge(config('meet.default_settings'), [
|
|
'audio_only' => true,
|
|
]),
|
|
]);
|
|
|
|
$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(),
|
|
]);
|
|
|
|
$this->actingAs($this->user)
|
|
->withSession(["meet.participant.{$session->uuid}" => $host->uuid])
|
|
->postJson('/room/'.$session->uuid.'/breakouts', ['count' => 2])
|
|
->assertStatus(422);
|
|
}
|
|
|
|
public function test_space_live_room_view_omits_meeting_only_features(): 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' => 'Leadership space',
|
|
'type' => 'space',
|
|
'status' => 'live',
|
|
'scheduled_at' => now()->addHour(),
|
|
'timezone' => 'UTC',
|
|
'settings' => array_merge(config('meet.default_settings'), [
|
|
'audio_only' => true,
|
|
]),
|
|
]);
|
|
|
|
$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(),
|
|
]);
|
|
|
|
$response = $this->actingAs($this->user)
|
|
->withSession(["meet.participant.{$session->uuid}" => $host->uuid])
|
|
->get('/room/'.$session->uuid);
|
|
|
|
$response->assertOk();
|
|
$response->assertSee('data-is-space="1"', false);
|
|
$response->assertSee('title="Chat"', false);
|
|
$response->assertDontSee('title="People"', false);
|
|
$response->assertDontSee('Start breakouts', false);
|
|
$response->assertDontSee('Share files', false);
|
|
$response->assertDontSee('View the room lineup from Events', false);
|
|
}
|
|
|
|
public function test_space_host_can_invite_listener_to_speak_and_listener_accepts(): void
|
|
{
|
|
$room = Room::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'organization_id' => $this->organization->id,
|
|
'host_user_ref' => $this->user->public_id,
|
|
'title' => 'Leadership space',
|
|
'type' => 'space',
|
|
'status' => 'live',
|
|
'scheduled_at' => now()->addHour(),
|
|
'timezone' => 'UTC',
|
|
'settings' => array_merge(config('meet.default_settings'), [
|
|
'audio_only' => true,
|
|
'speaker_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(),
|
|
]);
|
|
|
|
$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(),
|
|
]);
|
|
|
|
$listener = \App\Models\Participant::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'session_id' => $session->id,
|
|
'user_ref' => 'listener-ref',
|
|
'display_name' => 'Listener',
|
|
'role' => 'attendee',
|
|
'status' => 'joined',
|
|
'hand_raised' => true,
|
|
'joined_at' => now(),
|
|
]);
|
|
|
|
$this->actingAs($this->user)
|
|
->withSession(["meet.participant.{$session->uuid}" => $host->uuid])
|
|
->postJson('/room/'.$session->uuid.'/participants/'.$listener->uuid.'/space/speaker')
|
|
->assertOk()
|
|
->assertJsonPath('participant.role', 'attendee')
|
|
->assertJsonPath('participant.speak_requested', true);
|
|
|
|
$listener->refresh();
|
|
$this->assertSame('attendee', $listener->role);
|
|
$this->assertTrue($listener->speak_requested);
|
|
$this->assertFalse($listener->hand_raised);
|
|
|
|
$this->withSession(["meet.participant.{$session->uuid}" => $listener->uuid])
|
|
->postJson('/room/'.$session->uuid.'/space/speaker/accept')
|
|
->assertOk()
|
|
->assertJsonPath('participant.role', 'panelist')
|
|
->assertJsonPath('participant.speak_requested', false);
|
|
|
|
$listener->refresh();
|
|
$room->refresh();
|
|
|
|
$this->assertSame('panelist', $listener->role);
|
|
$this->assertFalse($listener->speak_requested);
|
|
$this->assertContains('listener-ref', (array) $room->setting('speaker_refs', []));
|
|
}
|
|
|
|
public function test_space_host_can_make_speaker_co_host_without_acceptance(): void
|
|
{
|
|
$room = Room::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'organization_id' => $this->organization->id,
|
|
'host_user_ref' => $this->user->public_id,
|
|
'title' => 'Leadership space',
|
|
'type' => 'space',
|
|
'status' => 'live',
|
|
'scheduled_at' => now()->addHour(),
|
|
'timezone' => 'UTC',
|
|
'settings' => array_merge(config('meet.default_settings'), [
|
|
'audio_only' => true,
|
|
'speaker_refs' => ['speaker-ref'],
|
|
]),
|
|
]);
|
|
|
|
$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(),
|
|
]);
|
|
|
|
$speaker = \App\Models\Participant::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'session_id' => $session->id,
|
|
'user_ref' => 'speaker-ref',
|
|
'display_name' => 'Speaker',
|
|
'role' => 'panelist',
|
|
'status' => 'joined',
|
|
'joined_at' => now(),
|
|
]);
|
|
|
|
$this->actingAs($this->user)
|
|
->withSession(["meet.participant.{$session->uuid}" => $host->uuid])
|
|
->postJson('/room/'.$session->uuid.'/participants/'.$speaker->uuid.'/space/co-host')
|
|
->assertOk()
|
|
->assertJsonPath('participant.role', 'co_host');
|
|
|
|
$speaker->refresh();
|
|
$this->assertSame('co_host', $speaker->role);
|
|
}
|
|
|
|
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_conference_events_link_uses_app_modals_not_native_confirm(): void
|
|
{
|
|
config([
|
|
'meet.events_api_url' => 'https://events.test/api/service/v1',
|
|
'meet.events_api_key' => 'test-events-key',
|
|
]);
|
|
|
|
Http::fake([
|
|
'https://events.test/api/service/v1/events*' => Http::response(['events' => []]),
|
|
]);
|
|
|
|
$room = Room::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'organization_id' => $this->organization->id,
|
|
'host_user_ref' => $this->user->public_id,
|
|
'title' => 'Leadership conference',
|
|
'type' => 'town_hall',
|
|
'status' => 'scheduled',
|
|
'scheduled_at' => now()->addDay(),
|
|
'timezone' => 'UTC',
|
|
'settings' => config('meet.default_settings'),
|
|
'source' => [
|
|
'app' => 'events',
|
|
'entity_type' => 'virtual_session',
|
|
'entity_id' => 'vs-1',
|
|
'event_id' => '42',
|
|
'event_title' => 'Summit',
|
|
],
|
|
]);
|
|
|
|
$this->actingAs($this->user)
|
|
->get(route('meet.conferences.show', $room))
|
|
->assertOk()
|
|
->assertSee('Remove Events link?', false)
|
|
->assertSee('data-modal-name="events-unlink-'.$room->uuid.'"', false)
|
|
->assertDontSee("confirm('Remove the Events link?", false);
|
|
|
|
$room->update(['source' => null]);
|
|
|
|
$this->actingAs($this->user)
|
|
->get(route('meet.conferences.show', $room))
|
|
->assertOk()
|
|
->assertSee('open-modal', false)
|
|
->assertSee('data-modal-name="events-link-'.$room->uuid.'"', false)
|
|
->assertSee('Link to Ladill Events', false);
|
|
|
|
$this->actingAs($this->user)
|
|
->get(route('meet.events.link', $room))
|
|
->assertRedirect(route('meet.conferences.show', $room));
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|