feat: upgrade Laravel to 7.x

This commit is contained in:
Phan An 2020-09-06 20:21:39 +02:00
parent f9d0017d11
commit e356e72814
76 changed files with 3220 additions and 2853 deletions

View file

@ -17,9 +17,6 @@ DB_PASSWORD=SoSecureMuchWow
# A random 32-char string. You can leave this empty if use php artisan koel:init.
APP_KEY=
# Another random 32-char string. You can leave this empty if use php artisan koel:init.
JWT_SECRET=
# Credentials and other info to be used when Koel is installed in non-interactive mode
# (php artisan koel:init --no-interaction)
# By default (interactive mode), Koel will still prompt for these information during installation,

1
.gitignore vendored
View file

@ -81,3 +81,4 @@ cypress/videos
/log
coverage.xml
.phpunit.result.cache
log.json

View file

@ -1,39 +0,0 @@
From 653ea67ccf5987b16815d0d4cd4ae4f6147b8649 Mon Sep 17 00:00:00 2001
From: An Phan <me@phanan.net>
Date: Mon, 24 Feb 2020 22:28:43 +0100
Subject: [PATCH] Replace fire() with dispatch()
---
src/Middleware/BaseMiddleware.php | 2 +-
src/Middleware/GetUserFromToken.php | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Middleware/BaseMiddleware.php b/src/Middleware/BaseMiddleware.php
index 9715f0c..cba7f8f 100644
--- a/src/Middleware/BaseMiddleware.php
+++ b/src/Middleware/BaseMiddleware.php
@@ -57,7 +57,7 @@ abstract class BaseMiddleware
*/
protected function respond($event, $error, $status, $payload = [])
{
- $response = $this->events->fire($event, $payload, true);
+ $response = $this->events->dispatch($event, $payload, true);
return $response ?: $this->response->json(['error' => $error], $status);
}
diff --git a/src/Middleware/GetUserFromToken.php b/src/Middleware/GetUserFromToken.php
index af3b21c..7542158 100644
--- a/src/Middleware/GetUserFromToken.php
+++ b/src/Middleware/GetUserFromToken.php
@@ -41,7 +41,7 @@ class GetUserFromToken extends BaseMiddleware
return $this->respond('tymon.jwt.user_not_found', 'user_not_found', 404);
}
- $this->events->fire('tymon.jwt.valid', $user);
+ $this->events->dispatch('tymon.jwt.valid', $user);
return $next($request);
}
--
2.24.0

View file

@ -1,33 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Jackiedo\DotenvEditor\DotenvEditor;
class GenerateJwtSecretCommand extends Command
{
protected $name = 'koel:generate-jwt-secret';
protected $description = 'Set the JWTAuth secret key used to sign the tokens';
private $dotenvEditor;
public function __construct(DotenvEditor $dotenvEditor)
{
parent::__construct();
$this->dotenvEditor = $dotenvEditor;
}
public function handle(): void
{
if (config('jwt.secret')) {
$this->comment('JWT secret exists -- skipping');
return;
}
$this->info('Generating JWT secret');
$this->dotenvEditor->setKey('JWT_SECRET', Str::random(32))->save();
}
}

View file

