Big revamp for lastfm and youtube services

This commit is contained in:
Phan An 2018-08-19 13:08:16 +02:00
parent d4d2b0aff3
commit 67357316bc
24 changed files with 298 additions and 321 deletions

View file

@ -3,10 +3,21 @@
namespace App\Http\Controllers\API\MediaInformation; namespace App\Http\Controllers\API\MediaInformation;
use App\Models\Song; use App\Models\Song;
use App\Services\MediaInformationService;
use App\Services\YouTubeService;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
class SongController extends Controller class SongController extends Controller
{ {
private $youTubeService;
public function __construct(MediaInformationService $mediaInformationService, YouTubeService $youTubeService)
{
parent::__construct($mediaInformationService);
$this->youTubeService = $youTubeService;
}
/** /**
* Get extra information about a song. * Get extra information about a song.
* *
@ -20,7 +31,7 @@ class SongController extends Controller
'lyrics' => $song->lyrics, 'lyrics' => $song->lyrics,
'album_info' => $this->mediaInformationService->getAlbumInformation($song->album), 'album_info' => $this->mediaInformationService->getAlbumInformation($song->album),
'artist_info' => $this->mediaInformationService->getArtistInformation($song->artist), 'artist_info' => $this->mediaInformationService->getArtistInformation($song->artist),
'youtube' => $song->getRelatedYouTubeVideos(), 'youtube' => $this->youTubeService->searchVideosRelatedToSong($song),
]); ]);
} }
} }

View file

@ -4,10 +4,18 @@ namespace App\Http\Controllers\API;
use App\Http\Requests\API\YouTubeSearchRequest; use App\Http\Requests\API\YouTubeSearchRequest;
use App\Models\Song; use App\Models\Song;
use App\Services\YouTubeService;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
class YouTubeController extends Controller class YouTubeController extends Controller
{ {
private $youTubeService;
public function __construct(YouTubeService $youTubeService)
{
$this->youTubeService = $youTubeService;
}
/** /**
* Search for YouTube videos related to a song (using its title and artist name). * Search for YouTube videos related to a song (using its title and artist name).
* *
@ -18,6 +26,6 @@ class YouTubeController extends Controller
*/ */
public function searchVideosRelatedToSong(YouTubeSearchRequest $request, Song $song) public function searchVideosRelatedToSong(YouTubeSearchRequest $request, Song $song)
{ {
return response()->json($song->getRelatedYouTubeVideos($request->pageToken)); return response()->json($this->youTubeService->searchVideosRelatedToSong($song, $request->pageToken));
} }
} }

View file

