464 lines
16 KiB
PHP
464 lines
16 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Project;
|
|
|
|
use App\Jobs\OllamaPostProcessJob;
|
|
use App\Models\ProjectCollaborator;
|
|
use App\Models\TranscriptVersion;
|
|
use App\Models\TranscriptionProject;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Tests\TestCase;
|
|
|
|
class TranscriptEditorTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private function makeProject(User $owner, array $attrs = []): TranscriptionProject
|
|
{
|
|
return TranscriptionProject::factory()->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();
|
|
}
|
|
}
|