feat: adapt downloading to Plus

This commit is contained in:
Phan An 2024-01-07 13:43:10 +01:00
parent f04f940ffa
commit 473f1c11a1
26 changed files with 175 additions and 173 deletions

View file

@ -25,7 +25,7 @@ class LicenseInstanceCast implements CastsAttributes
}
}
/** @param LicenseInstance $value */
/** @param ?LicenseInstance $value */
public function set($model, string $key, $value, array $attributes): ?string
{
try {

View file

@ -25,7 +25,7 @@ class LicenseMetaCast implements CastsAttributes
}
}
/** @param LicenseMeta $value */
/** @param ?LicenseMeta $value */
public function set($model, string $key, $value, array $attributes): ?string
{
try {

View file

@ -39,7 +39,7 @@ class ActivateLicenseCommand extends Command
$this->components->twoColumnDetail('Expires On', 'Never ever');
$this->line('');
$this->newLine();
return self::SUCCESS;
}

View file

@ -35,7 +35,7 @@ class CheckLicenseStatusCommand extends Command
);
$this->components->twoColumnDetail('Expires On', 'Never ever');
$this->line('');
$this->newLine();
break;
case LicenseStatus::STATUS_NO_LICENSE:

View file

@ -27,7 +27,6 @@ class ScanCommand extends Command
protected $description = 'Scan for songs in the configured directory.';
private ?string $mediaPath;
private ?User $owner;
private ProgressBar $progressBar;
public function __construct(private MediaScanner $mediaScanner)
@ -50,15 +49,22 @@ class ScanCommand extends Command
public function handle(): int
{
$this->owner = $this->getOwner();
$this->mediaPath = $this->getMediaPath();
$config = ScanConfiguration::make(
owner: $this->getOwner(),
// When scanning via CLI, the songs should be public by default, unless explicitly specified otherwise.
makePublic: !$this->option('private'),
ignores: collect($this->option('ignore'))->sort()->values()->all(),
force: $this->option('force')
);
$record = $this->argument('record');
if ($record) {
$this->scanSingleRecord($record);
$this->scanSingleRecord($record, $config);
} else {
$this->scanMediaPath();
$this->scanMediaPath($config);
}
return self::SUCCESS;
@ -67,27 +73,14 @@ class ScanCommand extends Command
/**
* Scan all files in the configured media path.
*/
private function scanMediaPath(): void
private function scanMediaPath(ScanConfiguration $config): void
{
$this->components->info('Scanning ' . $this->mediaPath);
// The tags to ignore from scanning.
// Notice that this is only meaningful for existing records.
// New records will have every applicable field scanned.
$ignores = collect($this->option('ignore'))->sort()->values()->all();
if ($ignores) {
$this->components->info('Ignoring tag(s): ' . implode(', ', $ignores));
if ($config->ignores) {
$this->components->info('Ignoring tag(s): ' . implode(', ', $config->ignores));
}
$config = ScanConfiguration::make(
owner: $this->owner,
// When scanning via CLI, the songs should be public by default, unless explicitly specified otherwise.
makePublic: !$this->option('private'),
ignores: $ignores,
force: $this->option('force')
);
$results = $this->mediaScanner->scan($config);
$this->newLine(2);
@ -110,9 +103,9 @@ class ScanCommand extends Command
*
* @see http://man7.org/linux/man-pages/man1/inotifywait.1.html
*/
private function scanSingleRecord(string $record): void
private function scanSingleRecord(string $record, ScanConfiguration $config): void
{
$this->mediaScanner->scanWatchRecord(new InotifyWatchRecord($record));
$this->mediaScanner->scanWatchRecord(new InotifyWatchRecord($record), $config);
}
public function onScanProgress(ScanResult $result): void

View file

@ -7,6 +7,7 @@ use Illuminate\Support\Facades\Facade;
/**
* @method static string fromSong(Song $song)
* @see \App\Services\DownloadService
*/
class Download extends Facade
{

View file

@ -7,6 +7,7 @@ use Illuminate\Support\Facades\Facade;
/**
* @method static bool isPlus()
* @method static bool isCommunity()
* @see \App\Services\LicenseService
*/
class License extends Facade
{

View file

@ -4,12 +4,16 @@ namespace App\Http\Controllers\Download;
use App\Http\Controllers\Controller;
use App\Models\Album;
use App\Models\User;
use App\Repositories\SongRepository;
use App\Services\DownloadService;
use Illuminate\Contracts\Auth\Authenticatable;
class DownloadAlbumController extends Controller
{
public function __invoke(Album $album, DownloadService $download)
/** @param User $user */
public function __invoke(Album $album, SongRepository $repository, DownloadService $download, Authenticatable $user)
{
return response()->download($download->from($album));
return response()->download($download->from($repository->getByAlbum($album, $user)));
}
}

View file

@ -4,12 +4,20 @@ namespace App\Http\Controllers\Download;
use App\Http\Controllers\Controller;
use App\Models\Artist;
use App\Models\User;
use App\Repositories\SongRepository;
use App\Services\DownloadService;
use Illuminate\Contracts\Auth\Authenticatable;
class DownloadArtistController extends Controller
{
public function __invoke(Artist $artist, DownloadService $download)
{
return response()->download($download->from($artist));
/** @param User $user */
public function __invoke(
Artist $artist,
SongRepository $repository,
DownloadService $download,
Authenticatable $user
) {
return response()->download($download->from($repository->getByArtist($artist, $user)));
}
}

View file

@ -4,15 +4,15 @@ namespace App\Http\Controllers\Download;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Repositories\InteractionRepository;
use App\Repositories\SongRepository;
use App\Services\DownloadService;
use Illuminate\Contracts\Auth\Authenticatable;
class DownloadFavoritesController extends Controller
{
/** @param User $user */
public function __invoke(DownloadService $download, InteractionRepository $repository, Authenticatable $user)
public function __invoke(DownloadService $download, SongRepository $repository, Authenticatable $user)
{
return response()->download($download->from($repository->getUserFavorites($user)));
return response()->download($download->from($repository->getFavorites($user)));
}
}

View file

@ -4,14 +4,30 @@ namespace App\Http\Controllers\Download;
use App\Http\Controllers\Controller;
use App\Models\Playlist;
use App\Models\User;
use App\Repositories\SongRepository;
use App\Services\DownloadService;
use App\Services\SmartPlaylistService;
use Illuminate\Contracts\Auth\Authenticatable;
class DownloadPlaylistController extends Controller
{
public function __invoke(Playlist $playlist, DownloadService $download)
{
$this->authorize('own', $playlist);
/** @param User $user */
public function __invoke(
Playlist $playlist,
SongRepository $repository,
SmartPlaylistService $smartPlaylistService,
DownloadService $download,
Authenticatable $user
) {
$this->authorize('download', $playlist);
return response()->download($download->from($playlist));
return response()->download(
$download->from(
$playlist->is_smart
? $smartPlaylistService->getSongs($playlist, $user)
: $repository->getByStandardPlaylist($playlist, $user)
)
);
}
}

View file

@ -11,6 +11,9 @@ class DownloadSongsController extends Controller
{
public function __invoke(DownloadSongsRequest $request, DownloadService $download, SongRepository $repository)
{
$songs = $repository->getMany($request->songs);
$songs->each(fn ($song) => $this->authorize('download', $song));
return response()->download($download->from($repository->getMany($request->songs)));
}
}

View file

@ -18,7 +18,7 @@ class SongZipArchive
*/
private array $fileNames = [];
public function __construct(string $path = '')
public function __construct(?string $path = null)
{
$this->path = $path ?: self::generateRandomArchivePath();

View file

@ -11,4 +11,9 @@ class PlaylistPolicy
{
return $playlist->user->is($user);
}
public function download(User $user, Playlist $playlist): bool
{
return $this->own($user, $playlist);
}
}

View file

@ -27,4 +27,13 @@ class SongPolicy
{
return (License::isCommunity() && $user->is_admin) || $song->owner_id === $user->id;
}
public function download(User $user, Song $song): bool
{
if (!config('koel.download.allow')) {
return false;
}
return License::isCommunity() || $song->is_public || $song->owner_id === $user->id;
}
}

View file

@ -3,9 +3,12 @@
namespace App\Providers;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\ServiceProvider;
use Illuminate\Testing\TestResponse;
class MacroProvider extends ServiceProvider
{
@ -22,5 +25,18 @@ class MacroProvider extends ServiceProvider
return $this;
});
if (app()->runningUnitTests()) {
UploadedFile::macro('fromFile', static function (string $path, ?string $name = null): UploadedFile {
return UploadedFile::fake()->createWithContent($name ?? basename($path), File::get($path));
});
TestResponse::macro('log', function (string $file = 'test-response.json'): TestResponse {
/** @var TestResponse $this */
File::put(storage_path('logs/' . $file), $this->getContent());
return $this;
});
}
}
}

View file

@ -28,7 +28,6 @@ class AlbumRepository extends Repository
/** @return Collection|array<array-key, Album> */
public function getMostPlayed(int $count = 6, ?User $user = null): Collection
{
/** @var User $user */
$user ??= $this->auth->user();
$query = Album::query()

View file

@ -14,7 +14,7 @@ class ArtistRepository extends Repository
/** @return Collection|array<array-key, Artist> */
public function getMostPlayed(int $count = 6, ?User $user = null): Collection
{
/** @var User $user */
/** @var ?User $user */
$user ??= auth()->user();
$query = Artist::query()

View file

@ -29,7 +29,7 @@ class SongRepository extends Repository
/** @return Collection|array<array-key, Song> */
public function getRecentlyAdded(int $count = 10, ?User $scopedUser = null): Collection
{
/** @var User $scopedUser */
/** @var ?User $scopedUser */
$scopedUser ??= $this->auth->user();
return Song::query()
@ -43,7 +43,7 @@ class SongRepository extends Repository
/** @return Collection|array<array-key, Song> */
public function getMostPlayed(int $count = 7, ?User $scopedUser = null): Collection
{
/** @var User $scopedUser */
/** @var ?User $scopedUser */
$scopedUser ??= $this->auth->user();
return Song::query()
@ -58,7 +58,7 @@ class SongRepository extends Repository
/** @return Collection|array<array-key, Song> */
public function getRecentlyPlayed(int $count = 7, ?User $scopedUser = null): Collection
{
/** @var User $scopedUser */
/** @var ?User $scopedUser */
$scopedUser ??= $this->auth->user();
return Song::query()
@ -75,7 +75,7 @@ class SongRepository extends Repository
?User $scopedUser = null,
int $perPage = 50
): Paginator {
/** @var User $scopedUser */
/** @var ?User $scopedUser */
$scopedUser ??= $this->auth->user();
return Song::query()
@ -92,7 +92,7 @@ class SongRepository extends Repository
?User $scopedUser = null,
int $perPage = 50
): Paginator {
/** @var User $scopedUser */
/** @var ?User $scopedUser */
$scopedUser ??= $this->auth->user();
return Song::query()
@ -110,7 +110,7 @@ class SongRepository extends Repository
int $limit = self::DEFAULT_QUEUE_LIMIT,
?User $scopedUser = null,
): Collection {
/** @var User $scopedUser */
/** @var ?User $scopedUser */
$scopedUser ??= $this->auth->user();
return Song::query()
@ -124,7 +124,7 @@ class SongRepository extends Repository
/** @return Collection|array<array-key, Song> */
public function getFavorites(?User $scopedUser = null): Collection
{
/** @var User $scopedUser */
/** @var ?User $scopedUser */
$scopedUser ??= $this->auth->user();
return Song::query()
@ -137,7 +137,7 @@ class SongRepository extends Repository
/** @return Collection|array<array-key, Song> */
public function getByAlbum(Album $album, ?User $scopedUser = null): Collection
{
/** @var User $scopedUser */
/** @var ?User $scopedUser */
$scopedUser ??= $this->auth->user();
return Song::query()
@ -153,7 +153,7 @@ class SongRepository extends Repository
/** @return Collection|array<array-key, Song> */
public function getByArtist(Artist $artist, ?User $scopedUser = null): Collection
{
/** @var User $scopedUser */
/** @var ?User $scopedUser */
$scopedUser ??= $this->auth->user();
return Song::query()
@ -171,7 +171,7 @@ class SongRepository extends Repository
/** @return Collection|array<array-key, Song> */
public function getByStandardPlaylist(Playlist $playlist, ?User $scopedUser = null): Collection
{
/** @var User $scopedUser */
/** @var ?User $scopedUser */
$scopedUser ??= $this->auth->user();
return Song::query()
@ -187,7 +187,7 @@ class SongRepository extends Repository
/** @return Collection|array<array-key, Song> */
public function getRandom(int $limit, ?User $scopedUser = null): Collection
{
/** @var User $scopedUser */
/** @var ?User $scopedUser */
$scopedUser ??= $this->auth->user();
return Song::query()
@ -201,7 +201,7 @@ class SongRepository extends Repository
/** @return Collection|array<array-key, Song> */
public function getMany(array $ids, bool $inThatOrder = false, ?User $scopedUser = null): Collection
{
/** @var User $scopedUser */
/** @var ?User $scopedUser */
$scopedUser ??= $this->auth->user();
$songs = Song::query()
@ -215,7 +215,7 @@ class SongRepository extends Repository
public function getOne($id, ?User $scopedUser = null): Song
{
/** @var User $scopedUser */
/** @var ?User $scopedUser */
$scopedUser ??= $this->auth->user();
return Song::query()
@ -226,7 +226,7 @@ class SongRepository extends Repository
public function findOne($id, ?User $scopedUser = null): ?Song
{
/** @var User $scopedUser */
/** @var ?User $scopedUser */
$scopedUser ??= $this->auth->user();
return Song::query()
@ -248,7 +248,7 @@ class SongRepository extends Repository
/** @return Collection|array<array-key, Song> */
public function getRandomByGenre(string $genre, int $limit, ?User $scopedUser = null): Collection
{
/** @var User $scopedUser */
/** @var ?User $scopedUser */
$scopedUser ??= $this->auth->user();
return Song::query()

View file

@ -2,14 +2,11 @@
namespace App\Services;
use App\Models\Album;
use App\Models\Artist;
use App\Models\Playlist;
use App\Models\Song;
use App\Models\SongZipArchive;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
use InvalidArgumentException;
use Illuminate\Support\Facades\File;
class DownloadService
{
@ -17,57 +14,7 @@ class DownloadService
{
}
/**
* Generic method to generate a download archive from various source types.
*
* @return string Full path to the generated archive
*/
public function from(Playlist|Song|Album|Artist|Collection $downloadable): string
{
switch (get_class($downloadable)) {
case Song::class:
return $this->fromSong($downloadable);
case Collection::class:
case EloquentCollection::class:
return $this->fromMultipleSongs($downloadable);
case Album::class:
return $this->fromAlbum($downloadable);
case Artist::class:
return $this->fromArtist($downloadable);
case Playlist::class:
return $this->fromPlaylist($downloadable);
}
throw new InvalidArgumentException('Unsupported download type.');
}
public function fromSong(Song $song): string
{
if ($song->s3_params) {
// The song is hosted on Amazon S3.
// We download it back to our local server first.
$url = $this->s3Service->getSongPublicUrl($song);
abort_unless((bool) $url, 404);
$localPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . basename($song->s3_params['key']);
// The following function requires allow_url_fopen to be ON.
// We're just assuming that to be the case here.
copy($url, $localPath);
} else {
// The song is hosted locally. Make sure the file exists.
$localPath = $song->path;
abort_unless(file_exists($localPath), 404);
}
return $localPath;
}
private function fromMultipleSongs(Collection $songs): string
public function from(Collection $songs): string
{
if ($songs->count() === 1) {
return $this->fromSong($songs->first());
@ -79,24 +26,25 @@ class DownloadService
->getPath();
}
private function fromPlaylist(Playlist $playlist): string
public function fromSong(Song $song): string
{
return $this->fromMultipleSongs($playlist->songs);
}
if ($song->s3_params) {
// The song is hosted on Amazon S3.
// We download it back to our local server first.
$url = $this->s3Service->getSongPublicUrl($song);
abort_unless((bool) $url, Response::HTTP_NOT_FOUND);
private function fromAlbum(Album $album): string
{
return $this->fromMultipleSongs($album->songs);
}
$localPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . basename($song->s3_params['key']);
public function fromArtist(Artist $artist): string
{
// We cater to the case where the artist is an "album artist," which means she has songs through albums as well.
$songs = $artist->albums->reduce(
static fn (Collection $songs, Album $album) => $songs->merge($album->songs),
$artist->songs
)->unique('id');
// The following function requires allow_url_fopen to be ON.
// We're just assuming that to be the case here.
copy($url, $localPath);
} else {
// The song is hosted locally. Make sure the file exists.
$localPath = $song->path;
abort_unless(File::exists($localPath), Response::HTTP_NOT_FOUND);
}
return $this->fromMultipleSongs($songs);
return $localPath;
}
}

View file

@ -42,7 +42,7 @@ class LicenseService
return Cache::get('license_status');
}
/** @var License $license */
/** @var ?License $license */
$license = License::query()->latest()->first();
if (!$license) {

View file

@ -30,12 +30,14 @@ Route::middleware('web')->group(static function (): void {
Route::middleware('audio.auth')->group(static function (): void {
Route::get('play/{song}/{transcode?}/{bitrate?}', PlayController::class)->name('song.play');
Route::prefix('download')->group(static function (): void {
Route::get('songs', DownloadSongsController::class);
Route::get('album/{album}', DownloadAlbumController::class);
Route::get('artist/{artist}', DownloadArtistController::class);
Route::get('playlist/{playlist}', DownloadPlaylistController::class);
Route::get('favorites', DownloadFavoritesController::class);
});
if (config('koel.download.allow')) {
Route::prefix('download')->group(static function (): void {
Route::get('songs', DownloadSongsController::class);
Route::get('album/{album}', DownloadAlbumController::class);
Route::get('artist/{artist}', DownloadArtistController::class);
Route::get('playlist/{playlist}', DownloadPlaylistController::class);
Route::get('favorites', DownloadFavoritesController::class);
});
}
});
});

View file

@ -19,7 +19,7 @@ class UploadTest extends TestCase
{
parent::setUp();
$this->file = UploadedFile::fromFile(test_path('songs/full.mp3'), 'song.mp3');
$this->file = UploadedFile::fromFile(test_path('songs/full.mp3'), 'song.mp3'); //@phpstan-ignore-line
}
public function testUnauthorizedPost(): void

View file

@ -37,11 +37,10 @@ class MediaScannerTest extends TestCase
public function testScan(): void
{
/** @var User $owner */
$owner = User::factory()->admin()->create();
$this->expectsEvents(MediaScanCompleted::class);
/** @var User $owner */
$owner = User::factory()->admin()->create();
$this->scanner->scan(ScanConfiguration::make(owner: $owner));
// Standard mp3 files under root path should be recognized
@ -94,10 +93,11 @@ class MediaScannerTest extends TestCase
public function testModifiedFileIsRescanned(): void
{
$config = ScanConfiguration::make(owner: User::factory()->admin()->create());
$this->expectsEvents(MediaScanCompleted::class);
/** @var User $owner */
$owner = User::factory()->admin()->create();
$config = ScanConfiguration::make(owner: $owner);
$this->scanner->scan($config);
/** @var Song $song */
@ -111,10 +111,12 @@ class MediaScannerTest extends TestCase
public function testRescanWithoutForceDoesNotResetData(): void
{
$config = ScanConfiguration::make(owner: User::factory()->admin()->create());
$this->expectsEvents(MediaScanCompleted::class);
/** @var User $owner */
$owner = User::factory()->admin()->create();
$config = ScanConfiguration::make(owner: $owner);
$this->scanner->scan($config);
/** @var Song $song */
@ -134,11 +136,10 @@ class MediaScannerTest extends TestCase
public function testForceScanResetsData(): void
{
/** @var User $owner */
$owner = User::factory()->admin()->create();
$this->expectsEvents(MediaScanCompleted::class);
/** @var User $owner */
$owner = User::factory()->admin()->create();
$this->scanner->scan(ScanConfiguration::make(owner: $owner));
/** @var Song $song */
@ -149,7 +150,9 @@ class MediaScannerTest extends TestCase
'lyrics' => 'Booom Wroooom',
]);
$this->scanner->scan(ScanConfiguration::make(owner: User::factory()->admin()->create(), force: true));
/** @var User $anotherOwner */
$anotherOwner = User::factory()->admin()->create();
$this->scanner->scan(ScanConfiguration::make(owner: $anotherOwner, force: true));
$song->refresh();
@ -161,11 +164,10 @@ class MediaScannerTest extends TestCase
public function testScanWithIgnoredTags(): void
{
/** @var User $owner */
$owner = User::factory()->admin()->create();
$this->expectsEvents(MediaScanCompleted::class);
/** @var User $owner */
$owner = User::factory()->admin()->create();
$this->scanner->scan(ScanConfiguration::make(owner: $owner));
/** @var Song $song */
@ -186,10 +188,10 @@ class MediaScannerTest extends TestCase
public function testScanAllTagsForNewFilesRegardlessOfIgnoredOption(): void
{
$this->expectsEvents(MediaScanCompleted::class);
/** @var User $owner */
$owner = User::factory()->admin()->create();
$this->expectsEvents(MediaScanCompleted::class);
$this->scanner->scan(ScanConfiguration::make(owner: $owner));
/** @var Song $song */
@ -214,11 +216,14 @@ class MediaScannerTest extends TestCase
{
$this->expectsEvents(LibraryChanged::class);
/** @var User $owner */
$owner = User::factory()->admin()->create();
$path = $this->path('/blank.mp3');
$this->scanner->scanWatchRecord(
new InotifyWatchRecord("CLOSE_WRITE,CLOSE $path"),
ScanConfiguration::make(owner: User::factory()->admin()->create())
ScanConfiguration::make(owner: $owner)
);
self::assertDatabaseHas(Song::class, ['path' => $path]);
@ -228,6 +233,9 @@ class MediaScannerTest extends TestCase
{
$this->expectsEvents(LibraryChanged::class);
/** @var User $owner */
$owner = User::factory()->admin()->create();
static::createSampleMediaSet();
/** @var Song $song */
@ -235,7 +243,7 @@ class MediaScannerTest extends TestCase
$this->scanner->scanWatchRecord(
new InotifyWatchRecord("DELETE $song->path"),
ScanConfiguration::make(owner: User::factory()->admin()->create())
ScanConfiguration::make(owner: $owner)
);
self::assertModelMissing($song);
@ -243,10 +251,12 @@ class MediaScannerTest extends TestCase
public function testScanDeletedDirectoryViaWatch(): void
{
$config = ScanConfiguration::make(owner: User::factory()->admin()->create());
$this->expectsEvents(LibraryChanged::class, MediaScanCompleted::class);
/** @var User $owner */
$owner = User::factory()->admin()->create();
$config = ScanConfiguration::make(owner: $owner);
$this->scanner->scan($config);
$this->scanner->scanWatchRecord(new InotifyWatchRecord("MOVED_FROM,ISDIR $this->mediaPath/subdir"), $config);
@ -290,7 +300,9 @@ class MediaScannerTest extends TestCase
public function testOptionallyIgnoreHiddenFiles(): void
{
$config = ScanConfiguration::make(owner: User::factory()->admin()->create());
/** @var User $owner */
$owner = User::factory()->admin()->create();
$config = ScanConfiguration::make(owner: $owner);
config(['koel.ignore_dot_files' => false]);
$this->scanner->scan($config);

View file

@ -51,7 +51,7 @@ class UploadServiceTest extends TestCase
/** @var User $user */
$user = User::factory()->create();
$song = $this->service->handleUploadedFile(UploadedFile::fromFile(test_path('songs/full.mp3')), $user);
$song = $this->service->handleUploadedFile(UploadedFile::fromFile(test_path('songs/full.mp3')), $user); //@phpstan-ignore-line
self::assertSame($song->owner_id, $user->id);
self::assertSame(public_path("sandbox/media/__KOEL_UPLOADS_\${$user->id}__/full.mp3"), $song->path);

View file

@ -10,9 +10,6 @@ use App\Services\CommunityLicenseService;
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\File;
use Illuminate\Testing\TestResponse;
use ReflectionClass;
use Tests\Traits\CreatesApplication;
use Tests\Traits\SandboxesTests;
@ -29,18 +26,6 @@ abstract class TestCase extends BaseTestCase
parent::setUp();
License::swap($this->app->make(CommunityLicenseService::class));
TestResponse::macro('log', function (string $file = 'test-response.json'): TestResponse {
/** @var TestResponse $this */
File::put(storage_path('logs/' . $file), $this->getContent());
return $this;
});
UploadedFile::macro('fromFile', static function (string $path, ?string $name = null): UploadedFile {
return UploadedFile::fake()->createWithContent($name ?? basename($path), File::get($path));
});
self::createSandbox();
}