Move ListeningRules to its own file and split listening tests by subject
CI/CD / BackEnd (push) Successful in 1m16s
CI/CD / BackEnd (pull_request) Canceled after 0s
CI/CD / WebPlayer (pull_request) Canceled after 0s
CI/CD / Deploy (pull_request) Canceled after 0s
CI/CD / WebPlayer (push) Successful in 40s
CI/CD / Deploy (push) Skipped
CI/CD / BackEnd (push) Successful in 1m16s
CI/CD / BackEnd (pull_request) Canceled after 0s
CI/CD / WebPlayer (pull_request) Canceled after 0s
CI/CD / Deploy (pull_request) Canceled after 0s
CI/CD / WebPlayer (push) Successful in 40s
CI/CD / Deploy (push) Skipped
The counted-listen tests lived in one catch-all UncountedListensTests file. Each now sits next to the handler it covers, and the cleanup handler gets its own test file.
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Musify.Application.Tracks
|
||||||
|
{
|
||||||
|
public static class ListeningRules
|
||||||
|
{
|
||||||
|
public const double CountedThresholdSeconds = 30;
|
||||||
|
public const double CountedThresholdFraction = 0.5;
|
||||||
|
public const double DurationToleranceSeconds = 2;
|
||||||
|
public const double ClockToleranceSeconds = 5;
|
||||||
|
public const int UncountedRetentionDays = 8;
|
||||||
|
|
||||||
|
public static double CountedThreshold(double durationSeconds) =>
|
||||||
|
Math.Min(CountedThresholdSeconds, durationSeconds * CountedThresholdFraction);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,18 +11,6 @@ namespace Musify.Application.Tracks
|
|||||||
double PlayedSeconds)
|
double PlayedSeconds)
|
||||||
: ICommand<ErrorOr<Success>>;
|
: ICommand<ErrorOr<Success>>;
|
||||||
|
|
||||||
public static class ListeningRules
|
|
||||||
{
|
|
||||||
public const double CountedThresholdSeconds = 30;
|
|
||||||
public const double CountedThresholdFraction = 0.5;
|
|
||||||
public const double DurationToleranceSeconds = 2;
|
|
||||||
public const double ClockToleranceSeconds = 5;
|
|
||||||
public const int UncountedRetentionDays = 8;
|
|
||||||
|
|
||||||
public static double CountedThreshold(double durationSeconds) =>
|
|
||||||
Math.Min(CountedThresholdSeconds, durationSeconds * CountedThresholdFraction);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RecordListeningProgressCommandHandler(IDatabase database)
|
public class RecordListeningProgressCommandHandler(IDatabase database)
|
||||||
: ICommandHandler<RecordListeningProgressCommand, ErrorOr<Success>>
|
: ICommandHandler<RecordListeningProgressCommand, ErrorOr<Success>>
|
||||||
{
|
{
|
||||||
|
|||||||
+20
@@ -57,5 +57,25 @@ namespace Musify.Application.Tests.Albums
|
|||||||
Assert.False(result.IsError);
|
Assert.False(result.IsError);
|
||||||
Assert.Equal(2, result.Value.Count);
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
@@ -72,5 +72,21 @@ namespace Musify.Application.Tests.Mixes
|
|||||||
.ToListAsync(TestContext.Current.CancellationToken);
|
.ToListAsync(TestContext.Current.CancellationToken);
|
||||||
Assert.All(discoveryItems, item => Assert.Equal(unheardTrack.Id, item.TrackId));
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+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);
|
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);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Musify.Application.Albums;
|
|
||||||
using Musify.Application.Mixes;
|
|
||||||
using Musify.Application.Tests.TestSupport;
|
|
||||||
using Musify.Application.Tracks;
|
|
||||||
using Musify.Domain.Entities;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Musify.Application.Tests.Tracks
|
|
||||||
{
|
|
||||||
public sealed class UncountedListensTests : HandlerTestBase
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public async Task ListeningHistory_IgnoresUncountedListens()
|
|
||||||
{
|
|
||||||
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 new GetListeningHistoryQueryHandler(Database)
|
|
||||||
.Handle(new GetListeningHistoryQuery(owner.Id), TestContext.Current.CancellationToken);
|
|
||||||
|
|
||||||
Assert.Equal(["Counted"], result.Select(track => track.Title));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task RecentlyListenedAlbums_IgnoresUncountedListens()
|
|
||||||
{
|
|
||||||
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 new GetRecentlyListenedAlbumsQueryHandler(Database)
|
|
||||||
.Handle(new GetRecentlyListenedAlbumsQuery(owner.Id, Limit: 10), TestContext.Current.CancellationToken);
|
|
||||||
|
|
||||||
Assert.Equal(["Counted album"], result.Value.Select(album => album.Title));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Mixes_TreatTracksWithOnlyUncountedListensAsUnheard()
|
|
||||||
{
|
|
||||||
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));
|
|
||||||
|
|
||||||
var handler = new GenerateMixesForUserCommandHandler(
|
|
||||||
Database, TestConfigurations.Mix(), NoOpLogger<GenerateMixesForUserCommandHandler>());
|
|
||||||
await handler.Handle(new GenerateMixesForUserCommand(user.Id), TestContext.Current.CancellationToken);
|
|
||||||
|
|
||||||
var mixes = await Database.Mixes.ToListAsync(TestContext.Current.CancellationToken);
|
|
||||||
Assert.Contains(mixes, mix => mix.Title == "Descubrimiento");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task TrackListensCount_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 new GetTrackByIdQueryHandler(Database)
|
|
||||||
.Handle(new GetTrackByIdQuery(track.Id), TestContext.Current.CancellationToken);
|
|
||||||
|
|
||||||
Assert.Equal(2, result.Value.ListensCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Cleanup_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 new DeleteStaleUncountedListensCommandHandler(Database, NoOpLogger<DeleteStaleUncountedListensCommandHandler>())
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user