trypost/tests/Feature/Api/UploadControllerTest.php

139 lines
4.5 KiB
PHP

<?php
declare(strict_types=1);
use App\Enums\UserWorkspace\Role;
use App\Models\Media;
use App\Models\User;
use App\Models\Workspace;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Str;
beforeEach(function () {
Storage::fake();
Cache::flush();
$this->user = User::factory()->create();
$this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]);
$this->workspace->members()->attach($this->user->id, ['role' => Role::Member->value]);
});
function signedUploadUrl(Workspace $ws, string $token, ?int $expiresInMinutes = 15): string
{
return URL::temporarySignedRoute(
'api.uploads.store',
now()->addMinutes($expiresInMinutes),
['token' => $token, 'workspace_id' => $ws->id],
);
}
test('valid signed POST stores Media with upload_token', function () {
$token = (string) Str::uuid();
$file = UploadedFile::fake()->image('shot.png', 50, 50);
$response = $this->post(signedUploadUrl($this->workspace, $token), [
'media' => $file,
]);
$media = Media::where('upload_token', $token)->first();
expect($media)->not->toBeNull();
expect($media->mediable_id)->toBe($this->workspace->id);
expect($media->mediable_type)->toBe('workspace');
$response->assertCreated()
->assertJson([
'upload_token' => $token,
'media_id' => $media->id,
'mime_type' => $media->mime_type,
'original_filename' => $media->original_filename,
]);
});
test('rejects unsigned request', function () {
$token = (string) Str::uuid();
$file = UploadedFile::fake()->image('shot.png', 50, 50);
$response = $this->postJson(route('api.uploads.store', ['token' => $token, 'workspace_id' => $this->workspace->id]), [
'media' => $file,
]);
$response->assertForbidden();
expect(Media::where('upload_token', $token)->exists())->toBeFalse();
});
test('rejects tampered workspace_id', function () {
$other = Workspace::factory()->create();
$token = (string) Str::uuid();
$file = UploadedFile::fake()->image('shot.png', 50, 50);
$url = signedUploadUrl($this->workspace, $token);
$tampered = str_replace("workspace_id={$this->workspace->id}", "workspace_id={$other->id}", $url);
$response = $this->postJson($tampered, ['media' => $file]);
$response->assertForbidden();
});
test('rejects expired URL', function () {
$token = (string) Str::uuid();
$file = UploadedFile::fake()->image('shot.png', 50, 50);
$url = URL::temporarySignedRoute(
'api.uploads.store',
now()->subMinute(),
['token' => $token, 'workspace_id' => $this->workspace->id],
);
$response = $this->postJson($url, ['media' => $file]);
$response->assertForbidden();
});
test('rejects replay of an already-used token', function () {
$token = (string) Str::uuid();
$file1 = UploadedFile::fake()->image('one.png', 50, 50);
$file2 = UploadedFile::fake()->image('two.png', 50, 50);
$this->post(signedUploadUrl($this->workspace, $token), ['media' => $file1])->assertCreated();
$this->postJson(signedUploadUrl($this->workspace, $token), ['media' => $file2])->assertStatus(409);
expect(Media::where('upload_token', $token)->count())->toBe(1);
});
test('rejects file larger than 50MB', function () {
$token = (string) Str::uuid();
$file = UploadedFile::fake()->create('huge.mp4', 51 * 1024 + 1, 'video/mp4');
$response = $this->postJson(signedUploadUrl($this->workspace, $token), ['media' => $file]);
$response->assertStatus(422);
expect(Media::where('upload_token', $token)->exists())->toBeFalse();
});
test('rejects disallowed mime type', function () {
$token = (string) Str::uuid();
$file = UploadedFile::fake()->create('evil.exe', 10, 'application/octet-stream');
$response = $this->postJson(signedUploadUrl($this->workspace, $token), ['media' => $file]);
$response->assertStatus(422);
expect(Media::where('upload_token', $token)->exists())->toBeFalse();
});
test('rate limits floods from the same IP', function () {
for ($i = 0; $i < 10; $i++) {
$this->postJson(
signedUploadUrl($this->workspace, (string) Str::uuid()),
['media' => UploadedFile::fake()->image("f{$i}.png", 16, 16)],
);
}
$response = $this->postJson(
signedUploadUrl($this->workspace, (string) Str::uuid()),
['media' => UploadedFile::fake()->image('over.png', 16, 16)],
);
$response->assertStatus(429);
});