Extracted test file handling to its own class

Closes #3995
This commit is contained in:
Dan Brown 2023-02-08 14:39:13 +00:00
parent 5d18e7df79
commit da1a66abd3
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
17 changed files with 293 additions and 309 deletions

View File

@ -10,6 +10,7 @@ use BookStack\Entities\Models\Page;
use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
@ -29,6 +30,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
class Attachment extends Model
{
use HasCreatorAndUpdater;
use HasFactory;
protected $fillable = ['name', 'order'];
protected $hidden = ['path', 'page'];

View File

@ -0,0 +1,39 @@
<?php
namespace Database\Factories\Uploads;
use BookStack\Auth\User;
use BookStack\Entities\Models\Page;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\BookStack\Uploads\Attachment>
*/
class AttachmentFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = \BookStack\Uploads\Attachment::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
'name' => $this->faker->words(2, true),
'path' => $this->faker->url(),
'extension' => '',
'external' => true,
'uploaded_to' => Page::factory(),
'created_by' => User::factory(),
'updated_by' => User::factory(),
'order' => 0,
];
}
}

View File

@ -6,12 +6,10 @@ use BookStack\Entities\Models\Book;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
use Tests\Uploads\UsesImages;
class BooksApiTest extends TestCase
{
use TestsApi;
use UsesImages;
protected string $baseEndpoint = '/api/books';
@ -157,7 +155,7 @@ class BooksApiTest extends TestCase
/** @var Book $book */
$book = $this->entities->book();
$this->assertNull($book->cover);
$file = $this->getTestImage('image.png');
$file = $this->files->uploadedImage('image.png');
// Ensure cover image can be set via API
$resp = $this->call('PUT', $this->baseEndpoint . "/{$book->id}", [

View File

@ -7,12 +7,10 @@ use BookStack\Entities\Models\Bookshelf;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
use Tests\Uploads\UsesImages;
class ShelvesApiTest extends TestCase
{
use TestsApi;
use UsesImages;
protected string $baseEndpoint = '/api/shelves';
@ -154,7 +152,7 @@ class ShelvesApiTest extends TestCase
/** @var Book $shelf */
$shelf = Bookshelf::visible()->first();
$this->assertNull($shelf->cover);
$file = $this->getTestImage('image.png');
$file = $this->files->uploadedImage('image.png');
// Ensure cover image can be set via API
$resp = $this->call('PUT', $this->baseEndpoint . "/{$shelf->id}", [

View File

@ -8,12 +8,9 @@ use BookStack\Entities\Models\Bookshelf;
use BookStack\Uploads\Image;
use Illuminate\Support\Str;
use Tests\TestCase;
use Tests\Uploads\UsesImages;
class BookShelfTest extends TestCase
{
use UsesImages;
public function test_shelves_shows_in_header_if_have_view_permissions()
{
$viewer = $this->users->viewer();
@ -114,7 +111,7 @@ class BookShelfTest extends TestCase
'description' => 'Test book description ' . Str::random(10),
];
$imageFile = $this->getTestImage('shelf-test.png');
$imageFile = $this->files->uploadedImage('shelf-test.png');
$resp = $this->asEditor()->call('POST', '/shelves', $shelfInfo, [], ['image' => $imageFile]);
$resp->assertRedirect();

View File

@ -7,12 +7,9 @@ use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Repos\BookRepo;
use Tests\TestCase;
use Tests\Uploads\UsesImages;
class BookTest extends TestCase
{
use UsesImages;
public function test_create()
{
$book = Book::factory()->make([
@ -333,7 +330,7 @@ class BookTest extends TestCase
{
$book = $this->entities->book();
$bookRepo = $this->app->make(BookRepo::class);
$coverImageFile = $this->getTestImage('cover.png');
$coverImageFile = $this->files->uploadedImage('cover.png');
$bookRepo->updateCoverImage($book, $coverImageFile);
$this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);

View File

@ -5,12 +5,9 @@ namespace Tests\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\PageContent;
use Tests\TestCase;
use Tests\Uploads\UsesImages;
class PageContentTest extends TestCase
{
use UsesImages;
protected $base64Jpeg = '/9j/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k=';
public function test_page_includes()
@ -591,7 +588,7 @@ class PageContentTest extends TestCase
$imageFile = public_path($imagePath);
$this->assertEquals(base64_decode($this->base64Jpeg), file_get_contents($imageFile));
$this->deleteImage($imagePath);
$this->files->deleteAtRelativePath($imagePath);
}
public function test_base64_images_get_extracted_when_containing_whitespace()
@ -615,7 +612,7 @@ class PageContentTest extends TestCase
$imageFile = public_path($imagePath);
$this->assertEquals(base64_decode($base64PngWithoutWhitespace), file_get_contents($imageFile));
$this->deleteImage($imagePath);
$this->files->deleteAtRelativePath($imagePath);
}
public function test_base64_images_within_html_blanked_if_not_supported_extension_for_extract()
@ -659,7 +656,7 @@ class PageContentTest extends TestCase
$imageFile = public_path($imagePath);
$this->assertEquals(base64_decode($this->base64Jpeg), file_get_contents($imageFile));
$this->deleteImage($imagePath);
$this->files->deleteAtRelativePath($imagePath);
}
public function test_markdown_base64_extract_not_limited_by_pcre_limits()
@ -690,7 +687,7 @@ class PageContentTest extends TestCase
$imageFile = public_path($imagePath);
$this->assertEquals($content, file_get_contents($imageFile));
$this->deleteImage($imagePath);
$this->files->deleteAtRelativePath($imagePath);
ini_set('pcre.backtrack_limit', $pcreBacktrackLimit);
ini_set('pcre.recursion_limit', $pcreRecursionLimit);
}

View File

@ -0,0 +1,153 @@
<?php
namespace Tests\Helpers;
use BookStack\Entities\Models\Page;
use BookStack\Uploads\Attachment;
use BookStack\Uploads\AttachmentService;
use Illuminate\Http\UploadedFile;
use Illuminate\Testing\TestResponse;
use stdClass;
use Tests\TestCase;
class FileProvider
{
/**
* Get the path to a file in the test-data-directory.
*/
public function testFilePath(string $fileName): string
{
return base_path('tests/test-data/' . $fileName);
}
/**
* Creates a new temporary image file using the given name,
* with the content decoded from the given bas64 file name.
* Is generally used for testing sketchy files that could trip AV.
*/
public function imageFromBase64File(string $base64FileName, string $imageFileName): UploadedFile
{
$imagePath = implode(DIRECTORY_SEPARATOR, [sys_get_temp_dir(), $imageFileName]);
$base64FilePath = $this->testFilePath($base64FileName);
$data = file_get_contents($base64FilePath);
$decoded = base64_decode($data);
file_put_contents($imagePath, $decoded);
return new UploadedFile($imagePath, $imageFileName, 'image/png', null, true);
}
/**
* Get a test image UploadedFile instance, that can be uploaded via test requests.
*/
public function uploadedImage(string $fileName, string $testDataFileName = ''): UploadedFile
{
return new UploadedFile($this->testFilePath($testDataFileName ?: 'test-image.png'), $fileName, 'image/png', null, true);
}
/**
* Get a test txt UploadedFile instance, that can be uploaded via test requests.
*/
public function uploadedTextFile(string $fileName): UploadedFile
{
return new UploadedFile($this->testFilePath('test-file.txt'), $fileName, 'text/plain', null, true);
}
/**
* Get raw data for a PNG image test file.
*/
public function pngImageData(): string
{
return file_get_contents($this->testFilePath('test-image.png'));
}
/**
* Get the expected relative path for an uploaded image of the given type and filename.
*/
public function expectedImagePath(string $imageType, string $fileName): string
{
return '/uploads/images/' . $imageType . '/' . date('Y-m') . '/' . $fileName;
}
/**
* Performs an image gallery upload request with the given name.
*/
public function uploadGalleryImage(TestCase $case, string $name, int $uploadedTo = 0, string $contentType = 'image/png', string $testDataFileName = ''): TestResponse
{
$file = $this->uploadedImage($name, $testDataFileName);
return $case->call('POST', '/images/gallery', ['uploaded_to' => $uploadedTo], [], ['file' => $file], ['CONTENT_TYPE' => $contentType]);
}
/**
* Upload a new gallery image and return a set of details about the image,
* including the json decoded response of the upload.
* Ensures the upload succeeds.
*
* @return array{name: string, path: string, page: Page, response: stdClass}
*/
public function uploadGalleryImageToPage(TestCase $case, Page $page, string $testDataFileName = ''): array
{
$imageName = $testDataFileName ?: 'first-image.png';
$relPath = $this->expectedImagePath('gallery', $imageName);
$this->deleteAtRelativePath($relPath);
$upload = $this->uploadGalleryImage($case, $imageName, $page->id, 'image/png', $testDataFileName);
$upload->assertStatus(200);
return [
'name' => $imageName,
'path' => $relPath,
'page' => $page,
'response' => json_decode($upload->getContent()),
];
}
/**
* Uploads an attachment file with the given name.
*/
public function uploadAttachmentFile(TestCase $case, string $name, int $uploadedTo = 0): TestResponse
{
$file = $this->uploadedTextFile($name);
return $case->call('POST', '/attachments/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []);
}
/**
* Upload a new attachment from the given raw data of the given type, to the given page.
* Returns the attachment
*/
public function uploadAttachmentDataToPage(TestCase $case, Page $page, string $filename, string $content, string $mimeType): Attachment
{
$file = tmpfile();
$filePath = stream_get_meta_data($file)['uri'];
file_put_contents($filePath, $content);
$upload = new UploadedFile($filePath, $filename, $mimeType, null, true);
$case->call('POST', '/attachments/upload', ['uploaded_to' => $page->id], [], ['file' => $upload], []);
return $page->attachments()->where('uploaded_to', '=', $page->id)->latest()->firstOrFail();
}
/**
* Delete an uploaded image.
*/
public function deleteAtRelativePath(string $path): void
{
$fullPath = public_path($path);
if (file_exists($fullPath)) {
unlink($fullPath);
}
}
/**
* Delete all uploaded files.
* To assist with cleanup.
*/
public function deleteAllAttachmentFiles(): void
{
$fileService = app()->make(AttachmentService::class);
foreach (Attachment::all() as $file) {
$fileService->deleteFile($file);
}
}
}

View File

@ -6,12 +6,9 @@ use BookStack\Entities\Repos\BaseRepo;
use BookStack\Entities\Repos\BookRepo;
use Illuminate\Support\Str;
use Illuminate\Testing\TestResponse;
use Tests\Uploads\UsesImages;
class OpenGraphTest extends TestCase
{
use UsesImages;
public function test_page_tags()
{
$page = $this->entities->page();
@ -47,7 +44,7 @@ class OpenGraphTest extends TestCase
// Test image set if image has cover image
$bookRepo = app(BookRepo::class);
$bookRepo->updateCoverImage($book, $this->getTestImage('image.png'));
$bookRepo->updateCoverImage($book, $this->files->uploadedImage('image.png'));
$resp = $this->asEditor()->get($book->getUrl());
$tags = $this->getOpenGraphTags($resp);
@ -67,7 +64,7 @@ class OpenGraphTest extends TestCase
// Test image set if image has cover image
$baseRepo = app(BaseRepo::class);
$baseRepo->updateCoverImage($shelf, $this->getTestImage('image.png'));
$baseRepo->updateCoverImage($shelf, $this->files->uploadedImage('image.png'));
$resp = $this->asEditor()->get($shelf->getUrl());
$tags = $this->getOpenGraphTags($resp);

View File

@ -3,12 +3,9 @@
namespace Tests\Settings;
use Tests\TestCase;
use Tests\Uploads\UsesImages;
class SettingsTest extends TestCase
{
use UsesImages;
public function test_settings_endpoint_redirects_to_settings_view()
{
$resp = $this->asAdmin()->get('/settings');
@ -47,7 +44,7 @@ class SettingsTest extends TestCase
public function test_updating_and_removing_app_icon()
{
$this->asAdmin();
$galleryFile = $this->getTestImage('my-app-icon.png');
$galleryFile = $this->files->uploadedImage('my-app-icon.png');
$expectedPath = public_path('uploads/images/system/' . date('Y-m') . '/my-app-icon.png');
$this->assertFalse(setting()->get('app-icon'));

View File

@ -23,6 +23,7 @@ use Monolog\Logger;
use Psr\Http\Client\ClientInterface;
use Ssddanbrown\AssertHtml\TestsHtml;
use Tests\Helpers\EntityProvider;
use Tests\Helpers\FileProvider;
use Tests\Helpers\PermissionsProvider;
use Tests\Helpers\TestServiceProvider;
use Tests\Helpers\UserRoleProvider;
@ -36,12 +37,14 @@ abstract class TestCase extends BaseTestCase
protected EntityProvider $entities;
protected UserRoleProvider $users;
protected PermissionsProvider $permissions;
protected FileProvider $files;
protected function setUp(): void
{
$this->entities = new EntityProvider();
$this->users = new UserRoleProvider();
$this->permissions = new PermissionsProvider($this->users);
$this->files = new FileProvider();
parent::setUp();

View File

@ -6,71 +6,10 @@ use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Entities\Tools\TrashCan;
use BookStack\Uploads\Attachment;
use BookStack\Uploads\AttachmentService;
use Illuminate\Http\UploadedFile;
use Tests\TestCase;
class AttachmentTest extends TestCase
{
/**
* Get a test file that can be uploaded.
*/
protected function getTestFile(string $fileName): UploadedFile
{
return new UploadedFile(base_path('tests/test-data/test-file.txt'), $fileName, 'text/plain', null, true);
}
/**
* Uploads a file with the given name.
*/
protected function uploadFile(string $name, int $uploadedTo = 0): \Illuminate\Testing\TestResponse
{
$file = $this->getTestFile($name);
return $this->call('POST', '/attachments/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []);
}
/**
* Create a new attachment.
*/
protected function createAttachment(Page $page): Attachment
{
$this->post('attachments/link', [
'attachment_link_url' => 'https://example.com',
'attachment_link_name' => 'Example Attachment Link',
'attachment_link_uploaded_to' => $page->id,
]);
return Attachment::query()->latest()->first();
}
/**
* Create a new upload attachment from the given data.
*/
protected function createUploadAttachment(Page $page, string $filename, string $content, string $mimeType): Attachment
{
$file = tmpfile();
$filePath = stream_get_meta_data($file)['uri'];
file_put_contents($filePath, $content);
$upload = new UploadedFile($filePath, $filename, $mimeType, null, true);
$this->call('POST', '/attachments/upload', ['uploaded_to' => $page->id], [], ['file' => $upload], []);
return $page->attachments()->latest()->firstOrFail();
}
/**
* Delete all uploaded files.
* To assist with cleanup.
*/
protected function deleteUploads()
{
$fileService = $this->app->make(AttachmentService::class);
foreach (Attachment::all() as $file) {
$fileService->deleteFile($file);
}
}
public function test_file_upload()
{
$page = $this->entities->page();
@ -87,7 +26,7 @@ class AttachmentTest extends TestCase
'updated_by' => $admin->id,
];
$upload = $this->uploadFile($fileName, $page->id);
$upload = $this->files->uploadAttachmentFile($this, $fileName, $page->id);
$upload->assertStatus(200);
$attachment = Attachment::query()->orderBy('id', 'desc')->first();
@ -96,7 +35,7 @@ class AttachmentTest extends TestCase
$expectedResp['path'] = $attachment->path;
$this->assertDatabaseHas('attachments', $expectedResp);
$this->deleteUploads();
$this->files->deleteAllAttachmentFiles();
}
public function test_file_upload_does_not_use_filename()
@ -104,13 +43,14 @@ class AttachmentTest extends TestCase
$page = $this->entities->page();
$fileName = 'upload_test_file.txt';
$upload = $this->asAdmin()->uploadFile($fileName, $page->id);
$this->asAdmin();
$upload = $this->files->uploadAttachmentFile($this, $fileName, $page->id);
$upload->assertStatus(200);
$attachment = Attachment::query()->orderBy('id', 'desc')->first();
$this->assertStringNotContainsString($fileName, $attachment->path);
$this->assertStringEndsWith('-txt', $attachment->path);
$this->deleteUploads();
$this->files->deleteAllAttachmentFiles();
}
public function test_file_display_and_access()
@ -119,7 +59,7 @@ class AttachmentTest extends TestCase
$this->asAdmin();
$fileName = 'upload_test_file.txt';
$upload = $this->uploadFile($fileName, $page->id);
$upload = $this->files->uploadAttachmentFile($this, $fileName, $page->id);
$upload->assertStatus(200);
$attachment = Attachment::orderBy('id', 'desc')->take(1)->first();
@ -131,7 +71,7 @@ class AttachmentTest extends TestCase
$content = $attachmentGet->streamedContent();
$this->assertStringContainsString('Hi, This is a test file for testing the upload process.', $content);
$this->deleteUploads();
$this->files->deleteAllAttachmentFiles();
}
public function test_attaching_link_to_page()
@ -168,7 +108,7 @@ class AttachmentTest extends TestCase
$attachmentGet = $this->get($attachment->getUrl());
$attachmentGet->assertRedirect('https://example.com');
$this->deleteUploads();
$this->files->deleteAllAttachmentFiles();
}
public function test_attachment_updating()
@ -176,7 +116,7 @@ class AttachmentTest extends TestCase
$page = $this->entities->page();
$this->asAdmin();
$attachment = $this->createAttachment($page);
$attachment = Attachment::factory()->create(['uploaded_to' => $page->id]);
$update = $this->call('PUT', 'attachments/' . $attachment->id, [
'attachment_edit_name' => 'My new attachment name',
'attachment_edit_url' => 'https://test.example.com',
@ -192,7 +132,7 @@ class AttachmentTest extends TestCase
$update->assertStatus(200);
$this->assertDatabaseHas('attachments', $expectedData);
$this->deleteUploads();
$this->files->deleteAllAttachmentFiles();
}
public function test_file_deletion()
@ -200,7 +140,7 @@ class AttachmentTest extends TestCase
$page = $this->entities->page();
$this->asAdmin();
$fileName = 'deletion_test.txt';
$this->uploadFile($fileName, $page->id);
$this->files->uploadAttachmentFile($this, $fileName, $page->id);
$attachment = Attachment::query()->orderBy('id', 'desc')->first();
$filePath = storage_path($attachment->path);
@ -214,7 +154,7 @@ class AttachmentTest extends TestCase
]);
$this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected');
$this->deleteUploads();
$this->files->deleteAllAttachmentFiles();
}
public function test_attachment_deletion_on_page_deletion()
@ -222,7 +162,7 @@ class AttachmentTest extends TestCase
$page = $this->entities->page();
$this->asAdmin();
$fileName = 'deletion_test.txt';
$this->uploadFile($fileName, $page->id);
$this->files->uploadAttachmentFile($this, $fileName, $page->id);
$attachment = Attachment::query()->orderBy('id', 'desc')->first();
$filePath = storage_path($attachment->path);
@ -240,7 +180,7 @@ class AttachmentTest extends TestCase
]);
$this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected');
$this->deleteUploads();
$this->files->deleteAllAttachmentFiles();
}
public function test_attachment_access_without_permission_shows_404()
@ -250,7 +190,7 @@ class AttachmentTest extends TestCase
$page = $this->entities->page(); /** @var Page $page */
$this->actingAs($admin);
$fileName = 'permission_test.txt';
$this->uploadFile($fileName, $page->id);
$this->files->uploadAttachmentFile($this, $fileName, $page->id);
$attachment = Attachment::orderBy('id', 'desc')->take(1)->first();
$this->permissions->setEntityPermissions($page, [], []);
@ -260,7 +200,7 @@ class AttachmentTest extends TestCase
$attachmentGet->assertStatus(404);
$attachmentGet->assertSee('Attachment not found');
$this->deleteUploads();
$this->files->deleteAllAttachmentFiles();
}
public function test_data_and_js_links_cannot_be_attached_to_a_page()
@ -290,7 +230,7 @@ class AttachmentTest extends TestCase
]);
}
$attachment = $this->createAttachment($page);
$attachment = Attachment::factory()->create(['uploaded_to' => $page->id]);
foreach ($badLinks as $badLink) {
$linkReq = $this->put('attachments/' . $attachment->id, [
@ -310,7 +250,7 @@ class AttachmentTest extends TestCase
$this->asAdmin();
$fileName = 'upload_test_file.txt';
$upload = $this->uploadFile($fileName, $page->id);
$upload = $this->files->uploadAttachmentFile($this, $fileName, $page->id);
$upload->assertStatus(200);
$attachment = Attachment::query()->orderBy('id', 'desc')->take(1)->first();
@ -320,7 +260,7 @@ class AttachmentTest extends TestCase
$attachmentGet->assertHeader('Content-Disposition', 'inline; filename="upload_test_file.txt"');
$attachmentGet->assertHeader('X-Content-Type-Options', 'nosniff');
$this->deleteUploads();
$this->files->deleteAllAttachmentFiles();
}
public function test_html_file_access_with_open_forces_plain_content_type()
@ -328,14 +268,14 @@ class AttachmentTest extends TestCase
$page = $this->entities->page();
$this->asAdmin();
$attachment = $this->createUploadAttachment($page, 'test_file.html', '<html></html><p>testing</p>', 'text/html');
$attachment = $this->files->uploadAttachmentDataToPage($this, $page, 'test_file.html', '<html></html><p>testing</p>', 'text/html');
$attachmentGet = $this->get($attachment->getUrl(true));
// http-foundation/Response does some 'fixing' of responses to add charsets to text responses.
$attachmentGet->assertHeader('Content-Type', 'text/plain; charset=UTF-8');
$attachmentGet->assertHeader('Content-Disposition', 'inline; filename="test_file.html"');
$this->deleteUploads();
$this->files->deleteAllAttachmentFiles();
}
public function test_file_upload_works_when_local_secure_restricted_is_in_use()
@ -345,11 +285,12 @@ class AttachmentTest extends TestCase
$page = $this->entities->page();
$fileName = 'upload_test_file.txt';
$upload = $this->asAdmin()->uploadFile($fileName, $page->id);
$this->asAdmin();
$upload = $this->files->uploadAttachmentFile($this, $fileName, $page->id);
$upload->assertStatus(200);
$attachment = Attachment::query()->orderBy('id', 'desc')->where('uploaded_to', '=', $page->id)->first();
$this->assertFileExists(storage_path($attachment->path));
$this->deleteUploads();
$this->files->deleteAllAttachmentFiles();
}
}

View File

@ -9,8 +9,6 @@ use Tests\TestCase;
class AvatarTest extends TestCase
{
use UsesImages;
protected function createUserRequest($user): User
{
$this->asAdmin()->post('/settings/users/create', [
@ -29,12 +27,12 @@ class AvatarTest extends TestCase
$http->shouldReceive('fetch')
->once()->with($url)
->andReturn($this->getTestImageContent());
->andReturn($this->files->pngImageData());
}
protected function deleteUserImage(User $user)
{
$this->deleteImage($user->avatar->path);
$this->files->deleteAtRelativePath($user->avatar->path);
}
public function test_gravatar_fetched_on_user_create()

View File

@ -2,21 +2,18 @@
namespace Tests\Uploads;
use BookStack\Entities\Models\Page;
use BookStack\Uploads\Image;
use Tests\TestCase;
class DrawioTest extends TestCase
{
use UsesImages;
public function test_get_image_as_base64()
{
$page = $this->entities->page();
$this->asAdmin();
$imageName = 'first-image.png';
$this->uploadImage($imageName, $page->id);
$this->files->uploadGalleryImage($this, $imageName, $page->id);
/** @var Image $image */
$image = Image::query()->first();
$image->type = 'drawio';
@ -34,7 +31,7 @@ class DrawioTest extends TestCase
$this->asEditor();
$imageName = 'non-accessible-image.png';
$this->uploadImage($imageName, $page->id);
$this->files->uploadGalleryImage($this, $imageName, $page->id);
/** @var Image $image */
$image = Image::query()->first();
$image->type = 'drawio';
@ -70,7 +67,7 @@ class DrawioTest extends TestCase
$image = Image::where('type', '=', 'drawio')->first();
$this->assertTrue(file_exists(public_path($image->path)), 'Uploaded image not found at path: ' . public_path($image->path));
$testImageData = file_get_contents($this->getTestImageFilePath());
$testImageData = $this->files->pngImageData();
$uploadedImageData = file_get_contents(public_path($image->path));
$this->assertTrue($testImageData === $uploadedImageData, 'Uploaded image file data does not match our test image as expected');
}

View File

@ -2,7 +2,6 @@
namespace Tests\Uploads;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageService;
@ -11,20 +10,18 @@ use Tests\TestCase;
class ImageTest extends TestCase
{
use UsesImages;
public function test_image_upload()
{
$page = $this->entities->page();
$admin = $this->users->admin();
$this->actingAs($admin);
$imgDetails = $this->uploadGalleryImage($page);
$imgDetails = $this->files->uploadGalleryImageToPage($this, $page);
$relPath = $imgDetails['path'];
$this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image found at path: ' . public_path($relPath));
$this->deleteImage($relPath);
$this->files->deleteAtRelativePath($relPath);
$this->assertDatabaseHas('images', [
'url' => $this->baseUrl . $relPath,
@ -43,9 +40,9 @@ class ImageTest extends TestCase
$admin = $this->users->admin();
$this->actingAs($admin);
$originalFile = $this->getTestImageFilePath('compressed.png');
$originalFile = $this->files->testFilePath('compressed.png');
$originalFileSize = filesize($originalFile);
$imgDetails = $this->uploadGalleryImage($page, 'compressed.png');
$imgDetails = $this->files->uploadGalleryImageToPage($this, $page, 'compressed.png');
$relPath = $imgDetails['path'];
$this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image found at path: ' . public_path($relPath));
@ -55,8 +52,8 @@ class ImageTest extends TestCase
$displayImagePath = public_path($displayImageRelPath);
$displayFileSize = filesize($displayImagePath);
$this->deleteImage($relPath);
$this->deleteImage($displayImageRelPath);
$this->files->deleteAtRelativePath($relPath);
$this->files->deleteAtRelativePath($displayImageRelPath);
$this->assertEquals($originalFileSize, $displayFileSize, 'Display thumbnail generation should not increase image size');
}
@ -67,8 +64,8 @@ class ImageTest extends TestCase
$admin = $this->users->admin();
$this->actingAs($admin);
$imgDetails = $this->uploadGalleryImage($page, 'animated.png');
$this->deleteImage($imgDetails['path']);
$imgDetails = $this->files->uploadGalleryImageToPage($this, $page, 'animated.png');
$this->files->deleteAtRelativePath($imgDetails['path']);
$this->assertStringContainsString('thumbs-', $imgDetails['response']->thumbs->gallery);
$this->assertStringNotContainsString('thumbs-', $imgDetails['response']->thumbs->display);
@ -79,7 +76,7 @@ class ImageTest extends TestCase
$editor = $this->users->editor();
$this->actingAs($editor);
$imgDetails = $this->uploadGalleryImage();
$imgDetails = $this->files->uploadGalleryImageToPage($this, $this->entities->page());
$image = Image::query()->first();
$newName = Str::random();
@ -87,7 +84,7 @@ class ImageTest extends TestCase
$update->assertSuccessful();
$update->assertSee($newName);
$this->deleteImage($imgDetails['path']);
$this->files->deleteAtRelativePath($imgDetails['path']);
$this->assertDatabaseHas('images', [
'type' => 'gallery',
@ -99,7 +96,7 @@ class ImageTest extends TestCase
{
$this->asEditor();
$imgDetails = $this->uploadGalleryImage();
$imgDetails = $this->files->uploadGalleryImageToPage($this, $this->entities->page());
$image = Image::query()->first();
$pageId = $imgDetails['page']->id;
@ -129,7 +126,7 @@ class ImageTest extends TestCase
$editor = $this->users->editor();
$this->actingAs($editor);
$imgDetails = $this->uploadGalleryImage($page);
$imgDetails = $this->files->uploadGalleryImageToPage($this, $page);
$image = Image::query()->first();
$page->html = '<img src="' . $image->url . '">';
@ -140,7 +137,7 @@ class ImageTest extends TestCase
$usage->assertSeeText($page->name);
$usage->assertSee($page->getUrl());
$this->deleteImage($imgDetails['path']);
$this->files->deleteAtRelativePath($imgDetails['path']);
}
public function test_php_files_cannot_be_uploaded()
@ -150,10 +147,10 @@ class ImageTest extends TestCase
$this->actingAs($admin);
$fileName = 'bad.php';
$relPath = $this->getTestImagePath('gallery', $fileName);
$this->deleteImage($relPath);
$relPath = $this->files->expectedImagePath('gallery', $fileName);
$this->files->deleteAtRelativePath($relPath);
$file = $this->newTestImageFromBase64('bad-php.base64', $fileName);
$file = $this->files->imageFromBase64File('bad-php.base64', $fileName);
$upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []);
$upload->assertStatus(302);
@ -172,10 +169,10 @@ class ImageTest extends TestCase
$this->actingAs($admin);
$fileName = 'bad.phtml';
$relPath = $this->getTestImagePath('gallery', $fileName);
$this->deleteImage($relPath);
$relPath = $this->files->expectedImagePath('gallery', $fileName);
$this->files->deleteAtRelativePath($relPath);
$file = $this->newTestImageFromBase64('bad-phtml.base64', $fileName);
$file = $this->files->imageFromBase64File('bad-phtml.base64', $fileName);
$upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []);
$upload->assertStatus(302);
@ -189,11 +186,11 @@ class ImageTest extends TestCase
$this->actingAs($admin);
$fileName = 'bad.phtml.png';
$relPath = $this->getTestImagePath('gallery', $fileName);
$relPath = $this->files->expectedImagePath('gallery', $fileName);
$expectedRelPath = dirname($relPath) . '/bad-phtml.png';
$this->deleteImage($expectedRelPath);
$this->files->deleteAtRelativePath($expectedRelPath);
$file = $this->newTestImageFromBase64('bad-phtml-png.base64', $fileName);
$file = $this->files->imageFromBase64File('bad-phtml-png.base64', $fileName);
$upload = $this->withHeader('Content-Type', 'image/png')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []);
$upload->assertStatus(200);
@ -204,7 +201,7 @@ class ImageTest extends TestCase
$this->assertFileDoesNotExist(public_path($relPath), 'Uploaded image file name was not stripped of dots');
$this->assertFileExists(public_path($expectedRelPath));
$this->deleteImage($lastImage->path);
$this->files->deleteAtRelativePath($lastImage->path);
}
public function test_url_entities_removed_from_filenames()
@ -218,10 +215,10 @@ class ImageTest extends TestCase
'#.png',
];
foreach ($badNames as $name) {
$galleryFile = $this->getTestImage($name);
$galleryFile = $this->files->uploadedImage($name);
$page = $this->entities->page();
$badPath = $this->getTestImagePath('gallery', $name);
$this->deleteImage($badPath);
$badPath = $this->files->expectedImagePath('gallery', $name);
$this->files->deleteAtRelativePath($badPath);
$upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
$upload->assertStatus(200);
@ -235,7 +232,7 @@ class ImageTest extends TestCase
$this->assertTrue(strlen($newFileName) > 0, 'File name was reduced to nothing');
$this->deleteImage($lastImage->path);
$this->files->deleteAtRelativePath($lastImage->path);
}
}
@ -243,7 +240,7 @@ class ImageTest extends TestCase
{
config()->set('filesystems.images', 'local_secure');
$this->asEditor();
$galleryFile = $this->getTestImage('my-secure-test-upload.png');
$galleryFile = $this->files->uploadedImage('my-secure-test-upload.png');
$page = $this->entities->page();
$expectedPath = storage_path('uploads/images/gallery/' . date('Y-m') . '/my-secure-test-upload.png');
@ -291,7 +288,7 @@ class ImageTest extends TestCase
{
config()->set('filesystems.images', 'local_secure');
$this->asEditor();
$galleryFile = $this->getTestImage('my-secure-test-upload.png');
$galleryFile = $this->files->uploadedImage('my-secure-test-upload.png');
$page = $this->entities->page();
$expectedPath = storage_path('uploads/images/gallery/' . date('Y-m') . '/my-secure-test-upload.png');
@ -314,7 +311,7 @@ class ImageTest extends TestCase
{
config()->set('filesystems.images', 'local_secure');
$this->asAdmin();
$galleryFile = $this->getTestImage('my-system-test-upload.png');
$galleryFile = $this->files->uploadedImage('my-system-test-upload.png');
$expectedPath = public_path('uploads/images/system/' . date('Y-m') . '/my-system-test-upload.png');
$upload = $this->call('POST', '/settings/customization', [], [], ['app_logo' => $galleryFile], []);
@ -331,7 +328,7 @@ class ImageTest extends TestCase
{
config()->set('filesystems.images', 'local_secure_restricted');
$this->asAdmin();
$galleryFile = $this->getTestImage('my-system-test-restricted-upload.png');
$galleryFile = $this->files->uploadedImage('my-system-test-restricted-upload.png');
$expectedPath = public_path('uploads/images/system/' . date('Y-m') . '/my-system-test-restricted-upload.png');
$upload = $this->call('POST', '/settings/customization', [], [], ['app_logo' => $galleryFile], []);
@ -348,7 +345,7 @@ class ImageTest extends TestCase
{
config()->set('filesystems.images', 'local_secure_restricted');
$this->asEditor();
$galleryFile = $this->getTestImage('my-secure-restricted-test-upload.png');
$galleryFile = $this->files->uploadedImage('my-secure-restricted-test-upload.png');
$page = $this->entities->page();
$upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
@ -372,7 +369,7 @@ class ImageTest extends TestCase
{
config()->set('filesystems.images', 'local_secure_restricted');
$this->asEditor();
$galleryFile = $this->getTestImage('my-secure-restricted-thumb-test-test.png');
$galleryFile = $this->files->uploadedImage('my-secure-restricted-thumb-test-test.png');
$page = $this->entities->page();
$upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
@ -396,12 +393,10 @@ class ImageTest extends TestCase
{
config()->set('filesystems.images', 'local_secure_restricted');
$this->asEditor();
$galleryFile = $this->getTestImage('my-secure-restricted-export-test.png');
$galleryFile = $this->files->uploadedImage('my-secure-restricted-export-test.png');
/** @var Page $pageA */
/** @var Page $pageB */
$pageA = Page::query()->first();
$pageB = Page::query()->where('id', '!=', $pageA->id)->first();
$pageA = $this->entities->page();
$pageB = $this->entities->page();
$expectedPath = storage_path('uploads/images/gallery/' . date('Y-m') . '/my-secure-restricted-export-test.png');
$upload = $this->asEditor()->call('POST', '/images/gallery', ['uploaded_to' => $pageA->id], [], ['file' => $galleryFile], []);
@ -430,10 +425,10 @@ class ImageTest extends TestCase
$page = $this->entities->page();
$this->asAdmin();
$imageName = 'first-image.png';
$relPath = $this->getTestImagePath('gallery', $imageName);
$this->deleteImage($relPath);
$relPath = $this->files->expectedImagePath('gallery', $imageName);
$this->files->deleteAtRelativePath($relPath);
$this->uploadImage($imageName, $page->id);
$this->files->uploadGalleryImage($this, $imageName, $page->id);
$image = Image::first();
$delete = $this->delete('/images/' . $image->id);
@ -453,12 +448,12 @@ class ImageTest extends TestCase
$this->asAdmin();
$imageName = 'first-image.png';
$relPath = $this->getTestImagePath('gallery', $imageName);
$this->deleteImage($relPath);
$relPath = $this->files->expectedImagePath('gallery', $imageName);
$this->files->deleteAtRelativePath($relPath);
$this->uploadImage($imageName, $page->id);
$this->uploadImage($imageName, $page->id);
$this->uploadImage($imageName, $page->id);
$this->files->uploadGalleryImage($this, $imageName, $page->id);
$this->files->uploadGalleryImage($this, $imageName, $page->id);
$this->files->uploadGalleryImage($this, $imageName, $page->id);
$image = Image::first();
$folder = public_path(dirname($relPath));
@ -477,11 +472,11 @@ class ImageTest extends TestCase
$page = $this->entities->page();
$this->asAdmin();
$imageName = 'first-image.png';
$relPath = $this->getTestImagePath('gallery', $imageName);
$this->deleteImage($relPath);
$relPath = $this->files->expectedImagePath('gallery', $imageName);
$this->files->deleteAtRelativePath($relPath);
$viewer = $this->users->viewer();
$this->uploadImage($imageName, $page->id);
$this->files->uploadGalleryImage($this, $imageName, $page->id);
$image = Image::first();
$resp = $this->get("/images/edit/{$image->id}");
@ -495,16 +490,16 @@ class ImageTest extends TestCase
$resp = $this->actingAs($viewer)->get("/images/edit/{$image->id}");
$this->withHtml($resp)->assertElementExists('button#image-manager-delete[title="Delete"]');
$this->deleteImage($relPath);
$this->files->deleteAtRelativePath($relPath);
}
protected function getTestProfileImage()
{
$imageName = 'profile.png';
$relPath = $this->getTestImagePath('user', $imageName);
$this->deleteImage($relPath);
$relPath = $this->files->expectedImagePath('user', $imageName);
$this->files->deleteAtRelativePath($relPath);
return $this->getTestImage($imageName);
return $this->files->uploadedImage($imageName);
}
public function test_user_image_upload()
@ -559,10 +554,10 @@ class ImageTest extends TestCase
$this->actingAs($admin);
$imageName = 'unused-image.png';
$relPath = $this->getTestImagePath('gallery', $imageName);
$this->deleteImage($relPath);
$relPath = $this->files->expectedImagePath('gallery', $imageName);
$this->files->deleteAtRelativePath($relPath);
$upload = $this->uploadImage($imageName, $page->id);
$upload = $this->files->uploadGalleryImage($this, $imageName, $page->id);
$upload->assertStatus(200);
$image = Image::where('type', '=', 'gallery')->first();
@ -604,6 +599,6 @@ class ImageTest extends TestCase
$this->assertCount(1, $toDelete);
$this->assertFalse(file_exists($absPath));
$this->deleteImage($relPath);
$this->files->deleteAtRelativePath($relPath);
}
}

View File

@ -1,122 +0,0 @@
<?php
namespace Tests\Uploads;
use BookStack\Entities\Models\Page;
use Illuminate\Http\UploadedFile;
use stdClass;
trait UsesImages
{
/**
* Get the path to a file in the test-data-directory.
*/
protected function getTestImageFilePath(?string $fileName = null): string
{
if (is_null($fileName)) {
$fileName = 'test-image.png';
}
return base_path('tests/test-data/' . $fileName);
}
/**
* Creates a new temporary image file using the given name,
* with the content decoded from the given bas64 file name.
* Is generally used for testing sketchy files that could trip AV.
*/
protected function newTestImageFromBase64(string $base64FileName, $imageFileName): UploadedFile
{
$imagePath = implode(DIRECTORY_SEPARATOR, [sys_get_temp_dir(), $imageFileName]);
$base64FilePath = $this->getTestImageFilePath($base64FileName);
$data = file_get_contents($base64FilePath);
$decoded = base64_decode($data);
file_put_contents($imagePath, $decoded);
return new UploadedFile($imagePath, $imageFileName, 'image/png', null, true);
}
/**
* Get a test image that can be uploaded.
*/
protected function getTestImage(string $fileName, ?string $testDataFileName = null): UploadedFile
{
return new UploadedFile($this->getTestImageFilePath($testDataFileName), $fileName, 'image/png', null, true);
}
/**
* Get the raw file data for the test image.
*
* @return false|string
*/
protected function getTestImageContent()
{
return file_get_contents($this->getTestImageFilePath());
}
/**
* Get the path for a test image.
*/
protected function getTestImagePath(string $type, string $fileName): string
{
return '/uploads/images/' . $type . '/' . date('Y-m') . '/' . $fileName;
}
/**
* Uploads an image with the given name.
*
* @param $name
* @param int $uploadedTo
* @param string $contentType
*
* @return \Illuminate\Foundation\Testing\TestResponse
*/
protected function uploadImage($name, $uploadedTo = 0, $contentType = 'image/png', ?string $testDataFileName = null)
{
$file = $this->getTestImage($name, $testDataFileName);
return $this->withHeader('Content-Type', $contentType)
->call('POST', '/images/gallery', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []);
}
/**
* Upload a new gallery image.
* Returns the image name.
* Can provide a page to relate the image to.
*
* @param Page|null $page
*
* @return array{name: string, path: string, page: Page, response: stdClass}
*/
protected function uploadGalleryImage(Page $page = null, ?string $testDataFileName = null)
{
if ($page === null) {
$page = $this->entities->page();
}
$imageName = $testDataFileName ?? 'first-image.png';
$relPath = $this->getTestImagePath('gallery', $imageName);
$this->deleteImage($relPath);
$upload = $this->uploadImage($imageName, $page->id, 'image/png', $testDataFileName);
$upload->assertStatus(200);
return [
'name' => $imageName,
'path' => $relPath,
'page' => $page,
'response' => json_decode($upload->getContent()),
];
}
/**
* Delete an uploaded image.
*/
protected function deleteImage(string $relPath)
{
$path = public_path($relPath);
if (file_exists($path)) {
unlink($path);
}
}
}

View File

@ -12,12 +12,9 @@ use Illuminate\Support\Str;
use Mockery\MockInterface;
use RuntimeException;
use Tests\TestCase;
use Tests\Uploads\UsesImages;
class UserManagementTest extends TestCase
{
use UsesImages;
public function test_user_creation()
{
/** @var User $user */
@ -282,7 +279,7 @@ class UserManagementTest extends TestCase
public function test_user_avatar_update_and_reset()
{
$user = $this->users->viewer();
$avatarFile = $this->getTestImage('avatar-icon.png');
$avatarFile = $this->files->uploadedImage('avatar-icon.png');
$this->assertEquals(0, $user->image_id);