@ -266,6 +266,7 @@ class Song extends Model
*/ */
public static function getFavorites(User $user, $toArray = false) public static function getFavorites(User $user, $toArray = false)
{ {
/** @var Collection $songs */
$songs = Interaction::whereUserIdAndLike($user->id, true) $songs = Interaction::whereUserIdAndLike($user->id, true)
->with('song') ->with('song')
->get() ->get()
@ -302,18 +303,6 @@ class Song extends Model
}); });
} }
/**
* Get the YouTube videos related to this song.
*
* @param string $youTubePageToken The YouTube page token, for pagination purpose.
*
* @return false|object
*/
public function getRelatedYouTubeVideos($youTubePageToken = '')
{
return YouTube::searchVideosRelatedToSong($this, $youTubePageToken);
}
/** /**
* Sometimes the tags extracted from getID3 are HTML entity encoded. * Sometimes the tags extracted from getID3 are HTML entity encoded.
* This makes sure they are always sane. * This makes sure they are always sane.

View file

@ -25,7 +25,7 @@ class LastfmServiceProvider extends ServiceProvider
public function register() public function register()
{ {
app()->singleton('Lastfm', function () { app()->singleton('Lastfm', function () {
return new LastfmService(); return app()->make(LastfmService::class);
}); });
} }
} }

View file

@ -2,7 +2,7 @@
namespace App\Providers; namespace App\Providers;
use App\Services\YouTube; use App\Services\YouTubeService;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
class YouTubeServiceProvider extends ServiceProvider class YouTubeServiceProvider extends ServiceProvider
@ -25,7 +25,7 @@ class YouTubeServiceProvider extends ServiceProvider
public function register() public function register()
{ {
app()->singleton('YouTube', function () { app()->singleton('YouTube', function () {
return new YouTube(); return app()->make(YouTubeService::class);
}); });
} }
} }

View file

@ -7,26 +7,17 @@ use GuzzleHttp\Exception\ClientException;
use InvalidArgumentException; use InvalidArgumentException;
/** /**
* Class RESTfulService. * @method object get($uri, ...$args)
*
* @method object get($uri)
* @method object post($uri, ...$data) * @method object post($uri, ...$data)
* @method object put($uri, ...$data) * @method object put($uri, ...$data)
* @method object patch($uri, ...$data) * @method object patch($uri, ...$data)
* @method object head($uri, ...$data) * @method object head($uri, ...$data)
* @method object delete($uri) * @method object delete($uri)
*/ */
class RESTfulService abstract class ApiClient
{ {
protected $responseFormat = 'json'; protected $responseFormat = 'json';
/**
* The API endpoint.
*
* @var string
*/
protected $endpoint;
/** /**
* The GuzzleHttp client to talk to the API. * The GuzzleHttp client to talk to the API.
* *
@ -34,13 +25,6 @@ class RESTfulService
*/ */
protected $client; protected $client;
/**
* The API key.
*
* @var string
*/
protected $key;
/** /**
* The query parameter name for the key. * The query parameter name for the key.
* For example, Last.fm use api_key, like this: * For example, Last.fm use api_key, like this:
@ -50,19 +34,9 @@ class RESTfulService
*/ */
protected $keyParam = 'key'; protected $keyParam = 'key';
/** public function __construct(Client $client)
* The API secret.
*
* @var string
*/
protected $secret;
public function __construct($key, $secret, $endpoint, Client $client)
{ {
$this->setKey($key); $this->client = $client;
$this->setSecret($secret);
$this->setEndpoint($endpoint);
$this->setClient($client);
} }
/** /**
@ -80,7 +54,7 @@ class RESTfulService
public function request($verb, $uri, $appendKey = true, array $params = []) public function request($verb, $uri, $appendKey = true, array $params = [])
{ {
try { try {
$body = (string) $this->getClient() $body = (string)$this->getClient()
->$verb($this->buildUrl($uri, $appendKey), ['form_params' => $params]) ->$verb($this->buildUrl($uri, $appendKey), ['form_params' => $params])
->getBody(); ->getBody();
@ -104,7 +78,7 @@ class RESTfulService
* @param string $method The HTTP method * @param string $method The HTTP method
* @param array $args An array of parameters * @param array $args An array of parameters
* *
* @throws \InvalidArgumentException * @throws InvalidArgumentException
* *
* @return object * @return object
*/ */
@ -136,14 +110,14 @@ class RESTfulService
$uri = "/$uri"; $uri = "/$uri";
} }
$uri = $this->endpoint.$uri; $uri = $this->getEndpoint() . $uri;
} }
if ($appendKey) { if ($appendKey) {
if (parse_url($uri, PHP_URL_QUERY)) { if (parse_url($uri, PHP_URL_QUERY)) {
$uri .= "&{$this->keyParam}=".$this->getKey(); $uri .= "&{$this->keyParam}=" . $this->getKey();
} else { } else {
$uri .= "?{$this->keyParam}=".$this->getKey(); $uri .= "?{$this->keyParam}=" . $this->getKey();
} }
} }
@ -158,59 +132,9 @@ class RESTfulService
return $this->client; return $this->client;
} }
/** abstract public function getKey();
* @param Client $client
*/
public function setClient($client)
{
$this->client = $client;
}
/** abstract public function getSecret();
* @return string
*/
public function getKey()
{
return $this->key;
}
/** abstract public function getEndpoint();
* @param string $key
*/
public function setKey($key)
{
$this->key = $key;
}
/**
* @return string
*/
public function getSecret()
{
return $this->secret;
}
/**
* @param string $secret
*/
public function setSecret($secret)
{
$this->secret = $secret;
}
/**
* @return string
*/
public function getEndpoint()
{
return $this->endpoint;
}
/**
* @param string $endpoint
*/
public function setEndpoint($endpoint)
{
$this->endpoint = $endpoint;
}
} }

View file

@ -0,0 +1,16 @@
<?php
namespace App\Services;
interface ApiConsumerInterface
{
/** @return string */
public function getEndpoint();
/** @return string */
public function getKey();
/** @return string|null */
public function getSecret();
}

View file

