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