diff --git a/.env.testing b/.env.testing index a645f2c40..fe468da92 100644 --- a/.env.testing +++ b/.env.testing @@ -5,3 +5,4 @@ APP_TIMEZONE=Pacific/Auckland DB_DEFAULT=sqlite MAIL_ADDRESS=test@test.com MAIL_NAME=test +APP_UPLOAD_DIR=testing \ No newline at end of file diff --git a/app/Http/Controllers/Project/IssueController.php b/app/Http/Controllers/Project/IssueController.php index 4e0f9d409..4a65e87bb 100644 --- a/app/Http/Controllers/Project/IssueController.php +++ b/app/Http/Controllers/Project/IssueController.php @@ -310,7 +310,7 @@ public function getDisplayAttachment(Project $project, Issue $issue, Attachment $issue->setRelation('project', $project); $attachment->setRelation('issue', $issue); - $path = 'uploads/' . $issue->project_id . '/' . $attachment->upload_token . '/' . $attachment->filename; + $path = config('tinyissue.uploads_dir') . '/' . $issue->project_id . '/' . $attachment->upload_token . '/' . $attachment->filename; $storage = \Storage::disk('local'); $length = $storage->size($path); $time = $storage->lastModified($path); @@ -353,7 +353,7 @@ public function getDownloadAttachment(Project $project, Issue $issue, Attachment $issue->setRelation('project', $project); $attachment->setRelation('issue', $issue); - $path = config('filesystems.disks.local.root') . '/uploads/' . $this->issue->project_id . '/' . $this->upload_token . '/' . $attachment->filename; + $path = config('filesystems.disks.local.root') . '/' . config('tinyissue.uploads_dir') . '/' . $issue->project_id . '/' . $attachment->upload_token . '/' . $attachment->filename; return response()->download($path, $attachment->filename); } diff --git a/app/Model/Project/Issue.php b/app/Model/Project/Issue.php index 8325a6e3c..6b0a3e3aa 100644 --- a/app/Model/Project/Issue.php +++ b/app/Model/Project/Issue.php @@ -107,7 +107,9 @@ public function project() */ public function attachments() { - return $this->hasMany('Tinyissue\Model\Project\Issue\Attachment', 'issue_id')->where('comment_id', '=', 0); + return $this->hasMany('Tinyissue\Model\Project\Issue\Attachment', 'issue_id') + ->where('comment_id', '=', 0) + ->orWhere('comment_id', '=', null); } /** diff --git a/app/Model/Project/Issue/Attachment.php b/app/Model/Project/Issue/Attachment.php index b24dd2429..0e5786f14 100644 --- a/app/Model/Project/Issue/Attachment.php +++ b/app/Model/Project/Issue/Attachment.php @@ -61,6 +61,7 @@ public function user() { return $this->belongsTo('Tinyissue\Model\User', 'uploaded_by'); } + /** * An attachment can belong to a comment (inverse relationship of Comments::attachments). * @@ -82,7 +83,7 @@ public function comment() */ public function upload(array $input, Project $project, User $user) { - $relativePath = '/uploads/' . $project->id . '/' . $input['upload_token']; + $relativePath = '/' . config('tinyissue.uploads_dir') . '/' . $project->id . '/' . $input['upload_token']; \Storage::disk('local')->makeDirectory($relativePath, 0777, true); $path = config('filesystems.disks.local.root') . $relativePath; @@ -115,7 +116,7 @@ public function remove(array $input, Project $project, User $user) ->where('filename', '=', $input['filename']) ->delete(); - $path = config('filesystems.disks.local.root') . '/uploads/' . $project->id . '/' . $input['upload_token']; + $path = config('filesystems.disks.local.root') . '/' . config('tinyissue.uploads_dir') . '/' . $project->id . '/' . $input['upload_token']; $this->deleteFile($path, $input['filename']); } @@ -127,7 +128,7 @@ public function remove(array $input, Project $project, User $user) */ public function deleteFile($path, $filename) { - @unlink($path.'/'.$filename); + @unlink($path . '/' . $filename); @rmdir($path); } @@ -139,9 +140,14 @@ public function deleteFile($path, $filename) public function isImage() { return in_array($this->fileextension, [ - 'jpg', 'jpeg', 'JPG', 'JPEG', - 'png', 'PNG', - 'gif', 'GIF', + 'jpg', + 'jpeg', + 'JPG', + 'JPEG', + 'png', + 'PNG', + 'gif', + 'GIF', ]); } @@ -152,7 +158,7 @@ public function isImage() */ public function download() { - return \URL::to('project/'.$this->issue->project_id.'/issue/'.$this->issue_id.'/download/'.$this->id); + return \URL::to('project/' . $this->issue->project_id . '/issue/' . $this->issue_id . '/download/' . $this->id); } /** @@ -162,6 +168,6 @@ public function download() */ public function display() { - return \URL::to('project/'.$this->issue->project_id.'/issue/'.$this->issue_id.'/display/'.$this->id); + return \URL::to('project/' . $this->issue->project_id . '/issue/' . $this->issue_id . '/display/' . $this->id); } } diff --git a/app/Model/Project/Issue/Comment.php b/app/Model/Project/Issue/Comment.php index 258243215..a6e222055 100644 --- a/app/Model/Project/Issue/Comment.php +++ b/app/Model/Project/Issue/Comment.php @@ -121,7 +121,7 @@ public function deleteComment() $this->activity()->delete(); foreach ($this->attachments as $attachment) { - $path = config('filesystems.disks.local.root') . '/uploads/' . $this->project_id . '/' . $attachment->upload_token; + $path = config('filesystems.disks.local.root') . '/' . config('tinyissue.uploads_dir') . '/' . $this->project_id . '/' . $attachment->upload_token; $attachment->deleteFile($path, $attachment->filename); $attachment->delete(); } diff --git a/app/Providers/ConfigServiceProvider.php b/app/Providers/ConfigServiceProvider.php index ffac4ff5e..8d1cb47ea 100644 --- a/app/Providers/ConfigServiceProvider.php +++ b/app/Providers/ConfigServiceProvider.php @@ -31,6 +31,7 @@ public function register() config([ 'tinyissue.release_date' => '4-11-2013', 'tinyissue.version' => '1.3.1', + 'tinyissue.uploads_dir' => env('APP_UPLOAD_DIR', 'uploads'), ]); } } diff --git a/tests/_data/upload1.txt b/tests/_data/upload1.txt new file mode 100644 index 000000000..4ef4d72b4 --- /dev/null +++ b/tests/_data/upload1.txt @@ -0,0 +1 @@ +upload1 \ No newline at end of file diff --git a/tests/_data/upload2.txt b/tests/_data/upload2.txt new file mode 100644 index 000000000..a8554342b --- /dev/null +++ b/tests/_data/upload2.txt @@ -0,0 +1 @@ +upload2 \ No newline at end of file diff --git a/tests/_support/FunctionalHelper.php b/tests/_support/FunctionalHelper.php index d6c41a043..459a21235 100644 --- a/tests/_support/FunctionalHelper.php +++ b/tests/_support/FunctionalHelper.php @@ -4,7 +4,13 @@ // here you can define custom actions // all public methods declared in helper class will be available in $I +use Codeception\Configuration; +use Codeception\Exception\ElementNotFound; +use Codeception\Module; +use Codeception\TestCase; use Illuminate\Support\Facades\Hash; +use Symfony\Component\DomCrawler\Crawler; +use Symfony\Component\DomCrawler\Form; use Tinyissue\Model; class FunctionalHelper extends \Codeception\Module @@ -227,4 +233,90 @@ protected function debugResponse() $this->debugSection('Cookies', $module->client->getInternalRequest()->getCookies()); $this->debugSection('Headers', $module->client->getInternalResponse()->getHeaders()); } + + public function submitFormWithFileToUri($selector, $uri, array $files, array $params = []) + { + $form = $this->matchForm($selector); + + // Upload files + foreach ($files as $fieldName => $fileNames) { + $this->uploadFileWithForm($form, $uri, $fieldName, $fileNames); + } + + // Make sure upload token is same as upload files request + $params['upload_token'] = $form->get('upload_token')->getValue(); + $form->setValues($params); + + $this->debugSection('Uri', $form->getUri()); + $this->debugSection($form->getMethod(), $form->getValues()); + + // Save Form request + $module = $this->getModule('Laravel5'); + $module->client->request($form->getMethod(), $form->getUri(), $form->getPhpValues(), []); + $this->debugResponse(); + } + + /** + * @param string $selector + * + * @return Form + */ + protected function matchForm($selector) + { + $form = $this->match($selector)->form(); + + if (!$form instanceof Form) { + throw new ElementNotFound($selector, 'Form'); + } + + return $form; + } + + /** + * @param string $selector + * + * @return Crawler + */ + protected function match($selector) + { + return $this->getModule('Laravel5')->client->getCrawler()->filter($selector); + } + + public function uploadFileWithForm($selectorOrForm, $uri, $fieldName, $fileNames) + { + // find form + if (!$selectorOrForm instanceof Form) { + $form = $this->matchForm($selectorOrForm); + } else { + $form = $selectorOrForm; + } + + // Make sure fileNames is array + if (!is_array($fileNames)) { + $fileNames = [$fileNames]; + } + + /** @var $file \Symfony\Component\DomCrawler\Field\FileFormField */ + $file = $form->get($fieldName); + + // Upload files + foreach ($fileNames as $fileName) { + $filePath = Configuration::dataDir() . $fileName; + if (!is_readable($filePath)) { + $this->fail("file $filePath not found in Codeception data path. Only files stored in data path accepted"); + } + + // Attach file to form file + $file->upload($filePath); + + $this->debugSection('Uri', $uri); + $this->debugSection($form->getMethod(), $form->getValues()); + $this->debugSection('Files', $form->getPhpFiles()); + + // Upload files request + $module = $this->getModule('Laravel5'); + $module->client->request($form->getMethod(), $uri, $form->getPhpValues(), $form->getPhpFiles()); + $this->debugResponse(); + } + } } diff --git a/tests/functional/CrudAttachmentCest.php b/tests/functional/CrudAttachmentCest.php new file mode 100644 index 000000000..61c03d3d8 --- /dev/null +++ b/tests/functional/CrudAttachmentCest.php @@ -0,0 +1,162 @@ +am('Manager User'); + $I->wantTo('add new issue to a project with attachment'); + + $manager = $I->createUser(1, 3); + $I->amLoggedAs($manager); + $project = $I->createProject(1); + $I->amOnAction('Project\IssueController@getNew', ['project' => $project]); + $uri = $I->getApplication()->url->action('Project\IssueController@postUploadAttachment', [ + 'project' => $project + ]); + $I->submitFormWithFileToUri('.form-horizontal', $uri, ['upload' => $fileName], [ + 'title' => $title, + 'body' => $body, + ]); + $I->seeResponseCodeIs(200); + $issue = $I->fetchIssueBy('title', $title); + $I->seeCurrentActionIs('Project\IssueController@getIndex', ['project' => $project, 'issue' => $issue]); + $I->amOnAction('Project\IssueController@getIndex', ['project' => $project, 'issue' => $issue]); + $I->seeResponseCodeIs(200); + $I->seeLink($title); + $I->seeLink($fileName); + $I->see($fileName, '.attachments'); + $I->see($body, '.content'); + $I->seeElement('.attachments a', ['title' => $fileName]); + $attachment = $issue->attachments->first(); + $I->amOnAction('Project\IssueController@getDisplayAttachment', [ + 'project' => $project, + 'issue' => $issue, + 'attachment' => $attachment + ]); + $I->seeResponseCodeIs(200); + } + + /** + * @param FunctionalTester $I + * + * @actor FunctionalTester + * + * @return void + */ + public function addIssueComment(FunctionalTester $I) + { + $comment = 'Comment 1'; + $fileName1 = 'upload1.txt'; + $fileName2 = 'upload2.txt'; + + $I->am('Manager User'); + $I->wantTo('add new comment to a project issue with attachments'); + + $manager = $I->createUser(1, 3); + $I->amLoggedAs($manager); + $issue = $I->createIssue(1, $manager); + $project = $issue->project; + $I->amOnAction('Project\IssueController@getIndex', ['project' => $project, 'issue' => $issue]); + $I->seeResponseCodeIs(200); + + $uri = $I->getApplication()->url->action('Project\IssueController@postUploadAttachment', [ + 'project' => $project + ]); + $I->submitFormWithFileToUri('.new-comment form', $uri, ['upload' => [$fileName1, $fileName2]], [ + 'comment' => $comment, + ]); + $I->seeResponseCodeIs(200); + $I->seeCurrentActionIs('Project\IssueController@getIndex', ['project' => $project, 'issue' => $issue]); + $I->amOnAction('Project\IssueController@getIndex', ['project' => $project, 'issue' => $issue]); + $I->see($comment, '.comment .content'); + $I->seeLink($fileName1); + $I->seeLink($fileName2); + $I->see($fileName1, '.attachments'); + $I->see($fileName2, '.attachments'); + $attachments = $issue->comments->first()->attachments; + foreach ($attachments as $attachment) { + $I->amOnAction('Project\IssueController@getDisplayAttachment', [ + 'project' => $project, + 'issue' => $issue, + 'attachment' => $attachment + ]); + $I->seeResponseCodeIs(200); + } + } + + /** + * @param FunctionalTester $I + * + * @actor FunctionalTester + * + * @return void + */ + public function removeAttachment(FunctionalTester $I) + { + $I->am('Manager User'); + $I->wantTo('remove an attachment from a project issue comments'); + + $fileName = 'upload1.txt'; + + $manager = $I->createUser(1, 3); + $I->amLoggedAs($manager); + $issue = $I->createIssue(1, $manager); + $project = $issue->project; + $I->amOnAction('Project\IssueController@getIndex', ['project' => $project, 'issue' => $issue]); + $uploadToken = $I->grabValueFrom('//form/input[@name="upload_token"]'); + $uri = $I->getApplication()->url->action('Project\IssueController@postUploadAttachment', [ + 'project' => $project + ]); + $I->submitFormWithFileToUri('.new-comment form', $uri, ['upload' => $fileName], [ + 'comment' => 'Comment 1', + ]); + $attachment = $issue->comments->first()->attachments->first(); + $I->amOnAction('Project\IssueController@getDownloadAttachment', [ + 'project' => $project, + 'issue' => $issue, + 'attachment' => $attachment + ]); + $I->seeResponseCodeIs(200); + $I->amOnAction('Project\IssueController@getIndex', ['project' => $project, 'issue' => $issue]); + $I->seeElement('.attachments a', ['title' => $fileName]); + $uri = $I->getApplication()->url->action('Project\IssueController@postRemoveAttachment', [ + 'project' => $project + ]); + $I->sendAjaxPostRequest($uri, [ + '_token' => csrf_token(), + 'upload_token' => $uploadToken, + 'filename' => $fileName + ]); + $I->amOnAction('Project\IssueController@getIndex', ['project' => $project, 'issue' => $issue]); + $I->dontSeeElement('.attachments a', ['title' => $fileName]); + $I->amOnAction('Project\IssueController@getDisplayAttachment', [ + 'project' => $project, + 'issue' => $issue, + 'attachment' => $attachment + ]); + $I->seeResponseCodeIs(404); + } +} diff --git a/tests/functional/FunctionalTester.php b/tests/functional/FunctionalTester.php index 5d60e8536..6e7b2cd00 100644 --- a/tests/functional/FunctionalTester.php +++ b/tests/functional/FunctionalTester.php @@ -1,4 +1,4 @@ -scenario->runStep(new \Codeception\Step\Action('sendPostRequest', func_get_args())); } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\FunctionalHelper::submitFormWithFileToUri() + */ + public function submitFormWithFileToUri($selector, $uri, $files, $params = null) { + return $this->scenario->runStep(new \Codeception\Step\Action('submitFormWithFileToUri', func_get_args())); + } + + + /** + * [!] Method is generated. Documentation taken from corresponding module. + * + * + * @see \Codeception\Module\FunctionalHelper::uploadFileWithForm() + */ + public function uploadFileWithForm($selectorOrForm, $uri, $fieldName, $fileNames) { + return $this->scenario->runStep(new \Codeception\Step\Action('uploadFileWithForm', func_get_args())); + } }