sls/tests/Feature/ClientCallFeatureTest.php
2026-08-25 19:56:46 +05:30

1010 lines
38 KiB
PHP

<?php
namespace Tests\Feature;
use App\Mail\ClientCallMomMail;
use App\Mail\ClientMeetingInviteMail;
use App\Models\Client;
use App\Models\ClientCallNote;
use App\Models\ClientMeeting;
use App\Models\Role;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
class ClientCallFeatureTest extends TestCase
{
use RefreshDatabase;
protected User $admin;
protected User $pm;
protected User $developer;
protected Role $pmRole;
protected Role $devRole;
protected function setUp(): void
{
parent::setUp();
// Create roles
$this->pmRole = Role::create(['name' => 'Project Manager', 'description' => 'PM']);
$this->devRole = Role::create(['name' => 'Developer', 'description' => 'Dev']);
// Create users
$this->admin = User::create([
'name' => 'System Admin',
'email' => 'admin@company.com',
'role' => 'admin',
]);
$this->pm = User::create([
'name' => 'John PM',
'email' => 'john.pm@company.com',
'role' => 'user',
]);
$this->pm->roles()->attach($this->pmRole->id);
$this->developer = User::create([
'name' => 'Alex Dev',
'email' => 'alex.dev@company.com',
'role' => 'user',
]);
$this->developer->roles()->attach($this->devRole->id);
}
public function test_pm_and_admin_can_access_client_calls_and_create_client(): void
{
$this->actingAs($this->pm)
->get(route('client-calls.index'))
->assertStatus(200)
->assertSee('Client Projects', false);
$response = $this->actingAs($this->pm)
->post(route('client-calls.store'), [
'client_name' => 'Alice Walker',
'client_email' => 'alice@innovate.io',
'client_phone' => '+1 555 456 7890',
'company_name' => 'Innovate Tech',
'project_name' => 'AI Analytics Dashboard',
'project_details' => 'Full stack dashboard with live WebRTC meeting rooms and AI transcription.',
'responsible_user_id' => $this->pm->id,
'assigned_team_members' => [$this->pm->id, $this->developer->id],
]);
$response->assertRedirect(route('client-calls.index'));
$this->assertDatabaseHas('clients', [
'client_name' => 'Alice Walker',
'client_email' => 'alice@innovate.io',
'project_name' => 'AI Analytics Dashboard',
'responsible_user_id' => $this->pm->id,
]);
}
public function test_client_hub_page_shows_details_and_opens(): void
{
$client = Client::create([
'client_name' => 'Bob Smith',
'client_email' => 'bob@smithco.com',
'project_name' => 'Cloud Migration',
'responsible_user_id' => $this->pm->id,
'created_by' => $this->admin->id,
'status' => 'active',
'latest_sentiment' => 'happy',
]);
$this->actingAs($this->pm)
->get(route('client-calls.show', $client->id))
->assertStatus(200)
->assertSee('Cloud Migration')
->assertSee('Bob Smith')
->assertSee('Start Instant Meeting')
->assertSee('Schedule Meeting');
}
public function test_scheduling_future_meeting_sends_email_invites(): void
{
Mail::fake();
$client = Client::create([
'client_name' => 'Bob Smith',
'client_email' => 'bob@smithco.com',
'project_name' => 'Cloud Migration',
'responsible_user_id' => $this->pm->id,
'created_by' => $this->admin->id,
'status' => 'active',
]);
$response = $this->actingAs($this->pm)
->post(route('client-calls.schedule-meeting', $client->id), [
'title' => 'Sprint 1 Architecture Review',
'agenda' => 'Review AWS diagrams and API contracts.',
'scheduled_at' => now()->addDays(2)->format('Y-m-d H:i:s'),
'duration_minutes' => 45,
'client_emails' => 'bob@smithco.com, ctobob@smithco.com',
'team_attendees' => [$this->pm->id, $this->developer->id],
]);
$response->assertRedirect(route('client-calls.show', $client->id));
$this->assertDatabaseHas('client_meetings', [
'client_id' => $client->id,
'title' => 'Sprint 1 Architecture Review',
'meeting_type' => 'scheduled',
]);
Mail::assertSent(ClientMeetingInviteMail::class);
}
public function test_starting_instant_meeting_generates_shareable_link(): void
{
$client = Client::create([
'client_name' => 'Bob Smith',
'client_email' => 'bob@smithco.com',
'project_name' => 'Cloud Migration',
'responsible_user_id' => $this->pm->id,
'created_by' => $this->admin->id,
'status' => 'active',
]);
$response = $this->actingAs($this->pm)
->postJson(route('client-calls.instant-meeting', $client->id), [
'title' => 'Quick Alignment Sync',
]);
$response->assertStatus(200)
->assertJsonStructure(['success', 'meeting_id', 'meeting_code', 'join_url']);
$this->assertDatabaseHas('client_meetings', [
'client_id' => $client->id,
'meeting_type' => 'instant',
'status' => 'in_progress',
]);
}
public function test_external_guest_can_join_via_meeting_link(): void
{
$client = Client::create([
'client_name' => 'Bob Smith',
'client_email' => 'bob@smithco.com',
'project_name' => 'Cloud Migration',
'responsible_user_id' => $this->pm->id,
'created_by' => $this->admin->id,
'status' => 'active',
]);
$meeting = ClientMeeting::create([
'client_id' => $client->id,
'title' => 'Instant Sync',
'meeting_code' => 'meet-test-guest-123',
'meeting_type' => 'instant',
'host_user_id' => $this->pm->id,
'status' => 'in_progress',
]);
// Unauthenticated guest arrives at room -> gets guest entry screen
$this->get(route('client-calls.room', $meeting->meeting_code))
->assertStatus(200)
->assertSee("What's your name?", false);
// Guest submits name
$this->post(route('client-calls.join-guest', $meeting->meeting_code), [
'guest_name' => 'Bob Smith (Client)',
'guest_email' => 'bob@smithco.com',
])->assertRedirect(route('client-calls.room', $meeting->meeting_code));
// Now has session and enters room
$this->withSession(["guest_name_{$meeting->meeting_code}" => 'Bob Smith (Client)'])
->get(route('client-calls.room', $meeting->meeting_code))
->assertStatus(200)
->assertSee('Bob Smith (Client)');
}
public function test_ai_emotion_detection_endpoint(): void
{
$client = Client::create([
'client_name' => 'Bob Smith',
'client_email' => 'bob@smithco.com',
'project_name' => 'Cloud Migration',
'responsible_user_id' => $this->pm->id,
'created_by' => $this->admin->id,
'status' => 'active',
]);
$meeting = ClientMeeting::create([
'client_id' => $client->id,
'title' => 'Sync Call',
'meeting_code' => 'meet-emotion-test',
'host_user_id' => $this->pm->id,
'status' => 'in_progress',
]);
// Test angry sentiment detection on frustrated speech text
$response = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [
'speech_text' => 'This 2 week delay is totally unacceptable and broken webhooks are terrible.',
]);
$response->assertStatus(200)
->assertJson([
'emotion' => 'angry',
]);
// Test happy sentiment detection
$happyResponse = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [
'speech_text' => 'The demo looks fantastic, awesome work on the features!',
]);
$happyResponse->assertStatus(200)
->assertJson([
'emotion' => 'happy',
]);
}
public function test_ending_call_generates_mom_and_emails_stakeholders(): void
{
Mail::fake();
$client = Client::create([
'client_name' => 'Bob Smith',
'client_email' => 'bob@smithco.com',
'project_name' => 'Cloud Migration',
'responsible_user_id' => $this->pm->id,
'created_by' => $this->admin->id,
'status' => 'active',
]);
$meeting = ClientMeeting::create([
'client_id' => $client->id,
'title' => 'Sprint Wrap-up Call',
'meeting_code' => 'meet-wrapup-456',
'host_user_id' => $this->pm->id,
'status' => 'in_progress',
]);
$response = $this->actingAs($this->pm)
->postJson(route('client-calls.end-call', $meeting->meeting_code), [
'transcript' => "[10:00 AM] PM: Discussing migration progress.\n[10:10 AM] Client: Progress is good.",
'live_notes' => 'Agreed on migration date.',
]);
$response->assertStatus(200)
->assertJson(['success' => true]);
$this->assertDatabaseHas('client_call_notes', [
'client_meeting_id' => $meeting->id,
'client_id' => $client->id,
]);
$this->assertEquals('completed', $meeting->fresh()->status);
Mail::assertSent(ClientCallMomMail::class);
}
public function test_angry_analysis_is_strictly_restricted_to_admin_and_responsible_lead(): void
{
$client = Client::create([
'client_name' => 'Sarah Connor',
'client_email' => 'sarah@cyber.io',
'project_name' => 'Security Portal',
'responsible_user_id' => $this->pm->id,
'created_by' => $this->admin->id,
'status' => 'active',
'latest_sentiment' => 'angry',
]);
$meeting = ClientMeeting::create([
'client_id' => $client->id,
'title' => 'Escalation Review',
'meeting_code' => 'meet-angry-check',
'host_user_id' => $this->pm->id,
'status' => 'completed',
]);
$note = ClientCallNote::create([
'client_meeting_id' => $meeting->id,
'client_id' => $client->id,
'created_by' => $this->pm->id,
'raw_transcript' => 'Client is angry about milestone delays.',
'mom_content' => 'MoM Content',
'summary' => 'Urgent review',
'detected_sentiment' => 'angry',
'angry_analysis' => [
'is_angry' => true,
'trigger_words' => ['unacceptable', 'delay'],
'summary_why_angry' => 'Client is angry due to milestone delays.',
'grievances' => ['Delays', 'Lack of updates'],
'suggested_remedy' => 'Send remedial roadmap.',
],
]);
// 1. Admin CAN view angry analysis
$this->actingAs($this->admin)
->getJson(route('client-calls.angry-analysis', $note->id))
->assertStatus(200)
->assertJson([
'success' => true,
'detected_sentiment' => 'angry',
]);
// 2. Responsible PM CAN view angry analysis
$this->actingAs($this->pm)
->getJson(route('client-calls.angry-analysis', $note->id))
->assertStatus(200)
->assertJson([
'success' => true,
'detected_sentiment' => 'angry',
]);
// 3. Unrelated Developer CANNOT view angry analysis (403 Forbidden)
$this->actingAs($this->developer)
->getJson(route('client-calls.angry-analysis', $note->id))
->assertStatus(403)
->assertJsonStructure(['error']);
}
public function test_admin_dashboard_displays_client_projects_and_alerts(): void
{
Client::create([
'client_name' => 'Sarah Connor',
'client_email' => 'sarah@cyber.io',
'project_name' => 'Security Portal',
'responsible_user_id' => $this->pm->id,
'created_by' => $this->admin->id,
'status' => 'active',
'latest_sentiment' => 'angry',
]);
$this->actingAs($this->admin)
->get(route('admin.dashboard'))
->assertStatus(200)
->assertSee('Client Calls Hub')
->assertSee('Security Portal')
->assertSee('Sarah Connor');
}
public function test_can_view_call_logs_and_download_mom_and_todos(): void
{
$client = Client::create([
'client_name' => 'Robert Miller',
'client_email' => 'robert@acmecorp.com',
'project_name' => 'Enterprise Cloud Migration',
'responsible_user_id' => $this->pm->id,
'created_by' => $this->admin->id,
'status' => 'active',
'latest_sentiment' => 'happy',
]);
$meeting = ClientMeeting::create([
'client_id' => $client->id,
'title' => 'Sprint 2 Milestone Demo',
'meeting_code' => 'meet-logs-test-1',
'host_user_id' => $this->pm->id,
'status' => 'completed',
]);
$note = ClientCallNote::create([
'client_meeting_id' => $meeting->id,
'client_id' => $client->id,
'created_by' => $this->pm->id,
'raw_transcript' => 'Client is very happy with cloud deployment.',
'mom_content' => "### Minutes of Meeting\nObjectives: Deploy cloud infrastructure.\nNext steps: Testing.",
'summary' => 'Client praised the architecture blueprints.',
'detected_sentiment' => 'happy',
'todo_items' => [
['id' => 1, 'task' => 'Deploy AWS staging templates', 'owner' => 'Engineering Team', 'priority' => 'High', 'deadline' => 'Aug 22, 2026', 'completed' => false],
['id' => 2, 'task' => 'Provide SSO test credentials', 'owner' => 'Tech Lead', 'priority' => 'Medium', 'deadline' => 'Aug 23, 2026', 'completed' => false],
],
'emotion_details' => [
'emotion' => 'happy',
'headline' => 'Client Praised Sprint Progress',
'bullet_points' => [
'Pleased with clear architecture diagram.',
'Praised team responsiveness.'
],
'trigger_quotes' => ['looks great', 'very pleased'],
'suggested_action' => 'Maintain current sprint momentum.'
],
]);
// 1. Check Date-wise Call Logs view
$this->actingAs($this->pm)
->get(route('client-calls.logs'))
->assertStatus(200)
->assertSee('Date-wise Client Call Logs')
->assertSee('Enterprise Cloud Migration')
->assertSee('Sprint 2 Milestone Demo')
->assertSee('Download MoM (.txt)');
// 2. Test Download MoM as .txt
$responseMom = $this->actingAs($this->pm)
->get(route('client-calls.download-mom', $note->id));
$responseMom->assertStatus(200);
$this->assertStringContainsString('attachment;', $responseMom->headers->get('content-disposition'));
$this->assertStringContainsString('MINUTES OF MEETING (MoM)', $responseMom->getContent());
$this->assertStringContainsString('Enterprise Cloud Migration', $responseMom->getContent());
// 3. Test Download To-Dos as .txt
$responseTodos = $this->actingAs($this->pm)
->get(route('client-calls.download-todos', $note->id));
$responseTodos->assertStatus(200);
$this->assertStringContainsString('attachment;', $responseTodos->headers->get('content-disposition'));
$this->assertStringContainsString('ACTIONABLE TO-DO LIST', $responseTodos->getContent());
$this->assertStringContainsString('Deploy AWS staging templates', $responseTodos->getContent());
// 4. Test Fetch Emotion Details (Why happy)
$this->actingAs($this->pm)
->getJson(route('client-calls.emotion-details', $note->id))
->assertStatus(200)
->assertJson([
'success' => true,
'detected_sentiment' => 'happy',
])
->assertJsonStructure([
'emotion_details' => [
'emotion',
'headline',
'bullet_points',
'suggested_action',
]
]);
// 5. Test Toggle To-Do status
$this->actingAs($this->pm)
->postJson(route('client-calls.toggle-todo', ['id' => $note->id, 'todoId' => 1]))
->assertStatus(200)
->assertJson(['success' => true]);
$this->assertTrue($note->fresh()->todo_items[0]['completed']);
}
public function test_client_list_does_not_display_sentiment_badges_on_cards(): void
{
$client = Client::create([
'client_name' => 'Alice Wonder',
'client_email' => 'alice@wonderland.com',
'project_name' => 'Wonder App',
'responsible_user_id' => $this->pm->id,
'created_by' => $this->admin->id,
'status' => 'active',
'latest_sentiment' => 'angry',
]);
$response = $this->actingAs($this->pm)
->get(route('client-calls.index'));
$response->assertStatus(200)
->assertSee('Wonder App')
->assertSee('Alice Wonder')
->assertDontSee('sentiment-angry')
->assertDontSee('sentiment-happy');
}
public function test_client_hub_shows_date_wise_calls_section_and_tasks(): void
{
$client = Client::create([
'client_name' => 'David Warner',
'client_email' => 'david@cricket.io',
'project_name' => 'Live Score System',
'responsible_user_id' => $this->pm->id,
'created_by' => $this->admin->id,
'status' => 'active',
]);
$meeting = ClientMeeting::create([
'client_id' => $client->id,
'title' => 'Initial Sprint Review',
'meeting_code' => 'meet-score-1',
'host_user_id' => $this->pm->id,
'status' => 'completed',
]);
$note = ClientCallNote::create([
'client_meeting_id' => $meeting->id,
'client_id' => $client->id,
'created_by' => $this->pm->id,
'summary' => 'Reviewed live scoring latency.',
'mom_content' => 'Full MoM details here.',
'detected_sentiment' => 'happy',
'todo_items' => [
['id' => 1, 'task' => 'Optimize websocket latency', 'owner' => 'Dev Team', 'priority' => 'High', 'deadline' => 'Aug 24, 2026', 'completed' => false],
],
]);
// Access Client Hub / Show page
$response = $this->actingAs($this->pm)
->get(route('client-calls.show', $client->id));
$response->assertStatus(200)
->assertSee('Live Score System')
->assertSee('Date-wise Call History & MoM Details', false)
->assertSee('Initial Sprint Review')
->assertSee('Optimize websocket latency')
->assertSee('Happy / Satisfied');
}
public function test_add_and_delete_todo_items(): void
{
$client = Client::create([
'client_name' => 'Emma Watson',
'client_email' => 'emma@cinema.com',
'project_name' => 'Film Portal',
'responsible_user_id' => $this->pm->id,
'created_by' => $this->admin->id,
'status' => 'active',
]);
$meeting = ClientMeeting::create([
'client_id' => $client->id,
'title' => 'Post-Production Sync',
'meeting_code' => 'meet-film-1',
'host_user_id' => $this->pm->id,
'status' => 'completed',
]);
$note = ClientCallNote::create([
'client_meeting_id' => $meeting->id,
'client_id' => $client->id,
'created_by' => $this->pm->id,
'summary' => 'Sync on release roadmap.',
'mom_content' => 'MoM Content.',
'detected_sentiment' => 'neutral',
'todo_items' => [
['id' => 1, 'task' => 'First Task', 'owner' => 'PM', 'priority' => 'Medium', 'deadline' => 'TBD', 'completed' => false]
],
]);
// 1. Add new To-Do
$addRes = $this->actingAs($this->pm)
->postJson(route('client-calls.add-todo', $note->id), [
'task' => 'Deploy video CDN endpoints',
'owner' => 'Alex Dev',
'priority' => 'High',
'deadline' => 'Aug 26, 2026',
]);
$addRes->assertStatus(200)
->assertJson([
'success' => true,
'todo' => [
'task' => 'Deploy video CDN endpoints',
'owner' => 'Alex Dev',
'priority' => 'High',
]
]);
$this->assertCount(2, $note->fresh()->todo_items);
// 2. Delete the first To-Do
$delRes = $this->actingAs($this->pm)
->deleteJson(route('client-calls.delete-todo', ['id' => $note->id, 'todoId' => 1]));
$delRes->assertStatus(200)
->assertJson(['success' => true]);
$this->assertCount(1, $note->fresh()->todo_items);
$this->assertEquals('Deploy video CDN endpoints', $note->fresh()->todo_items[0]['task']);
}
public function test_upload_recording_endpoint_saves_file_and_updates_database(): void
{
\Illuminate\Support\Facades\Storage::fake('public');
$client = Client::create([
'client_name' => 'Sara Connor',
'client_email' => 'sara@cyberdyne.com',
'project_name' => 'AI Defense System',
'responsible_user_id' => $this->pm->id,
'created_by' => $this->admin->id,
'status' => 'active',
]);
$meeting = ClientMeeting::create([
'client_id' => $client->id,
'title' => 'Weekly Sync with Cyberdyne',
'meeting_code' => 'meet-cyber-rec-1',
'host_user_id' => $this->pm->id,
'status' => 'in_progress',
]);
$note = ClientCallNote::create([
'client_meeting_id' => $meeting->id,
'client_id' => $client->id,
'created_by' => $this->pm->id,
'summary' => 'Ongoing call notes',
'mom_content' => 'MoM in progress',
'detected_sentiment' => 'happy',
]);
$fakeVideo = \Illuminate\Http\UploadedFile::fake()->create('call_recording.webm', 2048, 'video/webm');
$response = $this->post(route('client-calls.upload-recording', $meeting->meeting_code), [
'video' => $fakeVideo,
]);
$response->assertStatus(200)
->assertJson([
'success' => true,
]);
$meeting->refresh();
$note->refresh();
$this->assertNotNull($meeting->recording_path);
$this->assertNotEmpty($meeting->recordings);
$this->assertNotNull($note->recording_path);
$this->assertNotEmpty($note->recordings);
$this->assertFileExists(public_path(ltrim($meeting->recording_path, '/')));
// Clean up created test recording file
@unlink(public_path(ltrim($meeting->recording_path, '/')));
}
public function test_meeting_and_note_recording_downloads(): void
{
$client = Client::create([
'client_name' => 'Thomas Anderson',
'client_email' => 'neo@matrix.io',
'project_name' => 'Matrix Simulator',
'responsible_user_id' => $this->pm->id,
'created_by' => $this->admin->id,
'status' => 'active',
]);
$meeting = ClientMeeting::create([
'client_id' => $client->id,
'title' => 'Matrix Review',
'meeting_code' => 'meet-matrix-99',
'host_user_id' => $this->pm->id,
'status' => 'completed',
]);
// Create a dummy video file in public storage
$dir = public_path('uploads/client_recordings/' . $meeting->meeting_code);
if (!file_exists($dir)) {
mkdir($dir, 0777, true);
}
$testFilePath = $dir . '/test_rec.webm';
file_put_contents($testFilePath, 'dummy webm content');
$relPath = 'uploads/client_recordings/' . $meeting->meeting_code . '/test_rec.webm';
$meeting->update(['recording_path' => $relPath]);
$note = ClientCallNote::create([
'client_meeting_id' => $meeting->id,
'client_id' => $client->id,
'created_by' => $this->pm->id,
'summary' => 'Discussion on simulated physics.',
'mom_content' => 'MoM details here.',
'recording_path' => $relPath,
]);
// 1. Download meeting recording
$resMeeting = $this->actingAs($this->pm)
->get(route('client-calls.download-meeting-recording', $meeting->id));
$resMeeting->assertStatus(200);
// 2. Download note recording
$resNote = $this->actingAs($this->pm)
->get(route('client-calls.download-note-recording', $note->id));
$resNote->assertStatus(200);
// Clean up
@unlink($testFilePath);
@rmdir($dir);
}
public function test_ending_meeting_links_recording_path_to_call_notes(): void
{
Mail::fake();
$client = Client::create([
'client_name' => 'Bruce Wayne',
'client_email' => 'bruce@waynecorp.com',
'project_name' => 'Batmobile OS',
'responsible_user_id' => $this->pm->id,
'created_by' => $this->admin->id,
'status' => 'active',
]);
$meeting = ClientMeeting::create([
'client_id' => $client->id,
'title' => 'Batmobile Specs Call',
'meeting_code' => 'meet-bat-101',
'host_user_id' => $this->pm->id,
'status' => 'in_progress',
]);
$response = $this->actingAs($this->pm)
->postJson(route('client-calls.end-call', $meeting->meeting_code), [
'transcript' => '[10:00 AM] PM: Testing client recording flow.',
'live_notes' => 'Armor upgrades agreed.',
'emotion_timeline' => [['emotion' => 'happy', 'time' => time()]],
'recording_path' => 'uploads/client_recordings/meet-bat-101/rec.webm',
]);
$response->assertStatus(200)
->assertJsonStructure(['success', 'redirect_url', 'note_id']);
$note = ClientCallNote::where('client_meeting_id', $meeting->id)->first();
$this->assertNotNull($note);
$this->assertEquals('uploads/client_recordings/meet-bat-101/rec.webm', $note->recording_path);
$this->assertNotEmpty($note->recordings);
$meeting->refresh();
$this->assertEquals('uploads/client_recordings/meet-bat-101/rec.webm', $meeting->recording_path);
}
public function test_client_hub_and_logs_pages_render_recording_actions_when_present(): void
{
$client = Client::create([
'client_name' => 'Diana Prince',
'client_email' => 'diana@themyscira.org',
'project_name' => 'Shield Security',
'responsible_user_id' => $this->pm->id,
'created_by' => $this->admin->id,
'status' => 'active',
]);
$meeting = ClientMeeting::create([
'client_id' => $client->id,
'title' => 'Defense Protocol Sync',
'meeting_code' => 'meet-shield-202',
'host_user_id' => $this->pm->id,
'status' => 'completed',
'recording_path' => 'uploads/client_recordings/meet-shield-202/rec.webm',
]);
$note = ClientCallNote::create([
'client_meeting_id' => $meeting->id,
'client_id' => $client->id,
'created_by' => $this->pm->id,
'summary' => 'Reviewed aegis defense protocols.',
'mom_content' => 'Full MoM details here.',
'detected_sentiment' => 'happy',
'recording_path' => 'uploads/client_recordings/meet-shield-202/rec.webm',
]);
// 1. Check Show page renders Video Call Recording
$showRes = $this->actingAs($this->pm)
->get(route('client-calls.show', $client->id));
$showRes->assertStatus(200)
->assertSee('Video Call Recording')
->assertSee('Watch Recording')
->assertSee('Download Video');
// 2. Check Logs page renders Recorded badge & Play Video button
$logsRes = $this->actingAs($this->pm)
->get(route('client-calls.logs'));
$logsRes->assertStatus(200)
->assertSee('Recorded')
->assertSee('Play Video')
->assertSee('Download Video Recording', false);
}
public function test_ai_emotion_detection_all_scenarios(): void
{
$client = Client::create([
'client_name' => 'Emotion Test Client',
'client_email' => 'emotion@test.com',
'project_name' => 'Emotion AI Testing',
'responsible_user_id' => $this->pm->id,
'created_by' => $this->admin->id,
'status' => 'active',
]);
$meeting = ClientMeeting::create([
'client_id' => $client->id,
'title' => 'Emotion Scenarios Test',
'meeting_code' => 'meet-emotion-scenarios',
'host_user_id' => $this->pm->id,
'status' => 'in_progress',
]);
// Scenario 1: Frustrated speech -> Angry
$resAngry = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [
'speech_text' => 'This continuous delay is unacceptable and we missed deadline.',
]);
$resAngry->assertStatus(200)->assertJson(['emotion' => 'angry']);
// Scenario 2: Negated happy phrase -> Angry
$resNegated = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [
'speech_text' => 'We are not happy and not satisfied with the progress.',
]);
$resNegated->assertStatus(200)->assertJson(['emotion' => 'angry']);
// Scenario 3: Concern / hesitation -> Sad
$resSad = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [
'speech_text' => 'We are worried and concerned about the upcoming launch date.',
]);
$resSad->assertStatus(200)->assertJson(['emotion' => 'sad']);
// Scenario 4: Praise and delight -> Happy
$resHappy = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [
'speech_text' => 'The sprint demo looks fantastic and we love the new dashboard.',
]);
$resHappy->assertStatus(200)->assertJson(['emotion' => 'happy']);
// Scenario 5: Dialogue with words like "made" and "message" (formerly false positive triggers) -> Neutral
$resNeutral = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [
'speech_text' => 'We made a roadmap plan and I will send you a message shortly.',
]);
$resNeutral->assertStatus(200)->assertJson(['emotion' => 'neutral']);
// Scenario 6: Empty speech -> Neutral
$resEmpty = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [
'speech_text' => '',
]);
$resEmpty->assertStatus(200)->assertJson(['emotion' => 'neutral']);
}
public function test_ai_service_mom_generation_and_fallback_engine(): void
{
$aiService = app(\App\Services\AiClientCallService::class);
// Test happy conversation MoM
$happyMoM = $aiService->generateMoM(
'Client: The release looks great, wonderful job team!',
'Reviewed sprint deliverables demo.',
'Payment Gateway',
'Fintech Corp'
);
$this->assertEquals('happy', $happyMoM['detected_sentiment']);
$this->assertNotEmpty($happyMoM['summary']);
$this->assertNotEmpty($happyMoM['todo_items']);
$this->assertNotEmpty($happyMoM['emotion_details']);
// Test angry conversation MoM
$angryMoM = $aiService->generateMoM(
'Client: This broken feature and missed deadline is unacceptable!',
'Client escalated timeline delay.',
'Payment Gateway',
'Fintech Corp'
);
$this->assertEquals('angry', $angryMoM['detected_sentiment']);
$this->assertTrue($angryMoM['angry_analysis']['is_angry']);
$this->assertNotEmpty($angryMoM['angry_analysis']['trigger_words']);
// Test sad conversation MoM
$sadMoM = $aiService->generateMoM(
'Client: We are disappointed and worried about budget allocations.',
'Discussion on risk factors.',
'Payment Gateway',
'Fintech Corp'
);
$this->assertEquals('sad', $sadMoM['detected_sentiment']);
// Test neutral conversation MoM
$neutralMoM = $aiService->generateMoM(
'Client: We made notes and will review the API contracts next week.',
'Standard sync.',
'Payment Gateway',
'Fintech Corp'
);
$this->assertEquals('neutral', $neutralMoM['detected_sentiment']);
}
public function test_resend_mom_email_endpoint(): void
{
Mail::fake();
$client = Client::create([
'client_name' => 'Email Test Client',
'client_email' => 'emailclient@test.com',
'project_name' => 'Email Dispatches',
'responsible_user_id' => $this->pm->id,
'created_by' => $this->admin->id,
'status' => 'active',
]);
$meeting = ClientMeeting::create([
'client_id' => $client->id,
'title' => 'MoM Dispatch Sync',
'meeting_code' => 'meet-dispatch-789',
'host_user_id' => $this->pm->id,
'status' => 'completed',
]);
$note = ClientCallNote::create([
'client_meeting_id' => $meeting->id,
'client_id' => $client->id,
'created_by' => $this->pm->id,
'summary' => 'MoM dispatch testing',
'mom_content' => 'Full MoM details here.',
'detected_sentiment' => 'happy',
]);
$response = $this->actingAs($this->pm)
->postJson(route('client-calls.resend-email', $note->id));
$response->assertStatus(200)
->assertJson([
'success' => true,
]);
Mail::assertSent(ClientCallMomMail::class);
}
public function test_signaling_heartbeat_and_chat_messages(): void
{
$client = Client::create([
'client_name' => 'Signal Client',
'client_email' => 'signal@test.com',
'project_name' => 'Signal Project',
'responsible_user_id' => $this->pm->id,
'created_by' => $this->admin->id,
'status' => 'active',
]);
$meeting = ClientMeeting::create([
'client_id' => $client->id,
'title' => 'WebRTC Signal Test',
'meeting_code' => 'meet-signal-test-321',
'host_user_id' => $this->pm->id,
'status' => 'in_progress',
]);
$response = $this->postJson(route('client-calls.heartbeat', $meeting->meeting_code), [
'peer_id' => 'peer_test_1',
'peer_name' => 'Alice Tester',
'chat_message' => ['text' => 'Hello everyone!'],
]);
$response->assertStatus(200)
->assertJsonStructure(['active_peers', 'signals', 'chat_messages']);
$meeting->refresh();
$this->assertNotEmpty($meeting->active_peers);
$this->assertNotEmpty($meeting->chat_messages);
$this->assertEquals('Hello everyone!', $meeting->chat_messages[0]['text']);
}
public function test_actual_discussed_tasks_and_mom_are_extracted_from_notes_and_transcript(): void
{
Mail::fake();
$client = Client::create([
'client_name' => 'Michael Scott',
'client_email' => 'michael@dunder.com',
'project_name' => 'Paper Ordering Portal',
'responsible_user_id' => $this->pm->id,
'created_by' => $this->admin->id,
'status' => 'active',
]);
$meeting = ClientMeeting::create([
'client_id' => $client->id,
'title' => 'Feature Review Call',
'meeting_code' => 'meet-paper-portal-77',
'host_user_id' => $this->pm->id,
'status' => 'in_progress',
]);
$response = $this->actingAs($this->pm)
->postJson(route('client-calls.end-call', $meeting->meeting_code), [
'transcript' => "[10:01 AM] Michael: We need to change the logo to navy blue and add PayPal checkout by Friday.\n[10:02 AM] Alex Dev: I will implement PayPal checkout by Friday and update the logo.",
'live_notes' => "- Fix shopping cart checkout discount code bug\n- Sarah will send updated invoice by tomorrow\n- Client approved the new homepage layout",
]);
$response->assertStatus(200)
->assertJson(['success' => true]);
$note = ClientCallNote::where('client_meeting_id', $meeting->id)->first();
$this->assertNotNull($note);
// Verify that extracted tasks contain the actual discussed items
$tasks = collect($note->todo_items)->pluck('task')->implode(' ');
$this->assertStringContainsString('shopping cart checkout discount code bug', strtolower($tasks));
$this->assertStringContainsString('invoice by tomorrow', strtolower($tasks));
// Verify MoM content contains the actual discussion points
$this->assertStringContainsString('shopping cart checkout discount code bug', strtolower($note->mom_content));
$this->assertStringContainsString('paypal', strtolower($note->mom_content));
// Verify that generic placeholder tasks are NOT present
$this->assertStringNotContainsString('deploy latest sprint milestone updates to staging environment', strtolower($tasks));
$this->assertStringNotContainsString('provide updated api contracts and sandbox test credentials', strtolower($tasks));
}
}