Added tests

This commit is contained in:
2026-05-29 13:37:34 +02:00
parent d387708e3a
commit b931958b16
7 changed files with 598 additions and 0 deletions
@@ -0,0 +1,136 @@
using System.Net;
using System.Net.Http.Json;
using FluentAssertions;
using Musify.Application.PlayLists.Responses;
using Musify.Application.Users.Responses;
using NSubstitute;
using WebApi.DataTransferObjects.PlayLists;
using WebApi.DataTransferObjects.Tracks;
using WebApi.DataTransferObjects.Users;
using Xunit;
namespace Musify.WebApi.Tests.Integration;
public sealed class ApiIntegrationTests(MusifyApiFactory factory)
: IClassFixture<MusifyApiFactory>
{
private sealed record ValidationProblemResponse(Dictionary<string, string[]> Errors);
private sealed record TrackUploadUrlsLikeResponse(string PictureUploadUrl, string AudioUploadUrl);
private readonly HttpClient _client = factory.CreateClient();
private async Task<Guid> CreateUserAsync(string name = "IntegrationUser")
{
var id = Guid.NewGuid();
var response = await _client.PostAsJsonAsync("/users/", new CreateUserRequest(id, name, null, null));
response.StatusCode.Should().Be(HttpStatusCode.Created);
return id;
}
[Fact]
public async Task Create_then_get_user_roundtrips_through_the_real_pipeline()
{
var id = Guid.NewGuid();
var create = await _client.PostAsJsonAsync("/users/", new CreateUserRequest(id, "Alice", "Al", "Ice"));
create.StatusCode.Should().Be(HttpStatusCode.Created);
var get = await _client.GetAsync($"/users/{id}");
get.StatusCode.Should().Be(HttpStatusCode.OK);
var user = await get.Content.ReadFromJsonAsync<UserApplicationResponse>();
user!.Id.Should().Be(id);
user.Name.Should().Be("Alice");
}
[Fact]
public async Task Create_user_with_invalid_body_returns_400_validation_problem()
{
var response = await _client.PostAsJsonAsync("/users/", new CreateUserRequest(Guid.Empty, "", null, null));
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
var problem = await response.Content.ReadFromJsonAsync<ValidationProblemResponse>();
problem!.Errors.Should().ContainKey(nameof(CreateUserRequest.Name));
}
[Fact]
public async Task Create_duplicate_user_returns_409_conflict()
{
var id = await CreateUserAsync();
var duplicate = await _client.PostAsJsonAsync("/users/", new CreateUserRequest(id, "Other", null, null));
duplicate.StatusCode.Should().Be(HttpStatusCode.Conflict);
}
[Fact]
public async Task Get_unknown_user_returns_404()
{
var response = await _client.GetAsync($"/users/{Guid.NewGuid()}");
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
[Fact]
public async Task Create_playlist_without_picture_returns_201_and_persists()
{
var userId = await CreateUserAsync();
var response = await _client.PostAsJsonAsync(
$"/playLists/users/{userId}",
new CreatePlayListRequest("My Mix", "A description", null));
response.StatusCode.Should().Be(HttpStatusCode.Created);
var playList = await response.Content.ReadFromJsonAsync<PlayListApplicationResponse>();
playList!.Name.Should().Be("My Mix");
var get = await _client.GetAsync($"/playLists/{playList.Id}");
get.StatusCode.Should().Be(HttpStatusCode.OK);
}
[Fact]
public async Task Create_playlist_for_unknown_user_returns_404()
{
var response = await _client.PostAsJsonAsync(
$"/playLists/users/{Guid.NewGuid()}",
new CreatePlayListRequest("Mix", "desc", null));
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
[Fact]
public async Task Create_playlist_with_invalid_body_returns_400()
{
var userId = await CreateUserAsync();
var response = await _client.PostAsJsonAsync(
$"/playLists/users/{userId}",
new CreatePlayListRequest("", "", null));
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
}
[Fact]
public async Task Get_unknown_track_returns_404()
{
var response = await _client.GetAsync($"/tracks/{Guid.NewGuid()}");
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
[Fact]
public async Task Request_track_upload_urls_issues_presigned_urls()
{
var userId = await CreateUserAsync();
factory.Storage
.GetUploadUrlAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<CancellationToken>())
.Returns("https://signed.example/put");
var response = await _client.PostAsJsonAsync(
$"/tracks/upload-urls/users/{userId}",
new RequestTrackUploadUrlsRequest("png", "image/png", "mp3", "audio/mpeg"));
response.StatusCode.Should().Be(HttpStatusCode.OK);
var body = await response.Content.ReadFromJsonAsync<TrackUploadUrlsLikeResponse>();
body!.PictureUploadUrl.Should().Be("https://signed.example/put");
body.AudioUploadUrl.Should().Be("https://signed.example/put");
}
}
@@ -0,0 +1,82 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Musify.Application.Contracts;
using Musify.Application.Tests.Infrastructure;
using NSubstitute;
namespace Musify.WebApi.Tests.Integration;
public sealed class MusifyApiFactory : WebApplicationFactory<Program>
{
private readonly SqliteConnection _connection = new("DataSource=:memory:;Foreign Keys=False");
public IStorageService Storage { get; } = Substitute.For<IStorageService>();
public IEventBus EventBus { get; } = Substitute.For<IEventBus>();
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Testing");
builder.ConfigureAppConfiguration((_, config) =>
{
config.AddInMemoryCollection(new Dictionary<string, string?>
{
["Database:ConnectionString"] = "Host=localhost;Database=test;Username=test;Password=test",
["ApplicationStorage:Bucket"] = TestData.Bucket,
["PlayList:Routes:ParentFolders"] = "PlayLists",
["Track:Routes:ParentFolder"] = "Tracks",
["UploadIntent:TempRootPrefix"] = "temp",
["InfrastructureStorage:Address"] = "http://localhost:9000",
["InfrastructureStorage:AccessKey"] = "test",
["InfrastructureStorage:SecretAccessKey"] = "test",
["InfrastructureStorage:ForcePathStyle"] = "true",
["InfrastructureStorage:UseHttp"] = "true",
["MassTransit:Address"] = "rabbitmq://localhost",
["MassTransit:Username"] = "guest",
["MassTransit:Password"] = "guest",
["OpenTelemetry:OtlpEndpoint"] = "http://localhost:4317",
} );
} );
builder.ConfigureTestServices(services =>
{
// Never start the RabbitMQ bus, EF outbox delivery or OTLP exporter in tests.
services.RemoveAll<IHostedService>();
_connection.Open();
// Replace the Npgsql-backed IDatabase with a shared SQLite TestDatabase.
services.RemoveAll<IDatabase>();
services.AddScoped<IDatabase>(_ =>
{
var options = new DbContextOptionsBuilder<TestDatabase>()
.UseSqlite(_connection)
.Options;
var db = new TestDatabase(options);
db.Database.EnsureCreated();
return db;
});
// Replace S3 and MassTransit with controllable test doubles.
services.RemoveAll<IStorageService>();
services.AddScoped(_ => Storage);
services.RemoveAll<IEventBus>();
services.AddScoped(_ => EventBus);
} );
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
_connection.Dispose();
}
}