feat(test): add tests for cloud storages

This commit is contained in:
Phan An 2024-02-24 01:36:02 +07:00
parent 3bf620039f
commit ff79332c6a
64 changed files with 379 additions and 83 deletions

View file

@ -2,7 +2,7 @@
namespace App\Console\Commands;
use App\Services\License\LicenseServiceInterface;
use App\Services\License\Contracts\LicenseServiceInterface;
use Illuminate\Console\Command;
use Throwable;

View file

@ -2,7 +2,7 @@
namespace App\Console\Commands;
use App\Services\License\LicenseServiceInterface;
use App\Services\License\Contracts\LicenseServiceInterface;
use App\Values\LicenseStatus;
use Illuminate\Console\Command;
use Throwable;

View file

@ -2,7 +2,7 @@
namespace App\Console\Commands;
use App\Services\License\LicenseServiceInterface;
use App\Services\License\Contracts\LicenseServiceInterface;
use Illuminate\Console\Command;
use Throwable;

View file

@ -2,13 +2,13 @@
namespace App\Console\Commands;
use App\Libraries\WatchRecord\InotifyWatchRecord;
use App\Models\Setting;
use App\Models\User;
use App\Repositories\UserRepository;
use App\Services\MediaScanner;
use App\Values\ScanConfiguration;
use App\Values\ScanResult;
use App\Values\WatchRecord\InotifyWatchRecord;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;

View file

@ -3,7 +3,7 @@
namespace App\Console\Commands;
use App\Facades\License;
use App\Services\SongStorage\DropboxStorage;
use App\Services\SongStorages\DropboxStorage;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel as Artisan;
use Illuminate\Support\Facades\Cache;

View file

@ -0,0 +1,25 @@
<?php
namespace App\Filesystems;
use DateTimeInterface;
use League\Flysystem\Filesystem;
use Spatie\FlysystemDropbox\DropboxAdapter;
class DropboxFilesystem extends Filesystem
{
public function __construct(private DropboxAdapter $adapter)
{
parent::__construct($adapter, ['case_sensitive' => false]);
}
public function temporaryUrl(string $path, ?DateTimeInterface $expiresAt = null, array $config = []): string
{
return $this->adapter->getUrl($path);
}
public function getAdapter(): DropboxAdapter
{
return $this->adapter;
}
}

View file

