refactor: avoid leadking database keys (#1874)

This commit is contained in:
Phan An 2024-11-09 15:56:48 +01:00 committed by GitHub
parent bcfbf31b35
commit aa7ddd9d94
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
76 changed files with 286 additions and 293 deletions

View file

@ -34,7 +34,7 @@ class ChangePasswordCommand extends Command
return self::FAILURE; return self::FAILURE;
} }
$this->comment("Changing the user's password (ID: $user->id, email: $user->email)"); $this->comment("Changing the user's password (ID: {$user->id}, email: $user->email)");
$user->password = $this->hash->make($this->askForPassword()); $user->password = $this->hash->make($this->askForPassword());
$user->save(); $user->save();

View file

@ -173,7 +173,7 @@ class ScanCommand extends Command
exit(self::INVALID); exit(self::INVALID);
}); });
$this->components->info("Setting owner to $user->name (ID $user->id)."); $this->components->info("Setting owner to $user->name (ID {$user->id}).");
return $user; return $user;
} }
@ -181,7 +181,7 @@ class ScanCommand extends Command
$user = $this->userRepository->getDefaultAdminUser(); $user = $this->userRepository->getDefaultAdminUser();
$this->components->warn( $this->components->warn(
"No song owner specified. Setting the first admin ($user->name, ID $user->id) as owner." "No song owner specified. Setting the first admin ($user->name, ID {$user->id}) as owner."
); );
return $user; return $user;

View file

@ -10,6 +10,6 @@ final class UserAlreadySubscribedToPodcast extends Exception
{ {
public static function make(User $user, Podcast $podcast): self public static function make(User $user, Podcast $podcast): self
{ {
return new self("User $user->id has already subscribed to podcast $podcast->id"); return new self("User {$user->id} has already subscribed to podcast {$podcast->id}");
} }
} }

View file

@ -2,6 +2,7 @@
namespace App\Http\Controllers\API; namespace App\Http\Controllers\API;
use App\Facades\License;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\API\ChangeSongsVisibilityRequest; use App\Http\Requests\API\ChangeSongsVisibilityRequest;
use App\Models\Song; use App\Models\Song;
@ -14,6 +15,8 @@ class PrivatizeSongsController extends Controller
/** @param User $user */ /** @param User $user */
public function __invoke(ChangeSongsVisibilityRequest $request, SongService $songService, Authenticatable $user) public function __invoke(ChangeSongsVisibilityRequest $request, SongService $songService, Authenticatable $user)
{ {
License::requirePlus();
$songs = Song::query()->findMany($request->songs); $songs = Song::query()->findMany($request->songs);
$songs->each(fn ($song) => $this->authorize('own', $song)); $songs->each(fn ($song) => $this->authorize('own', $song));

View file

@ -2,6 +2,7 @@
namespace App\Http\Controllers\API; namespace App\Http\Controllers\API;
use App\Facades\License;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\API\ChangeSongsVisibilityRequest; use App\Http\Requests\API\ChangeSongsVisibilityRequest;
use App\Models\Song; use App\Models\Song;
@ -14,6 +15,8 @@ class PublicizeSongsController extends Controller
/** @param User $user */ /** @param User $user */
public function __invoke(ChangeSongsVisibilityRequest $request, SongService $songService, Authenticatable $user) public function __invoke(ChangeSongsVisibilityRequest $request, SongService $songService, Authenticatable $user)
{ {
License::requirePlus();
$songs = Song::query()->findMany($request->songs); $songs = Song::query()->findMany($request->songs);
$songs->each(fn ($song) => $this->authorize('own', $song)); $songs->each(fn ($song) => $this->authorize('own', $song));

View file

@ -15,7 +15,7 @@ class UploadAlbumCoverController extends Controller
$this->authorize('update', $album); $this->authorize('update', $album);
$metadataService->writeAlbumCover($album, $request->getFileContent()); $metadataService->writeAlbumCover($album, $request->getFileContent());
Cache::delete("album.info.$album->id"); Cache::delete("album.info.{$album->id}");
return response()->json(['cover_url' => $album->cover]); return response()->json(['cover_url' => $album->cover]);
} }

View file

@ -15,7 +15,7 @@ class UploadArtistImageController extends Controller
$this->authorize('update', $artist); $this->authorize('update', $artist);
$metadataService->writeArtistImage($artist, $request->getFileContent()); $metadataService->writeArtistImage($artist, $request->getFileContent());
Cache::delete("artist.info.$artist->id"); Cache::delete("artist.info.{$artist->id}");
return response()->json(['image_url' => $artist->image]); return response()->json(['image_url' => $artist->image]);
} }

View file

@ -18,7 +18,7 @@ class ProfileUpdateRequest extends Request
{ {
return [ return [
'name' => 'required', 'name' => 'required',
'email' => 'required|email|unique:users,email,' . auth()->user()->id, 'email' => 'required|email|unique:users,email,' . auth()->user()->getAuthIdentifier(),
'current_password' => 'sometimes|required_with:new_password', 'current_password' => 'sometimes|required_with:new_password',
'new_password' => ['sometimes', Password::defaults()], 'new_password' => ['sometimes', Password::defaults()],
]; ];

View file

@ -19,7 +19,6 @@ use Laravel\Scout\Searchable;
* @property string $cover The album cover's URL * @property string $cover The album cover's URL
* @property string|null $cover_path The absolute path to the cover file * @property string|null $cover_path The absolute path to the cover file
* @property bool $has_cover If the album has a non-default cover image * @property bool $has_cover If the album has a non-default cover image
* @property int $id
* @property string $name Name of the album * @property string $name Name of the album
* @property Artist $artist The album's artist * @property Artist $artist The album's artist
* @property int $artist_id * @property int $artist_id

View file

@ -8,6 +8,7 @@ use App\Models\Song as Playable;
use App\Values\SmartPlaylistRuleGroupCollection; use App\Values\SmartPlaylistRuleGroupCollection;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -24,16 +25,16 @@ use Laravel\Scout\Searchable;
* @property bool $is_smart * @property bool $is_smart
* @property int $user_id * @property int $user_id
* @property User $user * @property User $user
* @property Collection<array-key, Playable> $playables
* @property ?SmartPlaylistRuleGroupCollection $rule_groups * @property ?SmartPlaylistRuleGroupCollection $rule_groups
* @property ?SmartPlaylistRuleGroupCollection $rules * @property ?SmartPlaylistRuleGroupCollection $rules
* @property Carbon $created_at * @property Carbon $created_at
* @property EloquentCollection<array-key, Playable> $playables
* @property EloquentCollection<array-key, User> $collaborators
* @property bool $own_songs_only * @property bool $own_songs_only
* @property Collection<array-key, User> $collaborators
* @property-read bool $is_collaborative
* @property-read ?string $cover The playlist cover's URL * @property-read ?string $cover The playlist cover's URL
* @property-read ?string $cover_path * @property-read ?string $cover_path
* @property-read Collection<array-key, PlaylistFolder> $folders * @property-read EloquentCollection<array-key, PlaylistFolder> $folders
* @property-read bool $is_collaborative
*/ */
class Playlist extends Model class Playlist extends Model
{ {
@ -107,7 +108,7 @@ class Playlist extends Model
public function ownedBy(User $user): bool public function ownedBy(User $user): bool
{ {
return $this->user_id === $user->id; return $this->user->is($user);
} }
public function inFolder(PlaylistFolder $folder): bool public function inFolder(PlaylistFolder $folder): bool

View file

@ -11,7 +11,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/** /**
* @property string $id
* @property string $name * @property string $name
* @property User $user * @property User $user
* @property Collection<array-key, Playlist> $playlists * @property Collection<array-key, Playlist> $playlists
@ -38,6 +37,6 @@ class PlaylistFolder extends Model
public function ownedBy(User $user): bool public function ownedBy(User $user): bool
{ {
return $this->user_id === $user->id; return $this->user->is($user);
} }
} }

View file

@ -162,6 +162,7 @@ class Song extends Model
public function ownedBy(User $user): bool public function ownedBy(User $user): bool
{ {
// Do not use $song->owner->is($user) here, as it may trigger an extra query.
return $this->owner_id === $user->id; return $this->owner_id === $user->id;
} }

View file

@ -92,7 +92,7 @@ class User extends Authenticatable
public function subscribedToPodcast(Podcast $podcast): bool public function subscribedToPodcast(Podcast $podcast): bool
{ {
return $this->podcasts()->where('podcast_id', $podcast->id)->exists(); return $this->podcasts()->whereKey($podcast)->exists();
} }
public function subscribeToPodcast(Podcast $podcast): void public function subscribeToPodcast(Podcast $podcast): void

View file

@ -10,6 +10,7 @@ class SongPolicy
{ {
public function own(User $user, Song $song): bool public function own(User $user, Song $song): bool
{ {
// Do not use $song->owner->is($user) here, as it may trigger an extra query.
return $song->owner_id === $user->id; return $song->owner_id === $user->id;
} }

View file

@ -14,7 +14,7 @@ class MacroProvider extends ServiceProvider
{ {
public function boot(): void public function boot(): void
{ {
Collection::macro('orderByArray', function (array $orderBy, string $key = 'id'): Collection { Collection::macro('orderByArray', function (array $orderBy, string $key = 'id') {
/** @var Collection $this */ /** @var Collection $this */
return $this->sortBy(static fn ($item) => array_search($item->$key, $orderBy, true))->values(); return $this->sortBy(static fn ($item) => array_search($item->$key, $orderBy, true))->values();
}); });

View file

