Design Prot Merge #1

Merged
NotS1mp merged 217 commits from design-prot into master 2026-09-21 15:48:16 +00:00
73 changed files with 3718 additions and 3645 deletions
Showing only changes of commit b5700614c1 - Show all commits
@@ -3,10 +3,10 @@ using Microsoft.AspNetCore.Http;
using Musify.Api.Authentication;
using Xunit;
namespace Musify.Api.Tests.Authentication;
public sealed class CurrentUserTests
namespace Musify.Api.Tests.Authentication
{
public sealed class CurrentUserTests
{
private static async Task<CurrentUser> BindAsync(ClaimsPrincipal principal)
{
var context = new DefaultHttpContext { User = principal };
@@ -81,4 +81,5 @@ public sealed class CurrentUserTests
Assert.Equal("https://avatar.example/jane.png", user.FindClaim("picture"));
Assert.Null(user.FindClaim("missing"));
}
}
}
@@ -6,11 +6,11 @@ using Musify.Api.Tests.TestSupport;
using Musify.Application.Albums.Responses;
using Xunit;
namespace Musify.Api.Tests.Endpoints;
[Collection(ApiCollection.Name)]
public sealed class AlbumEndpointsTests(ApiTestFixture fixture)
namespace Musify.Api.Tests.Endpoints
{
[Collection(ApiCollection.Name)]
public sealed class AlbumEndpointsTests(ApiTestFixture fixture)
{
private async Task<long> CreateUserAsync()
{
var userId = Random.Shared.NextInt64(1, long.MaxValue);
@@ -95,4 +95,5 @@ public sealed class AlbumEndpointsTests(ApiTestFixture fixture)
Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode);
Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode);
}
}
}
@@ -5,11 +5,11 @@ using Musify.Api.Tests.TestSupport;
using Musify.Application.Users.Responses;
using Xunit;
namespace Musify.Api.Tests.Endpoints;
[Collection(ApiCollection.Name)]
public sealed class UserEndpointsTests(ApiTestFixture fixture)
namespace Musify.Api.Tests.Endpoints
{
[Collection(ApiCollection.Name)]
public sealed class UserEndpointsTests(ApiTestFixture fixture)
{
[Fact]
public async Task PostUsers_NewUser_ReturnsCreatedWithLocationHeader()
{
@@ -70,4 +70,5 @@ public sealed class UserEndpointsTests(ApiTestFixture fixture)
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}
}
@@ -4,10 +4,10 @@ using Microsoft.Extensions.DependencyInjection;
using Musify.Api.Extensions;
using Xunit;
namespace Musify.Api.Tests.Extensions;
public sealed class ErrorOrHttpExtensionsTests
namespace Musify.Api.Tests.Extensions
{
public sealed class ErrorOrHttpExtensionsTests
{
// The concrete IResult types (NoContent, UnauthorizedHttpResult, ...) resolve logging/problem-
// details services from HttpContext.RequestServices when they execute, so it can't be empty.
private static readonly IServiceProvider Services = new ServiceCollection().AddLogging().AddProblemDetails().BuildServiceProvider();
@@ -134,4 +134,5 @@ public sealed class ErrorOrHttpExtensionsTests
Assert.Equal(StatusCodes.Status409Conflict, statusCode);
}
}
}
@@ -6,10 +6,10 @@ using Musify.Api.Filters;
using NSubstitute;
using Xunit;
namespace Musify.Api.Tests.Filters;
public sealed class ValidationFilterTests
namespace Musify.Api.Tests.Filters
{
public sealed class ValidationFilterTests
{
public sealed record Payload(string Name);
// EndpointFilterInvocationContext.Create has no (HttpContext, object[]) overload — only
@@ -63,4 +63,5 @@ public sealed class ValidationFilterTests
Assert.Equal("next-called", result);
await validator.DidNotReceiveWithAnyArgs().ValidateAsync(Arg.Any<Payload>(), Arg.Any<CancellationToken>());
}
}
}
@@ -11,18 +11,18 @@ using NSubstitute;
using Testcontainers.PostgreSql;
using Xunit;
namespace Musify.Api.Tests.TestSupport;
/// <summary>
/// Boots the real <c>Musify.Api</c> host — real routing, real auth/authorization pipeline,
/// real validators, real Mediator handlers — against a private Postgres container (so EF
/// queries behave like production, not like a stand-in). The services this API talks to over
/// the network (object storage, YouTube, the message bus, stream-ticket signing) are replaced
/// with NSubstitute fakes: they are Infrastructure's job to verify against the real thing (see
/// Musify.Infrastructure.Tests), not the HTTP layer's.
/// </summary>
public sealed class ApiTestFixture : WebApplicationFactory<Program>, IAsyncLifetime
namespace Musify.Api.Tests.TestSupport
{
/// <summary>
/// Boots the real <c>Musify.Api</c> host — real routing, real auth/authorization pipeline,
/// real validators, real Mediator handlers — against a private Postgres container (so EF
/// queries behave like production, not like a stand-in). The services this API talks to over
/// the network (object storage, YouTube, the message bus, stream-ticket signing) are replaced
/// with NSubstitute fakes: they are Infrastructure's job to verify against the real thing (see
/// Musify.Infrastructure.Tests), not the HTTP layer's.
/// </summary>
public sealed class ApiTestFixture : WebApplicationFactory<Program>, IAsyncLifetime
{
private readonly PostgreSqlContainer postgres = new PostgreSqlBuilder("postgres:17-alpine")
.WithDatabase("musify_api_test")
.WithUsername("postgres")
@@ -112,10 +112,11 @@ public sealed class ApiTestFixture : WebApplicationFactory<Program>, IAsyncLifet
.AddScheme<AuthenticationSchemeOptions, FakeAuthenticationHandler>(FakeAuthenticationHandler.SchemeName, _ => { });
});
}
}
}
[CollectionDefinition(Name)]
public sealed class ApiCollection : ICollectionFixture<ApiTestFixture>
{
[CollectionDefinition(Name)]
public sealed class ApiCollection : ICollectionFixture<ApiTestFixture>
{
public const string Name = "Api";
}
}
@@ -4,20 +4,20 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Musify.Api.Tests.TestSupport;
/// <summary>
/// Replaces the real JwtBearer handler in the test host: a request carrying the
/// <see cref="UserIdHeader"/> header authenticates as that user id, with no real token,
/// issuer or JWKS endpoint involved. A request without the header stays anonymous, so
/// endpoints under <c>.RequireAuthorization()</c> correctly answer 401.
/// </summary>
public sealed class FakeAuthenticationHandler(
namespace Musify.Api.Tests.TestSupport
{
/// <summary>
/// Replaces the real JwtBearer handler in the test host: a request carrying the
/// <see cref="UserIdHeader"/> header authenticates as that user id, with no real token,
/// issuer or JWKS endpoint involved. A request without the header stays anonymous, so
/// endpoints under <c>.RequireAuthorization()</c> correctly answer 401.
/// </summary>
public sealed class FakeAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder)
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
{
{
public const string SchemeName = "Test";
public const string UserIdHeader = "X-Test-User-Id";
@@ -34,4 +34,5 @@ public sealed class FakeAuthenticationHandler(
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), SchemeName);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}
}
@@ -2,10 +2,10 @@ using Musify.Api.DataTransferObjects.Albums;
using Musify.Api.Validators.Albums;
using Xunit;
namespace Musify.Api.Tests.Validators;
public sealed class CreateAlbumRequestValidatorTests
namespace Musify.Api.Tests.Validators
{
public sealed class CreateAlbumRequestValidatorTests
{
private readonly CreateAlbumRequestValidator validator = new();
[Fact]
@@ -72,4 +72,5 @@ public sealed class CreateAlbumRequestValidatorTests
Assert.False(result.IsValid);
}
}
}
@@ -2,10 +2,10 @@ using Musify.Api.DataTransferObjects.Users;
using Musify.Api.Validators.Users;
using Xunit;
namespace Musify.Api.Tests.Validators;
public sealed class CreateUserRequestValidatorTests
namespace Musify.Api.Tests.Validators
{
public sealed class CreateUserRequestValidatorTests
{
private readonly CreateUserRequestValidator validator = new();
[Fact]
@@ -48,4 +48,5 @@ public sealed class CreateUserRequestValidatorTests
Assert.True(result.IsValid);
}
}
}
@@ -5,10 +5,10 @@ using Musify.Application.Tests.TestSupport;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.Albums;
public sealed class AddTrackToAlbumCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Albums
{
public sealed class AddTrackToAlbumCommandHandlerTests : HandlerTestBase
{
private AddTrackToAlbumCommandHandler CreateHandler() => new(Database, NoOpLogger<AddTrackToAlbumCommandHandler>());
[Fact]
@@ -108,4 +108,5 @@ public sealed class AddTrackToAlbumCommandHandlerTests : HandlerTestBase
var newLink = await Database.AlbumHasTracks.SingleAsync(l => l.TrackId == secondTrack.Id, TestContext.Current.CancellationToken);
Assert.Equal(4, newLink.TrackNumber);
}
}
}
@@ -3,10 +3,10 @@ using Musify.Application.Albums;
using Musify.Application.Tests.TestSupport;
using Xunit;
namespace Musify.Application.Tests.Albums;
public sealed class CreateAlbumCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Albums
{
public sealed class CreateAlbumCommandHandlerTests : HandlerTestBase
{
private CreateAlbumCommandHandler CreateHandler() => new(Database, NoOpLogger<CreateAlbumCommandHandler>());
[Fact]
@@ -40,4 +40,5 @@ public sealed class CreateAlbumCommandHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
}
@@ -3,10 +3,10 @@ using Musify.Application.Albums;
using Musify.Application.Tests.TestSupport;
using Xunit;
namespace Musify.Application.Tests.Albums;
public sealed class DeleteAlbumCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Albums
{
public sealed class DeleteAlbumCommandHandlerTests : HandlerTestBase
{
private DeleteAlbumCommandHandler CreateHandler() => new(Database, NoOpLogger<DeleteAlbumCommandHandler>());
[Fact]
@@ -43,4 +43,5 @@ public sealed class DeleteAlbumCommandHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
Assert.NotNull(await Database.UserAlbums.FindAsync([album.Id], TestContext.Current.CancellationToken));
}
}
}
@@ -4,10 +4,10 @@ using Musify.Application.Tests.TestSupport;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.Albums;
public sealed class GetAlbumByIdQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Albums
{
public sealed class GetAlbumByIdQueryHandlerTests : HandlerTestBase
{
private GetAlbumByIdQueryHandler CreateHandler() => new(Database);
[Fact]
@@ -33,4 +33,5 @@ public sealed class GetAlbumByIdQueryHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
}
@@ -4,10 +4,10 @@ using Musify.Application.Tests.TestSupport;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.Albums;
public sealed class GetAlbumTracksQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Albums
{
public sealed class GetAlbumTracksQueryHandlerTests : HandlerTestBase
{
private GetAlbumTracksQueryHandler CreateHandler() => new(Database);
[Fact]
@@ -35,4 +35,5 @@ public sealed class GetAlbumTracksQueryHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
}
@@ -3,10 +3,10 @@ using Musify.Application.Albums;
using Musify.Application.Tests.TestSupport;
using Xunit;
namespace Musify.Application.Tests.Albums;
public sealed class GetAlbumsByUserIdQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Albums
{
public sealed class GetAlbumsByUserIdQueryHandlerTests : HandlerTestBase
{
private GetAlbumsByUserIdQueryHandler CreateHandler() => new(Database);
[Fact]
@@ -30,4 +30,5 @@ public sealed class GetAlbumsByUserIdQueryHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
}
@@ -2,10 +2,10 @@ using Musify.Application.Albums;
using Musify.Application.Tests.TestSupport;
using Xunit;
namespace Musify.Application.Tests.Albums;
public sealed class GetAlbumsQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Albums
{
public sealed class GetAlbumsQueryHandlerTests : HandlerTestBase
{
private GetAlbumsQueryHandler CreateHandler() => new(Database);
[Fact]
@@ -45,4 +45,5 @@ public sealed class GetAlbumsQueryHandlerTests : HandlerTestBase
Assert.Equal(2, result.Value.Items.Count);
Assert.True(result.Value.HasNextPage);
}
}
}
@@ -3,10 +3,10 @@ using Musify.Application.Tests.TestSupport;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.Albums;
public sealed class GetRecentlyListenedAlbumsQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Albums
{
public sealed class GetRecentlyListenedAlbumsQueryHandlerTests : HandlerTestBase
{
private GetRecentlyListenedAlbumsQueryHandler CreateHandler() => new(Database);
[Fact]
@@ -57,4 +57,5 @@ public sealed class GetRecentlyListenedAlbumsQueryHandlerTests : HandlerTestBase
Assert.False(result.IsError);
Assert.Equal(2, result.Value.Count);
}
}
}
@@ -5,10 +5,10 @@ using Musify.Application.Tests.TestSupport;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.Albums;
public sealed class RemoveTrackFromAlbumCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Albums
{
public sealed class RemoveTrackFromAlbumCommandHandlerTests : HandlerTestBase
{
private RemoveTrackFromAlbumCommandHandler CreateHandler() =>
new(Database, NoOpLogger<RemoveTrackFromAlbumCommandHandler>());
@@ -69,4 +69,5 @@ public sealed class RemoveTrackFromAlbumCommandHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
}
@@ -3,10 +3,10 @@ using Musify.Application.Albums;
using Musify.Application.Tests.TestSupport;
using Xunit;
namespace Musify.Application.Tests.Albums;
public sealed class UpdateAlbumCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Albums
{
public sealed class UpdateAlbumCommandHandlerTests : HandlerTestBase
{
private UpdateAlbumCommandHandler CreateHandler() => new(Database, NoOpLogger<UpdateAlbumCommandHandler>());
[Fact]
@@ -54,4 +54,5 @@ public sealed class UpdateAlbumCommandHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
}
}
}
@@ -8,10 +8,10 @@ using Musify.Domain.ValueObjects;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.Mixes;
public sealed class GenerateMixesForUserCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Mixes
{
public sealed class GenerateMixesForUserCommandHandlerTests : HandlerTestBase
{
private readonly IYouTubeMusicService youTubeMusicService = Substitute.For<IYouTubeMusicService>();
private GenerateMixesForUserCommandHandler CreateHandler() => new(
@@ -103,4 +103,5 @@ public sealed class GenerateMixesForUserCommandHandlerTests : HandlerTestBase
Assert.NotEmpty(items);
Assert.All(items, item => Assert.Equal(MixItemSource.Musify, item.Source));
}
}
}
@@ -3,10 +3,10 @@ using Musify.Application.Mixes;
using Musify.Application.Tests.TestSupport;
using Xunit;
namespace Musify.Application.Tests.Mixes;
public sealed class GetMixByIdQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Mixes
{
public sealed class GetMixByIdQueryHandlerTests : HandlerTestBase
{
private GetMixByIdQueryHandler CreateHandler() => new(Database);
[Fact]
@@ -44,4 +44,5 @@ public sealed class GetMixByIdQueryHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
}
@@ -2,10 +2,10 @@ using Musify.Application.Mixes;
using Musify.Application.Tests.TestSupport;
using Xunit;
namespace Musify.Application.Tests.Mixes;
public sealed class GetMixesByUserIdQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Mixes
{
public sealed class GetMixesByUserIdQueryHandlerTests : HandlerTestBase
{
private GetMixesByUserIdQueryHandler CreateHandler() => new(Database);
[Fact]
@@ -35,4 +35,5 @@ public sealed class GetMixesByUserIdQueryHandlerTests : HandlerTestBase
Assert.False(result.IsError);
Assert.Empty(result.Value);
}
}
}
@@ -5,10 +5,10 @@ using Musify.Application.Tests.TestSupport;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.PlayLists;
public sealed class AddTrackToPlayListCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.PlayLists
{
public sealed class AddTrackToPlayListCommandHandlerTests : HandlerTestBase
{
private AddTrackToPlayListCommandHandler CreateHandler() => new(Database, NoOpLogger<AddTrackToPlayListCommandHandler>());
[Fact]
@@ -89,4 +89,5 @@ public sealed class AddTrackToPlayListCommandHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
}
}
@@ -8,10 +8,10 @@ using Musify.Domain.Entities;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.PlayLists;
public sealed class AddYouTubeTrackToPlayListCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.PlayLists
{
public sealed class AddYouTubeTrackToPlayListCommandHandlerTests : HandlerTestBase
{
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
private readonly IYouTubeMusicService youTubeMusicService = Substitute.For<IYouTubeMusicService>();
@@ -101,4 +101,5 @@ public sealed class AddYouTubeTrackToPlayListCommandHandlerTests : HandlerTestBa
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
}
}
@@ -6,10 +6,10 @@ using Musify.Application.Tests.TestSupport;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.PlayLists;
public sealed class CreatePlayListCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.PlayLists
{
public sealed class CreatePlayListCommandHandlerTests : HandlerTestBase
{
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
private readonly IStorageService storageService = Substitute.For<IStorageService>();
@@ -82,4 +82,5 @@ public sealed class CreatePlayListCommandHandlerTests : HandlerTestBase
Assert.True(result.IsError);
}
}
}
@@ -6,10 +6,10 @@ using Musify.Domain.ValueObjects;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.PlayLists;
public sealed class DeletePlayListCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.PlayLists
{
public sealed class DeletePlayListCommandHandlerTests : HandlerTestBase
{
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
private DeletePlayListCommandHandler CreateHandler() => new(Database, eventBus, NoOpLogger<DeletePlayListCommandHandler>());
@@ -50,4 +50,5 @@ public sealed class DeletePlayListCommandHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
}
}
}
@@ -4,10 +4,10 @@ using Musify.Application.Tests.TestSupport;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.PlayLists;
public sealed class GetPlayListByIdQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.PlayLists
{
public sealed class GetPlayListByIdQueryHandlerTests : HandlerTestBase
{
private GetPlayListByIdQueryHandler CreateHandler() => new(Database);
[Fact]
@@ -32,4 +32,5 @@ public sealed class GetPlayListByIdQueryHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
}
@@ -3,10 +3,10 @@ using Musify.Application.PlayLists;
using Musify.Application.Tests.TestSupport;
using Xunit;
namespace Musify.Application.Tests.PlayLists;
public sealed class GetPlayListCoverQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.PlayLists
{
public sealed class GetPlayListCoverQueryHandlerTests : HandlerTestBase
{
private GetPlayListCoverQueryHandler CreateHandler() =>
new(Database, TestConfigurations.Storage("covers-bucket"), TestConfigurations.PlayList());
@@ -34,4 +34,5 @@ public sealed class GetPlayListCoverQueryHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
}
@@ -4,10 +4,10 @@ using Musify.Application.Tests.TestSupport;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.PlayLists;
public sealed class GetPlayListTracksQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.PlayLists
{
public sealed class GetPlayListTracksQueryHandlerTests : HandlerTestBase
{
private GetPlayListTracksQueryHandler CreateHandler() => new(Database);
[Fact]
@@ -35,4 +35,5 @@ public sealed class GetPlayListTracksQueryHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
}
@@ -3,10 +3,10 @@ using Musify.Application.Tests.TestSupport;
using Musify.Domain.ValueObjects;
using Xunit;
namespace Musify.Application.Tests.PlayLists;
public sealed class GetPlayListsByUserIdQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.PlayLists
{
public sealed class GetPlayListsByUserIdQueryHandlerTests : HandlerTestBase
{
private GetPlayListsByUserIdQueryHandler CreateHandler() => new(Database);
[Fact]
@@ -39,4 +39,5 @@ public sealed class GetPlayListsByUserIdQueryHandlerTests : HandlerTestBase
var playList = Assert.Single(result.Value.Items);
Assert.Equal("Road Trip", playList.Name);
}
}
}
@@ -2,10 +2,10 @@ using Musify.Application.PlayLists;
using Musify.Application.Tests.TestSupport;
using Xunit;
namespace Musify.Application.Tests.PlayLists;
public sealed class GetPlayListsQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.PlayLists
{
public sealed class GetPlayListsQueryHandlerTests : HandlerTestBase
{
private GetPlayListsQueryHandler CreateHandler() => new(Database);
[Fact]
@@ -28,4 +28,5 @@ public sealed class GetPlayListsQueryHandlerTests : HandlerTestBase
Assert.False(result.IsError);
Assert.Empty(result.Value.Items);
}
}
}
@@ -4,10 +4,10 @@ using Musify.Application.Tests.TestSupport;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.PlayLists;
public sealed class RemoveTrackFromPlayListCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.PlayLists
{
public sealed class RemoveTrackFromPlayListCommandHandlerTests : HandlerTestBase
{
private RemoveTrackFromPlayListCommandHandler CreateHandler() => new(Database, NoOpLogger<RemoveTrackFromPlayListCommandHandler>());
[Fact]
@@ -56,4 +56,5 @@ public sealed class RemoveTrackFromPlayListCommandHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
}
@@ -8,10 +8,10 @@ using Musify.Application.Tests.TestSupport;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.PlayLists;
public sealed class RequestPlayListPictureUploadCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.PlayLists
{
public sealed class RequestPlayListPictureUploadCommandHandlerTests : HandlerTestBase
{
private readonly IStorageService storageService = Substitute.For<IStorageService>();
private RequestPlayListPictureUploadCommandHandler CreateHandler(UploadIntentConfiguration? uploadIntentConfig = null)
@@ -71,4 +71,5 @@ public sealed class RequestPlayListPictureUploadCommandHandlerTests : HandlerTes
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
Assert.Empty(await Database.UploadIntents.ToListAsync(TestContext.Current.CancellationToken));
}
}
}
@@ -6,10 +6,10 @@ using Musify.Application.Tests.TestSupport;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.PlayLists;
public sealed class UpdatePlayListCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.PlayLists
{
public sealed class UpdatePlayListCommandHandlerTests : HandlerTestBase
{
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
private readonly IStorageService storageService = Substitute.For<IStorageService>();
@@ -77,4 +77,5 @@ public sealed class UpdatePlayListCommandHandlerTests : HandlerTestBase
Assert.Equal("Original Name", result.Value.Name);
Assert.Equal("Original description", result.Value.Description);
}
}
}
@@ -5,10 +5,10 @@ using Musify.Application.Tests.TestSupport;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.Services;
public sealed class TrackStreamIssuerTests : HandlerTestBase
namespace Musify.Application.Tests.Services
{
public sealed class TrackStreamIssuerTests : HandlerTestBase
{
private readonly IStreamTicketService ticketService = Substitute.For<IStreamTicketService>();
private TrackStreamIssuer CreateIssuer() => new(
@@ -47,4 +47,5 @@ public sealed class TrackStreamIssuerTests : HandlerTestBase
ticketService.Received(1).IssueTicket(1, "Tracks/ProcessedAudios/folder-42/");
}
}
}
@@ -6,10 +6,10 @@ using Musify.Domain.ValueObjects;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.Services;
public sealed class UploadIntentValidatorTests : HandlerTestBase
namespace Musify.Application.Tests.Services
{
public sealed class UploadIntentValidatorTests : HandlerTestBase
{
private readonly IStorageService storageService = Substitute.For<IStorageService>();
private readonly Domain.Entities.User owner = TestEntities.User();
@@ -142,4 +142,5 @@ public sealed class UploadIntentValidatorTests : HandlerTestBase
Assert.False(result.IsError);
}
}
}
@@ -5,10 +5,10 @@ using Musify.Application.Tests.TestSupport;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.Services;
public sealed class YouTubeTrackProvisionerTests : HandlerTestBase
namespace Musify.Application.Tests.Services
{
public sealed class YouTubeTrackProvisionerTests : HandlerTestBase
{
private readonly IYouTubeMusicService youTubeMusicService = Substitute.For<IYouTubeMusicService>();
private YouTubeTrackProvisioner CreateProvisioner() =>
@@ -100,4 +100,5 @@ public sealed class YouTubeTrackProvisionerTests : HandlerTestBase
Assert.Single(await Database.AlbumHasTracks.ToListAsync(TestContext.Current.CancellationToken));
Assert.Single(await Database.ExternalAlbums.ToListAsync(TestContext.Current.CancellationToken));
}
}
}
@@ -1,10 +1,10 @@
using Musify.Application.Shared;
using Xunit;
namespace Musify.Application.Tests.Shared;
public sealed class StorageKeyTests
namespace Musify.Application.Tests.Shared
{
public sealed class StorageKeyTests
{
[Fact]
public void Combine_MultipleSegments_JoinsThemWithForwardSlashes()
{
@@ -37,4 +37,5 @@ public sealed class StorageKeyTests
{
Assert.Equal(string.Empty, StorageKey.Combine());
}
}
}
@@ -4,10 +4,10 @@ using Musify.Domain.Entities;
using Musify.Domain.ValueObjects;
using Xunit;
namespace Musify.Application.Tests.Shared;
public sealed class TrackApplicationResponseTests
namespace Musify.Application.Tests.Shared
{
public sealed class TrackApplicationResponseTests
{
[Fact]
public void FromEntity_LocalTrack_UsesOwnerNameAsArtist()
{
@@ -49,4 +49,5 @@ public sealed class TrackApplicationResponseTests
Assert.Null(response.Artist);
}
}
}
@@ -2,16 +2,16 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace Musify.Application.Tests.TestSupport;
/// <summary>
/// Base class for handler tests: gives every test method its own empty, isolated
/// <see cref="TestDatabase"/> (xUnit creates a new instance of the test class per test method,
/// so there is no state to reset between tests) and a no-op logger factory for handlers that
/// take an <see cref="ILogger{TCategoryName}"/>.
/// </summary>
public abstract class HandlerTestBase : IAsyncLifetime
namespace Musify.Application.Tests.TestSupport
{
/// <summary>
/// Base class for handler tests: gives every test method its own empty, isolated
/// <see cref="TestDatabase"/> (xUnit creates a new instance of the test class per test method,
/// so there is no state to reset between tests) and a no-op logger factory for handlers that
/// take an <see cref="ILogger{TCategoryName}"/>.
/// </summary>
public abstract class HandlerTestBase : IAsyncLifetime
{
private TestDatabase database = null!;
protected TestDatabase Database => database;
@@ -30,4 +30,5 @@ public abstract class HandlerTestBase : IAsyncLifetime
await Database.SaveChangesAsync(CancellationToken.None);
}
}
}
@@ -1,13 +1,13 @@
using Musify.Application.Configuration;
namespace Musify.Application.Tests.TestSupport;
/// <summary>
/// Configuration POCOs with every <c>required</c> member filled and the rest left at their
/// production defaults, so handler tests don't have to know which fields matter for them.
/// </summary>
public static class TestConfigurations
namespace Musify.Application.Tests.TestSupport
{
/// <summary>
/// Configuration POCOs with every <c>required</c> member filled and the rest left at their
/// production defaults, so handler tests don't have to know which fields matter for them.
/// </summary>
public static class TestConfigurations
{
public static ApplicationStorageConfiguration Storage(string bucket = "test-bucket") =>
new() { Bucket = bucket };
@@ -21,4 +21,5 @@ public static class TestConfigurations
public static StreamGatewayConfiguration StreamGateway(string publicBaseUrl = "https://stream.musify.test") =>
new() { PublicBaseUrl = publicBaseUrl };
}
}
@@ -3,29 +3,29 @@ using Microsoft.EntityFrameworkCore;
using Musify.Application.Contracts;
using Musify.Domain.Entities;
namespace Musify.Application.Tests.TestSupport;
/// <summary>
/// An <see cref="IDatabase"/> backed by a private, in-memory SQLite database.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="IDatabase"/> exposes raw <see cref="DbSet{TEntity}"/> properties, so handlers
/// compose real LINQ queries (<c>Where</c>, <c>Select</c>, <c>Include</c>, ...) against it.
/// Substituting <see cref="IDatabase"/> with a mock cannot express that — there is no
/// <see cref="IQueryable{T}"/> provider behind a mock. SQLite's in-memory mode gives us a real
/// relational engine instead: fast, no external dependency, and — unlike the EF Core InMemory
/// provider — it actually supports transactions, which two handlers rely on.
/// </para>
/// <para>
/// The model mirrors the parts of <c>Musify.Infrastructure.Persistence.Database</c> that affect
/// query shape (owned types, table-per-type inheritance, unique indexes). MassTransit outbox/saga
/// state and Postgres-specific configuration are left out — the Application layer never touches
/// them.
/// </para>
/// </remarks>
public sealed class TestDatabase : DbContext, IDatabase
namespace Musify.Application.Tests.TestSupport
{
/// <summary>
/// An <see cref="IDatabase"/> backed by a private, in-memory SQLite database.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="IDatabase"/> exposes raw <see cref="DbSet{TEntity}"/> properties, so handlers
/// compose real LINQ queries (<c>Where</c>, <c>Select</c>, <c>Include</c>, ...) against it.
/// Substituting <see cref="IDatabase"/> with a mock cannot express that — there is no
/// <see cref="IQueryable{T}"/> provider behind a mock. SQLite's in-memory mode gives us a real
/// relational engine instead: fast, no external dependency, and — unlike the EF Core InMemory
/// provider — it actually supports transactions, which two handlers rely on.
/// </para>
/// <para>
/// The model mirrors the parts of <c>Musify.Infrastructure.Persistence.Database</c> that affect
/// query shape (owned types, table-per-type inheritance, unique indexes). MassTransit outbox/saga
/// state and Postgres-specific configuration are left out — the Application layer never touches
/// them.
/// </para>
/// </remarks>
public sealed class TestDatabase : DbContext, IDatabase
{
private readonly SqliteConnection connection;
private TestDatabase(SqliteConnection connection, DbContextOptions<TestDatabase> options)
@@ -208,4 +208,5 @@ public sealed class TestDatabase : DbContext, IDatabase
public ValueTask DisposeAsync() => inner.DisposeAsync();
}
}
}
@@ -1,15 +1,15 @@
using Musify.Domain.Entities;
using Musify.Domain.ValueObjects;
namespace Musify.Application.Tests.TestSupport;
/// <summary>
/// Builds fully-populated domain entities for tests, with sensible defaults for every
/// <c>required</c> member. Every factory always returns a valid instance — never null — so
/// callers never need a null check on what this class hands back.
/// </summary>
public static class TestEntities
namespace Musify.Application.Tests.TestSupport
{
/// <summary>
/// Builds fully-populated domain entities for tests, with sensible defaults for every
/// <c>required</c> member. Every factory always returns a valid instance — never null — so
/// callers never need a null check on what this class hands back.
/// </summary>
public static class TestEntities
{
public static User User(
long id = 1,
string name = "test-user",
@@ -234,4 +234,5 @@ public static class TestEntities
TrackId = trackId,
ListenedAt = listenedAt ?? DateTime.UtcNow
};
}
}
@@ -7,10 +7,10 @@ using Musify.Domain.ValueObjects;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.Tracks;
public sealed class CreateTrackCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Tracks
{
public sealed class CreateTrackCommandHandlerTests : HandlerTestBase
{
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
private readonly IStorageService storageService = Substitute.For<IStorageService>();
@@ -95,4 +95,5 @@ public sealed class CreateTrackCommandHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
}
}
@@ -6,10 +6,10 @@ using Musify.Domain.ValueObjects;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.Tracks;
public sealed class DeleteTrackCommandHandlerTests : HandlerTestBase
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>());
@@ -73,4 +73,5 @@ public sealed class DeleteTrackCommandHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
}
}
@@ -2,10 +2,10 @@ using Musify.Application.Tests.TestSupport;
using Musify.Application.Tracks;
using Xunit;
namespace Musify.Application.Tests.Tracks;
public sealed class GetListeningHistoryQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Tracks
{
public sealed class GetListeningHistoryQueryHandlerTests : HandlerTestBase
{
private GetListeningHistoryQueryHandler CreateHandler() => new(Database);
[Fact]
@@ -35,4 +35,5 @@ public sealed class GetListeningHistoryQueryHandlerTests : HandlerTestBase
Assert.Empty(result);
}
}
}
@@ -3,10 +3,10 @@ using Musify.Application.Tests.TestSupport;
using Musify.Application.Tracks;
using Xunit;
namespace Musify.Application.Tests.Tracks;
public sealed class GetTrackByIdQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Tracks
{
public sealed class GetTrackByIdQueryHandlerTests : HandlerTestBase
{
private GetTrackByIdQueryHandler CreateHandler() => new(Database);
[Fact]
@@ -31,4 +31,5 @@ public sealed class GetTrackByIdQueryHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
}
@@ -3,10 +3,10 @@ using Musify.Application.Tests.TestSupport;
using Musify.Application.Tracks;
using Xunit;
namespace Musify.Application.Tests.Tracks;
public sealed class GetTrackCoverQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Tracks
{
public sealed class GetTrackCoverQueryHandlerTests : HandlerTestBase
{
private GetTrackCoverQueryHandler CreateHandler() =>
new(Database, TestConfigurations.Storage("covers-bucket"), TestConfigurations.Track());
@@ -48,4 +48,5 @@ public sealed class GetTrackCoverQueryHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
}
@@ -7,10 +7,10 @@ using Musify.Application.Tracks;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.Tracks;
public sealed class GetTrackStreamQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Tracks
{
public sealed class GetTrackStreamQueryHandlerTests : HandlerTestBase
{
private readonly IStreamTicketService ticketService = Substitute.For<IStreamTicketService>();
private GetTrackStreamQueryHandler CreateHandler()
@@ -64,4 +64,5 @@ public sealed class GetTrackStreamQueryHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
}
}
@@ -3,10 +3,10 @@ using Musify.Application.Tracks;
using Musify.Domain.Entities;
using Xunit;
namespace Musify.Application.Tests.Tracks;
public sealed class GetTracksByUserIdQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Tracks
{
public sealed class GetTracksByUserIdQueryHandlerTests : HandlerTestBase
{
private GetTracksByUserIdQueryHandler CreateHandler() => new(Database);
[Fact]
@@ -42,4 +42,5 @@ public sealed class GetTracksByUserIdQueryHandlerTests : HandlerTestBase
Assert.False(result.IsError);
Assert.Equal(expectedPageNumber, result.Value.PageNumber);
}
}
}
@@ -2,10 +2,10 @@ using Musify.Application.Tests.TestSupport;
using Musify.Application.Tracks;
using Xunit;
namespace Musify.Application.Tests.Tracks;
public sealed class GetTracksQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Tracks
{
public sealed class GetTracksQueryHandlerTests : HandlerTestBase
{
private GetTracksQueryHandler CreateHandler() => new(Database);
[Fact]
@@ -42,4 +42,5 @@ public sealed class GetTracksQueryHandlerTests : HandlerTestBase
var track = Assert.Single(result.Value.Items);
Assert.Equal("Bohemian Rhapsody", track.Title);
}
}
}
@@ -9,10 +9,10 @@ using Musify.Domain.ValueObjects;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.Tracks;
public sealed class RequestTrackUploadUrlsCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Tracks
{
public sealed class RequestTrackUploadUrlsCommandHandlerTests : HandlerTestBase
{
private readonly IStorageService storageService = Substitute.For<IStorageService>();
private RequestTrackUploadUrlsCommandHandler CreateHandler(UploadIntentConfiguration? uploadIntentConfig = null)
@@ -77,4 +77,5 @@ public sealed class RequestTrackUploadUrlsCommandHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
Assert.Equal(1, await Database.UploadIntents.CountAsync(TestContext.Current.CancellationToken));
}
}
}
@@ -3,10 +3,10 @@ using Musify.Application.Tests.TestSupport;
using Musify.Application.Users;
using Xunit;
namespace Musify.Application.Tests.Users;
public sealed class CreateUserCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Users
{
public sealed class CreateUserCommandHandlerTests : HandlerTestBase
{
private CreateUserCommandHandler CreateHandler() => new(Database, NoOpLogger<CreateUserCommandHandler>());
[Fact]
@@ -37,4 +37,5 @@ public sealed class CreateUserCommandHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
}
}
}
@@ -3,10 +3,10 @@ using Musify.Application.Tests.TestSupport;
using Musify.Application.Users;
using Xunit;
namespace Musify.Application.Tests.Users;
public sealed class GetUserByIdQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Users
{
public sealed class GetUserByIdQueryHandlerTests : HandlerTestBase
{
private GetUserByIdQueryHandler CreateHandler() => new(Database);
[Fact]
@@ -28,4 +28,5 @@ public sealed class GetUserByIdQueryHandlerTests : HandlerTestBase
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
}
}
}
@@ -2,10 +2,10 @@ using Musify.Application.Tests.TestSupport;
using Musify.Application.Users;
using Xunit;
namespace Musify.Application.Tests.Users;
public sealed class GetUsersQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Users
{
public sealed class GetUsersQueryHandlerTests : HandlerTestBase
{
private GetUsersQueryHandler CreateHandler() => new(Database);
[Fact]
@@ -30,4 +30,5 @@ public sealed class GetUsersQueryHandlerTests : HandlerTestBase
var user = Assert.Single(result.Value.Items);
Assert.Equal(1, user.Id);
}
}
}
@@ -2,10 +2,10 @@ using Musify.Application.Tests.TestSupport;
using Musify.Application.Users;
using Xunit;
namespace Musify.Application.Tests.Users;
public sealed class SyncUserCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.Users
{
public sealed class SyncUserCommandHandlerTests : HandlerTestBase
{
private SyncUserCommandHandler CreateHandler() => new(Database, NoOpLogger<SyncUserCommandHandler>());
[Fact]
@@ -39,4 +39,5 @@ public sealed class SyncUserCommandHandlerTests : HandlerTestBase
Assert.Equal("NEW NAME", stored.NormalizedName);
Assert.Equal("https://avatar.example/new.png", stored.ProfilePictureUrl);
}
}
}
@@ -7,10 +7,10 @@ using Musify.Application.YouTube;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.YouTube;
public sealed class GetYouTubeAlbumQueryHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.YouTube
{
public sealed class GetYouTubeAlbumQueryHandlerTests : HandlerTestBase
{
private readonly IYouTubeMusicService youTubeMusicService = Substitute.For<IYouTubeMusicService>();
private GetYouTubeAlbumQueryHandler CreateHandler()
@@ -58,4 +58,5 @@ public sealed class GetYouTubeAlbumQueryHandlerTests : HandlerTestBase
Assert.True(result.IsError);
}
}
}
@@ -7,10 +7,10 @@ using Musify.Application.YouTube.Responses;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.YouTube;
public sealed class ResolveYouTubeTrackStreamCommandHandlerTests : HandlerTestBase
namespace Musify.Application.Tests.YouTube
{
public sealed class ResolveYouTubeTrackStreamCommandHandlerTests : HandlerTestBase
{
private readonly IYouTubeMusicService youTubeMusicService = Substitute.For<IYouTubeMusicService>();
private readonly IStreamTicketService ticketService = Substitute.For<IStreamTicketService>();
@@ -62,4 +62,5 @@ public sealed class ResolveYouTubeTrackStreamCommandHandlerTests : HandlerTestBa
var history = Assert.Single(await Database.ListeningHistories.ToListAsync(TestContext.Current.CancellationToken));
Assert.Equal(42, history.UserId);
}
}
}
@@ -4,10 +4,10 @@ using Musify.Application.YouTube;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.YouTube;
public sealed class SearchYouTubeAlbumsQueryHandlerTests
namespace Musify.Application.Tests.YouTube
{
public sealed class SearchYouTubeAlbumsQueryHandlerTests
{
private readonly IYouTubeMusicService youTubeMusicService = Substitute.For<IYouTubeMusicService>();
private SearchYouTubeAlbumsQueryHandler CreateHandler() => new(youTubeMusicService);
@@ -33,4 +33,5 @@ public sealed class SearchYouTubeAlbumsQueryHandlerTests
Assert.False(result.IsError);
Assert.Same(expected, result.Value);
}
}
}
@@ -4,10 +4,10 @@ using Musify.Application.YouTube;
using NSubstitute;
using Xunit;
namespace Musify.Application.Tests.YouTube;
public sealed class SearchYouTubeTracksQueryHandlerTests
namespace Musify.Application.Tests.YouTube
{
public sealed class SearchYouTubeTracksQueryHandlerTests
{
private readonly IYouTubeMusicService youTubeMusicService = Substitute.For<IYouTubeMusicService>();
private SearchYouTubeTracksQueryHandler CreateHandler() => new(youTubeMusicService);
@@ -33,4 +33,5 @@ public sealed class SearchYouTubeTracksQueryHandlerTests
Assert.False(result.IsError);
Assert.Same(expected, result.Value);
}
}
}
@@ -1,10 +1,10 @@
using Musify.Domain.ValueObjects;
using Xunit;
namespace Musify.Domain.Tests.ValueObjects;
public sealed class TrackAudioTests
namespace Musify.Domain.Tests.ValueObjects
{
public sealed class TrackAudioTests
{
[Theory]
[InlineData(ProcessingStatus.Pending, false, false, true)]
[InlineData(ProcessingStatus.Processing, false, false, true)]
@@ -19,4 +19,5 @@ public sealed class TrackAudioTests
Assert.Equal(expectedFailed, audio.IsFailed);
Assert.Equal(expectedInProgress, audio.IsInProgress);
}
}
}
@@ -1,10 +1,10 @@
using Musify.Domain.ValueObjects;
using Xunit;
namespace Musify.Domain.Tests.ValueObjects;
public sealed class TrackPicturesTests
namespace Musify.Domain.Tests.ValueObjects
{
public sealed class TrackPicturesTests
{
[Theory]
[InlineData(ProcessingStatus.Pending, false)]
[InlineData(ProcessingStatus.Processing, false)]
@@ -16,4 +16,5 @@ public sealed class TrackPicturesTests
Assert.Equal(expected, pictures.IsProcessed);
}
}
}
@@ -2,11 +2,11 @@ using Musify.Domain.Entities;
using Musify.Infrastructure.Tests.TestSupport;
using Xunit;
namespace Musify.Infrastructure.Tests.Persistence;
[Collection(InfrastructureCollection.Name)]
public sealed class AuditableEntityInterceptorTests(InfrastructureTestFixture fixture)
namespace Musify.Infrastructure.Tests.Persistence
{
[Collection(InfrastructureCollection.Name)]
public sealed class AuditableEntityInterceptorTests(InfrastructureTestFixture fixture)
{
[Fact]
public async Task SavingChanges_NewEntity_SetsCreatedAtAndUpdatedAtToNow()
{
@@ -42,4 +42,5 @@ public sealed class AuditableEntityInterceptorTests(InfrastructureTestFixture fi
Assert.True(user.UpdatedAt > originalCreatedAt,
$"original-created-at={originalCreatedAt:O}, final-updated-at={user.UpdatedAt:O}");
}
}
}
@@ -4,11 +4,11 @@ using Musify.Domain.ValueObjects;
using Musify.Infrastructure.Tests.TestSupport;
using Xunit;
namespace Musify.Infrastructure.Tests.Persistence;
[Collection(InfrastructureCollection.Name)]
public sealed class DatabaseTests(InfrastructureTestFixture fixture)
namespace Musify.Infrastructure.Tests.Persistence
{
[Collection(InfrastructureCollection.Name)]
public sealed class DatabaseTests(InfrastructureTestFixture fixture)
{
[Fact]
public async Task SaveChangesAsync_LocalTrackWithOwnedTypes_RoundTripsThroughPostgres()
{
@@ -88,4 +88,5 @@ public sealed class DatabaseTests(InfrastructureTestFixture fixture)
Pictures = new TrackPictures(),
Audio = new TrackAudio()
};
}
}
@@ -2,10 +2,10 @@ using ErrorOr;
using Musify.Infrastructure.Services;
using Xunit;
namespace Musify.Infrastructure.Tests.Services;
public sealed class DisabledYouTubeMusicServiceTests
namespace Musify.Infrastructure.Tests.Services
{
public sealed class DisabledYouTubeMusicServiceTests
{
private readonly DisabledYouTubeMusicService service = new();
[Fact]
@@ -55,4 +55,5 @@ public sealed class DisabledYouTubeMusicServiceTests
Assert.Equal("https://img.example/thumb.jpg", result);
}
}
}
@@ -4,10 +4,10 @@ using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
namespace Musify.Infrastructure.Tests.Services;
public sealed class PictureServiceTests
namespace Musify.Infrastructure.Tests.Services
{
public sealed class PictureServiceTests
{
private static PictureService CreateService() => new(NullLogger<PictureService>.Instance);
private static MemoryStream CreateSourceImage(int width, int height)
@@ -54,4 +54,5 @@ public sealed class PictureServiceTests
Assert.Equal(100, result.Width);
Assert.Equal(50, result.Height);
}
}
}
@@ -7,11 +7,11 @@ using Musify.Infrastructure.Services;
using Musify.Infrastructure.Tests.TestSupport;
using Xunit;
namespace Musify.Infrastructure.Tests.Services;
[Collection(InfrastructureCollection.Name)]
public sealed class PlayListPresetSeederTests(InfrastructureTestFixture fixture)
namespace Musify.Infrastructure.Tests.Services
{
[Collection(InfrastructureCollection.Name)]
public sealed class PlayListPresetSeederTests(InfrastructureTestFixture fixture)
{
private static PlayListConfiguration NewConfiguration(bool overwritePresetPictures) => new()
{
Routes = new PlayListRoutes { ParentFolders = $"PlayLists-{Guid.NewGuid():N}" },
@@ -99,4 +99,5 @@ public sealed class PlayListPresetSeederTests(InfrastructureTestFixture fixture)
InfrastructureTestFixture.S3Bucket, configuration.Routes.PresetSmallPicturePath, TestContext.Current.CancellationToken);
Assert.Null(metadata);
}
}
}
@@ -3,10 +3,10 @@ using Microsoft.Extensions.Caching.Memory;
using Musify.Infrastructure.Services;
using Xunit;
namespace Musify.Infrastructure.Tests.Services;
public sealed class SingleFlightCacheTests
namespace Musify.Infrastructure.Tests.Services
{
public sealed class SingleFlightCacheTests
{
private static SingleFlightCache CreateCache() => new(new MemoryCache(new MemoryCacheOptions()));
[Fact]
@@ -77,4 +77,5 @@ public sealed class SingleFlightCacheTests
Assert.Equal(2, calls);
}
}
}
@@ -8,11 +8,11 @@ using Musify.Infrastructure.Services;
using Musify.Infrastructure.Tests.TestSupport;
using Xunit;
namespace Musify.Infrastructure.Tests.Services;
[Collection(InfrastructureCollection.Name)]
public sealed class StorageServiceTests(InfrastructureTestFixture fixture) : IAsyncLifetime
namespace Musify.Infrastructure.Tests.Services
{
[Collection(InfrastructureCollection.Name)]
public sealed class StorageServiceTests(InfrastructureTestFixture fixture) : IAsyncLifetime
{
private Database database = null!;
private StorageService service = null!;
@@ -148,4 +148,5 @@ public sealed class StorageServiceTests(InfrastructureTestFixture fixture) : IAs
Assert.NotNull(metadata);
Assert.Equal("presigned content".Length, metadata.ContentLength);
}
}
}
@@ -5,10 +5,10 @@ using Musify.Infrastructure.Configuration;
using Musify.Infrastructure.Services;
using Xunit;
namespace Musify.Infrastructure.Tests.Services;
public sealed class StreamTicketServiceTests : IDisposable
namespace Musify.Infrastructure.Tests.Services
{
public sealed class StreamTicketServiceTests : IDisposable
{
private readonly string privateKeyPath = Path.GetTempFileName();
private readonly RSA rsa = RSA.Create(2048);
@@ -68,4 +68,5 @@ public sealed class StreamTicketServiceTests : IDisposable
Assert.True(result.IsValid);
}
}
}
@@ -10,17 +10,17 @@ using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Containers;
using Xunit;
namespace Musify.Infrastructure.Tests.TestSupport;
/// <summary>
/// One Postgres container and one SeaweedFS container (same image and startup
/// command as <c>deploy/compose.yml</c>), started once for every test in the
/// <see cref="InfrastructureCollection"/> and torn down when the run finishes.
/// EF Core migrations run once here too, against the real <c>Database</c>
/// DbContext, so a broken migration fails the whole run immediately.
/// </summary>
public sealed class InfrastructureTestFixture : IAsyncLifetime
namespace Musify.Infrastructure.Tests.TestSupport
{
/// <summary>
/// One Postgres container and one SeaweedFS container (same image and startup
/// command as <c>deploy/compose.yml</c>), started once for every test in the
/// <see cref="InfrastructureCollection"/> and torn down when the run finishes.
/// EF Core migrations run once here too, against the real <c>Database</c>
/// DbContext, so a broken migration fails the whole run immediately.
/// </summary>
public sealed class InfrastructureTestFixture : IAsyncLifetime
{
public const string S3AccessKey = "musify-test";
public const string S3SecretKey = "musify-test-secret";
public const string S3Bucket = "musify-test-bucket";
@@ -108,10 +108,11 @@ public sealed class InfrastructureTestFixture : IAsyncLifetime
exec weed server -ip.bind=0.0.0.0 -dir=/data -master.volumeSizeLimitMB=1024 -volume.max=0 -filer -s3 -s3.config=/tmp/s3.conf
""";
}
}
}
[CollectionDefinition(Name)]
public sealed class InfrastructureCollection : ICollectionFixture<InfrastructureTestFixture>
{
[CollectionDefinition(Name)]
public sealed class InfrastructureCollection : ICollectionFixture<InfrastructureTestFixture>
{
public const string Name = "Infrastructure";
}
}
@@ -6,10 +6,10 @@ using Musify.StreamingGateway.Authentication;
using Musify.StreamingGateway.Configuration;
using Xunit;
namespace Musify.StreamingGateway.Tests.Authentication;
public sealed class TicketValidatorTests : IDisposable
namespace Musify.StreamingGateway.Tests.Authentication
{
public sealed class TicketValidatorTests : IDisposable
{
private const string Issuer = "musify-webapi";
private const string Audience = "media-gateway";
@@ -143,4 +143,5 @@ public sealed class TicketValidatorTests : IDisposable
Assert.Equal(string.Empty, prefix);
}
}
}
@@ -9,10 +9,10 @@ using Musify.StreamingGateway.Configuration;
using Musify.StreamingGateway.Middleware;
using Xunit;
namespace Musify.StreamingGateway.Tests.Middleware;
public sealed class TicketValidationMiddlewareTests : IDisposable
namespace Musify.StreamingGateway.Tests.Middleware
{
public sealed class TicketValidationMiddlewareTests : IDisposable
{
private const string Prefix = "Tracks/ProcessedAudios/abc";
private readonly string publicKeyPath = Path.GetTempFileName();
@@ -150,4 +150,5 @@ public sealed class TicketValidationMiddlewareTests : IDisposable
Assert.True(wasNextCalled());
}
}
}