@ -3,11 +3,9 @@
namespace App\Services; namespace App\Services;
use Exception; use Exception;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Cache;
use Log; use Log;
class LastfmService extends RESTfulService class LastfmService extends ApiClient implements ApiConsumerInterface
{ {
/** /**
* Specify the response format, since Last.fm only returns XML. * Specify the response format, since Last.fm only returns XML.
@ -23,23 +21,6 @@ class LastfmService extends RESTfulService
*/ */
protected $keyParam = 'api_key'; protected $keyParam = 'api_key';
/**
* Construct an instance of Lastfm service.
*
* @param string $key Last.fm API key.
* @param string $secret Last.fm API shared secret.
* @param Client $client The Guzzle HTTP client.
*/
public function __construct($key = null, $secret = null, Client $client = null)
{
parent::__construct(
$key ?: config('koel.lastfm.key'),
$secret ?: config('koel.lastfm.secret'),
'https://ws.audioscrobbler.com/2.0',
$client ?: new Client()
);
}
/** /**
* Determine if our application is using Last.fm. * Determine if our application is using Last.fm.
* *
@ -47,7 +28,7 @@ class LastfmService extends RESTfulService
*/ */
public function used() public function used()
{ {
return config('koel.lastfm.key') && config('koel.lastfm.secret'); return $this->getKey();
} }
/** /**
@ -67,7 +48,7 @@ class LastfmService extends RESTfulService
* *
* @return array|false * @return array|false
*/ */
public function getArtistInfo($name) public function getArtistInformation($name)
{ {
if (!$this->enabled()) { if (!$this->enabled()) {
return false; return false;
@ -92,7 +73,7 @@ class LastfmService extends RESTfulService
return false; return false;
} }
return $this->buildArtistInfo($artist); return $this->buildArtistInformation($artist);
} catch (Exception $e) { } catch (Exception $e) {
Log::error($e); Log::error($e);
@ -107,7 +88,7 @@ class LastfmService extends RESTfulService
* *
* @return array * @return array
*/ */
private function buildArtistInfo(array $lastfmArtist) private function buildArtistInformation(array $lastfmArtist)
{ {
return [ return [
'url' => array_get($lastfmArtist, 'url'), 'url' => array_get($lastfmArtist, 'url'),
@ -127,7 +108,7 @@ class LastfmService extends RESTfulService
* *
* @return array|false * @return array|false
*/ */
public function getAlbumInfo($name, $artistName) public function getAlbumInformation($name, $artistName)
{ {
if (!$this->enabled()) { if (!$this->enabled()) {
return false; return false;
@ -153,7 +134,7 @@ class LastfmService extends RESTfulService
return false; return false;
} }
return $this->buildAlbumInfo($album); return $this->buildAlbumInformation($album);
} catch (Exception $e) { } catch (Exception $e) {
Log::error($e); Log::error($e);
@ -168,7 +149,7 @@ class LastfmService extends RESTfulService
* *
* @return array * @return array
*/ */
private function buildAlbumInfo(array $lastfmAlbum) private function buildAlbumInformation(array $lastfmAlbum)
{ {
return [ return [
'url' => array_get($lastfmAlbum, 'url'), 'url' => array_get($lastfmAlbum, 'url'),
@ -349,4 +330,19 @@ class LastfmService extends RESTfulService
return trim(str_replace('Read more on Last.fm', '', nl2br(strip_tags(html_entity_decode($str))))); return trim(str_replace('Read more on Last.fm', '', nl2br(strip_tags(html_entity_decode($str)))));
} }
public function getKey()
{
return config('koel.lastfm.key');
}
public function getEndpoint()
{
return config('koel.lastfm.endpoint');
}
public function getSecret()
{
return config('koel.lastfm.secret');
}
} }

View file

@ -29,12 +29,15 @@ class MediaInformationService
return false; return false;
} }
$info = $this->lastfmService->getAlbumInfo($album->name, $album->artist->name); $info = $this->lastfmService->getAlbumInformation($album->name, $album->artist->name);
event(new AlbumInformationFetched($album, $info));
// The album may have been updated. if ($info) {
$album->refresh(); event(new AlbumInformationFetched($album, $info));
$info['cover'] = $album->cover;
// The album may have been updated.
$album->refresh();
$info['cover'] = $album->cover;
}
return $info; return $info;
} }
@ -52,12 +55,15 @@ class MediaInformationService
return false; return false;
} }
$info = $this->lastfmService->getArtistInfo($artist->name); $info = $this->lastfmService->getArtistInformation($artist->name);
event(new ArtistInformationFetched($artist, $info));
// The artist may have been updated. if ($info) {
$artist->refresh(); event(new ArtistInformationFetched($artist, $info));
$info['image'] = $artist->image;
// The artist may have been updated.
$artist->refresh();
$info['image'] = $artist->image;
}
return $info; return $info;
} }

View file

@ -4,26 +4,9 @@ namespace App\Services;
use App\Models\Song; use App\Models\Song;
use Cache; use Cache;
use GuzzleHttp\Client;
class YouTube extends RESTfulService class YouTubeService extends ApiClient implements ApiConsumerInterface
{ {
/**
* Construct an instance of YouTube service.
*
* @param string $key The YouTube API key
* @param Client|null $client The Guzzle HTTP client
*/
public function __construct($key = null, Client $client = null)
{
parent::__construct(
$key ?: config('koel.youtube.key'),
null,
'https://www.googleapis.com/youtube/v3',
$client ?: new Client()
);
}
/** /**
* Determine if our application is using YouTube. * Determine if our application is using YouTube.
* *
@ -31,7 +14,7 @@ class YouTube extends RESTfulService
*/ */
public function enabled() public function enabled()
{ {
return (bool) config('koel.youtube.key'); return (bool) $this->getKey();
} }
/** /**
@ -79,4 +62,22 @@ class YouTube extends RESTfulService
return $this->get($uri); return $this->get($uri);
}); });
} }
/** @return string */
public function getEndpoint()
{
return config('koel.youtube.endpoint');
}
/** @return string */
public function getKey()
{
return config('koel.youtube.key');
}
/** @return string|null */
public function getSecret()
{
return null;
}
} }

View file

