create(array_merge([ 'owner_user_id' => $owner->id, 'transcription_status' => 'completed', 'transcript_text' => 'Teks asal transkripsi.', ], $attrs)); } private function addCollaborator(TranscriptionProject $project, User $user, string $role = 'editor'): void { ProjectCollaborator::create([ 'project_id' => $project->id, 'user_id' => $user->id, 'role' => $role, 'added_by' => $project->owner_user_id, ]); } // ------------------------------------------------------- // Edit transcript (PATCH) // ------------------------------------------------------- public function test_owner_can_edit_transcript(): void { $owner = User::factory()->create(); $project = $this->makeProject($owner); $this->actingAs($owner) ->patch(route('user.projects.transcript.update', $project), [ 'transcript_text' => 'Teks baru selepas diedit.', 'change_summary' => 'Perbaikan ejaan', ])->assertRedirect(); $project->refresh(); $this->assertEquals('Teks baru selepas diedit.', $project->transcript_text); $this->assertDatabaseHas('transcript_versions', [ 'project_id' => $project->id, 'version_number' => 1, 'change_summary' => 'Perbaikan ejaan', ]); } public function test_editor_collaborator_can_edit_transcript(): void { $owner = User::factory()->create(); $editor = User::factory()->create(); $project = $this->makeProject($owner); $this->addCollaborator($project, $editor, 'editor'); $this->actingAs($editor) ->patch(route('user.projects.transcript.update', $project), [ 'transcript_text' => 'Diedit oleh editor.', ])->assertRedirect(); $project->refresh(); $this->assertEquals('Diedit oleh editor.', $project->transcript_text); } public function test_viewer_collaborator_cannot_edit_transcript(): void { $owner = User::factory()->create(); $viewer = User::factory()->create(); $project = $this->makeProject($owner); $this->addCollaborator($project, $viewer, 'viewer'); $this->actingAs($viewer) ->patch(route('user.projects.transcript.update', $project), [ 'transcript_text' => 'Cuba edit oleh viewer.', ])->assertForbidden(); } public function test_admin_cannot_edit_transcript(): void { $admin = User::factory()->admin()->create(); $owner = User::factory()->create(); $project = $this->makeProject($owner); $this->actingAs($admin) ->patch(route('user.projects.transcript.update', $project), [ 'transcript_text' => 'Admin cuba edit.', ])->assertForbidden(); } public function test_non_collaborator_cannot_edit_transcript(): void { $owner = User::factory()->create(); $other = User::factory()->create(); $project = $this->makeProject($owner); $this->actingAs($other) ->patch(route('user.projects.transcript.update', $project), [ 'transcript_text' => 'Cubaan haram.', ])->assertForbidden(); } public function test_edit_creates_version_history(): void { $owner = User::factory()->create(); $project = $this->makeProject($owner, ['transcript_text' => 'Asal.']); $this->actingAs($owner) ->patch(route('user.projects.transcript.update', $project), [ 'transcript_text' => 'Kemaskini pertama.', ]); $this->actingAs($owner) ->patch(route('user.projects.transcript.update', $project), [ 'transcript_text' => 'Kemaskini kedua.', ]); $this->assertEquals(2, TranscriptVersion::where('project_id', $project->id)->count()); $v1 = TranscriptVersion::where('project_id', $project->id)->where('version_number', 1)->first(); $v2 = TranscriptVersion::where('project_id', $project->id)->where('version_number', 2)->first(); $this->assertEquals('Asal.', $v1->old_text); $this->assertEquals('Kemaskini pertama.', $v1->new_text); $this->assertEquals('Kemaskini pertama.', $v2->old_text); $this->assertEquals('Kemaskini kedua.', $v2->new_text); } // ------------------------------------------------------- // Upload transkripsi luar (.txt) // ------------------------------------------------------- public function test_owner_can_upload_external_transcript(): void { $owner = User::factory()->create(); $project = $this->makeProject($owner, ['transcript_text' => null, 'transcription_status' => 'failed']); $file = UploadedFile::fake()->createWithContent('transkripsi.txt', "Ini teks dari luar sistem.\nBaris kedua."); $this->actingAs($owner) ->post(route('user.projects.transcript.upload', $project), [ 'transcript_file' => $file, ])->assertRedirect(); $project->refresh(); $this->assertEquals('completed', $project->transcription_status); $this->assertEquals('manual', $project->transcription_engine); $this->assertStringContainsString('Ini teks dari luar sistem.', $project->transcript_text); $this->assertDatabaseHas('transcript_versions', [ 'project_id' => $project->id, 'version_number' => 1, 'change_summary' => 'Transkripsi dimuat naik secara manual', ]); } public function test_upload_external_replaces_existing_transcript(): void { $owner = User::factory()->create(); $project = $this->makeProject($owner, ['transcript_text' => 'Teks whisper asal.']); $file = UploadedFile::fake()->createWithContent('baru.txt', 'Teks baru dari sumber luar.'); $this->actingAs($owner) ->post(route('user.projects.transcript.upload', $project), [ 'transcript_file' => $file, ])->assertRedirect(); $project->refresh(); $this->assertEquals('Teks baru dari sumber luar.', $project->transcript_text); // Versi lama tersimpan $v = TranscriptVersion::where('project_id', $project->id)->first(); $this->assertEquals('Teks whisper asal.', $v->old_text); } public function test_upload_external_dispatches_ollama_when_checkbox_checked(): void { Queue::fake(); config(['speech2text.ollama.enabled' => true]); $owner = User::factory()->create(); $project = $this->makeProject($owner); $file = UploadedFile::fake()->createWithContent('t.txt', 'Teks untuk dibersihkan.'); $this->actingAs($owner) ->post(route('user.projects.transcript.upload', $project), [ 'transcript_file' => $file, 'clean_with_ollama' => '1', ])->assertRedirect(); Queue::assertPushed(OllamaPostProcessJob::class); } public function test_upload_external_does_not_dispatch_ollama_without_checkbox(): void { Queue::fake(); config(['speech2text.ollama.enabled' => true]); $owner = User::factory()->create(); $project = $this->makeProject($owner); $file = UploadedFile::fake()->createWithContent('t.txt', 'Teks biasa.'); $this->actingAs($owner) ->post(route('user.projects.transcript.upload', $project), [ 'transcript_file' => $file, ])->assertRedirect(); Queue::assertNotPushed(OllamaPostProcessJob::class); } public function test_viewer_cannot_upload_external_transcript(): void { $owner = User::factory()->create(); $viewer = User::factory()->create(); $project = $this->makeProject($owner); $this->addCollaborator($project, $viewer, 'viewer'); $file = UploadedFile::fake()->createWithContent('t.txt', 'Cuba upload.'); $this->actingAs($viewer) ->post(route('user.projects.transcript.upload', $project), [ 'transcript_file' => $file, ])->assertForbidden(); } public function test_upload_rejects_non_txt_file(): void { $owner = User::factory()->create(); $project = $this->makeProject($owner); $file = UploadedFile::fake()->create('teks.docx', 50, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'); $this->actingAs($owner) ->post(route('user.projects.transcript.upload', $project), [ 'transcript_file' => $file, ])->assertSessionHasErrors('transcript_file'); } // ------------------------------------------------------- // Version history & restore // ------------------------------------------------------- public function test_owner_can_view_version_history(): void { $owner = User::factory()->create(); $project = $this->makeProject($owner); TranscriptVersion::create([ 'project_id' => $project->id, 'edited_by' => $owner->id, 'version_number' => 1, 'old_text' => 'Lama.', 'new_text' => 'Baru.', 'change_summary' => 'Edit pertama', 'created_at' => now(), ]); $this->actingAs($owner) ->get(route('user.projects.versions.index', $project)) ->assertOk() ->assertViewIs('user.projects.versions') ->assertSee('Edit pertama'); } public function test_admin_cannot_view_version_history(): void { $admin = User::factory()->admin()->create(); $owner = User::factory()->create(); $project = $this->makeProject($owner); $this->actingAs($admin) ->get(route('user.projects.versions.index', $project)) ->assertForbidden(); } public function test_owner_can_restore_version(): void { $owner = User::factory()->create(); $project = $this->makeProject($owner, ['transcript_text' => 'Teks semasa.']); $version = TranscriptVersion::create([ 'project_id' => $project->id, 'edited_by' => $owner->id, 'version_number' => 1, 'old_text' => 'Sebelum edit.', 'new_text' => 'Teks versi 1.', 'created_at' => now(), ]); $this->actingAs($owner) ->post(route('user.projects.versions.restore', [$project, $version])) ->assertRedirect(route('user.projects.show', $project)); $project->refresh(); $this->assertEquals('Teks versi 1.', $project->transcript_text); // Pemulihan cipta versi baru $this->assertDatabaseHas('transcript_versions', [ 'project_id' => $project->id, 'version_number' => 2, 'change_summary' => 'Dipulihkan dari versi 1', ]); } public function test_viewer_cannot_restore_version(): void { $owner = User::factory()->create(); $viewer = User::factory()->create(); $project = $this->makeProject($owner); $this->addCollaborator($project, $viewer, 'viewer'); $version = TranscriptVersion::create([ 'project_id' => $project->id, 'edited_by' => $owner->id, 'version_number' => 1, 'old_text' => null, 'new_text' => 'Versi 1.', 'created_at' => now(), ]); $this->actingAs($viewer) ->post(route('user.projects.versions.restore', [$project, $version])) ->assertForbidden(); } // ------------------------------------------------------- // Comments // ------------------------------------------------------- public function test_owner_can_add_comment(): void { $owner = User::factory()->create(); $project = $this->makeProject($owner); $this->actingAs($owner) ->post(route('user.projects.comments.store', $project), [ 'message' => 'Ini komen pertama saya.', ])->assertRedirect(); $this->assertDatabaseHas('project_comments', [ 'project_id' => $project->id, 'user_id' => $owner->id, 'message' => 'Ini komen pertama saya.', ]); } public function test_collaborator_can_add_comment(): void { $owner = User::factory()->create(); $collab = User::factory()->create(); $project = $this->makeProject($owner); $this->addCollaborator($project, $collab, 'viewer'); $this->actingAs($collab) ->post(route('user.projects.comments.store', $project), [ 'message' => 'Komen daripada viewer.', ])->assertRedirect(); $this->assertDatabaseHas('project_comments', [ 'project_id' => $project->id, 'user_id' => $collab->id, ]); } public function test_non_collaborator_cannot_add_comment(): void { $owner = User::factory()->create(); $other = User::factory()->create(); $project = $this->makeProject($owner); $this->actingAs($other) ->post(route('user.projects.comments.store', $project), [ 'message' => 'Cubaan haram.', ])->assertForbidden(); } public function test_admin_cannot_add_comment(): void { $admin = User::factory()->admin()->create(); $owner = User::factory()->create(); $project = $this->makeProject($owner); $this->actingAs($admin) ->post(route('user.projects.comments.store', $project), [ 'message' => 'Admin cuba komen.', ])->assertForbidden(); } public function test_owner_can_delete_any_comment_in_project(): void { $owner = User::factory()->create(); $collab = User::factory()->create(); $project = $this->makeProject($owner); $this->addCollaborator($project, $collab); $comment = $project->comments()->create([ 'user_id' => $collab->id, 'message' => 'Komen collaborator.', ]); $this->actingAs($owner) ->delete(route('user.projects.comments.destroy', [$project, $comment])) ->assertRedirect(); $this->assertSoftDeleted('project_comments', ['id' => $comment->id]); } public function test_comment_author_can_delete_own_comment(): void { $owner = User::factory()->create(); $collab = User::factory()->create(); $project = $this->makeProject($owner); $this->addCollaborator($project, $collab); $comment = $project->comments()->create([ 'user_id' => $collab->id, 'message' => 'Komen saya sendiri.', ]); $this->actingAs($collab) ->delete(route('user.projects.comments.destroy', [$project, $comment])) ->assertRedirect(); $this->assertSoftDeleted('project_comments', ['id' => $comment->id]); } public function test_non_author_cannot_delete_others_comment(): void { $owner = User::factory()->create(); $collab1 = User::factory()->create(); $collab2 = User::factory()->create(); $project = $this->makeProject($owner); $this->addCollaborator($project, $collab1); $this->addCollaborator($project, $collab2); $comment = $project->comments()->create([ 'user_id' => $collab1->id, 'message' => 'Milik collab1.', ]); $this->actingAs($collab2) ->delete(route('user.projects.comments.destroy', [$project, $comment])) ->assertForbidden(); } }