mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
feat(plus): add song policy tests
This commit is contained in:
parent
9a89828384
commit
cc12618a95
61 changed files with 460 additions and 95 deletions
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\LicenseService;
|
||||
use App\Services\License\LicenseServiceInterface;
|
||||
use Illuminate\Console\Command;
|
||||
use Throwable;
|
||||
|
||||
|
@ -11,7 +11,7 @@ class ActivateLicenseCommand extends Command
|
|||
protected $signature = 'koel:license:activate {key : The license key to activate.}';
|
||||
protected $description = 'Activate a Koel Plus license';
|
||||
|
||||
public function __construct(private LicenseService $licenseService)
|
||||
public function __construct(private LicenseServiceInterface $licenseService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\LicenseService;
|
||||
use App\Services\License\LicenseServiceInterface;
|
||||
use App\Values\LicenseStatus;
|
||||
use Illuminate\Console\Command;
|
||||
use Throwable;
|
||||
|
@ -12,7 +12,7 @@ class CheckLicenseStatusCommand extends Command
|
|||
protected $signature = 'koel:license:status';
|
||||
protected $description = 'Check the current Koel Plus license status.';
|
||||
|
||||
public function __construct(private LicenseService $licenseService)
|
||||
public function __construct(private LicenseServiceInterface $licenseService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\LicenseService;
|
||||
use App\Services\License\LicenseServiceInterface;
|
||||
use Illuminate\Console\Command;
|
||||
use Throwable;
|
||||
|
||||
|
@ -11,7 +11,7 @@ class DeactivateLicenseCommand extends Command
|
|||
protected $signature = 'koel:license:deactivate';
|
||||
protected $description = 'Deactivate the currently active Koel Plus license';
|
||||
|
||||
public function __construct(private LicenseService $plusService)
|
||||
public function __construct(private LicenseServiceInterface $plusService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ class DeactivateLicenseCommand extends Command
|
|||
$this->components->info('Deactivating your license…');
|
||||
|
||||
try {
|
||||
$this->plusService->deactivateLicense($status->license);
|
||||
$this->plusService->deactivate($status->license);
|
||||
$this->components->info('Koel Plus has been deactivated. Plus features are now disabled.');
|
||||
|
||||
return self::SUCCESS;
|
||||
|
|
13
app/Exceptions/MethodNotImplementedException.php
Normal file
13
app/Exceptions/MethodNotImplementedException.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class MethodNotImplementedException extends Exception
|
||||
{
|
||||
public static function method(string $method): self
|
||||
{
|
||||
return new self("Method $method is not implemented.");
|
||||
}
|
||||
}
|
|
@ -2,12 +2,13 @@
|
|||
|
||||
namespace App\Facades;
|
||||
|
||||
use App\Services\License\FakePlusLicenseService;
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
/**
|
||||
* @method static bool isPlus()
|
||||
* @method static bool isCommunity()
|
||||
* @see \App\Services\LicenseService
|
||||
* @see \App\Services\License\LicenseService
|
||||
*/
|
||||
class License extends Facade
|
||||
{
|
||||
|
@ -15,4 +16,9 @@ class License extends Facade
|
|||
{
|
||||
return 'License';
|
||||
}
|
||||
|
||||
public static function fakePlusLicense(): void
|
||||
{
|
||||
self::swap(app(FakePlusLicenseService::class));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ use App\Repositories\SongRepository;
|
|||
use App\Services\ApplicationInformationService;
|
||||
use App\Services\ITunesService;
|
||||
use App\Services\LastfmService;
|
||||
use App\Services\LicenseService;
|
||||
use App\Services\License\LicenseServiceInterface;
|
||||
use App\Services\QueueService;
|
||||
use App\Services\SpotifyService;
|
||||
use App\Services\YouTubeService;
|
||||
|
@ -28,7 +28,7 @@ class FetchInitialDataController extends Controller
|
|||
SongRepository $songRepository,
|
||||
ApplicationInformationService $applicationInformationService,
|
||||
QueueService $queueService,
|
||||
LicenseService $licenseService,
|
||||
LicenseServiceInterface $licenseService,
|
||||
?Authenticatable $user
|
||||
) {
|
||||
$licenseStatus = $licenseService->getStatus();
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace App\Http\Controllers\API;
|
|||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\API\ChangeSongsVisibilityRequest;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use App\Repositories\SongRepository;
|
||||
use App\Services\SongService;
|
||||
|
@ -18,7 +19,7 @@ class MakeSongsPublicController extends Controller
|
|||
SongService $songService,
|
||||
Authenticatable $user
|
||||
) {
|
||||
$songs = $repository->getMany(ids: $request->songs, scopedUser: $user);
|
||||
$songs = Song::find($request->songs);
|
||||
$songs->each(fn ($song) => $this->authorize('own', $song));
|
||||
|
||||
$songService->makeSongsPublic($songs);
|
||||
|
|
|
@ -45,13 +45,15 @@ class SongController extends Controller
|
|||
|
||||
public function show(Song $song)
|
||||
{
|
||||
$this->authorize('access', $song);
|
||||
|
||||
return SongResource::make($this->songRepository->getOne($song->id));
|
||||
}
|
||||
|
||||
public function update(SongUpdateRequest $request)
|
||||
{
|
||||
$this->songRepository->getMany(ids: $request->songs, scopedUser: $this->user)
|
||||
->each(fn (Song $song) => $this->authorize('edit', $song));
|
||||
// Don't use SongRepository::findMany() because it'd be already catered to the current user.
|
||||
Song::find($request->songs)->each(fn (Song $song) => $this->authorize('edit', $song));
|
||||
|
||||
$updatedSongs = $this->songService->updateSongs($request->songs, SongUpdateData::fromRequest($request));
|
||||
$albums = $this->albumRepository->getMany($updatedSongs->pluck('album_id')->toArray());
|
||||
|
@ -73,8 +75,8 @@ class SongController extends Controller
|
|||
|
||||
public function destroy(DeleteSongsRequest $request)
|
||||
{
|
||||
$this->songRepository->getMany(ids: $request->songs, scopedUser: $this->user)
|
||||
->each(fn (Song $song) => $this->authorize('delete', $song));
|
||||
// Don't use SongRepository::findMany() because it'd be already catered to the current user.
|
||||
Song::find($request->songs)->each(fn (Song $song) => $this->authorize('delete', $song));
|
||||
|
||||
$this->songService->deleteSongs($request->songs);
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace App\Http\Requests\API;
|
||||
|
||||
use App\Facades\License;
|
||||
|
||||
/**
|
||||
* @property-read array<string> $songs
|
||||
*/
|
||||
|
@ -14,4 +16,9 @@ class ChangeSongsVisibilityRequest extends Request
|
|||
'songs' => 'required|exists:songs,id',
|
||||
];
|
||||
}
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return License::isPlus();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,8 @@ namespace App\Providers;
|
|||
use App\Services\ApiClients\ApiClient;
|
||||
use App\Services\ApiClients\LemonSqueezyApiClient;
|
||||
use App\Services\LastfmService;
|
||||
use App\Services\LicenseService;
|
||||
use App\Services\License\LicenseService;
|
||||
use App\Services\License\LicenseServiceInterface;
|
||||
use App\Services\MusicEncyclopedia;
|
||||
use App\Services\NullMusicEncyclopedia;
|
||||
use App\Services\SpotifyService;
|
||||
|
@ -48,6 +49,8 @@ class AppServiceProvider extends ServiceProvider
|
|||
return $this->app->get(LastfmService::enabled() ? LastfmService::class : NullMusicEncyclopedia::class);
|
||||
});
|
||||
|
||||
$this->app->bind(LicenseServiceInterface::class, LicenseService::class);
|
||||
|
||||
$this->app->when(LicenseService::class)
|
||||
->needs(ApiClient::class)
|
||||
->give(fn () => $this->app->get(LemonSqueezyApiClient::class));
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Services\LicenseService;
|
||||
use App\Services\License\LicenseServiceInterface;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class LicenseServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
app()->singleton('License', static fn (): LicenseService => app(LicenseService::class));
|
||||
app()->singleton('License', static fn (): LicenseServiceInterface => app(LicenseServiceInterface::class));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Services\ApiClients\LemonSqueezyApiClient;
|
||||
|
||||
class CommunityLicenseService extends LicenseService
|
||||
{
|
||||
public function __construct(LemonSqueezyApiClient $client)
|
||||
{
|
||||
parent::__construct($client, config('app.key'));
|
||||
}
|
||||
|
||||
public function isPlus(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
35
app/Services/License/CommunityLicenseService.php
Normal file
35
app/Services/License/CommunityLicenseService.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\License;
|
||||
|
||||
use App\Exceptions\MethodNotImplementedException;
|
||||
use App\Models\License;
|
||||
use App\Values\LicenseStatus;
|
||||
|
||||
class CommunityLicenseService implements LicenseServiceInterface
|
||||
{
|
||||
public function activate(string $key): License
|
||||
{
|
||||
throw MethodNotImplementedException::method(__METHOD__);
|
||||
}
|
||||
|
||||
public function deactivate(License $license): void
|
||||
{
|
||||
throw MethodNotImplementedException::method(__METHOD__);
|
||||
}
|
||||
|
||||
public function getStatus(): LicenseStatus
|
||||
{
|
||||
throw MethodNotImplementedException::method(__METHOD__);
|
||||
}
|
||||
|
||||
public function isPlus(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isCommunity(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
35
app/Services/License/FakePlusLicenseService.php
Normal file
35
app/Services/License/FakePlusLicenseService.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\License;
|
||||
|
||||
use App\Exceptions\MethodNotImplementedException;
|
||||
use App\Models\License;
|
||||
use App\Values\LicenseStatus;
|
||||
|
||||
class FakePlusLicenseService implements LicenseServiceInterface
|
||||
{
|
||||
public function activate(string $key): License
|
||||
{
|
||||
throw MethodNotImplementedException::method(__METHOD__);
|
||||
}
|
||||
|
||||
public function deactivate(License $license): void
|
||||
{
|
||||
throw MethodNotImplementedException::method(__METHOD__);
|
||||
}
|
||||
|
||||
public function getStatus(): LicenseStatus
|
||||
{
|
||||
throw MethodNotImplementedException::method(__METHOD__);
|
||||
}
|
||||
|
||||
public function isPlus(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isCommunity(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
namespace App\Services\License;
|
||||
|
||||
use App\Exceptions\FailedToActivateLicenseException;
|
||||
use App\Models\License;
|
||||
|
@ -14,7 +14,7 @@ use Illuminate\Support\Facades\Cache;
|
|||
use Illuminate\Support\Facades\Log;
|
||||
use Throwable;
|
||||
|
||||
class LicenseService
|
||||
class LicenseService implements LicenseServiceInterface
|
||||
{
|
||||
public function __construct(private ApiClient $client, private string $hashSalt)
|
||||
{
|
||||
|
@ -41,7 +41,7 @@ class LicenseService
|
|||
}
|
||||
}
|
||||
|
||||
public function deactivateLicense(License $license): void
|
||||
public function deactivate(License $license): void
|
||||
{
|
||||
try {
|
||||
$response = $this->client->post('licenses/deactivate', [
|
19
app/Services/License/LicenseServiceInterface.php
Normal file
19
app/Services/License/LicenseServiceInterface.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\License;
|
||||
|
||||
use App\Models\License;
|
||||
use App\Values\LicenseStatus;
|
||||
|
||||
interface LicenseServiceInterface
|
||||
{
|
||||
public function isPlus(): bool;
|
||||
|
||||
public function isCommunity(): bool;
|
||||
|
||||
public function activate(string $key): License;
|
||||
|
||||
public function deactivate(License $license): void;
|
||||
|
||||
public function getStatus(): LicenseStatus;
|
||||
}
|
|
@ -17,7 +17,7 @@ class InteractionFactory extends Factory
|
|||
return [
|
||||
'song_id' => Song::factory(),
|
||||
'user_id' => User::factory(),
|
||||
'liked' => $this->faker->boolean,
|
||||
'liked' => $this->faker->boolean(),
|
||||
'play_count' => $this->faker->randomNumber(),
|
||||
];
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace Database\Factories;
|
|||
|
||||
use App\Models\Album;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class SongFactory extends Factory
|
||||
|
@ -27,7 +28,19 @@ class SongFactory extends Factory
|
|||
'path' => '/tmp/' . uniqid() . '.mp3',
|
||||
'genre' => $this->faker->randomElement(['Rock', 'Pop', 'Jazz', 'Classical', 'Metal', 'Hip Hop', 'Rap']),
|
||||
'year' => $this->faker->year(),
|
||||
'is_public' => $this->faker->boolean(),
|
||||
'owner_id' => User::factory(),
|
||||
'mtime' => time(),
|
||||
];
|
||||
}
|
||||
|
||||
public function public(): self
|
||||
{
|
||||
return $this->state(fn () => ['is_public' => true]); // @phpcs:ignore
|
||||
}
|
||||
|
||||
public function private(): self
|
||||
{
|
||||
return $this->state(fn () => ['is_public' => false]); // @phpcs:ignore
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<?php
|
||||
|
||||
use App\Facades\License;
|
||||
use App\Facades\YouTube;
|
||||
use App\Http\Controllers\API\AlbumController;
|
||||
use App\Http\Controllers\API\AlbumSongController;
|
||||
|
@ -159,10 +158,8 @@ Route::prefix('api')->middleware('api')->group(static function (): void {
|
|||
Route::post('invitations', [UserInvitationController::class, 'invite']);
|
||||
Route::delete('invitations', [UserInvitationController::class, 'revoke']);
|
||||
|
||||
if (License::isPlus()) {
|
||||
Route::put('songs/make-public', MakeSongsPublicController::class);
|
||||
Route::put('songs/make-private', MakeSongsPrivateController::class);
|
||||
}
|
||||
Route::put('songs/make-public', MakeSongsPublicController::class);
|
||||
Route::put('songs/make-private', MakeSongsPrivateController::class);
|
||||
});
|
||||
|
||||
// Object-storage (S3) routes
|
||||
|
|
|
@ -8,6 +8,7 @@ use App\Models\User;
|
|||
use App\Services\MediaMetadataService;
|
||||
use Mockery;
|
||||
use Mockery\MockInterface;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AlbumCoverTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -6,6 +6,7 @@ use App\Models\Album;
|
|||
use App\Services\MediaInformationService;
|
||||
use App\Values\AlbumInformation;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AlbumInformationTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace Tests\Feature;
|
|||
|
||||
use App\Models\Album;
|
||||
use App\Models\Song;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AlbumSongTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Album;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AlbumTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -6,6 +6,7 @@ use App\Models\Album;
|
|||
use App\Services\MediaMetadataService;
|
||||
use Mockery;
|
||||
use Mockery\MockInterface;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AlbumThumbnailTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace Tests\Feature;
|
|||
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ArtistAlbumTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -8,6 +8,7 @@ use App\Models\User;
|
|||
use App\Services\MediaMetadataService;
|
||||
use Mockery;
|
||||
use Mockery\MockInterface;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ArtistImageTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -6,6 +6,7 @@ use App\Models\Artist;
|
|||
use App\Services\MediaInformationService;
|
||||
use App\Values\ArtistInformation;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ArtistInformationTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace Tests\Feature;
|
|||
|
||||
use App\Models\Artist;
|
||||
use App\Models\Song;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ArtistSongTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Artist;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ArtistTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace Tests\Feature;
|
|||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AuthTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
class DataTest extends TestCase
|
||||
{
|
||||
public function testIndex(): void
|
||||
|
|
|
@ -4,14 +4,15 @@ namespace Tests\Feature;
|
|||
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Models\Interaction;
|
||||
use App\Models\Playlist;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use App\Repositories\InteractionRepository;
|
||||
use App\Services\DownloadService;
|
||||
use Illuminate\Support\Collection;
|
||||
use Mockery;
|
||||
use Mockery\MockInterface;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DownloadTest extends TestCase
|
||||
{
|
||||
|
@ -90,14 +91,18 @@ class DownloadTest extends TestCase
|
|||
/** @var Album $album */
|
||||
$album = Album::query()->first();
|
||||
|
||||
$songs = Song::factory(3)->create(['album_id' => $album->id]);
|
||||
|
||||
/** @var User $user */
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->downloadService
|
||||
->shouldReceive('from')
|
||||
->once()
|
||||
->with(Mockery::on(static function (Album $retrievedAlbum) use ($album): bool {
|
||||
return $retrievedAlbum->id === $album->id;
|
||||
->with(Mockery::on(static function (Collection $retrievedSongs) use ($songs): bool {
|
||||
self::assertEqualsCanonicalizing($retrievedSongs->pluck('id')->all(), $songs->pluck('id')->all());
|
||||
|
||||
return true;
|
||||
}))
|
||||
->andReturn($this->mediaPath . '/blank.mp3');
|
||||
|
||||
|
@ -110,14 +115,18 @@ class DownloadTest extends TestCase
|
|||
/** @var Artist $artist */
|
||||
$artist = Artist::query()->first();
|
||||
|
||||
$songs = Song::factory(3)->create(['artist_id' => $artist->id]);
|
||||
|
||||
/** @var User $user */
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->downloadService
|
||||
->shouldReceive('from')
|
||||
->once()
|
||||
->with(Mockery::on(static function (Artist $retrievedArtist) use ($artist): bool {
|
||||
return $retrievedArtist->id === $artist->id;
|
||||
->with(Mockery::on(static function (Collection $retrievedSongs) use ($songs): bool {
|
||||
self::assertEqualsCanonicalizing($retrievedSongs->pluck('id')->all(), $songs->pluck('id')->all());
|
||||
|
||||
return true;
|
||||
}))
|
||||
->andReturn($this->mediaPath . '/blank.mp3');
|
||||
|
||||
|
@ -130,13 +139,19 @@ class DownloadTest extends TestCase
|
|||
/** @var User $user */
|
||||
$user = User::factory()->create();
|
||||
|
||||
$songs = Song::factory(3)->create();
|
||||
|
||||
/** @var Playlist $playlist */
|
||||
$playlist = Playlist::factory()->for($user)->create();
|
||||
|
||||
$playlist->songs()->attach($songs);
|
||||
|
||||
$this->downloadService
|
||||
->shouldReceive('from')
|
||||
->with(Mockery::on(static function (Playlist $retrievedPlaylist) use ($playlist): bool {
|
||||
return $retrievedPlaylist->id === $playlist->id;
|
||||
->with(Mockery::on(static function (Collection $retrievedSongs) use ($songs): bool {
|
||||
self::assertEqualsCanonicalizing($retrievedSongs->pluck('id')->all(), $songs->pluck('id')->all());
|
||||
|
||||
return true;
|
||||
}))
|
||||
->once()
|
||||
->andReturn($this->mediaPath . '/blank.mp3');
|
||||
|
@ -161,17 +176,16 @@ class DownloadTest extends TestCase
|
|||
{
|
||||
/** @var User $user */
|
||||
$user = User::factory()->create();
|
||||
$favorites = Collection::make();
|
||||
|
||||
self::mock(InteractionRepository::class)
|
||||
->shouldReceive('getUserFavorites')
|
||||
->once()
|
||||
->with(Mockery::on(static fn (User $retrievedUser) => $retrievedUser->is($user)))
|
||||
->andReturn($favorites);
|
||||
$favorites = Interaction::factory(3)->for($user)->create(['liked' => true]);
|
||||
|
||||
$this->downloadService
|
||||
->shouldReceive('from')
|
||||
->with($favorites)
|
||||
->with(Mockery::on(static function (Collection $songs) use ($favorites): bool {
|
||||
self::assertEqualsCanonicalizing($songs->pluck('id')->all(), $favorites->pluck('song_id')->all());
|
||||
|
||||
return true;
|
||||
}))
|
||||
->once()
|
||||
->andReturn($this->mediaPath . '/blank.mp3');
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace Tests\Feature;
|
|||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Models\Song;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExcerptSearchTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace Tests\Feature;
|
|||
|
||||
use App\Models\Interaction;
|
||||
use App\Models\User;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FavoriteSongTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace Tests\Feature;
|
|||
|
||||
use App\Models\Song;
|
||||
use App\Values\Genre;
|
||||
use Tests\TestCase;
|
||||
|
||||
class GenreTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -7,6 +7,7 @@ use App\Events\SongsBatchLiked;
|
|||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use Tests\TestCase;
|
||||
|
||||
class InteractionTest extends TestCase
|
||||
{
|
||||
|
|
110
tests/Feature/KoelPlus/SongTest.php
Normal file
110
tests/Feature/KoelPlus/SongTest.php
Normal file
|
@ -0,0 +1,110 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\KoelPlus;
|
||||
|
||||
use App\Facades\License;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SongTest extends TestCase
|
||||
{
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
License::fakePlusLicense();
|
||||
}
|
||||
|
||||
public function testShowSongPolicy(): void
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = User::factory()->create();
|
||||
|
||||
/** @var Song $publicSong */
|
||||
$publicSong = Song::factory()->public()->create();
|
||||
|
||||
// We can access public songs.
|
||||
$this->getAs('api/songs/' . $publicSong->id, $user)->assertSuccessful();
|
||||
|
||||
/** @var Song $ownPrivateSong */
|
||||
$ownPrivateSong = Song::factory()->for($user, 'owner')->private()->create();
|
||||
|
||||
// We can access our own private songs.
|
||||
$this->getAs('api/songs/' . $ownPrivateSong->id, $user)->assertSuccessful();
|
||||
|
||||
$externalPrivateSong = Song::factory()->private()->create();
|
||||
|
||||
// But we can't access private songs that are not ours.
|
||||
$this->getAs('api/songs/' . $externalPrivateSong->id, $user)->assertForbidden();
|
||||
}
|
||||
|
||||
public function testEditSongsPolicy(): void
|
||||
{
|
||||
/** @var User $currentUser */
|
||||
$currentUser = User::factory()->create();
|
||||
|
||||
/** @var User $anotherUser */
|
||||
$anotherUser = User::factory()->create();
|
||||
|
||||
/** @var Collection<Song> $externalSongs */
|
||||
$externalSongs = Song::factory(3)->for($anotherUser, 'owner')->private()->create();
|
||||
|
||||
// We can't edit songs that are not ours.
|
||||
$this->putAs('api/songs', [
|
||||
'songs' => $externalSongs->pluck('id')->toArray(),
|
||||
'data' => [
|
||||
'title' => 'New Title',
|
||||
],
|
||||
], $currentUser)->assertForbidden();
|
||||
|
||||
// Even if some of the songs are owned by us, we still can't edit them.
|
||||
$mixedSongs = $externalSongs->merge(Song::factory(2)->for($currentUser, 'owner')->create());
|
||||
|
||||
$this->putAs('api/songs', [
|
||||
'songs' => $mixedSongs->pluck('id')->toArray(),
|
||||
'data' => [
|
||||
'title' => 'New Title',
|
||||
],
|
||||
], $currentUser)->assertForbidden();
|
||||
|
||||
// But we can edit our own songs.
|
||||
$ownSongs = Song::factory(3)->for($currentUser, 'owner')->create();
|
||||
|
||||
$this->putAs('api/songs', [
|
||||
'songs' => $ownSongs->pluck('id')->toArray(),
|
||||
'data' => [
|
||||
'title' => 'New Title',
|
||||
],
|
||||
], $currentUser)->assertSuccessful();
|
||||
}
|
||||
|
||||
public function testDeleteSongsPolicy(): void
|
||||
{
|
||||
/** @var User $currentUser */
|
||||
$currentUser = User::factory()->create();
|
||||
|
||||
/** @var User $anotherUser */
|
||||
$anotherUser = User::factory()->create();
|
||||
|
||||
/** @var Collection<Song> $externalSongs */
|
||||
$externalSongs = Song::factory(3)->for($anotherUser, 'owner')->private()->create();
|
||||
|
||||
// We can't delete songs that are not ours.
|
||||
$this->deleteAs('api/songs', ['songs' => $externalSongs->pluck('id')->toArray()], $currentUser)
|
||||
->assertForbidden();
|
||||
|
||||
// Even if some of the songs are owned by us, we still can't delete them.
|
||||
$mixedSongs = $externalSongs->merge(Song::factory(2)->for($currentUser, 'owner')->create());
|
||||
|
||||
$this->deleteAs('api/songs', ['songs' => $mixedSongs->pluck('id')->toArray()], $currentUser)
|
||||
->assertForbidden();
|
||||
|
||||
// But we can delete our own songs.
|
||||
$ownSongs = Song::factory(3)->for($currentUser, 'owner')->create();
|
||||
|
||||
$this->deleteAs('api/songs', ['songs' => $ownSongs->pluck('id')->toArray()], $currentUser)
|
||||
->assertSuccessful();
|
||||
}
|
||||
}
|
67
tests/Feature/KoelPlus/SongVisibilityTest.php
Normal file
67
tests/Feature/KoelPlus/SongVisibilityTest.php
Normal file
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\KoelPlus;
|
||||
|
||||
use App\Facades\License;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SongVisibilityTest extends TestCase
|
||||
{
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
License::fakePlusLicense();
|
||||
}
|
||||
|
||||
public function testMakingSongPublic(): void
|
||||
{
|
||||
/** @var User $currentUser */
|
||||
$currentUser = User::factory()->create();
|
||||
|
||||
/** @var User $anotherUser */
|
||||
$anotherUser = User::factory()->create();
|
||||
|
||||
/** @var Collection<Song> $externalSongs */
|
||||
$externalSongs = Song::factory(3)->for($anotherUser, 'owner')->private()->create();
|
||||
|
||||
// We can't make public songs that are not ours.
|
||||
$this->putAs('api/songs/make-public', ['songs' => $externalSongs->pluck('id')->toArray()], $currentUser)
|
||||
->assertForbidden();
|
||||
|
||||
// But we can our own songs.
|
||||
$ownSongs = Song::factory(3)->for($currentUser, 'owner')->create();
|
||||
|
||||
$this->putAs('api/songs/make-public', ['songs' => $ownSongs->pluck('id')->toArray()], $currentUser)
|
||||
->assertSuccessful();
|
||||
|
||||
$ownSongs->each(static fn (Song $song) => self::assertTrue($song->refresh()->is_public));
|
||||
}
|
||||
|
||||
public function testMakingSongPrivate(): void
|
||||
{
|
||||
/** @var User $currentUser */
|
||||
$currentUser = User::factory()->create();
|
||||
|
||||
/** @var User $anotherUser */
|
||||
$anotherUser = User::factory()->create();
|
||||
|
||||
/** @var Collection<Song> $externalSongs */
|
||||
$externalSongs = Song::factory(3)->for($anotherUser, 'owner')->public()->create();
|
||||
|
||||
// We can't make private songs that are not ours.
|
||||
$this->putAs('api/songs/make-private', ['songs' => $externalSongs->pluck('id')->toArray()], $currentUser)
|
||||
->assertForbidden();
|
||||
|
||||
// But we can our own songs.
|
||||
$ownSongs = Song::factory(3)->for($currentUser, 'owner')->create();
|
||||
|
||||
$this->putAs('api/songs/make-private', ['songs' => $ownSongs->pluck('id')->toArray()], $currentUser)
|
||||
->assertSuccessful();
|
||||
|
||||
$ownSongs->each(static fn (Song $song) => self::assertFalse($song->refresh()->is_public));
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ use Laravel\Sanctum\NewAccessToken;
|
|||
use Laravel\Sanctum\PersonalAccessToken;
|
||||
use Mockery;
|
||||
use Mockery\MockInterface;
|
||||
use Tests\TestCase;
|
||||
|
||||
class LastfmTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -5,7 +5,7 @@ namespace Tests\Feature\ObjectStorage;
|
|||
use App\Events\LibraryChanged;
|
||||
use App\Models\Song;
|
||||
use Illuminate\Foundation\Testing\WithoutMiddleware;
|
||||
use Tests\Feature\TestCase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class S3Test extends TestCase
|
||||
{
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace Tests\Feature;
|
|||
|
||||
use App\Models\Interaction;
|
||||
use App\Models\User;
|
||||
use Tests\TestCase;
|
||||
|
||||
class OverviewTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -7,6 +7,7 @@ use App\Models\Interaction;
|
|||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PlayCountTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace Tests\Feature;
|
|||
|
||||
use App\Models\PlaylistFolder;
|
||||
use App\Models\User;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PlaylistFolderTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -6,6 +6,7 @@ use App\Models\Playlist;
|
|||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PlaylistSongTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -7,6 +7,7 @@ use App\Models\Song;
|
|||
use App\Models\User;
|
||||
use App\Values\SmartPlaylistRule;
|
||||
use Illuminate\Support\Collection;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PlaylistTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace Tests\Feature;
|
|||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProfileTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace Tests\Feature;
|
|||
use App\Models\QueueState;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use Tests\TestCase;
|
||||
|
||||
class QueueTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace Tests\Feature;
|
|||
|
||||
use App\Models\Interaction;
|
||||
use App\Models\User;
|
||||
use Tests\TestCase;
|
||||
|
||||
class RecentlyPlayedSongTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -6,6 +6,7 @@ use App\Models\Song;
|
|||
use App\Models\User;
|
||||
use App\Services\LastfmService;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ScrobbleTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -7,16 +7,17 @@ use App\Models\User;
|
|||
use App\Services\MediaScanner;
|
||||
use App\Values\ScanResultCollection;
|
||||
use Mockery\MockInterface;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SettingTest extends TestCase
|
||||
{
|
||||
private MediaScanner|MockInterface $mediaSyncService;
|
||||
private MediaScanner|MockInterface $mediaScanner;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->mediaSyncService = self::mock(MediaScanner::class);
|
||||
$this->mediaScanner = self::mock(MediaScanner::class);
|
||||
}
|
||||
|
||||
public function testSaveSettings(): void
|
||||
|
@ -24,7 +25,7 @@ class SettingTest extends TestCase
|
|||
/** @var User $admin */
|
||||
$admin = User::factory()->admin()->create();
|
||||
|
||||
$this->mediaSyncService->shouldReceive('scan')->once()
|
||||
$this->mediaScanner->shouldReceive('scan')->once()
|
||||
->andReturn(ScanResultCollection::create());
|
||||
|
||||
$this->putAs('/api/settings', ['media_path' => __DIR__], $admin)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Song;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SongSearchTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -7,6 +7,7 @@ use App\Models\Artist;
|
|||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SongTest extends TestCase
|
||||
{
|
||||
|
|
24
tests/Feature/SongVisibilityTest.php
Normal file
24
tests/Feature/SongVisibilityTest.php
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SongVisibilityTest extends TestCase
|
||||
{
|
||||
public function testChangingVisibilityIsForbiddenInCommunityEdition(): void
|
||||
{
|
||||
/** @var User $user */
|
||||
$owner = User::factory()->admin()->create();
|
||||
|
||||
Song::factory(3)->create();
|
||||
|
||||
$this->putAs('api/songs/make-public', ['songs' => Song::query()->pluck('id')->all()], $owner)
|
||||
->assertForbidden();
|
||||
|
||||
$this->putAs('api/songs/make-private', ['songs' => Song::query()->pluck('id')->all()], $owner)
|
||||
->assertForbidden();
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ use App\Models\User;
|
|||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UploadTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -4,8 +4,9 @@ namespace Tests\Feature;
|
|||
|
||||
use App\Mail\UserInvite;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Str;
|
||||
use Mail;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UserInvitationTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace Tests\Feature;
|
|||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UserTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -6,6 +6,7 @@ use App\Models\Song;
|
|||
use App\Services\YouTubeService;
|
||||
use Mockery;
|
||||
use Mockery\MockInterface;
|
||||
use Tests\TestCase;
|
||||
|
||||
class YouTubeTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -16,7 +16,7 @@ use App\Values\ScanConfiguration;
|
|||
use getID3;
|
||||
use Illuminate\Support\Arr;
|
||||
use Mockery;
|
||||
use Tests\Feature\TestCase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MediaScannerTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -6,20 +6,21 @@ use App\Facades\License;
|
|||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Models\Song;
|
||||
use App\Services\CommunityLicenseService;
|
||||
use App\Services\License\CommunityLicenseService;
|
||||
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use ReflectionClass;
|
||||
use Tests\Traits\CreatesApplication;
|
||||
use Tests\Traits\SandboxesTests;
|
||||
use Tests\Traits\MakesHttpRequests;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
use ArraySubsetAsserts;
|
||||
use CreatesApplication;
|
||||
use DatabaseTransactions;
|
||||
use SandboxesTests;
|
||||
use MakesHttpRequests;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
|
@ -58,4 +59,19 @@ abstract class TestCase extends BaseTestCase
|
|||
|
||||
return $property->getValue($object);
|
||||
}
|
||||
|
||||
private static function createSandbox(): void
|
||||
{
|
||||
config(['koel.album_cover_dir' => 'sandbox/img/covers/']);
|
||||
config(['koel.artist_image_dir' => 'sandbox/img/artists/']);
|
||||
|
||||
File::ensureDirectoryExists(public_path(config('koel.album_cover_dir')));
|
||||
File::ensureDirectoryExists(public_path(config('koel.artist_image_dir')));
|
||||
File::ensureDirectoryExists(public_path('sandbox/media/'));
|
||||
}
|
||||
|
||||
private static function destroySandbox(): void
|
||||
{
|
||||
File::deleteDirectory(public_path('sandbox'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
namespace Tests\Traits;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Testing\TestResponse;
|
||||
use Tests\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
/**
|
||||
* @extends TestCase
|
||||
*/
|
||||
|
||||
trait MakesHttpRequests
|
||||
{
|
||||
/**
|
||||
* @param string $method
|
||||
* @param string $uri
|
||||
* @return TestResponse
|
||||
*/
|
||||
abstract public function json($method, $uri, array $data = [], array $headers = []); // @phpcs:ignore
|
||||
|
||||
private function jsonAs(?User $user, string $method, $uri, array $data = [], array $headers = []): TestResponse
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $user ?: User::factory()->create();
|
||||
$this->withToken($user->createToken('koel')->plainTextToken);
|
||||
|
||||
return parent::json($method, $uri, $data, $headers);
|
||||
return $this->json($method, $uri, $data, $headers);
|
||||
}
|
||||
|
||||
protected function getAs(string $url, ?User $user = null): TestResponse
|
|
@ -1,23 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Traits;
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
trait SandboxesTests
|
||||
{
|
||||
private static function createSandbox(): void
|
||||
{
|
||||
config(['koel.album_cover_dir' => 'sandbox/img/covers/']);
|
||||
config(['koel.artist_image_dir' => 'sandbox/img/artists/']);
|
||||
|
||||
File::ensureDirectoryExists(public_path(config('koel.album_cover_dir')));
|
||||
File::ensureDirectoryExists(public_path(config('koel.artist_image_dir')));
|
||||
File::ensureDirectoryExists(public_path('sandbox/media/'));
|
||||
}
|
||||
|
||||
private static function destroySandbox(): void
|
||||
{
|
||||
File::deleteDirectory(public_path('sandbox'));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue