create(array_merge([ 'owner_user_id' => $owner->id, ], $attrs)); } // ------------------------------------------------------- // Admin tidak boleh akses kandungan projek // ------------------------------------------------------- public function test_admin_cannot_view_project_detail(): void { $admin = User::factory()->admin()->create(); $owner = User::factory()->create(); $project = $this->makeProject($owner); $this->actingAs($admin) ->get(route('user.projects.show', $project)) ->assertForbidden(); } public function test_admin_cannot_stream_audio(): void { $admin = User::factory()->admin()->create(); $owner = User::factory()->create(); $project = $this->makeProject($owner); $this->actingAs($admin) ->get(route('user.projects.audio.stream', $project)) ->assertForbidden(); } public function test_admin_can_view_project_metadata_list(): void { $admin = User::factory()->admin()->create(); $this->actingAs($admin) ->get(route('admin.projects.index')) ->assertOk() ->assertViewIs('admin.projects.index'); } // ------------------------------------------------------- // Owner boleh akses projek sendiri // ------------------------------------------------------- public function test_owner_can_view_own_project(): void { $owner = User::factory()->create(); $project = $this->makeProject($owner); $this->actingAs($owner) ->get(route('user.projects.show', $project)) ->assertOk() ->assertViewIs('user.projects.show'); } // ------------------------------------------------------- // Collaborator boleh akses projek yang dikongsi // ------------------------------------------------------- public function test_collaborator_can_view_shared_project(): void { $owner = User::factory()->create(); $collab = User::factory()->create(); $project = $this->makeProject($owner); ProjectCollaborator::create([ 'project_id' => $project->id, 'user_id' => $collab->id, 'role' => 'editor', 'added_by' => $owner->id, ]); $this->actingAs($collab) ->get(route('user.projects.show', $project)) ->assertOk(); } // ------------------------------------------------------- // Bukan collaborator tidak boleh akses // ------------------------------------------------------- public function test_non_collaborator_cannot_view_project(): void { $owner = User::factory()->create(); $other = User::factory()->create(); $project = $this->makeProject($owner); $this->actingAs($other) ->get(route('user.projects.show', $project)) ->assertForbidden(); } // ------------------------------------------------------- // Hanya owner boleh delete projek // ------------------------------------------------------- public function test_only_owner_can_delete_project(): void { $owner = User::factory()->create(); $collab = User::factory()->create(); $project = $this->makeProject($owner); ProjectCollaborator::create([ 'project_id' => $project->id, 'user_id' => $collab->id, 'role' => 'editor', 'added_by' => $owner->id, ]); // Collaborator tidak boleh delete $this->actingAs($collab) ->delete(route('user.projects.destroy', $project)) ->assertForbidden(); // Owner boleh delete $this->actingAs($owner) ->delete(route('user.projects.destroy', $project)) ->assertRedirect(route('user.dashboard')); $this->assertSoftDeleted('transcription_projects', ['id' => $project->id]); } // ------------------------------------------------------- // Upload audio dan dispatch job // ------------------------------------------------------- public function test_user_can_upload_audio_and_job_dispatched(): void { Queue::fake(); Storage::fake('private'); $user = User::factory()->create(); // Create a temp file with valid MPEG-1 L3 frame sync bytes (FF FB) $tmpPath = tempnam(sys_get_temp_dir(), 'mp3_'); file_put_contents($tmpPath, "\xFF\xFB\x90\x00" . str_repeat("\xFF", 1024)); $file = new UploadedFile($tmpPath, 'rakaman.mp3', 'audio/mpeg', null, true); $this->actingAs($user) ->post(route('user.projects.store'), [ 'title' => 'Mesyuarat Mac 2025', 'description' => 'Rakaman mesyuarat bulanan', 'audio' => $file, 'language' => 'ms', ])->assertRedirect(); $project = TranscriptionProject::where('title', 'Mesyuarat Mac 2025')->first(); $this->assertNotNull($project); $this->assertEquals('pending', $project->transcription_status); $this->assertEquals($user->id, $project->owner_user_id); // Fail disimpan dalam private storage (bukan public) Storage::disk('private')->assertExists($project->stored_audio_path); // Job dihantar ke queue Queue::assertPushed(\App\Jobs\TranscribeAudioJob::class); @unlink($tmpPath); } // ------------------------------------------------------- // Admin — metadata list tidak dedahkan transcript // ------------------------------------------------------- public function test_admin_metadata_list_does_not_expose_transcript(): void { $admin = User::factory()->admin()->create(); $owner = User::factory()->create(); TranscriptionProject::factory()->create([ 'owner_user_id' => $owner->id, 'transcript_text' => 'Ini adalah teks rahsia yang sensitif.', 'transcription_status' => 'completed', ]); $response = $this->actingAs($admin) ->get(route('admin.projects.index')) ->assertOk(); $response->assertDontSee('Ini adalah teks rahsia yang sensitif.'); } // ------------------------------------------------------- // Collaborator management // ------------------------------------------------------- public function test_owner_can_add_collaborator(): void { $owner = User::factory()->create(); $newUser = User::factory()->create(); $project = $this->makeProject($owner); $this->actingAs($owner) ->post(route('user.projects.collaborators.store', $project), [ 'email' => $newUser->email, 'role' => 'editor', ])->assertRedirect(); $this->assertDatabaseHas('project_collaborators', [ 'project_id' => $project->id, 'user_id' => $newUser->id, 'role' => 'editor', ]); } public function test_non_owner_cannot_add_collaborator(): void { $owner = User::factory()->create(); $collab = User::factory()->create(); $other = User::factory()->create(); $project = $this->makeProject($owner); ProjectCollaborator::create([ 'project_id' => $project->id, 'user_id' => $collab->id, 'role' => 'editor', 'added_by' => $owner->id, ]); $this->actingAs($collab) ->post(route('user.projects.collaborators.store', $project), [ 'email' => $other->email, ])->assertForbidden(); } }