@ -5,7 +5,7 @@ namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Http\Requests\API\ActivateLicenseRequest;
use App\Models\License;
use App\Services\License\LicenseServiceInterface;
use App\Services\License\Contracts\LicenseServiceInterface;
class ActivateLicenseController extends Controller
{

View file

@ -14,7 +14,7 @@ use App\Repositories\SongRepository;
use App\Services\ApplicationInformationService;
use App\Services\ITunesService;
use App\Services\LastfmService;
use App\Services\License\LicenseServiceInterface;
use App\Services\License\Contracts\LicenseServiceInterface;
use App\Services\QueueService;
use App\Services\SpotifyService;
use App\Services\YouTubeService;

View file

@ -6,7 +6,7 @@ use App\Exceptions\SongPathNotFoundException;
use App\Http\Controllers\Controller;
use App\Http\Requests\API\ObjectStorage\S3\PutSongRequest;
use App\Http\Requests\API\ObjectStorage\S3\RemoveSongRequest;
use App\Services\SongStorage\S3LambdaStorage;
use App\Services\SongStorages\S3LambdaStorage;
use Illuminate\Http\Response;
use Illuminate\Support\Arr;

View file

@ -12,7 +12,7 @@ use App\Http\Resources\SongResource;
use App\Models\User;
use App\Repositories\AlbumRepository;
use App\Repositories\SongRepository;
use App\Services\SongStorage\SongStorage;
use App\Services\SongStorages\SongStorage;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Http\Response;

View file

@ -3,6 +3,7 @@
namespace App\Models;
use App\Builders\AlbumBuilder;
use App\Models\Concerns\SupportsDeleteWhereValueNotIn;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;

View file

@ -4,6 +4,7 @@ namespace App\Models;
use App\Builders\ArtistBuilder;
use App\Facades\Util;
use App\Models\Concerns\SupportsDeleteWhereValueNotIn;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;

View file

@ -1,6 +1,6 @@
<?php
namespace App\Models;
namespace App\Models\Concerns;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;

View file

@ -33,7 +33,7 @@ class Setting extends Model
* @param array|string $key the key of the setting, or an associative array of settings,
* in which case $value will be discarded
*/
public static function set(array|string $key, $value): void
public static function set(array|string $key, $value = ''): void
{
if (is_array($key)) {
foreach ($key as $k => $v) {
@ -45,9 +45,4 @@ class Setting extends Model
self::query()->updateOrCreate(compact('key'), compact('value'));
}
public static function unset(string $key): void
{
static::set($key, null);
}
}

View file

@ -3,11 +3,12 @@
namespace App\Models;
use App\Builders\SongBuilder;
use App\Models\Concerns\SupportsDeleteWhereValueNotIn;
use App\Values\SongStorageMetadata\Contracts\SongStorageMetadata;
use App\Values\SongStorageMetadata\DropboxMetadata;
use App\Values\SongStorageMetadata\LegacyS3Metadata;
use App\Values\SongStorageMetadata\LocalMetadata;
use App\Values\SongStorageMetadata\S3CompatibleMetadata;
use App\Values\SongStorageMetadata\SongStorageMetadata;
use App\Values\SongStorageTypes;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Casts\Attribute;

View file

@ -4,10 +4,10 @@ namespace App\Providers;
use App\Services\ApiClients\ApiClient;
use App\Services\ApiClients\LemonSqueezyApiClient;
use App\Services\Contracts\MusicEncyclopedia;
use App\Services\LastfmService;
use App\Services\License\LicenseServiceInterface;
use App\Services\License\Contracts\LicenseServiceInterface;
use App\Services\LicenseService;
use App\Services\MusicEncyclopedia;
use App\Services\NullMusicEncyclopedia;
use App\Services\SpotifyService;
use Illuminate\Database\DatabaseManager;

View file

@ -2,7 +2,7 @@
namespace App\Providers;
use App\Services\License\LicenseServiceInterface;
use App\Services\License\Contracts\LicenseServiceInterface;
use Illuminate\Support\ServiceProvider;
class LicenseServiceProvider extends ServiceProvider

View file

@ -2,10 +2,10 @@
namespace App\Providers;
use App\Services\SongStorage\DropboxStorage;
use App\Services\SongStorage\LocalStorage;
use App\Services\SongStorage\S3CompatibleStorage;
use App\Services\SongStorage\SongStorage;
use App\Services\SongStorages\DropboxStorage;
use App\Services\SongStorages\LocalStorage;
use App\Services\SongStorages\S3CompatibleStorage;
use App\Services\SongStorages\SongStorage;
use Illuminate\Support\ServiceProvider;
class SongStorageServiceProvider extends ServiceProvider

View file

@ -1,6 +1,6 @@
<?php
namespace App\Repositories;
namespace App\Repositories\Contracts;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;

View file

@ -2,6 +2,7 @@
namespace App\Repositories;
use App\Repositories\Contracts\RepositoryInterface;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;

View file

@ -1,6 +1,6 @@
<?php
namespace App\Services;
namespace App\Services\Contracts;
use App\Models\Album;
use App\Models\Artist;

View file

@ -1,6 +1,6 @@
<?php
namespace App\Services;
namespace App\Services\Contracts;
use App\Models\Song;

View file

@ -4,9 +4,9 @@ namespace App\Services;
use App\Models\Song;
use App\Models\SongZipArchive;
use App\Services\SongStorage\CloudStorage;
use App\Services\SongStorage\DropboxStorage;
use App\Services\SongStorage\S3CompatibleStorage;
use App\Services\SongStorages\CloudStorage;
use App\Services\SongStorages\DropboxStorage;
use App\Services\SongStorages\S3CompatibleStorage;
use App\Values\SongStorageTypes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\File;

View file

@ -99,6 +99,7 @@ class FileScanner
$data['owner_id'] = $config->owner->id;
}
// @todo Decouple song creation from scanning.
$this->song = Song::query()->updateOrCreate(['path' => $this->filePath], $data); // @phpstan-ignore-line
return ScanResult::success($this->filePath);

View file

@ -7,6 +7,7 @@ use App\Models\Artist;
use App\Models\Song;
use App\Models\User;
use App\Services\ApiClients\LastfmClient;
use App\Services\Contracts\MusicEncyclopedia;
use App\Values\AlbumInformation;
use App\Values\ArtistInformation;
use GuzzleHttp\Promise\Promise;

View file

@ -4,6 +4,7 @@ namespace App\Services\License;
use App\Exceptions\MethodNotImplementedException;
use App\Models\License;
use App\Services\License\Contracts\LicenseServiceInterface;
use App\Values\LicenseStatus;
class CommunityLicenseService implements LicenseServiceInterface

View file

@ -1,6 +1,6 @@
<?php
namespace App\Services\License;
namespace App\Services\License\Contracts;
use App\Models\License;
use App\Values\LicenseStatus;

View file

@ -5,7 +5,7 @@ namespace App\Services;
use App\Exceptions\FailedToActivateLicenseException;
use App\Models\License;
use App\Services\ApiClients\ApiClient;
use App\Services\License\LicenseServiceInterface;
use App\Services\License\Contracts\LicenseServiceInterface;
use App\Values\LicenseInstance;
use App\Values\LicenseMeta;
use App\Values\LicenseStatus;

View file

@ -4,6 +4,7 @@ namespace App\Services;
use App\Models\Album;
use App\Models\Artist;
use App\Services\Contracts\MusicEncyclopedia;
use App\Values\AlbumInformation;
use App\Values\ArtistInformation;
use Illuminate\Cache\Repository as Cache;

View file

@ -4,13 +4,13 @@ namespace App\Services;
use App\Events\LibraryChanged;
use App\Events\MediaScanCompleted;
use App\Libraries\WatchRecord\WatchRecordInterface;
use App\Models\Song;
use App\Repositories\SettingRepository;
use App\Repositories\SongRepository;
use App\Values\ScanConfiguration;
use App\Values\ScanResult;
use App\Values\ScanResultCollection;
use App\Values\WatchRecord\Contracts\WatchRecordInterface;
use Psr\Log\LoggerInterface;
use SplFileInfo;
use Symfony\Component\Finder\Finder;

View file

@ -4,6 +4,7 @@ namespace App\Services;
use App\Models\Album;
use App\Models\Artist;
use App\Services\Contracts\MusicEncyclopedia;
use App\Values\AlbumInformation;
use App\Values\ArtistInformation;

View file

@ -8,7 +8,7 @@ use App\Models\Album;
use App\Models\Artist;
use App\Models\Song;
use App\Repositories\SongRepository;
use App\Services\SongStorage\SongStorage;
use App\Services\SongStorages\SongStorage;
use App\Values\SongUpdateData;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;

View file

@ -1,6 +1,6 @@
<?php
namespace App\Services\SongStorage;
namespace App\Services\SongStorages;
use App\Exceptions\SongUploadFailedException;
use App\Models\Song;

View file

@ -1,7 +1,8 @@
<?php
namespace App\Services\SongStorage;
namespace App\Services\SongStorages;
use App\Filesystems\DropboxFilesystem;
use App\Models\Song;
use App\Models\User;
use App\Services\FileScanner;
@ -11,22 +12,17 @@ use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use League\Flysystem\Filesystem;
use Spatie\Dropbox\Client;
use Spatie\FlysystemDropbox\DropboxAdapter;
final class DropboxStorage extends CloudStorage
{
public Filesystem $filesystem;
private DropboxAdapter $adapter;
public function __construct(protected FileScanner $scanner, private array $config)
{
public function __construct(
protected FileScanner $scanner,
private DropboxFilesystem $filesystem,
private array $config
) {
parent::__construct($scanner);
$client = new Client($this->maybeRefreshAccessToken());
$this->adapter = new DropboxAdapter($client);
$this->filesystem = new Filesystem($this->adapter, ['case_sensitive' => false]);
$this->filesystem->getAdapter()->getClient()->setAccessToken($this->maybeRefreshAccessToken());
}
public function storeUploadedFile(UploadedFile $file, User $uploader): Song
@ -37,6 +33,7 @@ final class DropboxStorage extends CloudStorage
$key = $this->generateStorageKey($file->getClientOriginalName(), $uploader);
$this->filesystem->write($key, File::get($result->path));
$song->update([
'path' => "dropbox://$key",
'storage' => SongStorageTypes::DROPBOX,
@ -61,20 +58,20 @@ final class DropboxStorage extends CloudStorage
->post('https://api.dropboxapi.com/oauth2/token', [
'refresh_token' => $this->config['refresh_token'],
'grant_type' => 'refresh_token',
])->json();
]);
Cache::put(
'dropbox_access_token',
$response['access_token'],
now()->addSeconds($response['expires_in'] - 60) // 60 seconds buffer
$response->json('access_token'),
now()->addSeconds($response->json('expires_in') - 60) // 60 seconds buffer
);
return $response['access_token'];
return $response->json('access_token');
}
public function getSongPresignedUrl(Song $song): string
{
return $this->adapter->getUrl($song->storage_metadata->getPath());
return $this->filesystem->temporaryUrl($song->storage_metadata->getPath());
}
public function supported(): bool

View file

@ -1,6 +1,6 @@
<?php
namespace App\Services\SongStorage;
namespace App\Services\SongStorages;
use App\Exceptions\MediaPathNotSetException;
use App\Exceptions\SongUploadFailedException;

View file

@ -1,6 +1,6 @@
<?php
namespace App\Services\SongStorage;
namespace App\Services\SongStorages;
use App\Models\Song;
use App\Models\User;

View file

@ -1,6 +1,6 @@
<?php
namespace App\Services\SongStorage;
namespace App\Services\SongStorages;
use App\Events\LibraryChanged;
use App\Exceptions\MethodNotImplementedException;

View file

@ -1,6 +1,6 @@
<?php
namespace App\Services\SongStorage;
namespace App\Services\SongStorages;
use App\Exceptions\KoelPlusRequiredException;
use App\Models\Song;

View file

@ -2,7 +2,7 @@
namespace App\Services\Streamers;
use App\Services\SongStorage\DropboxStorage;
use App\Services\SongStorages\DropboxStorage;
use Illuminate\Http\RedirectResponse;
use Illuminate\Routing\Redirector;

View file

@ -2,7 +2,7 @@
namespace App\Services\Streamers;
use App\Services\SongStorage\S3CompatibleStorage;
use App\Services\SongStorages\S3CompatibleStorage;
use Illuminate\Http\RedirectResponse;
use Illuminate\Routing\Redirector;

View file

@ -2,6 +2,7 @@
namespace App\Values;
use App\Values\Concerns\FormatsLastFmText;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Arr;

View file

@ -2,6 +2,7 @@
namespace App\Values;
use App\Values\Concerns\FormatsLastFmText;
use Illuminate\Contracts\Support\Arrayable;
final class ArtistInformation implements Arrayable

View file

@ -1,6 +1,6 @@
<?php
namespace App\Values;
namespace App\Values\Concerns;
trait FormatsLastFmText
{

View file

@ -1,6 +1,6 @@
<?php
namespace App\Values\SongStorageMetadata;
namespace App\Values\SongStorageMetadata\Contracts;
interface SongStorageMetadata
{

View file

@ -2,6 +2,8 @@
namespace App\Values\SongStorageMetadata;
use App\Values\SongStorageMetadata\Contracts\SongStorageMetadata;
class DropboxMetadata implements SongStorageMetadata
{
private function __construct(public string $path)

View file

@ -2,6 +2,8 @@
namespace App\Values\SongStorageMetadata;
use App\Values\SongStorageMetadata\Contracts\SongStorageMetadata;
final class LocalMetadata implements SongStorageMetadata
{
private function __construct(public string $path)

View file

@ -2,6 +2,8 @@
namespace App\Values\SongStorageMetadata;
use App\Values\SongStorageMetadata\Contracts\SongStorageMetadata;
class S3CompatibleMetadata implements SongStorageMetadata
{
private function __construct(public string $bucket, public string $key)

View file

@ -1,6 +1,6 @@
<?php
namespace App\Libraries\WatchRecord;
namespace App\Values\WatchRecord\Contracts;
interface WatchRecordInterface
{

View file

@ -1,6 +1,8 @@
<?php
namespace App\Libraries\WatchRecord;
namespace App\Values\WatchRecord;
use App\Values\WatchRecord\Contracts\WatchRecordInterface;
class InotifyWatchRecord extends WatchRecord implements WatchRecordInterface
{

View file

@ -1,6 +1,8 @@
<?php
namespace App\Libraries\WatchRecord;
namespace App\Values\WatchRecord;
use App\Values\WatchRecord\Contracts\WatchRecordInterface;
abstract class WatchRecord implements WatchRecordInterface
{

View file

@ -4,7 +4,7 @@ namespace Tests\Fakes;
use App\Exceptions\MethodNotImplementedException;
use App\Models\License;
use App\Services\License\LicenseServiceInterface;
use App\Services\License\Contracts\LicenseServiceInterface;
use App\Values\LicenseStatus;
class FakePlusLicenseService implements LicenseServiceInterface

View file

@ -27,7 +27,7 @@ class UploadTest extends TestCase
public function testUnauthorizedPost(): void
{
Setting::unset('media_path');
Setting::set('media_path', '');
$this->postAs('/api/upload', ['file' => $this->file])->assertForbidden();
}
@ -43,7 +43,7 @@ class UploadTest extends TestCase
public function testUploadFailsIfMediaPathIsNotSet(): void
{
Setting::unset('media_path');
Setting::set('media_path', '');
$this->postAs('/api/upload', ['file' => $this->file], create_admin())->assertForbidden();
}

View file

@ -2,6 +2,7 @@
namespace Tests\Integration\Factories;
use App\Exceptions\KoelPlusRequiredException;
use App\Models\Song;
use App\Services\Streamers\PhpStreamer;
use App\Services\Streamers\S3CompatibleStreamer;
@ -10,6 +11,8 @@ use App\Services\Streamers\TranscodingStreamer;
use App\Services\Streamers\XAccelRedirectStreamer;
use App\Services\Streamers\XSendFileStreamer;
use App\Services\TranscodingService;
use App\Values\SongStorageTypes;
use Exception;
use Mockery;
use phpmock\mockery\PHPMockery;
use Tests\TestCase;
@ -28,12 +31,41 @@ class StreamerFactoryTest extends TestCase
PHPMockery::mock('App\Services\Streamers', 'file_exists')->andReturn(true);
}
public function testCreateS3Streamer(): void
public function testCreateStreamer(): void
{
/** @var Song $song */
$song = Song::factory()->make(['path' => 's3://bucket/foo.mp3']);
collect(SongStorageTypes::ALL_TYPES)
->each(function (?string $type): void {
switch ($type) {
case SongStorageTypes::S3:
self::expectException(KoelPlusRequiredException::class);
$this->streamerFactory->createStreamer(
Song::factory()->make(['path' => "s3://bucket/foo.mp3", 'storage' => $type])
);
break;
self::assertInstanceOf(S3CompatibleStreamer::class, $this->streamerFactory->createStreamer($song));
case SongStorageTypes::S3_LAMBDA:
self::assertInstanceOf(S3CompatibleStreamer::class, $this->streamerFactory->createStreamer(
Song::factory()->make(['path' => "s3://bucket/foo.mp3", 'storage' => $type])
));
break;
case SongStorageTypes::DROPBOX:
self::expectException(KoelPlusRequiredException::class);
$this->streamerFactory->createStreamer(
Song::factory()->make(['path' => "dropbox://foo.mp3", 'storage' => $type])
);
break;
case SongStorageTypes::LOCAL:
self::assertInstanceOf(PhpStreamer::class, $this->streamerFactory->createStreamer(
Song::factory()->make(['path' => test_path('songs/blank.mp3'), 'storage' => $type])
));
break;
default:
throw new Exception("Unhandled storage type: $type");
}
});
}
public function testCreateTranscodingStreamerIfSupported(): void

View file

@ -0,0 +1,122 @@
<?php
namespace Tests\Integration\KoelPlus\Services\SongStorages;
use App\Filesystems\DropboxFilesystem;
use App\Models\Song;
use App\Services\SongStorages\DropboxStorage;
use Illuminate\Http\Client\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Mockery;
use Mockery\MockInterface;
use Spatie\Dropbox\Client;
use Spatie\FlysystemDropbox\DropboxAdapter;
use Tests\PlusTestCase;
use function Tests\create_user;
use function Tests\test_path;
class DropboxStorageTest extends PlusTestCase
{
private MockInterface|DropboxFilesystem $filesystem;
private MockInterface|Client $client;
private UploadedFile $file;
public function setUp(): void
{
parent::setUp();
config([
'koel.storage_driver' => 'dropbox',
'filesystems.disks.dropbox' => [
'app_key' => 'dropbox-key',
'app_secret' => 'dropbox-secret',
'refresh_token' => 'coca-cola',
],
]);
$this->client = $this->mock(Client::class);
$this->filesystem = $this->mock(DropboxFilesystem::class);
$this->filesystem->allows('getAdapter')->andReturns(
Mockery::mock(DropboxAdapter::class, ['getClient' => $this->client])
);
self::mockRefreshAccessTokenCall();
$this->file = UploadedFile::fromFile(test_path('songs/full.mp3'), 'song.mp3'); //@phpstan-ignore-line
}
public function testSupported(): void
{
$this->client->allows('setAccessToken');
self::assertTrue(app(DropboxStorage::class)->supported());
}
public function testStoreUploadedFile(): void
{
$this->client->shouldReceive('setAccessToken')->with('free-bird')->once();
/** @var DropboxStorage $service */
$service = app(DropboxStorage::class);
Http::assertSent(static function (Request $request) {
return $request->hasHeader('Authorization', 'Basic ' . base64_encode('dropbox-key:dropbox-secret'))
&& $request->isForm()
&& $request['refresh_token'] === 'coca-cola'
&& $request['grant_type'] === 'refresh_token';
});
self::assertSame(0, Song::query()->where('storage', 'dropbox')->count());
$this->filesystem->shouldReceive('write')->once();
$service->storeUploadedFile($this->file, create_user());
self::assertSame(1, Song::query()->where('storage', 'dropbox')->count());
self::assertSame('free-bird', Cache::get('dropbox_access_token'));
}
public function testAccessTokenCache(): void
{
Cache::put('dropbox_access_token', 'cached-token', now()->addHour());
$this->client->shouldReceive('setAccessToken')->with('cached-token')->once();
app(DropboxStorage::class);
self::assertSame('cached-token', Cache::get('dropbox_access_token'));
Http::assertNothingSent();
}
public function testGetSongPresignedUrl(): void
{
$this->client->allows('setAccessToken');
/** @var Song $song */
$song = Song::factory()->create(['path' => 'dropbox://song.mp3', 'storage' => 'dropbox']);
/** @var DropboxStorage $service */
$service = app(DropboxStorage::class);
$this->filesystem->shouldReceive('temporaryUrl')
->once()
->with('song.mp3')
->andReturn('https://dropbox.com/song.mp3?token=123');
self::assertSame('https://dropbox.com/song.mp3?token=123', $service->getSongPresignedUrl($song));
}
private static function mockRefreshAccessTokenCall(): void
{
Http::preventStrayRequests();
Http::fake([
'https://api.dropboxapi.com/oauth2/token' => Http::response([
'access_token' => 'free-bird',
'expires_in' => 3600,
]),
]);
}
}

View file

@ -1,9 +1,9 @@
<?php
namespace Tests\Integration\KoelPlus\Services\FileStorage;
namespace Tests\Integration\KoelPlus\Services\SongStorages;
use App\Models\Song;
use App\Services\SongStorage\S3CompatibleStorage;
use App\Services\SongStorages\S3CompatibleStorage;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\PlusTestCase;
@ -24,6 +24,11 @@ class S3CompatibleStorageTest extends PlusTestCase
$this->file = UploadedFile::fromFile(test_path('songs/full.mp3'), 'song.mp3'); //@phpstan-ignore-line
}
public function testSupported(): void
{
self::assertTrue($this->service->supported());
}
public function testStoreUploadedFile(): void
{
self::assertEquals(0, Song::query()->where('storage', 's3')->count());

View file

@ -0,0 +1,57 @@
<?php
namespace Tests\Integration\KoelPlus;
use App\Models\Song;
use App\Services\Streamers\PhpStreamer;
use App\Services\Streamers\S3CompatibleStreamer;
use App\Services\Streamers\StreamerFactory;
use App\Values\SongStorageTypes;
use Exception;
use phpmock\mockery\PHPMockery;
use Tests\PlusTestCase;
use function Tests\test_path;
class StreamerFactoryTest extends PlusTestCase
{
private StreamerFactory $streamerFactory;
public function setUp(): void
{
parent::setUp();
$this->streamerFactory = app(StreamerFactory::class);
PHPMockery::mock('App\Services\Streamers', 'file_exists')->andReturn(true);
}
public function testCreateStreamer(): void
{
collect(SongStorageTypes::ALL_TYPES)
->each(function (?string $type): void {
switch ($type) {
case SongStorageTypes::S3:
case SongStorageTypes::S3_LAMBDA:
self::assertInstanceOf(S3CompatibleStreamer::class, $this->streamerFactory->createStreamer(
Song::factory()->make(['path' => "s3://bucket/foo.mp3", 'storage' => $type])
));
break;
case SongStorageTypes::DROPBOX:
$this->streamerFactory->createStreamer(
Song::factory()->make(['path' => "dropbox://foo.mp3", 'storage' => $type])
);
break;
case SongStorageTypes::LOCAL:
self::assertInstanceOf(PhpStreamer::class, $this->streamerFactory->createStreamer(
Song::factory()->make(['path' => test_path('songs/blank.mp3'), 'storage' => $type])
));
break;
default:
throw new Exception("Unhandled storage type: $type");
}
});
}
}

View file

@ -7,6 +7,7 @@ use App\Listeners\DeleteNonExistingRecordsPostScan;
use App\Models\Song;
use App\Values\ScanResult;
use App\Values\ScanResultCollection;
use App\Values\SongStorageTypes;
use Illuminate\Database\Eloquent\Collection;
use Tests\TestCase;
@ -21,12 +22,16 @@ class DeleteNonExistingRecordsPostSyncTest extends TestCase
$this->listener = app(DeleteNonExistingRecordsPostScan::class);
}
public function testHandleDoesNotDeleteS3Entries(): void
public function testHandleDoesNotDeleteCloudEntries(): void
{
$song = Song::factory()->create(['path' => 's3://do-not/delete-me.mp3']);
$this->listener->handle(new MediaScanCompleted(ScanResultCollection::create()));
collect(SongStorageTypes::ALL_TYPES)
->filter(static fn ($type) => $type !== SongStorageTypes::LOCAL)
->each(function ($type): void {
$song = Song::factory()->create(['storage' => $type]);
$this->listener->handle(new MediaScanCompleted(ScanResultCollection::create()));
self::assertModelExists($song);
self::assertModelExists($song);
});
}
public function testHandle(): void

View file

@ -4,7 +4,6 @@ namespace Tests\Integration\Services;
use App\Events\LibraryChanged;
use App\Events\MediaScanCompleted;
use App\Libraries\WatchRecord\InotifyWatchRecord;
use App\Models\Album;
use App\Models\Artist;
use App\Models\Setting;
@ -12,6 +11,7 @@ use App\Models\Song;
use App\Services\FileScanner;
use App\Services\MediaScanner;
use App\Values\ScanConfiguration;
use App\Values\WatchRecord\InotifyWatchRecord;
use getID3;
use Illuminate\Support\Arr;
use Mockery;

View file

@ -1,11 +1,11 @@
<?php
namespace Tests\Integration\Services\FileStorage;
namespace Tests\Integration\Services\SongStorages;
use App\Exceptions\MediaPathNotSetException;
use App\Exceptions\SongUploadFailedException;
use App\Models\Setting;
use App\Services\SongStorage\LocalStorage;
use App\Services\SongStorages\LocalStorage;
use Illuminate\Http\UploadedFile;
use Mockery;
use Tests\TestCase;
@ -26,7 +26,7 @@ class LocalStorageTest extends TestCase
public function testStoreUploadedFileWithMediaPathNotSet(): void
{
Setting::unset('media_path');
Setting::set('media_path', '');
self::expectException(MediaPathNotSetException::class);
$this->service->storeUploadedFile(Mockery::mock(UploadedFile::class), create_user());

View file

@ -4,10 +4,10 @@ namespace Tests\Unit\Services;
use App\Models\Album;
use App\Models\Artist;
use App\Services\Contracts\MusicEncyclopedia;
use App\Services\LastfmService;
use App\Services\MediaInformationService;
use App\Services\MediaMetadataService;
use App\Services\MusicEncyclopedia;
use App\Values\AlbumInformation;
use App\Values\ArtistInformation;
use Illuminate\Cache\Repository as Cache;

View file

@ -0,0 +1,16 @@
<?php
namespace Tests\Unit\Services\SongStorages;
use App\Exceptions\KoelPlusRequiredException;
use App\Services\SongStorages\DropboxStorage;
use Tests\TestCase;
class DropboxStorageTest extends TestCase
{
public function testSupported(): void
{
self::expectException(KoelPlusRequiredException::class);
app(DropboxStorage::class);
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace Tests\Unit\Services\SongStorages;
use App\Exceptions\KoelPlusRequiredException;
use App\Services\SongStorages\S3CompatibleStorage;
use Tests\TestCase;
class S3CompatibleStorageTest extends TestCase
{
public function testSupported(): void
{
self::expectException(KoelPlusRequiredException::class);
app(S3CompatibleStorage::class);
}
}

View file

@ -1,13 +1,13 @@
<?php
namespace Tests\Unit\Services\SongStorage;
namespace Tests\Unit\Services\SongStorages;
use App\Events\LibraryChanged;
use App\Models\Song;
use App\Repositories\SongRepository;
use App\Repositories\UserRepository;
use App\Services\MediaMetadataService;
use App\Services\SongStorage\S3LambdaStorage;
use App\Services\SongStorages\S3LambdaStorage;
use Mockery;
use Mockery\LegacyMockInterface;
use Mockery\MockInterface;
@ -127,4 +127,9 @@ class S3LambdaStorageTest extends TestCase
self::assertModelMissing($song);
}
public function testSupported(): void
{
self::assertTrue($this->storage->supported());
}
}