556 lines
17 KiB
PHP
556 lines
17 KiB
PHP
<?php
|
|
|
|
use App\Http\Controllers\AttachmentController;
|
|
use App\Models\Attachment;
|
|
use App\Models\AttachmentExtension;
|
|
use App\Models\AttachmentGroup;
|
|
use App\Models\Forum;
|
|
use App\Models\Post;
|
|
use App\Models\Thread;
|
|
use App\Models\User;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
|
|
|
function makeForumForAttachments(): Forum
|
|
{
|
|
$category = Forum::create([
|
|
'name' => 'Category',
|
|
'description' => null,
|
|
'type' => 'category',
|
|
'parent_id' => null,
|
|
'position' => 1,
|
|
]);
|
|
|
|
return Forum::create([
|
|
'name' => 'Forum',
|
|
'description' => null,
|
|
'type' => 'forum',
|
|
'parent_id' => $category->id,
|
|
'position' => 1,
|
|
]);
|
|
}
|
|
|
|
it('store returns unauthorized without user', function (): void {
|
|
$controller = new AttachmentController();
|
|
$request = Request::create('/api/attachments', 'POST');
|
|
$request->setUserResolver(fn () => null);
|
|
|
|
$response = $controller->store($request);
|
|
|
|
expect($response->getStatusCode())->toBe(401);
|
|
});
|
|
|
|
it('store returns file missing when file not provided', function (): void {
|
|
$controller = new AttachmentController();
|
|
$user = User::factory()->create();
|
|
|
|
$request = Request::create('/api/attachments', 'POST', [
|
|
'thread' => '/api/threads/1',
|
|
]);
|
|
$request->setUserResolver(fn () => $user);
|
|
|
|
try {
|
|
$controller->store($request);
|
|
$this->fail('Expected ValidationException not thrown.');
|
|
} catch (Illuminate\Validation\ValidationException $e) {
|
|
expect($e->errors())->toHaveKey('file');
|
|
}
|
|
});
|
|
|
|
it('store returns file missing when request file is null after validation', function (): void {
|
|
$controller = new AttachmentController();
|
|
$user = User::factory()->create();
|
|
$forum = makeForumForAttachments();
|
|
$thread = Thread::create([
|
|
'forum_id' => $forum->id,
|
|
'user_id' => $user->id,
|
|
'title' => 'Thread',
|
|
'body' => 'Body',
|
|
]);
|
|
|
|
$request = Mockery::mock(Request::class)->makePartial();
|
|
$request->shouldReceive('user')->andReturn($user);
|
|
$request->shouldReceive('validate')->andReturn([
|
|
'thread' => "/api/threads/{$thread->id}",
|
|
'post' => null,
|
|
'file' => 'ignored',
|
|
]);
|
|
$request->shouldReceive('file')->andReturn(null);
|
|
|
|
$response = $controller->store($request);
|
|
|
|
expect($response->getStatusCode())->toBe(422);
|
|
});
|
|
|
|
it('store rejects disallowed extension and mime and size', function (): void {
|
|
Storage::fake('local');
|
|
|
|
$controller = new AttachmentController();
|
|
$user = User::factory()->create();
|
|
$forum = makeForumForAttachments();
|
|
$thread = Thread::create([
|
|
'forum_id' => $forum->id,
|
|
'user_id' => $user->id,
|
|
'title' => 'Thread',
|
|
'body' => 'Body',
|
|
]);
|
|
|
|
$group = AttachmentGroup::create([
|
|
'name' => 'Docs',
|
|
'max_size_kb' => 1,
|
|
'is_active' => false,
|
|
]);
|
|
|
|
$ext = AttachmentExtension::create([
|
|
'extension' => 'pdf',
|
|
'attachment_group_id' => $group->id,
|
|
'allowed_mimes' => ['application/pdf'],
|
|
]);
|
|
|
|
$file = UploadedFile::fake()->create('doc.pdf', 2, 'application/pdf');
|
|
|
|
$request = Request::create('/api/attachments', 'POST', [
|
|
'thread' => "/api/threads/{$thread->id}",
|
|
], [], ['file' => $file]);
|
|
$request->setUserResolver(fn () => $user);
|
|
|
|
$response = $controller->store($request);
|
|
|
|
expect($response->getStatusCode())->toBe(422);
|
|
|
|
$group->is_active = true;
|
|
$group->save();
|
|
|
|
$response = $controller->store($request);
|
|
expect($response->getStatusCode())->toBe(422);
|
|
|
|
$group->max_size_kb = 1000;
|
|
$group->save();
|
|
|
|
$ext->allowed_mimes = ['image/png'];
|
|
$ext->save();
|
|
|
|
$response = $controller->store($request);
|
|
expect($response->getStatusCode())->toBe(422);
|
|
|
|
$ext->allowed_mimes = ['application/pdf'];
|
|
$ext->save();
|
|
|
|
$group->max_size_kb = 0;
|
|
$group->save();
|
|
|
|
$response = $controller->store($request);
|
|
expect($response->getStatusCode())->toBe(422);
|
|
});
|
|
|
|
it('store returns forbidden when user cannot attach to post', function (): void {
|
|
Storage::fake('local');
|
|
|
|
$controller = new AttachmentController();
|
|
$owner = User::factory()->create();
|
|
$viewer = User::factory()->create();
|
|
$forum = makeForumForAttachments();
|
|
$thread = Thread::create([
|
|
'forum_id' => $forum->id,
|
|
'user_id' => $owner->id,
|
|
'title' => 'Thread',
|
|
'body' => 'Body',
|
|
]);
|
|
$post = Post::create([
|
|
'thread_id' => $thread->id,
|
|
'user_id' => $owner->id,
|
|
'body' => 'Post',
|
|
]);
|
|
|
|
$group = AttachmentGroup::create([
|
|
'name' => 'Images',
|
|
'max_size_kb' => 100,
|
|
'is_active' => true,
|
|
]);
|
|
|
|
AttachmentExtension::create([
|
|
'extension' => 'png',
|
|
'attachment_group_id' => $group->id,
|
|
'allowed_mimes' => ['image/png'],
|
|
]);
|
|
|
|
$file = UploadedFile::fake()->image('photo.png');
|
|
|
|
$request = Request::create('/api/attachments', 'POST', [
|
|
'post' => "/api/posts/{$post->id}",
|
|
], [], ['file' => $file]);
|
|
$request->setUserResolver(fn () => $viewer);
|
|
|
|
$response = $controller->store($request);
|
|
|
|
expect($response->getStatusCode())->toBe(403);
|
|
});
|
|
|
|
it('index filters by post id', function (): void {
|
|
$controller = new AttachmentController();
|
|
$forum = makeForumForAttachments();
|
|
$thread = Thread::create([
|
|
'forum_id' => $forum->id,
|
|
'user_id' => null,
|
|
'title' => 'Thread',
|
|
'body' => 'Body',
|
|
]);
|
|
$postA = Post::create([
|
|
'thread_id' => $thread->id,
|
|
'user_id' => null,
|
|
'body' => 'Post A',
|
|
]);
|
|
$postB = Post::create([
|
|
'thread_id' => $thread->id,
|
|
'user_id' => null,
|
|
'body' => 'Post B',
|
|
]);
|
|
|
|
Attachment::create([
|
|
'thread_id' => null,
|
|
'post_id' => $postA->id,
|
|
'attachment_extension_id' => null,
|
|
'attachment_group_id' => null,
|
|
'user_id' => null,
|
|
'disk' => 'local',
|
|
'path' => 'attachments/posts/'.$postA->id.'/a.txt',
|
|
'original_name' => 'a.txt',
|
|
'extension' => 'txt',
|
|
'mime_type' => 'text/plain',
|
|
'size_bytes' => 1,
|
|
]);
|
|
Attachment::create([
|
|
'thread_id' => null,
|
|
'post_id' => $postB->id,
|
|
'attachment_extension_id' => null,
|
|
'attachment_group_id' => null,
|
|
'user_id' => null,
|
|
'disk' => 'local',
|
|
'path' => 'attachments/posts/'.$postB->id.'/b.txt',
|
|
'original_name' => 'b.txt',
|
|
'extension' => 'txt',
|
|
'mime_type' => 'text/plain',
|
|
'size_bytes' => 1,
|
|
]);
|
|
|
|
$request = Request::create('/api/attachments', 'GET', [
|
|
'post' => "/api/posts/{$postA->id}",
|
|
]);
|
|
|
|
$response = $controller->index($request);
|
|
|
|
expect($response->getStatusCode())->toBe(200);
|
|
$payload = $response->getData(true);
|
|
expect(count($payload))->toBe(1);
|
|
expect($payload[0]['post_id'])->toBe($postA->id);
|
|
});
|
|
|
|
it('show returns not found when attachment is not viewable', function (): void {
|
|
$controller = new AttachmentController();
|
|
$attachment = new Attachment([
|
|
'disk' => 'local',
|
|
'path' => 'missing',
|
|
]);
|
|
$attachment->setRawAttributes(['id' => 1, 'deleted_at' => now()]);
|
|
|
|
$response = $controller->show($attachment);
|
|
|
|
expect($response->getStatusCode())->toBe(404);
|
|
});
|
|
|
|
it('show returns attachment when viewable', function (): void {
|
|
$controller = new AttachmentController();
|
|
$forum = makeForumForAttachments();
|
|
$thread = Thread::create([
|
|
'forum_id' => $forum->id,
|
|
'user_id' => null,
|
|
'title' => 'Thread',
|
|
'body' => 'Body',
|
|
]);
|
|
$attachment = Attachment::create([
|
|
'thread_id' => $thread->id,
|
|
'post_id' => null,
|
|
'attachment_extension_id' => null,
|
|
'attachment_group_id' => null,
|
|
'user_id' => null,
|
|
'disk' => 'local',
|
|
'path' => 'attachments/threads/'.$thread->id.'/file.pdf',
|
|
'original_name' => 'file.pdf',
|
|
'extension' => 'pdf',
|
|
'mime_type' => 'application/pdf',
|
|
'size_bytes' => 10,
|
|
]);
|
|
|
|
$response = $controller->show($attachment);
|
|
|
|
expect($response->getStatusCode())->toBe(200);
|
|
});
|
|
|
|
it('download aborts when file missing or not viewable', function (): void {
|
|
Storage::fake('local');
|
|
|
|
$controller = new AttachmentController();
|
|
$forum = makeForumForAttachments();
|
|
$thread = Thread::create([
|
|
'forum_id' => $forum->id,
|
|
'user_id' => null,
|
|
'title' => 'Thread',
|
|
'body' => 'Body',
|
|
]);
|
|
$attachment = Attachment::create([
|
|
'thread_id' => $thread->id,
|
|
'post_id' => null,
|
|
'attachment_extension_id' => null,
|
|
'attachment_group_id' => null,
|
|
'user_id' => null,
|
|
'disk' => 'local',
|
|
'path' => 'attachments/threads/'.$thread->id.'/missing.pdf',
|
|
'original_name' => 'missing.pdf',
|
|
'extension' => 'pdf',
|
|
'mime_type' => 'application/pdf',
|
|
'size_bytes' => 10,
|
|
]);
|
|
|
|
expect(fn () => $controller->download($attachment))->toThrow(HttpException::class);
|
|
});
|
|
|
|
it('download aborts when attachment is not viewable', function (): void {
|
|
Storage::fake('local');
|
|
|
|
$controller = new AttachmentController();
|
|
$attachment = Attachment::create([
|
|
'thread_id' => null,
|
|
'post_id' => null,
|
|
'attachment_extension_id' => null,
|
|
'attachment_group_id' => null,
|
|
'user_id' => null,
|
|
'disk' => 'local',
|
|
'path' => 'attachments/threads/1/missing.pdf',
|
|
'original_name' => 'missing.pdf',
|
|
'extension' => 'pdf',
|
|
'mime_type' => 'application/pdf',
|
|
'size_bytes' => 10,
|
|
]);
|
|
|
|
expect(fn () => $controller->download($attachment))->toThrow(HttpException::class);
|
|
});
|
|
|
|
it('thumbnail aborts when missing path or file', function (): void {
|
|
Storage::fake('local');
|
|
|
|
$controller = new AttachmentController();
|
|
$forum = makeForumForAttachments();
|
|
$thread = Thread::create([
|
|
'forum_id' => $forum->id,
|
|
'user_id' => null,
|
|
'title' => 'Thread',
|
|
'body' => 'Body',
|
|
]);
|
|
$attachment = Attachment::create([
|
|
'thread_id' => $thread->id,
|
|
'post_id' => null,
|
|
'attachment_extension_id' => null,
|
|
'attachment_group_id' => null,
|
|
'user_id' => null,
|
|
'disk' => 'local',
|
|
'path' => 'attachments/threads/'.$thread->id.'/file.pdf',
|
|
'original_name' => 'file.pdf',
|
|
'extension' => 'pdf',
|
|
'mime_type' => 'application/pdf',
|
|
'size_bytes' => 10,
|
|
]);
|
|
|
|
expect(fn () => $controller->thumbnail($attachment))->toThrow(HttpException::class);
|
|
|
|
$attachment->thumbnail_path = 'attachments/threads/'.$thread->id.'/thumb.jpg';
|
|
$attachment->save();
|
|
|
|
expect(fn () => $controller->thumbnail($attachment))->toThrow(HttpException::class);
|
|
});
|
|
|
|
it('thumbnail aborts when attachment is not viewable', function (): void {
|
|
Storage::fake('local');
|
|
|
|
$controller = new AttachmentController();
|
|
$attachment = Attachment::create([
|
|
'thread_id' => null,
|
|
'post_id' => null,
|
|
'attachment_extension_id' => null,
|
|
'attachment_group_id' => null,
|
|
'user_id' => null,
|
|
'disk' => 'local',
|
|
'path' => 'attachments/threads/1/file.pdf',
|
|
'thumbnail_path' => 'attachments/threads/1/thumb.jpg',
|
|
'original_name' => 'file.pdf',
|
|
'extension' => 'pdf',
|
|
'mime_type' => 'application/pdf',
|
|
'size_bytes' => 10,
|
|
]);
|
|
|
|
expect(fn () => $controller->thumbnail($attachment))->toThrow(HttpException::class);
|
|
});
|
|
|
|
it('destroy returns unauthorized or forbidden', function (): void {
|
|
$controller = new AttachmentController();
|
|
$attachment = new Attachment(['user_id' => 999]);
|
|
|
|
$request = Request::create('/api/attachments/1', 'DELETE');
|
|
$request->setUserResolver(fn () => null);
|
|
$response = $controller->destroy($request, $attachment);
|
|
expect($response->getStatusCode())->toBe(401);
|
|
|
|
$user = User::factory()->create();
|
|
$request->setUserResolver(fn () => $user);
|
|
$response = $controller->destroy($request, $attachment);
|
|
expect($response->getStatusCode())->toBe(403);
|
|
});
|
|
|
|
it('private helpers cover parse and match branches', function (): void {
|
|
$controller = new AttachmentController();
|
|
|
|
$refThread = new ReflectionMethod($controller, 'parseThreadId');
|
|
$refThread->setAccessible(true);
|
|
$refPost = new ReflectionMethod($controller, 'parsePostId');
|
|
$refPost->setAccessible(true);
|
|
$refMatch = new ReflectionMethod($controller, 'matchesAllowed');
|
|
$refMatch->setAccessible(true);
|
|
$refResolve = new ReflectionMethod($controller, 'resolveExtension');
|
|
$refResolve->setAccessible(true);
|
|
|
|
expect($refThread->invoke($controller, null))->toBeNull();
|
|
expect($refThread->invoke($controller, '/threads/12'))->toBe(12);
|
|
expect($refThread->invoke($controller, '5'))->toBe(5);
|
|
expect($refThread->invoke($controller, 'abc'))->toBeNull();
|
|
|
|
expect($refPost->invoke($controller, null))->toBeNull();
|
|
expect($refPost->invoke($controller, '/posts/7'))->toBe(7);
|
|
expect($refPost->invoke($controller, '9'))->toBe(9);
|
|
expect($refPost->invoke($controller, 'nope'))->toBeNull();
|
|
|
|
expect($refMatch->invoke($controller, 'image/png', null))->toBeTrue();
|
|
expect($refMatch->invoke($controller, 'image/png', []))->toBeTrue();
|
|
expect($refMatch->invoke($controller, 'image/png', ['image/jpeg']))->toBeFalse();
|
|
expect($refMatch->invoke($controller, 'image/png', ['image/png']))->toBeTrue();
|
|
|
|
expect($refResolve->invoke($controller, ''))->toBeNull();
|
|
});
|
|
|
|
it('canViewAttachment handles trashed and missing parents', function (): void {
|
|
$controller = new AttachmentController();
|
|
|
|
$forum = makeForumForAttachments();
|
|
$thread = Thread::create([
|
|
'forum_id' => $forum->id,
|
|
'user_id' => null,
|
|
'title' => 'Thread',
|
|
'body' => 'Body',
|
|
]);
|
|
|
|
$attachment = Attachment::create([
|
|
'thread_id' => $thread->id,
|
|
'post_id' => null,
|
|
'attachment_extension_id' => null,
|
|
'attachment_group_id' => null,
|
|
'user_id' => null,
|
|
'disk' => 'local',
|
|
'path' => 'attachments/threads/'.$thread->id.'/file.pdf',
|
|
'original_name' => 'file.pdf',
|
|
'extension' => 'pdf',
|
|
'mime_type' => 'application/pdf',
|
|
'size_bytes' => 10,
|
|
]);
|
|
|
|
$refView = new ReflectionMethod($controller, 'canViewAttachment');
|
|
$refView->setAccessible(true);
|
|
|
|
expect($refView->invoke($controller, $attachment))->toBeTrue();
|
|
|
|
$attachment->delete();
|
|
expect($refView->invoke($controller, $attachment))->toBeFalse();
|
|
|
|
$attachment->restore();
|
|
$thread->delete();
|
|
expect($refView->invoke($controller, $attachment))->toBeFalse();
|
|
|
|
$thread->restore();
|
|
|
|
$post = Post::create([
|
|
'thread_id' => $thread->id,
|
|
'user_id' => null,
|
|
'body' => 'Post',
|
|
]);
|
|
$attachment->post_id = $post->id;
|
|
$attachment->thread_id = null;
|
|
$attachment->save();
|
|
expect($refView->invoke($controller, $attachment))->toBeTrue();
|
|
|
|
$post->delete();
|
|
expect($refView->invoke($controller, $attachment))->toBeFalse();
|
|
|
|
$attachment->post_id = null;
|
|
$attachment->thread_id = null;
|
|
$attachment->save();
|
|
expect($refView->invoke($controller, $attachment))->toBeFalse();
|
|
});
|
|
|
|
it('serializeAttachment returns thumbnail_url null when missing', function (): void {
|
|
$controller = new AttachmentController();
|
|
|
|
$attachment = new Attachment([
|
|
'id' => 1,
|
|
'thread_id' => null,
|
|
'post_id' => null,
|
|
'extension' => 'pdf',
|
|
'original_name' => 'file.pdf',
|
|
'mime_type' => 'application/pdf',
|
|
'size_bytes' => 10,
|
|
]);
|
|
|
|
$ref = new ReflectionMethod($controller, 'serializeAttachment');
|
|
$ref->setAccessible(true);
|
|
|
|
$payload = $ref->invoke($controller, $attachment);
|
|
|
|
expect($payload['thumbnail_url'])->toBeNull();
|
|
expect($payload['is_image'])->toBeFalse();
|
|
});
|
|
|
|
it('serializeAttachment includes thumbnail url when present', function (): void {
|
|
$controller = new AttachmentController();
|
|
|
|
$attachment = new Attachment([
|
|
'id' => 2,
|
|
'thread_id' => null,
|
|
'post_id' => null,
|
|
'extension' => 'png',
|
|
'original_name' => 'file.png',
|
|
'mime_type' => 'image/png',
|
|
'thumbnail_path' => 'thumbs/file.png',
|
|
'size_bytes' => 10,
|
|
]);
|
|
|
|
$ref = new ReflectionMethod($controller, 'serializeAttachment');
|
|
$ref->setAccessible(true);
|
|
|
|
$payload = $ref->invoke($controller, $attachment);
|
|
|
|
expect($payload['thumbnail_url'])->toContain('/thumbnail');
|
|
expect($payload['is_image'])->toBeTrue();
|
|
});
|
|
|
|
it('canManageAttachments handles null user and admin', function (): void {
|
|
$controller = new AttachmentController();
|
|
$ref = new ReflectionMethod($controller, 'canManageAttachments');
|
|
$ref->setAccessible(true);
|
|
|
|
expect($ref->invoke($controller, null, 1))->toBeFalse();
|
|
|
|
$admin = User::factory()->create();
|
|
$role = \App\Models\Role::create(['name' => 'ROLE_ADMIN']);
|
|
$admin->roles()->attach($role);
|
|
|
|
expect($ref->invoke($controller, $admin, 999))->toBeTrue();
|
|
});
|