@ -8,8 +8,8 @@ use App\Models\Album;
use App\Models\Artist; use App\Models\Artist;
use App\Models\User; use App\Models\User;
use Illuminate\Contracts\Pagination\Paginator; use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Query\JoinClause; use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
/** /**
* @extends Repository<Album> * @extends Repository<Album>

View file

@ -9,7 +9,6 @@ use App\Models\User;
use Illuminate\Contracts\Pagination\Paginator; use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Query\JoinClause; use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection as BaseCollection;
/** @extends Repository<Artist> */ /** @extends Repository<Artist> */
class ArtistRepository extends Repository class ArtistRepository extends Repository
@ -44,7 +43,7 @@ class ArtistRepository extends Repository
} }
/** @return Collection|array<array-key, Artist> */ /** @return Collection|array<array-key, Artist> */
public function getMany(array $ids, bool $preserveOrder = false, ?User $user = null): Collection|BaseCollection public function getMany(array $ids, bool $preserveOrder = false, ?User $user = null): Collection
{ {
$artists = Artist::query() $artists = Artist::query()
->isStandard() ->isStandard()

View file

@ -2,9 +2,8 @@
namespace App\Repositories\Contracts; namespace App\Repositories\Contracts;
use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
/** @template T of Model */ /** @template T of Model */
interface RepositoryInterface interface RepositoryInterface
@ -25,7 +24,7 @@ interface RepositoryInterface
public function getMany(array $ids, bool $preserveOrder = false): Collection; public function getMany(array $ids, bool $preserveOrder = false): Collection;
/** @return Collection<int, T> */ /** @return Collection<int, T> */
public function getAll(): EloquentCollection; public function getAll(): Collection;
/** @return T|null */ /** @return T|null */
public function findFirstWhere(...$params): ?Model; public function findFirstWhere(...$params): ?Model;

View file

@ -4,7 +4,7 @@ namespace App\Repositories;
use App\Models\Podcast; use App\Models\Podcast;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Collection;
/** @extends Repository<Podcast> */ /** @extends Repository<Podcast> */
class PodcastRepository extends Repository class PodcastRepository extends Repository

View file

@ -4,9 +4,8 @@ namespace App\Repositories;
use App\Repositories\Contracts\RepositoryInterface; use App\Repositories\Contracts\RepositoryInterface;
use Illuminate\Contracts\Auth\Guard; use Illuminate\Contracts\Auth\Guard;
use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
/** /**
* @template T of Model * @template T of Model
@ -68,9 +67,9 @@ abstract class Repository implements RepositoryInterface
} }
/** @inheritDoc */ // @phpcs:ignore /** @inheritDoc */ // @phpcs:ignore
public function getAll(): EloquentCollection public function getAll(): Collection
{ {
return $this->modelClass::all(); return $this->modelClass::all(); // @phpstan-ignore-line
} }
/** @inheritDoc */ /** @inheritDoc */

View file

@ -13,7 +13,7 @@ use App\Models\Song;
use App\Models\User; use App\Models\User;
use App\Values\Genre; use App\Values\Genre;
use Illuminate\Contracts\Pagination\Paginator; use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Collection;
/** @extends Repository<Song> */ /** @extends Repository<Song> */
class SongRepository extends Repository class SongRepository extends Repository
@ -78,7 +78,10 @@ class SongRepository extends Repository
return Song::query(type: PlayableType::SONG, user: $scopedUser) return Song::query(type: PlayableType::SONG, user: $scopedUser)
->accessible() ->accessible()
->withMeta() ->withMeta()
->when($ownSongsOnly, static fn (SongBuilder $query) => $query->where('songs.owner_id', $scopedUser->id)) ->when(
$ownSongsOnly,
static fn (SongBuilder $query) => $query->where('songs.owner_id', $scopedUser->id)
)
->sort($sortColumns, $sortDirection) ->sort($sortColumns, $sortDirection)
->simplePaginate($perPage); ->simplePaginate($perPage);
} }
@ -129,7 +132,7 @@ class SongRepository extends Repository
return Song::query(user: $scopedUser ?? $this->auth->user()) return Song::query(user: $scopedUser ?? $this->auth->user())
->accessible() ->accessible()
->withMeta() ->withMeta()
->where('album_id', $album->id) ->whereBelongsTo($album)
->orderBy('songs.disc') ->orderBy('songs.disc')
->orderBy('songs.track') ->orderBy('songs.track')
->orderBy('songs.title') ->orderBy('songs.title')
@ -195,7 +198,7 @@ class SongRepository extends Repository
->whereIn('songs.id', $ids) ->whereIn('songs.id', $ids)
->get(); ->get();
return $preserveOrder ? $songs->orderByArray($ids) : $songs; return $preserveOrder ? $songs->orderByArray($ids) : $songs; // @phpstan-ignore-line
} }
/** /**

View file

@ -22,7 +22,7 @@ final class AllPlaylistsAreAccessibleBy implements ValidationRule
$accessiblePlaylists = $accessiblePlaylists->merge($this->user->collaboratedPlaylists); $accessiblePlaylists = $accessiblePlaylists->merge($this->user->collaboratedPlaylists);
} }
if (array_diff(Arr::wrap($value), $accessiblePlaylists->pluck('id')->toArray())) { if (array_diff(Arr::wrap($value), $accessiblePlaylists->modelKeys())) {
$fail( $fail(
License::isPlus() License::isPlus()
? 'Not all playlists are accessible by the user' ? 'Not all playlists are accessible by the user'

View file

@ -9,7 +9,7 @@ use App\Services\SongStorages\DropboxStorage;
use App\Services\SongStorages\S3CompatibleStorage; use App\Services\SongStorages\S3CompatibleStorage;
use App\Services\SongStorages\SftpStorage; use App\Services\SongStorages\SftpStorage;
use App\Values\Podcast\EpisodePlayable; use App\Values\Podcast\EpisodePlayable;
use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
class DownloadService class DownloadService
@ -17,7 +17,7 @@ class DownloadService
public function getDownloadablePath(Collection $songs): ?string public function getDownloadablePath(Collection $songs): ?string
{ {
if ($songs->count() === 1) { if ($songs->count() === 1) {
return $this->getLocalPath($songs->first()); return $this->getLocalPath($songs->first()); // @phpstan-ignore-line
} }
return (new SongZipArchive()) return (new SongZipArchive())

View file

@ -8,7 +8,7 @@ use App\Events\SongLikeToggled;
use App\Models\Interaction; use App\Models\Interaction;
use App\Models\Song as Playable; use App\Models\Song as Playable;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Collection;
class InteractionService class InteractionService
{ {
@ -57,14 +57,20 @@ class InteractionService
public function likeMany(Collection $playables, User $user): Collection public function likeMany(Collection $playables, User $user): Collection
{ {
$interactions = $playables->map(static function (Playable $playable) use ($user): Interaction { $interactions = $playables->map(static function (Playable $playable) use ($user): Interaction {
return tap(Interaction::query()->firstOrCreate([ $interaction = Interaction::query()->whereBelongsTo($playable)->whereBelongsTo($user)->first();
'song_id' => $playable->id,
'user_id' => $user->id, if ($interaction) {
]), static function (Interaction $interaction): void { $interaction->update(['liked' => true]);
$interaction->play_count ??= 0; } else {
$interaction->liked = true; $interaction = Interaction::query()->create([
$interaction->save(); 'song_id' => $playable->id,
}); 'user_id' => $user->id,
'play_count' => 0,
'liked' => true,
]);
}
return $interaction;
}); });
event(new MultipleSongsLiked($playables, $user)); event(new MultipleSongsLiked($playables, $user));
@ -80,8 +86,8 @@ class InteractionService
public function unlikeMany(Collection $playables, User $user): void public function unlikeMany(Collection $playables, User $user): void
{ {
Interaction::query() Interaction::query()
->whereIn('song_id', $playables->pluck('id')->all()) ->whereBelongsTo($playables)
->where('user_id', $user->id) ->whereBelongsTo($user)
->update(['liked' => false]); ->update(['liked' => false]);
event(new MultipleSongsUnliked($playables, $user)); event(new MultipleSongsUnliked($playables, $user));

View file

@ -50,7 +50,7 @@ class LastfmService implements MusicEncyclopedia
return rescue_if(static::enabled(), function () use ($artist): ?ArtistInformation { return rescue_if(static::enabled(), function () use ($artist): ?ArtistInformation {
return Cache::remember( return Cache::remember(
"lastfm.artist.$artist->id", "lastfm.artist.{$artist->id}",
now()->addWeek(), now()->addWeek(),
fn () => $this->connector->send(new GetArtistInfoRequest($artist))->dto() fn () => $this->connector->send(new GetArtistInfoRequest($artist))->dto()
); );
@ -65,7 +65,7 @@ class LastfmService implements MusicEncyclopedia
return rescue_if(static::enabled(), function () use ($album): ?AlbumInformation { return rescue_if(static::enabled(), function () use ($album): ?AlbumInformation {
return Cache::remember( return Cache::remember(
"lastfm.album.$album->id", "lastfm.album.{$album->id}",
now()->addWeek(), now()->addWeek(),
fn () => $this->connector->send(new GetAlbumInfoRequest($album))->dto() fn () => $this->connector->send(new GetAlbumInfoRequest($album))->dto()
); );

View file

@ -23,16 +23,20 @@ class MediaInformationService
return null; return null;
} }
return Cache::remember("album.info.$album->id", now()->addWeek(), function () use ($album): AlbumInformation { return Cache::remember(
$info = $this->encyclopedia->getAlbumInformation($album) ?: AlbumInformation::make(); "album.info.{$album->id}",
now()->addWeek(),
function () use ($album): AlbumInformation {
$info = $this->encyclopedia->getAlbumInformation($album) ?: AlbumInformation::make();
rescue_unless($album->has_cover, function () use ($info, $album): void { rescue_unless($album->has_cover, function () use ($info, $album): void {
$this->mediaMetadataService->tryDownloadAlbumCover($album); $this->mediaMetadataService->tryDownloadAlbumCover($album);
$info->cover = $album->cover; $info->cover = $album->cover;
}); });
return $info; return $info;
}); }
);
} }
public function getArtistInformation(Artist $artist): ?ArtistInformation public function getArtistInformation(Artist $artist): ?ArtistInformation
@ -42,7 +46,7 @@ class MediaInformationService
} }
return Cache::remember( return Cache::remember(
"artist.info.$artist->id", "artist.info.{$artist->id}",
now()->addWeek(), now()->addWeek(),
function () use ($artist): ArtistInformation { function () use ($artist): ArtistInformation {
$info = $this->encyclopedia->getArtistInformation($artist) ?: ArtistInformation::make(); $info = $this->encyclopedia->getArtistInformation($artist) ?: ArtistInformation::make();

View file

@ -11,6 +11,7 @@ use App\Models\Song as Playable;
use App\Models\User; use App\Models\User;
use App\Repositories\SongRepository; use App\Repositories\SongRepository;
use App\Values\SmartPlaylistRuleGroupCollection; use App\Values\SmartPlaylistRuleGroupCollection;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use InvalidArgumentException; use InvalidArgumentException;
@ -85,12 +86,12 @@ class PlaylistService
return $playlist; return $playlist;
} }
/** @return Collection<array-key, Playable> */ /** @return EloquentCollection<array-key, Playable> */
public function addPlayablesToPlaylist( public function addPlayablesToPlaylist(
Playlist $playlist, Playlist $playlist,
Collection|Playable|array $playables, Collection|Playable|array $playables,
User $user User $user
): Collection { ): EloquentCollection {
return DB::transaction(function () use ($playlist, $playables, $user) { return DB::transaction(function () use ($playlist, $playables, $user) {
$playables = Collection::wrap($playables); $playables = Collection::wrap($playables);

View file

@ -16,11 +16,12 @@ class QueueService
public function getQueueState(User $user): QueueStateDTO public function getQueueState(User $user): QueueStateDTO
{ {
$state = QueueState::query()->where('user_id', $user->id)->firstOrCreate([ $state = QueueState::query()
'user_id' => $user->id, ->whereBelongsTo($user)
], [ ->firstOrCreate(
'song_ids' => [], ['user_id' => $user->id],
]); ['song_ids' => []],
);
$currentSong = $state->current_song_id ? $this->songRepository->findOne($state->current_song_id, $user) : null; $currentSong = $state->current_song_id ? $this->songRepository->findOne($state->current_song_id, $user) : null;
@ -33,11 +34,7 @@ class QueueService
public function updateQueueState(User $user, array $songIds): void public function updateQueueState(User $user, array $songIds): void
{ {
QueueState::query()->updateOrCreate([ QueueState::query()->updateOrCreate(['user_id' => $user->id], ['song_ids' => $songIds]);
'user_id' => $user->id,
], [
'song_ids' => $songIds,
]);
} }
public function updatePlaybackStatus(User $user, Song $song, int $position): void public function updatePlaybackStatus(User $user, Song $song, int $position): void

View file

@ -52,7 +52,7 @@ class SearchService
{ {
try { try {
return $repository->getMany( return $repository->getMany(
ids: $repository->modelClass::search($keywords)->get()->take($count)->pluck('id')->all(), // @phpstan-ignore-line ids: $repository->modelClass::search($keywords)->get()->take($count)->modelKeys(), // @phpstan-ignore-line
preserveOrder: true, preserveOrder: true,
); );
} catch (Throwable $e) { } catch (Throwable $e) {

View file

@ -12,7 +12,7 @@ use App\Models\User;
use App\Values\SmartPlaylistRule as Rule; use App\Values\SmartPlaylistRule as Rule;
use App\Values\SmartPlaylistRuleGroup as RuleGroup; use App\Values\SmartPlaylistRuleGroup as RuleGroup;
use App\Values\SmartPlaylistSqlElements as SqlElements; use App\Values\SmartPlaylistSqlElements as SqlElements;
use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Collection;
class SmartPlaylistService class SmartPlaylistService
{ {

View file

@ -9,6 +9,7 @@ use App\Models\Song;
use App\Repositories\SongRepository; use App\Repositories\SongRepository;
use App\Services\SongStorages\SongStorage; use App\Services\SongStorages\SongStorage;
use App\Values\SongUpdateData; use App\Values\SongUpdateData;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@ -97,34 +98,31 @@ class SongService
return $this->songRepository->getOne($song->id); return $this->songRepository->getOne($song->id);
} }
public function markSongsAsPublic(Collection $songs): void public function markSongsAsPublic(EloquentCollection $songs): void
{ {
Song::query()->whereIn('id', $songs->pluck('id'))->update(['is_public' => true]); $songs->toQuery()->update(['is_public' => true]);
} }
/** @return array<string> IDs of songs that are marked as private */ /** @return array<string> IDs of songs that are marked as private */
public function markSongsAsPrivate(Collection $songs): array public function markSongsAsPrivate(EloquentCollection $songs): array
{ {
if (License::isPlus()) { License::requirePlus();
// Songs that are in collaborative playlists can't be marked as private.
/**
* @var Collection<array-key, Song> $collaborativeSongs
*/
$collaborativeSongs = Song::query()
->whereIn('songs.id', $songs->pluck('id'))
->join('playlist_song', 'songs.id', '=', 'playlist_song.song_id')
->join('playlist_collaborators', 'playlist_song.playlist_id', '=', 'playlist_collaborators.playlist_id')
->select('songs.id')
->distinct()
->pluck('songs.id')
->all();
$applicableSongIds = $songs->whereNotIn('id', $collaborativeSongs)->pluck('id')->all(); // Songs that are in collaborative playlists can't be marked as private.
} else { /**
$applicableSongIds = $songs->pluck('id')->all(); * @var Collection<array-key, Song> $collaborativeSongs
} */
$collaborativeSongs = $songs->toQuery()
->join('playlist_song', 'songs.id', '=', 'playlist_song.song_id')
->join('playlist_collaborators', 'playlist_song.playlist_id', '=', 'playlist_collaborators.playlist_id')
->select('songs.id')
->distinct()
->pluck('songs.id')
->all();
Song::query()->whereIn('id', $applicableSongIds)->update(['is_public' => false]); $applicableSongIds = $songs->whereNotIn('id', $collaborativeSongs)->modelKeys();
Song::query()->whereKey($applicableSongIds)->update(['is_public' => false]);
return $applicableSongIds; return $applicableSongIds;
} }

View file

@ -24,9 +24,9 @@ final class LicenseInstance implements Arrayable, Jsonable
public static function fromJsonObject(object $json): self public static function fromJsonObject(object $json): self
{ {
return new self( return new self(
id: $json->id, id: object_get($json, 'id'),
name: $json->name, name: object_get($json, 'name'),
createdAt: Carbon::parse($json->created_at), createdAt: Carbon::parse(object_get($json, 'created_at')),
); );
} }

View file

@ -28,7 +28,7 @@ final class EpisodePlayable implements Arrayable, Jsonable
public static function getForEpisode(Episode $episode): ?self public static function getForEpisode(Episode $episode): ?self
{ {
/** @var self|null $cached */ /** @var self|null $cached */
$cached = Cache::get("episode-playable.$episode->id"); $cached = Cache::get("episode-playable.{$episode->id}");
return $cached?->valid() ? $cached : self::createForEpisode($episode); return $cached?->valid() ? $cached : self::createForEpisode($episode);
} }
@ -44,7 +44,7 @@ final class EpisodePlayable implements Arrayable, Jsonable
} }
$playable = new self($file, md5_file($file)); $playable = new self($file, md5_file($file));
Cache::forever("episode-playable.$episode->id", $playable); Cache::forever("episode-playable.{$episode->id}", $playable);
return $playable; return $playable;
} }

View file

@ -13,7 +13,7 @@ class QueueStateFactory extends Factory
{ {
return [ return [
'user_id' => User::factory(), 'user_id' => User::factory(),
'song_ids' => Song::factory()->count(3)->create()->pluck('id')->toArray(), 'song_ids' => Song::factory()->count(3)->create()->modelKeys(),
'current_song_id' => null, 'current_song_id' => null,
'playback_position' => 0, 'playback_position' => 0,
]; ];

View file

@ -26,10 +26,12 @@ parameters:
# Laravel factories allow declaration of dynamic methods as "states" # Laravel factories allow declaration of dynamic methods as "states"
- '#Call to an undefined method Illuminate\\Database\\Eloquent\\Factories\\Factory::#' - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Factories\\Factory::#'
- '#expects App\\Models\\User\|null, Illuminate\\Database\\Eloquent\\Collection\|Illuminate\\Database\\Eloquent\\Model given#' - '#expects App\\Models\\User\|null, Illuminate\\Database\\Eloquent\\Collection\|Illuminate\\Database\\Eloquent\\Model given#'
- '#expects App\\Models\\[a-zA-Z]*, Illuminate\\Database\\Eloquent\\Model\|null given#'
- '#Method App\\Models\\.*::query\(\) should return App\\Builders\\.*Builder but returns Illuminate\\Database\\Eloquent\\Builder<Illuminate\\Database\\Eloquent\\Model>#' - '#Method App\\Models\\.*::query\(\) should return App\\Builders\\.*Builder but returns Illuminate\\Database\\Eloquent\\Builder<Illuminate\\Database\\Eloquent\\Model>#'
- '#Parameter \#1 \$callback of method Illuminate\\Support\\Collection<int,Illuminate\\Database\\Eloquent\\Model>::each\(\) expects callable\(Illuminate\\Database\\Eloquent\\Model, int\)#' - '#Parameter \#1 \$callback of method Illuminate\\Support\\Collection<int,Illuminate\\Database\\Eloquent\\Model>::each\(\) expects callable\(Illuminate\\Database\\Eloquent\\Model, int\)#'
- '#Access to an undefined property Illuminate\\Database\\Eloquent\\Model::#' - '#Access to an undefined property Illuminate\\Database\\Eloquent\\Model::#'
- '#Unknown parameter \$(type|user) in call to static method App\\Models\\Song::query\(\)#' - '#Unknown parameter \$(type|user) in call to static method App\\Models\\Song::query\(\)#'
- "#Called 'modelKeys' on Laravel collection, but could have been retrieved as a query#"
excludePaths: excludePaths:

View file

@ -33,7 +33,7 @@ class AlbumCoverTest extends TestCase
->once() ->once()
->with(Mockery::on(static fn (Album $target) => $target->is($album)), 'data:image/jpeg;base64,Rm9v'); ->with(Mockery::on(static fn (Album $target) => $target->is($album)), 'data:image/jpeg;base64,Rm9v');
$this->putAs("api/album/$album->id/cover", ['cover' => 'data:image/jpeg;base64,Rm9v'], create_admin()) $this->putAs("api/album/{$album->id}/cover", ['cover' => 'data:image/jpeg;base64,Rm9v'], create_admin())
->assertOk(); ->assertOk();
} }
@ -44,7 +44,7 @@ class AlbumCoverTest extends TestCase
$this->mediaMetadataService->shouldNotReceive('writeAlbumCover'); $this->mediaMetadataService->shouldNotReceive('writeAlbumCover');
$this->putAs('api/album/' . $album->id . '/cover', ['cover' => 'data:image/jpeg;base64,Rm9v'], create_user()) $this->putAs("api/album/{$album->id}/cover", ['cover' => 'data:image/jpeg;base64,Rm9v'], create_user())
->assertForbidden(); ->assertForbidden();
} }
} }

View file

@ -43,7 +43,7 @@ class AlbumInformationTest extends TestCase
] ]
)); ));
$this->getAs('api/albums/' . $album->id . '/information') $this->getAs("api/albums/{$album->id}/information")
->assertJsonStructure(AlbumInformation::JSON_STRUCTURE); ->assertJsonStructure(AlbumInformation::JSON_STRUCTURE);
} }