@ -15,7 +15,9 @@
"predis/predis": "~1.0", "predis/predis": "~1.0",
"doctrine/dbal": "^2.5", "doctrine/dbal": "^2.5",
"jackiedo/dotenv-editor": "^1.0", "jackiedo/dotenv-editor": "^1.0",
"ext-exif": "*" "ext-exif": "*",
"ext-json": "*",
"ext-SimpleXML": "*"
}, },
"require-dev": { "require-dev": {
"fzaninotto/faker": "~1.4", "fzaninotto/faker": "~1.4",

View file

@ -58,6 +58,7 @@ return [
'youtube' => [ 'youtube' => [
'key' => env('YOUTUBE_API_KEY'), 'key' => env('YOUTUBE_API_KEY'),
'endpoint' => 'https://www.googleapis.com/youtube/v3',
], ],
/* /*
@ -72,6 +73,7 @@ return [
'lastfm' => [ 'lastfm' => [
'key' => env('LASTFM_API_KEY'), 'key' => env('LASTFM_API_KEY'),
'secret' => env('LASTFM_API_SECRET'), 'secret' => env('LASTFM_API_SECRET'),
'endpoint' => 'https://ws.audioscrobbler.com/2.0',
], ],
/* /*

View file

@ -12,37 +12,31 @@ use App\Models\Interaction;
use App\Models\Song; use App\Models\Song;
use App\Models\User; use App\Models\User;
use App\Services\LastfmService; use App\Services\LastfmService;
use Exception;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Response;
use Illuminate\Contracts\Auth\Guard; use Illuminate\Contracts\Auth\Guard;
use Illuminate\Foundation\Testing\WithoutMiddleware; use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Routing\Redirector; use Illuminate\Routing\Redirector;
use Mockery as m; use Mockery as m;
use Mockery\MockInterface;
use Tymon\JWTAuth\JWTAuth; use Tymon\JWTAuth\JWTAuth;
class LastfmTest extends TestCase class LastfmTest extends TestCase
{ {
use WithoutMiddleware; use WithoutMiddleware;
protected function tearDown()
{
m::close();
parent::tearDown();
}
public function testGetSessionKey() public function testGetSessionKey()
{ {
/** @var Client $client */
$client = m::mock(Client::class, [ $client = m::mock(Client::class, [
'get' => new Response(200, [], file_get_contents(__DIR__.'../../blobs/lastfm/session-key.xml')), 'get' => new Response(200, [], file_get_contents(__DIR__.'../../blobs/lastfm/session-key.xml')),
]); ]);
$api = new LastfmService(null, null, $client); $this->assertEquals('foo', (new LastfmService($client))->getSessionKey('bar'));
$this->assertEquals('foo', $api->getSessionKey('bar'));
} }
/** @test */ public function testSetSessionKey()
public function session_key_can_be_set()
{ {
$user = factory(User::class)->create(); $user = factory(User::class)->create();
$this->postAsUser('api/lastfm/session-key', ['key' => 'foo'], $user); $this->postAsUser('api/lastfm/session-key', ['key' => 'foo'], $user);
@ -53,31 +47,30 @@ class LastfmTest extends TestCase
/** @test */ /** @test */
public function user_can_connect_to_lastfm() public function user_can_connect_to_lastfm()
{ {
/** @var Redirector|m\MockInterface $redirector */ /** @var Redirector|MockInterface $redirector */
$redirector = m::mock(Redirector::class); $redirector = m::mock(Redirector::class);
$redirector->shouldReceive('to')->once(); $redirector->shouldReceive('to')->once();
/** @var Guard|m\MockInterface $guard */ /** @var Guard|MockInterface $guard */
$guard = m::mock(Guard::class, ['user' => factory(User::class)->create()]); $guard = m::mock(Guard::class, ['user' => factory(User::class)->create()]);
$auth = m::mock(JWTAuth::class, [ $auth = m::mock(JWTAuth::class, [
'parseToken' => '', 'parseToken' => '',
'getToken' => '', 'getToken' => '',
]); ]);
(new LastfmController($guard))->connect($redirector, new LastfmService(), $auth); (new LastfmController($guard))->connect($redirector, app(LastfmService::class), $auth);
} }
/** @test */ public function testRetrieveAndStoreSessionKey()
public function lastfm_session_key_can_be_retrieved_and_stored()
{ {
/** @var LastfmCallbackRequest|m\MockInterface $request */ /** @var LastfmCallbackRequest $request */
$request = m::mock(LastfmCallbackRequest::class); $request = m::mock(LastfmCallbackRequest::class);
$request->token = 'foo'; $request->token = 'foo';
/** @var LastfmService|m\MockInterface $lastfm */ /** @var LastfmService $lastfm */
$lastfm = m::mock(LastfmService::class, ['getSessionKey' => 'bar']); $lastfm = m::mock(LastfmService::class, ['getSessionKey' => 'bar']);
$user = factory(User::class)->create(); $user = factory(User::class)->create();
/** @var Guard|m\MockInterface $guard */ /** @var Guard $guard */
$guard = m::mock(Guard::class, ['user' => $user]); $guard = m::mock(Guard::class, ['user' => $user]);
(new LastfmController($guard))->callback($request, $lastfm); (new LastfmController($guard))->callback($request, $lastfm);
@ -85,8 +78,7 @@ class LastfmTest extends TestCase
$this->assertEquals('bar', $user->lastfm_session_key); $this->assertEquals('bar', $user->lastfm_session_key);
} }
/** @test */ public function testDisconnectUser()
public function user_can_disconnect_from_lastfm()
{ {
$user = factory(User::class)->create(['preferences' => ['lastfm_session_key' => 'bar']]); $user = factory(User::class)->create(['preferences' => ['lastfm_session_key' => 'bar']]);
$this->deleteAsUser('api/lastfm/disconnect', [], $user); $this->deleteAsUser('api/lastfm/disconnect', [], $user);
@ -94,8 +86,10 @@ class LastfmTest extends TestCase
$this->assertNull($user->lastfm_session_key); $this->assertNull($user->lastfm_session_key);
} }
/** @test */ /**
public function user_can_love_a_track_on_lastfm() * @throws Exception
*/
public function testLoveTrack()
{ {
$this->withoutEvents(); $this->withoutEvents();
$this->createSampleMediaSet(); $this->createSampleMediaSet();
@ -107,16 +101,19 @@ class LastfmTest extends TestCase
'song_id' => Song::first()->id, 'song_id' => Song::first()->id,
]); ]);
/** @var LastfmService|m\MockInterface $lastfm */ /** @var LastfmService|MockInterface $lastfm */
$lastfm = m::mock(LastfmService::class, ['enabled' => true]); $lastfm = m::mock(LastfmService::class, ['enabled' => true]);
$lastfm->shouldReceive('toggleLoveTrack') $lastfm->shouldReceive('toggleLoveTrack')
->once()
->with($interaction->song->title, $interaction->song->album->artist->name, 'bar', false); ->with($interaction->song->title, $interaction->song->album->artist->name, 'bar', false);
(new LoveTrackOnLastfm($lastfm))->handle(new SongLikeToggled($interaction, $user)); (new LoveTrackOnLastfm($lastfm))->handle(new SongLikeToggled($interaction, $user));
} }
/** @test */ /**
public function user_now_playing_status_can_be_updated_to_lastfm() * @throws Exception
*/
public function testUpdateNowPlayingStatus()
{ {
$this->withoutEvents(); $this->withoutEvents();
$this->createSampleMediaSet(); $this->createSampleMediaSet();
@ -124,9 +121,10 @@ class LastfmTest extends TestCase
$user = factory(User::class)->create(['preferences' => ['lastfm_session_key' => 'bar']]); $user = factory(User::class)->create(['preferences' => ['lastfm_session_key' => 'bar']]);
$song = Song::first(); $song = Song::first();
/** @var LastfmService|m\MockInterface $lastfm */ /** @var LastfmService|MockInterface $lastfm */
$lastfm = m::mock(LastfmService::class, ['enabled' => true]); $lastfm = m::mock(LastfmService::class, ['enabled' => true]);
$lastfm->shouldReceive('updateNowPlaying') $lastfm->shouldReceive('updateNowPlaying')
->once()
->with($song->album->artist->name, $song->title, $song->album->name, $song->length, 'bar'); ->with($song->album->artist->name, $song->title, $song->album->name, $song->length, 'bar');
(new UpdateLastfmNowPlaying($lastfm))->handle(new SongStartedPlaying($song, $user)); (new UpdateLastfmNowPlaying($lastfm))->handle(new SongStartedPlaying($song, $user));

View file

@ -10,6 +10,7 @@ use Exception;
use JWTAuth; use JWTAuth;
use Laravel\BrowserKitTesting\DatabaseTransactions; use Laravel\BrowserKitTesting\DatabaseTransactions;
use Laravel\BrowserKitTesting\TestCase as BaseTestCase; use Laravel\BrowserKitTesting\TestCase as BaseTestCase;
use Mockery;
use Tests\CreatesApplication; use Tests\CreatesApplication;
use Tests\Traits\InteractsWithIoc; use Tests\Traits\InteractsWithIoc;
@ -89,4 +90,10 @@ abstract class TestCase extends BaseTestCase
'Authorization' => 'Bearer '.JWTAuth::fromUser($user), 'Authorization' => 'Bearer '.JWTAuth::fromUser($user),
]); ]);
} }
protected function tearDown()
{
Mockery::close();
parent::tearDown();
}
} }

