From d8e71235766a7748ca952bdee5d71b0909aa5744 Mon Sep 17 00:00:00 2001 From: snipe Date: Fri, 27 Jun 2025 11:37:31 +0100 Subject: [PATCH] Added uploaded files API controllers and presenters Signed-off-by: snipe --- app/Exceptions/Handler.php | 10 +- app/Helpers/StorageHelper.php | 13 + .../Api/UploadedFilesController.php | 244 ++++++++++++++++++ app/Http/Requests/UploadFileRequest.php | 65 +++-- .../Transformers/DatatablesTransformer.php | 17 +- .../Transformers/UploadedFilesTransformer.php | 26 +- app/Models/Actionlog.php | 86 ++++-- app/Models/User.php | 2 +- app/Presenters/UploadedFilesPresenter.php | 101 ++++++++ resources/lang/en-US/general.php | 20 ++ routes/api.php | 101 +++----- .../Accessories/Api/AccessoryFilesTest.php | 194 ++++++++++++++ .../AssetModels/Api/AssetModelFilesTest.php | 176 ++++++++----- tests/Feature/Assets/Api/AssetFilesTest.php | 54 ++-- .../Components/Api/ComponentFileTest.php | 192 ++++++++++++++ .../Consumables/Api/ConsumableFileTest.php | 194 ++++++++++++++ .../Licenses/Api/LicenseUploadTest.php | 194 ++++++++++++++ .../Locations/Api/LocationFileTest.php | 192 ++++++++++++++ tests/Feature/Users/Api/UserFileTest.php | 191 ++++++++++++++ 19 files changed, 1857 insertions(+), 215 deletions(-) create mode 100644 app/Http/Controllers/Api/UploadedFilesController.php create mode 100644 app/Presenters/UploadedFilesPresenter.php create mode 100644 tests/Feature/Accessories/Api/AccessoryFilesTest.php create mode 100644 tests/Feature/Components/Api/ComponentFileTest.php create mode 100644 tests/Feature/Consumables/Api/ConsumableFileTest.php create mode 100644 tests/Feature/Licenses/Api/LicenseUploadTest.php create mode 100644 tests/Feature/Locations/Api/LocationFileTest.php create mode 100644 tests/Feature/Users/Api/UserFileTest.php diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index f544180750..dc5057cb3d 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -116,9 +116,17 @@ class Handler extends ExceptionHandler return response()->json(Helper::formatStandardApiResponse('error', null, 'Method not allowed'), 405); default: return response()->json(Helper::formatStandardApiResponse('error', null, $statusCode), $statusCode); - } + } + + // This handles API validation exceptions that happen at the Form Request level, so they + // never even get to the controller where we normally nicely format JSON responses + if ($e instanceof ValidationException) { + $response = $this->invalidJson($request, $e); + return response()->json(Helper::formatStandardApiResponse('error', null, $e->errors()), 200); + } + } diff --git a/app/Helpers/StorageHelper.php b/app/Helpers/StorageHelper.php index 47700f913a..38a5769f1e 100644 --- a/app/Helpers/StorageHelper.php +++ b/app/Helpers/StorageHelper.php @@ -48,6 +48,7 @@ class StorageHelper 'avif', 'webp', 'png', + 'gif', ]; @@ -59,6 +60,18 @@ class StorageHelper } + public static function getFiletype($file_with_path) { + + // The file exists and is allowed to be displayed inline + if (Storage::exists($file_with_path)) { + return pathinfo($file_with_path, PATHINFO_EXTENSION); + } + + return null; + + } + + /** * Decide whether to show the file inline or download it. */ diff --git a/app/Http/Controllers/Api/UploadedFilesController.php b/app/Http/Controllers/Api/UploadedFilesController.php new file mode 100644 index 0000000000..eb66e73dbf --- /dev/null +++ b/app/Http/Controllers/Api/UploadedFilesController.php @@ -0,0 +1,244 @@ + Accessory::class, + 'assets' => Asset::class, + 'components' => Component::class, + 'consumables' => Consumable::class, + 'licenses' => License::class, + 'locations' => Location::class, + 'models' => AssetModel::class, + 'users' => User::class, + ]; + + static $map_storage_path = [ + 'accessories' => 'private_uploads/accessories/', + 'assets' => 'private_uploads/assets/', + 'components' => 'private_uploads/components/', + 'consumables' => 'private_uploads/consumables/', + 'licenses' => 'private_uploads/licenses/', + 'locations' => 'private_uploads/locations/', + 'models' => 'private_uploads/assetmodels/', + 'users' => 'private_uploads/users/', + ]; + + static $map_file_prefix= [ + 'accessories' => 'accessory', + 'assets' => 'asset', + 'components' => 'component', + 'consumables' => 'consumable', + 'licenses' => 'license', + 'locations' => 'location', + 'models' => 'model', + 'users' => 'user', + ]; + + + + + /** + * List the files for an object. + * + * @since [v7.0.12] + * @author [r-xyz] + */ + public function index(Request $request, $object_type, $id) : JsonResponse | array + { + + $object = self::$map_object_type[$object_type]::find($id); + $this->authorize('view', $object); + + if (!$object) { + return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object'))); + } + + // Columns allowed for sorting + $allowed_columns = + [ + 'id', + 'filename', + 'action_type', + 'note', + 'created_at', + ]; + + $uploads = $object->uploads(); + $offset = ($request->input('offset') > $object->count()) ? $object->count() : abs($request->input('offset')); + $limit = app('api_limit_value'); + $order = $request->input('order') === 'asc' ? 'asc' : 'desc'; + $sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'action_logs.created_at'; + + // Text search on action_logs fields + // We could use the normal Actionlogs text scope, but it's a very heavy query since it's searcghing across all relations + // And we generally won't need that here + if ($request->filled('search')) { + + $uploads->where(function ($query) use ($request) { + $query->where('filename', 'LIKE', '%' . $request->input('search') . '%') + ->orWhere('note', 'LIKE', '%' . $request->input('search') . '%'); + }); + } + + $uploads = $uploads->skip($offset)->take($limit)->orderBy($sort, $order)->get(); + return (new UploadedFilesTransformer())->transformFiles($uploads, $uploads->count()); + } + + + /** + * Accepts a POST to upload a file to the server. + * + * @param \App\Http\Requests\UploadFileRequest $request + * @param int $assetModelId + * @since [v7.0.12] + * @author [r-xyz] + */ + public function store(UploadFileRequest $request, $object_type, $id) : JsonResponse + { + + $object = self::$map_object_type[$object_type]::find($id); + $this->authorize('view', $object); + + if (!$object) { + return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object'))); + } + + // If the file storage directory doesn't exist, create it + if (! Storage::exists(self::$map_storage_path[$object_type])) { + Storage::makeDirectory(self::$map_storage_path[$object_type], 775); + } + + + if ($request->hasFile('file')) { + // Loop over the attached files and add them to the object + foreach ($request->file('file') as $file) { + $file_name = $request->handleFile(self::$map_storage_path[$object_type],self::$map_file_prefix[$object_type].'-'.$object->id, $file); + $files[] = $file_name; + $object->logUpload($file_name, $request->get('notes')); + } + + $files = Actionlog::select('action_logs.*')->where('action_type', '=', 'uploaded') + ->where('item_type', '=', self::$map_object_type[$object_type]) + ->where('item_id', '=', $id)->whereIn('filename', $files) + ->get(); + + return response()->json(Helper::formatStandardApiResponse('success', (new UploadedFilesTransformer())->transformFiles($files, count($files)), trans_choice('general.file_upload_status.upload.success', count($files)))); + } + + // No files were submitted + return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.nofiles'))); + } + + + + /** + * Check for permissions and display the file. + * + * @param AssetModel $model + * @param int $fileId + * @return \Illuminate\Http\JsonResponse + * @throws \Illuminate\Auth\Access\AuthorizationException + * @since [v7.0.12] + * @author [r-xyz] + */ + public function show($object_type, $id, $file_id) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse + { + $object = self::$map_object_type[$object_type]::find($id); + $this->authorize('view', $object); + + if (!$object) { + return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object'))); + } + + + // Check that the file being requested exists for the asset + if (! $log = Actionlog::whereNotNull('filename') + ->where('item_type', AssetModel::class) + ->where('item_id', $object->id)->find($file_id)) { + return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_id')), 200); + } + + + if (! Storage::exists(self::$map_storage_path[$object_type].'/'.$log->filename)) { + return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.file_not_found'), 200)); + } + + if (request('inline') == 'true') { + $headers = [ + 'Content-Disposition' => 'inline', + ]; + return Storage::download(self::$map_storage_path[$object_type].'/'.$log->filename, $log->filename, $headers); + } + + return StorageHelper::downloader(self::$map_storage_path[$object_type].'/'.$log->filename); + + } + + /** + * Delete the associated file + * + * @param AssetModel $model + * @param int $fileId + * @since [v7.0.12] + * @author [r-xyz] + */ + public function destroy($object_type, $id, $file_id) : JsonResponse + { + + $object = self::$map_object_type[$object_type]::find($id); + $this->authorize('update', self::$map_object_type[$object_type]); + + if (!$object) { + return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object'))); + } + + + // Check for the file + $log = Actionlog::find($file_id)->where('item_type', self::$map_object_type[$object_type]) + ->where('item_id', $object->id)->first(); + + if ($log) { + // Check the file actually exists, and delete it + if (Storage::exists(self::$map_storage_path[$object_type].'/'.$log->filename)) { + Storage::delete(self::$map_storage_path[$object_type].'/'.$log->filename); + } + // Delete the record of the file + if ($log->delete()) { + return response()->json(Helper::formatStandardApiResponse('success', null, trans_choice('general.file_upload_status.delete.success', 1)), 200); + } + + + } + + // The file doesn't seem to really exist, so report an error + return response()->json(Helper::formatStandardApiResponse('error', null, trans_choice('general.file_upload_status.delete.error', 1)), 500); + + } +} \ No newline at end of file diff --git a/app/Http/Requests/UploadFileRequest.php b/app/Http/Requests/UploadFileRequest.php index e58f1a1be3..1150107f2f 100644 --- a/app/Http/Requests/UploadFileRequest.php +++ b/app/Http/Requests/UploadFileRequest.php @@ -6,6 +6,7 @@ use App\Http\Traits\ConvertsBase64ToFiles; use enshrined\svgSanitize\Sanitizer; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Log; +use \App\Helpers\Helper; class UploadFileRequest extends Request { @@ -27,44 +28,62 @@ class UploadFileRequest extends Request */ public function rules() { - $max_file_size = \App\Helpers\Helper::file_upload_max_size(); + $max_file_size = Helper::file_upload_max_size(); return [ - 'file.*' => 'required|mimes:png,gif,jpg,svg,jpeg,doc,docx,pdf,txt,zip,rar,xls,xlsx,lic,xml,rtf,json,webp,avif|max:'.$max_file_size, + 'file.*' => 'required|mimes:png,gif,jpg,svg,jpeg,doc,docx,pdf,txt,zip,rar,xls,xlsx,lic,xml,rtf,json,webp,avif|max:'.$max_file_size, ]; } /** * Sanitizes (if needed) and Saves a file to the appropriate location * Returns the 'short' (storage-relative) filename - * - * TODO - this has a lot of similarities to UploadImageRequest's handleImage; is there - * a way to merge them or extend one into the other? */ public function handleFile(string $dirname, string $name_prefix, $file): string { - $extension = $file->getClientOriginalExtension(); - $file_name = $name_prefix.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$file->guessExtension(); + + $file_name = $name_prefix.'-'.str_random(8).'-'.str_replace(' ', '-', $file->getClientOriginalName()); // Check for SVG and sanitize it if ($file->getMimeType() === 'image/svg+xml') { - Log::debug('This is an SVG'); - Log::debug($file_name); - - $sanitizer = new Sanitizer(); - $dirtySVG = file_get_contents($file->getRealPath()); - $cleanSVG = $sanitizer->sanitize($dirtySVG); - - try { - Storage::put($dirname.$file_name, $cleanSVG); - } catch (\Exception $e) { - Log::debug('Upload no workie :( '); - Log::debug($e); - } - + $uploaded_file = $this->handleSVG($file); } else { - $put_results = Storage::put($dirname.$file_name, file_get_contents($file)); + $uploaded_file = file_get_contents($file); } + + try { + Storage::put($dirname.$file_name, $uploaded_file); + } catch (\Exception $e) { + Log::debug($e); + } + return $file_name; } -} + + public function handleSVG($file) { + $sanitizer = new Sanitizer(); + $dirtySVG = file_get_contents($file->getRealPath()); + return $sanitizer->sanitize($dirtySVG); + } + + + /** + * Get the validation error messages that apply to the request, but + * replace the attribute name with the name of the file that was attempted and failed + * to make it clearer to the user which file is the bad one. + * @return array + */ + public function attributes(): array + { + $attributes = []; + + if ($this->file) { + for ($i = 0; $i < count($this->file); $i++) { + $attributes['file.'.$i] = $this->file[$i]->getClientOriginalName(); + } + } + + return $attributes; + + } +} \ No newline at end of file diff --git a/app/Http/Transformers/DatatablesTransformer.php b/app/Http/Transformers/DatatablesTransformer.php index 0e69109391..2ec993536d 100644 --- a/app/Http/Transformers/DatatablesTransformer.php +++ b/app/Http/Transformers/DatatablesTransformer.php @@ -4,6 +4,10 @@ namespace App\Http\Transformers; class DatatablesTransformer { + + /** + * Transform data for bootstrap tables and API responses for lists of things + **/ public function transformDatatables($objects, $total = null) { (isset($total)) ? $objects_array['total'] = $total : $objects_array['total'] = count($objects); @@ -11,4 +15,15 @@ class DatatablesTransformer return $objects_array; } -} + + /** + * Transform data for returning the status of items within a bulk action + **/ + public function transformBulkResponseWithStatusAndObjects($objects, $total) + { + (isset($total)) ? $objects_array['total'] = $total : $objects_array['total'] = count($objects); + $objects_array['rows'] = $objects; + + return $objects_array; + } +} \ No newline at end of file diff --git a/app/Http/Transformers/UploadedFilesTransformer.php b/app/Http/Transformers/UploadedFilesTransformer.php index a18c9f9b65..48aebbe34a 100644 --- a/app/Http/Transformers/UploadedFilesTransformer.php +++ b/app/Http/Transformers/UploadedFilesTransformer.php @@ -3,10 +3,10 @@ namespace App\Http\Transformers; use App\Helpers\Helper; +use App\Helpers\StorageHelper; use App\Models\Actionlog; -use App\Models\Asset; -use Illuminate\Support\Facades\Gate; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Storage; class UploadedFilesTransformer @@ -26,23 +26,26 @@ class UploadedFilesTransformer { $snipeModel = $file->item_type; - - // This will be used later as we extend out this transformer to handle more types of uploads - if ($file->item_type == Asset::class) { - $file_url = route('show/assetfile', [$file->item_id, $file->id]); - } - $array = [ 'id' => (int) $file->id, + 'icon' => Helper::filetype_icon($file->filename), + 'name' => e($file->filename), + 'item' => ($file->item_type) ? [ + 'id' => (int) $file->item_id, + 'type' => strtolower(class_basename($file->item_type)), + ] : null, 'filename' => e($file->filename), - 'url' => $file_url, + 'filetype' => StorageHelper::getFiletype($file->uploads_file_path()), + 'url' => $file->uploads_file_url(), + 'note' => ($file->note) ? e($file->note) : null, 'created_by' => ($file->adminuser) ? [ 'id' => (int) $file->adminuser->id, 'name'=> e($file->adminuser->present()->fullName), ] : null, 'created_at' => Helper::getFormattedDateObject($file->created_at, 'datetime'), - 'updated_at' => Helper::getFormattedDateObject($file->updated_at, 'datetime'), 'deleted_at' => Helper::getFormattedDateObject($file->deleted_at, 'datetime'), + 'inline' => StorageHelper::allowSafeInline($file->uploads_file_path()), + 'exists_on_disk' => (Storage::exists($file->uploads_file_path()) ? true : false), ]; $permissions_array['available_actions'] = [ @@ -53,4 +56,5 @@ class UploadedFilesTransformer return $array; } -} + +} \ No newline at end of file diff --git a/app/Models/Actionlog.php b/app/Models/Actionlog.php index dd8dcb2c44..d31a459851 100755 --- a/app/Models/Actionlog.php +++ b/app/Models/Actionlog.php @@ -69,8 +69,8 @@ class Actionlog extends SnipeModel */ protected $searchableRelations = [ 'company' => ['name'], - 'adminuser' => ['first_name','last_name','username', 'email', 'employee_num'], - 'user' => ['first_name','last_name','username', 'email', 'employee_num'], + 'adminuser' => ['first_name','last_name','username', 'email'], + 'user' => ['first_name','last_name','username', 'email'], 'assets' => ['asset_tag','name', 'serial', 'order_number', 'notes', 'purchase_date'], 'assets.model' => ['name', 'model_number', 'eol', 'notes'], 'assets.model.category' => ['name', 'notes'], @@ -113,13 +113,7 @@ class Actionlog extends SnipeModel } elseif (auth()->user() && auth()->user()->company) { $actionlog->company_id = auth()->user()->company_id; } - - if ($actionlog->action_date == '') { - $actionlog->action_date = Carbon::now(); - } - }); - } @@ -251,8 +245,8 @@ class Actionlog extends SnipeModel public function uploads() { return $this->morphTo('item') - ->where('action_type', '=', 'uploaded') - ->withTrashed(); + ->where('action_type', '=', 'uploaded') + ->withTrashed(); } /** @@ -277,7 +271,7 @@ class Actionlog extends SnipeModel public function adminuser() { return $this->belongsTo(User::class, 'created_by') - ->withTrashed(); + ->withTrashed(); } /** @@ -382,7 +376,7 @@ class Actionlog extends SnipeModel if ($this->created_at > $override_default_next) { $next_audit_days = '-'.$next_audit_days; } - + return $next_audit_days; } @@ -414,10 +408,10 @@ class Actionlog extends SnipeModel public function getListingOfActionLogsChronologicalOrder() { return $this->all() - ->where('action_type', '!=', 'uploaded') - ->orderBy('item_id', 'asc') - ->orderBy('created_at', 'asc') - ->get(); + ->where('action_type', '!=', 'uploaded') + ->orderBy('item_id', 'asc') + ->orderBy('created_at', 'asc') + ->get(); } /** @@ -440,7 +434,7 @@ class Actionlog extends SnipeModel return 'api'; } - // This is probably NOT an API call + // This is probably NOT an API call if (request()->filled('_token')) { return 'gui'; } @@ -450,6 +444,62 @@ class Actionlog extends SnipeModel } + public function uploads_file_url() + { + + switch ($this->item_type) { + case Accessory::class: + return route('show.accessoryfile', [$this->item_id, $this->id]); + case Asset::class: + return route('show/assetfile', [$this->item_id, $this->id]); + case AssetModel::class: + return route('show/modelfile', [$this->item_id, $this->id]); + case Consumable::class: + return route('show/locationsfile', [$this->item_id, $this->id]); + case Component::class: + return route('show.componentfile', [$this->item_id, $this->id]); + case License::class: + return route('show.licensefile', [$this->item_id, $this->id]); + case Location::class: + return route('show/locationsfile', [$this->item_id, $this->id]); + case User::class: + return route('show/userfile', [$this->item_id, $this->id]); + default: + return null; + } + } + + public function uploads_file_path() + { + + switch ($this->item_type) { + case Accessory::class: + return 'private_uploads/accessories/'.$this->filename; + case Asset::class: + return 'private_uploads/assets/'.$this->filename; + case AssetModel::class: + return 'private_uploads/assetmodels/'.$this->filename; + case Consumable::class: + return 'private_uploads/consumables/'.$this->filename; + case Component::class: + return 'private_uploads/components/'.$this->filename; + case License::class: + return 'private_uploads/licenses/'.$this->filename; + case Location::class: + return 'private_uploads/locations/'.$this->filename; + case User::class: + return 'private_uploads/users/'.$this->filename; + default: + return null; + } + } + + + + + + + // Manually sets $this->source for determineActionSource() public function setActionSource($source = null): void { @@ -460,4 +510,4 @@ class Actionlog extends SnipeModel { return $query->leftJoin('users as admin_sort', 'action_logs.created_by', '=', 'admin_sort.id')->select('action_logs.*')->orderBy('admin_sort.first_name', $order)->orderBy('admin_sort.last_name', $order); } -} +} \ No newline at end of file diff --git a/app/Models/User.php b/app/Models/User.php index 55d79808ca..9ae50c2fa9 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -29,7 +29,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo use CompanyableTrait; protected $presenter = \App\Presenters\UserPresenter::class; - use SoftDeletes, ValidatingTrait; + use SoftDeletes, ValidatingTrait, Loggable; use Authenticatable, Authorizable, CanResetPassword, HasApiTokens; use UniqueUndeletedTrait; use Notifiable; diff --git a/app/Presenters/UploadedFilesPresenter.php b/app/Presenters/UploadedFilesPresenter.php new file mode 100644 index 0000000000..26614def56 --- /dev/null +++ b/app/Presenters/UploadedFilesPresenter.php @@ -0,0 +1,101 @@ + 'id', + 'searchable' => false, + 'sortable' => true, + 'switchable' => true, + 'title' => trans('general.id'), + 'visible' => false, + ], + [ + 'field' => 'icon', + 'searchable' => false, + 'sortable' => false, + 'switchable' => false, + 'title' => trans('general.type'), + 'formatter' => 'iconFormatter', + ], + [ + 'field' => 'image', + 'searchable' => false, + 'sortable' => false, + 'switchable' => true, + 'title' => trans('general.image'), + 'formatter' => 'inlineImageFormatter', + ], + [ + 'field' => 'filename', + 'searchable' => false, + 'sortable' => false, + 'switchable' => true, + 'title' => trans('general.file_name'), + 'visible' => true, + 'formatter' => 'fileUploadNameFormatter', + ], + [ + 'field' => 'download', + 'searchable' => false, + 'sortable' => false, + 'switchable' => true, + 'title' => trans('general.download'), + 'visible' => true, + 'formatter' => 'downloadOrOpenInNewWindowFormatter', + ], + [ + 'field' => 'note', + 'searchable' => true, + 'sortable' => true, + 'switchable' => true, + 'title' => trans('general.notes'), + 'visible' => true, + ], + [ + 'field' => 'created_by', + 'searchable' => false, + 'sortable' => true, + 'title' => trans('general.created_by'), + 'visible' => false, + 'formatter' => 'usersLinkObjFormatter', + ], + [ + 'field' => 'created_at', + 'searchable' => true, + 'sortable' => true, + 'switchable' => true, + 'title' => trans('general.created_at'), + 'visible' => false, + 'formatter' => 'dateDisplayFormatter', + ], [ + 'field' => 'available_actions', + 'searchable' => false, + 'sortable' => false, + 'switchable' => false, + 'title' => trans('table.actions'), + 'formatter' => 'deleteUploadFormatter', + ], + ]; + + return json_encode($layout); + } + +} \ No newline at end of file diff --git a/resources/lang/en-US/general.php b/resources/lang/en-US/general.php index ffed385d38..361f5cf3a5 100644 --- a/resources/lang/en-US/general.php +++ b/resources/lang/en-US/general.php @@ -640,4 +640,24 @@ return [ ], ], + 'file_upload_status' => [ + + 'upload' => [ + 'success' => 'File successfully uploaded |:count files successfully uploaded', + 'error' => 'File upload failed |:count file uploads failed', + ], + + 'delete' => [ + 'success' => 'File successfully deleted |:count files successfully deleted', + 'error' => 'File deletion failed |:count file deletions failed', + ], + + 'file_not_found' => 'The selected file was not found on server', + 'invalid_id' => 'That file ID is invalid', + 'invalid_object' => 'That object ID is invalid', + 'nofiles' => 'No files were included for upload', + 'confirm_delete' => 'Are you sure you want to delete this file?', + ], + + ]; diff --git a/routes/api.php b/routes/api.php index 71b821ea0f..389e9f45dd 100644 --- a/routes/api.php +++ b/routes/api.php @@ -556,35 +556,6 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'api-throttle:api']], fu ] )->name('api.assets.restore'); - Route::post('{asset}/files', - [ - Api\AssetFilesController::class, - 'store' - ] - )->name('api.assets.files.store'); - - Route::get('{asset}/files', - [ - Api\AssetFilesController::class, - 'list' - ] - )->name('api.assets.files.index'); - - Route::get('{asset_id}/file/{file_id}', - [ - Api\AssetFilesController::class, - 'show' - ] - )->name('api.assets.files.show'); - - Route::delete('{asset_id}/file/{file_id}', - [ - Api\AssetFilesController::class, - 'destroy' - ] - )->name('api.assets.files.destroy'); - - /** Begin assigned routes */ Route::get('{asset}/assigned/assets', @@ -853,33 +824,6 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'api-throttle:api']], fu ] )->name('api.models.restore'); - Route::post('{model_id}/files', - [ - Api\AssetModelFilesController::class, - 'store' - ] - )->name('api.models.files.store'); - - Route::get('{model_id}/files', - [ - Api\AssetModelFilesController::class, - 'list' - ] - )->name('api.models.files.index'); - - Route::get('{model_id}/file/{file_id}', - [ - Api\AssetModelFilesController::class, - 'show' - ] - )->name('api.models.files.show'); - - Route::delete('{model_id}/file/{file_id}', - [ - Api\AssetModelFilesController::class, - 'destroy' - ] - )->name('api.models.files.destroy'); }); Route::resource('models', @@ -1144,12 +1088,6 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'api-throttle:api']], fu ] )->name('api.users.licenselist'); - Route::post('{user}/upload', - [ - Api\UsersController::class, - 'postUpload' - ] - )->name('api.users.uploads'); Route::post('{user}/restore', [ @@ -1365,5 +1303,44 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'api-throttle:api']], fu ])->name('api.assets.labels'); // end generate label routes + /** + * Uploaded files API routes + */ + + // List files + Route::get('{object_type}/{id}/files', + [ + Api\UploadedFilesController::class, + 'index' + ] + )->name('api.files.index') + ->where(['object_type' => 'assets|models|users|locations|accessories|consumables|licenses|components']); + + // Get a file + Route::get('{object_type}/{id}/files/{file_id}', + [ + Api\UploadedFilesController::class, + 'show' + ] + )->name('api.files.show') + ->where(['object_type' => 'assets|models|users|locations|accessories|consumables|licenses|components']); + + // Upload files(s) + Route::post('{object_type}/{id}/files', + [ + Api\UploadedFilesController::class, + 'store' + ] + )->name('api.files.store') + ->where(['object_type' => 'assets|models|users|locations|accessories|consumables|licenses|components']); + + // Delete files(s) + Route::delete('{object_type}/{id}/files/{file_id}/delete', + [ + Api\UploadedFilesController::class, + 'destroy' + ] + )->name('api.files.destroy') + ->where(['object_type' => 'assets|models|users|locations|accessories|consumables|licenses|components']); }); // end API routes diff --git a/tests/Feature/Accessories/Api/AccessoryFilesTest.php b/tests/Feature/Accessories/Api/AccessoryFilesTest.php new file mode 100644 index 0000000000..1287eb38bc --- /dev/null +++ b/tests/Feature/Accessories/Api/AccessoryFilesTest.php @@ -0,0 +1,194 @@ +create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + //Upload a file + $this->actingAsForApi($user) + ->post( + route('api.files.store', ['object_type' => 'accessories', 'id' => $accessory->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)] + ]) + ->assertOk(); + } + + public function testAccessoryApiListsFiles() + { + // List all files on a model + + // Create a model to work with + $accessory = Accessory::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + // List the files + $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'accessories', 'id' => $accessory->id])) + ->assertOk() + ->assertJsonStructure([ + 'rows', + 'total', + ]); + } + + public function testAccessoryFailsIfInvalidTypePassedInUrl() + { + // List all files on a model + + // Create an model to work with + $accessory = Accessory::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + // List the files + $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'shibboleeeeeet', 'id' => $accessory->id])) + ->assertStatus(404); + } + + public function testAccessoryFailsIfInvalidIdPassedInUrl() + { + // List all files on a model + + // Create an model to work with + $accessory = Accessory::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + // List the files + $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'accessories', 'id' => 100000])) + ->assertOk() + ->assertStatusMessageIs('error'); + } + + public function testAccessoryApiDownloadsFile() + { + // Download a file from a model + + // Create a model to work with + $accessory = Accessory::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + // Upload a file + $this->actingAsForApi($user) + ->post( + route('api.files.store', ['object_type' => 'accessories', 'id' => $accessory->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)], + ]) + ->assertOk() + ->assertJsonStructure([ + 'status', + 'messages', + ]); + + // Upload a file with notes + $this->actingAsForApi($user) + ->post( + route('api.files.store', ['object_type' => 'accessories', 'id' => $accessory->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)], + 'notes' => 'manual' + ]) + ->assertOk() + ->assertJsonStructure([ + 'status', + 'messages', + ]); + + // List the files to get the file ID + $result = $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'accessories', 'id' => $accessory->id])) + ->assertOk() + ->assertJsonStructure([ + 'total', + 'rows'=>[ + '*' => [ + 'id', + 'filename', + 'url', + 'created_by', + 'created_at', + 'deleted_at', + 'note', + 'available_actions' + ] + ] + ]) + ->assertJsonPath('rows.0.note',null) + ->assertJsonPath('rows.1.note','manual'); + + // Get the file + $this->actingAsForApi($user) + ->get( + route('api.files.show', [ + 'object_type' => 'accessories', + 'id' => $accessory->id, + 'file_id' => $result->decodeResponseJson()->json()["rows"][0]["id"], + ])) + ->assertOk(); + } + + public function testAccessoryApiDeletesFile() + { + // Delete a file from a model + + // Create a model to work with + $accessory = Accessory::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + //Upload a file + $this->actingAsForApi($user) + ->post( + route('api.files.store', ['object_type' => 'accessories', 'id' => $accessory->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)] + ]) + ->assertOk(); + + // List the files to get the file ID + $result = $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'accessories', 'id' => $accessory->id])) + ->assertOk(); + + // Delete the file + $this->actingAsForApi($user) + ->delete( + route('api.files.destroy', [ + 'object_type' => 'accessories', + 'id' => $accessory->id, + 'file_id' => $result->decodeResponseJson()->json()["rows"][0]["id"], + ])) + ->assertOk() + ->assertJsonStructure([ + 'status', + 'messages', + ]); + } +} \ No newline at end of file diff --git a/tests/Feature/AssetModels/Api/AssetModelFilesTest.php b/tests/Feature/AssetModels/Api/AssetModelFilesTest.php index d29ea4f002..93eeca386b 100644 --- a/tests/Feature/AssetModels/Api/AssetModelFilesTest.php +++ b/tests/Feature/AssetModels/Api/AssetModelFilesTest.php @@ -14,39 +14,74 @@ class AssetModelFilesTest extends TestCase // Upload a file to a model // Create a model to work with - $model = AssetModel::factory()->count(1)->create(); + $model = AssetModel::factory()->create(); - // Create a superuser to run this as - $user = User::factory()->superuser()->create(); + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); - //Upload a file - $this->actingAsForApi($user) + //Upload a file + $this->actingAsForApi($user) ->post( - route('api.models.files.store', ['model_id' => $model[0]["id"]]), [ - 'file' => [UploadedFile::fake()->create("test.jpg", 100)] - ]) - ->assertOk(); + route('api.files.store', ['object_type' => 'models', 'id' => $model->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)] + ]) + ->assertOk(); } public function testAssetModelApiListsFiles() { // List all files on a model - + // Create an model to work with - $model = AssetModel::factory()->count(1)->create(); + $model = AssetModel::factory()->create(); - // Create a superuser to run this as - $user = User::factory()->superuser()->create(); + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); - // List the files - $this->actingAsForApi($user) + // List the files + $this->actingAsForApi($user) ->getJson( - route('api.models.files.index', ['model_id' => $model[0]["id"]])) - ->assertOk() - ->assertJsonStructure([ - 'rows', - 'total', - ]); + route('api.files.index', ['object_type' => 'models', 'id' => $model->id])) + ->assertOk() + ->assertJsonStructure([ + 'rows', + 'total', + ]); + } + + public function testAssetModelFailsIfInvalidTypePassedInUrl() + { + // List all files on a model + + // Create an model to work with + $model = AssetModel::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + // List the files + $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'shibboleeeeeet', 'id' => $model->id])) + ->assertStatus(404); + } + + public function testAssetModelFailsIfInvalidIdPassedInUrl() + { + // List all files on a model + + // Create an model to work with + $model = AssetModel::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + // List the files + $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'models', 'id' => 100000])) + ->assertOk() + ->assertStatusMessageIs('error'); } public function testAssetModelApiDownloadsFile() @@ -54,40 +89,40 @@ class AssetModelFilesTest extends TestCase // Download a file from a model // Create a model to work with - $model = AssetModel::factory()->count(1)->create(); + $model = AssetModel::factory()->create(); - // Create a superuser to run this as - $user = User::factory()->superuser()->create(); + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); - // Upload a file - $this->actingAsForApi($user) + // Upload a file + $this->actingAsForApi($user) ->post( - route('api.models.files.store', ['model_id' => $model[0]["id"]]), [ - 'file' => [UploadedFile::fake()->create("test.jpg", 100)], - ]) + route('api.files.store', ['object_type' => 'models', 'id' => $model->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)], + ]) ->assertOk() ->assertJsonStructure([ 'status', 'messages', ]); - // Upload a file with notes - $this->actingAsForApi($user) + // Upload a file with notes + $this->actingAsForApi($user) ->post( - route('api.models.files.store', ['model_id' => $model[0]["id"]]), [ + route('api.files.store', ['object_type' => 'models', 'id' => $model->id]), [ 'file' => [UploadedFile::fake()->create("test.jpg", 100)], 'notes' => 'manual' - ]) + ]) ->assertOk() ->assertJsonStructure([ 'status', 'messages', ]); - // List the files to get the file ID - $result = $this->actingAsForApi($user) + // List the files to get the file ID + $result = $this->actingAsForApi($user) ->getJson( - route('api.models.files.index', ['model_id' => $model[0]["id"]])) + route('api.files.index', ['object_type' => 'models', 'id' => $model->id])) ->assertOk() ->assertJsonStructure([ 'total', @@ -98,26 +133,24 @@ class AssetModelFilesTest extends TestCase 'url', 'created_by', 'created_at', - 'updated_at', 'deleted_at', 'note', 'available_actions' ] ] ]) - ->assertJsonPath('rows.0.note','') + ->assertJsonPath('rows.0.note',null) ->assertJsonPath('rows.1.note','manual'); - - - // Get the file - $this->actingAsForApi($user) + // Get the file + $this->actingAsForApi($user) ->get( - route('api.models.files.show', [ - 'model_id' => $model[0]["id"], - 'file_id' => $result->decodeResponseJson()->json()["rows"][0]["id"], - ])) - ->assertOk(); + route('api.files.show', [ + 'object_type' => 'models', + 'id' => $model->id, + 'file_id' => $result->decodeResponseJson()->json()["rows"][0]["id"], + ])) + ->assertOk(); } public function testAssetModelApiDeletesFile() @@ -125,36 +158,37 @@ class AssetModelFilesTest extends TestCase // Delete a file from a model // Create a model to work with - $model = AssetModel::factory()->count(1)->create(); + $model = AssetModel::factory()->create(); - // Create a superuser to run this as - $user = User::factory()->superuser()->create(); + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); - //Upload a file - $this->actingAsForApi($user) + //Upload a file + $this->actingAsForApi($user) ->post( - route('api.models.files.store', ['model_id' => $model[0]["id"]]), [ - 'file' => [UploadedFile::fake()->create("test.jpg", 100)] - ]) - ->assertOk(); + route('api.files.store', ['object_type' => 'models', 'id' => $model->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)] + ]) + ->assertOk(); - // List the files to get the file ID - $result = $this->actingAsForApi($user) + // List the files to get the file ID + $result = $this->actingAsForApi($user) ->getJson( - route('api.models.files.index', ['model_id' => $model[0]["id"]])) - ->assertOk(); + route('api.files.index', ['object_type' => 'models', 'id' => $model->id])) + ->assertOk(); - // Delete the file - $this->actingAsForApi($user) + // Delete the file + $this->actingAsForApi($user) ->delete( - route('api.models.files.destroy', [ - 'model_id' => $model[0]["id"], - 'file_id' => $result->decodeResponseJson()->json()["rows"][0]["id"], - ])) - ->assertOk() + route('api.files.destroy', [ + 'object_type' => 'models', + 'id' => $model->id, + 'file_id' => $result->decodeResponseJson()->json()["rows"][0]["id"], + ])) + ->assertOk() ->assertJsonStructure([ - 'status', - 'messages', - ]); + 'status', + 'messages', + ]); } -} +} \ No newline at end of file diff --git a/tests/Feature/Assets/Api/AssetFilesTest.php b/tests/Feature/Assets/Api/AssetFilesTest.php index 3e3e576c25..251480338d 100644 --- a/tests/Feature/Assets/Api/AssetFilesTest.php +++ b/tests/Feature/Assets/Api/AssetFilesTest.php @@ -14,7 +14,7 @@ class AssetFilesTest extends TestCase // Upload a file to an asset // Create an asset to work with - $asset = Asset::factory()->count(1)->create(); + $asset = Asset::factory()->create(); // Create a superuser to run this as $user = User::factory()->superuser()->create(); @@ -22,30 +22,30 @@ class AssetFilesTest extends TestCase //Upload a file $this->actingAsForApi($user) ->post( - route('api.assets.files.store', $asset), [ - 'file' => [UploadedFile::fake()->create("test.jpg", 100)] - ]) - ->assertOk(); + route('api.files.store', ['object_type' => 'assets', 'id' => $asset->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)] + ]) + ->assertOk(); } public function testAssetApiListsFiles() { // List all files on an asset - + // Create an asset to work with - $asset = Asset::factory()->count(1)->create(); + $asset = Asset::factory()->create(); // Create a superuser to run this as $user = User::factory()->superuser()->create(); // List the files $this->actingAsForApi($user) - ->getJson(route('api.assets.files.index', $asset)) - ->assertOk() - ->assertJsonStructure([ - 'rows', - 'total', - ]); + ->getJson(route('api.files.index', ['object_type' => 'assets', 'id' => $asset->id])) + ->assertOk() + ->assertJsonStructure([ + 'rows', + 'total', + ]); } public function testAssetApiDownloadsFile() @@ -53,21 +53,21 @@ class AssetFilesTest extends TestCase // Download a file from an asset // Create an asset to work with - $asset = Asset::factory()->count(1)->create(); + $asset = Asset::factory()->create(); // Create a superuser to run this as $user = User::factory()->superuser()->create(); //Upload a file $this->actingAsForApi($user) - ->post(route('api.assets.files.store', $asset), [ - 'file' => [UploadedFile::fake()->create("test.jpg", 100)] - ]) + ->post(route('api.files.store', ['object_type' => 'assets', 'id' => $asset->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)] + ]) ->assertOk(); // List the files to get the file ID $result = $this->actingAsForApi($user) - ->getJson(route('api.assets.files.index', $asset)) + ->getJson(route('api.files.index', ['object_type' => 'assets', 'id' => $asset->id])) ->assertOk(); } @@ -76,7 +76,7 @@ class AssetFilesTest extends TestCase // Delete a file from an asset // Create an asset to work with - $asset = Asset::factory()->count(1)->create(); + $asset = Asset::factory()->create(); // Create a superuser to run this as $user = User::factory()->superuser()->create(); @@ -84,16 +84,16 @@ class AssetFilesTest extends TestCase //Upload a file $this->actingAsForApi($user) ->post( - route('api.assets.files.store', $asset), [ - 'file' => [UploadedFile::fake()->create("test.jpg", 100)] - ]) - ->assertOk(); + route('api.files.store', ['object_type' => 'assets', 'id' => $asset->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)] + ]) + ->assertOk(); // List the files to get the file ID $result = $this->actingAsForApi($user) ->getJson( - route('api.assets.files.index', $asset)) - ->assertOk(); - + route('api.files.index', ['object_type' => 'assets', 'id' => $asset->id])) + ->assertOk(); + } -} +} \ No newline at end of file diff --git a/tests/Feature/Components/Api/ComponentFileTest.php b/tests/Feature/Components/Api/ComponentFileTest.php new file mode 100644 index 0000000000..5b13659fa5 --- /dev/null +++ b/tests/Feature/Components/Api/ComponentFileTest.php @@ -0,0 +1,192 @@ +create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + //Upload a file + $this->actingAsForApi($user) + ->post( + route('api.files.store', ['object_type' => 'components', 'id' => $component->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)] + ]) + ->assertOk(); + } + + public function testComponentApiListsFiles() + { + // List all files on a model + + // Create a model to work with + $component = Component::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + // List the files + $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'components', 'id' => $component->id])) + ->assertOk() + ->assertJsonStructure([ + 'rows', + 'total', + ]); + } + + public function testComponentFailsIfInvalidTypePassedInUrl() + { + // List all files on a model + + // Create an model to work with + $component = Component::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + // List the files + $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'shibboleeeeeet', 'id' => $component->id])) + ->assertStatus(404); + } + + public function testComponentFailsIfInvalidIdPassedInUrl() + { + // List all files on a model + + // Create an model to work with + $component = Component::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + // List the files + $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'components', 'id' => 100000])) + ->assertOk() + ->assertStatusMessageIs('error'); + } + + public function testComponentApiDownloadsFile() + { + // Download a file from a model + + // Create a model to work with + $component = Component::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + // Upload a file + $this->actingAsForApi($user) + ->post( + route('api.files.store', ['object_type' => 'components', 'id' => $component->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)], + ]) + ->assertOk() + ->assertJsonStructure([ + 'status', + 'messages', + ]); + + // Upload a file with notes + $this->actingAsForApi($user) + ->post( + route('api.files.store', ['object_type' => 'components', 'id' => $component->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)], + 'notes' => 'manual' + ]) + ->assertOk() + ->assertJsonStructure([ + 'status', + 'messages', + ]); + + // List the files to get the file ID + $result = $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'components', 'id' => $component->id])) + ->assertOk() + ->assertJsonStructure([ + 'total', + 'rows'=>[ + '*' => [ + 'id', + 'filename', + 'url', + 'created_by', + 'created_at', + 'deleted_at', + 'note', + 'available_actions' + ] + ] + ]) + ->assertJsonPath('rows.0.note',null) + ->assertJsonPath('rows.1.note','manual'); + + // Get the file + $this->actingAsForApi($user) + ->get( + route('api.files.show', [ + 'object_type' => 'components', + 'id' => $component->id, + 'file_id' => $result->decodeResponseJson()->json()["rows"][0]["id"], + ])) + ->assertOk(); + } + + public function testComponentApiDeletesFile() + { + // Delete a file from a model + + // Create a model to work with + $component = Component::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + //Upload a file + $this->actingAsForApi($user) + ->post( + route('api.files.store', ['object_type' => 'components', 'id' => $component->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)] + ]) + ->assertOk(); + + // List the files to get the file ID + $result = $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'components', 'id' => $component->id])) + ->assertOk(); + + // Delete the file + $this->actingAsForApi($user) + ->delete( + route('api.files.destroy', [ + 'object_type' => 'components', + 'id' => $component->id, + 'file_id' => $result->decodeResponseJson()->json()["rows"][0]["id"], + ])) + ->assertOk() + ->assertJsonStructure([ + 'status', + 'messages', + ]); + } +} \ No newline at end of file diff --git a/tests/Feature/Consumables/Api/ConsumableFileTest.php b/tests/Feature/Consumables/Api/ConsumableFileTest.php new file mode 100644 index 0000000000..c515c64d5c --- /dev/null +++ b/tests/Feature/Consumables/Api/ConsumableFileTest.php @@ -0,0 +1,194 @@ +create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + //Upload a file + $this->actingAsForApi($user) + ->post( + route('api.files.store', ['object_type' => 'consumables', 'id' => $consumable->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)] + ]) + ->assertOk(); + } + + public function testConsumableApiListsFiles() + { + // List all files on a model + + // Create a model to work with + $consumable = Consumable::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + // List the files + $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'consumables', 'id' => $consumable->id])) + ->assertOk() + ->assertJsonStructure([ + 'rows', + 'total', + ]); + } + + public function testConsumableFailsIfInvalidTypePassedInUrl() + { + // List all files on a model + + // Create an model to work with + $consumable = Consumable::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + // List the files + $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'shibboleeeeeet', 'id' => $consumable->id])) + ->assertStatus(404); + } + + public function testConsumableFailsIfInvalidIdPassedInUrl() + { + // List all files on a model + + // Create an model to work with + $consumable = Consumable::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + // List the files + $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'consumables', 'id' => 100000])) + ->assertOk() + ->assertStatusMessageIs('error'); + } + + public function testConsumableApiDownloadsFile() + { + // Download a file from a model + + // Create a model to work with + $consumable = Consumable::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + // Upload a file + $this->actingAsForApi($user) + ->post( + route('api.files.store', ['object_type' => 'consumables', 'id' => $consumable->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)], + ]) + ->assertOk() + ->assertJsonStructure([ + 'status', + 'messages', + ]); + + // Upload a file with notes + $this->actingAsForApi($user) + ->post( + route('api.files.store', ['object_type' => 'consumables', 'id' => $consumable->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)], + 'notes' => 'manual' + ]) + ->assertOk() + ->assertJsonStructure([ + 'status', + 'messages', + ]); + + // List the files to get the file ID + $result = $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'consumables', 'id' => $consumable->id])) + ->assertOk() + ->assertJsonStructure([ + 'total', + 'rows'=>[ + '*' => [ + 'id', + 'filename', + 'url', + 'created_by', + 'created_at', + 'deleted_at', + 'note', + 'available_actions' + ] + ] + ]) + ->assertJsonPath('rows.0.note',null) + ->assertJsonPath('rows.1.note','manual'); + + // Get the file + $this->actingAsForApi($user) + ->get( + route('api.files.show', [ + 'object_type' => 'consumables', + 'id' => $consumable->id, + 'file_id' => $result->decodeResponseJson()->json()["rows"][0]["id"], + ])) + ->assertOk(); + } + + public function testConsumableApiDeletesFile() + { + // Delete a file from a model + + // Create a model to work with + $consumable = Consumable::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + //Upload a file + $this->actingAsForApi($user) + ->post( + route('api.files.store', ['object_type' => 'consumables', 'id' => $consumable->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)] + ]) + ->assertOk(); + + // List the files to get the file ID + $result = $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'consumables', 'id' => $consumable->id])) + ->assertOk(); + + // Delete the file + $this->actingAsForApi($user) + ->delete( + route('api.files.destroy', [ + 'object_type' => 'consumables', + 'id' => $consumable->id, + 'file_id' => $result->decodeResponseJson()->json()["rows"][0]["id"], + ])) + ->assertOk() + ->assertJsonStructure([ + 'status', + 'messages', + ]); + } +} \ No newline at end of file diff --git a/tests/Feature/Licenses/Api/LicenseUploadTest.php b/tests/Feature/Licenses/Api/LicenseUploadTest.php new file mode 100644 index 0000000000..a35151074f --- /dev/null +++ b/tests/Feature/Licenses/Api/LicenseUploadTest.php @@ -0,0 +1,194 @@ +create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + //Upload a file + $this->actingAsForApi($user) + ->post( + route('api.files.store', ['object_type' => 'licenses', 'id' => $license->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)] + ]) + ->assertOk(); + } + + public function testLicenseApiListsFiles() + { + // List all files on a model + + // Create an model to work with + $license = License::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + // List the files + $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'licenses', 'id' => $license->id])) + ->assertOk() + ->assertJsonStructure([ + 'rows', + 'total', + ]); + } + + public function testLicenseFailsIfInvalidTypePassedInUrl() + { + // List all files on a model + + // Create an model to work with + $license = License::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + // List the files + $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'shibboleeeeeet', 'id' => $license->id])) + ->assertStatus(404); + } + + public function testLicenseFailsIfInvalidIdPassedInUrl() + { + // List all files on a model + + // Create an model to work with + $license = License::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + // List the files + $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'licenses', 'id' => 100000])) + ->assertOk() + ->assertStatusMessageIs('error'); + } + + public function testLicenseApiDownloadsFile() + { + // Download a file from a model + + // Create a model to work with + $license = License::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + // Upload a file + $this->actingAsForApi($user) + ->post( + route('api.files.store', ['object_type' => 'licenses', 'id' => $license->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)], + ]) + ->assertOk() + ->assertJsonStructure([ + 'status', + 'messages', + ]); + + // Upload a file with notes + $this->actingAsForApi($user) + ->post( + route('api.files.store', ['object_type' => 'licenses', 'id' => $license->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)], + 'notes' => 'manual' + ]) + ->assertOk() + ->assertJsonStructure([ + 'status', + 'messages', + ]); + + // List the files to get the file ID + $result = $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'licenses', 'id' => $license->id])) + ->assertOk() + ->assertJsonStructure([ + 'total', + 'rows'=>[ + '*' => [ + 'id', + 'filename', + 'url', + 'created_by', + 'created_at', + 'deleted_at', + 'note', + 'available_actions' + ] + ] + ]) + ->assertJsonPath('rows.0.note',null) + ->assertJsonPath('rows.1.note','manual'); + + // Get the file + $this->actingAsForApi($user) + ->get( + route('api.files.show', [ + 'object_type' => 'licenses', + 'id' => $license->id, + 'file_id' => $result->decodeResponseJson()->json()["rows"][0]["id"], + ])) + ->assertOk(); + } + + public function testLicenseApiDeletesFile() + { + // Delete a file from a model + + // Create a model to work with + $license = License::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + //Upload a file + $this->actingAsForApi($user) + ->post( + route('api.files.store', ['object_type' => 'licenses', 'id' => $license->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)] + ]) + ->assertOk(); + + // List the files to get the file ID + $result = $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'licenses', 'id' => $license->id])) + ->assertOk(); + + // Delete the file + $this->actingAsForApi($user) + ->delete( + route('api.files.destroy', [ + 'object_type' => 'licenses', + 'id' => $license->id, + 'file_id' => $result->decodeResponseJson()->json()["rows"][0]["id"], + ])) + ->assertOk() + ->assertJsonStructure([ + 'status', + 'messages', + ]); + } +} \ No newline at end of file diff --git a/tests/Feature/Locations/Api/LocationFileTest.php b/tests/Feature/Locations/Api/LocationFileTest.php new file mode 100644 index 0000000000..370446d321 --- /dev/null +++ b/tests/Feature/Locations/Api/LocationFileTest.php @@ -0,0 +1,192 @@ +create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + //Upload a file + $this->actingAsForApi($user) + ->post( + route('api.files.store', ['object_type' => 'locations', 'id' => $location->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)] + ]) + ->assertOk(); + } + + public function testLocationApiListsFiles() + { + // List all files on a model + + // Create a model to work with + $location = Location::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + // List the files + $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'locations', 'id' => $location->id])) + ->assertOk() + ->assertJsonStructure([ + 'rows', + 'total', + ]); + } + + public function testLocationFailsIfInvalidTypePassedInUrl() + { + // List all files on a model + + // Create an model to work with + $location = Location::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + // List the files + $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'shibboleeeeeet', 'id' => $location->id])) + ->assertStatus(404); + } + + public function testLocationFailsIfInvalidIdPassedInUrl() + { + // List all files on a model + + // Create an model to work with + $location = Location::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + // List the files + $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'locations', 'id' => 100000])) + ->assertOk() + ->assertStatusMessageIs('error'); + } + + public function testLocationApiDownloadsFile() + { + // Download a file from a model + + // Create a model to work with + $location = Location::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + // Upload a file + $this->actingAsForApi($user) + ->post( + route('api.files.store', ['object_type' => 'locations', 'id' => $location->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)], + ]) + ->assertOk() + ->assertJsonStructure([ + 'status', + 'messages', + ]); + + // Upload a file with notes + $this->actingAsForApi($user) + ->post( + route('api.files.store', ['object_type' => 'locations', 'id' => $location->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)], + 'notes' => 'manual' + ]) + ->assertOk() + ->assertJsonStructure([ + 'status', + 'messages', + ]); + + // List the files to get the file ID + $result = $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'locations', 'id' => $location->id])) + ->assertOk() + ->assertJsonStructure([ + 'total', + 'rows'=>[ + '*' => [ + 'id', + 'filename', + 'url', + 'created_by', + 'created_at', + 'deleted_at', + 'note', + 'available_actions' + ] + ] + ]) + ->assertJsonPath('rows.0.note',null) + ->assertJsonPath('rows.1.note','manual'); + + // Get the file + $this->actingAsForApi($user) + ->get( + route('api.files.show', [ + 'object_type' => 'locations', + 'id' => $location->id, + 'file_id' => $result->decodeResponseJson()->json()["rows"][0]["id"], + ])) + ->assertOk(); + } + + public function testLocationApiDeletesFile() + { + // Delete a file from a model + + // Create a model to work with + $location = Location::factory()->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + //Upload a file + $this->actingAsForApi($user) + ->post( + route('api.files.store', ['object_type' => 'locations', 'id' => $location->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)] + ]) + ->assertOk(); + + // List the files to get the file ID + $result = $this->actingAsForApi($user) + ->getJson( + route('api.files.index', ['object_type' => 'locations', 'id' => $location->id])) + ->assertOk(); + + // Delete the file + $this->actingAsForApi($user) + ->delete( + route('api.files.destroy', [ + 'object_type' => 'locations', + 'id' => $location->id, + 'file_id' => $result->decodeResponseJson()->json()["rows"][0]["id"], + ])) + ->assertOk() + ->assertJsonStructure([ + 'status', + 'messages', + ]); + } +} \ No newline at end of file diff --git a/tests/Feature/Users/Api/UserFileTest.php b/tests/Feature/Users/Api/UserFileTest.php new file mode 100644 index 0000000000..e7a43e0c27 --- /dev/null +++ b/tests/Feature/Users/Api/UserFileTest.php @@ -0,0 +1,191 @@ +create(); + + // Create a superuser to run this as + $admin = User::factory()->superuser()->create(); + + //Upload a file + $this->actingAsForApi($admin) + ->post( + route('api.files.store', ['object_type' => 'users', 'id' => $user->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)] + ]) + ->assertOk(); + } + + public function testUserApiListsFiles() + { + // List all files on a model + + // Create a model to work with + $user = User::factory()->create(); + + // Create a superuser to run this as + $admin = User::factory()->superuser()->create(); + + // List the files + $this->actingAsForApi($admin) + ->getJson( + route('api.files.index', ['object_type' => 'users', 'id' => $user->id])) + ->assertOk() + ->assertJsonStructure([ + 'rows', + 'total', + ]); + } + + public function testUserFailsIfInvalidTypePassedInUrl() + { + // List all files on a model + + // Create an model to work with + $user = User::factory()->create(); + + // Create a superuser to run this as + $admin = User::factory()->superuser()->create(); + + // List the files + $this->actingAsForApi($admin) + ->getJson( + route('api.files.index', ['object_type' => 'shibboleeeeeet', 'id' => $user->id])) + ->assertStatus(404); + } + + public function testUserFailsIfInvalidIdPassedInUrl() + { + // List all files on a model + + // Create an model to work with + $user = User::factory()->create(); + + // Create a superuser to run this as + $admin = User::factory()->superuser()->create(); + + // List the files + $this->actingAsForApi($admin) + ->getJson( + route('api.files.index', ['object_type' => 'users', 'id' => 100000])) + ->assertOk() + ->assertStatusMessageIs('error'); + } + + public function testUserApiDownloadsFile() + { + // Download a file from a model + + // Create a model to work with + $user = User::factory()->create(); + + // Create a superuser to run this as + $admin = User::factory()->superuser()->create(); + + // Upload a file + $this->actingAsForApi($admin) + ->post( + route('api.files.store', ['object_type' => 'users', 'id' => $user->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)], + ]) + ->assertOk() + ->assertJsonStructure([ + 'status', + 'messages', + ]); + + // Upload a file with notes + $this->actingAsForApi($admin) + ->post( + route('api.files.store', ['object_type' => 'users', 'id' => $user->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)], + 'notes' => 'manual' + ]) + ->assertOk() + ->assertJsonStructure([ + 'status', + 'messages', + ]); + + // List the files to get the file ID + $result = $this->actingAsForApi($admin) + ->getJson( + route('api.files.index', ['object_type' => 'users', 'id' => $user->id])) + ->assertOk() + ->assertJsonStructure([ + 'total', + 'rows'=>[ + '*' => [ + 'id', + 'filename', + 'url', + 'created_by', + 'created_at', + 'deleted_at', + 'note', + 'available_actions' + ] + ] + ]) + ->assertJsonPath('rows.0.note',null) + ->assertJsonPath('rows.1.note','manual'); + + // Get the file + $this->actingAsForApi($admin) + ->get( + route('api.files.show', [ + 'object_type' => 'users', + 'id' => $user->id, + 'file_id' => $result->decodeResponseJson()->json()["rows"][0]["id"], + ])) + ->assertOk(); + } + + public function testUserApiDeletesFile() + { + // Delete a file from a model + + // Create a model to work with + $user = User::factory()->create(); + + // Create a superuser to run this as + $admin = User::factory()->superuser()->create(); + + //Upload a file + $this->actingAsForApi($admin) + ->post( + route('api.files.store', ['object_type' => 'users', 'id' => $user->id]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)] + ]) + ->assertOk(); + + // List the files to get the file ID + $result = $this->actingAsForApi($admin) + ->getJson( + route('api.files.index', ['object_type' => 'users', 'id' => $user->id])) + ->assertOk(); + + // Delete the file + $this->actingAsForApi($admin) + ->delete( + route('api.files.destroy', [ + 'object_type' => 'users', + 'id' => $user->id, + 'file_id' => $result->decodeResponseJson()->json()["rows"][0]["id"], + ])) + ->assertOk() + ->assertJsonStructure([ + 'status', + 'messages', + ]); + } +} \ No newline at end of file