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,40 @@
|
||||
using ErrorOr;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Users;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Users;
|
||||
|
||||
public sealed class CreateUserCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
private CreateUserCommandHandler CreateHandler() => new(Database, NoOpLogger<CreateUserCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NewId_CreatesUser()
|
||||
{
|
||||
var command = new CreateUserCommand(1, "Jane Doe", "Jane", "Doe");
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("Jane Doe", result.Value.Name);
|
||||
Assert.Equal(1, result.Value.Id);
|
||||
|
||||
var stored = await Database.Users.FindAsync(1L);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("JANE DOE", stored.NormalizedName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_IdAlreadyExists_ReturnsConflict()
|
||||
{
|
||||
var existing = TestEntities.User(1);
|
||||
await SeedAsync(existing);
|
||||
|
||||
var command = new CreateUserCommand(1, "Someone Else", null, null);
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using ErrorOr;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Users;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Users;
|
||||
|
||||
public sealed class GetUserByIdQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
private GetUserByIdQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ExistingUser_ReturnsIt()
|
||||
{
|
||||
var user = TestEntities.User(1, "Jane Doe");
|
||||
await SeedAsync(user);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetUserByIdQuery(1), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("Jane Doe", result.Value.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetUserByIdQuery(404), CancellationToken.None);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Users;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Users;
|
||||
|
||||
public sealed class GetUsersQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
private GetUsersQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoSearch_ReturnsEveryUser()
|
||||
{
|
||||
await SeedAsync(TestEntities.User(1, "alice"), TestEntities.User(2, "bob"));
|
||||
|
||||
var result = await CreateHandler().Handle(new GetUsersQuery(PageNumber: 1, PageSize: 10, UsernameSearch: null), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.TotalItemCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UsernameSearch_ReturnsOnlyMatches()
|
||||
{
|
||||
await SeedAsync(TestEntities.User(1, "alice-wonderland"), TestEntities.User(2, "bob-builder"));
|
||||
|
||||
var result = await CreateHandler().Handle(new GetUsersQuery(PageNumber: 1, PageSize: 10, UsernameSearch: "alice"), CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var user = Assert.Single(result.Value.Items);
|
||||
Assert.Equal(1, user.Id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Users;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Users;
|
||||
|
||||
public sealed class SyncUserCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
private SyncUserCommandHandler CreateHandler() => new(Database, NoOpLogger<SyncUserCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UnknownUser_ProvisionsIt()
|
||||
{
|
||||
var command = new SyncUserCommand(1, "Jane Doe", "Jane", "Doe", "https://avatar.example/jane.png");
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var stored = await Database.Users.FindAsync(1L);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("Jane Doe", stored.Name);
|
||||
Assert.Equal("https://avatar.example/jane.png", stored.ProfilePictureUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ExistingUser_UpdatesProfileFields()
|
||||
{
|
||||
var user = TestEntities.User(1, "Old Name", profilePictureUrl: null);
|
||||
await SeedAsync(user);
|
||||
|
||||
var command = new SyncUserCommand(1, "New Name", "New", "Name", "https://avatar.example/new.png");
|
||||
|
||||
var result = await CreateHandler().Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var stored = await Database.Users.FindAsync(1L);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("New Name", stored.Name);
|
||||
Assert.Equal("NEW NAME", stored.NormalizedName);
|
||||
Assert.Equal("https://avatar.example/new.png", stored.ProfilePictureUrl);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user