admin = User::factory()->create(['role' => Role::ADMIN]); $this->manager = User::factory()->create(['role' => Role::MANAGER]); $this->brigadier = User::factory()->create(['role' => Role::BRIGADIER]); } public function test_guest_cannot_open_documentation(): void { $this->get(route('documents.index'))->assertRedirect(route('login')); } public function test_authenticated_user_can_open_year_independent_documentation(): void { $folder = $this->createRootFolder(); $this->actingAs($this->manager) ->withSession(['year' => 2020]) ->get(route('documents.index', ['folder' => $folder->id])) ->assertOk() ->assertViewIs('documents.index') ->assertSeeText($folder->name); } public function test_user_without_documents_view_permission_cannot_enter_module(): void { $role = Role::query()->create([ 'slug' => 'documentation_denied', 'name' => 'Без документации', 'is_active' => true, ]); $user = User::factory()->create([ 'role' => $role->slug, 'role_id' => $role->id, ]); $this->actingAs($user)->get(route('documents.index'))->assertForbidden(); } public function test_only_real_administrator_can_create_root_folder(): void { $assistant = User::factory()->create(['role' => Role::ASSISTANT_HEAD]); $this->actingAs($this->manager) ->post(route('documents.folders.store'), ['name' => 'Закрытая папка']) ->assertForbidden(); $this->actingAs($assistant) ->post(route('documents.folders.store'), ['name' => 'Папка помощника']) ->assertForbidden(); $this->actingAs($this->admin) ->post(route('documents.folders.store'), ['name' => 'Общие инструкции']) ->assertRedirect(); $folder = DocumentationFolder::query()->where('name', 'Общие инструкции')->firstOrFail(); $this->assertFalse($folder->inherits_permissions); $this->assertDatabaseHas('documentation_folder_permissions', [ 'folder_id' => $folder->id, 'subject_type' => DocumentationFolderPermission::SUBJECT_ALL, 'can_read' => true, 'can_write' => false, ]); } public function test_folder_permissions_support_all_roles_and_users(): void { $folder = $this->createRootFolder(); $managerRole = Role::query()->where('slug', Role::MANAGER)->firstOrFail(); $this->actingAs($this->admin) ->put(route('documents.folders.permissions.update', $folder), [ 'role_read_ids' => [$managerRole->id], 'user_write_ids' => [$this->brigadier->id], ]) ->assertRedirect(route('documents.index', ['folder' => $folder->id])); $this->actingAs($this->manager) ->get(route('documents.index', ['folder' => $folder->id])) ->assertOk(); $this->actingAs($this->manager) ->post(route('documents.folders.store'), ['parent_id' => $folder->id, 'name' => 'Нельзя']) ->assertForbidden(); $this->actingAs($this->brigadier) ->post(route('documents.folders.store'), ['parent_id' => $folder->id, 'name' => 'Можно']) ->assertRedirect(); $this->assertDatabaseHas('documentation_folder_permissions', [ 'folder_id' => $folder->id, 'subject_type' => DocumentationFolderPermission::SUBJECT_ROLE, 'role_id' => $managerRole->id, 'can_read' => true, 'can_write' => false, ]); $this->assertDatabaseHas('documentation_folder_permissions', [ 'folder_id' => $folder->id, 'subject_type' => DocumentationFolderPermission::SUBJECT_USER, 'user_id' => $this->brigadier->id, 'can_read' => true, 'can_write' => true, ]); } public function test_subfolder_inherits_parent_permissions(): void { $folder = $this->createRootFolder(); $this->actingAs($this->admin) ->put(route('documents.folders.permissions.update', $folder), [ 'user_write_ids' => [$this->manager->id], ]) ->assertRedirect(); $this->actingAs($this->manager) ->post(route('documents.folders.store'), [ 'parent_id' => $folder->id, 'name' => 'Регламенты', ]) ->assertRedirect(); $child = DocumentationFolder::query()->where('name', 'Регламенты')->firstOrFail(); $this->assertTrue($child->inherits_permissions); $this->assertDatabaseCount('documentation_folder_permissions', 1); $this->actingAs($this->manager) ->post(route('documents.store', $child), [ 'name' => 'Регламент монтажа', 'file' => UploadedFile::fake()->create('rules.pdf', 12, 'application/pdf'), ]) ->assertRedirect(); $this->assertDatabaseHas('documentation_documents', [ 'folder_id' => $child->id, 'name' => 'Регламент монтажа', 'last_version_number' => 1, ]); } public function test_document_versions_can_be_downloaded_and_current_version_falls_back_after_deletion(): void { $folder = $this->createRootFolder(); $this->actingAs($this->admin) ->post(route('documents.store', $folder), [ 'name' => 'Прайс', 'file' => UploadedFile::fake()->createWithContent('price.xlsx', 'version-one'), ]) ->assertRedirect(); $document = DocumentationDocument::query()->firstOrFail(); $firstVersion = $document->versions()->firstOrFail(); Storage::disk('local')->assertExists($firstVersion->file->path); Storage::disk('public')->assertMissing($firstVersion->file->path); $this->actingAs($this->admin) ->post(route('documents.versions.store', $document), [ 'file' => UploadedFile::fake()->createWithContent('price.xlsx', 'version-two'), ]) ->assertRedirect(); $document->refresh(); $secondVersion = $document->versions()->where('version', 2)->firstOrFail(); $secondPath = $secondVersion->file->path; $this->assertSame(2, $document->currentVersion()->firstOrFail()->version); $this->actingAs($this->manager) ->get(route('documents.versions.download', $firstVersion)) ->assertOk(); $this->actingAs($this->admin) ->delete(route('documents.versions.destroy', $secondVersion)) ->assertRedirect(); Storage::disk('local')->assertMissing($secondPath); $this->assertSame(1, $document->refresh()->currentVersion()->firstOrFail()->version); } public function test_deleting_last_version_deletes_document_and_file(): void { $folder = $this->createRootFolder(); $this->actingAs($this->admin) ->post(route('documents.store', $folder), [ 'file' => UploadedFile::fake()->createWithContent('manual.txt', 'manual'), ]) ->assertRedirect(); $document = DocumentationDocument::query()->firstOrFail(); $version = DocumentationDocumentVersion::query()->with('file')->firstOrFail(); $fileId = $version->file_id; $path = $version->file->path; $this->actingAs($this->admin) ->delete(route('documents.versions.destroy', $version)) ->assertRedirect(); $this->assertDatabaseMissing('documentation_documents', ['id' => $document->id]); $this->assertDatabaseMissing('files', ['id' => $fileId]); Storage::disk('local')->assertMissing($path); } public function test_deleting_folder_removes_nested_documents_versions_and_physical_files(): void { $folder = $this->createRootFolder(); $this->actingAs($this->admin) ->post(route('documents.folders.store'), ['parent_id' => $folder->id, 'name' => 'Архив']) ->assertRedirect(); $child = DocumentationFolder::query()->where('parent_id', $folder->id)->firstOrFail(); $this->actingAs($this->admin) ->post(route('documents.store', $child), [ 'file' => UploadedFile::fake()->createWithContent('archive.txt', 'archive'), ]) ->assertRedirect(); $version = DocumentationDocumentVersion::query()->with('file')->firstOrFail(); $path = $version->file->path; $fileId = $version->file_id; $this->actingAs($this->admin) ->delete(route('documents.folders.destroy', $folder)) ->assertRedirect(route('documents.index')); $this->assertDatabaseMissing('documentation_folders', ['id' => $folder->id]); $this->assertDatabaseMissing('documentation_folders', ['id' => $child->id]); $this->assertDatabaseMissing('files', ['id' => $fileId]); Storage::disk('local')->assertMissing($path); } public function test_document_can_be_renamed_and_found_by_name(): void { $folder = $this->createRootFolder(); $this->actingAs($this->admin) ->post(route('documents.store', $folder), [ 'name' => 'Старое название', 'file' => UploadedFile::fake()->create('document.pdf', 5, 'application/pdf'), ]) ->assertRedirect(); $document = DocumentationDocument::query()->firstOrFail(); $this->actingAs($this->admin) ->put(route('documents.update', $document), ['name' => 'Инструкция по монтажу']) ->assertRedirect(); $this->actingAs($this->manager) ->get(route('documents.index', ['folder' => $folder->id, 'search' => 'монтажу'])) ->assertOk() ->assertSeeText('Инструкция по монтажу'); } public function test_file_icons_follow_mime_type_and_images_have_protected_preview(): void { $folder = $this->createRootFolder(); $this->actingAs($this->admin) ->post(route('documents.store', $folder), [ 'name' => 'Фотография', 'file' => UploadedFile::fake()->image('photo.png', 120, 80), ]) ->assertRedirect(); $this->actingAs($this->admin) ->post(route('documents.store', $folder), [ 'name' => 'Инструкция PDF', 'file' => UploadedFile::fake()->create('manual.pdf', 5, 'application/pdf'), ]) ->assertRedirect(); $imageVersion = DocumentationDocument::query() ->where('name', 'Фотография') ->firstOrFail() ->currentVersion() ->with('file') ->firstOrFail(); $pdfVersion = DocumentationDocument::query() ->where('name', 'Инструкция PDF') ->firstOrFail() ->currentVersion() ->with('file') ->firstOrFail(); $this->actingAs($this->manager) ->get(route('documents.index', ['folder' => $folder->id])) ->assertOk() ->assertSee('bi-file-earmark-image', false) ->assertSee('bi-file-earmark-pdf', false) ->assertSee(route('documents.versions.preview', $imageVersion), false); $this->actingAs($this->manager) ->get(route('documents.versions.preview', $imageVersion)) ->assertOk() ->assertHeader('Content-Type', 'image/png') ->assertHeader('X-Content-Type-Options', 'nosniff'); $this->actingAs($this->manager) ->get(route('documents.versions.preview', $pdfVersion)) ->assertNotFound(); } private function createRootFolder(): DocumentationFolder { $this->actingAs($this->admin) ->post(route('documents.folders.store'), ['name' => 'Общая документация']) ->assertRedirect(); return DocumentationFolder::query()->whereNull('parent_id')->latest('id')->firstOrFail(); } }