diff --git a/app/Exceptions/FailedToActivateLicenseException.php b/app/Exceptions/FailedToActivateLicenseException.php index 3cb61684..d9570f96 100644 --- a/app/Exceptions/FailedToActivateLicenseException.php +++ b/app/Exceptions/FailedToActivateLicenseException.php @@ -15,6 +15,10 @@ final class FailedToActivateLicenseException extends Exception public static function fromRequestException(RequestException $e): self { - return new self(object_get($e->getResponse()->object(), 'error'), $e->getStatus()); + try { + return new self(object_get($e->getResponse()->object(), 'error'), $e->getStatus()); + } catch (Throwable) { + return self::fromThrowable($e); + } } } diff --git a/app/Models/User.php b/app/Models/User.php index e39e9d9d..8b53a63f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -13,6 +13,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Illuminate\Support\Arr; use Laravel\Sanctum\HasApiTokens; use Laravel\Sanctum\PersonalAccessToken; @@ -77,11 +78,9 @@ class User extends Authenticatable protected function avatar(): Attribute { return Attribute::get(function (): string { - if ($this->attributes['avatar']) { - return user_avatar_url($this->attributes['avatar']); - } + $avatar = Arr::get($this->attributes, 'avatar'); - return gravatar($this->email); + return $avatar ? user_avatar_url($avatar) : gravatar($this->email); }); } diff --git a/app/Services/YouTubeService.php b/app/Services/YouTubeService.php index ec44f548..54650e2b 100644 --- a/app/Services/YouTubeService.php +++ b/app/Services/YouTubeService.php @@ -18,8 +18,7 @@ class YouTubeService return (bool) config('koel.youtube.key'); } - /** @return array|null */ - public function searchVideosRelatedToSong(Song $song, string $pageToken = ''): ?array + public function searchVideosRelatedToSong(Song $song, string $pageToken = ''): ?object { if (!self::enabled()) { return null; @@ -29,9 +28,9 @@ class YouTubeService $hash = md5(serialize($request->query()->all())); return $this->cache->remember( - "youtube:$hash", + "youtube.$hash", now()->addWeek(), - fn () => $this->connector->send($request)->json() + fn () => $this->connector->send($request)->object() ); } } diff --git a/storage/framework/.gitignore b/storage/framework/.gitignore index 953edb7a..d145119e 100644 --- a/storage/framework/.gitignore +++ b/storage/framework/.gitignore @@ -4,4 +4,5 @@ compiled.php services.json events.scanned.php routes.scanned.php +testing/* down diff --git a/tests/Feature/AlbumCoverTest.php b/tests/Feature/AlbumCoverTest.php index cdefc4c5..5866c05f 100644 --- a/tests/Feature/AlbumCoverTest.php +++ b/tests/Feature/AlbumCoverTest.php @@ -30,9 +30,9 @@ class AlbumCoverTest extends TestCase $this->mediaMetadataService ->shouldReceive('writeAlbumCover') ->once() - ->with(Mockery::on(static fn (Album $target) => $target->is($album)), 'Foo', 'jpeg'); + ->with(Mockery::on(static fn (Album $target) => $target->is($album)), 'data:image/jpeg;base64,Rm9v'); - $this->putAs('api/album/' . $album->id . '/cover', ['cover' => 'data:image/jpeg;base64,Rm9v'], create_admin()) + $this->putAs("api/album/$album->id/cover", ['cover' => 'data:image/jpeg;base64,Rm9v'], create_admin()) ->assertOk(); } diff --git a/tests/Feature/ArtistImageTest.php b/tests/Feature/ArtistImageTest.php index c376fd53..53b9b035 100644 --- a/tests/Feature/ArtistImageTest.php +++ b/tests/Feature/ArtistImageTest.php @@ -23,14 +23,15 @@ class ArtistImageTest extends TestCase public function testUpdate(): void { - Artist::factory()->create(['id' => 9999]); + /** @var Artist $artist */ + $artist = Artist::factory()->create(); $this->mediaMetadataService ->shouldReceive('writeArtistImage') ->once() - ->with(Mockery::on(static fn (Artist $artist) => $artist->id === 9999), 'Foo', 'jpeg'); + ->with(Mockery::on(static fn (Artist $target) => $target->is($artist)), 'data:image/jpeg;base64,Rm9v'); - $this->putAs('api/artist/9999/image', ['image' => 'data:image/jpeg;base64,Rm9v'], create_admin()) + $this->putAs("api/artist/$artist->id/image", ['image' => 'data:image/jpeg;base64,Rm9v'], create_admin()) ->assertOk(); } diff --git a/tests/Feature/KoelPlus/AlbumCoverTest.php b/tests/Feature/KoelPlus/AlbumCoverTest.php index 6cc92666..efca63d7 100644 --- a/tests/Feature/KoelPlus/AlbumCoverTest.php +++ b/tests/Feature/KoelPlus/AlbumCoverTest.php @@ -31,7 +31,7 @@ class AlbumCoverTest extends PlusTestCase $this->mediaMetadataService ->shouldReceive('writeAlbumCover') ->once() - ->with(Mockery::on(static fn (Album $target) => $target->is($album)), 'Foo', 'jpeg'); + ->with(Mockery::on(static fn (Album $target) => $target->is($album)), 'data:image/jpeg;base64,Rm9v'); $this->putAs("api/albums/$album->id/cover", ['cover' => 'data:image/jpeg;base64,Rm9v'], $user) ->assertOk(); @@ -65,7 +65,7 @@ class AlbumCoverTest extends PlusTestCase $this->mediaMetadataService ->shouldReceive('writeAlbumCover') ->once() - ->with(Mockery::on(static fn (Album $target) => $target->is($album)), 'Foo', 'jpeg'); + ->with(Mockery::on(static fn (Album $target) => $target->is($album)), 'data:image/jpeg;base64,Rm9v'); $this->putAs("api/albums/$album->id/cover", ['cover' => 'data:image/jpeg;base64,Rm9v'], create_admin()) ->assertOk(); diff --git a/tests/Feature/KoelPlus/ArtistImageTest.php b/tests/Feature/KoelPlus/ArtistImageTest.php index 52ec599e..88e7db98 100644 --- a/tests/Feature/KoelPlus/ArtistImageTest.php +++ b/tests/Feature/KoelPlus/ArtistImageTest.php @@ -6,6 +6,7 @@ use App\Models\Artist; use App\Models\Song; use App\Services\MediaMetadataService; use Mockery; +use Mockery\MockInterface; use Tests\PlusTestCase; use function Tests\create_admin; @@ -13,6 +14,8 @@ use function Tests\create_user; class ArtistImageTest extends PlusTestCase { + private MockInterface|MediaMetadataService $mediaMetadataService; + public function setUp(): void { parent::setUp(); @@ -31,7 +34,7 @@ class ArtistImageTest extends PlusTestCase $this->mediaMetadataService ->shouldReceive('writeArtistImage') ->once() - ->with(Mockery::on(static fn (Artist $target) => $target->is($artist)), 'Foo', 'jpeg'); + ->with(Mockery::on(static fn (Artist $target) => $target->is($artist)), 'data:image/jpeg;base64,Rm9v'); $this->putAs("api/artists/$artist->id/image", ['image' => 'data:image/jpeg;base64,Rm9v'], $user) ->assertOk(); @@ -65,7 +68,7 @@ class ArtistImageTest extends PlusTestCase $this->mediaMetadataService ->shouldReceive('writeArtistImage') ->once() - ->with(Mockery::on(static fn (Artist $target) => $target->is($artist)), 'Foo', 'jpeg'); + ->with(Mockery::on(static fn (Artist $target) => $target->is($artist)), 'data:image/jpeg;base64,Rm9v'); $this->putAs("api/artists/$artist->id/image", ['image' => 'data:image/jpeg;base64,Rm9v'], create_admin()) ->assertOk(); diff --git a/tests/Feature/KoelPlus/PlaylistCoverTest.php b/tests/Feature/KoelPlus/PlaylistCoverTest.php index 649b54ef..2850ad56 100644 --- a/tests/Feature/KoelPlus/PlaylistCoverTest.php +++ b/tests/Feature/KoelPlus/PlaylistCoverTest.php @@ -31,13 +31,9 @@ class PlaylistCoverTest extends PlusTestCase $this->mediaMetadataService ->shouldReceive('writePlaylistCover') ->once() - ->with(Mockery::on(static fn (Playlist $target) => $target->is($playlist)), 'Foo', 'jpeg'); + ->with(Mockery::on(static fn (Playlist $target) => $target->is($playlist)), 'data:image/jpeg;base64,Rm9v'); - $this->putAs( - "api/playlists/$playlist->id/cover", - ['cover' => 'data:image/jpeg;base64,Rm9v'], - $collaborator - ) + $this->putAs("api/playlists/$playlist->id/cover", ['cover' => 'data:image/jpeg;base64,Rm9v'], $collaborator) ->assertOk(); } } diff --git a/tests/Feature/PlaylistCoverTest.php b/tests/Feature/PlaylistCoverTest.php index f4f4baf6..6e3c7ce4 100644 --- a/tests/Feature/PlaylistCoverTest.php +++ b/tests/Feature/PlaylistCoverTest.php @@ -30,13 +30,9 @@ class PlaylistCoverTest extends TestCase $this->mediaMetadataService ->shouldReceive('writePlaylistCover') ->once() - ->with(Mockery::on(static fn (Playlist $target) => $target->is($playlist)), 'Foo', 'jpeg'); + ->with(Mockery::on(static fn (Playlist $target) => $target->is($playlist)), 'data:image/jpeg;base64,Rm9v'); - $this->putAs( - "api/playlists/$playlist->id/cover", - ['cover' => 'data:image/jpeg;base64,Rm9v'], - $playlist->user - ) + $this->putAs("api/playlists/$playlist->id/cover", ['cover' => 'data:image/jpeg;base64,Rm9v'], $playlist->user) ->assertOk(); } @@ -47,11 +43,7 @@ class PlaylistCoverTest extends TestCase $this->mediaMetadataService->shouldNotReceive('writePlaylistCover'); - $this->putAs( - "api/playlists/$playlist->id/cover", - ['cover' => 'data:image/jpeg;base64,Rm9v'], - create_user() - ) + $this->putAs("api/playlists/$playlist->id/cover", ['cover' => 'data:image/jpeg;base64,Rm9v'], create_user()) ->assertForbidden(); } } diff --git a/tests/Integration/KoelPlus/Services/PlaylistServiceTest.php b/tests/Integration/KoelPlus/Services/PlaylistServiceTest.php index f424b09d..120a4e36 100644 --- a/tests/Integration/KoelPlus/Services/PlaylistServiceTest.php +++ b/tests/Integration/KoelPlus/Services/PlaylistServiceTest.php @@ -53,8 +53,8 @@ class PlaylistServiceTest extends PlusTestCase public function testOwnSongsOnlyOptionOnlyWorksWithSmartPlaylistsWhenCreate(): void { - self::expectException(InvalidArgumentException::class); - self::expectExceptionMessage('"Own songs only" option only works with smart playlists and Plus license.'); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('"Own songs only" option only works with smart playlists and Plus license.'); $this->service->createPlaylist( name: 'foo', @@ -65,8 +65,8 @@ class PlaylistServiceTest extends PlusTestCase public function testOwnSongsOnlyOptionOnlyWorksWithSmartPlaylistsWhenUpdate(): void { - self::expectException(InvalidArgumentException::class); - self::expectExceptionMessage('"Own songs only" option only works with smart playlists and Plus license.'); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('"Own songs only" option only works with smart playlists and Plus license.'); /** @var Playlist */ $playlist = Playlist::factory()->create(); diff --git a/tests/Integration/Services/ITunesServiceTest.php b/tests/Integration/Services/ITunesServiceTest.php new file mode 100644 index 00000000..0ce3f71f --- /dev/null +++ b/tests/Integration/Services/ITunesServiceTest.php @@ -0,0 +1,72 @@ +service = app(ITunesService::class); + } + + public function testConfiguration(): void + { + config(['koel.itunes.enabled' => true]); + self::assertTrue($this->service->used()); + + config(['koel.itunes.enabled' => false]); + self::assertFalse($this->service->used()); + } + + public function testGetTrackUrl(): void + { + config(['koel.itunes.enabled' => true]); + config(['koel.itunes.affiliate_id' => 'foo']); + + Saloon::fake([ + GetTrackRequest::class => MockResponse::make(body: [ + 'resultCount' => 1, + 'results' => [['trackViewUrl' => 'https://itunes.apple.com/bar']], + ]), + ]); + + /** @var Album $album */ + $album = Album::factory() + ->for(Artist::factory()->create(['name' => 'Queen'])) + ->create(['name' => 'A Night at the Opera']); + + self::assertSame( + 'https://itunes.apple.com/bar?at=foo', + $this->service->getTrackUrl('Bohemian Rhapsody', $album) + ); + + self::assertSame( + 'https://itunes.apple.com/bar?at=foo', + Cache::get('itunes.track.5f0467bebbb2b26bf9dc7b19f3d85077') + ); + + Saloon::assertSent(static function (GetTrackRequest $request): bool { + self::assertSame([ + 'term' => 'Bohemian Rhapsody A Night at the Opera Queen', + 'media' => 'music', + 'entity' => 'song', + 'limit' => 1, + ], $request->query()->all()); + + return true; + }); + } +} diff --git a/tests/Unit/Services/LastfmServiceTest.php b/tests/Integration/Services/LastfmServiceTest.php similarity index 56% rename from tests/Unit/Services/LastfmServiceTest.php rename to tests/Integration/Services/LastfmServiceTest.php index 86766cd4..61cbe33e 100644 --- a/tests/Unit/Services/LastfmServiceTest.php +++ b/tests/Integration/Services/LastfmServiceTest.php @@ -1,16 +1,19 @@ 'secret', ]); - $this->client = Mockery::mock(LastfmClient::class); - $this->service = new LastfmService($this->client); + $this->service = app(LastfmService::class); } public function testGetArtistInformation(): void { /** @var Artist $artist */ - $artist = Artist::factory()->make(['name' => 'foo']); + $artist = Artist::factory()->make(['name' => 'Kamelot']); - $this->client->shouldReceive('get') - ->with('?method=artist.getInfo&autocorrect=1&artist=foo&format=json') - ->once() - ->andReturn(json_decode(File::get(test_path('blobs/lastfm/artist.json')))); + Saloon::fake([ + GetArtistInfoRequest::class => MockResponse::make(body: File::get(test_path('blobs/lastfm/artist.json'))), + ]); $info = $this->service->getArtistInformation($artist); + Saloon::assertSent(static function (GetArtistInfoRequest $request): bool { + self::assertSame([ + 'method' => 'artist.getInfo', + 'artist' => 'Kamelot', + 'autocorrect' => 1, + 'format' => 'json', + ], $request->query()->all()); + + return true; + }); + self::assertEquals([ 'url' => 'https://www.last.fm/music/Kamelot', 'image' => null, @@ -61,10 +72,11 @@ class LastfmServiceTest extends TestCase /** @var Artist $artist */ $artist = Artist::factory()->make(['name' => 'bar']); - $this->client->shouldReceive('get') - ->with('?method=artist.getInfo&autocorrect=1&artist=bar&format=json') - ->once() - ->andReturn(json_decode(test_path('blobs/lastfm/artist-notfound.json'))); + Saloon::fake([ + GetArtistInfoRequest::class => MockResponse::make( + body: File::get(test_path('blobs/lastfm/artist-notfound.json')) + ), + ]); self::assertNull($this->service->getArtistInformation($artist)); } @@ -72,15 +84,26 @@ class LastfmServiceTest extends TestCase public function testGetAlbumInformation(): void { /** @var Album $album */ - $album = Album::factory()->for(Artist::factory()->create(['name' => 'bar']))->create(['name' => 'foo']); + $album = Album::factory()->for(Artist::factory()->create(['name' => 'Kamelot']))->create(['name' => 'Epica']); - $this->client->shouldReceive('get') - ->with('?method=album.getInfo&autocorrect=1&album=foo&artist=bar&format=json') - ->once() - ->andReturn(json_decode(File::get(test_path('blobs/lastfm/album.json')))); + Saloon::fake([ + GetAlbumInfoRequest::class => MockResponse::make(body: File::get(test_path('blobs/lastfm/album.json'))), + ]); $info = $this->service->getAlbumInformation($album); + Saloon::assertSent(static function (GetAlbumInfoRequest $request): bool { + self::assertSame([ + 'method' => 'album.getInfo', + 'artist' => 'Kamelot', + 'album' => 'Epica', + 'autocorrect' => 1, + 'format' => 'json', + ], $request->query()->all()); + + return true; + }); + self::assertEquals([ 'url' => 'https://www.last.fm/music/Kamelot/Epica', 'cover' => null, @@ -106,12 +129,13 @@ class LastfmServiceTest extends TestCase public function testGetAlbumInformationForNonExistentAlbum(): void { /** @var Album $album */ - $album = Album::factory()->for(Artist::factory()->create(['name' => 'bar']))->create(['name' => 'foo']); + $album = Album::factory()->for(Artist::factory()->create(['name' => 'Kamelot']))->create(['name' => 'Foo']); - $this->client->shouldReceive('get') - ->with('?method=album.getInfo&autocorrect=1&album=foo&artist=bar&format=json') - ->once() - ->andReturn(json_decode(File::get(test_path('blobs/lastfm/album-notfound.json')))); + Saloon::fake([ + GetAlbumInfoRequest::class => MockResponse::make( + body: File::get(test_path('blobs/lastfm/album-notfound.json')) + ), + ]); self::assertNull($this->service->getAlbumInformation($album)); } @@ -127,18 +151,22 @@ class LastfmServiceTest extends TestCase /** @var Song $song */ $song = Song::factory()->create(); - $this->client->shouldReceive('post') - ->with('/', [ + Saloon::fake([ScrobbleRequest::class => MockResponse::make()]); + + $this->service->scrobble($song, $user, 100); + + Saloon::assertSent(static function (ScrobbleRequest $request) use ($song): bool { + self::assertSame([ + 'method' => 'track.scrobble', 'artist' => $song->artist->name, 'track' => $song->title, 'timestamp' => 100, 'sk' => 'my_key', - 'method' => 'track.scrobble', 'album' => $song->album->name, - ], false) - ->once(); + ], $request->body()->all()); - $this->service->scrobble($song, $user, 100); + return true; + }); } /** @return array */ @@ -157,18 +185,22 @@ class LastfmServiceTest extends TestCase ]); /** @var Song $song */ - $song = Song::factory()->for(Artist::factory()->create(['name' => 'foo']))->create(['title' => 'bar']); + $song = Song::factory()->create(); - $this->client->shouldReceive('post') - ->with('/', [ - 'artist' => 'foo', - 'track' => 'bar', - 'sk' => 'my_key', - 'method' => $method, - ], false) - ->once(); + Saloon::fake([ToggleLoveTrackRequest::class => MockResponse::make()]); $this->service->toggleLoveTrack($song, $user, $love); + + Saloon::assertSent(static function (ToggleLoveTrackRequest $request) use ($song, $love): bool { + self::assertSame([ + 'method' => $love ? 'track.love' : 'track.unlove', + 'sk' => 'my_key', + 'artist' => $song->artist->name, + 'track' => $song->title, + ], $request->body()->all()); + + return true; + }); } public function testUpdateNowPlaying(): void @@ -182,17 +214,21 @@ class LastfmServiceTest extends TestCase /** @var Song $song */ $song = Song::factory()->for(Artist::factory()->create(['name' => 'foo']))->create(['title' => 'bar']); - $this->client->shouldReceive('post') - ->with('/', [ - 'artist' => 'foo', - 'track' => 'bar', - 'duration' => $song->length, - 'sk' => 'my_key', - 'method' => 'track.updateNowPlaying', - 'album' => $song->album->name, - ], false) - ->once(); + Saloon::fake([UpdateNowPlayingRequest::class => MockResponse::make()]); $this->service->updateNowPlaying($song, $user); + + Saloon::assertSent(static function (UpdateNowPlayingRequest $request) use ($song): bool { + self::assertSame([ + 'method' => 'track.updateNowPlaying', + 'artist' => $song->artist->name, + 'track' => $song->title, + 'duration' => $song->length, + 'sk' => 'my_key', + 'album' => $song->album->name, + ], $request->body()->all()); + + return true; + }); } } diff --git a/tests/Integration/Services/LicenseServiceTest.php b/tests/Integration/Services/LicenseServiceTest.php index b8ece25e..21bccee7 100644 --- a/tests/Integration/Services/LicenseServiceTest.php +++ b/tests/Integration/Services/LicenseServiceTest.php @@ -3,19 +3,17 @@ namespace Tests\Integration\Services; use App\Exceptions\FailedToActivateLicenseException; +use App\Http\Integrations\LemonSqueezy\Requests\ActivateLicenseRequest; +use App\Http\Integrations\LemonSqueezy\Requests\DeactivateLicenseRequest; +use App\Http\Integrations\LemonSqueezy\Requests\ValidateLicenseRequest; use App\Models\License; -use App\Services\ApiClients\ApiClient; use App\Services\LicenseService; use App\Values\LicenseStatus; -use Exception; -use GuzzleHttp\Exception\ClientException; use Illuminate\Http\Response; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\File; -use Mockery; -use Mockery\LegacyMockInterface; -use Mockery\MockInterface; -use Psr\Http\Message\ResponseInterface; +use Saloon\Http\Faking\MockResponse; +use Saloon\Laravel\Facades\Saloon; use Tests\TestCase; use function Tests\test_path; @@ -23,13 +21,11 @@ use function Tests\test_path; class LicenseServiceTest extends TestCase { private LicenseService $service; - private ApiClient|MockInterface|LegacyMockInterface $apiClient; public function setUp(): void { parent::setUp(); - $this->apiClient = $this->mock(ApiClient::class); $this->service = app(LicenseService::class); } @@ -38,14 +34,11 @@ class LicenseServiceTest extends TestCase config(['lemonsqueezy.store_id' => 42]); $key = '38b1460a-5104-4067-a91d-77b872934d51'; - $this->apiClient - ->shouldReceive('post') - ->with('licenses/activate', [ - 'license_key' => $key, - 'instance_name' => 'Koel Plus', - ]) - ->once() - ->andReturn(json_decode(File::get(test_path('blobs/lemonsqueezy/license-activated-successful.json')))); + Saloon::fake([ + ActivateLicenseRequest::class => MockResponse::make( + body: File::get(test_path('blobs/lemonsqueezy/license-activated-successful.json')), + ), + ]); $license = $this->service->activate($key); @@ -59,49 +52,46 @@ class LicenseServiceTest extends TestCase self::assertSame($license->key, $cachedLicenseStatus->license->key); self::assertTrue($cachedLicenseStatus->isValid()); + + Saloon::assertSent(static function (ActivateLicenseRequest $request) use ($key): bool { + self::assertSame([ + 'license_key' => $key, + 'instance_name' => 'Koel Plus', + ], $request->body()->all()); + + return true; + }); } public function testActivateLicenseFailsBecauseOfIncorrectStoreId(): void { - self::expectException(FailedToActivateLicenseException::class); - self::expectExceptionMessage('This license key is not from Koel’s official store.'); + $this->expectException(FailedToActivateLicenseException::class); + $this->expectExceptionMessage('This license key is not from Koel’s official store.'); config(['lemonsqueezy.store_id' => 43]); $key = '38b1460a-5104-4067-a91d-77b872934d51'; - $this->apiClient - ->shouldReceive('post') - ->with('licenses/activate', [ - 'license_key' => $key, - 'instance_name' => 'Koel Plus', - ]) - ->once() - ->andReturn(json_decode(File::get(test_path('blobs/lemonsqueezy/license-activated-successful.json')))); + Saloon::fake([ + ActivateLicenseRequest::class => MockResponse::make( + body: File::get(test_path('blobs/lemonsqueezy/license-activated-successful.json')), + ), + ]); $this->service->activate($key); } public function testActivateLicenseFailsForInvalidLicenseKey(): void { - self::expectException(FailedToActivateLicenseException::class); - self::expectExceptionMessage('license_key not found'); + $this->expectException(FailedToActivateLicenseException::class); + $this->expectExceptionMessage('license_key not found'); - $exception = Mockery::mock(ClientException::class, [ - 'getResponse' => Mockery::mock(ResponseInterface::class, [ - 'getBody' => File::get(test_path('blobs/lemonsqueezy/license-invalid.json')), - 'getStatusCode' => Response::HTTP_NOT_FOUND, - ]), + Saloon::fake([ + ActivateLicenseRequest::class => MockResponse::make( + body: File::get(test_path('blobs/lemonsqueezy/license-invalid.json')), + status: Response::HTTP_NOT_FOUND, + ), ]); - $this->apiClient - ->shouldReceive('post') - ->with('licenses/activate', [ - 'license_key' => 'invalid-key', - 'instance_name' => 'Koel Plus', - ]) - ->once() - ->andThrow($exception); - $this->service->activate('invalid-key'); } @@ -110,40 +100,33 @@ class LicenseServiceTest extends TestCase /** @var License $license */ $license = License::factory()->create(); - $this->apiClient - ->shouldReceive('post') - ->with('licenses/deactivate', [ - 'license_key' => $license->key, - 'instance_id' => $license->instance->id, - ]) - ->once() - ->andReturn(json_decode(File::get(test_path('blobs/lemonsqueezy/license-deactivated-successful.json')))); + Saloon::fake([ + DeactivateLicenseRequest::class => MockResponse::make( + body: File::get(test_path('blobs/lemonsqueezy/license-deactivated-successful.json')), + status: Response::HTTP_NOT_FOUND, + ), + ]); $this->service->deactivate($license); self::assertModelMissing($license); self::assertFalse(Cache::has('license_status')); + + Saloon::assertSent(static function (DeactivateLicenseRequest $request) use ($license): bool { + self::assertSame([ + 'license_key' => $license->key, + 'instance_id' => $license->instance->id, + ], $request->body()->all()); + + return true; + }); } public function testDeactivateLicenseHandlesLeftoverRecords(): void { /** @var License $license */ $license = License::factory()->create(); - - $exception = Mockery::mock(ClientException::class, [ - 'getResponse' => Mockery::mock(ResponseInterface::class, [ - 'getStatusCode' => Response::HTTP_NOT_FOUND, - ]), - ]); - - $this->apiClient - ->shouldReceive('post') - ->with('licenses/deactivate', [ - 'license_key' => $license->key, - 'instance_id' => $license->instance->id, - ]) - ->once() - ->andThrow($exception); + Saloon::fake([DeactivateLicenseRequest::class => MockResponse::make(status: Response::HTTP_NOT_FOUND)]); $this->service->deactivate($license); @@ -152,43 +135,43 @@ class LicenseServiceTest extends TestCase public function testDeactivateLicenseFails(): void { - self::expectExceptionMessage('Something went horribly wrong'); + $this->expectExceptionMessage('Unprocessable Entity (422) Response: Something went horrible wrong'); /** @var License $license */ $license = License::factory()->create(); - $this->apiClient - ->shouldReceive('post') - ->with('licenses/deactivate', [ - 'license_key' => $license->key, - 'instance_id' => $license->instance->id, - ]) - ->once() - ->andThrow(new Exception('Something went horribly wrong')); + Saloon::fake([ + DeactivateLicenseRequest::class => MockResponse::make( + body: 'Something went horrible wrong', + status: Response::HTTP_UNPROCESSABLE_ENTITY + ), + ]); $this->service->deactivate($license); } public function testGetLicenseStatusFromCache(): void { + Saloon::fake([]); + /** @var License $license */ $license = License::factory()->create(); Cache::put('license_status', LicenseStatus::valid($license)); - $this->apiClient->shouldNotReceive('post'); - self::assertTrue($this->service->getStatus()->license->is($license)); self::assertTrue($this->service->getStatus()->isValid()); + + Saloon::assertNothingSent(); } public function testGetLicenseStatusWithNoLicenses(): void { + Saloon::fake([]); License::query()->delete(); - $this->apiClient->shouldNotReceive('post'); - self::assertTrue($this->service->getStatus()->hasNoLicense()); + Saloon::assertNothingSent(); } public function testGetLicenseStatusValidatesWithApi(): void @@ -198,40 +181,32 @@ class LicenseServiceTest extends TestCase self::assertFalse(Cache::has('license_status')); - $this->apiClient - ->shouldReceive('post') - ->with('licenses/validate', [ - 'license_key' => $license->key, - 'instance_id' => $license->instance->id, - ]) - ->once() - ->andReturn(json_decode(File::get(test_path('blobs/lemonsqueezy/license-validated-successful.json')))); + Saloon::fake([ + ValidateLicenseRequest::class => MockResponse::make( + body: File::get(test_path('blobs/lemonsqueezy/license-validated-successful.json')), + ), + ]); self::assertTrue($this->service->getStatus()->isValid()); self::assertTrue(Cache::has('license_status')); + + Saloon::assertSent(static function (ValidateLicenseRequest $request) use ($license): bool { + self::assertSame([ + 'license_key' => $license->key, + 'instance_id' => $license->instance->id, + ], $request->body()->all()); + + return true; + }); } public function testGetLicenseStatusValidatesWithApiWithInvalidLicense(): void { - /** @var License $license */ - $license = License::factory()->create(); + License::factory()->create(); self::assertFalse(Cache::has('license_status')); - $exception = Mockery::mock(ClientException::class, [ - 'getResponse' => Mockery::mock(ResponseInterface::class, [ - 'getStatusCode' => Response::HTTP_NOT_FOUND, - ]), - ]); - - $this->apiClient - ->shouldReceive('post') - ->with('licenses/validate', [ - 'license_key' => $license->key, - 'instance_id' => $license->instance->id, - ]) - ->once() - ->andThrow($exception); + Saloon::fake([ValidateLicenseRequest::class => MockResponse::make(status: Response::HTTP_NOT_FOUND)]); self::assertFalse($this->service->getStatus()->isValid()); self::assertTrue(Cache::has('license_status')); diff --git a/tests/Integration/Services/PlaylistServiceTest.php b/tests/Integration/Services/PlaylistServiceTest.php index dbf30b07..b71887ce 100644 --- a/tests/Integration/Services/PlaylistServiceTest.php +++ b/tests/Integration/Services/PlaylistServiceTest.php @@ -94,7 +94,7 @@ class PlaylistServiceTest extends TestCase /** @var PlaylistFolder $folder */ $folder = PlaylistFolder::factory()->create(); - self::expectException(InvalidArgumentException::class); + $this->expectException(InvalidArgumentException::class); $this->service->createPlaylist('foo', create_user(), $folder); } @@ -151,8 +151,8 @@ class PlaylistServiceTest extends TestCase public function testSettingOwnsSongOnlyFailsForCommunityLicenseWhenCreate(): void { - self::expectException(BaseInvalidArgumentException::class); - self::expectExceptionMessage('"Own songs only" option only works with smart playlists and Plus license.'); + $this->expectException(BaseInvalidArgumentException::class); + $this->expectExceptionMessage('"Own songs only" option only works with smart playlists and Plus license.'); $this->service->createPlaylist( name: 'foo', @@ -176,8 +176,8 @@ class PlaylistServiceTest extends TestCase public function testSettingOwnsSongOnlyFailsForCommunityLicenseWhenUpdate(): void { - self::expectException(BaseInvalidArgumentException::class); - self::expectExceptionMessage('"Own songs only" option only works with smart playlists and Plus license.'); + $this->expectException(BaseInvalidArgumentException::class); + $this->expectExceptionMessage('"Own songs only" option only works with smart playlists and Plus license.'); /** @var Playlist $playlist */ $playlist = Playlist::factory()->smart()->create(); diff --git a/tests/Integration/Services/SongStorages/LocalStorageTest.php b/tests/Integration/Services/SongStorages/LocalStorageTest.php index 4374fc2a..6ec582a9 100644 --- a/tests/Integration/Services/SongStorages/LocalStorageTest.php +++ b/tests/Integration/Services/SongStorages/LocalStorageTest.php @@ -28,7 +28,7 @@ class LocalStorageTest extends TestCase { Setting::set('media_path', ''); - self::expectException(MediaPathNotSetException::class); + $this->expectException(MediaPathNotSetException::class); $this->service->storeUploadedFile(Mockery::mock(UploadedFile::class), create_user()); } @@ -36,7 +36,7 @@ class LocalStorageTest extends TestCase { Setting::set('media_path', public_path('sandbox/media')); - self::expectException(SongUploadFailedException::class); + $this->expectException(SongUploadFailedException::class); $this->service->storeUploadedFile(UploadedFile::fake()->create('fake.mp3'), create_user()); } diff --git a/tests/Integration/Services/Streamer/StreamerTest.php b/tests/Integration/Services/Streamer/StreamerTest.php index e76654d9..229922d5 100644 --- a/tests/Integration/Services/Streamer/StreamerTest.php +++ b/tests/Integration/Services/Streamer/StreamerTest.php @@ -23,14 +23,14 @@ class StreamerTest extends TestCase public function testResolveAdapters(): void { collect(SongStorageTypes::ALL_TYPES) - ->each(static function (?string $type): void { + ->each(function (?string $type): void { /** @var Song $song */ $song = Song::factory()->make(['storage' => $type]); switch ($type) { case SongStorageTypes::S3: case SongStorageTypes::DROPBOX: - self::expectException(KoelPlusRequiredException::class); + $this->expectException(KoelPlusRequiredException::class); new Streamer($song); break; diff --git a/tests/Integration/Services/YouTubeServiceTest.php b/tests/Integration/Services/YouTubeServiceTest.php new file mode 100644 index 00000000..6381b5f0 --- /dev/null +++ b/tests/Integration/Services/YouTubeServiceTest.php @@ -0,0 +1,54 @@ +service = app(YouTubeService::class); + } + + public function testSearchVideosRelatedToSong(): void + { + /** @var Song $song */ + $song = Song::factory()->for(Artist::factory()->create(['name' => 'Slipknot']))->create(['title' => 'Snuff']); + + Saloon::fake([ + SearchVideosRequest::class => MockResponse::make(body: File::get(test_path('blobs/youtube/search.json'))), + ]); + + $response = $this->service->searchVideosRelatedToSong($song, 'my-token'); + + self::assertSame('Slipknot - Snuff [OFFICIAL VIDEO]', $response->items[0]->snippet->title); + self::assertNotNull(Cache::get('youtube.cce909a3df066c88c2666d4283697867')); + + Saloon::assertSent(static function (SearchVideosRequest $request): bool { + self::assertSame([ + 'part' => 'snippet', + 'type' => 'video', + 'maxResults' => 10, + 'pageToken' => 'my-token', + 'q' => 'Snuff Slipknot', + ], $request->query()->all()); + + return true; + }); + } +} diff --git a/tests/Unit/Http/Integrations/Spotify/SpotifyClientTest.php b/tests/Unit/Http/Integrations/Spotify/SpotifyClientTest.php index 20d3d546..eb1d4f3a 100644 --- a/tests/Unit/Http/Integrations/Spotify/SpotifyClientTest.php +++ b/tests/Unit/Http/Integrations/Spotify/SpotifyClientTest.php @@ -71,7 +71,7 @@ class SpotifyClientTest extends TestCase 'koel.spotify.client_secret' => null, ]); - self::expectException(SpotifyIntegrationDisabledException::class); + $this->expectException(SpotifyIntegrationDisabledException::class); (new SpotifyClient($this->wrapped, $this->session, $this->cache))->search('foo', 'track'); } diff --git a/tests/Unit/Rules/ValidSmartPlaylistRulePayloadTest.php b/tests/Unit/Rules/ValidSmartPlaylistRulePayloadTest.php index 2b1b5ce7..251b23c1 100644 --- a/tests/Unit/Rules/ValidSmartPlaylistRulePayloadTest.php +++ b/tests/Unit/Rules/ValidSmartPlaylistRulePayloadTest.php @@ -94,7 +94,7 @@ class ValidSmartPlaylistRulePayloadTest extends TestCase /** @dataProvider provideInvalidPayloads */ public function testInvalidCases($value): void { - self::expectException(Throwable::class); + $this->expectException(Throwable::class); self::assertFalse((new ValidSmartPlaylistRulePayload())->passes('rules', $value)); } diff --git a/tests/Unit/Services/ITunesServiceTest.php b/tests/Unit/Services/ITunesServiceTest.php deleted file mode 100644 index 18741c84..00000000 --- a/tests/Unit/Services/ITunesServiceTest.php +++ /dev/null @@ -1,87 +0,0 @@ - true]); - /** @var ITunesService $iTunes */ - $iTunes = app()->make(ITunesService::class); - self::assertTrue($iTunes->used()); - - config(['koel.itunes.enabled' => false]); - self::assertFalse($iTunes->used()); - } - - /** @return array */ - public function provideGetTrackUrlData(): array - { - return [ - [ - 'Foo', - 'Bar', - 'Baz', - 'Foo Bar Baz', - 'https://itunes.apple.com/bar', - 'https://itunes.apple.com/bar?at=foo', - '2ce68c30758ed9496c72c36ff49c50b2', - ], [ - 'Foo', - '', - 'Baz', - 'Foo Baz', - 'https://itunes.apple.com/bar?qux=qux', - 'https://itunes.apple.com/bar?qux=qux&at=foo', - 'cda57916eb80c2ee79b16e218bdb70d2', - ], - ]; - } - - /** @dataProvider provideGetTrackUrlData */ - public function testGetTrackUrl( - string $term, - string $album, - string $artist, - string $constructedTerm, - string $trackViewUrl, - string $affiliateUrl, - string $cacheKey - ): void { - config(['koel.itunes.affiliate_id' => 'foo']); - $cache = Mockery::mock(Cache::class); - $client = Mockery::mock(ITunesClient::class); - - $client->shouldReceive('get') - ->with('/', [ - 'query' => [ - 'term' => $constructedTerm, - 'media' => 'music', - 'entity' => 'song', - 'limit' => 1, - ], - ]) - ->andReturn(json_decode(json_encode([ - 'resultCount' => 1, - 'results' => [['trackViewUrl' => $trackViewUrl]], - ]))); - - $service = new ITunesService($client, $cache); - - $cache - ->shouldReceive('remember') - ->with($cacheKey, 10_080, Mockery::on(static function (callable $generator) use ($affiliateUrl): bool { - self::assertSame($generator(), $affiliateUrl); - return true; - })); - - $service->getTrackUrl($term, $album, $artist); - } -} diff --git a/tests/Unit/Services/SongStorages/DropboxStorageTest.php b/tests/Unit/Services/SongStorages/DropboxStorageTest.php index 5cd1a873..48eb3d74 100644 --- a/tests/Unit/Services/SongStorages/DropboxStorageTest.php +++ b/tests/Unit/Services/SongStorages/DropboxStorageTest.php @@ -10,7 +10,7 @@ class DropboxStorageTest extends TestCase { public function testSupported(): void { - self::expectException(KoelPlusRequiredException::class); + $this->expectException(KoelPlusRequiredException::class); app(DropboxStorage::class); } } diff --git a/tests/Unit/Services/SongStorages/S3CompatibleStorageTest.php b/tests/Unit/Services/SongStorages/S3CompatibleStorageTest.php index 5efbc64f..af99f849 100644 --- a/tests/Unit/Services/SongStorages/S3CompatibleStorageTest.php +++ b/tests/Unit/Services/SongStorages/S3CompatibleStorageTest.php @@ -10,7 +10,7 @@ class S3CompatibleStorageTest extends TestCase { public function testSupported(): void { - self::expectException(KoelPlusRequiredException::class); + $this->expectException(KoelPlusRequiredException::class); app(S3CompatibleStorage::class); } } diff --git a/tests/Unit/Services/YouTubeServiceTest.php b/tests/Unit/Services/YouTubeServiceTest.php deleted file mode 100644 index b17c4626..00000000 --- a/tests/Unit/Services/YouTubeServiceTest.php +++ /dev/null @@ -1,34 +0,0 @@ -for(Artist::factory()->create(['name' => 'Bar']))->create(['title' => 'Foo']); - $client = Mockery::mock(YouTubeClient::class); - - $client->shouldReceive('get') - ->with('search?part=snippet&type=video&maxResults=10&pageToken=my-token&q=Foo+Bar') - ->andReturn(json_decode(File::get(test_path('blobs/youtube/search.json')))); - - $service = new YouTubeService($client, app(Repository::class)); - $response = $service->searchVideosRelatedToSong($song, 'my-token'); - - self::assertSame('Slipknot - Snuff [OFFICIAL VIDEO]', $response->items[0]->snippet->title); - self::assertNotNull(cache()->get('5becf539115b18b2df11c39adbc2bdfa')); - } -}