A lot of changes
This commit is contained in:
@@ -3,13 +3,8 @@ using Mediator;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Musify.Application.Albums.Responses;
|
using Musify.Application.Albums.Responses;
|
||||||
using Musify.Application.Configuration;
|
|
||||||
using Musify.Application.Contracts;
|
using Musify.Application.Contracts;
|
||||||
using Musify.Application.Events;
|
|
||||||
using Musify.Application.Pictures;
|
|
||||||
using Musify.Application.Services;
|
|
||||||
using Musify.Application.Shared;
|
using Musify.Application.Shared;
|
||||||
using Musify.Domain.ValueObjects;
|
|
||||||
|
|
||||||
namespace Musify.Application.Albums
|
namespace Musify.Application.Albums
|
||||||
{
|
{
|
||||||
@@ -18,16 +13,12 @@ namespace Musify.Application.Albums
|
|||||||
Guid AlbumId,
|
Guid AlbumId,
|
||||||
string Title,
|
string Title,
|
||||||
string? Description,
|
string? Description,
|
||||||
int? ReleaseYear,
|
int? ReleaseYear)
|
||||||
Guid? NewPictureIntentId)
|
|
||||||
: ICommand<ErrorOr<AlbumApplicationResponse>>;
|
: ICommand<ErrorOr<AlbumApplicationResponse>>;
|
||||||
|
|
||||||
public class UpdateAlbumCommandHandler(
|
public class UpdateAlbumCommandHandler(
|
||||||
IEventBus eventBus,
|
|
||||||
IDatabase database,
|
IDatabase database,
|
||||||
UploadIntentValidator uploadIntentValidator,
|
ILogger<UpdateAlbumCommandHandler> logger)
|
||||||
ILogger<UpdateAlbumCommandHandler> logger,
|
|
||||||
AlbumConfiguration albumConfiguration)
|
|
||||||
: ICommandHandler<UpdateAlbumCommand, ErrorOr<AlbumApplicationResponse>>
|
: ICommandHandler<UpdateAlbumCommand, ErrorOr<AlbumApplicationResponse>>
|
||||||
{
|
{
|
||||||
public async ValueTask<ErrorOr<AlbumApplicationResponse>> Handle(UpdateAlbumCommand request, CancellationToken cancellationToken)
|
public async ValueTask<ErrorOr<AlbumApplicationResponse>> Handle(UpdateAlbumCommand request, CancellationToken cancellationToken)
|
||||||
@@ -39,27 +30,12 @@ namespace Musify.Application.Albums
|
|||||||
var album = found.Value;
|
var album = found.Value;
|
||||||
var title = request.Title.Trim();
|
var title = request.Title.Trim();
|
||||||
|
|
||||||
|
// This is a full replace of the plain fields; the picture has its own command.
|
||||||
album.Title = title;
|
album.Title = title;
|
||||||
album.NormalizedTitle = TextNormalizer.Normalize(title);
|
album.NormalizedTitle = TextNormalizer.Normalize(title);
|
||||||
album.Description = request.Description;
|
album.Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim();
|
||||||
album.ReleaseYear = request.ReleaseYear;
|
album.ReleaseYear = request.ReleaseYear;
|
||||||
|
|
||||||
if (request.NewPictureIntentId is { } intentId)
|
|
||||||
{
|
|
||||||
var prepared = await PictureSourceChange.PrepareAsync(
|
|
||||||
uploadIntentValidator, intentId, request.UserId, UploadIntentPurpose.AlbumPicture, albumConfiguration, cancellationToken);
|
|
||||||
if (prepared.IsError)
|
|
||||||
return prepared.Errors;
|
|
||||||
|
|
||||||
var picture = prepared.Value;
|
|
||||||
album.Pictures = picture.Pictures;
|
|
||||||
|
|
||||||
await eventBus.PublishAsync(
|
|
||||||
new UpdateAlbumPictureSourceEvent(
|
|
||||||
album.Id, picture.Intent.Id, picture.Intent.Bucket, picture.Intent.Key, picture.FinalKey, picture.Sizes),
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
await database.SaveChangesAsync(cancellationToken);
|
await database.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
logger.LogInformation("Updated album {AlbumId}", album.Id);
|
logger.LogInformation("Updated album {AlbumId}", album.Id);
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using ErrorOr;
|
||||||
|
using Mediator;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Musify.Application.Albums.Responses;
|
||||||
|
using Musify.Application.Configuration;
|
||||||
|
using Musify.Application.Contracts;
|
||||||
|
using Musify.Application.Events;
|
||||||
|
using Musify.Application.Pictures;
|
||||||
|
using Musify.Application.Services;
|
||||||
|
using Musify.Application.Shared;
|
||||||
|
using Musify.Domain.ValueObjects;
|
||||||
|
|
||||||
|
namespace Musify.Application.Albums
|
||||||
|
{
|
||||||
|
/// <summary>Replaces an album's picture with a previously uploaded image; the plain fields are untouched.</summary>
|
||||||
|
public record UpdateAlbumPictureCommand(long UserId, Guid AlbumId, Guid PictureIntentId)
|
||||||
|
: ICommand<ErrorOr<AlbumApplicationResponse>>;
|
||||||
|
|
||||||
|
public class UpdateAlbumPictureCommandHandler(
|
||||||
|
IEventBus eventBus,
|
||||||
|
IDatabase database,
|
||||||
|
UploadIntentValidator uploadIntentValidator,
|
||||||
|
ILogger<UpdateAlbumPictureCommandHandler> logger,
|
||||||
|
AlbumConfiguration albumConfiguration)
|
||||||
|
: ICommandHandler<UpdateAlbumPictureCommand, ErrorOr<AlbumApplicationResponse>>
|
||||||
|
{
|
||||||
|
public async ValueTask<ErrorOr<AlbumApplicationResponse>> Handle(UpdateAlbumPictureCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var found = await database.Albums.FindOwnedAsync(request.AlbumId, request.UserId, cancellationToken);
|
||||||
|
if (found.IsError)
|
||||||
|
return found.Errors;
|
||||||
|
|
||||||
|
var album = found.Value;
|
||||||
|
|
||||||
|
var prepared = await PictureSourceChange.PrepareAsync(
|
||||||
|
uploadIntentValidator, request.PictureIntentId, request.UserId, UploadIntentPurpose.AlbumPicture, albumConfiguration, cancellationToken);
|
||||||
|
if (prepared.IsError)
|
||||||
|
return prepared.Errors;
|
||||||
|
|
||||||
|
var picture = prepared.Value;
|
||||||
|
album.Pictures = picture.Pictures;
|
||||||
|
|
||||||
|
await eventBus.PublishAsync(
|
||||||
|
new UpdateAlbumPictureSourceEvent(
|
||||||
|
album.Id, picture.Intent.Id, picture.Intent.Bucket, picture.Intent.Key, picture.FinalKey, picture.Sizes),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
await database.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
logger.LogInformation("Updated picture of album {AlbumId}", album.Id);
|
||||||
|
|
||||||
|
return await database.Albums
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(a => a.Id == album.Id)
|
||||||
|
.SelectResponse()
|
||||||
|
.SingleAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ namespace Musify.Application.Genres
|
|||||||
{
|
{
|
||||||
public ValueTask<IReadOnlyList<AvailableGenreResponse>> Handle(GetAvailableGenresQuery request, CancellationToken cancellationToken)
|
public ValueTask<IReadOnlyList<AvailableGenreResponse>> Handle(GetAvailableGenresQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var all = Enum.GetValues<Genre>();
|
var all = Enum.GetValues<Genre>().Where(genre => genre != Genre.None).ToArray();
|
||||||
|
|
||||||
IReadOnlyList<AvailableGenreResponse> result = all
|
IReadOnlyList<AvailableGenreResponse> result = all
|
||||||
.Select(genre => new AvailableGenreResponse(
|
.Select(genre => new AvailableGenreResponse(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Musify.Application.Contracts;
|
|||||||
using Musify.Application.PlayLists.Responses;
|
using Musify.Application.PlayLists.Responses;
|
||||||
using Musify.Application.Shared;
|
using Musify.Application.Shared;
|
||||||
using Musify.Domain.Entities;
|
using Musify.Domain.Entities;
|
||||||
|
using Musify.Domain.ValueObjects;
|
||||||
|
|
||||||
namespace Musify.Application.PlayLists
|
namespace Musify.Application.PlayLists
|
||||||
{
|
{
|
||||||
@@ -34,7 +35,7 @@ namespace Musify.Application.PlayLists
|
|||||||
|
|
||||||
var albumTrackIds = await database.AlbumHasTracks
|
var albumTrackIds = await database.AlbumHasTracks
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(albumTrack => albumTrack.AlbumId == request.AlbumId && albumTrack.Track.LifeCycleStatus == Domain.ValueObjects.LifeCycleStatus.Active)
|
.Where(albumTrack => albumTrack.AlbumId == request.AlbumId && albumTrack.Track.LifeCycleStatus == LifeCycleStatus.Active)
|
||||||
.OrderBy(albumTrack => albumTrack.TrackNumber)
|
.OrderBy(albumTrack => albumTrack.TrackNumber)
|
||||||
.Select(albumTrack => albumTrack.TrackId)
|
.Select(albumTrack => albumTrack.TrackId)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
@@ -54,8 +55,9 @@ namespace Musify.Application.PlayLists
|
|||||||
return new AddAlbumToPlayListResponse(0);
|
return new AddAlbumToPlayListResponse(0);
|
||||||
|
|
||||||
// Playlist positions are internal indexes, so they start at 0.
|
// Playlist positions are internal indexes, so they start at 0.
|
||||||
var nextPosition = (await playListTracksQuery
|
var nextPosition = existingTrackIds.Count > 0
|
||||||
.MaxAsync(link => (int?)link.Position, cancellationToken) ?? -1) + 1;
|
? await playListTracksQuery.MaxAsync(link => link.Position, cancellationToken) + 1
|
||||||
|
: 0;
|
||||||
|
|
||||||
foreach (var trackId in newTrackIds)
|
foreach (var trackId in newTrackIds)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -47,9 +47,12 @@ namespace Musify.Application.PlayLists
|
|||||||
return Result.Success;
|
return Result.Success;
|
||||||
|
|
||||||
// Playlist positions are internal indexes, so they start at 0.
|
// Playlist positions are internal indexes, so they start at 0.
|
||||||
var nextPosition = (await database.PlayListHasTracks
|
var playListTracksQuery = database.PlayListHasTracks
|
||||||
.Where(link => link.PlayListId == request.PlayListId)
|
.Where(link => link.PlayListId == request.PlayListId);
|
||||||
.MaxAsync(link => (int?)link.Position, cancellationToken) ?? -1) + 1;
|
|
||||||
|
var nextPosition = await playListTracksQuery.AnyAsync(cancellationToken)
|
||||||
|
? await playListTracksQuery.MaxAsync(link => link.Position, cancellationToken) + 1
|
||||||
|
: 0;
|
||||||
|
|
||||||
foreach (var trackId in toAdd)
|
foreach (var trackId in toAdd)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -39,10 +39,10 @@ namespace Musify.Application.PlayLists
|
|||||||
return AppErrors.NotFound("User", request.UserId);
|
return AppErrors.NotFound("User", request.UserId);
|
||||||
|
|
||||||
PreparedPicture? picture = null;
|
PreparedPicture? picture = null;
|
||||||
if (request.PictureIntentId is { } intentId)
|
if (request.PictureIntentId.HasValue)
|
||||||
{
|
{
|
||||||
var prepared = await PictureSourceChange.PrepareAsync(
|
var prepared = await PictureSourceChange.PrepareAsync(
|
||||||
uploadIntentValidator, intentId, request.UserId, UploadIntentPurpose.PlayListPicture, playListConfiguration, cancellationToken);
|
uploadIntentValidator, request.PictureIntentId.Value, request.UserId, UploadIntentPurpose.PlayListPicture, playListConfiguration, cancellationToken);
|
||||||
if (prepared.IsError)
|
if (prepared.IsError)
|
||||||
return prepared.Errors;
|
return prepared.Errors;
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,8 @@ using ErrorOr;
|
|||||||
using Mediator;
|
using Mediator;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Musify.Application.Configuration;
|
|
||||||
using Musify.Application.Contracts;
|
using Musify.Application.Contracts;
|
||||||
using Musify.Application.Events;
|
|
||||||
using Musify.Application.Pictures;
|
|
||||||
using Musify.Application.PlayLists.Responses;
|
using Musify.Application.PlayLists.Responses;
|
||||||
using Musify.Application.Services;
|
|
||||||
using Musify.Application.Shared;
|
using Musify.Application.Shared;
|
||||||
using Musify.Domain.ValueObjects;
|
using Musify.Domain.ValueObjects;
|
||||||
|
|
||||||
@@ -16,18 +12,14 @@ namespace Musify.Application.PlayLists
|
|||||||
public record UpdatePlayListCommand(
|
public record UpdatePlayListCommand(
|
||||||
long UserId,
|
long UserId,
|
||||||
Guid PlayListId,
|
Guid PlayListId,
|
||||||
string? NewName,
|
string NewName,
|
||||||
string? NewDescription,
|
string? NewDescription,
|
||||||
Guid? NewPictureIntentId,
|
PlayListVisibility NewVisibility)
|
||||||
PlayListVisibility? NewVisibility = null)
|
|
||||||
: ICommand<ErrorOr<PlayListApplicationResponse>>;
|
: ICommand<ErrorOr<PlayListApplicationResponse>>;
|
||||||
|
|
||||||
public class UpdatePlayListCommandHandler(
|
public class UpdatePlayListCommandHandler(
|
||||||
IEventBus eventBus,
|
|
||||||
IDatabase database,
|
IDatabase database,
|
||||||
UploadIntentValidator uploadIntentValidator,
|
ILogger<UpdatePlayListCommandHandler> logger)
|
||||||
ILogger<UpdatePlayListCommandHandler> logger,
|
|
||||||
PlayListConfiguration playListConfiguration)
|
|
||||||
: ICommandHandler<UpdatePlayListCommand, ErrorOr<PlayListApplicationResponse>>
|
: ICommandHandler<UpdatePlayListCommand, ErrorOr<PlayListApplicationResponse>>
|
||||||
{
|
{
|
||||||
public async ValueTask<ErrorOr<PlayListApplicationResponse>> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken)
|
public async ValueTask<ErrorOr<PlayListApplicationResponse>> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken)
|
||||||
@@ -38,34 +30,11 @@ namespace Musify.Application.PlayLists
|
|||||||
|
|
||||||
var playList = found.Value;
|
var playList = found.Value;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(request.NewName))
|
// This is a full replace of the plain fields; the picture has its own command.
|
||||||
{
|
playList.Name = request.NewName.Trim();
|
||||||
playList.Name = request.NewName.Trim();
|
playList.NormalizedName = TextNormalizer.Normalize(request.NewName);
|
||||||
playList.NormalizedName = TextNormalizer.Normalize(request.NewName);
|
playList.Description = string.IsNullOrWhiteSpace(request.NewDescription) ? null : request.NewDescription.Trim();
|
||||||
}
|
playList.Visibility = request.NewVisibility;
|
||||||
|
|
||||||
// null means "unchanged"; a blank description clears it.
|
|
||||||
if (request.NewDescription != null)
|
|
||||||
playList.Description = string.IsNullOrWhiteSpace(request.NewDescription) ? null : request.NewDescription.Trim();
|
|
||||||
|
|
||||||
if (request.NewVisibility is { } visibility)
|
|
||||||
playList.Visibility = visibility;
|
|
||||||
|
|
||||||
if (request.NewPictureIntentId is { } intentId)
|
|
||||||
{
|
|
||||||
var prepared = await PictureSourceChange.PrepareAsync(
|
|
||||||
uploadIntentValidator, intentId, request.UserId, UploadIntentPurpose.PlayListPicture, playListConfiguration, cancellationToken);
|
|
||||||
if (prepared.IsError)
|
|
||||||
return prepared.Errors;
|
|
||||||
|
|
||||||
var picture = prepared.Value;
|
|
||||||
playList.Pictures = picture.Pictures;
|
|
||||||
|
|
||||||
await eventBus.PublishAsync(
|
|
||||||
new UpdatePlayListPictureSourceEvent(
|
|
||||||
playList.Id, picture.Intent.Id, picture.Intent.Bucket, picture.Intent.Key, picture.FinalKey, picture.Sizes),
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
await database.SaveChangesAsync(cancellationToken);
|
await database.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using ErrorOr;
|
||||||
|
using Mediator;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Musify.Application.Configuration;
|
||||||
|
using Musify.Application.Contracts;
|
||||||
|
using Musify.Application.Events;
|
||||||
|
using Musify.Application.Pictures;
|
||||||
|
using Musify.Application.PlayLists.Responses;
|
||||||
|
using Musify.Application.Services;
|
||||||
|
using Musify.Application.Shared;
|
||||||
|
using Musify.Domain.ValueObjects;
|
||||||
|
|
||||||
|
namespace Musify.Application.PlayLists
|
||||||
|
{
|
||||||
|
/// <summary>Replaces a playlist's picture with a previously uploaded image; the plain fields are untouched.</summary>
|
||||||
|
public record UpdatePlayListPictureCommand(long UserId, Guid PlayListId, Guid PictureIntentId)
|
||||||
|
: ICommand<ErrorOr<PlayListApplicationResponse>>;
|
||||||
|
|
||||||
|
public class UpdatePlayListPictureCommandHandler(
|
||||||
|
IEventBus eventBus,
|
||||||
|
IDatabase database,
|
||||||
|
UploadIntentValidator uploadIntentValidator,
|
||||||
|
ILogger<UpdatePlayListPictureCommandHandler> logger,
|
||||||
|
PlayListConfiguration playListConfiguration)
|
||||||
|
: ICommandHandler<UpdatePlayListPictureCommand, ErrorOr<PlayListApplicationResponse>>
|
||||||
|
{
|
||||||
|
public async ValueTask<ErrorOr<PlayListApplicationResponse>> Handle(UpdatePlayListPictureCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var found = await database.PlayLists.FindOwnedAsync(request.PlayListId, request.UserId, cancellationToken);
|
||||||
|
if (found.IsError)
|
||||||
|
return found.Errors;
|
||||||
|
|
||||||
|
var playList = found.Value;
|
||||||
|
|
||||||
|
var prepared = await PictureSourceChange.PrepareAsync(
|
||||||
|
uploadIntentValidator, request.PictureIntentId, request.UserId, UploadIntentPurpose.PlayListPicture, playListConfiguration, cancellationToken);
|
||||||
|
if (prepared.IsError)
|
||||||
|
return prepared.Errors;
|
||||||
|
|
||||||
|
var picture = prepared.Value;
|
||||||
|
playList.Pictures = picture.Pictures;
|
||||||
|
|
||||||
|
await eventBus.PublishAsync(
|
||||||
|
new UpdatePlayListPictureSourceEvent(
|
||||||
|
playList.Id, picture.Intent.Id, picture.Intent.Bucket, picture.Intent.Key, picture.FinalKey, picture.Sizes),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
await database.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
logger.LogInformation("Updated picture of playlist {PlayListId}", playList.Id);
|
||||||
|
|
||||||
|
return await database.PlayLists
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(p => p.Id == playList.Id)
|
||||||
|
.SelectResponse()
|
||||||
|
.SingleAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -90,7 +90,6 @@ namespace Musify.Application.Tracks
|
|||||||
|
|
||||||
await database.Tracks.AddAsync(trackEntity, cancellationToken);
|
await database.Tracks.AddAsync(trackEntity, cancellationToken);
|
||||||
|
|
||||||
|
|
||||||
var sizes = trackConfiguration.PicturesSizes.ToImageSizes(trackConfiguration.Routes);
|
var sizes = trackConfiguration.PicturesSizes.ToImageSizes(trackConfiguration.Routes);
|
||||||
await eventBus.PublishAsync(
|
await eventBus.PublishAsync(
|
||||||
new CreateTrackResourcesEvent(
|
new CreateTrackResourcesEvent(
|
||||||
|
|||||||
@@ -2,31 +2,33 @@ namespace Musify.Domain.ValueObjects
|
|||||||
{
|
{
|
||||||
public enum Genre
|
public enum Genre
|
||||||
{
|
{
|
||||||
Pop = 0,
|
/// <summary>Unset value (the enum default); it is not a real genre and is never stored or offered to clients.</summary>
|
||||||
Rock = 1,
|
None = 0,
|
||||||
HipHop = 2,
|
Pop = 1,
|
||||||
RnB = 3,
|
Rock = 2,
|
||||||
Jazz = 4,
|
HipHop = 3,
|
||||||
Blues = 5,
|
RnB = 4,
|
||||||
Classical = 6,
|
Jazz = 5,
|
||||||
Electronic = 7,
|
Blues = 6,
|
||||||
House = 8,
|
Classical = 7,
|
||||||
Techno = 9,
|
Electronic = 8,
|
||||||
Trance = 10,
|
House = 9,
|
||||||
Dubstep = 11,
|
Techno = 10,
|
||||||
DrumAndBass = 12,
|
Trance = 11,
|
||||||
Metal = 13,
|
Dubstep = 12,
|
||||||
Punk = 14,
|
DrumAndBass = 13,
|
||||||
Reggae = 15,
|
Metal = 14,
|
||||||
Reggaeton = 16,
|
Punk = 15,
|
||||||
Country = 17,
|
Reggae = 16,
|
||||||
Folk = 18,
|
Reggaeton = 17,
|
||||||
Indie = 19,
|
Country = 18,
|
||||||
KPop = 20,
|
Folk = 19,
|
||||||
Latin = 21,
|
Indie = 20,
|
||||||
Soul = 22,
|
KPop = 21,
|
||||||
Funk = 23,
|
Latin = 22,
|
||||||
Ambient = 24,
|
Soul = 23,
|
||||||
Lofi = 25
|
Funk = 24,
|
||||||
|
Ambient = 25,
|
||||||
|
Lofi = 26
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1084
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class GenreNone : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterDatabase()
|
||||||
|
.Annotation("Npgsql:Enum:genre", "ambient,blues,classical,country,drum_and_bass,dubstep,electronic,folk,funk,hip_hop,house,indie,jazz,k_pop,latin,lofi,metal,none,pop,punk,reggae,reggaeton,rn_b,rock,soul,techno,trance")
|
||||||
|
.OldAnnotation("Npgsql:Enum:genre", "ambient,blues,classical,country,drum_and_bass,dubstep,electronic,folk,funk,hip_hop,house,indie,jazz,k_pop,latin,lofi,metal,pop,punk,reggae,reggaeton,rn_b,rock,soul,techno,trance");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterDatabase()
|
||||||
|
.Annotation("Npgsql:Enum:genre", "ambient,blues,classical,country,drum_and_bass,dubstep,electronic,folk,funk,hip_hop,house,indie,jazz,k_pop,latin,lofi,metal,pop,punk,reggae,reggaeton,rn_b,rock,soul,techno,trance")
|
||||||
|
.OldAnnotation("Npgsql:Enum:genre", "ambient,blues,classical,country,drum_and_bass,dubstep,electronic,folk,funk,hip_hop,house,indie,jazz,k_pop,latin,lofi,metal,none,pop,punk,reggae,reggaeton,rn_b,rock,soul,techno,trance");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
.HasAnnotation("ProductVersion", "10.0.12")
|
.HasAnnotation("ProductVersion", "10.0.12")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "genre", new[] { "ambient", "blues", "classical", "country", "drum_and_bass", "dubstep", "electronic", "folk", "funk", "hip_hop", "house", "indie", "jazz", "k_pop", "latin", "lofi", "metal", "pop", "punk", "reggae", "reggaeton", "rn_b", "rock", "soul", "techno", "trance" });
|
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "genre", new[] { "ambient", "blues", "classical", "country", "drum_and_bass", "dubstep", "electronic", "folk", "funk", "hip_hop", "house", "indie", "jazz", "k_pop", "latin", "lofi", "metal", "none", "pop", "punk", "reggae", "reggaeton", "rn_b", "rock", "soul", "techno", "trance" });
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b =>
|
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b =>
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
namespace Musify.Api.DataTransferObjects.Albums
|
||||||
|
{
|
||||||
|
public record UpdateAlbumPictureRequest(Guid PictureIntentId);
|
||||||
|
}
|
||||||
@@ -3,6 +3,5 @@ namespace Musify.Api.DataTransferObjects.Albums
|
|||||||
public record UpdateAlbumRequest(
|
public record UpdateAlbumRequest(
|
||||||
string Title,
|
string Title,
|
||||||
string? Description,
|
string? Description,
|
||||||
int? ReleaseYear,
|
int? ReleaseYear);
|
||||||
Guid? PictureIntentId);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
namespace Musify.Api.DataTransferObjects.PlayLists
|
||||||
|
{
|
||||||
|
public record UpdatePlayListPictureRequest(Guid PictureIntentId);
|
||||||
|
}
|
||||||
@@ -3,8 +3,7 @@ using Musify.Domain.ValueObjects;
|
|||||||
namespace Musify.Api.DataTransferObjects.PlayLists
|
namespace Musify.Api.DataTransferObjects.PlayLists
|
||||||
{
|
{
|
||||||
public record UpdatePlayListRequest(
|
public record UpdatePlayListRequest(
|
||||||
string? Name,
|
string Name,
|
||||||
string? Description,
|
string? Description,
|
||||||
Guid? PictureIntentId,
|
PlayListVisibility? Visibility);
|
||||||
PlayListVisibility? Visibility = null);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,17 @@ namespace Musify.Api.Endpoints
|
|||||||
.Produces(StatusCodes.Status403Forbidden)
|
.Produces(StatusCodes.Status403Forbidden)
|
||||||
.Produces(StatusCodes.Status404NotFound);
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
|
group.MapPut("/{id:guid}/picture", UpdateAlbumPicture)
|
||||||
|
.WithName("UpdateAlbumPicture")
|
||||||
|
.WithSummary("Replace an album picture with an uploaded image")
|
||||||
|
.AddEndpointFilter<ValidationFilter<UpdateAlbumPictureRequest>>()
|
||||||
|
.RequireAuthorization()
|
||||||
|
.Produces<AlbumApplicationResponse>()
|
||||||
|
.ProducesValidationProblem()
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.Produces(StatusCodes.Status403Forbidden)
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
group.MapDelete("/{id:guid}", DeleteAlbum)
|
group.MapDelete("/{id:guid}", DeleteAlbum)
|
||||||
.WithName("DeleteAlbum")
|
.WithName("DeleteAlbum")
|
||||||
.WithSummary("Delete an album")
|
.WithSummary("Delete an album")
|
||||||
@@ -229,7 +240,21 @@ namespace Musify.Api.Endpoints
|
|||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var result = await mediator.Send(
|
var result = await mediator.Send(
|
||||||
new UpdateAlbumCommand(currentUser.RequiredId, id, request.Title, request.Description, request.ReleaseYear, request.PictureIntentId),
|
new UpdateAlbumCommand(currentUser.RequiredId, id, request.Title, request.Description, request.ReleaseYear),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return result.ToHttpResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> UpdateAlbumPicture(
|
||||||
|
IMediator mediator,
|
||||||
|
CurrentUser currentUser,
|
||||||
|
Guid id,
|
||||||
|
UpdateAlbumPictureRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(
|
||||||
|
new UpdateAlbumPictureCommand(currentUser.RequiredId, id, request.PictureIntentId),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
return result.ToHttpResult();
|
return result.ToHttpResult();
|
||||||
|
|||||||
@@ -68,6 +68,17 @@ namespace Musify.Api.Endpoints
|
|||||||
.Produces(StatusCodes.Status403Forbidden)
|
.Produces(StatusCodes.Status403Forbidden)
|
||||||
.Produces(StatusCodes.Status404NotFound);
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
|
group.MapPut("/{id:guid}/picture", UpdatePlayListPicture)
|
||||||
|
.WithName("UpdatePlayListPicture")
|
||||||
|
.WithSummary("Replace a playlist picture with an uploaded image")
|
||||||
|
.AddEndpointFilter<ValidationFilter<UpdatePlayListPictureRequest>>()
|
||||||
|
.RequireAuthorization()
|
||||||
|
.Produces<PlayListApplicationResponse>()
|
||||||
|
.ProducesValidationProblem()
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.Produces(StatusCodes.Status403Forbidden)
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
group.MapDelete("/{id:guid}", DeletePlayList)
|
group.MapDelete("/{id:guid}", DeletePlayList)
|
||||||
.WithName("DeletePlayList")
|
.WithName("DeletePlayList")
|
||||||
.WithSummary("Delete a playlist")
|
.WithSummary("Delete a playlist")
|
||||||
@@ -185,7 +196,21 @@ namespace Musify.Api.Endpoints
|
|||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var result = await mediator.Send(
|
var result = await mediator.Send(
|
||||||
new UpdatePlayListCommand(currentUser.RequiredId, id, request.Name, request.Description, request.PictureIntentId, request.Visibility),
|
new UpdatePlayListCommand(currentUser.RequiredId, id, request.Name, request.Description, request.Visibility!.Value),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return result.ToHttpResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> UpdatePlayListPicture(
|
||||||
|
IMediator mediator,
|
||||||
|
CurrentUser currentUser,
|
||||||
|
Guid id,
|
||||||
|
UpdatePlayListPictureRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(
|
||||||
|
new UpdatePlayListPictureCommand(currentUser.RequiredId, id, request.PictureIntentId),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
return result.ToHttpResult();
|
return result.ToHttpResult();
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace Musify.Api.Models
|
|||||||
{
|
{
|
||||||
public static bool TryParse(string? value, out GenreParameter result)
|
public static bool TryParse(string? value, out GenreParameter result)
|
||||||
{
|
{
|
||||||
if (Enum.TryParse<Genre>(value, ignoreCase: true, out var genre) && Enum.IsDefined(genre))
|
if (Enum.TryParse<Genre>(value, ignoreCase: true, out var genre) && genre != Genre.None && Enum.IsDefined(genre))
|
||||||
{
|
{
|
||||||
result = new GenreParameter(genre);
|
result = new GenreParameter(genre);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using Musify.Api.DataTransferObjects.Albums;
|
||||||
|
|
||||||
|
namespace Musify.Api.Validators.Albums
|
||||||
|
{
|
||||||
|
public sealed class UpdateAlbumPictureRequestValidator : AbstractValidator<UpdateAlbumPictureRequest>
|
||||||
|
{
|
||||||
|
public UpdateAlbumPictureRequestValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.PictureIntentId)
|
||||||
|
.NotEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using Musify.Api.DataTransferObjects.PlayLists;
|
||||||
|
|
||||||
|
namespace Musify.Api.Validators.PlayLists
|
||||||
|
{
|
||||||
|
public sealed class UpdatePlayListPictureRequestValidator : AbstractValidator<UpdatePlayListPictureRequest>
|
||||||
|
{
|
||||||
|
public UpdatePlayListPictureRequestValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.PictureIntentId)
|
||||||
|
.NotEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,16 +9,14 @@ namespace Musify.Api.Validators.PlayLists
|
|||||||
{
|
{
|
||||||
RuleFor(x => x.Name)
|
RuleFor(x => x.Name)
|
||||||
.NotEmpty()
|
.NotEmpty()
|
||||||
.MaximumLength(Limits.PlayListName)
|
.MaximumLength(Limits.PlayListName);
|
||||||
.When(x => x.Name != null);
|
|
||||||
|
|
||||||
RuleFor(x => x.Description)
|
RuleFor(x => x.Description)
|
||||||
.MaximumLength(Limits.Description)
|
.MaximumLength(Limits.Description);
|
||||||
.When(x => x.Description != null);
|
|
||||||
|
|
||||||
RuleFor(x => x.Visibility)
|
RuleFor(x => x.Visibility)
|
||||||
.IsInEnum()
|
.NotNull()
|
||||||
.When(x => x.Visibility != null);
|
.IsInEnum();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ namespace Musify.Api.Validators.Tracks
|
|||||||
.NotEmpty()
|
.NotEmpty()
|
||||||
.WithMessage("At least one tag is required.");
|
.WithMessage("At least one tag is required.");
|
||||||
|
|
||||||
|
RuleFor(x => x.Tags)
|
||||||
|
.Must(tags => !tags.Contains(Genre.None))
|
||||||
|
.When(x => x.Tags is { Count: > 0 })
|
||||||
|
.WithMessage("'None' is not a valid tag.");
|
||||||
|
|
||||||
RuleFor(x => x.Tags)
|
RuleFor(x => x.Tags)
|
||||||
.Must(tags => GenreCompatibility.FindConflicts(tags).Count == 0)
|
.Must(tags => GenreCompatibility.FindConflicts(tags).Count == 0)
|
||||||
.When(x => x.Tags is { Count: > 0 })
|
.When(x => x.Tags is { Count: > 0 })
|
||||||
|
|||||||
@@ -111,6 +111,37 @@ namespace Musify.Api.Tests.Endpoints
|
|||||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateAlbumPicture_NotTheOwner_ReturnsForbidden()
|
||||||
|
{
|
||||||
|
var owner = await CreateUserAsync();
|
||||||
|
var stranger = await CreateUserAsync();
|
||||||
|
var ownerClient = fixture.CreateAuthenticatedClient(owner);
|
||||||
|
var pictureIntentId = await CreatePictureIntentAsync(ownerClient);
|
||||||
|
var created = await (await ownerClient.PostAsJsonAsync("/albums", new CreateAlbumRequest("Owned", null, null, pictureIntentId), TestContext.Current.CancellationToken))
|
||||||
|
.Content.ReadFromJsonAsync<AlbumApplicationResponse>(TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
var response = await fixture.CreateAuthenticatedClient(stranger).PutAsJsonAsync(
|
||||||
|
$"/albums/{created!.Id}/picture", new UpdateAlbumPictureRequest(Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateAlbumPicture_EmptyIntent_ReturnsBadRequest()
|
||||||
|
{
|
||||||
|
var owner = await CreateUserAsync();
|
||||||
|
var ownerClient = fixture.CreateAuthenticatedClient(owner);
|
||||||
|
var pictureIntentId = await CreatePictureIntentAsync(ownerClient);
|
||||||
|
var created = await (await ownerClient.PostAsJsonAsync("/albums", new CreateAlbumRequest("Owned", null, null, pictureIntentId), TestContext.Current.CancellationToken))
|
||||||
|
.Content.ReadFromJsonAsync<AlbumApplicationResponse>(TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
var response = await ownerClient.PutAsJsonAsync(
|
||||||
|
$"/albums/{created!.Id}/picture", new UpdateAlbumPictureRequest(Guid.Empty), TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DeleteAlbum_Owner_ReturnsNoContentAndAlbumIsGone()
|
public async Task DeleteAlbum_Owner_ReturnsNoContentAndAlbumIsGone()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ namespace Musify.Api.Tests.Endpoints
|
|||||||
|
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
var body = await response.Content.ReadFromJsonAsync<List<AvailableGenreResponse>>(JsonOptions, TestContext.Current.CancellationToken);
|
var body = await response.Content.ReadFromJsonAsync<List<AvailableGenreResponse>>(JsonOptions, TestContext.Current.CancellationToken);
|
||||||
Assert.Equal(Enum.GetValues<Genre>().Length, body!.Count);
|
Assert.Equal(Enum.GetValues<Genre>().Count(genre => genre != Genre.None), body!.Count);
|
||||||
Assert.Contains(Genre.Lofi, body.Single(g => g.Genre == Genre.Metal).IncompatibleWith);
|
Assert.Contains(Genre.Lofi, body.Single(g => g.Genre == Genre.Metal).IncompatibleWith);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ namespace Musify.Api.Tests.Endpoints
|
|||||||
var playList = await CreatePlayListAsync(owner, PlayListVisibility.Public);
|
var playList = await CreatePlayListAsync(owner, PlayListVisibility.Public);
|
||||||
|
|
||||||
var response = await fixture.CreateAuthenticatedClient(stranger).PutAsJsonAsync(
|
var response = await fixture.CreateAuthenticatedClient(stranger).PutAsJsonAsync(
|
||||||
$"/playlists/{playList.Id}", new UpdatePlayListRequest("Hijacked", null, null), Ct);
|
$"/playlists/{playList.Id}", new UpdatePlayListRequest("Hijacked", null, PlayListVisibility.Public), Ct);
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||||
}
|
}
|
||||||
@@ -120,13 +120,50 @@ namespace Musify.Api.Tests.Endpoints
|
|||||||
var playList = await CreatePlayListAsync(owner, PlayListVisibility.Private);
|
var playList = await CreatePlayListAsync(owner, PlayListVisibility.Private);
|
||||||
|
|
||||||
var response = await fixture.CreateAuthenticatedClient(owner).PutAsJsonAsync(
|
var response = await fixture.CreateAuthenticatedClient(owner).PutAsJsonAsync(
|
||||||
$"/playlists/{playList.Id}", new UpdatePlayListRequest("Renamed", null, null), Ct);
|
$"/playlists/{playList.Id}", new UpdatePlayListRequest("Renamed", null, PlayListVisibility.Private), Ct);
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
var body = await response.Content.ReadFromJsonAsync<PlayListApplicationResponse>(JsonOptions, Ct);
|
var body = await response.Content.ReadFromJsonAsync<PlayListApplicationResponse>(JsonOptions, Ct);
|
||||||
Assert.Equal("Renamed", body!.Name);
|
Assert.Equal("Renamed", body!.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdatePlayList_MissingVisibility_ReturnsBadRequest()
|
||||||
|
{
|
||||||
|
var owner = await fixture.SeedUserAsync();
|
||||||
|
var playList = await CreatePlayListAsync(owner, PlayListVisibility.Public);
|
||||||
|
|
||||||
|
var response = await fixture.CreateAuthenticatedClient(owner).PutAsJsonAsync(
|
||||||
|
$"/playlists/{playList.Id}", new UpdatePlayListRequest("Renamed", null, null), Ct);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdatePlayListPicture_NotTheOwner_ReturnsForbidden()
|
||||||
|
{
|
||||||
|
var owner = await fixture.SeedUserAsync();
|
||||||
|
var stranger = await fixture.SeedUserAsync();
|
||||||
|
var playList = await CreatePlayListAsync(owner, PlayListVisibility.Public);
|
||||||
|
|
||||||
|
var response = await fixture.CreateAuthenticatedClient(stranger).PutAsJsonAsync(
|
||||||
|
$"/playlists/{playList.Id}/picture", new UpdatePlayListPictureRequest(Guid.NewGuid()), Ct);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdatePlayListPicture_EmptyIntent_ReturnsBadRequest()
|
||||||
|
{
|
||||||
|
var owner = await fixture.SeedUserAsync();
|
||||||
|
var playList = await CreatePlayListAsync(owner, PlayListVisibility.Public);
|
||||||
|
|
||||||
|
var response = await fixture.CreateAuthenticatedClient(owner).PutAsJsonAsync(
|
||||||
|
$"/playlists/{playList.Id}/picture", new UpdatePlayListPictureRequest(Guid.Empty), Ct);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetPlayListById_NotAGuid_ReturnsNotFound()
|
public async Task GetPlayListById_NotAGuid_ReturnsNotFound()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,24 +1,15 @@
|
|||||||
using ErrorOr;
|
using ErrorOr;
|
||||||
using Musify.Application.Albums;
|
using Musify.Application.Albums;
|
||||||
using Musify.Application.Contracts;
|
|
||||||
using Musify.Application.Services;
|
|
||||||
using Musify.Application.Tests.TestSupport;
|
using Musify.Application.Tests.TestSupport;
|
||||||
using NSubstitute;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Musify.Application.Tests.Albums
|
namespace Musify.Application.Tests.Albums
|
||||||
{
|
{
|
||||||
public sealed class UpdateAlbumCommandHandlerTests : HandlerTestBase
|
public sealed class UpdateAlbumCommandHandlerTests : HandlerTestBase
|
||||||
{
|
{
|
||||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
|
||||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
|
||||||
|
|
||||||
private UpdateAlbumCommandHandler CreateHandler() => new(
|
private UpdateAlbumCommandHandler CreateHandler() => new(
|
||||||
eventBus,
|
|
||||||
Database,
|
Database,
|
||||||
new UploadIntentValidator(Database, storageService, TestConfigurations.UploadIntent()),
|
NoOpLogger<UpdateAlbumCommandHandler>());
|
||||||
NoOpLogger<UpdateAlbumCommandHandler>(),
|
|
||||||
TestConfigurations.Album());
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Handle_Owner_UpdatesTitleDescriptionAndReleaseYear()
|
public async Task Handle_Owner_UpdatesTitleDescriptionAndReleaseYear()
|
||||||
@@ -27,7 +18,7 @@ namespace Musify.Application.Tests.Albums
|
|||||||
var album = TestEntities.Album(owner.Id, title: "Old Title", description: "Old", releaseYear: 2020);
|
var album = TestEntities.Album(owner.Id, title: "Old Title", description: "Old", releaseYear: 2020);
|
||||||
await SeedAsync(owner, album);
|
await SeedAsync(owner, album);
|
||||||
|
|
||||||
var command = new UpdateAlbumCommand(owner.Id, album.Id, " New Title ", "New description", 2025, null);
|
var command = new UpdateAlbumCommand(owner.Id, album.Id, " New Title ", "New description", 2025);
|
||||||
|
|
||||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
@@ -42,32 +33,22 @@ namespace Musify.Application.Tests.Albums
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Handle_ValidPictureIntent_UpdatesPictureAndPublishesSourceEvent()
|
public async Task Handle_BlankDescription_ClearsIt()
|
||||||
{
|
{
|
||||||
storageService
|
|
||||||
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
|
||||||
.Returns(new ObjectMetaData("image/webp", 1024));
|
|
||||||
|
|
||||||
var owner = TestEntities.User();
|
var owner = TestEntities.User();
|
||||||
var album = TestEntities.Album(owner.Id, title: "Old Title");
|
var album = TestEntities.Album(owner.Id, title: "Title", description: "Old");
|
||||||
var intent = TestEntities.UploadIntent(owner.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.AlbumPicture, objectName: "new-cover.webp");
|
await SeedAsync(owner, album);
|
||||||
await SeedAsync(owner, album, intent);
|
|
||||||
|
|
||||||
var command = new UpdateAlbumCommand(owner.Id, album.Id, "Old Title", null, null, intent.Id);
|
var result = await CreateHandler().Handle(new UpdateAlbumCommand(owner.Id, album.Id, "Title", " ", null), TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
|
||||||
|
|
||||||
Assert.False(result.IsError);
|
Assert.False(result.IsError);
|
||||||
var stored = await Database.Albums.FindAsync([album.Id], TestContext.Current.CancellationToken);
|
Assert.Null(result.Value.Description);
|
||||||
Assert.NotNull(stored);
|
|
||||||
Assert.Equal("new-cover.webp", stored.Pictures?.OriginalName);
|
|
||||||
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.UpdateAlbumPictureSourceEvent>(), Arg.Any<CancellationToken>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Handle_AlbumMissing_ReturnsNotFound()
|
public async Task Handle_AlbumMissing_ReturnsNotFound()
|
||||||
{
|
{
|
||||||
var command = new UpdateAlbumCommand(1, Guid.NewGuid(), "Title", null, null, null);
|
var command = new UpdateAlbumCommand(1, Guid.NewGuid(), "Title", null, null);
|
||||||
|
|
||||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
@@ -82,7 +63,7 @@ namespace Musify.Application.Tests.Albums
|
|||||||
var album = TestEntities.Album(owner.Id);
|
var album = TestEntities.Album(owner.Id);
|
||||||
await SeedAsync(owner, stranger, album);
|
await SeedAsync(owner, stranger, album);
|
||||||
|
|
||||||
var command = new UpdateAlbumCommand(stranger.Id, album.Id, "Hijacked", null, null, null);
|
var command = new UpdateAlbumCommand(stranger.Id, album.Id, "Hijacked", null, null);
|
||||||
|
|
||||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
|||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
using ErrorOr;
|
||||||
|
using Musify.Application.Albums;
|
||||||
|
using Musify.Application.Contracts;
|
||||||
|
using Musify.Application.Services;
|
||||||
|
using Musify.Application.Tests.TestSupport;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Musify.Application.Tests.Albums
|
||||||
|
{
|
||||||
|
public sealed class UpdateAlbumPictureCommandHandlerTests : HandlerTestBase
|
||||||
|
{
|
||||||
|
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||||
|
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||||
|
|
||||||
|
private UpdateAlbumPictureCommandHandler CreateHandler() => new(
|
||||||
|
eventBus,
|
||||||
|
Database,
|
||||||
|
new UploadIntentValidator(Database, storageService, TestConfigurations.UploadIntent()),
|
||||||
|
NoOpLogger<UpdateAlbumPictureCommandHandler>(),
|
||||||
|
TestConfigurations.Album());
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ValidPictureIntent_UpdatesPictureAndPublishesSourceEvent()
|
||||||
|
{
|
||||||
|
storageService
|
||||||
|
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(new ObjectMetaData("image/webp", 1024));
|
||||||
|
|
||||||
|
var owner = TestEntities.User();
|
||||||
|
var album = TestEntities.Album(owner.Id, title: "Keep Title", description: "Keep description", releaseYear: 2020);
|
||||||
|
var intent = TestEntities.UploadIntent(owner.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.AlbumPicture, objectName: "new-cover.webp");
|
||||||
|
await SeedAsync(owner, album, intent);
|
||||||
|
|
||||||
|
var result = await CreateHandler().Handle(new UpdateAlbumPictureCommand(owner.Id, album.Id, intent.Id), TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
Assert.False(result.IsError);
|
||||||
|
Assert.Equal("Keep Title", result.Value.Title);
|
||||||
|
Assert.Equal("Keep description", result.Value.Description);
|
||||||
|
Assert.Equal(2020, result.Value.ReleaseYear);
|
||||||
|
var stored = await Database.Albums.FindAsync([album.Id], TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(stored);
|
||||||
|
Assert.Equal("new-cover.webp", stored.Pictures?.OriginalName);
|
||||||
|
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.UpdateAlbumPictureSourceEvent>(), Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_AlbumMissing_ReturnsNotFound()
|
||||||
|
{
|
||||||
|
var result = await CreateHandler().Handle(new UpdateAlbumPictureCommand(1, Guid.NewGuid(), Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_NotOwner_ReturnsForbidden()
|
||||||
|
{
|
||||||
|
var owner = TestEntities.User(1, "owner");
|
||||||
|
var stranger = TestEntities.User(2, "stranger");
|
||||||
|
var album = TestEntities.Album(owner.Id);
|
||||||
|
await SeedAsync(owner, stranger, album);
|
||||||
|
|
||||||
|
var result = await CreateHandler().Handle(new UpdateAlbumPictureCommand(stranger.Id, album.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
Assert.Equal(ErrorType.Forbidden, result.FirstError.Type);
|
||||||
|
await eventBus.DidNotReceive().PublishAsync(Arg.Any<Musify.Application.Events.UpdateAlbumPictureSourceEvent>(), Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -11,11 +11,11 @@ namespace Musify.Application.Tests.Genres
|
|||||||
await new GetAvailableGenresQueryHandler().Handle(new GetAvailableGenresQuery(), TestContext.Current.CancellationToken);
|
await new GetAvailableGenresQueryHandler().Handle(new GetAvailableGenresQuery(), TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Handle_ReturnsEveryGenre()
|
public async Task Handle_ReturnsEveryGenreExceptNone()
|
||||||
{
|
{
|
||||||
var result = await RunAsync();
|
var result = await RunAsync();
|
||||||
|
|
||||||
Assert.Equal(Enum.GetValues<Genre>(), result.Select(r => r.Genre));
|
Assert.Equal(Enum.GetValues<Genre>().Where(genre => genre != Genre.None), result.Select(r => r.Genre));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
+13
-46
@@ -1,24 +1,15 @@
|
|||||||
using ErrorOr;
|
using ErrorOr;
|
||||||
using Musify.Application.Contracts;
|
|
||||||
using Musify.Application.PlayLists;
|
using Musify.Application.PlayLists;
|
||||||
using Musify.Application.Services;
|
|
||||||
using Musify.Application.Tests.TestSupport;
|
using Musify.Application.Tests.TestSupport;
|
||||||
using NSubstitute;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Musify.Application.Tests.PlayLists
|
namespace Musify.Application.Tests.PlayLists
|
||||||
{
|
{
|
||||||
public sealed class UpdatePlayListCommandHandlerTests : HandlerTestBase
|
public sealed class UpdatePlayListCommandHandlerTests : HandlerTestBase
|
||||||
{
|
{
|
||||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
|
||||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
|
||||||
|
|
||||||
private UpdatePlayListCommandHandler CreateHandler() => new(
|
private UpdatePlayListCommandHandler CreateHandler() => new(
|
||||||
eventBus,
|
|
||||||
Database,
|
Database,
|
||||||
new UploadIntentValidator(Database, storageService, TestConfigurations.UploadIntent()),
|
NoOpLogger<UpdatePlayListCommandHandler>());
|
||||||
NoOpLogger<UpdatePlayListCommandHandler>(),
|
|
||||||
TestConfigurations.PlayList());
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Handle_Owner_RenamesPlayList()
|
public async Task Handle_Owner_RenamesPlayList()
|
||||||
@@ -27,7 +18,7 @@ namespace Musify.Application.Tests.PlayLists
|
|||||||
var playList = TestEntities.PlayList(owner.Id, "Old Name");
|
var playList = TestEntities.PlayList(owner.Id, "Old Name");
|
||||||
await SeedAsync(owner, playList);
|
await SeedAsync(owner, playList);
|
||||||
|
|
||||||
var command = new UpdatePlayListCommand(owner.Id, playList.Id, "New Name", null, null);
|
var command = new UpdatePlayListCommand(owner.Id, playList.Id, "New Name", null, Musify.Domain.ValueObjects.PlayListVisibility.Private);
|
||||||
|
|
||||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
@@ -38,7 +29,7 @@ namespace Musify.Application.Tests.PlayLists
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||||
{
|
{
|
||||||
var command = new UpdatePlayListCommand(1, Guid.NewGuid(), "New Name", null, null);
|
var command = new UpdatePlayListCommand(1, Guid.NewGuid(), "New Name", null, Musify.Domain.ValueObjects.PlayListVisibility.Private);
|
||||||
|
|
||||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
@@ -53,7 +44,7 @@ namespace Musify.Application.Tests.PlayLists
|
|||||||
var playList = TestEntities.PlayList(owner.Id);
|
var playList = TestEntities.PlayList(owner.Id);
|
||||||
await SeedAsync(owner, stranger, playList);
|
await SeedAsync(owner, stranger, playList);
|
||||||
|
|
||||||
var command = new UpdatePlayListCommand(stranger.Id, playList.Id, "Hijacked", null, null);
|
var command = new UpdatePlayListCommand(stranger.Id, playList.Id, "Hijacked", null, Musify.Domain.ValueObjects.PlayListVisibility.Private);
|
||||||
|
|
||||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
@@ -67,7 +58,7 @@ namespace Musify.Application.Tests.PlayLists
|
|||||||
var playList = TestEntities.PlayList(owner.Id);
|
var playList = TestEntities.PlayList(owner.Id);
|
||||||
await SeedAsync(owner, playList);
|
await SeedAsync(owner, playList);
|
||||||
|
|
||||||
var command = new UpdatePlayListCommand(owner.Id, playList.Id, null, null, null, Musify.Domain.ValueObjects.PlayListVisibility.Public);
|
var command = new UpdatePlayListCommand(owner.Id, playList.Id, "Same Name", null, Musify.Domain.ValueObjects.PlayListVisibility.Public);
|
||||||
|
|
||||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
@@ -76,59 +67,35 @@ namespace Musify.Application.Tests.PlayLists
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Handle_NoVisibilitySpecified_LeavesItUnchanged()
|
public async Task Handle_ReplacesNameDescriptionAndVisibility()
|
||||||
{
|
{
|
||||||
var owner = TestEntities.User();
|
var owner = TestEntities.User();
|
||||||
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlayListVisibility.Public);
|
var playList = TestEntities.PlayList(owner.Id, "Original Name", "Original description", visibility: Musify.Domain.ValueObjects.PlayListVisibility.Public);
|
||||||
await SeedAsync(owner, playList);
|
await SeedAsync(owner, playList);
|
||||||
|
|
||||||
var command = new UpdatePlayListCommand(owner.Id, playList.Id, "New Name", null, null);
|
var command = new UpdatePlayListCommand(owner.Id, playList.Id, " New Name ", "New description", Musify.Domain.ValueObjects.PlayListVisibility.Private);
|
||||||
|
|
||||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
Assert.False(result.IsError);
|
Assert.False(result.IsError);
|
||||||
Assert.Equal(Musify.Domain.ValueObjects.PlayListVisibility.Public, result.Value.Visibility);
|
Assert.Equal("New Name", result.Value.Name);
|
||||||
|
Assert.Equal("New description", result.Value.Description);
|
||||||
|
Assert.Equal(Musify.Domain.ValueObjects.PlayListVisibility.Private, result.Value.Visibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Handle_BlankName_LeavesItUnchangedAndBlankDescriptionClearsIt()
|
public async Task Handle_MissingDescription_ClearsIt()
|
||||||
{
|
{
|
||||||
var owner = TestEntities.User();
|
var owner = TestEntities.User();
|
||||||
var playList = TestEntities.PlayList(owner.Id, "Original Name", "Original description");
|
var playList = TestEntities.PlayList(owner.Id, "Original Name", "Original description");
|
||||||
await SeedAsync(owner, playList);
|
await SeedAsync(owner, playList);
|
||||||
|
|
||||||
var command = new UpdatePlayListCommand(owner.Id, playList.Id, " ", " ", null);
|
var command = new UpdatePlayListCommand(owner.Id, playList.Id, "Original Name", null, Musify.Domain.ValueObjects.PlayListVisibility.Private);
|
||||||
|
|
||||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
Assert.False(result.IsError);
|
Assert.False(result.IsError);
|
||||||
Assert.Equal("Original Name", result.Value.Name);
|
|
||||||
Assert.Null(result.Value.Description);
|
Assert.Null(result.Value.Description);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Handle_PlayListWithoutPictures_ValidPictureIntent_SetsPictureAndPublishesSourceEvent()
|
|
||||||
{
|
|
||||||
storageService
|
|
||||||
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
|
||||||
.Returns(new ObjectMetaData("image/webp", 1024));
|
|
||||||
|
|
||||||
var owner = TestEntities.User();
|
|
||||||
var playList = TestEntities.PlayList(owner.Id);
|
|
||||||
playList.Pictures = null;
|
|
||||||
var intent = TestEntities.UploadIntent(owner.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.PlayListPicture, objectName: "new-cover.webp");
|
|
||||||
await SeedAsync(owner, playList, intent);
|
|
||||||
|
|
||||||
var command = new UpdatePlayListCommand(owner.Id, playList.Id, null, null, intent.Id);
|
|
||||||
|
|
||||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
|
||||||
|
|
||||||
Assert.False(result.IsError);
|
|
||||||
Assert.Null(result.Value.SmallImageKeyName);
|
|
||||||
var stored = await Database.PlayLists.FindAsync([playList.Id], TestContext.Current.CancellationToken);
|
|
||||||
Assert.NotNull(stored);
|
|
||||||
Assert.Equal("new-cover.webp", stored.Pictures?.OriginalName);
|
|
||||||
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.UpdatePlayListPictureSourceEvent>(), Arg.Any<CancellationToken>());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
using ErrorOr;
|
||||||
|
using Musify.Application.Contracts;
|
||||||
|
using Musify.Application.PlayLists;
|
||||||
|
using Musify.Application.Services;
|
||||||
|
using Musify.Application.Tests.TestSupport;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Musify.Application.Tests.PlayLists
|
||||||
|
{
|
||||||
|
public sealed class UpdatePlayListPictureCommandHandlerTests : HandlerTestBase
|
||||||
|
{
|
||||||
|
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||||
|
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||||
|
|
||||||
|
private UpdatePlayListPictureCommandHandler CreateHandler() => new(
|
||||||
|
eventBus,
|
||||||
|
Database,
|
||||||
|
new UploadIntentValidator(Database, storageService, TestConfigurations.UploadIntent()),
|
||||||
|
NoOpLogger<UpdatePlayListPictureCommandHandler>(),
|
||||||
|
TestConfigurations.PlayList());
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ValidPictureIntent_SetsPictureAndPublishesSourceEvent()
|
||||||
|
{
|
||||||
|
storageService
|
||||||
|
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(new ObjectMetaData("image/webp", 1024));
|
||||||
|
|
||||||
|
var owner = TestEntities.User();
|
||||||
|
var playList = TestEntities.PlayList(owner.Id, "Keep Name", "Keep description");
|
||||||
|
playList.Pictures = null;
|
||||||
|
var intent = TestEntities.UploadIntent(owner.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.PlayListPicture, objectName: "new-cover.webp");
|
||||||
|
await SeedAsync(owner, playList, intent);
|
||||||
|
|
||||||
|
var result = await CreateHandler().Handle(new UpdatePlayListPictureCommand(owner.Id, playList.Id, intent.Id), TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
Assert.False(result.IsError);
|
||||||
|
Assert.Equal("Keep Name", result.Value.Name);
|
||||||
|
Assert.Equal("Keep description", result.Value.Description);
|
||||||
|
var stored = await Database.PlayLists.FindAsync([playList.Id], TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(stored);
|
||||||
|
Assert.Equal("new-cover.webp", stored.Pictures?.OriginalName);
|
||||||
|
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.UpdatePlayListPictureSourceEvent>(), Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||||
|
{
|
||||||
|
var result = await CreateHandler().Handle(new UpdatePlayListPictureCommand(1, Guid.NewGuid(), Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_NotOwner_ReturnsForbidden()
|
||||||
|
{
|
||||||
|
var owner = TestEntities.User(1, "owner");
|
||||||
|
var stranger = TestEntities.User(2, "stranger");
|
||||||
|
var playList = TestEntities.PlayList(owner.Id);
|
||||||
|
await SeedAsync(owner, stranger, playList);
|
||||||
|
|
||||||
|
var result = await CreateHandler().Handle(new UpdatePlayListPictureCommand(stranger.Id, playList.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
Assert.Equal(ErrorType.Forbidden, result.FirstError.Type);
|
||||||
|
await eventBus.DidNotReceive().PublishAsync(Arg.Any<Musify.Application.Events.UpdatePlayListPictureSourceEvent>(), Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+155
-5
@@ -75,6 +75,23 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/albums/{id}/picture": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
/** Replace an album picture with an uploaded image */
|
||||||
|
put: operations["UpdateAlbumPicture"];
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/albums/{id}/tracks": {
|
"/albums/{id}/tracks": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -282,6 +299,23 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/playlists/{id}/picture": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
/** Replace a playlist picture with an uploaded image */
|
||||||
|
put: operations["UpdatePlayListPicture"];
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/playlists/users/{userId}": {
|
"/playlists/users/{userId}": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -958,20 +992,24 @@ export interface components {
|
|||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
expiresInSeconds: number | string;
|
expiresInSeconds: number | string;
|
||||||
};
|
};
|
||||||
|
UpdateAlbumPictureRequest: {
|
||||||
|
/** Format: uuid */
|
||||||
|
pictureIntentId: string;
|
||||||
|
};
|
||||||
UpdateAlbumRequest: {
|
UpdateAlbumRequest: {
|
||||||
title: string;
|
title: string;
|
||||||
description: null | string;
|
description: null | string;
|
||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
releaseYear: null | number | string;
|
releaseYear: null | number | string;
|
||||||
|
};
|
||||||
|
UpdatePlayListPictureRequest: {
|
||||||
/** Format: uuid */
|
/** Format: uuid */
|
||||||
pictureIntentId: null | string;
|
pictureIntentId: string;
|
||||||
};
|
};
|
||||||
UpdatePlayListRequest: {
|
UpdatePlayListRequest: {
|
||||||
name: null | string;
|
name: string;
|
||||||
description: null | string;
|
description: null | string;
|
||||||
/** Format: uuid */
|
visibility: null | components["schemas"]["PlayListVisibility"];
|
||||||
pictureIntentId: null | string;
|
|
||||||
visibility?: null | components["schemas"]["PlayListVisibility"];
|
|
||||||
};
|
};
|
||||||
UserApplicationResponse: {
|
UserApplicationResponse: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -1172,6 +1210,62 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
UpdateAlbumPicture: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["UpdateAlbumPictureRequest"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description OK */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["AlbumApplicationResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Bad Request */
|
||||||
|
400: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/problem+json": components["schemas"]["HttpValidationProblemDetails"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Unauthorized */
|
||||||
|
401: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
/** @description Forbidden */
|
||||||
|
403: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
/** @description Not Found */
|
||||||
|
404: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
DeleteAlbum: {
|
DeleteAlbum: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -1761,6 +1855,62 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
UpdatePlayListPicture: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["UpdatePlayListPictureRequest"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description OK */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["PlayListApplicationResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Bad Request */
|
||||||
|
400: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/problem+json": components["schemas"]["HttpValidationProblemDetails"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Unauthorized */
|
||||||
|
401: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
/** @description Forbidden */
|
||||||
|
403: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
/** @description Not Found */
|
||||||
|
404: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
DeletePlayList: {
|
DeletePlayList: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|||||||
@@ -71,21 +71,34 @@ export const actions: Actions = {
|
|||||||
const parsed = parseAlbumForm(form);
|
const parsed = parseAlbumForm(form);
|
||||||
if ('failMessage' in parsed) return fail(400, { message: parsed.failMessage });
|
if ('failMessage' in parsed) return fail(400, { message: parsed.failMessage });
|
||||||
|
|
||||||
const cover = await uploadOptionalCover(api, '/albums/upload-picture', formFile(form, 'cover'));
|
|
||||||
if ('failMessage' in cover) {
|
|
||||||
return fail(502, { message: cover.failMessage, detail: cover.detail });
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await api.PUT('/albums/{id}', {
|
const result = await api.PUT('/albums/{id}', {
|
||||||
params: { path: { id: params.id } },
|
params: { path: { id: params.id } },
|
||||||
body: {
|
body: {
|
||||||
title: parsed.body.title,
|
title: parsed.body.title,
|
||||||
description: parsed.body.description,
|
description: parsed.body.description,
|
||||||
releaseYear: parsed.body.releaseYear,
|
releaseYear: parsed.body.releaseYear
|
||||||
pictureIntentId: cover.intentId
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return failOnError(result, 'No se pudo actualizar el álbum.') ?? { edited: true };
|
const failure = failOnError(result, 'No se pudo actualizar el álbum.');
|
||||||
|
if (failure) return failure;
|
||||||
|
|
||||||
|
const coverFile = formFile(form, 'cover');
|
||||||
|
if (coverFile) {
|
||||||
|
const cover = await uploadOptionalCover(api, '/albums/upload-picture', coverFile);
|
||||||
|
if ('failMessage' in cover) {
|
||||||
|
return fail(502, { message: cover.failMessage, detail: cover.detail });
|
||||||
|
}
|
||||||
|
if (cover.intentId) {
|
||||||
|
const pictureResult = await api.PUT('/albums/{id}/picture', {
|
||||||
|
params: { path: { id: params.id } },
|
||||||
|
body: { pictureIntentId: cover.intentId }
|
||||||
|
});
|
||||||
|
const pictureFailure = failOnError(pictureResult, 'No se pudo actualizar la portada.');
|
||||||
|
if (pictureFailure) return pictureFailure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { edited: true };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
delete: authedAction(async ({ api, params }) => {
|
delete: authedAction(async ({ api, params }) => {
|
||||||
|
|||||||
@@ -52,21 +52,29 @@ export const actions: Actions = {
|
|||||||
if ('failMessage' in parsed) return fail(400, { message: parsed.failMessage });
|
if ('failMessage' in parsed) return fail(400, { message: parsed.failMessage });
|
||||||
const { name, description, visibility, cover } = parsed.body;
|
const { name, description, visibility, cover } = parsed.body;
|
||||||
|
|
||||||
const uploaded = await uploadOptionalCover(api, '/playlists/upload-picture', cover);
|
|
||||||
if ('failMessage' in uploaded) {
|
|
||||||
return fail(502, { message: uploaded.failMessage, detail: uploaded.detail });
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await api.PUT('/playlists/{id}', {
|
const result = await api.PUT('/playlists/{id}', {
|
||||||
params: { path: { id: params.id } },
|
params: { path: { id: params.id } },
|
||||||
body: {
|
body: { name, description, visibility }
|
||||||
name,
|
|
||||||
description,
|
|
||||||
pictureIntentId: uploaded.intentId,
|
|
||||||
visibility
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return failOnError(result, 'No se pudo actualizar la playlist.') ?? { edited: true };
|
const failure = failOnError(result, 'No se pudo actualizar la playlist.');
|
||||||
|
if (failure) return failure;
|
||||||
|
|
||||||
|
if (cover) {
|
||||||
|
const uploaded = await uploadOptionalCover(api, '/playlists/upload-picture', cover);
|
||||||
|
if ('failMessage' in uploaded) {
|
||||||
|
return fail(502, { message: uploaded.failMessage, detail: uploaded.detail });
|
||||||
|
}
|
||||||
|
if (uploaded.intentId) {
|
||||||
|
const pictureResult = await api.PUT('/playlists/{id}/picture', {
|
||||||
|
params: { path: { id: params.id } },
|
||||||
|
body: { pictureIntentId: uploaded.intentId }
|
||||||
|
});
|
||||||
|
const pictureFailure = failOnError(pictureResult, 'No se pudo actualizar la portada.');
|
||||||
|
if (pictureFailure) return pictureFailure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { edited: true };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
delete: authedAction(async ({ api, params }) => {
|
delete: authedAction(async ({ api, params }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user