Added tests
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
using Ardalis.Result;
|
||||
using FluentAssertions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Events;
|
||||
using Musify.Application.PlayLists.Commands;
|
||||
using Musify.Application.PlayLists.Handlers;
|
||||
using Musify.Application.Services;
|
||||
using Musify.Application.Tests.Infrastructure;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Handlers;
|
||||
|
||||
public sealed class CreatePlayListCommandHandlerTests
|
||||
{
|
||||
private readonly IEventBus _eventBus = Substitute.For<IEventBus>();
|
||||
private readonly IStorageService _storage = Substitute.For<IStorageService>();
|
||||
|
||||
private CreatePlayListCommandHandler CreateSut(TestDatabase db) =>
|
||||
new(
|
||||
_eventBus,
|
||||
db,
|
||||
new UploadIntentValidator(db, _storage),
|
||||
NullLogger<CreatePlayListCommandHandler>.Instance,
|
||||
TestData.StorageConfig(),
|
||||
TestData.PlayListConfig(),
|
||||
TestData.UploadIntentConfig());
|
||||
|
||||
[Fact]
|
||||
public async Task When_user_missing_returns_not_found()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var sut = CreateSut(ctx.Database);
|
||||
|
||||
var result = await sut.Handle(
|
||||
new CreatePlayListCommand(Guid.NewGuid(), "List", "desc", null), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.NotFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Without_picture_intent_uses_preset_and_does_not_publish()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var user = TestData.User();
|
||||
ctx.Database.Users.Add(user);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var sut = CreateSut(ctx.Database);
|
||||
var result = await sut.Handle(
|
||||
new CreatePlayListCommand(user.Id, " Chill Vibes ", "desc", null), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.Created);
|
||||
await _eventBus.DidNotReceive().PublishAsync(Arg.Any<CreatePlayListResourcesEvent>(), Arg.Any<CancellationToken>());
|
||||
|
||||
var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.UserId == user.Id);
|
||||
persisted.OriginalPictureName.Should().Be(TestData.PlayListConfig().Routes.PresetOriginalPicture);
|
||||
persisted.NormalizedName.Should().Be("CHILL VIBES");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task With_valid_picture_intent_publishes_event_with_intent_object_name()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var user = TestData.User();
|
||||
var intent = TestData.UploadIntent(user.Id, UploadIntentPurpose.PlayListPicture, objectName: "cover.webp");
|
||||
ctx.Database.Users.Add(user);
|
||||
ctx.Database.UploadIntents.Add(intent);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
_storage.HeadObjectAsync(intent.Bucket, intent.Key, Arg.Any<CancellationToken>())
|
||||
.Returns(new ObjectMetaData("image/webp", 500));
|
||||
|
||||
var sut = CreateSut(ctx.Database);
|
||||
var result = await sut.Handle(
|
||||
new CreatePlayListCommand(user.Id, "List", "desc", intent.Id), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.Created);
|
||||
await _eventBus.Received(1).PublishAsync(
|
||||
Arg.Is<CreatePlayListResourcesEvent>(e => e.PictureIntentId == intent.Id),
|
||||
Arg.Any<CancellationToken>());
|
||||
|
||||
var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.UserId == user.Id);
|
||||
persisted.OriginalPictureName.Should().Be("cover.webp");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task With_invalid_picture_intent_propagates_error_and_does_not_persist()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var user = TestData.User();
|
||||
ctx.Database.Users.Add(user);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var sut = CreateSut(ctx.Database);
|
||||
var result = await sut.Handle(
|
||||
new CreatePlayListCommand(user.Id, "List", "desc", Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.NotFound);
|
||||
(await ctx.NewContext().PlayLists.CountAsync()).Should().Be(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
using Ardalis.Result;
|
||||
using FluentAssertions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Events;
|
||||
using Musify.Application.Services;
|
||||
using Musify.Application.Tests.Infrastructure;
|
||||
using Musify.Application.Tracks.Commands;
|
||||
using Musify.Application.Tracks.Handlers;
|
||||
using Musify.Domain.Entities;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Handlers;
|
||||
|
||||
public sealed class CreateTrackCommandHandlerTests
|
||||
{
|
||||
private readonly IEventBus _eventBus = Substitute.For<IEventBus>();
|
||||
private readonly IStorageService _storage = Substitute.For<IStorageService>();
|
||||
|
||||
private CreateTrackCommandHandler CreateSut(TestDatabase db) =>
|
||||
new(
|
||||
db,
|
||||
_eventBus,
|
||||
new UploadIntentValidator(db, _storage),
|
||||
NullLogger<CreateTrackCommandHandler>.Instance,
|
||||
TestData.StorageConfig(),
|
||||
TestData.TrackConfig(),
|
||||
TestData.UploadIntentConfig());
|
||||
|
||||
private void StorageHasObject() =>
|
||||
_storage.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ObjectMetaData("application/octet-stream", 500));
|
||||
|
||||
private static async Task<(UploadIntent picture, UploadIntent audio)> SeedIntentsAsync(
|
||||
SqliteTestContext ctx, Guid userId)
|
||||
{
|
||||
var picture = TestData.UploadIntent(userId, UploadIntentPurpose.TrackPicture, objectName: "pic.png", key: $"temp/{userId}/pic.png");
|
||||
var audio = TestData.UploadIntent(userId, UploadIntentPurpose.TrackAudio, objectName: "song.mp3", key: $"temp/{userId}/song.mp3");
|
||||
ctx.Database.UploadIntents.AddRange(picture, audio);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
return (picture, audio);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task When_user_missing_returns_not_found()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var sut = CreateSut(ctx.Database);
|
||||
|
||||
var result = await sut.Handle(
|
||||
new CreateTrackCommand(Guid.NewGuid(), "Song", Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.NotFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task When_picture_intent_invalid_propagates_and_creates_no_track()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var user = TestData.User();
|
||||
ctx.Database.Users.Add(user);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var sut = CreateSut(ctx.Database);
|
||||
var result = await sut.Handle(
|
||||
new CreateTrackCommand(user.Id, "Song", Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.NotFound);
|
||||
(await ctx.NewContext().Tracks.CountAsync()).Should().Be(0);
|
||||
await _eventBus.DidNotReceive().PublishAsync(Arg.Any<CreateTrackResourcesEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task When_audio_intent_invalid_propagates()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var user = TestData.User();
|
||||
var picture = TestData.UploadIntent(user.Id, UploadIntentPurpose.TrackPicture, objectName: "pic.png", key: $"temp/{user.Id}/pic.png");
|
||||
ctx.Database.Users.Add(user);
|
||||
ctx.Database.UploadIntents.Add(picture);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
StorageHasObject();
|
||||
|
||||
var sut = CreateSut(ctx.Database);
|
||||
var result = await sut.Handle(
|
||||
new CreateTrackCommand(user.Id, "Song", picture.Id, Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.NotFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Happy_path_creates_track_publishes_event_and_normalizes_title()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var user = TestData.User();
|
||||
ctx.Database.Users.Add(user);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
var (picture, audio) = await SeedIntentsAsync(ctx, user.Id);
|
||||
StorageHasObject();
|
||||
|
||||
var sut = CreateSut(ctx.Database);
|
||||
var result = await sut.Handle(
|
||||
new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.Created);
|
||||
await _eventBus.Received(1).PublishAsync(
|
||||
Arg.Is<CreateTrackResourcesEvent>(e =>
|
||||
e.PictureIntentId == picture.Id && e.AudioIntentId == audio.Id),
|
||||
Arg.Any<CancellationToken>());
|
||||
|
||||
var persisted = await ctx.NewContext().Tracks.SingleAsync();
|
||||
persisted.NormalizedTitle.Should().Be("MY SONG");
|
||||
persisted.OriginalPictureName.Should().Be("pic.png");
|
||||
persisted.OriginalAudioName.Should().Be("song.mp3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task When_publish_throws_returns_error_and_does_not_persist()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var user = TestData.User();
|
||||
ctx.Database.Users.Add(user);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
var (picture, audio) = await SeedIntentsAsync(ctx, user.Id);
|
||||
StorageHasObject();
|
||||
|
||||
_eventBus.PublishAsync(Arg.Any<CreateTrackResourcesEvent>(), Arg.Any<CancellationToken>())
|
||||
.Returns<Task>(_ => throw new InvalidOperationException("bus down"));
|
||||
|
||||
var sut = CreateSut(ctx.Database);
|
||||
var result = await sut.Handle(
|
||||
new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.Error);
|
||||
(await ctx.NewContext().Tracks.CountAsync()).Should().Be(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Ardalis.Result;
|
||||
using FluentAssertions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Musify.Application.Tests.Infrastructure;
|
||||
using Musify.Application.Users.Commands;
|
||||
using Musify.Application.Users.Handlers;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Handlers;
|
||||
|
||||
public sealed class CreateUserCommandHandlerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task When_user_already_exists_returns_conflict()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var existing = TestData.User();
|
||||
ctx.Database.Users.Add(existing);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var sut = new CreateUserCommandHandler(ctx.Database, NullLogger<CreateUserCommandHandler>.Instance);
|
||||
var result = await sut.Handle(new CreateUserCommand(existing.Id, "Bob", null, null), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.Conflict);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Happy_path_creates_user_and_persists()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var id = Guid.NewGuid();
|
||||
|
||||
var sut = new CreateUserCommandHandler(ctx.Database, NullLogger<CreateUserCommandHandler>.Instance);
|
||||
var result = await sut.Handle(new CreateUserCommand(id, "Bob", "Robert", "Smith"), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.Created);
|
||||
result.Value.Name.Should().Be("Bob");
|
||||
|
||||
var persisted = await ctx.NewContext().Users.SingleAsync(u => u.Id == id);
|
||||
persisted.NormalizedName.Should().Be("BOB");
|
||||
persisted.FirstName.Should().Be("Robert");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using Ardalis.Result;
|
||||
using FluentAssertions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Events;
|
||||
using Musify.Application.PlayLists.Commands;
|
||||
using Musify.Application.PlayLists.Handlers;
|
||||
using Musify.Application.Tests.Infrastructure;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Handlers;
|
||||
|
||||
public sealed class DeletePlayListCommandHandlerTests
|
||||
{
|
||||
private readonly IEventBus _eventBus = Substitute.For<IEventBus>();
|
||||
|
||||
private DeletePlayListCommandHandler CreateSut(TestDatabase db) =>
|
||||
new(db, _eventBus, NullLogger<DeletePlayListCommandHandler>.Instance);
|
||||
|
||||
[Fact]
|
||||
public async Task When_playlist_missing_returns_not_found()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var sut = CreateSut(ctx.Database);
|
||||
|
||||
var result = await sut.Handle(new DeletePlayListCommand(Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.NotFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task When_user_not_owner_returns_unauthorized()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var playList = TestData.PlayList(Guid.NewGuid());
|
||||
ctx.Database.PlayLists.Add(playList);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var sut = CreateSut(ctx.Database);
|
||||
var result = await sut.Handle(new DeletePlayListCommand(Guid.NewGuid(), playList.Id), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.Unauthorized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Happy_path_marks_removing_publishes_event_and_returns_no_content()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var userId = Guid.NewGuid();
|
||||
var playList = TestData.PlayList(userId);
|
||||
ctx.Database.PlayLists.Add(playList);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var sut = CreateSut(ctx.Database);
|
||||
var result = await sut.Handle(new DeletePlayListCommand(userId, playList.Id), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.NoContent);
|
||||
await _eventBus.Received(1).PublishAsync(
|
||||
Arg.Is<DeletePlayListEvent>(e => e.PlayListId == playList.Id && e.UserId == userId),
|
||||
Arg.Any<CancellationToken>());
|
||||
|
||||
var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.Id == playList.Id);
|
||||
persisted.LifeCycleStatus.Should().Be(LifeCycleStatus.Removing);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task When_publish_throws_returns_error_and_does_not_persist()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var userId = Guid.NewGuid();
|
||||
var playList = TestData.PlayList(userId);
|
||||
ctx.Database.PlayLists.Add(playList);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
_eventBus.PublishAsync(Arg.Any<DeletePlayListEvent>(), Arg.Any<CancellationToken>())
|
||||
.Returns<Task>(_ => throw new InvalidOperationException("bus down"));
|
||||
|
||||
var sut = CreateSut(ctx.Database);
|
||||
var result = await sut.Handle(new DeletePlayListCommand(userId, playList.Id), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.Error);
|
||||
var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.Id == playList.Id);
|
||||
persisted.LifeCycleStatus.Should().Be(LifeCycleStatus.Active);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
using Ardalis.Result;
|
||||
using FluentAssertions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Events;
|
||||
using Musify.Application.Tests.Infrastructure;
|
||||
using Musify.Application.Tracks.Commands;
|
||||
using Musify.Application.Tracks.Handlers;
|
||||
using Musify.Domain.Entities;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Handlers;
|
||||
|
||||
public sealed class DeleteTrackCommandHandlerTests
|
||||
{
|
||||
private readonly IEventBus _eventBus = Substitute.For<IEventBus>();
|
||||
|
||||
private DeleteTrackCommandHandler CreateSut(TestDatabase db) =>
|
||||
new(db, _eventBus, NullLogger<DeleteTrackCommandHandler>.Instance);
|
||||
|
||||
private static async Task<(Guid trackId, Guid userId)> SeedOwnedTrackAsync(
|
||||
SqliteTestContext ctx, ProcessingStatus audioStatus)
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
var track = TestData.Track(audioStatus: audioStatus);
|
||||
ctx.Database.Tracks.Add(track);
|
||||
ctx.Database.UserHasTracks.Add(new UserHasTrack { UserId = userId, TrackId = track.Id });
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
return (track.Id, userId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task When_track_missing_returns_not_found()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var sut = CreateSut(ctx.Database);
|
||||
|
||||
var result = await sut.Handle(new DeleteTrackCommand(Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.NotFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task When_user_not_owner_returns_unauthorized()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var track = TestData.Track();
|
||||
ctx.Database.Tracks.Add(track);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var sut = CreateSut(ctx.Database);
|
||||
var result = await sut.Handle(new DeleteTrackCommand(Guid.NewGuid(), track.Id), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.Unauthorized);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(ProcessingStatus.Pending)]
|
||||
[InlineData(ProcessingStatus.Processing)]
|
||||
public async Task When_audio_still_processing_returns_conflict(ProcessingStatus status)
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var (trackId, userId) = await SeedOwnedTrackAsync(ctx, status);
|
||||
|
||||
var sut = CreateSut(ctx.Database);
|
||||
var result = await sut.Handle(new DeleteTrackCommand(userId, trackId), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.Conflict);
|
||||
await _eventBus.DidNotReceive().PublishAsync(Arg.Any<DeleteTrackEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Happy_path_marks_removing_publishes_event_and_returns_no_content()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var (trackId, userId) = await SeedOwnedTrackAsync(ctx, ProcessingStatus.Completed);
|
||||
|
||||
var sut = CreateSut(ctx.Database);
|
||||
var result = await sut.Handle(new DeleteTrackCommand(userId, trackId), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.NoContent);
|
||||
await _eventBus.Received(1).PublishAsync(
|
||||
Arg.Is<DeleteTrackEvent>(e => e.TrackId == trackId), Arg.Any<CancellationToken>());
|
||||
|
||||
var persisted = await ctx.NewContext().Tracks.SingleAsync(t => t.Id == trackId);
|
||||
persisted.LifeCycleStatus.Should().Be(LifeCycleStatus.Removing);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task When_publish_throws_returns_error_and_does_not_persist()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var (trackId, userId) = await SeedOwnedTrackAsync(ctx, ProcessingStatus.Completed);
|
||||
|
||||
_eventBus.PublishAsync(Arg.Any<DeleteTrackEvent>(), Arg.Any<CancellationToken>())
|
||||
.Returns<Task>(_ => throw new InvalidOperationException("bus down"));
|
||||
|
||||
var sut = CreateSut(ctx.Database);
|
||||
var result = await sut.Handle(new DeleteTrackCommand(userId, trackId), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.Error);
|
||||
var persisted = await ctx.NewContext().Tracks.SingleAsync(t => t.Id == trackId);
|
||||
persisted.LifeCycleStatus.Should().Be(LifeCycleStatus.Active);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using Ardalis.Result;
|
||||
using FluentAssertions;
|
||||
using Musify.Application.PlayLists.Handlers;
|
||||
using Musify.Application.PlayLists.Queries;
|
||||
using Musify.Application.Tests.Infrastructure;
|
||||
using Musify.Application.Tracks.Handlers;
|
||||
using Musify.Application.Tracks.Queries;
|
||||
using Musify.Application.Users.Handlers;
|
||||
using Musify.Application.Users.Queries;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Handlers;
|
||||
|
||||
public sealed class GetByIdQueryHandlerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetTrackById_returns_track_when_found()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var track = TestData.Track(title: "Found");
|
||||
ctx.Database.Tracks.Add(track);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var sut = new GetTrackByIdQueryHandler(ctx.Database);
|
||||
var result = await sut.Handle(new GetTrackByIdQuery(track.Id), CancellationToken.None);
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.Title.Should().Be("Found");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTrackById_returns_not_found_when_missing()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var sut = new GetTrackByIdQueryHandler(ctx.Database);
|
||||
|
||||
var result = await sut.Handle(new GetTrackByIdQuery(Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.NotFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlayListById_returns_playlist_when_found()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var playList = TestData.PlayList(Guid.NewGuid(), name: "MyList");
|
||||
ctx.Database.PlayLists.Add(playList);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var sut = new GetPlayListByIdQueryHandler(ctx.Database);
|
||||
var result = await sut.Handle(new GetPlayListByIdQuery(playList.Id), CancellationToken.None);
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.Name.Should().Be("MyList");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlayListById_returns_not_found_when_missing()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var sut = new GetPlayListByIdQueryHandler(ctx.Database);
|
||||
|
||||
var result = await sut.Handle(new GetPlayListByIdQuery(Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.NotFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUserById_returns_user_when_found()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var user = TestData.User(name: "Carol");
|
||||
ctx.Database.Users.Add(user);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var sut = new GetUserByIdQueryHandler(ctx.Database);
|
||||
var result = await sut.Handle(new GetUserByIdQuery(user.Id), CancellationToken.None);
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.Name.Should().Be("Carol");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUserById_returns_not_found_when_missing()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var sut = new GetUserByIdQueryHandler(ctx.Database);
|
||||
|
||||
var result = await sut.Handle(new GetUserByIdQuery(Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.NotFound);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using FluentAssertions;
|
||||
using Musify.Application.Tests.Infrastructure;
|
||||
using Musify.Application.Tracks.Handlers;
|
||||
using Musify.Application.Tracks.Queries;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Handlers;
|
||||
|
||||
public sealed class GetTracksByUserIdQueryHandlerTests
|
||||
{
|
||||
private static async Task SeedTrackForUserAsync(SqliteTestContext ctx, Guid userId, string title, DateTime created)
|
||||
{
|
||||
var track = TestData.Track(title: title);
|
||||
track.CreatedDate = created;
|
||||
ctx.Database.Tracks.Add(track);
|
||||
ctx.Database.UserHasTracks.Add(new UserHasTrack { UserId = userId, TrackId = track.Id });
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Returns_only_tracks_owned_by_the_user_ordered_by_created_date()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var userId = Guid.NewGuid();
|
||||
var otherUser = Guid.NewGuid();
|
||||
await SeedTrackForUserAsync(ctx, userId, "Beta", new DateTime(2026, 1, 2, 0, 0, 0, DateTimeKind.Utc));
|
||||
await SeedTrackForUserAsync(ctx, userId, "Alpha", new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
await SeedTrackForUserAsync(ctx, otherUser, "Gamma", new DateTime(2026, 1, 3, 0, 0, 0, DateTimeKind.Utc));
|
||||
|
||||
var sut = new GetTracksByUserIdQueryHandler(ctx.Database);
|
||||
var result = await sut.Handle(new GetTracksByUserIdQuery(userId), CancellationToken.None);
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.TotalItemCount.Should().Be(2);
|
||||
result.Value.Items.Select(t => t.Title).Should().Equal("Alpha", "Beta");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Filters_by_normalized_name_contains()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var userId = Guid.NewGuid();
|
||||
await SeedTrackForUserAsync(ctx, userId, "Summer Hit", DateTime.UtcNow);
|
||||
await SeedTrackForUserAsync(ctx, userId, "Winter Song", DateTime.UtcNow);
|
||||
|
||||
var sut = new GetTracksByUserIdQueryHandler(ctx.Database);
|
||||
var result = await sut.Handle(new GetTracksByUserIdQuery(userId, Name: "summer"), CancellationToken.None);
|
||||
|
||||
result.Value.TotalItemCount.Should().Be(1);
|
||||
result.Value.Items.Single().Title.Should().Be("Summer Hit");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Normalizes_invalid_paging_arguments_to_defaults()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var userId = Guid.NewGuid();
|
||||
for (var i = 0; i < 3; i++)
|
||||
await SeedTrackForUserAsync(ctx, userId, $"Track {i}", DateTime.UtcNow.AddSeconds(i));
|
||||
|
||||
var sut = new GetTracksByUserIdQueryHandler(ctx.Database);
|
||||
var result = await sut.Handle(
|
||||
new GetTracksByUserIdQuery(userId, PageNumber: 0, PageSize: -5), CancellationToken.None);
|
||||
|
||||
result.Value.PageNumber.Should().Be(1);
|
||||
result.Value.PageSize.Should().Be(10);
|
||||
result.Value.TotalItemCount.Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Returns_empty_page_when_user_has_no_tracks()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
|
||||
var sut = new GetTracksByUserIdQueryHandler(ctx.Database);
|
||||
var result = await sut.Handle(new GetTracksByUserIdQuery(Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.TotalItemCount.Should().Be(0);
|
||||
result.Value.Items.Should().BeEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
using FluentAssertions;
|
||||
using Musify.Application.PlayLists.Handlers;
|
||||
using Musify.Application.PlayLists.Queries;
|
||||
using Musify.Application.Tests.Infrastructure;
|
||||
using Musify.Application.Tracks.Handlers;
|
||||
using Musify.Application.Tracks.Queries;
|
||||
using Musify.Application.Users.Handlers;
|
||||
using Musify.Application.Users.Queries;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Handlers;
|
||||
|
||||
public sealed class ListQueryHandlerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetTracks_paginates_across_all_users()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
for (var i = 0; i < 15; i++)
|
||||
{
|
||||
var track = TestData.Track(title: $"Track {i:D2}");
|
||||
ctx.Database.Tracks.Add(track);
|
||||
ctx.Database.UserHasTracks.Add(new UserHasTrack { UserId = Guid.NewGuid(), TrackId = track.Id });
|
||||
}
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var sut = new GetTracksQueryHandler(ctx.Database);
|
||||
var result = await sut.Handle(new GetTracksQuery(PageNumber: 2, PageSize: 10), CancellationToken.None);
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.TotalItemCount.Should().Be(15);
|
||||
result.Value.PageNumber.Should().Be(2);
|
||||
result.Value.Items.Should().HaveCount(5);
|
||||
result.Value.HasPreviousPage.Should().BeTrue();
|
||||
result.Value.HasNextPage.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTracks_normalizes_invalid_paging_to_defaults()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var track = TestData.Track();
|
||||
ctx.Database.Tracks.Add(track);
|
||||
ctx.Database.UserHasTracks.Add(new UserHasTrack { UserId = Guid.NewGuid(), TrackId = track.Id });
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var sut = new GetTracksQueryHandler(ctx.Database);
|
||||
var result = await sut.Handle(new GetTracksQuery(PageNumber: 0, PageSize: -3), CancellationToken.None);
|
||||
|
||||
result.Value.PageNumber.Should().Be(1);
|
||||
result.Value.PageSize.Should().Be(10);
|
||||
result.Value.TotalItemCount.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlayLists_returns_all_playlists_paginated()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
for (var i = 0; i < 3; i++)
|
||||
ctx.Database.PlayLists.Add(TestData.PlayList(Guid.NewGuid(), name: $"PL {i}"));
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var sut = new GetPlayListsQueryHandler(ctx.Database);
|
||||
var result = await sut.Handle(new GetPlayListsQuery(), CancellationToken.None);
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.TotalItemCount.Should().Be(3);
|
||||
result.Value.Items.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlayListsByUserId_filters_by_user_and_name()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var userId = Guid.NewGuid();
|
||||
ctx.Database.PlayLists.Add(TestData.PlayList(userId, name: "Summer Mix"));
|
||||
ctx.Database.PlayLists.Add(TestData.PlayList(userId, name: "Winter Mix"));
|
||||
ctx.Database.PlayLists.Add(TestData.PlayList(Guid.NewGuid(), name: "Summer Other"));
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var sut = new GetPlayListsByUserIdQueryHandler(ctx.Database);
|
||||
|
||||
var all = await sut.Handle(new GetPlayListsByUserIdQuery(userId), CancellationToken.None);
|
||||
all.Value.TotalItemCount.Should().Be(2);
|
||||
|
||||
var filtered = await sut.Handle(new GetPlayListsByUserIdQuery(userId, Name: "summer"), CancellationToken.None);
|
||||
filtered.Value.TotalItemCount.Should().Be(1);
|
||||
filtered.Value.Items.Single().Name.Should().Be("Summer Mix");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlayListsByUserId_returns_empty_for_unknown_user()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var sut = new GetPlayListsByUserIdQueryHandler(ctx.Database);
|
||||
|
||||
var result = await sut.Handle(new GetPlayListsByUserIdQuery(Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.TotalItemCount.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUsers_filters_by_username_search_case_insensitively()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
ctx.Database.Users.Add(TestData.User(name: "Alice"));
|
||||
ctx.Database.Users.Add(TestData.User(name: "Bob"));
|
||||
ctx.Database.Users.Add(TestData.User(name: "Alicia"));
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var sut = new GetUsersQueryHandler(ctx.Database);
|
||||
var result = await sut.Handle(new GetUsersQuery(1, 10, UsernameSearch: "ali"), CancellationToken.None);
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.TotalItemCount.Should().Be(2);
|
||||
result.Value.Items.Select(u => u.Name).Should().BeEquivalentTo("Alice", "Alicia");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUsers_normalizes_invalid_paging_to_defaults()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
ctx.Database.Users.Add(TestData.User(name: "Solo"));
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var sut = new GetUsersQueryHandler(ctx.Database);
|
||||
var result = await sut.Handle(new GetUsersQuery(0, -1), CancellationToken.None);
|
||||
|
||||
result.Value.PageNumber.Should().Be(1);
|
||||
result.Value.PageSize.Should().Be(10);
|
||||
result.Value.TotalItemCount.Should().Be(1);
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
using Ardalis.Result;
|
||||
using FluentAssertions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Musify.Application.Configuration;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.PlayLists.Commands;
|
||||
using Musify.Application.PlayLists.Handlers;
|
||||
using Musify.Application.Services;
|
||||
using Musify.Application.Tests.Infrastructure;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Handlers;
|
||||
|
||||
public sealed class RequestPlayListPictureUploadCommandHandlerTests
|
||||
{
|
||||
private readonly IStorageService _storage = Substitute.For<IStorageService>();
|
||||
|
||||
private RequestPlayListPictureUploadCommandHandler CreateSut(TestDatabase db, UploadIntentConfiguration? config = null) =>
|
||||
new(
|
||||
db,
|
||||
new UploadIntentValidator(db, _storage),
|
||||
_storage,
|
||||
NullLogger<RequestPlayListPictureUploadCommandHandler>.Instance,
|
||||
TestData.StorageConfig(),
|
||||
TestData.PlayListConfig(),
|
||||
config ?? TestData.UploadIntentConfig());
|
||||
|
||||
private static RequestPlayListPictureUploadCommand Command(Guid userId, long? size = null) =>
|
||||
new(userId, ".WEBP", "image/webp", size);
|
||||
|
||||
[Fact]
|
||||
public async Task When_user_missing_returns_not_found()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var sut = CreateSut(ctx.Database);
|
||||
|
||||
var result = await sut.Handle(Command(Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.NotFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Happy_path_creates_single_intent_and_returns_url()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var user = TestData.User();
|
||||
ctx.Database.Users.Add(user);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
_storage.GetUploadUrlAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<CancellationToken>())
|
||||
.Returns("https://signed.example/upload");
|
||||
|
||||
var sut = CreateSut(ctx.Database);
|
||||
var result = await sut.Handle(Command(user.Id), CancellationToken.None);
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.UploadUrl.Should().Be("https://signed.example/upload");
|
||||
|
||||
var intents = await ctx.NewContext().UploadIntents.Where(i => i.UserId == user.Id).ToListAsync();
|
||||
intents.Should().ContainSingle();
|
||||
intents[0].Purpose.Should().Be(UploadIntentPurpose.PlayListPicture);
|
||||
intents[0].ObjectName.Should().EndWith(".webp");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task When_quota_exceeded_returns_invalid_and_creates_no_intent()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var user = TestData.User();
|
||||
ctx.Database.Users.Add(user);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
_storage.GetUploadUrlAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<CancellationToken>())
|
||||
.Returns("https://signed.example/upload");
|
||||
|
||||
var config = TestData.UploadIntentConfig();
|
||||
config.MaxActiveUploadIntentsPerUser = 0;
|
||||
|
||||
var sut = CreateSut(ctx.Database, config);
|
||||
var result = await sut.Handle(Command(user.Id), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.Invalid);
|
||||
(await ctx.NewContext().UploadIntents.CountAsync()).Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Uses_default_picture_size_when_not_provided()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var user = TestData.User();
|
||||
ctx.Database.Users.Add(user);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
_storage.GetUploadUrlAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<CancellationToken>())
|
||||
.Returns("https://signed.example/upload");
|
||||
|
||||
var config = TestData.UploadIntentConfig();
|
||||
var sut = CreateSut(ctx.Database, config);
|
||||
await sut.Handle(Command(user.Id), CancellationToken.None);
|
||||
|
||||
var intent = await ctx.NewContext().UploadIntents.SingleAsync(i => i.UserId == user.Id);
|
||||
intent.ExpectedSizeBytes.Should().Be(config.DefaultExpectedPictureSizeBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task When_storage_throws_returns_error()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var user = TestData.User();
|
||||
ctx.Database.Users.Add(user);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
_storage.GetUploadUrlAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<CancellationToken>())
|
||||
.Returns<string>(_ => throw new InvalidOperationException("s3 down"));
|
||||
|
||||
var sut = CreateSut(ctx.Database);
|
||||
var result = await sut.Handle(Command(user.Id), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.Error);
|
||||
(await ctx.NewContext().UploadIntents.CountAsync()).Should().Be(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
using Ardalis.Result;
|
||||
using FluentAssertions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Musify.Application.Configuration;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Services;
|
||||
using Musify.Application.Tests.Infrastructure;
|
||||
using Musify.Application.Tracks.Commands;
|
||||
using Musify.Application.Tracks.Handlers;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Handlers;
|
||||
|
||||
public sealed class RequestTrackUploadUrlsCommandHandlerTests
|
||||
{
|
||||
private readonly IStorageService _storage = Substitute.For<IStorageService>();
|
||||
|
||||
private RequestTrackUploadUrlsCommandHandler CreateSut(TestDatabase db, UploadIntentConfiguration? config = null) =>
|
||||
new(
|
||||
db,
|
||||
new UploadIntentValidator(db, _storage),
|
||||
_storage,
|
||||
NullLogger<RequestTrackUploadUrlsCommandHandler>.Instance,
|
||||
TestData.StorageConfig(),
|
||||
TestData.TrackConfig(),
|
||||
config ?? TestData.UploadIntentConfig());
|
||||
|
||||
private static RequestTrackUploadUrlsCommand Command(Guid userId, long? pic = null, long? audio = null) =>
|
||||
new(userId, ".PNG", "image/png", "mp3", "audio/mpeg", pic, audio);
|
||||
|
||||
[Fact]
|
||||
public async Task When_user_missing_returns_not_found()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var sut = CreateSut(ctx.Database);
|
||||
|
||||
var result = await sut.Handle(Command(Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.NotFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Happy_path_creates_two_intents_and_returns_urls()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var user = TestData.User();
|
||||
ctx.Database.Users.Add(user);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
_storage.GetUploadUrlAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<CancellationToken>())
|
||||
.Returns("https://signed.example/upload");
|
||||
|
||||
var sut = CreateSut(ctx.Database);
|
||||
var result = await sut.Handle(Command(user.Id), CancellationToken.None);
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.PictureUploadUrl.Should().Be("https://signed.example/upload");
|
||||
|
||||
var intents = await ctx.NewContext().UploadIntents.Where(i => i.UserId == user.Id).ToListAsync();
|
||||
intents.Should().HaveCount(2);
|
||||
intents.Select(i => i.Purpose).Should().BeEquivalentTo(
|
||||
new[] { UploadIntentPurpose.TrackPicture, UploadIntentPurpose.TrackAudio });
|
||||
intents.Should().Contain(i => i.ObjectName.EndsWith(".png"));
|
||||
intents.Should().Contain(i => i.ObjectName.EndsWith(".mp3"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task When_quota_exceeded_returns_invalid_and_creates_no_intents()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var user = TestData.User();
|
||||
ctx.Database.Users.Add(user);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
_storage.GetUploadUrlAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<CancellationToken>())
|
||||
.Returns("https://signed.example/upload");
|
||||
|
||||
var config = TestData.UploadIntentConfig();
|
||||
config.MaxActiveUploadIntentsPerUser = 1;
|
||||
|
||||
var sut = CreateSut(ctx.Database, config);
|
||||
var result = await sut.Handle(Command(user.Id), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.Invalid);
|
||||
(await ctx.NewContext().UploadIntents.CountAsync()).Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Uses_default_sizes_when_not_provided()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var user = TestData.User();
|
||||
ctx.Database.Users.Add(user);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
_storage.GetUploadUrlAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<CancellationToken>())
|
||||
.Returns("https://signed.example/upload");
|
||||
|
||||
var config = TestData.UploadIntentConfig();
|
||||
var sut = CreateSut(ctx.Database, config);
|
||||
await sut.Handle(Command(user.Id), CancellationToken.None);
|
||||
|
||||
var intents = await ctx.NewContext().UploadIntents.Where(i => i.UserId == user.Id).ToListAsync();
|
||||
intents.Single(i => i.Purpose == UploadIntentPurpose.TrackPicture)
|
||||
.ExpectedSizeBytes.Should().Be(config.DefaultExpectedPictureSizeBytes);
|
||||
intents.Single(i => i.Purpose == UploadIntentPurpose.TrackAudio)
|
||||
.ExpectedSizeBytes.Should().Be(config.DefaultExpectedAudioSizeBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task When_storage_throws_returns_error()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var user = TestData.User();
|
||||
ctx.Database.Users.Add(user);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
_storage.GetUploadUrlAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<CancellationToken>())
|
||||
.Returns<string>(_ => throw new InvalidOperationException("s3 down"));
|
||||
|
||||
var sut = CreateSut(ctx.Database);
|
||||
var result = await sut.Handle(Command(user.Id), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.Error);
|
||||
(await ctx.NewContext().UploadIntents.CountAsync()).Should().Be(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
using Ardalis.Result;
|
||||
using FluentAssertions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Events;
|
||||
using Musify.Application.PlayLists.Commands;
|
||||
using Musify.Application.PlayLists.Handlers;
|
||||
using Musify.Application.Services;
|
||||
using Musify.Application.Tests.Infrastructure;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Handlers;
|
||||
|
||||
public sealed class UpdatePlayListCommandHandlerTests
|
||||
{
|
||||
private readonly IEventBus _eventBus = Substitute.For<IEventBus>();
|
||||
private readonly IStorageService _storage = Substitute.For<IStorageService>();
|
||||
|
||||
private UpdatePlayListCommandHandler CreateSut(TestDatabase db) =>
|
||||
new(
|
||||
_eventBus,
|
||||
db,
|
||||
new UploadIntentValidator(db, _storage),
|
||||
NullLogger<UpdatePlayListCommandHandler>.Instance,
|
||||
TestData.StorageConfig(),
|
||||
TestData.PlayListConfig(),
|
||||
TestData.UploadIntentConfig());
|
||||
|
||||
[Fact]
|
||||
public async Task When_playlist_missing_returns_not_found()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var sut = CreateSut(ctx.Database);
|
||||
|
||||
var result = await sut.Handle(
|
||||
new UpdatePlayListCommand(Guid.NewGuid(), Guid.NewGuid(), "n", null, null), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.NotFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task When_user_not_owner_returns_unauthorized()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var playList = TestData.PlayList(Guid.NewGuid());
|
||||
ctx.Database.PlayLists.Add(playList);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var sut = CreateSut(ctx.Database);
|
||||
var result = await sut.Handle(
|
||||
new UpdatePlayListCommand(Guid.NewGuid(), playList.Id, "n", null, null), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.Unauthorized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Updates_name_and_description_without_publishing()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var userId = Guid.NewGuid();
|
||||
var playList = TestData.PlayList(userId, name: "Old");
|
||||
ctx.Database.PlayLists.Add(playList);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var sut = CreateSut(ctx.Database);
|
||||
var result = await sut.Handle(
|
||||
new UpdatePlayListCommand(userId, playList.Id, " New Name ", "new desc", null), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.Ok);
|
||||
await _eventBus.DidNotReceive().PublishAsync(Arg.Any<UpdatePlayListPictureSourceEvent>(), Arg.Any<CancellationToken>());
|
||||
|
||||
var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.Id == playList.Id);
|
||||
persisted.Name.Should().Be(" New Name ");
|
||||
persisted.NormalizedName.Should().Be("NEW NAME");
|
||||
persisted.Description.Should().Be("new desc");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task When_only_description_provided_name_is_unchanged()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var userId = Guid.NewGuid();
|
||||
var playList = TestData.PlayList(userId, name: "Keep");
|
||||
ctx.Database.PlayLists.Add(playList);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var sut = CreateSut(ctx.Database);
|
||||
var result = await sut.Handle(
|
||||
new UpdatePlayListCommand(userId, playList.Id, null, "only desc", null), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.Ok);
|
||||
var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.Id == playList.Id);
|
||||
persisted.Name.Should().Be("Keep");
|
||||
persisted.Description.Should().Be("only desc");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task With_valid_new_picture_intent_publishes_source_event_and_updates_picture_name()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var userId = Guid.NewGuid();
|
||||
var playList = TestData.PlayList(userId);
|
||||
var intent = TestData.UploadIntent(userId, UploadIntentPurpose.PlayListPicture, objectName: "newcover.webp");
|
||||
ctx.Database.PlayLists.Add(playList);
|
||||
ctx.Database.UploadIntents.Add(intent);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
_storage.HeadObjectAsync(intent.Bucket, intent.Key, Arg.Any<CancellationToken>())
|
||||
.Returns(new ObjectMetaData("image/webp", 500));
|
||||
|
||||
var sut = CreateSut(ctx.Database);
|
||||
var result = await sut.Handle(
|
||||
new UpdatePlayListCommand(userId, playList.Id, null, null, intent.Id), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.Ok);
|
||||
await _eventBus.Received(1).PublishAsync(
|
||||
Arg.Is<UpdatePlayListPictureSourceEvent>(e => e.PlayListId == playList.Id && e.PictureIntentId == intent.Id),
|
||||
Arg.Any<CancellationToken>());
|
||||
|
||||
var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.Id == playList.Id);
|
||||
persisted.OriginalPictureName.Should().Be("newcover.webp");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task With_invalid_new_picture_intent_propagates_error()
|
||||
{
|
||||
await using var ctx = new SqliteTestContext();
|
||||
var userId = Guid.NewGuid();
|
||||
var playList = TestData.PlayList(userId);
|
||||
var intent = TestData.UploadIntent(userId, UploadIntentPurpose.PlayListPicture, status: UploadIntentStatus.Consumed);
|
||||
ctx.Database.PlayLists.Add(playList);
|
||||
ctx.Database.UploadIntents.Add(intent);
|
||||
await ctx.Database.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var sut = CreateSut(ctx.Database);
|
||||
var result = await sut.Handle(
|
||||
new UpdatePlayListCommand(userId, playList.Id, null, null, intent.Id), CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(ResultStatus.Conflict);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user