Fix basic errors detected by PHPStan

This commit is contained in:
Phan An 2018-08-31 20:47:15 +07:00
parent c4beca787b
commit d88dd79f15
45 changed files with 505 additions and 1403 deletions

View file

@ -59,7 +59,7 @@ class InitCommand extends Command
$this->comment(PHP_EOL.'🎆 Success! Koel can now be run from localhost with `php artisan serve`.');
if ($this->settingRepository->getMediaPath()) {
if (Setting::get('media_path')) {
$this->comment('You can also scan for media with `php artisan koel:sync`.');
}
@ -150,7 +150,7 @@ class InitCommand extends Command
private function maybeSetMediaPath(): void
{
if ($this->settingRepository->getMediaPath()) {
if (Setting::get('media_path')) {
return;
}

View file

@ -128,7 +128,7 @@ class SyncMediaCommand extends Command
private function ensureMediaPath(): void
{
if ($this->settingRepository->getMediaPath()) {
if (Setting::get('media_path')) {
return;
}

View file

@ -7,7 +7,9 @@ use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Foundation\Validation\ValidationException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@ -30,9 +32,9 @@ class Handler extends ExceptionHandler
*
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
*
* @param \Exception $e
* @throws Exception
*/
public function report(Exception $e)
public function report(Exception $e): void
{
parent::report($e);
}
@ -40,12 +42,9 @@ class Handler extends ExceptionHandler
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $e
*
* @return \Illuminate\Http\Response
* @param Request $request
*/
public function render($request, Exception $e)
public function render($request, Exception $e): Response
{
if ($e instanceof ModelNotFoundException) {
$e = new NotFoundHttpException($e->getMessage(), $e);
@ -57,12 +56,9 @@ class Handler extends ExceptionHandler
/**
* Convert an authentication exception into an unauthenticated response.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Auth\AuthenticationException $exception
*
* @return \Illuminate\Http\Response
* @param Request $request
*/
protected function unauthenticated($request, AuthenticationException $exception)
protected function unauthenticated($request, AuthenticationException $exception): Response
{
if ($request->expectsJson()) {
return response()->json(['error' => 'Unauthenticated.'], 401);

View file

@ -5,11 +5,20 @@ namespace App\Http\Controllers\API;
use App\Http\Requests\API\UserLoginRequest;
use Exception;
use Illuminate\Http\JsonResponse;
use JWTAuth;
use Log;
use Illuminate\Log\Logger;
use Tymon\JWTAuth\JWTAuth;
class AuthController extends Controller
{
private $auth;
private $logger;
public function __construct(JWTAuth $auth, Logger $logger)
{
$this->auth = $auth;
$this->logger = $logger;
}
/**
* Log a user in.
*
@ -17,7 +26,7 @@ class AuthController extends Controller
*/
public function login(UserLoginRequest $request)
{
$token = JWTAuth::attempt($request->only('email', 'password'));
$token = $this->auth->attempt($request->only('email', 'password'));
abort_unless($token, 401, 'Invalid credentials');
return response()->json(compact('token'));
@ -30,11 +39,11 @@ class AuthController extends Controller
*/
public function logout()
{
if ($token = JWTAuth::getToken()) {
if ($token = $this->auth->getToken()) {
try {
JWTAuth::invalidate($token);
$this->auth->invalidate($token);
} catch (Exception $e) {
Log::error($e);
$this->logger->error($e);
}
}

View file

@ -4,10 +4,17 @@ namespace App\Observers;
use App\Models\Album;
use Exception;
use Log;
use Illuminate\Log\Logger;
class AlbumObserver
{
private $logger;
public function __construct(Logger $logger)
{
$this->logger = $logger;
}
public function deleted(Album $album): void
{
if (!$album->has_cover) {
@ -17,7 +24,7 @@ class AlbumObserver
try {
unlink($album->cover_path);
} catch (Exception $e) {
Log::error($e);
$this->logger->error($e);
}
}
}

View file

@ -3,40 +3,36 @@
namespace App\Providers;
use Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider;
use DB;
use Illuminate\Database\DatabaseManager;
use Illuminate\Database\Schema\Builder;
use Illuminate\Database\SQLiteConnection;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Factory as Validator;
use Laravel\Tinker\TinkerServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
public function boot(Builder $schema, DatabaseManager $db, Validator $validator): void
{
// Fix utf8mb4-related error starting from Laravel 5.4
Schema::defaultStringLength(191);
$schema->defaultStringLength(191);
// Enable on delete cascade for sqlite connections
if (DB::connection() instanceof SQLiteConnection) {
DB::statement(DB::raw('PRAGMA foreign_keys = ON'));
if ($db->connection() instanceof SQLiteConnection) {
$db->statement($db->raw('PRAGMA foreign_keys = ON'));
}
// Add some custom validation rules
Validator::extend('path.valid', function ($attribute, $value, $parameters, $validator) {
$validator->extend('path.valid', static function ($attribute, $value): bool {
return is_dir($value) && is_readable($value);
});
}
/**
* Register any application services.
*
* @return void
*/
public function register(): void
{

View file

@ -2,7 +2,6 @@
namespace App\Providers;
use AWS;
use Aws\AwsClientInterface;
use Aws\S3\S3ClientInterface;
use Illuminate\Support\ServiceProvider;
@ -18,7 +17,7 @@ class ObjectStorageServiceProvider extends ServiceProvider
return null;
}
return AWS::createClient('s3');
return app('aws')->createClient('s3');
});
}
}

View file

@ -4,8 +4,9 @@ namespace App\Services;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Log\Logger;
use Illuminate\Contracts\Cache\Repository as Cache;
use InvalidArgumentException;
use Log;
use SimpleXMLElement;
/**
@ -19,11 +20,9 @@ use SimpleXMLElement;
abstract class ApiClient
{
protected $responseFormat = 'json';
/**
* The GuzzleHttp client to talk to the API.
*/
protected $client;
protected $cache;
protected $logger;
/**
* The query parameter name for the key.
@ -34,9 +33,11 @@ abstract class ApiClient
*/
protected $keyParam = 'key';
public function __construct(Client $client)
public function __construct(Client $client, Cache $cache, Logger $logger)
{
$this->client = $client;
$this->cache = $cache;
$this->logger = $logger;
}
/**
@ -68,9 +69,9 @@ abstract class ApiClient
return $body;
} catch (ClientException $e) {
Log::error($e);
$this->logger->error($e);
return;
return null;
}
}

View file

@ -5,8 +5,6 @@ namespace App\Services;
interface ApiConsumerInterface
{
public function getEndpoint(): ?string;
public function getKey(): ?string;
public function getSecret(): ?string;
}

View file

@ -3,9 +3,6 @@
namespace App\Services;
use Exception;
use GuzzleHttp\Client;
use Illuminate\Contracts\Cache\Repository as Cache;
use Log;
class LastfmService extends ApiClient implements ApiConsumerInterface
{
@ -22,16 +19,6 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
* @var string
*/
protected $keyParam = 'api_key';
/**
* @var Cache
*/
private $cache;
public function __construct(Client $client, Cache $cache)
{
parent::__construct($client);
$this->cache = $cache;
}
/**
* Determine if our application is using Last.fm.
@ -82,7 +69,7 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
return $this->buildArtistInformation($artist);
});
} catch (Exception $e) {
Log::error($e);
$this->logger->error($e);
return null;
}
@ -141,7 +128,7 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
return $this->buildAlbumInformation($album);
});
} catch (Exception $e) {
Log::error($e);
$this->logger->error($e);
return null;
}
@ -190,7 +177,7 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
try {
return (string) $this->get("/?$query", [], false)->session->key;
} catch (Exception $e) {
Log::error($e);
$this->logger->error($e);
return null;
}
@ -218,7 +205,7 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
try {
$this->post('/', $this->buildAuthCallParams($params), false);
} catch (Exception $e) {
Log::error($e);
$this->logger->error($e);
}
}
@ -238,7 +225,7 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
try {
$this->post('/', $this->buildAuthCallParams($params), false);
} catch (Exception $e) {
Log::error($e);
$this->logger->error($e);
}
}
@ -263,7 +250,7 @@ class LastfmService extends ApiClient implements ApiConsumerInterface
try {
$this->post('/', $this->buildAuthCallParams($params), false);
} catch (Exception $e) {
Log::error($e);
$this->logger->error($e);
}
}

View file

@ -5,10 +5,17 @@ namespace App\Services;
use App\Models\Album;
use App\Models\Artist;
use Exception;
use Log;
use Illuminate\Log\Logger;
class MediaMetadataService
{
private $logger;
public function __construct(Logger $logger)
{
$this->logger = $logger;
}
/**
* Download a copy of the album cover.
*/
@ -47,7 +54,7 @@ class MediaMetadataService
$album->update(['cover' => basename($destination)]);
} catch (Exception $e) {
Log::error($e);
$this->logger->error($e);
}
}
@ -78,7 +85,7 @@ class MediaMetadataService
$artist->update(['image' => basename($destination)]);
} catch (Exception $e) {
Log::error($e);
$this->logger->error($e);
}
}

View file

@ -7,13 +7,14 @@ use App\Events\LibraryChanged;
use App\Libraries\WatchRecord\WatchRecordInterface;
use App\Models\Album;
use App\Models\Artist;
use App\Models\Setting;
use App\Models\Song;
use App\Repositories\AlbumRepository;
use App\Repositories\ArtistRepository;
use App\Repositories\SettingRepository;
use App\Repositories\SongRepository;
use Exception;
use Log;
use Illuminate\Log\Logger;
use SplFileInfo;
use Symfony\Component\Finder\Finder;
@ -46,6 +47,7 @@ class MediaSyncService
private $artistRepository;
private $albumRepository;
private $settingRepository;
private $logger;
public function __construct(
MediaMetadataService $mediaMetadataService,
@ -55,7 +57,8 @@ class MediaSyncService
SettingRepository $settingRepository,
HelperService $helperService,
FileSynchronizer $fileSynchronizer,
Finder $finder
Finder $finder,
Logger $logger
) {
$this->mediaMetadataService = $mediaMetadataService;
$this->songRepository = $songRepository;
@ -65,6 +68,7 @@ class MediaSyncService
$this->artistRepository = $artistRepository;
$this->albumRepository = $albumRepository;
$this->settingRepository = $settingRepository;
$this->logger = $logger;
}
/**
@ -100,7 +104,7 @@ class MediaSyncService
'unmodified' => [],
];
$songPaths = $this->gatherFiles($mediaPath ?: $this->settingRepository->getMediaPath());
$songPaths = $this->gatherFiles($mediaPath ?: Setting::get('media_path'));
$syncCommand && $syncCommand->createProgressBar(count($songPaths));
foreach ($songPaths as $path) {
@ -162,7 +166,7 @@ class MediaSyncService
*/
public function syncByWatchRecord(WatchRecordInterface $record): void
{
Log::info("New watch record received: '$record'");
$this->logger->info("New watch record received: '$record'");
$record->isFile() ? $this->syncFileRecord($record) : $this->syncDirectoryRecord($record);
}
@ -174,7 +178,7 @@ class MediaSyncService
private function syncFileRecord(WatchRecordInterface $record): void
{
$path = $record->getPath();
Log::info("'$path' is a file.");
$this->logger->info("'$path' is a file.");
// If the file has been deleted...
if ($record->isDeleted()) {
@ -192,7 +196,7 @@ class MediaSyncService
private function syncDirectoryRecord(WatchRecordInterface $record): void
{
$path = $record->getPath();
Log::info("'$path' is a directory.");
$this->logger->info("'$path' is a directory.");
if ($record->isDeleted()) {
$this->handleDeletedDirectoryRecord($path);
@ -253,11 +257,11 @@ class MediaSyncService
{
if ($song = $this->songRepository->getOneByPath($path)) {
$song->delete();
Log::info("$path deleted.");
$this->logger->info("$path deleted.");
event(new LibraryChanged());
} else {
Log::info("$path doesn't exist in our database--skipping.");
$this->logger->info("$path doesn't exist in our database--skipping.");
}
}
@ -266,9 +270,9 @@ class MediaSyncService
$result = $this->fileSynchronizer->setFile($path)->sync($this->tags);
if ($result === FileSynchronizer::SYNC_RESULT_SUCCESS) {
Log::info("Synchronized $path");
$this->logger->info("Synchronized $path");
} else {
Log::info("Failed to synchronized $path. Maybe an invalid file?");
$this->logger->info("Failed to synchronized $path. Maybe an invalid file?");
}
event(new LibraryChanged());
@ -277,11 +281,11 @@ class MediaSyncService
private function handleDeletedDirectoryRecord(string $path): void
{
if ($count = Song::inDirectory($path)->delete()) {
Log::info("Deleted $count song(s) under $path");
$this->logger->info("Deleted $count song(s) under $path");
event(new LibraryChanged());
} else {
Log::info("$path is empty--no action needed.");
$this->logger->info("$path is empty--no action needed.");
}
}
@ -291,7 +295,7 @@ class MediaSyncService
$this->fileSynchronizer->setFile($file)->sync($this->tags);
}
Log::info("Synced all song(s) under $path");
$this->logger->info("Synced all song(s) under $path");
event(new LibraryChanged());
}

View file

@ -3,7 +3,6 @@
namespace App\Services;
use App\Models\Song;
use Cache;
class YouTubeService extends ApiClient implements ApiConsumerInterface
{
@ -44,7 +43,7 @@ class YouTubeService extends ApiClient implements ApiConsumerInterface
public function search(string $q, string $pageToken = '', int $perPage = 10)
{
if (!$this->enabled()) {
return;
return null;
}
$uri = sprintf('search?part=snippet&type=video&maxResults=%s&pageToken=%s&q=%s',
@ -53,7 +52,7 @@ class YouTubeService extends ApiClient implements ApiConsumerInterface
urlencode($q)
);
return Cache::remember(md5("youtube_$uri"), 60 * 24 * 7, function () use ($uri) {
return $this->cache->remember(md5("youtube_$uri"), 60 * 24 * 7, function () use ($uri) {
return $this->get($uri);
});
}

View file

@ -3,20 +3,9 @@
namespace App\Services;
use Exception;
use GuzzleHttp\Client;
use Illuminate\Contracts\Cache\Repository as Cache;
use Log;
class iTunesService extends ApiClient implements ApiConsumerInterface
{
private $cache;
public function __construct(Client $client, Cache $cache)
{
parent::__construct($client);
$this->cache = $cache;
}
/**
* Determines whether to use iTunes services.
*/
@ -62,7 +51,7 @@ class iTunesService extends ApiClient implements ApiConsumerInterface
}
);
} catch (Exception $e) {
Log::error($e);
$this->logger->error($e);
return null;
}

View file

@ -2,8 +2,8 @@
namespace App\Traits;
use DB;
use Exception;
use Illuminate\Database\DatabaseManager;
/**
* With reference to GitHub issue #463.
@ -56,16 +56,18 @@ trait SupportsDeleteWhereIDsNotIn
*/
public static function deleteByChunk(array $ids, string $key = 'id', int $chunkSize = 65535): void
{
DB::beginTransaction();
/** @var DatabaseManager $db */
$db = app(DatabaseManager::class);
$db->beginTransaction();
try {
foreach (array_chunk($ids, $chunkSize) as $chunk) {
static::whereIn($key, $chunk)->delete();
}
DB::commit();
$db->commit();
} catch (Exception $e) {
DB::rollBack();
$db->rollBack();
}
}
}

View file

@ -35,7 +35,8 @@
"laravel/tinker": "^1.0",
"laravel/browser-kit-testing": "^2.0",
"mikey179/vfsStream": "^1.6",
"phpstan/phpstan": "^0.10.3"
"phpstan/phpstan": "^0.10.3",
"php-mock/php-mock-mockery": "^1.3"
},
"autoload": {
"classmap": [
@ -48,8 +49,7 @@
},
"autoload-dev": {
"classmap": [
"tests/TestCase.php",
"tests/e2e"
"tests/TestCase.php"
]
},
"scripts": {

170
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "85f650d77535301c67a4dc4a0fa378ee",
"content-hash": "f40663bd1d80d0151691dbcb57b9fbb8",
"packages": [
{
"name": "asm89/stack-cors",
@ -4857,6 +4857,174 @@
"description": "Library for handling version information and constraints",
"time": "2018-07-08T19:19:57+00:00"
},
{
"name": "php-mock/php-mock",
"version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-mock/php-mock.git",
"reference": "22d297231118e6fd5b9db087fbe1ef866c2b95d2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-mock/php-mock/zipball/22d297231118e6fd5b9db087fbe1ef866c2b95d2",
"reference": "22d297231118e6fd5b9db087fbe1ef866c2b95d2",
"shasum": ""
},
"require": {
"php": ">=5.6",
"phpunit/php-text-template": "^1"
},
"replace": {
"malkusch/php-mock": "*"
},
"require-dev": {
"phpunit/phpunit": "^5.7"
},
"suggest": {
"php-mock/php-mock-phpunit": "Allows integration into PHPUnit testcase with the trait PHPMock."
},
"type": "library",
"autoload": {
"psr-4": {
"phpmock\\": [
"classes/",
"tests/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"WTFPL"
],
"authors": [
{
"name": "Markus Malkusch",
"email": "markus@malkusch.de",
"homepage": "http://markus.malkusch.de",
"role": "Developer"
}
],
"description": "PHP-Mock can mock built-in PHP functions (e.g. time()). PHP-Mock relies on PHP's namespace fallback policy. No further extension is needed.",
"homepage": "https://github.com/php-mock/php-mock",
"keywords": [
"BDD",
"TDD",
"function",
"mock",
"stub",
"test",
"test double"
],
"time": "2017-02-17T20:52:52+00:00"
},
{
"name": "php-mock/php-mock-integration",
"version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-mock/php-mock-integration.git",
"reference": "5a0d7d7755f823bc2a230cfa45058b40f9013bc4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-mock/php-mock-integration/zipball/5a0d7d7755f823bc2a230cfa45058b40f9013bc4",
"reference": "5a0d7d7755f823bc2a230cfa45058b40f9013bc4",
"shasum": ""
},
"require": {
"php": ">=5.6",
"php-mock/php-mock": "^2",
"phpunit/php-text-template": "^1"
},
"require-dev": {
"phpunit/phpunit": "^4|^5"
},
"type": "library",
"autoload": {
"psr-4": {
"phpmock\\integration\\": "classes/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"WTFPL"
],
"authors": [
{
"name": "Markus Malkusch",
"email": "markus@malkusch.de",
"homepage": "http://markus.malkusch.de",
"role": "Developer"
}
],
"description": "Integration package for PHP-Mock",
"homepage": "https://github.com/php-mock/php-mock-integration",
"keywords": [
"BDD",
"TDD",
"function",
"mock",
"stub",
"test",
"test double"
],
"time": "2017-02-17T21:31:34+00:00"
},
{
"name": "php-mock/php-mock-mockery",
"version": "1.3.0",
"source": {
"type": "git",
"url": "https://github.com/php-mock/php-mock-mockery.git",
"reference": "d6d3df9d9232f1623f1ca3cfdaacd53415593825"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-mock/php-mock-mockery/zipball/d6d3df9d9232f1623f1ca3cfdaacd53415593825",
"reference": "d6d3df9d9232f1623f1ca3cfdaacd53415593825",
"shasum": ""
},
"require": {
"mockery/mockery": "^1",
"php": ">=5.6",
"php-mock/php-mock-integration": "^2"
},
"require-dev": {
"phpunit/phpunit": "^4|^5"
},
"type": "library",
"autoload": {
"psr-4": {
"phpmock\\mockery\\": "classes/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"WTFPL"
],
"authors": [
{
"name": "Markus Malkusch",
"email": "markus@malkusch.de",
"homepage": "http://markus.malkusch.de",
"role": "Developer"
}
],
"description": "Mock built-in PHP functions (e.g. time()) with Mockery. This package relies on PHP's namespace fallback policy. No further extension is needed.",
"homepage": "https://github.com/php-mock/php-mock-mockery",
"keywords": [
"BDD",
"TDD",
"function",
"mock",
"mockery",
"stub",
"test",
"test double"
],
"time": "2018-03-27T07:00:25+00:00"
},
{
"name": "phpdocumentor/reflection-common",
"version": "1.0.1",

View file

@ -3,8 +3,8 @@
namespace Tests;
use App\Models\User;
use Artisan;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Contracts\Console\Kernel as Artisan;
use Illuminate\Foundation\Application;
trait CreatesApplication
@ -12,6 +12,9 @@ trait CreatesApplication
protected $coverPath;
protected $mediaPath = __DIR__.'/songs';
/** @var Artisan */
private $artisan;
/**
* The base URL to use while testing the application.
*
@ -28,7 +31,8 @@ trait CreatesApplication
{
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
$this->artisan = $app->make(Artisan::class);
$this->artisan->bootstrap();
$this->coverPath = $app->basePath().'/public/img/covers';
@ -37,10 +41,10 @@ trait CreatesApplication
private function prepareForTests()
{
Artisan::call('migrate');
$this->artisan->call('migrate');
if (!User::all()->count()) {
Artisan::call('db:seed');
$this->artisan->call('db:seed');
}
if (!file_exists($this->coverPath)) {

View file

@ -7,22 +7,24 @@ use App\Services\LastfmService;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Log\Logger;
use Mockery;
use Tymon\JWTAuth\JWTAuth;
class LastfmTest extends TestCase
{
public function testGetSessionKey()
public function testGetSessionKey(): void
{
/** @var Client $client */
$client = Mockery::mock(Client::class, [
'get' => new Response(200, [], file_get_contents(__DIR__.'../../blobs/lastfm/session-key.xml')),
]);
self::assertEquals('foo', (new LastfmService($client, app(Cache::class)))->getSessionKey('bar'));
$service = new LastfmService($client, app(Cache::class), app(Logger::class));
self::assertEquals('foo', $service->getSessionKey('bar'));
}
public function testSetSessionKey()
public function testSetSessionKey(): void
{
$user = factory(User::class)->create();
$this->postAsUser('api/lastfm/session-key', ['key' => 'foo'], $user)
@ -32,7 +34,7 @@ class LastfmTest extends TestCase
self::assertEquals('foo', $user->lastfm_session_key);
}
public function testConnectToLastfm()
public function testConnectToLastfm(): void
{
$this->mockIocDependency(JWTAuth::class, [
'parseToken' => null,
@ -43,7 +45,7 @@ class LastfmTest extends TestCase
->assertRedirectedTo('https://www.last.fm/api/auth/?api_key=foo&cb=http%3A%2F%2Flocalhost%2Fapi%2Flastfm%2Fcallback%3Fjwt-token%3Dfoo');
}
public function testRetrieveAndStoreSessionKey()
public function testRetrieveAndStoreSessionKey(): void
{
$lastfm = $this->mockIocDependency(LastfmService::class);
$lastfm->shouldReceive('getSessionKey')
@ -59,7 +61,7 @@ class LastfmTest extends TestCase
$this->assertEquals('bar', $user->lastfm_session_key);
}
public function testDisconnectUser()
public function testDisconnectUser(): void
{
/** @var User $user */
$user = factory(User::class)->create(['preferences' => ['lastfm_session_key' => 'bar']]);

View file

@ -8,19 +8,25 @@ use App\Models\Song;
use App\Models\User;
use Exception;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use JWTAuth;
use Laravel\BrowserKitTesting\TestCase as BaseTestCase;
use Mockery;
use Tests\CreatesApplication;
use Tests\Traits\InteractsWithIoc;
use Tymon\JWTAuth\JWTAuth;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication, DatabaseTransactions, InteractsWithIoc;
/** @var JWTAuth */
private $auth;
public function setUp()
{
parent::setUp();
$this->auth = app(JWTAuth::class);
$this->prepareForTests();
}
@ -29,7 +35,7 @@ abstract class TestCase extends BaseTestCase
*
* @throws Exception
*/
protected function createSampleMediaSet()
protected function createSampleMediaSet(): void
{
$artist = factory(Artist::class)->create();
@ -47,50 +53,39 @@ abstract class TestCase extends BaseTestCase
}
}
protected function getAsUser($url, $user = null)
protected function getAsUser($url, $user = null): self
{
if (!$user) {
$user = factory(User::class)->create();
}
return $this->get($url, [
'Authorization' => 'Bearer '.JWTAuth::fromUser($user),
'Authorization' => 'Bearer ' . $this->generateJwtToken($user),
]);
}
protected function deleteAsUser($url, $data = [], $user = null)
protected function deleteAsUser($url, $data = [], $user = null): self
{
if (!$user) {
$user = factory(User::class)->create();
}
return $this->delete($url, $data, [
'Authorization' => 'Bearer '.JWTAuth::fromUser($user),
'Authorization' => 'Bearer ' . $this->generateJwtToken($user),
]);
}
protected function postAsUser($url, $data, $user = null)
protected function postAsUser($url, $data, $user = null): self
{
if (!$user) {
$user = factory(User::class)->create();
}
return $this->post($url, $data, [
'Authorization' => 'Bearer '.JWTAuth::fromUser($user),
'Authorization' => 'Bearer ' . $this->generateJwtToken($user),
]);
}
protected function putAsUser($url, $data, $user = null)
protected function putAsUser($url, $data, $user = null): self
{
if (!$user) {
$user = factory(User::class)->create();
}
return $this->put($url, $data, [
'Authorization' => 'Bearer '.JWTAuth::fromUser($user),
'Authorization' => 'Bearer ' . $this->generateJwtToken($user),
]);
}
private function generateJwtToken(?User $user): string
{
return $this->auth->fromUser($user ?: factory(User::class)->create());
}
protected function tearDown()
{
if ($container = Mockery::getContainer()) {

View file

@ -1,96 +1,91 @@
<?php
namespace App\Services\Streamers {
function file_exists()
namespace Tests\Integration\Factories;
use App\Factories\StreamerFactory;
use App\Models\Song;
use App\Services\Streamers\PHPStreamer;
use App\Services\Streamers\S3Streamer;
use App\Services\Streamers\TranscodingStreamer;
use App\Services\Streamers\XAccelRedirectStreamer;
use App\Services\Streamers\XSendFileStreamer;
use App\Services\TranscodingService;
use Tests\TestCase;
use phpmock\mockery\PHPMockery;
class StreamerFactoryTest extends TestCase
{
/** @var StreamerFactory */
private $streamerFactory;
public function setUp()
{
return true;
}
}
namespace Tests\Integration\Factories {
use App\Factories\StreamerFactory;
use App\Models\Song;
use App\Services\Streamers\PHPStreamer;
use App\Services\Streamers\S3Streamer;
use App\Services\Streamers\TranscodingStreamer;
use App\Services\Streamers\XAccelRedirectStreamer;
use App\Services\Streamers\XSendFileStreamer;
use App\Services\TranscodingService;
use Tests\TestCase;
class StreamerFactoryTest extends TestCase
{
/** @var StreamerFactory */
private $streamerFactory;
public function setUp()
{
parent::setUp();
$this->streamerFactory = app(StreamerFactory::class);
}
public function testCreateS3Streamer()
{
/** @var Song $song */
$song = factory(Song::class)->make(['path' => 's3://bucket/foo.mp3']);
self::assertInstanceOf(S3Streamer::class, $this->streamerFactory->createStreamer($song));
}
public function testCreateTranscodingStreamerIfSupported()
{
$this->mockIocDependency(TranscodingService::class, [
'songShouldBeTranscoded' => true,
]);
/** @var StreamerFactory $streamerFactory */
$streamerFactory = app(StreamerFactory::class);
/** @var Song $song */
$song = factory(Song::class)->make();
self::assertInstanceOf(TranscodingStreamer::class, $streamerFactory->createStreamer($song, null));
}
public function testCreateTranscodingStreamerIfForced()
{
$this->mockIocDependency(TranscodingService::class, [
'songShouldBeTranscoded' => false,
]);
/** @var StreamerFactory $streamerFactory */
$streamerFactory = app(StreamerFactory::class);
$song = factory(Song::class)->make();
self::assertInstanceOf(TranscodingStreamer::class, $streamerFactory->createStreamer($song, true));
}
public function provideStreamingConfigData()
{
return [
[null, PHPStreamer::class],
['x-sendfile', XSendFileStreamer::class],
['x-accel-redirect', XAccelRedirectStreamer::class],
];
}
/**
* @dataProvider provideStreamingConfigData
*
* @param string|null $config
* @param string $expectedClass
*/
public function testCreatePHPStreamer($config, $expectedClass)
{
$this->mockIocDependency(TranscodingService::class, [
'songShouldBeTranscoded' => false,
]);
config(['koel.streaming.method' => $config]);
/** @var StreamerFactory $streamerFactory */
$streamerFactory = app(StreamerFactory::class);
$song = factory(Song::class)->make();
self::assertInstanceOf($expectedClass, $streamerFactory->createStreamer($song));
}
parent::setUp();
$this->streamerFactory = app(StreamerFactory::class);
PHPMockery::mock('App\Services\Streamers', 'file_exists')->andReturn(true);
}
public function testCreateS3Streamer(): void
{
/** @var Song $song */
$song = factory(Song::class)->make(['path' => 's3://bucket/foo.mp3']);
self::assertInstanceOf(S3Streamer::class, $this->streamerFactory->createStreamer($song));
}
public function testCreateTranscodingStreamerIfSupported(): void
{
$this->mockIocDependency(TranscodingService::class, [
'songShouldBeTranscoded' => true,
]);
/** @var StreamerFactory $streamerFactory */
$streamerFactory = app(StreamerFactory::class);
/** @var Song $song */
$song = factory(Song::class)->make();
self::assertInstanceOf(TranscodingStreamer::class, $streamerFactory->createStreamer($song, null));
}
public function testCreateTranscodingStreamerIfForced(): void
{
$this->mockIocDependency(TranscodingService::class, [
'songShouldBeTranscoded' => false,
]);
/** @var StreamerFactory $streamerFactory */
$streamerFactory = app(StreamerFactory::class);
$song = factory(Song::class)->make();
self::assertInstanceOf(TranscodingStreamer::class, $streamerFactory->createStreamer($song, true));
}
public function provideStreamingConfigData(): array
{
return [
[null, PHPStreamer::class],
['x-sendfile', XSendFileStreamer::class],
['x-accel-redirect', XAccelRedirectStreamer::class],
];
}
/**
* @dataProvider provideStreamingConfigData
*
* @param string|null $config
* @param string $expectedClass
*/
public function testCreatePHPStreamer($config, $expectedClass): void
{
$this->mockIocDependency(TranscodingService::class, [
'songShouldBeTranscoded' => false,
]);
config(['koel.streaming.method' => $config]);
/** @var StreamerFactory $streamerFactory */
$streamerFactory = app(StreamerFactory::class);
$song = factory(Song::class)->make();
self::assertInstanceOf($expectedClass, $streamerFactory->createStreamer($song));
}
}

View file

@ -1,48 +1,37 @@
<?php
namespace App\Listeners {
if (!function_exists(__NAMESPACE__.'/init_get')) {
function ini_get($key)
{
if ($key === 'allow_url_fopen') {
return true;
}
namespace Tests\Integration\Listeners;
return \ini_get($key);
}
}
}
use App\Events\AlbumInformationFetched;
use App\Models\Album;
use App\Services\MediaMetadataService;
use Mockery\MockInterface;
use phpmock\mockery\PHPMockery;
use Tests\TestCase;
namespace Tests\Integration\Listeners {
use App\Events\AlbumInformationFetched;
use App\Models\Album;
use App\Services\MediaMetadataService;
use Mockery\MockInterface;
use Tests\TestCase;
class DownloadAlbumCoverTest extends TestCase
{
/** @var MediaMetadataService|MockInterface */
private $mediaMetaDataService;
class DownloadAlbumCoverTest extends TestCase
public function setUp()
{
/** @var MediaMetadataService|MockInterface */
private $mediaMetaDataService;
parent::setUp();
public function setUp()
{
parent::setUp();
$this->mediaMetaDataService = $this->mockIocDependency(MediaMetadataService::class);
PHPMockery::mock('App\Listeners', 'ini_get')->andReturn(true);
}
$this->mediaMetaDataService = $this->mockIocDependency(MediaMetadataService::class);
}
public function testHandle()
{
$album = factory(Album::class)->make(['cover' => null]);
$event = new AlbumInformationFetched($album, ['image' => 'https://foo.bar/baz.jpg']);
public function testHandle()
{
$album = factory(Album::class)->make(['cover' => null]);
$event = new AlbumInformationFetched($album, ['image' => 'https://foo.bar/baz.jpg']);
$this->mediaMetaDataService
->shouldReceive('downloadAlbumCover')
->once()
->with($album, 'https://foo.bar/baz.jpg');
$this->mediaMetaDataService
->shouldReceive('downloadAlbumCover')
->once()
->with($album, 'https://foo.bar/baz.jpg');
event($event);
}
event($event);
}
}

View file

@ -1,48 +1,37 @@
<?php
namespace App\Listeners {
if (function_exists(__NAMESPACE__.'/init_get')) {
function ini_get($key)
{
if ($key === 'allow_url_fopen') {
return true;
}
namespace Tests\Integration\Listeners;
return \ini_get($key);
}
}
}
use App\Events\ArtistInformationFetched;
use App\Models\Artist;
use App\Services\MediaMetadataService;
use Mockery\MockInterface;
use phpmock\mockery\PHPMockery;
use Tests\TestCase;
namespace Tests\Integration\Listeners {
use App\Events\ArtistInformationFetched;
use App\Models\Artist;
use App\Services\MediaMetadataService;
use Mockery\MockInterface;
use Tests\TestCase;
class DownloadArtistImageTest extends TestCase
{
/** @var MediaMetadataService|MockInterface */
private $mediaMetaDataService;
class DownloadArtistImageTest extends TestCase
public function setUp()
{
/** @var MediaMetadataService|MockInterface */
private $mediaMetaDataService;
parent::setUp();
public function setUp()
{
parent::setUp();
$this->mediaMetaDataService = $this->mockIocDependency(MediaMetadataService::class);
PHPMockery::mock('App\Listeners', 'ini_get')->andReturn(true);
}
$this->mediaMetaDataService = $this->mockIocDependency(MediaMetadataService::class);
}
public function testHandle()
{
$artist = factory(Artist::class)->make(['image' => null]);
$event = new ArtistInformationFetched($artist, ['image' => 'https://foo.bar/baz.jpg']);
public function testHandle()
{
$artist = factory(Artist::class)->make(['image' => null]);
$event = new ArtistInformationFetched($artist, ['image' => 'https://foo.bar/baz.jpg']);
$this->mediaMetaDataService
->shouldReceive('downloadArtistImage')
->once()
->with($artist, 'https://foo.bar/baz.jpg');
$this->mediaMetaDataService
->shouldReceive('downloadArtistImage')
->once()
->with($artist, 'https://foo.bar/baz.jpg');
event($event);
}
event($event);
}
}

View file

@ -9,20 +9,16 @@ use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Log\Logger;
use Mockery;
use Tests\TestCase;
class LastfmServiceTest extends TestCase
{
public function setUp()
{
parent::setUp();
}
/**
* @throws Exception
*/
public function testGetArtistInformation()
public function testGetArtistInformation(): void
{
/** @var Artist $artist */
$artist = factory(Artist::class)->make(['name' => 'foo']);
@ -32,7 +28,7 @@ class LastfmServiceTest extends TestCase
'get' => new Response(200, [], file_get_contents(__DIR__.'../../../blobs/lastfm/artist.xml')),
]);
$api = new LastfmService($client, app(Cache::class));
$api = new LastfmService($client, app(Cache::class), app(Logger::class));
$info = $api->getArtistInformation($artist->name);
$this->assertEquals([
@ -44,11 +40,10 @@ class LastfmServiceTest extends TestCase
],
], $info);
// And the response XML is cached as well
$this->assertNotNull(cache('0aff3bc1259154f0e9db860026cda7a6'));
self::assertNotNull(cache('0aff3bc1259154f0e9db860026cda7a6'));
}
public function testGetArtistInformationForNonExistentArtist()
public function testGetArtistInformationForNonExistentArtist(): void
{
/** @var Artist $artist */
$artist = factory(Artist::class)->make();
@ -58,15 +53,15 @@ class LastfmServiceTest extends TestCase
'get' => new Response(400, [], file_get_contents(__DIR__.'../../../blobs/lastfm/artist-notfound.xml')),
]);
$api = new LastfmService($client, app(Cache::class));
$api = new LastfmService($client, app(Cache::class), app(Logger::class));
$this->assertNull($api->getArtistInformation($artist->name));
self::assertNull($api->getArtistInformation($artist->name));
}
/**
* @throws Exception
*/
public function testGetAlbumInformation()
public function testGetAlbumInformation(): void
{
/** @var Album $album */
$album = factory(Album::class)->create([
@ -79,7 +74,7 @@ class LastfmServiceTest extends TestCase
'get' => new Response(200, [], file_get_contents(__DIR__.'../../../blobs/lastfm/album.xml')),
]);
$api = new LastfmService($client, app(Cache::class));
$api = new LastfmService($client, app(Cache::class), app(Logger::class));
$info = $api->getAlbumInformation($album->name, $album->artist->name);
// Then I get the album's info
@ -104,10 +99,10 @@ class LastfmServiceTest extends TestCase
],
], $info);
$this->assertNotNull(cache('fca889d13b3222589d7d020669cc5a38'));
self::assertNotNull(cache('fca889d13b3222589d7d020669cc5a38'));
}
public function testGetAlbumInformationForNonExistentAlbum()
public function testGetAlbumInformationForNonExistentAlbum(): void
{
/** @var Album $album */
$album = factory(Album::class)->create();
@ -117,8 +112,8 @@ class LastfmServiceTest extends TestCase
'get' => new Response(400, [], file_get_contents(__DIR__.'../../../blobs/lastfm/album-notfound.xml')),
]);
$api = new LastfmService($client, app(Cache::class));
$api = new LastfmService($client, app(Cache::class), app(Logger::class));
$this->assertNull($api->getAlbumInformation($album->name, $album->artist->name));
self::assertNull($api->getAlbumInformation($album->name, $album->artist->name));
}
}

View file

@ -5,6 +5,7 @@ namespace Tests\Integration\Services;
use App\Models\Album;
use App\Models\Artist;
use App\Services\MediaMetadataService;
use Illuminate\Log\Logger;
use org\bovigo\vfs\vfsStream;
use Tests\TestCase;
@ -16,11 +17,10 @@ class MediaMetadataServiceTest extends TestCase
public function setUp()
{
parent::setUp();
$this->mediaMetadataService = new MediaMetadataService();
$this->mediaMetadataService = new MediaMetadataService(app(Logger::class));
}
public function testCopyAlbumCover()
public function testCopyAlbumCover(): void
{
/** @var Album $album */
$album = factory(Album::class)->create();
@ -34,7 +34,7 @@ class MediaMetadataServiceTest extends TestCase
$this->assertEquals('http://localhost/public/img/covers/bar.jpg', Album::find($album->id)->cover);
}
public function testWriteAlbumCover()
public function testWriteAlbumCover(): void
{
/** @var Album $album */
$album = factory(Album::class)->create();
@ -48,7 +48,7 @@ class MediaMetadataServiceTest extends TestCase
$this->assertEquals('http://localhost/public/img/covers/foo.jpg', Album::find($album->id)->cover);
}
public function testWriteArtistImage()
public function testWriteArtistImage(): void
{
/** @var Artist $artist */
$artist = factory(Artist::class)->create();

View file

@ -6,30 +6,26 @@ use App\Services\YouTubeService;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response;
use Mockery as m;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Log\Logger;
use Mockery;
use Tests\TestCase;
class YouTubeServiceTest extends TestCase
{
protected function tearDown()
{
m::close();
parent::tearDown();
}
/**
* @throws Exception
*/
public function testSearch()
public function testSearch(): void
{
$this->withoutEvents();
/** @var Client $client */
$client = m::mock(Client::class, [
$client = Mockery::mock(Client::class, [
'get' => new Response(200, [], file_get_contents(__DIR__.'../../../blobs/youtube/search.json')),
]);
$api = new YouTubeService($client);
$api = new YouTubeService($client, app(Repository::class), app(Logger::class));
$response = $api->search('Lorem Ipsum');
$this->assertEquals('Slipknot - Snuff [OFFICIAL VIDEO]', $response->items[0]->snippet->title);

View file

@ -7,8 +7,8 @@ use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Log\Logger;
use Mockery;
use Mockery\MockInterface;
use Tests\TestCase;
class iTunesServiceTest extends TestCase
@ -16,7 +16,7 @@ class iTunesServiceTest extends TestCase
/**
* @throws Exception
*/
public function testGetTrackUrl()
public function testGetTrackUrl(): void
{
$term = 'Foo Bar';
@ -25,10 +25,10 @@ class iTunesServiceTest extends TestCase
'get' => new Response(200, [], file_get_contents(__DIR__.'../../../blobs/itunes/track.json')),
]);
/** @var Cache|MockInterface $cache */
$cache = app(Cache::class);
$logger = app(Logger::class);
$url = (new iTunesService($client, $cache))->getTrackUrl($term);
$url = (new iTunesService($client, $cache, $logger))->getTrackUrl($term);
self::assertEquals(
'https://itunes.apple.com/us/album/i-remember-you/id265611220?i=265611396&uo=4&at=1000lsGu',

View file

@ -4,8 +4,10 @@ namespace Tests\Unit\Services;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Mockery as m;
use Illuminate\Log\Logger;
use Mockery;
use Tests\TestCase;
use Tests\Unit\Stubs\ConcreteApiClient;
@ -13,15 +15,36 @@ class ApiClientTest extends TestCase
{
use WithoutMiddleware;
public function testBuildUri()
{
/** @var Client $client */
$client = m::mock(Client::class);
$api = new ConcreteApiClient($client);
/** @var Cache */
private $cache;
$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/'));
/** @var Client */
private $client;
/** @var Logger */
private $logger;
public function setUp()
{
parent::setUp();
/**
* @var Client client
* @var Cache cache
* @var Logger logger
*/
$this->client = Mockery::mock(Client::class);
$this->cache = Mockery::mock(Cache::class);
$this->logger = Mockery::mock(Logger::class);
}
public function testBuildUri(): void
{
$api = new ConcreteApiClient($this->client, $this->cache, $this->logger);
self::assertEquals('http://foo.com/get/param?key=bar', $api->buildUrl('get/param'));
self::assertEquals('http://foo.com/get/param?baz=moo&key=bar', $api->buildUrl('/get/param?baz=moo'));
self::assertEquals('http://baz.com/?key=bar', $api->buildUrl('http://baz.com/'));
}
public function provideRequestData()
@ -43,12 +66,12 @@ class ApiClientTest extends TestCase
public function testRequest($method, $responseBody)
{
/** @var Client $client */
$client = m::mock(Client::class, [
$client = Mockery::mock(Client::class, [
$method => new Response(200, [], $responseBody),
]);
$api = new ConcreteApiClient($client);
$api = new ConcreteApiClient($client, $this->cache, $this->logger);
$this->assertSame((array) json_decode($responseBody), (array) $api->$method('/'));
self::assertSame((array) json_decode($responseBody), (array) $api->$method('/'));
}
}

View file

@ -1,25 +0,0 @@
<?php
namespace E2E;
use Facebook\WebDriver\WebDriverBy;
class AlbumsScreenTest extends TestCase
{
public function testAlbumsScreen()
{
$this->loginAndGoTo('albums');
static::assertNotEmpty($this->els('#albumsWrapper .albums article.item'));
$firstAlbum = $this->el('#albumsWrapper .albums article.item:nth-child(1)');
static::assertNotEmpty($firstAlbum->findElement(WebDriverBy::cssSelector('.info .name'))->getText());
static::assertNotEmpty($firstAlbum->findElement(WebDriverBy::cssSelector('.info .artist'))->getText());
static::assertContains('10 songs', $firstAlbum->findElement(WebDriverBy::cssSelector('.meta'))->getText());
// test the view modes
$this->click('#albumsWrapper > h1.heading > span.view-modes > a.list');
static::assertCount(1, $this->els('#albumsWrapper > div.albums.as-list'));
$this->click('#albumsWrapper > h1.heading > span.view-modes > a.thumbnails');
static::assertCount(1, $this->els('#albumsWrapper > div.albums.as-thumbnails'));
}
}

View file

@ -1,8 +0,0 @@
<?php
namespace E2E;
abstract class AllSongsScreenTest extends TestCase
{
// All we need to test should have been cover in SongListTest class.
}

View file

@ -1,24 +0,0 @@
<?php
namespace E2E;
use Facebook\WebDriver\WebDriverBy;
class ArtistsScreenTest extends TestCase
{
public function testArtistsScreen()
{
$this->loginAndGoTo('artists');
static::assertNotEmpty($this->els('#artistsWrapper .artists article.item'));
$firstArtist = $this->el('#artistsWrapper .artists article.item:nth-child(1)');
static::assertNotEmpty($firstArtist->findElement(WebDriverBy::cssSelector('.info .name'))->getText());
static::assertContains('5 albums • 50 songs', $firstArtist->findElement(WebDriverBy::cssSelector('.meta'))->getText());
// test the view modes
$this->click('#artistsWrapper > h1.heading > span.view-modes > a.list');
static::assertCount(1, $this->els('#artistsWrapper > div.artists.as-list'));
$this->click('#artistsWrapper > h1.heading > span.view-modes > a.thumbnails');
static::assertCount(1, $this->els('#artistsWrapper > div.artists.as-thumbnails'));
}
}

View file

@ -1,31 +0,0 @@
<?php
namespace E2E;
class DefaultsTest extends TestCase
{
public function testDefaults()
{
static::assertContains('Koel', $this->driver->getTitle());
$formSelector = '#app > div.login-wrapper > form';
// Our login form should be there.
static::assertCount(1, $this->els($formSelector));
// We submit rubbish and expect an error class on the form.
$this->login('foo@bar.com', 'ThisIsWongOnSoManyLevels')
->see("$formSelector.error");
// Now we submit good stuff and make sure we're in.
$this->login()
->seeText('Koel Admin', '#userBadge > a.view-profile.control > span');
// Default URL must be Home
static::assertEquals($this->url.'/#!/home', $this->driver->getCurrentURL());
// While we're at this, test logging out as well.
$this->click('#userBadge > a.logout');
$this->see($formSelector);
}
}

View file

@ -1,8 +0,0 @@
<?php
namespace E2E;
abstract class FavoritesScreenTest extends TestCase
{
// "Favorites" functionality has been covered in SongListTest
}

View file

@ -1,37 +0,0 @@
<?php
namespace E2E;
use Facebook\WebDriver\Remote\RemoteWebElement;
class HomeScreenTest extends TestCase
{
public function testHomeScreen()
{
$this->loginAndGoTo('home');
// We must see some greetings
static::assertTrue($this->el('#homeWrapper > h1')->isDisplayed());
// 6 recently added albums
static::assertCount(6, $this->els('#homeWrapper section.recently-added article'));
// 10 recently added songs
static::assertCount(10, $this->els('#homeWrapper .recently-added-song-list .song-item-home'));
// Shuffle must work for latest albums
$this->click('#homeWrapper section.recently-added article:nth-child(1) a.shuffle-album');
static::assertCount(10, $this->els('#queueWrapper .song-list-wrap tr.song-item'));
$this->goto('home');
// Simulate a "double click to play" action
/** @var $clickedSong RemoteWebElement */
$clickedSong = $this->el('#homeWrapper section.recently-added > div > div:nth-child(2) li:nth-child(1) .details');
$this->doubleClick($clickedSong);
// The song must appear at the top of "Recently played" section
/** @var $mostRecentSong RemoteWebElement */
$mostRecentSong = $this->el('#homeWrapper .recently-added-song-list .song-item-home:nth-child(1) .details');
static::assertEquals($mostRecentSong->getText(), $clickedSong->getText());
}
}

View file

@ -1,46 +0,0 @@
<?php
namespace E2E;
/**
* Class PlaybackControlsTest.
*
* Tests the playback controls (the footer buttons).
*/
class PlaybackControlsTest extends TestCase
{
public function testPlaybackControls()
{
$this->loginAndWait();
// Show and hide the extra panel
$this->click('#mainFooter .control.info');
$this->notSee('#extra');
$this->click('#mainFooter .control.info');
$this->see('#extra');
// Show and hide the Presets
$this->click('#mainFooter .control.equalizer');
$this->see('#equalizer');
// clicking anywhere else should close the equalizer
$this->click('#extra');
$this->notSee('#equalizer');
// Circle around the repeat state
$this->click('#mainFooter .control.repeat');
$this->see('#mainFooter .control.repeat.REPEAT_ALL');
$this->click('#mainFooter .control.repeat');
$this->see('#mainFooter .control.repeat.REPEAT_ONE');
$this->click('#mainFooter .control.repeat');
$this->see('#mainFooter .control.repeat.NO_REPEAT');
// Mute/unmute
$currentValue = $this->el('#volumeRange')->getAttribute('value');
$this->click('#volume .fa-volume-up');
$this->see('#mainFooter .fa-volume-off');
static::assertEquals(0, $this->el('#volumeRange')->getAttribute('value'));
$this->click('#volume .fa-volume-off');
$this->see('#mainFooter .fa-volume-up');
static::assertEquals($currentValue, $this->el('#volumeRange')->getAttribute('value'));
}
}

View file

@ -1,23 +0,0 @@
<?php
namespace E2E;
class PlaylistScreenTest extends TestCase
{
use SongListActions;
public function testPlaylistScreen()
{
$this->loginAndGoTo('songs')
->selectRange()
->createPlaylist('Bar')
->seeText('Bar', '#playlists > ul');
$this->click('#sidebar .playlist:nth-last-child(1)');
$this->see('#playlistWrapper');
$this->click('#playlistWrapper .btn-delete-playlist');
// expect a confirmation
$this->see('.alertify');
}
}

View file

@ -1,43 +0,0 @@
<?php
namespace E2E;
use Facebook\WebDriver\WebDriverExpectedCondition;
use Facebook\WebDriver\WebDriverKeys;
class ProfileScreenTest extends TestCase
{
public function testProfileScreen()
{
$this->loginAndWait()
->click('a.view-profile');
$this->see('#profileWrapper')
// Now we change some user profile details
->typeIn('#profileWrapper input[name="name"]', 'Mr Bar')
->typeIn('#profileWrapper input[name="email"]', 'bar@koel.net')
->enter()
->see('.alertify-logs')
// Dismiss the alert first
->press(WebDriverKeys::ESCAPE)
->notSee('.alertify-logs');
$avatar = $this->el('a.view-profile img');
// Expect the Gravatar to be updated
static::assertEquals('https://www.gravatar.com/avatar/36df72b4484fed183fad058f30b55d21?s=256', $avatar->getAttribute('src'));
// Check "Confirm Closing" and validate its functionality
$this->click('#profileWrapper input[name="confirmClosing"]');
$this->refresh()
->waitUntil(WebDriverExpectedCondition::alertIsPresent());
$this->driver->switchTo()->alert()->dismiss();
// Reverse all changes for other tests to not be affected
$this->typeIn('#profileWrapper input[name="name"]', 'Koel Admin')
->typeIn('#profileWrapper input[name="email"]', 'koel@example.com')
->enter()
->see('.alertify-logs')
->press(WebDriverKeys::ESCAPE)
->notSee('.alertify-logs')
->click('#profileWrapper input[name="confirmClosing"]');
}
}

View file

@ -1,20 +0,0 @@
<?php
namespace E2E;
class QueueScreenTest extends TestCase
{
public function test()
{
$this->loginAndGoTo('queue');
static::assertContains('Current Queue', $this->el('#queueWrapper > h1 > span')->getText());
// As the queue is currently empty, the "Shuffling all song" link should be there
$this->click('#queueWrapper a.start');
$this->see('#queueWrapper .song-item');
// Clear the queue
$this->click('#queueWrapper .buttons button.btn-clear-queue');
static::assertEmpty($this->els('#queueWrapper tr.song-item'));
}
}

View file

@ -1,42 +0,0 @@
<?php
namespace E2E;
class SideBarTest extends TestCase
{
public function testSideBar()
{
$this->loginAndWait();
// All basic navigation
foreach (['home', 'queue', 'songs', 'albums', 'artists', 'youtube', 'settings', 'users'] as $screen) {
$this->goto($screen)
->waitUntil(function () use ($screen) {
return $this->driver->getCurrentURL() === $this->url.'/#!/'.$screen;
});
}
// Add a playlist
$this->click('#playlists > h1 > i.create');
$this->see('#playlists > form.create')
->typeIn('#playlists > form > input[type="text"]', 'Bar')
->enter()
->waitUntil(function () {
$list = $this->els('#playlists .playlist');
return end($list)->getText() === 'Bar';
});
// Double click to edit/rename a playlist
$this->doubleClick('#playlists .playlist:nth-child(2)')
->see('#playlists .playlist:nth-child(2) input[type="text"]')
->typeIn('#playlists .playlist:nth-child(2) input[type="text"]', 'Qux')
->enter()
->seeText('Qux', '#playlists .playlist:nth-child(2)');
// Edit with an empty name shouldn't do anything.
$this->doubleClick('#playlists .playlist:nth-child(2)');
$this->click('#playlists .playlist:nth-child(2) input[type="text"]')->clear();
$this->enter()->seeText('Qux', '#playlists .playlist:nth-child(2)');
}
}

View file

@ -1,83 +0,0 @@
<?php
namespace E2E;
use Facebook\WebDriver\Interactions\WebDriverActions;
use Facebook\WebDriver\WebDriverKeys;
trait SongListActions
{
use WebDriverShortcuts;
public function selectAllSongs()
{
$this->click($this->wrapperId); // make sure focus is there before executing shortcut keys
(new WebDriverActions($this->driver))
->keyDown(null, WebDriverKeys::COMMAND)
->keyDown(null, 'A')
->keyUp(null, 'A')
->keyUp(null, WebDriverKeys::COMMAND)
->perform();
return $this;
}
public function selectSong($i = 1)
{
return $this->click("{$this->wrapperId} tr.song-item:nth-child($i)");
}
/**
* Select a range of songs.
*
* @param int $from
* @param int $to
*
* @return $this
*/
public function selectRange($from = 1, $to = 5)
{
$this->click("{$this->wrapperId} tr.song-item:nth-child($from)");
(new WebDriverActions($this->driver))
->keyDown(null, WebDriverKeys::SHIFT)
->click($this->el("{$this->wrapperId} tr.song-item:nth-child($to)"))
->keyUp(null, WebDriverKeys::SHIFT)
->perform();
return $this;
}
/**
* Deselect (Ctrl/Cmd+click) songs.
*
* @return $this
*/
public function cmdSelectSongs()
{
$actions = (new WebDriverActions($this->driver))->keyDown(null, WebDriverKeys::COMMAND);
foreach (func_get_args() as $i) {
$actions->click($this->el("{$this->wrapperId} tr.song-item:nth-child($i)"));
}
$actions->keyUp(null, WebDriverKeys::COMMAND)->perform();
return $this;
}
public function rightClickOnSong($i = 1)
{
$this->rightClick("{$this->wrapperId} tr.song-item:nth-child($i)");
return $this;
}
public function createPlaylist($playlistName)
{
// Try adding a song into a new playlist
$this->click("{$this->wrapperId} .buttons button.btn-add-to");
$this->typeIn("{$this->wrapperId} .buttons input[type='text']", $playlistName)
->enter();
return $this;
}
}

View file

@ -1,159 +0,0 @@
<?php
namespace E2E;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverElement;
use Facebook\WebDriver\WebDriverKeys;
class SongListTest extends TestCase
{
use SongListActions;
public function testSelection()
{
$this->loginAndWait()->repopulateList();
// Single song selection
static::assertContains('selected', $this->selectSong()->getAttribute('class'));
// Shift+Click
$this->selectRange();
// should have 5 selected rows
static::assertCount(5, $this->els('#queueWrapper tr.song-item.selected'));
// Cmd+Click
$this->cmdSelectSongs(2, 3);
// should have only 3 selected rows remaining
static::assertCount(3, $this->els('#queueWrapper tr.song-item.selected'));
// 2nd and 3rd rows must not be selected
static::assertNotContains(
'selected',
$this->el('#queueWrapper tr.song-item:nth-child(2)')->getAttribute('class')
);
static::assertNotContains(
'selected',
$this->el('#queueWrapper tr.song-item:nth-child(3)')->getAttribute('class')
);
// Delete key should remove selected songs
$this->press(WebDriverKeys::DELETE)->waitUntil(function () {
return count($this->els('#queueWrapper tr.song-item.selected')) === 0
&& count($this->els('#queueWrapper tr.song-item')) === 7;
});
// Ctrl+A/Cmd+A should select all songs
$this->selectAllSongs();
static::assertCount(7, $this->els('#queueWrapper tr.song-item.selected'));
}
public function testActionButtons()
{
$this->loginAndWait()
->repopulateList()
// Since no songs are selected, the "Shuffle All" button must be shown
->see('#queueWrapper button.btn-shuffle-all')
// Now we selected all songs for the "Shuffle Selected" button to be shown
->selectAllSongs()
->see('#queueWrapper button.btn-shuffle-selected');
// Add to favorites
$this->selectSong();
$this->click('#queueWrapper .buttons button.btn-add-to');
$this->click('#queueWrapper .buttons .add-to li.favorites');
$this->goto('favorites');
static::assertCount(1, $this->els('#favoritesWrapper tr.song-item'));
$this->goto('queue')
->selectSong();
// Try adding a song into a new playlist
$this->createPlaylist('Foo')
->seeText('Foo', '#playlists > ul');
}
public function testSorting()
{
$this->loginAndWait()->repopulateList();
// Confirm that we can't sort in Queue screen
/** @var WebDriverElement $th */
foreach ($this->els('#queueWrapper div.song-list-wrap th') as $th) {
if (!$th->isDisplayed()) {
continue;
}
foreach ($th->findElements(WebDriverBy::tagName('i')) as $sortDirectionIcon) {
static::assertFalse($sortDirectionIcon->isDisplayed());
}
}
// Now go to All Songs screen and sort there
$this->goto('songs')
->click('#songsWrapper div.song-list-wrap th:nth-child(2)');
$last = null;
$results = [];
/** @var WebDriverElement $td */
foreach ($this->els('#songsWrapper div.song-list-wrap td.title') as $td) {
$current = $td->getText();
$results[] = $last === null ? true : $current <= $last;
$last = $current;
}
static::assertNotContains(false, $results);
// Second click will reverse the sort
$this->click('#songsWrapper div.song-list-wrap th:nth-child(2)');
$last = null;
$results = [];
/** @var WebDriverElement $td */
foreach ($this->els('#songsWrapper div.song-list-wrap td.title') as $td) {
$current = $td->getText();
$results[] = $last === null ? true : $current >= $last;
$last = $current;
}
static::assertNotContains(false, $results);
}
public function testContextMenu()
{
$this->loginAndGoTo('songs')
->rightClickOnSong()
->see('#songsWrapper .song-menu');
// 7 sub menu items
static::assertCount(7, $this->els('#songsWrapper .song-menu > li'));
// Clicking the "Go to Album" menu item
$this->click('#songsWrapper .song-menu > li:nth-child(2)');
$this->see('#albumWrapper');
// Clicking the "Go to Artist" menu item
$this->back()
->rightClickOnSong()
->click('#songsWrapper .song-menu > li:nth-child(3)');
$this->see('#artistWrapper');
// Clicking "Edit"
$this->back()
->rightClickOnSong()
->click('#songsWrapper .song-menu > li:nth-child(5)');
$this->see('#editSongsOverlay form');
// Updating song
$this->typeIn('#editSongsOverlay form input[name="title"]', 'Foo')
->typeIn('#editSongsOverlay form input[name="track"]', 99)
->enter()
->notSee('#editSongsOverlay form');
static::assertEquals('99', $this->el('#songsWrapper tr.song-item:nth-child(1) .track-number')->getText());
static::assertEquals('Foo', $this->el('#songsWrapper tr.song-item:nth-child(1) .title')->getText());
}
private function repopulateList()
{
// Go back to Albums and queue an album of 10 songs
$this->goto('albums');
$this->click('#albumsWrapper > div > article:nth-child(1) .meta a.shuffle-album');
$this->goto('queue');
return $this;
}
}

View file

@ -1,173 +0,0 @@
<?php
namespace E2E;
use App\Application;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\WebDriverDimension;
use Facebook\WebDriver\WebDriverPoint;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\Artisan;
abstract class TestCase extends \PHPUnit_Framework_TestCase
{
use WebDriverShortcuts;
/**
* @var Application
*/
protected $app;
/**
* ID of the current screen wrapper (with leading #).
*
* @var string
*/
public $wrapperId;
/**
* The default Koel URL for E2E (server by `php artisan serve --port=8081`).
*
* @var string
*/
protected $url = 'http://localhost:8081';
protected $coverPath;
/**
* TestCase constructor.
*/
public function __construct()
{
parent::__construct();
$this->createApp();
$this->resetData();
$this->driver = RemoteWebDriver::create('http://localhost:4444/wd/hub', DesiredCapabilities::chrome());
$this->driver->manage()->window()->setSize(new WebDriverDimension(1440, 900));
$this->driver->manage()->window()->setPosition(new WebDriverPoint(0, 0));
}
/**
* @return Application
*/
protected function createApp()
{
$this->app = require __DIR__.'/../../bootstrap/app.php';
$this->app->make(Kernel::class)->bootstrap();
return $this->app;
}
/**
* Reset the test data for E2E tests.
*/
protected function resetData()
{
// Make sure we have a fresh database.
@unlink(__DIR__.'/../../database/e2e.sqlite');
touch(__DIR__.'/../../database/e2e.sqlite');
Artisan::call('migrate');
Artisan::call('db:seed');
Artisan::call('db:seed', ['--class' => 'E2EDataSeeder']);
if (!file_exists($this->coverPath)) {
@mkdir($this->coverPath, 0777, true);
}
}
/**
* Log into Koel.
*
* @param string $username
* @param string $password
*
* @return $this
*/
protected function login($username = 'koel@example.com', $password = 'SoSecureK0el')
{
$this->typeIn("#app > div.login-wrapper > form > [type='email']", $username);
$this->typeIn("#app > div.login-wrapper > form > [type='password']", $password);
$this->enter();
return $this;
}
/**
* Log in and wait for the app to finish loading.
*
* @throws \Exception
*
* @return $this
*/
protected function loginAndWait()
{
$this->login()
->seeText('Koel Admin', '#userBadge > a.view-profile');
return $this;
}
/**
* Go to a specific screen.
*
* @param $screen
*
* @throws \Exception
*
* @return $this
*/
protected function goto($screen)
{
$this->wrapperId = "#{$screen}Wrapper";
if ($screen === 'favorites') {
$this->click('#sidebar .favorites a');
} else {
$this->click("#sidebar a.$screen");
}
$this->see($this->wrapperId);
return $this;
}
/**
* Log in and go to a specific screen.
*
* @param $screen string
*
* @throws \Exception
*
* @return $this
*/
protected function loginAndGoTo($screen)
{
return $this->loginAndWait()->goto($screen);
}
/**
* Wait for the user to press ENTER key before continuing.
*/
protected function waitForUserInput()
{
if (trim(fgets(fopen('php://stdin', 'rb'))) !== chr(13)) {
return;
}
}
protected function focusIntoApp()
{
$this->click('#app');
}
public function setUp()
{
$this->driver->get($this->url);
}
public function tearDown()
{
$this->driver->quit();
}
}

View file

@ -1,42 +0,0 @@
<?php
namespace E2E;
use Facebook\WebDriver\Interactions\Internal\WebDriverMouseMoveAction;
class UsersScreenTest extends TestCase
{
public function testUsersScreen()
{
$this->loginAndGoTo('users')->see('.user-item.me');
// Hover to the first user item
(new WebDriverMouseMoveAction($this->driver->getMouse(), $this->el('#usersWrapper .user-item.me')))
->perform();
// and validate that the button reads "Update Profile" instead of "Edit"
static::assertContains('Update Profile',
$this->el('#usersWrapper .user-item.me .btn-edit')->getText());
// Also, clicking it should show the "Profile & Preferences" panel
$this->click('#usersWrapper .user-item.me .btn-edit');
$this->see('#profileWrapper')
->back()
// Add new user
->click('#usersWrapper .btn-add');
$this->see('form.user-add')
->typeIn('form.user-add input[name="name"]', 'Foo')
->typeIn('form.user-add input[name="email"]', 'foo@koel.net')
->typeIn('form.user-add input[name="password"]', 'SecureMuch')
->enter()
->seeText('foo@koel.net', '#usersWrapper');
// Hover the next user item (not me)
(new WebDriverMouseMoveAction($this->driver->getMouse(), $this->el('#usersWrapper .user-item:not(.me)')))
->perform();
static::assertContains('Edit', $this->el('#usersWrapper .user-item:not(.me) .btn-edit')->getText());
// Edit user
$this->click('#usersWrapper .user-item:not(.me) .btn-edit');
$this->see('form.user-edit')
->typeIn('form.user-edit input[name="email"]', 'bar@koel.net')
->enter()
->seeText('bar@koel.net', '#usersWrapper');
}
}

View file

@ -1,257 +0,0 @@
<?php
namespace E2E;
use Facebook\WebDriver\Interactions\Internal\WebDriverDoubleClickAction;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverElement;
use Facebook\WebDriver\WebDriverExpectedCondition;
use Facebook\WebDriver\WebDriverKeys;
trait WebDriverShortcuts
{
/**
* @var RemoteWebDriver
*/
protected $driver;
/**
* @param $selector WebDriverElement|string
*
* @return WebDriverElement
*/
protected function el($selector)
{
if (is_string($selector)) {
return $this->driver->findElement(WebDriverBy::cssSelector($selector));
}
return $selector;
}
/**
* Get a list of elements by a selector.
*
* @param $selector
*
* @return \Facebook\WebDriver\Remote\RemoteWebElement[]
*/
protected function els($selector)
{
return $this->driver->findElements(WebDriverBy::cssSelector($selector));
}
/**
* Type a string.
*
* @param $string
*
* @return $this
*/
protected function type($string)
{
$this->driver->getKeyboard()->sendKeys($string);
return $this;
}
/**
* Type into an element.
*
* @param $element
* @param $string
*
* @return $this
*/
protected function typeIn($element, $string)
{
$this->click($element)->clear();
return $this->type($string);
}
/**
* Press a key.
*
* @param string $key
*
* @return $this
*/
protected function press($key = WebDriverKeys::ENTER)
{
$this->driver->getKeyboard()->pressKey($key);
return $this;
}
/**
* Press enter.
*
* @return $this
*/
protected function enter()
{
return $this->press();
}
/**
* Click an element.
*
* @param $element
*
* @return WebDriverElement
*/
protected function click($element)
{
return $this->el($element)->click();
}
/**
* Right-click an element.
*
* @param $element
*
* @return \Facebook\WebDriver\Remote\RemoteMouse
*/
protected function rightClick($element)
{
return $this->driver->getMouse()->contextClick($this->el($element)->getCoordinates());
}
/**
* Double-click and element.
*
* @param $element
*
* @return $this
*/
protected function doubleClick($element)
{
(new WebDriverDoubleClickAction($this->driver->getMouse(), $this->el($element)))->perform();
return $this;
}
/**
* Sleep (implicit wait) for some seconds.
*
* @param $seconds
*
* @return $this
*/
protected function sleep($seconds)
{
$this->driver->manage()->timeouts()->implicitlyWait($seconds);
return $this;
}
/**
* Wait until a condition is met.
*
* @param $func (closure|WebDriverExpectedCondition)
* @param int $timeout
*
* @throws \Exception
*
* @return $this
*/
protected function waitUntil($func, $timeout = 10)
{
$this->driver->wait($timeout)->until($func);
return $this;
}
/**
* Wait and validate an element to be visible.
*
* @param $selector
*
* @throws \Exception
*
* @return $this
*/
public function see($selector)
{
$this->waitUntil(WebDriverExpectedCondition::visibilityOfElementLocated(
WebDriverBy::cssSelector($selector)
));
return $this;
}
/**
* Wait and validate an element to be invisible.
*
* @param $selector string The element's CSS selector.
*
* @throws \Exception
*
* @return $this
*/
public function notSee($selector)
{
$this->waitUntil(WebDriverExpectedCondition::invisibilityOfElementLocated(
WebDriverBy::cssSelector($selector)
));
return $this;
}
/**
* Wait and validate a text to be visible in an element.
*
* @param $text
* @param $selector string The element's CSS selector.
*
* @throws \Exception
*
* @return $this
*/
public function seeText($text, $selector)
{
$this->waitUntil(WebDriverExpectedCondition::textToBePresentInElement(
WebDriverBy::cssSelector($selector), $text
));
return $this;
}
/**
* Navigate back.
*
* @return $this
*/
protected function back()
{
$this->driver->navigate()->back();
return $this;
}
/**
* Navigate forward.
*
* @return $this
*/
protected function forward()
{
$this->driver->navigate()->forward();
return $this;
}
/**
* Refresh the page.
*
* @return $this
*/
protected function refresh()
{
$this->driver->navigate()->refresh();
return $this;
}
}

View file

@ -1,27 +0,0 @@
<?php
namespace E2E;
/**
* Class ZSettingsScreenTest
* The name is an ugly trick to force this test to run last, due to it changing the whole suite's
* data, causing other tests to fail otherwise.
*/
class ZSettingsScreenTest extends TestCase
{
public function testSettingsScreen()
{
$this->loginAndGoTo('settings')
->typeIn('#inputSettingsPath', dirname(__DIR__.'/../songs'))
->enter()
// Wait for the page to reload
->waitUntil(function () {
return $this->driver->executeScript('return document.readyState') === 'complete';
})
// And for the loading screen to disappear
->notSee('#overlay')
->goto('albums')
// and make sure the scanning is good.
->seeText('Koel Testing Vol', '#albumsWrapper');
}
}