Files
ladill-meet/tests/Feature/MeetWebTest.php
T
isaaccladandCursor 7ae26a467c
Deploy Ladill Meet / deploy (push) Successful in 47s
Add kiosk join/leave pages, waiting room, and meeting feedback.
Redesign public join flows on the Frontdesk kiosk template, enforce host
admission when waiting room is enabled, and collect star ratings after leave.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 13:33:48 +00:00

538 lines
17 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 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',
]);
}
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_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_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_create_template(): void
{
$this->actingAs($this->user)
->post('/templates', [
'name' => 'Weekly standup',
'duration_minutes' => 30,
'waiting_room' => '1',
])
->assertRedirect(route('meet.templates.index'));
$this->assertDatabaseHas('meet_templates', [
'name' => 'Weekly standup',
'organization_id' => $this->organization->id,
]);
}
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
{
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_invitation_rsvp_accepts(): void
{
$room = $this->createRoom();
$invitation = \App\Models\Invitation::create([
'owner_ref' => $this->user->public_id,
'room_id' => $room->id,
'email' => 'guest@example.com',
'status' => 'pending',
'rsvp_token' => 'test-rsvp-token',
]);
$this->post('/invite/'.$invitation->uuid.'/rsvp?token=test-rsvp-token', [
'status' => 'accepted',
])->assertOk();
$this->assertDatabaseHas('meet_invitations', [
'id' => $invitation->id,
'status' => 'accepted',
]);
}
public function test_admin_can_add_webhook_endpoint(): void
{
$this->actingAs($this->user)
->post('/settings/webhooks', [
'url' => 'https://care.example.com/webhooks/meet',
'secret' => 'whsec_test',
'events' => ['meeting.started', 'meeting.ended'],
])
->assertRedirect(route('meet.settings.webhooks'));
$this->assertDatabaseHas('meet_webhook_endpoints', [
'organization_id' => $this->organization->id,
'url' => 'https://care.example.com/webhooks/meet',
]);
}
public function test_webinar_registration_flow(): void
{
$room = $this->createRoom([
'type' => 'webinar',
'settings' => array_merge(config('meet.default_settings'), ['webinar_auto_approve' => true]),
]);
$this->post('/w/'.$room->uuid.'/register', [
'email' => 'attendee@example.com',
'display_name' => 'Attendee One',
])->assertOk();
$this->assertDatabaseHas('meet_webinar_registrations', [
'room_id' => $room->id,
'email' => 'attendee@example.com',
'status' => 'approved',
]);
}
public function test_host_can_create_webinar_room(): void
{
$this->actingAs($this->user)
->post('/meetings', [
'title' => 'Product launch',
'type' => 'webinar',
'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_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_town_hall_room(): void
{
$this->actingAs($this->user)
->post('/meetings', [
'title' => 'All hands',
'type' => 'town_hall',
'scheduled_at' => now()->addDay()->format('Y-m-d\TH:i'),
'duration_minutes' => 90,
])
->assertRedirect();
$this->assertDatabaseHas('meet_rooms', [
'title' => 'All hands',
'type' => 'town_hall',
]);
}
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));
}
}