Fix query and API bugs: 403 for non-owners, page bounds, lifecycle filters, cover fallback, race-safe inserts, async album delete

This commit is contained in:
2026-09-24 05:14:05 +02:00
parent ab388e6ad6
commit 0e3576922f
90 changed files with 2083 additions and 322 deletions
@@ -53,7 +53,7 @@ public sealed class AddTrackToPlayListCommandHandlerTests : HandlerTestBase
}
[Fact]
public async Task Handle_NotOwner_ReturnsUnauthorized()
public async Task Handle_NotOwner_ReturnsForbidden()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
@@ -62,7 +62,7 @@ public sealed class AddTrackToPlayListCommandHandlerTests : HandlerTestBase
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(stranger.Id, playList.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
Assert.Equal(ErrorType.Forbidden, result.FirstError.Type);
}
[Fact]
@@ -89,4 +89,18 @@ public sealed class AddTrackToPlayListCommandHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
[Fact]
public async Task TrySaveChangesAsync_DuplicatePlayListTrack_MapsTheUniqueViolationToTheGivenError()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
var track = TestEntities.Track(owner);
await SeedAsync(owner, playList, track, new PlayListHasTrack { PlayListId = playList.Id, TrackId = track.Id, Position = 0 });
Database.PlayListHasTracks.Add(new PlayListHasTrack { PlayListId = playList.Id, TrackId = track.Id, Position = 1 });
var result = await Database.TrySaveChangesAsync(Error.Conflict("Test.Duplicate", "duplicate"), TestContext.Current.CancellationToken);
Assert.Equal("Test.Duplicate", result.FirstError.Code);
}
}
@@ -39,7 +39,7 @@ public sealed class DeletePlayListCommandHandlerTests : HandlerTestBase
}
[Fact]
public async Task Handle_NotOwner_ReturnsUnauthorized()
public async Task Handle_NotOwner_ReturnsForbidden()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
@@ -48,6 +48,6 @@ public sealed class DeletePlayListCommandHandlerTests : HandlerTestBase
var result = await CreateHandler().Handle(new DeletePlayListCommand(stranger.Id, playList.Id), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
Assert.Equal(ErrorType.Forbidden, result.FirstError.Type);
}
}
@@ -33,7 +33,7 @@ public sealed class RemoveTrackFromPlayListCommandHandlerTests : HandlerTestBase
}
[Fact]
public async Task Handle_NotOwner_ReturnsUnauthorized()
public async Task Handle_NotOwner_ReturnsForbidden()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
@@ -42,7 +42,7 @@ public sealed class RemoveTrackFromPlayListCommandHandlerTests : HandlerTestBase
var result = await CreateHandler().Handle(new RemoveTrackFromPlayListCommand(stranger.Id, playList.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
Assert.Equal(ErrorType.Forbidden, result.FirstError.Type);
}
[Fact]
@@ -56,4 +56,26 @@ public sealed class RemoveTrackFromPlayListCommandHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
[Fact]
public async Task Handle_TrackInTheMiddle_RenumbersTheRemainingTracks()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id);
var first = TestEntities.Track(owner, "First");
var second = TestEntities.Track(owner, "Second");
var third = TestEntities.Track(owner, "Third");
await SeedAsync(
owner, playList, first, second, third,
new PlayListHasTrack { PlayListId = playList.Id, TrackId = first.Id, Position = 0 },
new PlayListHasTrack { PlayListId = playList.Id, TrackId = second.Id, Position = 1 },
new PlayListHasTrack { PlayListId = playList.Id, TrackId = third.Id, Position = 2 });
var result = await CreateHandler().Handle(new RemoveTrackFromPlayListCommand(owner.Id, playList.Id, second.Id), TestContext.Current.CancellationToken);
Assert.False(result.IsError);
var remaining = Database.PlayListHasTracks.OrderBy(link => link.Position).ToList();
Assert.Equal([first.Id, third.Id], remaining.Select(link => link.TrackId));
Assert.Equal([0, 1], remaining.Select(link => link.Position));
}
}
@@ -48,7 +48,7 @@ public sealed class UpdatePlayListCommandHandlerTests : HandlerTestBase
}
[Fact]
public async Task Handle_NotOwner_ReturnsUnauthorized()
public async Task Handle_NotOwner_ReturnsForbidden()
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
@@ -59,7 +59,7 @@ public sealed class UpdatePlayListCommandHandlerTests : HandlerTestBase
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
Assert.Equal(ErrorType.Forbidden, result.FirstError.Type);
}
[Fact]
@@ -93,7 +93,7 @@ public sealed class UpdatePlayListCommandHandlerTests : HandlerTestBase
}
[Fact]
public async Task Handle_BlankNameAndDescription_LeavesThemUnchanged()
public async Task Handle_BlankName_LeavesItUnchangedAndBlankDescriptionClearsIt()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id, "Original Name", "Original description");
@@ -105,7 +105,7 @@ public sealed class UpdatePlayListCommandHandlerTests : HandlerTestBase
Assert.False(result.IsError);
Assert.Equal("Original Name", result.Value.Name);
Assert.Equal("Original description", result.Value.Description);
Assert.Null(result.Value.Description);
}
[Fact]
@@ -126,7 +126,7 @@ public sealed class UpdatePlayListCommandHandlerTests : HandlerTestBase
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal("new-cover.webp", result.Value.SmallImageKeyName);
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);