Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions app/Http/Controllers/Api/V1/FileController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api\V1;

use App\Actions\Files\StoreUploadedFileAction;
use App\Http\Requests\Api\V1\UploadFileRequest;
use App\Http\Resources\Api\V1\FileResource;
use App\Http\Resources\Api\V1\TargetFormatResource;
use App\Models\FileRecord;
use App\Support\Converters\ConverterRegistry;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

final class FileController
{
public function __construct(
private readonly ConverterRegistry $registry,
) {}

public function store(UploadFileRequest $request): JsonResponse
{
$fileRecord = app(StoreUploadedFileAction::class)->handle(
user: $request->user(),
file: $request->file('file'),
);

return (new FileResource($fileRecord))
->response()
->setStatusCode(201);
}

public function targets(Request $request, FileRecord $file): JsonResource
{
abort_if(
$file->user_id !== $request->user()->id,
403,
'You do not own this file.',
);

$targets = $this->registry->targetsFor($file->extension);

return TargetFormatResource::collection($targets);
}
}
17 changes: 17 additions & 0 deletions app/Http/Requests/Api/V1/UploadFileRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace App\Http\Requests\Api\V1;

use Illuminate\Foundation\Http\FormRequest;

final class UploadFileRequest extends FormRequest
{
public function rules(): array
{
return [
'file' => ['required', 'file'],
];
}
}
26 changes: 26 additions & 0 deletions app/Http/Resources/Api/V1/FileResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace App\Http\Resources\Api\V1;

use App\Models\FileRecord;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

/** @mixin FileRecord */
final class FileResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'original_name' => $this->original_name,
'extension' => $this->extension,
'mime_type' => $this->mime_type,
'size_bytes' => $this->size_bytes,
'created_at' => $this->created_at?->toIso8601String(),
'expires_at' => $this->expires_at?->toIso8601String(),
];
}
}
23 changes: 23 additions & 0 deletions app/Http/Resources/Api/V1/TargetFormatResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace App\Http\Resources\Api\V1;

use App\Support\Converters\DTO\ConverterTarget;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

/** @mixin ConverterTarget */
final class TargetFormatResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'target_format' => $this->format,
'label' => $this->label,
'description' => $this->description,
'recommended' => $this->recommended,
];
}
}
3 changes: 3 additions & 0 deletions routes/api.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use App\Http\Controllers\Api\V1\ConverterController;
use App\Http\Controllers\Api\V1\FileController;
use Illuminate\Support\Facades\Route;

Route::prefix('v1')
Expand All @@ -24,5 +25,7 @@

Route::get('/converters', [ConverterController::class, 'index'])->name('converters.index');
Route::get('/converters/{source}/{target}/schema', [ConverterController::class, 'schema'])->name('converters.schema');

Route::post('/files', [FileController::class, 'store'])->name('files.store');
});
});
55 changes: 55 additions & 0 deletions tests/Feature/Api/V1/FileUploadEndpointTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

use App\Enums\Plan;
use App\Models\User;
use App\Services\Api\ApiKeyGenerator;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;

it('uploads file through api', function () {
Storage::fake('local');

$user = User::factory()->create(['plan' => Plan::Pro]);
$token = app(ApiKeyGenerator::class)->create($user, 'API')->plainToken;

$file = UploadedFile::fake()->image('image.png', 100, 100);

$this->withToken($token)
->postJson('/api/v1/files', ['file' => $file])
->assertCreated()
->assertJsonPath('data.original_name', 'image.png')
->assertJsonPath('data.extension', 'png')
->assertJsonStructure(['data' => ['id', 'original_name', 'extension', 'size_bytes', 'created_at']]);

$this->assertDatabaseHas('files', [
'user_id' => $user->id,
'original_name' => 'image.png',
'extension' => 'png',
]);
});

it('requires file field to upload', function () {
$user = User::factory()->create(['plan' => Plan::Pro]);
$token = app(ApiKeyGenerator::class)->create($user, 'API')->plainToken;

$this->withToken($token)
->postJson('/api/v1/files', [])
->assertUnprocessable();
});

it('requires api key authentication to upload file', function () {
$this->postJson('/api/v1/files', [])
->assertUnauthorized();
});

it('rejects free user from file upload endpoint', function () {
$user = User::factory()->create(['plan' => Plan::Free]);
$token = app(ApiKeyGenerator::class)->create($user, 'Free')->plainToken;

$this->withToken($token)
->postJson('/api/v1/files', [])
->assertForbidden()
->assertJsonPath('error.code', 'api_not_available');
});