View file

@ -3,21 +3,37 @@
namespace Tests\Feature; namespace Tests\Feature;
use App\Models\Song; use App\Models\Song;
use App\Services\YouTubeService;
use Exception;
use Illuminate\Foundation\Testing\WithoutMiddleware; use Illuminate\Foundation\Testing\WithoutMiddleware;
use Mockery\MockInterface;
use YouTube; use YouTube;
class YouTubeTest extends TestCase class YouTubeTest extends TestCase
{ {
use WithoutMiddleware; use WithoutMiddleware;
/** @test */ /** @var YouTubeService|MockInterface */
public function youtube_videos_related_to_a_song_can_be_searched() private $youTubeService;
public function setUp()
{
parent::setUp();
$this->youTubeService = $this->mockIocDependency(YouTubeService::class);
}
/**
* @throws Exception
*/
public function testSearchYouTubeVideos()
{ {
$this->createSampleMediaSet(); $this->createSampleMediaSet();
$song = Song::first(); $song = Song::first();
// We test on the facade here $this->youTubeService
YouTube::shouldReceive('searchVideosRelatedToSong')->once(); ->shouldReceive('searchVideosRelatedToSong')
->once();
$this->getAsUser("/api/youtube/search/song/{$song->id}"); $this->getAsUser("/api/youtube/search/song/{$song->id}");
} }

View file

@ -86,25 +86,6 @@ class SongTest extends TestCase
// Then the song shouldn't be scrobbled // Then the song shouldn't be scrobbled
} }
/** @test */
public function it_gets_related_youtube_videos()
{
// Given there's a song
/** @var Song $song */
$song = factory(Song::class)->create();
// When I get is related YouTube videos
YouTube::shouldReceive('searchVideosRelatedToSong')
->once()
->with($song, 'foo')
->andReturn(['bar' => 'baz']);
$videos = $song->getRelatedYouTubeVideos('foo');
// Then I see the related YouTube videos returned
$this->assertEquals(['bar' => 'baz'], $videos);
}
/** @test */ /** @test */
public function it_can_be_retrieved_using_its_path() public function it_can_be_retrieved_using_its_path()
{ {

View file

@ -5,39 +5,32 @@ namespace Tests\Integration\Services;
use App\Models\Album; use App\Models\Album;
use App\Models\Artist; use App\Models\Artist;
use App\Services\LastfmService; use App\Services\LastfmService;
use Exception;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Response;
use Mockery as m; use Mockery as m;
use Tests\TestCase; use Tests\TestCase;
class LastfmTest extends TestCase class LastfmServiceTest extends TestCase
{ {
protected function tearDown()
{
m::close();
parent::tearDown();
}
/** /**
* @test * @throws Exception
*
* @throws \Exception
*/ */
public function it_returns_artist_info_if_artist_is_found_on_lastfm() public function testGetArtistInformation()
{ {
// Given an artist that exists on Last.fm
/** @var Artist $artist */ /** @var Artist $artist */
$artist = factory(Artist::class)->create(['name' => 'foo']); $artist = factory(Artist::class)->make(['name' => 'foo']);
// When I request the service for the artist's info /** @var Client $client */
$client = m::mock(Client::class, [ $client = m::mock(Client::class, [
'get' => new Response(200, [], file_get_contents(__DIR__.'../../../blobs/lastfm/artist.xml')), 'get' => new Response(200, [], file_get_contents(__DIR__.'../../../blobs/lastfm/artist.xml')),
]); ]);
$api = new LastfmService(null, null, $client); $api = new LastfmService($client);
$info = $api->getArtistInfo($artist->name); $info = $api->getArtistInformation($artist->name
);
// Then I see the info when the request is the successful
$this->assertEquals([ $this->assertEquals([
'url' => 'http://www.last.fm/music/Kamelot', 'url' => 'http://www.last.fm/music/Kamelot',
'image' => 'http://foo.bar/extralarge.jpg', 'image' => 'http://foo.bar/extralarge.jpg',
@ -51,46 +44,40 @@ class LastfmTest extends TestCase
$this->assertNotNull(cache('0aff3bc1259154f0e9db860026cda7a6')); $this->assertNotNull(cache('0aff3bc1259154f0e9db860026cda7a6'));
} }
/** @test */ public function testGetArtistInformationForNonExistentArtist()
public function it_returns_false_if_artist_info_is_not_found_on_lastfm()
{ {
// Given an artist that doesn't exist on Last.fm
/** @var Artist $artist */ /** @var Artist $artist */
$artist = factory(Artist::class)->create(); $artist = factory(Artist::class)->make();
// When I request the service for the artist info /** @var Client $client */
$client = m::mock(Client::class, [ $client = m::mock(Client::class, [
'get' => new Response(400, [], file_get_contents(__DIR__.'../../../blobs/lastfm/artist-notfound.xml')), 'get' => new Response(400, [], file_get_contents(__DIR__.'../../../blobs/lastfm/artist-notfound.xml')),
]); ]);
$api = new LastfmService(null, null, $client); $api = new LastfmService($client);
$result = $api->getArtistInfo($artist->name); $result = $api->getArtistInformation($artist->name);
// Then I receive boolean false
$this->assertFalse($result); $this->assertFalse($result);
} }
/** /**
* @test * @throws Exception
*
* @throws \Exception
*/ */
public function it_returns_album_info_if_album_is_found_on_lastfm() public function testGetAlbumInformation()
{ {
// Given an album that exists on Last.fm
/** @var Album $album */ /** @var Album $album */
$album = factory(Album::class)->create([ $album = factory(Album::class)->create([
'artist_id' => factory(Artist::class)->create(['name' => 'bar'])->id, 'artist_id' => factory(Artist::class)->create(['name' => 'bar'])->id,
'name' => 'foo', 'name' => 'foo',
]); ]);
// When I request the service for the album's info /** @var Client $client */
$client = m::mock(Client::class, [ $client = m::mock(Client::class, [
'get' => new Response(200, [], file_get_contents(__DIR__.'../../../blobs/lastfm/album.xml')), 'get' => new Response(200, [], file_get_contents(__DIR__.'../../../blobs/lastfm/album.xml')),
]); ]);
$api = new LastfmService(null, null, $client); $api = new LastfmService($client);
$info = $api->getAlbumInfo($album->name, $album->artist->name); $info = $api->getAlbumInformation($album->name, $album->artist->name);
// Then I get the album's info // Then I get the album's info
$this->assertEquals([ $this->assertEquals([
@ -114,25 +101,22 @@ class LastfmTest extends TestCase
], ],
], $info); ], $info);
// And the response is cached
$this->assertNotNull(cache('fca889d13b3222589d7d020669cc5a38')); $this->assertNotNull(cache('fca889d13b3222589d7d020669cc5a38'));
} }
/** @test */ public function testGetAlbumInformationForNonExistentAlbum()
public function it_returns_false_if_album_info_is_not_found_on_lastfm()
{ {
// Given there's an album which doesn't exist on Last.fm /** @var Album $album */
$album = factory(Album::class)->create(); $album = factory(Album::class)->create();
// When I request the service for the album's info /** @var Client $client */
$client = m::mock(Client::class, [ $client = m::mock(Client::class, [
'get' => new Response(400, [], file_get_contents(__DIR__.'../../../blobs/lastfm/album-notfound.xml')), 'get' => new Response(400, [], file_get_contents(__DIR__.'../../../blobs/lastfm/album-notfound.xml')),
]); ]);
$api = new LastfmService(null, null, $client); $api = new LastfmService($client);
$result = $api->getAlbumInfo($album->name, $album->artist->name); $result = $api->getAlbumInformation($album->name, $album->artist->name);
// Then I receive a boolean false
$this->assertFalse($result); $this->assertFalse($result);
} }
} }

View file

@ -44,7 +44,7 @@ class MediaInformationServiceTest extends TestCase
$album = factory(Album::class)->create(); $album = factory(Album::class)->create();
$this->lastFmService $this->lastFmService
->shouldReceive('getAlbumInfo') ->shouldReceive('getAlbumInformation')
->once() ->once()
->with($album->name, $album->artist->name) ->with($album->name, $album->artist->name)
->andReturn(['foo' => 'bar']); ->andReturn(['foo' => 'bar']);
@ -68,7 +68,7 @@ class MediaInformationServiceTest extends TestCase
$artist = factory(Artist::class)->create(); $artist = factory(Artist::class)->create();
$this->lastFmService $this->lastFmService
->shouldReceive('getArtistInfo') ->shouldReceive('getArtistInformation')
->once() ->once()
->with($artist->name) ->with($artist->name)
->andReturn(['foo' => 'bar']); ->andReturn(['foo' => 'bar']);

View file

@ -2,13 +2,14 @@
namespace Tests\Integration\Services; namespace Tests\Integration\Services;
use App\Services\YouTube; use App\Services\YouTubeService;
use Exception;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Response;
use Mockery as m; use Mockery as m;
use Tests\TestCase; use Tests\TestCase;
class YouTubeTest extends TestCase class YouTubeServiceTest extends TestCase
{ {
protected function tearDown() protected function tearDown()
{ {
@ -17,24 +18,21 @@ class YouTubeTest extends TestCase
} }
/** /**
* @test * @throws Exception
*
* @throws \Exception
*/ */
public function videos_can_be_searched_from_youtube() public function testSearch()
{ {
$this->withoutEvents(); $this->withoutEvents();
/** @var Client $client */
$client = m::mock(Client::class, [ $client = m::mock(Client::class, [
'get' => new Response(200, [], file_get_contents(__DIR__.'../../../blobs/youtube/search.json')), 'get' => new Response(200, [], file_get_contents(__DIR__.'../../../blobs/youtube/search.json')),
]); ]);
$api = new YouTube(null, $client); $api = new YouTubeService($client);
$response = $api->search('Lorem Ipsum'); $response = $api->search('Lorem Ipsum');
$this->assertEquals('Slipknot - Snuff [OFFICIAL VIDEO]', $response->items[0]->snippet->title); $this->assertEquals('Slipknot - Snuff [OFFICIAL VIDEO]', $response->items[0]->snippet->title);
// Is it cached?
$this->assertNotNull(cache('1492972ec5c8e6b3a9323ba719655ddb')); $this->assertNotNull(cache('1492972ec5c8e6b3a9323ba719655ddb'));
} }
} }

View file

@ -4,6 +4,7 @@ namespace Tests;
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase; use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Mockery;
use Tests\Traits\InteractsWithIoc; use Tests\Traits\InteractsWithIoc;
abstract class TestCase extends BaseTestCase abstract class TestCase extends BaseTestCase
@ -15,4 +16,10 @@ abstract class TestCase extends BaseTestCase
parent::setUp(); parent::setUp();
$this->prepareForTests(); $this->prepareForTests();
} }
protected function tearDown()
{
Mockery::close();
parent::tearDown();
}
} }