View file

@ -14,10 +14,9 @@ class AlbumSongTest extends TestCase
public function index(): void public function index(): void
{ {
$album = Album::factory()->create(); $album = Album::factory()->create();
Song::factory(5)->for($album)->create(); Song::factory(5)->for($album)->create();
$this->getAs('api/albums/' . $album->id . '/songs') $this->getAs("api/albums/{$album->id}/songs")
->assertJsonStructure(['*' => SongResource::JSON_STRUCTURE]); ->assertJsonStructure(['*' => SongResource::JSON_STRUCTURE]);
} }
} }

View file

@ -14,10 +14,9 @@ class ArtistAlbumTest extends TestCase
public function index(): void public function index(): void
{ {
$artist = Artist::factory()->create(); $artist = Artist::factory()->create();
Album::factory(5)->for($artist)->create(); Album::factory(5)->for($artist)->create();
$this->getAs('api/artists/' . $artist->id . '/albums') $this->getAs("api/artists/{$artist->id}/albums")
->assertJsonStructure(['*' => AlbumResource::JSON_STRUCTURE]); ->assertJsonStructure(['*' => AlbumResource::JSON_STRUCTURE]);
} }
} }

View file

@ -32,18 +32,18 @@ class ArtistImageTest extends TestCase
->once() ->once()
->with(Mockery::on(static fn (Artist $target) => $target->is($artist)), 'data:image/jpeg;base64,Rm9v'); ->with(Mockery::on(static fn (Artist $target) => $target->is($artist)), 'data:image/jpeg;base64,Rm9v');
$this->putAs("api/artist/$artist->id/image", ['image' => 'data:image/jpeg;base64,Rm9v'], create_admin()) $this->putAs("api/artist/{$artist->id}/image", ['image' => 'data:image/jpeg;base64,Rm9v'], create_admin())
->assertOk(); ->assertOk();
} }
#[Test] #[Test]
public function updateNotAllowedForNormalUsers(): void public function updateNotAllowedForNormalUsers(): void
{ {
Artist::factory()->create(['id' => 9999]); $artist = Artist::factory()->create();
$this->mediaMetadataService->shouldNotReceive('writeArtistImage'); $this->mediaMetadataService->shouldNotReceive('writeArtistImage');
$this->putAs('api/artist/9999/image', ['image' => 'data:image/jpeg;base64,Rm9v']) $this->putAs("api/artist/{$artist->id}/image", ['image' => 'data:image/jpeg;base64,Rm9v'])
->assertForbidden(); ->assertForbidden();
} }
} }

View file

@ -31,7 +31,7 @@ class ArtistInformationTest extends TestCase
], ],
)); ));
$this->getAs('api/artists/' . $artist->id . '/information') $this->getAs("api/artists/{$artist->id}/information")
->assertJsonStructure(ArtistInformation::JSON_STRUCTURE); ->assertJsonStructure(ArtistInformation::JSON_STRUCTURE);
} }

View file

@ -17,7 +17,7 @@ class ArtistSongTest extends TestCase
Song::factory(5)->for($artist)->create(); Song::factory(5)->for($artist)->create();
$this->getAs('api/artists/' . $artist->id . '/songs') $this->getAs("api/artists/{$artist->id}/songs")
->assertJsonStructure(['*' => SongResource::JSON_STRUCTURE]); ->assertJsonStructure(['*' => SongResource::JSON_STRUCTURE]);
} }
} }

View file

@ -8,7 +8,7 @@ use App\Models\Interaction;
use App\Models\Playlist; use App\Models\Playlist;
use App\Models\Song; use App\Models\Song;
use App\Services\DownloadService; use App\Services\DownloadService;
use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Collection;
use Mockery; use Mockery;
use Mockery\MockInterface; use Mockery\MockInterface;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
@ -47,7 +47,7 @@ class DownloadTest extends TestCase
->shouldReceive('getDownloadablePath') ->shouldReceive('getDownloadablePath')
->once() ->once()
->with(Mockery::on(static function (Collection $retrievedSongs) use ($song) { ->with(Mockery::on(static function (Collection $retrievedSongs) use ($song) {
return $retrievedSongs->count() === 1 && $retrievedSongs->first()->id === $song->id; return $retrievedSongs->count() === 1 && $retrievedSongs->first()->is($song);
})) }))
->andReturn(test_path('songs/blank.mp3')); ->andReturn(test_path('songs/blank.mp3'));
@ -65,7 +65,7 @@ class DownloadTest extends TestCase
->shouldReceive('getDownloadablePath') ->shouldReceive('getDownloadablePath')
->once() ->once()
->with(Mockery::on(static function (Collection $retrievedSongs) use ($songs): bool { ->with(Mockery::on(static function (Collection $retrievedSongs) use ($songs): bool {
self::assertEqualsCanonicalizing($retrievedSongs->pluck('id')->all(), $songs->pluck('id')->all()); self::assertEqualsCanonicalizing($retrievedSongs->modelKeys(), $songs->modelKeys());
return true; return true;
})) }))
@ -89,7 +89,7 @@ class DownloadTest extends TestCase
->shouldReceive('getDownloadablePath') ->shouldReceive('getDownloadablePath')
->once() ->once()
->with(Mockery::on(static function (Collection $retrievedSongs) use ($songs): bool { ->with(Mockery::on(static function (Collection $retrievedSongs) use ($songs): bool {
self::assertEqualsCanonicalizing($retrievedSongs->pluck('id')->all(), $songs->pluck('id')->all()); self::assertEqualsCanonicalizing($retrievedSongs->modelKeys(), $songs->modelKeys());
return true; return true;
})) }))
@ -110,7 +110,7 @@ class DownloadTest extends TestCase
->shouldReceive('getDownloadablePath') ->shouldReceive('getDownloadablePath')
->once() ->once()
->with(Mockery::on(static function (Collection $retrievedSongs) use ($songs): bool { ->with(Mockery::on(static function (Collection $retrievedSongs) use ($songs): bool {
self::assertEqualsCanonicalizing($retrievedSongs->pluck('id')->all(), $songs->pluck('id')->all()); self::assertEqualsCanonicalizing($retrievedSongs->modelKeys(), $songs->modelKeys());
return true; return true;
})) }))
@ -133,7 +133,7 @@ class DownloadTest extends TestCase
$this->downloadService $this->downloadService
->shouldReceive('getDownloadablePath') ->shouldReceive('getDownloadablePath')
->with(Mockery::on(static function (Collection $retrievedSongs) use ($songs): bool { ->with(Mockery::on(static function (Collection $retrievedSongs) use ($songs): bool {
self::assertEqualsCanonicalizing($retrievedSongs->pluck('id')->all(), $songs->pluck('id')->all()); self::assertEqualsCanonicalizing($retrievedSongs->modelKeys(), $songs->modelKeys());
return true; return true;
})) }))
@ -162,7 +162,7 @@ class DownloadTest extends TestCase
$this->downloadService $this->downloadService
->shouldReceive('getDownloadablePath') ->shouldReceive('getDownloadablePath')
->with(Mockery::on(static function (Collection $songs) use ($favorites): bool { ->with(Mockery::on(static function (Collection $songs) use ($favorites): bool {
self::assertEqualsCanonicalizing($songs->pluck('id')->all(), $favorites->pluck('song_id')->all()); self::assertEqualsCanonicalizing($songs->modelKeys(), $favorites->pluck('song_id')->all());
return true; return true;
})) }))

View file

@ -76,7 +76,7 @@ class InteractionTest extends TestCase
$user = create_user(); $user = create_user();
$songs = Song::factory(2)->create(); $songs = Song::factory(2)->create();
$songIds = $songs->pluck('id')->all(); $songIds = $songs->modelKeys();
$this->postAs('api/interaction/batch/like', ['songs' => $songIds], $user); $this->postAs('api/interaction/batch/like', ['songs' => $songIds], $user);

View file

@ -38,7 +38,7 @@ class AlbumCoverTest extends PlusTestCase
->once() ->once()
->with(Mockery::on(static fn (Album $target) => $target->is($album)), 'data:image/jpeg;base64,Rm9v'); ->with(Mockery::on(static fn (Album $target) => $target->is($album)), 'data:image/jpeg;base64,Rm9v');
$this->putAs("api/albums/$album->id/cover", ['cover' => 'data:image/jpeg;base64,Rm9v'], $user) $this->putAs("api/albums/{$album->id}/cover", ['cover' => 'data:image/jpeg;base64,Rm9v'], $user)
->assertOk(); ->assertOk();
} }
@ -56,7 +56,7 @@ class AlbumCoverTest extends PlusTestCase
->shouldReceive('writeAlbumCover') ->shouldReceive('writeAlbumCover')
->never(); ->never();
$this->putAs("api/albums/$album->id/cover", ['cover' => 'data:image/jpeg;base64,Rm9v'], $user) $this->putAs("api/albums/{$album->id}/cover", ['cover' => 'data:image/jpeg;base64,Rm9v'], $user)
->assertForbidden(); ->assertForbidden();
} }
@ -74,7 +74,7 @@ class AlbumCoverTest extends PlusTestCase
->once() ->once()
->with(Mockery::on(static fn (Album $target) => $target->is($album)), 'data:image/jpeg;base64,Rm9v'); ->with(Mockery::on(static fn (Album $target) => $target->is($album)), 'data:image/jpeg;base64,Rm9v');
$this->putAs("api/albums/$album->id/cover", ['cover' => 'data:image/jpeg;base64,Rm9v'], create_admin()) $this->putAs("api/albums/{$album->id}/cover", ['cover' => 'data:image/jpeg;base64,Rm9v'], create_admin())
->assertOk(); ->assertOk();
} }
} }

View file

@ -38,7 +38,7 @@ class ArtistImageTest extends PlusTestCase
->once() ->once()
->with(Mockery::on(static fn (Artist $target) => $target->is($artist)), 'data:image/jpeg;base64,Rm9v'); ->with(Mockery::on(static fn (Artist $target) => $target->is($artist)), 'data:image/jpeg;base64,Rm9v');
$this->putAs("api/artists/$artist->id/image", ['image' => 'data:image/jpeg;base64,Rm9v'], $user) $this->putAs("api/artists/{$artist->id}/image", ['image' => 'data:image/jpeg;base64,Rm9v'], $user)
->assertOk(); ->assertOk();
} }
@ -56,7 +56,7 @@ class ArtistImageTest extends PlusTestCase
->shouldReceive('writeArtistImage') ->shouldReceive('writeArtistImage')
->never(); ->never();
$this->putAs("api/artists/$artist->id/image", ['image' => 'data:image/jpeg;base64,Rm9v'], $user) $this->putAs("api/artists/{$artist->id}/image", ['image' => 'data:image/jpeg;base64,Rm9v'], $user)
->assertForbidden(); ->assertForbidden();
} }
@ -74,7 +74,11 @@ class ArtistImageTest extends PlusTestCase
->once() ->once()
->with(Mockery::on(static fn (Artist $target) => $target->is($artist)), 'data:image/jpeg;base64,Rm9v'); ->with(Mockery::on(static fn (Artist $target) => $target->is($artist)), 'data:image/jpeg;base64,Rm9v');
$this->putAs("api/artists/$artist->id/image", ['image' => 'data:image/jpeg;base64,Rm9v'], create_admin()) $this->putAs(
"api/artists/{$artist->id}/image",
['image' => 'data:image/jpeg;base64,Rm9v'],
create_admin()
)
->assertOk(); ->assertOk();
} }
} }

