From c954b21dd2c54d46eaaefc32c70a50bae5c57a55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Jaussoin?= Date: Thu, 9 Oct 2025 15:06:30 +0200 Subject: [PATCH] Fix FLEXIAPI-366 Add voicemails endpoints --- cron/flexiapi.debian | 5 +- cron/flexiapi.redhat | 5 +- flexiapi/app/Account.php | 12 ++ flexiapi/app/AccountFile.php | 52 ++++++ .../Accounts/ClearAccountsTombstones.php | 5 - .../Console/Commands/Accounts/ClearFiles.php | 35 ++++ .../Commands/Accounts/ClearUnconfirmed.php | 5 - .../Commands/Accounts/CreateAdminTest.php | 5 - .../app/Console/Commands/Accounts/Seed.php | 5 - .../Console/Commands/Accounts/SetAdmin.php | 5 - flexiapi/app/Helpers/Utils.php | 22 ++- .../Api/Account/FileController.php | 51 ++++++ .../Api/Account/VoicemailController.php | 30 ++++ .../Api/Admin/Account/VoicemailController.php | 50 ++++++ ...0_20_093414_create_account_files_table.php | 29 ++++ .../accounts/voicemail.blade.php | 57 +++++++ .../api/documentation_markdown.blade.php | 17 ++ flexiapi/routes/api.php | 6 + flexiapi/tests/Feature/ApiSpaceTest.php | 1 - flexiapi/tests/Feature/ApiVoicemailTest.php | 157 ++++++++++++++++++ 20 files changed, 520 insertions(+), 34 deletions(-) create mode 100644 flexiapi/app/AccountFile.php create mode 100644 flexiapi/app/Console/Commands/Accounts/ClearFiles.php create mode 100644 flexiapi/app/Http/Controllers/Api/Account/FileController.php create mode 100644 flexiapi/app/Http/Controllers/Api/Account/VoicemailController.php create mode 100644 flexiapi/app/Http/Controllers/Api/Admin/Account/VoicemailController.php create mode 100644 flexiapi/database/migrations/2025_10_20_093414_create_account_files_table.php create mode 100644 flexiapi/resources/views/api/documentation/accounts/voicemail.blade.php create mode 100644 flexiapi/tests/Feature/ApiVoicemailTest.php diff --git a/cron/flexiapi.debian b/cron/flexiapi.debian index 73a4447..a44db02 100644 --- a/cron/flexiapi.debian +++ b/cron/flexiapi.debian @@ -1,8 +1,9 @@ #!/bin/sh cd /opt/belledonne-communications/share/flexisip-account-manager/flexiapi/ -sudo -su www-data && php artisan digest:clear-nonces 60 -sudo -su www-data && php artisan accounts:clear-api-keys 60 sudo -su www-data && php artisan accounts:clear-accounts-tombstones 7 --apply +sudo -su www-data && php artisan accounts:clear-api-keys 60 +sudo -su www-data && php artisan accounts:clear-files 30 --apply sudo -su www-data && php artisan accounts:clear-unconfirmed 30 --apply +sudo -su www-data && php artisan digest:clear-nonces 60 sudo -su www-data && php artisan spaces:expiration-emails diff --git a/cron/flexiapi.redhat b/cron/flexiapi.redhat index af3c529..1954c82 100644 --- a/cron/flexiapi.redhat +++ b/cron/flexiapi.redhat @@ -1,8 +1,9 @@ #!/bin/sh cd /opt/belledonne-communications/share/flexisip-account-manager/flexiapi/ -php artisan digest:clear-nonces 60 -php artisan accounts:clear-api-keys 60 php artisan accounts:clear-accounts-tombstones 7 --apply +php artisan accounts:clear-api-keys 60 +php artisan accounts:clear-files 30 --apply php artisan accounts:clear-unconfirmed 30 --apply +php artisan digest:clear-nonces 60 php artisan spaces:expiration-emails \ No newline at end of file diff --git a/flexiapi/app/Account.php b/flexiapi/app/Account.php index e193c2b..df15c77 100644 --- a/flexiapi/app/Account.php +++ b/flexiapi/app/Account.php @@ -135,6 +135,18 @@ class Account extends Authenticatable return $this->belongsToMany(Account::class, 'contacts', 'account_id', 'contact_id'); } + public function files() + { + return $this->hasMany(AccountFile::class)->latest(); + } + + public function voicemails() + { + return $this->hasMany(AccountFile::class) + ->whereIn('content_type', AccountFile::VOICEMAIL_CONTENTTYPES) + ->latest(); + } + public function vcardsStorage() { return $this->hasMany(VcardStorage::class); diff --git a/flexiapi/app/AccountFile.php b/flexiapi/app/AccountFile.php new file mode 100644 index 0000000..404f28f --- /dev/null +++ b/flexiapi/app/AccountFile.php @@ -0,0 +1,52 @@ +getPathAttribute()); + }); + } + + public function account() + { + return $this->belongsTo(Account::class); + } + + public function getMaxUploadSizeAttribute(): ?int + { + return maxUploadSize(); + } + + public function getUploadUrlAttribute(): ?string + { + return route('file.upload', $this->attributes['id']); + } + + public function getPathAttribute(): string + { + return self::FILES_PATH . '/' . $this->attributes['name']; + } + + public function getDownloadUrlAttribute(): ?string + { + return !empty($this->attributes['name']) + && !empty($this->attributes['id']) + ? route('file.show', [$this->attributes['id'], $this->attributes['name']]) + : null; + } +} diff --git a/flexiapi/app/Console/Commands/Accounts/ClearAccountsTombstones.php b/flexiapi/app/Console/Commands/Accounts/ClearAccountsTombstones.php index 4493e08..1fed49d 100644 --- a/flexiapi/app/Console/Commands/Accounts/ClearAccountsTombstones.php +++ b/flexiapi/app/Console/Commands/Accounts/ClearAccountsTombstones.php @@ -29,11 +29,6 @@ class ClearAccountsTombstones extends Command protected $signature = 'accounts:clear-accounts-tombstones {days} {--apply}'; protected $description = 'Clear deleted accounts tombstones after n days'; - public function __construct() - { - parent::__construct(); - } - public function handle() { $tombstones = AccountTombstone::where( diff --git a/flexiapi/app/Console/Commands/Accounts/ClearFiles.php b/flexiapi/app/Console/Commands/Accounts/ClearFiles.php new file mode 100644 index 0000000..e6a7ac5 --- /dev/null +++ b/flexiapi/app/Console/Commands/Accounts/ClearFiles.php @@ -0,0 +1,35 @@ +subDays($this->argument('days'))->toDateTimeString() + ); + + $count = $files->count(); + + if ($this->option('apply')) { + $this->info($count . ' files in deletion…'); + $files->delete(); + $this->info($count . ' files deleted'); + + return Command::SUCCESS; + } + + $this->info($count . ' files to delete'); + return Command::SUCCESS; + } +} diff --git a/flexiapi/app/Console/Commands/Accounts/ClearUnconfirmed.php b/flexiapi/app/Console/Commands/Accounts/ClearUnconfirmed.php index 50d333e..f1d2a1e 100644 --- a/flexiapi/app/Console/Commands/Accounts/ClearUnconfirmed.php +++ b/flexiapi/app/Console/Commands/Accounts/ClearUnconfirmed.php @@ -29,11 +29,6 @@ class ClearUnconfirmed extends Command protected $signature = 'accounts:clear-unconfirmed {days} {--apply} {--and-confirmed}'; protected $description = 'Clear unconfirmed accounts after n days'; - public function __construct() - { - parent::__construct(); - } - public function handle() { $accounts = Account::where( diff --git a/flexiapi/app/Console/Commands/Accounts/CreateAdminTest.php b/flexiapi/app/Console/Commands/Accounts/CreateAdminTest.php index 9e1c674..5f405c8 100644 --- a/flexiapi/app/Console/Commands/Accounts/CreateAdminTest.php +++ b/flexiapi/app/Console/Commands/Accounts/CreateAdminTest.php @@ -30,11 +30,6 @@ class CreateAdminTest extends Command protected $signature = 'accounts:create-admin-test'; protected $description = 'Create a test admin account, only for tests purpose'; - public function __construct() - { - parent::__construct(); - } - public function handle() { $username = 'admin_test'; diff --git a/flexiapi/app/Console/Commands/Accounts/Seed.php b/flexiapi/app/Console/Commands/Accounts/Seed.php index f54cbf6..889ebf4 100644 --- a/flexiapi/app/Console/Commands/Accounts/Seed.php +++ b/flexiapi/app/Console/Commands/Accounts/Seed.php @@ -28,11 +28,6 @@ class Seed extends Command protected $signature = 'accounts:seed {json-file-path}'; protected $description = 'Seed some accounts from a JSON file'; - public function __construct() - { - parent::__construct(); - } - public function handle() { $file = $this->argument('json-file-path'); diff --git a/flexiapi/app/Console/Commands/Accounts/SetAdmin.php b/flexiapi/app/Console/Commands/Accounts/SetAdmin.php index 157cf46..d6d0540 100644 --- a/flexiapi/app/Console/Commands/Accounts/SetAdmin.php +++ b/flexiapi/app/Console/Commands/Accounts/SetAdmin.php @@ -28,11 +28,6 @@ class SetAdmin extends Command protected $signature = 'accounts:set-admin {id}'; protected $description = 'Give the admin role to an account'; - public function __construct() - { - parent::__construct(); - } - public function handle() { $account = Account::withoutGlobalScopes()->where('id', $this->argument('id'))->first(); diff --git a/flexiapi/app/Helpers/Utils.php b/flexiapi/app/Helpers/Utils.php index f61d0a8..7ee62c2 100644 --- a/flexiapi/app/Helpers/Utils.php +++ b/flexiapi/app/Helpers/Utils.php @@ -17,6 +17,7 @@ along with this program. If not, see . */ +use Illuminate\Support\Facades\App; use Illuminate\Support\Str; use App\Account; @@ -30,13 +31,13 @@ use Illuminate\Support\Facades\DB; function space(): ?Space { - return is_object(request()->space) ? request()->space :null; + return is_object(request()->space) ? request()->space : null; } function passwordAlgorithms(): array { return [ - 'MD5' => 'md5', + 'MD5' => 'md5', 'SHA-256' => 'sha256', ]; } @@ -100,7 +101,7 @@ function markdownDocumentationView(string $view): string $converter->getEnvironment()->addExtension(new TableOfContentsExtension()); return (string) $converter->convert( - (string)view($view, [ + (string) view($view, [ 'app_name' => space()->name ])->render() ); @@ -161,6 +162,19 @@ function resolveDomain(Request $request): string : $request->space->domain; } +function maxUploadSize(): ?int +{ + $uploadMaxSizeInBytes = ini_parse_quantity(ini_get('upload_max_filesize')); + if ($uploadMaxSizeInBytes > 0) { + return $uploadMaxSizeInBytes / 1024; + } + + $postMaxSizeInBytes = ini_parse_quantity(ini_get('post_max_size')); + if ($postMaxSizeInBytes > 0) { + return $postMaxSizeInBytes / 1024; + } +} + function captchaConfigured(): bool { return env('HCAPTCHA_SECRET', false) != false || env('HCAPTCHA_SITEKEY', false) != false; @@ -205,7 +219,7 @@ function validateIsoDate($attribute, $value, $parameters, $validator): bool // Regex from https://www.myintervals.com/blog/2009/05/20/iso-8601-date-validation-that-doesnt-suck/ : '/^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/'; - return (bool)preg_match($regex, $value); + return (bool) preg_match($regex, $value); } /** diff --git a/flexiapi/app/Http/Controllers/Api/Account/FileController.php b/flexiapi/app/Http/Controllers/Api/Account/FileController.php new file mode 100644 index 0000000..6c61b9f --- /dev/null +++ b/flexiapi/app/Http/Controllers/Api/Account/FileController.php @@ -0,0 +1,51 @@ +name != $name) { + abort(404); + } + + return Storage::download($file->path); + } + + public function upload(Request $request, string $uuid) + { + $file = AccountFile::findOrFail($uuid); + + if (!empty($file->name)) { + abort(404); + } + + $request->validate([ + 'file' => 'required|file|mimetypes:' . $file->content_type + ]); + + $uploadedFile = $request->file('file'); + $name = Str::random(8) . '_' . $uploadedFile->getClientOriginalName(); + + if ($uploadedFile->storeAs('files', $name)) { + $file->name = $name; + $file->size = $uploadedFile->getSize(); + $file->uploaded_at = Carbon::now(); + $file->save(); + + return $file; + } + + abort(503); + } +} diff --git a/flexiapi/app/Http/Controllers/Api/Account/VoicemailController.php b/flexiapi/app/Http/Controllers/Api/Account/VoicemailController.php new file mode 100644 index 0000000..5039121 --- /dev/null +++ b/flexiapi/app/Http/Controllers/Api/Account/VoicemailController.php @@ -0,0 +1,30 @@ +index($request, $request->user()->id); + } + + public function store(Request $request) + { + return (new AdminVoicemailController)->store($request, $request->user()->id); + } + + public function show(Request $request, string $uuid) + { + return (new AdminVoicemailController)->show($request, $request->user()->id, $uuid); + } + + public function destroy(Request $request, string $uuid) + { + return (new AdminVoicemailController)->destroy($request, $request->user()->id, $uuid); + } +} diff --git a/flexiapi/app/Http/Controllers/Api/Admin/Account/VoicemailController.php b/flexiapi/app/Http/Controllers/Api/Admin/Account/VoicemailController.php new file mode 100644 index 0000000..e2db717 --- /dev/null +++ b/flexiapi/app/Http/Controllers/Api/Admin/Account/VoicemailController.php @@ -0,0 +1,50 @@ +voicemails; + } + + public function store(Request $request, int $accountId) + { + $account = Account::findOrFail($accountId); + + $request->validate([ + 'sip_from' => 'nullable|starts_with:sip', + 'content_type' => [ + 'required', + Rule::in(AccountFile::VOICEMAIL_CONTENTTYPES), + ] + ]); + + $voicemail = new AccountFile; + $voicemail->account_id = $account->id; + $voicemail->sip_from = $request->get('sip_from'); + $voicemail->content_type = $request->get('content_type'); + $voicemail->save(); + + $voicemail->append(['upload_url', 'max_upload_size']); + + return $voicemail; + } + + public function show(Request $request, int $accountId, string $uuid) + { + return Account::findOrFail($accountId)->voicemails()->where('id', $uuid)->firstOrFail(); + } + + public function destroy(Request $request, int $accountId, string $uuid) + { + return Account::findOrFail($accountId)->voicemails()->where('id', $uuid)->delete(); + } +} diff --git a/flexiapi/database/migrations/2025_10_20_093414_create_account_files_table.php b/flexiapi/database/migrations/2025_10_20_093414_create_account_files_table.php new file mode 100644 index 0000000..6be05d2 --- /dev/null +++ b/flexiapi/database/migrations/2025_10_20_093414_create_account_files_table.php @@ -0,0 +1,29 @@ +uuid('id')->primary(); + $table->integer('account_id')->unsigned()->nullable(); + $table->foreign('account_id')->references('id') + ->on('accounts')->onDelete('cascade'); + $table->string('name')->nullable(); + $table->integer('size')->nullable(); + $table->string('content_type')->index(); + $table->text('sip_from')->nullable(); + $table->dateTime('uploaded_at')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('account_files'); + } +}; diff --git a/flexiapi/resources/views/api/documentation/accounts/voicemail.blade.php b/flexiapi/resources/views/api/documentation/accounts/voicemail.blade.php new file mode 100644 index 0000000..7e4f505 --- /dev/null +++ b/flexiapi/resources/views/api/documentation/accounts/voicemail.blade.php @@ -0,0 +1,57 @@ +## Voicemails + +### `GET /accounts/{id/me}/voicemails` +Admin +User + +Return the currently stored voicemails + +### `GET /accounts/{id/me}/voicemails/{uuid}` +Admin +User + +``` +{ + id: '{uuid}', + sip_from: '{sip_address}', + get_url: 'https://{the file_url}', + file_size: 2451400, // the file size, in bytes + content_type: 'audio/{format}', + created_at: '2025-10-09T12:59:32Z', + uploaded_at: '2025-10-09T12:59:40Z' +} +``` + +Return a stored voicemail + +### `POST /accounts/{id/me}/voicemails` +Admin +User + +Create a new voicemail slot + +JSON parameters: + +* `sip_from`, mandatory, a valid SIP address +* `content_type`, mandatory, the content type of the audio file to upload, must be `audio/opus` or `audio/wav` + +This endpoint will return the following JSON: + +``` +{ + id: '{uuid}', + sip_from: '{sip_address}', + upload_url: 'https://{upload_service_unique_url}', // unique URL generated to upload the audio file + download_url: 'https://{download_service_unique_url}', // unique URL generated to download the audio file, null before upload + max_upload_size: 3000000, // here 3MB file size limit, in bytes + content_type: 'audio/{format}', + created_at: '2025-10-09T12:59:32Z', // time of the slot creation + uploaded_at: null // time when the slot was filled with the audio file +} +``` + +### `DELETE /accounts/{id/me}/voicemails/{uuid}` +Admin +User + +Delete a stored voicemail, if the file is managed by the platform it will be deleted as well \ No newline at end of file diff --git a/flexiapi/resources/views/api/documentation_markdown.blade.php b/flexiapi/resources/views/api/documentation_markdown.blade.php index 5b5f5a0..4d390df 100644 --- a/flexiapi/resources/views/api/documentation_markdown.blade.php +++ b/flexiapi/resources/views/api/documentation_markdown.blade.php @@ -39,6 +39,23 @@ Returns `pong` @include('api.documentation.accounts.vcards_storage') +@include('api.documentation.accounts.voicemail') + +## File Upload + +### `POST /files/{uuid}` +User + +Upload a file to a previously created slot. This endpoint will directly be returned when creating the upload slot in the `upload_url` parameter. + +Related endpoints: + +* [Voicemails](#voicemails) + +HTTP [Form-Data](https://developer.mozilla.org/fr/docs/Web/API/FormData) parameters: + +* `file` **required**, the file to upload, must have the same `content_type` as requested in the slot + ## Messages ### `POST /messages` diff --git a/flexiapi/routes/api.php b/flexiapi/routes/api.php index 5a988ee..f1728da 100644 --- a/flexiapi/routes/api.php +++ b/flexiapi/routes/api.php @@ -18,11 +18,13 @@ */ use App\Http\Controllers\Api\Account\VcardsStorageController; +use App\Http\Controllers\Api\Account\VoicemailController; use App\Http\Controllers\Api\Admin\Account\ActionController; use App\Http\Controllers\Api\Admin\Account\CardDavCredentialsController; use App\Http\Controllers\Api\Admin\Account\ContactController; use App\Http\Controllers\Api\Admin\Account\DictionaryController; use App\Http\Controllers\Api\Admin\Account\TypeController; +use App\Http\Controllers\Api\Admin\Account\VoicemailController as AdminVoicemailController; use App\Http\Controllers\Api\Admin\AccountController as AdminAccountController; use App\Http\Controllers\Api\Admin\ContactsListController; use App\Http\Controllers\Api\Admin\ExternalAccountController; @@ -55,10 +57,12 @@ Route::post('accounts/auth_token', 'Api\Account\AuthTokenController@store'); Route::get('accounts/me/api_key/{auth_token}', 'Api\Account\ApiKeyController@generateFromToken')->middleware('cookie', 'cookie.encrypt'); Route::get('phone_countries', 'Api\PhoneCountryController@index'); +Route::get('files/{uuid}/{name}', 'Api\Account\FileController@show')->name('file.show'); Route::group(['middleware' => ['auth.jwt', 'auth.digest_or_key', 'auth.check_blocked']], function () { Route::get('accounts/auth_token/{auth_token}/attach', 'Api\Account\AuthTokenController@attach'); Route::post('account_creation_tokens/consume', 'Api\Account\CreationTokenController@consume'); + Route::post('files/{uuid}', 'Api\Account\FileController@upload')->name('file.upload'); Route::post('push_notification', 'Api\Account\PushNotificationController@push'); @@ -86,6 +90,7 @@ Route::group(['middleware' => ['auth.jwt', 'auth.digest_or_key', 'auth.check_blo Route::get('contacts', 'Api\Account\ContactController@index'); Route::apiResource('vcards-storage', VcardsStorageController::class); + Route::apiResource('voicemails', VoicemailController::class, ['only' => ['index', 'show', 'store', 'destroy']]); }); Route::group(['middleware' => ['auth.admin']], function () { @@ -152,6 +157,7 @@ Route::group(['middleware' => ['auth.jwt', 'auth.digest_or_key', 'auth.check_blo Route::apiResource('accounts/{id}/actions', ActionController::class); Route::apiResource('account_types', TypeController::class); Route::apiResource('accounts/{account_id}/vcards-storage', AdminVcardsStorageController::class); + Route::apiResource('accounts/{id}/voicemails', AdminVoicemailController::class, ['only' => ['index', 'show', 'store', 'destroy']]); Route::apiResource('contacts_lists', ContactsListController::class); Route::prefix('contacts_lists')->controller(ContactsListController::class)->group(function () { diff --git a/flexiapi/tests/Feature/ApiSpaceTest.php b/flexiapi/tests/Feature/ApiSpaceTest.php index 4300988..4b840be 100644 --- a/flexiapi/tests/Feature/ApiSpaceTest.php +++ b/flexiapi/tests/Feature/ApiSpaceTest.php @@ -21,7 +21,6 @@ namespace Tests\Feature; use App\Account; use App\Space; -use Carbon\Carbon; use Tests\TestCase; class ApiSpaceTest extends TestCase diff --git a/flexiapi/tests/Feature/ApiVoicemailTest.php b/flexiapi/tests/Feature/ApiVoicemailTest.php new file mode 100644 index 0000000..829426d --- /dev/null +++ b/flexiapi/tests/Feature/ApiVoicemailTest.php @@ -0,0 +1,157 @@ +. +*/ + +namespace Tests\Feature; + +use App\Account; +use Illuminate\Http\UploadedFile; +use Tests\TestCase; + +class ApiVoicemailTest extends TestCase +{ + protected $route = '/api/accounts/me/voicemails'; + protected $uploadRoute = '/api/files/'; + + public function testAccount() + { + $account = Account::factory()->create(); + $account->generateUserApiKey(); + + $this->keyAuthenticated($account) + ->json('POST', $this->route, []) + ->assertJsonValidationErrors(['content_type']); + + $this->keyAuthenticated($account) + ->json('POST', $this->route, [ + 'content_type' => 'image/jpg' + ]) + ->assertJsonValidationErrors(['content_type']); + + $accountFile = $this->keyAuthenticated($account) + ->json('POST', $this->route, [ + 'content_type' => 'audio/opus' + ])->assertCreated(); + + $uuid = $accountFile->json()['id']; + + $this->keyAuthenticated($account) + ->get($this->route) + ->assertJsonFragment(['id' => $uuid]); + + $this->keyAuthenticated($account) + ->get($this->route . '/' . $uuid) + ->assertJsonFragment([ + 'id' => $uuid, + 'name' => null, + 'size' => null, + 'sip_from' => null, + 'uploaded_at' => null, + ]); + + $this->keyAuthenticated($account) + ->delete($this->route . '/' . $uuid) + ->assertOk(); + + $this->keyAuthenticated($account) + ->get($this->route . '/' . $uuid) + ->assertNotFound(); + } + + public function testAdmin() + { + $admin = Account::factory()->admin()->create(); + $admin->generateUserApiKey(); + + $account = Account::factory()->create(); + $account->generateUserApiKey(); + + $adminRoute = '/api/accounts/' . $account->id . '/voicemails'; + + $this->keyAuthenticated($account) + ->json('POST', $adminRoute, []) + ->assertForbidden(); + + $accountFile = $this->keyAuthenticated($admin) + ->json('POST', $adminRoute, [ + 'content_type' => 'audio/opus' + ])->assertCreated(); + + $uuid = $accountFile->json()['id']; + + $this->keyAuthenticated($admin) + ->get($adminRoute . '/' . $uuid) + ->assertJsonFragment(['id' => $uuid]); + + $this->keyAuthenticated($admin) + ->get($adminRoute . '/' . $uuid) + ->assertJsonFragment([ + 'id' => $uuid + ]); + + $this->keyAuthenticated($admin) + ->delete($adminRoute . '/' . $uuid) + ->assertOk(); + + $this->keyAuthenticated($admin) + ->get($adminRoute . '/' . $uuid) + ->assertNotFound(); + } + + public function testUpload() + { + $account = Account::factory()->create(); + $account->generateUserApiKey(); + + $accountFile = $this->keyAuthenticated($account) + ->json('POST', $this->route, [ + 'content_type' => 'audio/opus' + ])->assertCreated(); + + $uuid = $accountFile->json()['id']; + + $this->keyAuthenticated($account) + ->json('POST', $this->uploadRoute . $uuid, [ + 'file' => UploadedFile::fake()->image('photo.jpg') + ])->assertJsonValidationErrors(['file']); + + $this->keyAuthenticated($account) + ->json('POST', $this->uploadRoute . $uuid, [ + 'file' => UploadedFile::fake()->create('audio.wav', 500, 'audio/wav') + ])->assertJsonValidationErrors(['file']); + + $file = $this->keyAuthenticated($account) + ->json('POST', $this->uploadRoute . $uuid, data: [ + 'file' => UploadedFile::fake()->create('audio.opus', 500, 'audio/opus') + ])->assertOk(); + + $this->keyAuthenticated($account) + ->json('POST', $this->uploadRoute . $uuid, data: [ + 'file' => UploadedFile::fake()->create('audio.opus', 500, 'audio/opus') + ])->assertNotFound(); + + $this->head($file->json()['download_url'])->assertOk(); + + // Delete the file + $this->keyAuthenticated($account) + ->delete($this->route . '/' . $file->json()['id']) + ->assertOk(); + + $this->head($file->json()['download_url'])->assertNotFound(); + } +}