View file

@ -0,0 +1,54 @@
<?php
namespace Tests\Unit\Services;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Mockery as m;
use Tests\TestCase;
use Tests\Unit\Stubs\ConcreteApiClient;
class ApiClientTest extends TestCase
{
use WithoutMiddleware;
public function testBuildUri()
{
/** @var Client $client */
$client = m::mock(Client::class);
$api = new ConcreteApiClient($client);
$this->assertEquals('http://foo.com/get/param?key=bar', $api->buildUrl('get/param'));
$this->assertEquals('http://foo.com/get/param?baz=moo&key=bar', $api->buildUrl('/get/param?baz=moo'));
$this->assertEquals('http://baz.com/?key=bar', $api->buildUrl('http://baz.com/'));
}
public function provideRequestData()
{
return [
['get', '{"foo":"bar"}'],
['post', '{"foo":"bar"}'],
['put', '{"foo":"bar"}'],
['delete', '{"foo":"bar"}'],
];
}
/**
* @dataProvider provideRequestData
*
* @param $method
* @param $responseBody
*/
public function testRequest($method, $responseBody)
{
/** @var Client $client */
$client = m::mock(Client::class, [
$method => new Response(200, [], $responseBody),
]);
$api = new ConcreteApiClient($client);
$this->assertSame((array) json_decode($responseBody), (array) $api->$method('/'));
}
}