View file

@ -21,7 +21,7 @@ class DownloadTest extends PlusTestCase
// Can't download a private song that doesn't belong to the user // Can't download a private song that doesn't belong to the user
/** @var Song $externalPrivateSong */ /** @var Song $externalPrivateSong */
$externalPrivateSong = Song::factory()->private()->create(); $externalPrivateSong = Song::factory()->private()->create();
$this->get("download/songs?songs[]=$externalPrivateSong->id&api_token=" . $apiToken) $this->get("download/songs?songs[]={$externalPrivateSong->id}&api_token=" . $apiToken)
->assertForbidden(); ->assertForbidden();
// Can download a public song that doesn't belong to the user // Can download a public song that doesn't belong to the user
@ -33,7 +33,7 @@ class DownloadTest extends PlusTestCase
->once() ->once()
->andReturn(test_path('songs/blank.mp3')); ->andReturn(test_path('songs/blank.mp3'));
$this->get("download/songs?songs[]=$externalPublicSong->id&api_token=" . $apiToken) $this->get("download/songs?songs[]={$externalPublicSong->id}&api_token=" . $apiToken)
->assertOk(); ->assertOk();
// Can download a private song that belongs to the user // Can download a private song that belongs to the user
@ -42,7 +42,7 @@ class DownloadTest extends PlusTestCase
$downloadService->shouldReceive('getDownloadablePath') $downloadService->shouldReceive('getDownloadablePath')
->once() ->once()
->andReturn(test_path('songs/blank.mp3')); ->andReturn(test_path('songs/blank.mp3'));
$this->get("download/songs?songs[]=$ownSong->id&api_token=" . $apiToken) $this->get("download/songs?songs[]={$ownSong->id}&api_token=" . $apiToken)
->assertOk(); ->assertOk();
} }
} }

View file

@ -6,7 +6,6 @@ use App\Events\MultipleSongsLiked;
use App\Events\MultipleSongsUnliked; use App\Events\MultipleSongsUnliked;
use App\Events\SongLikeToggled; use App\Events\SongLikeToggled;
use App\Models\Song; use App\Models\Song;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Event;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Tests\PlusTestCase; use Tests\PlusTestCase;
@ -23,7 +22,6 @@ class InteractionTest extends PlusTestCase
$owner = create_user(); $owner = create_user();
// Can't increase play count of a private song that doesn't belong to the user // Can't increase play count of a private song that doesn't belong to the user
/** @var Song $externalPrivateSong */
$externalPrivateSong = Song::factory()->private()->create(); $externalPrivateSong = Song::factory()->private()->create();
$this->postAs('api/interaction/play', ['song' => $externalPrivateSong->id], $owner) $this->postAs('api/interaction/play', ['song' => $externalPrivateSong->id], $owner)
->assertForbidden(); ->assertForbidden();
@ -75,26 +73,23 @@ class InteractionTest extends PlusTestCase
$owner = create_user(); $owner = create_user();
// Can't batch like private songs that don't belong to the user // Can't batch like private songs that don't belong to the user
/** @var Collection $externalPrivateSongs */
$externalPrivateSongs = Song::factory()->count(3)->private()->create(); $externalPrivateSongs = Song::factory()->count(3)->private()->create();
$this->postAs('api/interaction/batch/like', ['songs' => $externalPrivateSongs->pluck('id')->all()], $owner) $this->postAs('api/interaction/batch/like', ['songs' => $externalPrivateSongs->modelKeys()], $owner)
->assertForbidden(); ->assertForbidden();
// Can batch like public songs that don't belong to the user // Can batch like public songs that don't belong to the user
/** @var Collection $externalPublicSongs */
$externalPublicSongs = Song::factory()->count(3)->public()->create(); $externalPublicSongs = Song::factory()->count(3)->public()->create();
$this->postAs('api/interaction/batch/like', ['songs' => $externalPublicSongs->pluck('id')->all()], $owner) $this->postAs('api/interaction/batch/like', ['songs' => $externalPublicSongs->modelKeys()], $owner)
->assertSuccessful(); ->assertSuccessful();
// Can batch like private songs that belong to the user // Can batch like private songs that belong to the user
/** @var Collection $ownPrivateSongs */
$ownPrivateSongs = Song::factory()->count(3)->private()->for($owner, 'owner')->create(); $ownPrivateSongs = Song::factory()->count(3)->private()->for($owner, 'owner')->create();
$this->postAs('api/interaction/batch/like', ['songs' => $ownPrivateSongs->pluck('id')->all()], $owner) $this->postAs('api/interaction/batch/like', ['songs' => $ownPrivateSongs->modelKeys()], $owner)
->assertSuccessful(); ->assertSuccessful();
// Can't batch like a mix of inaccessible and accessible songs // Can't batch like a mix of inaccessible and accessible songs
$mixedSongs = $externalPrivateSongs->merge($externalPublicSongs); $mixedSongs = $externalPrivateSongs->merge($externalPublicSongs);
$this->postAs('api/interaction/batch/like', ['songs' => $mixedSongs->pluck('id')->all()], $owner) $this->postAs('api/interaction/batch/like', ['songs' => $mixedSongs->modelKeys()], $owner)
->assertForbidden(); ->assertForbidden();
} }
@ -106,26 +101,23 @@ class InteractionTest extends PlusTestCase
$owner = create_user(); $owner = create_user();
// Can't batch unlike private songs that don't belong to the user // Can't batch unlike private songs that don't belong to the user
/** @var Collection $externalPrivateSongs */
$externalPrivateSongs = Song::factory()->count(3)->private()->create(); $externalPrivateSongs = Song::factory()->count(3)->private()->create();
$this->postAs('api/interaction/batch/unlike', ['songs' => $externalPrivateSongs->pluck('id')->all()], $owner) $this->postAs('api/interaction/batch/unlike', ['songs' => $externalPrivateSongs->modelKeys()], $owner)
->assertForbidden(); ->assertForbidden();
// Can batch unlike public songs that don't belong to the user // Can batch unlike public songs that don't belong to the user
/** @var Collection $externalPublicSongs */
$externalPublicSongs = Song::factory()->count(3)->public()->create(); $externalPublicSongs = Song::factory()->count(3)->public()->create();
$this->postAs('api/interaction/batch/unlike', ['songs' => $externalPublicSongs->pluck('id')->all()], $owner) $this->postAs('api/interaction/batch/unlike', ['songs' => $externalPublicSongs->modelKeys()], $owner)
->assertSuccessful(); ->assertSuccessful();
// Can batch unlike private songs that belong to the user // Can batch unlike private songs that belong to the user
/** @var Collection $ownPrivateSongs */
$ownPrivateSongs = Song::factory()->count(3)->private()->for($owner, 'owner')->create(); $ownPrivateSongs = Song::factory()->count(3)->private()->for($owner, 'owner')->create();
$this->postAs('api/interaction/batch/unlike', ['songs' => $ownPrivateSongs->pluck('id')->all()], $owner) $this->postAs('api/interaction/batch/unlike', ['songs' => $ownPrivateSongs->modelKeys()], $owner)
->assertSuccessful(); ->assertSuccessful();
// Can't batch unlike a mix of inaccessible and accessible songs // Can't batch unlike a mix of inaccessible and accessible songs
$mixedSongs = $externalPrivateSongs->merge($externalPublicSongs); $mixedSongs = $externalPrivateSongs->merge($externalPublicSongs);
$this->postAs('api/interaction/batch/unlike', ['songs' => $mixedSongs->pluck('id')->all()], $owner) $this->postAs('api/interaction/batch/unlike', ['songs' => $mixedSongs->modelKeys()], $owner)
->assertForbidden(); ->assertForbidden();
} }
} }

View file

@ -19,7 +19,7 @@ class PlaylistCollaborationTest extends PlusTestCase
/** @var Playlist $playlist */ /** @var Playlist $playlist */
$playlist = Playlist::factory()->create(); $playlist = Playlist::factory()->create();
$this->postAs("api/playlists/$playlist->id/collaborators/invite", [], $playlist->user) $this->postAs("api/playlists/{$playlist->id}/collaborators/invite", [], $playlist->user)
->assertJsonStructure(PlaylistCollaborationTokenResource::JSON_STRUCTURE); ->assertJsonStructure(PlaylistCollaborationTokenResource::JSON_STRUCTURE);
} }

View file

@ -18,7 +18,11 @@ class PlaylistCoverTest extends PlusTestCase
$collaborator = create_user(); $collaborator = create_user();
$playlist->addCollaborator($collaborator); $playlist->addCollaborator($collaborator);
$this->putAs("api/playlists/$playlist->id/cover", ['cover' => 'data:image/jpeg;base64,Rm9v'], $collaborator) $this->putAs(
"api/playlists/{$playlist->id}/cover",
['cover' => 'data:image/jpeg;base64,Rm9v'],
$collaborator
)
->assertForbidden(); ->assertForbidden();
} }
@ -30,7 +34,7 @@ class PlaylistCoverTest extends PlusTestCase
$collaborator = create_user(); $collaborator = create_user();
$playlist->addCollaborator($collaborator); $playlist->addCollaborator($collaborator);
$this->deleteAs("api/playlists/$playlist->id/cover", [], $collaborator) $this->deleteAs("api/playlists/{$playlist->id}/cover", [], $collaborator)
->assertForbidden(); ->assertForbidden();
} }
} }

View file

@ -22,7 +22,7 @@ class PlaylistFolderTest extends PlusTestCase
/** @var PlaylistFolder $ownerFolder */ /** @var PlaylistFolder $ownerFolder */
$ownerFolder = PlaylistFolder::factory()->for($playlist->user)->create(); $ownerFolder = PlaylistFolder::factory()->for($playlist->user)->create();
$ownerFolder->playlists()->attach($playlist->id); $ownerFolder->playlists()->attach($playlist);
self::assertTrue($playlist->refresh()->getFolder($playlist->user)?->is($ownerFolder)); self::assertTrue($playlist->refresh()->getFolder($playlist->user)?->is($ownerFolder));
/** @var PlaylistFolder $collaboratorFolder */ /** @var PlaylistFolder $collaboratorFolder */
@ -30,7 +30,7 @@ class PlaylistFolderTest extends PlusTestCase
self::assertNull($playlist->getFolder($collaborator)); self::assertNull($playlist->getFolder($collaborator));
$this->postAs( $this->postAs(
"api/playlist-folders/$collaboratorFolder->id/playlists", "api/playlist-folders/{$collaboratorFolder->id}/playlists",
['playlists' => [$playlist->id]], ['playlists' => [$playlist->id]],
$collaborator $collaborator
) )
@ -54,17 +54,17 @@ class PlaylistFolderTest extends PlusTestCase
/** @var PlaylistFolder $ownerFolder */ /** @var PlaylistFolder $ownerFolder */
$ownerFolder = PlaylistFolder::factory()->for($playlist->user)->create(); $ownerFolder = PlaylistFolder::factory()->for($playlist->user)->create();
$ownerFolder->playlists()->attach($playlist->id); $ownerFolder->playlists()->attach($playlist);
self::assertTrue($playlist->refresh()->getFolder($playlist->user)?->is($ownerFolder)); self::assertTrue($playlist->refresh()->getFolder($playlist->user)?->is($ownerFolder));
/** @var PlaylistFolder $collaboratorFolder */ /** @var PlaylistFolder $collaboratorFolder */
$collaboratorFolder = PlaylistFolder::factory()->for($collaborator)->create(); $collaboratorFolder = PlaylistFolder::factory()->for($collaborator)->create();
$collaboratorFolder->playlists()->attach($playlist->id); $collaboratorFolder->playlists()->attach($playlist);
self::assertTrue($playlist->refresh()->getFolder($collaborator)?->is($collaboratorFolder)); self::assertTrue($playlist->refresh()->getFolder($collaborator)?->is($collaboratorFolder));
$this->deleteAs( $this->deleteAs(
"api/playlist-folders/$collaboratorFolder->id/playlists", "api/playlist-folders/{$collaboratorFolder->id}/playlists",
['playlists' => [$playlist->id]], ['playlists' => [$playlist->id]],
$collaborator $collaborator
) )

View file

