269 lines
10 KiB
PHP
269 lines
10 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Interview;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Tests\TestCase;
|
|
|
|
class CandidateInterviewPortalTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->withoutMiddleware();
|
|
}
|
|
|
|
public function test_hr_can_create_candidate_interview_and_generate_temp_credentials(): void
|
|
{
|
|
$hr = User::create([
|
|
'name' => 'HR Manager',
|
|
'email' => 'hr@company.com',
|
|
'role' => 'user',
|
|
]);
|
|
|
|
$response = $this->actingAs($hr)
|
|
->post(route('interview.store'), [
|
|
'candidate_name' => 'Alex Candidate',
|
|
'candidate_email' => 'alex.cand@gmail.com',
|
|
'candidate_phone' => '9876543210',
|
|
'valid_hours' => 2,
|
|
'language' => 'python',
|
|
]);
|
|
|
|
$response->assertRedirect(route('interview.index'));
|
|
|
|
$this->assertDatabaseHas('interviews', [
|
|
'candidate_name' => 'Alex Candidate',
|
|
'candidate_email' => 'alex.cand@gmail.com',
|
|
'candidate_phone' => '9876543210',
|
|
'status' => 'scheduled',
|
|
]);
|
|
}
|
|
|
|
public function test_candidate_can_login_with_temp_credentials_and_access_ide_room(): void
|
|
{
|
|
$interview = Interview::create([
|
|
'candidate_name' => 'Sarah Candidate',
|
|
'candidate_email' => 'sarah@gmail.com',
|
|
'candidate_phone' => '9123456789',
|
|
'temp_password' => 'Pass-123456',
|
|
'expires_at' => now()->addHours(2),
|
|
'language' => 'cpp',
|
|
'status' => 'scheduled',
|
|
'submission_unique_id' => 'sarah-candidate_9123456789_1752000000',
|
|
]);
|
|
|
|
$response = $this->post(route('interview.candidate.login.submit'), [
|
|
'identifier' => 'sarah@gmail.com',
|
|
'temp_password' => 'Pass-123456',
|
|
]);
|
|
|
|
$response->assertRedirect(route('interview.room', $interview->submission_unique_id));
|
|
|
|
$this->get(route('interview.room', $interview->submission_unique_id))
|
|
->assertStatus(200)
|
|
->assertSee('Sarah Candidate')
|
|
->assertSee('sarah-candidate_9123456789_1752000000');
|
|
}
|
|
|
|
public function test_live_code_execution_via_piston_api_proxy(): void
|
|
{
|
|
Http::fake([
|
|
'https://emkc.org/api/v2/piston/execute' => Http::response([
|
|
'run' => [
|
|
'output' => "Hello from SingleLogin Assessment!\nComputed Result: 84\n"
|
|
]
|
|
], 200)
|
|
]);
|
|
|
|
$response = $this->post(route('interview.execute'), [
|
|
'language' => 'python',
|
|
'code' => 'print("Hello from SingleLogin Assessment!")',
|
|
]);
|
|
|
|
$response->assertStatus(200)
|
|
->assertJson([
|
|
'success' => true,
|
|
'output' => "Hello from SingleLogin Assessment!\nComputed Result: 84\n"
|
|
]);
|
|
}
|
|
|
|
public function test_candidate_code_submission_saves_under_unique_submission_id(): void
|
|
{
|
|
$interview = Interview::create([
|
|
'candidate_name' => 'Michael Dev',
|
|
'candidate_email' => 'michael@gmail.com',
|
|
'candidate_phone' => '9988776655',
|
|
'temp_password' => 'Pass-654321',
|
|
'expires_at' => now()->addHours(2),
|
|
'language' => 'php',
|
|
'status' => 'in_progress',
|
|
'submission_unique_id' => 'michael-dev_9988776655_1752000001',
|
|
]);
|
|
|
|
$response = $this->post(route('interview.submit', $interview->id), [
|
|
'code' => '<?php echo "Solution completed";',
|
|
'language' => 'php',
|
|
'output' => 'Solution completed',
|
|
]);
|
|
|
|
$response->assertStatus(200)
|
|
->assertJson([
|
|
'success' => true,
|
|
'unique_id' => 'michael-dev_9988776655_1752000001'
|
|
]);
|
|
|
|
$interview->refresh();
|
|
$this->assertEquals('completed', $interview->status);
|
|
$this->assertEquals('<?php echo "Solution completed";', $interview->submitted_code);
|
|
}
|
|
|
|
public function test_proctoring_violation_logging_and_silent_warning(): void
|
|
{
|
|
$hr = User::create([
|
|
'name' => 'Interviewer HR',
|
|
'email' => 'hrpanel@company.com',
|
|
'role' => 'admin',
|
|
]);
|
|
|
|
$interview = Interview::create([
|
|
'candidate_name' => 'Test Candidate',
|
|
'candidate_email' => 'test@gmail.com',
|
|
'candidate_phone' => '9111111111',
|
|
'temp_password' => 'Pass-111111',
|
|
'expires_at' => now()->addHours(2),
|
|
'language' => 'python',
|
|
'status' => 'in_progress',
|
|
'submission_unique_id' => 'test-candidate_9111111111_1752000002',
|
|
]);
|
|
|
|
// Log tab switch violation
|
|
$this->post(route('interview.violation', $interview->id), [
|
|
'type' => 'tab_switch',
|
|
'details' => 'Candidate switched tab',
|
|
])->assertStatus(200);
|
|
|
|
// Send silent warning from HR
|
|
$this->actingAs($hr)
|
|
->post(route('interview.warning', $interview->id), [
|
|
'message' => 'Please stay on the test tab.',
|
|
])->assertStatus(200);
|
|
|
|
// Verify poll endpoint returns violation and warning
|
|
$pollResponse = $this->get(route('interview.poll', $interview->id));
|
|
$pollResponse->assertStatus(200)
|
|
->assertJsonFragment(['type' => 'tab_switch'])
|
|
->assertJsonFragment(['message' => 'Please stay on the test tab.']);
|
|
}
|
|
|
|
public function test_candidate_login_page_renders_dedicated_route(): void
|
|
{
|
|
$response = $this->get(route('interview.candidate.login'));
|
|
$response->assertStatus(200)
|
|
->assertSee('Candidate Assessment Portal')
|
|
->assertSee('Enter temporary login credentials provided by HR');
|
|
}
|
|
|
|
public function test_interview_zone_tab_visibility_for_assigned_interviewer_during_active_timeline(): void
|
|
{
|
|
$interviewer = User::create([
|
|
'name' => 'Jane Interviewer',
|
|
'email' => 'jane@company.com',
|
|
'role' => 'user',
|
|
]);
|
|
|
|
$unassignedUser = User::create([
|
|
'name' => 'Bob Developer',
|
|
'email' => 'bob@company.com',
|
|
'role' => 'user',
|
|
]);
|
|
|
|
// Before creating an assigned interview, neither has active interview zone
|
|
$this->assertFalse($interviewer->hasActiveInterviewZone());
|
|
$this->assertFalse($unassignedUser->hasActiveInterviewZone());
|
|
|
|
// Create an interview with timeline scheduled now + 2 hours valid, assigning Jane Interviewer
|
|
$interview = Interview::create([
|
|
'candidate_name' => 'John Candidate',
|
|
'candidate_email' => 'john.cand@gmail.com',
|
|
'candidate_phone' => '9888877777',
|
|
'temp_password' => 'Pass-999888',
|
|
'scheduled_at' => now(),
|
|
'expires_at' => now()->addHours(2),
|
|
'language' => 'python',
|
|
'status' => 'scheduled',
|
|
'assigned_interviewers' => [$interviewer->id],
|
|
'submission_unique_id' => 'john-candidate_9888877777_1752000003',
|
|
]);
|
|
|
|
// Assigned interviewer should have active interview zone during active timeline
|
|
$this->assertTrue($interviewer->hasActiveInterviewZone());
|
|
$this->assertFalse($unassignedUser->hasActiveInterviewZone());
|
|
|
|
// After timeline expires, the temporary Interview Zone tab is no longer active
|
|
$interview->update(['expires_at' => now()->subMinute()]);
|
|
$this->assertFalse($interviewer->fresh()->hasActiveInterviewZone());
|
|
}
|
|
|
|
public function test_candidate_notes_drawing_saving_and_interviewer_password_regen_blocking(): void
|
|
{
|
|
$hr = User::create([
|
|
'name' => 'HR Admin',
|
|
'email' => 'admin@company.com',
|
|
'role' => 'admin',
|
|
]);
|
|
|
|
$interview = Interview::create([
|
|
'candidate_name' => 'David Candidate',
|
|
'candidate_email' => 'david@gmail.com',
|
|
'candidate_phone' => '9777766666',
|
|
'temp_password' => 'Pass-777666',
|
|
'expires_at' => now()->addHours(2),
|
|
'language' => 'python',
|
|
'status' => 'in_progress',
|
|
'submission_unique_id' => 'david-candidate_9777766666_1752000004',
|
|
]);
|
|
|
|
// 1. Candidate saves notepad notes
|
|
$this->post(route('interview.candidate.notes', $interview->id), [
|
|
'notes' => 'Scratchpad logic: O(N log N) sorting approach',
|
|
])->assertStatus(200);
|
|
|
|
// 2. Candidate saves canvas drawing data
|
|
$this->post(route('interview.candidate.drawing', $interview->id), [
|
|
'drawing' => 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
|
|
])->assertStatus(200);
|
|
|
|
$interview->refresh();
|
|
$this->assertEquals('Scratchpad logic: O(N log N) sorting approach', $interview->candidate_notes);
|
|
$this->assertNotNull($interview->candidate_drawing);
|
|
|
|
// 3. Interviewer regenerates password
|
|
$regenResp = $this->actingAs($hr)
|
|
->post(route('interview.regenerate-password', $interview->id));
|
|
$regenResp->assertStatus(200)->assertJson(['success' => true]);
|
|
|
|
$interview->refresh();
|
|
$this->assertNotEquals('Pass-777666', $interview->temp_password);
|
|
|
|
// 4. Interviewer blocks candidate and IP
|
|
$blockResp = $this->actingAs($hr)
|
|
->post(route('interview.block-candidate', $interview->id));
|
|
$blockResp->assertStatus(200)->assertJson(['success' => true]);
|
|
|
|
$interview->refresh();
|
|
$this->assertEquals('blocked', $interview->status);
|
|
|
|
// Blocked candidate login should be denied
|
|
$this->post(route('interview.candidate.login.submit'), [
|
|
'identifier' => 'david@gmail.com',
|
|
'temp_password' => $interview->temp_password,
|
|
])->assertSessionHas('error');
|
|
}
|
|
}
|