- Event-Medien tragen Typ (image/video/audio), Dauer und Notiz; Videos bekommen ein Poster, Audio wird per ffmpeg nach AAC (audio/mp4) gewandelt, Originale werden mit Range-Unterstützung gestreamt. - Upload nimmt eine Notiz (caption) direkt mit, PATCH ändert sie später. - Neue Event-Felder: Zeitraum (end_date), Kategorie, Personen, Meilenstein. Regeln zentral in EventFields, damit CRUD und Sync gleich prüfen. - Titel darf leer sein: ein Event entsteht mit dem ersten Eintrag. - Artisan-Befehl media:transcode-voice-notes wandelt alte WebM/Ogg-Notizen, die Safari nicht abspielen kann. - Login-Throttling, Settings-Validierung, Sync-Fehlercodes ohne Rohtext. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
102 lines
3.5 KiB
PHP
102 lines
3.5 KiB
PHP
<?php
|
|
|
|
use App\Models\Event;
|
|
use App\Models\EventMedia;
|
|
use App\Models\User;
|
|
use App\Services\EventMediaProbe;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
function legacyVoiceNote(string $mime = 'audio/webm', string $extension = 'webm'): EventMedia
|
|
{
|
|
$user = User::factory()->create();
|
|
$event = Event::factory()->create(['user_id' => $user->id]);
|
|
$path = "event-media/{$user->id}/{$event->client_id}/legacy.{$extension}";
|
|
Storage::disk('local')->put($path, 'not really audio');
|
|
|
|
return EventMedia::factory()->create([
|
|
'event_id' => $event->id,
|
|
'user_id' => $user->id,
|
|
'collection' => 'voice_note',
|
|
'type' => 'audio',
|
|
'mime_type' => $mime,
|
|
'path' => $path,
|
|
'disk' => 'local',
|
|
'thumbnail_path' => null,
|
|
'preview_path' => null,
|
|
]);
|
|
}
|
|
|
|
test('converts legacy webm voice notes to audio/mp4 and drops the old file', function () {
|
|
Storage::fake('local');
|
|
$media = legacyVoiceNote();
|
|
$oldPath = $media->path;
|
|
|
|
$probe = Mockery::mock(EventMediaProbe::class);
|
|
$probe->shouldReceive('transcodeAudioToM4a')
|
|
->once()
|
|
->withArgs(function (string $source, string $target) use ($oldPath): bool {
|
|
expect($source)->toEndWith($oldPath);
|
|
expect($target)->toEndWith('legacy.m4a');
|
|
file_put_contents($target, 'aac bytes');
|
|
|
|
return true;
|
|
})
|
|
->andReturn(true);
|
|
$probe->shouldReceive('durationMs')->once()->andReturn(4200);
|
|
$this->app->instance(EventMediaProbe::class, $probe);
|
|
|
|
$this->artisan('media:transcode-voice-notes')
|
|
->expectsOutputToContain('1 voice note(s) to convert.')
|
|
->expectsOutputToContain('Converted 1, failed 0.')
|
|
->assertSuccessful();
|
|
|
|
$media->refresh();
|
|
expect($media->mime_type)->toBe('audio/mp4')
|
|
->and($media->path)->toEndWith('legacy.m4a')
|
|
->and($media->duration_ms)->toBe(4200)
|
|
->and($media->size)->toBe(strlen('aac bytes'));
|
|
Storage::disk('local')->assertExists($media->path);
|
|
Storage::disk('local')->assertMissing($oldPath);
|
|
});
|
|
|
|
test('leaves audio/mp4 voice notes alone and reports when nothing is to do', function () {
|
|
Storage::fake('local');
|
|
legacyVoiceNote('audio/mp4', 'm4a');
|
|
|
|
$probe = Mockery::mock(EventMediaProbe::class);
|
|
$probe->shouldNotReceive('transcodeAudioToM4a');
|
|
$this->app->instance(EventMediaProbe::class, $probe);
|
|
|
|
$this->artisan('media:transcode-voice-notes')
|
|
->expectsOutputToContain('already audio/mp4')
|
|
->assertSuccessful();
|
|
});
|
|
|
|
test('dry run only lists what would change', function () {
|
|
Storage::fake('local');
|
|
$media = legacyVoiceNote();
|
|
|
|
$probe = Mockery::mock(EventMediaProbe::class);
|
|
$probe->shouldNotReceive('transcodeAudioToM4a');
|
|
$this->app->instance(EventMediaProbe::class, $probe);
|
|
|
|
$this->artisan('media:transcode-voice-notes --dry-run')
|
|
->expectsOutputToContain("#{$media->id}: audio/webm → audio/mp4")
|
|
->assertSuccessful();
|
|
|
|
expect($media->fresh()->mime_type)->toBe('audio/webm');
|
|
});
|
|
|
|
test('a failed conversion keeps the original and exits with failure', function () {
|
|
Storage::fake('local');
|
|
$media = legacyVoiceNote();
|
|
|
|
$probe = Mockery::mock(EventMediaProbe::class);
|
|
$probe->shouldReceive('transcodeAudioToM4a')->once()->andReturn(false);
|
|
$this->app->instance(EventMediaProbe::class, $probe);
|
|
|
|
$this->artisan('media:transcode-voice-notes')->assertFailed();
|
|
|
|
expect($media->fresh()->mime_type)->toBe('audio/webm');
|
|
Storage::disk('local')->assertExists($media->path);
|
|
});
|