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,98 @@
|
||||
using ErrorOr;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Services;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Tracks;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks;
|
||||
|
||||
public sealed class CreateTrackCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
|
||||
private CreateTrackCommandHandler CreateHandler()
|
||||
{
|
||||
storageService
|
||||
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ObjectMetaData("image/webp", 1024));
|
||||
|
||||
return new CreateTrackCommandHandler(
|
||||
Database,
|
||||
eventBus,
|
||||
new UploadIntentValidator(Database, storageService),
|
||||
NoOpLogger<CreateTrackCommandHandler>(),
|
||||
TestConfigurations.Storage(),
|
||||
TestConfigurations.Track(),
|
||||
TestConfigurations.UploadIntent());
|
||||
}
|
||||
|
||||
private static (Domain.Entities.UploadIntent Picture, Domain.Entities.UploadIntent Audio) SeedIntents(long userId) =>
|
||||
(TestEntities.UploadIntent(userId, UploadIntentPurpose.TrackPicture, objectName: "cover.webp"),
|
||||
TestEntities.UploadIntent(userId, UploadIntentPurpose.TrackAudio, objectName: "song.mp3"));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ValidIntents_CreatesTrackAndPublishesEvent()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var (picture, audio) = SeedIntents(user.Id);
|
||||
await SeedAsync(user, picture, audio);
|
||||
|
||||
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id);
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("My Song", result.Value.Title);
|
||||
Assert.Equal(user.Id, result.Value.OwnerUserId);
|
||||
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.CreateTrackResourcesEvent>(), Arg.Any<CancellationToken>());
|
||||
|
||||
var stored = await Database.LocalTracks.FindAsync(result.Value.Id);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal(ProcessingStatus.Pending, stored.Audio.TranscodeStatus);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var command = new CreateTrackCommand(404, "Orphan Song", Guid.NewGuid(), Guid.NewGuid());
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PictureIntentBelongsToAnotherUser_ReturnsError()
|
||||
{
|
||||
var user = TestEntities.User(1, "user");
|
||||
var otherUser = TestEntities.User(2, "other");
|
||||
var (picture, audio) = SeedIntents(otherUser.Id);
|
||||
await SeedAsync(user, otherUser, picture, audio);
|
||||
|
||||
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id);
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.True(result.IsError);
|
||||
await eventBus.DidNotReceive().PublishAsync(Arg.Any<Musify.Application.Events.CreateTrackResourcesEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AudioIntentAlreadyConsumed_ReturnsError()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var picture = TestEntities.UploadIntent(user.Id, UploadIntentPurpose.TrackPicture, objectName: "cover.webp");
|
||||
var audio = TestEntities.UploadIntent(user.Id, UploadIntentPurpose.TrackAudio, objectName: "song.mp3", status: UploadIntentStatus.Consumed);
|
||||
await SeedAsync(user, picture, audio);
|
||||
|
||||
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id);
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using ErrorOr;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Tracks;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks;
|
||||
|
||||
public sealed class DeleteTrackCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||
|
||||
private DeleteTrackCommandHandler CreateHandler() => new(Database, eventBus, NoOpLogger<DeleteTrackCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Owner_MarksTrackAsRemovingAndPublishesEvent()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.LocalTrack(owner);
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
var result = await CreateHandler().Handle(new DeleteTrackCommand(owner.Id, track.Id), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var stored = await Database.Tracks.FindAsync(track.Id);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal(LifeCycleStatus.Removing, stored.LifeCycleStatus);
|
||||
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.DeleteTrackEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new DeleteTrackCommand(1, 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 track = TestEntities.LocalTrack(owner);
|
||||
await SeedAsync(owner, stranger, track);
|
||||
|
||||
var result = await CreateHandler().Handle(new DeleteTrackCommand(stranger.Id, track.Id), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ExternalTrack_ReturnsUnauthorized()
|
||||
{
|
||||
var externalTrack = TestEntities.ExternalTrack();
|
||||
await SeedAsync(externalTrack);
|
||||
|
||||
var result = await CreateHandler().Handle(new DeleteTrackCommand(1, externalTrack.Id), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AudioStillProcessing_ReturnsConflict()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.LocalTrack(owner, audio: TestEntities.PendingAudio());
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
var result = await CreateHandler().Handle(new DeleteTrackCommand(owner.Id, track.Id), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Tracks;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks;
|
||||
|
||||
public sealed class GetListeningHistoryQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
private GetListeningHistoryQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_MultipleListens_ReturnsDistinctTracksMostRecentFirst()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var older = TestEntities.LocalTrack(owner, "Older listen");
|
||||
var newer = TestEntities.LocalTrack(owner, "Newer listen");
|
||||
await SeedAsync(
|
||||
owner, older, newer,
|
||||
TestEntities.ListeningHistory(owner.Id, older.Id, DateTime.UtcNow.AddMinutes(-10)),
|
||||
TestEntities.ListeningHistory(owner.Id, older.Id, DateTime.UtcNow.AddMinutes(-5)),
|
||||
TestEntities.ListeningHistory(owner.Id, newer.Id, DateTime.UtcNow));
|
||||
|
||||
var result = await CreateHandler().Handle(new GetListeningHistoryQuery(owner.Id), CancellationToken.None);
|
||||
|
||||
Assert.Equal(["Newer listen", "Older listen"], result.Select(track => track.Title));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoHistory_ReturnsEmpty()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetListeningHistoryQuery(owner.Id), CancellationToken.None);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using ErrorOr;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Tracks;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks;
|
||||
|
||||
public sealed class GetTrackByIdQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
private GetTrackByIdQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_LocalTrack_ReturnsItWithOwnerAsArtist()
|
||||
{
|
||||
var owner = TestEntities.User(1, "artist-name");
|
||||
var track = TestEntities.LocalTrack(owner, "My Song");
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetTrackByIdQuery(track.Id), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("My Song", result.Value.Title);
|
||||
Assert.Equal(owner.Name, result.Value.Artist);
|
||||
Assert.Equal(owner.Id, result.Value.OwnerUserId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetTrackByIdQuery(Guid.NewGuid()), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using ErrorOr;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Tracks;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks;
|
||||
|
||||
public sealed class GetTrackCoverQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
private GetTrackCoverQueryHandler CreateHandler() =>
|
||||
new(Database, TestConfigurations.Storage("covers-bucket"), TestConfigurations.Track());
|
||||
|
||||
[Theory]
|
||||
[InlineData("small", "small.webp")]
|
||||
[InlineData("medium", "medium.webp")]
|
||||
[InlineData("large", "large.webp")]
|
||||
[InlineData("unknown-size-falls-back-to-medium", "medium.webp")]
|
||||
public async Task Handle_ProcessedTrack_ReturnsRequestedSize(string size, string expectedFileName)
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.LocalTrack(owner);
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetTrackCoverQuery(track.Id, size), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("covers-bucket", result.Value.Bucket);
|
||||
Assert.EndsWith(expectedFileName, result.Value.Key);
|
||||
Assert.Equal("image/webp", result.Value.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetTrackCoverQuery(Guid.NewGuid(), "small"), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PictureNotProcessedYet_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.LocalTrack(owner, pictures: TestEntities.PendingPictures());
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetTrackCoverQuery(track.Id, "small"), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using ErrorOr;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Services;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Tracks;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks;
|
||||
|
||||
public sealed class GetTrackStreamQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
private readonly IStreamTicketService ticketService = Substitute.For<IStreamTicketService>();
|
||||
|
||||
private GetTrackStreamQueryHandler CreateHandler()
|
||||
{
|
||||
ticketService.IssueTicket(Arg.Any<long>(), Arg.Any<string>()).Returns(new StreamTicket("ticket-token", 60));
|
||||
|
||||
var issuer = new TrackStreamIssuer(
|
||||
Database,
|
||||
ticketService,
|
||||
TestConfigurations.Track(),
|
||||
TestConfigurations.StreamGateway("https://stream.musify.test"));
|
||||
|
||||
return new GetTrackStreamQueryHandler(Database, issuer, NoOpLogger<GetTrackStreamQueryHandler>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ProcessedTrack_IssuesTicketAndRecordsListen()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.LocalTrack(owner);
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetTrackStreamQuery(track.Id, owner.Id), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("ticket-token", result.Value.Ticket);
|
||||
Assert.Equal(60, result.Value.ExpiresInSeconds);
|
||||
Assert.Contains("audio-folder", result.Value.ManifestUrl);
|
||||
|
||||
var history = Assert.Single(await Database.ListeningHistories.ToListAsync());
|
||||
Assert.Equal(owner.Id, history.UserId);
|
||||
Assert.Equal(track.Id, history.TrackId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetTrackStreamQuery(Guid.NewGuid(), 1), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AudioNotProcessedYet_ReturnsConflict()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.LocalTrack(owner, audio: TestEntities.PendingAudio());
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetTrackStreamQuery(track.Id, owner.Id), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Tracks;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks;
|
||||
|
||||
public sealed class GetTracksByUserIdQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
private GetTracksByUserIdQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsOnlyTracksLinkedToThatUser()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var other = TestEntities.User(2, "other");
|
||||
var mine = TestEntities.LocalTrack(owner, "Mine");
|
||||
var theirs = TestEntities.LocalTrack(other, "Theirs");
|
||||
await SeedAsync(
|
||||
owner, other, mine, theirs,
|
||||
new UserHasTrack { UserId = owner.Id, TrackId = mine.Id },
|
||||
new UserHasTrack { UserId = other.Id, TrackId = theirs.Id });
|
||||
|
||||
var result = await CreateHandler().Handle(new GetTracksByUserIdQuery(owner.Id, Name: null, PageNumber: 1, PageSize: 10), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var track = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Mine", track.Title);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 1)]
|
||||
[InlineData(-5, 1)]
|
||||
public async Task Handle_NonPositivePageNumber_FallsBackToFirstPage(int requestedPageNumber, int expectedPageNumber)
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.LocalTrack(owner);
|
||||
await SeedAsync(owner, track, new UserHasTrack { UserId = owner.Id, TrackId = track.Id });
|
||||
|
||||
var result = await CreateHandler().Handle(new GetTracksByUserIdQuery(owner.Id, Name: null, PageNumber: requestedPageNumber, PageSize: 10), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(expectedPageNumber, result.Value.PageNumber);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Tracks;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks;
|
||||
|
||||
public sealed class GetTracksQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
private GetTracksQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoNameFilter_ReturnsEveryTrackAUserOwns()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var trackA = TestEntities.LocalTrack(owner, "Alpha");
|
||||
var trackB = TestEntities.LocalTrack(owner, "Beta");
|
||||
await SeedAsync(
|
||||
owner, trackA, trackB,
|
||||
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = trackA.Id },
|
||||
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = trackB.Id });
|
||||
|
||||
var result = await CreateHandler().Handle(new GetTracksQuery(Name: null, PageNumber: 1, PageSize: 10), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.TotalItemCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NameFilter_ReturnsOnlyMatchingTracks()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var match = TestEntities.LocalTrack(owner, "Bohemian Rhapsody");
|
||||
var other = TestEntities.LocalTrack(owner, "Imagine");
|
||||
await SeedAsync(
|
||||
owner, match, other,
|
||||
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = match.Id },
|
||||
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = other.Id });
|
||||
|
||||
var result = await CreateHandler().Handle(new GetTracksQuery(Name: "rhapsody", PageNumber: 1, PageSize: 10), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var track = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Bohemian Rhapsody", track.Title);
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
using ErrorOr;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Configuration;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Services;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Tracks;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks;
|
||||
|
||||
public sealed class RequestTrackUploadUrlsCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
|
||||
private RequestTrackUploadUrlsCommandHandler CreateHandler(UploadIntentConfiguration? uploadIntentConfig = null)
|
||||
{
|
||||
storageService
|
||||
.GetUploadUrlAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<CancellationToken>())
|
||||
.Returns("https://storage.musify.test/presigned-upload");
|
||||
|
||||
return new RequestTrackUploadUrlsCommandHandler(
|
||||
Database,
|
||||
new UploadIntentValidator(Database, storageService),
|
||||
storageService,
|
||||
NoOpLogger<RequestTrackUploadUrlsCommandHandler>(),
|
||||
TestConfigurations.Storage(),
|
||||
TestConfigurations.Track(),
|
||||
uploadIntentConfig ?? TestConfigurations.UploadIntent());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserExists_IssuesBothUploadUrlsAndCreatesIntents()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
|
||||
var command = new RequestTrackUploadUrlsCommand(user.Id, "webp", "image/webp", "mp3", "audio/mpeg");
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("https://storage.musify.test/presigned-upload", result.Value.PictureUploadUrl);
|
||||
Assert.Equal("https://storage.musify.test/presigned-upload", result.Value.AudioUploadUrl);
|
||||
|
||||
var intents = await Database.UploadIntents.Where(intent => intent.UserId == user.Id).ToListAsync();
|
||||
Assert.Equal(2, intents.Count);
|
||||
Assert.Contains(intents, intent => intent.Purpose == UploadIntentPurpose.TrackPicture);
|
||||
Assert.Contains(intents, intent => intent.Purpose == UploadIntentPurpose.TrackAudio);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var command = new RequestTrackUploadUrlsCommand(404, "webp", "image/webp", "mp3", "audio/mpeg");
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserAtActiveIntentLimit_ReturnsValidationErrorAndCreatesNothing()
|
||||
{
|
||||
var config = TestConfigurations.UploadIntent();
|
||||
config.MaxActiveUploadIntentsPerUser = 1;
|
||||
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user, TestEntities.UploadIntent(user.Id));
|
||||
|
||||
var command = new RequestTrackUploadUrlsCommand(user.Id, "webp", "image/webp", "mp3", "audio/mpeg");
|
||||
|
||||
var result = await CreateHandler(config).Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
||||
Assert.Equal(1, await Database.UploadIntents.CountAsync());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user