DocumentationControllerTest.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. <?php
  2. declare(strict_types=1);
  3. namespace Tests\Feature;
  4. use App\Models\DocumentationDocument;
  5. use App\Models\DocumentationDocumentVersion;
  6. use App\Models\DocumentationFolder;
  7. use App\Models\DocumentationFolderPermission;
  8. use App\Models\Role;
  9. use App\Models\User;
  10. use Illuminate\Foundation\Testing\RefreshDatabase;
  11. use Illuminate\Http\UploadedFile;
  12. use Illuminate\Support\Facades\Storage;
  13. use Tests\TestCase;
  14. class DocumentationControllerTest extends TestCase
  15. {
  16. use RefreshDatabase;
  17. protected bool $seed = true;
  18. private User $admin;
  19. private User $manager;
  20. private User $brigadier;
  21. protected function setUp(): void
  22. {
  23. parent::setUp();
  24. Storage::fake('public');
  25. Storage::fake('local');
  26. $this->admin = User::factory()->create(['role' => Role::ADMIN]);
  27. $this->manager = User::factory()->create(['role' => Role::MANAGER]);
  28. $this->brigadier = User::factory()->create(['role' => Role::BRIGADIER]);
  29. }
  30. public function test_guest_cannot_open_documentation(): void
  31. {
  32. $this->get(route('documents.index'))->assertRedirect(route('login'));
  33. }
  34. public function test_authenticated_user_can_open_year_independent_documentation(): void
  35. {
  36. $folder = $this->createRootFolder();
  37. $this->actingAs($this->manager)
  38. ->withSession(['year' => 2020])
  39. ->get(route('documents.index', ['folder' => $folder->id]))
  40. ->assertOk()
  41. ->assertViewIs('documents.index')
  42. ->assertSeeText($folder->name);
  43. }
  44. public function test_user_without_documents_view_permission_cannot_enter_module(): void
  45. {
  46. $role = Role::query()->create([
  47. 'slug' => 'documentation_denied',
  48. 'name' => 'Без документации',
  49. 'is_active' => true,
  50. ]);
  51. $user = User::factory()->create([
  52. 'role' => $role->slug,
  53. 'role_id' => $role->id,
  54. ]);
  55. $this->actingAs($user)->get(route('documents.index'))->assertForbidden();
  56. }
  57. public function test_only_real_administrator_can_create_root_folder(): void
  58. {
  59. $assistant = User::factory()->create(['role' => Role::ASSISTANT_HEAD]);
  60. $this->actingAs($this->manager)
  61. ->post(route('documents.folders.store'), ['name' => 'Закрытая папка'])
  62. ->assertForbidden();
  63. $this->actingAs($assistant)
  64. ->post(route('documents.folders.store'), ['name' => 'Папка помощника'])
  65. ->assertForbidden();
  66. $this->actingAs($this->admin)
  67. ->post(route('documents.folders.store'), ['name' => 'Общие инструкции'])
  68. ->assertRedirect();
  69. $folder = DocumentationFolder::query()->where('name', 'Общие инструкции')->firstOrFail();
  70. $this->assertFalse($folder->inherits_permissions);
  71. $this->assertDatabaseHas('documentation_folder_permissions', [
  72. 'folder_id' => $folder->id,
  73. 'subject_type' => DocumentationFolderPermission::SUBJECT_ALL,
  74. 'can_read' => true,
  75. 'can_write' => false,
  76. ]);
  77. }
  78. public function test_folder_permissions_support_all_roles_and_users(): void
  79. {
  80. $folder = $this->createRootFolder();
  81. $managerRole = Role::query()->where('slug', Role::MANAGER)->firstOrFail();
  82. $this->actingAs($this->admin)
  83. ->put(route('documents.folders.permissions.update', $folder), [
  84. 'role_read_ids' => [$managerRole->id],
  85. 'user_write_ids' => [$this->brigadier->id],
  86. ])
  87. ->assertRedirect(route('documents.index', ['folder' => $folder->id]));
  88. $this->actingAs($this->manager)
  89. ->get(route('documents.index', ['folder' => $folder->id]))
  90. ->assertOk();
  91. $this->actingAs($this->manager)
  92. ->post(route('documents.folders.store'), ['parent_id' => $folder->id, 'name' => 'Нельзя'])
  93. ->assertForbidden();
  94. $this->actingAs($this->brigadier)
  95. ->post(route('documents.folders.store'), ['parent_id' => $folder->id, 'name' => 'Можно'])
  96. ->assertRedirect();
  97. $this->assertDatabaseHas('documentation_folder_permissions', [
  98. 'folder_id' => $folder->id,
  99. 'subject_type' => DocumentationFolderPermission::SUBJECT_ROLE,
  100. 'role_id' => $managerRole->id,
  101. 'can_read' => true,
  102. 'can_write' => false,
  103. ]);
  104. $this->assertDatabaseHas('documentation_folder_permissions', [
  105. 'folder_id' => $folder->id,
  106. 'subject_type' => DocumentationFolderPermission::SUBJECT_USER,
  107. 'user_id' => $this->brigadier->id,
  108. 'can_read' => true,
  109. 'can_write' => true,
  110. ]);
  111. }
  112. public function test_subfolder_inherits_parent_permissions(): void
  113. {
  114. $folder = $this->createRootFolder();
  115. $this->actingAs($this->admin)
  116. ->put(route('documents.folders.permissions.update', $folder), [
  117. 'user_write_ids' => [$this->manager->id],
  118. ])
  119. ->assertRedirect();
  120. $this->actingAs($this->manager)
  121. ->post(route('documents.folders.store'), [
  122. 'parent_id' => $folder->id,
  123. 'name' => 'Регламенты',
  124. ])
  125. ->assertRedirect();
  126. $child = DocumentationFolder::query()->where('name', 'Регламенты')->firstOrFail();
  127. $this->assertTrue($child->inherits_permissions);
  128. $this->assertDatabaseCount('documentation_folder_permissions', 1);
  129. $this->actingAs($this->manager)
  130. ->post(route('documents.store', $child), [
  131. 'name' => 'Регламент монтажа',
  132. 'file' => UploadedFile::fake()->create('rules.pdf', 12, 'application/pdf'),
  133. ])
  134. ->assertRedirect();
  135. $this->assertDatabaseHas('documentation_documents', [
  136. 'folder_id' => $child->id,
  137. 'name' => 'Регламент монтажа',
  138. 'last_version_number' => 1,
  139. ]);
  140. }
  141. public function test_document_versions_can_be_downloaded_and_current_version_falls_back_after_deletion(): void
  142. {
  143. $folder = $this->createRootFolder();
  144. $this->actingAs($this->admin)
  145. ->post(route('documents.store', $folder), [
  146. 'name' => 'Прайс',
  147. 'file' => UploadedFile::fake()->createWithContent('price.xlsx', 'version-one'),
  148. ])
  149. ->assertRedirect();
  150. $document = DocumentationDocument::query()->firstOrFail();
  151. $firstVersion = $document->versions()->firstOrFail();
  152. Storage::disk('local')->assertExists($firstVersion->file->path);
  153. Storage::disk('public')->assertMissing($firstVersion->file->path);
  154. $this->actingAs($this->admin)
  155. ->post(route('documents.versions.store', $document), [
  156. 'file' => UploadedFile::fake()->createWithContent('price.xlsx', 'version-two'),
  157. ])
  158. ->assertRedirect();
  159. $document->refresh();
  160. $secondVersion = $document->versions()->where('version', 2)->firstOrFail();
  161. $secondPath = $secondVersion->file->path;
  162. $this->assertSame(2, $document->currentVersion()->firstOrFail()->version);
  163. $this->actingAs($this->manager)
  164. ->get(route('documents.versions.download', $firstVersion))
  165. ->assertOk();
  166. $this->actingAs($this->admin)
  167. ->delete(route('documents.versions.destroy', $secondVersion))
  168. ->assertRedirect();
  169. Storage::disk('local')->assertMissing($secondPath);
  170. $this->assertSame(1, $document->refresh()->currentVersion()->firstOrFail()->version);
  171. }
  172. public function test_deleting_last_version_deletes_document_and_file(): void
  173. {
  174. $folder = $this->createRootFolder();
  175. $this->actingAs($this->admin)
  176. ->post(route('documents.store', $folder), [
  177. 'file' => UploadedFile::fake()->createWithContent('manual.txt', 'manual'),
  178. ])
  179. ->assertRedirect();
  180. $document = DocumentationDocument::query()->firstOrFail();
  181. $version = DocumentationDocumentVersion::query()->with('file')->firstOrFail();
  182. $fileId = $version->file_id;
  183. $path = $version->file->path;
  184. $this->actingAs($this->admin)
  185. ->delete(route('documents.versions.destroy', $version))
  186. ->assertRedirect();
  187. $this->assertDatabaseMissing('documentation_documents', ['id' => $document->id]);
  188. $this->assertDatabaseMissing('files', ['id' => $fileId]);
  189. Storage::disk('local')->assertMissing($path);
  190. }
  191. public function test_deleting_folder_removes_nested_documents_versions_and_physical_files(): void
  192. {
  193. $folder = $this->createRootFolder();
  194. $this->actingAs($this->admin)
  195. ->post(route('documents.folders.store'), ['parent_id' => $folder->id, 'name' => 'Архив'])
  196. ->assertRedirect();
  197. $child = DocumentationFolder::query()->where('parent_id', $folder->id)->firstOrFail();
  198. $this->actingAs($this->admin)
  199. ->post(route('documents.store', $child), [
  200. 'file' => UploadedFile::fake()->createWithContent('archive.txt', 'archive'),
  201. ])
  202. ->assertRedirect();
  203. $version = DocumentationDocumentVersion::query()->with('file')->firstOrFail();
  204. $path = $version->file->path;
  205. $fileId = $version->file_id;
  206. $this->actingAs($this->admin)
  207. ->delete(route('documents.folders.destroy', $folder))
  208. ->assertRedirect(route('documents.index'));
  209. $this->assertDatabaseMissing('documentation_folders', ['id' => $folder->id]);
  210. $this->assertDatabaseMissing('documentation_folders', ['id' => $child->id]);
  211. $this->assertDatabaseMissing('files', ['id' => $fileId]);
  212. Storage::disk('local')->assertMissing($path);
  213. }
  214. public function test_document_can_be_renamed_and_found_by_name(): void
  215. {
  216. $folder = $this->createRootFolder();
  217. $this->actingAs($this->admin)
  218. ->post(route('documents.store', $folder), [
  219. 'name' => 'Старое название',
  220. 'file' => UploadedFile::fake()->create('document.pdf', 5, 'application/pdf'),
  221. ])
  222. ->assertRedirect();
  223. $document = DocumentationDocument::query()->firstOrFail();
  224. $this->actingAs($this->admin)
  225. ->put(route('documents.update', $document), ['name' => 'Инструкция по монтажу'])
  226. ->assertRedirect();
  227. $this->actingAs($this->manager)
  228. ->get(route('documents.index', ['folder' => $folder->id, 'search' => 'монтажу']))
  229. ->assertOk()
  230. ->assertSeeText('Инструкция по монтажу');
  231. }
  232. public function test_file_icons_follow_mime_type_and_images_have_protected_preview(): void
  233. {
  234. $folder = $this->createRootFolder();
  235. $this->actingAs($this->admin)
  236. ->post(route('documents.store', $folder), [
  237. 'name' => 'Фотография',
  238. 'file' => UploadedFile::fake()->image('photo.png', 120, 80),
  239. ])
  240. ->assertRedirect();
  241. $this->actingAs($this->admin)
  242. ->post(route('documents.store', $folder), [
  243. 'name' => 'Инструкция PDF',
  244. 'file' => UploadedFile::fake()->create('manual.pdf', 5, 'application/pdf'),
  245. ])
  246. ->assertRedirect();
  247. $imageVersion = DocumentationDocument::query()
  248. ->where('name', 'Фотография')
  249. ->firstOrFail()
  250. ->currentVersion()
  251. ->with('file')
  252. ->firstOrFail();
  253. $pdfVersion = DocumentationDocument::query()
  254. ->where('name', 'Инструкция PDF')
  255. ->firstOrFail()
  256. ->currentVersion()
  257. ->with('file')
  258. ->firstOrFail();
  259. $this->actingAs($this->manager)
  260. ->get(route('documents.index', ['folder' => $folder->id]))
  261. ->assertOk()
  262. ->assertSee('bi-file-earmark-image', false)
  263. ->assertSee('bi-file-earmark-pdf', false)
  264. ->assertSee(route('documents.versions.preview', $imageVersion), false);
  265. $this->actingAs($this->manager)
  266. ->get(route('documents.versions.preview', $imageVersion))
  267. ->assertOk()
  268. ->assertHeader('Content-Type', 'image/png')
  269. ->assertHeader('X-Content-Type-Options', 'nosniff');
  270. $this->actingAs($this->manager)
  271. ->get(route('documents.versions.preview', $pdfVersion))
  272. ->assertNotFound();
  273. }
  274. private function createRootFolder(): DocumentationFolder
  275. {
  276. $this->actingAs($this->admin)
  277. ->post(route('documents.folders.store'), ['name' => 'Общая документация'])
  278. ->assertRedirect();
  279. return DocumentationFolder::query()->whereNull('parent_id')->latest('id')->firstOrFail();
  280. }
  281. }