sls/tests/Feature/ScheduleInterviewWithInvitationsTest.php
kushal.saha 1541c8193e feat(interview): add candidate resume upload, lightbox viewer, and panelist email attachments
- Add resume file upload to schedule drawer with instant preview and validation
- Create resume lightbox modal viewer with background scroll lock and CV SVG icon
- Integrate CV tab into inspector panel with enlarge action and state 
- Attach candidate resume to panelist notification emails when 
- Fix Outlook RFC 5546 iTIP element mismatch and add table credentials copy button
2026-08-27 11:37:01 +00:00

262 lines
9.7 KiB
PHP

<?php
namespace Tests\Feature;
use App\Mail\CandidateInterviewInvitationMail;
use App\Mail\InterviewerAssessmentNotificationMail;
use App\Mail\InterviewReminderMail;
use App\Models\Interview;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class ScheduleInterviewWithInvitationsTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
}
public function test_scheduling_interview_creates_record_and_queues_candidate_and_interviewer_invitations(): void
{
Mail::fake();
$hr = User::create([
'name' => 'HR Manager',
'email' => 'hr@company.com',
'role' => 'hr',
]);
$interviewer1 = User::create([
'name' => 'Tech Lead',
'email' => 'techlead@company.com',
'role' => 'user',
]);
$interviewer2 = User::create([
'name' => 'Senior Engineer',
'email' => 'srengineer@company.com',
'role' => 'user',
]);
$response = $this->actingAs($hr)->post(route('interview.store'), [
'candidate_name' => 'Soumyadeep Mondal',
'candidate_email' => 'soumyadeep@example.com',
'candidate_phone' => '9876543210',
'job_title' => 'AIML (Computer Vision & Agentic AI) Engineer',
'round' => 'R1',
'description' => 'Live coding session on Agentic workflows',
'scheduled_at' => now()->addDays(2)->format('Y-m-d\TH:i'),
'valid_hours' => 2,
'language' => 'python',
'assigned_interviewers' => [$interviewer1->id, $interviewer2->id],
]);
$response->assertRedirect(route('interview.index'));
// Database assertion
$this->assertDatabaseHas('interviews', [
'candidate_name' => 'Soumyadeep Mondal',
'candidate_email' => 'soumyadeep@example.com',
'job_title' => 'AIML (Computer Vision & Agentic AI) Engineer',
'round' => 'R1',
'description' => 'Live coding session on Agentic workflows',
'language' => 'python',
'status' => 'scheduled',
]);
$interview = Interview::where('candidate_email', 'soumyadeep@example.com')->first();
$this->assertNotNull($interview);
// Candidate mail assertion
Mail::assertQueued(CandidateInterviewInvitationMail::class, function ($mail) use ($interview) {
return $mail->hasTo('soumyadeep@example.com')
&& $mail->interview->id === $interview->id
&& ! empty($mail->icsContent);
});
// Interviewers mail assertions
Mail::assertQueued(InterviewerAssessmentNotificationMail::class, function ($mail) use ($interviewer1) {
return $mail->hasTo('techlead@company.com') && $mail->interviewer->id === $interviewer1->id;
});
Mail::assertQueued(InterviewerAssessmentNotificationMail::class, function ($mail) use ($interviewer2) {
return $mail->hasTo('srengineer@company.com') && $mail->interviewer->id === $interviewer2->id;
});
}
public function test_send_interview_reminders_command_dispatches_15min_advance_emails(): void
{
Mail::fake();
$interviewer = User::create([
'name' => 'Panelist User',
'email' => 'panelist@company.com',
'role' => 'user',
]);
// Interview scheduled 10 minutes from now (within 20m window)
$interview = Interview::create([
'candidate_name' => 'Upcoming Candidate',
'candidate_email' => 'upcoming@example.com',
'candidate_phone' => '1122334455',
'job_title' => 'Backend Architect',
'round' => 'Technical Round',
'temp_password' => 'Pass-778899',
'scheduled_at' => now()->addMinutes(10),
'expires_at' => now()->addHours(2),
'language' => 'php',
'status' => 'scheduled',
'assigned_interviewers' => [$interviewer->id],
'submission_unique_id' => 'upcoming-candidate_1122334455_1752000099',
'reminder_sent_at' => null,
]);
$this->artisan('interview:send-reminders')
->expectsOutputToContain('Successfully processed and dispatched reminders for 1 interview session(s).')
->assertExitCode(0);
// Assert reminder email sent to candidate
Mail::assertQueued(InterviewReminderMail::class, function ($mail) {
return $mail->hasTo('upcoming@example.com') && $mail->recipientType === 'candidate';
});
// Assert reminder email sent to interviewer
Mail::assertQueued(InterviewReminderMail::class, function ($mail) {
return $mail->hasTo('panelist@company.com') && $mail->recipientType === 'interviewer';
});
$interview->refresh();
$this->assertNotNull($interview->reminder_sent_at);
}
public function test_scheduling_validation_errors_render_open_drawer_and_error_directives(): void
{
$this->withMiddleware();
$hr = User::create([
'name' => 'HR Manager',
'email' => 'hr@company.com',
'role' => 'hr',
]);
$response = $this->actingAs($hr)
->from(route('interview.index'))
->followingRedirects()
->post(route('interview.store'), [
'candidate_name' => '',
'candidate_email' => 'invalid-email-address',
'candidate_phone' => '',
'job_title' => '',
'valid_hours' => 2,
'language' => 'python',
'description' => 'Test instructions',
]);
$response->assertOk();
$response->assertSee('create-interview-drawer');
$response->assertSee('active');
$response->assertSee('Please correct the following errors:');
$response->assertSee('The candidate name is required.');
$response->assertSee('Please provide a valid candidate email address.');
$response->assertSee('The candidate phone number is required.');
$response->assertSee('The job title / position is required.');
$response->assertSee('Test instructions');
}
public function test_scheduling_interview_with_resume_file_stores_path_and_serves_via_route(): void
{
Storage::fake('public');
Mail::fake();
$hr = User::create([
'name' => 'HR Manager',
'email' => 'hr@company.com',
'role' => 'hr',
]);
$interviewer = User::create([
'name' => 'Tech Panelist',
'email' => 'techpanelist@company.com',
'role' => 'user',
]);
$file = UploadedFile::fake()->create('resume.pdf', 500, 'application/pdf');
$response = $this->actingAs($hr)->post(route('interview.store'), [
'candidate_name' => 'Kushal Candidate',
'candidate_email' => 'kushal@example.com',
'candidate_phone' => '9988776655',
'job_title' => 'Lead AI Engineer',
'round' => 'Technical',
'valid_hours' => 2,
'language' => 'python',
'assigned_interviewers' => [$interviewer->id],
'resume' => $file,
]);
$response->assertRedirect(route('interview.index'));
$interview = Interview::where('candidate_email', 'kushal@example.com')->first();
$this->assertNotNull($interview);
$this->assertNotNull($interview->resume_path);
Storage::disk('public')->assertExists($interview->resume_path);
// Test view / download route
$viewResponse = $this->actingAs($hr)->get(route('interview.resume', $interview->id));
$viewResponse->assertOk();
$viewResponse->assertHeader('Content-Disposition', 'inline; filename="'.basename($interview->resume_path).'"');
// Test interviewer mail attachments include the resume
Mail::assertQueued(InterviewerAssessmentNotificationMail::class, function ($mail) {
$attachments = $mail->attachments();
$hasIcs = false;
$hasResume = false;
foreach ($attachments as $att) {
if (str_ends_with($att->as ?? '', '.ics') || str_contains($att->mime ?? '', 'text/calendar')) {
$hasIcs = true;
}
if (str_contains($att->as ?? '', 'Resume_kushal-candidate.pdf')) {
$hasResume = true;
}
}
return $mail->hasTo('techpanelist@company.com') && $hasIcs && $hasResume;
});
}
public function test_scheduling_interview_rejects_invalid_resume_mime_type(): void
{
$this->withMiddleware();
$hr = User::create([
'name' => 'HR Manager',
'email' => 'hr@company.com',
'role' => 'hr',
]);
$invalidFile = UploadedFile::fake()->create('malicious.exe', 500, 'application/octet-stream');
$response = $this->actingAs($hr)
->from(route('interview.index'))
->followingRedirects()
->post(route('interview.store'), [
'candidate_name' => 'Invalid File Candidate',
'candidate_email' => 'invalidfile@example.com',
'candidate_phone' => '9988776655',
'job_title' => 'Developer',
'valid_hours' => 2,
'language' => 'python',
'resume' => $invalidFile,
]);
$response->assertOk();
$response->assertSee('The candidate resume must be a PDF or Word document (.pdf, .doc, .docx).');
}
}