@ -22,7 +22,7 @@ class PlaylistSongTest extends PlusTestCase
$collaborator = create_user(); $collaborator = create_user();
$playlist->addCollaborator($collaborator); $playlist->addCollaborator($collaborator);
$this->getAs("api/playlists/$playlist->id/songs", $collaborator) $this->getAs("api/playlists/{$playlist->id}/songs", $collaborator)
->assertSuccessful() ->assertSuccessful()
->assertJsonStructure(['*' => CollaborativeSongResource::JSON_STRUCTURE]) ->assertJsonStructure(['*' => CollaborativeSongResource::JSON_STRUCTURE])
->assertJsonCount(3); ->assertJsonCount(3);
@ -42,7 +42,7 @@ class PlaylistSongTest extends PlusTestCase
$collaborator = create_user(); $collaborator = create_user();
$playlist->addCollaborator($collaborator); $playlist->addCollaborator($collaborator);
$this->getAs("api/playlists/$playlist->id/songs", $collaborator) $this->getAs("api/playlists/{$playlist->id}/songs", $collaborator)
->assertSuccessful() ->assertSuccessful()
->assertJsonStructure(['*' => CollaborativeSongResource::JSON_STRUCTURE]) ->assertJsonStructure(['*' => CollaborativeSongResource::JSON_STRUCTURE])
->assertJsonCount(3) ->assertJsonCount(3)
@ -58,7 +58,7 @@ class PlaylistSongTest extends PlusTestCase
$playlist->addCollaborator($collaborator); $playlist->addCollaborator($collaborator);
$songs = Song::factory()->for($collaborator, 'owner')->count(3)->create(); $songs = Song::factory()->for($collaborator, 'owner')->count(3)->create();
$this->postAs("api/playlists/$playlist->id/songs", ['songs' => $songs->pluck('id')->all()], $collaborator) $this->postAs("api/playlists/{$playlist->id}/songs", ['songs' => $songs->modelKeys()], $collaborator)
->assertSuccessful(); ->assertSuccessful();
$playlist->refresh(); $playlist->refresh();
@ -75,7 +75,7 @@ class PlaylistSongTest extends PlusTestCase
$songs = Song::factory()->for($collaborator, 'owner')->count(3)->create(); $songs = Song::factory()->for($collaborator, 'owner')->count(3)->create();
$playlist->addPlayables($songs); $playlist->addPlayables($songs);
$this->deleteAs("api/playlists/$playlist->id/songs", ['songs' => $songs->pluck('id')->all()], $collaborator) $this->deleteAs("api/playlists/{$playlist->id}/songs", ['songs' => $songs->modelKeys()], $collaborator)
->assertSuccessful(); ->assertSuccessful();
$playlist->refresh(); $playlist->refresh();

View file

@ -52,7 +52,7 @@ class PlaylistTest extends PlusTestCase
/** @var Playlist $playlist */ /** @var Playlist $playlist */
$playlist = Playlist::factory()->smart()->create(); $playlist = Playlist::factory()->smart()->create();
$this->putAs("api/playlists/$playlist->id", [ $this->putAs("api/playlists/{$playlist->id}", [
'name' => 'Foo', 'name' => 'Foo',
'own_songs_only' => true, 'own_songs_only' => true,
'rules' => $playlist->rules->toArray(), 'rules' => $playlist->rules->toArray(),
@ -72,7 +72,7 @@ class PlaylistTest extends PlusTestCase
$collaborator = create_user(); $collaborator = create_user();
$playlist->addCollaborator($collaborator); $playlist->addCollaborator($collaborator);
$this->putAs("api/playlists/$playlist->id", ['name' => 'Nope'], $collaborator) $this->putAs("api/playlists/{$playlist->id}", ['name' => 'Nope'], $collaborator)
->assertForbidden(); ->assertForbidden();
} }
} }

View file

@ -29,7 +29,7 @@ class SongPlayTest extends PlusTestCase
->shouldReceive('stream') ->shouldReceive('stream')
->once(); ->once();
$this->get("play/$song->id?t=$token->audioToken") $this->get("play/{$song->id}?t=$token->audioToken")
->assertOk(); ->assertOk();
} }
@ -48,7 +48,7 @@ class SongPlayTest extends PlusTestCase
->shouldReceive('stream') ->shouldReceive('stream')
->once(); ->once();
$this->get("play/$song->id?t=$token->audioToken") $this->get("play/{$song->id}?t=$token->audioToken")
->assertOk(); ->assertOk();
} }
@ -63,7 +63,7 @@ class SongPlayTest extends PlusTestCase
/** @var CompositeToken $token */ /** @var CompositeToken $token */
$token = app(TokenManager::class)->createCompositeToken(create_user()); $token = app(TokenManager::class)->createCompositeToken(create_user());
$this->get("play/$song->id?t=$token->audioToken") $this->get("play/{$song->id}?t=$token->audioToken")
->assertForbidden(); ->assertForbidden();
} }
} }

View file

