diff --git a/backend/Tests/Musify.Api.Tests/Extensions/ErrorOrHttpExtensionsTests.cs b/backend/Tests/Musify.Api.Tests/Extensions/ErrorOrHttpExtensionsTests.cs index 86a7c4e..773c1ca 100644 --- a/backend/Tests/Musify.Api.Tests/Extensions/ErrorOrHttpExtensionsTests.cs +++ b/backend/Tests/Musify.Api.Tests/Extensions/ErrorOrHttpExtensionsTests.cs @@ -8,8 +8,6 @@ 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(); private static async Task<(int StatusCode, string Body)> ExecuteAsync(IResult result) diff --git a/backend/Tests/Musify.Api.Tests/Filters/ValidationFilterTests.cs b/backend/Tests/Musify.Api.Tests/Filters/ValidationFilterTests.cs index 32fbe2f..8b16cfe 100644 --- a/backend/Tests/Musify.Api.Tests/Filters/ValidationFilterTests.cs +++ b/backend/Tests/Musify.Api.Tests/Filters/ValidationFilterTests.cs @@ -12,11 +12,6 @@ namespace Musify.Api.Tests.Filters { public sealed record Payload(string Name); - // EndpointFilterInvocationContext.Create has no (HttpContext, object[]) overload — only - // Create, Create, etc. Passing an object[] to a params-object[] call site binds - // to Create with T0 inferred as object[] itself, so .Arguments ends up with one element - // (the array, boxed) instead of the array's contents — OfType() then never matches - // and the filter silently falls through to "no argument, skip validation" for every case. private static EndpointFilterInvocationContext CreateContext(T argument) => EndpointFilterInvocationContext.Create(new DefaultHttpContext(), argument); diff --git a/backend/Tests/Musify.Api.Tests/TestSupport/ApiTestFixture.cs b/backend/Tests/Musify.Api.Tests/TestSupport/ApiTestFixture.cs index 0ed2877..00bc5e7 100644 --- a/backend/Tests/Musify.Api.Tests/TestSupport/ApiTestFixture.cs +++ b/backend/Tests/Musify.Api.Tests/TestSupport/ApiTestFixture.cs @@ -13,14 +13,6 @@ using Xunit; namespace Musify.Api.Tests.TestSupport { - /// - /// Boots the real Musify.Api 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. - /// public sealed class ApiTestFixture : WebApplicationFactory, IAsyncLifetime { private readonly PostgreSqlContainer postgres = new PostgreSqlBuilder("postgres:17-alpine") @@ -46,20 +38,14 @@ namespace Musify.Api.Tests.TestSupport await database.Database.MigrateAsync(); } - // xUnit v3's IAsyncLifetime IS System.IAsyncDisposable (rather than declaring its own - // Task-returning DisposeAsync as in v2), so there is a single DisposeAsync slot to fill — - // override the one WebApplicationFactory already provides instead of implementing the - // interface explicitly alongside it. public override async ValueTask DisposeAsync() { await base.DisposeAsync(); await postgres.DisposeAsync(); } - /// An with no auth header — every protected endpoint answers 401. public HttpClient CreateAnonymousClient() => CreateClient(); - /// An authenticated as the given user id (see ). public HttpClient CreateAuthenticatedClient(long userId, string userName = "test-user") { var client = CreateClient(); @@ -105,8 +91,6 @@ namespace Musify.Api.Tests.TestSupport services.Replace(ServiceDescriptor.Singleton(YouTubeMusicService)); services.Replace(ServiceDescriptor.Singleton(StreamTicketService)); - // Overrides the real JwtBearer scheme as the default, so requests authenticate via - // FakeAuthenticationHandler instead of needing a real token and a reachable IdP. services .AddAuthentication(FakeAuthenticationHandler.SchemeName) .AddScheme(FakeAuthenticationHandler.SchemeName, _ => { }); diff --git a/backend/Tests/Musify.Api.Tests/TestSupport/FakeAuthenticationHandler.cs b/backend/Tests/Musify.Api.Tests/TestSupport/FakeAuthenticationHandler.cs index d35009e..497bc7b 100644 --- a/backend/Tests/Musify.Api.Tests/TestSupport/FakeAuthenticationHandler.cs +++ b/backend/Tests/Musify.Api.Tests/TestSupport/FakeAuthenticationHandler.cs @@ -6,12 +6,6 @@ using Microsoft.Extensions.Options; namespace Musify.Api.Tests.TestSupport { - /// - /// Replaces the real JwtBearer handler in the test host: a request carrying the - /// 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 .RequireAuthorization() correctly answer 401. - /// public sealed class FakeAuthenticationHandler( IOptionsMonitor options, ILoggerFactory logger, diff --git a/backend/Tests/Musify.Application.Tests/Mixes/GenerateMixesForUserCommandHandlerTests.cs b/backend/Tests/Musify.Application.Tests/Mixes/GenerateMixesForUserCommandHandlerTests.cs index f6fddfd..5d0405c 100644 --- a/backend/Tests/Musify.Application.Tests/Mixes/GenerateMixesForUserCommandHandlerTests.cs +++ b/backend/Tests/Musify.Application.Tests/Mixes/GenerateMixesForUserCommandHandlerTests.cs @@ -78,9 +78,6 @@ namespace Musify.Application.Tests.Mixes [Fact] public async Task Handle_YouTubeSourceUnavailable_StillCreatesMixesFromLocalCandidatesOnly() { - // Simulates IYouTubeMusicService.SearchSongsAsync as it behaves when the YouTube source is - // disabled (DisabledYouTubeMusicService, see Infrastructure): every call fails. Mix - // generation must degrade to local-only candidates instead of failing the whole command. youTubeMusicService .SearchSongsAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Error.Forbidden(description: "The YouTube Music source is disabled.")); diff --git a/backend/Tests/Musify.Application.Tests/Shared/TrackApplicationResponseTests.cs b/backend/Tests/Musify.Application.Tests/Shared/TrackApplicationResponseTests.cs index 151137d..5141a69 100644 --- a/backend/Tests/Musify.Application.Tests/Shared/TrackApplicationResponseTests.cs +++ b/backend/Tests/Musify.Application.Tests/Shared/TrackApplicationResponseTests.cs @@ -26,8 +26,8 @@ namespace Musify.Application.Tests.Shared public void FromEntity_ExternalTrackWithArtists_JoinsThemInPositionOrder() { var track = TestEntities.ExternalTrack(); - var second = new Artist { Name = "Second Artist", NormalizedName = "SECOND ARTIST", ExternalId = "second" }; - var first = new Artist { Name = "First Artist", NormalizedName = "FIRST ARTIST", ExternalId = "first" }; + var second = TestEntities.Artist("Second Artist", externalId: "second"); + var first = TestEntities.Artist("First Artist", externalId: "first"); track.TrackArtists = [ new TrackArtist { TrackId = track.Id, ArtistId = second.Id, Position = 1, Artist = second, Track = track }, diff --git a/backend/Tests/Musify.Application.Tests/TestSupport/HandlerTestBase.cs b/backend/Tests/Musify.Application.Tests/TestSupport/HandlerTestBase.cs index 4752509..653dfeb 100644 --- a/backend/Tests/Musify.Application.Tests/TestSupport/HandlerTestBase.cs +++ b/backend/Tests/Musify.Application.Tests/TestSupport/HandlerTestBase.cs @@ -4,12 +4,6 @@ using Xunit; namespace Musify.Application.Tests.TestSupport { - /// - /// Base class for handler tests: gives every test method its own empty, isolated - /// (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 . - /// public abstract class HandlerTestBase : IAsyncLifetime { private TestDatabase database = null!; @@ -22,7 +16,6 @@ namespace Musify.Application.Tests.TestSupport protected static ILogger NoOpLogger() => NullLogger.Instance; - /// Adds the entities and persists them, so handlers under test see committed data. protected async Task SeedAsync(params object[] entities) { foreach (var entity in entities) diff --git a/backend/Tests/Musify.Application.Tests/TestSupport/TestConfigurations.cs b/backend/Tests/Musify.Application.Tests/TestSupport/TestConfigurations.cs index 6b401b6..3fedba1 100644 --- a/backend/Tests/Musify.Application.Tests/TestSupport/TestConfigurations.cs +++ b/backend/Tests/Musify.Application.Tests/TestSupport/TestConfigurations.cs @@ -2,10 +2,6 @@ using Musify.Application.Configuration; namespace Musify.Application.Tests.TestSupport { - /// - /// Configuration POCOs with every required member filled and the rest left at their - /// production defaults, so handler tests don't have to know which fields matter for them. - /// public static class TestConfigurations { public static ApplicationStorageConfiguration Storage(string bucket = "test-bucket") => diff --git a/backend/Tests/Musify.Application.Tests/TestSupport/TestDatabase.cs b/backend/Tests/Musify.Application.Tests/TestSupport/TestDatabase.cs index b2a913d..80a240b 100644 --- a/backend/Tests/Musify.Application.Tests/TestSupport/TestDatabase.cs +++ b/backend/Tests/Musify.Application.Tests/TestSupport/TestDatabase.cs @@ -5,25 +5,6 @@ using Musify.Domain.Entities; namespace Musify.Application.Tests.TestSupport { - /// - /// An backed by a private, in-memory SQLite database. - /// - /// - /// - /// exposes raw properties, so handlers - /// compose real LINQ queries (Where, Select, Include, ...) against it. - /// Substituting with a mock cannot express that — there is no - /// 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. - /// - /// - /// The model mirrors the parts of Musify.Infrastructure.Persistence.Database 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. - /// - /// public sealed class TestDatabase : DbContext, IDatabase { private readonly SqliteConnection connection; @@ -34,11 +15,6 @@ namespace Musify.Application.Tests.TestSupport this.connection = connection; } - /// - /// Creates a fresh, empty database with its own private connection. Call - /// (via ) to populate it, then - /// when the test is done. - /// public static async Task CreateAsync() { var connection = new SqliteConnection("Filename=:memory:"); diff --git a/backend/Tests/Musify.Application.Tests/TestSupport/TestEntities.cs b/backend/Tests/Musify.Application.Tests/TestSupport/TestEntities.cs index ca3556f..5cbeaa9 100644 --- a/backend/Tests/Musify.Application.Tests/TestSupport/TestEntities.cs +++ b/backend/Tests/Musify.Application.Tests/TestSupport/TestEntities.cs @@ -3,11 +3,6 @@ using Musify.Domain.ValueObjects; namespace Musify.Application.Tests.TestSupport { - /// - /// Builds fully-populated domain entities for tests, with sensible defaults for every - /// required member. Every factory always returns a valid instance — never null — so - /// callers never need a null check on what this class hands back. - /// public static class TestEntities { public static User User( diff --git a/backend/Tests/Musify.Infrastructure.Tests/Persistence/AuditableEntityInterceptorTests.cs b/backend/Tests/Musify.Infrastructure.Tests/Persistence/AuditableEntityInterceptorTests.cs index d4ae5ac..b6ef79e 100644 --- a/backend/Tests/Musify.Infrastructure.Tests/Persistence/AuditableEntityInterceptorTests.cs +++ b/backend/Tests/Musify.Infrastructure.Tests/Persistence/AuditableEntityInterceptorTests.cs @@ -13,7 +13,8 @@ namespace Musify.Infrastructure.Tests.Persistence await using var database = fixture.CreateDatabase(); var before = DateTime.UtcNow; - var user = new User { Id = Random.Shared.NextInt64(1, long.MaxValue), Name = "audit-user", NormalizedName = "AUDIT-USER" }; + var name = "audit-user"; + var user = new User { Id = Random.Shared.NextInt64(1, long.MaxValue), Name = name, NormalizedName = name.ToUpperInvariant() }; await database.Users.AddAsync(user, TestContext.Current.CancellationToken); await database.SaveChangesAsync(TestContext.Current.CancellationToken); @@ -28,7 +29,8 @@ namespace Musify.Infrastructure.Tests.Persistence { await using var database = fixture.CreateDatabase(); - var user = new User { Id = Random.Shared.NextInt64(1, long.MaxValue), Name = "audit-user-2", NormalizedName = "AUDIT-USER-2" }; + var name = "audit-user-2"; + var user = new User { Id = Random.Shared.NextInt64(1, long.MaxValue), Name = name, NormalizedName = name.ToUpperInvariant() }; await database.Users.AddAsync(user, TestContext.Current.CancellationToken); await database.SaveChangesAsync(TestContext.Current.CancellationToken); var originalCreatedAt = user.CreatedAt; diff --git a/backend/Tests/Musify.Infrastructure.Tests/Persistence/DatabaseTests.cs b/backend/Tests/Musify.Infrastructure.Tests/Persistence/DatabaseTests.cs index 6de31d1..62afb43 100644 --- a/backend/Tests/Musify.Infrastructure.Tests/Persistence/DatabaseTests.cs +++ b/backend/Tests/Musify.Infrastructure.Tests/Persistence/DatabaseTests.cs @@ -14,13 +14,15 @@ namespace Musify.Infrastructure.Tests.Persistence { await using var database = fixture.CreateDatabase(); - var user = new User { Id = Random.Shared.NextInt64(1, long.MaxValue), Name = "roundtrip-user", NormalizedName = "ROUNDTRIP-USER" }; + var userName = "roundtrip-user"; + var user = new User { Id = Random.Shared.NextInt64(1, long.MaxValue), Name = userName, NormalizedName = userName.ToUpperInvariant() }; await database.Users.AddAsync(user, TestContext.Current.CancellationToken); + var trackTitle = "Round Trip Track"; var track = new LocalTrack { - Title = "Round Trip Track", - NormalizedTitle = "ROUND TRIP TRACK", + Title = trackTitle, + NormalizedTitle = trackTitle.ToUpperInvariant(), DurationSeconds = 123.4, OwnerUserId = user.Id, Owner = user, @@ -34,7 +36,7 @@ namespace Musify.Infrastructure.Tests.Persistence var stored = await reloaded.Tracks.AsNoTracking().SingleAsync(t => t.Id == track.Id, TestContext.Current.CancellationToken); Assert.IsType(stored); - Assert.Equal("Round Trip Track", stored.Title); + Assert.Equal(trackTitle, stored.Title); Assert.Equal("cover.webp", stored.Pictures.OriginalName); Assert.Equal("song.mp3", stored.Audio.OriginalName); Assert.Equal(user.Id, ((LocalTrack)stored).OwnerUserId); @@ -60,8 +62,10 @@ namespace Musify.Infrastructure.Tests.Persistence { await using var database = fixture.CreateDatabase(); - var user = new User { Id = Random.Shared.NextInt64(1, long.MaxValue), Name = "cascade-user", NormalizedName = "CASCADE-USER" }; - var album = new UserAlbum { Title = "Cascade Album", NormalizedTitle = "CASCADE ALBUM", OwnerUserId = user.Id }; + var userName = "cascade-user"; + var user = new User { Id = Random.Shared.NextInt64(1, long.MaxValue), Name = userName, NormalizedName = userName.ToUpperInvariant() }; + var albumTitle = "Cascade Album"; + var album = new UserAlbum { Title = albumTitle, NormalizedTitle = albumTitle.ToUpperInvariant(), OwnerUserId = user.Id }; var track = BuildExternalTrack($"cascade-{Guid.NewGuid():N}"); var link = new AlbumHasTrack { AlbumId = album.Id, TrackId = track.Id, TrackNumber = 1 }; @@ -78,15 +82,19 @@ namespace Musify.Infrastructure.Tests.Persistence Assert.True(await verifier.ExternalTracks.AnyAsync(t => t.Id == track.Id, TestContext.Current.CancellationToken)); } - private static ExternalTrack BuildExternalTrack(string externalId) => new() + private static ExternalTrack BuildExternalTrack(string externalId) { - Title = "External", - NormalizedTitle = "EXTERNAL", - DurationSeconds = 100, - Source = TrackSource.YouTube, - ExternalId = externalId, - Pictures = new TrackPictures(), - Audio = new TrackAudio() - }; + var title = "External"; + return new ExternalTrack + { + Title = title, + NormalizedTitle = title.ToUpperInvariant(), + DurationSeconds = 100, + Source = TrackSource.YouTube, + ExternalId = externalId, + Pictures = new TrackPictures(), + Audio = new TrackAudio() + }; + } } } diff --git a/backend/Tests/Musify.Infrastructure.Tests/Services/StorageServiceTests.cs b/backend/Tests/Musify.Infrastructure.Tests/Services/StorageServiceTests.cs index 5cc045c..d2420f0 100644 --- a/backend/Tests/Musify.Infrastructure.Tests/Services/StorageServiceTests.cs +++ b/backend/Tests/Musify.Infrastructure.Tests/Services/StorageServiceTests.cs @@ -131,15 +131,9 @@ namespace Musify.Infrastructure.Tests.Services using var httpClient = new HttpClient(); using var request = new HttpRequestMessage(HttpMethod.Put, uploadUrl) { - // Not StringContent: it appends "; charset=utf-8" to Content-Type, which no longer - // matches the "text/plain" the presigned URL's signature was computed over (SigV4 - // signs Content-Type byte-for-byte) and the server answers 403 SignatureDoesNotMatch. Content = new ByteArrayContent(Encoding.UTF8.GetBytes("presigned content")) }; request.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain"); - // GetUploadUrlAsync defaults preventOverwrite to true, which signs the URL together with - // this header (see StorageService.GetUploadUrlAsync) — the actual PUT has to send it too, - // or the signature no longer matches and the server answers 403. request.Headers.TryAddWithoutValidation("If-None-Match", "*"); var response = await httpClient.SendAsync(request, TestContext.Current.CancellationToken); diff --git a/backend/Tests/Musify.Infrastructure.Tests/TestSupport/InfrastructureTestFixture.cs b/backend/Tests/Musify.Infrastructure.Tests/TestSupport/InfrastructureTestFixture.cs index 940dfd3..1e1f45d 100644 --- a/backend/Tests/Musify.Infrastructure.Tests/TestSupport/InfrastructureTestFixture.cs +++ b/backend/Tests/Musify.Infrastructure.Tests/TestSupport/InfrastructureTestFixture.cs @@ -12,13 +12,6 @@ using Xunit; namespace Musify.Infrastructure.Tests.TestSupport { - /// - /// One Postgres container and one SeaweedFS container (same image and startup - /// command as deploy/compose.yml), started once for every test in the - /// and torn down when the run finishes. - /// EF Core migrations run once here too, against the real Database - /// DbContext, so a broken migration fails the whole run immediately. - /// public sealed class InfrastructureTestFixture : IAsyncLifetime { public const string S3AccessKey = "musify-test"; @@ -51,7 +44,6 @@ namespace Musify.Infrastructure.Tests.TestSupport await using var database = new Database(new DatabaseConfiguration { ConnectionString = ConnectionString }); await database.Database.MigrateAsync(); - // SeaweedFS does not auto-create buckets (see deploy/compose.yml's seaweedfs-init). using var s3 = CreateS3Client(); await s3.PutBucketAsync(S3Bucket); } @@ -62,9 +54,6 @@ namespace Musify.Infrastructure.Tests.TestSupport await seaweedFs.DisposeAsync(); } - /// A fresh, DI-wired , so AuditableEntityInterceptor - /// (attached in AddDatabase's DI registration, not in the class itself) is active — - /// same wiring the real app uses, not a bare new Database(...). public Database CreateDatabase() { var configuration = new ConfigurationBuilder() @@ -76,8 +65,6 @@ namespace Musify.Infrastructure.Tests.TestSupport return services.BuildServiceProvider().GetRequiredService(); } - /// An client configured exactly like - /// ServicesDependencyInjection.AddStorageService, pointed at the SeaweedFS container. public IAmazonS3 CreateS3Client() => new AmazonS3Client(S3AccessKey, S3SecretKey, new AmazonS3Config {