feat: add routes, lang, tests, stubs, docs, and docker configurations

This commit is contained in:
2026-05-21 16:05:16 +07:00
parent fad70d096b
commit 28a06315b8
3385 changed files with 177070 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
<?php
use App\Models\Permission;
use App\Models\User;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
function makeEditorUser(): User
{
$user = User::factory()->create();
$perm = Permission::firstOrCreate(['name' => 'manage global settings', 'guard_name' => 'web']);
$user->givePermissionTo($perm);
return $user;
}
beforeEach(function () {
Storage::fake('public');
});
// ── Access control ─────────────────────────────────────────────────────────────
test('guest cannot upload editor images', function () {
$this->postJson('/editor/upload')->assertUnauthorized();
});
test('user without manage global settings cannot upload', function () {
$user = User::factory()->create();
$this->actingAs($user)
->postJson('/editor/upload')
->assertForbidden();
});
// ── Missing file ───────────────────────────────────────────────────────────────
test('upload without file returns 400', function () {
$user = makeEditorUser();
$this->actingAs($user)
->postJson('/editor/upload')
->assertStatus(400)
->assertJsonPath('error.message', 'No file uploaded.');
});
// ── Type validation ────────────────────────────────────────────────────────────
test('upload rejects non-image file', function () {
$user = makeEditorUser();
$file = UploadedFile::fake()->create('shell.php', 100, 'application/x-php');
$this->actingAs($user)
->postJson('/editor/upload', ['upload' => $file])
->assertStatus(422);
});
test('upload rejects svg (not in allowed mimes)', function () {
$user = makeEditorUser();
$file = UploadedFile::fake()->create('vector.svg', 50, 'image/svg+xml');
$this->actingAs($user)
->postJson('/editor/upload', ['upload' => $file])
->assertStatus(422);
});
test('upload accepts jpeg image', function () {
$user = makeEditorUser();
$file = UploadedFile::fake()->image('photo.jpg', 800, 600);
$this->actingAs($user)
->post('/editor/upload', ['upload' => $file])
->assertOk()
->assertJsonPath('uploaded', 1)
->assertJsonStructure(['url', 'fileName']);
});
test('upload accepts png image', function () {
$user = makeEditorUser();
$file = UploadedFile::fake()->image('icon.png', 100, 100);
$this->actingAs($user)
->post('/editor/upload', ['upload' => $file])
->assertOk()
->assertJsonPath('uploaded', 1);
});
test('upload accepts webp image', function () {
$user = makeEditorUser();
$file = UploadedFile::fake()->image('modern.webp', 400, 300);
$this->actingAs($user)
->post('/editor/upload', ['upload' => $file])
->assertOk()
->assertJsonPath('uploaded', 1);
});
// ── Size limit ─────────────────────────────────────────────────────────────────
test('upload rejects image exceeding 5 MB', function () {
$user = makeEditorUser();
$file = UploadedFile::fake()->create('big.jpg', 6000, 'image/jpeg'); // 6 MB
$this->actingAs($user)
->postJson('/editor/upload', ['upload' => $file])
->assertStatus(422);
});
// ── Response structure ─────────────────────────────────────────────────────────
test('successful upload response has uploaded, fileName, url keys', function () {
$user = makeEditorUser();
$file = UploadedFile::fake()->image('test.jpg');
$response = $this->actingAs($user)
->post('/editor/upload', ['upload' => $file])
->assertOk()
->json();
expect($response)->toHaveKeys(['uploaded', 'fileName', 'url']);
expect($response['uploaded'])->toBe(1);
expect($response['url'])->toStartWith('/storage/');
});
// ── File is actually stored ────────────────────────────────────────────────────
test('uploaded file is persisted to the public disk under editor/', function () {
$user = makeEditorUser();
$file = UploadedFile::fake()->image('stored.jpg');
$response = $this->actingAs($user)
->post('/editor/upload', ['upload' => $file])
->json();
// Strip /storage/ prefix to get the relative path on the public disk
$relativePath = substr((string) $response['url'], strlen('/storage/'));
Storage::disk('public')->assertExists($relativePath);
});