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;
use App\Models\Song;
use App\Services\MediaInformationService;
use App\Services\YouTubeService;
use Illuminate\Http\JsonResponse;
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.
*
@ -20,7 +31,7 @@ class SongController extends Controller
'lyrics' => $song->lyrics,
'album_info' => $this->mediaInformationService->getAlbumInformation($song->album),
'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\Models\Song;
use App\Services\YouTubeService;
use Illuminate\Http\JsonResponse;
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).
*
@ -18,6 +26,6 @@ class YouTubeController extends Controller
*/
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)
{
/** @var Collection $songs */
$songs = Interaction::whereUserIdAndLike($user->id, true)
->with('song')
->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.
* This makes sure they are always sane.

View file

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

View file

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

View file

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

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;
use Exception;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Cache;
use Log;
class LastfmService extends RESTfulService
class LastfmService extends ApiClient implements ApiConsumerInterface
{
/**
* Specify the response format, since Last.fm only returns XML.
@ -23,23 +21,6 @@ class LastfmService extends RESTfulService
*/
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.
*
@ -47,7 +28,7 @@ class LastfmService extends RESTfulService
*/
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
*/
public function getArtistInfo($name)
public function getArtistInformation($name)
{
if (!$this->enabled()) {
return false;
@ -92,7 +73,7 @@ class LastfmService extends RESTfulService
return false;
}
return $this->buildArtistInfo($artist);
return $this->buildArtistInformation($artist);
} catch (Exception $e) {
Log::error($e);
@ -107,7 +88,7 @@ class LastfmService extends RESTfulService
*
* @return array
*/
private function buildArtistInfo(array $lastfmArtist)
private function buildArtistInformation(array $lastfmArtist)
{
return [
'url' => array_get($lastfmArtist, 'url'),
@ -127,7 +108,7 @@ class LastfmService extends RESTfulService
*
* @return array|false
*/
public function getAlbumInfo($name, $artistName)
public function getAlbumInformation($name, $artistName)
{
if (!$this->enabled()) {
return false;
@ -153,7 +134,7 @@ class LastfmService extends RESTfulService
return false;
}
return $this->buildAlbumInfo($album);
return $this->buildAlbumInformation($album);
} catch (Exception $e) {
Log::error($e);
@ -168,7 +149,7 @@ class LastfmService extends RESTfulService
*
* @return array
*/
private function buildAlbumInfo(array $lastfmAlbum)
private function buildAlbumInformation(array $lastfmAlbum)
{
return [
'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)))));
}
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;
}
$info = $this->lastfmService->getAlbumInfo($album->name, $album->artist->name);
event(new AlbumInformationFetched($album, $info));
$info = $this->lastfmService->getAlbumInformation($album->name, $album->artist->name);
// The album may have been updated.
$album->refresh();
$info['cover'] = $album->cover;
if ($info) {
event(new AlbumInformationFetched($album, $info));
// The album may have been updated.
$album->refresh();
$info['cover'] = $album->cover;
}
return $info;
}
@ -52,12 +55,15 @@ class MediaInformationService
return false;
}
$info = $this->lastfmService->getArtistInfo($artist->name);
event(new ArtistInformationFetched($artist, $info));
$info = $this->lastfmService->getArtistInformation($artist->name);
// The artist may have been updated.
$artist->refresh();
$info['image'] = $artist->image;
if ($info) {
event(new ArtistInformationFetched($artist, $info));
// The artist may have been updated.
$artist->refresh();
$info['image'] = $artist->image;
}
return $info;
}

View file

@ -4,26 +4,9 @@ namespace App\Services;
use App\Models\Song;
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.
*
@ -31,7 +14,7 @@ class YouTube extends RESTfulService
*/
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 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",
"doctrine/dbal": "^2.5",
"jackiedo/dotenv-editor": "^1.0",
"ext-exif": "*"
"ext-exif": "*",
"ext-json": "*",
"ext-SimpleXML": "*"
},
"require-dev": {
"fzaninotto/faker": "~1.4",

View file

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

View file

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

View file

@ -3,21 +3,37 @@
namespace Tests\Feature;
use App\Models\Song;
use App\Services\YouTubeService;
use Exception;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Mockery\MockInterface;
use YouTube;
class YouTubeTest extends TestCase
{
use WithoutMiddleware;
/** @test */
public function youtube_videos_related_to_a_song_can_be_searched()
/** @var YouTubeService|MockInterface */
private $youTubeService;
public function setUp()
{
parent::setUp();
$this->youTubeService = $this->mockIocDependency(YouTubeService::class);
}
/**
* @throws Exception
*/
public function testSearchYouTubeVideos()
{
$this->createSampleMediaSet();
$song = Song::first();
// We test on the facade here
YouTube::shouldReceive('searchVideosRelatedToSong')->once();
$this->youTubeService
->shouldReceive('searchVideosRelatedToSong')
->once();
$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
}
/** @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 */
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\Artist;
use App\Services\LastfmService;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response;
use Mockery as m;
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 */
$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, [
'get' => new Response(200, [], file_get_contents(__DIR__.'../../../blobs/lastfm/artist.xml')),
]);
$api = new LastfmService(null, null, $client);
$info = $api->getArtistInfo($artist->name);
$api = new LastfmService($client);
$info = $api->getArtistInformation($artist->name
);
// Then I see the info when the request is the successful
$this->assertEquals([
'url' => 'http://www.last.fm/music/Kamelot',
'image' => 'http://foo.bar/extralarge.jpg',
@ -51,46 +44,40 @@ class LastfmTest extends TestCase
$this->assertNotNull(cache('0aff3bc1259154f0e9db860026cda7a6'));
}
/** @test */
public function it_returns_false_if_artist_info_is_not_found_on_lastfm()
public function testGetArtistInformationForNonExistentArtist()
{
// Given an artist that doesn't exist on Last.fm
/** @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, [
'get' => new Response(400, [], file_get_contents(__DIR__.'../../../blobs/lastfm/artist-notfound.xml')),
]);
$api = new LastfmService(null, null, $client);
$result = $api->getArtistInfo($artist->name);
$api = new LastfmService($client);
$result = $api->getArtistInformation($artist->name);
// Then I receive boolean false
$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 */
$album = factory(Album::class)->create([
'artist_id' => factory(Artist::class)->create(['name' => 'bar'])->id,
'name' => 'foo',
]);
// When I request the service for the album's info
/** @var Client $client */
$client = m::mock(Client::class, [
'get' => new Response(200, [], file_get_contents(__DIR__.'../../../blobs/lastfm/album.xml')),
]);
$api = new LastfmService(null, null, $client);
$info = $api->getAlbumInfo($album->name, $album->artist->name);
$api = new LastfmService($client);
$info = $api->getAlbumInformation($album->name, $album->artist->name);
// Then I get the album's info
$this->assertEquals([
@ -114,25 +101,22 @@ class LastfmTest extends TestCase
],
], $info);
// And the response is cached
$this->assertNotNull(cache('fca889d13b3222589d7d020669cc5a38'));
}
/** @test */
public function it_returns_false_if_album_info_is_not_found_on_lastfm()
public function testGetAlbumInformationForNonExistentAlbum()
{
// Given there's an album which doesn't exist on Last.fm
/** @var Album $album */
$album = factory(Album::class)->create();
// When I request the service for the album's info
/** @var Client $client */
$client = m::mock(Client::class, [
'get' => new Response(400, [], file_get_contents(__DIR__.'../../../blobs/lastfm/album-notfound.xml')),
]);
$api = new LastfmService(null, null, $client);
$result = $api->getAlbumInfo($album->name, $album->artist->name);
$api = new LastfmService($client);
$result = $api->getAlbumInformation($album->name, $album->artist->name);
// Then I receive a boolean false
$this->assertFalse($result);
}
}

View file

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

View file

@ -2,13 +2,14 @@
namespace Tests\Integration\Services;
use App\Services\YouTube;
use App\Services\YouTubeService;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response;
use Mockery as m;
use Tests\TestCase;
class YouTubeTest extends TestCase
class YouTubeServiceTest extends TestCase
{
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();
/** @var Client $client */
$client = m::mock(Client::class, [
'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');
$this->assertEquals('Slipknot - Snuff [OFFICIAL VIDEO]', $response->items[0]->snippet->title);
// Is it cached?
$this->assertNotNull(cache('1492972ec5c8e6b3a9323ba719655ddb'));
}
}

View file

@ -4,6 +4,7 @@ namespace Tests;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Mockery;
use Tests\Traits\InteractsWithIoc;
abstract class TestCase extends BaseTestCase
@ -15,4 +16,10 @@ abstract class TestCase extends BaseTestCase
parent::setUp();
$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;
use App\Services\LastfmService;
use Mockery;
use Mockery\MockInterface;
use Tests\TestCase;
class LastfmTest extends TestCase
class LastfmServiceTest extends TestCase
{
/** @test */
public function it_builds_lastfm_compatible_api_parameters()
public function testBuildAuthCallParams()
{
// Given there are raw parameters
$api = new LastfmService('key', 'secret');
/** @var LastfmService|MockInterface $lastfm */
$lastfm = Mockery::mock(LastfmService::class)->makePartial();
$lastfm->shouldReceive('getKey')->andReturn('key');
$lastfm->shouldReceive('getSecret')->andReturn('secret');
$params = [
'qux' => '安',
'bar' => 'baz',
];
// When I build Last.fm-compatible API parameters using the raw parameters
$builtParams = $api->buildAuthCallParams($params);
$builtParamsAsString = $api->buildAuthCallParams($params, true);
$builtParams = $lastfm->buildAuthCallParams($params);
$builtParamsAsString = $lastfm->buildAuthCallParams($params, true);
// Then I receive the Last.fm-compatible API parameters
$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';
}
}