Files
SkinbaseNova/tests/Feature/Uploads/UploadQuotaTest.php
2026-03-28 19:15:39 +01:00

231 lines
6.6 KiB
PHP

<?php
use App\Models\Artwork;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
uses(RefreshDatabase::class);
function createUploadRowForQuota(int $userId, array $overrides = []): string
{
$id = (string) Str::uuid();
$defaults = [
'id' => $id,
'user_id' => $userId,
'type' => 'image',
'status' => 'draft',
'title' => null,
'slug' => null,
'category_id' => null,
'description' => null,
'tags' => null,
'license' => null,
'nsfw' => false,
'is_scanned' => false,
'has_tags' => false,
'preview_path' => null,
'published_at' => null,
'final_path' => null,
'expires_at' => null,
'created_at' => now(),
'updated_at' => now(),
];
DB::table('uploads')->insert(array_merge($defaults, $overrides));
return $id;
}
function attachMainUploadFileForQuota(string $uploadId, int $size, string $hash = 'hash-main'): void
{
DB::table('upload_files')->insert([
'upload_id' => $uploadId,
'path' => "tmp/drafts/{$uploadId}/main/file.bin",
'type' => 'main',
'hash' => $hash,
'size' => $size,
'mime' => 'application/octet-stream',
'created_at' => now(),
]);
}
it('enforces draft count limit', function () {
Storage::fake('local');
config(['uploads.draft_quota.max_drafts_per_user' => 1]);
$user = User::factory()->create();
createUploadRowForQuota($user->id, ['status' => 'draft']);
$main = UploadedFile::fake()->image('wallpaper.jpg', 600, 400);
$response = $this->actingAs($user)->postJson('/api/uploads/preload', [
'main' => $main,
]);
$response->assertStatus(429)
->assertJsonPath('message', 'draft_limit')
->assertJsonPath('code', 'draft_limit');
});
it('enforces draft storage limit', function () {
Storage::fake('local');
config([
'uploads.draft_quota.max_drafts_per_user' => 20,
'uploads.draft_quota.max_draft_storage_mb_per_user' => 1,
]);
$user = User::factory()->create();
$existingDraftId = createUploadRowForQuota($user->id, ['status' => 'draft']);
attachMainUploadFileForQuota($existingDraftId, 400 * 1024, 'existing-hash');
$main = UploadedFile::fake()->create('large.jpg', 700, 'image/jpeg');
$response = $this->actingAs($user)->postJson('/api/uploads/preload', [
'main' => $main,
]);
$response->assertStatus(413)
->assertJsonPath('message', 'storage_limit')
->assertJsonPath('code', 'storage_limit');
});
it('blocks duplicate hash when policy is block', function () {
Storage::fake('local');
config([
'uploads.draft_quota.max_drafts_per_user' => 20,
'uploads.draft_quota.duplicate_hash_policy' => 'block',
]);
$owner = User::factory()->create();
$uploader = User::factory()->create();
$main = UploadedFile::fake()->image('dupe.jpg', 400, 400);
$hash = hash_file('sha256', $main->getPathname());
Artwork::factory()->create([
'user_id' => $owner->id,
'hash' => $hash,
'artwork_status' => 'published',
'published_at' => now()->subMinute(),
'is_public' => true,
]);
$response = $this->actingAs($uploader)->postJson('/api/uploads/preload', [
'main' => $main,
]);
$response->assertStatus(422)
->assertJsonPath('message', 'duplicate_upload')
->assertJsonPath('code', 'duplicate_upload');
});
it('allows duplicate hash and returns warning when policy is warn', function () {
Storage::fake('local');
config([
'uploads.draft_quota.max_drafts_per_user' => 20,
'uploads.draft_quota.duplicate_hash_policy' => 'warn',
]);
$owner = User::factory()->create();
$uploader = User::factory()->create();
$main = UploadedFile::fake()->image('dupe-warn.jpg', 400, 400);
$hash = hash_file('sha256', $main->getPathname());
Artwork::factory()->create([
'user_id' => $owner->id,
'hash' => $hash,
'artwork_status' => 'published',
'published_at' => now()->subMinute(),
'is_public' => true,
]);
$response = $this->actingAs($uploader)->postJson('/api/uploads/preload', [
'main' => $main,
]);
$response->assertOk()
->assertJsonStructure(['upload_id', 'status', 'expires_at', 'warnings'])
->assertJsonPath('warnings.0', 'duplicate_hash');
});
it('does not count published uploads as drafts', function () {
Storage::fake('local');
config(['uploads.draft_quota.max_drafts_per_user' => 1]);
$user = User::factory()->create();
createUploadRowForQuota($user->id, [
'status' => 'published',
'published_at' => now()->subHour(),
]);
$main = UploadedFile::fake()->image('new.jpg', 640, 480);
$response = $this->actingAs($user)->postJson('/api/uploads/preload', [
'main' => $main,
]);
$response->assertOk()->assertJsonStructure([
'upload_id',
'status',
'expires_at',
]);
});
it('ignores duplicate hashes that exist only in temporary upload tables', function () {
Storage::fake('local');
config([
'uploads.draft_quota.max_drafts_per_user' => 20,
'uploads.draft_quota.duplicate_hash_policy' => 'block',
]);
$owner = User::factory()->create();
$uploader = User::factory()->create();
$main = UploadedFile::fake()->image('temp-only-dupe.jpg', 400, 400);
$hash = hash_file('sha256', $main->getPathname());
$publishedUploadId = createUploadRowForQuota($owner->id, [
'status' => 'published',
'published_at' => now()->subMinute(),
]);
attachMainUploadFileForQuota($publishedUploadId, (int) $main->getSize(), $hash);
$response = $this->actingAs($uploader)->postJson('/api/uploads/preload', [
'main' => $main,
]);
$response->assertOk()->assertJsonStructure([
'upload_id',
'status',
'expires_at',
]);
});
it('returns stable machine codes for quota errors', function () {
Storage::fake('local');
config(['uploads.draft_quota.max_drafts_per_user' => 1]);
$user = User::factory()->create();
createUploadRowForQuota($user->id, ['status' => 'draft']);
$main = UploadedFile::fake()->image('machine-code.jpg', 600, 400);
$response = $this->actingAs($user)->postJson('/api/uploads/preload', [
'main' => $main,
]);
$response->assertStatus(429)
->assertJson([
'message' => 'draft_limit',
'code' => 'draft_limit',
]);
});