thats-me/backend/tests/Feature/Console/TranscodeVoiceNotesTest.php
Kevin Adametz b262a6771f Backend: Medien-Typen, Sprachnotizen, Event-Felder und Sync-Härtung
- 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>
2026-09-25 07:00:35 +00:00

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);
});