@ -3,7 +3,6 @@
namespace Tests\Feature\KoelPlus; namespace Tests\Feature\KoelPlus;
use App\Models\Song; use App\Models\Song;
use Illuminate\Support\Collection;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Tests\PlusTestCase; use Tests\PlusTestCase;
@ -18,7 +17,6 @@ class SongTest extends PlusTestCase
Song::factory(2)->public()->create(); Song::factory(2)->public()->create();
/** @var Collection<array-key, Song> $ownSongs */
$ownSongs = Song::factory(3)->for($user, 'owner')->create(); $ownSongs = Song::factory(3)->for($user, 'owner')->create();
$this->getAs('api/songs?own_songs_only=true', $user) $this->getAs('api/songs?own_songs_only=true', $user)
@ -55,19 +53,19 @@ class SongTest extends PlusTestCase
$publicSong = Song::factory()->public()->create(); $publicSong = Song::factory()->public()->create();
// We can access public songs. // We can access public songs.
$this->getAs("api/songs/$publicSong->id", $user)->assertSuccessful(); $this->getAs("api/songs/{$publicSong->id}", $user)->assertSuccessful();
/** @var Song $ownPrivateSong */ /** @var Song $ownPrivateSong */
$ownPrivateSong = Song::factory()->for($user, 'owner')->private()->create(); $ownPrivateSong = Song::factory()->for($user, 'owner')->private()->create();
// We can access our own private songs. // We can access our own private songs.
$this->getAs('api/songs/' . $ownPrivateSong->id, $user)->assertSuccessful(); $this->getAs("api/songs/{$ownPrivateSong->id}", $user)->assertSuccessful();
/** @var Song $externalUnownedSong */ /** @var Song $externalUnownedSong */
$externalUnownedSong = Song::factory()->private()->create(); $externalUnownedSong = Song::factory()->private()->create();
// But we can't access private songs that are not ours. // But we can't access private songs that are not ours.
$this->getAs("api/songs/$externalUnownedSong->id", $user)->assertForbidden(); $this->getAs("api/songs/{$externalUnownedSong->id}", $user)->assertForbidden();
} }
#[Test] #[Test]
@ -76,12 +74,11 @@ class SongTest extends PlusTestCase
$currentUser = create_user(); $currentUser = create_user();
$anotherUser = create_user(); $anotherUser = create_user();
/** @var Collection<Song> $externalUnownedSongs */
$externalUnownedSongs = Song::factory(3)->for($anotherUser, 'owner')->private()->create(); $externalUnownedSongs = Song::factory(3)->for($anotherUser, 'owner')->private()->create();
// We can't edit songs that are not ours. // We can't edit songs that are not ours.
$this->putAs('api/songs', [ $this->putAs('api/songs', [
'songs' => $externalUnownedSongs->pluck('id')->toArray(), 'songs' => $externalUnownedSongs->modelKeys(),
'data' => [ 'data' => [
'title' => 'New Title', 'title' => 'New Title',
], ],
@ -91,7 +88,7 @@ class SongTest extends PlusTestCase
$mixedSongs = $externalUnownedSongs->merge(Song::factory(2)->for($currentUser, 'owner')->create()); $mixedSongs = $externalUnownedSongs->merge(Song::factory(2)->for($currentUser, 'owner')->create());
$this->putAs('api/songs', [ $this->putAs('api/songs', [
'songs' => $mixedSongs->pluck('id')->toArray(), 'songs' => $mixedSongs->modelKeys(),
'data' => [ 'data' => [
'title' => 'New Title', 'title' => 'New Title',
], ],
@ -101,7 +98,7 @@ class SongTest extends PlusTestCase
$ownSongs = Song::factory(3)->for($currentUser, 'owner')->create(); $ownSongs = Song::factory(3)->for($currentUser, 'owner')->create();
$this->putAs('api/songs', [ $this->putAs('api/songs', [
'songs' => $ownSongs->pluck('id')->toArray(), 'songs' => $ownSongs->modelKeys(),
'data' => [ 'data' => [
'title' => 'New Title', 'title' => 'New Title',
], ],
@ -114,35 +111,33 @@ class SongTest extends PlusTestCase
$currentUser = create_user(); $currentUser = create_user();
$anotherUser = create_user(); $anotherUser = create_user();
/** @var Collection<Song> $externalUnownedSongs */
$externalUnownedSongs = Song::factory(3)->for($anotherUser, 'owner')->private()->create(); $externalUnownedSongs = Song::factory(3)->for($anotherUser, 'owner')->private()->create();
// We can't delete songs that are not ours. // We can't delete songs that are not ours.
$this->deleteAs('api/songs', ['songs' => $externalUnownedSongs->pluck('id')->toArray()], $currentUser) $this->deleteAs('api/songs', ['songs' => $externalUnownedSongs->modelKeys()], $currentUser)
->assertForbidden(); ->assertForbidden();
// Even if some of the songs are owned by us, we still can't delete them. // Even if some of the songs are owned by us, we still can't delete them.
$mixedSongs = $externalUnownedSongs->merge(Song::factory(2)->for($currentUser, 'owner')->create()); $mixedSongs = $externalUnownedSongs->merge(Song::factory(2)->for($currentUser, 'owner')->create());
$this->deleteAs('api/songs', ['songs' => $mixedSongs->pluck('id')->toArray()], $currentUser) $this->deleteAs('api/songs', ['songs' => $mixedSongs->modelKeys()], $currentUser)
->assertForbidden(); ->assertForbidden();
// But we can delete our own songs. // But we can delete our own songs.
$ownSongs = Song::factory(3)->for($currentUser, 'owner')->create(); $ownSongs = Song::factory(3)->for($currentUser, 'owner')->create();
$this->deleteAs('api/songs', ['songs' => $ownSongs->pluck('id')->toArray()], $currentUser) $this->deleteAs('api/songs', ['songs' => $ownSongs->modelKeys()], $currentUser)
->assertSuccessful(); ->assertSuccessful();
} }
#[Test] #[Test]
public function publicizeSongs(): void public function markSongsAsPublic(): void
{ {
$user = create_user(); $user = create_user();
/** @var Song $songs */
$songs = Song::factory(3)->for($user, 'owner')->private()->create(); $songs = Song::factory(3)->for($user, 'owner')->private()->create();
$this->putAs('api/songs/publicize', ['songs' => $songs->pluck('id')->toArray()], $user) $this->putAs('api/songs/publicize', ['songs' => $songs->modelKeys()], $user)
->assertSuccessful(); ->assertSuccessful();
$songs->each(static function (Song $song): void { $songs->each(static function (Song $song): void {
@ -152,14 +147,13 @@ class SongTest extends PlusTestCase
} }
#[Test] #[Test]
public function privatizeSongs(): void public function markSongsAsPrivate(): void
{ {
$user = create_user(); $user = create_user();
/** @var Song $songs */
$songs = Song::factory(3)->for($user, 'owner')->public()->create(); $songs = Song::factory(3)->for($user, 'owner')->public()->create();
$this->putAs('api/songs/privatize', ['songs' => $songs->pluck('id')->toArray()], $user) $this->putAs('api/songs/privatize', ['songs' => $songs->modelKeys()], $user)
->assertSuccessful(); ->assertSuccessful();
$songs->each(static function (Song $song): void { $songs->each(static function (Song $song): void {
@ -173,12 +167,12 @@ class SongTest extends PlusTestCase
{ {
$songs = Song::factory(3)->public()->create(); $songs = Song::factory(3)->public()->create();
$this->putAs('api/songs/privatize', ['songs' => $songs->pluck('id')->toArray()]) $this->putAs('api/songs/privatize', ['songs' => $songs->modelKeys()])
->assertForbidden(); ->assertForbidden();
$otherSongs = Song::factory(3)->private()->create(); $otherSongs = Song::factory(3)->private()->create();
$this->putAs('api/songs/publicize', ['songs' => $otherSongs->pluck('id')->toArray()]) $this->putAs('api/songs/publicize', ['songs' => $otherSongs->modelKeys()])
->assertForbidden(); ->assertForbidden();
} }
} }

View file

@ -3,7 +3,6 @@
namespace Tests\Feature\KoelPlus; namespace Tests\Feature\KoelPlus;
use App\Models\Song; use App\Models\Song;
use Illuminate\Support\Collection;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Tests\PlusTestCase; use Tests\PlusTestCase;
@ -17,17 +16,16 @@ class SongVisibilityTest extends PlusTestCase
$currentUser = create_user(); $currentUser = create_user();
$anotherUser = create_user(); $anotherUser = create_user();
/** @var Collection<Song> $externalSongs */
$externalSongs = Song::factory(3)->for($anotherUser, 'owner')->private()->create(); $externalSongs = Song::factory(3)->for($anotherUser, 'owner')->private()->create();
// We can't make public songs that are not ours. // We can't make public songs that are not ours.
$this->putAs('api/songs/publicize', ['songs' => $externalSongs->pluck('id')->toArray()], $currentUser) $this->putAs('api/songs/publicize', ['songs' => $externalSongs->modelKeys()], $currentUser)
->assertForbidden(); ->assertForbidden();
// But we can our own songs. // But we can our own songs.
$ownSongs = Song::factory(3)->for($currentUser, 'owner')->create(); $ownSongs = Song::factory(3)->for($currentUser, 'owner')->create();
$this->putAs('api/songs/publicize', ['songs' => $ownSongs->pluck('id')->toArray()], $currentUser) $this->putAs('api/songs/publicize', ['songs' => $ownSongs->modelKeys()], $currentUser)
->assertSuccessful(); ->assertSuccessful();
$ownSongs->each(static fn (Song $song) => self::assertTrue($song->refresh()->is_public)); $ownSongs->each(static fn (Song $song) => self::assertTrue($song->refresh()->is_public));
@ -39,17 +37,16 @@ class SongVisibilityTest extends PlusTestCase
$currentUser = create_user(); $currentUser = create_user();
$anotherUser = create_user(); $anotherUser = create_user();
/** @var Collection<Song> $externalSongs */
$externalSongs = Song::factory(3)->for($anotherUser, 'owner')->public()->create(); $externalSongs = Song::factory(3)->for($anotherUser, 'owner')->public()->create();
// We can't Mark as Private songs that are not ours. // We can't Mark as Private songs that are not ours.
$this->putAs('api/songs/privatize', ['songs' => $externalSongs->pluck('id')->toArray()], $currentUser) $this->putAs('api/songs/privatize', ['songs' => $externalSongs->modelKeys()], $currentUser)
->assertForbidden(); ->assertForbidden();
// But we can our own songs. // But we can our own songs.
$ownSongs = Song::factory(3)->for($currentUser, 'owner')->create(); $ownSongs = Song::factory(3)->for($currentUser, 'owner')->create();
$this->putAs('api/songs/privatize', ['songs' => $ownSongs->pluck('id')->toArray()], $currentUser) $this->putAs('api/songs/privatize', ['songs' => $ownSongs->modelKeys()], $currentUser)
->assertSuccessful(); ->assertSuccessful();
$ownSongs->each(static fn (Song $song) => self::assertFalse($song->refresh()->is_public)); $ownSongs->each(static fn (Song $song) => self::assertFalse($song->refresh()->is_public));

View file

@ -22,7 +22,7 @@ class PlayCountTest extends TestCase
'play_count' => 10, 'play_count' => 10,
]); ]);
$this->postAs('/api/interaction/play', ['song' => $interaction->song->id], $interaction->user) $this->postAs('/api/interaction/play', ['song' => $interaction->song_id], $interaction->user)
->assertJsonStructure([ ->assertJsonStructure([
'type', 'type',
'id', 'id',
@ -53,8 +53,8 @@ class PlayCountTest extends TestCase
]); ]);
$interaction = Interaction::query() $interaction = Interaction::query()
->where('song_id', $song->id) ->whereBelongsTo($song)
->where('user_id', $user->id) ->whereBelongsTo($user)
->first(); ->first();
self::assertSame(1, $interaction->play_count); self::assertSame(1, $interaction->play_count);

View file

@ -19,7 +19,7 @@ class PlaylistCoverTest extends TestCase
self::assertNull($playlist->cover); self::assertNull($playlist->cover);
$this->putAs( $this->putAs(
"api/playlists/$playlist->id/cover", "api/playlists/{$playlist->id}/cover",
['cover' => read_as_data_url(test_path('blobs/cover.png'))], ['cover' => read_as_data_url(test_path('blobs/cover.png'))],
$playlist->user $playlist->user
) )
@ -33,7 +33,11 @@ class PlaylistCoverTest extends TestCase
{ {
$playlist = Playlist::factory()->create(); $playlist = Playlist::factory()->create();
$this->putAs("api/playlists/$playlist->id/cover", ['cover' => 'data:image/jpeg;base64,Rm9v'], create_user()) $this->putAs(
"api/playlists/{$playlist->id}/cover",
['cover' => 'data:image/jpeg;base64,Rm9v'],
create_user()
)
->assertForbidden(); ->assertForbidden();
} }
@ -42,7 +46,7 @@ class PlaylistCoverTest extends TestCase
{ {
$playlist = Playlist::factory()->create(['cover' => 'cover.jpg']); $playlist = Playlist::factory()->create(['cover' => 'cover.jpg']);
$this->deleteAs("api/playlists/$playlist->id/cover", [], $playlist->user) $this->deleteAs("api/playlists/{$playlist->id}/cover", [], $playlist->user)
->assertNoContent(); ->assertNoContent();
self::assertNull($playlist->refresh()->cover); self::assertNull($playlist->refresh()->cover);
@ -53,7 +57,7 @@ class PlaylistCoverTest extends TestCase
{ {
$playlist = Playlist::factory()->create(['cover' => 'cover.jpg']); $playlist = Playlist::factory()->create(['cover' => 'cover.jpg']);
$this->deleteAs("api/playlists/$playlist->id/cover", [], create_user()) $this->deleteAs("api/playlists/{$playlist->id}/cover", [], create_user())
->assertForbidden(); ->assertForbidden();
self::assertSame('cover.jpg', $playlist->refresh()->getRawOriginal('cover')); self::assertSame('cover.jpg', $playlist->refresh()->getRawOriginal('cover'));

View file

@ -39,7 +39,7 @@ class PlaylistFolderTest extends TestCase
{ {
$folder = PlaylistFolder::factory()->create(['name' => 'Metal']); $folder = PlaylistFolder::factory()->create(['name' => 'Metal']);
$this->patchAs('api/playlist-folders/' . $folder->id, ['name' => 'Classical'], $folder->user) $this->patchAs("api/playlist-folders/{$folder->id}", ['name' => 'Classical'], $folder->user)
->assertJsonStructure(PlaylistFolderResource::JSON_STRUCTURE); ->assertJsonStructure(PlaylistFolderResource::JSON_STRUCTURE);
self::assertSame('Classical', $folder->fresh()->name); self::assertSame('Classical', $folder->fresh()->name);
@ -50,7 +50,7 @@ class PlaylistFolderTest extends TestCase
{ {
$folder = PlaylistFolder::factory()->create(['name' => 'Metal']); $folder = PlaylistFolder::factory()->create(['name' => 'Metal']);
$this->patchAs('api/playlist-folders/' . $folder->id, ['name' => 'Classical']) $this->patchAs("api/playlist-folders/{$folder->id}", ['name' => 'Classical'])
->assertForbidden(); ->assertForbidden();
self::assertSame('Metal', $folder->fresh()->name); self::assertSame('Metal', $folder->fresh()->name);
@ -61,7 +61,7 @@ class PlaylistFolderTest extends TestCase
{ {
$folder = PlaylistFolder::factory()->create(); $folder = PlaylistFolder::factory()->create();
$this->deleteAs('api/playlist-folders/' . $folder->id, ['name' => 'Classical'], $folder->user) $this->deleteAs("api/playlist-folders/{$folder->id}", ['name' => 'Classical'], $folder->user)
->assertNoContent(); ->assertNoContent();
self::assertModelMissing($folder); self::assertModelMissing($folder);
@ -73,7 +73,7 @@ class PlaylistFolderTest extends TestCase
/** @var PlaylistFolder $folder */ /** @var PlaylistFolder $folder */
$folder = PlaylistFolder::factory()->create(); $folder = PlaylistFolder::factory()->create();
$this->deleteAs('api/playlist-folders/' . $folder->id, ['name' => 'Classical']) $this->deleteAs("api/playlist-folders/{$folder->id}", ['name' => 'Classical'])
->assertForbidden(); ->assertForbidden();
self::assertModelExists($folder); self::assertModelExists($folder);
@ -89,7 +89,11 @@ class PlaylistFolderTest extends TestCase
$playlist = Playlist::factory()->for($folder->user)->create(); $playlist = Playlist::factory()->for($folder->user)->create();
self::assertNull($playlist->getFolderId($folder->user)); self::assertNull($playlist->getFolderId($folder->user));
$this->postAs("api/playlist-folders/$folder->id/playlists", ['playlists' => [$playlist->id]], $folder->user) $this->postAs(
"api/playlist-folders/{$folder->id}/playlists",
['playlists' => [$playlist->id]],
$folder->user
)
->assertSuccessful(); ->assertSuccessful();
self::assertTrue($playlist->fresh()->getFolder($folder->user)->is($folder)); self::assertTrue($playlist->fresh()->getFolder($folder->user)->is($folder));
@ -105,7 +109,7 @@ class PlaylistFolderTest extends TestCase
$playlist = Playlist::factory()->for($folder->user)->create(); $playlist = Playlist::factory()->for($folder->user)->create();
self::assertNull($playlist->getFolderId($folder->user)); self::assertNull($playlist->getFolderId($folder->user));
$this->postAs("api/playlist-folders/$folder->id/playlists", ['playlists' => [$playlist->id]]) $this->postAs("api/playlist-folders/{$folder->id}/playlists", ['playlists' => [$playlist->id]])
->assertUnprocessable(); ->assertUnprocessable();
self::assertNull($playlist->fresh()->getFolder($folder->user)); self::assertNull($playlist->fresh()->getFolder($folder->user));
@ -120,10 +124,14 @@ class PlaylistFolderTest extends TestCase
/** @var Playlist $playlist */ /** @var Playlist $playlist */
$playlist = Playlist::factory()->for($folder->user)->create(); $playlist = Playlist::factory()->for($folder->user)->create();
$folder->playlists()->attach($playlist->id); $folder->playlists()->attach($playlist);
self::assertTrue($playlist->refresh()->getFolder($folder->user)->is($folder)); self::assertTrue($playlist->refresh()->getFolder($folder->user)->is($folder));
$this->deleteAs("api/playlist-folders/$folder->id/playlists", ['playlists' => [$playlist->id]], $folder->user) $this->deleteAs(
"api/playlist-folders/{$folder->id}/playlists",
['playlists' => [$playlist->id]],
$folder->user
)
->assertSuccessful(); ->assertSuccessful();
self::assertNull($playlist->fresh()->getFolder($folder->user)); self::assertNull($playlist->fresh()->getFolder($folder->user));
@ -138,10 +146,10 @@ class PlaylistFolderTest extends TestCase
/** @var Playlist $playlist */ /** @var Playlist $playlist */
$playlist = Playlist::factory()->for($folder->user)->create(); $playlist = Playlist::factory()->for($folder->user)->create();
$folder->playlists()->attach($playlist->id); $folder->playlists()->attach($playlist);
self::assertTrue($playlist->refresh()->getFolder($folder->user)->is($folder)); self::assertTrue($playlist->refresh()->getFolder($folder->user)->is($folder));
$this->deleteAs("api/playlist-folders/$folder->id/playlists", ['playlists' => [$playlist->id]]) $this->deleteAs("api/playlist-folders/{$folder->id}/playlists", ['playlists' => [$playlist->id]])
->assertUnprocessable(); ->assertUnprocessable();
self::assertTrue($playlist->refresh()->getFolder($folder->user)->is($folder)); self::assertTrue($playlist->refresh()->getFolder($folder->user)->is($folder));

View file

@ -5,7 +5,6 @@ namespace Tests\Feature;
use App\Http\Resources\SongResource; use App\Http\Resources\SongResource;
use App\Models\Playlist; use App\Models\Playlist;
use App\Models\Song; use App\Models\Song;
use Illuminate\Support\Collection;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase; use Tests\TestCase;
@ -20,7 +19,7 @@ class PlaylistSongTest extends TestCase
$playlist = Playlist::factory()->create(); $playlist = Playlist::factory()->create();
$playlist->addPlayables(Song::factory(5)->create()); $playlist->addPlayables(Song::factory(5)->create());
$this->getAs("api/playlists/$playlist->id/songs", $playlist->user) $this->getAs("api/playlists/{$playlist->id}/songs", $playlist->user)
->assertJsonStructure(['*' => SongResource::JSON_STRUCTURE]); ->assertJsonStructure(['*' => SongResource::JSON_STRUCTURE]);
} }
@ -46,7 +45,7 @@ class PlaylistSongTest extends TestCase
], ],
]); ]);
$this->getAs("api/playlists/$playlist->id/songs", $playlist->user) $this->getAs("api/playlists/{$playlist->id}/songs", $playlist->user)
->assertJsonStructure(['*' => SongResource::JSON_STRUCTURE]); ->assertJsonStructure(['*' => SongResource::JSON_STRUCTURE]);
} }
@ -57,7 +56,7 @@ class PlaylistSongTest extends TestCase
$playlist = Playlist::factory()->for(create_user())->create(); $playlist = Playlist::factory()->for(create_user())->create();
$playlist->addPlayables(Song::factory(5)->create()); $playlist->addPlayables(Song::factory(5)->create());
$this->getAs("api/playlists/$playlist->id/songs") $this->getAs("api/playlists/{$playlist->id}/songs")
->assertForbidden(); ->assertForbidden();
} }
@ -67,13 +66,12 @@ class PlaylistSongTest extends TestCase
/** @var Playlist $playlist */ /** @var Playlist $playlist */
$playlist = Playlist::factory()->create(); $playlist = Playlist::factory()->create();
/** @var Collection<array-key, Song> $songs */
$songs = Song::factory(2)->create(); $songs = Song::factory(2)->create();
$this->postAs("api/playlists/$playlist->id/songs", ['songs' => $songs->pluck('id')->all()], $playlist->user) $this->postAs("api/playlists/{$playlist->id}/songs", ['songs' => $songs->modelKeys()], $playlist->user)
->assertSuccessful(); ->assertSuccessful();
self::assertEqualsCanonicalizing($songs->pluck('id')->all(), $playlist->playables->pluck('id')->all()); self::assertEqualsCanonicalizing($songs->modelKeys(), $playlist->playables->modelKeys());
} }
#[Test] #[Test]
@ -84,7 +82,6 @@ class PlaylistSongTest extends TestCase
$toRemainSongs = Song::factory(5)->create(); $toRemainSongs = Song::factory(5)->create();
/** @var Collection<array-key, Song> $toBeRemovedSongs */
$toBeRemovedSongs = Song::factory(2)->create(); $toBeRemovedSongs = Song::factory(2)->create();
$playlist->addPlayables($toRemainSongs->merge($toBeRemovedSongs)); $playlist->addPlayables($toRemainSongs->merge($toBeRemovedSongs));
@ -92,15 +89,15 @@ class PlaylistSongTest extends TestCase
self::assertCount(7, $playlist->playables); self::assertCount(7, $playlist->playables);
$this->deleteAs( $this->deleteAs(
"api/playlists/$playlist->id/songs", "api/playlists/{$playlist->id}/songs",
['songs' => $toBeRemovedSongs->pluck('id')->all()], ['songs' => $toBeRemovedSongs->modelKeys()],
$playlist->user $playlist->user
) )
->assertNoContent(); ->assertNoContent();
$playlist->refresh(); $playlist->refresh();
self::assertEqualsCanonicalizing($toRemainSongs->pluck('id')->all(), $playlist->playables->pluck('id')->all()); self::assertEqualsCanonicalizing($toRemainSongs->modelKeys(), $playlist->playables->modelKeys());
} }
#[Test] #[Test]
@ -112,10 +109,10 @@ class PlaylistSongTest extends TestCase
/** @var Song $song */ /** @var Song $song */
$song = Song::factory()->create(); $song = Song::factory()->create();
$this->postAs("api/playlists/$playlist->id/songs", ['songs' => [$song->id]]) $this->postAs("api/playlists/{$playlist->id}/songs", ['songs' => [$song->id]])
->assertForbidden(); ->assertForbidden();
$this->deleteAs("api/playlists/$playlist->id/songs", ['songs' => [$song->id]]) $this->deleteAs("api/playlists/{$playlist->id}/songs", ['songs' => [$song->id]])
->assertForbidden(); ->assertForbidden();
} }
@ -139,12 +136,12 @@ class PlaylistSongTest extends TestCase
], ],
]); ]);
$songs = Song::factory(2)->create()->pluck('id')->all(); $songs = Song::factory(2)->create()->modelKeys();
$this->postAs("api/playlists/$playlist->id/songs", ['songs' => $songs], $playlist->user) $this->postAs("api/playlists/{$playlist->id}/songs", ['songs' => $songs], $playlist->user)
->assertForbidden(); ->assertForbidden();
$this->deleteAs("api/playlists/$playlist->id/songs", ['songs' => $songs], $playlist->user) $this->deleteAs("api/playlists/{$playlist->id}/songs", ['songs' => $songs], $playlist->user)
->assertForbidden(); ->assertForbidden();
} }
} }

