Merge master into web-player-general-changes
CI/CD / BackEnd (push) Canceled after 0s
CI/CD / WebPlayer (push) Canceled after 0s
CI/CD / BackEnd (pull_request) Successful in 1m23s
CI/CD / WebPlayer (pull_request) Successful in 40s
CI/CD / Deploy (pull_request) Skipped
CI/CD / Deploy (push) Canceled after 0s
CI/CD / BackEnd (push) Canceled after 0s
CI/CD / WebPlayer (push) Canceled after 0s
CI/CD / BackEnd (pull_request) Successful in 1m23s
CI/CD / WebPlayer (pull_request) Successful in 40s
CI/CD / Deploy (pull_request) Skipped
CI/CD / Deploy (push) Canceled after 0s
This commit is contained in:
+20
@@ -57,5 +57,25 @@ namespace Musify.Application.Tests.Albums
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UncountedListens_AreIgnored()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var countedAlbum = TestEntities.Album(owner.Id, "Counted album");
|
||||
var skippedAlbum = TestEntities.Album(owner.Id, "Skipped album");
|
||||
var countedTrack = TestEntities.Track(owner, "In counted");
|
||||
var skippedTrack = TestEntities.Track(owner, "In skipped");
|
||||
await SeedAsync(
|
||||
owner, countedAlbum, skippedAlbum, countedTrack, skippedTrack,
|
||||
new AlbumHasTrack { AlbumId = countedAlbum.Id, TrackId = countedTrack.Id, TrackNumber = 1 },
|
||||
new AlbumHasTrack { AlbumId = skippedAlbum.Id, TrackId = skippedTrack.Id, TrackNumber = 1 },
|
||||
TestEntities.ListeningHistory(owner.Id, countedTrack.Id, DateTime.UtcNow.AddDays(-1), playedSeconds: 120),
|
||||
TestEntities.ListeningHistory(owner.Id, skippedTrack.Id, DateTime.UtcNow, playedSeconds: 1, isCounted: false));
|
||||
|
||||
var result = await CreateHandler().Handle(new GetRecentlyListenedAlbumsQuery(owner.Id, Limit: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(["Counted album"], result.Value.Select(album => album.Title));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using Musify.Application.Genres;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Genres
|
||||
{
|
||||
public sealed class GetAvailableGenresQueryHandlerTests
|
||||
{
|
||||
private static async Task<IReadOnlyList<AvailableGenreResponse>> RunAsync() =>
|
||||
await new GetAvailableGenresQueryHandler().Handle(new GetAvailableGenresQuery(), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsEveryGenre()
|
||||
{
|
||||
var result = await RunAsync();
|
||||
|
||||
Assert.Equal(Enum.GetValues<Genre>(), result.Select(r => r.Genre));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ListsIncompatibleGenresSymmetrically()
|
||||
{
|
||||
var result = await RunAsync();
|
||||
|
||||
var metal = result.Single(r => r.Genre == Genre.Metal);
|
||||
var lofi = result.Single(r => r.Genre == Genre.Lofi);
|
||||
Assert.Contains(Genre.Lofi, metal.IncompatibleWith);
|
||||
Assert.Contains(Genre.Metal, lofi.IncompatibleWith);
|
||||
Assert.Empty(result.Single(r => r.Genre == Genre.Pop).IncompatibleWith);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using Musify.Application.Genres;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.Entities;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Genres
|
||||
{
|
||||
public sealed class GetGenresQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
private GetGenresQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoTracks_ReturnsEmpty()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetGenresQuery(), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsOnlyGenresInUse_MostPopulatedFirst()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var rockA = TestEntities.Track(owner, "Rock A", tags: [Genre.Rock]);
|
||||
var rockB = TestEntities.Track(owner, "Rock B", tags: [Genre.Rock, Genre.Indie]);
|
||||
var jazz = TestEntities.Track(owner, "Jazz", tags: [Genre.Jazz]);
|
||||
await SeedAsync(
|
||||
owner, rockA, rockB, jazz,
|
||||
new UserHasTrack { UserId = owner.Id, TrackId = rockA.Id },
|
||||
new UserHasTrack { UserId = owner.Id, TrackId = rockB.Id },
|
||||
new UserHasTrack { UserId = owner.Id, TrackId = jazz.Id });
|
||||
|
||||
var result = await CreateHandler().Handle(new GetGenresQuery(), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(
|
||||
[(Genre.Rock, 2), (Genre.Jazz, 1), (Genre.Indie, 1)],
|
||||
result.Select(r => (r.Genre, r.TrackCount)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackInSeveralLibraries_IsCountedOnce()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var other = TestEntities.User(id: 2, name: "other-user");
|
||||
var track = TestEntities.Track(owner, tags: [Genre.Pop]);
|
||||
await SeedAsync(
|
||||
owner, other, track,
|
||||
new UserHasTrack { UserId = owner.Id, TrackId = track.Id },
|
||||
new UserHasTrack { UserId = other.Id, TrackId = track.Id });
|
||||
|
||||
var result = await CreateHandler().Handle(new GetGenresQuery(), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(1, Assert.Single(result).TrackCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackNotInAnyLibrary_IsIgnored()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner, tags: [Genre.Metal]);
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetGenresQuery(), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -72,5 +72,21 @@ namespace Musify.Application.Tests.Mixes
|
||||
.ToListAsync(TestContext.Current.CancellationToken);
|
||||
Assert.All(discoveryItems, item => Assert.Equal(unheardTrack.Id, item.TrackId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackWithOnlyUncountedListens_IsTreatedAsUnheard()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var skipped = TestEntities.Track(user, "Only skipped");
|
||||
await SeedAsync(
|
||||
user, skipped,
|
||||
new UserHasTrack { UserId = user.Id, TrackId = skipped.Id },
|
||||
TestEntities.ListeningHistory(user.Id, skipped.Id, playedSeconds: 3, isCounted: false));
|
||||
|
||||
await CreateHandler().Handle(new GenerateMixesForUserCommand(user.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var mixes = await Database.Mixes.ToListAsync(TestContext.Current.CancellationToken);
|
||||
Assert.Contains(mixes, mix => mix.Title == "Descubrimiento");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,12 +179,16 @@ namespace Musify.Application.Tests.TestSupport
|
||||
public static ListeningHistory ListeningHistory(
|
||||
long userId,
|
||||
Guid trackId,
|
||||
DateTime? listenedAt = null) =>
|
||||
DateTime? listenedAt = null,
|
||||
double? playedSeconds = null,
|
||||
bool isCounted = true) =>
|
||||
new()
|
||||
{
|
||||
UserId = userId,
|
||||
TrackId = trackId,
|
||||
ListenedAt = listenedAt ?? DateTime.UtcNow
|
||||
ListenedAt = listenedAt ?? DateTime.UtcNow,
|
||||
PlayedSeconds = playedSeconds,
|
||||
IsCounted = isCounted
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Tracks;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks
|
||||
{
|
||||
public sealed class DeleteStaleUncountedListensCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
private DeleteStaleUncountedListensCommandHandler CreateHandler() =>
|
||||
new(Database, NoOpLogger<DeleteStaleUncountedListensCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_DeletesOnlyOldUncountedListens()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
var oldUncounted = TestEntities.ListeningHistory(owner.Id, track.Id, DateTime.UtcNow.AddDays(-9), playedSeconds: 2, isCounted: false);
|
||||
var oldCounted = TestEntities.ListeningHistory(owner.Id, track.Id, DateTime.UtcNow.AddDays(-9), playedSeconds: 100);
|
||||
var recentUncounted = TestEntities.ListeningHistory(owner.Id, track.Id, DateTime.UtcNow.AddDays(-2), playedSeconds: 2, isCounted: false);
|
||||
await SeedAsync(owner, track, oldUncounted, oldCounted, recentUncounted);
|
||||
|
||||
var deleted = await CreateHandler().Handle(new DeleteStaleUncountedListensCommand(), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(1, deleted);
|
||||
var remaining = await Database.ListeningHistories.Select(l => l.Id).ToListAsync(TestContext.Current.CancellationToken);
|
||||
Assert.DoesNotContain(oldUncounted.Id, remaining);
|
||||
Assert.Contains(oldCounted.Id, remaining);
|
||||
Assert.Contains(recentUncounted.Id, remaining);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,5 +35,21 @@ namespace Musify.Application.Tests.Tracks
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UncountedListens_AreIgnored()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var counted = TestEntities.Track(owner, "Counted");
|
||||
var skipped = TestEntities.Track(owner, "Skipped");
|
||||
await SeedAsync(
|
||||
owner, counted, skipped,
|
||||
TestEntities.ListeningHistory(owner.Id, counted.Id, DateTime.UtcNow.AddMinutes(-5), playedSeconds: 90),
|
||||
TestEntities.ListeningHistory(owner.Id, skipped.Id, DateTime.UtcNow, playedSeconds: 2, isCounted: false));
|
||||
|
||||
var result = await CreateHandler().Handle(new GetListeningHistoryQuery(owner.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(["Counted"], result.Select(track => track.Title));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,5 +33,21 @@ namespace Musify.Application.Tests.Tracks
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ListensCount_OnlyCountsCountedListens()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(
|
||||
owner, track,
|
||||
TestEntities.ListeningHistory(owner.Id, track.Id, playedSeconds: 100),
|
||||
TestEntities.ListeningHistory(owner.Id, track.Id, playedSeconds: 100),
|
||||
TestEntities.ListeningHistory(owner.Id, track.Id, playedSeconds: 1, isCounted: false));
|
||||
|
||||
var result = await CreateHandler().Handle(new GetTrackByIdQuery(track.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(2, result.Value.ListensCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ namespace Musify.Application.Tests.Tracks
|
||||
var history = Assert.Single(await Database.ListeningHistories.ToListAsync(TestContext.Current.CancellationToken));
|
||||
Assert.Equal(owner.Id, history.UserId);
|
||||
Assert.Equal(track.Id, history.TrackId);
|
||||
Assert.Equal(history.Id, result.Value.ListenId);
|
||||
Assert.False(history.IsCounted);
|
||||
Assert.Null(history.PlayedSeconds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Tracks;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks
|
||||
@@ -43,5 +44,23 @@ namespace Musify.Application.Tests.Tracks
|
||||
var item = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Bohemian Rhapsody", item.Track.Title);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_GenreFilter_ReturnsOnlyTracksWithThatTag()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var rock = TestEntities.Track(owner, "Loud", tags: [Genre.Rock, Genre.Indie]);
|
||||
var jazz = TestEntities.Track(owner, "Smooth", tags: [Genre.Jazz]);
|
||||
await SeedAsync(
|
||||
owner, rock, jazz,
|
||||
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = rock.Id },
|
||||
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = jazz.Id });
|
||||
|
||||
var result = await CreateHandler().Handle(new GetTracksQuery(null, 1, 10, Genre.Indie), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var item = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Loud", item.Track.Title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
using ErrorOr;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Tracks;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks
|
||||
{
|
||||
public sealed class RecordListeningProgressCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
private RecordListeningProgressCommandHandler CreateHandler() => new(Database);
|
||||
|
||||
private async Task<ListeningHistory> ReloadAsync(Guid id) =>
|
||||
await Database.ListeningHistories.AsNoTracking()
|
||||
.SingleAsync(l => l.Id == id, TestContext.Current.CancellationToken);
|
||||
|
||||
private async Task<(User User, ListeningHistory Listen)> SeedListenAsync(
|
||||
double durationSeconds, DateTime listenedAt)
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var track = TestEntities.Track(user, durationSeconds: durationSeconds);
|
||||
var listen = TestEntities.ListeningHistory(user.Id, track.Id, listenedAt, isCounted: false);
|
||||
await SeedAsync(user, track, listen);
|
||||
return (user, listen);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_BelowThreshold_StoresSecondsWithoutCounting()
|
||||
{
|
||||
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
|
||||
|
||||
var result = await CreateHandler().Handle(
|
||||
new RecordListeningProgressCommand(user.Id, listen.Id, 10), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var stored = await ReloadAsync(listen.Id);
|
||||
Assert.Equal(10, stored.PlayedSeconds);
|
||||
Assert.False(stored.IsCounted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReachesThirtySeconds_MarksAsCounted()
|
||||
{
|
||||
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
|
||||
|
||||
await CreateHandler().Handle(
|
||||
new RecordListeningProgressCommand(user.Id, listen.Id, 30), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True((await ReloadAsync(listen.Id)).IsCounted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ShortTrack_CountsAtHalfDuration()
|
||||
{
|
||||
var (user, listen) = await SeedListenAsync(20, DateTime.UtcNow.AddSeconds(-60));
|
||||
|
||||
await CreateHandler().Handle(
|
||||
new RecordListeningProgressCommand(user.Id, listen.Id, 10), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True((await ReloadAsync(listen.Id)).IsCounted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_LowerValueThanStored_KeepsHighest()
|
||||
{
|
||||
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
|
||||
var handler = CreateHandler();
|
||||
|
||||
await handler.Handle(new RecordListeningProgressCommand(user.Id, listen.Id, 40), TestContext.Current.CancellationToken);
|
||||
await handler.Handle(new RecordListeningProgressCommand(user.Id, listen.Id, 15), TestContext.Current.CancellationToken);
|
||||
|
||||
var stored = await ReloadAsync(listen.Id);
|
||||
Assert.Equal(40, stored.PlayedSeconds);
|
||||
Assert.True(stored.IsCounted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_MoreThanElapsedClock_IsCappedToElapsedTime()
|
||||
{
|
||||
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-20));
|
||||
|
||||
await CreateHandler().Handle(
|
||||
new RecordListeningProgressCommand(user.Id, listen.Id, 150), TestContext.Current.CancellationToken);
|
||||
|
||||
var stored = await ReloadAsync(listen.Id);
|
||||
Assert.InRange(stored.PlayedSeconds!.Value, 24, 27);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_MoreThanTrackDuration_IsCappedToDuration()
|
||||
{
|
||||
var (user, listen) = await SeedListenAsync(50, DateTime.UtcNow.AddMinutes(-30));
|
||||
|
||||
await CreateHandler().Handle(
|
||||
new RecordListeningProgressCommand(user.Id, listen.Id, 900), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(52, (await ReloadAsync(listen.Id)).PlayedSeconds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_OtherUsersListen_ReturnsNotFound()
|
||||
{
|
||||
var (_, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
|
||||
|
||||
var result = await CreateHandler().Handle(
|
||||
new RecordListeningProgressCommand(long.MaxValue, listen.Id, 10), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(result.IsError);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-1)]
|
||||
[InlineData(double.NaN)]
|
||||
[InlineData(double.PositiveInfinity)]
|
||||
public async Task Handle_InvalidSeconds_ReturnsValidationError(double seconds)
|
||||
{
|
||||
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
|
||||
|
||||
var result = await CreateHandler().Handle(
|
||||
new RecordListeningProgressCommand(user.Id, listen.Id, seconds), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,37 @@ namespace Musify.Application.Tests.Users
|
||||
Assert.Equal(500, result.SecondsThisWeek);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PlayedSeconds_SumsPlayedTimeInsteadOfDuration()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var track = TestEntities.Track(user, durationSeconds: 200);
|
||||
await SeedAsync(
|
||||
user, track,
|
||||
TestEntities.ListeningHistory(user.Id, track.Id, playedSeconds: 200),
|
||||
TestEntities.ListeningHistory(user.Id, track.Id, playedSeconds: 12, isCounted: false));
|
||||
|
||||
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(212, result.SecondsThisWeek);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UncountedListens_AreExcludedFromTracksAndStreak()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var track = TestEntities.Track(user);
|
||||
await SeedAsync(
|
||||
user, track,
|
||||
TestEntities.ListeningHistory(user.Id, track.Id, playedSeconds: 3, isCounted: false));
|
||||
|
||||
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(0, result.TracksThisWeek);
|
||||
Assert.Equal(0, result.StreakDays);
|
||||
Assert.Equal(3, result.SecondsThisWeek);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ListenedTodayAndYesterday_StreakIsTwo()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user