diff --git a/Musify.WebApi.slnx b/Musify.WebApi.slnx
index 5a8e7b9..c239259 100644
--- a/Musify.WebApi.slnx
+++ b/Musify.WebApi.slnx
@@ -1,6 +1,3 @@
-
-
-
diff --git a/Tests/Musify.WebApi.Tests/Extensions/ResultHttpExtensionsTests.cs b/Tests/Musify.WebApi.Tests/Extensions/ResultHttpExtensionsTests.cs
deleted file mode 100644
index 7aa73d9..0000000
--- a/Tests/Musify.WebApi.Tests/Extensions/ResultHttpExtensionsTests.cs
+++ /dev/null
@@ -1,122 +0,0 @@
-using Ardalis.Result;
-using FluentAssertions;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.HttpResults;
-using WebApi.Extensions;
-using Xunit;
-using IResult = Microsoft.AspNetCore.Http.IResult;
-
-namespace Musify.WebApi.Tests.Extensions;
-
-public sealed class ResultHttpExtensionsTests
-{
- private static int StatusCodeOf(IResult result) =>
- ((IStatusCodeHttpResult)result).StatusCode ?? 0;
-
- [Fact]
- public void Result_Ok_maps_to_204_no_content()
- {
- StatusCodeOf(Result.Success().ToHttpResult()).Should().Be(StatusCodes.Status204NoContent);
- }
-
- [Fact]
- public void Result_NoContent_maps_to_204()
- {
- StatusCodeOf(Result.NoContent().ToHttpResult()).Should().Be(StatusCodes.Status204NoContent);
- }
-
- [Fact]
- public void Result_NotFound_maps_to_404()
- {
- StatusCodeOf(Result.NotFound("nope").ToHttpResult()).Should().Be(StatusCodes.Status404NotFound);
- }
-
- [Fact]
- public void Result_Invalid_maps_to_validation_problem_400()
- {
- var result = Result.Invalid(new ValidationError("field", "bad")).ToHttpResult();
- result.Should().BeOfType();
- StatusCodeOf(result).Should().Be(StatusCodes.Status400BadRequest);
- }
-
- [Fact]
- public void Result_Unauthorized_maps_to_401()
- {
- StatusCodeOf(Result.Unauthorized().ToHttpResult()).Should().Be(StatusCodes.Status401Unauthorized);
- }
-
- [Fact]
- public void Result_Forbidden_maps_to_403()
- {
- StatusCodeOf(Result.Forbidden().ToHttpResult()).Should().Be(StatusCodes.Status403Forbidden);
- }
-
- [Fact]
- public void Result_Conflict_maps_to_409()
- {
- StatusCodeOf(Result.Conflict("dup").ToHttpResult()).Should().Be(StatusCodes.Status409Conflict);
- }
-
- [Fact]
- public void Result_Error_maps_to_problem_500()
- {
- var result = Result.Error("boom").ToHttpResult();
- result.Should().BeOfType();
- StatusCodeOf(result).Should().Be(StatusCodes.Status500InternalServerError);
- }
-
- [Fact]
- public void ResultT_Ok_maps_to_200_with_value()
- {
- var result = Result.Success("payload").ToHttpResult();
- result.Should().BeOfType>();
- result.As>().Value.Should().Be("payload");
- }
-
- [Fact]
- public void ResultT_NotFound_maps_to_404()
- {
- StatusCodeOf(Result.NotFound("nope").ToHttpResult()).Should().Be(StatusCodes.Status404NotFound);
- }
-
- [Fact]
- public void ResultT_Invalid_maps_to_validation_problem_with_grouped_errors()
- {
- var result = Result.Invalid(
- new ValidationError("Name", "required"),
- new ValidationError("Name", "too short"),
- new ValidationError("Age", "invalid")).ToHttpResult();
-
- result.Should().BeOfType();
- var details = result.As().ProblemDetails.Should().BeOfType().Subject;
- details.Errors.Should().ContainKey("Name");
- details.Errors["Name"].Should().BeEquivalentTo("required", "too short");
- details.Errors.Should().ContainKey("Age");
- }
-
- [Fact]
- public void ResultT_Unauthorized_maps_to_401()
- {
- StatusCodeOf(Result.Unauthorized().ToHttpResult()).Should().Be(StatusCodes.Status401Unauthorized);
- }
-
- [Fact]
- public void ResultT_Forbidden_maps_to_403()
- {
- StatusCodeOf(Result.Forbidden().ToHttpResult()).Should().Be(StatusCodes.Status403Forbidden);
- }
-
- [Fact]
- public void ResultT_Conflict_maps_to_409()
- {
- StatusCodeOf(Result.Conflict("dup").ToHttpResult()).Should().Be(StatusCodes.Status409Conflict);
- }
-
- [Fact]
- public void ResultT_Error_maps_to_problem_500()
- {
- var result = Result.Error("boom").ToHttpResult();
- result.Should().BeOfType();
- StatusCodeOf(result).Should().Be(StatusCodes.Status500InternalServerError);
- }
-}
diff --git a/Tests/Musify.WebApi.Tests/Integration/ApiIntegrationTests.cs b/Tests/Musify.WebApi.Tests/Integration/ApiIntegrationTests.cs
deleted file mode 100644
index af7aae9..0000000
--- a/Tests/Musify.WebApi.Tests/Integration/ApiIntegrationTests.cs
+++ /dev/null
@@ -1,140 +0,0 @@
-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
-{
- private sealed record ValidationProblemResponse(Dictionary Errors);
-
- private sealed record TrackUploadUrlsLikeResponse(string PictureUploadUrl, string AudioUploadUrl);
-
- private readonly HttpClient _client = factory.CreateClient();
-
- private async Task 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);
- UserApplicationResponse user = await get.Content.ReadFromJsonAsync()
- ?? throw new InvalidOperationException("The response body was null.");
- 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);
- ValidationProblemResponse problem = await response.Content.ReadFromJsonAsync()
- ?? throw new InvalidOperationException("The response body was null.");
- 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);
- PlayListApplicationResponse playList = await response.Content.ReadFromJsonAsync()
- ?? throw new InvalidOperationException("The response body was null.");
- 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(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any())
- .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);
- TrackUploadUrlsLikeResponse body = await response.Content.ReadFromJsonAsync()
- ?? throw new InvalidOperationException("The response body was null.");
- body.PictureUploadUrl.Should().Be("https://signed.example/put");
- body.AudioUploadUrl.Should().Be("https://signed.example/put");
- }
-}
diff --git a/Tests/Musify.WebApi.Tests/Integration/MusifyApiFactory.cs b/Tests/Musify.WebApi.Tests/Integration/MusifyApiFactory.cs
deleted file mode 100644
index 71cb896..0000000
--- a/Tests/Musify.WebApi.Tests/Integration/MusifyApiFactory.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-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
-{
- private readonly SqliteConnection _connection = new("DataSource=:memory:;Foreign Keys=False");
-
- public IStorageService Storage { get; } = Substitute.For();
- public IEventBus EventBus { get; } = Substitute.For();
-
- protected override void ConfigureWebHost(IWebHostBuilder builder)
- {
- builder.UseEnvironment("Testing");
-
- builder.ConfigureAppConfiguration((_, config) =>
- {
- config.AddInMemoryCollection(new Dictionary
- {
- ["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 =>
- {
- services.RemoveAll();
-
- _connection.Open();
-
- services.RemoveAll();
- services.AddScoped(_ =>
- {
- var options = new DbContextOptionsBuilder()
- .UseSqlite(_connection)
- .Options;
- var db = new TestDatabase(options);
- db.Database.EnsureCreated();
- return db;
- });
-
- services.RemoveAll();
- services.AddScoped(_ => Storage);
-
- services.RemoveAll();
- services.AddScoped(_ => EventBus);
- } );
- }
-
- protected override void Dispose(bool disposing)
- {
- base.Dispose(disposing);
- if (disposing)
- _connection.Dispose();
- }
-}
diff --git a/Tests/Musify.WebApi.Tests/Musify.WebApi.Tests.csproj b/Tests/Musify.WebApi.Tests/Musify.WebApi.Tests.csproj
deleted file mode 100644
index 356c23c..0000000
--- a/Tests/Musify.WebApi.Tests/Musify.WebApi.Tests.csproj
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
- net10.0
- enable
- enable
- false
- Musify.WebApi.Tests
-
-
-
-
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Tests/Musify.WebApi.Tests/Validators/ValidatorTests.cs b/Tests/Musify.WebApi.Tests/Validators/ValidatorTests.cs
deleted file mode 100644
index 046f9ed..0000000
--- a/Tests/Musify.WebApi.Tests/Validators/ValidatorTests.cs
+++ /dev/null
@@ -1,214 +0,0 @@
-using FluentValidation.TestHelper;
-using WebApi.DataTransferObjects.PlayLists;
-using WebApi.DataTransferObjects.Tracks;
-using WebApi.DataTransferObjects.Users;
-using WebApi.Validators.PlayLists;
-using WebApi.Validators.Tracks;
-using WebApi.Validators.Users;
-using Xunit;
-
-namespace Musify.WebApi.Tests.Validators;
-
-public sealed class CreateTrackRequestValidatorTests
-{
- private readonly CreateTrackRequestValidator _validator = new();
-
- [Fact]
- public void Valid_request_passes()
- {
- var model = new CreateTrackRequest("Song", Guid.NewGuid(), Guid.NewGuid());
- _validator.TestValidate(model).ShouldNotHaveAnyValidationErrors();
- }
-
- [Theory]
- [InlineData("")]
- [InlineData(null)]
- public void Empty_title_fails(string? title)
- {
- var model = new CreateTrackRequest(title!, Guid.NewGuid(), Guid.NewGuid());
- _validator.TestValidate(model).ShouldHaveValidationErrorFor(x => x.Title);
- }
-
- [Fact]
- public void Title_longer_than_50_fails()
- {
- var model = new CreateTrackRequest(new string('a', 51), Guid.NewGuid(), Guid.NewGuid());
- _validator.TestValidate(model).ShouldHaveValidationErrorFor(x => x.Title);
- }
-
- [Fact]
- public void Empty_intent_ids_fail()
- {
- var model = new CreateTrackRequest("Song", Guid.Empty, Guid.Empty);
- var result = _validator.TestValidate(model);
- result.ShouldHaveValidationErrorFor(x => x.PictureIntentId);
- result.ShouldHaveValidationErrorFor(x => x.AudioIntentId);
- }
-}
-
-public sealed class RequestTrackUploadUrlsRequestValidatorTests
-{
- private readonly RequestTrackUploadUrlsRequestValidator _validator = new();
-
- private static RequestTrackUploadUrlsRequest Valid() =>
- new("png", "image/png", "mp3", "audio/mpeg");
-
- [Fact]
- public void Valid_request_passes()
- {
- _validator.TestValidate(Valid()).ShouldNotHaveAnyValidationErrors();
- }
-
- [Fact]
- public void Missing_required_fields_fail()
- {
- var model = new RequestTrackUploadUrlsRequest("", "", "", "");
- var result = _validator.TestValidate(model);
- result.ShouldHaveValidationErrorFor(x => x.PictureFileType);
- result.ShouldHaveValidationErrorFor(x => x.PictureContentType);
- result.ShouldHaveValidationErrorFor(x => x.AudioFileType);
- result.ShouldHaveValidationErrorFor(x => x.AudioContentType);
- }
-
- [Fact]
- public void Non_positive_expected_sizes_fail_when_provided()
- {
- var model = Valid() with { ExpectedPictureSizeBytes = 0, ExpectedAudioSizeBytes = -1 };
- var result = _validator.TestValidate(model);
- result.ShouldHaveValidationErrorFor(x => x.ExpectedPictureSizeBytes);
- result.ShouldHaveValidationErrorFor(x => x.ExpectedAudioSizeBytes);
- }
-
- [Fact]
- public void Null_expected_sizes_are_allowed()
- {
- var model = Valid() with { ExpectedPictureSizeBytes = null, ExpectedAudioSizeBytes = null };
- var result = _validator.TestValidate(model);
- result.ShouldNotHaveValidationErrorFor(x => x.ExpectedPictureSizeBytes);
- result.ShouldNotHaveValidationErrorFor(x => x.ExpectedAudioSizeBytes);
- }
-}
-
-public sealed class CreatePlayListRequestValidatorTests
-{
- private readonly CreatePlayListRequestValidator _validator = new();
-
- [Fact]
- public void Valid_request_passes()
- {
- var model = new CreatePlayListRequest("My List", "A description", null);
- _validator.TestValidate(model).ShouldNotHaveAnyValidationErrors();
- }
-
- [Fact]
- public void Empty_name_and_description_fail()
- {
- var model = new CreatePlayListRequest("", "", null);
- var result = _validator.TestValidate(model);
- result.ShouldHaveValidationErrorFor(x => x.Name);
- result.ShouldHaveValidationErrorFor(x => x.Description);
- }
-
- [Fact]
- public void Over_length_name_and_description_fail()
- {
- var model = new CreatePlayListRequest(new string('n', 51), new string('d', 257), null);
- var result = _validator.TestValidate(model);
- result.ShouldHaveValidationErrorFor(x => x.Name);
- result.ShouldHaveValidationErrorFor(x => x.Description);
- }
-}
-
-public sealed class UpdatePlayListRequestValidatorTests
-{
- private readonly UpdatePlayListRequestValidator _validator = new();
-
- [Fact]
- public void All_null_is_valid()
- {
- _validator.TestValidate(new UpdatePlayListRequest(null, null, null))
- .ShouldNotHaveAnyValidationErrors();
- }
-
- [Fact]
- public void Over_length_values_fail_only_when_provided()
- {
- var model = new UpdatePlayListRequest(new string('n', 51), new string('d', 257), null);
- var result = _validator.TestValidate(model);
- result.ShouldHaveValidationErrorFor(x => x.NewName);
- result.ShouldHaveValidationErrorFor(x => x.NewDescription);
- }
-
- [Fact]
- public void Empty_string_name_is_allowed_by_length_rule()
- {
- var model = new UpdatePlayListRequest("", null, null);
- _validator.TestValidate(model).ShouldNotHaveValidationErrorFor(x => x.NewName);
- }
-}
-
-public sealed class RequestPlayListPictureUploadRequestValidatorTests
-{
- private readonly RequestPlayListPictureUploadRequestValidator _validator = new();
-
- [Fact]
- public void Valid_request_passes()
- {
- _validator.TestValidate(new RequestPlayListPictureUploadRequest("webp", "image/webp"))
- .ShouldNotHaveAnyValidationErrors();
- }
-
- [Fact]
- public void Missing_filetype_and_contenttype_fail()
- {
- var result = _validator.TestValidate(new RequestPlayListPictureUploadRequest("", ""));
- result.ShouldHaveValidationErrorFor(x => x.FileType);
- result.ShouldHaveValidationErrorFor(x => x.ContentType);
- }
-
- [Fact]
- public void Non_positive_size_fails_when_provided()
- {
- var result = _validator.TestValidate(new RequestPlayListPictureUploadRequest("webp", "image/webp", 0));
- result.ShouldHaveValidationErrorFor(x => x.ExpectedSizeBytes);
- }
-}
-
-public sealed class CreateUserRequestValidatorTests
-{
- private readonly CreateUserRequestValidator _validator = new();
-
- [Fact]
- public void Valid_request_passes()
- {
- var model = new CreateUserRequest(Guid.NewGuid(), "Alice", "Al", "Ice");
- _validator.TestValidate(model).ShouldNotHaveAnyValidationErrors();
- }
-
- [Fact]
- public void Empty_id_and_name_fail()
- {
- var model = new CreateUserRequest(Guid.Empty, "", null, null);
- var result = _validator.TestValidate(model);
- result.ShouldHaveValidationErrorFor(x => x.Id);
- result.ShouldHaveValidationErrorFor(x => x.Name);
- }
-
- [Fact]
- public void Over_length_optional_names_fail_only_when_provided()
- {
- var model = new CreateUserRequest(Guid.NewGuid(), "Alice", new string('f', 49), new string('s', 49));
- var result = _validator.TestValidate(model);
- result.ShouldHaveValidationErrorFor(x => x.FirstName);
- result.ShouldHaveValidationErrorFor(x => x.SecondName);
- }
-
- [Fact]
- public void Null_optional_names_are_allowed()
- {
- var model = new CreateUserRequest(Guid.NewGuid(), "Alice", null, null);
- var result = _validator.TestValidate(model);
- result.ShouldNotHaveValidationErrorFor(x => x.FirstName);
- result.ShouldNotHaveValidationErrorFor(x => x.SecondName);
- }
-}