View file

@ -6,7 +6,6 @@ use App\Http\Resources\PlaylistResource;
use App\Models\Playlist; use App\Models\Playlist;
use App\Models\Song; use App\Models\Song;
use App\Values\SmartPlaylistRule; use App\Values\SmartPlaylistRule;
use Illuminate\Support\Collection;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase; use Tests\TestCase;
@ -30,23 +29,21 @@ class PlaylistTest extends TestCase
{ {
$user = create_user(); $user = create_user();
/** @var array<Song>|Collection $songs */
$songs = Song::factory(4)->create(); $songs = Song::factory(4)->create();
$this->postAs('api/playlists', [ $this->postAs('api/playlists', [
'name' => 'Foo Bar', 'name' => 'Foo Bar',
'songs' => $songs->pluck('id')->all(), 'songs' => $songs->modelKeys(),
'rules' => [], 'rules' => [],
], $user) ], $user)
->assertJsonStructure(PlaylistResource::JSON_STRUCTURE); ->assertJsonStructure(PlaylistResource::JSON_STRUCTURE);
/** @var Playlist $playlist */
$playlist = Playlist::query()->latest()->first(); $playlist = Playlist::query()->latest()->first();
self::assertSame('Foo Bar', $playlist->name); self::assertSame('Foo Bar', $playlist->name);
self::assertTrue($playlist->ownedBy($user)); self::assertTrue($playlist->ownedBy($user));
self::assertNull($playlist->getFolder()); self::assertNull($playlist->getFolder());
self::assertEqualsCanonicalizing($songs->pluck('id')->all(), $playlist->playables->pluck('id')->all()); self::assertEqualsCanonicalizing($songs->modelKeys(), $playlist->playables->modelKeys());
} }
#[Test] #[Test]
@ -70,7 +67,6 @@ class PlaylistTest extends TestCase
], ],
], $user)->assertJsonStructure(PlaylistResource::JSON_STRUCTURE); ], $user)->assertJsonStructure(PlaylistResource::JSON_STRUCTURE);
/** @var Playlist $playlist */
$playlist = Playlist::query()->latest()->first(); $playlist = Playlist::query()->latest()->first();
self::assertSame('Smart Foo Bar', $playlist->name); self::assertSame('Smart Foo Bar', $playlist->name);
@ -98,7 +94,7 @@ class PlaylistTest extends TestCase
], ],
], ],
], ],
'songs' => Song::factory(3)->create()->pluck('id')->all(), 'songs' => Song::factory(3)->create()->modelKeys(),
])->assertUnprocessable(); ])->assertUnprocessable();
} }
@ -116,10 +112,9 @@ class PlaylistTest extends TestCase
#[Test] #[Test]
public function updatePlaylistName(): void public function updatePlaylistName(): void
{ {
/** @var Playlist $playlist */
$playlist = Playlist::factory()->create(['name' => 'Foo']); $playlist = Playlist::factory()->create(['name' => 'Foo']);
$this->putAs("api/playlists/$playlist->id", ['name' => 'Bar'], $playlist->user) $this->putAs("api/playlists/{$playlist->id}", ['name' => 'Bar'], $playlist->user)
->assertJsonStructure(PlaylistResource::JSON_STRUCTURE); ->assertJsonStructure(PlaylistResource::JSON_STRUCTURE);
self::assertSame('Bar', $playlist->refresh()->name); self::assertSame('Bar', $playlist->refresh()->name);
@ -128,20 +123,18 @@ class PlaylistTest extends TestCase
#[Test] #[Test]
public function nonOwnerCannotUpdatePlaylist(): void public function nonOwnerCannotUpdatePlaylist(): void
{ {
/** @var Playlist $playlist */
$playlist = Playlist::factory()->create(['name' => 'Foo']); $playlist = Playlist::factory()->create(['name' => 'Foo']);
$this->putAs("api/playlists/$playlist->id", ['name' => 'Qux'])->assertForbidden(); $this->putAs("api/playlists/{$playlist->id}", ['name' => 'Qux'])->assertForbidden();
self::assertSame('Foo', $playlist->refresh()->name); self::assertSame('Foo', $playlist->refresh()->name);
} }
#[Test] #[Test]
public function deletePlaylist(): void public function deletePlaylist(): void
{ {
/** @var Playlist $playlist */
$playlist = Playlist::factory()->create(); $playlist = Playlist::factory()->create();
$this->deleteAs("api/playlists/$playlist->id", [], $playlist->user); $this->deleteAs("api/playlists/{$playlist->id}", [], $playlist->user);
self::assertModelMissing($playlist); self::assertModelMissing($playlist);
} }
@ -149,10 +142,9 @@ class PlaylistTest extends TestCase
#[Test] #[Test]
public function nonOwnerCannotDeletePlaylist(): void public function nonOwnerCannotDeletePlaylist(): void
{ {
/** @var Playlist $playlist */
$playlist = Playlist::factory()->create(); $playlist = Playlist::factory()->create();
$this->deleteAs("api/playlists/$playlist->id")->assertForbidden(); $this->deleteAs("api/playlists/{$playlist->id}")->assertForbidden();
self::assertModelExists($playlist); self::assertModelExists($playlist);
} }

View file

@ -45,13 +45,13 @@ class QueueTest extends TestCase
self::assertDatabaseMissing(QueueState::class, ['user_id' => $user->id]); self::assertDatabaseMissing(QueueState::class, ['user_id' => $user->id]);
$songIds = Song::factory(3)->create()->pluck('id')->toArray(); $songIds = Song::factory(3)->create()->modelKeys();
$this->putAs('api/queue/state', ['songs' => $songIds], $user) $this->putAs('api/queue/state', ['songs' => $songIds], $user)
->assertNoContent(); ->assertNoContent();
/** @var QueueState $queue */ /** @var QueueState $queue */
$queue = QueueState::query()->where('user_id', $user->id)->firstOrFail(); $queue = QueueState::query()->whereBelongsTo($user)->firstOrFail();
self::assertEqualsCanonicalizing($songIds, $queue->song_ids); self::assertEqualsCanonicalizing($songIds, $queue->song_ids);
} }

View file

@ -30,7 +30,7 @@ class ScrobbleTest extends TestCase
) )
->once(); ->once();
$this->postAs("/api/songs/$song->id/scrobble", ['timestamp' => 100], $user) $this->postAs("/api/songs/{$song->id}/scrobble", ['timestamp' => 100], $user)
->assertNoContent(); ->assertNoContent();
} }
} }

View file

@ -33,7 +33,7 @@ class SongPlayTest extends TestCase
->shouldReceive('stream') ->shouldReceive('stream')
->once(); ->once();
$this->get("play/$song->id?t=$token->audioToken") $this->get("play/{$song->id}?t=$token->audioToken")
->assertOk(); ->assertOk();
} }
@ -58,7 +58,7 @@ class SongPlayTest extends TestCase
->shouldReceive('stream') ->shouldReceive('stream')
->once(); ->once();
$this->get("play/$song->id?t=$token->audioToken") $this->get("play/{$song->id}?t=$token->audioToken")
->assertOk(); ->assertOk();
config(['koel.streaming.transcode_flac' => false]); config(['koel.streaming.transcode_flac' => false]);
@ -79,7 +79,7 @@ class SongPlayTest extends TestCase
->shouldReceive('stream') ->shouldReceive('stream')
->once(); ->once();
$this->get("play/$song->id/1?t=$token->audioToken") $this->get("play/{$song->id}/1?t=$token->audioToken")
->assertOk(); ->assertOk();
} }
} }

View file

@ -35,10 +35,9 @@ class SongTest extends TestCase
#[Test] #[Test]
public function destroy(): void public function destroy(): void
{ {
/** @var Collection<array-key, Song> $songs */
$songs = Song::factory(3)->create(); $songs = Song::factory(3)->create();
$this->deleteAs('api/songs', ['songs' => $songs->pluck('id')->all()], create_admin()) $this->deleteAs('api/songs', ['songs' => $songs->modelKeys()], create_admin())
->assertNoContent(); ->assertNoContent();
$songs->each(fn (Song $song) => $this->assertModelMissing($song)); $songs->each(fn (Song $song) => $this->assertModelMissing($song));
@ -47,10 +46,9 @@ class SongTest extends TestCase
#[Test] #[Test]
public function unauthorizedDelete(): void public function unauthorizedDelete(): void
{ {
/** @var Collection<array-key, Song> $songs */
$songs = Song::factory(3)->create(); $songs = Song::factory(3)->create();
$this->deleteAs('api/songs', ['songs' => $songs->pluck('id')->all()]) $this->deleteAs('api/songs', ['songs' => $songs->modelKeys()])
->assertForbidden(); ->assertForbidden();
$songs->each(fn (Song $song) => $this->assertModelExists($song)); $songs->each(fn (Song $song) => $this->assertModelExists($song));
@ -122,7 +120,7 @@ class SongTest extends TestCase
#[Test] #[Test]
public function multipleUpdateNoCompilation(): void public function multipleUpdateNoCompilation(): void
{ {
$songIds = Song::factory(3)->create()->pluck('id')->all(); $songIds = Song::factory(3)->create()->modelKeys();
$this->putAs('/api/songs', [ $this->putAs('/api/songs', [
'songs' => $songIds, 'songs' => $songIds,
@ -159,9 +157,8 @@ class SongTest extends TestCase
#[Test] #[Test]
public function multipleUpdateCreatingNewAlbumsAndArtists(): void public function multipleUpdateCreatingNewAlbumsAndArtists(): void
{ {
/** @var Collection<array-key, Song> $originalSongs */
$originalSongs = Song::factory(3)->create(); $originalSongs = Song::factory(3)->create();
$originalSongIds = $originalSongs->pluck('id')->all(); $originalSongIds = $originalSongs->modelKeys();
$originalAlbumNames = $originalSongs->pluck('album.name')->all(); $originalAlbumNames = $originalSongs->pluck('album.name')->all();
$originalAlbumIds = $originalSongs->pluck('album_id')->all(); $originalAlbumIds = $originalSongs->pluck('album_id')->all();
@ -177,7 +174,6 @@ class SongTest extends TestCase
], create_admin()) ], create_admin())
->assertOk(); ->assertOk();
/** @var Collection<array-key, Song> $songs */
$songs = Song::query()->whereIn('id', $originalSongIds)->get()->orderByArray($originalSongIds); $songs = Song::query()->whereIn('id', $originalSongIds)->get()->orderByArray($originalSongIds);
// Even though the album name doesn't change, a new artist should have been created // Even though the album name doesn't change, a new artist should have been created
@ -263,10 +259,7 @@ class SongTest extends TestCase
{ {
Song::factory(5)->create(); Song::factory(5)->create();
self::assertNotSame(0, Song::query()->count()); Song::deleteByChunk(Song::query()->get()->modelKeys(), 1);
$ids = Song::query()->select('id')->get()->pluck('id')->all();
Song::deleteByChunk($ids, 1);
self::assertSame(0, Song::query()->count()); self::assertSame(0, Song::query()->count());
} }

View file

@ -16,10 +16,10 @@ class SongVisibilityTest extends TestCase
$owner = create_admin(); $owner = create_admin();
Song::factory(3)->create(); Song::factory(3)->create();
$this->putAs('api/songs/publicize', ['songs' => Song::query()->pluck('id')->all()], $owner) $this->putAs('api/songs/publicize', ['songs' => Song::query()->get()->modelKeys()], $owner)
->assertForbidden(); ->assertForbidden();
$this->putAs('api/songs/privatize', ['songs' => Song::query()->pluck('id')->all()], $owner) $this->putAs('api/songs/privatize', ['songs' => Song::query()->get()->modelKeys()], $owner)
->assertForbidden(); ->assertForbidden();
} }
} }

View file

@ -51,7 +51,7 @@ class UserTest extends TestCase
$admin = create_admin(); $admin = create_admin();
$user = create_admin(['password' => 'secret']); $user = create_admin(['password' => 'secret']);
$this->putAs("api/user/$user->id", [ $this->putAs("api/user/{$user->id}", [
'name' => 'Foo', 'name' => 'Foo',
'email' => 'bar@baz.com', 'email' => 'bar@baz.com',
'password' => 'new-secret', 'password' => 'new-secret',
@ -72,7 +72,7 @@ class UserTest extends TestCase
{ {
$user = create_user(); $user = create_user();
$this->deleteAs("api/user/$user->id", [], create_admin()); $this->deleteAs("api/user/{$user->id}", [], create_admin());
self::assertModelMissing($user); self::assertModelMissing($user);
} }
@ -81,7 +81,7 @@ class UserTest extends TestCase
{ {
$admin = create_admin(); $admin = create_admin();
$this->deleteAs("api/user/$admin->id", [], $admin)->assertForbidden(); $this->deleteAs("api/user/{$admin->id}", [], $admin)->assertForbidden();
self::assertModelExists($admin); self::assertModelExists($admin);
} }
} }

View file

@ -52,8 +52,8 @@ class SmartPlaylistServiceTest extends PlusTestCase
]); ]);
self::assertEqualsCanonicalizing( self::assertEqualsCanonicalizing(
$matches->pluck('id')->all(), $matches->modelKeys(),
$this->service->getSongs($playlist, $owner)->pluck('id')->all() $this->service->getSongs($playlist, $owner)->modelKeys()
); );
} }
} }