View file

@ -3,23 +3,28 @@
namespace Tests\Unit\Services; namespace Tests\Unit\Services;
use App\Services\LastfmService; use App\Services\LastfmService;
use Mockery;
use Mockery\MockInterface;
use Tests\TestCase; use Tests\TestCase;
class LastfmTest extends TestCase class LastfmServiceTest extends TestCase
{ {
/** @test */ /** @test */
public function it_builds_lastfm_compatible_api_parameters() public function testBuildAuthCallParams()
{ {
// Given there are raw parameters /** @var LastfmService|MockInterface $lastfm */
$api = new LastfmService('key', 'secret'); $lastfm = Mockery::mock(LastfmService::class)->makePartial();
$lastfm->shouldReceive('getKey')->andReturn('key');
$lastfm->shouldReceive('getSecret')->andReturn('secret');
$params = [ $params = [
'qux' => '安', 'qux' => '安',
'bar' => 'baz', 'bar' => 'baz',
]; ];
// When I build Last.fm-compatible API parameters using the raw parameters // When I build Last.fm-compatible API parameters using the raw parameters
$builtParams = $api->buildAuthCallParams($params); $builtParams = $lastfm->buildAuthCallParams($params);
$builtParamsAsString = $api->buildAuthCallParams($params, true); $builtParamsAsString = $lastfm->buildAuthCallParams($params, true);
// Then I receive the Last.fm-compatible API parameters // Then I receive the Last.fm-compatible API parameters
$this->assertEquals([ $this->assertEquals([

View file

@ -1,51 +0,0 @@
<?php
namespace Tests\Unit\Services;
use App\Services\RESTfulService;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Mockery as m;
use Tests\TestCase;
class RESTfulAPIServiceTest extends TestCase
{
use WithoutMiddleware;
protected function tearDown()
{
m::close();
parent::tearDown();
}
/** @test */
public function a_uri_can_be_constructed()
{
/** @var Client $client */
$client = m::mock(Client::class);
$api = new RESTfulService('bar', null, 'http://foo.com', $client);
$this->assertEquals('http://foo.com/get/param?key=bar', $api->buildUrl('get/param'));
$this->assertEquals('http://foo.com/get/param?baz=moo&key=bar', $api->buildUrl('/get/param?baz=moo'));
$this->assertEquals('http://baz.com/?key=bar', $api->buildUrl('http://baz.com/'));
}
/** @test */
public function a_request_can_be_made()
{
/** @var Client $client */
$client = m::mock(Client::class, [
'get' => new Response(200, [], '{"foo":"bar"}'),
'post' => new Response(200, [], '{"foo":"bar"}'),
'delete' => new Response(200, [], '{"foo":"bar"}'),
'put' => new Response(200, [], '{"foo":"bar"}'),
]);
$api = new RESTfulService('foo', null, 'http://foo.com', $client);
$this->assertObjectHasAttribute('foo', $api->get('/'));
$this->assertObjectHasAttribute('foo', $api->post('/'));
$this->assertObjectHasAttribute('foo', $api->put('/'));
$this->assertObjectHasAttribute('foo', $api->delete('/'));
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Tests\Unit\Stubs;
use App\Services\ApiClient;
class ConcreteApiClient extends ApiClient
{
public function getKey()
{
return 'bar';
}
public function getSecret()
{
return null;
}
public function getEndpoint()
{
return 'http://foo.com';
}
}