在 Laravel 中测试文件上传

Testing file uploads in Laravel

我正在使用 Laravel 5.8 和 Dropzone.js 将文件上传到库,我能够成功地做到这一点。我认为编写测试来验证这一点是个好习惯,但它总是失败。

我在这个问题中看到了类似的情况:

我的控制器方法看起来像这样被调用 store 并且它看起来像这样:

/**
 * Store a new library file in the database
 *
 * @param StoreArticle $request
 * @return void
 */
public function store(StoreLibrary $request)
{
    $data = $request->validated();

    $category = $data['category'];

    $files = $data['file'];

    foreach ($files as $file) {
        $original_name = $file->getClientOriginalName();
        $mime_type = $file->getClientOriginalExtension();
        $size = $file->getSize();

        // Generate a name for this file 
        $system_generated_name = sha1(date('YmdHis') . str_random(30)) . '.' . $file->getClientOriginalExtension();

        // Store the file on the disk 'library'
        $path = Storage::disk('library')->putFileAs(null, $file, $system_generated_name);

        // Store a reference to this file in the database
        Library::create([
            'display_name' => $original_name,
            'file_name' => $system_generated_name,
            'mime_type' => $mime_type,
            'size' => $size,
            'disk' => $this->disk,
            'storage_location' => $path,
            'category' => $category,
        ]);
    }

    // Return a JSON response
    return response()->json([
        'success' => true,
        'file' => [
            'original_name' => $original_name,
            'generated_name' => $system_generated_name,
            'path' => $path,
            'size' => $size,
        ]
    ], 200);
}

StoreLibrary class 是一个 FormRequest,看起来像这样:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreLibrary extends FormRequest
{
    /**
     * Set allowed extensions for each file category
     * This can be appended to as necessary as it's somewhat restrictive
     */
    private $image_ext = [
        'jpg', 'jpeg', 'png', 'gif', 'ai', 'svg', 'eps', 'ps'
    ];

    private $audio_ext = [
        'mp3', 'ogg', 'mpga'
    ];

    private $video_ext = [
        'mp4', 'mpeg'
    ];

    private $document_ext = [
        'doc', 'docx', 'dotx', 'pdf', 'odt', 'xls', 'xlsm', 'xlsx', 'ppt', 'pptx', 'vsd'
    ];

    /**
     * Merge all listed extensions into one massive array
     *
     * @return array Extensions of all file types
     */
    private function extension_whitelist()
    {
        return array_merge($this->image_ext, $this->audio_ext, $this->video_ext, $this->document_ext);
    }

    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'category' => [
                'required',
                'string'
            ],
            'file.*' => 'required|file|mimes:' . implode(',', $this->extension_whitelist()) . '|max:50000'
        ];
    }

    /**
     * Get the error messages for the defined validation rules.
     *
     * @return array
     */
    public function messages()
    {
        return [
            'category.required' => 'A category is required when uploading library files.',
            'file.*.required' => 'Please select a file to upload',
            'file.*.mimes' => 'This type of file is not permitted on the Intranet'
        ];
    }
}

我写的测试是这样的:

/** @test */
public function a_user_with_permission_can_add_files_to_the_library()
{
    $this->withoutExceptionHandling();

    Storage::fake('library');

    $this->setupPermissions();

    $user = factory(User::class)->create();

    $user->assignRole('admin');

    // Assert the uploading an image returns a 200 response
    $this->actingAs($user)
        ->post(route('admin.library.store'), [
            'category' => 'Some category',
            'file' => UploadedFile::fake()->create("test.jpg", 100)
        ])->assertStatus(200);

    Storage::disk('library')->assertExists("test.jpg");
}

运行测试总是returns下面的错误ErrorException: Undefined index: file但是文件输入肯定叫file.

关联的 blade 文件部分:

<form autocomplete="off">                

                        <div class="form-group">
                            <label for="category">What category do these files fall into? <span class="required">*</span></label>
                            <select v-model="category" name="category" id="category" class="form-control form-control--citibase">
                                <option value="">Select a category</option>
                                @foreach($categories as $category)
                                <option value="{{ $category }}">{{ $category }}</option>
                                @endforeach
                            </select>
                        </div>

                        <div class="form-group">
                            <vue-dropzone 
                                ref="myVueDropzone" 
                                id="dropzone" 
                                v-bind:options="dropzoneOptions" 
                                v-bind:duplicate-check=true
                                v-on:vdropzone-sending="sendFile"
                                v-on:vdropzone-success-multiple="uploadSuccessful"
                                v-on:vdropzone-queue-complete="queueComplete"
                                v-on:vdropzone-error="uploadError">
                            </vue-dropzone>
                        </div>


                        <div class="d-flex justify-content-end">
                            <input type="button" v-on:click="upload_files" class="btn btn-pink" value="Upload files"/>
                        </div>
                    </form>

Vue代码:

if (document.getElementById("library-admin")) {
    const app = new Vue({
        el: "#library-admin",
        components: {
            vueDropzone: vue2Dropzone
        },
        data: {
            query: "",
            category: "",
            results: [],
            errors: [],
            loading: true,
            pagination: {},
            current_page_url: "",
            dropzoneOptions: {
                url: "/admin/library",
                acceptedFiles: ".jpg, .jpeg, .png, .gif, .svg, .eps, ps, .doc, .docx, .dotx, .pdf, .odt, .xls, .xlsm, .xlsx, .ppt, .pptx, .vsd",
                autoProcessQueue: false,
                uploadMultiple: true,
                parallelUploads: 2,
                maxFilesize: 50,
                thumbnailWidth: 100,
                thumbnailHeight: 100,
                dictDefaultMessage: "Drop files here, or click to select them",
                addRemoveLinks: true,
                headers: {
                    "x-csrf-token": document
                        .querySelector('meta[name="csrf-token"]')
                        .getAttribute("content")
                }
            }
        },
        computed: {
            show_table() {
                return true;
            },
            has_pagination() {
                return true;
            }
        },
        created() {
            this.getResults();
        },
        watch: {
            query(after, before) {
                this.search();
            }
        },
        methods: {
            rename_file(file, index) {
                this.$swal({
                    title: 'Rename file',
                    input: 'text',
                    inputValue: file.display_name_excluding_extension,
                    showCancelButton: true,
                    inputValidator: (value) => {
                      if (!value) {
                        return 'You need to write something!'
                      }
                      else if(value){
                        axios
                        .patch("/admin/api/library/rename/" + file.id, {
                            name: value
                        })
                        .then(response => {
                            this.$swal({
                                type: "success",
                                title: "File successfully renamed."
                            });

                            Vue.set(this.results[index], 'display_name', response.data.display_name);

                        })
                        .catch(error => {
                            this.$swal({
                                type: "error",
                                title: "File could not be renamed.",
                                text: "Please ensure that the file name does not include any dots or special characters."
                            })
                        });
                      }
                    }
                });
            },
            upload_files() {
                this.$refs.myVueDropzone.processQueue();
            },
            sendFile(file, xhr, formData) {
                formData.append("category", this.category);
            },
            uploadError(file, message, xhr) {
                console.log(message.errors);
                this.errors = message.errors;
            },
            uploadSuccessful(files, response) {
                this.errors = [];
            },
            queueComplete(files) {
                if (this.errors.length == 0) {
                    this.getResults();

                    this.$refs.myVueDropzone.removeAllFiles();
                }
            },
            getResults: function (page_url) {
                let vm = this;

                page_url = page_url || "/admin/api/library";

                this.current_page_url = page_url;

                this.results = [];
                this.errors = [];
                this.loading = true;
                axios
                    .get(page_url)
                    .then(response => [
                        vm.makePagination(response.data),
                        response.data.error ?
                        (this.error = response.data.error) :
                        (this.results = response.data.data),

                        (this.loading = false)
                    ])
                    .catch((this.error = ""));
            },
            search: _.debounce(function () {
                if (this.query !== "") {
                    let vm = this;

                    this.results = [];
                    this.error = "";
                    this.loading = true;

                    axios
                        .get("/admin/api/library/search", {
                            params: {
                                q: this.query
                            }
                        })
                        .then(response => [
                            vm.makePagination(response.data),

                            response.data.error ?
                            (this.errors = response.data.error) :
                            (this.results = response.data.data),
                            (this.loading = false)
                        ])
                        .catch(error => {
                            console.log(error.response.data.errors);
                            this.errors = error.response.data.errors;
                        });
                } else {
                    this.getResults();
                }
            }, 500),
            makePagination: function (data) {
                var pagination = {
                    from: data.from,
                    to: data.to,
                    total: data.total,
                    current_page: data.current_page,
                    last_page: data.last_page,
                    next_page_url: data.next_page_url,
                    prev_page_url: data.prev_page_url
                };

                this.pagination = pagination;
            },
            delete_file: function (element) {
                if (
                    confirm(
                        "Do you really want to delete " + element.display_name
                    )
                ) {
                    axios
                        .delete("/admin/library/" + element.id)
                        .then(response => [this.getResults()])
                        .catch(error => {
                            console.log(error.response.data.errors);
                            this.errors = error.response.data.error;
                        });
                }
            },
            archive_file: function (element) {
                axios
                    .patch("/admin/library/" + element.id)
                    .then(response => [
                        element.status == "published" ?
                        (element.status = "draft") :
                        (element.status = "published")
                    ])
                    .catch(error => {
                        console.log(error.response.data.errors);
                        this.success = false;
                        this.errors = error.response.data.errors;
                    });
            }
        }
    });
}

我设法找到了解决此问题的方法。因为file是一个数组,测试的时候需要有键和索引。

如下代码所示:

/** @test */
public function a_user_with_permission_can_add_files_to_the_library()
{
    $this->withoutExceptionHandling();

    Storage::fake('library');

    $this->setupPermissions();

    $user = factory(User::class)->create();

    $user->assignRole('admin');

    // Assert the uploading an image returns a 200 response
    $this->actingAs($user)
        ->post(route('admin.library.store'), [
            'category' => 'Some category',
            'file' => [
                0 => UploadedFile::fake()->create("test.jpg", 100),
                1 => UploadedFile::fake()->create("test.png", 100),
                2 => UploadedFile::fake()->create("test.doc", 100),
                3 => UploadedFile::fake()->create("test.ppt", 100),
                4 => UploadedFile::fake()->create("test.pdf", 100),
            ]
        ])->assertStatus(200);

    $this->assertEquals(5, Library::count());
}