A lot of changes
This commit is contained in:
@@ -3,13 +3,8 @@ 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
|
||||
{
|
||||
@@ -18,16 +13,12 @@ namespace Musify.Application.Albums
|
||||
Guid AlbumId,
|
||||
string Title,
|
||||
string? Description,
|
||||
int? ReleaseYear,
|
||||
Guid? NewPictureIntentId)
|
||||
int? ReleaseYear)
|
||||
: ICommand<ErrorOr<AlbumApplicationResponse>>;
|
||||
|
||||
public class UpdateAlbumCommandHandler(
|
||||
IEventBus eventBus,
|
||||
IDatabase database,
|
||||
UploadIntentValidator uploadIntentValidator,
|
||||
ILogger<UpdateAlbumCommandHandler> logger,
|
||||
AlbumConfiguration albumConfiguration)
|
||||
ILogger<UpdateAlbumCommandHandler> logger)
|
||||
: ICommandHandler<UpdateAlbumCommand, ErrorOr<AlbumApplicationResponse>>
|
||||
{
|
||||
public async ValueTask<ErrorOr<AlbumApplicationResponse>> Handle(UpdateAlbumCommand request, CancellationToken cancellationToken)
|
||||
@@ -39,27 +30,12 @@ namespace Musify.Application.Albums
|
||||
var album = found.Value;
|
||||
var title = request.Title.Trim();
|
||||
|
||||
// This is a full replace of the plain fields; the picture has its own command.
|
||||
album.Title = title;
|
||||
album.NormalizedTitle = TextNormalizer.Normalize(title);
|
||||
album.Description = request.Description;
|
||||
album.Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim();
|
||||
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);
|
||||
|
||||
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)
|
||||
{
|
||||
var all = Enum.GetValues<Genre>();
|
||||
var all = Enum.GetValues<Genre>().Where(genre => genre != Genre.None).ToArray();
|
||||
|
||||
IReadOnlyList<AvailableGenreResponse> result = all
|
||||
.Select(genre => new AvailableGenreResponse(
|
||||
|
||||
@@ -7,6 +7,7 @@ using Musify.Application.Contracts;
|
||||
using Musify.Application.PlayLists.Responses;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Domain.Entities;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Application.PlayLists
|
||||
{
|
||||
@@ -34,7 +35,7 @@ namespace Musify.Application.PlayLists
|
||||
|
||||
var albumTrackIds = await database.AlbumHasTracks
|
||||
.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)
|
||||
.Select(albumTrack => albumTrack.TrackId)
|
||||
.ToListAsync(cancellationToken);
|
||||
@@ -54,8 +55,9 @@ namespace Musify.Application.PlayLists
|
||||
return new AddAlbumToPlayListResponse(0);
|
||||
|
||||
// Playlist positions are internal indexes, so they start at 0.
|
||||
var nextPosition = (await playListTracksQuery
|
||||
.MaxAsync(link => (int?)link.Position, cancellationToken) ?? -1) + 1;
|
||||
var nextPosition = existingTrackIds.Count > 0
|
||||
? await playListTracksQuery.MaxAsync(link => link.Position, cancellationToken) + 1
|
||||
: 0;
|
||||
|
||||
foreach (var trackId in newTrackIds)
|
||||
{
|
||||
|
||||
@@ -47,9 +47,12 @@ namespace Musify.Application.PlayLists
|
||||
return Result.Success;
|
||||
|
||||
// Playlist positions are internal indexes, so they start at 0.
|
||||
var nextPosition = (await database.PlayListHasTracks
|
||||
.Where(link => link.PlayListId == request.PlayListId)
|
||||
.MaxAsync(link => (int?)link.Position, cancellationToken) ?? -1) + 1;
|
||||
var playListTracksQuery = database.PlayListHasTracks
|
||||
.Where(link => link.PlayListId == request.PlayListId);
|
||||
|
||||
var nextPosition = await playListTracksQuery.AnyAsync(cancellationToken)
|
||||
? await playListTracksQuery.MaxAsync(link => link.Position, cancellationToken) + 1
|
||||
: 0;
|
||||
|
||||
foreach (var trackId in toAdd)
|
||||
{
|
||||
|
||||
@@ -39,10 +39,10 @@ namespace Musify.Application.PlayLists
|
||||
return AppErrors.NotFound("User", request.UserId);
|
||||
|
||||
PreparedPicture? picture = null;
|
||||
if (request.PictureIntentId is { } intentId)
|
||||
if (request.PictureIntentId.HasValue)
|
||||
{
|
||||
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)
|
||||
return prepared.Errors;
|
||||
|
||||
|
||||
@@ -2,12 +2,8 @@ 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;
|
||||
|
||||
@@ -16,18 +12,14 @@ namespace Musify.Application.PlayLists
|
||||
public record UpdatePlayListCommand(
|
||||
long UserId,
|
||||
Guid PlayListId,
|
||||
string? NewName,
|
||||
string NewName,
|
||||
string? NewDescription,
|
||||
Guid? NewPictureIntentId,
|
||||
PlayListVisibility? NewVisibility = null)
|
||||
PlayListVisibility NewVisibility)
|
||||
: ICommand<ErrorOr<PlayListApplicationResponse>>;
|
||||
|
||||
public class UpdatePlayListCommandHandler(
|
||||
IEventBus eventBus,
|
||||
IDatabase database,
|
||||
UploadIntentValidator uploadIntentValidator,
|
||||
ILogger<UpdatePlayListCommandHandler> logger,
|
||||
PlayListConfiguration playListConfiguration)
|
||||
ILogger<UpdatePlayListCommandHandler> logger)
|
||||
: ICommandHandler<UpdatePlayListCommand, ErrorOr<PlayListApplicationResponse>>
|
||||
{
|
||||
public async ValueTask<ErrorOr<PlayListApplicationResponse>> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken)
|
||||
@@ -38,34 +30,11 @@ namespace Musify.Application.PlayLists
|
||||
|
||||
var playList = found.Value;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.NewName))
|
||||
{
|
||||
playList.Name = request.NewName.Trim();
|
||||
playList.NormalizedName = TextNormalizer.Normalize(request.NewName);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
// This is a full replace of the plain fields; the picture has its own command.
|
||||
playList.Name = request.NewName.Trim();
|
||||
playList.NormalizedName = TextNormalizer.Normalize(request.NewName);
|
||||
playList.Description = string.IsNullOrWhiteSpace(request.NewDescription) ? null : request.NewDescription.Trim();
|
||||
playList.Visibility = request.NewVisibility;
|
||||
|
||||
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);
|
||||
|
||||
|
||||
var sizes = trackConfiguration.PicturesSizes.ToImageSizes(trackConfiguration.Routes);
|
||||
await eventBus.PublishAsync(
|
||||
new CreateTrackResourcesEvent(
|
||||
|
||||
@@ -2,31 +2,33 @@ namespace Musify.Domain.ValueObjects
|
||||
{
|
||||
public enum Genre
|
||||
{
|
||||
Pop = 0,
|
||||
Rock = 1,
|
||||
HipHop = 2,
|
||||
RnB = 3,
|
||||
Jazz = 4,
|
||||
Blues = 5,
|
||||
Classical = 6,
|
||||
Electronic = 7,
|
||||
House = 8,
|
||||
Techno = 9,
|
||||
Trance = 10,
|
||||
Dubstep = 11,
|
||||
DrumAndBass = 12,
|
||||
Metal = 13,
|
||||
Punk = 14,
|
||||
Reggae = 15,
|
||||
Reggaeton = 16,
|
||||
Country = 17,
|
||||
Folk = 18,
|
||||
Indie = 19,
|
||||
KPop = 20,
|
||||
Latin = 21,
|
||||
Soul = 22,
|
||||
Funk = 23,
|
||||
Ambient = 24,
|
||||
Lofi = 25
|
||||
/// <summary>Unset value (the enum default); it is not a real genre and is never stored or offered to clients.</summary>
|
||||
None = 0,
|
||||
Pop = 1,
|
||||
Rock = 2,
|
||||
HipHop = 3,
|
||||
RnB = 4,
|
||||
Jazz = 5,
|
||||
Blues = 6,
|
||||
Classical = 7,
|
||||
Electronic = 8,
|
||||
House = 9,
|
||||
Techno = 10,
|
||||
Trance = 11,
|
||||
Dubstep = 12,
|
||||
DrumAndBass = 13,
|
||||
Metal = 14,
|
||||
Punk = 15,
|
||||
Reggae = 16,
|
||||
Reggaeton = 17,
|
||||
Country = 18,
|
||||
Folk = 19,
|
||||
Indie = 20,
|
||||
KPop = 21,
|
||||
Latin = 22,
|
||||
Soul = 23,
|
||||
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("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);
|
||||
|
||||
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(
|
||||
string Title,
|
||||
string? Description,
|
||||
int? ReleaseYear,
|
||||
Guid? PictureIntentId);
|
||||
int? ReleaseYear);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
public record UpdatePlayListRequest(
|
||||
string? Name,
|
||||
string Name,
|
||||
string? Description,
|
||||
Guid? PictureIntentId,
|
||||
PlayListVisibility? Visibility = null);
|
||||
PlayListVisibility? Visibility);
|
||||
}
|
||||
|
||||
@@ -94,6 +94,17 @@ namespace Musify.Api.Endpoints
|
||||
.Produces(StatusCodes.Status403Forbidden)
|
||||
.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)
|
||||
.WithName("DeleteAlbum")
|
||||
.WithSummary("Delete an album")
|
||||
@@ -229,7 +240,21 @@ namespace Musify.Api.Endpoints
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
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);
|
||||
|
||||
return result.ToHttpResult();
|
||||
|
||||
@@ -68,6 +68,17 @@ namespace Musify.Api.Endpoints
|
||||
.Produces(StatusCodes.Status403Forbidden)
|
||||
.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)
|
||||
.WithName("DeletePlayList")
|
||||
.WithSummary("Delete a playlist")
|
||||
@@ -185,7 +196,21 @@ namespace Musify.Api.Endpoints
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
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);
|
||||
|
||||
return result.ToHttpResult();
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Musify.Api.Models
|
||||
{
|
||||
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);
|
||||
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)
|
||||
.NotEmpty()
|
||||
.MaximumLength(Limits.PlayListName)
|
||||
.When(x => x.Name != null);
|
||||
.MaximumLength(Limits.PlayListName);
|
||||
|
||||
RuleFor(x => x.Description)
|
||||
.MaximumLength(Limits.Description)
|
||||
.When(x => x.Description != null);
|
||||
.MaximumLength(Limits.Description);
|
||||
|
||||
RuleFor(x => x.Visibility)
|
||||
.IsInEnum()
|
||||
.When(x => x.Visibility != null);
|
||||
.NotNull()
|
||||
.IsInEnum();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,11 @@ namespace Musify.Api.Validators.Tracks
|
||||
.NotEmpty()
|
||||
.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)
|
||||
.Must(tags => GenreCompatibility.FindConflicts(tags).Count == 0)
|
||||
.When(x => x.Tags is { Count: > 0 })
|
||||
|
||||
@@ -111,6 +111,37 @@ namespace Musify.Api.Tests.Endpoints
|
||||
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]
|
||||
public async Task DeleteAlbum_Owner_ReturnsNoContentAndAlbumIsGone()
|
||||
{
|
||||
|
||||
@@ -63,7 +63,7 @@ namespace Musify.Api.Tests.Endpoints
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ namespace Musify.Api.Tests.Endpoints
|
||||
var playList = await CreatePlayListAsync(owner, PlayListVisibility.Public);
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -120,13 +120,50 @@ namespace Musify.Api.Tests.Endpoints
|
||||
var playList = await CreatePlayListAsync(owner, PlayListVisibility.Private);
|
||||
|
||||
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);
|
||||
var body = await response.Content.ReadFromJsonAsync<PlayListApplicationResponse>(JsonOptions, Ct);
|
||||
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]
|
||||
public async Task GetPlayListById_NotAGuid_ReturnsNotFound()
|
||||
{
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
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 UpdateAlbumCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
|
||||
private UpdateAlbumCommandHandler CreateHandler() => new(
|
||||
eventBus,
|
||||
Database,
|
||||
new UploadIntentValidator(Database, storageService, TestConfigurations.UploadIntent()),
|
||||
NoOpLogger<UpdateAlbumCommandHandler>(),
|
||||
TestConfigurations.Album());
|
||||
NoOpLogger<UpdateAlbumCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
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);
|
||||
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);
|
||||
|
||||
@@ -42,32 +33,22 @@ namespace Musify.Application.Tests.Albums
|
||||
}
|
||||
|
||||
[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 album = TestEntities.Album(owner.Id, title: "Old Title");
|
||||
var intent = TestEntities.UploadIntent(owner.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.AlbumPicture, objectName: "new-cover.webp");
|
||||
await SeedAsync(owner, album, intent);
|
||||
var album = TestEntities.Album(owner.Id, title: "Title", description: "Old");
|
||||
await SeedAsync(owner, album);
|
||||
|
||||
var command = new UpdateAlbumCommand(owner.Id, album.Id, "Old Title", null, null, intent.Id);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
var result = await CreateHandler().Handle(new UpdateAlbumCommand(owner.Id, album.Id, "Title", " ", null), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
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>());
|
||||
Assert.Null(result.Value.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
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);
|
||||
|
||||
@@ -82,7 +63,7 @@ namespace Musify.Application.Tests.Albums
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
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);
|
||||
|
||||
|
||||
+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);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsEveryGenre()
|
||||
public async Task Handle_ReturnsEveryGenreExceptNone()
|
||||
{
|
||||
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]
|
||||
|
||||
+13
-46
@@ -1,24 +1,15 @@
|
||||
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 UpdatePlayListCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
|
||||
private UpdatePlayListCommandHandler CreateHandler() => new(
|
||||
eventBus,
|
||||
Database,
|
||||
new UploadIntentValidator(Database, storageService, TestConfigurations.UploadIntent()),
|
||||
NoOpLogger<UpdatePlayListCommandHandler>(),
|
||||
TestConfigurations.PlayList());
|
||||
NoOpLogger<UpdatePlayListCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Owner_RenamesPlayList()
|
||||
@@ -27,7 +18,7 @@ namespace Musify.Application.Tests.PlayLists
|
||||
var playList = TestEntities.PlayList(owner.Id, "Old Name");
|
||||
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);
|
||||
|
||||
@@ -38,7 +29,7 @@ namespace Musify.Application.Tests.PlayLists
|
||||
[Fact]
|
||||
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);
|
||||
|
||||
@@ -53,7 +44,7 @@ namespace Musify.Application.Tests.PlayLists
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
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);
|
||||
|
||||
@@ -67,7 +58,7 @@ namespace Musify.Application.Tests.PlayLists
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
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);
|
||||
|
||||
@@ -76,59 +67,35 @@ namespace Musify.Application.Tests.PlayLists
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoVisibilitySpecified_LeavesItUnchanged()
|
||||
public async Task Handle_ReplacesNameDescriptionAndVisibility()
|
||||
{
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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]
|
||||
public async Task Handle_BlankName_LeavesItUnchangedAndBlankDescriptionClearsIt()
|
||||
public async Task Handle_MissingDescription_ClearsIt()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id, "Original Name", "Original description");
|
||||
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);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("Original Name", result.Value.Name);
|
||||
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>());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user