- 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
262 lines
9.7 KiB
PHP
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).');
|
|
}
|
|
}
|