Add backend test suite under backend/Tests/
Two xUnit projects, wired into Musify.slnx under a /Tests/ folder that
mirrors Core/ and Hosts/:
Musify.Domain.Tests Value-object logic (TrackAudio/TrackPictures'
IsProcessed/IsFailed/IsInProgress). Entities are
otherwise anemic EF models with no behavior to
test.
Musify.Application.Tests All ~45 Application handlers and services
(Albums, Tracks, PlayLists, Users, Mixes,
YouTube, plus TrackStreamIssuer,
UploadIntentValidator, YouTubeTrackProvisioner).
IDatabase exposes DbSet<T> directly, so handlers compose real LINQ queries
against it — that can't be expressed with a mock (no IQueryable provider
behind one). TestSupport/TestDatabase.cs is instead a real DbContext on a
private SQLite in-memory connection per test (fresh per test since xUnit
instantiates the test class per test method): fast, no external dependency,
and — unlike EF Core's InMemory provider — SQLite enforces real foreign keys
and supports transactions, which two handlers need. External dependencies
(IStorageService, IEventBus, IYouTubeMusicService, ...) are substituted with
NSubstitute.
TestSupport/TestEntities.cs and TestConfigurations.cs build fully-populated
domain entities and configuration POCOs with sensible defaults for every
required member, so no test method needs to fill in properties it doesn't
care about, and no builder ever hands back null.
Also, needed to make the Application layer itself testable without a real
Postgres or user-secrets:
- DatabaseDesignTimeFactory now falls back to the Database__ConnectionString
env var, and DesignSettings.json is optional — unblocks the `migrate`
one-shot container from deploy/compose.yml, which hit the same gap.
- Directory.Packages.props: xunit, xunit.runner.visualstudio, NSubstitute,
Microsoft.EntityFrameworkCore.Sqlite (pinned to the same 10.0.8 as the rest
of EF Core), Microsoft.NET.Test.Sdk, coverlet.collector. Also pins
SQLitePCLRaw.bundle_e_sqlite3 to 2.1.13 — EFCore.Sqlite 10.0.8 pulls 2.1.11
transitively, which has a known high-severity advisory (GHSA-2m69-gcr7-jv3q).
- backend/.dockerignore excludes Tests/: no application Dockerfile needs it
in its build context.
162 tests, all green: `dotnet test backend/Musify.slnx`.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
using ErrorOr;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Albums;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Albums;
|
||||
|
||||
public sealed class AddTrackToAlbumCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
private AddTrackToAlbumCommandHandler CreateHandler() => new(Database, NoOpLogger<AddTrackToAlbumCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_FirstTrack_AddsItAtNumberOne()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.UserAlbum(owner.Id);
|
||||
var track = TestEntities.LocalTrack(owner);
|
||||
await SeedAsync(owner, album, track);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, track.Id), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var link = Assert.Single(await Database.AlbumHasTracks.ToListAsync());
|
||||
Assert.Equal(1, link.TrackNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AlbumMissing_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.LocalTrack(owner);
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, Guid.NewGuid(), track.Id), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var album = TestEntities.UserAlbum(owner.Id);
|
||||
var track = TestEntities.LocalTrack(owner);
|
||||
await SeedAsync(owner, stranger, album, track);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(stranger.Id, album.Id, track.Id), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackMissing_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.UserAlbum(owner.Id);
|
||||
await SeedAsync(owner, album);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackOwnedByAnotherUser_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var otherOwner = TestEntities.User(2, "other");
|
||||
var album = TestEntities.UserAlbum(owner.Id);
|
||||
var foreignTrack = TestEntities.LocalTrack(otherOwner);
|
||||
await SeedAsync(owner, otherOwner, album, foreignTrack);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, foreignTrack.Id), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AlreadyInAlbum_ReturnsConflict()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.UserAlbum(owner.Id);
|
||||
var track = TestEntities.LocalTrack(owner);
|
||||
var link = new AlbumHasTrack { AlbumId = album.Id, TrackId = track.Id, TrackNumber = 1 };
|
||||
await SeedAsync(owner, album, track, link);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, track.Id), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AlbumAlreadyHasTracks_AppendsAtNextNumber()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.UserAlbum(owner.Id);
|
||||
var firstTrack = TestEntities.LocalTrack(owner, "First");
|
||||
var secondTrack = TestEntities.LocalTrack(owner, "Second");
|
||||
var existingLink = new AlbumHasTrack { AlbumId = album.Id, TrackId = firstTrack.Id, TrackNumber = 3 };
|
||||
await SeedAsync(owner, album, firstTrack, secondTrack, existingLink);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, secondTrack.Id), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var newLink = await Database.AlbumHasTracks.SingleAsync(l => l.TrackId == secondTrack.Id);
|
||||
Assert.Equal(4, newLink.TrackNumber);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Musify.Application.Albums;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Albums;
|
||||
|
||||
public sealed class CreateAlbumCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
private CreateAlbumCommandHandler CreateHandler() => new(Database, NoOpLogger<CreateAlbumCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserExists_CreatesAlbumAndReturnsIt()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner);
|
||||
|
||||
var command = new CreateAlbumCommand(owner.Id, " My Album ", "A description", 2024);
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("My Album", result.Value.Title);
|
||||
Assert.Equal("A description", result.Value.Description);
|
||||
Assert.Equal(2024, result.Value.ReleaseYear);
|
||||
Assert.Equal(owner.Id, result.Value.OwnerUserId);
|
||||
Assert.Equal(0, result.Value.TrackCount);
|
||||
|
||||
var stored = await Database.UserAlbums.FindAsync(result.Value.Id);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("MY ALBUM", stored.NormalizedTitle);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserDoesNotExist_ReturnsNotFound()
|
||||
{
|
||||
var command = new CreateAlbumCommand(UserId: 404, "Orphan Album", Description: null, ReleaseYear: null);
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.True(result.IsError);
|
||||
Assert.Equal(ErrorOr.ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using ErrorOr;
|
||||
using Musify.Application.Albums;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Albums;
|
||||
|
||||
public sealed class DeleteAlbumCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
private DeleteAlbumCommandHandler CreateHandler() => new(Database, NoOpLogger<DeleteAlbumCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Owner_RemovesAlbum()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.UserAlbum(owner.Id);
|
||||
await SeedAsync(owner, album);
|
||||
|
||||
var result = await CreateHandler().Handle(new DeleteAlbumCommand(owner.Id, album.Id), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Null(await Database.UserAlbums.FindAsync(album.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AlbumMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new DeleteAlbumCommand(1, Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorizedAndKeepsAlbum()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var album = TestEntities.UserAlbum(owner.Id);
|
||||
await SeedAsync(owner, stranger, album);
|
||||
|
||||
var result = await CreateHandler().Handle(new DeleteAlbumCommand(stranger.Id, album.Id), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
Assert.NotNull(await Database.UserAlbums.FindAsync(album.Id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using ErrorOr;
|
||||
using Musify.Application.Albums;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Albums;
|
||||
|
||||
public sealed class GetAlbumByIdQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
private GetAlbumByIdQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ExistingAlbum_ReturnsItWithTrackCountAndCovers()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.UserAlbum(owner.Id);
|
||||
var track = TestEntities.LocalTrack(owner);
|
||||
await SeedAsync(owner, album, track, new AlbumHasTrack { AlbumId = album.Id, TrackId = track.Id, TrackNumber = 1 });
|
||||
|
||||
var result = await CreateHandler().Handle(new GetAlbumByIdQuery(album.Id), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(album.Id, result.Value.Id);
|
||||
Assert.Equal(1, result.Value.TrackCount);
|
||||
Assert.Equal([track.Id], result.Value.CoverTrackIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AlbumMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetAlbumByIdQuery(Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using ErrorOr;
|
||||
using Musify.Application.Albums;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Albums;
|
||||
|
||||
public sealed class GetAlbumTracksQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
private GetAlbumTracksQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AlbumWithTracks_ReturnsThemOrderedByTrackNumber()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.UserAlbum(owner.Id);
|
||||
var second = TestEntities.LocalTrack(owner, "Second");
|
||||
var first = TestEntities.LocalTrack(owner, "First");
|
||||
await SeedAsync(
|
||||
owner, album, second, first,
|
||||
new AlbumHasTrack { AlbumId = album.Id, TrackId = second.Id, TrackNumber = 2 },
|
||||
new AlbumHasTrack { AlbumId = album.Id, TrackId = first.Id, TrackNumber = 1 });
|
||||
|
||||
var result = await CreateHandler().Handle(new GetAlbumTracksQuery(album.Id, PageNumber: 1, PageSize: 10), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(["First", "Second"], result.Value.Items.Select(track => track.Title));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AlbumMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetAlbumTracksQuery(Guid.NewGuid(), PageNumber: 1, PageSize: 10), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using ErrorOr;
|
||||
using Musify.Application.Albums;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Albums;
|
||||
|
||||
public sealed class GetAlbumsByUserIdQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
private GetAlbumsByUserIdQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserWithAlbums_ReturnsOnlyTheirAlbums()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var other = TestEntities.User(2, "other");
|
||||
await SeedAsync(owner, other, TestEntities.UserAlbum(owner.Id, "Mine"), TestEntities.UserAlbum(other.Id, "Theirs"));
|
||||
|
||||
var result = await CreateHandler().Handle(new GetAlbumsByUserIdQuery(owner.Id, PageNumber: 1, PageSize: 10), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var album = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Mine", album.Title);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetAlbumsByUserIdQuery(404, PageNumber: 1, PageSize: 10), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using Musify.Application.Albums;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Albums;
|
||||
|
||||
public sealed class GetAlbumsQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
private GetAlbumsQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoTitleFilter_ReturnsAllAlbumsPaged()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner, TestEntities.UserAlbum(owner.Id, "Alpha"), TestEntities.UserAlbum(owner.Id, "Beta"));
|
||||
|
||||
var result = await CreateHandler().Handle(new GetAlbumsQuery(Title: null, PageNumber: 1, PageSize: 10), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.TotalItemCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TitleFilter_ReturnsOnlyMatchingAlbums()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner, TestEntities.UserAlbum(owner.Id, "Rock Classics"), TestEntities.UserAlbum(owner.Id, "Jazz Favorites"));
|
||||
|
||||
var result = await CreateHandler().Handle(new GetAlbumsQuery(Title: "rock", PageNumber: 1, PageSize: 10), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var album = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Rock Classics", album.Title);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PageSizeSmallerThanTotal_ReportsHasNextPage()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner, TestEntities.UserAlbum(owner.Id, "One"), TestEntities.UserAlbum(owner.Id, "Two"), TestEntities.UserAlbum(owner.Id, "Three"));
|
||||
|
||||
var result = await CreateHandler().Handle(new GetAlbumsQuery(Title: null, PageNumber: 1, PageSize: 2), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.Items.Count);
|
||||
Assert.True(result.Value.HasNextPage);
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
using Musify.Application.Albums;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Albums;
|
||||
|
||||
public sealed class GetRecentlyListenedAlbumsQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
private GetRecentlyListenedAlbumsQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ListenedTracks_ReturnsTheirAlbumsMostRecentFirst()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var olderAlbum = TestEntities.UserAlbum(owner.Id, "Older");
|
||||
var newerAlbum = TestEntities.UserAlbum(owner.Id, "Newer");
|
||||
var olderTrack = TestEntities.LocalTrack(owner, "In older album");
|
||||
var newerTrack = TestEntities.LocalTrack(owner, "In newer album");
|
||||
await SeedAsync(
|
||||
owner, olderAlbum, newerAlbum, olderTrack, newerTrack,
|
||||
new AlbumHasTrack { AlbumId = olderAlbum.Id, TrackId = olderTrack.Id, TrackNumber = 1 },
|
||||
new AlbumHasTrack { AlbumId = newerAlbum.Id, TrackId = newerTrack.Id, TrackNumber = 1 },
|
||||
TestEntities.ListeningHistory(owner.Id, olderTrack.Id, DateTime.UtcNow.AddDays(-2)),
|
||||
TestEntities.ListeningHistory(owner.Id, newerTrack.Id, DateTime.UtcNow.AddDays(-1)));
|
||||
|
||||
var result = await CreateHandler().Handle(new GetRecentlyListenedAlbumsQuery(owner.Id, Limit: 10), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(["Newer", "Older"], result.Value.Select(album => album.Title));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoListeningHistory_ReturnsEmptyList()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetRecentlyListenedAlbumsQuery(owner.Id, Limit: 10), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Empty(result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_LimitLowerThanCandidates_ReturnsOnlyThatManyAlbums()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var albums = Enumerable.Range(1, 3).Select(i => TestEntities.UserAlbum(owner.Id, $"Album {i}")).ToList();
|
||||
var tracks = albums.Select((_, i) => TestEntities.LocalTrack(owner, $"Track {i}")).ToList();
|
||||
var links = albums.Zip(tracks, (album, track) => new AlbumHasTrack { AlbumId = album.Id, TrackId = track.Id, TrackNumber = 1 }).ToList();
|
||||
var histories = tracks.Select((track, i) => TestEntities.ListeningHistory(owner.Id, track.Id, DateTime.UtcNow.AddMinutes(-i))).ToList();
|
||||
await SeedAsync([owner, .. albums, .. tracks, .. links, .. histories]);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetRecentlyListenedAlbumsQuery(owner.Id, Limit: 2), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.Count);
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
using ErrorOr;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Albums;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Albums;
|
||||
|
||||
public sealed class RemoveTrackFromAlbumCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
private RemoveTrackFromAlbumCommandHandler CreateHandler() =>
|
||||
new(Database, NoOpLogger<RemoveTrackFromAlbumCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackInAlbum_RemovesItAndRenumbersTheRest()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.UserAlbum(owner.Id);
|
||||
var first = TestEntities.LocalTrack(owner, "First");
|
||||
var second = TestEntities.LocalTrack(owner, "Second");
|
||||
var third = TestEntities.LocalTrack(owner, "Third");
|
||||
await SeedAsync(
|
||||
owner, album, first, second, third,
|
||||
new AlbumHasTrack { AlbumId = album.Id, TrackId = first.Id, TrackNumber = 1 },
|
||||
new AlbumHasTrack { AlbumId = album.Id, TrackId = second.Id, TrackNumber = 2 },
|
||||
new AlbumHasTrack { AlbumId = album.Id, TrackId = third.Id, TrackNumber = 3 });
|
||||
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromAlbumCommand(owner.Id, album.Id, second.Id), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var remaining = await Database.AlbumHasTracks
|
||||
.Where(link => link.AlbumId == album.Id)
|
||||
.OrderBy(link => link.TrackNumber)
|
||||
.ToListAsync();
|
||||
Assert.Equal([first.Id, third.Id], remaining.Select(link => link.TrackId));
|
||||
Assert.Equal([1, 2], remaining.Select(link => link.TrackNumber));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AlbumMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromAlbumCommand(1, Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var album = TestEntities.UserAlbum(owner.Id);
|
||||
await SeedAsync(owner, stranger, album);
|
||||
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromAlbumCommand(stranger.Id, album.Id, Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackNotInAlbum_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.UserAlbum(owner.Id);
|
||||
await SeedAsync(owner, album);
|
||||
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromAlbumCommand(owner.Id, album.Id, Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using ErrorOr;
|
||||
using Musify.Application.Albums;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Albums;
|
||||
|
||||
public sealed class UpdateAlbumCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
private UpdateAlbumCommandHandler CreateHandler() => new(Database, NoOpLogger<UpdateAlbumCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Owner_UpdatesTitleDescriptionAndReleaseYear()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.UserAlbum(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);
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("New Title", result.Value.Title);
|
||||
Assert.Equal("New description", result.Value.Description);
|
||||
Assert.Equal(2025, result.Value.ReleaseYear);
|
||||
|
||||
var stored = await Database.UserAlbums.FindAsync(album.Id);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("NEW TITLE", stored.NormalizedTitle);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AlbumMissing_ReturnsNotFound()
|
||||
{
|
||||
var command = new UpdateAlbumCommand(1, Guid.NewGuid(), "Title", null, null);
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var album = TestEntities.UserAlbum(owner.Id);
|
||||
await SeedAsync(owner, stranger, album);
|
||||
|
||||
var command = new UpdateAlbumCommand(stranger.Id, album.Id, "Hijacked", null, null);
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user