View file

@ -66,8 +66,8 @@ class InteractionServiceTest extends TestCase
$songs->each(static function (Song $song) use ($user): void { $songs->each(static function (Song $song) use ($user): void {
/** @var Interaction $interaction */ /** @var Interaction $interaction */
$interaction = Interaction::query() $interaction = Interaction::query()
->where('song_id', $song->id) ->whereBelongsTo($song)
->where('user_id', $user->id) ->whereBelongsTo($user)
->first(); ->first();
self::assertTrue($interaction->liked); self::assertTrue($interaction->liked);
@ -82,10 +82,9 @@ class InteractionServiceTest extends TestCase
Event::fake(MultipleSongsUnliked::class); Event::fake(MultipleSongsUnliked::class);
$user = create_user(); $user = create_user();
/** @var Collection $interactions */
$interactions = Interaction::factory(3)->for($user)->create(['liked' => true]); $interactions = Interaction::factory(3)->for($user)->create(['liked' => true]);
$this->interactionService->unlikeMany($interactions->map(static fn (Interaction $i) => $i->song), $user); $this->interactionService->unlikeMany($interactions->map(static fn (Interaction $i) => $i->song), $user); // @phpstan-ignore-line
$interactions->each(static function (Interaction $interaction): void { $interactions->each(static function (Interaction $interaction): void {
self::assertFalse($interaction->refresh()->liked); self::assertFalse($interaction->refresh()->liked);

View file

@ -8,7 +8,6 @@ use App\Models\Podcast;
use App\Models\Song; use App\Models\Song;
use App\Services\PlaylistService; use App\Services\PlaylistService;
use App\Values\SmartPlaylistRuleGroupCollection; use App\Values\SmartPlaylistRuleGroupCollection;
use Illuminate\Support\Collection;
use InvalidArgumentException as BaseInvalidArgumentException; use InvalidArgumentException as BaseInvalidArgumentException;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Tests\PlusTestCase; use Tests\PlusTestCase;
@ -43,17 +42,15 @@ class PlaylistServiceTest extends TestCase
#[Test] #[Test]
public function createPlaylistWithSongs(): void public function createPlaylistWithSongs(): void
{ {
/** @var Collection<array-key, Song> $songs */
$songs = Song::factory(3)->create(); $songs = Song::factory(3)->create();
$user = create_user(); $user = create_user();
$playlist = $this->service->createPlaylist('foo', $user, null, $songs->pluck('id')->all()); $playlist = $this->service->createPlaylist('foo', $user, null, $songs->modelKeys());
self::assertSame('foo', $playlist->name); self::assertSame('foo', $playlist->name);
self::assertTrue($user->is($playlist->user)); self::assertTrue($user->is($playlist->user));
self::assertFalse($playlist->is_smart); self::assertFalse($playlist->is_smart);
self::assertEqualsCanonicalizing($playlist->playables->pluck('id')->all(), $songs->pluck('id')->all()); self::assertEqualsCanonicalizing($playlist->playables->modelKeys(), $songs->modelKeys());
} }
#[Test] #[Test]
@ -213,7 +210,7 @@ class PlaylistServiceTest extends TestCase
self::assertCount(2, $addedSongs); self::assertCount(2, $addedSongs);
self::assertCount(5, $playlist->playables); self::assertCount(5, $playlist->playables);
self::assertEqualsCanonicalizing($addedSongs->pluck('id')->all(), $songs->pluck('id')->all()); self::assertEqualsCanonicalizing($addedSongs->modelKeys(), $songs->modelKeys());
$songs->each(static fn (Song $song) => self::assertTrue($playlist->playables->contains($song))); $songs->each(static fn (Song $song) => self::assertTrue($playlist->playables->contains($song)));
} }
@ -235,7 +232,7 @@ class PlaylistServiceTest extends TestCase
self::assertCount(2, $addedEpisodes); self::assertCount(2, $addedEpisodes);
self::assertCount(5, $playlist->playables); self::assertCount(5, $playlist->playables);
self::assertEqualsCanonicalizing($addedEpisodes->pluck('id')->all(), $episodes->pluck('id')->all()); self::assertEqualsCanonicalizing($addedEpisodes->modelKeys(), $episodes->modelKeys());
} }
#[Test] #[Test]
@ -252,7 +249,7 @@ class PlaylistServiceTest extends TestCase
self::assertCount(4, $addedEpisodes); self::assertCount(4, $addedEpisodes);
self::assertCount(7, $playlist->playables); self::assertCount(7, $playlist->playables);
self::assertEqualsCanonicalizing($addedEpisodes->pluck('id')->all(), $playables->pluck('id')->all()); self::assertEqualsCanonicalizing($addedEpisodes->modelKeys(), $playables->modelKeys());
} }
#[Test] #[Test]
@ -292,23 +289,22 @@ class PlaylistServiceTest extends TestCase
/** @var Playlist $playlist */ /** @var Playlist $playlist */
$playlist = Playlist::factory()->create(); $playlist = Playlist::factory()->create();
/** @var Collection<array-key, Song> $songs */
$songs = Song::factory(4)->create(); $songs = Song::factory(4)->create();
$ids = $songs->pluck('id')->all(); $ids = $songs->modelKeys();
$playlist->addPlayables($songs); $playlist->addPlayables($songs);
$this->service->movePlayablesInPlaylist($playlist, [$ids[2], $ids[3]], $ids[0], 'after'); $this->service->movePlayablesInPlaylist($playlist, [$ids[2], $ids[3]], $ids[0], 'after');
self::assertSame([$ids[0], $ids[2], $ids[3], $ids[1]], $playlist->refresh()->playables->pluck('id')->all()); self::assertSame([$ids[0], $ids[2], $ids[3], $ids[1]], $playlist->refresh()->playables->modelKeys());
$this->service->movePlayablesInPlaylist($playlist, [$ids[0]], $ids[3], 'before'); $this->service->movePlayablesInPlaylist($playlist, [$ids[0]], $ids[3], 'before');
self::assertSame([$ids[2], $ids[0], $ids[3], $ids[1]], $playlist->refresh()->playables->pluck('id')->all()); self::assertSame([$ids[2], $ids[0], $ids[3], $ids[1]], $playlist->refresh()->playables->modelKeys());
// move to the first position // move to the first position
$this->service->movePlayablesInPlaylist($playlist, [$ids[0], $ids[1]], $ids[2], 'before'); $this->service->movePlayablesInPlaylist($playlist, [$ids[0], $ids[1]], $ids[2], 'before');
self::assertSame([$ids[0], $ids[1], $ids[2], $ids[3]], $playlist->refresh()->playables->pluck('id')->all()); self::assertSame([$ids[0], $ids[1], $ids[2], $ids[3]], $playlist->refresh()->playables->modelKeys());
// move to the last position // move to the last position
$this->service->movePlayablesInPlaylist($playlist, [$ids[0], $ids[1]], $ids[3], 'after'); $this->service->movePlayablesInPlaylist($playlist, [$ids[0], $ids[1]], $ids[3], 'after');
self::assertSame([$ids[2], $ids[3], $ids[0], $ids[1]], $playlist->refresh()->playables->pluck('id')->all()); self::assertSame([$ids[2], $ids[3], $ids[0], $ids[1]], $playlist->refresh()->playables->modelKeys());
} }
} }

View file

@ -49,11 +49,11 @@ class QueueServiceTest extends TestCase
'user_id' => $user->id, 'user_id' => $user->id,
]); ]);
$songIds = Song::factory()->count(3)->create()->pluck('id')->toArray(); $songIds = Song::factory()->count(3)->create()->modelKeys();
$this->service->updateQueueState($user, $songIds); $this->service->updateQueueState($user, $songIds);
/** @var QueueState $queueState */ /** @var QueueState $queueState */
$queueState = QueueState::query()->where('user_id', $user->id)->firstOrFail(); $queueState = QueueState::query()->whereBelongsTo($user)->firstOrFail();
self::assertEqualsCanonicalizing($songIds, $queueState->song_ids); self::assertEqualsCanonicalizing($songIds, $queueState->song_ids);
self::assertNull($queueState->current_song_id); self::assertNull($queueState->current_song_id);
self::assertSame(0, $queueState->playback_position); self::assertSame(0, $queueState->playback_position);
@ -65,7 +65,7 @@ class QueueServiceTest extends TestCase
/** @var QueueState $state */ /** @var QueueState $state */
$state = QueueState::factory()->create(); $state = QueueState::factory()->create();
$songIds = Song::factory()->count(3)->create()->pluck('id')->toArray(); $songIds = Song::factory()->count(3)->create()->modelKeys();
$this->service->updateQueueState($state->user, $songIds); $this->service->updateQueueState($state->user, $songIds);
$state->refresh(); $state->refresh();

View file

@ -9,7 +9,7 @@ use App\Models\Playlist;
use App\Models\Song; use App\Models\Song;
use App\Models\User; use App\Models\User;
use App\Services\SmartPlaylistService; use App\Services\SmartPlaylistService;
use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Collection;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase; use Tests\TestCase;
@ -564,8 +564,8 @@ class SmartPlaylistServiceTest extends TestCase
$playlist = Playlist::factory()->for($owner ?? create_admin())->create(['rules' => $rules]); $playlist = Playlist::factory()->for($owner ?? create_admin())->create(['rules' => $rules]);
self::assertEqualsCanonicalizing( self::assertEqualsCanonicalizing(
$matches->pluck('id')->all(), $matches->modelKeys(),
$this->service->getSongs($playlist)->pluck('id')->all() $this->service->getSongs($playlist)->modelKeys()
); );
} }
} }

View file

@ -28,7 +28,7 @@ class EpisodePlayableTest extends TestCase
Http::assertSentCount(1); Http::assertSentCount(1);
self::assertSame('acbd18db4cc2f85cedef654fccc4a4d8', $playable->checksum); self::assertSame('acbd18db4cc2f85cedef654fccc4a4d8', $playable->checksum);
self::assertTrue(Cache::has("episode-playable.$episode->id")); self::assertTrue(Cache::has("episode-playable.{$episode->id}"));
$retrieved = EpisodePlayable::getForEpisode($episode); $retrieved = EpisodePlayable::getForEpisode($episode);

View file

@ -26,7 +26,7 @@ class AlbumTest extends TestCase
$artist = Artist::factory()->create(); $artist = Artist::factory()->create();
$name = 'Foo'; $name = 'Foo';
self::assertNull(Album::query()->where('artist_id', $artist->id)->where('name', $name)->first()); self::assertNull(Album::query()->whereBelongsTo($artist)->where('name', $name)->first());
$album = Album::getOrCreate($artist, $name); $album = Album::getOrCreate($artist, $name);
self::assertSame('Foo', $album->name); self::assertSame('Foo', $album->name);

View file

@ -49,7 +49,7 @@ class MediaInformationServiceTest extends TestCase
->shouldNotReceive('tryDownloadAlbumCover'); ->shouldNotReceive('tryDownloadAlbumCover');
self::assertSame($info, $this->mediaInformationService->getAlbumInformation($album)); self::assertSame($info, $this->mediaInformationService->getAlbumInformation($album));
self::assertNotNull(cache()->get('album.info.' . $album->id)); self::assertNotNull(cache()->get("album.info.{$album->id}"));
} }
#[Test] #[Test]
@ -93,7 +93,7 @@ class MediaInformationServiceTest extends TestCase
->shouldNotReceive('tryDownloadArtistImage'); ->shouldNotReceive('tryDownloadArtistImage');
self::assertSame($info, $this->mediaInformationService->getArtistInformation($artist)); self::assertSame($info, $this->mediaInformationService->getArtistInformation($artist));
self::assertNotNull(cache()->get('artist.info.' . $artist->id)); self::assertNotNull(cache()->get("artist.info.{$artist->id}"));
} }
#[Test] #[Test]

View file

@ -5,7 +5,7 @@ namespace Tests\Unit\Services;
use App\Models\Playlist; use App\Models\Playlist;
use App\Models\PlaylistFolder; use App\Models\PlaylistFolder;
use App\Services\PlaylistFolderService; use App\Services\PlaylistFolderService;
use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Collection;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase; use Tests\TestCase;
@ -57,7 +57,7 @@ class PlaylistFolderServiceTest extends TestCase
/** @var PlaylistFolder $folder */ /** @var PlaylistFolder $folder */
$folder = PlaylistFolder::factory()->for($user)->create(); $folder = PlaylistFolder::factory()->for($user)->create();
$this->service->addPlaylistsToFolder($folder, $playlists->pluck('id')->all()); $this->service->addPlaylistsToFolder($folder, $playlists->modelKeys());
self::assertCount(3, $folder->playlists); self::assertCount(3, $folder->playlists);
} }
@ -70,9 +70,9 @@ class PlaylistFolderServiceTest extends TestCase
/** @var Collection<array-key, Playlist> $playlists */ /** @var Collection<array-key, Playlist> $playlists */
$playlists = Playlist::factory()->count(3)->create(); $playlists = Playlist::factory()->count(3)->create();
$folder->playlists()->attach($playlists->pluck('id')->all()); $folder->playlists()->attach($playlists);
$this->service->movePlaylistsToRootLevel($folder, $playlists->pluck('id')->all()); $this->service->movePlaylistsToRootLevel($folder, $playlists->modelKeys());
self::assertCount(0, $folder->playlists); self::assertCount(0, $folder->playlists);