@ -59,7 +59,6 @@ class InitCommand extends Command
try {
$this->maybeGenerateAppKey();
$this->maybeGenerateJwtSecret();
$this->maybeSetUpDatabase();
$this->migrateDatabase();
$this->maybeSeedDatabase();
@ -200,16 +199,6 @@ class InitCommand extends Command
}
}
private function maybeGenerateJwtSecret(): void
{
if (!config('jwt.secret')) {
$this->info('Generating JWT secret');
$this->artisan->call('koel:generate-jwt-secret');
} else {
$this->comment('JWT secret exists -- skipping');
}
}
private function maybeSeedDatabase(): void
{
if (!User::count()) {

View file

@ -2,24 +2,18 @@
namespace App\Exceptions;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
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;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that should not be reported.
*
* @var array
*/
protected $dontReport = [
AuthorizationException::class,
HttpException::class,
@ -27,24 +21,12 @@ class Handler extends ExceptionHandler
ValidationException::class,
];
/**
* Report or log an exception.
*
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
*
* @throws Exception
*/
public function report(Exception $e): void
public function report(Throwable $e): void
{
parent::report($e);
}
/**
* Render an exception into an HTTP response.
*
* @param Request $request
*/
public function render($request, Exception $e): Response
public function render($request, Throwable $e): Response
{
if ($e instanceof ModelNotFoundException) {
$e = new NotFoundHttpException($e->getMessage(), $e);
@ -53,17 +35,12 @@ class Handler extends ExceptionHandler
return parent::render($request, $e);
}
/**
* Convert an authentication exception into an unauthenticated response.
*
* @param Request $request
*/
protected function unauthenticated($request, AuthenticationException $exception): Response
{
if ($request->expectsJson()) {
return response()->json(['error' => 'Unauthenticated.'], 401);
}
return redirect()->guest('login');
return redirect()->guest('/');
}
}

View file

@ -3,23 +3,37 @@
namespace App\Http\Controllers\API;
use App\Http\Requests\API\UserLoginRequest;
use Exception;
use App\Models\User;
use App\Repositories\UserRepository;
use App\Services\TokenManager;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Hashing\HashManager;
use Illuminate\Http\JsonResponse;
use Illuminate\Log\Logger;
use Tymon\JWTAuth\JWTAuth;
use Illuminate\Http\Response;
/**
* @group 1. Authentication
*/
class AuthController extends Controller
{
private $auth;
private $logger;
private $userRepository;
private $hash;
private $tokenManager;
public function __construct(JWTAuth $auth, Logger $logger)
/** @var User|null */
private $currentUser;
public function __construct(
UserRepository $userRepository,
HashManager $hash,
TokenManager $tokenManager,
?Authenticatable $currentUser
)
{
$this->auth = $auth;
$this->logger = $logger;
$this->userRepository = $userRepository;
$this->hash = $hash;
$this->currentUser = $currentUser;
$this->tokenManager = $tokenManager;
}
/**
@ -46,10 +60,16 @@ class AuthController extends Controller
*/
public function login(UserLoginRequest $request)
{
$token = $this->auth->attempt($request->only('email', 'password'));
abort_unless($token, 401, 'Invalid credentials');
/** @var User $user */
$user = $this->userRepository->getFirstWhere('email', $request->email);
return response()->json(compact('token'));
if (!$user || !$this->hash->check($request->password, $user->password)) {
abort(Response::HTTP_UNAUTHORIZED, 'Invalid credentials');
}
return response()->json([
'token' => $this->tokenManager->createToken($user)->plainTextToken
]);
}
/**
@ -59,13 +79,7 @@ class AuthController extends Controller
*/
public function logout()
{
if ($token = $this->auth->getToken()) {
try {
$this->auth->invalidate($token);
} catch (Exception $e) {
$this->logger->error($e);
}
}
$this->tokenManager->destroyTokens($this->currentUser);
return response()->json();
}

View file

@ -4,27 +4,29 @@ namespace App\Http\Controllers\API;
use App\Http\Requests\API\LastfmCallbackRequest;
use App\Http\Requests\API\LastfmSetSessionKeyRequest;
use App\Models\User;
use App\Services\LastfmService;
use Illuminate\Contracts\Auth\Guard;
use App\Services\TokenManager;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\JWTAuth;
/**
* @group Last.fm integration
*/
class LastfmController extends Controller
{
protected $auth;
private $lastfmService;
private $jwtAuth;
private $tokenManager;
public function __construct(Guard $auth, LastfmService $lastfmService, JWTAuth $jwtAuth)
/** @var User */
private $currentUser;
public function __construct(LastfmService $lastfmService, TokenManager $tokenManager, Authenticatable $currentUser)
{
$this->auth = $auth;
$this->lastfmService = $lastfmService;
$this->jwtAuth = $jwtAuth;
$this->tokenManager = $tokenManager;
$this->currentUser = $currentUser;
}
/**
@ -35,23 +37,22 @@ class LastfmController extends Controller
* which will send them to Last.fm for authentication. After authentication is successful, the user will be
* redirected back to `api/lastfm/callback?token=<Last.fm token>`.
*
* @queryParam jwt-token required The JWT token of the user.
* @queryParam jwt-token required The JWT token of the user. (Deprecated. Use api_token instead).
* @queryParam api_token required Authentication token of the current user.
* @response []
*
* @throws JWTException
*
* @return RedirectResponse
*/
public function connect()
{
abort_unless($this->lastfmService->enabled(), 401, 'Koel is not configured to use with Last.fm yet.');
// A workaround to make sure Tymon's JWTAuth get the correct token via our custom
// "jwt-token" query string instead of the default "token".
// This is due to the problem that Last.fm returns the token via "token" as well.
$this->jwtAuth->parseToken('', '', 'jwt-token');
$callbackUrl = urlencode(sprintf(
'%s?api_token=%s',
route('lastfm.callback'),
request('api_token')
));
$callbackUrl = urlencode(sprintf('%s?jwt-token=%s', route('lastfm.callback'), $this->jwtAuth->getToken()));
$url = sprintf('https://www.last.fm/api/auth/?api_key=%s&cb=%s', $this->lastfmService->getKey(), $callbackUrl);
return redirect($url);
@ -66,7 +67,7 @@ class LastfmController extends Controller
abort_unless($sessionKey, 500, 'Invalid token key.');
$this->auth->user()->savePreference('lastfm_session_key', $sessionKey);
$this->currentUser->savePreference('lastfm_session_key', $sessionKey);
return view('api.lastfm.callback');
}
@ -86,7 +87,7 @@ class LastfmController extends Controller
*/
public function setSessionKey(LastfmSetSessionKeyRequest $request)
{
$this->auth->user()->savePreference('lastfm_session_key', trim($request->key));
$this->currentUser->savePreference('lastfm_session_key', trim($request->key));
return response()->json();
}
@ -98,7 +99,7 @@ class LastfmController extends Controller
*/
public function disconnect()
{
$this->auth->user()->deletePreference('lastfm_session_key');
$this->currentUser->deletePreference('lastfm_session_key');
return response()->json();
}

View file

@ -5,12 +5,15 @@ namespace App\Http\Controllers\API;
use App\Http\Requests\API\PlaylistStoreRequest;
use App\Http\Requests\API\PlaylistSyncRequest;
use App\Models\Playlist;
use App\Models\User;
use App\Repositories\PlaylistRepository;
use App\Services\SmartPlaylistService;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
/**
* @group 4. Playlist management
@ -20,10 +23,18 @@ class PlaylistController extends Controller
private $playlistRepository;
private $smartPlaylistService;
public function __construct(PlaylistRepository $playlistRepository, SmartPlaylistService $smartPlaylistService)
/** @var User */
private $currentUser;
public function __construct(
PlaylistRepository $playlistRepository,
SmartPlaylistService $smartPlaylistService,
Authenticatable $currentUser
)
{
$this->playlistRepository = $playlistRepository;
$this->smartPlaylistService = $smartPlaylistService;
$this->currentUser = $currentUser;
}
/**
@ -50,7 +61,7 @@ class PlaylistController extends Controller
public function store(PlaylistStoreRequest $request)
{
/** @var Playlist $playlist */
$playlist = $request->user()->playlists()->create([
$playlist = $this->currentUser->playlists()->create([
'name' => $request->name,
'rules' => $request->rules,
]);
@ -78,7 +89,7 @@ class PlaylistController extends Controller
*/
public function update(Request $request, Playlist $playlist)
{
$this->authorize('owner', $playlist);
abort_unless($this->currentUser->can('owner', $playlist), Response::HTTP_FORBIDDEN);
$playlist->update($request->only('name', 'rules'));

View file

@ -4,10 +4,8 @@ namespace App\Http;
use App\Http\Middleware\Authenticate;
use App\Http\Middleware\ForceHttps;
use App\Http\Middleware\GetUserFromToken;
use App\Http\Middleware\ObjectStorageAuthenticate;
use App\Http\Middleware\TrimStrings;
use App\Http\Middleware\UseDifferentConfigIfE2E;
use Illuminate\Auth\Middleware\Authorize;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode;
@ -15,6 +13,7 @@ use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;
class Kernel extends HttpKernel
{
@ -25,7 +24,6 @@ class Kernel extends HttpKernel
*/
protected $middleware = [
CheckForMaintenanceMode::class,
UseDifferentConfigIfE2E::class,
ValidatePostSize::class,
TrimStrings::class,
ConvertEmptyStringsToNull::class,
@ -54,7 +52,6 @@ class Kernel extends HttpKernel
*/
protected $routeMiddleware = [
'auth' => Authenticate::class,
'jwt.auth' => GetUserFromToken::class,
'os.auth' => ObjectStorageAuthenticate::class,
'bindings' => SubstituteBindings::class,
'can' => Authorize::class,

View file

@ -21,7 +21,7 @@ class Authenticate
if ($request->ajax() || $request->route()->getName() === 'play') {
return response('Unauthorized.', 401);
} else {
return redirect()->guest('login');
return redirect()->guest('/');
}
}

View file

@ -1,19 +0,0 @@
<?php
namespace App\Http\Middleware;
use App\JWTAuth;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Routing\ResponseFactory;
use Tymon\JWTAuth\Middleware\BaseMiddleware as JWTBaseMiddleware;
abstract class BaseMiddleware extends JWTBaseMiddleware
{
/**
* {@inheritdoc}
*/
public function __construct(ResponseFactory $response, Dispatcher $events, JWTAuth $auth)
{
parent::__construct($response, $events, $auth);
}
}

View file

@ -1,34 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Tymon\JWTAuth\Exceptions\TokenInvalidException;
class GetUserFromToken extends BaseMiddleware
{
/**
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
if (!$token = $this->auth->setRequest($request)->getToken()) {
return $this->respond('tymon.jwt.absent', 'token_not_provided', 401);
}
try {
$user = $this->auth->authenticate($token);
} catch (TokenInvalidException $exception) {
abort(401, 'Invalid or expired token');
}
if (!$user) {
return $this->respond('tymon.jwt.user_not_found', 'user_not_found', 401);
}
$this->events->dispatch('tymon.jwt.valid', $user);
return $next($request);
}
}

View file

@ -2,6 +2,10 @@
namespace App\Http\Requests\API;
/**
* @property string $email
* @property string $password
*/
class UserLoginRequest extends Request
{
public function rules(): array

View file

@ -1,28 +0,0 @@
<?php
namespace App;
use Illuminate\Http\Request;
use Tymon\JWTAuth\JWTAuth as BaseJWTAuth;
use Tymon\JWTAuth\JWTManager;
use Tymon\JWTAuth\Providers\Auth\AuthInterface;
use Tymon\JWTAuth\Providers\User\UserInterface;
class JWTAuth extends BaseJWTAuth
{
/**
* {@inheritdoc}
*/
public function __construct(JWTManager $manager, UserInterface $user, AuthInterface $auth, Request $request)
{
parent::__construct($manager, $user, $auth, $request);
}
/**
* {@inheritdoc}
*/
public function parseToken($method = 'bearer', $header = 'authorization', $query = 'jwt-token'): BaseJWTAuth
{
return parent::parseToken($method, $header, $query);
}
}

View file

@ -3,6 +3,7 @@
namespace App\Models;
use App\Traits\SupportsDeleteWhereIDsNotIn;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -28,6 +29,7 @@ use function App\Helpers\album_cover_url;
*
* @method static self firstOrCreate(array $where, array $params = [])
* @method static self|null find(int $id)
* @method static Builder where(...$params)
*/
class Album extends Model
{

View file

@ -4,6 +4,7 @@ namespace App\Models;
use App\Facades\Util;
use App\Traits\SupportsDeleteWhereIDsNotIn;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
@ -23,6 +24,7 @@ use function App\Helpers\artist_image_url;
*
* @method static self find(int $id)
* @method static self firstOrCreate(array $where, array $params = [])
* @method static Builder where(...$params)
*/
class Artist extends Model
{

View file

@ -16,6 +16,7 @@ use Illuminate\Support\Collection;
* @property bool $is_smart
* @property string $name
* @property user $user
* @method static \Illuminate\Database\Eloquent\Collection orderBy(string $field, string $order = 'asc')
*/
class Playlist extends Model
{

View file

@ -5,6 +5,7 @@ namespace App\Models;
use App\Events\LibraryChanged;
use App\Traits\SupportsDeleteWhereIDsNotIn;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@ -30,7 +31,9 @@ use Illuminate\Support\Collection;
* @method static Builder select(string $string)
* @method static Builder inDirectory(string $path)
* @method static self first()
* @method static Collection orderBy(...$args)
* @method static EloquentCollection orderBy(...$args)
* @method static int count()
* @method static self|null find($id)
*/
class Song extends Model
{
@ -57,6 +60,8 @@ class Song extends Model
'disc' => 'int',
];
protected $keyType = 'string';
/**
* Indicates if the IDs are auto-incrementing.
*
@ -234,7 +239,7 @@ class Song extends Model
return null;
}
list($bucket, $key) = explode('/', $matches[1], 2);
[$bucket, $key] = explode('/', $matches[1], 2);
return compact('bucket', 'key');
}

View file

@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
/**
* @property array $preferences
@ -22,6 +23,7 @@ use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use Notifiable;
use HasApiTokens;
/**
* The preferences that we don't want to show to the client.

View file

@ -43,4 +43,9 @@ abstract class AbstractRepository implements RepositoryInterface
{
return $this->model->all();
}
public function getFirstWhere(...$params): Model
{
return $this->model->where(...$params)->first();
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Services;
use App\Models\User;
use Laravel\Sanctum\NewAccessToken;
class TokenManager
{
public function createToken(User $user, array $abilities = ['*']): NewAccessToken
{
return $user->createToken(config('app.name'), $abilities);
}
public function destroyTokens(User $user): void
{
$user->tokens()->delete();
}
}

View file

@ -11,9 +11,7 @@
|
*/
$app = new App\Application(
realpath(__DIR__.'/../')
);
$app = new App\Application(__DIR__.'/../');
/*
|--------------------------------------------------------------------------

View file

@ -5,15 +5,13 @@
"license": "MIT",
"type": "project",
"require": {
"php": ">=7.1.3",
"laravel/framework": "5.8.*",
"php": ">=7.2.5",
"laravel/framework": "^7.0",
"james-heinrich/getid3": "^1.9",
"guzzlehttp/guzzle": "^6.1",
"tymon/jwt-auth": "^0.5.12",
"aws/aws-sdk-php-laravel": "^3.1",
"pusher/pusher-php-server": "^4.0",
"predis/predis": "~1.0",
"doctrine/dbal": "^2.5",
"jackiedo/dotenv-editor": "^1.0",
"ext-exif": "*",
"ext-fileinfo": "*",
@ -22,24 +20,24 @@
"fideloper/proxy": "^4.0",
"daverandom/resume": "^0.0.3",
"laravel/helpers": "^1.0",
"cweagans/composer-patches": "^1.6",
"intervention/image": "^2.5"
"intervention/image": "^2.5",
"laravel/sanctum": "^2.6",
"doctrine/dbal": "^2.10"
},
"require-dev": {
"filp/whoops": "~2.0",
"fzaninotto/faker": "~1.4",
"mockery/mockery": "~1.0",
"phpunit/phpunit": "~7.5",
"symfony/css-selector": "~3.1",
"symfony/dom-crawler": "^3.2",
"phpunit/phpunit": "^8.5",
"symfony/css-selector": "~5.0",
"symfony/dom-crawler": "^5.0",
"facebook/webdriver": "^1.2",
"barryvdh/laravel-ide-helper": "^2.1",
"laravel/tinker": "^1.0",
"laravel/browser-kit-testing": "^2.0",
"laravel/tinker": "^2.0",
"laravel/browser-kit-testing": "^6.0",
"mikey179/vfsstream": "^1.6",
"php-mock/php-mock-mockery": "^1.3",
"mpociot/laravel-apidoc-generator": "^3.1",
"nunomaduro/larastan": "^0.4.0"
"mpociot/laravel-apidoc-generator": "^4.1"
},
"suggest": {
"ext-zip": "Allow downloading multiple songs as Zip archives"
@ -83,7 +81,7 @@
"post-create-project-cmd": [
"@php artisan key:generate"
],
"test": "phpunit --colors=always --order-by=defects --stop-on-defect",
"test": "phpunit --colors=always --order-by=defects",
"coverage": "phpunit --colors=always --coverage-clover=coverage.xml",
"analyze": "phpstan analyse app --level=5",
"gen-api-docs": "@php artisan apidoc:generate"
@ -93,12 +91,5 @@
"optimize-autoloader": true
},
"minimum-stability": "stable",
"prefer-stable": false,
"extra": {
"patches": {
"tymon/jwt-auth": {
"Replace fire() with dispatch()": ".patches/tymon/jwt-auth/replace-fire-with-dispatch.patch"
}
}
}
"prefer-stable": false
}

4654
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -4,6 +4,7 @@ return [
'tagline' => 'Personal audio streaming service that works.',
'env' => env('APP_ENV', 'production'),
'name' => 'Koel',
/*
|--------------------------------------------------------------------------
@ -123,7 +124,6 @@ return [
Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
'Tymon\JWTAuth\Providers\JWTAuthServiceProvider', // hard-coding to make it compatible with patching procedure
Aws\Laravel\AwsServiceProvider::class,
Jackiedo\DotenvEditor\DotenvEditorServiceProvider::class,
Intervention\Image\ImageServiceProvider::class,
@ -195,8 +195,6 @@ return [
'Util' => App\Facades\Util::class,
'YouTube' => App\Facades\YouTube::class,
'Download' => App\Facades\Download::class,
'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class,
'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class,
'AWS' => Aws\Laravel\AwsFacade::class,
'iTunes' => App\Facades\iTunes::class,

View file

@ -13,7 +13,7 @@ return [
|
*/
'defaults' => [
'guard' => 'web',
'guard' => 'api',
'passwords' => 'users',
],
/*
@ -38,7 +38,7 @@ return [
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'driver' => 'sanctum',
'provider' => 'users',
],
],

View file

@ -1,164 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| JWT Authentication Secret
|--------------------------------------------------------------------------
|
| Don't forget to set this, as it will be used to sign your tokens.
| A helper command is provided for this: `php artisan jwt:generate`
|
*/
'secret' => env('JWT_SECRET'),
/*
|--------------------------------------------------------------------------
| JWT time to live
|--------------------------------------------------------------------------
|
| Specify the length of time (in minutes) that the token will be valid for.
| Defaults to 1 hour
|
*/
'ttl' => 60 * 24 * 7,
/*
|--------------------------------------------------------------------------
| Refresh time to live
|--------------------------------------------------------------------------
|
| Specify the length of time (in minutes) that the token can be refreshed
| within. I.E. The user can refresh their token within a 2 week window of
| the original token being created until they must re-authenticate.
| Defaults to 2 weeks
|
*/
'refresh_ttl' => 20160,
/*
|--------------------------------------------------------------------------
| JWT hashing algorithm
|--------------------------------------------------------------------------
|
| Specify the hashing algorithm that will be used to sign the token.
|
| See here: https://github.com/namshi/jose/tree/2.2.0/src/Namshi/JOSE/Signer
| for possible values
|
*/
'algo' => 'HS256',
/*
|--------------------------------------------------------------------------
| User Model namespace
|--------------------------------------------------------------------------
|
| Specify the full namespace to your User model.
| e.g. 'Acme\Entities\User'
|
*/
'user' => App\Models\User::class,
/*
|--------------------------------------------------------------------------
| User identifier
|--------------------------------------------------------------------------
|
| Specify a unique property of the user that will be added as the 'sub'
| claim of the token payload.
|
*/
'identifier' => 'id',
/*
|--------------------------------------------------------------------------
| Required Claims
|--------------------------------------------------------------------------
|
| Specify the required claims that must exist in any token.
| A TokenInvalidException will be thrown if any of these claims are not
| present in the payload.
|
*/
'required_claims' => ['iss', 'iat', 'exp', 'nbf', 'sub', 'jti'],
/*
|--------------------------------------------------------------------------
| Blacklist Enabled
|--------------------------------------------------------------------------
|
| In order to invalidate tokens, you must have the the blacklist enabled.
| If you do not want or need this functionality, then set this to false.
|
*/
'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true),
/*
|--------------------------------------------------------------------------
| Providers
|--------------------------------------------------------------------------
|
| Specify the various providers used throughout the package.
|
*/
'providers' => [
/*
|--------------------------------------------------------------------------
| User Provider
|--------------------------------------------------------------------------
|
| Specify the provider that is used to find the user based
| on the subject claim
|
*/
'user' => Tymon\JWTAuth\Providers\User\EloquentUserAdapter::class,
/*
|--------------------------------------------------------------------------
| JWT Provider
|--------------------------------------------------------------------------
|
| Specify the provider that is used to create and decode the tokens.
|
*/
'jwt' => Tymon\JWTAuth\Providers\JWT\NamshiAdapter::class,
/*
|--------------------------------------------------------------------------
| Authentication Provider
|--------------------------------------------------------------------------
|
| Specify the provider that is used to authenticate users.
|
*/
'auth' => 'Tymon\JWTAuth\Providers\Auth\IlluminateAuthAdapter',
/*
|--------------------------------------------------------------------------
| Storage Provider
|--------------------------------------------------------------------------
|
| Specify the provider that is used to store tokens in the blacklist
|
*/
'storage' => 'Tymon\JWTAuth\Providers\Storage\IlluminateCacheAdapter',
],
];

45
config/sanctum.php Normal file
View file

@ -0,0 +1,45 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Stateful Domains
|--------------------------------------------------------------------------
|
| Requests from the following domains / hosts will receive stateful API
| authentication cookies. Typically, these should include your local
| and production domains which access your API via a frontend SPA.
|
*/
'stateful' => [],
/*
|--------------------------------------------------------------------------
| Expiration Minutes
|--------------------------------------------------------------------------
|
| This value controls the number of minutes until an issued token will be
| considered expired. If this value is null, personal access tokens do
| not expire. This won't tweak the lifetime of first-party sessions.
|
*/
'expiration' => null,
/*
|--------------------------------------------------------------------------
| Sanctum Middleware
|--------------------------------------------------------------------------
|
| When authenticating your first-party SPA with Sanctum you may need to
| customize some of the middleware Sanctum uses while processing the
| request. You may change the middleware listed below as required.
|
*/
'middleware' => [
],
];

View file

@ -8,22 +8,23 @@ use App\Models\Setting;
use App\Models\Song;
use App\Models\User;
use Faker\Generator as Faker;
use Illuminate\Database\Eloquent\Factory;
use Illuminate\Support\Facades\Hash;
/** @var Factory $factory */
$factory->define(User::class, function ($faker) {
return [
'name' => $faker->name,
'email' => $faker->email,
'password' => bcrypt(str_random(10)),
'password' => Hash::make('secret'),
'is_admin' => false,
'preferences' => [],
'remember_token' => str_random(10),
];
});
$factory->defineAs(User::class, 'admin', function () use ($factory) {
$user = $factory->raw(User::class);
return array_merge($user, ['is_admin' => true]);
$factory->state(User::class, 'admin', function () use ($factory) {
return ['is_admin' => true];
});
$factory->define(Artist::class, function (Faker $faker) {
@ -35,7 +36,9 @@ $factory->define(Artist::class, function (Faker $faker) {
$factory->define(Album::class, function (Faker $faker) {
return [
'artist_id' => factory(Artist::class)->create()->id,
'artist_id' => static function (): int {
return factory(Artist::class)->create()->id;
},
'name' => ucwords($faker->words(random_int(2, 5), true)),
'cover' => md5(uniqid()).'.jpg',
];
@ -60,7 +63,7 @@ $factory->define(Song::class, function (Faker $faker) {
$factory->define(Playlist::class, function (Faker $faker) {
return [
'user_id' => static function (): int {
throw new InvalidArgumentException('A user_id must be supplied');
return factory(User::class)->create()->id;
},
'name' => $faker->name,
'rules' => null,
@ -69,8 +72,12 @@ $factory->define(Playlist::class, function (Faker $faker) {
$factory->define(Interaction::class, function (Faker $faker) {
return [
'song_id' => factory(Song::class)->create()->id,
'user_id' => factory(User::class)->create()->id,
'song_id' => static function (): string {
return factory(Song::class)->create()->id;
},
'user_id' => static function (): int {
return factory(User::class)->create()->id;
},
'liked' => $faker->boolean,
'play_count' => $faker->randomNumber,
];

View file

@ -2,6 +2,8 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class CreateAlbumsTable extends Migration
{
@ -18,7 +20,9 @@ class CreateAlbumsTable extends Migration
$table->string('name');
$table->string('cover')->default('');
$table->timestamps();
});
Schema::table('albums', function (Blueprint $table) {
$table->foreign('artist_id')->references('id')->on('artists')->onDelete('cascade');
});
}

View file

@ -2,6 +2,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateSongsTable extends Migration
{
@ -13,7 +14,7 @@ class CreateSongsTable extends Migration
public function up()
{
Schema::create('songs', function (Blueprint $table) {
$table->string('id', 32);
$table->string('id', 32)->primary();
$table->integer('album_id')->unsigned();
$table->string('title');
$table->float('length');
@ -21,8 +22,9 @@ class CreateSongsTable extends Migration
$table->text('path');
$table->integer('mtime');
$table->timestamps();
});
$table->primary('id');
Schema::table('songs', function (Blueprint $table) {
$table->foreign('album_id')->references('id')->on('albums');
});
}

View file

@ -2,6 +2,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePlaylistsTable extends Migration
{
@ -17,7 +18,9 @@ class CreatePlaylistsTable extends Migration
$table->integer('user_id')->unsigned();
$table->string('name');
$table->timestamps();
});
Schema::table('playlists', function (Blueprint $table) {
$table->foreign('user_id')->references('id')->on('users');
});
}

View file

@ -2,6 +2,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateInteractionsTable extends Migration
{
@ -19,7 +20,9 @@ class CreateInteractionsTable extends Migration
$table->boolean('liked')->default(false);
$table->integer('play_count')->default(0);
$table->timestamps();
});
Schema::table('interactions', function (Blueprint $table) {
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('song_id')->references('id')->on('songs')->onDelete('cascade');
});

View file

@ -2,6 +2,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePlaylistSongTable extends Migration
{
@ -16,7 +17,9 @@ class CreatePlaylistSongTable extends Migration
$table->increments('id');
$table->integer('playlist_id')->unsigned();
$table->string('song_id', 32);
});
Schema::table('playlist_song', function (Blueprint $table) {
$table->foreign('playlist_id')->references('id')->on('playlists')->onDelete('cascade');
$table->foreign('song_id')->references('id')->on('songs')->onDelete('cascade');
});

View file

@ -1,6 +1,7 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class DropIsComplicationFromAlbums extends Migration
@ -12,7 +13,7 @@ class DropIsComplicationFromAlbums extends Migration
*/
public function up()
{
Schema::table('albums', function ($table) {
Schema::table('albums', function (Blueprint $table) {
$table->dropColumn('is_compilation');
});
}

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePersonalAccessTokensTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->bigIncrements('id');
$table->morphs('tokenable');
$table->string('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('personal_access_tokens');
}
}

View file

@ -31,7 +31,6 @@
<env name="APP_ENV" value="testing"/>
<env name="APP_URL" value="http://localhost/"/>
<env name="APP_KEY" value="16efa6c23c2e8c705826b0e66778fbe7"/>
<env name="JWT_SECRET" value="ki8jSvMf5wFrlSRBAWcGbmAzBUJfc8p8"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
@ -44,5 +43,7 @@
<env name="ADMIN_PASSWORD" value="SoSecureK0el"/>
<env name="BROADCAST_DRIVER" value="log"/>
<env name="CACHE_MEDIA" value="true"/>
<ini name="memory_limit" value="512M" />
</php>
</phpunit>

View file

@ -6,7 +6,7 @@ Route::group(['namespace' => 'API'], function () {
Route::post('me', 'AuthController@login')->name('auth.login');
Route::delete('me', 'AuthController@logout');
Route::group(['middleware' => 'jwt.auth'], function () {
Route::group(['middleware' => 'auth'], function () {
Route::get('/ping', function () {
// Only acting as a ping service.
});

View file

@ -1,5 +1,7 @@
<?php
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('index');
});

View file

@ -7,11 +7,9 @@ use App\Models\Album;
use App\Models\User;
use App\Services\MediaMetadataService;
use Mockery;
use Mockery\MockInterface;
class AlbumCoverTest extends TestCase
{
/** @var MockInterface|MediaMetadataService */
private $mediaMetadataService;
public function setUp(): void
@ -23,7 +21,9 @@ class AlbumCoverTest extends TestCase
public function testUpdate(): void
{
$this->expectsEvents(LibraryChanged::class);
factory(Album::class)->create(['id' => 9999]);
/** @var Album $album */
$album = factory(Album::class)->create(['id' => 9999]);
$this->mediaMetadataService
->shouldReceive('writeAlbumCover')
@ -32,23 +32,25 @@ class AlbumCoverTest extends TestCase
return $album->id === 9999;
}), 'Foo', 'jpeg');
$this->putAsUser('api/album/9999/cover', [
'cover' => 'data:image/jpeg;base64,Rm9v'
], factory(User::class, 'admin')->create())
->seeStatusCode(200);
$response = $this->putAsUser('api/album/'.$album->id.'/cover', [
'cover' => 'data:image/jpeg;base64,Rm9v',
], factory(User::class)->states('admin')->create());
$response->assertStatus(200);
}
public function testUpdateNotAllowedForNormalUsers(): void
{
factory(Album::class)->create(['id' => 9999]);
/** @var Album $album */
$album = factory(Album::class)->create();
$this->mediaMetadataService
->shouldReceive('writeAlbumCover')
->never();
$this->putAsUser('api/album/9999/cover', [
'cover' => 'data:image/jpeg;base64,Rm9v'
$this->putAsUser('api/album/'.$album->id.'/cover', [
'cover' => 'data:image/jpeg;base64,Rm9v',
], factory(User::class)->create())
->seeStatusCode(403);
->assertStatus(403);
}
}

View file

@ -5,14 +5,9 @@ namespace Tests\Feature;
use App\Models\Album;
use App\Services\MediaMetadataService;
use Mockery;
use Mockery\MockInterface;
use function App\Helpers\album_cover_url;
class AlbumThumbnailTest extends TestCase
{
/**
* @var MediaMetadataService|MockInterface
*/
private $mediaMetadataService;
public function setUp(): void
@ -26,9 +21,7 @@ class AlbumThumbnailTest extends TestCase
return [['http://localhost/public/img/covers/foo_thumbnail.jpg'], [null]];
}
/**
* @dataProvider provideAlbumThumbnailData
*/
/** @dataProvider provideAlbumThumbnailData */
public function testGetAlbumThumbnail(?string $thumbnailUrl): void
{
/** @var Album $createdAlbum */
@ -42,7 +35,7 @@ class AlbumThumbnailTest extends TestCase
}))
->andReturn($thumbnailUrl);
$this->getAsUser("api/album/{$createdAlbum->id}/thumbnail")
->seeJson(['thumbnailUrl' => $thumbnailUrl]);
$response = $this->getAsUser("api/album/{$createdAlbum->id}/thumbnail");
$response->assertJson(['thumbnailUrl' => $thumbnailUrl]);
}
}

View file

@ -34,8 +34,8 @@ class ArtistImageTest extends TestCase
$this->putAsUser('api/artist/9999/image', [
'image' => 'data:image/jpeg;base64,Rm9v'
], factory(User::class, 'admin')->create())
->seeStatusCode(200);
], factory(User::class)->states('admin')->create())
->assertStatus(200);
}
public function testUpdateNotAllowedForNormalUsers(): void
@ -49,6 +49,6 @@ class ArtistImageTest extends TestCase
$this->putAsUser('api/artist/9999/image', [
'image' => 'data:image/jpeg;base64,Rm9v'
], factory(User::class)->create())
->seeStatusCode(403);
->assertStatus(403);
}
}

View file

@ -11,6 +11,7 @@ use App\Repositories\InteractionRepository;
use App\Services\DownloadService;
use Exception;
use Illuminate\Support\Collection;
use Illuminate\Testing\TestResponse;
use Mockery;
use Mockery\MockInterface;
@ -44,7 +45,7 @@ class DownloadTest extends TestCase
->andReturn($this->mediaPath.'/blank.mp3');
$this->getAsUser("api/download/songs?songs[]={$song->id}")
->assertResponseOk();
->assertOk();
}
public function testDownloadMultipleSongs(): void
@ -63,7 +64,7 @@ class DownloadTest extends TestCase
->andReturn($this->mediaPath.'/blank.mp3'); // should be a zip file, but we're testing here…
$this->getAsUser("api/download/songs?songs[]={$songs[0]->id}&songs[]={$songs[1]->id}")
->assertResponseOk();
->assertOk();
}
public function testDownloadAlbum(): void
@ -79,7 +80,7 @@ class DownloadTest extends TestCase
->andReturn($this->mediaPath.'/blank.mp3');
$this->getAsUser("api/download/album/{$album->id}")
->assertResponseOk();
->assertOk();
}
public function testDownloadArtist(): void
@ -95,11 +96,12 @@ class DownloadTest extends TestCase
->andReturn($this->mediaPath.'/blank.mp3');
$this->getAsUser("api/download/artist/{$artist->id}")
->assertResponseOk();
->assertOk();
}
public function testDownloadPlaylist(): void
{
/** @var User $user */
$user = factory(User::class)->create();
/** @var Playlist $playlist */
@ -107,9 +109,6 @@ class DownloadTest extends TestCase
'user_id' => $user->id,
]);
$this->getAsUser("api/download/playlist/{$playlist->id}")
->assertResponseStatus(403);
$this->downloadService
->shouldReceive('from')
->with(Mockery::on(static function (Playlist $retrievedPlaylist) use ($playlist) {
@ -119,7 +118,16 @@ class DownloadTest extends TestCase
->andReturn($this->mediaPath.'/blank.mp3');
$this->getAsUser("api/download/playlist/{$playlist->id}", $user)
->assertResponseOk();
->assertOk();
}
public function testNonOwnerCannotDownloadPlaylist(): void
{
/** @var Playlist $playlist */
$playlist = factory(Playlist::class)->create();
$this->getAsUser("api/download/playlist/{$playlist->id}")
->assertStatus(403);
}
public function testDownloadFavorites(): void
@ -143,6 +151,6 @@ class DownloadTest extends TestCase
->andReturn($this->mediaPath.'/blank.mp3');
$this->getAsUser('api/download/favorites', $user)
->seeStatusCode(200);
->assertStatus(200);
}
}

View file

@ -27,7 +27,7 @@ class InteractionTest extends TestCase
$song = Song::orderBy('id')->first();
$this->postAsUser('api/interaction/play', ['song' => $song->id], $user);
$this->seeInDatabase('interactions', [
self::assertDatabaseHas('interactions', [
'user_id' => $user->id,
'song_id' => $song->id,
'play_count' => 1,
@ -36,7 +36,7 @@ class InteractionTest extends TestCase
// Try again
$this->postAsUser('api/interaction/play', ['song' => $song->id], $user);
$this->seeInDatabase('interactions', [
self::assertDatabaseHas('interactions', [
'user_id' => $user->id,
'song_id' => $song->id,
'play_count' => 2,
@ -57,7 +57,7 @@ class InteractionTest extends TestCase
$song = Song::orderBy('id')->first();
$this->postAsUser('api/interaction/like', ['song' => $song->id], $user);
$this->seeInDatabase('interactions', [
self::assertDatabaseHas('interactions', [
'user_id' => $user->id,
'song_id' => $song->id,
'liked' => 1,
@ -66,7 +66,7 @@ class InteractionTest extends TestCase
// Try again
$this->postAsUser('api/interaction/like', ['song' => $song->id], $user);
$this->seeInDatabase('interactions', [
self::assertDatabaseHas('interactions', [
'user_id' => $user->id,
'song_id' => $song->id,
'liked' => 0,
@ -90,7 +90,7 @@ class InteractionTest extends TestCase
$this->postAsUser('api/interaction/batch/like', ['songs' => $songIds], $user);
foreach ($songs as $song) {
$this->seeInDatabase('interactions', [
self::assertDatabaseHas('interactions', [
'user_id' => $user->id,
'song_id' => $song->id,
'liked' => 1,
@ -100,7 +100,7 @@ class InteractionTest extends TestCase
$this->postAsUser('api/interaction/batch/unlike', ['songs' => $songIds], $user);
foreach ($songs as $song) {
$this->seeInDatabase('interactions', [
self::assertDatabaseHas('interactions', [
'user_id' => $user->id,
'song_id' => $song->id,
'liked' => 0,

View file

@ -4,12 +4,13 @@ namespace Tests\Feature;
use App\Models\User;
use App\Services\LastfmService;
use App\Services\TokenManager;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Log\Logger;
use Laravel\Sanctum\NewAccessToken;
use Mockery;
use Tymon\JWTAuth\JWTAuth;
class LastfmTest extends TestCase
{
@ -28,7 +29,7 @@ class LastfmTest extends TestCase
{
$user = factory(User::class)->create();
$this->postAsUser('api/lastfm/session-key', ['key' => 'foo'], $user)
->assertResponseOk();
->assertOk();
$user = User::find($user->id);
self::assertEquals('foo', $user->lastfm_session_key);
@ -36,13 +37,15 @@ class LastfmTest extends TestCase
public function testConnectToLastfm(): void
{
static::mockIocDependency(JWTAuth::class, [
'parseToken' => null,
'getToken' => 'foo',
]);
/** @var User $user */
$user = factory(User::class)->create();
$token = $user->createToken('Koel')->plainTextToken;
$this->getAsUser('api/lastfm/connect')
->assertRedirectedTo('https://www.last.fm/api/auth/?api_key=foo&cb=http%3A%2F%2Flocalhost%2Fapi%2Flastfm%2Fcallback%3Fjwt-token%3Dfoo');
$this->getAsUser('api/lastfm/connect?api_token='.$token, $user)
->assertRedirect(
'https://www.last.fm/api/auth/?api_key=foo&cb=http%3A%2F%2Flocalhost%2Fapi%2Flastfm%2Fcallback%3Fapi_token%3D'
.urlencode($token)
);
}
public function testRetrieveAndStoreSessionKey(): void
@ -58,7 +61,7 @@ class LastfmTest extends TestCase
$this->getAsUser('api/lastfm/callback?token=foo', $user);
$user->refresh();
$this->assertEquals('bar', $user->lastfm_session_key);
self::assertEquals('bar', $user->lastfm_session_key);
}
public function testDisconnectUser(): void
@ -68,6 +71,6 @@ class LastfmTest extends TestCase
$this->deleteAsUser('api/lastfm/disconnect', [], $user);
$user->refresh();
$this->assertNull($user->lastfm_session_key);
self::assertNull($user->lastfm_session_key);
}
}

View file

@ -38,45 +38,45 @@ class MediaSyncTest extends TestCase
$this->mediaService->sync($this->mediaPath);
// Standard mp3 files under root path should be recognized
$this->seeInDatabase('songs', [
self::assertDatabaseHas('songs', [
'path' => $this->mediaPath.'/full.mp3',
// Track # should be recognized
'track' => 5,
]);
// Ogg files and audio files in subdirectories should be recognized
$this->seeInDatabase('songs', ['path' => $this->mediaPath.'/subdir/back-in-black.ogg']);
self::assertDatabaseHas('songs', ['path' => $this->mediaPath.'/subdir/back-in-black.ogg']);
// GitHub issue #380. folder.png should be copied and used as the cover for files
// under subdir/
$song = Song::wherePath($this->mediaPath.'/subdir/back-in-black.ogg')->first();
$this->assertNotNull($song->album->cover);
self::assertNotNull($song->album->cover);
// File search shouldn't be case-sensitive.
$this->seeInDatabase('songs', ['path' => $this->mediaPath.'/subdir/no-name.mp3']);
self::assertDatabaseHas('songs', ['path' => $this->mediaPath.'/subdir/no-name.mp3']);
// Non-audio files shouldn't be recognized
$this->notSeeInDatabase('songs', ['path' => $this->mediaPath.'/rubbish.log']);
self::assertDatabaseMissing('songs', ['path' => $this->mediaPath.'/rubbish.log']);
// Broken/corrupted audio files shouldn't be recognized
$this->notSeeInDatabase('songs', ['path' => $this->mediaPath.'/fake.mp3']);
self::assertDatabaseMissing('songs', ['path' => $this->mediaPath.'/fake.mp3']);
// Artists should be created
$this->seeInDatabase('artists', ['name' => 'Cuckoo']);
$this->seeInDatabase('artists', ['name' => 'Koel']);
self::assertDatabaseHas('artists', ['name' => 'Cuckoo']);
self::assertDatabaseHas('artists', ['name' => 'Koel']);
// Albums should be created
$this->seeInDatabase('albums', ['name' => 'Koel Testing Vol. 1']);
self::assertDatabaseHas('albums', ['name' => 'Koel Testing Vol. 1']);
// Albums and artists should be correctly linked
$album = Album::whereName('Koel Testing Vol. 1')->first();
$this->assertEquals('Koel', $album->artist->name);
self::assertEquals('Koel', $album->artist->name);
// Compilation albums, artists and songs must be recognized
$song = Song::whereTitle('This song belongs to a compilation')->first();
$this->assertNotNull($song->artist_id);
$this->assertTrue($song->album->is_compilation);
$this->assertEquals(Artist::VARIOUS_ID, $song->album->artist_id);
self::assertNotNull($song->artist_id);
self::assertTrue($song->album->is_compilation);
self::assertEquals(Artist::VARIOUS_ID, $song->album->artist_id);
$currentCover = $album->cover;
@ -86,10 +86,10 @@ class MediaSyncTest extends TestCase
touch($song->path, $time = time());
$this->mediaService->sync($this->mediaPath);
$song = Song::find($song->id);
$this->assertEquals($time, $song->mtime);
self::assertEquals($time, $song->mtime);
// Albums with a non-default cover should have their covers overwritten
$this->assertEquals($currentCover, Album::find($album->id)->cover);
self::assertEquals($currentCover, Album::find($album->id)->cover);
}
/**
@ -118,16 +118,16 @@ class MediaSyncTest extends TestCase
// Validate that the changes are not lost
$song = Song::orderBy('id', 'desc')->first();
$this->assertEquals("It's John Cena!", $song->title);
$this->assertEquals('Booom Wroooom', $song->lyrics);
self::assertEquals("It's John Cena!", $song->title);
self::assertEquals('Booom Wroooom', $song->lyrics);
// Resync with force
$this->mediaService->sync($this->mediaPath, [], true);
// All is lost.
$song = Song::orderBy('id', 'desc')->first();
$this->assertEquals($originalTitle, $song->title);
$this->assertEquals($originalLyrics, $song->lyrics);
self::assertEquals($originalTitle, $song->title);
self::assertEquals($originalLyrics, $song->lyrics);
}
/**
@ -155,8 +155,8 @@ class MediaSyncTest extends TestCase
// Validate that the specified tags are changed, other remains the same
$song = Song::orderBy('id', 'desc')->first();
$this->assertEquals($originalTitle, $song->title);
$this->assertEquals('Booom Wroooom', $song->lyrics);
self::assertEquals($originalTitle, $song->title);
self::assertEquals('Booom Wroooom', $song->lyrics);
}
/**
@ -181,7 +181,7 @@ class MediaSyncTest extends TestCase
$song = $song->toArray();
array_forget($addedSong, 'created_at');
array_forget($song, 'created_at');
$this->assertEquals($song, $addedSong);
self::assertEquals($song, $addedSong);
}
/**
@ -197,7 +197,7 @@ class MediaSyncTest extends TestCase
$this->mediaService->syncByWatchRecord(new InotifyWatchRecord("CLOSE_WRITE,CLOSE $path"));
$this->seeInDatabase('songs', ['path' => $path]);
self::assertDatabaseHas('songs', ['path' => $path]);
}
/**
@ -214,7 +214,7 @@ class MediaSyncTest extends TestCase
$this->mediaService->syncByWatchRecord(new InotifyWatchRecord("DELETE {$song->path}"));
$this->notSeeInDatabase('songs', ['id' => $song->id]);
self::assertDatabaseMissing('songs', ['id' => $song->id]);
}
/**
@ -230,9 +230,9 @@ class MediaSyncTest extends TestCase
$this->mediaService->syncByWatchRecord(new InotifyWatchRecord("MOVED_FROM,ISDIR {$this->mediaPath}/subdir"));
$this->notSeeInDatabase('songs', ['path' => $this->mediaPath.'/subdir/sic.mp3']);
$this->notSeeInDatabase('songs', ['path' => $this->mediaPath.'/subdir/no-name.mp3']);
$this->notSeeInDatabase('songs', ['path' => $this->mediaPath.'/subdir/back-in-black.mp3']);
self::assertDatabaseMissing('songs', ['path' => $this->mediaPath.'/subdir/sic.mp3']);
self::assertDatabaseMissing('songs', ['path' => $this->mediaPath.'/subdir/no-name.mp3']);
self::assertDatabaseMissing('songs', ['path' => $this->mediaPath.'/subdir/back-in-black.mp3']);
}
/** @test */
@ -270,10 +270,10 @@ class MediaSyncTest extends TestCase
{
config(['koel.ignore_dot_files' => false]);
$this->mediaService->sync($this->mediaPath);
$this->seeInDatabase('albums', ['name' => 'Hidden Album']);
self::assertDatabaseHas('albums', ['name' => 'Hidden Album']);
config(['koel.ignore_dot_files' => true]);
$this->mediaService->sync($this->mediaPath);
$this->notSeeInDatabase('albums', ['name' => 'Hidden Album']);
self::assertDatabaseMissing('albums', ['name' => 'Hidden Album']);
}
}

View file

@ -30,7 +30,9 @@ class S3Test extends TestCase
'duration' => 10,
'track' => 5,
],
])->seeInDatabase('songs', ['path' => 's3://koel/sample.mp3']);
]);
self::assertDatabaseHas('songs', ['path' => 's3://koel/sample.mp3']);
}
public function testRemovingASong(): void
@ -44,6 +46,8 @@ class S3Test extends TestCase
$this->delete('api/os/s3/song', [
'bucket' => 'koel',
'key' => 'sample.mp3',
])->notSeeInDatabase('songs', ['path' => 's3://koel/sample.mp3']);
]);
self::assertDatabaseMissing('songs', ['path' => 's3://koel/sample.mp3']);
}
}

View file

@ -5,13 +5,9 @@ namespace Tests\Feature;
use App\Models\Playlist;
use App\Models\Song;
use App\Models\User;
use Exception;
class PlaylistTest extends TestCase
{
/**
* @throws Exception
*/
public function setUp(): void
{
parent::setUp();
@ -20,6 +16,7 @@ class PlaylistTest extends TestCase
public function testCreatingPlaylist(): void
{
/** @var User $user */
$user = factory(User::class)->create();
$songs = Song::orderBy('id')->take(3)->get();
@ -29,7 +26,7 @@ class PlaylistTest extends TestCase
'rules' => [],
], $user);
$this->seeInDatabase('playlists', [
self::assertDatabaseHas('playlists', [
'user_id' => $user->id,
'name' => 'Foo Bar',
]);
@ -37,45 +34,46 @@ class PlaylistTest extends TestCase
$playlist = Playlist::orderBy('id', 'desc')->first();
foreach ($songs as $song) {
$this->seeInDatabase('playlist_song', [
self::assertDatabaseHas('playlist_song', [
'playlist_id' => $playlist->id,
'song_id' => $song->id,
]);
}
$this->getAsUser('api/playlist', $user)
->seeJson([
'id' => $playlist->id,
'name' => 'Foo Bar',
]);
}
/** @test */
public function user_can_update_a_playlists_name(): void
public function testUpdatePlaylistName(): void
{
/** @var User $user */
$user = factory(User::class)->create();
/** @var Playlist $playlist */
$playlist = factory(Playlist::class)->create([
'user_id' => $user->id,
'name' => 'Foo',
]);
$this->putAsUser("api/playlist/{$playlist->id}", ['name' => 'Foo Bar'], $user);
$this->putAsUser("api/playlist/{$playlist->id}", ['name' => 'Bar'], $user);
$this->seeInDatabase('playlists', [
'user_id' => $user->id,
'name' => 'Foo Bar',
]);
// Other users can't modify it
$this->putAsUser("api/playlist/{$playlist->id}", ['name' => 'Foo Bar'])
->seeStatusCode(403);
self::assertSame('Bar', $playlist->refresh()->name);
}
/** @test */
public function playlists_can_be_synced(): void
public function testNonOwnerCannotUpdatePlaylist(): void
{
/** @var Playlist $playlist */
$playlist = factory(Playlist::class)->create([
'name' => 'Foo',
]);
$response = $this->putAsUser("api/playlist/{$playlist->id}", ['name' => 'Qux']);
$response->assertStatus(403);
}
public function testSyncPlaylist(): void
{
/** @var User $user */
$user = factory(User::class)->create();
/** @var Playlist $playlist */
$playlist = factory(Playlist::class)->create([
'user_id' => $user->id,
]);
@ -85,50 +83,53 @@ class PlaylistTest extends TestCase
$removedSong = $songs->pop();
$this->putAsUser("api/playlist/{$playlist->id}/sync", [
'songs' => $songs->pluck('id')->toArray(),
])
->seeStatusCode(403);
$this->putAsUser("api/playlist/{$playlist->id}/sync", [
'songs' => $songs->pluck('id')->toArray(),
], $user);
// We should still see the first 3 songs, but not the removed one
foreach ($songs as $song) {
$this->seeInDatabase('playlist_song', [
self::assertDatabaseHas('playlist_song', [
'playlist_id' => $playlist->id,
'song_id' => $song->id,
]);
}
$this->notSeeInDatabase('playlist_song', [
self::assertDatabaseMissing('playlist_song', [
'playlist_id' => $playlist->id,
'song_id' => $removedSong->id,
]);
}
/** @test */
public function user_can_delete_a_playlist(): void
public function testDeletePlaylist(): void
{
/** @var User $user */
$user = factory(User::class)->create();
/** @var Playlist $playlist */
$playlist = factory(Playlist::class)->create([
'user_id' => $user->id,
]);
$this->deleteAsUser("api/playlist/{$playlist->id}")
->seeStatusCode(403);
$this->deleteAsUser("api/playlist/{$playlist->id}", [], $user)
->notSeeInDatabase('playlists', ['id' => $playlist->id]);
$this->deleteAsUser("api/playlist/{$playlist->id}", [], $user);
self::assertDatabaseMissing('playlists', ['id' => $playlist->id]);
}
/** @test */
public function playlist_content_can_be_retrieved(): void
public function testNonOwnerCannotDeletePlaylist(): void
{
/** @var Playlist $playlist */
$playlist = factory(Playlist::class)->create();
$this->deleteAsUser("api/playlist/{$playlist->id}")
->assertStatus(403);
}
public function testGetPlaylist(): void
{
/** @var User $user */
$user = factory(User::class)->create();
/** @var Playlist $playlist */
$playlist = factory(Playlist::class)->create([
'user_id' => $user->id,
]);
@ -137,6 +138,6 @@ class PlaylistTest extends TestCase
$playlist->songs()->saveMany($songs);
$this->getAsUser("api/playlist/{$playlist->id}/songs", $user)
->seeJson($songs->pluck('id')->all());
->assertJson($songs->pluck('id')->all());
}
}

View file

@ -26,7 +26,7 @@ class ProfileTest extends TestCase
$this->putAsUser('api/me', ['name' => 'Foo', 'email' => 'bar@baz.com'], $user);
$this->seeInDatabase('users', ['name' => 'Foo', 'email' => 'bar@baz.com']);
self::assertDatabaseHas('users', ['name' => 'Foo', 'email' => 'bar@baz.com']);
}
public function testUpdateProfileWithPassword(): void
@ -46,7 +46,7 @@ class ProfileTest extends TestCase
'password' => 'qux',
], $user);
$this->seeInDatabase('users', [
self::assertDatabaseHas('users', [
'id' => $user->id,
'name' => 'Foo',
'email' => 'bar@baz.com',

View file

@ -30,6 +30,6 @@ class ScrobbleTest extends TestCase
->once();
$this->postAsUser("/api/{$song->id}/scrobble/$ts", [], $user)
->assertResponseOk();
->assertOk();
}
}

View file

@ -5,11 +5,9 @@ namespace Tests\Feature;
use App\Models\Setting;
use App\Models\User;
use App\Services\MediaSyncService;
use Mockery\MockInterface;
class SettingTest extends TestCase
{
/** @var MockInterface */
private $mediaSyncService;
public function setUp(): void
@ -22,9 +20,8 @@ class SettingTest extends TestCase
{
$this->mediaSyncService->shouldReceive('sync')->once();
$user = factory(User::class, 'admin')->create();
file_put_contents('log', $this->postAsUser('/api/settings', ['media_path' => __DIR__], $user)
->response->content());
$user = factory(User::class)->states('admin')->create();
$this->postAsUser('/api/settings', ['media_path' => __DIR__], $user);
self::assertEquals(__DIR__, Setting::get('media_path'));
}

View file

@ -29,7 +29,7 @@ class SongTest extends TestCase
$this->expectsEvents(LibraryChanged::class);
$song = Song::orderBy('id', 'desc')->first();
$user = factory(User::class, 'admin')->create();
$user = factory(User::class)->states('admin')->create();
$this->putAsUser('/api/songs', [
'songs' => [$song->id],
'data' => [
@ -41,15 +41,15 @@ class SongTest extends TestCase
'compilationState' => 0,
],
], $user)
->seeStatusCode(200);
->assertStatus(200);
$artist = Artist::whereName('John Cena')->first();
$this->assertNotNull($artist);
$artist = Artist::where('name', 'John Cena')->first();
self::assertNotNull($artist);
$album = Album::whereName('One by One')->first();
$this->assertNotNull($album);
$album = Album::where('name', 'One by One')->first();
self::assertNotNull($album);
$this->seeInDatabase('songs', [
self::assertDatabaseHas('songs', [
'id' => $song->id,
'album_id' => $album->id,
'lyrics' => 'Lorem ipsum dolor sic amet.',
@ -62,7 +62,7 @@ class SongTest extends TestCase
$song = Song::orderBy('id', 'desc')->first();
$originalArtistId = $song->album->artist->id;
$user = factory(User::class, 'admin')->create();
$user = factory(User::class)->states('admin')->create();
$this->putAsUser('/api/songs', [
'songs' => [$song->id],
'data' => [
@ -74,20 +74,20 @@ class SongTest extends TestCase
'compilationState' => 0,
],
], $user)
->seeStatusCode(200);
->assertStatus(200);
// We don't expect the song's artist to change
$this->assertEquals($originalArtistId, Song::find($song->id)->album->artist->id);
self::assertEquals($originalArtistId, Song::find($song->id)->album->artist->id);
// But we expect a new album to be created for this artist and contain this song
$this->assertEquals('One by One', Song::find($song->id)->album->name);
self::assertEquals('One by One', Song::find($song->id)->album->name);
}
public function testMultipleUpdateAllInfoNoCompilation(): void
{
$songIds = Song::orderBy('id', 'desc')->take(3)->pluck('id')->toArray();
$user = factory(User::class, 'admin')->create();
$user = factory(User::class)->states('admin')->create();
$this->putAsUser('/api/songs', [
'songs' => $songIds,
'data' => [
@ -99,24 +99,24 @@ class SongTest extends TestCase
'compilationState' => 0,
],
], $user)
->seeStatusCode(200);
->assertStatus(200);
$songs = Song::orderBy('id', 'desc')->take(3)->get();
// Even though we post the title, lyrics, and tracks, we don't expect them to take any effect
// because we're updating multiple songs here.
$this->assertNotEquals('foo', $songs[0]->title);
$this->assertNotEquals('bar', $songs[2]->lyrics);
$this->assertNotEquals(9999, $songs[2]->track);
self::assertNotEquals('foo', $songs[0]->title);
self::assertNotEquals('bar', $songs[2]->lyrics);
self::assertNotEquals(9999, $songs[2]->track);
// But all of these songs must now belong to a new album and artist set
$this->assertEquals('One by One', $songs[0]->album->name);
$this->assertEquals('One by One', $songs[1]->album->name);
$this->assertEquals('One by One', $songs[2]->album->name);
self::assertEquals('One by One', $songs[0]->album->name);
self::assertEquals('One by One', $songs[1]->album->name);
self::assertEquals('One by One', $songs[2]->album->name);
$this->assertEquals('John Cena', $songs[0]->album->artist->name);
$this->assertEquals('John Cena', $songs[1]->album->artist->name);
$this->assertEquals('John Cena', $songs[2]->album->artist->name);
self::assertEquals('John Cena', $songs[0]->album->artist->name);
self::assertEquals('John Cena', $songs[1]->album->artist->name);
self::assertEquals('John Cena', $songs[2]->album->artist->name);
}
public function testMultipleUpdateSomeInfoNoCompilation(): void
@ -124,7 +124,7 @@ class SongTest extends TestCase
$originalSongs = Song::orderBy('id', 'desc')->take(3)->get();
$songIds = $originalSongs->pluck('id')->toArray();
$user = factory(User::class, 'admin')->create();
$user = factory(User::class)->states('admin')->create();
$this->putAsUser('/api/songs', [
'songs' => $songIds,
'data' => [
@ -136,30 +136,30 @@ class SongTest extends TestCase
'compilationState' => 0,
],
], $user)
->seeStatusCode(200);
->assertStatus(200);
$songs = Song::orderBy('id', 'desc')->take(3)->get();
// Even though the album name doesn't change, a new artist should have been created
// and thus, a new album with the same name was created as well.
$this->assertEquals($songs[0]->album->name, $originalSongs[0]->album->name);
$this->assertNotEquals($songs[0]->album->id, $originalSongs[0]->album->id);
$this->assertEquals($songs[1]->album->name, $originalSongs[1]->album->name);
$this->assertNotEquals($songs[1]->album->id, $originalSongs[1]->album->id);
$this->assertEquals($songs[2]->album->name, $originalSongs[2]->album->name);
$this->assertNotEquals($songs[2]->album->id, $originalSongs[2]->album->id);
self::assertEquals($songs[0]->album->name, $originalSongs[0]->album->name);
self::assertNotEquals($songs[0]->album->id, $originalSongs[0]->album->id);
self::assertEquals($songs[1]->album->name, $originalSongs[1]->album->name);
self::assertNotEquals($songs[1]->album->id, $originalSongs[1]->album->id);
self::assertEquals($songs[2]->album->name, $originalSongs[2]->album->name);
self::assertNotEquals($songs[2]->album->id, $originalSongs[2]->album->id);
// And of course, the new artist is...
$this->assertEquals('John Cena', $songs[0]->album->artist->name); // JOHN CENA!!!
$this->assertEquals('John Cena', $songs[1]->album->artist->name); // JOHN CENA!!!
$this->assertEquals('John Cena', $songs[2]->album->artist->name); // And... JOHN CENAAAAAAAAAAA!!!
self::assertEquals('John Cena', $songs[0]->album->artist->name); // JOHN CENA!!!
self::assertEquals('John Cena', $songs[1]->album->artist->name); // JOHN CENA!!!
self::assertEquals('John Cena', $songs[2]->album->artist->name); // And... JOHN CENAAAAAAAAAAA!!!
}
public function testSingleUpdateAllInfoYesCompilation(): void
{
$song = Song::orderBy('id', 'desc')->first();
$user = factory(User::class, 'admin')->create();
$user = factory(User::class)->states('admin')->create();
$this->putAsUser('/api/songs', [
'songs' => [$song->id],
'data' => [
@ -171,15 +171,15 @@ class SongTest extends TestCase
'compilationState' => 1,
],
], $user)
->seeStatusCode(200);
->assertStatus(200);
$compilationAlbum = Album::whereArtistIdAndName(Artist::VARIOUS_ID, 'One by One')->first();
$this->assertNotNull($compilationAlbum);
self::assertNotNull($compilationAlbum);
$artist = Artist::whereName('John Cena')->first();
$this->assertNotNull($artist);
self::assertNotNull($artist);
$this->seeInDatabase('songs', [
self::assertDatabaseHas('songs', [
'id' => $song->id,
'artist_id' => $artist->id,
'album_id' => $compilationAlbum->id,
@ -200,15 +200,20 @@ class SongTest extends TestCase
'compilationState' => 2,
],
], $user)
->seeStatusCode(200);
->assertStatus(200);
$compilationAlbum = Album::whereArtistIdAndName(Artist::VARIOUS_ID, 'Two by Two')->first();
$this->assertNotNull($compilationAlbum);
/** @var Album $compilationAlbum */
$compilationAlbum = Album::where([
'artist_id' => Artist::VARIOUS_ID,
'name' => 'Two by Two'
])->first();
$contributingArtist = Artist::whereName('John Cena')->first();
$this->assertNotNull($contributingArtist);
self::assertNotNull($compilationAlbum);
$this->seeInDatabase('songs', [
$contributingArtist = Artist::where('name', 'John Cena')->first();
self::assertNotNull($contributingArtist);
self::assertDatabaseHas('songs', [
'id' => $song->id,
'artist_id' => $contributingArtist->id,
'album_id' => $compilationAlbum->id,
@ -226,15 +231,19 @@ class SongTest extends TestCase
'compilationState' => 2,
],
], $user)
->seeStatusCode(200);
->assertStatus(200);
$compilationAlbum = Album::whereArtistIdAndName(Artist::VARIOUS_ID, 'One by One')->first();
$this->assertNotNull($compilationAlbum);
$compilationAlbum = Album::where([
'artist_id' => Artist::VARIOUS_ID,
'name' => 'One by One'
])->first();
self::assertNotNull($compilationAlbum);
$contributingArtist = Artist::whereName('Foo Fighters')->first();
$this->assertNotNull($contributingArtist);
/** @var Artist $contributingArtist */
$contributingArtist = Artist::where('name', 'Foo Fighters')->first();
self::assertNotNull($contributingArtist);
$this->seeInDatabase('songs', [
self::assertDatabaseHas('songs', [
'id' => $song->id,
'artist_id' => $contributingArtist->id,
'album_id' => $compilationAlbum->id,
@ -252,14 +261,19 @@ class SongTest extends TestCase
'compilationState' => 0,
],
], $user)
->seeStatusCode(200);
->assertStatus(200);
$artist = Artist::whereName('Foo Fighters')->first();
$this->assertNotNull($artist);
$album = Album::whereArtistIdAndName($artist->id, 'One by One')->first();
$this->assertNotNull($album);
/** @var Artist $artist */
$artist = Artist::where('name', 'Foo Fighters')->first();
self::assertNotNull($artist);
$this->seeInDatabase('songs', [
$album = Album::where([
'artist_id' => $artist->id,
'name' => 'One by One',
])->first();
self::assertNotNull($album);
self::assertDatabaseHas('songs', [
'id' => $song->id,
'artist_id' => $artist->id,
'album_id' => $album->id,
@ -277,25 +291,29 @@ class SongTest extends TestCase
'track' => 1,
'compilationState' => 1,
],
], $user)
->putAsUser('/api/songs', [
'songs' => [$song->id],
'data' => [
'title' => 'Twilight of the Thunder God',
'artistName' => 'Amon Amarth',
'albumName' => 'Twilight of the Thunder God',
'lyrics' => 'Thor! Nanananananana Batman.',
'track' => 1,
'compilationState' => 0,
],
], $user)
->seeStatusCode(200);
], $user);
$artist = Artist::whereName('Amon Amarth')->first();
$this->assertNotNull($artist);
$album = Album::whereArtistIdAndName($artist->id, 'Twilight of the Thunder God')->first();
$this->assertNotNull($album);
$this->seeInDatabase('songs', [
$this->putAsUser('/api/songs', [
'songs' => [$song->id],
'data' => [
'title' => 'Twilight of the Thunder God',
'artistName' => 'Amon Amarth',
'albumName' => 'Twilight of the Thunder God',
'lyrics' => 'Thor! Nanananananana Batman.',
'track' => 1,
'compilationState' => 0,
],
], $user)
->assertStatus(200);
$artist = Artist::where('name', 'Amon Amarth')->first();
self::assertNotNull($artist);
$album = Album::where([
'artist_id' => $artist->id,
'name' => 'Twilight of the Thunder God',
])->first();
self::assertNotNull($album);
self::assertDatabaseHas('songs', [
'id' => $song->id,
'artist_id' => $artist->id,
'album_id' => $album->id,
@ -303,14 +321,11 @@ class SongTest extends TestCase
]);
}
/**
* @throws Exception
*/
public function testDeletingByChunk(): void
{
$this->assertNotEquals(0, Song::count());
self::assertNotEquals(0, Song::count());
$ids = Song::select('id')->get()->pluck('id')->all();
Song::deleteByChunk($ids, 'id', 1);
$this->assertEquals(0, Song::count());
self::assertEquals(0, Song::count());
}
}

View file

@ -8,13 +8,13 @@ use App\Models\Song;
use App\Models\User;
use Exception;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Laravel\BrowserKitTesting\TestCase as BaseTestCase;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Testing\TestResponse;
use Mockery;
use ReflectionClass;
use Tests\Traits\CreatesApplication;
use Tests\Traits\InteractsWithIoc;
use Tests\Traits\SandboxesTests;
use Tymon\JWTAuth\JWTAuth;
abstract class TestCase extends BaseTestCase
{
@ -23,14 +23,10 @@ abstract class TestCase extends BaseTestCase
use InteractsWithIoc;
use SandboxesTests;
/** @var JWTAuth */
private $auth;
public function setUp(): void
{
parent::setUp();
$this->auth = app(JWTAuth::class);
$this->prepareForTests();
self::createSandbox();
}
@ -60,37 +56,33 @@ abstract class TestCase extends BaseTestCase
}
}
protected function getAsUser($url, $user = null): self
private function jsonAsUser(?User $user, string $method, $uri, array $data = [], array $headers = []): TestResponse
{
return $this->get($url, [
'Authorization' => 'Bearer '.$this->generateJwtToken($user),
]);
$user = $user ?: factory(User::class)->create();
$headers['X-Requested-With'] = 'XMLHttpRequest';
$headers['Authorization'] = 'Bearer '.$user->createToken('koel')->plainTextToken;
return parent::json($method, $uri, $data, $headers);
}
protected function deleteAsUser($url, $data = [], $user = null): self
protected function getAsUser(string $url, ?User $user = null): TestResponse
{
return $this->delete($url, $data, [
'Authorization' => 'Bearer '.$this->generateJwtToken($user),
]);
return $this->jsonAsUser($user, 'get', $url);
}
protected function postAsUser($url, $data, $user = null): self
protected function deleteAsUser(string $url, array $data = [], ?User $user = null): TestResponse
{
return $this->post($url, $data, [
'Authorization' => 'Bearer '.$this->generateJwtToken($user),
]);
return $this->jsonAsUser($user, 'delete', $url, $data);
}
protected function putAsUser($url, $data, $user = null): self
protected function postAsUser(string $url, array $data, ?User $user = null): TestResponse
{
return $this->put($url, $data, [
'Authorization' => 'Bearer '.$this->generateJwtToken($user),
]);
return $this->jsonAsUser($user, 'post', $url, $data);
}
private function generateJwtToken(?User $user): string
protected function putAsUser(string $url, array $data, ?User $user = null): TestResponse
{
return $this->auth->fromUser($user ?: factory(User::class)->create());
return $this->jsonAsUser($user, 'put', $url, $data);
}
protected static function getNonPublicProperty($object, string $property)
@ -107,6 +99,7 @@ abstract class TestCase extends BaseTestCase
$this->addToAssertionCount(Mockery::getContainer()->mockery_getExpectationCount());
Mockery::close();
self::destroySandbox();
parent::tearDown();
}
}

View file

@ -39,7 +39,7 @@ class UploadTest extends TestCase
'/api/upload',
['file' => $file],
factory(User::class)->create()
)->seeStatusCode(403);
)->assertStatus(403);
}
public function provideUploadExceptions(): array
@ -67,8 +67,8 @@ class UploadTest extends TestCase
$this->postAsUser(
'/api/upload',
['file' => $file],
factory(User::class, 'admin')->create()
)->seeStatusCode($statusCode);
factory(User::class)->states('admin')->create()
)->assertStatus($statusCode);
}
public function testPost(): void
@ -87,8 +87,8 @@ class UploadTest extends TestCase
$this->postAsUser(
'/api/upload',
['file' => $file],
factory(User::class, 'admin')->create()
)->seeJsonStructure([
factory(User::class)->states('admin')->create()
)->assertJsonStructure([
'album',
'artist',
]);

View file

@ -24,7 +24,7 @@ class UserTest extends TestCase
'email' => 'bar@baz.com',
'password' => 'qux',
'is_admin' => false
])->seeStatusCode(403);
])->assertStatus(403);
}
public function testAdminCreatesUser(): void
@ -40,9 +40,9 @@ class UserTest extends TestCase
'email' => 'bar@baz.com',
'password' => 'qux',
'is_admin' => true
], factory(User::class, 'admin')->create());
], factory(User::class)->states('admin')->create());
self::seeInDatabase('users', [
self::assertDatabaseHas('users', [
'name' => 'Foo',
'email' => 'bar@baz.com',
'password' => 'hashed',
@ -71,9 +71,9 @@ class UserTest extends TestCase
'email' => 'bar@baz.com',
'password' => 'qux',
'is_admin' => false,
], factory(User::class, 'admin')->create());
], factory(User::class)->states('admin')->create());
self::seeInDatabase('users', [
self::assertDatabaseHas('users', [
'id' => $user->id,
'name' => 'Foo',
'email' => 'bar@baz.com',
@ -84,32 +84,35 @@ class UserTest extends TestCase
public function testAdminDeletesUser(): void
{
/** @var User $user */
$user = factory(User::class)->create();
$admin = factory(User::class, 'admin')->create();
$admin = factory(User::class)->states('admin')->create();
$this->deleteAsUser("api/user/{$user->id}", [], $admin)
->notSeeInDatabase('users', ['id' => $user->id]);
$this->deleteAsUser("api/user/{$user->id}", [], $admin);
self::assertDatabaseMissing('users', ['id' => $user->id]);
}
public function testSeppukuNotAllowed(): void
{
$admin = factory(User::class, 'admin')->create();
/** @var User $admin */
$admin = factory(User::class)->states('admin')->create();
// A user can't delete himself
$this->deleteAsUser("api/user/{$admin->id}", [], $admin)
->seeStatusCode(403)
->seeInDatabase('users', ['id' => $admin->id]);
->assertStatus(403);
self::assertDatabaseHas('users', ['id' => $admin->id]);
}
public function testUpdateUserProfile(): void
{
$user = factory(User::class)->create();
$this->assertNull($user->getPreference('foo'));
self::assertNull($user->getPreference('foo'));
$user->setPreference('foo', 'bar');
$this->assertEquals('bar', $user->getPreference('foo'));
self::assertEquals('bar', $user->getPreference('foo'));
$user->deletePreference('foo');
$this->assertNull($user->getPreference('foo'));
self::assertNull($user->getPreference('foo'));
}
}

View file

@ -36,6 +36,6 @@ class YouTubeTest extends TestCase
->once();
$this->getAsUser("/api/youtube/search/song/{$song->id}?pageToken=foo")
->assertResponseOk();
->assertOk();
}
}

View file

@ -1,52 +0,0 @@
<?php
namespace Tests\Integration\Commands;
use App\Console\Commands\GenerateJwtSecretCommand;
use App\Console\Kernel;
use Jackiedo\DotenvEditor\DotenvEditor;
use Mockery;
use Mockery\MockInterface;
use Tests\TestCase;
class GenerateJwtSecretCommandTest extends TestCase
{
/** @var DotenvEditor|MockInterface */
private $dotenvEditor;
/** @var GenerateJwtSecretCommand */
private $command;
public function setUp(): void
{
parent::setUp();
$this->dotenvEditor = static::mockIocDependency(DotenvEditor::class);
$this->command = app(GenerateJwtSecretCommand::class);
app(Kernel::class)->registerCommand($this->command);
}
public function testGenerateJwtSecret(): void
{
config(['jwt.secret' => false]);
$this->dotenvEditor
->shouldReceive('setKey')
->with('JWT_SECRET', Mockery::on(function ($key) {
return preg_match('/[a-f0-9]{32}$/i', $key);
}));
$this->artisan('koel:generate-jwt-secret');
}
public function testNotRegenerateJwtSecret(): void
{
config(['jwt.secret' => '12345678901234567890123456789012']);
$this->dotenvEditor
->shouldReceive('setKey')
->never();
$this->artisan('koel:generate-jwt-secret');
}
}

View file

@ -23,7 +23,7 @@ class AlbumTest extends TestCase
$gottenAlbum = Album::get($artist, $album->name);
// Then I get the album
$this->assertSame($album->id, $gottenAlbum->id);
self::assertSame($album->id, $gottenAlbum->id);
}
/** @test */
@ -34,13 +34,13 @@ class AlbumTest extends TestCase
$name = 'Foo';
// And an album with such details doesn't exist yet
$this->assertNull(Album::whereArtistIdAndName($artist->id, $name)->first());
self::assertNull(Album::whereArtistIdAndName($artist->id, $name)->first());
// When I try to get the album by such artist and name
$album = Album::get($artist, $name);
// Then I get the new album
$this->assertNotNull($album);
self::assertNotNull($album);
}
/** @test */
@ -53,7 +53,7 @@ class AlbumTest extends TestCase
$album = Album::get(factory(Artist::class)->create(), $name);
// Then the album's name is "Unknown Album"
$this->assertEquals('Unknown Album', $album->name);
self::assertEquals('Unknown Album', $album->name);
}
/** @test */
@ -66,6 +66,6 @@ class AlbumTest extends TestCase
$album = Album::get(factory(Artist::class)->create(), 'Foo', $isCompilation);
// Then its artist is Various Artist
$this->assertTrue($album->artist->is_various);
self::assertTrue($album->artist->is_various);
}
}

View file

@ -18,7 +18,7 @@ class ArtistTest extends TestCase
$gottenArtist = Artist::get('Foo');
// Then I get the artist
$this->assertEquals($artist->id, $gottenArtist->id);
self::assertEquals($artist->id, $gottenArtist->id);
}
/** @test */
@ -28,13 +28,13 @@ class ArtistTest extends TestCase
$name = 'Foo';
// And an artist with such a name doesn't exist yet
$this->assertNull(Artist::whereName($name)->first());
self::assertNull(Artist::whereName($name)->first());
// When I get the artist by name
$artist = Artist::get($name);
// Then I get the newly created artist
$this->assertInstanceOf(Artist::class, $artist);
self::assertInstanceOf(Artist::class, $artist);
}
/** @test */
@ -47,7 +47,7 @@ class ArtistTest extends TestCase
$artist = Artist::get($name);
// Then I get the artist as Unknown Artist
$this->assertTrue($artist->is_unknown);
self::assertTrue($artist->is_unknown);
}
/** @test */
@ -61,6 +61,6 @@ class ArtistTest extends TestCase
$retrieved = Artist::get($name);
// Then I receive the artist
$this->assertEquals($artist->id, $retrieved->id);
self::assertEquals($artist->id, $retrieved->id);
}
}

View file

@ -18,7 +18,7 @@ class SettingTest extends TestCase
Setting::set($key, $value);
// Then I see the key and serialized value in the database
$this->assertDatabaseHas('settings', [
self::assertDatabaseHas('settings', [
'key' => 'foo',
'value' => serialize('bar'),
]);
@ -37,7 +37,7 @@ class SettingTest extends TestCase
Setting::set($settings);
// Then I see all settings the database
$this->assertDatabaseHas('settings', [
self::assertDatabaseHas('settings', [
'key' => 'foo',
'value' => serialize('bar'),
])->assertDatabaseHas('settings', [
@ -52,7 +52,7 @@ class SettingTest extends TestCase
Setting::set('foo', 'bar');
Setting::set('foo', 'baz');
$this->assertEquals('baz', Setting::get('foo'));
self::assertEquals('baz', Setting::get('foo'));
}
/** @test */
@ -68,6 +68,6 @@ class SettingTest extends TestCase
$value = Setting::get('foo');
// Then I receive the value in an unserialized format
$this->assertSame('bar', $value);
self::assertSame('bar', $value);
}
}

View file

@ -20,7 +20,7 @@ class SongTest extends TestCase
$lyrics = $song->lyrics;
// Then I see the new line characters replaced by <br />
$this->assertEquals('foo<br />bar<br />baz<br />qux', $lyrics);
self::assertEquals('foo<br />bar<br />baz<br />qux', $lyrics);
}
/** @test */
@ -34,6 +34,6 @@ class SongTest extends TestCase
$params = $song->s3_params;
// Then I receive the correct parameters
$this->assertEquals(['bucket' => 'foo', 'key' => 'bar'], $params);
self::assertEquals(['bucket' => 'foo', 'key' => 'bar'], $params);
}
}

View file

@ -18,8 +18,8 @@ class SongZipArchiveTest extends TestCase
$songZipArchive = new SongZipArchive();
$songZipArchive->addSong($song);
$this->assertEquals(1, $songZipArchive->getArchive()->numFiles);
$this->assertEquals('full.mp3', $songZipArchive->getArchive()->getNameIndex(0));
self::assertEquals(1, $songZipArchive->getArchive()->numFiles);
self::assertEquals('full.mp3', $songZipArchive->getArchive()->getNameIndex(0));
}
/** @test */
@ -37,8 +37,8 @@ class SongZipArchiveTest extends TestCase
$songZipArchive = new SongZipArchive();
$songZipArchive->addSongs($songs);
$this->assertEquals(2, $songZipArchive->getArchive()->numFiles);
$this->assertEquals('full.mp3', $songZipArchive->getArchive()->getNameIndex(0));
$this->assertEquals('lorem.mp3', $songZipArchive->getArchive()->getNameIndex(1));
self::assertEquals(2, $songZipArchive->getArchive()->numFiles);
self::assertEquals('full.mp3', $songZipArchive->getArchive()->getNameIndex(0));
self::assertEquals('lorem.mp3', $songZipArchive->getArchive()->getNameIndex(1));
}
}

View file

@ -7,65 +7,43 @@ use Tests\TestCase;
class UserTest extends TestCase
{
/** @test */
public function user_preferences_can_be_set()
public function testSetUserPreferences(): void
{
// Given a user
/** @var User $user */
$user = factory(User::class)->create();
// When I see the user's preference
$user->setPreference('foo', 'bar');
// Then I see the preference properly set
$this->assertArraySubset(['foo' => 'bar'], $user->preferences);
self::assertSame('bar', $user->preferences['foo']);
}
/** @test */
public function user_preferences_can_be_retrieved()
public function testGetUserPreferences(): void
{
// Given a user with some preferences
/** @var User $user */
$user = factory(User::class)->create([
'preferences' => ['foo' => 'bar'],
]);
// When I get a preference by its key
$value = $user->getPreference('foo');
// Then I retrieve the preference value
$this->assertEquals('bar', $value);
self::assertEquals('bar', $user->getPreference('foo'));
}
/** @test */
public function user_preferences_can_be_deleted()
public function testDeleteUserPreferences(): void
{
// Given a user with some preferences
/** @var User $user */
$user = factory(User::class)->create([
'preferences' => ['foo' => 'bar'],
]);
// When I delete the preference by its key
$user->deletePreference('foo');
// Then I see the preference gets deleted
$this->assertArrayNotHasKey('foo', $user->preferences);
self::assertArrayNotHasKey('foo', $user->preferences);
}
/** @test */
public function sensitive_preferences_are_hidden()
public function testSensitivePreferencesAreHidden(): void
{
// Given a user with sensitive preferences
/** @var User $user */
$user = factory(User::class)->create([
'preferences' => ['lastfm_session_key' => 'foo'],
]);
// When I try to access the sensitive preferences
$value = $user->preferences['lastfm_session_key'];
// Then the sensitive preferences are replaced with "hidden"
$this->assertEquals('hidden', $value);
self::assertEquals('hidden', $user->preferences['lastfm_session_key']);
}
}

View file

@ -44,7 +44,7 @@ class FileSynchronizerTest extends TestCase
];
self::assertArraySubset($expectedData, $info);
self::assertEquals(10.083, $info['length'], '', 0.001);
self::assertEqualsWithDelta(10.083, $info['length'], 0.001);
}
/** @test */

View file

@ -31,7 +31,7 @@ class LastfmServiceTest extends TestCase
$api = new LastfmService($client, app(Cache::class), app(Logger::class));
$info = $api->getArtistInformation($artist->name);
$this->assertEquals([
self::assertEquals([
'url' => 'https://www.last.fm/music/Kamelot',
'image' => 'http://foo.bar/extralarge.jpg',
'bio' => [
@ -78,7 +78,7 @@ class LastfmServiceTest extends TestCase
$info = $api->getAlbumInformation($album->name, $album->artist->name);
// Then I get the album's info
$this->assertEquals([
self::assertEquals([
'url' => 'https://www.last.fm/music/Kamelot/Epica',
'image' => 'http://foo.bar/extralarge.jpg',
'tracks' => [

View file

@ -43,9 +43,9 @@ class MediaCacheServiceTest extends TestCase
$data = $this->mediaCacheService->get();
$this->assertCount(6, $data['albums']); // 5 new albums and the default Unknown Album
$this->assertCount(7, $data['artists']); // 5 new artists and the default Various and Unknown Artist
$this->assertCount(5, $data['songs']);
self::assertCount(6, $data['albums']); // 5 new albums and the default Unknown Album
self::assertCount(7, $data['artists']); // 5 new artists and the default Various and Unknown Artist
self::assertCount(5, $data['songs']);
}
public function testGetIfCacheIsAvailable(): void
@ -56,7 +56,7 @@ class MediaCacheServiceTest extends TestCase
$data = $this->mediaCacheService->get();
$this->assertEquals(['dummy'], $data);
self::assertEquals(['dummy'], $data);
}
public function testCacheDisabled(): void

View file

@ -24,7 +24,7 @@ class MediaMetadataServiceTest extends TestCase
self::assertSame(
album_cover_url('album-cover-for-thumbnail-test_thumb.jpg'),
app()->get(MediaMetadataService::class)->getAlbumThumbnailUrl($album)
app(MediaMetadataService::class)->getAlbumThumbnailUrl($album)
);
self::assertFileExists(album_cover_path('album-cover-for-thumbnail-test_thumb.jpg'));
@ -33,7 +33,7 @@ class MediaMetadataServiceTest extends TestCase
public function testGetAlbumThumbnailUrlWithNoCover(): void
{
$album = factory(Album::class)->create(['cover' => null]);
self::assertNull(app()->get(MediaMetadataService::class)->getAlbumThumbnailUrl($album));
self::assertNull(app(MediaMetadataService::class)->getAlbumThumbnailUrl($album));
}
private function cleanUp(): void

View file

@ -111,11 +111,11 @@ class SmartPlaylistServiceTest extends TestCase
public function testBuildQueryForRules(array $rules, string $sql, array $bindings): void
{
$query = $this->service->buildQueryFromRules($rules);
$this->assertSame($sql, $query->toSql());
self::assertSame($sql, $query->toSql());
$queryBinding = $query->getBindings();
for ($i = 0, $count = count($queryBinding); $i < $count; $i++) {
$this->assertSame(
self::assertSame(
$bindings[$i],
is_object($queryBinding[$i]) ? (string) $queryBinding[$i] : $queryBinding[$i]
);
@ -129,7 +129,7 @@ class SmartPlaylistServiceTest extends TestCase
/** @var User $user */
$user = factory(User::class)->create();
$this->assertEquals([
self::assertEquals([
'model' => 'interactions.user_id',
'operator' => 'is',
'value' => [$user->id],
@ -152,6 +152,6 @@ class SmartPlaylistServiceTest extends TestCase
}
}
$this->assertSame(count(Rule::VALID_OPERATORS), count(array_unique($operators)));
self::assertSame(count(Rule::VALID_OPERATORS), count(array_unique($operators)));
}
}

View file

@ -28,7 +28,7 @@ class YouTubeServiceTest extends TestCase
$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);
$this->assertNotNull(cache('1492972ec5c8e6b3a9323ba719655ddb'));
self::assertEquals('Slipknot - Snuff [OFFICIAL VIDEO]', $response->items[0]->snippet->title);
self::assertNotNull(cache('1492972ec5c8e6b3a9323ba719655ddb'));
}
}

View file

@ -36,7 +36,7 @@ trait CreatesApplication
{
$this->artisan->call('migrate');
if (!User::all()->count()) {
if (!User::count()) {
$this->artisan->call('db:seed');
}
}

View file

@ -23,8 +23,8 @@ class ApplicationTest extends TestCase
$assetURL = app()->staticUrl('/foo.css ');
// Then I see they're constructed correctly
$this->assertEquals('http://localhost/', $root);
$this->assertEquals('http://localhost/foo.css', $assetURL);
self::assertEquals('http://localhost/', $root);
self::assertEquals('http://localhost/foo.css', $assetURL);
}
/** @test */
@ -38,8 +38,8 @@ class ApplicationTest extends TestCase
$assetURL = app()->staticUrl('/foo.css ');
// Then I see they're constructed correctly
$this->assertEquals('http://cdn.tld/', $root);
$this->assertEquals('http://cdn.tld/foo.css', $assetURL);
self::assertEquals('http://cdn.tld/', $root);
self::assertEquals('http://cdn.tld/foo.css', $assetURL);
}
/** @test */
@ -55,7 +55,7 @@ class ApplicationTest extends TestCase
$assetURL = app()->rev('/foo.css', $manifestFile);
// Then I see they're constructed correctly
$this->assertEquals('http://localhost/public/foo00.css', $assetURL);
self::assertEquals('http://localhost/public/foo00.css', $assetURL);
}
/** @test */
@ -71,6 +71,6 @@ class ApplicationTest extends TestCase
$assetURL = app()->rev('/foo.css', $manifestFile);
// Then I see they're constructed correctly
$this->assertEquals('http://cdn.tld/public/foo00.css', $assetURL);
self::assertEquals('http://cdn.tld/public/foo00.css', $assetURL);
}
}

View file

@ -44,7 +44,7 @@ class ForceHttpsTest extends TestCase
return $request;
};
$this->assertSame($request, $this->middleware->handle($request, $next));
self::assertSame($request, $this->middleware->handle($request, $next));
}
public function testNotHandle(): void
@ -60,6 +60,6 @@ class ForceHttpsTest extends TestCase
return $request;
};
$this->assertSame($request, $this->middleware->handle($request, $next));
self::assertSame($request, $this->middleware->handle($request, $next));
}
}

View file

@ -26,7 +26,7 @@ class LastfmServiceTest extends TestCase
$builtParamsAsString = $lastfm->buildAuthCallParams($params, true);
// Then I receive the Last.fm-compatible API parameters
$this->assertEquals([
self::assertEquals([
'api_key' => 'key',
'bar' => 'baz',
'qux' => '安',
@ -34,7 +34,7 @@ class LastfmServiceTest extends TestCase
], $builtParams);
// And the string version as well
$this->assertEquals(
self::assertEquals(
'api_key=key&bar=baz&qux=安&api_sig=7f21233b54edea994aa0f23cf55f18a2',
$builtParamsAsString
);

View file

@ -42,7 +42,7 @@ class MediaMetadataServiceTest extends TestCase
->with('/koel/public/images/album/foo.jpg', 'dummy');
$this->mediaMetadataService->writeAlbumCover($album, $coverContent, 'jpg', $coverPath);
$this->assertEquals(album_cover_url('foo.jpg'), Album::find($album->id)->cover);
self::assertEquals(album_cover_url('foo.jpg'), Album::find($album->id)->cover);
}
public function testWriteArtistImage(): void
@ -58,6 +58,6 @@ class MediaMetadataServiceTest extends TestCase
->with('/koel/public/images/artist/foo.jpg', 'dummy');
$this->mediaMetadataService->writeArtistImage($artist, $imageContent, 'jpg', $imagePath);
$this->assertEquals(artist_image_url('foo.jpg'), Artist::find($artist->id)->image);
self::assertEquals(artist_image_url('foo.jpg'), Artist::find($artist->id)->image);
}
}

View file

@ -19,10 +19,10 @@ class iTunesServiceTest extends TestCase
config(['koel.itunes.enabled' => true]);
/** @var iTunesService $iTunes */
$iTunes = app()->make(iTunesService::class);
$this->assertTrue($iTunes->used());
self::assertTrue($iTunes->used());
config(['koel.itunes.enabled' => false]);
$this->assertFalse($iTunes->used());
self::assertFalse($iTunes->used());
}
public function provideGetTrackUrlData(): array