From afd75faa1193b9e0d27b0f121869984c9ec33773 Mon Sep 17 00:00:00 2001 From: Ryan Enns Date: Fri, 12 Dec 2025 10:17:14 -0500 Subject: [PATCH 1/5] many-to-many playlist <> playlist transfers --- app/Models/PlaylistTransfer.php | 11 ++++++++ ...eate_playlist_transfer_playlists_table.php | 23 +++++++++++++++++ tests/Feature/Models/PlaylistTransferTest.php | 25 +++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 database/migrations/2025_12_12_151235_create_playlist_transfer_playlists_table.php create mode 100644 tests/Feature/Models/PlaylistTransferTest.php diff --git a/app/Models/PlaylistTransfer.php b/app/Models/PlaylistTransfer.php index 547a632..5fff924 100644 --- a/app/Models/PlaylistTransfer.php +++ b/app/Models/PlaylistTransfer.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; class PlaylistTransfer extends Model { @@ -48,4 +49,14 @@ public function destinationApi(): StreamingService ->firstOrFail() ); } + + public function playlists(): BelongsToMany + { + return $this->belongsToMany( + Playlist::class, + 'playlist_transfer_playlists', + 'playlist_id', + 'playlist_transfer_id' + ); + } } diff --git a/database/migrations/2025_12_12_151235_create_playlist_transfer_playlists_table.php b/database/migrations/2025_12_12_151235_create_playlist_transfer_playlists_table.php new file mode 100644 index 0000000..5ba8c38 --- /dev/null +++ b/database/migrations/2025_12_12_151235_create_playlist_transfer_playlists_table.php @@ -0,0 +1,23 @@ +id(); + $table->uuid('playlist_id'); + $table->uuid('playlist_transfer_id'); + $table->unique(['playlist_id', 'playlist_transfer_id']); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('playlist_transfer_playlists'); + } +}; diff --git a/tests/Feature/Models/PlaylistTransferTest.php b/tests/Feature/Models/PlaylistTransferTest.php new file mode 100644 index 0000000..ee2899e --- /dev/null +++ b/tests/Feature/Models/PlaylistTransferTest.php @@ -0,0 +1,25 @@ +create(['user_id' => $this->user()->getKey()]); + + $playlist = Playlist::factory()->create(['user_id' => $this->user()->getKey()]); + $playlistTransfer->playlists()->save($playlist); + + $this->assertCount(1, $playlistTransfer->playlists()->get()); + } +} From 9c792d49dd12ac9c1165f71f00e1e53ccda99b47 Mon Sep 17 00:00:00 2001 From: Ryan Enns Date: Fri, 12 Dec 2025 10:46:46 -0500 Subject: [PATCH 2/5] create playlist models on trigger endpoint --- .../TriggerPlaylistTransferController.php | 11 +++ .../TriggerPlaylistTransferControllerTest.php | 74 +++++++++++++++---- 2 files changed, 72 insertions(+), 13 deletions(-) diff --git a/app/Http/Controllers/TriggerPlaylistTransferController.php b/app/Http/Controllers/TriggerPlaylistTransferController.php index f2fdb03..dd93bf7 100644 --- a/app/Http/Controllers/TriggerPlaylistTransferController.php +++ b/app/Http/Controllers/TriggerPlaylistTransferController.php @@ -31,6 +31,17 @@ public function __invoke(Request $request): JsonResponse 'status' => PlaylistTransfer::STATUS_PENDING, ]); + collect($playlists) + ->each(function ($playlist) use ($playlistTransfer, $source, $user) { + $playlist = $user->playlists()->create([ + 'service' => $source, + 'name' => $playlist['name'], + 'remote_id' => $playlist['id'], + ]); + + $playlistTransfer->playlists()->save($playlist); + }); + PlaylistTransferJob::dispatch($playlistTransfer); return response()->json([ diff --git a/tests/Feature/Controllers/TriggerPlaylistTransferControllerTest.php b/tests/Feature/Controllers/TriggerPlaylistTransferControllerTest.php index 74e0a30..1b68621 100644 --- a/tests/Feature/Controllers/TriggerPlaylistTransferControllerTest.php +++ b/tests/Feature/Controllers/TriggerPlaylistTransferControllerTest.php @@ -3,10 +3,12 @@ namespace Tests\Feature\Controllers; use App\Jobs\PlaylistTransferJob; -use App\Models\OauthCredential; +use App\Models\Playlist; +use App\Models\PlaylistTransfer; use App\Services\SpotifyService; use App\Services\TidalService; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Foundation\Testing\WithFaker; use Illuminate\Support\Facades\Bus; use PHPUnit\Framework\Attributes\DataProvider; use Tests\TestCase; @@ -14,26 +16,28 @@ class TriggerPlaylistTransferControllerTest extends TestCase { use RefreshDatabase; + use WithFaker; public function test_it_dispatches_playlist_transfer_job() { Bus::fake(); - OauthCredential::query()->create([ - 'user_id' => $this->user()->getKey(), - 'provider' => TidalService::PROVIDER, - 'provider_id' => TidalService::PROVIDER, - ]); - OauthCredential::query()->create([ - 'user_id' => $this->user()->getKey(), - 'provider' => SpotifyService::PROVIDER, - 'provider_id' => SpotifyService::PROVIDER, - ]); - $this->actingAs($this->user())->post('api/playlist-transfers/trigger', [ 'source' => SpotifyService::PROVIDER, 'destination' => TidalService::PROVIDER, - 'playlists' => ['playlist1', 'playlist2'], + 'playlists' => [ + [ + 'id' => $this->faker->uuid(), + 'name' => $this->faker->word(), + 'tracks' => 'asdf', + 'owner' => [ + 'id' => $this->user()->getKey(), + 'name' => 'ronald mcdonanld', + ], + 'number_of_tracks' => 5, + 'image_uri' => $this->faker->url(), + ] + ], ])->assertCreated()->assertJsonStructure([ 'message', 'data' => [ @@ -55,6 +59,50 @@ public function test_it_rejects_with_incomplete_data($payload) ->assertUnprocessable(); } + public function test_it_creates_playlists_and_associates_them_with_playlist_transfer_model() + { + Bus::fake(); + + $this->assertDatabaseCount('playlists', 0); + $this->assertDatabaseCount('playlist_transfers', 0); + + $this->actingAs($this->user()) + ->post('api/playlist-transfers/trigger', [ + 'source' => SpotifyService::PROVIDER, + 'destination' => TidalService::PROVIDER, + 'playlists' => [ + [ + 'id' => $this->faker->uuid(), + 'name' => $this->faker->word(), + 'tracks' => 'asdf', + 'owner' => [ + 'id' => $this->user()->getKey(), + 'name' => 'ronald mcdonanld', + ], + 'number_of_tracks' => 5, + 'image_uri' => $this->faker->url(), + ] + ], + ])->assertCreated()->assertJsonStructure([ + 'message', + 'data' => [ + 'id', + 'source', + 'destination', + 'playlists', + ], + ]); + + $this->assertDatabaseCount('playlists', 1); + $this->assertDatabaseCount('playlist_transfers', 1); + + $pt = PlaylistTransfer::query()->first(); + $this->assertEquals( + $pt->playlists()->first()->getKey(), + Playlist::query()->first()->getKey() + ); + } + public static function provideIncompletePayloads(): array { return [ From d95090adc363a7f1a91737dffe24e6d813de8da7 Mon Sep 17 00:00:00 2001 From: Ryan Enns Date: Fri, 12 Dec 2025 11:14:28 -0500 Subject: [PATCH 3/5] untangle playlist array usage --- app/Helpers/StreamingServicePlaylistDto.php | 25 ++++++ .../TriggerPlaylistTransferController.php | 1 - app/Jobs/CreateAndSearchForTracksJob.php | 9 +- .../CreatePlaylistAndDispatchTracksJob.php | 14 +-- app/Jobs/PlaylistTransferJob.php | 7 +- app/Models/PlaylistTransfer.php | 2 +- app/Observers/PlaylistTransferObserver.php | 2 +- .../factories/PlaylistTransferFactory.php | 1 - ...175333_create_playlist_transfers_table.php | 1 - .../TriggerPlaylistTransferControllerTest.php | 65 +++++++++++++- tests/Feature/Models/PlaylistTransferTest.php | 5 +- tests/TestCase.php | 9 ++ ...CreatePlaylistAndDispatchTracksJobTest.php | 5 +- tests/Unit/Jobs/PlaylistTransferJobTest.php | 88 ++++--------------- 14 files changed, 136 insertions(+), 98 deletions(-) create mode 100644 app/Helpers/StreamingServicePlaylistDto.php diff --git a/app/Helpers/StreamingServicePlaylistDto.php b/app/Helpers/StreamingServicePlaylistDto.php new file mode 100644 index 0000000..3213bbe --- /dev/null +++ b/app/Helpers/StreamingServicePlaylistDto.php @@ -0,0 +1,25 @@ +id = Arr::get($params, 'id'); + $this->name = Arr::get($params, 'name'); + $this->tracks = Arr::get($params, 'tracks'); + $this->owner = Arr::get($params, 'owner'); + $this->number_of_tracks = Arr::get($params, 'number_of_tracks'); + $this->image_uri = Arr::get($params, 'image_uri'); + } +} diff --git a/app/Http/Controllers/TriggerPlaylistTransferController.php b/app/Http/Controllers/TriggerPlaylistTransferController.php index dd93bf7..109740a 100644 --- a/app/Http/Controllers/TriggerPlaylistTransferController.php +++ b/app/Http/Controllers/TriggerPlaylistTransferController.php @@ -27,7 +27,6 @@ public function __invoke(Request $request): JsonResponse $playlistTransfer = $user->playlistTransfers()->create([ 'source' => $source, 'destination' => $destination, - 'playlists' => $playlists, 'status' => PlaylistTransfer::STATUS_PENDING, ]); diff --git a/app/Jobs/CreateAndSearchForTracksJob.php b/app/Jobs/CreateAndSearchForTracksJob.php index 0111657..d1f63b4 100644 --- a/app/Jobs/CreateAndSearchForTracksJob.php +++ b/app/Jobs/CreateAndSearchForTracksJob.php @@ -99,10 +99,13 @@ public function updateOrCreateTrack(TrackDto $track, array $remoteIds, array|nul $trackModel->remote_ids, $remoteIds, ); - $trackModel->isrc_ids = array_merge( - $trackModel->isrc_ids, - $isrc + $trackModel->isrc_ids = array_unique( + array_merge( + $trackModel->isrc_ids, + $isrc + ) ); + $trackModel->save(); return $trackModel; diff --git a/app/Jobs/CreatePlaylistAndDispatchTracksJob.php b/app/Jobs/CreatePlaylistAndDispatchTracksJob.php index f1f67b4..7bd6504 100644 --- a/app/Jobs/CreatePlaylistAndDispatchTracksJob.php +++ b/app/Jobs/CreatePlaylistAndDispatchTracksJob.php @@ -20,7 +20,7 @@ class CreatePlaylistAndDispatchTracksJob implements ShouldQueue public function __construct( private readonly PlaylistTransfer $playlistTransfer, - private readonly array $playlist + private readonly Playlist $playlist ) { } @@ -31,14 +31,6 @@ public function handle(): void $source = $this->playlistTransfer->sourceApi(); $destination = $this->playlistTransfer->destinationApi(); - $playlistModel = Playlist::query()->firstOrCreate([ - 'service' => $source::PROVIDER, - 'remote_id' => $this->playlist['id'], - ], [ - 'user_id' => $this->playlistTransfer->user_id, - 'name' => $this->playlist['name'], - ]); - $tracks = $source->getPlaylistTracks($this->playlist['id']); $destinationPlaylistId = $destination->createPlaylist($this->playlist['name']); @@ -58,14 +50,14 @@ public function handle(): void ...collect($tracks) ->map(fn($t) => new CreateAndSearchForTracksJob( $this->playlistTransfer, - $playlistModel, + $this->playlist, $t, )) ->toArray(), new PopulatePlaylistWithTracksJob( $this->playlistTransfer, $destinationPlaylistId, - $playlistModel + $this->playlist ), new IncrementPlaylistsProcessedJob($this->playlistTransfer), ] diff --git a/app/Jobs/PlaylistTransferJob.php b/app/Jobs/PlaylistTransferJob.php index 238e02e..6fc032f 100644 --- a/app/Jobs/PlaylistTransferJob.php +++ b/app/Jobs/PlaylistTransferJob.php @@ -2,6 +2,7 @@ namespace App\Jobs; +use App\Models\Playlist; use App\Models\PlaylistTransfer; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; @@ -30,8 +31,10 @@ public function handle(): void Bus::chain( collect($this->playlistTransfer->playlists) - ->map(fn($pt) => new CreatePlaylistAndDispatchTracksJob($this->playlistTransfer, $pt)) - ->toArray(), + ->map(fn(Playlist $p) => new CreatePlaylistAndDispatchTracksJob( + $this->playlistTransfer, + $p + ))->toArray(), )->catch(function (Throwable $throwable) { Log::error( "A failure occurred with a playlist transfer ", diff --git a/app/Models/PlaylistTransfer.php b/app/Models/PlaylistTransfer.php index 5fff924..25b2011 100644 --- a/app/Models/PlaylistTransfer.php +++ b/app/Models/PlaylistTransfer.php @@ -16,7 +16,7 @@ class PlaylistTransfer extends Model protected $guarded = []; - protected $casts = ['playlists' => 'json']; + protected $with = ['playlists']; public const string STATUS_PENDING = 'pending'; public const string STATUS_IN_PROGRESS = 'in_progress'; diff --git a/app/Observers/PlaylistTransferObserver.php b/app/Observers/PlaylistTransferObserver.php index 5acddb1..15943d4 100644 --- a/app/Observers/PlaylistTransferObserver.php +++ b/app/Observers/PlaylistTransferObserver.php @@ -15,7 +15,7 @@ public function updated(PlaylistTransfer $playlistTransfer): void ) { if ( - $playlistTransfer->playlists_processed === count($playlistTransfer->playlists) + $playlistTransfer->playlists_processed === $playlistTransfer->playlists()->count() ) { // oh god no $playlistTransfer->status = PlaylistTransfer::STATUS_COMPLETED; diff --git a/database/factories/PlaylistTransferFactory.php b/database/factories/PlaylistTransferFactory.php index 9d06681..40bc971 100644 --- a/database/factories/PlaylistTransferFactory.php +++ b/database/factories/PlaylistTransferFactory.php @@ -11,7 +11,6 @@ public function definition(): array return [ 'source' => $this->faker->word, 'destination' => $this->faker->word, - 'playlists' => [['id' => $this->faker->uuid, 'name' => $this->faker->sentence]], 'status' => $this->faker->randomElement(['pending', 'completed', 'failed']), 'playlists_processed' => 0 ]; diff --git a/database/migrations/2025_06_20_175333_create_playlist_transfers_table.php b/database/migrations/2025_06_20_175333_create_playlist_transfers_table.php index 438a4ed..435b7b5 100644 --- a/database/migrations/2025_06_20_175333_create_playlist_transfers_table.php +++ b/database/migrations/2025_06_20_175333_create_playlist_transfers_table.php @@ -11,7 +11,6 @@ public function up(): void $table->uuid('id')->primary(); $table->string('source'); $table->string('destination'); - $table->json('playlists'); $table->integer('playlists_processed')->default(0); $table->enum('status', ['pending', 'in_progress', 'completed', 'failed']) ->default('pending'); diff --git a/tests/Feature/Controllers/TriggerPlaylistTransferControllerTest.php b/tests/Feature/Controllers/TriggerPlaylistTransferControllerTest.php index 1b68621..ef555c4 100644 --- a/tests/Feature/Controllers/TriggerPlaylistTransferControllerTest.php +++ b/tests/Feature/Controllers/TriggerPlaylistTransferControllerTest.php @@ -44,7 +44,6 @@ public function test_it_dispatches_playlist_transfer_job() 'id', 'source', 'destination', - 'playlists', ], ]); @@ -89,7 +88,6 @@ public function test_it_creates_playlists_and_associates_them_with_playlist_tran 'id', 'source', 'destination', - 'playlists', ], ]); @@ -103,6 +101,69 @@ public function test_it_creates_playlists_and_associates_them_with_playlist_tran ); } + public function test_it_updates_existing_playlist_if_already_exists() + { + $this->assertDatabaseCount('playlists', 0); + $this->assertDatabaseCount('playlist_transfers', 0); + + $this->actingAs($this->user()) + ->post('api/playlist-transfers/trigger', [ + 'source' => SpotifyService::PROVIDER, + 'destination' => TidalService::PROVIDER, + 'playlists' => [ + [ + 'id' => $this->faker->uuid(), + 'name' => $this->faker->word(), + 'tracks' => 'asdf', + 'owner' => [ + 'id' => $this->user()->getKey(), + 'name' => 'ronald mcdonanld', + ], + 'number_of_tracks' => 5, + 'image_uri' => $this->faker->url(), + ] + ], + ])->assertCreated()->assertJsonStructure([ + 'message', + 'data' => [ + 'id', + 'source', + 'destination', + ], + ]); + + $this->assertDatabaseCount('playlists', 1); + $this->assertDatabaseCount('playlist_transfers', 1); + + $this->actingAs($this->user()) + ->post('api/playlist-transfers/trigger', [ + 'source' => SpotifyService::PROVIDER, + 'destination' => TidalService::PROVIDER, + 'playlists' => [ + [ + 'id' => $this->faker->uuid(), + 'name' => $this->faker->word(), + 'tracks' => 'asdf', + 'owner' => [ + 'id' => $this->user()->getKey(), + 'name' => 'ronald mcdonanld', + ], + 'number_of_tracks' => 5, + 'image_uri' => $this->faker->url(), + ] + ], + ])->assertCreated()->assertJsonStructure([ + 'message', + 'data' => [ + 'id', + 'source', + 'destination', + ], + ]); + + $this->assertDatabaseCount('playlists', 1); + } + public static function provideIncompletePayloads(): array { return [ diff --git a/tests/Feature/Models/PlaylistTransferTest.php b/tests/Feature/Models/PlaylistTransferTest.php index ee2899e..7bcd7b9 100644 --- a/tests/Feature/Models/PlaylistTransferTest.php +++ b/tests/Feature/Models/PlaylistTransferTest.php @@ -4,8 +4,8 @@ use App\Models\Playlist; use App\Models\PlaylistTransfer; -use App\Models\Track; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\DB; use Tests\TestCase; class PlaylistTransferTest extends TestCase @@ -20,6 +20,7 @@ public function test_it_has_many_playlists() $playlist = Playlist::factory()->create(['user_id' => $this->user()->getKey()]); $playlistTransfer->playlists()->save($playlist); - $this->assertCount(1, $playlistTransfer->playlists()->get()); + $playlistTransfer = PlaylistTransfer::query()->first(); + $this->assertCount(1, $playlistTransfer->playlists); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index f50cc22..b4a61e5 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,6 +3,7 @@ namespace Tests; use App\Models\OauthCredential; +use App\Models\Playlist; use App\Models\User; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; @@ -29,4 +30,12 @@ public function user(): User return $this->user; } + + public function newPlaylist(array $props = []): Playlist + { + return Playlist::factory()->create([ + 'user_id' => $this->user()->getKey(), + ...$props, + ]); + } } diff --git a/tests/Unit/Jobs/CreatePlaylistAndDispatchTracksJobTest.php b/tests/Unit/Jobs/CreatePlaylistAndDispatchTracksJobTest.php index f687cc9..2ed4ad3 100644 --- a/tests/Unit/Jobs/CreatePlaylistAndDispatchTracksJobTest.php +++ b/tests/Unit/Jobs/CreatePlaylistAndDispatchTracksJobTest.php @@ -4,6 +4,7 @@ use App\Helpers\TrackDto; use App\Jobs\CreatePlaylistAndDispatchTracksJob; +use App\Models\Playlist; use App\Models\PlaylistTransfer; use App\Services\SpotifyService; use App\Services\TidalService; @@ -63,7 +64,7 @@ public function test_it_marks_playlist_as_processed_on_search_track_failure() new CreatePlaylistAndDispatchTracksJob( $playlistTransfer, - ['name' => 'snickers', 'id' => '123'] + Playlist::factory()->create(['user_id' => $this->user()->getKey()]), )->handle(); $playlistTransfer->refresh(); @@ -114,7 +115,7 @@ public function test_it_marks_playlist_as_processed_on_populate_playlist_failure new CreatePlaylistAndDispatchTracksJob( $playlistTransfer, - ['name' => 'snickers', 'id' => '123'] + Playlist::factory()->create(['user_id' => $this->user()->getKey()]), )->handle(); $playlistTransfer->refresh(); diff --git a/tests/Unit/Jobs/PlaylistTransferJobTest.php b/tests/Unit/Jobs/PlaylistTransferJobTest.php index ba7ba43..2d671f6 100644 --- a/tests/Unit/Jobs/PlaylistTransferJobTest.php +++ b/tests/Unit/Jobs/PlaylistTransferJobTest.php @@ -88,6 +88,7 @@ public function test_it_updates_status_to_complete_on_completion() 'destination' => TidalService::PROVIDER, 'user_id' => $this->user()->getKey(), ]); + $pt->playlists()->save($this->newPlaylist()); (new PlaylistTransferJob($pt))->handle(); $pt->refresh(); $this->assertEquals(PlaylistTransfer::STATUS_COMPLETED, $pt->status); @@ -101,10 +102,10 @@ public function test_it_updates_processed_playlists() 'source' => SpotifyService::PROVIDER, 'destination' => TidalService::PROVIDER, 'user_id' => $this->user()->getKey(), - 'playlists' => [ - ['id' => 'asdf', 'name' => 'snickers'] - ] ]); + $job->playlists()->save( + $this->newPlaylist() + ); (new PlaylistTransferJob($job))->handle(); $job->refresh(); $this->assertEquals(1, $job->playlists_processed); @@ -118,10 +119,10 @@ public function test_it_creates_track_models() 'source' => SpotifyService::PROVIDER, 'destination' => TidalService::PROVIDER, 'user_id' => $this->user()->getKey(), - 'playlists' => [ - ['id' => 'asdf', 'name' => 'snickers'] - ] ]); + + $job->playlists()->save($this->newPlaylist()); + (new PlaylistTransferJob($job))->handle(); $this->assertDatabaseHas('tracks', [ 'isrc_ids' => json_encode(['USUM72005901']), @@ -156,10 +157,8 @@ public function test_it_creates_track_with_one_remote_id_if_no_final_candidate() 'source' => SpotifyService::PROVIDER, 'destination' => TidalService::PROVIDER, 'user_id' => $this->user()->getKey(), - 'playlists' => [ - ['id' => 'asdf', 'name' => 'snickers'] - ] ]); + $pt->playlists()->save($this->newPlaylist()); (new PlaylistTransferJob($pt))->handle(); $this->assertDatabaseHas('tracks', [ @@ -204,15 +203,13 @@ public function test_it_creates_track_with_two_remote_ids_if_final_canddidate_fo $this->destinationMock->shouldReceive('addTracksToPlaylist'); - $job = PlaylistTransfer::factory()->create([ + $pt = PlaylistTransfer::factory()->create([ 'source' => SpotifyService::PROVIDER, 'destination' => TidalService::PROVIDER, 'user_id' => $this->user()->getKey(), - 'playlists' => [ - ['id' => 'asdf', 'name' => 'snickers'] - ] ]); - (new PlaylistTransferJob($job))->handle(); + $pt->playlists()->save($this->newPlaylist()); + (new PlaylistTransferJob($pt))->handle(); $this->assertDatabaseHas('tracks', [ 'isrc_ids' => json_encode(['USUM72005901']), @@ -223,28 +220,6 @@ public function test_it_creates_track_with_two_remote_ids_if_final_canddidate_fo ]); } - public function test_it_creates_playlist_models() - { - $this->happyPathApiMocks(); - - $job = PlaylistTransfer::factory()->create([ - 'source' => SpotifyService::PROVIDER, - 'destination' => TidalService::PROVIDER, - 'user_id' => $this->user()->getKey(), - 'playlists' => [ - ['id' => 1, 'name' => 'snickers1'], - ], - ]); - (new PlaylistTransferJob($job))->handle(); - - $this->assertDatabaseHas('playlists', [ - 'name' => 'snickers1', - 'service' => SpotifyService::PROVIDER, - 'remote_id' => "1", - 'user_id' => $this->user()->getKey(), - ]); - } - public function test_it_associates_tracks_with_playlist() { $trackOne = new TrackDto([ @@ -273,52 +248,23 @@ public function test_it_associates_tracks_with_playlist() $this->destinationMock->shouldReceive('addTracksToPlaylist') ->once(); - $job = PlaylistTransfer::factory()->create([ + $pt = PlaylistTransfer::factory()->create([ 'source' => SpotifyService::PROVIDER, 'destination' => TidalService::PROVIDER, 'user_id' => $this->user()->getKey(), - 'playlists' => [ - ['id' => 1, 'name' => 'snickers1'], - ], ]); - (new PlaylistTransferJob($job))->handle(); + $pt->playlists()->save($this->newPlaylist([ + 'name' => 'snickers', + ])); + (new PlaylistTransferJob($pt))->handle(); $playlist = Playlist::query() - ->where(['name' => 'snickers1']) + ->where(['name' => 'snickers']) ->firstOrFail(); $this->assertNotEmpty($playlist->tracks()->get()); } - public function test_it_creates_one_playlist_if_transferred_twice() - { - $this->happyPathApiMocks(); - $job = PlaylistTransfer::factory()->create([ - 'source' => SpotifyService::PROVIDER, - 'destination' => TidalService::PROVIDER, - 'user_id' => $this->user()->getKey(), - 'playlists' => [ - ['id' => 1, 'name' => 'snickers1'], - ], - ]); - (new PlaylistTransferJob($job))->handle(); - - $this->assertDatabaseCount('playlists', 1); - - $this->happyPathApiMocks(); - $job = PlaylistTransfer::factory()->create([ - 'source' => SpotifyService::PROVIDER, - 'destination' => TidalService::PROVIDER, - 'user_id' => $this->user()->getKey(), - 'playlists' => [ - ['id' => 1, 'name' => 'snickers1'], - ], - ]); - (new PlaylistTransferJob($job))->handle(); - - $this->assertDatabaseCount('playlists', 1); - } - public function happyPathApiMocks(): void { $this->sourceMock->shouldReceive('getPlaylistTracks') From b0c1348e85b4fe1469cc02c16a481828e782eabf Mon Sep 17 00:00:00 2001 From: Ryan Enns Date: Fri, 12 Dec 2025 11:49:14 -0500 Subject: [PATCH 4/5] untangle playlist array usage II --- .../TriggerPlaylistTransferController.php | 7 ++-- .../TriggerPlaylistTransferControllerTest.php | 37 ++++++++----------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/app/Http/Controllers/TriggerPlaylistTransferController.php b/app/Http/Controllers/TriggerPlaylistTransferController.php index 109740a..9672fe9 100644 --- a/app/Http/Controllers/TriggerPlaylistTransferController.php +++ b/app/Http/Controllers/TriggerPlaylistTransferController.php @@ -32,10 +32,11 @@ public function __invoke(Request $request): JsonResponse collect($playlists) ->each(function ($playlist) use ($playlistTransfer, $source, $user) { - $playlist = $user->playlists()->create([ - 'service' => $source, - 'name' => $playlist['name'], + $playlist = $user->playlists()->updateOrCreate([ 'remote_id' => $playlist['id'], + 'service' => $source, + ], [ + 'name' => $playlist['name'], ]); $playlistTransfer->playlists()->save($playlist); diff --git a/tests/Feature/Controllers/TriggerPlaylistTransferControllerTest.php b/tests/Feature/Controllers/TriggerPlaylistTransferControllerTest.php index ef555c4..3f1dbf9 100644 --- a/tests/Feature/Controllers/TriggerPlaylistTransferControllerTest.php +++ b/tests/Feature/Controllers/TriggerPlaylistTransferControllerTest.php @@ -103,25 +103,28 @@ public function test_it_creates_playlists_and_associates_them_with_playlist_tran public function test_it_updates_existing_playlist_if_already_exists() { + Bus::fake(); + $this->assertDatabaseCount('playlists', 0); $this->assertDatabaseCount('playlist_transfers', 0); + $playlist = [ + 'id' => $this->faker->uuid(), + 'name' => $this->faker->word(), + 'tracks' => 'asdf', + 'owner' => [ + 'id' => $this->user()->getKey(), + 'name' => 'ronald mcdonanld', + ], + 'number_of_tracks' => 5, + 'image_uri' => $this->faker->url(), + ]; $this->actingAs($this->user()) ->post('api/playlist-transfers/trigger', [ 'source' => SpotifyService::PROVIDER, 'destination' => TidalService::PROVIDER, 'playlists' => [ - [ - 'id' => $this->faker->uuid(), - 'name' => $this->faker->word(), - 'tracks' => 'asdf', - 'owner' => [ - 'id' => $this->user()->getKey(), - 'name' => 'ronald mcdonanld', - ], - 'number_of_tracks' => 5, - 'image_uri' => $this->faker->url(), - ] + $playlist ], ])->assertCreated()->assertJsonStructure([ 'message', @@ -140,17 +143,7 @@ public function test_it_updates_existing_playlist_if_already_exists() 'source' => SpotifyService::PROVIDER, 'destination' => TidalService::PROVIDER, 'playlists' => [ - [ - 'id' => $this->faker->uuid(), - 'name' => $this->faker->word(), - 'tracks' => 'asdf', - 'owner' => [ - 'id' => $this->user()->getKey(), - 'name' => 'ronald mcdonanld', - ], - 'number_of_tracks' => 5, - 'image_uri' => $this->faker->url(), - ] + $playlist ], ])->assertCreated()->assertJsonStructure([ 'message', From df1b0b1a7fe071f7cc2836e1dca8719f3c6f7759 Mon Sep 17 00:00:00 2001 From: Ryan Enns Date: Fri, 12 Dec 2025 12:40:19 -0500 Subject: [PATCH 5/5] cleanup --- ...eate_playlist_transfer_playlists_table.php | 1 - .../TriggerPlaylistTransferControllerTest.php | 60 +++++++------------ ...CreatePlaylistAndDispatchTracksJobTest.php | 5 +- 3 files changed, 23 insertions(+), 43 deletions(-) diff --git a/database/migrations/2025_12_12_151235_create_playlist_transfer_playlists_table.php b/database/migrations/2025_12_12_151235_create_playlist_transfer_playlists_table.php index 5ba8c38..94f5385 100644 --- a/database/migrations/2025_12_12_151235_create_playlist_transfer_playlists_table.php +++ b/database/migrations/2025_12_12_151235_create_playlist_transfer_playlists_table.php @@ -12,7 +12,6 @@ public function up(): void $table->uuid('playlist_id'); $table->uuid('playlist_transfer_id'); $table->unique(['playlist_id', 'playlist_transfer_id']); - $table->timestamps(); }); } diff --git a/tests/Feature/Controllers/TriggerPlaylistTransferControllerTest.php b/tests/Feature/Controllers/TriggerPlaylistTransferControllerTest.php index 3f1dbf9..3c62b98 100644 --- a/tests/Feature/Controllers/TriggerPlaylistTransferControllerTest.php +++ b/tests/Feature/Controllers/TriggerPlaylistTransferControllerTest.php @@ -18,6 +18,23 @@ class TriggerPlaylistTransferControllerTest extends TestCase use RefreshDatabase; use WithFaker; + protected function setUp(): void + { + parent::setUp(); + + $this->playlistPayload = [ + 'id' => $this->faker->uuid(), + 'name' => $this->faker->word(), + 'tracks' => 'asdf', + 'owner' => [ + 'id' => $this->user()->getKey(), + 'name' => 'ronald mcdonanld', + ], + 'number_of_tracks' => 5, + 'image_uri' => $this->faker->url(), + ]; + } + public function test_it_dispatches_playlist_transfer_job() { Bus::fake(); @@ -25,19 +42,7 @@ public function test_it_dispatches_playlist_transfer_job() $this->actingAs($this->user())->post('api/playlist-transfers/trigger', [ 'source' => SpotifyService::PROVIDER, 'destination' => TidalService::PROVIDER, - 'playlists' => [ - [ - 'id' => $this->faker->uuid(), - 'name' => $this->faker->word(), - 'tracks' => 'asdf', - 'owner' => [ - 'id' => $this->user()->getKey(), - 'name' => 'ronald mcdonanld', - ], - 'number_of_tracks' => 5, - 'image_uri' => $this->faker->url(), - ] - ], + 'playlists' => [$this->playlistPayload], ])->assertCreated()->assertJsonStructure([ 'message', 'data' => [ @@ -69,19 +74,7 @@ public function test_it_creates_playlists_and_associates_them_with_playlist_tran ->post('api/playlist-transfers/trigger', [ 'source' => SpotifyService::PROVIDER, 'destination' => TidalService::PROVIDER, - 'playlists' => [ - [ - 'id' => $this->faker->uuid(), - 'name' => $this->faker->word(), - 'tracks' => 'asdf', - 'owner' => [ - 'id' => $this->user()->getKey(), - 'name' => 'ronald mcdonanld', - ], - 'number_of_tracks' => 5, - 'image_uri' => $this->faker->url(), - ] - ], + 'playlists' => [$this->playlistPayload], ])->assertCreated()->assertJsonStructure([ 'message', 'data' => [ @@ -108,23 +101,12 @@ public function test_it_updates_existing_playlist_if_already_exists() $this->assertDatabaseCount('playlists', 0); $this->assertDatabaseCount('playlist_transfers', 0); - $playlist = [ - 'id' => $this->faker->uuid(), - 'name' => $this->faker->word(), - 'tracks' => 'asdf', - 'owner' => [ - 'id' => $this->user()->getKey(), - 'name' => 'ronald mcdonanld', - ], - 'number_of_tracks' => 5, - 'image_uri' => $this->faker->url(), - ]; $this->actingAs($this->user()) ->post('api/playlist-transfers/trigger', [ 'source' => SpotifyService::PROVIDER, 'destination' => TidalService::PROVIDER, 'playlists' => [ - $playlist + $this->playlistPayload ], ])->assertCreated()->assertJsonStructure([ 'message', @@ -143,7 +125,7 @@ public function test_it_updates_existing_playlist_if_already_exists() 'source' => SpotifyService::PROVIDER, 'destination' => TidalService::PROVIDER, 'playlists' => [ - $playlist + $this->playlistPayload ], ])->assertCreated()->assertJsonStructure([ 'message', diff --git a/tests/Unit/Jobs/CreatePlaylistAndDispatchTracksJobTest.php b/tests/Unit/Jobs/CreatePlaylistAndDispatchTracksJobTest.php index 2ed4ad3..1c2b4b4 100644 --- a/tests/Unit/Jobs/CreatePlaylistAndDispatchTracksJobTest.php +++ b/tests/Unit/Jobs/CreatePlaylistAndDispatchTracksJobTest.php @@ -4,7 +4,6 @@ use App\Helpers\TrackDto; use App\Jobs\CreatePlaylistAndDispatchTracksJob; -use App\Models\Playlist; use App\Models\PlaylistTransfer; use App\Services\SpotifyService; use App\Services\TidalService; @@ -64,7 +63,7 @@ public function test_it_marks_playlist_as_processed_on_search_track_failure() new CreatePlaylistAndDispatchTracksJob( $playlistTransfer, - Playlist::factory()->create(['user_id' => $this->user()->getKey()]), + $this->newPlaylist(), )->handle(); $playlistTransfer->refresh(); @@ -115,7 +114,7 @@ public function test_it_marks_playlist_as_processed_on_populate_playlist_failure new CreatePlaylistAndDispatchTracksJob( $playlistTransfer, - Playlist::factory()->create(['user_id' => $this->user()->getKey()]), + $this->newPlaylist(), )->handle(); $playlistTransfer->refresh();