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', 'output' => 'Solution completed', ]); $response->assertStatus(200) ->assertJson([ 'success' => true, 'unique_id' => 'michael-dev_9988776655_1752000001' ]); $interview->refresh(); $this->assertEquals('completed', $interview->status); $this->assertEquals('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'); } public function test_executive_candidate_evaluation_pdf_report_generation(): void { $hr = User::create([ 'name' => 'Report Viewer HR', 'email' => 'hrreport@company.com', 'role' => 'admin', ]); $interview = Interview::create([ 'candidate_name' => 'Rachel Candidate', 'candidate_email' => 'rachel@gmail.com', 'candidate_phone' => '9666655555', 'temp_password' => 'Pass-666555', 'expires_at' => now()->addHours(2), 'language' => 'python', 'status' => 'completed', 'submission_unique_id' => 'rachel-candidate_9666655555_1752000005', 'submitted_code' => 'def solve(): return 42', 'code_output' => 'Result: 42', 'candidate_notes' => 'Complexity analysis: O(1)', ]); $response = $this->actingAs($hr)->get(route('interview.report', $interview->id)); $response->assertStatus(200) ->assertSee('Rachel Candidate') ->assertSee('rachel@gmail.com') ->assertSee('Executive Summary') ->assertSee('Behavioral Integrity Rating') ->assertSee('Technical Code Competency') ->assertSee('Complexity analysis: O(1)'); } public function test_silent_proctoring_violations_and_multi_party_active_call_status(): void { $hr = User::create([ 'name' => 'Interviewer One', 'email' => 'interviewer1@company.com', 'role' => 'admin', ]); $interview = Interview::create([ 'candidate_name' => 'Proctor Test Candidate', 'candidate_email' => 'proctortest@gmail.com', 'candidate_phone' => '9555544444', 'temp_password' => 'Pass-555444', 'expires_at' => now()->addHours(2), 'language' => 'python', 'status' => 'in_progress', 'submission_unique_id' => 'proctor-test-candidate_9555544444_1752000006', ]); // 1. Silent Lower Screen Gaze Stare violation $this->post(route('interview.violation', $interview->id), [ 'type' => 'gaze_lower_device', 'details' => 'Candidate continuously looking at lower portion of screen for 10s (suspected reading from mobile device or cheat sheet)', ])->assertStatus(200); // 2. Silent Question Repetition violation $this->post(route('interview.violation', $interview->id), [ 'type' => 'question_repetition', 'details' => 'Candidate reading/repeating question out loud: "Write a function to reverse a linked list" (suspected Parakeet AI speech prompt)', ])->assertStatus(200); // 3. Silent Talking to Secondary Person violation $this->post(route('interview.violation', $interview->id), [ 'type' => 'talking_secondary_person', 'details' => 'Candidate speaking out loud to secondary person in room: "Can you search the solution on your laptop?"', ])->assertStatus(200); // 4. Silent External AI Overlay Hotkey violation $this->post(route('interview.violation', $interview->id), [ 'type' => 'external_ai_detected', 'details' => 'Parakeet AI / External AI Hotkey Intercepted (KeyA): Candidate activated external AI assistant overlay shortcut', ])->assertStatus(200); // Verify poll endpoint returns all 4 silent proctoring violations to interviewer dashboard $pollResp = $this->get(route('interview.poll', $interview->id)); $pollResp->assertStatus(200) ->assertJsonFragment(['type' => 'gaze_lower_device']) ->assertJsonFragment(['type' => 'question_repetition']) ->assertJsonFragment(['type' => 'talking_secondary_person']) ->assertJsonFragment(['type' => 'external_ai_detected']); // 5. Update call status for multi-party call test $callResp = $this->actingAs($hr)->post(route('interview.call-status', $interview->id), [ 'call_status' => 'active', ]); $callResp->assertStatus(200)->assertJson(['success' => true, 'call_status' => 'active']); // Active calls endpoint should list the active session for multiple interviewers to join $activeCallsResp = $this->actingAs($hr)->get(route('interview.active-calls')); $activeCallsResp->assertStatus(200) ->assertJsonFragment(['submission_unique_id' => 'proctor-test-candidate_9555544444_1752000006']); } public function test_tab_switch_logging_and_external_ai_detection_behavior(): void { $interview = Interview::create([ 'candidate_name' => 'Tab Switch Candidate', 'candidate_email' => 'tabswitch@gmail.com', 'candidate_phone' => '9998887776', 'temp_password' => 'Pass-999888', 'expires_at' => now()->addHours(2), 'language' => 'python', 'status' => 'in_progress', 'submission_unique_id' => 'tab-switch-candidate_9998887776_1752000099', ]); // 1. Tab switch log violation should log type 'tab_switch' $resp = $this->post(route('interview.violation', $interview->id), [ 'type' => 'tab_switch', 'details' => 'Candidate switched browser tab or window', ]); $resp->assertStatus(200); $poll = $this->get(route('interview.poll', $interview->id)); $poll->assertStatus(200) ->assertJsonFragment(['type' => 'tab_switch', 'details' => 'Candidate switched browser tab or window']); // 2. External AI hotkey violation should log type 'external_ai_detected' with meaningful message $aiResp = $this->post(route('interview.violation', $interview->id), [ 'type' => 'external_ai_detected', 'details' => 'candidate might use external ai: Intercepted AI assistant shortcut hotkey (KeyA)', ]); $aiResp->assertStatus(200); // 3. Talking on phone violation $phoneResp = $this->post(route('interview.violation', $interview->id), [ 'type' => 'talking_on_phone', 'details' => 'Candidate detected talking on phone / phone call during assessment: "hello hold on calling you back"', ]); $phoneResp->assertStatus(200); // 4. Reading from external device violation $extDevResp = $this->post(route('interview.violation', $interview->id), [ 'type' => 'reading_external_device', 'details' => 'Candidate detected reading from external device, mobile screen, or secondary monitor', ]); $extDevResp->assertStatus(200); $poll3 = $this->get(route('interview.poll', $interview->id)); $poll3->assertStatus(200) ->assertJsonFragment(['type' => 'talking_on_phone']) ->assertJsonFragment(['type' => 'reading_external_device']); } }