trypost/tests/Feature/Api/UploadControllerTest.php
Paulo Castellano 564f157e44 Fix MCP upload rate limits and Instagram Reel duration caps.
Key signed uploads by workspace so ChatGPT's shared egress IPs don't throttle tenants together, raise the MCP upload cap to 300MB, and expose accurate Reel max durations via API/MCP.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 21:21:39 -03:00

160 lines
5.3 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 the MCP upload cap', function () {
config(['ai.mcp.upload.max_size_mb' => 1]);
$token = (string) Str::uuid();
$file = UploadedFile::fake()->create('huge.mp4', 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 workspace', function () {
for ($i = 0; $i < 60; $i++) {
$this->postJson(
signedUploadUrl($this->workspace, (string) Str::uuid()),
['media' => UploadedFile::fake()->image("f{$i}.png", 16, 16)],
)->assertSuccessful();
}
$this->postJson(
signedUploadUrl($this->workspace, (string) Str::uuid()),
['media' => UploadedFile::fake()->image('over.png', 16, 16)],
)->assertStatus(429);
});
test('different workspaces on the same IP do not share the upload rate limit', function () {
$otherWorkspace = Workspace::factory()->create();
for ($i = 0; $i < 60; $i++) {
$this->postJson(
signedUploadUrl($this->workspace, (string) Str::uuid()),
['media' => UploadedFile::fake()->image("a{$i}.png", 16, 16)],
)->assertSuccessful();
}
$this->postJson(
signedUploadUrl($this->workspace, (string) Str::uuid()),
['media' => UploadedFile::fake()->image('blocked.png', 16, 16)],
)->assertStatus(429);
$this->postJson(
signedUploadUrl($otherWorkspace, (string) Str::uuid()),
['media' => UploadedFile::fake()->image('other.png', 16, 16)],
)->assertSuccessful();
});